import prettyBytes from "pretty-bytes"; import { LitElement, html, css, unsafeCSS, type PropertyValues, nothing, } from "lit"; import { property, state } from "lit/decorators.js"; import "keyword-mark-element/lib/keyword-mark.js"; import iconImageAlt from "~icons/image-alt.svg"; import { getPageDateTS, getReplayLink } from "./pageutils"; import { wrapCss } from "./misc"; import type { Page, URLTsChange } from "./types"; import { dateFormatter, dateTimeFormatter, timeFormatter, } from "./utils/dateTimeFormatter"; import type { TabNavEvent } from "./events"; import { ifDefined } from "lit/directives/if-defined.js"; // =========================================================================== class PageEntry extends LitElement { @property({ type: String }) query = ""; @property({ type: String }) textSnippet: string | null = ""; @property({ type: Object }) page: Page | null = null; @property({ type: String }) replayPrefix = ""; @property({ type: Boolean }) deleting = false; @property({ type: Boolean }) selected = false; @property({ type: Boolean }) editable = false; @property({ type: Number }) index = 0; @property({ type: Boolean }) isCurrent = false; @property({ type: Boolean }) isSidebar = false; @property({ type: Boolean }) useFaviconFallback = false; @state() thumbnailValid = true; @state() iconValid = true; static get styles() { return wrapCss(css` :host { min-height: min-content; width: 100%; word-break: break-all; position: relative; display: flex; flex-direction: row; background: transparent; } :host(.sidebar) .column { width: unset !important; } :host(.sidebar) .page-title { font-size: var(--sl-font-size-small); } :host(.sidebar) .page-title, :host(.sidebar) .page-url { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; } :host(.sidebar) .page-link:not(.current):hover { background-color: var(--sl-color-neutral-50); } :host(.sidebar) .current { background-color: var(--sl-color-blue-100); } :host(.sidebar) { width: 100%; } .check-select { padding: 0 1em 0 0.5em; height: 100%; margin: auto 0 auto 0; } .columns { width: 100%; } /* Override Bulma to add the tiniest margin, so the focus indicator isn't obscured */ .columns { margin-top: calc(-0.75rem + 2px); } .columns:last-child { margin-bottom: calc(-0.75rem + 2px); } .thumbnail { background-color: var(--sl-color-neutral-100); border: 1px solid var(--sl-color-neutral-200); border-radius: var(--sl-border-radius-large); } .thumbnail-placeholder { position: relative; } .thumbnail-placeholder-content { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; overflow: hidden; padding: var(--sl-spacing-2x-small); } .thumbnail-placeholder-content .icon { margin: var(--sl-spacing-2x-small) 0; color: var(--sl-color-neutral-300); } .thumbnail-placeholder-label { font-size: var(--sl-font-size-2x-small); color: var(--sl-color-neutral-500); line-height: 1; } .page-link { transition: background-color var(--sl-transition-x-fast); } .page-link:hover .page-title { color: var(--sl-color-blue-700); } .page-title { font-weight: var(--sl-font-weight-bold); color: var(--sl-color-blue-600); transition: color var(--sl-transition-x-fast); } .media-left .favicon { object-fit: contain; } .media-content .favicon { width: 1.15rem; height: 1.15rem; margin: 0 0.25rem; display: inline-block; vertical-align: -0.25rem; } .media-left { width: 6rem; align-self: center; text-align: center; } .delete-button { position: absolute; top: 8px; right: 8px; } .delete:hover { background-color: rgb(241, 70, 104); } .is-loading { line-height: 1.5em; height: 1.5em; border: 0px; background-color: transparent !important; width: auto; } @media screen and (max-width: 768px) { ${PageEntry.sidebarStyles()} } ${PageEntry.sidebarStyles(unsafeCSS`:host(.sidebar)`)} .is-inline-date { display: none; } .media-content a { display: block; } .col-date { font-variant-numeric: tabular-nums; } .date-time, .is-inline-date { font-size: var(--sl-font-size-small); color: var(--sl-color-neutral-700); } .page-url { font-size: var(--sl-font-size-small); } `); } static sidebarStyles(prefix = css``) { return css` ${prefix} .col-date { margin-left: calc(24px + 1rem); display: none; } ${prefix} .col-date div { display: inline; } ${prefix} .col-index { display: none; } ${prefix} .columns { display: flex; flex-direction: column-reverse; } ${prefix} .is-inline-date { display: initial !important; } `; } updated(changedProperties: PropertyValues) { if (changedProperties.has("page") || changedProperties.has("query")) { this.updateSnippet(); this.deleting = false; } } render() { const p = this.page!; const { date } = getPageDateTS(p); const hasSize = typeof p.size === "number"; const editable = this.editable && !this.isSidebar; return html` ${editable ? html`
` : ""}
${this.index ? html`
${this.index}.
` : ""}
${date ? dateFormatter.format(date) : ""}
${date ? timeFormatter.format(date) : ""}
${editable ? html` ${!this.deleting ? html` ` : html` `}` : ""}
`; } private getReplayPrefix() { const waczhash = this.page!.waczhash ? `:${this.page!.waczhash}/` : ""; const ts = this.page!.ts || ""; return this.replayPrefix + "/" + waczhash + ts + "id_"; } private renderPageIcon() { if (!this.thumbnailValid) { return html`
${this.useFaviconFallback ? this.renderFavicon() : html` No image`}
`; } return html`
(this.thumbnailValid = false)} src=${`${this.getReplayPrefix()}/urn:thumbnail:${this.page!.url}`} loading="lazy" />
`; } private renderFavicon() { if (!this.iconValid || !this.page!.favIconUrl) { return; } return html` (this.iconValid = false)} src=${`${this.getReplayPrefix()}/${this.page!.favIconUrl}`} loading="lazy" />`; } updateSnippet() { const oldVal = this.textSnippet; if (!this.query || !this.page!.text) { this.textSnippet = null; this.requestUpdate("textSnippet", oldVal); return; } let textContent = this.page!.text; let query = this.query; let inx = textContent.indexOf(this.query); if (inx < 0) { const textLower = textContent.toLowerCase(); const queryLower = query.toLowerCase(); inx = textLower.indexOf(queryLower); if (inx < 0) { this.textSnippet = null; this.requestUpdate("textSnippet", oldVal); return; } textContent = textLower; query = queryLower; } //let lastInx = textContent.lastIndexOf(query); let lastInx = inx; inx = Math.max(inx - 100, 0); lastInx = Math.min(lastInx + 200, textContent.length); if (inx === 0 && lastInx === textContent.length) { this.textSnippet = textContent; } else { this.textSnippet = "..." + textContent.slice(inx, lastInx) + "..."; } this.requestUpdate("textSnippet", oldVal); } onReplay(event: Event, reload = false) { event.preventDefault(); const data: URLTsChange = { url: this.page!.url, ts: this.page!.timestamp, waczhash: this.page!.waczhash, }; this.sendChangeEvent(data, reload); return false; } onReload(event: Event) { return this.onReplay(event, true); } sendChangeEvent(data: URLTsChange, reload: boolean) { this.dispatchEvent( new CustomEvent("coll-tab-nav", { bubbles: true, composed: true, detail: { data, reload }, }), ); } onSendDeletePage() { const page = this.page; this.dispatchEvent(new CustomEvent("delete-page", { detail: { page } })); } // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'event' implicitly has an 'any' type. onSendSelToggle(event) { const page = this.page!.id; const selected = event.currentTarget.checked; this.dispatchEvent( new CustomEvent("sel-page", { detail: { page, selected } }), ); } } customElements.define("wr-page-entry", PageEntry); export { PageEntry };