big ui update, page search + full text
- split file chooser from coll index, insert as header, make file chooser collapsible - revamp collection index to be responsive - add separate sorter component, used for collection index and pages - combine 'curated lists' + pages, with page search + narrative - ensure page + narrative lists are collapsible - page search: support title, url and full text search if available, using flexsearch - page search: keyword highlighting, load all pages, showing in increments of 100 - page and resource search: delay search to 1/4 sec of inactivity to avoid too many queries - electron app: ensure noCache is set to true for local files - embedding: remove separate embed.js, just use ui.js for embed as well - embedding: support embedding iframe in a local origin with just sw.js and ui.js: sw.js serves index.html, <replay-web-page> embed injects ui.js into iframe - sw.js, new service worker entry point - sw and ww inited only from <replay-app-main>, ui.js loaded from <replay-web-page> embed component
This commit is contained in:
+25
-3
@@ -1,6 +1,8 @@
|
||||
import { LitElement, html, css } from 'lit-element';
|
||||
import { wrapCss, rwpLogo, IS_APP } from './misc';
|
||||
|
||||
import { registerSW } from './pageutils';
|
||||
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
import fasHelp from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg';
|
||||
@@ -19,10 +21,15 @@ class App extends LitElement
|
||||
this.collInfo = null;
|
||||
this.showTerms = false;
|
||||
this.pageParams = {};
|
||||
|
||||
this.inited = false;
|
||||
|
||||
registerSW(__SW_NAME__);
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
inited: { type: Boolean },
|
||||
pageParams: { type: Object },
|
||||
sourceUrl: { type: String },
|
||||
navMenuShown: { type: Boolean },
|
||||
@@ -63,6 +70,9 @@ class App extends LitElement
|
||||
div.navbar-menu fa-icon {
|
||||
vertical-align: sub;
|
||||
}
|
||||
.tagline {
|
||||
margin-top: 1.0rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 840px) {
|
||||
.menu-only {
|
||||
@@ -82,8 +92,8 @@ class App extends LitElement
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.logo-text:hover {
|
||||
background-color: unset;
|
||||
a.navbar-item.logo-text:hover {
|
||||
background-color: initial;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +107,9 @@ class App extends LitElement
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.inited) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
${!this.embed ? html`
|
||||
<nav class="navbar has-background-info" role="navigation" aria-label="main navigation">
|
||||
@@ -162,14 +175,22 @@ class App extends LitElement
|
||||
` : ''}
|
||||
|
||||
${this.sourceUrl ? html`
|
||||
|
||||
<wr-coll .loadInfo="${this.loadInfo}"
|
||||
sourceUrl="${this.sourceUrl}"
|
||||
embed="${this.embed}"
|
||||
@replay-favicons=${this.onFavIcons}
|
||||
@coll-loaded=${this.onCollLoaded}></wr-coll>
|
||||
` : html`
|
||||
<wr-coll-index @load-start=${this.onStartLoad}></wr-coll-index>
|
||||
|
||||
<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.</p>
|
||||
` : ``}
|
||||
<wr-chooser slot="header" @load-start=${this.onStartLoad}></wr-chooser>
|
||||
</wr-coll-index>
|
||||
`}
|
||||
|
||||
${this.showTerms ? html`
|
||||
<div class="modal is-active">
|
||||
<div class="modal-background" @click="${(e) => this.showTerms = false}"></div>
|
||||
@@ -241,6 +262,7 @@ class App extends LitElement
|
||||
}
|
||||
|
||||
initRoute() {
|
||||
this.inited = true;
|
||||
this.pageParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// Google Drive
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import { LitElement, html, css } from 'lit-element';
|
||||
import { wrapCss } from './misc';
|
||||
|
||||
import fasUpload from '@fortawesome/fontawesome-free/svgs/solid/upload.svg';
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
class Chooser extends LitElement
|
||||
{
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.fileDisplayName = "";
|
||||
this.file = null;
|
||||
}
|
||||
|
||||
onChooseFile(event) {
|
||||
if (event.currentTarget.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.file = event.currentTarget.files[0];
|
||||
// file.path only available in electron app
|
||||
this.fileDisplayName = "file://" + (this.file.path || this.file.name);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
onStartLoad(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const loadInfo = {sourceUrl: this.fileDisplayName};
|
||||
|
||||
if (this.file) {
|
||||
loadInfo.isFile = true;
|
||||
// file.path only available in electron app
|
||||
loadInfo.loadUrl = this.file.path ? __APP_FILE_SERVE_PREFIX__ + this.file.path : URL.createObjectURL(this.file);
|
||||
loadInfo.noCache = this.file.path !== undefined;
|
||||
loadInfo.name = this.fileDisplayName;
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent("load-start", {detail: loadInfo}));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onInput(event) {
|
||||
this.fileDisplayName = event.currentTarget.value;
|
||||
|
||||
if (this.file && this.fileDisplayName && this.fileDisplayName.startsWith("file://")) {
|
||||
this.file = null;
|
||||
this.fileDisplayName = "";
|
||||
}
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return wrapCss(css`
|
||||
.extra-padding {
|
||||
padding: 2em;
|
||||
}
|
||||
.less-padding {
|
||||
padding-top: 1.0em;
|
||||
padding-bottom: 1.0em;
|
||||
}
|
||||
div.field.has-addons {
|
||||
flex: auto;
|
||||
}
|
||||
form {
|
||||
flex-grow: 1;
|
||||
}
|
||||
input.input.file-name:invalid {
|
||||
border: 1px dashed red;
|
||||
}
|
||||
input.input.file-name {
|
||||
max-width: 100%;
|
||||
border-width: 1px;
|
||||
margin-left: -1px;
|
||||
}
|
||||
@media screen and (max-width: 1023px) {
|
||||
.file-icon {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
#filename {
|
||||
border-bottom-right-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
}
|
||||
`);
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<section class="section less-padding">
|
||||
<nav class="panel is-warning">
|
||||
<p class="panel-heading">Load Web Archive</p>
|
||||
<div class="extra-padding panel-block file has-name">
|
||||
<form class="content is-flex" @submit="${this.onStartLoad}">
|
||||
<label class="file-label">
|
||||
<input class="file-input"
|
||||
@click="${(e) => e.currentTarget.value = null}"
|
||||
@change=${this.onChooseFile} type="file" id="fileupload" name="fileupload">
|
||||
<span class="file-cta">
|
||||
<span class="file-icon">
|
||||
<fa-icon size="0.9em" .svg=${fasUpload}></fa-icon>
|
||||
</span>
|
||||
<span class="file-label is-hidden-touch">
|
||||
Choose File...
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="field has-addons">
|
||||
<p class="control is-expanded">
|
||||
<input class="file-name input" type="text"
|
||||
name="filename" id="filename"
|
||||
pattern="((file|http|https|s3):\/\/.*\.(warc|warc.gz|har|zip|wacz))|(googledrive:\/\/.+)"
|
||||
.value="${this.fileDisplayName}"
|
||||
@input="${this.onInput}"
|
||||
autocomplete="off"
|
||||
placeholder="Enter a URL or click 'Choose File' to select a (WARC, HAR, WBN, or WACZ) archive source">
|
||||
</p>
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-hidden-mobile is-primary">Load</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
</section>`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("wr-chooser", Chooser);
|
||||
|
||||
export { Chooser };
|
||||
+129
-199
@@ -1,38 +1,54 @@
|
||||
import { LitElement, html, css } from 'lit-element';
|
||||
import { wrapCss, IS_APP } from './misc';
|
||||
import { wrapCss } from './misc';
|
||||
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
import fasUpload from '@fortawesome/fontawesome-free/svgs/solid/upload.svg';
|
||||
import fasCopy from '@fortawesome/fontawesome-free/svgs/regular/copy.svg';
|
||||
|
||||
import fasArrowUp from '@fortawesome/fontawesome-free/svgs/solid/angle-double-up.svg';
|
||||
import fasArrowDown from '@fortawesome/fontawesome-free/svgs/solid/angle-double-down.svg';
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
class CollIndex extends LitElement
|
||||
{
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.colls = [];
|
||||
|
||||
this.sortedColls = [];
|
||||
this.sortKey = "title";
|
||||
this.sortDesc = false;
|
||||
//this.sortKey = localStorage.getItem("index:sortKey") || "title";
|
||||
//this.sortDesc = localStorage.getItem("index:sortDesc") === "1";
|
||||
|
||||
this.fileDisplayName = "";
|
||||
this.file = null;
|
||||
this.hideHeader = localStorage.getItem("index:hideHeader") === "1";
|
||||
|
||||
this._deleting = {};
|
||||
}
|
||||
|
||||
static get sortKeys() {
|
||||
return [
|
||||
{key: "title",
|
||||
name: "Title"},
|
||||
|
||||
{key: "sourceUrl",
|
||||
name: "Source"},
|
||||
|
||||
{key: "ctime",
|
||||
name: "Date Created"},
|
||||
|
||||
{key: "size",
|
||||
name: "Total Size"}
|
||||
];
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
colls: { type: Array },
|
||||
|
||||
sortedColls: { type: Array },
|
||||
sortKey: { type: String },
|
||||
sortDesc: { type: Boolean },
|
||||
|
||||
fileDisplayName: { type: String },
|
||||
hideHeader: { type: Boolean },
|
||||
|
||||
_deleting: { type: Object }
|
||||
}
|
||||
}
|
||||
@@ -42,35 +58,19 @@ class CollIndex extends LitElement
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
if (changedProperties.has("colls") ||
|
||||
changedProperties.has("sortKey") ||
|
||||
changedProperties.has("sortDesc")) {
|
||||
this.sortColls();
|
||||
if (changedProperties.has("hideHeader")) {
|
||||
localStorage.setItem("index:hideHeader", this.hideHeader ? "1" : "0");
|
||||
}
|
||||
}
|
||||
|
||||
sortColls() {
|
||||
this.sortedColls = [];
|
||||
|
||||
this.colls.forEach((coll) => {
|
||||
coll.title = coll.title || coll.filename;
|
||||
this.sortedColls.push(coll);
|
||||
});
|
||||
|
||||
this.sortedColls.sort((first, second) => {
|
||||
if (first[this.sortKey] === second[this.sortKey]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (this.sortDesc == (first[this.sortKey] < second[this.sortKey])) ? 1 : -1;
|
||||
});
|
||||
}
|
||||
|
||||
async loadColls() {
|
||||
const resp = await fetch("./wabac/api/index");
|
||||
try {
|
||||
const json = await resp.json();
|
||||
this.colls = json.colls;
|
||||
this.colls = json.colls.map((coll) => {
|
||||
coll.title = coll.title || coll.filename;
|
||||
return coll;
|
||||
});
|
||||
} catch (e) {
|
||||
// likely no sw registered yet
|
||||
}
|
||||
@@ -82,7 +82,12 @@ class CollIndex extends LitElement
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (!this.sortedColls) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = Number(event.currentTarget.getAttribute("data-coll-index"));
|
||||
|
||||
const coll = this.sortedColls[index];
|
||||
|
||||
if (!coll || this._deleting[coll.sourceUrl]) {
|
||||
@@ -100,41 +105,6 @@ class CollIndex extends LitElement
|
||||
return false;
|
||||
}
|
||||
|
||||
onChooseFile(event) {
|
||||
if (event.currentTarget.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.file = event.currentTarget.files[0];
|
||||
this.fileDisplayName = "file://" + (this.file.path || this.file.name);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
onStartLoad(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const detail = {sourceUrl: this.fileDisplayName};
|
||||
|
||||
if (this.file) {
|
||||
detail.isFile = true;
|
||||
detail.loadUrl = this.file.path ? __APP_FILE_SERVE_PREFIX__ + this.file.path : URL.createObjectURL(this.file);
|
||||
detail.name = this.fileDisplayName;
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent("load-start", {detail}));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onInput(event) {
|
||||
this.fileDisplayName = event.currentTarget.value;
|
||||
|
||||
if (this.file && this.fileDisplayName && this.fileDisplayName.startsWith("file://")) {
|
||||
this.file = null;
|
||||
this.fileDisplayName = "";
|
||||
}
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return wrapCss(css`
|
||||
:host {
|
||||
@@ -149,12 +119,6 @@ class CollIndex extends LitElement
|
||||
.no-top-padding {
|
||||
padding-top: 1.0em;
|
||||
}
|
||||
div.field.has-addons {
|
||||
flex: auto;
|
||||
}
|
||||
form, .level {
|
||||
flex-grow: 1;
|
||||
}
|
||||
button.is-loading {
|
||||
line-height: 1.5em;
|
||||
height: 1.5em;
|
||||
@@ -165,33 +129,10 @@ class CollIndex extends LitElement
|
||||
nav.panel.is-light {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
input.input.file-name:invalid {
|
||||
border: 1px dashed red;
|
||||
}
|
||||
input.input.file-name {
|
||||
max-width: 100%;
|
||||
border-width: 1px;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
|
||||
table tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
tbody tr:hover {
|
||||
background-color: aliceblue;
|
||||
}
|
||||
fa-icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.asc:after {
|
||||
content: "▼";
|
||||
font-size: 0.75em;
|
||||
}
|
||||
.desc:after {
|
||||
content: "▲";
|
||||
font-size: 0.75em;
|
||||
}
|
||||
.copy {
|
||||
color: black;
|
||||
margin: 0px;
|
||||
@@ -205,112 +146,110 @@ class CollIndex extends LitElement
|
||||
.copy:active {
|
||||
background-color: lightgray;
|
||||
}
|
||||
.col-source {
|
||||
.sort-header{
|
||||
padding-bottom: 0.3rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.columns {
|
||||
width: 100vw;
|
||||
}
|
||||
.column {
|
||||
word-break: break-word;
|
||||
position: relative;
|
||||
}
|
||||
.col-source:hover > .copy, .source-text:hover + .copy, .copy:hover {
|
||||
.col-title:hover {
|
||||
|
||||
}
|
||||
.col-title a {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
.column:hover > .copy, .source-text:hover + .copy, .copy:hover {
|
||||
display: inline;
|
||||
}
|
||||
.coll-block {
|
||||
position: relative;
|
||||
}
|
||||
.delete {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
.minihead {
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
#sort-select::after {
|
||||
display: none;
|
||||
}
|
||||
header {
|
||||
transform: translate(0px, 0px);
|
||||
transition: all 0.5s ease 0s;
|
||||
visibility: visible;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
header.closed {
|
||||
transform: translate(0, -100%);
|
||||
transition: all 0.5s ease 0s;
|
||||
visibility: visible;
|
||||
height: 269px;
|
||||
margin-top: -269px;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<header class="${this.hideHeader ? 'closed' : ''}">
|
||||
<slot name="header"></slot>
|
||||
</header>
|
||||
<section class="section no-top-padding">
|
||||
${!IS_APP ? html`
|
||||
<p class="subtitle has-text-centered">Explore and Replay Interactive Archived Webpages Directly in your Browser.</p>
|
||||
` : ``}
|
||||
<nav class="panel is-warning">
|
||||
<p class="panel-heading">Load Web Archive</p>
|
||||
<div class="extra-padding panel-block file has-name">
|
||||
<form class="content is-flex" @submit="${this.onStartLoad}">
|
||||
<label class="file-label">
|
||||
<input class="file-input"
|
||||
@click="${(e) => e.currentTarget.value = null}"
|
||||
@change=${this.onChooseFile} type="file" id="fileupload" name="fileupload">
|
||||
<span class="file-cta">
|
||||
<span class="file-icon">
|
||||
<fa-icon size="0.9em" .svg=${fasUpload}></fa-icon>
|
||||
</span>
|
||||
<span class="file-label is-hidden-touch">
|
||||
Choose File...
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="field has-addons">
|
||||
<p class="control is-expanded">
|
||||
<input class="file-name input" type="text"
|
||||
name="filename" id="filename"
|
||||
pattern="((file|http|https|s3):\/\/.*\.(warc|warc.gz|har|zip|wacz))|(googledrive:\/\/.+)"
|
||||
.value="${this.fileDisplayName}"
|
||||
@input="${this.onInput}"
|
||||
autocomplete="off"
|
||||
placeholder="Enter a URL or click 'Choose File' to select a (WARC, HAR, WBN, or WACZ) archive source">
|
||||
</p>
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-hidden-mobile is-primary">Load</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
</section>
|
||||
|
||||
<section class="section no-top-padding">
|
||||
<div class="sort-header is-small">
|
||||
<a @click=${(e) => this.hideHeader = !this.hideHeader} class="button is-small">
|
||||
<span class="icon"><fa-icon .svg=${this.hideHeader ? fasArrowDown : fasArrowUp}></span>
|
||||
<span>${this.hideHeader ? 'Expand' : 'Collapse'}</span>
|
||||
</a>
|
||||
<wr-sorter id="index"
|
||||
.sortKeys="${CollIndex.sortKeys}"
|
||||
.data="${this.colls}"
|
||||
@sort-changed="${(e) => this.sortedColls = e.detail.sortedData}">
|
||||
</wr-sorter>
|
||||
</div>
|
||||
<nav class="panel is-light">
|
||||
<p class="panel-heading">Loaded Archives</p>
|
||||
<div class="panel-heading"><span>Loaded Archives</span>
|
||||
</div>
|
||||
<div class="coll-list">
|
||||
${this.sortedColls.length ? html`
|
||||
<div class="panel-block">
|
||||
<table class="table is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th @click="${this.onSort}" data-key="title"
|
||||
class="${this.sortKey === "title" ? (this.sortDesc ? "desc" : "asc") : ''} col-title">
|
||||
Title</th>
|
||||
|
||||
<th @click="${this.onSort}" data-key="sourceUrl"
|
||||
class="${this.sortKey === "sourceUrl" ? (this.sortDesc ? "desc" : "asc") : ''} col-source">
|
||||
Source URL (or Filename)</th>
|
||||
|
||||
<th @click="${this.onSort}" data-key="ctime"
|
||||
class="${this.sortKey === "ctime" ? (this.sortDesc ? "desc" : "asc") : ''} col-ctime">
|
||||
Date Loaded</th>
|
||||
|
||||
<th @click="${this.onSort}" data-key="size"
|
||||
class="${this.sortKey === "size" ? (this.sortDesc ? "desc" : "asc") : ''} col-size">
|
||||
Total Size</th>
|
||||
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${this.sortedColls.map((coll, i) => html`
|
||||
<tr @click=${(e) => window.location.href = "?source=" + coll.sourceUrl}>
|
||||
<td class="col-title"><span class="subtitle has-text-weight-bold">
|
||||
<a href="?source=${coll.sourceUrl}">${coll.title || coll.filename}</a>
|
||||
</td>
|
||||
<td class="col-source"><span class="source-text">${coll.sourceUrl} </span>
|
||||
<a @click="${(e) => this.onCopy(e, coll.sourceUrl)}" class="copy"><fa-icon .svg="${fasCopy}"/></a>
|
||||
${coll.sourceUrl && coll.sourceUrl.startsWith("googledrive://") ? html`
|
||||
<p><i>(${coll.filename})</i></p>` : ''}
|
||||
</td>
|
||||
<td class="col-date">${coll.ctime ? new Date(coll.ctime).toLocaleString() : ""}</td>
|
||||
<td class="col-size">${prettyBytes(Number(coll.size || 0))}</td>
|
||||
<td class="col-delete">
|
||||
${!this._deleting[coll.sourceUrl] ? html`
|
||||
<button data-coll-index="${i}" @click="${this.onDeleteColl}" class="delete"></button>
|
||||
` : html`
|
||||
<button class="button is-loading is-static"></button>`}
|
||||
</td>
|
||||
</tr>
|
||||
`)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>` : html`
|
||||
${this.sortedColls.length ? html`
|
||||
${this.sortedColls.map((coll, i) => html`
|
||||
<div class="coll-block panel-block">
|
||||
<div class="columns">
|
||||
<div class="column col-title is-4">
|
||||
<span class="subtitle has-text-weight-bold">
|
||||
<a href="?source=${coll.sourceUrl}">${coll.title || coll.filename}</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<span class="source-text"><p class="minihead">Source</p>${coll.sourceUrl} </span>
|
||||
<a @click="${(e) => this.onCopy(e, coll.sourceUrl)}" class="copy"><fa-icon .svg="${fasCopy}"/></a>
|
||||
${coll.sourceUrl && coll.sourceUrl.startsWith("googledrive://") ? html`
|
||||
<p><i>(${coll.filename})</i></p>` : ''}
|
||||
</div>
|
||||
<div class="column is-2"><p class="minihead">Date Loaded</p>${coll.ctime ? new Date(coll.ctime).toLocaleString() : ""}</div>
|
||||
<div class="column is-2"><p class="minihead">Total Size</p>${prettyBytes(Number(coll.size || 0))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
${!this._deleting[coll.sourceUrl] ? html`
|
||||
<button class="delete" title="Unload Collection" data-coll-index="${i}" @click="${this.onDeleteColl}"></button>
|
||||
` : html`
|
||||
<button class="button delete is-loading is-static"></button>`}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
` : html`
|
||||
<div class="panel-block extra-padding">
|
||||
<i>No Archives so far! Archives loaded in the section above will appear here.</i>
|
||||
</div>
|
||||
@@ -327,18 +266,9 @@ class CollIndex extends LitElement
|
||||
navigator.clipboard.writeText(sourceUrl);
|
||||
return false;
|
||||
}
|
||||
|
||||
onSort(event) {
|
||||
const key = event.currentTarget.getAttribute("data-key");
|
||||
if (key === this.sortKey) {
|
||||
this.sortDesc = !this.sortDesc;
|
||||
} else {
|
||||
this.sortDesc = false;
|
||||
this.sortKey = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
customElements.define("wr-coll-index", CollIndex);
|
||||
|
||||
export { CollIndex };
|
||||
+63
-64
@@ -3,9 +3,8 @@ import { wrapCss, IS_APP } from './misc';
|
||||
|
||||
import { sourceToId } from './pageutils';
|
||||
|
||||
import farListAlt from '@fortawesome/fontawesome-free/svgs/solid/list-alt.svg';
|
||||
import fasPage from '@fortawesome/fontawesome-free/svgs/solid/file-alt.svg';
|
||||
import fasSearch from '@fortawesome/fontawesome-free/svgs/solid/search.svg';
|
||||
import farListAlt from '@fortawesome/fontawesome-free/svgs/regular/list-alt.svg';
|
||||
import fasResources from '@fortawesome/fontawesome-free/svgs/regular/file-code.svg';
|
||||
import farPlayCircle from '@fortawesome/fontawesome-free/svgs/regular/play-circle.svg';
|
||||
|
||||
|
||||
@@ -15,6 +14,7 @@ class Coll extends LitElement
|
||||
constructor() {
|
||||
super();
|
||||
this.sourceUrl = null;
|
||||
this.inited = false;
|
||||
|
||||
this.baseApiPrefix = "./wabac/api";
|
||||
this.baseReplayPrefix = "./wabac";
|
||||
@@ -25,27 +25,27 @@ class Coll extends LitElement
|
||||
|
||||
this.collInfo = null;
|
||||
|
||||
this.hasCurated = false;
|
||||
|
||||
this._replaceLoc = false;
|
||||
this._locUpdateNeeded = false;
|
||||
|
||||
this._locationHash = "";
|
||||
|
||||
this.tabData = {};
|
||||
|
||||
this.tabNames = ["pages", "resources", "replay"];
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
sourceUrl: { type: String },
|
||||
loadInfo: { type: Object },
|
||||
inited: { type: Boolean },
|
||||
|
||||
collInfo: { type: Object },
|
||||
sourceUrl: { type: String },
|
||||
loadInfo: { type: Object, attribute: false },
|
||||
|
||||
collInfo: { type: Object, attribute: false },
|
||||
coll: { type: String },
|
||||
|
||||
hasCurated: { type: Boolean },
|
||||
|
||||
tabData: { type: Object },
|
||||
tabData: { type: Object, attribute: false },
|
||||
|
||||
replayUrl: { type: String },
|
||||
replayTS: { type: String },
|
||||
@@ -56,6 +56,7 @@ class Coll extends LitElement
|
||||
|
||||
firstUpdated() {
|
||||
//this.doUpdateInfo();
|
||||
this.inited = true;
|
||||
window.addEventListener("hashchange", (event) => this.onHashChange(event));
|
||||
}
|
||||
|
||||
@@ -109,7 +110,6 @@ class Coll extends LitElement
|
||||
|
||||
if (resp.status != 200) {
|
||||
this.collInfo = {};
|
||||
this.hasCurated = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -121,21 +121,12 @@ class Coll extends LitElement
|
||||
this.collInfo.title = this.collInfo.filename;
|
||||
}
|
||||
|
||||
this.hasCurated = (this.collInfo.lists && this.collInfo.lists.length);
|
||||
this.dispatchEvent(new CustomEvent("coll-loaded", {detail: {
|
||||
collInfo: this.collInfo,
|
||||
alreadyLoaded: true
|
||||
}}));
|
||||
|
||||
const hash = window.location.hash;
|
||||
if (hash) {
|
||||
this.tabData = Object.fromEntries(new URLSearchParams(hash.slice(1)).entries());
|
||||
}
|
||||
|
||||
if (this.collInfo.coll && !this.tabData.view) {
|
||||
this.tabData = {...this.tabData, view: this.hasCurated ? "curated" : "resources"};
|
||||
//this.replaceLoc = true;
|
||||
}
|
||||
this.onHashChange();
|
||||
}
|
||||
|
||||
onCollLoaded(event) {
|
||||
@@ -153,6 +144,13 @@ class Coll extends LitElement
|
||||
this.tabData = Object.fromEntries(new URLSearchParams(hash.slice(1)).entries());
|
||||
this._locationHash = hash;
|
||||
}
|
||||
|
||||
if (this.collInfo.coll && !this.tabNames.includes(this.tabData.view)) {
|
||||
this.tabData = {
|
||||
...this.tabData,
|
||||
view: this.collInfo.numPages || this.collInfo.numLists ? "pages" : "resources"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
onTabClick(event) {
|
||||
@@ -196,26 +194,32 @@ class Coll extends LitElement
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 320px) {
|
||||
.main.tabs {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 50%;
|
||||
-webkit-transform: translateX(-50%);
|
||||
transform: translateX(-50%);
|
||||
z-index: 35;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.main.tabs li {
|
||||
line-height: 1.5;
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
.main.tabs {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 50%;
|
||||
min-height: 41px;
|
||||
-webkit-transform: translateX(-50%);
|
||||
transform: translateX(-50%);
|
||||
z-index: 35;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.main.tabs {
|
||||
min-height: 41px;
|
||||
.main.tabs li {
|
||||
line-height: 1.5;
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 319px) {
|
||||
.main.tabs li a {
|
||||
padding-right: 4px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.tabs span.icon {
|
||||
margin: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
#contents {
|
||||
@@ -234,6 +238,8 @@ class Coll extends LitElement
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.inited) return html``;
|
||||
|
||||
if (this.collInfo && !this.collInfo.coll) {
|
||||
return html`
|
||||
<wr-loader .loadInfo="${this.loadInfo}"
|
||||
@@ -244,26 +250,17 @@ class Coll extends LitElement
|
||||
${!this.embed ? html`
|
||||
<div class="main tabs has-background-info is-centered">
|
||||
<ul>
|
||||
${this.hasCurated ? html`
|
||||
<li class="${this.tabData.view === 'curated' ? 'is-active' : ''}">
|
||||
<a @click="${this.onTabClick}" href="#curated" class="is-size-6">
|
||||
<span class="icon"><fa-icon .svg="${farListAlt}"></fa-icon></span>
|
||||
<span class="tab-label" title="Curated Lists">Curated Lists</span>
|
||||
</a>
|
||||
</li>
|
||||
` : ``}
|
||||
|
||||
<li class="${this.tabData.view === 'pages' ? 'is-active' : ''}">
|
||||
<a @click="${this.onTabClick}" href="#pages" class="is-size-6">
|
||||
<span class="icon"><fa-icon .svg="${fasPage}"></fa-icon></span>
|
||||
<span class="icon"><fa-icon .svg="${farListAlt}"></fa-icon></span>
|
||||
<span class="tab-label" title="Pages">Pages</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="${this.tabData.view === 'resources' ? 'is-active' : ''}">
|
||||
<a @click="${this.onTabClick}" href="#resources" class="is-size-6">
|
||||
<span class="icon"><fa-icon .svg="${fasSearch}"></fa-icon></span>
|
||||
<span class="tab-label" title="URL Resources">URL Resources</span>
|
||||
<span class="icon"><fa-icon .svg="${fasResources}"></fa-icon></span>
|
||||
<span class="tab-label" title="URL Resources">Page Resources</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -283,34 +280,36 @@ class Coll extends LitElement
|
||||
}
|
||||
|
||||
renderCollTabs() {
|
||||
const isPages = this.tabData.view === 'pages';
|
||||
const isResources = this.tabData.view === 'resources';
|
||||
const isReplay = this.tabData.view === 'replay';
|
||||
|
||||
return html`
|
||||
<wr-coll-curated .collInfo="${this.collInfo}"
|
||||
currList="${this.tabData.currList || 0}"
|
||||
@coll-tab-nav="${this.onCollTabNav}" id="curated"
|
||||
class="${this.tabData.view === 'curated' ? '' : 'is-hidden'}">
|
||||
.active="${isPages}"
|
||||
currList="${this.tabData.currList === undefined ? "pages" : this.tabData.currList}"
|
||||
query="${this.tabData.query || ""}"
|
||||
@coll-tab-nav="${this.onCollTabNav}" id="pages"
|
||||
class="${isPages ? '' : 'is-hidden'}">
|
||||
</wr-coll-curated>
|
||||
|
||||
<wr-coll-pages .collInfo="${this.collInfo}"
|
||||
@coll-tab-nav="${this.onCollTabNav}" id="pages"
|
||||
class="${this.tabData.view === 'pages' ? '' : 'is-hidden'}">
|
||||
</wr-coll-pages>
|
||||
|
||||
<wr-coll-resources .collInfo="${this.collInfo}"
|
||||
urlSearch="${this.tabData.urlSearch || ""}"
|
||||
.active="${isResources}"
|
||||
query="${this.tabData.query || ""}"
|
||||
urlSearchType="${this.tabData.urlSearchType || ""}"
|
||||
.currMime="${this.tabData.currMime || ""}"
|
||||
@coll-tab-nav="${this.onCollTabNav}" id="resources"
|
||||
class="is-paddingless ${this.tabData.view === 'resources' ? '' : 'is-hidden'}">
|
||||
class="is-paddingless ${isResources ? '' : 'is-hidden'}">
|
||||
</wr-coll-resources>
|
||||
|
||||
${this.tabData.view === 'replay' ? html`
|
||||
${isReplay ? html`
|
||||
<wr-coll-replay .collInfo="${this.collInfo}"
|
||||
sourceUrl="${this.sourceUrl}"
|
||||
embed="${this.embed}"
|
||||
url="${this.tabData.url || ""}"
|
||||
ts="${this.tabData.ts || ""}"
|
||||
@coll-tab-nav="${this.onCollTabNav}" id="replay"
|
||||
class="${this.tabData.view === 'replay' ? '' : 'is-hidden'}">
|
||||
class="${isReplay ? '' : 'is-hidden'}">
|
||||
</wr-coll-replay>
|
||||
` : ``}
|
||||
`;
|
||||
|
||||
+172
-57
@@ -14,23 +14,38 @@ class CuratedLists extends LitElement
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.active = false;
|
||||
|
||||
this.collInfo = null;
|
||||
|
||||
this.currList = 0;
|
||||
this.query = "";
|
||||
|
||||
this.currList = "pages";
|
||||
|
||||
this.lists = [];
|
||||
this.curatedPages = {};
|
||||
|
||||
this.offset = 0;
|
||||
this.lastST = 0;
|
||||
this.clickTime = 0;
|
||||
|
||||
this.hasNarrative = false;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
active: { type: Boolean },
|
||||
|
||||
collInfo: { type: Object },
|
||||
|
||||
query: { type: String },
|
||||
|
||||
lists: { type: Array },
|
||||
curatedPages: { type: Object },
|
||||
currList: { type: Number },
|
||||
|
||||
currList: { type: String },
|
||||
|
||||
hasNarrative: { type: Boolean },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,15 +57,33 @@ class CuratedLists extends LitElement
|
||||
if (changedProperties.has("collInfo")) {
|
||||
this.doLoadCurated();
|
||||
}
|
||||
|
||||
if (changedProperties.has("currList") || changedProperties.has("query")) {
|
||||
this.sendChangeEvent({
|
||||
currList: this.currList,
|
||||
query: this.query
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async doLoadCurated() {
|
||||
const resp = await fetch(`${this.collInfo.apiPrefix}/curatedPages?offset=${this.offset}&count=1000`);
|
||||
this.hasNarrative = this.collInfo.desc || this.collInfo.numLists;
|
||||
|
||||
if (!this.collInfo.numLists) {
|
||||
this.total = 0;
|
||||
this.curatedPages = {};
|
||||
this.lists = [];
|
||||
this.currList = "pages";
|
||||
return;
|
||||
}
|
||||
|
||||
const resp = await fetch(`${this.collInfo.apiPrefix}/curatedPages?offset=${this.offset}&count=10000`);
|
||||
const json = await resp.json();
|
||||
|
||||
this.total = json.total;
|
||||
|
||||
this.curatedPages = {};
|
||||
this.lists = json.lists;
|
||||
|
||||
for (const curated of json.curatedPages) {
|
||||
if (!this.curatedPages[curated.list]) {
|
||||
@@ -59,80 +92,150 @@ class CuratedLists extends LitElement
|
||||
this.curatedPages[curated.list].push(curated);
|
||||
}
|
||||
|
||||
if (this.currList) {
|
||||
this.scrollToList(this.currList);
|
||||
}
|
||||
this.scrollToList();
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return wrapCss(css`
|
||||
:host {
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.columns {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-height: 100%
|
||||
align-items: flex-start;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
justify-self: stretch;
|
||||
}
|
||||
|
||||
.column {
|
||||
max-height: calc(100% - 70px);
|
||||
}
|
||||
|
||||
#content {
|
||||
margin-top: 10px;
|
||||
max-height: calc(100% - 90px);
|
||||
.column.main-content {
|
||||
margin: 12px 0px 0px 0px;
|
||||
padding: 0px;
|
||||
max-height: calc(100% - 0.75em);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: min-content;
|
||||
padding-left: 0.75em;
|
||||
}
|
||||
|
||||
.column.main-content.main-scroll {
|
||||
padding-right: 0.75em;
|
||||
word-break: break-all;
|
||||
}
|
||||
.column.main-content.pages {
|
||||
height: calc(100% - 1.2em);
|
||||
}
|
||||
|
||||
ul.menu-list a.is-active {
|
||||
background-color: #55be6f;
|
||||
}
|
||||
|
||||
:host {
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-top: 1em;
|
||||
@media screen and (min-width: 768px) {
|
||||
.columns {
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
.column.sidebar {
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.columns {
|
||||
position: relative;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.column.sidebar {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
.column.main-content {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
|
||||
border-top: 1px solid black;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: calc(100% - 150px - 0.75em);
|
||||
}
|
||||
|
||||
.menu {
|
||||
font-size: 0.80rem;
|
||||
}
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
render() {
|
||||
const currListNum = Number(this.currList);
|
||||
|
||||
return html`
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-one-third">
|
||||
<div class="menu" style="overflow-y: auto; height: 100%;">
|
||||
<div class="column sidebar is-one-fifth">
|
||||
<aside class="menu">
|
||||
<p class="menu-label">${this.collInfo.title}</p>
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
<a href="#list-0" data-list="0" class="${!this.currList ? 'is-active' : ''}"
|
||||
@click=${this.onClickScroll}>${this.collInfo.title}</a>
|
||||
<ul class="menu-list">${this.collInfo.lists.map(list => html`
|
||||
<li>
|
||||
<a @click=${this.onClickScroll} href="#list-${list.id}"
|
||||
data-list="${list.id}"
|
||||
class="${this.currList === list.id ? 'is-active' : ''}">${list.title}</a>
|
||||
</li>`)}
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="${this.currList === 'pages' ? 'is-active' : ''}" data-list="all-pages" @click=${this.onClickPages}>Page Search</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="content" @scroll=${this.onScroll} class="is-two-thirds column main-scroll">
|
||||
<section id="list-0" class="container">
|
||||
<h2 class="title is-3">${this.collInfo.title}</h2>
|
||||
${this.collInfo.desc ? unsafeHTML(marked(this.collInfo.desc)) : ''}
|
||||
</section>
|
||||
${this.renderContent()}
|
||||
${this.hasNarrative ? html`
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
<a href="#list-0" data-list="0" class="${currListNum === 0 ? 'is-active' : ''}"
|
||||
@click=${this.onClickScroll}>Narrative</a>
|
||||
<ul class="menu-list">${this.lists.map(list => html`
|
||||
<li>
|
||||
<a @click=${this.onClickScroll} href="#list-${list.id}"
|
||||
data-list="${list.id}"
|
||||
class="${currListNum === list.id ? 'is-active' : ''}">${list.title}</a>
|
||||
</li>`)}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>` : ``}
|
||||
</aside>
|
||||
</div>
|
||||
${this.renderPages()}
|
||||
${this.renderLists()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
renderPages() {
|
||||
return html`
|
||||
${this.collInfo.lists.map((list, i) => html`
|
||||
<div class="column main-content pages ${this.currList === "pages" ? '' : 'is-hidden'}">
|
||||
<wr-page-view
|
||||
.active="${this.active && this.currList === "pages"}"
|
||||
query="${this.query}" .collInfo="${this.collInfo}"
|
||||
@coll-tab-nav="${this.onCollTabNav}">
|
||||
</wr-page-view>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
onCollTabNav(event) {
|
||||
if (event.detail.data && event.detail.data.query !== undefined) {
|
||||
this.query = event.detail.data.query;
|
||||
}
|
||||
}
|
||||
|
||||
renderLists() {
|
||||
if (!this.hasNarrative) {
|
||||
return;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div @scroll=${this.onScroll} class="${this.currList !== "pages" ? '' : 'is-hidden'} column main-content main-scroll">
|
||||
<section id="list-0" class="">
|
||||
<h2 class="has-text-centered title is-3">${this.collInfo.title}</h2>
|
||||
${this.collInfo.desc ? unsafeHTML(marked(this.collInfo.desc)) : ''}
|
||||
</section>
|
||||
${this.lists.map((list, i) => html`
|
||||
<article id="list-${list.id}">
|
||||
<div class="content">
|
||||
<hr/>
|
||||
@@ -156,6 +259,7 @@ class CuratedLists extends LitElement
|
||||
</div>
|
||||
</article>
|
||||
`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -174,25 +278,37 @@ class CuratedLists extends LitElement
|
||||
this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {data}}));
|
||||
}
|
||||
|
||||
onClickScroll(event) {
|
||||
onClickPages(event) {
|
||||
event.preventDefault();
|
||||
this.scrollToList(event.currentTarget.getAttribute("data-list"));
|
||||
//this.pageView = true;
|
||||
this.currList = "pages";
|
||||
return false;
|
||||
}
|
||||
|
||||
scrollToList(id) {
|
||||
this.currList = Number(id);
|
||||
onClickScroll(event) {
|
||||
event.preventDefault();
|
||||
//this.pageView = false;
|
||||
this.currList = event.currentTarget.getAttribute("data-list");
|
||||
this.scrollToList();
|
||||
return false;
|
||||
}
|
||||
|
||||
scrollToList() {
|
||||
if (this.currList === "pages") {
|
||||
return;
|
||||
}
|
||||
|
||||
// lists are 1 based, 0 is header, 1 is first list
|
||||
if (this.currList > this.collInfo.lists.length) {
|
||||
this.currList = 0;
|
||||
if (Number(this.currList) > this.lists.length) {
|
||||
this.currList = "0";
|
||||
}
|
||||
|
||||
this.sendChangeEvent({currList: this.currList || undefined});
|
||||
|
||||
const opts = {behavior: "smooth", block: "nearest", inline: "nearest"};
|
||||
this.clickTime = new Date().getTime();
|
||||
this.renderRoot.getElementById("list-" + this.currList).scrollIntoView(opts);
|
||||
const curr = this.renderRoot.getElementById("list-" + this.currList);
|
||||
if (curr) {
|
||||
curr.scrollIntoView(opts);
|
||||
}
|
||||
}
|
||||
|
||||
onScroll(event) {
|
||||
@@ -219,8 +335,7 @@ class CuratedLists extends LitElement
|
||||
this.lastST = currST;
|
||||
if (next && next != curr) {
|
||||
if (next.id.startsWith("list-")) {
|
||||
this.currList = Number(next.id.slice(5));
|
||||
this.sendChangeEvent({currList: this.currList});
|
||||
this.currList = next.id.slice(5);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+38
-5
@@ -1,12 +1,18 @@
|
||||
import { LitElement, html, css } from 'lit-element';
|
||||
|
||||
import { registerSW } from './pageutils';
|
||||
|
||||
|
||||
var scriptSrc = document.currentScript && document.currentScript.src;
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
class EmbedRWP extends LitElement
|
||||
class Embed extends LitElement
|
||||
{
|
||||
constructor() {
|
||||
super();
|
||||
this.replayBase = "http://localhost:9990/";
|
||||
this.replayBase = window.location.origin + "/replay/";
|
||||
this.swName = __SW_NAME__;
|
||||
this.view = "replay";
|
||||
this.ts = "";
|
||||
this.url = "";
|
||||
@@ -14,6 +20,9 @@ class EmbedRWP extends LitElement
|
||||
this.coll = "";
|
||||
this.paramString = null;
|
||||
this.deepLink = false;
|
||||
this.swInited = false;
|
||||
|
||||
this.doRegister();
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
@@ -21,13 +30,17 @@ class EmbedRWP extends LitElement
|
||||
url: { type: String },
|
||||
ts: { type: String },
|
||||
source: { type: String },
|
||||
|
||||
replayBase: { type: String },
|
||||
swName: { type: String },
|
||||
|
||||
title: { type: String },
|
||||
|
||||
coll: { type: String },
|
||||
config: { type: String },
|
||||
|
||||
swInited: { type: Boolean },
|
||||
|
||||
paramString: { type: String },
|
||||
hashString: { type: String },
|
||||
|
||||
@@ -35,6 +48,12 @@ class EmbedRWP extends LitElement
|
||||
}
|
||||
}
|
||||
|
||||
async doRegister() {
|
||||
await registerSW(this.swName, this.replayBase);
|
||||
|
||||
this.swInited = true;
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
window.addEventListener("message", (event) => {
|
||||
const iframe = this.renderRoot.querySelector("iframe");
|
||||
@@ -115,16 +134,30 @@ class EmbedRWP extends LitElement
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${this.paramString && this.hashString ? html`
|
||||
<iframe src="${this.replayBase}?${this.paramString}#${this.hashString}" allow="autoplay *; fullscreen"></iframe>
|
||||
${this.paramString && this.hashString && this.swInited ? html`
|
||||
<iframe @load="${this.onLoad}" src="${this.replayBase}?${this.paramString}#${this.hashString}" allow="autoplay *; fullscreen"></iframe>
|
||||
` : html``}
|
||||
`;
|
||||
}
|
||||
|
||||
onLoad(event) {
|
||||
if (this.injected) {
|
||||
return;
|
||||
}
|
||||
if (event.target.contentWindow.customElements.get("replay-app-main")) {
|
||||
return;
|
||||
}
|
||||
const script = event.target.contentDocument.createElement("script");
|
||||
//const script = event.target.contentDocument.querySelector("script");
|
||||
script.src = scriptSrc;
|
||||
event.target.contentDocument.head.appendChild(script);
|
||||
this.injected = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
async function main() {
|
||||
customElements.define("replay-web-page", EmbedRWP);
|
||||
customElements.define("replay-web-page", Embed);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+12
-6
@@ -1,12 +1,20 @@
|
||||
import { LitElement, html, css } from 'lit-element';
|
||||
import { wrapCss, dbworker } from './misc';
|
||||
import { wrapCss } from './misc';
|
||||
|
||||
import { parseURLSchemeHostPath } from './pageutils';
|
||||
|
||||
let dbworker = null;
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
class Loader extends LitElement
|
||||
{
|
||||
static initDBWorker() {
|
||||
if (dbworker === null) {
|
||||
dbworker = new Worker(__SW_NAME__);
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.progress = 0;
|
||||
@@ -15,6 +23,8 @@ class Loader extends LitElement
|
||||
this.coll = "";
|
||||
this.state = "waiting";
|
||||
this.loadInfo = null;
|
||||
|
||||
Loader.initDBWorker();
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
@@ -92,11 +102,7 @@ You can select a file to upload from the main page by clicking the \'Choose File
|
||||
return;
|
||||
}
|
||||
|
||||
source = {
|
||||
sourceUrl: this.loadInfo.sourceUrl,
|
||||
loadUrl: this.loadInfo.loadUrl,
|
||||
name: this.loadInfo.name
|
||||
}
|
||||
source = this.loadInfo;
|
||||
break;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
+3
-3
@@ -1,3 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
import { LitElement, html, css, unsafeCSS } from 'lit-element';
|
||||
import { unsafeSVG } from 'lit-html/directives/unsafe-svg';
|
||||
import { styleMap } from 'lit-html/directives/style-map';
|
||||
@@ -15,8 +17,6 @@ function wrapCss(custom) {
|
||||
|
||||
const IS_APP = window.electron && window.electron.IS_APP || window.matchMedia('(display-mode: standalone)').matches;
|
||||
|
||||
const dbworker = new Worker(__SW_PATH__);
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
class FaIcon extends LitElement
|
||||
@@ -101,4 +101,4 @@ class AnimLogo extends FaIcon
|
||||
customElements.define("fa-icon", FaIcon);
|
||||
customElements.define("wr-anim-logo", AnimLogo);
|
||||
|
||||
export { wrapCss, IS_APP, dbworker, rwpLogo, FaIcon, AnimLogo };
|
||||
export { wrapCss, IS_APP, rwpLogo, FaIcon, AnimLogo };
|
||||
+316
-21
@@ -1,8 +1,16 @@
|
||||
"use strict";
|
||||
|
||||
import { LitElement, html, css } from 'lit-element';
|
||||
import { wrapCss } from './misc';
|
||||
|
||||
import { getTS } from './pageutils';
|
||||
|
||||
import FlexSearch from 'flexsearch';
|
||||
import "keyword-mark-element/lib/keyword-mark.js";
|
||||
//const FlexSearch = Object;
|
||||
|
||||
import fasSearch from '@fortawesome/fontawesome-free/svgs/solid/search.svg';
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
class Pages extends LitElement
|
||||
@@ -10,57 +18,339 @@ class Pages extends LitElement
|
||||
constructor() {
|
||||
super();
|
||||
this.pages = [];
|
||||
this.filteredPages = [];
|
||||
this.sortedPages = [];
|
||||
this.query = "";
|
||||
this.flex = null;
|
||||
this.newQuery = null;
|
||||
this.loading = false;
|
||||
|
||||
this.active = false;
|
||||
this.changeNeeded = false;
|
||||
}
|
||||
|
||||
static get sortKeys() {
|
||||
return [
|
||||
{
|
||||
"key": "title",
|
||||
"name": "Title"
|
||||
},
|
||||
{
|
||||
"key": "ts",
|
||||
"name": "Date"
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
active: { type: Boolean},
|
||||
|
||||
collInfo: { type: Object },
|
||||
pages: { type: Array }
|
||||
pages: { type: Array },
|
||||
filteredPages: { type: Array },
|
||||
sortedPages: { type: Array },
|
||||
query: { type: String },
|
||||
loading: { type: Boolean },
|
||||
}
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
//this._ival = window.setInterval(() => {
|
||||
|
||||
//}, 10000);
|
||||
}
|
||||
|
||||
_timedUpdate() {
|
||||
if (this.newQuery !== null) {
|
||||
this.query = this.newQuery;
|
||||
this.newQuery = null;
|
||||
this.filter();
|
||||
}
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
if (changedProperties.has("collInfo")) {
|
||||
this.doUpdate();
|
||||
} else if (changedProperties.has("query")) {
|
||||
this.filter();
|
||||
}
|
||||
if (changedProperties.has("active") && this.active) {
|
||||
if (this.changeNeeded) {
|
||||
this.filter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async doUpdate() {
|
||||
const result = await fetch(`${this.collInfo.apiPrefix}/pages`);
|
||||
const json = await result.json();
|
||||
console.log(json);
|
||||
this.pages = json.pages || [];
|
||||
|
||||
this.updateTextSearch();
|
||||
this.filter();
|
||||
}
|
||||
|
||||
onChangeQuery(event) {
|
||||
this.newQuery = event.currentTarget.value;
|
||||
//this.loading = true;
|
||||
if (this._ival) {
|
||||
window.clearTimeout(this._ival);
|
||||
}
|
||||
this._ival = window.setTimeout(() => this._timedUpdate(), 250);
|
||||
}
|
||||
|
||||
async filter() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
if (!this.active) {
|
||||
this.changeNeeded = true;
|
||||
}
|
||||
this.loading = true;
|
||||
if (this.flex && this.query) {
|
||||
const result = await this.flex.search(this.query);
|
||||
this.filteredPages = result;
|
||||
} else {
|
||||
this.filteredPages = this.pages;
|
||||
}
|
||||
this.loading = false;
|
||||
this.changeNeeded = false;
|
||||
const data = {query: this.query};
|
||||
this.sendChangeEvent(data);
|
||||
}
|
||||
|
||||
sendChangeEvent(data) {
|
||||
this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {data}}));
|
||||
}
|
||||
|
||||
updateTextSearch() {
|
||||
this.flex = new FlexSearch({
|
||||
doc: {
|
||||
id: "id",
|
||||
field: ["url", "title", "text"],
|
||||
},
|
||||
async: true
|
||||
});
|
||||
|
||||
this.flex.add(this.pages);
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return wrapCss(css`
|
||||
.main.section {
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 0.75rem 0.75rem 0;
|
||||
}
|
||||
|
||||
.scroller {
|
||||
overflow-y: auto;
|
||||
padding: 3em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-block:last-child {
|
||||
border-bottom: 1px solid rgb(237, 237, 237);
|
||||
}
|
||||
.panel-block {
|
||||
border-left: 1px solid rgb(237, 237, 237);
|
||||
border-right: 1px solid rgb(237, 237, 237);
|
||||
}
|
||||
.panel-block.status {
|
||||
justify-content: space-between;
|
||||
}
|
||||
nav.panel {
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
.light-blue {
|
||||
background-color: #43daff;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="main section">
|
||||
${this.pages.map((p) => html`
|
||||
<div class="columns">
|
||||
<div class="column is-one-fifth">
|
||||
${new Date(p.date).toLocaleString()}
|
||||
</div>
|
||||
<div class="column">
|
||||
<a @click="${this.onReplay}" data-url="${p.url}" data-ts="${getTS(p.date)}" href="#">
|
||||
<p class="is-size-6 has-text-weight-bold has-text-link">${p.title || p.url}</p>
|
||||
<p class="has-text-dark">${p.url}</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>`
|
||||
)}
|
||||
<nav class="panel">
|
||||
<div class="panel-heading light-blue">Pages for: ${this.collInfo.title}</div>
|
||||
<div class="panel-block">
|
||||
<div class="control has-icons-left ${this.loading ? 'is-loading' : ''}">
|
||||
<input type="text" class="input" @input="${this.onChangeQuery}" .value="${this.query}" type="text"
|
||||
placeholder="Search by Page URL, Title or Text">
|
||||
<span class="icon is-left"><fa-icon .svg="${fasSearch}"/></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-block status">
|
||||
<span class="num-results">${this.formatResults()}</span>
|
||||
<wr-sorter id="pages"
|
||||
.sortKeys="${Pages.sortKeys}"
|
||||
.data="${this.filteredPages}"
|
||||
@sort-changed="${(e) => this.sortedPages = e.detail.sortedData}"
|
||||
class="${this.filteredPages.length ? '' : 'is-hidden'}">
|
||||
</wr-sorter>
|
||||
</div>
|
||||
<div class="scroller" @scroll="${this.onScroll}">
|
||||
${this.sortedPages.map((p) => html`
|
||||
<div class="panel-block"><wr-page-entry query="${this.query}" .page="${p}"></wr-page-entry></div>`)}
|
||||
</div>
|
||||
</nav>
|
||||
`;
|
||||
}
|
||||
|
||||
formatResults() {
|
||||
if (!this.collInfo || !this.collInfo.numPages) {
|
||||
return html`No Pages defined this archive. Check out <a href="#view=resources">Page Resources</a> to search by URL.`;
|
||||
}
|
||||
|
||||
if (this.loading) {
|
||||
return "Searching...";
|
||||
}
|
||||
|
||||
if (!this.sortedPages.length) {
|
||||
return "No Pages Found."
|
||||
} else if (this.sortedPages.length === 1) {
|
||||
return "1 Page";
|
||||
} else {
|
||||
return `${this.sortedPages.length} Pages`;
|
||||
}
|
||||
}
|
||||
|
||||
onScroll(event) {
|
||||
const element = event.currentTarget;
|
||||
const diff = (element.scrollHeight - element.scrollTop) - element.clientHeight;
|
||||
if (diff < 40) {
|
||||
const sorter = this.renderRoot.querySelector("wr-sorter");
|
||||
if (sorter) {
|
||||
sorter.getMore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
class PageEntry extends LitElement
|
||||
{
|
||||
constructor() {
|
||||
super();
|
||||
this.query = "";
|
||||
this.textSnippet = "";
|
||||
this.page = null;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
query: { type: String },
|
||||
textSnippet: { type: String },
|
||||
page: { type: Object }
|
||||
}
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return wrapCss(css`
|
||||
:host {
|
||||
min-height: min-content;
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
a {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.col-date div {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.columns {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
if (changedProperties.has("page") || changedProperties.has("query")) {
|
||||
this.updateSnippet();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const p = this.page;
|
||||
const date = new Date(p.ts);
|
||||
|
||||
return html`
|
||||
<div class="columns">
|
||||
<div class="column col-date is-one-fifth">
|
||||
<div>${date.toLocaleDateString()}</div>
|
||||
<div>${date.toLocaleTimeString()}</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a @click="${this.onReplay}" data-url="${p.url}" data-ts="${getTS(date.toISOString())}" href="#">
|
||||
<p class="is-size-6 has-text-weight-bold has-text-link text">
|
||||
<keyword-mark keywords="${this.query}">${p.title || p.url}</keyword-mark>
|
||||
</p>
|
||||
<p class="has-text-dark text"><keyword-mark keywords="${this.query}">${p.url}</keyword-mark></p>
|
||||
</a>
|
||||
${this.textSnippet ? html`
|
||||
<div class="text"><keyword-mark keywords="${this.query}">${this.textSnippet}</keyword-mark></div>` : html``}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
updateSnippet() {
|
||||
const oldVal = this.textSnippet;
|
||||
|
||||
if (!this.query || !this.page.text) {
|
||||
this.textSnippet = null;
|
||||
this.requestUpdate('textSnippet', oldVal);
|
||||
return;
|
||||
}
|
||||
|
||||
let textContent = this.page.text;
|
||||
let query = this.query;
|
||||
|
||||
let inx = textContent.indexOf(this.query);
|
||||
|
||||
if (inx < 0) {
|
||||
let textLower = textContent.toLowerCase();
|
||||
let queryLower = query.toLowerCase();
|
||||
|
||||
inx = textLower.indexOf(queryLower);
|
||||
|
||||
if (inx < 0) {
|
||||
this.textSnippet = null;
|
||||
this.requestUpdate('textSnippet', oldVal);
|
||||
return;
|
||||
}
|
||||
|
||||
textContent = textLower;
|
||||
query = queryLower;
|
||||
}
|
||||
|
||||
let lastInx = textContent.lastIndexOf(query);
|
||||
|
||||
inx = Math.max(inx - 100, 0);
|
||||
lastInx = Math.min(lastInx + 200, textContent.length);
|
||||
|
||||
if (inx === 0 && lastInx === textContent.length) {
|
||||
this.textSnippet = textContent;
|
||||
} else {
|
||||
this.textSnippet = "..." + textContent.slice(inx, lastInx) + "...";
|
||||
}
|
||||
|
||||
this.requestUpdate('textSnippet', oldVal);
|
||||
}
|
||||
|
||||
onReplay(event) {
|
||||
event.preventDefault();
|
||||
const data = {
|
||||
@@ -68,11 +358,16 @@ class Pages extends LitElement
|
||||
ts: event.currentTarget.getAttribute("data-ts"),
|
||||
view: "replay"
|
||||
};
|
||||
this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {data}}));
|
||||
this.sendChangeEvent(data);
|
||||
return false;
|
||||
}
|
||||
|
||||
sendChangeEvent(data) {
|
||||
this.dispatchEvent(new CustomEvent("coll-tab-nav", {bubbles: true, composed: true, detail: {data}}));
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("wr-coll-pages", Pages);
|
||||
customElements.define("wr-page-view", Pages);
|
||||
customElements.define("wr-page-entry", PageEntry);
|
||||
|
||||
export { Pages };
|
||||
export { Pages, PageEntry };
|
||||
+27
-4
@@ -1,3 +1,29 @@
|
||||
import { register } from 'register-service-worker';
|
||||
|
||||
async function registerSW(name = "sw.js", scope = "./") {
|
||||
let resolve, reject;
|
||||
|
||||
const p = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
register(scope + name, {
|
||||
registrationOptions: { scope },
|
||||
registered (reg) {
|
||||
console.log('Service worker is registered');
|
||||
resolve();
|
||||
},
|
||||
|
||||
error (error) {
|
||||
console.error('Error during service worker registration:', error);
|
||||
reject();
|
||||
}
|
||||
});
|
||||
|
||||
await p;
|
||||
}
|
||||
|
||||
async function digestMessage(message, hashtype) {
|
||||
const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
|
||||
const hashBuffer = await crypto.subtle.digest(hashtype, msgUint8); // hash the message
|
||||
@@ -71,7 +97,4 @@ function parseURLSchemeHostPath(url) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export { digestMessage, tsToDate, getTS, sourceToId, parseURLSchemeHostPath };
|
||||
export { digestMessage, tsToDate, getTS, sourceToId, parseURLSchemeHostPath, registerSW };
|
||||
|
||||
+29
-6
@@ -94,7 +94,7 @@ class Replay extends LitElement
|
||||
}
|
||||
|
||||
if (this.iframeUrl && changedProperties.has("iframeUrl")) {
|
||||
this.isLoading = true;
|
||||
this.waitForLoad();
|
||||
}
|
||||
|
||||
if (this.replayUrl && changedProperties.has("replayUrl")) {
|
||||
@@ -123,7 +123,7 @@ class Replay extends LitElement
|
||||
this.replayTS = event.data.ts;
|
||||
this.replayUrl = event.data.url;
|
||||
this.title = event.data.title || this.title;
|
||||
this.isLoading = false;
|
||||
this.clearLoading();
|
||||
|
||||
if (event.data.icons) {
|
||||
const icons = event.data.icons;
|
||||
@@ -155,11 +155,30 @@ class Replay extends LitElement
|
||||
const iframe = this.renderRoot.querySelector("iframe");
|
||||
|
||||
if (iframe) {
|
||||
this.isLoading = true;
|
||||
this.waitForLoad();
|
||||
iframe.contentWindow.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
waitForLoad() {
|
||||
this.isLoading = true;
|
||||
this._loadPoll = window.setInterval(() => {
|
||||
const iframe = this.renderRoot.querySelector("iframe");
|
||||
if (!iframe || !iframe.contentDocument || !iframe.contentWindow ||
|
||||
(iframe.contentDocument.readyState === "complete" && !iframe.contentWindow._WBWombat)) {
|
||||
this.clearLoading();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
clearLoading() {
|
||||
this.isLoading = false;
|
||||
if (this._loadPoll) {
|
||||
window.clearInterval(this._loadPoll);
|
||||
this._loadPoll = null;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return wrapCss(css`
|
||||
host: {
|
||||
@@ -206,6 +225,7 @@ class Replay extends LitElement
|
||||
|
||||
.intro-panel .panel-block {
|
||||
padding: 1.0em;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#datetime {
|
||||
@@ -239,13 +259,13 @@ class Replay extends LitElement
|
||||
<fa-icon size="1.0em" class="has-text-grey" .svg="${this.isFullscreen ? fasUnfullscreen : fasFullscreen}"></fa-icon>
|
||||
</span>
|
||||
</a>
|
||||
<a id="refresh" class="button is-borderless ${this.isLoading ? 'is-loading' : ''}" @click="${this.onRefresh}">
|
||||
<button id="refresh" class="button is-borderless ${this.isLoading ? 'is-loading' : ''}" @click="${this.onRefresh}">
|
||||
<span class="icon is-small">
|
||||
${!this.isLoading ? html`
|
||||
<fa-icon size="1.0em" class="has-text-grey" .svg="${fasRefresh}"></fa-icon>
|
||||
` : ``}
|
||||
</span>
|
||||
</a>
|
||||
</button>
|
||||
<p class="control is-expanded">
|
||||
<input id="url" class="input" type="text" .value="${this.replayUrl}" placeholder="https://... Enter a URL to replay from the archive here">
|
||||
</p>
|
||||
@@ -261,7 +281,10 @@ class Replay extends LitElement
|
||||
` : html`
|
||||
<nav class="panel intro-panel">
|
||||
<p class="panel-heading">Replay Web Page</p>
|
||||
<p class="panel-block">Enter a URL above to replay it from the web archive!</p>
|
||||
<div class="panel-block">
|
||||
<p>Enter a URL above to replay it from the web archive!</p>
|
||||
<p>(Check out the <a href="#view=pages">Pages</a> or <a href="#view=resources">Page Resources</a> to find URLs in this archive.)</p>
|
||||
</div>
|
||||
</nav>
|
||||
`}
|
||||
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import { LitElement, html, css } from 'lit-element';
|
||||
import { wrapCss } from './misc';
|
||||
|
||||
import fasSortDown from '@fortawesome/fontawesome-free/svgs/solid/sort-down.svg';
|
||||
import fasSortUp from '@fortawesome/fontawesome-free/svgs/solid/sort-up.svg';
|
||||
|
||||
const DEFAULT_RESULTS = 100;
|
||||
|
||||
// ===========================================================================
|
||||
class Sorter extends LitElement
|
||||
{
|
||||
constructor() {
|
||||
super();
|
||||
this.sortedData = [];
|
||||
this.data = [];
|
||||
|
||||
this.numResults = DEFAULT_RESULTS;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
id: { type: String },
|
||||
defaultKey: {type: String },
|
||||
|
||||
//numResults: {type: Number},
|
||||
|
||||
data: { type: Array },
|
||||
sortedData: { type: Array },
|
||||
|
||||
sortKey: { type: String },
|
||||
sortDesc: { type: Boolean },
|
||||
}
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.sortKey = localStorage.getItem(`${this.id}:sortKey`) || this.defaultKey;
|
||||
this.sortDesc = localStorage.getItem(`${this.id}:sortDesc`) === "1";
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
const keyChanged = changedProperties.has("sortKey");
|
||||
const descChanged = changedProperties.has("sortDesc");
|
||||
const dataChanged = changedProperties.has("data");
|
||||
const numResultsChanged = changedProperties.has("numResults");
|
||||
|
||||
if (keyChanged) {
|
||||
localStorage.setItem(`${this.id}:sortKey`, this.sortKey);
|
||||
}
|
||||
if (descChanged) {
|
||||
localStorage.setItem(`${this.id}:sortDesc`, this.sortDesc ? "1" : "0");
|
||||
}
|
||||
if (keyChanged || descChanged || dataChanged) {
|
||||
this.sortData();
|
||||
}
|
||||
if (dataChanged) {
|
||||
|
||||
}
|
||||
//if (numResultsChanged && !dataChanged) {
|
||||
//this.sendSortChanged();
|
||||
//}
|
||||
}
|
||||
|
||||
sortData() {
|
||||
this.sortedData = [...this.data];
|
||||
this.numResults = DEFAULT_RESULTS;
|
||||
|
||||
this.sortedData.sort((first, second) => {
|
||||
if (first[this.sortKey] === second[this.sortKey]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (this.sortDesc == (first[this.sortKey] < second[this.sortKey])) ? 1 : -1;
|
||||
});
|
||||
|
||||
this.sendSortChanged();
|
||||
}
|
||||
|
||||
sendSortChanged() {
|
||||
const detail = {sortedData: this.sortedData.slice(0, this.numResults)};
|
||||
this.dispatchEvent(new CustomEvent("sort-changed", {detail}));
|
||||
}
|
||||
|
||||
getMore(more = 100) {
|
||||
if (this.numResults >= this.sortedData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.numResults += more;
|
||||
this.sendSortChanged();
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return wrapCss(css`
|
||||
:host {
|
||||
min-width: 100px;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="select is-small">
|
||||
<select id="sort-select" @change=${(e) => this.sortKey = e.currentTarget.value}>
|
||||
${this.sortKeys.map((sort) => html`
|
||||
<option value="${sort.key}" ?selected="${sort.key === this.sortKey}">Sort By: ${sort.name}
|
||||
</option>
|
||||
`)}
|
||||
</select>
|
||||
</div>
|
||||
<button @click=${(e) => this.sortDesc = !this.sortDesc} class="button is-small">
|
||||
<span>Order:</span>
|
||||
<span class="icon"><fa-icon .svg=${this.sortDesc ? fasSortUp : fasSortDown}></span>
|
||||
</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("wr-sorter", Sorter);
|
||||
|
||||
export { Sorter };
|
||||
@@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
|
||||
import INDEX_HTML from '../index.html';
|
||||
|
||||
if (self.registration) {
|
||||
const staticData = new Map();
|
||||
|
||||
const prefix = self.registration.scope;
|
||||
|
||||
staticData.set(prefix, {type: "text/html", content: INDEX_HTML});
|
||||
staticData.set(prefix + "index.html", {type: "text/html", content: INDEX_HTML});
|
||||
//staticData.set(prefix + "ui.js", {type: "application/javascript", content: UI_JS});
|
||||
|
||||
const { SWReplay } = require('wabac/src/swmain');
|
||||
self.sw = new SWReplay(staticData);
|
||||
|
||||
} else if (self.postMessage) {
|
||||
// Inited as Web Worker
|
||||
const { WorkerLoader } = require('wabac/src/loaders');
|
||||
const loader = new WorkerLoader(self);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { App } from './appmain';
|
||||
import { Chooser } from './chooser';
|
||||
import { CollIndex } from './coll-index';
|
||||
import { Coll } from './coll';
|
||||
import { CuratedList } from './curated-lists';
|
||||
@@ -6,34 +7,7 @@ import { GDrive } from './gdrive';
|
||||
import { Loader } from './loader';
|
||||
import { Pages } from './pages';
|
||||
import { Replay } from './replay';
|
||||
import { Sorter } from './sorter';
|
||||
import { URLResources } from './url-resources';
|
||||
import { Embed } from './embed';
|
||||
|
||||
import { register } from 'register-service-worker';
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
// ===========================================================================
|
||||
|
||||
async function main(url) {
|
||||
let resolve, reject;
|
||||
const p = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
register(url, {
|
||||
registrationOptions: { scope: './' },
|
||||
ready (registration) {
|
||||
console.log('Service worker is active.');
|
||||
resolve();
|
||||
},
|
||||
error (error) {
|
||||
console.error('Error during service worker registration:', error);
|
||||
reject();
|
||||
}
|
||||
});
|
||||
|
||||
await p;
|
||||
}
|
||||
|
||||
main(__SW_PATH__);
|
||||
|
||||
+26
-13
@@ -1,6 +1,8 @@
|
||||
import { LitElement, html, css } from 'lit-element';
|
||||
import { wrapCss } from './misc';
|
||||
|
||||
import fasSearch from '@fortawesome/fontawesome-free/svgs/solid/search.svg';
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
class URLResources extends LitElement
|
||||
@@ -26,13 +28,15 @@ class URLResources extends LitElement
|
||||
this.collInfo = null;
|
||||
|
||||
this.currMime = "";
|
||||
this.urlSearch = "";
|
||||
this.query = "";
|
||||
this.urlSearchType = "";
|
||||
|
||||
this.filteredResults = [];
|
||||
|
||||
this.results = [];
|
||||
|
||||
this.newQuery = null;
|
||||
|
||||
this.tryMore = false;
|
||||
this.loading = false;
|
||||
|
||||
@@ -44,7 +48,7 @@ class URLResources extends LitElement
|
||||
return {
|
||||
collInfo: { type: Object },
|
||||
currMime: { type: String },
|
||||
urlSearch: { type: String },
|
||||
query: { type: String },
|
||||
urlSearchType: { type: String },
|
||||
filteredResults: { type: Array },
|
||||
loading: { type: Boolean },
|
||||
@@ -58,14 +62,21 @@ class URLResources extends LitElement
|
||||
//this.doLoadResources();
|
||||
}
|
||||
|
||||
_timedUpdate() {
|
||||
if (this.newQuery !== null) {
|
||||
this.query = this.newQuery;
|
||||
this.newQuery = null;
|
||||
}
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
if (changedProperties.has("urlSearch") ||
|
||||
if (changedProperties.has("query") ||
|
||||
changedProperties.has("urlSearchType") ||
|
||||
changedProperties.has("currMime")) {
|
||||
|
||||
this.doLoadResources();
|
||||
const data = {
|
||||
urlSearch: this.urlSearch,
|
||||
query: this.query,
|
||||
urlSearchType: this.urlSearchType,
|
||||
currMime: this.currMime
|
||||
};
|
||||
@@ -90,7 +101,7 @@ class URLResources extends LitElement
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
let url = (this.urlSearchType !== "" ? this.urlSearch : "");
|
||||
let url = (this.urlSearchType !== "" ? this.query : "");
|
||||
const prefix = url && this.urlSearchType === "prefix" ? 1 : 0;
|
||||
|
||||
// optimization: if not starting with http, likely won't have a match here, so just add https://
|
||||
@@ -139,22 +150,23 @@ class URLResources extends LitElement
|
||||
|
||||
onChangeTypeSearch(event) {
|
||||
this.currMime = event.currentTarget.value;
|
||||
//this.doLoadResources();
|
||||
}
|
||||
|
||||
onChangeUrlSearch(event) {
|
||||
this.urlSearch = event.currentTarget.value;
|
||||
//this.doLoadResources();
|
||||
onChangeQuery(event) {
|
||||
this.newQuery = event.currentTarget.value;
|
||||
if (this._ival) {
|
||||
window.clearTimeout(this._ival);
|
||||
}
|
||||
this._ival = window.setTimeout(() => this._timedUpdate(), 250);
|
||||
}
|
||||
|
||||
onClickUrlType(event) {
|
||||
this.urlSearchType = event.currentTarget.value;
|
||||
//this.doLoadResources();
|
||||
}
|
||||
|
||||
filter() {
|
||||
const filteredResults = [];
|
||||
const filterText = (this.urlSearchType === "" ? this.urlSearch : "");
|
||||
const filterText = (this.urlSearchType === "" ? this.query : "");
|
||||
for (const result of this.results) {
|
||||
if (!filterText || result.url.indexOf(filterText) >= 0) {
|
||||
filteredResults.push(result);
|
||||
@@ -270,8 +282,9 @@ class URLResources extends LitElement
|
||||
</select>
|
||||
</div>
|
||||
<div class="field flex-auto">
|
||||
<div class="control ${this.loading ? 'is-loading' : ''}">
|
||||
<input type="text" class="input" @input="${this.onChangeUrlSearch}" value="${this.urlSearch}" type="text" placeholder="Enter URL search here">
|
||||
<div class="control has-icons-left ${this.loading ? 'is-loading' : ''}">
|
||||
<input type="text" class="input" @input="${this.onChangeQuery}" .value="${this.query}" type="text" placeholder="Enter URL to Search">
|
||||
<span class="icon is-left"><fa-icon .svg="${fasSearch}"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user