Show snapshot timestamp in location bar (#228)
- Shows how many snapshots are available per page in URL bar - Allows users to navigate through snapshots through timestamp dropdown - Refactors coll.js and pageutils to typescript
This commit is contained in:
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||||
"@shoelace-style/shoelace": "^2.7.0",
|
"@shoelace-style/shoelace": "^2.8.0",
|
||||||
"@webrecorder/wabac": "^2.16.12",
|
"@webrecorder/wabac": "^2.16.12",
|
||||||
"bulma": "^0.9.3",
|
"bulma": "^0.9.3",
|
||||||
"electron-log": "^4.4.1",
|
"electron-log": "^4.4.1",
|
||||||
|
|||||||
+326
-139
@@ -1,4 +1,11 @@
|
|||||||
import { LitElement, html, css } from "lit";
|
import { LitElement, html, css } from "lit";
|
||||||
|
import { property, state } from "lit/decorators.js";
|
||||||
|
import { map } from "lit/directives/map.js";
|
||||||
|
import type {
|
||||||
|
SlDropdown,
|
||||||
|
SlMenu,
|
||||||
|
SlSelectEvent,
|
||||||
|
} from "@shoelace-style/shoelace";
|
||||||
import {
|
import {
|
||||||
wrapCss,
|
wrapCss,
|
||||||
rwpLogo,
|
rwpLogo,
|
||||||
@@ -9,7 +16,12 @@ import {
|
|||||||
replayPrefix,
|
replayPrefix,
|
||||||
} from "./misc";
|
} from "./misc";
|
||||||
|
|
||||||
import { sourceToId, tsToDate, getPageDateTS } from "./pageutils";
|
import {
|
||||||
|
sourceToId,
|
||||||
|
tsToDate,
|
||||||
|
getPageDateTS,
|
||||||
|
getDateFromTS,
|
||||||
|
} from "./pageutils";
|
||||||
|
|
||||||
import fasBook from "@fortawesome/fontawesome-free/svgs/solid/book.svg";
|
import fasBook from "@fortawesome/fontawesome-free/svgs/solid/book.svg";
|
||||||
|
|
||||||
@@ -31,112 +43,122 @@ import fasMenuV from "@fortawesome/fontawesome-free/svgs/solid/ellipsis-v.svg";
|
|||||||
|
|
||||||
import fasAngleLeft from "@fortawesome/fontawesome-free/svgs/solid/angle-left.svg";
|
import fasAngleLeft from "@fortawesome/fontawesome-free/svgs/solid/angle-left.svg";
|
||||||
import fasAngleRight from "@fortawesome/fontawesome-free/svgs/solid/angle-right.svg";
|
import fasAngleRight from "@fortawesome/fontawesome-free/svgs/solid/angle-right.svg";
|
||||||
|
import fasCaretDown from "@fortawesome/fontawesome-free/svgs/solid/caret-down.svg";
|
||||||
|
|
||||||
import { RWPEmbedReceipt } from "./embed-receipt.js";
|
import { RWPEmbedReceipt } from "./embed-receipt.js";
|
||||||
import Split from "split.js";
|
import Split from "split.js";
|
||||||
|
import type { Replay } from "./replay";
|
||||||
|
|
||||||
const RWP_SCHEME = "search://";
|
const RWP_SCHEME = "search://";
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
class Coll extends LitElement {
|
class Coll extends LitElement {
|
||||||
|
@property({ type: Boolean })
|
||||||
|
inited = false;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
sourceUrl: string | null = null;
|
||||||
|
|
||||||
|
@property({ type: Object, attribute: false })
|
||||||
|
loadInfo: any;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
showSidebar: boolean | null = null;
|
||||||
|
|
||||||
|
@property({ type: Object, attribute: false })
|
||||||
|
collInfo: any = null;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
coll = "";
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
hasStory = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
isLoading = false;
|
||||||
|
|
||||||
|
@property({ type: Object, attribute: false })
|
||||||
|
tabData: {
|
||||||
|
view?: string;
|
||||||
|
url?: string;
|
||||||
|
ts?: string;
|
||||||
|
multiTs?: string[];
|
||||||
|
currList?: number;
|
||||||
|
query?: string;
|
||||||
|
urlSearchType?: string;
|
||||||
|
currMime?: string;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
url = "";
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
ts = "";
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
isFullscreen: any;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
menuActive = false;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
embed: any;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
embedDropdownActive = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
editable = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
browsable = true;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
clearable = true;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
isVisible = true;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
favIconUrl = "";
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
appName = "ReplayWeb.page";
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
appVersion = VERSION;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
appLogo = rwpLogo;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
autoUpdateInterval = 10;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
swName: any;
|
||||||
|
|
||||||
|
private tabNames = ["pages", "story", "resources", "info"];
|
||||||
|
private tabLabels = {
|
||||||
|
pages: "Pages",
|
||||||
|
story: "Story",
|
||||||
|
resources: "URLs",
|
||||||
|
info: "Archive Info",
|
||||||
|
};
|
||||||
|
private splitter: any = null;
|
||||||
|
private _replaceLoc = false;
|
||||||
|
private _locUpdateNeeded = false;
|
||||||
|
private _locationHash = "";
|
||||||
|
private _autoUpdater: Promise<void> | null = null;
|
||||||
|
private observer?: IntersectionObserver;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.sourceUrl = null;
|
|
||||||
this.inited = false;
|
|
||||||
this.isLoading = false;
|
|
||||||
|
|
||||||
this.coll = "";
|
|
||||||
|
|
||||||
this.collInfo = null;
|
|
||||||
|
|
||||||
this._replaceLoc = false;
|
|
||||||
this._locUpdateNeeded = false;
|
|
||||||
|
|
||||||
this._locationHash = "";
|
|
||||||
|
|
||||||
this.tabData = {};
|
|
||||||
this.url = "";
|
|
||||||
this.ts = "";
|
|
||||||
|
|
||||||
this.tabNames = ["pages", "story", "resources", "info"];
|
|
||||||
this.tabLabels = {
|
|
||||||
pages: "Pages",
|
|
||||||
story: "Story",
|
|
||||||
resources: "URLs",
|
|
||||||
info: "Archive Info",
|
|
||||||
};
|
|
||||||
|
|
||||||
this.menuActive = false;
|
|
||||||
this.embedDropdownActive = false;
|
|
||||||
|
|
||||||
this.hasStory = false;
|
|
||||||
|
|
||||||
this.editable = false;
|
|
||||||
this.browsable = true;
|
|
||||||
this.clearable = true;
|
|
||||||
|
|
||||||
this.showSidebar = localStorage.getItem("pages:showSidebar") === "1";
|
this.showSidebar = localStorage.getItem("pages:showSidebar") === "1";
|
||||||
this.splitter = null;
|
|
||||||
|
|
||||||
this.isVisible = true;
|
|
||||||
|
|
||||||
this.favIconUrl = "";
|
|
||||||
|
|
||||||
this.autoUpdateInterval = 10;
|
|
||||||
this._autoUpdater = null;
|
|
||||||
|
|
||||||
this.appName = "ReplayWeb.page";
|
|
||||||
this.appVersion = VERSION;
|
|
||||||
this.appLogo = rwpLogo;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
inited: { type: Boolean },
|
|
||||||
|
|
||||||
sourceUrl: { type: String },
|
|
||||||
loadInfo: { type: Object, attribute: false },
|
|
||||||
|
|
||||||
showSidebar: { type: Boolean },
|
|
||||||
|
|
||||||
collInfo: { type: Object, attribute: false },
|
|
||||||
coll: { type: String },
|
|
||||||
|
|
||||||
hasStory: { type: Boolean },
|
|
||||||
isLoading: { type: Boolean },
|
|
||||||
|
|
||||||
tabData: { type: Object, attribute: false },
|
|
||||||
|
|
||||||
url: { type: String },
|
|
||||||
ts: { type: String },
|
|
||||||
|
|
||||||
isFullscreen: { type: Boolean },
|
|
||||||
menuActive: { type: Boolean },
|
|
||||||
|
|
||||||
embed: { type: String },
|
|
||||||
embedDropdownActive: { type: Boolean },
|
|
||||||
|
|
||||||
editable: { type: Boolean },
|
|
||||||
browsable: { type: Boolean },
|
|
||||||
clearable: { type: Boolean },
|
|
||||||
|
|
||||||
isVisible: { type: Boolean },
|
|
||||||
|
|
||||||
favIconUrl: { type: String },
|
|
||||||
|
|
||||||
appName: { type: String },
|
|
||||||
appVersion: { type: String },
|
|
||||||
appLogo: { type: String },
|
|
||||||
|
|
||||||
autoUpdateInterval: { type: Number },
|
|
||||||
|
|
||||||
swName: { type: String },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
this.inited = true;
|
this.inited = true;
|
||||||
window.addEventListener("hashchange", (event) => this.onHashChange(event));
|
window.addEventListener("hashchange", (event) => this.onHashChange());
|
||||||
|
|
||||||
this.addEventListener("fullscreenchange", () => {
|
this.addEventListener("fullscreenchange", () => {
|
||||||
this.isFullscreen = !!document.fullscreenElement;
|
this.isFullscreen = !!document.fullscreenElement;
|
||||||
@@ -171,6 +193,43 @@ class Coll extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMultiTimestamps(): Promise<void> {
|
||||||
|
if (!this.tabData.url) return;
|
||||||
|
const resp = await fetch(
|
||||||
|
apiPrefix +
|
||||||
|
"/c/" +
|
||||||
|
this.coll +
|
||||||
|
"/ts/?url=" +
|
||||||
|
window.encodeURIComponent(this.tabData.url),
|
||||||
|
);
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const json = await resp.json();
|
||||||
|
this.updateTabData({ multiTs: json.timestamps });
|
||||||
|
}
|
||||||
|
|
||||||
|
willUpdate(changedProperties: Map<string, any>) {
|
||||||
|
if (changedProperties.has("tabData") && this.tabData) {
|
||||||
|
// Format tab data from URL query params
|
||||||
|
const tabData = {};
|
||||||
|
Object.entries(this.tabData).forEach(([key, value]) => {
|
||||||
|
if (!value) return;
|
||||||
|
if (key === "multiTs" && typeof value === "string") {
|
||||||
|
tabData[key] = value.split(",");
|
||||||
|
} else {
|
||||||
|
tabData[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.tabData = tabData;
|
||||||
|
|
||||||
|
const prevTabData = changedProperties.get("tabData");
|
||||||
|
if (this.tabData.url && this.tabData.url !== prevTabData?.url) {
|
||||||
|
this.getMultiTimestamps();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updated(changedProperties) {
|
updated(changedProperties) {
|
||||||
// if (changedProperties.has("url") || changedProperties.has("ts")) {
|
// if (changedProperties.has("url") || changedProperties.has("ts")) {
|
||||||
// if (this.url.startsWith("rwp?")) {
|
// if (this.url.startsWith("rwp?")) {
|
||||||
@@ -192,13 +251,7 @@ class Coll extends LitElement {
|
|||||||
if (!this.collInfo || !this.collInfo.coll) {
|
if (!this.collInfo || !this.collInfo.coll) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const newHash = "#" + new URLSearchParams(this.tabData as any).toString();
|
||||||
// don't add empty params to shorten query
|
|
||||||
Object.keys(this.tabData).forEach(
|
|
||||||
(key) => !this.tabData[key] && delete this.tabData[key],
|
|
||||||
);
|
|
||||||
|
|
||||||
const newHash = "#" + new URLSearchParams(this.tabData).toString();
|
|
||||||
|
|
||||||
if (!this.tabData.url) {
|
if (!this.tabData.url) {
|
||||||
this.url =
|
this.url =
|
||||||
@@ -206,6 +259,7 @@ class Coll extends LitElement {
|
|||||||
}
|
}
|
||||||
if (newHash !== this._locationHash) {
|
if (newHash !== this._locationHash) {
|
||||||
this._locationHash = newHash;
|
this._locationHash = newHash;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this._replaceLoc ||
|
this._replaceLoc ||
|
||||||
Object.keys(changedProperties.get("tabData")).length === 0
|
Object.keys(changedProperties.get("tabData")).length === 0
|
||||||
@@ -217,7 +271,9 @@ class Coll extends LitElement {
|
|||||||
} else {
|
} else {
|
||||||
window.location.hash = this._locationHash;
|
window.location.hash = this._locationHash;
|
||||||
if (!this.showSidebar) {
|
if (!this.showSidebar) {
|
||||||
const replay = this.renderRoot.querySelector("wr-coll-replay");
|
const replay = this.renderRoot.querySelector(
|
||||||
|
"wr-coll-replay",
|
||||||
|
) as Replay;
|
||||||
if (replay) {
|
if (replay) {
|
||||||
replay.focus();
|
replay.focus();
|
||||||
}
|
}
|
||||||
@@ -246,7 +302,7 @@ class Coll extends LitElement {
|
|||||||
configureSplitter() {
|
configureSplitter() {
|
||||||
if (this.tabData.url && this.showSidebar) {
|
if (this.tabData.url && this.showSidebar) {
|
||||||
const contents = this.renderRoot.querySelector("#contents");
|
const contents = this.renderRoot.querySelector("#contents");
|
||||||
const replay = this.renderRoot.querySelector("wr-coll-replay");
|
const replay = this.renderRoot.querySelector("wr-coll-replay") as Replay;
|
||||||
|
|
||||||
if (contents && replay && !this.splitter) {
|
if (contents && replay && !this.splitter) {
|
||||||
const opts = {
|
const opts = {
|
||||||
@@ -265,7 +321,7 @@ class Coll extends LitElement {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.splitter = Split([contents, replay], opts);
|
this.splitter = Split([contents as any, replay], opts);
|
||||||
}
|
}
|
||||||
} else if (this.splitter) {
|
} else if (this.splitter) {
|
||||||
try {
|
try {
|
||||||
@@ -378,7 +434,10 @@ class Coll extends LitElement {
|
|||||||
this._locationHash = hash;
|
this._locationHash = hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.collInfo.coll && !this.tabNames.includes(this.tabData.view)) {
|
if (
|
||||||
|
this.collInfo.coll &&
|
||||||
|
(!this.tabData.view || !this.tabNames.includes(this.tabData.view))
|
||||||
|
) {
|
||||||
const view = this.hasStory
|
const view = this.hasStory
|
||||||
? "story"
|
? "story"
|
||||||
: this.editable || this.collInfo.pages.length
|
: this.editable || this.collInfo.pages.length
|
||||||
@@ -427,9 +486,15 @@ class Coll extends LitElement {
|
|||||||
event.target.id === this.tabData.view ||
|
event.target.id === this.tabData.view ||
|
||||||
(event.target.id === "replay" && this.tabData.url)
|
(event.target.id === "replay" && this.tabData.url)
|
||||||
) {
|
) {
|
||||||
this.updateTabData(event.detail.data, event.detail.replaceLoc, false);
|
this.updateTabData(
|
||||||
|
event.detail.data,
|
||||||
|
event.detail.replaceLoc /*, false */,
|
||||||
|
);
|
||||||
} else if (this.showSidebar && this.tabData.url) {
|
} else if (this.showSidebar && this.tabData.url) {
|
||||||
this.updateTabData(event.detail.data, event.detail.replaceLoc, true);
|
this.updateTabData(
|
||||||
|
event.detail.data,
|
||||||
|
event.detail.replaceLoc /*, true */,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,17 +640,69 @@ class Coll extends LitElement {
|
|||||||
|
|
||||||
#datetime {
|
#datetime {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 1em;
|
right: 0.5rem;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
background: #fff;
|
||||||
|
top: 1px;
|
||||||
|
bottom: 1px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient to indicate URL clipping */
|
||||||
|
#datetime:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 2em;
|
||||||
|
height: 100%;
|
||||||
|
transform: translateX(-100%);
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
rgba(255, 255, 255, 0),
|
rgba(255, 255, 255, 0),
|
||||||
#fff 15%,
|
#fff 50%,
|
||||||
#fff
|
#fff
|
||||||
);
|
);
|
||||||
margin: -35px 0 0 0px;
|
pointer-events: none;
|
||||||
padding-left: 3em;
|
}
|
||||||
line-height: 2;
|
|
||||||
|
.timestamp-dropdown-btn {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--sl-spacing-x-small);
|
||||||
|
align-items: center;
|
||||||
|
transition: background-color var(--sl-transition-fast);
|
||||||
|
color: var(--sl-color-neutral-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp-dropdown-btn:hover {
|
||||||
|
color: var(--sl-color-neutral-900);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp-dropdown-btn:hover .timestamp-count-badge {
|
||||||
|
background-color: var(--sl-color-blue-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp-count-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: var(--sl-spacing-2x-small);
|
||||||
|
background-color: var(--sl-color-blue-500);
|
||||||
|
color: var(--sl-color-neutral-0);
|
||||||
|
line-height: 1;
|
||||||
|
padding: var(--sl-spacing-3x-small) var(--sl-spacing-x-small);
|
||||||
|
border-radius: var(--sl-border-radius-small);
|
||||||
|
transition: background-color var(--sl-transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp-count {
|
||||||
|
font-weight: 600;
|
||||||
|
transform: translateY(0.075em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp-menu-item[aria-selected="true"]::part(label) {
|
||||||
|
color: var(--sl-color-blue-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-head {
|
.menu-head {
|
||||||
@@ -720,7 +837,7 @@ class Coll extends LitElement {
|
|||||||
|
|
||||||
if (!isReplay && this.tabData && this.tabData.view) {
|
if (!isReplay && this.tabData && this.tabData.view) {
|
||||||
const detail = {
|
const detail = {
|
||||||
title: this.tabLabels[this.tabData.view],
|
title: (this.tabLabels as any)[this.tabData.view],
|
||||||
replayTitle: false,
|
replayTitle: false,
|
||||||
};
|
};
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
@@ -738,7 +855,7 @@ class Coll extends LitElement {
|
|||||||
embed="${this.embed}"
|
embed="${this.embed}"
|
||||||
swName="${this.swName}"
|
swName="${this.swName}"
|
||||||
.coll="${this.coll}"
|
.coll="${this.coll}"
|
||||||
.sourceUrl="${this.sourceUrl}"
|
sourceUrl="${this.sourceUrl || ""}"
|
||||||
@coll-loaded=${this.onCollLoaded}
|
@coll-loaded=${this.onCollLoaded}
|
||||||
></wr-loader>`;
|
></wr-loader>`;
|
||||||
} else if (this.collInfo) {
|
} else if (this.collInfo) {
|
||||||
@@ -752,7 +869,7 @@ class Coll extends LitElement {
|
|||||||
: isReplay
|
: isReplay
|
||||||
? "is-hidden"
|
? "is-hidden"
|
||||||
: "full-pages"}"
|
: "full-pages"}"
|
||||||
role="${isSidebar ? "complementary" : ""}"
|
role="${(isSidebar ? "complementary" : "") as any}"
|
||||||
aria-label="${isSidebar ? "Browse Contents" : ""}"
|
aria-label="${isSidebar ? "Browse Contents" : ""}"
|
||||||
>
|
>
|
||||||
${this.renderTabHeader(isSidebar)}
|
${this.renderTabHeader(isSidebar)}
|
||||||
@@ -765,7 +882,7 @@ class Coll extends LitElement {
|
|||||||
role="main"
|
role="main"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
.collInfo="${this.collInfo}"
|
.collInfo="${this.collInfo}"
|
||||||
sourceUrl="${this.sourceUrl}"
|
sourceUrl="${this.sourceUrl || ""}"
|
||||||
url="${this.tabData.url || ""}"
|
url="${this.tabData.url || ""}"
|
||||||
ts="${this.tabData.ts || ""}"
|
ts="${this.tabData.ts || ""}"
|
||||||
@coll-tab-nav="${this.onCollTabNav}"
|
@coll-tab-nav="${this.onCollTabNav}"
|
||||||
@@ -822,9 +939,9 @@ class Coll extends LitElement {
|
|||||||
href="#story"
|
href="#story"
|
||||||
class="is-size-6"
|
class="is-size-6"
|
||||||
aria-label="Story"
|
aria-label="Story"
|
||||||
aria-current="${this.tabData.view === "story"
|
aria-current="${(this.tabData.view === "story"
|
||||||
? "location"
|
? "location"
|
||||||
: ""}"
|
: "") as any}"
|
||||||
>
|
>
|
||||||
<span class="icon"
|
<span class="icon"
|
||||||
><fa-icon
|
><fa-icon
|
||||||
@@ -848,7 +965,9 @@ class Coll extends LitElement {
|
|||||||
href="#pages"
|
href="#pages"
|
||||||
class="is-size-6"
|
class="is-size-6"
|
||||||
aria-label="Pages"
|
aria-label="Pages"
|
||||||
aria-current="${this.tabData.view === "pages" ? "location" : ""}"
|
aria-current="${(this.tabData.view === "pages"
|
||||||
|
? "location"
|
||||||
|
: "") as any}"
|
||||||
>
|
>
|
||||||
<span class="icon"
|
<span class="icon"
|
||||||
><fa-icon
|
><fa-icon
|
||||||
@@ -871,9 +990,9 @@ class Coll extends LitElement {
|
|||||||
href="#resources"
|
href="#resources"
|
||||||
class="is-size-6"
|
class="is-size-6"
|
||||||
aria-label="URLs"
|
aria-label="URLs"
|
||||||
aria-current="${this.tabData.view === "resources"
|
aria-current="${(this.tabData.view === "resources"
|
||||||
? "location"
|
? "location"
|
||||||
: ""}"
|
: "") as any}"
|
||||||
>
|
>
|
||||||
<span class="icon"
|
<span class="icon"
|
||||||
><fa-icon
|
><fa-icon
|
||||||
@@ -894,7 +1013,9 @@ class Coll extends LitElement {
|
|||||||
href="#info"
|
href="#info"
|
||||||
class="is-size-6"
|
class="is-size-6"
|
||||||
aria-label="Archive Info"
|
aria-label="Archive Info"
|
||||||
aria-current="${this.tabData.view === "info" ? "location" : ""}"
|
aria-current="${(this.tabData.view === "info"
|
||||||
|
? "location"
|
||||||
|
: "") as any}"
|
||||||
>
|
>
|
||||||
<span class="icon"
|
<span class="icon"
|
||||||
><fa-icon
|
><fa-icon
|
||||||
@@ -1057,7 +1178,7 @@ class Coll extends LitElement {
|
|||||||
</span>
|
</span>
|
||||||
</a>`
|
</a>`
|
||||||
: ""}
|
: ""}
|
||||||
${this.renderExtraToolbar(false)}
|
${this.renderExtraToolbar()}
|
||||||
<form @submit="${this.onSubmit}">
|
<form @submit="${this.onSubmit}">
|
||||||
<div
|
<div
|
||||||
class="control is-expanded ${showFavIcon ? "has-icons-left" : ""}"
|
class="control is-expanded ${showFavIcon ? "has-icons-left" : ""}"
|
||||||
@@ -1071,11 +1192,7 @@ class Coll extends LitElement {
|
|||||||
.value="${this.url}"
|
.value="${this.url}"
|
||||||
placeholder="Enter text to search or a URL to replay"
|
placeholder="Enter text to search or a URL to replay"
|
||||||
/>
|
/>
|
||||||
${isReplay
|
${isReplay ? this.renderTimestamp() : ""}
|
||||||
? html`<p id="datetime" class="control is-hidden-mobile">
|
|
||||||
${dateStr}
|
|
||||||
</p>`
|
|
||||||
: html``}
|
|
||||||
${showFavIcon
|
${showFavIcon
|
||||||
? html` <span class="favicon icon is-small is-left">
|
? html` <span class="favicon icon is-small is-left">
|
||||||
<img src="${this.favIconUrl}" />
|
<img src="${this.favIconUrl}" />
|
||||||
@@ -1200,7 +1317,7 @@ class Coll extends LitElement {
|
|||||||
<span>Browse Contents</span>
|
<span>Browse Contents</span>
|
||||||
</a>`
|
</a>`
|
||||||
: ""}
|
: ""}
|
||||||
${this.renderExtraToolbar(true)}
|
${this.renderExtraToolbar()}
|
||||||
${this.clearable
|
${this.clearable
|
||||||
? html` <hr class="dropdown-divider is-hidden-desktop" />
|
? html` <hr class="dropdown-divider is-hidden-desktop" />
|
||||||
<a
|
<a
|
||||||
@@ -1221,8 +1338,8 @@ class Coll extends LitElement {
|
|||||||
<span>Purge Cache + Full Reload</span>
|
<span>Purge Cache + Full Reload</span>
|
||||||
</a>`
|
</a>`
|
||||||
: html``}
|
: html``}
|
||||||
${(!this.editable && this.sourceUrl.startsWith("http://")) ||
|
${(!this.editable && this.sourceUrl?.startsWith("http://")) ||
|
||||||
this.sourceUrl.startsWith("https://")
|
this.sourceUrl?.startsWith("https://")
|
||||||
? html` <hr class="dropdown-divider" />
|
? html` <hr class="dropdown-divider" />
|
||||||
<a
|
<a
|
||||||
href="${this.sourceUrl}"
|
href="${this.sourceUrl}"
|
||||||
@@ -1271,6 +1388,54 @@ class Coll extends LitElement {
|
|||||||
<p id="skip-replay-target" tabindex="-1" class="is-sr-only">Skipped</p>`;
|
<p id="skip-replay-target" tabindex="-1" class="is-sr-only">Skipped</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderTimestamp() {
|
||||||
|
const timestampStrs: { date: string; label: string }[] = [];
|
||||||
|
this.tabData.multiTs?.forEach((ts) => {
|
||||||
|
// Filter out invalid dates
|
||||||
|
try {
|
||||||
|
const date = getDateFromTS(+ts);
|
||||||
|
const dateStr = tsToDate(date).toLocaleString();
|
||||||
|
timestampStrs.push({
|
||||||
|
date,
|
||||||
|
label: dateStr,
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
const currDateStr = tsToDate(this.ts).toLocaleString();
|
||||||
|
return html`<div id="datetime" class="control is-hidden-mobile">
|
||||||
|
${timestampStrs.length > 1
|
||||||
|
? html`
|
||||||
|
<sl-dropdown placement="top-end" hoist>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="timestamp-dropdown-btn"
|
||||||
|
slot="trigger"
|
||||||
|
@blur=${this.onTimestampDropdownBtnBlur}
|
||||||
|
>
|
||||||
|
<div>${currDateStr}</div>
|
||||||
|
<div class="timestamp-count-badge">
|
||||||
|
<div class="timestamp-count">${timestampStrs.length}</div>
|
||||||
|
<fa-icon .svg="${fasCaretDown}" aria-hidden="true"></fa-icon>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<sl-menu @sl-select=${this.onSelectTimestamp}>
|
||||||
|
${timestampStrs.map(({ date: ts, label }) => {
|
||||||
|
const selected = this.ts === ts;
|
||||||
|
return html`<sl-menu-item
|
||||||
|
class="timestamp-menu-item"
|
||||||
|
value=${ts}
|
||||||
|
aria-selected="${selected}"
|
||||||
|
>
|
||||||
|
${label}</sl-menu-item
|
||||||
|
>`;
|
||||||
|
})}
|
||||||
|
</sl-menu>
|
||||||
|
</sl-dropdown>
|
||||||
|
`
|
||||||
|
: currDateStr}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
renderVerifyInfo() {
|
renderVerifyInfo() {
|
||||||
if (this.embed !== "replay-with-info") {
|
if (this.embed !== "replay-with-info") {
|
||||||
return "";
|
return "";
|
||||||
@@ -1286,14 +1451,14 @@ class Coll extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dragStart() {
|
dragStart() {
|
||||||
const replay = this.renderRoot.querySelector("wr-coll-replay");
|
const replay = this.renderRoot.querySelector("wr-coll-replay") as Replay;
|
||||||
if (replay) {
|
if (replay) {
|
||||||
replay.setDisablePointer(true);
|
replay.setDisablePointer(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dragEnd() {
|
dragEnd() {
|
||||||
const replay = this.renderRoot.querySelector("wr-coll-replay");
|
const replay = this.renderRoot.querySelector("wr-coll-replay") as Replay;
|
||||||
if (replay) {
|
if (replay) {
|
||||||
replay.setDisablePointer(false);
|
replay.setDisablePointer(false);
|
||||||
}
|
}
|
||||||
@@ -1332,7 +1497,7 @@ class Coll extends LitElement {
|
|||||||
id="story"
|
id="story"
|
||||||
.isSidebar="${isSidebar}"
|
.isSidebar="${isSidebar}"
|
||||||
class="${isStory ? "" : "is-hidden"} ${isSidebar ? "sidebar" : ""}"
|
class="${isStory ? "" : "is-hidden"} ${isSidebar ? "sidebar" : ""}"
|
||||||
role="${isSidebar ? "" : "main"}"
|
role="${(isSidebar ? "" : "main") as any}"
|
||||||
>
|
>
|
||||||
</wr-coll-story>`
|
</wr-coll-story>`
|
||||||
: ""}
|
: ""}
|
||||||
@@ -1349,7 +1514,7 @@ class Coll extends LitElement {
|
|||||||
class="is-paddingless ${isResources ? "" : "is-hidden"} ${isSidebar
|
class="is-paddingless ${isResources ? "" : "is-hidden"} ${isSidebar
|
||||||
? "sidebar"
|
? "sidebar"
|
||||||
: ""}"
|
: ""}"
|
||||||
role="${isSidebar ? "" : "main"}"
|
role="${(isSidebar ? "" : "main") as any}"
|
||||||
>
|
>
|
||||||
</wr-coll-resources>`
|
</wr-coll-resources>`
|
||||||
: ""}
|
: ""}
|
||||||
@@ -1367,7 +1532,7 @@ class Coll extends LitElement {
|
|||||||
id="pages"
|
id="pages"
|
||||||
@coll-update="${this.onCollUpdate}"
|
@coll-update="${this.onCollUpdate}"
|
||||||
class="${isPages ? "" : "is-hidden"} ${isSidebar ? "sidebar" : ""}"
|
class="${isPages ? "" : "is-hidden"} ${isSidebar ? "sidebar" : ""}"
|
||||||
role="${isSidebar ? "" : "main"}"
|
role="${(isSidebar ? "" : "main") as any}"
|
||||||
>
|
>
|
||||||
</wr-page-view>`
|
</wr-page-view>`
|
||||||
: ""}
|
: ""}
|
||||||
@@ -1378,7 +1543,9 @@ class Coll extends LitElement {
|
|||||||
// This is a workaround, since this app's routing doesn't permit normal
|
// This is a workaround, since this app's routing doesn't permit normal
|
||||||
// following of in-page anchors.
|
// following of in-page anchors.
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.renderRoot.querySelector("#skip-replay-target").focus();
|
(
|
||||||
|
this.renderRoot.querySelector("#skip-replay-target") as HTMLElement
|
||||||
|
).focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyDown(event) {
|
onKeyDown(event) {
|
||||||
@@ -1496,7 +1663,7 @@ class Coll extends LitElement {
|
|||||||
|
|
||||||
onSubmit(event) {
|
onSubmit(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const input = this.renderRoot.querySelector("input");
|
const input = this.renderRoot.querySelector("input")!;
|
||||||
if (input.value) {
|
if (input.value) {
|
||||||
this.navigateTo(input.value);
|
this.navigateTo(input.value);
|
||||||
} else {
|
} else {
|
||||||
@@ -1511,6 +1678,24 @@ class Coll extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onTimestampDropdownBtnBlur(event: MouseEvent) {
|
||||||
|
const btn = event.currentTarget as HTMLButtonElement;
|
||||||
|
const dropdown = btn.closest("sl-dropdown") as SlDropdown;
|
||||||
|
const relatedTarget = event.relatedTarget as HTMLElement;
|
||||||
|
if (relatedTarget && dropdown.contains(relatedTarget)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dropdown.open) {
|
||||||
|
dropdown.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectTimestamp(event: SlSelectEvent) {
|
||||||
|
const menu = event.currentTarget as SlMenu;
|
||||||
|
const { item } = event.detail;
|
||||||
|
this.updateTabData({ ts: item.value });
|
||||||
|
}
|
||||||
|
|
||||||
navigateTo(value) {
|
navigateTo(value) {
|
||||||
let data;
|
let data;
|
||||||
|
|
||||||
@@ -1518,7 +1703,9 @@ class Coll extends LitElement {
|
|||||||
data = { url: value };
|
data = { url: value };
|
||||||
|
|
||||||
if (value === this.tabData.url) {
|
if (value === this.tabData.url) {
|
||||||
const replay = this.renderRoot.querySelector("wr-coll-replay");
|
const replay = this.renderRoot.querySelector(
|
||||||
|
"wr-coll-replay",
|
||||||
|
) as Replay;
|
||||||
if (replay) {
|
if (replay) {
|
||||||
replay.refresh();
|
replay.refresh();
|
||||||
}
|
}
|
||||||
@@ -1536,7 +1723,7 @@ class Coll extends LitElement {
|
|||||||
|
|
||||||
_stringToParams(value) {
|
_stringToParams(value) {
|
||||||
const q = new URLSearchParams(value.slice(RWP_SCHEME.length));
|
const q = new URLSearchParams(value.slice(RWP_SCHEME.length));
|
||||||
const data = {};
|
const data: any = {};
|
||||||
data.url = "";
|
data.url = "";
|
||||||
data.ts = "";
|
data.ts = "";
|
||||||
|
|
||||||
@@ -1581,7 +1768,7 @@ class Coll extends LitElement {
|
|||||||
this.menuActive = false;
|
this.menuActive = false;
|
||||||
|
|
||||||
if (this.tabData.url) {
|
if (this.tabData.url) {
|
||||||
const replay = this.renderRoot.querySelector("wr-coll-replay");
|
const replay = this.renderRoot.querySelector("wr-coll-replay") as Replay;
|
||||||
if (replay) {
|
if (replay) {
|
||||||
replay.refresh();
|
replay.refresh();
|
||||||
}
|
}
|
||||||
@@ -36,16 +36,26 @@ function tsToDate(ts) {
|
|||||||
return new Date(datestr);
|
return new Date(datestr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
function getDateFromTS(ts: number) {
|
||||||
|
let date: Date | null = null;
|
||||||
|
date = new Date(ts);
|
||||||
|
const timestamp =
|
||||||
|
date && date instanceof Date ? getTS(date.toISOString()) : "";
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
function getPageDateTS(page) {
|
function getPageDateTS(page) {
|
||||||
let date = null;
|
let date: Date | null = null;
|
||||||
try {
|
try {
|
||||||
date = new Date(page.ts || page.date);
|
date = new Date(page.ts || page.date);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// leave date unchanged in case of error
|
// leave date unchanged in case of error
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamp = date && !isNaN(date) ? getTS(date.toISOString()) : "";
|
const timestamp =
|
||||||
|
date && date instanceof Date ? getTS(date.toISOString()) : "";
|
||||||
return { date, timestamp };
|
return { date, timestamp };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +124,7 @@ export {
|
|||||||
tsToDate,
|
tsToDate,
|
||||||
getTS,
|
getTS,
|
||||||
getPageDateTS,
|
getPageDateTS,
|
||||||
|
getDateFromTS,
|
||||||
getReplayLink,
|
getReplayLink,
|
||||||
sourceToId,
|
sourceToId,
|
||||||
parseURLSchemeHostPath,
|
parseURLSchemeHostPath,
|
||||||
@@ -1 +0,0 @@
|
|||||||
// Cherry-picked Shoelace components:
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// Cherry-picked Shoelace components:
|
||||||
|
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";
|
||||||
@@ -153,10 +153,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
|
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
|
||||||
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
|
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
|
||||||
|
|
||||||
"@lit-labs/react@^1.1.1":
|
"@lit-labs/react@^2.0.1":
|
||||||
version "1.2.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@lit-labs/react/-/react-1.2.1.tgz#5b421502cdf68a3639dec431318eeed2285f1c0e"
|
resolved "https://registry.yarnpkg.com/@lit-labs/react/-/react-2.0.1.tgz#a670d4a6f3cb39393af787189e02da2803f61cd0"
|
||||||
integrity sha512-DiZdJYFU0tBbdQkfwwRSwYyI/mcWkg3sWesKRsHUd4G+NekTmmeq9fzsurvcKTNVa0comNljwtg4Hvi1ds3V+A==
|
integrity sha512-Nj+XB3HamqaWefN91lpFPJaqjJ78XzGkPWCedB4jyH22GBFEenpE9A/h8B/2dnIGXtNtd9D/RFpUdQ/dBtWFqA==
|
||||||
|
|
||||||
"@lit-labs/ssr-dom-shim@^1.0.0", "@lit-labs/ssr-dom-shim@^1.1.0":
|
"@lit-labs/ssr-dom-shim@^1.0.0", "@lit-labs/ssr-dom-shim@^1.1.0":
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
@@ -374,14 +374,14 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@shoelace-style/localize/-/localize-3.1.1.tgz#f5b96e35a9a8709aa46d1aaa2359069c0db71534"
|
resolved "https://registry.yarnpkg.com/@shoelace-style/localize/-/localize-3.1.1.tgz#f5b96e35a9a8709aa46d1aaa2359069c0db71534"
|
||||||
integrity sha512-NkM/hj3Js6yXCU9WxhsyxRUdyqUUUl/BSvIluUMptQteUWGOJaoyP1iMbOMqO544DYMzBfnoCw66ZHkGuTdKgA==
|
integrity sha512-NkM/hj3Js6yXCU9WxhsyxRUdyqUUUl/BSvIluUMptQteUWGOJaoyP1iMbOMqO544DYMzBfnoCw66ZHkGuTdKgA==
|
||||||
|
|
||||||
"@shoelace-style/shoelace@^2.7.0":
|
"@shoelace-style/shoelace@^2.8.0":
|
||||||
version "2.7.0"
|
version "2.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/@shoelace-style/shoelace/-/shoelace-2.7.0.tgz#3844488cc1d5de4dfa966949961cb6faf4c2b35a"
|
resolved "https://registry.yarnpkg.com/@shoelace-style/shoelace/-/shoelace-2.8.0.tgz#995377abcff7d055c0c07dab63d97460453546ad"
|
||||||
integrity sha512-w7FnHqBoYR6Ne42WZPW+bIJGIAGsUZgxF3WA9LLfqn3+Q4NOQ4r5jl/DEfsQmRUTmY0nUrs4WGO1CeTrhv6taA==
|
integrity sha512-WigVUaW+MptefOW4zUlZaq+zn0n2hP+I4/mztoeJii5u3ex1CGexfQGcdw2gx6d7P7LAa6/NW0TlgAELzJQnCA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ctrl/tinycolor" "^3.5.0"
|
"@ctrl/tinycolor" "^3.5.0"
|
||||||
"@floating-ui/dom" "^1.2.1"
|
"@floating-ui/dom" "^1.2.1"
|
||||||
"@lit-labs/react" "^1.1.1"
|
"@lit-labs/react" "^2.0.1"
|
||||||
"@shoelace-style/animations" "^1.1.0"
|
"@shoelace-style/animations" "^1.1.0"
|
||||||
"@shoelace-style/localize" "^3.1.1"
|
"@shoelace-style/localize" "^3.1.1"
|
||||||
composed-offset-position "^0.0.4"
|
composed-offset-position "^0.0.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user