diff --git a/frontend/src/components/crawl-list.ts b/frontend/src/components/crawl-list.ts index 0b13dc95..d820f0ab 100644 --- a/frontend/src/components/crawl-list.ts +++ b/frontend/src/components/crawl-list.ts @@ -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; + + @state() + private dropdownIsOpen?: boolean; + + // TODO localize + private numberFormatter = new Intl.NumberFormat(); + + willUpdate(changedProperties: Map) { + 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 { }} >
-
${this.safeRender(this.renderName)}
+
+ ${this.safeRender(this.renderName)} +
${this.safeRender( (crawl) => html` @@ -265,29 +303,35 @@ export class CrawlListItem extends LitElement {
- ${this.safeRender((crawl) => - isActive - ? html`${msg("In Progress")}` - : html`` + ${this.safeRender( + (crawl) => html`` )}
${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`)} `; })}
@@ -305,27 +349,53 @@ export class CrawlListItem extends LitElement {
- { // Prevent anchor link default behavior e.preventDefault(); // Stop prop to anchor link 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; + }} + >
`; } + private renderDropdown() { + return html` `; + } + private safeRender(render: (crawl: Crawl) => any) { if (!this.crawl) { return html``; @@ -355,6 +425,22 @@ export class CrawlListItem extends LitElement { ${crawl.firstSeed}${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" }) diff --git a/frontend/src/components/crawl-status.ts b/frontend/src/components/crawl-status.ts index d2b66ee9..d6c23720 100644 --- a/frontend/src/components/crawl-status.ts +++ b/frontend/src/components/crawl-status.ts @@ -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); diff --git a/frontend/src/components/tag-input.ts b/frontend/src/components/tag-input.ts index 36237223..78fb1dd0 100644 --- a/frontend/src/components/tag-input.ts +++ b/frontend/src/components/tag-input.ts @@ -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; diff --git a/frontend/src/pages/org/crawls-list.ts b/frontend/src/pages/org/crawls-list.ts index a16d7b3b..736a348a 100644 --- a/frontend/src/pages/org/crawls-list.ts +++ b/frontend/src/pages/org/crawls-list.ts @@ -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) => diff --git a/frontend/src/utils/css.ts b/frontend/src/utils/css.ts index 6851a003..66da3606 100644 --- a/frontend/src/utils/css.ts +++ b/frontend/src/utils/css.ts @@ -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); + } + } +`;