1147 lines
34 KiB
JavaScript
1147 lines
34 KiB
JavaScript
import { LitElement, html, css } from "lit";
|
|
import { wrapCss, rwpLogo, IS_APP, VERSION, clickOnSpacebarPress, apiPrefix, replayPrefix } from "./misc";
|
|
|
|
import { sourceToId, tsToDate, getPageDateTS } from "./pageutils";
|
|
|
|
import fasBook from "@fortawesome/fontawesome-free/svgs/solid/book.svg";
|
|
|
|
import fasDownload from "@fortawesome/fontawesome-free/svgs/solid/download.svg";
|
|
|
|
import farListAlt from "@fortawesome/fontawesome-free/svgs/regular/list-alt.svg";
|
|
import farResources from "@fortawesome/fontawesome-free/svgs/solid/puzzle-piece.svg";
|
|
import farPages from "@fortawesome/fontawesome-free/svgs/regular/file-image.svg";
|
|
import fasInfoIcon from "@fortawesome/fontawesome-free/svgs/solid/info-circle.svg";
|
|
import fasSync from "@fortawesome/fontawesome-free/svgs/solid/sync-alt.svg";
|
|
|
|
import fasRefresh from "@fortawesome/fontawesome-free/svgs/solid/redo-alt.svg";
|
|
import fasFullscreen from "@fortawesome/fontawesome-free/svgs/solid/desktop.svg";
|
|
import fasUnfullscreen from "@fortawesome/fontawesome-free/svgs/solid/compress-arrows-alt.svg";
|
|
|
|
import fasLeft from "@fortawesome/fontawesome-free/svgs/solid/arrow-left.svg";
|
|
import fasRight from "@fortawesome/fontawesome-free/svgs/solid/arrow-right.svg";
|
|
import fasMenuV from "@fortawesome/fontawesome-free/svgs/solid/ellipsis-v.svg";
|
|
|
|
import fasAngleLeft from "@fortawesome/fontawesome-free/svgs/solid/angle-left.svg";
|
|
import fasAngleRight from "@fortawesome/fontawesome-free/svgs/solid/angle-right.svg";
|
|
|
|
import Split from "split.js";
|
|
|
|
|
|
const RWP_SCHEME = "search://";
|
|
|
|
|
|
// ===========================================================================
|
|
class Coll extends LitElement
|
|
{
|
|
constructor() {
|
|
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.hasStory = false;
|
|
|
|
this.editable = false;
|
|
|
|
this.showSidebar = localStorage.getItem("pages:showSidebar") === "1";
|
|
this.splitter = null;
|
|
|
|
this.isVisible = true;
|
|
|
|
this.favIconUrl = "";
|
|
|
|
this.appName = "ReplayWeb.page";
|
|
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 },
|
|
editable: { type: Boolean },
|
|
|
|
isVisible: { type: Boolean },
|
|
|
|
favIconUrl: {type: String },
|
|
|
|
appName: { type: String },
|
|
appLogo: { type: String },
|
|
};
|
|
}
|
|
|
|
firstUpdated() {
|
|
this.inited = true;
|
|
window.addEventListener("hashchange", (event) => this.onHashChange(event));
|
|
|
|
this.addEventListener("fullscreenchange", () => {
|
|
this.isFullscreen = !!document.fullscreenElement;
|
|
});
|
|
|
|
if (this.embed) {
|
|
this.observer = new IntersectionObserver((entries/*, observer*/) => {
|
|
this.isVisible = entries[0].isIntersecting;
|
|
});
|
|
|
|
this.observer.observe(this);
|
|
}
|
|
}
|
|
|
|
updated(changedProperties) {
|
|
// if (changedProperties.has("url") || changedProperties.has("ts")) {
|
|
// if (this.url.startsWith("rwp?")) {
|
|
// this.tabData = Object.fromEntries(new URLSearchParams(this.url.slice(4)).entries());
|
|
// } else {
|
|
// this.tabData = {view: "replay", url: this.url, ts: this.ts};
|
|
// }
|
|
// }
|
|
|
|
if (changedProperties.has("sourceUrl")) {
|
|
this.doUpdateInfo();
|
|
}
|
|
if (changedProperties.has("editable")) {
|
|
if (this.editable) {
|
|
this._pollColl = setInterval(() => this.doUpdateInfo(true), 10000);
|
|
} else if (this._pollColl) {
|
|
clearInterval(this._pollColl);
|
|
}
|
|
}
|
|
if (changedProperties.has("tabData")) {
|
|
if (!this.collInfo || !this.collInfo.coll) {
|
|
return;
|
|
}
|
|
|
|
// 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) {
|
|
this.url = RWP_SCHEME + decodeURIComponent(this._paramsToString(this.tabData));
|
|
}
|
|
if (newHash !== this._locationHash) {
|
|
this._locationHash = newHash;
|
|
if (this._replaceLoc || Object.keys(changedProperties.get("tabData")).length === 0) {
|
|
const newLoc = new URL(window.location.href);
|
|
newLoc.hash = this._locationHash;
|
|
window.history.replaceState({}, "", newLoc.href);
|
|
this._replaceLoc = false;
|
|
} else {
|
|
window.location.hash = this._locationHash;
|
|
if (!this.showSidebar) {
|
|
const replay = this.renderRoot.querySelector("wr-coll-replay");
|
|
if (replay) {
|
|
replay.focus();
|
|
}
|
|
}
|
|
}
|
|
if (this.embed && window.parent !== window) {
|
|
window.parent.postMessage(this.tabData, "*");
|
|
}
|
|
}
|
|
this._locUpdateNeeded = false;
|
|
}
|
|
if (changedProperties.has("showSidebar")) {
|
|
if (!this.embed) {
|
|
localStorage.setItem("pages:showSidebar", this.showSidebar ? "1" : "0");
|
|
}
|
|
}
|
|
|
|
if (changedProperties.has("tabData") || changedProperties.has("showSidebar")) {
|
|
this.configureSplitter();
|
|
}
|
|
}
|
|
|
|
configureSplitter() {
|
|
if (this.tabData.url && this.showSidebar) {
|
|
const contents = this.renderRoot.querySelector("#contents");
|
|
const replay = this.renderRoot.querySelector("wr-coll-replay");
|
|
|
|
if (contents && replay && !this.splitter) {
|
|
const opts = {
|
|
sizes: [30, 70],
|
|
|
|
minSize: [300, 300],
|
|
|
|
gutterSize: 4,
|
|
|
|
onDragStart() {
|
|
replay.setDisablePointer(true);
|
|
},
|
|
|
|
onDragEnd() {
|
|
replay.setDisablePointer(false);
|
|
}
|
|
};
|
|
|
|
this.splitter = Split([contents, replay], opts);
|
|
}
|
|
} else if (this.splitter) {
|
|
try {
|
|
this.splitter.destroy();
|
|
} catch (e) {
|
|
// ignore, remove splitter
|
|
}
|
|
this.splitter = null;
|
|
}
|
|
}
|
|
|
|
async doUpdateInfo(autorefresh = false)
|
|
{
|
|
// if auto-refresh, and replay and no sidebar, than skip update
|
|
if (autorefresh && this.tabData.url && !this.showSidebar) {
|
|
return;
|
|
}
|
|
|
|
let coll = this.loadInfo && this.loadInfo.customColl;
|
|
|
|
if (!coll) {
|
|
const res = await sourceToId(this.sourceUrl);
|
|
coll = res.coll;
|
|
}
|
|
|
|
this.coll = coll;
|
|
|
|
const collApiPrefix = apiPrefix + "/c/" + coll;
|
|
const collReplayPrefix = replayPrefix + "/" + coll;
|
|
|
|
const resp = await fetch(collApiPrefix + "?all=1");
|
|
|
|
if (resp.status != 200) {
|
|
this.collInfo = {};
|
|
return;
|
|
}
|
|
|
|
const json = await resp.json();
|
|
|
|
this.collInfo = {
|
|
apiPrefix: collApiPrefix,
|
|
replayPrefix: collReplayPrefix,
|
|
coll,
|
|
...json
|
|
};
|
|
|
|
if (!this.collInfo.title) {
|
|
this.collInfo.title = this.collInfo.filename;
|
|
}
|
|
|
|
if (this.embed === "replayonly") {
|
|
this.showSidebar = false;
|
|
}
|
|
|
|
this.hasStory = this.collInfo.desc || this.collInfo.lists.length;
|
|
|
|
this.dispatchEvent(new CustomEvent("coll-loaded", {detail: {
|
|
collInfo: this.collInfo,
|
|
alreadyLoaded: true
|
|
}}));
|
|
|
|
this.onHashChange();
|
|
}
|
|
|
|
onCollLoaded(event) {
|
|
this.doUpdateInfo();
|
|
this.loadInfo = null;
|
|
if (event.detail.sourceUrl) {
|
|
this.sourceUrl = event.detail.sourceUrl;
|
|
}
|
|
this.dispatchEvent(new CustomEvent("coll-loaded", {detail: {
|
|
sourceUrl: this.sourceUrl,
|
|
collInfo: this.collInfo,
|
|
}}));
|
|
}
|
|
|
|
onCollUpdate(event) {
|
|
if (!this.editable) {
|
|
return;
|
|
}
|
|
|
|
this.collInfo = {...this.collInfo, ...event.detail};
|
|
}
|
|
|
|
onHashChange() {
|
|
const hash = window.location.hash;
|
|
if (hash && hash !== this._locationHash) {
|
|
this.tabData = Object.fromEntries(new URLSearchParams(hash.slice(1)).entries());
|
|
this._locationHash = hash;
|
|
}
|
|
|
|
if (this.collInfo.coll && !this.tabNames.includes(this.tabData.view)) {
|
|
const view = this.hasStory ? "story" : (this.editable || this.collInfo.pages.length ? "pages" : "resources");
|
|
|
|
this.tabData = {
|
|
...this.tabData, view
|
|
};
|
|
}
|
|
|
|
if (this.tabData.url && this.tabData.url.startsWith("page:")) {
|
|
const pageIndex = Number(this.tabData.url.slice("page:".length));
|
|
if (!isNaN(pageIndex) && pageIndex < this.collInfo.pages.length) {
|
|
const page = this.collInfo.pages[pageIndex];
|
|
this.tabData.url = page.url;
|
|
this.tabData.ts = getPageDateTS(page).timestamp;
|
|
}
|
|
}
|
|
|
|
if (!this.hasStory && this.tabData.view === "story") {
|
|
this.tabData.view = "pages";
|
|
}
|
|
|
|
if (this.tabData.url && this.tabData.query) {
|
|
this.showSidebar = true;
|
|
}
|
|
}
|
|
|
|
onTabClick(event) {
|
|
event.preventDefault();
|
|
const hash = event.currentTarget.getAttribute("href");
|
|
this.tabData = {...this.tabData, view: hash.slice(1)};
|
|
//this.tabData = {view: hash.slice(1)};
|
|
return false;
|
|
}
|
|
|
|
onCollTabNav(event) {
|
|
if (event.detail.reload) {
|
|
this.onRefresh(null, true);
|
|
return;
|
|
}
|
|
|
|
if (event.target.id === this.tabData.view || event.target.id === "replay" && this.tabData.url) {
|
|
this.updateTabData(event.detail.data, event.detail.replaceLoc, false);
|
|
} else if (this.showSidebar && this.tabData.url) {
|
|
this.updateTabData(event.detail.data, event.detail.replaceLoc, true);
|
|
}
|
|
}
|
|
|
|
updateTabData(data, replaceLoc = false/*, merge = false*/) {
|
|
this.tabData = {...this.tabData, ...data};
|
|
if (this.tabData.url) {
|
|
this.url = this.tabData.url || "";
|
|
}
|
|
if (this.tabData.ts) {
|
|
this.ts = this.tabData.ts || "";
|
|
}
|
|
this._replaceLoc = !this._locUpdateNeeded && replaceLoc;
|
|
this._locUpdateNeeded = true;
|
|
}
|
|
|
|
static get styles() {
|
|
return wrapCss(css`
|
|
:host {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
min-width: 0px;
|
|
}
|
|
|
|
.icon {
|
|
vertical-align: text-top;
|
|
}
|
|
|
|
.back fa-icon {
|
|
width: 1.5em;
|
|
vertical-align: bottom;
|
|
line-height: 0.5em;
|
|
}
|
|
|
|
li.is-active {
|
|
font-weight: bold;
|
|
}
|
|
|
|
.tab-label {
|
|
display: inline;
|
|
}
|
|
|
|
@media screen and (max-width: ${!IS_APP ? css`1053px` : css`1163px`}) {
|
|
.tab-label {
|
|
display: none;
|
|
}
|
|
|
|
.main.tabs span.icon {
|
|
margin: 0px;
|
|
}
|
|
}
|
|
|
|
.main.tabs {
|
|
display: flex;
|
|
flex-direction: row;
|
|
margin-bottom: 0px;
|
|
}
|
|
|
|
.main.tabs ul {
|
|
position: relative;
|
|
}
|
|
|
|
.main.tabs li {
|
|
line-height: 1.5;
|
|
padding: 8px 0 6px 0;
|
|
}
|
|
|
|
@media screen and (max-width: 319px) {
|
|
.main.tabs li a {
|
|
padding-right: 4px;
|
|
padding-left: 4px;
|
|
}
|
|
}
|
|
|
|
.sidebar.main.tabs li a {
|
|
padding-right: 6px;
|
|
padding-left: 6px;
|
|
}
|
|
|
|
#contents {
|
|
height: 100%;
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0px;
|
|
flex: auto;
|
|
}
|
|
|
|
#tabContents {
|
|
height: 100%;
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: row;
|
|
min-height: 0px;
|
|
flex: auto;
|
|
}
|
|
|
|
${Coll.replayBarStyles}
|
|
`);
|
|
}
|
|
|
|
static get replayBarStyles() {
|
|
return css`
|
|
.breadbar {
|
|
display: flex;
|
|
align-items: center;
|
|
height: 35px;
|
|
width: 100%;
|
|
background-color: aliceblue;
|
|
padding: 0.5em;
|
|
}
|
|
|
|
.replay-bar {
|
|
padding: 0.5em 0em 0.5em 0.5em;
|
|
max-width: none;
|
|
border-bottom: solid .1rem #97989A;
|
|
width: 100%;
|
|
background-color: white;
|
|
}
|
|
|
|
input#url {
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.favicon img {
|
|
width: 20px;
|
|
height: 20px;
|
|
margin: 8px;
|
|
/*filter: drop-shadow(1px 1px 2px grey);*/
|
|
}
|
|
|
|
#datetime {
|
|
position: absolute;
|
|
right: 1em;
|
|
z-index: 10;
|
|
background: linear-gradient(90deg, rgba(255, 255, 255, 0), #FFF 15%, #FFF);
|
|
margin: -35px 0 0 0px;
|
|
padding-left: 3em;
|
|
line-height: 2;
|
|
}
|
|
|
|
.menu-head {
|
|
font-size: 10px;
|
|
font-weight: bold;
|
|
display: block;
|
|
}
|
|
.menu-logo {
|
|
vertical-align: middle;
|
|
}
|
|
.menu-version {
|
|
font-size: 10px;
|
|
}
|
|
.dropdown-item.info {
|
|
font-style: italic;
|
|
}
|
|
|
|
input:focus + #datetime {
|
|
display: none;
|
|
}
|
|
|
|
.grey-disabled {
|
|
--fa-icon-fill-color: lightgrey;
|
|
color: lightgrey;
|
|
}
|
|
|
|
.replay-bar .button:focus {
|
|
box-shadow: none;
|
|
}
|
|
|
|
.is-borderless {
|
|
border: 0px;
|
|
}
|
|
|
|
.narrow {
|
|
padding: calc(0.5em - 1px) 0.8em;
|
|
}
|
|
|
|
form {
|
|
width: 100%;
|
|
margin: 0 0 0 0.5em;
|
|
}
|
|
|
|
.gutter.gutter-horizontal {
|
|
cursor: col-resize;
|
|
float: left;
|
|
background-color: rgb(151, 152, 154);
|
|
}
|
|
|
|
.gutter.gutter-horizontal:hover {
|
|
cursor: col-resize;
|
|
}
|
|
|
|
main, wr-coll-replay {
|
|
width: 100%;
|
|
}
|
|
|
|
.info-bg {
|
|
background-color: whitesmoke;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
}
|
|
|
|
.is-list {
|
|
margin: 1.0em;
|
|
background-color: whitesmoke;
|
|
}
|
|
|
|
#contents.full-pages {
|
|
width: 100% !important;
|
|
}
|
|
|
|
.sidebar-nav {
|
|
position: absolute;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.sidebar-nav a {
|
|
display: inline-block;
|
|
border-bottom: 0px;
|
|
}
|
|
|
|
.sidebar-nav span.nav-hover {
|
|
font-size: smaller;
|
|
display: none;
|
|
}
|
|
|
|
.sidebar-nav:hover span.nav-hover,
|
|
.sidebar-nav:focus-within span.nav-hover {
|
|
display: initial;
|
|
color: rgb(72, 118, 255);
|
|
}
|
|
|
|
.sidebar-nav fa-icon {
|
|
vertical-align: bottom;
|
|
}
|
|
|
|
.sidebar-nav:hover fa-icon {
|
|
color: rgb(72, 118, 255);
|
|
}
|
|
|
|
.sidebar-nav.left {
|
|
left: 8px;
|
|
}
|
|
|
|
.sidebar-nav.right {
|
|
right: 8px;
|
|
}
|
|
|
|
/* Since the replay sometimes programmatically receives keyboard focus,
|
|
and that is visually unexpected for mouse-users, and since this won't
|
|
particularly trip up keyboard users, just remove the focus style. */
|
|
wr-coll-replay:focus {
|
|
outline: none;
|
|
}
|
|
/* Some keyboard-users may see this replacement style */
|
|
wr-coll-replay:focus-visible {
|
|
outline: 1px solid rgb(72, 118, 255);
|
|
}
|
|
`;
|
|
}
|
|
|
|
render() {
|
|
if (!this.inited) return html``;
|
|
|
|
const isReplay = !!this.tabData.url;
|
|
const isSidebar = isReplay && this.showSidebar;
|
|
|
|
if (!isReplay && this.tabData && this.tabData.view){
|
|
const detail = {title: this.tabLabels[this.tabData.view], replayTitle: false};
|
|
this.dispatchEvent(new CustomEvent("update-title", {bubbles: true, composed: true, detail}));
|
|
}
|
|
|
|
if (this.collInfo && !this.collInfo.coll) {
|
|
return html`
|
|
<wr-loader .loadInfo="${this.loadInfo}" embed="${this.embed}"
|
|
.coll="${this.coll}" .sourceUrl="${this.sourceUrl}" @coll-loaded=${this.onCollLoaded}></wr-loader>`;
|
|
} else if (this.collInfo) {
|
|
return html`
|
|
${this.renderLocationBar()}
|
|
<div id="tabContents">
|
|
<div id="contents" class="is-light ${isSidebar ? "sidebar" : (isReplay ? "is-hidden" : "full-pages")}"
|
|
role="${isSidebar ? "complementary" : ""}" aria-label="${isSidebar ? "Browse Contents" : ""}">
|
|
${this.renderTabHeader(isSidebar)}
|
|
${isSidebar || !isReplay ? this.renderCollTabs(isSidebar) : html``}
|
|
</div>
|
|
|
|
${isReplay && this.isVisible ? html`
|
|
<wr-coll-replay
|
|
role="main"
|
|
tabindex="-1"
|
|
.collInfo="${this.collInfo}"
|
|
sourceUrl="${this.sourceUrl}"
|
|
url="${this.tabData.url || ""}"
|
|
ts="${this.tabData.ts || ""}"
|
|
@coll-tab-nav="${this.onCollTabNav}" id="replay"
|
|
@replay-loading="${(e) => this.isLoading = e.detail.loading}"
|
|
@replay-favicons="${this.onFavIcons}"
|
|
>
|
|
</wr-coll-replay>
|
|
` : ""}
|
|
</div>
|
|
`;
|
|
} else {
|
|
return html``;
|
|
}
|
|
}
|
|
|
|
renderTabHeader(isSidebar) {
|
|
// if (this.tabData.view === "replay") {
|
|
// return "";
|
|
// }
|
|
|
|
return html`
|
|
<nav class="main tabs is-centered ${isSidebar ? "sidebar" : ""}" aria-label="tabs">
|
|
<ul>
|
|
${isSidebar ? html`
|
|
<li class="sidebar-nav left">
|
|
<a role="button" href="#" @click="${this.onHideSidebar}" @keyup="${clickOnSpacebarPress}" class="is-marginless is-size-6 is-paddingless">
|
|
<fa-icon title="Hide" .svg="${fasAngleLeft}" aria-hidden="true"></fa-icon>
|
|
<span class="nav-hover" aria-hidden="true">Hide</span>
|
|
<span class="is-sr-only">Hide Sidebar</span>
|
|
</a>
|
|
</li>` : ""}
|
|
|
|
${this.hasStory ? html`
|
|
<li class="${this.tabData.view === "story" ? "is-active" : ""}">
|
|
<a @click="${this.onTabClick}" href="#story" class="is-size-6" aria-label="Story" aria-current="${this.tabData.view === "story" ? "location" : ""}">
|
|
<span class="icon"><fa-icon .svg="${fasBook}" aria-hidden="true" title="Story"></fa-icon></span>
|
|
<span class="tab-label ${isSidebar ? "is-hidden" : ""}" title="Story">Story</span>
|
|
</a>
|
|
</li>` : ""}
|
|
|
|
<li class="${this.tabData.view === "pages" ? "is-active" : ""}">
|
|
<a @click="${this.onTabClick}" href="#pages" class="is-size-6" aria-label="Pages" aria-current="${this.tabData.view === "pages" ? "location" : ""}">
|
|
<span class="icon"><fa-icon .svg="${farPages}" aria-hidden="true" title="Pages"></fa-icon></span>
|
|
<span class="tab-label ${isSidebar ? "is-hidden" : ""}" title="Pages">Pages</span>
|
|
</a>
|
|
</li>
|
|
|
|
<li class="${this.tabData.view === "resources" ? "is-active" : ""}">
|
|
<a @click="${this.onTabClick}" href="#resources" class="is-size-6" aria-label="URLs" aria-current="${this.tabData.view === "resources" ? "location" : ""}">
|
|
<span class="icon"><fa-icon .svg="${farResources}" aria-hidden="true" title="URLs"></fa-icon></span>
|
|
<span class="tab-label ${isSidebar ? "is-hidden" : ""}" title="URLs">URLs</span>
|
|
</a>
|
|
</li>
|
|
|
|
<li class="${this.tabData.view === "info" ? "is-active" : ""}">
|
|
<a @click="${this.onTabClick}" href="#info" class="is-size-6" aria-label="Archive Info" aria-current="${this.tabData.view === "info" ? "location" : ""}">
|
|
<span class="icon"><fa-icon .svg="${fasInfoIcon}" aria-hidden="true" title="Archive Info"></fa-icon></span>
|
|
<span class="tab-label ${isSidebar ? "is-hidden" : ""}" title="Archive Info">Info</span>
|
|
</a>
|
|
</li>
|
|
|
|
${isSidebar ? html`
|
|
<li class="sidebar-nav right">
|
|
<a role="button" href="#" @click="${this.onFullPageView}" @keyup="${clickOnSpacebarPress}" class="is-marginless is-size-6 is-paddingless">
|
|
<span class="nav-hover" aria-hidden="true">Expand</span>
|
|
<span class="is-sr-only">Expand Sidebar to Full View</span>
|
|
<fa-icon title="Expand" .svg="${fasAngleRight}" aria-hidden="true"></fa-icon>
|
|
</a>
|
|
</li>` : ""}
|
|
</ul>
|
|
</nav>`;
|
|
}
|
|
|
|
renderLocationBar() {
|
|
if (this.embed === "replayonly") {
|
|
return "";
|
|
}
|
|
|
|
const dateStr = tsToDate(this.ts).toLocaleString();
|
|
|
|
const isReplay = !!this.tabData.url;
|
|
|
|
const showFavIcon = isReplay && this.favIconUrl;
|
|
|
|
return html`
|
|
<a class="skip-link" href="#skip-replay-target" @click="${this.skipMenu}">Skip replay navigation</a>
|
|
<nav class="replay-bar" aria-label="replay">
|
|
<div class="field has-addons">
|
|
<a href="#" role="button" class="button narrow is-borderless is-hidden-touch" id="fullscreen" @click="${this.onFullscreenToggle}" @keyup="${clickOnSpacebarPress}"
|
|
title="${this.isFullscreen ? "Exit Full Screen" : "Full Screen"}" aria-label="${this.isFullscreen ? "Exit Fullscreen" : "Fullscreen"}">
|
|
<span class="icon is-small">
|
|
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${this.isFullscreen ? fasUnfullscreen : fasFullscreen}"></fa-icon>
|
|
</span>
|
|
</a>
|
|
<a href="#" role="button" class="button narrow is-borderless is-hidden-mobile" @click="${this.onGoBack}" @keyup="${clickOnSpacebarPress}"
|
|
title="Back" aria-label="Back">
|
|
<span class="icon is-small">
|
|
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasLeft}"></fa-icon>
|
|
</span>
|
|
</a>
|
|
<a href="#" role="button" class="button narrow is-borderless is-hidden-mobile" @click="${this.onGoForward}" @keyup="${clickOnSpacebarPress}"
|
|
title="Forward" aria-label="Forward">
|
|
<span class="icon is-small">
|
|
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasRight}"></fa-icon>
|
|
</span>
|
|
</a>
|
|
<a href="#" role="button" class="button narrow is-borderless ${this.isLoading ? "is-loading" : "is-hidden-mobile"}" id="refresh" @click="${this.onRefresh}" @keyup="${clickOnSpacebarPress}"
|
|
title="Reload" aria-label="Reload">
|
|
<span class="icon is-small">
|
|
${!this.isLoading ? html`
|
|
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasRefresh}"></fa-icon>
|
|
` : ""}
|
|
</span>
|
|
</a>
|
|
<a href="#" role="button" class="button narrow is-borderless is-hidden-mobile ${!isReplay ? "grey-disabled" : ""}" @click="${this.onShowPages}" @keyup="${clickOnSpacebarPress}"
|
|
?disabled="${!isReplay}" title="Browse Contents" aria-label="Browse Contents" aria-controls="contents">
|
|
<span class="icon is-small">
|
|
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${farListAlt}"></fa-icon>
|
|
</span>
|
|
</a>
|
|
${this.renderExtraToolbar(false)}
|
|
<form @submit="${this.onSubmit}">
|
|
<div class="control is-expanded ${showFavIcon ? "has-icons-left" : ""}">
|
|
<input id="url" class="input" type="text" @keydown="${this.onKeyDown}" @blur="${this.onLostFocus}" .value="${this.url}" placeholder="Enter text to search or a URL to replay"/>
|
|
${isReplay ? html`<p id="datetime" class="control is-hidden-mobile">${dateStr}</p>` : html``}
|
|
${showFavIcon ? html`
|
|
<span class="favicon icon is-small is-left">
|
|
<img src="${this.favIconUrl}"/>
|
|
</span>` : html``}
|
|
</div>
|
|
</form>
|
|
|
|
<div class="dropdown is-right ${this.menuActive ? "is-active" : ""}" @click="${() => this.menuActive = false}">
|
|
<div class="dropdown-trigger">
|
|
<button class="button is-borderless" aria-haspopup="true" aria-controls="menu-dropdown" aria-expanded="${this.menuActive}" @click="${this.onMenu}"
|
|
aria-label="more replay controls">
|
|
<span class="icon is-small">
|
|
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasMenuV}"></fa-icon>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
<div class="dropdown-menu" id="menu-dropdown">
|
|
<div class="dropdown-content">
|
|
<a href="#" role="button" class="dropdown-item is-hidden-desktop" @click="${this.onFullscreenToggle}" @keyup="${clickOnSpacebarPress}">
|
|
<span class="icon is-small">
|
|
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${this.isFullscreen ? fasUnfullscreen : fasFullscreen}"></fa-icon>
|
|
</span>
|
|
<span>Full Screen</span>
|
|
</a>
|
|
<a href="#" role="button" class="dropdown-item is-hidden-tablet" @click="${this.onGoBack}" @keyup="${clickOnSpacebarPress}">
|
|
<span class="icon is-small">
|
|
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasLeft}"></fa-icon>
|
|
</span>
|
|
<span>Back</span>
|
|
</a>
|
|
<a href="#" role="button" class="dropdown-item is-hidden-tablet" @click="${this.onGoForward}" @keyup="${clickOnSpacebarPress}">
|
|
<span class="icon is-small">
|
|
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasRight}"></fa-icon>
|
|
</span>
|
|
<span>Forward</span>
|
|
</a>
|
|
<a href="#" role="button" class="dropdown-item is-hidden-tablet" @click="${this.onRefresh}" @keyup="${clickOnSpacebarPress}">
|
|
<span class="icon is-small">
|
|
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasRefresh}"></fa-icon>
|
|
</span>
|
|
<span>Reload</span>
|
|
</a>
|
|
<a href="#" role="button" class="dropdown-item is-hidden-tablet ${!isReplay ? "grey-disabled" : ""}" @click="${this.onShowPages}" @keyup="${clickOnSpacebarPress}">
|
|
<span class="icon is-small">
|
|
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${farListAlt}"></fa-icon>
|
|
</span>
|
|
<span>Browse Contents</span>
|
|
</a>
|
|
${this.renderExtraToolbar(true)}
|
|
${!this.editable ? html`
|
|
<hr class="dropdown-divider is-hidden-desktop">
|
|
<a href="#" role="button" class="dropdown-item" @click="${this.onPurgeCache}" @keyup="${clickOnSpacebarPress}">
|
|
<span class="icon is-small">
|
|
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasSync}"></fa-icon>
|
|
</span>
|
|
<span>Purge Cache + Full Reload</span>
|
|
</a>` : html``}
|
|
${!this.editable && this.sourceUrl.startsWith("http://") || this.sourceUrl.startsWith("https://") ? html`
|
|
<hr class="dropdown-divider">
|
|
<a href="${this.sourceUrl}" role="button" class="dropdown-item" @keyup="${clickOnSpacebarPress}">
|
|
<span class="icon is-small">
|
|
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasDownload}"></fa-icon>
|
|
</span>
|
|
<span>Download Archive</span>
|
|
</a>` : html``}
|
|
${dateStr ? html`
|
|
<hr class="dropdown-divider is-hidden-desktop">
|
|
<div class="dropdown-item info is-hidden-desktop">
|
|
<span class="menu-head">Capture Date</span>${dateStr}
|
|
</div>` : ""}
|
|
<hr class="dropdown-divider">
|
|
<a href="#" role="button" class="dropdown-item" @click="${this.onAbout}">
|
|
<fa-icon class="menu-logo" size="1.0rem" aria-hidden="true" .svg=${this.appLogo}></fa-icon>
|
|
<span> About ${this.appName}</span>
|
|
<span class="menu-version">(${VERSION})</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav><p id="skip-replay-target" tabindex="-1" class="is-sr-only">Skipped</p>`;
|
|
}
|
|
|
|
dragStart() {
|
|
const replay = this.renderRoot.querySelector("wr-coll-replay");
|
|
if (replay) {
|
|
replay.setDisablePointer(true);
|
|
}
|
|
}
|
|
|
|
dragEnd() {
|
|
const replay = this.renderRoot.querySelector("wr-coll-replay");
|
|
if (replay) {
|
|
replay.setDisablePointer(false);
|
|
}
|
|
}
|
|
|
|
renderCollInfo() {
|
|
return html`
|
|
<div class="info-bg">
|
|
<wr-coll-info
|
|
class="is-list"
|
|
.coll="${this.collInfo}"
|
|
?detailed="${true}"
|
|
?canDelete="${!this.embed}"
|
|
@coll-purge="${this.onPurgeCache}"
|
|
></wr-coll-info>
|
|
</div>`;
|
|
}
|
|
|
|
renderExtraToolbar(/*isDropdown = false*/) {
|
|
return "";
|
|
}
|
|
|
|
renderCollTabs(isSidebar) {
|
|
const isStory = this.hasStory && this.tabData.view === "story";
|
|
const isPages = this.tabData.view === "pages";
|
|
const isResources = this.tabData.view === "resources";
|
|
const isInfo = this.tabData.view === "info";
|
|
|
|
return html`
|
|
|
|
${isInfo ? this.renderCollInfo() : html``}
|
|
|
|
${isStory ? html`
|
|
<wr-coll-story .collInfo="${this.collInfo}"
|
|
.active="${isStory}"
|
|
currList="${this.tabData.currList || 0}"
|
|
@coll-tab-nav="${this.onCollTabNav}" id="story"
|
|
.isSidebar="${isSidebar}"
|
|
class="${isStory ? "" : "is-hidden"} ${isSidebar ? "sidebar" : ""}"
|
|
role="${isSidebar ? "" : "main"}"
|
|
>
|
|
</wr-coll-story>` : ""}
|
|
|
|
${isResources ? html`
|
|
<wr-coll-resources .collInfo="${this.collInfo}"
|
|
.active="${isResources}"
|
|
query="${this.tabData.query || ""}"
|
|
urlSearchType="${this.tabData.urlSearchType || ""}"
|
|
.currMime="${this.tabData.currMime || ""}"
|
|
@coll-tab-nav="${this.onCollTabNav}" id="resources"
|
|
.isSidebar="${isSidebar}"
|
|
class="is-paddingless ${isResources ? "" : "is-hidden"} ${isSidebar ? "sidebar" : ""}"
|
|
role="${isSidebar ? "" : "main"}"
|
|
>
|
|
</wr-coll-resources>` : ""}
|
|
|
|
${isPages ? html`
|
|
<wr-page-view
|
|
.collInfo="${this.collInfo}"
|
|
.active="${isPages}"
|
|
.editable="${this.editable}"
|
|
.isSidebar="${isSidebar}"
|
|
currList="${this.tabData.currList || 0}"
|
|
query="${this.tabData.query || ""}"
|
|
.url="${this.tabData.url || ""}"
|
|
.ts="${this.tabData.ts || ""}"
|
|
@coll-tab-nav="${this.onCollTabNav}" id="pages"
|
|
@coll-update="${this.onCollUpdate}"
|
|
class="${isPages ? "" : "is-hidden"} ${isSidebar ? "sidebar" : ""}"
|
|
role="${isSidebar ? "" : "main"}"
|
|
>
|
|
</wr-page-view>` : ""}
|
|
`;
|
|
}
|
|
|
|
skipMenu(event){
|
|
// This is a workaround, since this app's routing doesn't permit normal
|
|
// following of in-page anchors.
|
|
event.preventDefault();
|
|
this.renderRoot.querySelector("#skip-replay-target").focus();
|
|
}
|
|
|
|
onKeyDown(event) {
|
|
if (event.key === "Esc" || event.key === "Escape") {
|
|
event.preventDefault();
|
|
event.currentTarget.value = this.url;
|
|
}
|
|
}
|
|
|
|
onMenu(event) {
|
|
event.stopPropagation();
|
|
this.menuActive = !this.menuActive;
|
|
|
|
if (this.menuActive) {
|
|
document.addEventListener("click", () => {
|
|
this.menuActive = false;
|
|
}, {once: true});
|
|
}
|
|
}
|
|
|
|
onFullscreenToggle(event) {
|
|
event.preventDefault();
|
|
this.menuActive = false;
|
|
if (!this.isFullscreen) {
|
|
this.requestFullscreen();
|
|
} else {
|
|
document.exitFullscreen();
|
|
}
|
|
}
|
|
|
|
onGoBack(event) {
|
|
event.preventDefault();
|
|
this.menuActive = false;
|
|
window.history.back();
|
|
}
|
|
|
|
onGoForward(event) {
|
|
event.preventDefault();
|
|
this.menuActive = false;
|
|
window.history.forward();
|
|
}
|
|
|
|
onShowPages(event) {
|
|
event.preventDefault();
|
|
// show sidebar for tablet or wider, or hide sidebar
|
|
if (this.showSidebar || (document.documentElement.clientWidth >= 769)) {
|
|
this.showSidebar = !this.showSidebar;
|
|
} else {
|
|
// otherwise, just go to full pages view
|
|
this.showSidebar = false;
|
|
this.updateTabData({url: "", ts: ""});
|
|
}
|
|
}
|
|
|
|
onFullPageView(event) {
|
|
event.preventDefault();
|
|
this.updateTabData({url: "", ts: ""});
|
|
}
|
|
|
|
onHideSidebar(event) {
|
|
event.preventDefault();
|
|
this.showSidebar = false;
|
|
}
|
|
|
|
async onFavIcons(event) {
|
|
for (const icon of event.detail.icons) {
|
|
const resp = await fetch(icon.href);
|
|
if (resp.status === 200) {
|
|
const ct = resp.headers.get("Content-Type");
|
|
if (ct && !ct.startsWith("text/")) {
|
|
this.favIconUrl = icon.href;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// not found
|
|
this.favIconUrl = "";
|
|
}
|
|
|
|
onPurgeCache(event) {
|
|
event.preventDefault();
|
|
|
|
const reload = event.detail && event.detail.reload !== undefined ? event.detail.reload : true;
|
|
|
|
this.deleteFully(reload);
|
|
}
|
|
|
|
async deleteFully(reload = false) {
|
|
const deleteURL = this.collInfo.apiPrefix + (reload ? "?reload=1" : "");
|
|
|
|
const resp = await fetch(deleteURL, {
|
|
method: "DELETE",
|
|
});
|
|
|
|
if (resp.status !== 200) {
|
|
console.warn("purge failed: " + resp.status);
|
|
}
|
|
|
|
if (!reload && !this.embed) {
|
|
window.location.search = "";
|
|
return;
|
|
}
|
|
|
|
//window.location.hash = "";
|
|
window.location.reload();
|
|
}
|
|
|
|
onSubmit(event) {
|
|
event.preventDefault();
|
|
const input = this.renderRoot.querySelector("input");
|
|
if (input.value) {
|
|
this.navigateTo(input.value);
|
|
} else {
|
|
input.value = this.url;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
onLostFocus(event) {
|
|
if (!event.currentTarget.value) {
|
|
event.currentTarget.value = this.url;
|
|
}
|
|
}
|
|
|
|
navigateTo(value) {
|
|
let data;
|
|
|
|
if (value.startsWith("http://") || value.startsWith("https://")) {
|
|
data = {url: value};
|
|
|
|
if (value === this.tabData.url) {
|
|
const replay = this.renderRoot.querySelector("wr-coll-replay");
|
|
if (replay) {
|
|
replay.refresh();
|
|
}
|
|
return;
|
|
}
|
|
|
|
} else {
|
|
if (!value.startsWith(RWP_SCHEME)) {
|
|
data = {query: value, view: "pages"};
|
|
} else {
|
|
data = this._stringToParams(value);
|
|
}
|
|
}
|
|
this.updateTabData(data);
|
|
}
|
|
|
|
_stringToParams(value) {
|
|
const q = new URLSearchParams(value.slice(RWP_SCHEME.length));
|
|
const data = {};
|
|
data.url = "";
|
|
data.ts = "";
|
|
|
|
for (const param of ["query", "view", "currList", "currMime", "urlSearchType"]) {
|
|
if (q.has(param)) {
|
|
data[param] = q.get(param);
|
|
}
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
_paramsToString(value) {
|
|
const q = new URLSearchParams();
|
|
|
|
for (const param of ["query", "view", "currList", "currMime", "urlSearchType"]) {
|
|
if (param in value) {
|
|
q.set(param, value[param]);
|
|
}
|
|
}
|
|
|
|
return q.toString();
|
|
}
|
|
|
|
onRefresh(event, replayOnly = false) {
|
|
if (event) {
|
|
event.preventDefault();
|
|
}
|
|
|
|
this.menuActive = false;
|
|
|
|
if (this.tabData.url) {
|
|
const replay = this.renderRoot.querySelector("wr-coll-replay");
|
|
if (replay) {
|
|
replay.refresh();
|
|
}
|
|
} else if (!replayOnly) {
|
|
window.location.reload();
|
|
}
|
|
}
|
|
|
|
onAbout() {
|
|
this.dispatchEvent(new CustomEvent("about-show"));
|
|
}
|
|
}
|
|
|
|
customElements.define("wr-coll", Coll);
|
|
|
|
export { Coll };
|