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:
sua yoo 2023-02-28 18:36:23 -08:00 committed by GitHub
parent a1f939ad29
commit f2b7946960
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 266 additions and 184 deletions

View File

@ -12,13 +12,19 @@
* ```
*/
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 { msg, localized, str } from "@lit/localize";
import type { SlIconButton, SlMenu } from "@shoelace-style/shoelace";
import { RelativeDuration } from "./relative-duration";
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";
const largeBreakpointCss = css`60rem`;
@ -40,7 +46,7 @@ const rowCss = css`
}
.col {
grid-column: span 1;
grid-column: span 1 / span 1;
}
`;
const columnCss = css`
@ -64,6 +70,7 @@ const hostVars = css`
export class CrawlListItem extends LitElement {
static styles = [
truncate,
dropdown,
rowCss,
columnCss,
hostVars,
@ -73,12 +80,26 @@ export class CrawlListItem extends LitElement {
}
.item {
contain: content;
content-visibility: auto;
contain-intrinsic-height: auto 4rem;
cursor: pointer;
transition-property: background-color, box-shadow, margin;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
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 {
background-color: var(--sl-color-neutral-50);
margin-left: calc(-1 * var(--row-offset));
@ -115,10 +136,8 @@ export class CrawlListItem extends LitElement {
.detail {
color: var(--sl-color-neutral-700);
font-size: var(--sl-font-size-medium);
line-height: 1.4;
margin-bottom: var(--sl-spacing-3x-small);
overflow: hidden;
text-overflow: ellipsis;
height: 1.5rem;
}
.desc {
@ -126,18 +145,13 @@ export class CrawlListItem extends LitElement {
font-size: var(--sl-font-size-x-small);
font-family: var(--font-monostyle-family);
font-variation-settings: var(--font-monostyle-variation);
line-height: 1.4;
height: 1rem;
}
.unknownValue {
color: var(--sl-color-neutral-500);
}
.name {
overflow: hidden;
text-overflow: ellipsis;
}
.url {
display: flex;
}
@ -181,13 +195,6 @@ export class CrawlListItem extends LitElement {
@media only screen and (min-width: ${largeBreakpointCss}) {
.action {
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 })
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() {
return html`${this.renderRow()}${this.renderDropdown()}`;
}
renderRow() {
const isActive =
this.crawl &&
["starting", "running", "stopping"].includes(this.crawl.state);
@ -219,7 +255,9 @@ export class CrawlListItem extends LitElement {
}}
>
<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">
${this.safeRender(
(crawl) => html`
@ -265,29 +303,35 @@ export class CrawlListItem extends LitElement {
</div>
<div class="col">
<div class="detail">
${this.safeRender((crawl) =>
isActive
? html`<span class="unknownValue">${msg("In Progress")}</span>`
: html`<sl-format-bytes
value=${crawl.fileSize || 0}
></sl-format-bytes>`
${this.safeRender(
(crawl) => html`<sl-format-bytes
value=${crawl.fileSize || 0}
></sl-format-bytes>`
)}
</div>
<div class="desc">
${this.safeRender((crawl) => {
const pagesComplete = crawl.stats?.done || 0;
const pagesComplete = +(crawl.stats?.done || 0);
if (isActive) {
const pagesFound = crawl.stats?.found || 0;
const pagesFound = +(crawl.stats?.found || 0);
return html`
${+pagesFound === 1
? msg(str`${pagesComplete} / ${pagesFound} page`)
: msg(str`${pagesComplete} / ${pagesFound} pages`)}
${pagesFound === 1
? msg(
str`${this.numberFormatter.format(
pagesComplete
)} / ${this.numberFormatter.format(pagesFound)} page`
)
: msg(
str`${this.numberFormatter.format(
pagesComplete
)} / ${this.numberFormatter.format(pagesFound)} pages`
)}
`;
}
return html`
${+pagesComplete === 1
? msg(str`${pagesComplete} page`)
: msg(str`${pagesComplete} pages`)}
${pagesComplete === 1
? msg(str`${this.numberFormatter.format(pagesComplete)} page`)
: msg(str`${this.numberFormatter.format(pagesComplete)} pages`)}
`;
})}
</div>
@ -305,27 +349,53 @@ export class CrawlListItem extends LitElement {
</div>
</div>
<div class="col action">
<sl-dropdown
distance="4"
hoist
<sl-icon-button
class="dropdownTrigger"
slot="trigger"
name="three-dots-vertical"
label=${msg("More")}
@click=${(e: MouseEvent) => {
// Prevent anchor link default behavior
e.preventDefault();
// Stop prop to anchor link
e.stopPropagation();
this.dropdownIsOpen = true;
}}
>
<sl-icon-button
slot="trigger"
name="three-dots-vertical"
label=${msg("More")}
></sl-icon-button>
<slot name="menu"></slot>
</sl-dropdown>
@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>
</div>
</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) {
if (!this.crawl) {
return html`<sl-skeleton></sl-skeleton>`;
@ -355,6 +425,22 @@ export class CrawlListItem extends LitElement {
<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()
@ -365,38 +451,38 @@ export class CrawlList extends LitElement {
columnCss,
hostVars,
css`
.listHeader,
.list {
margin-left: var(--row-offset);
margin-right: var(--row-offset);
}
.listHeader {
line-height: 1;
}
.row {
display none;
font-size: var(--sl-font-size-x-small);
color: var(--sl-color-neutral-600);
}
.col {
padding-top: var(--sl-spacing-x-small);
padding-bottom: var(--sl-spacing-x-small);
}
@media only screen and (min-width: ${largeBreakpointCss}) {
.row {
display: grid;
}
}
::slotted(btrix-crawl-list-item:not(:last-of-type)) {
display: block;
margin-bottom: var(--sl-spacing-x-small);
}
`,
.listHeader,
.list {
margin-left: var(--row-offset);
margin-right: var(--row-offset);
}
.listHeader {
line-height: 1;
}
.row {
display none;
font-size: var(--sl-font-size-x-small);
color: var(--sl-color-neutral-600);
}
.col {
padding-top: var(--sl-spacing-x-small);
padding-bottom: var(--sl-spacing-x-small);
}
@media only screen and (min-width: ${largeBreakpointCss}) {
.row {
display: grid;
}
}
::slotted(btrix-crawl-list-item:not(:last-of-type)) {
display: block;
margin-bottom: var(--sl-spacing-x-small);
}
`,
];
@queryAssignedElements({ selector: "btrix-crawl-list-item" })

View File

@ -16,16 +16,12 @@ export class CrawlStatus extends LitElement {
animatePulse,
css`
:host {
contain: content;
display: inline-flex;
align-items: center;
color: var(--sl-color-neutral-700);
}
sl-icon,
sl-skeleton,
span {
display: inline-block;
vertical-align: middle;
}
sl-icon {
font-size: 1rem;
margin-right: var(--sl-spacing-x-small);

View File

@ -11,6 +11,8 @@ import type {
import inputCss from "@shoelace-style/shoelace/dist/components/input/input.styles.js";
import debounce from "lodash/fp/debounce";
import { dropdown } from "../utils/css";
export type Tags = string[];
export type TagsChangeEvent = CustomEvent<{
tags: string[];
@ -33,114 +35,71 @@ export type TagInputEvent = CustomEvent<{
*/
@localized()
export class TagInput extends LitElement {
static styles = css`
:host {
--tag-height: 1.5rem;
}
${inputCss}
.input {
flex-wrap: wrap;
height: auto;
overflow: visible;
min-height: calc(var(--tag-height) + 1rem);
}
.input__control {
flex-grow: 1;
flex-shrink: 0;
}
.input__control:not(:first-child) {
padding-left: var(--sl-spacing-small);
padding-right: var(--sl-spacing-small);
}
btrix-tag {
margin-left: var(--sl-spacing-x-small);
margin-top: calc(0.5rem - 1px);
max-width: calc(
100% - var(--sl-spacing-x-small) - var(--sl-spacing-x-small)
);
}
sl-popup::part(popup) {
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);
static styles = [
dropdown,
inputCss,
css`
:host {
--tag-height: 1.5rem;
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes dropdownHide {
from {
opacity: 1;
transform: scale(1);
.input {
flex-wrap: wrap;
height: auto;
overflow: visible;
min-height: calc(var(--tag-height) + 1rem);
}
to {
opacity: 0;
transform: scale(0.9);
display: none;
}
}
.shake {
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
}
@keyframes shake {
10%,
90% {
transform: translate3d(-1px, 0, 0);
.input__control {
flex-grow: 1;
flex-shrink: 0;
}
20%,
80% {
transform: translate3d(2px, 0, 0);
.input__control:not(:first-child) {
padding-left: var(--sl-spacing-small);
padding-right: var(--sl-spacing-small);
}
30%,
50%,
70% {
transform: translate3d(-3px, 0, 0);
btrix-tag {
margin-left: var(--sl-spacing-x-small);
margin-top: calc(0.5rem - 1px);
max-width: calc(
100% - var(--sl-spacing-x-small) - var(--sl-spacing-x-small)
);
}
40%,
60% {
transform: translate3d(3px, 0, 0);
sl-popup::part(popup) {
z-index: 2;
}
}
`;
.shake {
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
}
@keyframes shake {
10%,
90% {
transform: translate3d(-1px, 0, 0);
}
20%,
80% {
transform: translate3d(2px, 0, 0);
}
30%,
50%,
70% {
transform: translate3d(-3px, 0, 0);
}
40%,
60% {
transform: translate3d(3px, 0, 0);
}
}
`,
];
@property({ type: Array })
initialTags?: Tags;

View File

@ -136,9 +136,6 @@ export class CrawlsList extends LiteElement {
private timerId?: number;
// TODO localize
private numberFormatter = new Intl.NumberFormat();
private filterCrawls = (crawls: Crawl[]) =>
this.filterByState.length
? crawls.filter((crawl) =>

View File

@ -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);
}
}
`;