update to latest wabac.js -- smaller size support for 'source' param on <replay-app-main> which sets sourceUrl and forces direct loading of a single archive only bump version to 1.3.5
520 lines
16 KiB
JavaScript
520 lines
16 KiB
JavaScript
import { LitElement, html, css } from 'lit-element';
|
|
import { wrapCss, rwpLogo, IS_APP, clickOnSpacebarPress } from './misc';
|
|
|
|
import { registerSW } from './pageutils';
|
|
|
|
import fasHelp from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg';
|
|
import fasArrowLeft from '@fortawesome/fontawesome-free/svgs/solid/arrow-left.svg';
|
|
import fasArrowRight from '@fortawesome/fontawesome-free/svgs/solid/arrow-right.svg';
|
|
|
|
|
|
// ===========================================================================
|
|
class ReplayWebApp extends LitElement
|
|
{
|
|
constructor() {
|
|
super();
|
|
this.sourceUrl = null;
|
|
this.collTitle = null;
|
|
this.showAbout = false;
|
|
this.pageParams = {};
|
|
|
|
this.inited = false;
|
|
this.navMenuShown = false;
|
|
|
|
this.collPageUrl = "";
|
|
this.pageTitle = "";
|
|
this.pageReplay = false;
|
|
|
|
registerSW(__SW_NAME__);
|
|
|
|
this.safariKeyframes();
|
|
}
|
|
|
|
get appName() {
|
|
return "ReplayWeb.page";
|
|
}
|
|
|
|
get homeUrl() {
|
|
return "/";
|
|
}
|
|
|
|
static get properties() {
|
|
return {
|
|
inited: { type: Boolean },
|
|
pageParams: { type: Object },
|
|
sourceUrl: { type: String },
|
|
navMenuShown: { type: Boolean },
|
|
showAbout: { type: Boolean },
|
|
collTitle: { type: String },
|
|
loadInfo: { type: Object },
|
|
embed: { type: String },
|
|
collPageUrl: { type: String },
|
|
pageTitle: { type: String },
|
|
pageReplay: { type: Boolean },
|
|
source: { type: String }
|
|
}
|
|
}
|
|
|
|
static get styles() {
|
|
return wrapCss(ReplayWebApp.appStyles);
|
|
}
|
|
|
|
static get appStyles() {
|
|
return css`
|
|
#wrlogo {
|
|
max-height: 1.0rem;
|
|
}
|
|
.navbar {
|
|
height: 1.5rem;
|
|
}
|
|
.navbar-brand {
|
|
height: 1.5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.wr-logo-item {
|
|
padding: 0 8px 0 0;
|
|
}
|
|
.no-wrap {
|
|
white-space: nowrap;
|
|
overflow-x: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.has-allcaps {
|
|
font-variant-caps: small-caps;
|
|
}
|
|
:host {
|
|
position: fixed;
|
|
left: 0px;
|
|
top: 0px;
|
|
bottom: 0px;
|
|
right: 0px;
|
|
display: flex;
|
|
min-width: 0px;
|
|
flex-direction: column;
|
|
}
|
|
wr-coll {
|
|
height: 100%;
|
|
}
|
|
.navbar {
|
|
padding: 0 0.5em;
|
|
}
|
|
|
|
div.navbar-menu fa-icon {
|
|
vertical-align: sub;
|
|
}
|
|
.tagline {
|
|
margin-top: 1.0rem;
|
|
}
|
|
|
|
@media screen and (min-width: 840px) {
|
|
.menu-only {
|
|
display: none;
|
|
}
|
|
|
|
a.arrow-button {
|
|
padding-left: 4px;
|
|
padding-right: 4px;
|
|
}
|
|
|
|
.info-menu {
|
|
padding: 0 1.0em;
|
|
}
|
|
|
|
.logo-text {
|
|
padding-left: 0px;
|
|
margin-left: 6px;
|
|
}
|
|
|
|
a.navbar-item.logo-text:hover {
|
|
background-color: initial;
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 840px) {
|
|
.wide-only {
|
|
display: none !important;
|
|
}
|
|
}
|
|
|
|
`;
|
|
}
|
|
|
|
get mainLogo() {
|
|
return rwpLogo;
|
|
}
|
|
|
|
renderNavBrand() {
|
|
return html`
|
|
<span id="home" class="logo-text has-text-weight-bold is-size-6 has-allcaps wide-only">
|
|
<span class="has-text-primary">replay</span>
|
|
<span class="has-text-link">web.page</span>
|
|
<span class="is-sr-only">Home</span>
|
|
</span>`;
|
|
}
|
|
|
|
renderNavBar() {
|
|
return html`
|
|
<a href="#skip-main-target" @click=${this.skipMenu} class="skip-link">Skip main navigation</a>
|
|
<nav class="navbar has-background-info" aria-label="main">
|
|
<div class="navbar-brand">
|
|
${!this.embed ? html`
|
|
<a href="${this.homeUrl}" class="navbar-item wr-logo-item" aria-labelledby="home">
|
|
<fa-icon id="wrlogo" size="1.0rem" .svg=${this.mainLogo} aria-hidden="true"></fa-icon>
|
|
${this.renderNavBrand()}
|
|
</a>
|
|
${this.collTitle ? html`
|
|
<a href="${this.collPageUrl}" class="no-wrap is-size-6 has-text-black">/ <i>${this.collTitle}</i></a>
|
|
<span class="no-wrap is-size-6"> /
|
|
${this.pageReplay ? html`<i>${this.pageTitle}</i>` : this.pageTitle}
|
|
</span>
|
|
` : ``}
|
|
`: html`
|
|
<span class="navbar-item wr-logo-item">
|
|
<fa-icon id="wrlogo" size="1.0rem" .svg=${this.mainLogo} aria-hidden="true"></fa-icon>
|
|
</span>
|
|
`}
|
|
<a href="#" role="button" id="menu-button" @click="${this.onNavMenu}" @keyup="${clickOnSpacebarPress}"
|
|
class="navbar-burger burger ${this.navMenuShown ? 'is-active' : ''}" aria-label="main menu" aria-haspopup="true" aria-expanded="${this.navMenuShown}">
|
|
<span aria-hidden="true"></span>
|
|
<span aria-hidden="true"></span>
|
|
<span aria-hidden="true"></span>
|
|
</a>
|
|
</div>
|
|
${!this.sourceUrl ? html`
|
|
<div class="navbar-menu ${this.navMenuShown ? 'is-active' : ''}">
|
|
<div class="navbar-start">
|
|
${IS_APP ? html`
|
|
<a role="button" href="#" class="navbar-item arrow-button" title="Go Back" @click="${(e) => window.history.back()}" @keyup="${clickOnSpacebarPress}">
|
|
<fa-icon size="1.0rem" .svg="${fasArrowLeft}" aria-hidden="true"></fa-icon><span class="menu-only is-size-7"> Go Back</span>
|
|
</a>
|
|
<a role="button" href="#" class="navbar-item arrow-button" title="Go Forward" @click="${(e) => window.history.forward()}" @keyup="${clickOnSpacebarPress}">
|
|
<fa-icon size="1.0rem" .svg="${fasArrowRight}" aria-hidden="true"></fa-icon><span class="menu-only is-size-7"> Go Forward</span>
|
|
</a>
|
|
` : ``}
|
|
</div>
|
|
${!this.embed ? html`
|
|
<div class="navbar-end">
|
|
${this.renderNavEnd()}
|
|
</div>` : html``}
|
|
</div>` : html``}
|
|
</nav>
|
|
<p id="skip-main-target" tabindex="-1" class="is-sr-only">Skipped</p>`;
|
|
}
|
|
|
|
renderNavEnd() {
|
|
return html`
|
|
<a href="/docs" target="_blank" class="navbar-item is-size-6">
|
|
<fa-icon .svg="${fasHelp}" aria-hidden="true"></fa-icon><span> User Docs</span>
|
|
</a>
|
|
<!--
|
|
-- NB: the About modal is currently inaccessible to people using keyboards or screen readers.
|
|
-- Should all the JS and infrastructure for accessible modals be added, or should About be a normal page?
|
|
-->
|
|
<a href="?terms" @click="${(e) => { e.preventDefault(); this.showAbout = true} }"class="navbar-item is-size-6">About
|
|
</a>`
|
|
}
|
|
|
|
renderColl() {
|
|
return html`
|
|
<wr-coll .loadInfo="${this.loadInfo}"
|
|
sourceUrl="${this.sourceUrl}"
|
|
embed="${this.embed}"
|
|
appName="${this.appName}"
|
|
.appLogo="${this.mainLogo}"
|
|
@replay-favicons=${this.onFavIcons}
|
|
@update-title=${this.onTitle}
|
|
@coll-loaded=${this.onCollLoaded}
|
|
@about-show=${(e) => this.showAbout = true}></wr-coll>`;
|
|
}
|
|
|
|
renderHomeIndex() {
|
|
return html`
|
|
<wr-coll-index>
|
|
${!IS_APP ? html`
|
|
<p slot="header" class="tagline is-size-5 has-text-centered">Explore and Replay Interactive Archived Webpages Directly in your Browser. <i><a target="_blank" href="./docs/examples">(See Examples)</a></i></p>
|
|
` : ``}
|
|
<wr-chooser slot="header" @load-start=${this.onStartLoad}></wr-chooser>
|
|
</wr-coll-index>`;
|
|
}
|
|
|
|
render() {
|
|
if (!this.inited) {
|
|
return html``;
|
|
}
|
|
|
|
return html`
|
|
${!this.embed || this.embed === "full" ? this.renderNavBar() : ''}
|
|
|
|
${this.sourceUrl ? this.renderColl() : this.renderHomeIndex()}
|
|
|
|
${this.showAbout ? this.renderAbout() : ``}
|
|
`;
|
|
}
|
|
|
|
firstUpdated() {
|
|
this.initRoute();
|
|
window.addEventListener("popstate", (event) => {
|
|
this.initRoute();
|
|
});
|
|
}
|
|
|
|
updated(changedProperties) {
|
|
if (changedProperties.has("sourceUrl")) {
|
|
this.collTitle = null;
|
|
}
|
|
}
|
|
|
|
onFavIcons(event) {
|
|
const head = document.querySelector('head');
|
|
const oldLinks = document.querySelectorAll("link[rel*='icon']");
|
|
|
|
for (const link of oldLinks) {
|
|
head.removeChild(link);
|
|
}
|
|
|
|
for (const icon of event.detail.icons) {
|
|
const link = document.createElement('link');
|
|
link.rel = icon.rel;
|
|
link.href = icon.href;
|
|
head.appendChild(link);
|
|
}
|
|
}
|
|
|
|
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-main-target").focus()
|
|
}
|
|
|
|
onNavMenu(event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
this.navMenuShown = !this.navMenuShown;
|
|
|
|
if (this.navMenuShown) {
|
|
// Since this menu can be large and obscure significant page content,
|
|
// dismiss like a modal, returning keyboard focus to the
|
|
// menu button on dismissal.
|
|
document.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
this.navMenuShown = false;
|
|
this.renderRoot.querySelector("#menu-button").focus();
|
|
}, {once: true});
|
|
document.addEventListener("keypress", (event) => {
|
|
if (event.key == "Escape") {
|
|
event.preventDefault();
|
|
this.navMenuShown = false;
|
|
this.renderRoot.querySelector("#menu-button").focus();
|
|
}
|
|
}, {once: true});
|
|
}
|
|
}
|
|
|
|
initRoute() {
|
|
this.inited = true;
|
|
this.pageParams = new URLSearchParams(window.location.search);
|
|
|
|
// Google Drive
|
|
let state = this.pageParams.get("state");
|
|
if (state) {
|
|
try {
|
|
state = JSON.parse(state);
|
|
if ((state.ids instanceof Array) && state.userId && state.action === "open") {
|
|
this.pageParams.set("source", "googledrive://" + state.ids[0]);
|
|
this.pageParams.delete("state");
|
|
window.location.search = this.pageParams.toString();
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
console.log(e);
|
|
}
|
|
}
|
|
|
|
if (this.source) {
|
|
this.pageParams.set("source", this.source);
|
|
const url = new URL(window.location.href);
|
|
url.search = this.pageParams.toString();
|
|
window.history.replaceState({}, document.title, url.href);
|
|
}
|
|
|
|
this.sourceUrl = this.pageParams.get("source") || "";
|
|
this.embed = this.pageParams.get("embed") || "";
|
|
|
|
if (this.pageParams.has("terms")) {
|
|
this.showAbout = true;
|
|
}
|
|
|
|
if (this.pageParams.get("config")) {
|
|
if (!this.loadInfo) {
|
|
this.loadInfo = {}
|
|
}
|
|
try {
|
|
this.loadInfo.extraConfig = JSON.parse(this.pageParams.get("config"));
|
|
} catch (e) {
|
|
console.log("invalid config: " + e);
|
|
}
|
|
}
|
|
|
|
if (this.pageParams.get("basePageUrl")) {
|
|
if (!this.loadInfo) {
|
|
this.loadInfo = {extraConfig: {}}
|
|
}
|
|
if (!this.loadInfo.extraConfig) {
|
|
this.loadInfo.extraConfig = {};
|
|
}
|
|
this.loadInfo.extraConfig.baseUrl = this.pageParams.get("basePageUrl");
|
|
}
|
|
|
|
if (this.pageParams.get("customColl")) {
|
|
if (!this.loadInfo) {
|
|
this.loadInfo = {}
|
|
}
|
|
this.loadInfo.customColl = this.pageParams.get("customColl");
|
|
}
|
|
|
|
if (IS_APP && this.sourceUrl.startsWith("file://")) {
|
|
const url = new URL(__APP_FILE_SERVE_PREFIX__);
|
|
url.searchParams.set("filename", this.sourceUrl.slice("file://".length));
|
|
this.loadInfo = {
|
|
sourceUrl: this.sourceUrl,
|
|
loadUrl: url.href,
|
|
}
|
|
}
|
|
}
|
|
|
|
onStartLoad(event) {
|
|
// just redirect right away?
|
|
this.pageParams.set("source", event.detail.sourceUrl);
|
|
|
|
const url = new URL(window.location.href);
|
|
url.search = this.pageParams.toString();
|
|
this.collPageUrl = url.toString();
|
|
|
|
if (!event.detail.isFile) {
|
|
window.location.search = this.pageParams.toString();
|
|
return;
|
|
} else {
|
|
|
|
window.history.pushState({}, "", this.collPageUrl);
|
|
}
|
|
|
|
this.sourceUrl = event.detail.sourceUrl;
|
|
this.loadInfo = event.detail;
|
|
}
|
|
|
|
onCollLoaded(event) {
|
|
this.loadInfo = null;
|
|
if (event.detail.collInfo) {
|
|
this.collTitle = event.detail.collInfo.title;
|
|
}
|
|
|
|
if (event.detail.alreadyLoaded) {
|
|
//this.sourceLoaded = true;
|
|
return;
|
|
}
|
|
|
|
//this.initRoute();
|
|
|
|
if (event.detail.sourceUrl !== this.sourceUrl) {
|
|
this.pageParams.set("source", event.detail.sourceUrl);
|
|
window.location.search = this.pageParams.toString();
|
|
}
|
|
}
|
|
|
|
onTitle(event) {
|
|
if (event.detail.title) {
|
|
this.pageTitle = event.detail.title;
|
|
this.pageReplay = event.detail.replayTitle;
|
|
|
|
document.title = (event.detail.replayTitle ? "Archive of " : "") + this.pageTitle + " | ReplayWeb.page";
|
|
}
|
|
}
|
|
|
|
safariKeyframes() {
|
|
// Safari requires keyframes at the document level, add spinner keyframes here
|
|
const css = `
|
|
@keyframes spinAround {
|
|
from {
|
|
transform: rotate(0deg);
|
|
}
|
|
to {
|
|
transform: rotate(359deg);
|
|
}
|
|
}
|
|
`;
|
|
|
|
const style = document.createElement("style");
|
|
document.head.appendChild(style);
|
|
style.appendChild(document.createTextNode(css));
|
|
}
|
|
|
|
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">
|
|
<wr-anim-logo class="logo" size="48px"></wr-anim-logo>
|
|
<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 at:
|
|
<a href="https://github.com/webrecorder/replayweb.page" target="_blank">https://github.com/webrecorder/replayweb.page</a>
|
|
</p>
|
|
|
|
<p>See the <a target="_blank" href="./docs">User Docs</a> or the GitHub README for more info on how it works.</p>
|
|
|
|
<p>ReplayWeb.page is part of the <a href="https://webrecorder.net/" target="_blank">Webrecorder Project</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.
|
|
</details>
|
|
<div class="has-text-centered">
|
|
<a class="button is-warning" href="#" @click="${this.onAboutClose}">Close</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
onAboutClose() {
|
|
this.showAbout = false;
|
|
}
|
|
}
|
|
|
|
customElements.define("replay-app-main", ReplayWebApp);
|
|
|
|
export { ReplayWebApp };
|