Improve crawl list rendering (#645)
* add load more button * adjust height * refactor to improve performance * remove unused observable component * contain status * update dropdown animation
This commit is contained in:
parent
a1f939ad29
commit
f2b7946960
@ -12,13 +12,19 @@
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
import { LitElement, html, css } from "lit";
|
import { LitElement, html, css } from "lit";
|
||||||
import { property, queryAssignedElements } from "lit/decorators.js";
|
import {
|
||||||
|
property,
|
||||||
|
query,
|
||||||
|
queryAssignedElements,
|
||||||
|
state,
|
||||||
|
} from "lit/decorators.js";
|
||||||
import { when } from "lit/directives/when.js";
|
import { when } from "lit/directives/when.js";
|
||||||
import { msg, localized, str } from "@lit/localize";
|
import { msg, localized, str } from "@lit/localize";
|
||||||
|
import type { SlIconButton, SlMenu } from "@shoelace-style/shoelace";
|
||||||
|
|
||||||
import { RelativeDuration } from "./relative-duration";
|
import { RelativeDuration } from "./relative-duration";
|
||||||
import type { Crawl } from "../types/crawler";
|
import type { Crawl } from "../types/crawler";
|
||||||
import { srOnly, truncate } from "../utils/css";
|
import { srOnly, truncate, dropdown } from "../utils/css";
|
||||||
import type { NavigateEvent } from "../utils/LiteElement";
|
import type { NavigateEvent } from "../utils/LiteElement";
|
||||||
|
|
||||||
const largeBreakpointCss = css`60rem`;
|
const largeBreakpointCss = css`60rem`;
|
||||||
@ -40,7 +46,7 @@ const rowCss = css`
|
|||||||
}
|
}
|
||||||
|
|
||||||
.col {
|
.col {
|
||||||
grid-column: span 1;
|
grid-column: span 1 / span 1;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
const columnCss = css`
|
const columnCss = css`
|
||||||
@ -64,6 +70,7 @@ const hostVars = css`
|
|||||||
export class CrawlListItem extends LitElement {
|
export class CrawlListItem extends LitElement {
|
||||||
static styles = [
|
static styles = [
|
||||||
truncate,
|
truncate,
|
||||||
|
dropdown,
|
||||||
rowCss,
|
rowCss,
|
||||||
columnCss,
|
columnCss,
|
||||||
hostVars,
|
hostVars,
|
||||||
@ -73,12 +80,26 @@ export class CrawlListItem extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
|
contain: content;
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-height: auto 4rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition-property: background-color, box-shadow, margin;
|
transition-property: background-color, box-shadow, margin;
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
transition-duration: 150ms;
|
transition-duration: 150ms;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item:hover,
|
||||||
|
.item:focus,
|
||||||
|
.item:focus-within {
|
||||||
|
background-color: var(--sl-color-neutral-50);
|
||||||
|
}
|
||||||
|
.dropdown {
|
||||||
|
contain: content;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
.item:hover {
|
.item:hover {
|
||||||
background-color: var(--sl-color-neutral-50);
|
background-color: var(--sl-color-neutral-50);
|
||||||
margin-left: calc(-1 * var(--row-offset));
|
margin-left: calc(-1 * var(--row-offset));
|
||||||
@ -115,10 +136,8 @@ export class CrawlListItem extends LitElement {
|
|||||||
.detail {
|
.detail {
|
||||||
color: var(--sl-color-neutral-700);
|
color: var(--sl-color-neutral-700);
|
||||||
font-size: var(--sl-font-size-medium);
|
font-size: var(--sl-font-size-medium);
|
||||||
line-height: 1.4;
|
|
||||||
margin-bottom: var(--sl-spacing-3x-small);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.desc {
|
.desc {
|
||||||
@ -126,18 +145,13 @@ export class CrawlListItem extends LitElement {
|
|||||||
font-size: var(--sl-font-size-x-small);
|
font-size: var(--sl-font-size-x-small);
|
||||||
font-family: var(--font-monostyle-family);
|
font-family: var(--font-monostyle-family);
|
||||||
font-variation-settings: var(--font-monostyle-variation);
|
font-variation-settings: var(--font-monostyle-variation);
|
||||||
line-height: 1.4;
|
height: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unknownValue {
|
.unknownValue {
|
||||||
color: var(--sl-color-neutral-500);
|
color: var(--sl-color-neutral-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.url {
|
.url {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
@ -181,13 +195,6 @@ export class CrawlListItem extends LitElement {
|
|||||||
@media only screen and (min-width: ${largeBreakpointCss}) {
|
@media only screen and (min-width: ${largeBreakpointCss}) {
|
||||||
.action {
|
.action {
|
||||||
border-left: 1px solid var(--sl-panel-border-color);
|
border-left: 1px solid var(--sl-panel-border-color);
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action sl-dropdown {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@ -196,7 +203,36 @@ export class CrawlListItem extends LitElement {
|
|||||||
@property({ type: Object })
|
@property({ type: Object })
|
||||||
crawl?: Crawl;
|
crawl?: Crawl;
|
||||||
|
|
||||||
|
@query(".dropdown")
|
||||||
|
dropdown!: HTMLElement;
|
||||||
|
|
||||||
|
@query(".dropdownTrigger")
|
||||||
|
dropdownTrigger!: SlIconButton;
|
||||||
|
|
||||||
|
@queryAssignedElements({ selector: "sl-menu", slot: "menu" })
|
||||||
|
private menuArr!: Array<SlMenu>;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private dropdownIsOpen?: boolean;
|
||||||
|
|
||||||
|
// TODO localize
|
||||||
|
private numberFormatter = new Intl.NumberFormat();
|
||||||
|
|
||||||
|
willUpdate(changedProperties: Map<string, any>) {
|
||||||
|
if (changedProperties.has("dropdownIsOpen")) {
|
||||||
|
if (this.dropdownIsOpen) {
|
||||||
|
this.openDropdown();
|
||||||
|
} else {
|
||||||
|
this.closeDropdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
return html`${this.renderRow()}${this.renderDropdown()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRow() {
|
||||||
const isActive =
|
const isActive =
|
||||||
this.crawl &&
|
this.crawl &&
|
||||||
["starting", "running", "stopping"].includes(this.crawl.state);
|
["starting", "running", "stopping"].includes(this.crawl.state);
|
||||||
@ -219,7 +255,9 @@ export class CrawlListItem extends LitElement {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="detail url">${this.safeRender(this.renderName)}</div>
|
<div class="detail url truncate">
|
||||||
|
${this.safeRender(this.renderName)}
|
||||||
|
</div>
|
||||||
<div class="desc">
|
<div class="desc">
|
||||||
${this.safeRender(
|
${this.safeRender(
|
||||||
(crawl) => html`
|
(crawl) => html`
|
||||||
@ -265,29 +303,35 @@ export class CrawlListItem extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="detail">
|
<div class="detail">
|
||||||
${this.safeRender((crawl) =>
|
${this.safeRender(
|
||||||
isActive
|
(crawl) => html`<sl-format-bytes
|
||||||
? html`<span class="unknownValue">${msg("In Progress")}</span>`
|
|
||||||
: html`<sl-format-bytes
|
|
||||||
value=${crawl.fileSize || 0}
|
value=${crawl.fileSize || 0}
|
||||||
></sl-format-bytes>`
|
></sl-format-bytes>`
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div class="desc">
|
<div class="desc">
|
||||||
${this.safeRender((crawl) => {
|
${this.safeRender((crawl) => {
|
||||||
const pagesComplete = crawl.stats?.done || 0;
|
const pagesComplete = +(crawl.stats?.done || 0);
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
const pagesFound = crawl.stats?.found || 0;
|
const pagesFound = +(crawl.stats?.found || 0);
|
||||||
return html`
|
return html`
|
||||||
${+pagesFound === 1
|
${pagesFound === 1
|
||||||
? msg(str`${pagesComplete} / ${pagesFound} page`)
|
? msg(
|
||||||
: msg(str`${pagesComplete} / ${pagesFound} pages`)}
|
str`${this.numberFormatter.format(
|
||||||
|
pagesComplete
|
||||||
|
)} / ${this.numberFormatter.format(pagesFound)} page`
|
||||||
|
)
|
||||||
|
: msg(
|
||||||
|
str`${this.numberFormatter.format(
|
||||||
|
pagesComplete
|
||||||
|
)} / ${this.numberFormatter.format(pagesFound)} pages`
|
||||||
|
)}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
${+pagesComplete === 1
|
${pagesComplete === 1
|
||||||
? msg(str`${pagesComplete} page`)
|
? msg(str`${this.numberFormatter.format(pagesComplete)} page`)
|
||||||
: msg(str`${pagesComplete} pages`)}
|
: msg(str`${this.numberFormatter.format(pagesComplete)} pages`)}
|
||||||
`;
|
`;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@ -305,27 +349,53 @@ export class CrawlListItem extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col action">
|
<div class="col action">
|
||||||
<sl-dropdown
|
<sl-icon-button
|
||||||
distance="4"
|
class="dropdownTrigger"
|
||||||
hoist
|
slot="trigger"
|
||||||
|
name="three-dots-vertical"
|
||||||
|
label=${msg("More")}
|
||||||
@click=${(e: MouseEvent) => {
|
@click=${(e: MouseEvent) => {
|
||||||
// Prevent anchor link default behavior
|
// Prevent anchor link default behavior
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Stop prop to anchor link
|
// Stop prop to anchor link
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
this.dropdownIsOpen = true;
|
||||||
|
}}
|
||||||
|
@focusout=${(e: FocusEvent) => {
|
||||||
|
const relatedTarget = e.relatedTarget as HTMLElement;
|
||||||
|
if (this.menuArr[0]?.contains(relatedTarget)) {
|
||||||
|
// Keep dropdown open if moving to menu selection
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.dropdownIsOpen = false;
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<sl-icon-button
|
|
||||||
slot="trigger"
|
|
||||||
name="three-dots-vertical"
|
|
||||||
label=${msg("More")}
|
|
||||||
></sl-icon-button>
|
></sl-icon-button>
|
||||||
<slot name="menu"></slot>
|
|
||||||
</sl-dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
</a>`;
|
</a>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderDropdown() {
|
||||||
|
return html`<div
|
||||||
|
class="dropdown hidden"
|
||||||
|
aria-hidden=${!this.dropdownIsOpen}
|
||||||
|
@animationend=${(e: AnimationEvent) => {
|
||||||
|
const el = e.target as HTMLDivElement;
|
||||||
|
if (e.animationName === "dropdownShow") {
|
||||||
|
el.classList.remove("animateShow");
|
||||||
|
}
|
||||||
|
if (e.animationName === "dropdownHide") {
|
||||||
|
el.classList.add("hidden");
|
||||||
|
el.classList.remove("animateHide");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<slot
|
||||||
|
name="menu"
|
||||||
|
@sl-select=${() => (this.dropdownIsOpen = false)}
|
||||||
|
></slot>
|
||||||
|
</div> `;
|
||||||
|
}
|
||||||
|
|
||||||
private safeRender(render: (crawl: Crawl) => any) {
|
private safeRender(render: (crawl: Crawl) => any) {
|
||||||
if (!this.crawl) {
|
if (!this.crawl) {
|
||||||
return html`<sl-skeleton></sl-skeleton>`;
|
return html`<sl-skeleton></sl-skeleton>`;
|
||||||
@ -355,6 +425,22 @@ export class CrawlListItem extends LitElement {
|
|||||||
<span class="primaryUrl truncate">${crawl.firstSeed}</span>${nameSuffix}
|
<span class="primaryUrl truncate">${crawl.firstSeed}</span>${nameSuffix}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private repositionDropdown() {
|
||||||
|
const { x, y } = this.dropdownTrigger.getBoundingClientRect();
|
||||||
|
this.dropdown.style.left = `${x + window.scrollX}px`;
|
||||||
|
this.dropdown.style.top = `${y + window.scrollY - 8}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private openDropdown() {
|
||||||
|
this.repositionDropdown();
|
||||||
|
this.dropdown.classList.add("animateShow");
|
||||||
|
this.dropdown.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
private closeDropdown() {
|
||||||
|
this.dropdown.classList.add("animateHide");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@localized()
|
@localized()
|
||||||
|
@ -16,16 +16,12 @@ export class CrawlStatus extends LitElement {
|
|||||||
animatePulse,
|
animatePulse,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
|
contain: content;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
color: var(--sl-color-neutral-700);
|
color: var(--sl-color-neutral-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
sl-icon,
|
|
||||||
sl-skeleton,
|
|
||||||
span {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
sl-icon {
|
sl-icon {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin-right: var(--sl-spacing-x-small);
|
margin-right: var(--sl-spacing-x-small);
|
||||||
|
@ -11,6 +11,8 @@ import type {
|
|||||||
import inputCss from "@shoelace-style/shoelace/dist/components/input/input.styles.js";
|
import inputCss from "@shoelace-style/shoelace/dist/components/input/input.styles.js";
|
||||||
import debounce from "lodash/fp/debounce";
|
import debounce from "lodash/fp/debounce";
|
||||||
|
|
||||||
|
import { dropdown } from "../utils/css";
|
||||||
|
|
||||||
export type Tags = string[];
|
export type Tags = string[];
|
||||||
export type TagsChangeEvent = CustomEvent<{
|
export type TagsChangeEvent = CustomEvent<{
|
||||||
tags: string[];
|
tags: string[];
|
||||||
@ -33,13 +35,14 @@ export type TagInputEvent = CustomEvent<{
|
|||||||
*/
|
*/
|
||||||
@localized()
|
@localized()
|
||||||
export class TagInput extends LitElement {
|
export class TagInput extends LitElement {
|
||||||
static styles = css`
|
static styles = [
|
||||||
|
dropdown,
|
||||||
|
inputCss,
|
||||||
|
css`
|
||||||
:host {
|
:host {
|
||||||
--tag-height: 1.5rem;
|
--tag-height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
${inputCss}
|
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
height: auto;
|
height: auto;
|
||||||
@ -69,48 +72,6 @@ export class TagInput extends LitElement {
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown {
|
|
||||||
position: absolute;
|
|
||||||
transform-origin: top left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animateShow {
|
|
||||||
animation: dropdownShow 100ms ease forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animateHide {
|
|
||||||
animation: dropdownHide 100ms ease forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes dropdownShow {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes dropdownHide {
|
|
||||||
from {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.9);
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.shake {
|
.shake {
|
||||||
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
@ -123,24 +84,22 @@ export class TagInput extends LitElement {
|
|||||||
90% {
|
90% {
|
||||||
transform: translate3d(-1px, 0, 0);
|
transform: translate3d(-1px, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
20%,
|
20%,
|
||||||
80% {
|
80% {
|
||||||
transform: translate3d(2px, 0, 0);
|
transform: translate3d(2px, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
30%,
|
30%,
|
||||||
50%,
|
50%,
|
||||||
70% {
|
70% {
|
||||||
transform: translate3d(-3px, 0, 0);
|
transform: translate3d(-3px, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
40%,
|
40%,
|
||||||
60% {
|
60% {
|
||||||
transform: translate3d(3px, 0, 0);
|
transform: translate3d(3px, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
@property({ type: Array })
|
@property({ type: Array })
|
||||||
initialTags?: Tags;
|
initialTags?: Tags;
|
||||||
|
@ -136,9 +136,6 @@ export class CrawlsList extends LiteElement {
|
|||||||
|
|
||||||
private timerId?: number;
|
private timerId?: number;
|
||||||
|
|
||||||
// TODO localize
|
|
||||||
private numberFormatter = new Intl.NumberFormat();
|
|
||||||
|
|
||||||
private filterCrawls = (crawls: Crawl[]) =>
|
private filterCrawls = (crawls: Crawl[]) =>
|
||||||
this.filterByState.length
|
this.filterByState.length
|
||||||
? crawls.filter((crawl) =>
|
? crawls.filter((crawl) =>
|
||||||
|
@ -43,3 +43,47 @@ export const animatePulse = css`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const dropdown = css`
|
||||||
|
.dropdown {
|
||||||
|
contain: content;
|
||||||
|
position: absolute;
|
||||||
|
transform-origin: top left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animateShow {
|
||||||
|
animation: dropdownShow 100ms ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animateHide {
|
||||||
|
animation: dropdownHide 100ms ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dropdownShow {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dropdownHide {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
Loading…
Reference in New Issue
Block a user