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` `; } else if (this.collInfo) { return html` ${this.renderLocationBar()}
${this.renderTabHeader(isSidebar)} ${isSidebar || !isReplay ? this.renderCollTabs(isSidebar) : html``}
${isReplay && this.isVisible ? html` ` : ""}
`; } else { return html``; } } renderTabHeader(isSidebar) { // if (this.tabData.view === "replay") { // return ""; // } return html`
    ${isSidebar ? html`
  • Hide Hide Sidebar
  • ` : ""} ${this.hasStory ? html`
  • Story
  • ` : ""}
  • Pages
  • URLs
  • Info
  • ${isSidebar ? html`
  • Expand Expand Sidebar to Full View
  • ` : ""}
`; } renderLocationBar() { if (this.embed === "replayonly") { return ""; } const dateStr = tsToDate(this.ts).toLocaleString(); const isReplay = !!this.tabData.url; const showFavIcon = isReplay && this.favIconUrl; return html` Skip replay navigation
${!this.isLoading ? html` ` : ""} ${this.renderExtraToolbar(false)}
${isReplay ? html`

${dateStr}

` : html``} ${showFavIcon ? html` ` : html``}
Full Screen Back Forward Reload Browse Contents ${this.renderExtraToolbar(true)} ${!this.editable ? html`
Purge Cache + Full Reload ` : html``} ${!this.editable && this.sourceUrl.startsWith("http://") || this.sourceUrl.startsWith("https://") ? html`
Download Archive ` : html``} ${dateStr ? html`
Capture Date${dateStr}
` : ""}
 About ${this.appName} (${VERSION})

Skipped

`; } 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`
`; } 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` ` : ""} ${isResources ? html` ` : ""} ${isPages ? html` ` : ""} `; } 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 };