mirror of
https://github.com/webrecorder/pywb.git
synced 2025-03-15 00:03:28 +01:00
initial pass on integrating vueui 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 - ensure banner updated in response to inner frame events - combine cdxquery classes into vue build, expose single entrypoint VueUI.main - initial set of vueui dev, calendar, banner, calendar hover mode - ensure embedded mode replay is working - config: rename ui vars to 'vue_calendar_ui' and 'vue_timeline_banner' - dependencies: update to wombat 3.3.6 - bump to 2.7.0b0
This commit is contained in:
parent
1dedc46dce
commit
028e7102c0
@ -1,6 +1,10 @@
|
|||||||
# pywb config file
|
# pywb config file
|
||||||
# ========================================
|
# ========================================
|
||||||
#
|
#
|
||||||
|
debug: true
|
||||||
|
ui:
|
||||||
|
vue_calendar_ui: true
|
||||||
|
vue_timeline_banner: true
|
||||||
|
|
||||||
collections:
|
collections:
|
||||||
all: $all
|
all: $all
|
||||||
@ -8,6 +12,8 @@ collections:
|
|||||||
index_paths: ./sample_archive/cdx/
|
index_paths: ./sample_archive/cdx/
|
||||||
archive_paths: ./sample_archive/warcs/
|
archive_paths: ./sample_archive/warcs/
|
||||||
|
|
||||||
|
ukwa: cdx+https://www.webarchive.org.uk/wayback/archive/cdx
|
||||||
|
|
||||||
# Settings for each collection
|
# Settings for each collection
|
||||||
use_js_obj_proxy: true
|
use_js_obj_proxy: true
|
||||||
|
|
||||||
@ -17,6 +23,8 @@ enable_memento: true
|
|||||||
# Replay content in an iframe
|
# Replay content in an iframe
|
||||||
framed_replay: true
|
framed_replay: true
|
||||||
|
|
||||||
|
redirect_to_exact: true
|
||||||
|
|
||||||
|
|
||||||
# uncomment and change to set default locale
|
# uncomment and change to set default locale
|
||||||
# default_locale: en
|
# default_locale: en
|
||||||
|
@ -363,6 +363,9 @@ class FrontEndApp(object):
|
|||||||
else:
|
else:
|
||||||
coll_config['metadata'] = self.metadata_cache.load(coll) or {}
|
coll_config['metadata'] = self.metadata_cache.load(coll) or {}
|
||||||
|
|
||||||
|
if 'ui' in self.warcserver.config:
|
||||||
|
coll_config['ui'] = self.warcserver.config['ui']
|
||||||
|
|
||||||
return coll_config
|
return coll_config
|
||||||
|
|
||||||
def serve_coll_page(self, environ, coll='$root'):
|
def serve_coll_page(self, environ, coll='$root'):
|
||||||
|
@ -532,6 +532,7 @@ class RewriterApp(object):
|
|||||||
coll=kwargs.get('coll', ''),
|
coll=kwargs.get('coll', ''),
|
||||||
replay_mod=self.replay_mod,
|
replay_mod=self.replay_mod,
|
||||||
metadata=kwargs.get('metadata', {}),
|
metadata=kwargs.get('metadata', {}),
|
||||||
|
ui=kwargs.get('ui', {}),
|
||||||
config=self.config))
|
config=self.config))
|
||||||
|
|
||||||
cookie_rewriter = None
|
cookie_rewriter = None
|
||||||
@ -802,7 +803,8 @@ class RewriterApp(object):
|
|||||||
prefix = self.get_full_prefix(environ)
|
prefix = self.get_full_prefix(environ)
|
||||||
|
|
||||||
params = dict(url=wb_url.url,
|
params = dict(url=wb_url.url,
|
||||||
prefix=prefix)
|
prefix=prefix,
|
||||||
|
ui=kwargs.get('ui', {}))
|
||||||
|
|
||||||
return self.query_view.render_to_string(environ, **params)
|
return self.query_view.render_to_string(environ, **params)
|
||||||
|
|
||||||
@ -909,7 +911,9 @@ class RewriterApp(object):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def get_top_frame_params(self, wb_url, kwargs):
|
def get_top_frame_params(self, wb_url, kwargs):
|
||||||
return {'metadata': kwargs.get('metadata', {})}
|
return {'metadata': kwargs.get('metadata', {}),
|
||||||
|
'ui': kwargs.get('ui', {})
|
||||||
|
}
|
||||||
|
|
||||||
def handle_custom_response(self, environ, wb_url, full_prefix, host_prefix, kwargs):
|
def handle_custom_response(self, environ, wb_url, full_prefix, host_prefix, kwargs):
|
||||||
if self.is_framed_replay(wb_url):
|
if self.is_framed_replay(wb_url):
|
||||||
|
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 |
@ -21,9 +21,11 @@ This file is part of pywb, https://github.com/webrecorder/pywb
|
|||||||
// Creates the default pywb banner.
|
// Creates the default pywb banner.
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
if (window.top !== window) {
|
try {
|
||||||
return;
|
if (window.parent !== window && window.parent.wbinfo) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default banner class
|
* The default banner class
|
||||||
|
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 |
BIN
pywb/static/timeline-icon.png
Normal file
BIN
pywb/static/timeline-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
14653
pywb/static/vue/vueui.js
Normal file
14653
pywb/static/vue/vueui.js
Normal file
File diff suppressed because one or more lines are too long
11
pywb/static/vue_banner.css
Normal file
11
pywb/static/vue_banner.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#wb_iframe_div, #replay_iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -231,7 +231,13 @@ ContentFrame.prototype.initBannerUpdateCheck = function(newUrl, newTs) {
|
|||||||
* operating in live mode
|
* operating in live mode
|
||||||
*/
|
*/
|
||||||
ContentFrame.prototype.load_url = function(newUrl, newTs) {
|
ContentFrame.prototype.load_url = function(newUrl, newTs) {
|
||||||
this.iframe.src = this.make_url(newUrl, newTs, true);
|
var newUrl = this.make_url(newUrl, newTs, true);
|
||||||
|
if (this.iframe.src === newUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.iframe.src = newUrl;
|
||||||
|
|
||||||
if (this.wbBanner) {
|
if (this.wbBanner) {
|
||||||
this.initBannerUpdateCheck(newUrl, newTs);
|
this.initBannerUpdateCheck(newUrl, newTs);
|
||||||
}
|
}
|
||||||
|
BIN
pywb/static/zoom-out-icon-333316.png
Normal file
BIN
pywb/static/zoom-out-icon-333316.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
@ -17,13 +17,33 @@ window.banner_info = {
|
|||||||
locales: {{ locales }},
|
locales: {{ locales }},
|
||||||
locale_prefixes: {{ get_locale_prefixes() | tojson }},
|
locale_prefixes: {{ get_locale_prefixes() | tojson }},
|
||||||
prefix: "{{ wb_prefix }}",
|
prefix: "{{ wb_prefix }}",
|
||||||
staticPrefix: "{{ static_prefix }}"
|
staticPrefix: "{{ static_prefix }}",
|
||||||
|
|
||||||
|
logo: "{{ ui.logo }}"
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
{% if is_framed or not ui.vue_timeline_banner %}
|
||||||
<!-- default banner, create through js -->
|
<!-- default banner, create through js -->
|
||||||
<script src='{{ static_prefix }}/default_banner.js'> </script>
|
|
||||||
<link rel='stylesheet' href='{{ static_prefix }}/default_banner.css'/>
|
<link rel='stylesheet' href='{{ static_prefix }}/default_banner.css'/>
|
||||||
|
<script src='{{ static_prefix }}/default_banner.js'> </script>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<link rel='stylesheet' href='{{ static_prefix }}/vue_banner.css'/>
|
||||||
|
<script src="{{ static_prefix }}/vue/vueui.js"></script>
|
||||||
|
<script>
|
||||||
|
VueUI.main("{{ static_prefix }}", "{{ url }}", "{{ wb_prefix }}", "{{ timestamp }}");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% if ui.vue_timeline_banner %}
|
||||||
|
<div id="app" style="width: 100%; height: 200px"></div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{% endautoescape %}
|
{% endautoescape %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -6,13 +6,24 @@
|
|||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
|
|
||||||
|
{% if not ui.vue_calendar_ui %}
|
||||||
<link rel="stylesheet" href="{{ static_prefix }}/css/query.css">
|
<link rel="stylesheet" href="{{ static_prefix }}/css/query.css">
|
||||||
<script src="{{ static_prefix }}/js/url-polyfill.min.js"></script>
|
<script src="{{ static_prefix }}/js/url-polyfill.min.js"></script>
|
||||||
<script src="{{ static_prefix }}/query.js"></script>
|
<script src="{{ static_prefix }}/query.js"></script>
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<script src="{{ static_prefix }}/vue/vueui.js"></script>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
|
{% if not ui.vue_calendar_ui %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<h4 class="display-4 text-center text-sm-left p-0">{{ _('Search Results') }}</h4>
|
<h4 class="display-4 text-center text-sm-left p-0">{{ _('Search Results') }}</h4>
|
||||||
@ -22,6 +33,9 @@
|
|||||||
<div class="row justify-content-center text-center text-sm-left mt-1" id="display-query-type-info"></div>
|
<div class="row justify-content-center text-center text-sm-left mt-1" id="display-query-type-info"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container mt-3 q-display" id="captures"></div>
|
<div class="container mt-3 q-display" id="captures"></div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var text = {
|
var text = {
|
||||||
months: {
|
months: {
|
||||||
@ -55,7 +69,20 @@
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
{% if not ui.vue_calendar_ui %}
|
||||||
|
|
||||||
var renderCal = new RenderCalendar({ prefix: "{{ prefix }}", staticPrefix: "{{ static_prefix }}", text: text });
|
var renderCal = new RenderCalendar({ prefix: "{{ prefix }}", staticPrefix: "{{ static_prefix }}", text: text });
|
||||||
renderCal.init();
|
renderCal.init();
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
VueUI.main("{{ static_prefix }}", "{{ url }}", "{{ prefix }}");
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{% if ui.vue_calendar_ui %}
|
||||||
|
<div id="app" style="width: 100%; height: 100%"></div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% 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: "VueUI",
|
||||||
|
format: "iife",
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
vue({css: true, compileTemplate: true}),
|
||||||
|
css(),
|
||||||
|
nodeResolve({browser: true})
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
245
pywb/vueui/src/App.vue
Normal file
245
pywb/vueui/src/App.vue
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app" :class="{expanded: showTimelineView}" data-app="webrecorder-replay-app">
|
||||||
|
<div class="banner">
|
||||||
|
<div class="line">
|
||||||
|
<div class="logo"><img :src="config.logoImg" /></div>
|
||||||
|
<div class="timeline-wrap">
|
||||||
|
<div class="line">
|
||||||
|
<div class="breadcrumbs-wrap">
|
||||||
|
<TimelineBreadcrumbs
|
||||||
|
v-if="currentPeriod && showTimelineView"
|
||||||
|
:period="currentPeriod"
|
||||||
|
@goto-period="gotoPeriod"
|
||||||
|
></TimelineBreadcrumbs>
|
||||||
|
<span v-if="!showTimelineView" v-html="' '"></span><!-- for spacing -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toggles">
|
||||||
|
<span class="toggle" :class="{expanded: showFullView}" @click="showFullView = !showFullView" :title="(showTimelineView ? 'show':'hide') + ' calendar'">
|
||||||
|
<img src="/static/calendar-icon.png" />
|
||||||
|
</span>
|
||||||
|
<span class="toggle" :class="{expanded: showTimelineView}" @click="showTimelineView = !showTimelineView" :title="(showTimelineView ? 'show':'hide') + ' timeline'">
|
||||||
|
<img src="/static/timeline-icon.png" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Timeline
|
||||||
|
v-if="currentPeriod && showTimelineView"
|
||||||
|
:period="currentPeriod"
|
||||||
|
:highlight="timelineHighlight"
|
||||||
|
:current-snapshot="currentSnapshot"
|
||||||
|
:max-zoom-level="maxTimelineZoomLevel"
|
||||||
|
@goto-period="gotoPeriod"
|
||||||
|
></Timeline>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="snapshot-title">
|
||||||
|
<div>{{ config.url }}</div>
|
||||||
|
<div v-if="currentSnapshot && !showFullView">
|
||||||
|
<span v-if="config.title">{{ config.title }}</span>
|
||||||
|
Current capture: {{currentSnapshot.getTimeDateFormatted()}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CalendarYear v-if="showFullView"
|
||||||
|
:period="currentPeriod"
|
||||||
|
:current-snapshot="currentSnapshot"
|
||||||
|
@goto-period="gotoPeriod">
|
||||||
|
</CalendarYear>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Timeline from "./components/Timeline.vue";
|
||||||
|
import TimelineBreadcrumbs from "./components/TimelineBreadcrumbs.vue";
|
||||||
|
import CalendarYear from "./components/CalendarYear.vue";
|
||||||
|
|
||||||
|
import { PywbSnapshot, PywbPeriod } from "./model.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "PywbReplayApp",
|
||||||
|
//el: '[data-app="webrecorder-replay-app"]',
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
snapshots: [],
|
||||||
|
currentPeriod: null,
|
||||||
|
currentSnapshot: null,
|
||||||
|
msgs: [],
|
||||||
|
showFullView: false,
|
||||||
|
showTimelineView: true,
|
||||||
|
maxTimelineZoomLevel: PywbPeriod.Type.day,
|
||||||
|
config: {
|
||||||
|
title: "",
|
||||||
|
initialView: {}
|
||||||
|
},
|
||||||
|
timelineHighlight: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
components: {Timeline, TimelineBreadcrumbs, CalendarYear},
|
||||||
|
mounted: function() {
|
||||||
|
this.init();
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
sessionStorageUrlKey() {
|
||||||
|
// remove http(s), www and trailing slash
|
||||||
|
return 'zoom__' + this.config.url.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
gotoPeriod: function(newPeriod, onlyZoomToPeriod) {
|
||||||
|
if (this.timelineHighlight) {
|
||||||
|
setTimeout((() => {
|
||||||
|
this.timelineHighlight=false;
|
||||||
|
}).bind(this), 3000);
|
||||||
|
}
|
||||||
|
// only go to snapshot if caller did not request to zoom only
|
||||||
|
if (newPeriod.snapshot && !onlyZoomToPeriod) {
|
||||||
|
this.gotoSnapshot(newPeriod.snapshot, newPeriod);
|
||||||
|
} else {
|
||||||
|
// save current period (aka zoom)
|
||||||
|
// use sessionStorage (not localStorage), as we want this to be a very temporary memory for current page tab/window and no longer; NOTE: it serves when navigating from an "*" query to a specific capture and subsequent reloads
|
||||||
|
if (window.sessionStorage) {
|
||||||
|
window.sessionStorage.setItem(this.sessionStorageUrlKey, newPeriod.fullId);
|
||||||
|
}
|
||||||
|
// If new period goes beyond allowed max level
|
||||||
|
if (newPeriod.type > this.maxTimelineZoomLevel) {
|
||||||
|
this.currentPeriod = newPeriod.get(this.maxTimelineZoomLevel);
|
||||||
|
} else {
|
||||||
|
this.currentPeriod = newPeriod;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gotoSnapshot(snapshot, fromPeriod) {
|
||||||
|
this.currentSnapshot = snapshot;
|
||||||
|
|
||||||
|
// if the current period is not matching the current snapshot, updated it!
|
||||||
|
if (fromPeriod && !this.currentPeriod.contains(fromPeriod)) {
|
||||||
|
const fromPeriodAtMaxZoomLevel = fromPeriod.get(this.maxTimelineZoomLevel);
|
||||||
|
if (fromPeriodAtMaxZoomLevel !== this.currentPeriod) {
|
||||||
|
this.currentPeriod = fromPeriodAtMaxZoomLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// COMMUNICATE TO ContentFrame
|
||||||
|
this.$emit("show-snapshot", snapshot);
|
||||||
|
this.showFullView = false;
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.config.url = this.config.initialView.url;
|
||||||
|
if (this.config.initialView.title) {
|
||||||
|
this.config.title = this.config.initialView.title;
|
||||||
|
}
|
||||||
|
if (this.config.initialView.timestamp === undefined) {
|
||||||
|
this.showFullView = true;
|
||||||
|
this.showTimelineView = true;
|
||||||
|
} else {
|
||||||
|
this.showFullView = false;
|
||||||
|
this.showFullView = true;
|
||||||
|
this.setSnapshot(this.config.initialView);
|
||||||
|
}
|
||||||
|
if (window.sessionStorage) {
|
||||||
|
const currentPeriodId = window.sessionStorage.getItem(this.sessionStorageUrlKey);
|
||||||
|
if (currentPeriodId) {
|
||||||
|
const newCurrentPeriodFromStorage = this.currentPeriod.findByFullId(currentPeriodId);
|
||||||
|
if (newCurrentPeriodFromStorage) {
|
||||||
|
this.currentPeriod = newCurrentPeriodFromStorage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setSnapshot(view) {
|
||||||
|
// convert to snapshot objec to support proper rendering of time/date
|
||||||
|
const snapshot = new PywbSnapshot(view, 0);
|
||||||
|
this.config.url = view.url;
|
||||||
|
this.config.title = view.title;
|
||||||
|
this.gotoSnapshot(snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.app {
|
||||||
|
font-family: Calibri, Arial, sans-serif;
|
||||||
|
border-bottom: 1px solid lightcoral;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.app.expanded {
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
.full-view {
|
||||||
|
/*position: fixed;*/
|
||||||
|
/*top: 150px;*/
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.iframe iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 80vh;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
margin-right: 30px;
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
.banner {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px; /* limit width */
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.banner .line {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner .logo {
|
||||||
|
flex-shrink: initial;
|
||||||
|
/* for any content/image inside the logo container */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.banner .logo img {
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner .timeline-wrap {
|
||||||
|
flex-grow: 2;
|
||||||
|
overflow-x: hidden;
|
||||||
|
text-align: left;
|
||||||
|
margin: 0 25px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-wrap .line .breadcrumbs-wrap {
|
||||||
|
display: inline-block;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.timeline-wrap .line .toggles {
|
||||||
|
display: inline-block;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggles > .toggle {
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0 4px;
|
||||||
|
height: 100%;
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
.toggles > .toggle > img {
|
||||||
|
height: 18px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.toggles .toggle:hover {
|
||||||
|
background-color: #eeeeee;
|
||||||
|
}
|
||||||
|
.toggles .toggle.expanded {
|
||||||
|
background-color: #eeeeee;
|
||||||
|
cursor: zoom-out;
|
||||||
|
}
|
||||||
|
.snapshot-title {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
173
pywb/vueui/src/components/CalendarMonth.vue
Normal file
173
pywb/vueui/src/components/CalendarMonth.vue
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
<style>
|
||||||
|
.calendar-month {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 0;
|
||||||
|
height: 260px;
|
||||||
|
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.contains-current-snapshot {
|
||||||
|
border: solid 1px red;
|
||||||
|
}
|
||||||
|
.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.contains-current-snapshot .size {
|
||||||
|
background-color: rgba(255, 100, 100, .85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
.calendar-month .day.empty:hover {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="calendar-month" :class="{current: isCurrent, 'contains-current-snapshot': containsCurrentSnapshot}">
|
||||||
|
<h3>{{month.getReadableId()}} <span v-if="month.snapshotCount">({{ month.snapshotCount }})</span></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, 'contains-current-snapshot':dayContainsCurrentSnapshot(day)}" :style="dayStyle" @click="gotoDay(day, $event)"><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 v-html="' '"></template></span></span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty">no captures</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {PywbPeriod} from "../model";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ["month", "year", "isCurrent", "yearContainsCurrentSnapshot", "currentSnapshot"],
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
containsCurrentSnapshot() {
|
||||||
|
return this.currentSnapshot &&
|
||||||
|
this.month.contains(this.currentSnapshot);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
gotoDay(day, event) {
|
||||||
|
if (!day || !day.snapshotCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// upon doing to day, tell timeline to highlight itself
|
||||||
|
// this.$root.timelineHighlight = true;
|
||||||
|
this.$emit("show-day-timeline", day, event);
|
||||||
|
},
|
||||||
|
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;`;
|
||||||
|
},
|
||||||
|
dayContainsCurrentSnapshot(day) {
|
||||||
|
return !!day && day.snapshotCount > 0 && this.containsCurrentSnapshot && day.contains(this.currentSnapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
140
pywb/vueui/src/components/CalendarYear.vue
Normal file
140
pywb/vueui/src/components/CalendarYear.vue
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<style>
|
||||||
|
.full-view {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10;
|
||||||
|
height: 80vh;
|
||||||
|
overflow: scroll;
|
||||||
|
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.id}} ({{year.snapshotCount}} captures)</h2>
|
||||||
|
<div class="months">
|
||||||
|
<CalendarMonth
|
||||||
|
v-for="month in year.children"
|
||||||
|
:key="month.id"
|
||||||
|
:month="month"
|
||||||
|
:year="year"
|
||||||
|
:current-snapshot="containsCurrentSnapshot ? currentSnapshot : null"
|
||||||
|
:is-current="month === currentMonth"
|
||||||
|
@goto-period="$emit('goto-period', $event)"
|
||||||
|
@show-day-timeline="setCurrentTimeline"
|
||||||
|
></CalendarMonth>
|
||||||
|
</div>
|
||||||
|
<Tooltip :position="currentTimelinePos" v-if="currentTimelinePeriod" ref="timelineLinearTooltip">
|
||||||
|
<TimelineLinear
|
||||||
|
:period="currentTimelinePeriod"
|
||||||
|
:current-snapshot="containsCurrentSnapshot ? currentSnapshot : null"
|
||||||
|
@goto-period="gotoPeriod"
|
||||||
|
></TimelineLinear>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CalendarMonth from "./CalendarMonth.vue";
|
||||||
|
import TimelineLinear from "./TimelineLinear.vue";
|
||||||
|
import Tooltip from "./Tooltip.vue";
|
||||||
|
import { PywbPeriod } from "../model.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {CalendarMonth, TimelineLinear, Tooltip},
|
||||||
|
props: ["period", "currentSnapshot"],
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
firstZoomLevel: PywbPeriod.Type.day,
|
||||||
|
currentTimelinePeriod: null,
|
||||||
|
currentTimelinePos: '0,0'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
document.querySelector('body').addEventListener('click', this.resetCurrentTimeline);
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
year() { // the year that the timeline period is in
|
||||||
|
let year = null;
|
||||||
|
// if timeline is showing all year
|
||||||
|
if (this.period.type === PywbPeriod.Type.all) {
|
||||||
|
// if no current snapshot => pick the LAST YEAR
|
||||||
|
if (!this.currentSnapshot) {
|
||||||
|
year = this.period.children[this.period.children.length-1];
|
||||||
|
} else {
|
||||||
|
year = this.period.findByFullId(String(this.currentSnapshot.year));
|
||||||
|
}
|
||||||
|
} 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() { // the month that the timeline period is in
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
containsCurrentSnapshot() {
|
||||||
|
return this.currentSnapshot &&
|
||||||
|
this.year.contains(this.currentSnapshot);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resetCurrentTimeline(event) {
|
||||||
|
if (event && this.$refs.timelineLinearTooltip) {
|
||||||
|
let el = event.target;
|
||||||
|
let clickWithinTooltip = false;
|
||||||
|
while(el.parentElement) {
|
||||||
|
if (el === this.$refs.timelineLinearTooltip.$el) {
|
||||||
|
clickWithinTooltip = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
el = el.parentElement;
|
||||||
|
}
|
||||||
|
if (!clickWithinTooltip) {
|
||||||
|
this.currentTimelinePeriod = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setCurrentTimeline(day, event) {
|
||||||
|
this.currentTimelinePeriod = day;
|
||||||
|
if (!day) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.currentTimelinePos = `${event.x},${event.y}`;
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
gotoPeriod(period) {
|
||||||
|
if (period.snapshot || period.snapshotPeriod) {
|
||||||
|
this.$emit('goto-period', period);
|
||||||
|
} else {
|
||||||
|
this.currentTimelinePeriod = period;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
151
pywb/vueui/src/components/PageRuler.vue
Normal file
151
pywb/vueui/src/components/PageRuler.vue
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ruler">
|
||||||
|
<div class="inner" :class="{hidden: !isShow}">
|
||||||
|
<div class="axis x" :style="{width: xAxisWidth}" ref="xAxis">
|
||||||
|
<div class="tick x" v-for="x in xTicks" >{{ x ? x : '' }}</div>
|
||||||
|
<div class="line x" v-if="xAxisLine" :style="{left: xAxisLine+'px'}"><span>{{xAxisLine}}</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="axis y" :style="{height: yAxisHeight}" ref="yAxis">
|
||||||
|
<div class="tick y" v-for="y in yTicks">{{ y ? y : '' }}</div>
|
||||||
|
<div class="line y" v-if="yAxisLine" :style="{top: yAxisLine+'px'}"><span>{{yAxisLine}}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="toggle" @click="isShow=!isShow">R</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ruler {
|
||||||
|
position: fixed;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
z-index: 90;
|
||||||
|
}
|
||||||
|
.ruler .inner.hidden { display: none; }
|
||||||
|
|
||||||
|
.ruler .toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 91;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.ruler .line {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
.ruler .line span {
|
||||||
|
color: red;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: rgba(255,255,255,.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruler .tick {
|
||||||
|
font-size: 15px; line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruler .axis {
|
||||||
|
background-color: rgba(255,255,255,.9);
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruler .axis.x { height: 15px;}
|
||||||
|
.ruler .tick.x {
|
||||||
|
display: inline-block;
|
||||||
|
height: 15px;
|
||||||
|
width: 49px;
|
||||||
|
border-left: 1px solid black;
|
||||||
|
}
|
||||||
|
.ruler .axis.y { width: 15px; }
|
||||||
|
.ruler .tick.y {
|
||||||
|
display: block;
|
||||||
|
width: 15px;
|
||||||
|
height: 49px;
|
||||||
|
border-top: 1px solid black;
|
||||||
|
}
|
||||||
|
.ruler .line.x {
|
||||||
|
height: 100vh;
|
||||||
|
border-left: 1px solid red;
|
||||||
|
}
|
||||||
|
.ruler .line.y {
|
||||||
|
width: 100vw;
|
||||||
|
border-top: 1px solid red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isShow: false,
|
||||||
|
xTicks: [],
|
||||||
|
xAxisLine: 0,
|
||||||
|
yTicks: [],
|
||||||
|
yAxisLine: 0,
|
||||||
|
increment: 50 //px
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
this.xTicks.push(i * this.increment);
|
||||||
|
this.yTicks.push(i * this.increment);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseMoveHandlerFn_ = (event) => {
|
||||||
|
if (event.currentTarget === this.$refs.xAxis) {
|
||||||
|
this.xAxisLine = event.x;
|
||||||
|
console.log(event.x);
|
||||||
|
} else if (event.currentTarget === this.$refs.yAxis) {
|
||||||
|
this.yAxisLine = event.y;
|
||||||
|
console.log(event.y);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onMouseMoveHandlerFn = onMouseMoveHandlerFn_.bind(this);
|
||||||
|
const toggleHandlers = (event) => {
|
||||||
|
if (event.currentTarget === this.$refs.xAxis) {
|
||||||
|
if (this.$refs.xAxis.isGuidelineOn) {
|
||||||
|
this.$refs.xAxis.isGuidelineOn = false;
|
||||||
|
this.xAxisLine = 0;
|
||||||
|
this.$refs.xAxis.removeEventListener('mousemove', onMouseMoveHandlerFn, true);
|
||||||
|
} else {
|
||||||
|
this.$refs.xAxis.isGuidelineOn = true;
|
||||||
|
this.xAxisLine = event.x;
|
||||||
|
this.$refs.xAxis.addEventListener('mousemove', onMouseMoveHandlerFn, true);
|
||||||
|
}
|
||||||
|
event.stopPropagation();
|
||||||
|
} else if (event.currentTarget === this.$refs.yAxis) {
|
||||||
|
if (this.$refs.yAxis.isGuidelineOn) {
|
||||||
|
this.$refs.yAxis.isGuidelineOn = false;
|
||||||
|
this.yAxisLine = 0;
|
||||||
|
this.$refs.yAxis.removeEventListener('mousemove', onMouseMoveHandlerFn, true);
|
||||||
|
} else {
|
||||||
|
this.$refs.yAxis.isGuidelineOn = true;
|
||||||
|
this.yAxisLine = event.y;
|
||||||
|
this.$refs.yAxis.addEventListener('mousemove', onMouseMoveHandlerFn, true);
|
||||||
|
}
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$refs.xAxis.addEventListener('click', toggleHandlers.bind(this), true);
|
||||||
|
this.$refs.yAxis.addEventListener('click', toggleHandlers.bind(this), true);
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
yAxisHeight() {
|
||||||
|
return `${this.yTicks.length * this.increment}px`;
|
||||||
|
},
|
||||||
|
xAxisWidth() {
|
||||||
|
return `${this.xTicks.length * this.increment}px`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
431
pywb/vueui/src/components/Timeline.vue
Normal file
431
pywb/vueui/src/components/Timeline.vue
Normal file
@ -0,0 +1,431 @@
|
|||||||
|
<template>
|
||||||
|
<div class="timeline">
|
||||||
|
<div class="period-tooltip" v-show="tooltipPeriod" :style="{left: tooltipPeriodPos.x+'px', top: tooltipPeriodPos.y+'px'}">
|
||||||
|
<template v-if="tooltipPeriod">
|
||||||
|
<div v-if="tooltipPeriod.snapshot">View capture on
|
||||||
|
{{ tooltipPeriod.snapshot.getTimeDateFormatted() }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="tooltipPeriod.snapshotPeriod">View capture on
|
||||||
|
{{ tooltipPeriod.snapshotPeriod.snapshot.getTimeDateFormatted() }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="tooltipPeriod.snapshotCount">
|
||||||
|
{{ tooltipPeriod.snapshotCount }} captures
|
||||||
|
<span v-if="isTooltipPeriodDayOrHour">on</span><span v-else>in</span>
|
||||||
|
{{ tooltipPeriod.getFullReadableId() }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div v-html="'◀'" 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': !canZoom, 'contains-current-snapshot': containsCurrentSnapshot(subPeriod) }"
|
||||||
|
>
|
||||||
|
<div class="histo">
|
||||||
|
<div class="line"
|
||||||
|
v-for="histoPeriod in subPeriod.children"
|
||||||
|
:key="histoPeriod.id"
|
||||||
|
:style="{height: getHistoLineHeight(histoPeriod.snapshotCount)}"
|
||||||
|
:class="{'has-single-snapshot': histoPeriod.snapshotCount === 1, 'contains-current-snapshot': containsCurrentSnapshot(histoPeriod)}"
|
||||||
|
@click="changePeriod(histoPeriod, $event)"
|
||||||
|
@mouseover="setTooltipPeriod(histoPeriod, $event)"
|
||||||
|
@mouseout="setTooltipPeriod(null, $event)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inner"
|
||||||
|
:class="{'has-single-snapshot': subPeriod.snapshotCount === 1}"
|
||||||
|
@click="changePeriod(subPeriod, $event)"
|
||||||
|
@mouseover="setTooltipPeriod(subPeriod, $event)"
|
||||||
|
@mouseout="setTooltipPeriod(null, $event)"
|
||||||
|
>
|
||||||
|
<div class="label">
|
||||||
|
{{subPeriod.getReadableId()}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-html="'▶'" 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: { required: true },
|
||||||
|
currentSnapshot: { required: false, default: null},
|
||||||
|
highlight: { required: false, default: false},
|
||||||
|
stayWithinPeriod: { required: false, default: false},
|
||||||
|
maxZoomLevel: { required: false, default: PywbPeriod.Type.snapshot}
|
||||||
|
},
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
highlightPeriod: null,
|
||||||
|
previousPeriod: null,
|
||||||
|
nextPeriod: null,
|
||||||
|
isScrollZero: true,
|
||||||
|
isScrollMax: true,
|
||||||
|
tooltipPeriod: null,
|
||||||
|
tooltipPeriodPos: {x:0,y:0}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
|
||||||
|
this.$refs.periodScroll.addEventListener("scroll", this.updateScrollArrows);
|
||||||
|
window.addEventListener("resize", this.updateScrollArrows);
|
||||||
|
this.updateScrollArrows();
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
// this determins which the last zoom level is before we go straight to showing snapshot
|
||||||
|
canZoom() {
|
||||||
|
return this.period.type < this.maxZoomLevel;
|
||||||
|
},
|
||||||
|
isTooltipPeriodDayOrHour() {
|
||||||
|
return this.tooltipPeriod.type >= PywbPeriod.Type.day;
|
||||||
|
},
|
||||||
|
iContainCurrentSnapshot() {
|
||||||
|
return this.currentSnapshot && this.period.contains(this.currentSnapshot);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updated() {
|
||||||
|
// do something on update
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
containsCurrentSnapshot(period) {
|
||||||
|
return this.iContainCurrentSnapshot && period.contains(this.currentSnapshot);
|
||||||
|
},
|
||||||
|
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, true /* onlyZoomToPeriod */);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$refs.periodScroll.scrollLeft += 30;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollPrev: function () {
|
||||||
|
if (this.isScrollZero) {
|
||||||
|
if (this.previousPeriod) {
|
||||||
|
this.$emit("goto-period", this.previousPeriod, true /* onlyZoomToPeriod */);
|
||||||
|
}
|
||||||
|
} 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, $event) {
|
||||||
|
// if not empty
|
||||||
|
if (period.snapshotCount) {
|
||||||
|
let periodToChangeTo = null;
|
||||||
|
// if contains a single snapshot only, navigate to snapshot (load snapshot in FRAME, do not ZOOM IN)
|
||||||
|
if (period.snapshot) {
|
||||||
|
// if period is at level "snapshot" (no more children), send period, else send the child period, a reference to which is stored (by data/model layer) in the current period; App event needs a period to be passed (cannot pass in snapshot object itself)
|
||||||
|
if (period.type === PywbPeriod.Type.snapshot) {
|
||||||
|
periodToChangeTo = period;
|
||||||
|
} else if (period.snapshotPeriod) {
|
||||||
|
periodToChangeTo = period.snapshotPeriod;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if contains mulitple snapshots,
|
||||||
|
// zoom if ZOOM level is day or less, OR if period contain TOO MANY (>10)
|
||||||
|
if (this.canZoom) {
|
||||||
|
periodToChangeTo = period;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we selected a period to go to, emit event
|
||||||
|
if (periodToChangeTo) {
|
||||||
|
this.$emit("goto-period", periodToChangeTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
onPeriodChanged(newPeriod, oldPeriod) {
|
||||||
|
this.addEmptySubPeriods();
|
||||||
|
const previousPeriod = this.period.getPrevious();
|
||||||
|
const nextPeriod = this.period.getNext();
|
||||||
|
if (!this.stayWithinPeriod || this.stayWithinPeriod.contains(previousPeriod)) {
|
||||||
|
this.previousPeriod = previousPeriod;
|
||||||
|
}
|
||||||
|
if (!this.stayWithinPeriod || this.stayWithinPeriod.contains(nextPeriod)) {
|
||||||
|
this.nextPeriod = nextPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
// detect if going up level of period (new period type should be in old period parents)
|
||||||
|
if (oldPeriod && 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() {
|
||||||
|
this.restoreScroll();
|
||||||
|
this.updateScrollArrows();
|
||||||
|
}).bind(this), 1);
|
||||||
|
},
|
||||||
|
setTooltipPeriod(period, event) {
|
||||||
|
if (!period || !period.snapshotCount) {
|
||||||
|
this.tooltipPeriod = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.tooltipPeriod = period;
|
||||||
|
|
||||||
|
this.$nextTick(function() {
|
||||||
|
const tooltipContentsEl = document.querySelector('.period-tooltip div');
|
||||||
|
if (!tooltipContentsEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const periodTooltipStyle = window.getComputedStyle(tooltipContentsEl);
|
||||||
|
const tooltipWidth = parseInt(periodTooltipStyle.width);
|
||||||
|
const tooltipHeight = parseInt(periodTooltipStyle.height);
|
||||||
|
const spacing = 10;
|
||||||
|
if (window.innerWidth < event.x + (spacing*2) + tooltipWidth) {
|
||||||
|
this.tooltipPeriodPos.x = event.x - (tooltipWidth + spacing);
|
||||||
|
} else {
|
||||||
|
this.tooltipPeriodPos.x = event.x + spacing;
|
||||||
|
}
|
||||||
|
this.tooltipPeriodPos.y = event.y - (spacing + tooltipHeight);
|
||||||
|
});
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.timeline {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
width: auto;
|
||||||
|
height: 60px;
|
||||||
|
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: 60px;
|
||||||
|
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;*/
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.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: transparent;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
.timeline .period.contains-current-snapshot, .timeline .period.contains-current-snapshot:hover {
|
||||||
|
background-color: #f7def4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* empty period */
|
||||||
|
.timeline .period.empty {
|
||||||
|
color: #aaa;
|
||||||
|
/*background-color: transparent;*/
|
||||||
|
}
|
||||||
|
/* highlighted period */
|
||||||
|
.timeline .period.highlight {
|
||||||
|
background-color: cyan;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline .period .inner {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 20px;
|
||||||
|
background-color: white;
|
||||||
|
border-top: 1px solid gray;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
.timeline .period .inner.has-single-snapshot {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.timeline .period.last-level .inner, .timeline .period.empty .inner {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline .period .label {
|
||||||
|
width: 100%;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background-color 500ms ease-in;
|
||||||
|
}
|
||||||
|
.timeline .period:hover .label {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 20;
|
||||||
|
background-color: lightgrey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline .period .histo {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 39px;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline .period .histo .line {
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #a6cdf5;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
.timeline .period .histo .line.has-single-snapshot {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* update line color on hover*/
|
||||||
|
.timeline .period .histo .line:hover {
|
||||||
|
background-color: #f5a6eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline .period .histo .line.contains-current-snapshot {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Period that contains ONE snapshot only will show snapshot info*/
|
||||||
|
.timeline .period-tooltip {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 100;
|
||||||
|
/*left or right set programmatically*/
|
||||||
|
display: block;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid gray;
|
||||||
|
padding: 2px;
|
||||||
|
white-space: nowrap; /*no wrapping allowed*/
|
||||||
|
}
|
||||||
|
/*show on hover*/
|
||||||
|
.timeline .period-tooltip.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
83
pywb/vueui/src/components/TimelineBreadcrumbs.vue
Normal file
83
pywb/vueui/src/components/TimelineBreadcrumbs.vue
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<div class="breadcrumbs">
|
||||||
|
<template v-if="parents.length">
|
||||||
|
<span class="item">
|
||||||
|
<span class="goto" @click="changePeriod(parents[0])" :title="getPeriodZoomOutText(parents[0])">
|
||||||
|
<img src="/static/zoom-out-icon-333316.png" /> {{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)" :title="getPeriodZoomOutText(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: {
|
||||||
|
getPeriodZoomOutText(period) {
|
||||||
|
return 'Zoom out to '+period.getReadableId(true)+ ' ('+period.snapshotCount+' captures)';
|
||||||
|
},
|
||||||
|
changePeriod(period) {
|
||||||
|
if (period.snapshotCount) {
|
||||||
|
this.$emit("goto-period", period);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.breadcrumbs {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.breadcrumbs .item {
|
||||||
|
position: relative;
|
||||||
|
display: inline;
|
||||||
|
margin: 0 2px 0 0;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
.breadcrumbs .count {
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs .item .goto {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 1px;
|
||||||
|
padding: 1px;
|
||||||
|
cursor: zoom-out;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #eeeeee;
|
||||||
|
}
|
||||||
|
.breadcrumbs .item .goto:hover {
|
||||||
|
background-color: #a6cdf5;
|
||||||
|
}
|
||||||
|
.breadcrumbs .item .goto img {
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs .item.snapshot {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
72
pywb/vueui/src/components/TimelineLinear.vue
Normal file
72
pywb/vueui/src/components/TimelineLinear.vue
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="timeline-linear">
|
||||||
|
<div class="title">
|
||||||
|
<div>{{ period.getFullReadableId() }}</div>
|
||||||
|
<div>{{ period.snapshotCount }} capture<span v-if="period.snapshotCount > 1">s</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list">
|
||||||
|
<div v-for="period in snapshotPeriods">
|
||||||
|
<span class="link" @click="gotoPeriod(period)" >{{period.snapshot.getTimeFormatted()}}</span>
|
||||||
|
<span v-if="isCurrentSnapshot(period)" class="current">current</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "TimelineLinear",
|
||||||
|
props: ['period', 'currentSnapshot'],
|
||||||
|
computed: {
|
||||||
|
snapshotPeriods() {
|
||||||
|
return this.period.getSnapshotPeriodsFlat();
|
||||||
|
},
|
||||||
|
containsCurrentSnapshot() {
|
||||||
|
return this.currentSnapshot &&
|
||||||
|
this.period.contains(this.currentSnapshot);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
gotoPeriod(period) {
|
||||||
|
this.$emit('goto-period', period);
|
||||||
|
},
|
||||||
|
isCurrentSnapshot(period) {
|
||||||
|
return this.currentSnapshot && this.currentSnapshot.id === period.snapshot.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.timeline-linear {
|
||||||
|
width: auto;
|
||||||
|
padding: 5px;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid gray;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.timeline-linear .list {
|
||||||
|
max-height: 80vh;
|
||||||
|
min-height: 50px;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
.timeline-linear .title {
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
.timeline-linear .link {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: darkblue;
|
||||||
|
}
|
||||||
|
.timeline-linear .link:hover {
|
||||||
|
color: lightseagreen;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.timeline-linear .current {
|
||||||
|
background-color: deeppink;
|
||||||
|
color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
58
pywb/vueui/src/components/Tooltip.vue
Normal file
58
pywb/vueui/src/components/Tooltip.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<div class="pywb-tooltip">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let elStyle = null;
|
||||||
|
export default {
|
||||||
|
name: "Tooltip",
|
||||||
|
props: ['position'],
|
||||||
|
mounted() {
|
||||||
|
this.$watch('position', this.updatePosition);
|
||||||
|
this.updatePosition();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updatePosition() {
|
||||||
|
this.$el.style.top = 0;
|
||||||
|
this.$el.style.left = 0;
|
||||||
|
this.$el.style.maxHeight = 'auto';
|
||||||
|
|
||||||
|
const style = window.getComputedStyle(this.$el);
|
||||||
|
const width = parseInt(style.width);
|
||||||
|
const height = parseInt(style.height);
|
||||||
|
const spacing = 10;
|
||||||
|
const [initX, initY] = this.position.split(',').map(s => parseInt(s));
|
||||||
|
if (window.innerWidth < initX + (spacing*2) + width) {
|
||||||
|
this.$el.style.left = (initX - (width + spacing)) + 'px';
|
||||||
|
} else {
|
||||||
|
this.$el.style.left = (initX + spacing) + 'px';
|
||||||
|
}
|
||||||
|
if ((window.innerHeight < initY + (spacing*2) + height) && (initY - (spacing*2) - height < 0) ) {
|
||||||
|
if (initY > window.innerHeight / 2) {
|
||||||
|
this.$el.style.top = (window.innerHeight - (height + (spacing*2))) + 'px';
|
||||||
|
} else {
|
||||||
|
this.$el.style.top = (spacing*2) + 'px';
|
||||||
|
}
|
||||||
|
} else if (window.innerHeight < initY + (spacing*2) + height) {
|
||||||
|
this.$el.style.top = (initY - (spacing + height)) + 'px';
|
||||||
|
} else {
|
||||||
|
this.$el.style.top = initY + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pywb-tooltip {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 40;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid grey;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
201
pywb/vueui/src/index.js
Normal file
201
pywb/vueui/src/index.js
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import appData from "./App.vue";
|
||||||
|
|
||||||
|
import { PywbData } from "./model.js";
|
||||||
|
|
||||||
|
import Vue from "vue/dist/vue.esm.browser";
|
||||||
|
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
export function main(staticPrefix, url, prefix, timestamp) {
|
||||||
|
new CDXLoader(staticPrefix, url, prefix, timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
class CDXLoader {
|
||||||
|
constructor(staticPrefix, url, prefix, timestamp) {
|
||||||
|
this.opts = {};
|
||||||
|
this.prefix = prefix;
|
||||||
|
this.staticPrefix = staticPrefix;
|
||||||
|
|
||||||
|
this.isReplay = (timestamp !== undefined);
|
||||||
|
|
||||||
|
if (this.isReplay) {
|
||||||
|
window.WBBanner = new VueBannerWrapper(this, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
let queryURL;
|
||||||
|
|
||||||
|
// query form *?=url...
|
||||||
|
if (window.location.href.indexOf("*?") > 0) {
|
||||||
|
queryURL = window.location.href.replace("*?", "cdx?") + "&output=json";
|
||||||
|
url = new URL(queryURL).searchParams.get("url");
|
||||||
|
|
||||||
|
// otherwise, traditional calendar form /*/<url>
|
||||||
|
} else if (url) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set("url", url);
|
||||||
|
params.set("output", "json");
|
||||||
|
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";
|
||||||
|
|
||||||
|
this.loadCDX(queryURL).then((cdxList) => {
|
||||||
|
this.app = this.initApp(cdxList, this.opts, (snapshot) => this.loadSnapshot(snapshot));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initApp(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);
|
||||||
|
|
||||||
|
app.$set(app, "config", {...app.config, ...config});
|
||||||
|
|
||||||
|
app.$mount("#app");
|
||||||
|
|
||||||
|
// TODO (Ilya): make this work with in-page snapshot/capture/replay updates!
|
||||||
|
// app.$on("show-snapshot", snapshot => {
|
||||||
|
// const replayUrl = app.config.url;
|
||||||
|
// const url = location.href.replace('/'+replayUrl, '').replace(/\d+$/, '') + snapshot.id + '/' + replayUrl;
|
||||||
|
// window.history.pushState({url: replayUrl, timestamp: snapshot.id}, document.title, url);
|
||||||
|
// if (!window.onpopstate) {
|
||||||
|
// window.onpopstate = (ev) => {
|
||||||
|
// updateSnapshot(ev.state.url, ev.state.timestamp);
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
if (loadCallback) {
|
||||||
|
app.$on("show-snapshot", loadCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSnapshot(url, timestamp) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set("url", url);
|
||||||
|
params.set("output", "json");
|
||||||
|
const queryURL = this.prefix + "cdx?" + params.toString();
|
||||||
|
|
||||||
|
const cdxList = await this.loadCDX(queryURL);
|
||||||
|
|
||||||
|
const pywbData = new PywbData(cdxList);
|
||||||
|
|
||||||
|
const app = this.app;
|
||||||
|
app.$set(app, "snapshots", pywbData.snapshots);
|
||||||
|
app.$set(app, "currentPeriod", pywbData.timeline);
|
||||||
|
|
||||||
|
app.setSnapshot({url, timestamp});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadCDX(queryURL) {
|
||||||
|
const queryWorker = new Worker(this.staticPrefix + "/queryWorker.js");
|
||||||
|
|
||||||
|
const p = new Promise((resolve) => {
|
||||||
|
const cdxList = [];
|
||||||
|
|
||||||
|
queryWorker.addEventListener("message", (event) => {
|
||||||
|
const data = event.data;
|
||||||
|
switch (data.type) {
|
||||||
|
case "cdxRecord":
|
||||||
|
cdxList.push(data.record);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "finished":
|
||||||
|
resolve(cdxList);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
queryWorker.postMessage({
|
||||||
|
type: "query",
|
||||||
|
queryURL
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await p;
|
||||||
|
|
||||||
|
queryWorker.terminate();
|
||||||
|
//delete queryWorker;
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSnapshot(snapshot) {
|
||||||
|
if (!this.isReplay) {
|
||||||
|
window.location.href = this.prefix + snapshot.id + "/" + snapshot.url;
|
||||||
|
} else if (window.cframe) {
|
||||||
|
window.cframe.load_url(snapshot.url, snapshot.id + "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
class VueBannerWrapper
|
||||||
|
{
|
||||||
|
constructor(loader, url) {
|
||||||
|
this.loading = true;
|
||||||
|
this.lastSurt = this.getSurt(url);
|
||||||
|
this.loader = loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
stillIndicatesLoading() {
|
||||||
|
return this.loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCaptureInfo(/*url, ts, is_live*/) {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage(event) {
|
||||||
|
const type = event.data.wb_type;
|
||||||
|
|
||||||
|
if (type === "load" || type === "replace-url") {
|
||||||
|
const surt = this.getSurt(event.data.url);
|
||||||
|
|
||||||
|
if (surt !== this.lastSurt) {
|
||||||
|
this.loader.updateSnapshot(event.data.url, event.data.ts);
|
||||||
|
this.lastSurt = surt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSurt(url) {
|
||||||
|
try {
|
||||||
|
if (!url.startsWith("https:") && !url.startsWith("http:")) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
url = url.replace(/^(https?:\/\/)www\d*\./, "$1");
|
||||||
|
const urlObj = new URL(url.toLowerCase());
|
||||||
|
|
||||||
|
const hostParts = urlObj.hostname.split(".").reverse();
|
||||||
|
let surt = hostParts.join(",");
|
||||||
|
if (urlObj.port) {
|
||||||
|
surt += ":" + urlObj.port;
|
||||||
|
}
|
||||||
|
surt += ")";
|
||||||
|
surt += urlObj.pathname;
|
||||||
|
if (urlObj.search) {
|
||||||
|
urlObj.searchParams.sort();
|
||||||
|
surt += urlObj.search;
|
||||||
|
}
|
||||||
|
return surt;
|
||||||
|
} catch (e) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
491
pywb/vueui/src/model.js
Normal file
491
pywb/vueui/src/model.js
Normal file
@ -0,0 +1,491 @@
|
|||||||
|
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"};
|
||||||
|
const PywbPeriodIdDelimiter = '-';
|
||||||
|
export function PywbData(rawSnaps) {
|
||||||
|
const allTimePeriod = new PywbPeriod({type: PywbPeriod.Type.all, id: "all"});
|
||||||
|
const snapshots = [];
|
||||||
|
let lastSingle = null;
|
||||||
|
let lastYear, lastMonth, lastDay, lastHour;
|
||||||
|
rawSnaps.forEach((rawSnap, i) => {
|
||||||
|
const snap = new PywbSnapshot(rawSnap, i);
|
||||||
|
let year, month, day, hour, single;
|
||||||
|
if (!(year = allTimePeriod.getChildById(snap.year))) {
|
||||||
|
if (lastYear) lastYear.checkIfSingleSnapshotOnly();
|
||||||
|
lastYear = year = new PywbPeriod({type: PywbPeriod.Type.year, id: snap.year});
|
||||||
|
allTimePeriod.addChild(year);
|
||||||
|
}
|
||||||
|
if (!(month = year.getChildById(snap.month))) {
|
||||||
|
if (lastMonth) lastMonth.checkIfSingleSnapshotOnly();
|
||||||
|
lastMonth = month = new PywbPeriod({type: PywbPeriod.Type.month, id: snap.month});
|
||||||
|
year.addChild(month);
|
||||||
|
}
|
||||||
|
if (!(day = month.getChildById(snap.day))) {
|
||||||
|
if (lastDay) lastDay.checkIfSingleSnapshotOnly();
|
||||||
|
lastDay = 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)
|
||||||
|
if (!(hour = day.getChildById(hourValue))) {
|
||||||
|
if (lastHour) lastHour.checkIfSingleSnapshotOnly();
|
||||||
|
lastHour = 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 `${PywbMonthLabels[this.month]} ${this.day}, ${this.year} at ${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");
|
||||||
|
}
|
||||||
|
|
||||||
|
getParentIds() {
|
||||||
|
return [this.year, this.month, this.day, Math.ceil((this.hour + .0001) / (24/8))];
|
||||||
|
}
|
||||||
|
|
||||||
|
getFullId() {
|
||||||
|
return [this.year, this.month, this.day, Math.ceil((this.hour + .0001) / (24/8)), this.id].join(PywbPeriodIdDelimiter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- PERIOD object ----------------- */
|
||||||
|
export function PywbPeriod(init) {
|
||||||
|
this.type = init.type;
|
||||||
|
this.id = init.id;
|
||||||
|
this.fullId = ''; // full-id property that include string id of parents and self with a delimitor
|
||||||
|
|
||||||
|
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);
|
||||||
|
period.initFullId();
|
||||||
|
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.contains = function(periodOrSnapshot) {
|
||||||
|
if (this.type === 0) {
|
||||||
|
return true; // all-time contains everything
|
||||||
|
}
|
||||||
|
if (periodOrSnapshot instanceof PywbPeriod) {
|
||||||
|
return periodOrSnapshot.getParents(true).slice(0,this.type).map(p => p.id).join(PywbPeriodIdDelimiter) === this.fullId;
|
||||||
|
}
|
||||||
|
if (periodOrSnapshot instanceof PywbSnapshot) {
|
||||||
|
if (this.type === PywbPeriod.Type.snapshot) {
|
||||||
|
return periodOrSnapshot.getFullId() === this.fullId;
|
||||||
|
} else {
|
||||||
|
return periodOrSnapshot.getParentIds(true).slice(0,this.type).join(PywbPeriodIdDelimiter) === this.fullId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
PywbPeriod.prototype.snapshot = null;
|
||||||
|
PywbPeriod.prototype.snapshotPeriod = null;
|
||||||
|
|
||||||
|
PywbPeriod.prototype.checkIfSingleSnapshotOnly = function() {
|
||||||
|
if (this.snapshotCount === 1) {
|
||||||
|
let snapshotPeriod = this;
|
||||||
|
let failSafe = PywbPeriod.Type.snapshot;
|
||||||
|
while(!snapshotPeriod.snapshot) {
|
||||||
|
if (--failSafe <=0) break;
|
||||||
|
snapshotPeriod = snapshotPeriod.children[0];
|
||||||
|
}
|
||||||
|
this.snapshot = snapshotPeriod.snapshot;
|
||||||
|
this.snapshotPeriod = snapshotPeriod;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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.getSnapshotPeriodsFlat = function(flatArray=false) {
|
||||||
|
if (!flatArray) {
|
||||||
|
flatArray = [];
|
||||||
|
}
|
||||||
|
if (!this.snapshotCount) {
|
||||||
|
return flatArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.snapshotCount === 1) {
|
||||||
|
flatArray.push(this.snapshotPeriod || this);
|
||||||
|
return flatArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.children.forEach(child => {
|
||||||
|
child.getSnapshotPeriodsFlat(flatArray);
|
||||||
|
});
|
||||||
|
return flatArray;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the "full" id, which includes all parents ID and self ID, delimited by a ${PywbPeriodIdDelimiter}
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
PywbPeriod.prototype.initFullId = function() {
|
||||||
|
const ids = this.getParents(true).map(p => p.id);
|
||||||
|
ids.push(this.id);
|
||||||
|
this.fullId = ids.join(PywbPeriodIdDelimiter);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a period by its full ID (of all ancestors and self, delimited by a hyphen). Start by locating the great-grand-parent (aka timeline), then looping on all IDs and finding the period in loop
|
||||||
|
* @param {string} fullId
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
PywbPeriod.prototype.findByFullId = function(fullId) {
|
||||||
|
let parent = this;
|
||||||
|
if (this.type !== PywbPeriod.Type.all) {
|
||||||
|
parent = this.getParents()[0];
|
||||||
|
}
|
||||||
|
const ids = fullId.split(PywbPeriodIdDelimiter);
|
||||||
|
|
||||||
|
let found = false;
|
||||||
|
for(let i=0; i<ids.length; i++) {
|
||||||
|
parent = parent.getChildById(ids[i]);
|
||||||
|
if (parent) {
|
||||||
|
// if last chunk of ID in loop, the period is found
|
||||||
|
if (i === ids.length - 1) {
|
||||||
|
found = parent;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if no parent is found with ID chunk, abort "mission"
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
};
|
||||||
|
PywbPeriod.prototype.getFullReadableId = function(hasDayCardinalSuffix) {
|
||||||
|
// remove "all-time" from parents (getParents(true) when printing readable id (of all parents and currrent
|
||||||
|
switch (this.type) {
|
||||||
|
case PywbPeriod.Type.all:
|
||||||
|
return "";
|
||||||
|
case PywbPeriod.Type.year:
|
||||||
|
return this.id;
|
||||||
|
case PywbPeriod.Type.month:
|
||||||
|
return this.getReadableId(hasDayCardinalSuffix) + ' ' + this.parent.id;
|
||||||
|
case PywbPeriod.Type.day: {
|
||||||
|
return this.parent.getReadableId(hasDayCardinalSuffix) + ' ' + this.getReadableId(hasDayCardinalSuffix) + ', ' + this.parent.parent.id;
|
||||||
|
}
|
||||||
|
case PywbPeriod.Type.hour:
|
||||||
|
return this.parent.parent.getReadableId(hasDayCardinalSuffix) + ' ' + this.parent.getReadableId(hasDayCardinalSuffix) + ', ' + this.parent.parent.parent.id + ' at ' + this.getReadableId(hasDayCardinalSuffix);
|
||||||
|
case PywbPeriod.Type.snapshot:
|
||||||
|
return this.parent.parent.getReadableId(hasDayCardinalSuffix) + ' ' + this.parent.getReadableId(hasDayCardinalSuffix) + ', ' + this.parent.parent.parent.id + ' at ' + this.snapshot.getTimeFormatted();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
PywbPeriod.prototype.getYear = function() { this.get(PywbPeriod.Type.year); };
|
||||||
|
PywbPeriod.prototype.getMonth = function() { this.get(PywbPeriod.Type.month); };
|
||||||
|
PywbPeriod.prototype.getDay = function() { this.get(PywbPeriod.Type.day); };
|
||||||
|
PywbPeriod.prototype.getHour = function() { this.get(PywbPeriod.Type.hour); };
|
||||||
|
PywbPeriod.prototype.get = function(type) {
|
||||||
|
if (this.type === type) {
|
||||||
|
return this;
|
||||||
|
} else if (this.type > type) {
|
||||||
|
return this.getParents()[type];
|
||||||
|
}
|
||||||
|
};
|
1810
pywb/vueui/yarn.lock
Normal file
1810
pywb/vueui/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -285,8 +285,8 @@ class LiveWebLoader(BaseLoader):
|
|||||||
self.socks_proxy = None
|
self.socks_proxy = None
|
||||||
|
|
||||||
def load_resource(self, cdx, params):
|
def load_resource(self, cdx, params):
|
||||||
if cdx.get('filename') and cdx.get('offset') is not None:
|
#if cdx.get('filename') and cdx.get('offset') is not None:
|
||||||
return None
|
# return None
|
||||||
|
|
||||||
load_url = cdx.get('load_url')
|
load_url = cdx.get('load_url')
|
||||||
if not load_url:
|
if not load_url:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user