Closes https://github.com/webrecorder/replayweb.page/issues/523, closes
https://github.com/webrecorder/replayweb.page/issues/396, closes
https://github.com/webrecorder/replayweb.page/issues/470, closes
https://github.com/webrecorder/replayweb.page/issues/237

## Changes

https://github.com/webrecorder/replayweb.page/pull/521
- Adds public `navigateReplayTo` method in `<replay-app-main>` that
allows embeds to control replay navigation directly without reloading
entire UI.
- Enables reloading `<wr-coll-replay>` iframe even if source URL hasn't
changed.
- Refactors lifecycle events to prevent unnecessary re-renders.

https://github.com/webrecorder/replayweb.page/pull/524
- Fixes incorrect page count shown
- Enables non-seed pages by default
- Improves UX and accessibility of scrolling to load more by displaying
help text

https://github.com/webrecorder/replayweb.page/pull/525
- Updates general styles and icons throughout to match Browsertrix
styles
- Allows deep customization of components with `part` and `exportparts`

https://github.com/webrecorder/replayweb.page/pull/526
- Enables importing Shoelace (ie Bootstrap) icons as inline SVG
- Updates location bar buttons to use Bootstrap icons
- Replaces deprecated raw-loader with Webpack-native asset rule

https://github.com/webrecorder/replayweb.page/pull/529
- Allows hiding collection metadata in embed.
- Moves non-seed toggle next to search bar to match resources controls
and enable hiding entire column when metadata is hidden.

https://github.com/webrecorder/replayweb.page/pull/533
- Fix non-seed pages toggle

https://github.com/webrecorder/replayweb.page/pull/535,
https://github.com/webrecorder/replayweb.page/pull/534
- Upgrade to Node 22 and Yarn 4

---------

Co-authored-by: Ilya Kreymer <ikreymer@gmail.com>
Co-authored-by: Misty De Méo <hi@mistydemeo.com>
This commit is contained in:
sua yoo
2026-08-07 16:05:09 -07:00
committed by GitHub
co-authored by Ilya Kreymer Misty De Méo
parent 830468e03a
commit 141d35d687
31 changed files with 10637 additions and 7622 deletions
+16 -14
View File
@@ -18,11 +18,13 @@ jobs:
- name: Check out Git repository
uses: actions/checkout@v2
- name: Set up corepack
run: corepack enable yarn
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'yarn'
node-version: 22
- name: Cache Dirs
uses: actions/cache@v4
@@ -36,7 +38,7 @@ jobs:
- name: Update Ruffle
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: yarn run update-ruffle
run: node --run update-ruffle
- name: Run Build
env:
@@ -58,9 +60,9 @@ jobs:
#fi
if [ "$GITHUB_REF" == "refs/heads/main" ] || [ -f ./force_release ]; then
export YARN_BUILD_CMD="release"
export BUILD_CMD="release"
else
export YARN_BUILD_CMD="dist"
export BUILD_CMD="dist"
fi
if [ "$RUNNER_OS" == "Linux" ]; then
@@ -73,12 +75,12 @@ jobs:
docker run --rm \
-e CI=${CI} \
-e GH_TOKEN=${GH_TOKEN} \
-e YARN_BUILD_CMD=${YARN_BUILD_CMD} \
-e BUILD_CMD=${BUILD_CMD} \
-v ${PWD}:/project \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
electronuserland/builder:14-wine-05.22 \
/bin/bash -c "yarn --link-duplicates --pure-lockfile --ignore-engines install && yarn run $YARN_BUILD_CMD --linux --x64"
electronuserland/builder:22-wine \
/bin/bash -c "corepack enable yarn && yarn install --immutable && yarn run $BUILD_CMD --linux --x64"
else
#mkdir -p plugins-win;
#pushd plugins-win;
@@ -91,12 +93,12 @@ jobs:
-e GH_TOKEN=${GH_TOKEN} \
-e WIN_CSC_LINK=${WIN_CSC_LINK} \
-e WIN_CSC_KEY_PASSWORD=${WIN_CSC_KEY_PASSWORD} \
-e YARN_BUILD_CMD=${YARN_BUILD_CMD} \
-e BUILD_CMD=${BUILD_CMD} \
-v ${PWD}:/project \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
electronuserland/builder:14-wine-05.22 \
/bin/bash -c "yarn --link-duplicates --pure-lockfile --ignore-engines install && yarn run $YARN_BUILD_CMD --win --x64 --ia32"
electronuserland/builder:22-wine \
/bin/bash -c "corepack enable yarn && yarn install --immutable && yarn run $BUILD_CMD --win --x64 --ia32"
fi
else
#mkdir -p plugins-mac;
@@ -105,7 +107,7 @@ jobs:
#unzip PepperFlashPlayer.plugin.zip;
#rm PepperFlashPlayer.plugin.zip;
#popd;
yarn --link-duplicates --pure-lockfile --ignore-engines install
yarn run $YARN_BUILD_CMD
yarn install --immutable
yarn run $BUILD_CMD
fi
+5 -2
View File
@@ -14,10 +14,13 @@ jobs:
- name: Check out Git repository
uses: actions/checkout@v2
- name: Set up corepack
run: corepack enable yarn
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v3
uses: actions/setup-node@v6
with:
node-version: 20
node-version: 22
cache: 'yarn'
- name: Yarn Install
+6 -3
View File
@@ -10,14 +10,17 @@ jobs:
- name: Check out Git repository
uses: actions/checkout@v2
- name: Set up corepack
run: corepack enable yarn
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v3
uses: actions/setup-node@v6
with:
node-version: 20
node-version: 22
cache: 'yarn'
- name: Yarn Install
run: yarn install --frozen-lockfile --ignore-optional
run: yarn install --immutable
- name: Yarn Lint
run: yarn lint
+6 -2
View File
@@ -12,14 +12,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- name: Set up corepack
run: corepack enable yarn
- uses: actions/setup-node@v6
with:
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
cache: "yarn"
- name: Yarn Install
run: yarn install --frozen-lockfile
run: yarn install --immutable
- name: Yarn Run Build
run: yarn run build
+6 -3
View File
@@ -20,14 +20,17 @@ jobs:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Set up corepack
run: corepack enable yarn
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v3
uses: actions/setup-node@v6
with:
node-version: 20
node-version: 22
cache: 'yarn'
- name: Yarn Install
run: yarn install --frozen-lockfile
run: yarn install --immutable
- uses: actions/setup-python@v4
with:
+1
View File
@@ -5,6 +5,7 @@
*.warc.gz
**/node_modules
yarn-error.log
/.yarn/install-state.gz
coverage/
**/local-examples/
**/.DS_Store
Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
approvedGitRepositories:
- "**"
enableScripts: true
nodeLinker: node-modules
npmMinimalAgeGate: 0
+1
View File
@@ -147,6 +147,7 @@ The `#!html <replay-web-page>` tag is a web component that supports a number of
| `noWebWorker`| If set, will not use Web Worker for loading, only Service Worker. May be useful for certain loading edge cases. |
| `noCache` | If set, will not cache any loaded content HTTP responses locally, always loading from original source. |
| `hideOffscreen` | If set, will unload the embed when it is not visible and reload when scrolled into view. Useful if multiple embeds per-page to avoid loading all at once. |
| `hideMetadataSidebar` | If set, will hide the sidebar shown on the page list. May be useful for embeds that show metadata elsewhere |
| `newWindowBase` | set base replay URL loaded when a page opens a new window, defaults to `https://replayweb.page/` if `deepLink` not enabled, otherwise, to current page with new link. |
| `requireSubdomainIframe` | If set, will only load embed in an iframe loaded from a subdomain, for increased origin isolation. |
| `loading="eager"` | If set, will load the entire WACZ file at once (regardless of size), and not attempt on-demand range request loading. |
+8 -5
View File
@@ -1,7 +1,7 @@
{
"name": "replaywebpage",
"productName": "ReplayWeb.page",
"version": "2.4.7",
"version": "2.5.0-rc.2",
"description": "Serverless Web Archive Replay",
"repository": "https://github.com/webrecorder/replayweb.page",
"homepage": "https://replayweb.page/",
@@ -64,7 +64,6 @@
"lint-staged": "^14.0.0",
"mini-css-extract-plugin": "^2.3.0",
"prettier": "^3.0.1",
"raw-loader": "^4.0.2",
"sass-embedded": "^1.70.0",
"sass-loader": "^13.3.2",
"serve": "^14.2.4",
@@ -78,7 +77,7 @@
"webpack-merge": "^5.10.0"
},
"resolutions": {
"**/lit": "3.1.1"
"lit": "3.1.1"
},
"files": [
"/dist",
@@ -109,7 +108,7 @@
"release": "yarn run build && electron-builder",
"lint": "eslint ./",
"format": "prettier --write ./",
"prepare": "husky install; node scripts/build-if-not-git-repo.js"
"prepare": "husky install; yarn node scripts/build-if-not-git-repo.js"
},
"build": {
"afterSign": "build/notarize.js",
@@ -205,10 +204,14 @@
}
]
},
"engines": {
"node": ">=22 <25"
},
"lint-staged": {
"*.ts": "prettier --write"
},
"prettier": {
"trailingComma": "all"
}
},
"packageManager": "yarn@4.17.1"
}
+162 -84
View File
@@ -6,7 +6,7 @@ import {
type TemplateResult,
type PropertyValues,
} from "lit";
import { customElement, property } from "lit/decorators.js";
import { customElement, property, query } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
wrapCss,
@@ -26,7 +26,13 @@ import { SWManager } from "./swmanager";
import "./item";
import "./item-index";
import "./chooser";
import { type EmbedOpts, type LoadInfo } from "./item";
import {
type Item,
type EmbedOpts,
type LoadInfo,
RWP_SCHEME,
EmbedReplayDataView,
} from "./item";
import type { FavIconEventDetail } from "./types";
// ===========================================================================
@@ -47,6 +53,9 @@ export class ReplayWebApp extends LitElement {
@property({ type: Boolean })
showAbout = false;
@property({ type: Boolean })
showAllPages = false;
@property({ type: Boolean })
showFileDropOverlay = false;
@@ -80,7 +89,40 @@ export class ReplayWebApp extends LitElement {
@property({ type: String })
swErrorMsg: TemplateResult<1> | string | null = null;
protected swName?: string;
@query("wr-item")
private readonly item?: Item | null;
/**
* Navigate to a view in replay.
*
* @param {string} urlOrView - The page URL or view to navigate to.
* @param {Object|undefined} data - Optional view data.
*/
public navigateReplayTo(
urlOrView: EmbedReplayDataView | string,
data?: { ts: string },
) {
if (!this.item) {
console.debug("missing `this.item`");
return;
}
let value = urlOrView;
if (
Object.values(EmbedReplayDataView).some(
(view) => (view as string) === urlOrView,
)
) {
value = `${RWP_SCHEME}view=${value}${
data ? `?${new URLSearchParams(data).toString()}` : ""
}`;
}
this.item.navigateTo(value, data);
}
swName?: string;
protected swmanager: SWManager | null;
private useRuffle = false;
@@ -219,6 +261,24 @@ export class ReplayWebApp extends LitElement {
margin: 15px;
}
.about-dialog-header {
display: flex;
}
.about-dialog h3,
h4 {
font-weight: var(--sl-font-weight-bold);
margin-bottom: var(--sl-spacing-x-small);
}
.about-dialog p + p {
margin-top: 1em;
}
.about-dialog details p {
font-size: var(--sl-font-size-x-small);
}
@media screen and (min-width: 840px) {
.menu-only {
display: none;
@@ -404,11 +464,21 @@ export class ReplayWebApp extends LitElement {
.embedOpts="${this.embedOpts}"
appName="${this.appName}"
swName="${ifDefined(this.swName)}"
?showAllPages=${this.showAllPages}
@replay-favicons=${this.onFavIcons}
@update-title=${this.onTitle}
@coll-loaded=${this.onCollLoaded}
@coll-load-cancel=${this.onCollLoadCancel}
@about-show=${() => (this.showAbout = true)}
exportparts="
replay-bar:wr-item__replay-bar,
replay-bar-container:wr-item__replay-bar-container,
replay-bar-input:wr-item__replay-bar-input,
replay-content:wr-item__replay-content,
replay-tabs-nav:wr-item__replay-tabs-nav,
replay-tabs-panel:wr-item__replay-tabs-panel,
replay-main:wr-item__replay-main,
"
></wr-item>`;
}
@@ -450,7 +520,7 @@ export class ReplayWebApp extends LitElement {
return html`
${!this.embed || this.embed === "full" ? this.renderNavBar() : ""}
${this.sourceUrl ? this.renderColl() : this.renderHomeIndex()}
${this.showAbout ? this.renderAbout() : ""}
${this.renderAbout()}
${this.showFileDropOverlay ? this.renderDropFileOverlay() : ""}
`;
}
@@ -595,6 +665,10 @@ export class ReplayWebApp extends LitElement {
if (this.pageParams.has("noMediaDownload")) {
this.embedOpts = { ...this.embedOpts, noMediaDownloadUI: true };
}
if (this.pageParams.has("hideMetadataSidebar")) {
this.embedOpts = { ...this.embedOpts, hideMetadataSidebar: true };
}
}
if (this.pageParams.get("config")) {
@@ -733,91 +807,95 @@ export class ReplayWebApp extends LitElement {
}
renderAbout() {
return html`
<div class="modal is-active">
<div class="modal-background" @click="${this.onAboutClose}"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">About ReplayWeb.page ${
IS_APP ? "App" : ""
}</p>
<button class="delete" aria-label="close" @click="${
this.onAboutClose
}"></button>
</header>
<section class="modal-card-body">
<div class="container">
<div class="content">
<div style="display: flex">
<div class="has-text-centered" style="width: 220px">
<fa-icon
size="3rem"
.svg=${rwpLogo}
aria-label="ReplayWeb.page Logo"
role="img"
></fa-icon>
<div style="font-size: smaller; margin-bottom: 1em">${
IS_APP ? "App" : ""
} v${VERSION}</div>
</div>
${
IS_APP
? html`
<p>
ReplayWeb.page App is a standalone app for Mac,
Windows and Linux that loads web archive files
provided by the user and renders them for replay.
</p>
`
: html` <p>
<a href="https://replayweb.page" target="_blank"
>ReplayWeb.page</a
>
is a browser-based viewer that loads web archive
files provided by the user and renders them for
replay in the browser.
</p>`
}
</div>
<p>Full source code is available
<a href="https://github.com/webrecorder/replayweb.page" target="_blank">on GitHub</a>.
</p>
<p>See the <a target="_blank" href="./docs">documentation</a> for more info on how it works.</p>
<p>ReplayWeb.page is developed by <a href="https://webrecorder.net/" target="_blank">Webrecorder</a>.</p>
<h3>Privacy</h3>
<p><b>No data is uploaded anywhere and no information is collected.</b></p>
<p>All content rendered stays directly in your browser.<br/>When loading an archive from Google Drive,
the site may ask for account authorization to download the specified file only.</p>
<h4>Disclaimer of Warranties</h4>
<p>The application is provided "as is" without any guarantees.</p>
<details>
<summary>Legalese:</summary>
<p style="font-size: 0.8rem">DISCLAIMER OF SOFTWARE WARRANTY. WEBRECORDER SOFTWARE PROVIDES THIS SOFTWARE TO YOU "AS AVAILABLE"
AND WITHOUT WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR OTHERWISE,
INCLUDING WITHOUT LIMITATION ANY WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.</p>
</details>
<div class="has-text-centered">
<a class="button is-warning" href="#" @click="${
this.onAboutClose
}">Close</a>
</div>
</div>
</div>
</section>
return html` <sl-dialog
class="about-dialog"
label=${`About ReplayWeb.page ${IS_APP ? "App" : ""}`}
?open=${this.showAbout}
@sl-after-hide=${this.onAboutClose}
>
<div class="about-dialog-header">
<div class="has-text-centered" style="width: 220px">
<fa-icon
size="3rem"
.svg=${rwpLogo}
aria-label="ReplayWeb.page Logo"
role="img"
></fa-icon>
<div style="font-size: smaller; margin-bottom: 1em">
${IS_APP ? "App" : ""} v${VERSION}
</div>
</div>
</div>`;
${IS_APP
? html`
<p>
ReplayWeb.page App is a standalone app for Mac, Windows and
Linux that loads web archive files provided by the user and
renders them for replay.
</p>
`
: html` <p>
<a href="https://replayweb.page" target="_blank"
>ReplayWeb.page</a
>
is a browser-based viewer that loads web archive files provided by
the user and renders them for replay in the browser.
</p>`}
</div>
<p>
Full source code is available
<a href="https://github.com/webrecorder/replayweb.page" target="_blank"
>on GitHub</a
>.
</p>
<p>
See the <a target="_blank" href="./docs">documentation</a> for more info
on how it works.
</p>
<p>
ReplayWeb.page is developed by
<a href="https://webrecorder.net/" target="_blank">Webrecorder</a>.
</p>
<sl-divider></sl-divider>
<h3>Privacy</h3>
<p>No data is uploaded anywhere and no information is collected.</p>
<details>
<summary>More info</summary>
<p>
All content rendered stays directly in your browser.<br />When loading
an archive from Google Drive, the site may ask for account
authorization to download the specified file only.
</p>
</details>
<sl-divider></sl-divider>
<h4>Disclaimer of Warranties</h4>
<p>The application is provided "as is" without any guarantees.</p>
<details>
<summary>Legal</summary>
<p>
DISCLAIMER OF SOFTWARE WARRANTY. WEBRECORDER SOFTWARE PROVIDES THIS
SOFTWARE TO YOU "AS AVAILABLE" AND WITHOUT WARRANTY OF ANY KIND,
EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION ANY
WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
</p>
</details>
<sl-button
slot="footer"
variant="primary"
size="small"
@click="${this.onAboutClose}"
>Close</sl-button
>
</sl-dialog>`;
}
onAboutClose() {
readonly onAboutClose = () => {
this.showAbout = false;
}
};
renderDropFileOverlay() {
return html`
+18
View File
@@ -1,6 +1,7 @@
@charset "utf-8";
@import "@shoelace-style/shoelace/dist/themes/light.css";
@import "./shoelace.scss";
/*$wr-green: #64e986;*/
$wr-green: #55be6f;
@@ -151,3 +152,20 @@ a:focus {
white-space: nowrap;
border-width: 0;
}
// Update size of small icons in controls
.control.has-icons-left .icon.is-small, .control.has-icons-right .icon.is-small {
margin-top: 1px;
width: 2rem;
height: 2rem;
font-size: var(--sl-font-size-medium);
}
.control.has-icons-left .icon.is-small {
margin-left: 1px;
}
.control.has-icons-right .icon.is-small {
margin-right: 1px;
}
+23
View File
@@ -0,0 +1,23 @@
sl-dialog {
--header-spacing: var(--sl-spacing-small);
--body-spacing: var(--sl-spacing-small);
--footer-spacing: var(--sl-spacing-small);
--width: 40rem;
}
sl-dialog::part(header) {
border-bottom: 1px solid var(--sl-panel-border-color);
}
sl-dialog::part(title) {
font-size: var(--sl-font-size-medium);
font-weight: var(--sl-font-weight-bold);
}
sl-dialog::part(body) {
font-size: var(--sl-font-size-small);
}
sl-dialog::part(footer) {
border-top: 1px solid var(--sl-panel-border-color);
}
+41
View File
@@ -0,0 +1,41 @@
import { css, LitElement } from "lit";
import { unsafeSVG } from "lit/directives/unsafe-svg.js";
import { customElement, property } from "lit/decorators.js";
/**
* Based on `<sl-icon>` but uses inline SVG.
*/
@customElement("wr-icon")
export class Icon extends LitElement {
static styles = css`
:host {
display: inline-block;
width: 1em;
height: 1em;
box-sizing: content-box !important;
}
svg {
display: block;
height: 100%;
width: 100%;
}
`;
/**
* Raw SVG string
*/
@property({ type: String })
svg?: string;
@property({ type: String, reflect: true })
ariaHidden = "true";
render() {
if (!this.svg) {
return;
}
return unsafeSVG(this.svg);
}
}
-15
View File
@@ -6,21 +6,6 @@ import faClone from "@fortawesome/fontawesome-free/svgs/regular/clone.svg";
import faCheck from "@fortawesome/fontawesome-free/svgs/solid/check.svg";
import faX from "@fortawesome/fontawesome-free/svgs/solid/times.svg";
import { registerIconLibrary } from "@shoelace-style/shoelace/dist/utilities/icon-library.js";
import systemLibrary from "@shoelace-style/shoelace/dist/components/icon/library.system.js";
// disable system library to prevent loading of unused data: URLs
// allow only "x-lg" as it is needed for sl-dialog
registerIconLibrary("system", {
resolver: (name) => {
if (name === "x-lg") {
return systemLibrary.resolver(name);
}
return "";
},
});
@customElement("wr-labeled-field")
class LabeledField extends LitElement {
@property({ type: String })
+3 -6
View File
@@ -3,7 +3,8 @@ import webrecorderLockupColor from "~assets/brand/webrecorder-lockup-color.svg";
import btAngleDoubleDown from "~assets/icons/chevron-double-down.svg";
import btAngleDoubleUp from "~assets/icons/chevron-double-up.svg";
import fabGithub from "@fortawesome/fontawesome-free/svgs/brands/github.svg";
import fasDownload from "@fortawesome/fontawesome-free/svgs/solid/download.svg";
import iconDownload from "~icons/download.svg";
import { clickOnSpacebarPress } from "./misc";
@@ -246,11 +247,7 @@ export class RWPEmbedReceipt extends LitElement {
@keyup="${clickOnSpacebarPress}"
>
<span class="icon is-small">
<fa-icon
size="1.0em"
aria-hidden="true"
.svg="${fasDownload}"
></fa-icon>
<wr-icon .svg="${iconDownload}"></wr-icon>
</span>
<span>Download Archive</span>
</a>
+35 -20
View File
@@ -13,6 +13,7 @@ import { property, query } from "lit/decorators.js";
import type { FavIconEventDetail } from "./types";
import type { EmbedReplayData } from "./item";
import type { RwpPageLoadingEvent, RwpUrlChangeEvent } from "./events";
import type { ReplayWebApp } from "./appmain";
type IframeMessage = MessageEvent<
| RwpUrlChangeEvent["detail"]
@@ -67,6 +68,7 @@ class Embed extends LitElement {
@property({ type: Boolean }) hideOffscreen: boolean | undefined;
@property({ type: Boolean }) noMediaDownload = false;
@property({ type: Boolean }) hideMetadataSidebar = false;
@property({ type: Boolean }) useAdblock = false;
@property({ type: String }) adblockRulesUrl = DEFAULT_ADBLOCK_FILE;
@@ -88,6 +90,8 @@ class Embed extends LitElement {
@query("iframe")
private readonly iframe?: HTMLIFrameElement | null;
mainElement?: ReplayWebApp | null;
replayfile = defaultReplayFile;
mainElementName = "replay-app-main";
appName = "ReplayWeb.page";
@@ -267,20 +271,16 @@ class Embed extends LitElement {
const qs = new URLSearchParams(window.location.hash.slice(1));
if (qs.has("url")) {
// @ts-expect-error - TS2339 - Property 'url' does not exist on type 'Embed'.
this.url = qs.get("url");
this.url = qs.get("url") ?? "";
}
if (qs.has("ts")) {
// @ts-expect-error - TS2339 - Property 'ts' does not exist on type 'Embed'.
this.ts = qs.get("ts");
this.ts = qs.get("ts") ?? "";
}
if (qs.has("query")) {
// @ts-expect-error - TS2339 - Property 'query' does not exist on type 'Embed'.
this.query = qs.get("query");
this.query = qs.get("query") ?? "";
}
if (qs.has("view")) {
// @ts-expect-error - TS2339 - Property 'view' does not exist on type 'Embed'.
this.view = qs.get("view");
this.view = qs.get("view") ?? "";
}
}
@@ -333,6 +333,7 @@ class Embed extends LitElement {
ruffle?: "1";
adblockUrl?: string;
noMediaDownload?: string;
hideMetadataSidebar?: string;
} = {
source,
customColl: this.coll,
@@ -378,6 +379,10 @@ class Embed extends LitElement {
params.noMediaDownload = "1";
}
if (this.hideMetadataSidebar) {
params.hideMetadataSidebar = "1";
}
this.paramString = new URLSearchParams(
// Converting to unknown here so that we can ignore the URL -> string conversion necessary for the types to work out
params as unknown as Record<string, string>,
@@ -452,30 +457,40 @@ class Embed extends LitElement {
`;
}
// @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'event' implicitly has an 'any' type.
onLoad(event) {
onLoad(event: Event) {
if (this.isCrossOrigin) {
return;
}
const win = event.target.contentWindow;
const doc = event.target.contentDocument;
const target = event.target as HTMLIFrameElement;
const win = target.contentWindow;
const doc = target.contentDocument;
if (
win.navigator.serviceWorker &&
!win.navigator.serviceWorker.controller &&
this.reloadCount <= 2
) {
if (!win) {
console.debug("missing `event.target.contentWindow`");
return;
}
if (!win.navigator.serviceWorker.controller && this.reloadCount <= 2) {
this.reloadCount++;
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
setTimeout(() => win.location.reload(), 100);
return;
}
this.reloadCount = 0;
if (win.customElements.get(this.mainElementName)) {
if (!doc) {
console.debug("missing `event.target.contentDocument`");
return;
}
if (win?.customElements.get(this.mainElementName)) {
this.mainElement = doc.querySelector(this.mainElementName);
return;
}
if (!scriptSrc) {
console.debug("missing `scriptSrc`");
return;
}
+1
View File
@@ -15,6 +15,7 @@ import { URLResources } from "./url-resources";
import { Embed } from "./embed";
import "./coll-description";
import "./shoelace";
import "./components/icon";
import rwpIcon from "~assets/icons/replaywebpage.svg";
import rwpLogoAnimated from "~assets/brand/replaywebpage-icon-color-animated.svg";
+2 -2
View File
@@ -5,7 +5,7 @@ import { wrapCss, apiPrefix } from "./misc";
import fasArrowUp from "@fortawesome/fontawesome-free/svgs/solid/angle-double-up.svg";
import fasArrowDown from "@fortawesome/fontawesome-free/svgs/solid/angle-double-down.svg";
import fasSearch from "@fortawesome/fontawesome-free/svgs/solid/search.svg";
import iconSearch from "~icons/search.svg";
import type { ItemType } from "./types";
@@ -306,7 +306,7 @@ class ItemIndex extends LitElement {
placeholder="Search by Archive Title or Source"
/>
<span class="icon is-left is-small">
<fa-icon .svg="${fasSearch}"></fa-icon>
<wr-icon .svg="${iconSearch}"></wr-icon>
</span>
</div>
<wr-sorter
+452 -408
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -57,6 +57,9 @@ export function updateFaviconLinks(data: FavIconEventDetail) {
}
// ===========================================================================
/**
* @deprecated Use `<wr-icon>`
*/
class FaIcon extends LitElement {
constructor() {
super();
+130 -44
View File
@@ -1,10 +1,19 @@
import prettyBytes from "pretty-bytes";
import { LitElement, html, css, unsafeCSS, type PropertyValues } from "lit";
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";
@@ -15,6 +24,7 @@ import {
timeFormatter,
} from "./utils/dateTimeFormatter";
import type { TabNavEvent } from "./events";
import { ifDefined } from "lit/directives/if-defined.js";
// ===========================================================================
class PageEntry extends LitElement {
@@ -48,6 +58,9 @@ class PageEntry extends LitElement {
@property({ type: Boolean })
isSidebar = false;
@property({ type: Boolean })
useFaviconFallback = false;
@state()
thumbnailValid = true;
@@ -70,6 +83,26 @@ class PageEntry extends LitElement {
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%;
}
@@ -92,23 +125,62 @@ class PageEntry extends LitElement {
margin-bottom: calc(-0.75rem + 2px);
}
.favicon {
display: inline-block;
vertical-align: text-bottom;
.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 {
width: 2rem;
height: 2rem;
}
.media-left img.favicon {
filter: drop-shadow(1px 1px 2px grey);
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 {
@@ -141,18 +213,6 @@ class PageEntry extends LitElement {
${PageEntry.sidebarStyles(unsafeCSS`:host(.sidebar)`)}
.current a {
background-color: rgb(207, 243, 255);
font-style: italic;
display: block;
}
.current .curr-page {
font-style: italic;
font-size: 9px;
color: black;
}
.is-inline-date {
display: none;
}
@@ -164,6 +224,16 @@ class PageEntry extends LitElement {
.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);
}
`);
}
@@ -185,10 +255,6 @@ class PageEntry extends LitElement {
}
${prefix} .is-inline-date {
display: initial !important;
font-style: italic;
}
${prefix} .media-left {
padding-left: 0.75rem;
}
`;
}
@@ -229,12 +295,15 @@ class PageEntry extends LitElement {
: ""}
<div class="column col-date is-2">
<div>${date ? dateFormatter.format(date) : ""}</div>
<div>${date ? timeFormatter.format(date) : ""}</div>
<div class="date-time">${date ? timeFormatter.format(date) : ""}</div>
</div>
<div class="column">
<div
class="page-link column ${this.isCurrent ? "current" : ""}"
aria-current=${ifDefined(this.isCurrent ? "location" : undefined)}
>
<div class="media">
<figure class="media-left">${this.renderPageIcon()}</figure>
<div class="media-content ${this.isCurrent ? "current" : ""}">
<div class="media-content">
<div role="heading" aria-level="${this.isSidebar ? "4" : "3"}">
<a
@dblclick="${this.onReload}"
@@ -246,17 +315,20 @@ class PageEntry extends LitElement {
this.page!.waczhash,
)}"
>
<p class="is-size-6 has-text-weight-bold has-text-link text">
<keyword-mark keywords="${this.query}"
<p class="page-title text">
${this.thumbnailValid ? this.renderFavicon() : nothing}
<keyword-mark
keywords="${this.query}"
title="${p.title || p.url}"
>${p.title || p.url}</keyword-mark
>
</p>
<p class="has-text-dark text">
<keyword-mark keywords="${this.query}"
<p class="page-url has-text-dark text">
<keyword-mark keywords="${this.query}" title="${p.url}"
>${p.url}</keyword-mark
>${this.thumbnailValid ? this.renderFavicon() : ""}
>
</p>
<p class="has-text-grey-dark text is-inline-date">
<p class="text is-inline-date">
${date ? dateTimeFormatter.format(date) : ""}
</p>
</a>
@@ -269,9 +341,7 @@ class PageEntry extends LitElement {
: html``}
</div>
${hasSize
? html` <div class="media-right" style="margin-right: 2em">
${prettyBytes(p.size)}
</div>`
? html` <div class="text">${prettyBytes(p.size)}</div>`
: ""}
</div>
</div>
@@ -301,14 +371,30 @@ class PageEntry extends LitElement {
private renderPageIcon() {
if (!this.thumbnailValid) {
return this.renderFavicon();
return html`<div
class="thumbnail-placeholder image is-16by9 ${this.useFaviconFallback
? "favicon-fallback"
: "thumbnail"}"
>
<div class="thumbnail-placeholder-content">
${this.useFaviconFallback
? this.renderFavicon()
: html`<span class="icon is-small">
<wr-icon .svg="${iconImageAlt}" aria-hidden="true"></wr-icon>
</span>
<span class="thumbnail-placeholder-label">No image</span>`}
</div>
</div>`;
}
return html`<img
class="thumbnail"
@error=${() => (this.thumbnailValid = false)}
src=${`${this.getReplayPrefix()}/urn:thumbnail:${this.page!.url}`}
loading="lazy"
/>`;
return html`<div class="image is-16by9">
<img
class="thumbnail"
@error=${() => (this.thumbnailValid = false)}
src=${`${this.getReplayPrefix()}/urn:thumbnail:${this.page!.url}`}
loading="lazy"
/>
</div>`;
}
private renderFavicon() {
+201 -106
View File
@@ -1,7 +1,14 @@
"use strict";
import { LitElement, html, css, unsafeCSS, type PropertyValues } from "lit";
import { property } from "lit/decorators.js";
import {
LitElement,
html,
css,
unsafeCSS,
type PropertyValues,
nothing,
} from "lit";
import { property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { wrapCss, clickOnSpacebarPress } from "./misc";
import ndjson from "fetch-ndjson";
@@ -11,9 +18,10 @@ import prettyBytes from "pretty-bytes";
import { getTS, getPageDateTS } from "./pageutils";
import fasSearch from "@fortawesome/fontawesome-free/svgs/solid/search.svg";
import fasAngleDown from "@fortawesome/fontawesome-free/svgs/solid/angle-down.svg";
import fasEdit from "@fortawesome/fontawesome-free/svgs/solid/edit.svg";
import iconArrowDown from "~icons/arrow-down.svg";
import iconChevronDown from "~icons/chevron-down.svg";
import iconPencil from "~icons/pencil.svg";
import iconSearch from "~icons/search.svg";
import type { Sorter } from "./sorter";
import type { PageEntry } from "./pageentry";
@@ -100,6 +108,9 @@ class Pages extends LitElement {
@property({ type: Object })
collInfo: ItemType | Record<string, never> | null = null;
@property({ type: Boolean })
hideMetadataSidebar = false;
@property({ type: Boolean })
allSelected = false;
@@ -109,15 +120,15 @@ class Pages extends LitElement {
@property({ type: Boolean })
dynamicPagesQuery = false;
@property({ type: Number })
totalPages = 0;
@property({ type: Number })
dynamicPageCount = 1;
@property({ type: Boolean })
skipScrollMore = false;
@state()
private filteredPagesTotal = 0;
private _ival: number | undefined;
static get sortKeys() {
@@ -222,17 +233,20 @@ class Pages extends LitElement {
}
this.loading = true;
if (this.flex && this.query && this.textPages) {
const results = await this.flex.searchAsync(this.query, 25);
const results = await this.flex.searchAsync(
this.query,
DYNAMIC_PAGE_SIZE,
);
this.filteredPages = results.map(
(inx: Id) => this.textPages![inx as number],
);
} else if (this.showAllPages && this.hasExtraPages) {
this.filteredPages = [...this.textPages!];
this.filteredPages = [...this.textPages!, ...this.collInfo!.pages];
} else if (!this.dynamicPagesQuery) {
this.filteredPages = [...this.collInfo!.pages];
}
this.totalPages = this.filteredPages.length;
this.filteredPagesTotal = this.filteredPages.length;
if (this.dynamicPagesQuery) {
if (!this.query) {
@@ -347,7 +361,7 @@ class Pages extends LitElement {
this.filteredPages = [...this.filteredPages, ...newPages];
}
this.totalPages = this.filteredPages.length;
this.filteredPagesTotal = this.filteredPages.length;
}
async filterCurated() {
@@ -369,10 +383,7 @@ class Pages extends LitElement {
);
}
// @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'pages' implicitly has an 'any' type.
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/promise-function-async
addPages(pages) {
async addPages(pages: Page[]) {
const flex = new FlexIndex();
this.flex = flex;
@@ -380,8 +391,7 @@ class Pages extends LitElement {
if (!this.dynamicPagesQuery) {
this.hasExtraPages = Boolean(
this.textPages &&
this.collInfo?.pages &&
this.collInfo?.pages &&
this.textPages.length > this.collInfo.pages.length,
);
}
@@ -391,10 +401,7 @@ class Pages extends LitElement {
}
return Promise.all(
// @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'page' implicitly has an 'any' type. | TS7006 - Parameter 'index' implicitly has an 'any' type.
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/promise-function-async
pages.map((page, index) => {
pages.map(async (page, index) => {
let text = page.url;
if (page.title) {
text += " " + page.title;
@@ -402,8 +409,6 @@ class Pages extends LitElement {
if (page.text) {
text += " " + page.text;
}
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
return flex.addAsync(index, text);
}),
);
@@ -433,7 +438,7 @@ class Pages extends LitElement {
}
}
const lines: unknown[] = [];
const lines: Page[] = [];
for await (const line of ndjson(resp.body!.getReader())) {
if (!line.url) {
@@ -445,7 +450,7 @@ class Pages extends LitElement {
if (typeof line.ts === "string") {
line.ts = new Date(line.ts as string).getTime();
}
lines.push(line);
lines.push(line as unknown as Page);
}
await this.addPages(lines);
@@ -489,6 +494,7 @@ class Pages extends LitElement {
.header.columns {
width: 100%;
margin-bottom: 0px;
padding: var(--sl-spacing-x-small) var(--sl-spacing-small);
}
.header a {
color: black;
@@ -503,8 +509,6 @@ class Pages extends LitElement {
display: flex;
flex-direction: column;
padding: 0px;
margin-top: 0.5em;
margin-left: 0.75em;
}
.thumbnail {
@@ -516,10 +520,10 @@ class Pages extends LitElement {
.index-bar {
display: flex;
flex-direction: column;
border-right: 3px solid rgb(237, 237, 237);
background-color: whitesmoke;
padding-right: 0px;
border-right: 1px solid var(--sl-panel-border-color);
background-color: var(--sl-color-neutral-50);
position: relative;
padding: 0;
}
.index-bar-label {
@@ -527,17 +531,27 @@ class Pages extends LitElement {
font-size: var(--sl-font-size-x-small);
font-weight: var(--sl-font-weight-semibold);
color: var(--sl-color-neutral-500);
margin-bottom: var(--sl-spacing-2x-small);
margin-bottom: var(--sl-spacing-x-small);
padding: 0 var(--sl-spacing-small);
line-height: 1;
}
.index-bar-label:first-child {
margin-top: var(--sl-spacing-medium);
}
.index-bar-title {
font-size: var(--sl-font-size-large);
font-size: var(--sl-font-size-small);
font-weight: var(--sl-font-weight-semibold);
margin-bottom: var(--sl-spacing-large);
margin-bottom: var(--sl-spacing-small);
padding: 0 var(--sl-spacing-small);
word-break: break-word;
}
.index-bar-title + .index-bar-label {
margin-top: var(--sl-spacing-medium);
}
.index-bar-title:hover + .editIcon {
display: block;
}
@@ -565,9 +579,59 @@ class Pages extends LitElement {
}
.num-results {
font-style: italic;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--sl-spacing-small);
justify-content: space-between;
font-weight: normal;
line-height: 2.5;
font-size: var(--sl-font-size-small);
color: var(--sl-color-neutral-700);
line-height: 1;
border-top: 1px solid var(--sl-panel-border-color);
}
.main-num-results {
padding: var(--sl-spacing-x-small) var(--sl-spacing-small);
}
.sidebar-num-results {
padding: var(--sl-spacing-x-small);
}
.seed-pages-filter {
font-size: var(--sl-font-size-small);
}
.seed-pages-filter .checkbox {
display: flex;
align-items: center;
gap: var(--sl-spacing-x-small);
line-height: 1.5;
padding-inline-end: var(--sl-spacing-x-small);
}
.seed-pages-filter .checkbox span {
margin-top: -0.0875rem;
}
.sorter {
gap: var(--sl-spacing-x-small);
align-items: center;
}
.sorter wr-sorter {
flex-grow: 1;
}
.sort-control-label {
display: inline-flex;
align-items: center;
gap: var(--sl-spacing-2x-small);
font-size: var(--sl-font-size-small);
font-weight: var(--sl-font-weight-semibold);
color: var(--sl-color-neutral-600);
padding-bottom: 0;
}
.asc:after {
@@ -613,19 +677,31 @@ class Pages extends LitElement {
display: flex !important;
}
:host(.sidebar) .search-bar {
flex-direction: column;
align-items: stretch;
}
.scroller {
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
flex: auto;
padding-bottom: 1em;
padding: var(--sl-spacing-small);
min-height: 0px;
}
.scroller-help-text {
display: inline-flex;
align-items: center;
gap: var(--sl-spacing-2x-small);
color: var(--sl-color-neutral-500);
}
.page-entry {
padding-bottom: 1.5rem;
margin-right: -1.5rem;
}
.selected {
@@ -643,19 +719,22 @@ class Pages extends LitElement {
align-items: baseline;
width: 100%;
min-height: fit-content;
margin-bottom: 1em;
border-bottom: 3px solid rgb(237, 237, 237);
}
.check-select {
padding: 0 1em 0 0.5em;
border-bottom: 1px solid var(--sl-panel-border-color);
}
.search-bar {
width: auto;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--sl-spacing-small);
background-color: var(--sl-color-neutral-100);
border-bottom: 1px solid var(--sl-panel-border-color);
font-size: var(--sl-font-size-small);
padding: var(--sl-spacing-x-small);
}
.search-bar .input {
font-size: var(--sl-font-size-small);
}
.flex-auto {
@@ -663,11 +742,11 @@ class Pages extends LitElement {
}
.index-bar-description {
margin-bottom: var(--sl-spacing-x-small);
font-size: var(--sl-font-size-small);
line-height: var(--sl-line-height-normal);
flex: 1 1 auto;
overflow: auto;
border-bottom: 1px solid var(--sl-panel-border-color);
padding: 0 var(--sl-spacing-small);
}
`);
}
@@ -698,8 +777,7 @@ class Pages extends LitElement {
}
${prefix} .mobile-header {
margin: 0.5rem;
margin-left: 1rem;
margin: var(--sl-spacing-x-small);
align-items: center;
display: flex;
justify-content: space-between;
@@ -728,6 +806,9 @@ class Pages extends LitElement {
render() {
const currList = this.currList;
const collList = this.collInfo?.lists.length;
const hideSideCol =
this.hideMetadataSidebar && !this.editable && !this.editing && !collList;
return html`
<div
@@ -737,11 +818,11 @@ class Pages extends LitElement {
>
Pages in ${this.collInfo!.title}
</div>
<div class="search-bar notification is-marginless">
<div class="search-bar is-marginless">
${this.isSidebar
? html`<h3 class="is-sr-only">Search and Filter Pages</h3>`
: ""}
<div class="field flex-auto">
<div class="flex-auto">
<div
class="control has-icons-left ${this.loading ? "is-loading" : ""}"
>
@@ -752,16 +833,19 @@ class Pages extends LitElement {
type="text"
placeholder="Search by Page URL, Title, or Text"
/>
<span class="icon is-left"
><fa-icon .svg="${fasSearch}" aria-hidden="true"></fa-icon
<span class="icon is-left is-small"
><wr-icon .svg="${iconSearch}"></wr-icon
></span>
</div>
</div>
${this.renderSeedPagesFilter()}
</div>
<div class="main columns">
<div
class="column index-bar is-one-fifth ${this.isSidebar
? "is-hidden-mobile"
: hideSideCol
? "is-hidden"
: ""}"
>
${this.editable && this.editing
@@ -775,11 +859,9 @@ class Pages extends LitElement {
/>
</form>
`
: html` <div
class="index-bar-label ${this.collInfo!.description
? "is-hidden-mobile"
: "is-sr-only"}"
>
: this.hideMetadataSidebar
? nothing
: html`<div class="index-bar-label is-hidden-mobile">
Collection Name
</div>
<div
@@ -802,36 +884,14 @@ class Pages extends LitElement {
</div>`
: html``}`}
${this.editable
? html`<fa-icon class="editIcon" .svg="${fasEdit}"></fa-icon>`
? html`<wr-icon class="editIcon" .svg="${iconPencil}"></wr-icon>`
: html``}
${this.hasExtraPages
? html` <span class="check-select">
<label class="checkbox">
<input
@change=${(e: Event) =>
(this.showAllPages = (
e.currentTarget as HTMLInputElement
).checked)}
type="checkbox"
.checked="${this.showAllPages}"
/>
Show Non-Seed Pages
</label>
</span>`
: ""}
<span
class="num-results is-hidden-mobile"
aria-live="polite"
aria-atomic="true"
>${this.formatResults()}</span
>
${this.editable
? html` <div class="index-bar-actions">
${this.renderDownloadMenu()}
</div>`
: ""}
${this.collInfo!.lists.length
${collList
? html`
<p id="filter-label" class="menu-label">Filter By List:</p>
<div class="index-bar-menu menu">
@@ -889,7 +949,7 @@ class Pages extends LitElement {
>
<span>Download</span>
<span class="icon is-small">
<fa-icon .svg="${fasAngleDown}" aria-hidden="true"></fa-icon>
<wr-icon .svg="${iconChevronDown}"></wr-icon>
</span>
</button>
</div>
@@ -967,7 +1027,7 @@ class Pages extends LitElement {
@click="${this.onSort}"
@keyup="${clickOnSpacebarPress}"
data-key=""
class="column is-1 ${this.sortKey === ""
class="sort-control-label column is-1 ${this.sortKey === ""
? this.sortDesc
? "desc"
: "asc"
@@ -982,7 +1042,7 @@ class Pages extends LitElement {
@click="${this.onSort}"
@keyup="${clickOnSpacebarPress}"
data-key="date"
class="column is-2 ${this.sortKey === "date"
class="sort-control-label column is-2 ${this.sortKey === "date"
? this.sortDesc
? "desc"
: "asc"
@@ -998,8 +1058,9 @@ class Pages extends LitElement {
@click="${this.onSort}"
@keyup="${clickOnSpacebarPress}"
data-key="title"
class="column is-6 pagetitle ${this.query ? "is-5" : "is-6"} ${this
.sortKey === "title"
class="sort-control-label column is-6 pagetitle ${this.query
? "is-5"
: "is-6"} ${this.sortKey === "title"
? this.sortDesc
? "desc"
: "asc"
@@ -1008,17 +1069,16 @@ class Pages extends LitElement {
>
</div>
<div class="is-hidden-tablet mobile-header">
<div class="num-results" aria-live="polite" aria-atomic="true">
${this.formatResults()}
</div>
<div class="sorter is-hidden-tablet mobile-header">
<wr-sorter
id="pages"
.sortKey="${this.sortKey}"
.sortDesc="${this.sortDesc}"
.sortKeys="${Pages.sortKeys}"
.data="${this.filteredPages}"
.pageResults="${this.dynamicPagesQuery ? this.totalPages : 100}"
.pageResults="${this.dynamicPagesQuery
? this.filteredPagesTotal
: 100}"
@sort-changed="${this.onSortChanged}"
class="${this.filteredPages.length ? "" : "is-hidden"}"
>
@@ -1092,9 +1152,7 @@ class Pages extends LitElement {
renderPages() {
//const name = this.currList === 0 ? "All Pages" : this.collInfo.lists[this.currList - 1].title;
return html`
<div class="page-header has-text-weight-bold">
${this.renderPageHeader()}
</div>
<div class="page-header">${this.renderPageHeader()}</div>
<ul class="scroller" @scroll="${this.onScroll}">
${this.sortedPages.length
? html` ${this.sortedPages.map((p, i) => {
@@ -1107,6 +1165,7 @@ class Pages extends LitElement {
?selected="${selected}"
?isCurrent="${this.isCurrPage(p)}"
?isSidebar="${this.isSidebar}"
useFaviconFallback
.page="${p}"
pid="${p.id}"
@sel-page="${this.onSelectToggle}"
@@ -1120,6 +1179,15 @@ class Pages extends LitElement {
})}`
: html`<p class="mobile-header">${this.getNoResultsMessage()}</p>`}
</ul>
<div
class="num-results ${this.isSidebar
? "sidebar-num-results"
: "main-num-results"}"
aria-live="polite"
aria-atomic="true"
>
${this.renderPaginationResults()}
</div>
`;
}
@@ -1310,13 +1378,41 @@ class Pages extends LitElement {
this.requestUpdate();
}
formatResults() {
const length = this.totalPages;
if (length === this.sortedPages.length) {
return `${length} Page${length !== 1 ? "s" : ""}`;
} else {
return `${this.sortedPages.length} of ${length} Pages Shown`;
}
renderPaginationResults() {
const total = this.filteredPagesTotal;
const shown = this.sortedPages.length;
const totalStr = `${total.toLocaleString()} ${
total === 1 ? "page" : "pages"
}`;
const allShown = total === shown;
const countMsg = allShown
? totalStr
: `${shown.toLocaleString()} of ${totalStr}`;
return html`<span>${countMsg}</span>
${allShown
? nothing
: html`<span class="scroller-help-text">
Scroll for more
<wr-icon .svg="${iconArrowDown}"></wr-icon>
</span>`} `;
}
renderSeedPagesFilter() {
if (!this.hasExtraPages) return;
return html`<div class="seed-pages-filter check-select">
<label class="checkbox">
<input
@change=${(e: Event) =>
(this.showAllPages = (e.currentTarget as HTMLInputElement).checked)}
type="checkbox"
.checked="${this.showAllPages}"
/>
<span>Include Non-Seed Pages</span>
</label>
</div>`;
}
getNoResultsMessage() {
@@ -1345,9 +1441,8 @@ class Pages extends LitElement {
return "No Pages Found";
}
// @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'event' implicitly has an 'any' type.
async onScroll(event) {
const element = event.currentTarget;
async onScroll(event: Event) {
const element = event.currentTarget as HTMLUListElement;
const diff =
element.scrollHeight - element.scrollTop - element.clientHeight;
if (diff < 40 && !this.skipScrollMore) {
+51 -15
View File
@@ -1,13 +1,17 @@
import { LitElement, html, css, type PropertyValues } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { property } from "lit/decorators.js";
import { property, query, state } from "lit/decorators.js";
import { wrapCss } from "./misc";
import rwpLogo from "~assets/brand/replaywebpage-icon-color.svg";
import type { ItemType } from "./types";
import type { ReplayLoadingDetail, TabNavEvent } from "./events";
import { keyed } from "lit/directives/keyed.js";
/**
* @cssPart iframe
* @cssPart iframe-container
* @cssPart iframe-page-not-found
* @fires update-title
* @fires coll-tab-nav
* @fires update-title
@@ -62,12 +66,31 @@ class Replay extends LitElement {
@property({ type: String })
downloadResUrl = "";
@query("iframe")
readonly iframe?: HTMLIFrameElement | null;
/**
* Use key to force iframe to update even if `src` hasn't changed,
* for example, if the iframe window has navigated to a linked page
* and is being externally reset to a seed URL.
*/
@state()
private iframeUrlUpdateKey?: number;
private reauthWait: null | Promise<void> = null;
private _loadPoll: null | number = null;
private hiliter: HoverHiliter | null = null;
disconnectedCallback(): void {
super.disconnectedCallback();
if (this._loadPoll) {
window.clearInterval(this._loadPoll);
}
}
firstUpdated() {
window.addEventListener("message", (event) => this.onReplayMessage(event));
// TODO: Fix this the next time the file is edited.
@@ -121,7 +144,7 @@ class Replay extends LitElement {
: "";
}
updated(changedProperties: PropertyValues<this>) {
willUpdate(changedProperties: PropertyValues<this>) {
if (
changedProperties.has("sourceUrl") ||
changedProperties.has("collInfo")
@@ -138,9 +161,17 @@ class Replay extends LitElement {
this.replayTS = this.ts;
this.showAuth = false;
this.reauthWait = null;
this.doSetIframeUrl();
}
const prevIframe = this.iframeUrl;
this.doSetIframeUrl();
if (prevIframe === this.iframeUrl) {
this.iframeUrlUpdateKey = Date.now();
}
}
}
updated(changedProperties: PropertyValues<this>) {
if (this.iframeUrl && changedProperties.has("iframeUrl")) {
this.waitForLoad();
@@ -434,17 +465,22 @@ class Replay extends LitElement {
: undefined,
)}"
></a>
<div class="iframe-container">
<iframe
class="iframe-main"
name="___wb_replay_top_frame"
@message="${this.onReplayMessage}"
allow="autoplay 'self'; fullscreen"
allowfullscreen
src="${this.iframeUrl}"
title="${title}"
></iframe>
<div part="iframe-container" class="iframe-container">
${keyed(
this.iframeUrlUpdateKey,
html`<iframe
part="iframe ${this.replayNotFoundError
? "iframe-page-not-found"
: ""}"
class="iframe-main"
name="___wb_replay_top_frame"
@message="${this.onReplayMessage}"
allow="autoplay 'self'; fullscreen"
allowfullscreen
src="${this.iframeUrl}"
title="${title}"
></iframe>`,
)}
${this.showAuth
? html`
<div class="iframe-main modal-bg">
+16
View File
@@ -1,8 +1,24 @@
import { registerIconLibrary } from "@shoelace-style/shoelace/dist/utilities/icon-library.js";
import systemLibrary from "@shoelace-style/shoelace/dist/components/icon/library.system.js";
// Cherry-picked Shoelace components
import "@shoelace-style/shoelace/dist/components/alert/alert.js";
import "@shoelace-style/shoelace/dist/components/button/button.js";
import "@shoelace-style/shoelace/dist/components/copy-button/copy-button.js";
import "@shoelace-style/shoelace/dist/components/dialog/dialog.js";
import "@shoelace-style/shoelace/dist/components/divider/divider.js";
import "@shoelace-style/shoelace/dist/components/dropdown/dropdown.js";
import "@shoelace-style/shoelace/dist/components/menu/menu.js";
import "@shoelace-style/shoelace/dist/components/menu-item/menu-item.js";
import "@shoelace-style/shoelace/dist/components/menu-label/menu-label.js";
// disable system library to prevent loading of unused data: URLs
// allow only "x-lg" as it is needed for sl-dialog
registerIconLibrary("system", {
resolver: (name) => {
if (name === "x-lg") {
return systemLibrary.resolver(name);
}
return "";
},
});
+36 -26
View File
@@ -3,8 +3,8 @@ import { customElement, property } from "lit/decorators.js";
import { wrapCss } from "./misc";
import fasSortDown from "@fortawesome/fontawesome-free/svgs/solid/sort-down.svg";
import fasSortUp from "@fortawesome/fontawesome-free/svgs/solid/sort-up.svg";
import iconSortDown from "~icons/sort-down.svg";
import iconSortUp from "~icons/sort-up.svg";
// ===========================================================================
@customElement("wr-sorter")
@@ -118,34 +118,44 @@ class Sorter<T = unknown> extends LitElement {
button.button.is-small {
border-radius: 4px;
}
.is-small {
font-size: var(--sl-font-size-x-small);
}
`);
}
render() {
return html`
<div class="select is-small">
<select id="sort-select" @change=${(e: Event) =>
(this.sortKey = (e.currentTarget as HTMLSelectElement).value)}>
${this.sortKeys?.map(
(sort) => html`
<option value="${sort.key}" ?selected="${sort.key === this.sortKey}">
Sort By: ${sort.name}
</option>
`,
)}
</select>
</div>
<button @click=${() =>
(this.sortDesc = !this.sortDesc)} class="button is-small">
<span>Order:</span>
<span class="is-sr-only">${
this.sortDesc ? "Ascending" : "Descending"
}</span>
<span class="icon"><fa-icon aria-hidden="true" .svg=${
this.sortDesc ? fasSortUp : fasSortDown
}></span>
</button>`;
return html` <div class="select is-small">
<select
id="sort-select"
@change=${(e: Event) =>
(this.sortKey = (e.currentTarget as HTMLSelectElement).value)}
>
${this.sortKeys?.map(
(sort) => html`
<option
value="${sort.key}"
?selected="${sort.key === this.sortKey}"
>
Sort By: ${sort.name}
</option>
`,
)}
</select>
</div>
<button
@click=${() => (this.sortDesc = !this.sortDesc)}
class="button is-small"
>
<span>Order:</span>
<span class="is-sr-only"
>${this.sortDesc ? "Ascending" : "Descending"}</span
>
<span class="icon"
><wr-icon .svg=${this.sortDesc ? iconSortDown : iconSortUp}></wr-icon
></span>
</button>`;
}
}
+1
View File
@@ -69,6 +69,7 @@ export type ItemType = {
totalSize?: unknown;
size?: number | string;
canQueryPages?: boolean;
pageCount?: number;
};
export type FavIconEventDetail = {
+110 -45
View File
@@ -3,8 +3,8 @@ import { wrapCss, clickOnSpacebarPress } from "./misc";
import { getReplayLink, getDownloadLink } from "./pageutils";
import fasSearch from "@fortawesome/fontawesome-free/svgs/solid/search.svg";
import fasDownload from "@fortawesome/fontawesome-free/svgs/solid/download.svg";
import iconDownload from "~icons/download.svg";
import iconSearch from "~icons/search.svg";
import "keyword-mark-element/lib/keyword-mark.js";
import { type ItemType } from "./types";
@@ -268,6 +268,10 @@ class URLResources extends LitElement {
display: block !important;
}
:host(.sidebar) .level-left {
margin-bottom: var(--sl-spacing-x-small);
}
:host(.sidebar) .columns {
display: flex !important;
flex-direction: column;
@@ -277,17 +281,33 @@ class URLResources extends LitElement {
width: 100% !important;
}
.notification {
.search-bar {
gap: var(--sl-spacing-small);
width: 100%;
background-color: var(--sl-color-neutral-100);
border-bottom: 1px solid var(--sl-panel-border-color);
font-size: var(--sl-font-size-small);
padding: var(--sl-spacing-x-small);
}
.search-bar .level-item {
gap: var(--sl-spacing-x-small);
}
.search-bar .input,
.search-bar .radio {
font-size: var(--sl-font-size-small);
}
.all-results {
margin: 0 0 0 0.5em;
display: flex;
flex-direction: column;
min-height: 0;
flex: 1 1 0;
}
.main-scroll {
flex-grow: 1;
flex: 1 1 0;
scrollbar-gutter: stable;
}
.minihead {
font-size: 10px;
@@ -296,32 +316,58 @@ class URLResources extends LitElement {
.columns {
margin: 0px;
}
thead {
margin-bottom: 24px;
}
table th:not([align]) {
text-align: left;
}
.result {
border-bottom: 1px #dbdbdb solid;
min-height: fit-content;
font-size: var(--sl-font-size-small);
}
.result:first-child {
margin-top: var(--sl-spacing-small);
}
.result:not(:last-child) {
margin-bottom: 0;
border-bottom: 1px solid var(--sl-panel-border-color);
}
.results-head {
border-bottom: 2px #dbdbdb solid;
margin-right: 16px;
border-bottom: 1px solid var(--sl-panel-border-color);
min-height: fit-content;
display: block;
width: 100%;
padding-right: 15px; // For scrollbar gutter
}
.results-head a {
color: black;
.results-head .column {
background-color: var(--sl-panel-background-color);
position: relative;
z-index: 2;
}
table .column {
padding: var(--sl-spacing-x-small);
}
.all-results .column {
word-break: break-word;
}
.dl-button {
padding: 4px;
tbody .col-url a {
word-break: break-all;
}
.dl-button {
display: inline-block;
font-size: 1.125rem;
width: 1.5rem;
height: 1.5rem;
padding: var(--sl-spacing-3x-small);
}
div.sort-header {
padding: 10px;
margin-bottom: 0px !important;
@@ -330,6 +376,17 @@ class URLResources extends LitElement {
.flex-auto {
flex: auto;
}
.sort-control-label {
display: inline-flex;
align-items: center;
gap: var(--sl-spacing-2x-small);
font-size: var(--sl-font-size-small);
font-weight: var(--sl-font-weight-semibold);
color: var(--sl-color-neutral-600);
padding-bottom: 0;
}
.asc:after {
content: "▼";
font-size: 0.75em;
@@ -338,9 +395,12 @@ class URLResources extends LitElement {
content: "▲";
font-size: 0.75em;
}
.num-results {
margin-left: 1em;
font-style: italic;
font-size: var(--sl-font-size-small);
color: var(--sl-color-neutral-500);
line-height: 1;
margin-left: var(--sl-spacing-small);
}
`);
}
@@ -362,10 +422,10 @@ class URLResources extends LitElement {
>
Search and Filter
</div>
<div class="notification level is-marginless">
<div class="search-bar level is-marginless">
<div class="level-left flex-auto">
<div class="level-item flex-auto">
<span class="is-hidden-mobile">Search:&nbsp;&nbsp;</span>
<span class="is-hidden-mobile">Search:</span>
<div class="select">
<select @change="${this.onChangeTypeSearch}">
${URLResources.filters.map(
@@ -393,15 +453,15 @@ class URLResources extends LitElement {
.value="${this.query}"
placeholder="Enter URL to Search"
/>
<span class="icon is-left"
><fa-icon .svg="${fasSearch}"></fa-icon
<span class="icon is-left is-small"
><wr-icon .svg="${iconSearch}"></wr-icon
></span>
</div>
</div>
</div>
</div>
<div class="control level-right">
<div style="margin-left: 1em" class="control">
<div class="control">
<label class="radio has-text-left"
><input
type="radio"
@@ -435,7 +495,9 @@ class URLResources extends LitElement {
is-pulled-right
aria-live="polite"
aria-atomic="true"
>${this.filteredResults.length} Result(s)</span
>${this.filteredResults.length.toLocaleString()}
${this.filteredResults.length === 1 ? "result" : "results"}
shown</span
>
</div>
</div>
@@ -463,7 +525,7 @@ class URLResources extends LitElement {
</div>
<table class="all-results" aria-labelledby="results-heading num-results">
<thead>
<thead class="all-results-header">
<tr class="columns results-head has-text-weight-bold">
<th scope="col" class="column col-url is-6 is-hidden-mobile">
<a
@@ -472,7 +534,7 @@ class URLResources extends LitElement {
@click="${this.onSort}"
@keyup="${clickOnSpacebarPress}"
data-key="url"
class="${this.sortKey === "url"
class="sort-control-label ${this.sortKey === "url"
? this.sortDesc
? "desc"
: "asc"
@@ -487,7 +549,7 @@ class URLResources extends LitElement {
@click="${this.onSort}"
@keyup="${clickOnSpacebarPress}"
data-key="ts"
class="${this.sortKey === "ts"
class="sort-control-label ${this.sortKey === "ts"
? this.sortDesc
? "desc"
: "asc"
@@ -495,14 +557,14 @@ class URLResources extends LitElement {
>Date</a
>
</th>
<th scope="col" class="column col-mime is-3 is-hidden-mobile">
<th scope="col" class="column col-mime is-2 is-hidden-mobile">
<a
role="button"
href="#"
@click="${this.onSort}"
@keyup="${clickOnSpacebarPress}"
data-key="mime"
class="${this.sortKey === "mime"
class="sort-control-label ${this.sortKey === "mime"
? this.sortDesc
? "desc"
: "asc"
@@ -517,7 +579,7 @@ class URLResources extends LitElement {
@click="${this.onSort}"
@keyup="${clickOnSpacebarPress}"
data-key="status"
class="${this.sortKey === "status"
class="sort-control-label ${this.sortKey === "status"
? this.sortDesc
? "desc"
: "asc"
@@ -525,6 +587,9 @@ class URLResources extends LitElement {
>Status</a
>
</th>
<th scope="col" class="column col-actions is-1 is-hidden-mobile">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
@@ -535,21 +600,6 @@ class URLResources extends LitElement {
<tr class="columns result">
<td class="column col-url is-6">
<p class="minihead is-hidden-tablet">URL</p>
<a
class="dl-button"
href="${getDownloadLink(
this.collInfo!.replayPrefix,
result.url,
result.ts,
)}"
><fa-icon
size="1.0em"
class="has-text-black"
aria-hidden="true"
title="Download Resource"
.svg="${fasDownload}"
></fa-icon>
</a>
<a
@click="${this.onReplay}"
data-url="${result.url}"
@@ -569,7 +619,7 @@ class URLResources extends LitElement {
<p class="minihead is-hidden-tablet">Date</p>
${dateTimeFormatter.format(new Date(result.date))}
</td>
<td class="column col-mime is-3">
<td class="column col-mime is-2">
<p class="minihead is-hidden-tablet">Media Type</p>
${result.mime}
</td>
@@ -577,6 +627,21 @@ class URLResources extends LitElement {
<p class="minihead is-hidden-tablet">Status</p>
${result.status}
</td>
<td class="column col-actions is-1">
<a
class="dl-button"
href="${getDownloadLink(
this.collInfo!.replayPrefix,
result.url,
result.ts,
)}"
download
><wr-icon
title="Download Resource"
.svg="${iconDownload}"
></wr-icon>
</a>
</td>
</tr>
`,
)
+20 -4
View File
@@ -2,7 +2,11 @@
"compilerOptions": {
"outDir": "./dist/",
"module": "esnext",
"lib": ["esnext", "dom", "dom.iterable"],
"lib": [
"esnext",
"dom",
"dom.iterable"
],
"target": "es6",
"moduleResolution": "Bundler",
"allowJs": true,
@@ -29,9 +33,21 @@
],
"incremental": true,
"paths": {
"flexsearch": ["./node_modules/@types/flexsearch"],
"~assets/*": ["./src/assets/*"]
"flexsearch": [
"./node_modules/@types/flexsearch"
],
"~assets/*": [
"./src/assets/*"
],
"~icons/*": [
"./node_modules/@shoelace-style/shoelace/dist/assets/icons/*",
"./node_modules/@fortawesome/fontawesome-free/svgs/*",
],
}
},
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.d.ts"]
"include": [
"src/**/*.ts",
"src/**/*.js",
"src/**/*.d.ts"
]
}
+18 -28
View File
@@ -51,6 +51,22 @@ const tsConfig = {
},
};
/** @type {import("webpack").Configuration.module.rules} */
const commonModuleRules = [
{
test: /\.svg$/,
type: "asset/source",
},
{
test: /main.scss$/,
use: ["css-loader", "sass-loader"],
},
{
test: /wombat.js|wombatWorkers.js|index.html$/i,
type: "asset/source",
},
];
const electronMainConfig = (/*env, argv*/) => {
/** @type {import('webpack').Configuration} */
const config = {
@@ -165,20 +181,7 @@ const libConfig = (env, argv) => {
],
module: {
rules: [
{
test: /\.svg$/,
use: ["raw-loader"],
},
{
test: /main.scss$/,
use: ["css-loader", "sass-loader"],
},
{
test: /wombat.js|wombatWorkers.js|index.html$/i,
use: ["raw-loader"],
},
],
rules: [...commonModuleRules],
},
};
return merge(tsConfig, config);
@@ -258,20 +261,7 @@ const browserConfig = (/*env, argv*/) => {
],
module: {
rules: [
{
test: /\.svg$/,
use: ["raw-loader"],
},
{
test: /main.scss$/,
use: ["css-loader", "sass-loader"],
},
{
test: /wombat.js|wombatWorkers.js|index.html$/i,
use: ["raw-loader"],
},
],
rules: [...commonModuleRules],
},
};
return merge(tsConfig, config);
+9256 -6789
View File
File diff suppressed because it is too large Load Diff