mirror of
https://github.com/webrecorder/pywb.git
synced 2025-03-15 00:03:28 +01:00
initial pass on integrating vue calendar + banner from @vanecat
- use rollup to build vue ui, build at vue/vueui.js - calendar page renders via both /*/ and /*? queries - banner support for framed mode only for now
This commit is contained in:
parent
96de80f83e
commit
f8a73c0b03
@ -8,6 +8,8 @@ collections:
|
||||
index_paths: ./sample_archive/cdx/
|
||||
archive_paths: ./sample_archive/warcs/
|
||||
|
||||
ukwa: cdx+https://www.webarchive.org.uk/wayback/archive/cdx
|
||||
|
||||
# Settings for each collection
|
||||
use_js_obj_proxy: true
|
||||
|
||||
|
BIN
pywb/static/calendar-icon.png
Normal file
BIN
pywb/static/calendar-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
91
pywb/static/js/cdxquery.js
Normal file
91
pywb/static/js/cdxquery.js
Normal file
@ -0,0 +1,91 @@
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
class CDXLoader {
|
||||
constructor(staticPrefix, url, prefix, timestamp) {
|
||||
this.opts = {};
|
||||
this.prefix = prefix;
|
||||
|
||||
this.isReplay = (timestamp !== undefined);
|
||||
|
||||
if (this.isReplay) {
|
||||
window.WBBanner = new VueBannerWrapper(this);
|
||||
}
|
||||
|
||||
this.queryWorker = new Worker(staticPrefix + '/queryWorker.js');
|
||||
|
||||
// query form *?=url...
|
||||
if (window.location.href.indexOf("*?") > 0) {
|
||||
this.queryURL = window.location.href.replace("*?", "cdx?") + "&output=json";
|
||||
url = new URL(this.queryURL).searchParams.get("url");
|
||||
|
||||
// otherwise, traditional calendar form /*/<url>
|
||||
} else if (url) {
|
||||
const params = new URLSearchParams();
|
||||
params.set("url", url);
|
||||
params.set("output", "json");
|
||||
this.queryURL = prefix + "cdx?" + params.toString();
|
||||
|
||||
// otherwise, an error since no URL
|
||||
} else {
|
||||
throw new Error("No query URL specified");
|
||||
}
|
||||
|
||||
this.opts.initialView = {url, timestamp};
|
||||
|
||||
// TODO: make configurable
|
||||
this.opts.logoImg = staticPrefix + "/pywb-logo-sm.png";
|
||||
|
||||
const cdxList = [];
|
||||
|
||||
this.queryWorker.addEventListener("message", (event) => {
|
||||
const data = event.data;
|
||||
switch (data.type) {
|
||||
case "cdxRecord":
|
||||
cdxList.push(data.record);
|
||||
break;
|
||||
|
||||
case "finished":
|
||||
PywbVue.init(cdxList, this.opts, (snapshot) => this.loadSnapshot(snapshot));
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
init() {
|
||||
this.queryWorker.postMessage({
|
||||
type: 'query',
|
||||
queryURL: this.queryURL
|
||||
});
|
||||
}
|
||||
|
||||
loadSnapshot(snapshot) {
|
||||
if (!isReplay) {
|
||||
window.location.href = this.prefix + snapshot.id + "/" + snapshot.url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
class VueBannerWrapper
|
||||
{
|
||||
constructor() {
|
||||
this.loading = true;
|
||||
this.lastUrl = null;
|
||||
}
|
||||
|
||||
init() {}
|
||||
|
||||
stillIndicatesLoading() {
|
||||
return this.loading;
|
||||
}
|
||||
|
||||
updateCaptureInfo(url, ts, is_live) {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
onMessage(event) {
|
||||
console.log(event);
|
||||
}
|
||||
}
|
BIN
pywb/static/pywb-logo-sm.png
Normal file
BIN
pywb/static/pywb-logo-sm.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
13608
pywb/static/vue/vueui.js
Normal file
13608
pywb/static/vue/vueui.js
Normal file
File diff suppressed because one or more lines are too long
@ -21,9 +21,28 @@ window.banner_info = {
|
||||
};
|
||||
</script>
|
||||
|
||||
<link rel='stylesheet' href='{{ static_prefix }}/default_banner.css'/>
|
||||
|
||||
{% if is_framed or old_banner %}
|
||||
<!-- default banner, create through js -->
|
||||
<script src='{{ static_prefix }}/default_banner.js'> </script>
|
||||
<link rel='stylesheet' href='{{ static_prefix }}/default_banner.css'/>
|
||||
|
||||
{% else %}
|
||||
|
||||
<script src="{{ static_prefix }}/vue/vueui.js"></script>
|
||||
<script src="{{ static_prefix }}/js/cdxquery.js"></script>
|
||||
<script>
|
||||
var loader = new CDXLoader("{{ static_prefix }}", "{{ url }}", "{{ wb_prefix }}", "{{ timestamp }}");
|
||||
loader.init();
|
||||
</script>
|
||||
|
||||
{% if not old_banner %}
|
||||
<div id="app" style="width: 100%; height: 200px"></div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endautoescape %}
|
||||
{% endif %}
|
||||
|
@ -6,22 +6,24 @@
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
|
||||
{% if table_ui %}
|
||||
<link rel="stylesheet" href="{{ static_prefix }}/css/query.css">
|
||||
<script src="{{ static_prefix }}/js/url-polyfill.min.js"></script>
|
||||
<script src="{{ static_prefix }}/query.js"></script>
|
||||
{% else %}
|
||||
|
||||
<script src="{{ static_prefix }}/vue/vueui.js"></script>
|
||||
<script src="{{ static_prefix }}/js/cdxquery.js"></script>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block body %}
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<h4 class="display-4 text-center text-sm-left p-0">{{ _('Search Results') }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center text-center text-sm-left mt-1" id="display-query-type-info"></div>
|
||||
</div>
|
||||
<div class="container mt-3 q-display" id="captures"></div>
|
||||
|
||||
<script>
|
||||
var text = {
|
||||
months: {
|
||||
@ -46,7 +48,20 @@
|
||||
dateTime: "{{ _('Date Time: ') }}",
|
||||
};
|
||||
|
||||
{% if table_ui %}
|
||||
var renderCal = new RenderCalendar({ prefix: "{{ prefix }}", staticPrefix: "{{ static_prefix }}", text: text });
|
||||
renderCal.init();
|
||||
|
||||
{% else %}
|
||||
var loader = new CDXLoader("{{ static_prefix }}", "{{ url }}", "{{ prefix }}");
|
||||
loader.init();
|
||||
|
||||
{% endif %}
|
||||
|
||||
</script>
|
||||
|
||||
{% if not table_ui %}
|
||||
<div id="app" style="width: 100%; height: 100%"></div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
39
pywb/vueui/.eslintrc.js
Normal file
39
pywb/vueui/.eslintrc.js
Normal file
@ -0,0 +1,39 @@
|
||||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/essential"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"vue"
|
||||
],
|
||||
"rules": {
|
||||
"no-restricted-globals": [
|
||||
2,
|
||||
"event", "error"
|
||||
],
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"double"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
};
|
21
pywb/vueui/package.json
Normal file
21
pywb/vueui/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "typescript",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"lint": "eslint ./src/*.js ./src/*.vue ./src/components/*.vue"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^2.6.11",
|
||||
"vue-template-compiler": "^2.6.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-node-resolve": "^13.0.4",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^7.17.0",
|
||||
"rollup": "^2.10.9",
|
||||
"rollup-plugin-css-only": "^3.1.0",
|
||||
"rollup-plugin-vue": "^5.0.0"
|
||||
}
|
||||
}
|
20
pywb/vueui/rollup.config.js
Normal file
20
pywb/vueui/rollup.config.js
Normal file
@ -0,0 +1,20 @@
|
||||
import vue from "rollup-plugin-vue";
|
||||
import css from "rollup-plugin-css-only";
|
||||
import { nodeResolve } from "@rollup/plugin-node-resolve";
|
||||
|
||||
export default [
|
||||
{
|
||||
input: "src/index.js",
|
||||
output: {
|
||||
file: "../static/vue/vueui.js",
|
||||
sourcemap: "inline",
|
||||
name: "PywbVue",
|
||||
format: "iife",
|
||||
},
|
||||
plugins: [
|
||||
vue({css: true, compileTemplate: true}),
|
||||
css(),
|
||||
nodeResolve({browser: true})
|
||||
],
|
||||
},
|
||||
];
|
172
pywb/vueui/src/App.vue
Normal file
172
pywb/vueui/src/App.vue
Normal file
@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div class="app" data-app="webrecorder-replay-app">
|
||||
<div class="short-nav">
|
||||
<div class="first-line">
|
||||
<div class="logo"><img :src="config.logoImg" /></div>
|
||||
<div class="url-and-timeline">
|
||||
<div class="url">
|
||||
<strong>{{config.title}}</strong> ({{snapshots.length}} captures: {{snapshots[0].getDateFormatted()}} - {{snapshots[snapshots.length-1].getDateFormatted()}})<br/>
|
||||
<TimelineSummary
|
||||
v-if="currentPeriod"
|
||||
:period="currentPeriod"
|
||||
@goto-period="gotoPeriod"
|
||||
></TimelineSummary>
|
||||
<span class="full-view-toggle" :class="{expanded: showFullView}" @click="showFullView = !showFullView">
|
||||
<template v-if="!showFullView"><span class="detail">show year calendar</span></template><template v-else><span class="detail">hide year calendar</span></template>
|
||||
<img src="/static/calendar-icon.png" />
|
||||
</span>
|
||||
</div>
|
||||
<Timeline
|
||||
v-if="currentPeriod"
|
||||
:period="currentPeriod"
|
||||
:highlight="timelineHighlight"
|
||||
@goto-period="gotoPeriod"
|
||||
></Timeline>
|
||||
</div>
|
||||
</div>
|
||||
<div class="second-line" v-if="currentSnapshot && !showFullView">
|
||||
<h2>Showing Capture from {{currentSnapshot.getTimeDateFormatted()}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CalendarYear v-if="showFullView"
|
||||
:period="currentPeriod"
|
||||
@goto-period="gotoPeriod">
|
||||
</CalendarYear>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Timeline from "./components/Timeline.vue";
|
||||
import TimelineSummary from "./components/Summary.vue";
|
||||
import CalendarYear from "./components/CalendarYear.vue";
|
||||
|
||||
import { PywbSnapshot } from "./model.js";
|
||||
|
||||
export default {
|
||||
name: "PywbReplayApp",
|
||||
//el: '[data-app="webrecorder-replay-app"]',
|
||||
data: function() {
|
||||
return {
|
||||
snapshots: [],
|
||||
currentPeriod: null,
|
||||
currentSnapshot: null,
|
||||
msgs: [],
|
||||
showFullView: false,
|
||||
config: {
|
||||
title: "",
|
||||
initialView: {}
|
||||
},
|
||||
timelineHighlight: false
|
||||
};
|
||||
},
|
||||
components: {Timeline, TimelineSummary, CalendarYear},
|
||||
mounted: function() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
gotoPeriod: function(newPeriod, initiator) {
|
||||
if (this.timelineHighlight) {
|
||||
setTimeout((() => {
|
||||
this.timelineHighlight=false;
|
||||
}).bind(this), 3000);
|
||||
}
|
||||
if (newPeriod.snapshot) {
|
||||
this.gotoSnapshot(newPeriod.snapshot);
|
||||
} else {
|
||||
this.currentPeriod = newPeriod;
|
||||
}
|
||||
},
|
||||
gotoSnapshot(snapshot) {
|
||||
this.currentSnapshot = snapshot;
|
||||
// COMMUNICATE TO ContentFrame
|
||||
this.$emit("show-snapshot", snapshot);
|
||||
this.showFullView = false;
|
||||
},
|
||||
init() {
|
||||
if (!this.config.title) {
|
||||
this.config.title = this.config.initialView.url;
|
||||
}
|
||||
if (this.config.initialView.timestamp === "undefined") {
|
||||
this.showFullView = true;
|
||||
} else {
|
||||
this.showFullView = false;
|
||||
|
||||
// convert to snapshot objec to support proper rendering of time/date
|
||||
const snapshot = new PywbSnapshot(this.config.initialView, 0);
|
||||
this.gotoSnapshot(snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.app {
|
||||
border-bottom: 1px solid lightcoral;
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
}
|
||||
.iframe iframe {
|
||||
width: 100%;
|
||||
height: 80vh;
|
||||
}
|
||||
.logo {
|
||||
margin-right: 30px;
|
||||
width: 180px;
|
||||
}
|
||||
.short-nav {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.short-nav .first-line {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.short-nav .second-line {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.short-nav .logo {
|
||||
flex-shrink: initial;
|
||||
}
|
||||
|
||||
.short-nav .url-and-timeline {
|
||||
flex-grow: 2;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.short-nav .url {
|
||||
text-align: left;
|
||||
margin: 0 25px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.full-view-toggle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-radius: 5px;
|
||||
padding: 4px;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
.full-view-toggle img {
|
||||
width: 17px;
|
||||
display: inline-block;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.full-view-toggle:hover {
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
.full-view-toggle .detail {
|
||||
display: none;
|
||||
}
|
||||
.full-view-toggle:hover .detail {
|
||||
display: inline;
|
||||
}
|
||||
.full-view-toggle.expanded {
|
||||
background-color: #eeeeee;
|
||||
cursor: zoom-out;
|
||||
}
|
||||
</style>
|
151
pywb/vueui/src/components/CalendarMonth.vue
Normal file
151
pywb/vueui/src/components/CalendarMonth.vue
Normal file
@ -0,0 +1,151 @@
|
||||
<style>
|
||||
.calendar-month {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 5px;
|
||||
margin: 0;
|
||||
height: 240px;
|
||||
width: 220px;
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
}
|
||||
.calendar-month:hover {
|
||||
background-color: #eeeeee;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.calendar-month.current {
|
||||
background-color: #fff7ce;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.calendar-month > h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
.calendar-month > .empty {
|
||||
position: absolute;
|
||||
top: 45%;
|
||||
width: 100%;
|
||||
color: gray;
|
||||
}
|
||||
.calendar-month .day {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.calendar-month .day.empty {
|
||||
color: gray;
|
||||
}
|
||||
.calendar-month .day .count {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 80%;
|
||||
left: 80%;
|
||||
line-height: 1; /* reset to normal */
|
||||
padding: 3px;
|
||||
border-radius: 10px;
|
||||
border-bottom-left-radius: 0;
|
||||
border: 1px solid gray;
|
||||
background-color: white;
|
||||
z-index: 30;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.calendar-month .day:hover .count {
|
||||
display: block;
|
||||
}
|
||||
.calendar-month .day .size {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
background-color: rgba(166, 205, 245, .85);
|
||||
z-index: 10;
|
||||
}
|
||||
.calendar-month .day .day-id {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 11;
|
||||
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
color: black;
|
||||
}
|
||||
.calendar-month .day:hover .size {
|
||||
border: 1px solid black;
|
||||
}
|
||||
.calendar-month .day:hover {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="calendar-month" :class="{current: isCurrent}">
|
||||
<h3>{{month.getReadableId()}} ({{month.snapshotCount}})</h3>
|
||||
<div v-if="month.snapshotCount">
|
||||
<span v-for="(day) in ['S', 'M', 'T', 'W', 'H', 'F', 'S']" class="day" :style="dayStyle">{{day}}</span><br/>
|
||||
<span v-for="(day,i) in days"><br v-if="i && i % 7===0"/><span class="day" :class="{empty: !day || !day.snapshotCount}" :style="dayStyle" @click="gotoDay(day)"><template v-if="day"><span class="size" v-if="day.snapshotCount" :style="getDayCountCircleStyle(day.snapshotCount)"> </span><span class="day-id">{{day.id}}</span><span v-if="day.snapshotCount" class="count">{{day.snapshotCount}} capture<span v-if="day.snapshotCount!==1">s</span></span></template><template v-else> </template></span></span>
|
||||
</div>
|
||||
<div v-else class="empty">no captures</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["month", "year", "isCurrent"],
|
||||
data: function() {
|
||||
return {
|
||||
maxInDay: 0,
|
||||
daySize: 30,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
dayStyle() {
|
||||
const s = this.daySize;
|
||||
return `height: ${s}px; width: ${s}px; line-height: ${s}px`;
|
||||
},
|
||||
days() {
|
||||
if (!this.month || !this.month.snapshotCount) {
|
||||
return [];
|
||||
}
|
||||
const days = [];
|
||||
// Get days in month, and days in the complete weeks before first day and after last day
|
||||
const [firstDay, lastDay] = this.month.getChildrenRange();
|
||||
const daysBeforeFirst = (new Date(this.year.id, this.month.id-1, firstDay)).getDay();
|
||||
const daysAfterLastDay = (6 - (new Date(this.year.id, this.month.id-1, lastDay)).getDay());
|
||||
for(let i=0; i<daysBeforeFirst; i++) {
|
||||
days.push(null);
|
||||
}
|
||||
const hasChildren = !!this.month.children.length;
|
||||
for(let i=0; i<lastDay; i++) {
|
||||
days.push(hasChildren ? this.month.children[i] : null);
|
||||
}
|
||||
for(let i=0; i<daysAfterLastDay; i++) {
|
||||
days.push(null);
|
||||
}
|
||||
return days;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
gotoDay(day) {
|
||||
// upon doing to day, tell timeline to highlight itself
|
||||
this.$root.timelineHighlight = true;
|
||||
this.$emit("goto-period", day);
|
||||
},
|
||||
getDayCountCircleStyle(snapshotCount) {
|
||||
const size = Math.ceil((snapshotCount/this.year.maxGrandchildSnapshotCount) * this.daySize);
|
||||
const scaledSize = size ? (this.daySize*.3 + Math.ceil(size*.7)) : 0;
|
||||
const margin = (this.daySize-scaledSize)/2;
|
||||
|
||||
// TEMPORARILY DISABLE AUTO-HUE calculation as it is contributing to better understand of data
|
||||
// color hue should go form blue (240deg) to red (360deg)
|
||||
// const colorHue = Math.ceil((snapshotCount/this.year.maxGrandchildSnapshotCount) * (360-240));
|
||||
// const scaledColorHue = size ? (240 + colorHue) : 240;
|
||||
// background-color: hsl(${scaledColorHue}, 100%, 50%, .2)
|
||||
|
||||
return `width: ${scaledSize}px; height: ${scaledSize}px; top: ${margin}px; left: ${margin}px; border-radius: ${scaledSize/2}px;`;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
77
pywb/vueui/src/components/CalendarYear.vue
Normal file
77
pywb/vueui/src/components/CalendarYear.vue
Normal file
@ -0,0 +1,77 @@
|
||||
<style>
|
||||
.full-view {
|
||||
position: absolute;
|
||||
top: 130px;
|
||||
left: 0;
|
||||
height: 80vh;
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
}
|
||||
.full-view .months {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.full-view h2 {
|
||||
margin: 10px 0;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="full-view">
|
||||
<h2>{{year.snapshotCount}} captures in {{year.id}}</h2>
|
||||
<div class="months">
|
||||
<CalendarMonth
|
||||
v-for="month in year.children"
|
||||
:key="month.id"
|
||||
:month="month"
|
||||
:year="year"
|
||||
:is-current="month === currentMonth"
|
||||
@goto-period="$emit('goto-period', $event)"
|
||||
></CalendarMonth>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CalendarMonth from "./CalendarMonth.vue";
|
||||
import { PywbPeriod } from "../model.js";
|
||||
|
||||
export default {
|
||||
components: {CalendarMonth},
|
||||
props: ["period"],
|
||||
data: function() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
year() {
|
||||
let year = null;
|
||||
if (this.period.type === PywbPeriod.Type.all) {
|
||||
year = this.period.children[this.period.children.length-1];
|
||||
} else if (this.period.type === PywbPeriod.Type.year) {
|
||||
year = this.period;
|
||||
} else {
|
||||
year = this.period.getParents().filter(p => p.type === PywbPeriod.Type.year)[0];
|
||||
}
|
||||
if (year) {
|
||||
year.fillEmptyChildPeriods(true);
|
||||
}
|
||||
return year;
|
||||
},
|
||||
currentMonth() {
|
||||
let month = null;
|
||||
if (this.period.type === PywbPeriod.Type.month) {
|
||||
month = this.period;
|
||||
} else {
|
||||
month = this.period.getParents().filter(p => p.type === PywbPeriod.Type.month)[0];
|
||||
}
|
||||
return month;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
76
pywb/vueui/src/components/Summary.vue
Normal file
76
pywb/vueui/src/components/Summary.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="summary">
|
||||
<template v-if="parents.length">
|
||||
<span class="item">
|
||||
<span class="goto" @click="changePeriod(parents[0])">{{parents[0].getReadableId(true)}}</span>
|
||||
</span>
|
||||
>
|
||||
<span v-for="(parent,i) in parents" :key="parent.id" class="item" v-if="i > 0">
|
||||
<span class="goto" @click="changePeriod(parent)">{{parent.getReadableId(true)}}</span>
|
||||
</span>
|
||||
</template>
|
||||
<span class="item">
|
||||
<span class="current">{{period.getReadableId(true)}}</span>
|
||||
<span class="count">({{period.snapshotCount}} capture<span v-if="period.snapshotCount !== 1">s</span>)</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
period: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
parents: function() {
|
||||
return this.period.getParents();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changePeriod(period) {
|
||||
if (period.snapshotCount) {
|
||||
this.$emit("goto-period", period);
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.summary {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.summary .item {
|
||||
position: relative;
|
||||
display: inline;
|
||||
margin: 0 2px 0 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
.summary .count {
|
||||
vertical-align: middle;
|
||||
font-size: inherit;
|
||||
}
|
||||
.summary .count .verbose {
|
||||
display: none;
|
||||
}
|
||||
.summary .count:hover .verbose {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.summary .item .goto {
|
||||
margin: 1px;
|
||||
cursor: pointer;
|
||||
color: darkslateblue;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.summary .item .goto:hover {
|
||||
background-color: #a6cdf5;
|
||||
}
|
||||
.summary .item.snapshot {
|
||||
display: block;
|
||||
}
|
||||
|
||||
</style>
|
379
pywb/vueui/src/components/Timeline.vue
Normal file
379
pywb/vueui/src/components/Timeline.vue
Normal file
@ -0,0 +1,379 @@
|
||||
<template>
|
||||
<div class="timeline">
|
||||
<div class="arrow previous" :class="{disabled: isScrollZero && !previousPeriod}" @click="scrollPrev" @dblclick.stop.prevent>◀</div>
|
||||
<div class="scroll" ref="periodScroll" :class="{highlight: highlight}">
|
||||
<div class="periods" ref="periods">
|
||||
<div v-for="subPeriod in period.children"
|
||||
:key="subPeriod.id"
|
||||
class="period"
|
||||
:class="{empty: !subPeriod.snapshotCount, highlight: highlightPeriod === subPeriod, 'last-level': isLastZoomLevel}"
|
||||
v-on:click="changePeriod(subPeriod)">
|
||||
<div class="histo">
|
||||
<div class="count" v-if="!subPeriod.snapshot && !isLastZoomLevel"><span class="count-inner">{{subPeriod.snapshotCount}}</span></div>
|
||||
<div class="line"
|
||||
v-for="histoPeriod in subPeriod.children"
|
||||
:key="histoPeriod.id"
|
||||
:style="{height: getHistoLineHeight(histoPeriod.snapshotCount)}"
|
||||
v-on:click="changePeriodFromHistogram(histoPeriod)"
|
||||
>
|
||||
<div class="snap-info" v-if="isLastZoomLevel">{{histoPeriod.snapshot.getTimeDateFormatted()}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inner">
|
||||
<div class="label">{{subPeriod.getReadableId()}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="arrow next" :class="{disabled: isScrollMax && !nextPeriod}" @click="scrollNext" @dblclick.stop.prevent>▶</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { PywbPeriod } from "../model.js";
|
||||
|
||||
export default{
|
||||
props: ["period", "highlight"],
|
||||
data: function() {
|
||||
return {
|
||||
// TODO: remove widths (now using flex css for width calculation)
|
||||
subPeriodBoxWidths: {// in pixels
|
||||
[PywbPeriod.Type.year]: 80, // year box
|
||||
[PywbPeriod.Type.month]: 80, // month box in year
|
||||
[PywbPeriod.Type.day]: 20, // days box in month
|
||||
[PywbPeriod.Type.hour]: 60, // hour box in day
|
||||
[PywbPeriod.Type.snapshot]: 10 // snapshot box in hour/day
|
||||
},
|
||||
// TODO: remove widths (now using flex css for width calculation)
|
||||
emptySubPeriodBoxWidths: {// in pixels
|
||||
[PywbPeriod.Type.year]: 40, // year box
|
||||
[PywbPeriod.Type.month]: 40, // month box in year
|
||||
[PywbPeriod.Type.day]: 20, // days box in month
|
||||
[PywbPeriod.Type.hour]: 40, // hour box in day
|
||||
[PywbPeriod.Type.snapshot]: 20 // snapshot box in hour/day
|
||||
},
|
||||
highlightPeriod: null,
|
||||
previousPeriod: null,
|
||||
nextPeriod: null,
|
||||
isScrollZero: true,
|
||||
isScrollMax: true,
|
||||
};
|
||||
},
|
||||
created: function() {
|
||||
this.addEmptySubPeriods();
|
||||
},
|
||||
mounted: function() {
|
||||
this.$refs.periods._computedStyle = window.getComputedStyle(this.$refs.periods);
|
||||
this.$refs.periodScroll._computedStyle = window.getComputedStyle(this.$refs.periodScroll);
|
||||
this.$watch("period", this.onPeriodChanged);
|
||||
// TODO: remove widths (now using flex css for width calculation), so we don't need manual calc
|
||||
//this.adjustScrollElWidth();
|
||||
|
||||
this.$refs.periodScroll.addEventListener("scroll", this.updateScrollArrows);
|
||||
window.addEventListener("resize", this.updateScrollArrows);
|
||||
this.updateScrollArrows();
|
||||
},
|
||||
computed: {
|
||||
subPeriodBoxWidth: function() {
|
||||
return this.subPeriodBoxWidths[this.period.type+1]; // the type of the period children
|
||||
},
|
||||
// TODO: remove widths (now using flex css for width calculation)
|
||||
emptySubPeriodBoxWidth: function() {
|
||||
return this.emptySubPeriodBoxWidths[this.period.type+1]; // the type of the period children
|
||||
},
|
||||
// this determins which the last zoom level is before we go straight to showing snapshot
|
||||
isLastZoomLevel() {
|
||||
return this.period.type === PywbPeriod.Type.day;
|
||||
}
|
||||
},
|
||||
updated() {
|
||||
// do something on update
|
||||
},
|
||||
methods: {
|
||||
addEmptySubPeriods() {
|
||||
this.period.fillEmptyChildPeriods(true);
|
||||
},
|
||||
updateScrollArrows() {
|
||||
this.period.scroll = this.$refs.periodScroll.scrollLeft;
|
||||
const maxScroll = parseInt(this.$refs.periods._computedStyle.width) - parseInt(this.$refs.periodScroll._computedStyle.width);
|
||||
this.isScrollZero = !this.period.scroll; // if 0, then true (we are at scroll zero)
|
||||
this.isScrollMax = Math.abs(maxScroll - this.period.scroll) < 5;
|
||||
},
|
||||
restoreScroll() {
|
||||
this.$refs.periodScroll.scrollLeft = this.period.scroll;
|
||||
},
|
||||
scrollNext: function () {
|
||||
if (this.isScrollMax) {
|
||||
if (this.nextPeriod) {
|
||||
this.$emit("goto-period", this.nextPeriod);
|
||||
}
|
||||
} else {
|
||||
this.$refs.periodScroll.scrollLeft += 30;
|
||||
}
|
||||
},
|
||||
scrollPrev: function () {
|
||||
if (this.isScrollZero) {
|
||||
if (this.previousPeriod) {
|
||||
this.$emit("goto-period", this.previousPeriod);
|
||||
}
|
||||
} else {
|
||||
this.$refs.periodScroll.scrollLeft -= 30;
|
||||
}
|
||||
},
|
||||
getTimeFormatted: function(date) {
|
||||
return (date.hour < 13 ? date.hour : (date.hour % 12)) + ":" + ((date.minute < 10 ? "0":"")+date.minute) + " " + (date.hour < 12 ? "am":"pm");
|
||||
},
|
||||
getHistoLineHeight: function(value) {
|
||||
const percent = Math.ceil((value/this.period.maxGrandchildSnapshotCount) * 100);
|
||||
return (percent ? (5 + Math.ceil(percent*.95)) : 0) + "%";
|
||||
// return percent + '%';
|
||||
},
|
||||
changePeriod(period) {
|
||||
if (period.snapshotCount) {
|
||||
if (this.isLastZoomLevel) {
|
||||
if (period.type === PywbPeriod.Type.snapshot) {
|
||||
this.$emit("goto-period", period);
|
||||
}
|
||||
} else {
|
||||
this.$emit("goto-period", period);
|
||||
}
|
||||
}
|
||||
},
|
||||
// special "change period" from histogram lines
|
||||
changePeriodFromHistogram(period) {
|
||||
if (this.isLastZoomLevel && period.type === PywbPeriod.Type.snapshot) {
|
||||
this.$emit("goto-period", period);
|
||||
}
|
||||
},
|
||||
onPeriodChanged(newPeriod, oldPeriod) {
|
||||
this.addEmptySubPeriods();
|
||||
this.previousPeriod = this.period.getPrevious();
|
||||
this.nextPeriod = this.period.getNext();
|
||||
|
||||
// detect if going up level of period (new period type should be in old period parents)
|
||||
if (oldPeriod.type - newPeriod.type > 0) {
|
||||
let highlightPeriod = oldPeriod;
|
||||
for (let i=oldPeriod.type - newPeriod.type; i > 1; i--) {
|
||||
highlightPeriod = highlightPeriod.parent;
|
||||
}
|
||||
this.highlightPeriod = highlightPeriod;
|
||||
setTimeout((function() {
|
||||
this.highlightPeriod = null;
|
||||
}).bind(this), 2000);
|
||||
}
|
||||
setTimeout((function() {
|
||||
// TODO: remove widths (now using flex css for width calculation), so we don't need manual calc
|
||||
//this.adjustScrollElWidth();
|
||||
this.restoreScroll();
|
||||
this.updateScrollArrows();
|
||||
}).bind(this), 1);
|
||||
},
|
||||
// TODO: remove widths (now using flex css for width calculation), so we don't need manual calc
|
||||
adjustScrollElWidth() {
|
||||
//this.$refs.periodScroll.style.maxWidth = this.$refs.periods._computedStyle.width;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style>
|
||||
.timeline {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: auto;
|
||||
height: 80px;
|
||||
margin: 5px;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.timeline .id {
|
||||
display: inline-block;
|
||||
font-size: 30px;
|
||||
}
|
||||
.timeline .arrow {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
font-size: 20px; /* font-size = width of arrow, as it UTF char */
|
||||
line-height: 80px;
|
||||
vertical-align: top;
|
||||
cursor: pointer;
|
||||
}
|
||||
.timeline .arrow.previous {
|
||||
}
|
||||
.timeline .arrow.next {
|
||||
}
|
||||
.timeline .arrow.disabled, .timeline .arrow.disabled:hover {
|
||||
color: lightgray;
|
||||
background-color: transparent;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.timeline .arrow:hover {
|
||||
background-color: antiquewhite;
|
||||
color: firebrick;
|
||||
}
|
||||
|
||||
.timeline .scroll {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%; /* */
|
||||
height: 100%;
|
||||
|
||||
/* maker scrollable horizontally */
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
text-align: center;
|
||||
|
||||
transition: background-color 500ms ease-in;
|
||||
}
|
||||
/* hide scroll bar */
|
||||
.timeline .scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
/* highlight the scroll period: usually triggered from root app */
|
||||
.timeline .scroll.highlight {
|
||||
background-color: #fff7ce;
|
||||
}
|
||||
.timeline .scroll .periods {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
|
||||
.timeline .period {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
/* line-height: 80px; /* use to center middle vertically */
|
||||
white-space: normal;
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
/*background-color: #eee;*/
|
||||
/*border-right: 1px solid white; !* all other periods have right border, except first period*!*/
|
||||
cursor: pointer;
|
||||
|
||||
transition: background-color 500ms ease-in-out;
|
||||
}
|
||||
/* 1st period period child el */
|
||||
.timeline .period:nth-child(1) {
|
||||
/*border-left: 1px solid white; !* has left border; all other periods have right border *!*/
|
||||
}
|
||||
|
||||
.timeline .period:hover {
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
/* empty period */
|
||||
.timeline .period.empty {
|
||||
color: #aaa;
|
||||
/*background-color: transparent;*/
|
||||
}
|
||||
/* highlighted period */
|
||||
.timeline .period.highlight {
|
||||
background-color: cyan;
|
||||
}
|
||||
|
||||
.timeline .period .label {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
font-family: Baskerville, sans-serif;
|
||||
}
|
||||
.timeline .period .count {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px 0 0 0;
|
||||
text-align: center;
|
||||
font-size: 25px;
|
||||
}
|
||||
.timeline .period .count .count-inner {
|
||||
display: inline;
|
||||
width: auto;
|
||||
background-color: rgba(255,255,255,.85);
|
||||
padding: 1px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.timeline .period:hover .count {
|
||||
display: block;
|
||||
}
|
||||
.timeline .period .inner {
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
background-color: white;
|
||||
border-top: 1px solid gray;
|
||||
}
|
||||
.timeline .period.last-level .inner {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.timeline .period .histo {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.timeline .period .histo .line {
|
||||
flex-grow: 1;
|
||||
display: inline-block;
|
||||
background-color: #a6cdf5;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Last level period histogram spaces things evenly */
|
||||
.timeline .period.last-level .histo {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
/* Last level period histogram lines do not grow, but are fixed width/margin */
|
||||
.timeline .period.last-level .histo .line {
|
||||
flex-grow: unset;
|
||||
width: 5px;
|
||||
margin-left: 2px;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* update line color on hover*/
|
||||
.timeline .period.last-level .histo .line:hover {
|
||||
background-color: #f5a6eb;
|
||||
}
|
||||
|
||||
/*Last level period histogram line has extra info*/
|
||||
.timeline .period.last-level .histo .line .snap-info {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 30%;
|
||||
left: 120%;
|
||||
display: none;
|
||||
background-color: white;
|
||||
border: 1px solid gray;
|
||||
padding: 2px;
|
||||
white-space: nowrap; /*no wrapping allowed*/
|
||||
}
|
||||
/*show on hover*/
|
||||
.timeline .period.last-level .histo .line:hover .snap-info {
|
||||
display: block;
|
||||
}
|
||||
|
||||
</style>
|
23
pywb/vueui/src/index.js
Normal file
23
pywb/vueui/src/index.js
Normal file
@ -0,0 +1,23 @@
|
||||
import appData from "./App.vue";
|
||||
|
||||
import { PywbData } from "./model.js";
|
||||
|
||||
import Vue from "vue/dist/vue.esm.browser";
|
||||
|
||||
export function init(data, config = {}, loadCallback = null) {
|
||||
const app = new Vue(appData);
|
||||
|
||||
const pywbData = new PywbData(data);
|
||||
|
||||
app.$set(app, "snapshots", pywbData.snapshots);
|
||||
app.$set(app, "currentPeriod", pywbData.timeline);
|
||||
|
||||
//console.log({...app.config, ...config}.logoImg);
|
||||
app.$set(app, "config", {...app.config, ...config});
|
||||
|
||||
app.$mount("#app");
|
||||
|
||||
if (loadCallback) {
|
||||
app.$on("show-snapshot", loadCallback);
|
||||
}
|
||||
}
|
360
pywb/vueui/src/model.js
Normal file
360
pywb/vueui/src/model.js
Normal file
@ -0,0 +1,360 @@
|
||||
const PywbMonthLabels = {1:"Jan", 2:"Feb",3:"Mar",4:"Apr",5:"May",6:"Jun",7:"Jul",8:"Aug",9:"Sep",10:"Oct",11:"Nov",12:"Dec"};
|
||||
|
||||
export function PywbData(rawSnaps) {
|
||||
const allTimePeriod = new PywbPeriod({type: PywbPeriod.Type.all, id: "all"});
|
||||
const snapshots = [];
|
||||
let lastSingle = null;
|
||||
rawSnaps.forEach((rawSnap, i) => {
|
||||
const snap = new PywbSnapshot(rawSnap, i);
|
||||
let year, month, day, hour, single;
|
||||
if (!(year = allTimePeriod.getChildById(snap.year))) {
|
||||
year = new PywbPeriod({type: PywbPeriod.Type.year, id: snap.year});
|
||||
allTimePeriod.addChild(year);
|
||||
}
|
||||
if (!(month = year.getChildById(snap.month))) {
|
||||
month = new PywbPeriod({type: PywbPeriod.Type.month, id: snap.month});
|
||||
year.addChild(month);
|
||||
}
|
||||
if (!(day = month.getChildById(snap.day))) {
|
||||
day = new PywbPeriod({type: PywbPeriod.Type.day, id: snap.day});
|
||||
month.addChild(day);
|
||||
}
|
||||
const hourValue = Math.ceil((snap.hour + .0001) / (24/8)); // divide day in 4 six-hour periods (aka quarters)
|
||||
//const hourValue = snap.hour;
|
||||
if (!(hour = day.getChildById(hourValue))) {
|
||||
hour = new PywbPeriod({type: PywbPeriod.Type.hour, id: hourValue});
|
||||
day.addChild(hour);
|
||||
}
|
||||
if (!(single = hour.getChildById(snap.id))) {
|
||||
single = new PywbPeriod({type: PywbPeriod.Type.snapshot, id: snap.id});
|
||||
hour.addChild(single);
|
||||
}
|
||||
single.setSnapshot(snap);
|
||||
if (lastSingle) {
|
||||
lastSingle.setNextSnapshotPeriod(single);
|
||||
single.setPreviousSnapshotPeriod(lastSingle);
|
||||
}
|
||||
lastSingle = single;
|
||||
|
||||
snapshots.push(snap);
|
||||
});
|
||||
|
||||
this.timeline = allTimePeriod;
|
||||
this.snapshots = snapshots;
|
||||
this.getSnapshot = function(index) {
|
||||
if (index < 0 || index >= this.snapshots.length) {
|
||||
return null;
|
||||
}
|
||||
return this.snapshots[index];
|
||||
};
|
||||
this.getPreviousSnapshot = function(snapshot) {
|
||||
const index = snapshot.index;
|
||||
return this.getSnapshot(index-1);
|
||||
};
|
||||
this.getNextSnapshot = function(snapshot) {
|
||||
const index = snapshot.index;
|
||||
return this.getSnapshot(index+1);
|
||||
};
|
||||
}
|
||||
/* ---------------- SNAP SHOT object ----------------- */
|
||||
export class PywbSnapshot {
|
||||
constructor(init, index) {
|
||||
this.index = index;
|
||||
this.year = parseInt(init.timestamp.substr(0, 4));
|
||||
this.month = parseInt(init.timestamp.substr(4, 2));
|
||||
this.day = parseInt(init.timestamp.substr(6, 2));
|
||||
this.hour = parseInt(init.timestamp.substr(8, 2));
|
||||
this.minute = parseInt(init.timestamp.substr(10, 2));
|
||||
this.second = parseInt(init.timestamp.substr(12, 2));
|
||||
this.id = parseInt(init.timestamp);
|
||||
|
||||
this.urlkey = init.urlkey;
|
||||
this.url = init.url;
|
||||
this.mime = init.mime;
|
||||
this.status = init.status;
|
||||
this.digest = init.digest;
|
||||
this.redirect = init.redirect;
|
||||
this.robotflags = init.robotflags;
|
||||
this.length = init.length;
|
||||
this.offset = init.offset;
|
||||
this.filename = init.filename;
|
||||
this.load_url = init.load_url;
|
||||
this["source-col"] = init["source-col"];
|
||||
this.access = init.access;
|
||||
}
|
||||
|
||||
getTimeDateFormatted() {
|
||||
return `${this.year}-${PywbMonthLabels[this.month]}-${this.day} ${this.getTimeFormatted()}`;
|
||||
}
|
||||
|
||||
getDateFormatted() {
|
||||
return `${this.year}-${PywbMonthLabels[this.month]}-${this.day}`;
|
||||
}
|
||||
|
||||
getTimeFormatted() {
|
||||
return (this.hour < 13 ? this.hour : (this.hour % 12)) + ":" + ((this.minute < 10 ? "0":"")+this.minute) + ":" + ((this.second < 10 ? "0":"")+this.second) + " " + (this.hour < 12 ? "am":"pm");
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- PERIOD object ----------------- */
|
||||
export function PywbPeriod(init) {
|
||||
this.type = init.type;
|
||||
this.id = init.id;
|
||||
|
||||
this.childrenIds = {}; // allow for query by ID
|
||||
this.children = []; // allow for sequentiality / order
|
||||
|
||||
this.maxGrandchildSnapshotCount = 0;
|
||||
this.snapshotCount = 0;
|
||||
}
|
||||
PywbPeriod.Type = {all: 0,year: 1,month: 2,day: 3,hour: 4,snapshot:5};
|
||||
PywbPeriod.TypeLabel = ["timeline","year","month","day","hour","snapshot"];
|
||||
|
||||
PywbPeriod.prototype.getTypeLabel = function() {
|
||||
return PywbPeriod.TypeLabel[this.type];
|
||||
};
|
||||
PywbPeriod.GetTypeLabel = function(type) {
|
||||
return PywbPeriod.TypeLabel[type] ? PywbPeriod.TypeLabel[type] : "";
|
||||
};
|
||||
|
||||
PywbPeriod.prototype.getChildById = function(id) {
|
||||
return this.children[this.childrenIds[id]];
|
||||
};
|
||||
|
||||
// previous period (ONLY SET at the period level/type: snapshot)
|
||||
PywbPeriod.prototype.getPreviousSnapshotPeriod = () => {};
|
||||
PywbPeriod.prototype.setPreviousSnapshotPeriod = function(period) {
|
||||
this.getPreviousSnapshotPeriod = () => period;
|
||||
};
|
||||
// next period (ONLY SET at the period level/type: snapshot)
|
||||
PywbPeriod.prototype.getNextSnapshotPeriod = () => {};
|
||||
PywbPeriod.prototype.setNextSnapshotPeriod = function(period) {
|
||||
this.getNextSnapshotPeriod = () => period;
|
||||
};
|
||||
|
||||
PywbPeriod.prototype.getFirstSnapshotPeriod = function() {
|
||||
return this.getFirstLastSnapshotPeriod_("first");
|
||||
};
|
||||
PywbPeriod.prototype.getLastSnapshotPeriod = function() {
|
||||
return this.getFirstLastSnapshotPeriod_("last");
|
||||
};
|
||||
PywbPeriod.prototype.getFirstLastSnapshotPeriod_ = function(direction) {
|
||||
let period = this;
|
||||
let iFailSafe = 100; // in case a parser has a bug and the snapshotCount is not correct; avoid infinite-loop
|
||||
while (period.snapshotCount && period.type !== PywbPeriod.Type.snapshot) {
|
||||
let i = 0;
|
||||
for(i=0; i < period.children.length; i++) {
|
||||
const ii = direction === "first" ? i : (period.children.length - 1 - i);
|
||||
if (period.children[ii].snapshotCount) {
|
||||
period = period.children[ii];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (iFailSafe-- < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (period.type === PywbPeriod.Type.snapshot && period.snapshot) {
|
||||
return period;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
PywbPeriod.prototype.getPrevious = function() {
|
||||
const firstSnapshotPeriod = this.getFirstSnapshotPeriod();
|
||||
if (!firstSnapshotPeriod) {
|
||||
return null;
|
||||
}
|
||||
const previousSnapshotPeriod = firstSnapshotPeriod.getPreviousSnapshotPeriod();
|
||||
if (!previousSnapshotPeriod) {
|
||||
return null;
|
||||
}
|
||||
if (this.type === PywbPeriod.Type.snapshot) {
|
||||
return previousSnapshotPeriod;
|
||||
}
|
||||
let parent = previousSnapshotPeriod.parent;
|
||||
while(parent) {
|
||||
if (parent.type === this.type) {
|
||||
break;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
return parent;
|
||||
};
|
||||
PywbPeriod.prototype.getNext = function() {
|
||||
const lastSnapshotPeriod = this.getLastSnapshotPeriod();
|
||||
if (!lastSnapshotPeriod) {
|
||||
return null;
|
||||
}
|
||||
const nextSnapshotPeriod = lastSnapshotPeriod.getNextSnapshotPeriod();
|
||||
if (!nextSnapshotPeriod) {
|
||||
return null;
|
||||
}
|
||||
if (this.type === PywbPeriod.Type.snapshot) {
|
||||
return nextSnapshotPeriod;
|
||||
}
|
||||
let parent = nextSnapshotPeriod.parent;
|
||||
while(parent) {
|
||||
if (parent.type === this.type) {
|
||||
break;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
return parent;
|
||||
};
|
||||
|
||||
PywbPeriod.prototype.parent = null;
|
||||
PywbPeriod.prototype.addChild = function(period) {
|
||||
if (this.getChildById(period.id)) {
|
||||
return false;
|
||||
}
|
||||
period.parent = this;
|
||||
this.childrenIds[period.id] = this.children.length;
|
||||
this.children.push(period);
|
||||
return true;
|
||||
};
|
||||
|
||||
PywbPeriod.prototype.getChildrenRange = function() {
|
||||
switch (this.type) {
|
||||
case PywbPeriod.Type.all:
|
||||
// year range: first to last year available
|
||||
return [this.children[0].id, this.children[this.children.length-1].id];
|
||||
case PywbPeriod.Type.year:
|
||||
// month is simple: 1 to 12
|
||||
return [1,12];
|
||||
case PywbPeriod.Type.month:
|
||||
// days in month: 1 to last day in month
|
||||
const y = this.parent.id; const m = this.id;
|
||||
const lastDateInMonth = (new Date((new Date(y, m, 1)).getTime() - 1000)).getDate(); // 1 sec earlier
|
||||
return [1, lastDateInMonth];
|
||||
case PywbPeriod.Type.day:
|
||||
// hours: 0 to 23
|
||||
// return [1,4];
|
||||
return [1,8];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
PywbPeriod.prototype.fillEmptyGrancChildPeriods = function() {
|
||||
if (this.hasFilledEmptyGrandchildPeriods) {
|
||||
return;
|
||||
}
|
||||
this.children.forEach(c => {
|
||||
c.fillEmptyChildPeriods();
|
||||
});
|
||||
this.hasFilledEmptyGrandchildPeriods = true;
|
||||
};
|
||||
|
||||
PywbPeriod.prototype.fillEmptyChildPeriods = function(isFillEmptyGrandChildrenPeriods=false) {
|
||||
if (this.snapshotCount === 0 || this.type > PywbPeriod.Type.day) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFillEmptyGrandChildrenPeriods) {
|
||||
this.fillEmptyGrancChildPeriods();
|
||||
}
|
||||
|
||||
if (this.hasFilledEmptyChildPeriods) {
|
||||
return;
|
||||
}
|
||||
this.hasFilledEmptyChildPeriods = true;
|
||||
|
||||
const idRange = this.getChildrenRange();
|
||||
if (!idRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
for (let newId = idRange[0]; newId <= idRange[1]; newId++) {
|
||||
if (i < this.children.length) {
|
||||
// if existing and new id match, skip, item already in place
|
||||
// else
|
||||
if (this.children[i].id !== newId) {
|
||||
const empty = new PywbPeriod({type: this.type + 1, id: newId});
|
||||
if (newId < this.children[i].id) {
|
||||
// insert new before existing
|
||||
this.children.splice(i, 0, empty);
|
||||
} else {
|
||||
// insert new after existing
|
||||
this.children.splice(i+1, 0, empty);
|
||||
}
|
||||
// manually push children (no need to reverse link parent
|
||||
//empty.parent = this;
|
||||
}
|
||||
i++;
|
||||
} else {
|
||||
const empty = new PywbPeriod({type: this.type + 1, id: newId});
|
||||
this.children.push(empty);
|
||||
// manually push children (no need to reverse link parent
|
||||
//empty.parent = this;
|
||||
}
|
||||
}
|
||||
|
||||
// re-calculate indexes
|
||||
for(let i=0;i<this.children.length;i++) {
|
||||
this.childrenIds[this.children[i].id] = i;
|
||||
}
|
||||
|
||||
return idRange;
|
||||
};
|
||||
|
||||
PywbPeriod.prototype.getParents = function(skipAllTime=false) {
|
||||
let parents = [];
|
||||
let parent = this.parent;
|
||||
while(parent) {
|
||||
parents.push(parent);
|
||||
parent = parent.parent;
|
||||
}
|
||||
parents = parents.reverse();
|
||||
if (skipAllTime) {
|
||||
parents.shift(); // skip first "all-time"
|
||||
}
|
||||
return parents;
|
||||
};
|
||||
|
||||
PywbPeriod.prototype.snapshot = null;
|
||||
PywbPeriod.prototype.setSnapshot = function(snap) {
|
||||
this.snapshot = snap;
|
||||
this.snapshotCount++;
|
||||
let parent = this.parent;
|
||||
let child = this;
|
||||
while (parent) {
|
||||
parent.snapshotCount++;
|
||||
|
||||
let grandParent = parent.parent;
|
||||
if (grandParent) { // grandparent
|
||||
grandParent.maxGrandchildSnapshotCount = Math.max(grandParent.maxGrandchildSnapshotCount, child.snapshotCount);
|
||||
}
|
||||
child = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
};
|
||||
|
||||
PywbPeriod.prototype.getFullReadableId = function(hasDayCardinalSuffix) {
|
||||
// remove "all-time" from parents (getParents(true) when printing readable id (of all parents and currrent
|
||||
return this.getParents(true).map(p => p.getReadableId(hasDayCardinalSuffix)).join(" ") + " " + this.getReadableId(hasDayCardinalSuffix);
|
||||
};
|
||||
PywbPeriod.prototype.getReadableId = function(hasDayCardinalSuffix) {
|
||||
switch (this.type) {
|
||||
case PywbPeriod.Type.all:
|
||||
return "All-time";
|
||||
case PywbPeriod.Type.year:
|
||||
return this.id;
|
||||
case PywbPeriod.Type.month:
|
||||
return PywbMonthLabels[this.id];
|
||||
case PywbPeriod.Type.day:
|
||||
let suffix = "";
|
||||
if (hasDayCardinalSuffix) {
|
||||
const singleDigit = this.id % 10;
|
||||
const isTens = Math.floor(this.id / 10) === 1;
|
||||
const suffixes = {1:"st", 2:"nd",3:"rd"};
|
||||
suffix = (isTens || !suffixes[singleDigit]) ? "th" : suffixes[singleDigit];
|
||||
}
|
||||
return this.id + suffix;
|
||||
case PywbPeriod.Type.hour:
|
||||
return ({1:"12 am", 2: "3 am", 3: "6 am", 4: "9 am", 5: "noon", 6: "3 pm", 7: "6 pm", 8: "9 pm"})[this.id];
|
||||
//return ({1:'midnight', 2: '6 am', 3: 'noon', 4: '6 pm'})[this.id];
|
||||
//return (this.id < 13 ? this.id : this.id % 12) + ' ' + (this.id < 12 ? 'am':'pm');
|
||||
case PywbPeriod.Type.snapshot:
|
||||
return this.snapshot.getTimeFormatted();
|
||||
}
|
||||
};
|
1810
pywb/vueui/yarn.lock
Normal file
1810
pywb/vueui/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user