1
0
mirror of https://github.com/webrecorder/pywb.git synced 2025-03-15 00:03:28 +01:00

Rework Vue banner UI

- Make Vue banner responsive with Bootstrap 4
- Add previous/next year arrows to calendar
- Make navbar background, text color, and button outlines configurable
via config.yaml
- Toggle calendar and timeline separately
- Fix bug preventing title from displaying
- Make app keyboard-navigable
- Fix banner background color configuration
- Comment out vue_navbar_background_hash
- Display linear timeline tooltip centrally on enter
- Improve header styling on small screens
- Add titles to font awesome icons
- Remove old default banner (calendar retained for advanced search
  results)
- Fix TimelineLinear TypeError that broke calendar
- Bump version to 2.7.0b2
- Set Cache-Control header on CDXJ API response to mark returned CDX as
stale after 1 day
- Add commented out UI values to config.yaml to aid users
- Remove timeline and calendar card borders
- Fix issues with snapshot navigation
- Center search bar and align with buttons
- Make Vue app bfcache-ineligible: By adding an empty unload event
listener, we make pages serving the Vue app ineligible for bfcache,
which prevents unexpected behavior when navigating via the browser's
back/forward buttons.
This commit is contained in:
Tessa Walsh 2022-07-28 02:29:18 -04:00
parent ff7783aa74
commit c28941a0b6
25 changed files with 1289 additions and 2237 deletions

View File

@ -2,9 +2,13 @@
# ======================================== # ========================================
# #
debug: true debug: true
ui:
vue_calendar_ui: true # Uncomment to set banner colors and logo
vue_timeline_banner: true # ui:
# logo: path/relative/from/static/logo.png
# navbar_background_hex: 0c49b0
# navbar_color_hex: fff
# navbar_light_buttons: true
collections: collections:
all: $all all: $all
@ -18,7 +22,7 @@ collections:
# Settings for each collection # Settings for each collection
use_js_obj_proxy: true use_js_obj_proxy: true
# Memento support, enable # Eanable Memento support
enable_memento: true enable_memento: true
# Replay content in an iframe # Replay content in an iframe
@ -26,11 +30,10 @@ framed_replay: true
redirect_to_exact: 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
# uncomment to set available locales # Uncomment to set available locales
locales: # locales:
- en # - en
- ru # - ru

View File

@ -432,7 +432,8 @@ class FrontEndApp(object):
return WbResponse.bin_stream(StreamIter(res.raw), return WbResponse.bin_stream(StreamIter(res.raw),
content_type=content_type, content_type=content_type,
status=status_line) status=status_line,
headers=[("Cache-Control", "max-age=86400, must-revalidate")])
except Exception as e: except Exception as e:
return WbResponse.text_response('Error: ' + str(e), status='400 Bad Request') return WbResponse.text_response('Error: ' + str(e), status='400 Bad Request')

View File

@ -1,191 +0,0 @@
#_wb_frame_top_banner
{
display: block !important;
top: 0px !important;
left: 0px !important;
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif !important;
width: 100% !important;
font-size: 18px !important;
background-color: #444 !important;
color: white !important;
z-index: 2147483643 !important;
line-height: normal !important;
position: absolute !important;
border: 0px;
height: 44px !important;
display: flex !important;
display: -webkit-box !important;
display: -moz-box !important;
display: -webkit-flex !important;
display: -ms-flexbox !important;
justify-content: space-between;
-webkit-box-pack: justify;
-moz-box-pack: justify;
-ms-flex-pack: justify;
align-items: center;
-webkit-box-align: center;
-moz-box-align: center;
-ms-flex-align: center;
}
#title_or_url
{
display: block !important;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
#_wb_frame_top_banner ._wb_linked_logo
{
display: block;
height: 26px;
width: 71px;
margin-left: 15px;
flex-shrink: 0;
-webkit-flex-shrink: 1 0;
-moz-flex-shrink: 1 0;
-ms-flex: 0 0 71px;
}
#_wb_frame_top_banner ._wb_linked_logo img
{
width: auto;
height: 100%;
border: none;
}
#_wb_capture_info
{
flex-grow: 1;
-webkit-box-flex: 1;
-moz-box-flex: 1;
-webkit-flex-grow: 1;
-ms-flex: 1;
min-width: 0;
margin: 0 15px;
display: flex !important;
display: -webkit-box !important;
display: -moz-box !important;
display: -webkit-flex !important;
display: -ms-flexbox !important;
flex-direction: column;
-webkit-box-direction: normal;
-webkit-box-orient: vertical;
-moz-box-direction: normal;
-moz-box-orient: vertical;
-ms-flex-direction: column;
justify-content: center;
-webkit-box-pack: center;
-moz-box-pack: center;
-ms-flex-pack: center;
align-items: center;
-webkit-box-align: center;
-moz-box-align: center;
-ms-flex-align: center;
height: 100%;
-webkit-font-smoothing: antialiased;
}
._wb_capture_date
{
font-size: 13px;
}
#_wb_frame_top_banner #_wb_ancillary_links
{
font-size: 12px;
color: #FFF;
text-align: right;
margin: 0px 15px 0px 0px;
padding: inherit;
background-color: inherit;
width: initial;
flex-shrink: 1;
-webkit-flex-shrink: 1;
-moz-flex-shrink: 1;
-ms-flex: 0 0 115px;
}
#_wb_frame_top_banner #_wb_ancillary_links a:link,
#_wb_frame_top_banner #_wb_ancillary_links a:visited,
#_wb_frame_top_banner #_wb_ancillary_links a:active
{
color: #FFF;
text-decoration: none;
}
#_wb_frame_top_banner #_wb_ancillary_links a:hover
{
text-decoration: underline;
}
#_wb_frame_top_banner #_wb_ancillary_links a img
{
width: 10px;
height: 10px;
}
#wb_iframe_div
{
position: absolute;
width: 100%;
height: 100%;
padding: 44px 0px 0px 0px;
border: none;
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
overflow: hidden;
}
.wb_iframe
{
width: 100%;
height: 100%;
border: 2px solid #FFF;
border-width: 2px 0 0 0;
padding: 0px 0px 0px 0px;
overflow: scroll;
}
._wb_mobile {
display: none;
}
@media screen and (max-width: 500px) {
#_wb_frame_top_banner ._wb_linked_logo
{
width: 26px;
height: 26px;
margin-left: 10px;
}
#_wb_frame_top_banner ._wb_linked_logo img:not(._wb_mobile)
{
display: none;
}
#_wb_frame_top_banner ._wb_mobile
{
display: block;
}
#_wb_capture_info
{
margin: 0 5px;
}
#_wb_frame_top_banner ._wb_no-mobile
{
display: none;
}
}

View File

@ -1,328 +0,0 @@
/*
Copyright(c) 2013-2018 Rhizome and Ilya Kreymer. Released under the GNU General Public License.
This file is part of pywb, https://github.com/webrecorder/pywb
pywb is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pywb is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with pywb. If not, see <http://www.gnu.org/licenses/>.
*/
// Creates the default pywb banner.
(function() {
try {
if (window.parent !== window && window.parent.wbinfo) {
return;
}
} catch (e) { }
/**
* The default banner class
*/
function DefaultBanner() {
if (!(this instanceof DefaultBanner)) return new DefaultBanner();
this.banner = null;
this.captureInfo = null;
this.last_state = {};
this.state = null;
this.title = '';
this.bannerUrlSet = false;
this.onMessage = this.onMessage.bind(this);
}
// Functions required to be exposed by all banners
/**
* @desc Initialize (display) the banner
*/
DefaultBanner.prototype.init = function() {
this.createBanner('_wb_frame_top_banner');
if (window.wbinfo) {
this.set_banner(
window.wbinfo.url,
window.wbinfo.timestamp,
window.wbinfo.is_live,
window.wbinfo.is_framed ? '' : document.title
);
}
};
/**
* @desc Called by ContentFrame to detect if the banner is still showing
* that the page is loading
* @returns {boolean}
*/
DefaultBanner.prototype.stillIndicatesLoading = function() {
return !this.bannerUrlSet;
};
/**
* @param {string} url - The URL of the replayed page
* @param {?string} ts - The timestamp of the replayed page.
* If we are in live mode this is undefined/empty string
* @param {boolean} is_live - A bool indicating if we are operating in live mode
*/
DefaultBanner.prototype.updateCaptureInfo = function(url, ts, is_live) {
if (is_live && !ts) {
ts = new Date().toISOString().replace(/[-T:.Z]/g, '');
}
this.set_banner(url, ts, is_live, null);
};
/**
* @desc Called by ContentFrame when a message is received from the replay iframe
* @param {MessageEvent} event - The message event containing the message received
* from the replayed page
*/
DefaultBanner.prototype.onMessage = function(event) {
var type = event.data.wb_type;
if (type === 'load' || type === 'replace-url') {
this.state = event.data;
this.last_state = this.state;
this.title = event.data.title || this.title;
} else if (type === 'title') {
this.state = this.last_state;
this.title = event.data.title;
} else {
return;
}
// favicon update
if (type === 'load') {
var head = document.querySelector('head');
var oldLink = document.querySelectorAll("link[rel*='icon']");
var i = 0;
for (; i < oldLink.length; i++) {
head.removeChild(oldLink[i]);
}
if (this.state.icons) {
for (i = 0; i < this.state.icons.length; i++) {
var icon = this.state.icons[i];
var link = document.createElement('link');
link.rel = icon.rel;
link.href = icon.href;
head.appendChild(link);
}
}
}
this.set_banner(
this.state.url,
this.state.ts,
this.state.is_live,
this.title
);
};
// Functions internal to the default banner
/**
* @desc Navigate to different language, if available
*/
DefaultBanner.prototype.changeLanguage = function(lang, evt) {
evt.preventDefault();
var path = window.location.href;
if (path.indexOf(window.banner_info.prefix) == 0) {
path = path.substring(window.banner_info.prefix.length);
if (window.banner_info.locale_prefixes && window.banner_info.locale_prefixes[lang]) {
window.location.pathname = window.banner_info.locale_prefixes[lang] + path;
}
}
}
/**
* @desc Creates the underlying HTML elements comprising the banner
* @param {string} bid - The id for the banner
*/
DefaultBanner.prototype.createBanner = function(bid) {
this.header = document.createElement('header');
this.header.setAttribute('role', 'banner');
this.nav = document.createElement('nav');
this.banner = document.createElement('wb_div', true);
this.banner.setAttribute('id', bid);
this.banner.setAttribute('lang', 'en');
if (window.banner_info.logoImg) {
var logo = document.createElement("a");
logo.setAttribute("href", "/" + (window.banner_info.curr_locale ? window.banner_info.curr_locale + "/" : ""));
logo.setAttribute("class", "_wb_linked_logo");
var logoContents = "";
var logoUrl = window.banner_info.staticPrefix + "/" + window.banner_info.logoImg;
logoContents += "<img src='" + logoUrl + "' alt='" + window.banner_info.logoAlt + "'>";
logoContents += "<img src='" + logoUrl + "' class='_wb_mobile' alt='" + window.banner_info.logoAlt + "'>";
logo.innerHTML = logoContents;
this.banner.appendChild(logo);
}
this.captureInfo = document.createElement("span");
this.captureInfo.setAttribute("id", "_wb_capture_info");
this.captureInfo.innerHTML = window.banner_info.loadingLabel;
this.banner.appendChild(this.captureInfo);
var ancillaryLinks = document.createElement("div");
ancillaryLinks.setAttribute("id", "_wb_ancillary_links");
var calendarImg = window.banner_info.calendarImg || window.banner_info.staticPrefix + "/calendar.svg";
var calendarLink = document.createElement("a");
calendarLink.setAttribute("id", "calendarLink");
calendarLink.setAttribute("href", "#");
calendarLink.innerHTML = "<img src='" + calendarImg + "' alt='" + window.banner_info.calendarAlt + "'><span class='_wb_no-mobile'>&nbsp;" +window.banner_info.calendarLabel + "</span>";
ancillaryLinks.appendChild(calendarLink);
this.calendarLink = calendarLink;
if (typeof window.banner_info.locales !== "undefined" && window.banner_info.locales.length > 1) {
var locales = window.banner_info.locales;
var languages = document.createElement("div");
var label = document.createElement("span");
label.setAttribute("class", "_wb_no-mobile");
label.appendChild(document.createTextNode(window.banner_info.choiceLabel + " "));
languages.appendChild(label);
for(var i = 0; i < locales.length; i++) {
var locale = locales[i];
var langLink = document.createElement("a");
langLink.setAttribute("href", "#");
langLink.addEventListener("click", this.changeLanguage.bind(this, locale));
langLink.appendChild(document.createTextNode(locale));
languages.appendChild(langLink);
if (i !== locales.length - 1) {
languages.appendChild(document.createTextNode(" / "));
}
}
ancillaryLinks.appendChild(languages);
}
this.banner.appendChild(ancillaryLinks);
this.nav.appendChild(this.banner);
this.header.appendChild(this.nav);
document.body.insertBefore(this.header, document.body.firstChild);
};
/**
* @desc Converts a timestamp to a date string. If is_gmt is truthy then
* the returned data string will be the results of date.toGMTString otherwise
* its date.toLocaleString()
* @param {?string} ts - The timestamp to receive the correct date string for
* @param {boolean} is_gmt - Is the returned date string to be in GMT time
* @returns {string}
*/
DefaultBanner.prototype.ts_to_date = function(ts, is_gmt) {
if (!ts) {
return '';
}
if (ts.length < 14) {
ts += '00000000000000'.substr(ts.length);
}
var datestr =
ts.substring(0, 4) +
'-' +
ts.substring(4, 6) +
'-' +
ts.substring(6, 8) +
'T' +
ts.substring(8, 10) +
':' +
ts.substring(10, 12) +
':' +
ts.substring(12, 14) +
'-00:00';
var date = new Date(datestr);
if (is_gmt) {
return date.toGMTString();
} else {
return date.toLocaleString(window.banner_info.locale);
}
};
/**
* @desc Updates the contents displayed by the banner
* @param {?string} url - The URL of the replayed page to be displayed in the banner
* @param {?string} ts - A timestamp to be displayed in the banner
* @param {boolean} is_live - Are we in live mode
* @param {?string} title - The title of the replayed page to be displayed in the banner
*/
DefaultBanner.prototype.set_banner = function(url, ts, is_live, title) {
var capture_str;
var title_str;
if (!url) {
this.captureInfo.innerHTML = window.banner_info.loadingLabel;
this.bannerUrlSet = false;
return;
}
if (!ts) {
return;
}
if (title) {
capture_str = title;
} else {
capture_str = url;
}
title_str = capture_str;
capture_str = "<b id='title_or_url' title='" + capture_str + "'>" + capture_str + "</b>";
capture_str += "<span class='_wb_capture_date'>";
if (is_live) {
title_str = window.banner_info.liveMsg + " " + title_str;
capture_str += "<b>" + window.banner_info.liveMsg + "&nbsp;</b>";
}
capture_str += this.ts_to_date(ts, window.banner_info.is_gmt);
capture_str += "</span>";
this.calendarLink.setAttribute("href", window.banner_info.prefix + "*/" + url);
this.calendarLink.style.display = is_live ? "none" : "";
this.captureInfo.innerHTML = capture_str;
window.document.title = title_str;
this.bannerUrlSet = true;
};
// all banners will expose themselves by adding themselves as WBBanner on window
window.WBBanner = new DefaultBanner();
// if wbinfo.url is set and not-framed, init banner in content frame
if (window.wbinfo && window.wbinfo.url && !window.wbinfo.is_framed) {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function() {
window.WBBanner.init();
});
} else {
window.WBBanner.init();
}
}
})();

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,4 @@
body {
display: flex;
flex-direction: column;
}
#wb_iframe_div, #replay_iframe { #wb_iframe_div, #replay_iframe {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }

View File

@ -22,23 +22,11 @@ window.banner_info = {
logoImg: "{{ ui.logo }}" logoImg: "{{ ui.logo }}"
}; };
</script> </script>
{% if is_framed or not ui.vue_timeline_banner %}
<!-- default banner, create through js -->
<link rel='stylesheet' href='{{ static_prefix }}/default_banner.css'/>
<script src='{{ static_prefix }}/default_banner.js'> </script>
{% else %}
{% if ui.vue_timeline_banner %}
<script src="{{ static_prefix }}/loading-spinner/loading-spinner.js"></script> <script src="{{ static_prefix }}/loading-spinner/loading-spinner.js"></script>
<link rel='stylesheet' href='{{ static_prefix }}/vue_banner.css'/>
<script src="{{ static_prefix }}/vue/vueui.js"></script> <script src="{{ static_prefix }}/vue/vueui.js"></script>
{% endif %} <link rel="stylesheet" href='{{ static_prefix }}/vue_banner.css'/>
{% endif %}
{% include 'bootstrap_jquery.html' ignore missing %}
{% endautoescape %} {% endautoescape %}
{% endif %} {% endif %}

View File

@ -6,13 +6,7 @@
<title>{% block title %}{% endblock %}</title> <title>{% block title %}{% endblock %}</title>
<!-- jquery and bootstrap dependencies query view --> {% include 'bootstrap_jquery.html' ignore missing %}
<link rel="stylesheet" href="{{ static_prefix }}/css/bootstrap.min.css"/>
<link rel="stylesheet" href="{{ static_prefix }}/css/font-awesome.min.css">
<link rel="stylesheet" href="{{ static_prefix }}/css/base.css">
<script src="{{ static_prefix }}/js/jquery-latest.min.js"></script>
<script src="{{ static_prefix }}/js/bootstrap.min.js"></script>
{% block head %} {% block head %}
{% include 'head.html' ignore missing %} {% include 'head.html' ignore missing %}

View File

@ -0,0 +1,6 @@
<link rel="stylesheet" href="{{ static_prefix }}/css/bootstrap.min.css"/>
<link rel="stylesheet" href="{{ static_prefix }}/css/font-awesome.min.css">
<link rel="stylesheet" href="{{ static_prefix }}/css/base.css">
<script src="{{ static_prefix }}/js/jquery-latest.min.js"></script>
<script src="{{ static_prefix }}/js/bootstrap.min.js"></script>

View File

@ -18,22 +18,16 @@ html, body
{{ banner_html }} {{ banner_html }}
{% if ui.vue_timeline_banner %}
{% include 'vue_loc.html' %} {% include 'vue_loc.html' %}
{% endif %}
</head> </head>
<body style="margin: 0px; padding: 0px;"> <body style="margin: 0px; padding: 0px;">
{% if ui.vue_timeline_banner %}
<div id="app" style="width: 100%; height: 200px"></div> <div id="app" style="width: 100%; height: 200px"></div>
<script> <script>
VueUI.main("{{ static_prefix }}", "{{ url }}", "{{ wb_prefix }}", "{{ timestamp }}", "{{ ui.logo }}", "{{ env.pywb_lang | default('en') }}", VueUI.main("{{ static_prefix }}", "{{ url }}", "{{ wb_prefix }}", "{{ timestamp }}", "{{ ui.logo }}", "{{ ui.navbar_background_hex | default('f8f9fa') }}", "{{ ui.navbar_color_hex | default('212529') }}", "{{ ui.navbar_light_buttons }}", "{{ env.pywb_lang | default('en') }}",
allLocales, i18nStrings); allLocales, i18nStrings);
</script> </script>
{% endif %}
<div id="wb_iframe_div"> <div id="wb_iframe_div">
<iframe id="replay_iframe" frameborder="0" seamless="seamless" scrolling="yes" class="wb_iframe" allow="autoplay; fullscreen"></iframe> <iframe id="replay_iframe" frameborder="0" seamless="seamless" scrolling="yes" class="wb_iframe" allow="autoplay; fullscreen"></iframe>

View File

@ -7,40 +7,17 @@
{% block head %} {% block head %}
{{ super() }} {{ super() }}
{% if not ui.vue_calendar_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 }}/loading-spinner/loading-spinner.js"></script> <script src="{{ static_prefix }}/loading-spinner/loading-spinner.js"></script>
<script src="{{ static_prefix }}/vue/vueui.js"></script> <script src="{{ static_prefix }}/vue/vueui.js"></script>
{% include 'vue_loc.html' %} {% include 'vue_loc.html' %}
{% endif %}
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{% if not ui.vue_calendar_ui %}
<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>
{% endif %}
{% if ui.vue_calendar_ui %}
<div id="app" style="width: 100%; height: 100%"></div> <div id="app" style="width: 100%; height: 100%"></div>
{% endif %}
<script> <script>
var text = { var text = {
@ -75,17 +52,9 @@
}, },
}; };
{% if not ui.vue_calendar_ui %} VueUI.main("{{ static_prefix }}", "{{ url }}", "{{ prefix }}", undefined, "{{ ui.logo }}", "{{ ui.navbar_background_hex | default('f8f9fa') }}", "{{ ui.navbar_color_hex | default('212529') }}", "{{ ui.navbar_light_buttons }}", "{{ env.pywb_lang | default('en') }}",
var renderCal = new RenderCalendar({ prefix: "{{ prefix }}", staticPrefix: "{{ static_prefix }}", text: text });
renderCal.init();
{% else %}
VueUI.main("{{ static_prefix }}", "{{ url }}", "{{ prefix }}", undefined, "{{ ui.logo }}", "{{ env.pywb_lang | default('en') }}",
allLocales, i18nStrings); allLocales, i18nStrings);
{% endif %}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,4 +1,4 @@
__version__ = '2.7.0b1' __version__ = '2.7.0b2'
if __name__ == '__main__': if __name__ == '__main__':
print(__version__) print(__version__)

View File

@ -1,36 +1,126 @@
<template> <template>
<div class="app" :class="{expanded: showTimelineView}" data-app="webrecorder-replay-app"> <div class="app" :class="{expanded: showTimelineView || showFullView }" data-app="webrecorder-replay-app">
<div class="banner"> <!-- Top navbar -->
<div class="line"> <nav
<div class="logo"><a href="/"><img :src="config.logoImg"/></a></div> class="navbar navbar-light navbar-expand-lg fixed-top top-navbar justify-content-center"
<div class="timeline-wrap"> :style="navbarStyle">
<div class="line"> <a class="navbar-brand flex-grow-1 my-1" href="/">
<div class="breadcrumbs-wrap"> <img :src="config.logoImg" id="logo-img" alt="_('pywb logo')">
<TimelineBreadcrumbs </a>
v-if="currentPeriod && showTimelineView" <div class="flex-grow-1 d-flex" id="searchdiv">
:period="currentPeriod" <form class="form-inline my-2 my-md-0 mx-lg-auto" @submit="gotoUrl">
@goto-period="gotoPeriod" <input id="theurl" type="text" :value="config.url" height="31"></input>
></TimelineBreadcrumbs> </form>
<span v-if="!showTimelineView" v-html="'&nbsp;'"></span><!-- for spacing --> </div>
<button
class="navbar-toggler btn btn-sm"
id="collapse-button"
type="button"
data-toggle="collapse"
data-target="#navbarCollapse"
aria-controls="navbarCollapse"
aria-expanded="false"
aria-label="_('Toggle navigation')">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse ml-auto" id="navbarCollapse">
<ul class="navbar-nav ml-3" id="toggles">
<li class="nav-item">
<button
class="btn btn-sm"
:class="{active: showFullView, 'btn-outline-light': lightButtons, 'btn-outline-dark': !lightButtons}"
:title="_('Previous capture')"
v-if="previousSnapshot"
@click="gotoPreviousSnapshot">
<i class="fas fa-arrow-left" :title="_('Previous capture')"></i>
</button>
</li>
<li class="nav-item">
<button
class="btn btn-sm"
:class="{active: showFullView, 'btn-outline-light': lightButtons, 'btn-outline-dark': !lightButtons}"
:title="_('Next capture')"
v-if="nextSnapshot"
@click="gotoNextSnapshot">
<i class="fas fa-arrow-right" :title="_('Next capture')"></i>
</button>
</li>
<li class="nav-item active">
<button
class="btn btn-sm"
:class="{active: showFullView, 'btn-outline-light': lightButtons, 'btn-outline-dark': !lightButtons}"
:aria-pressed="(showFullView ? true : false)"
@click="showFullView = !showFullView"
:title="(showFullView ? _('Hide calendar') : _('Show calendar'))">
<i class="far fa-calendar-alt" :title="_('Calendar')"></i>
</button>
</li>
<li class="nav-item">
<button
class="btn btn-sm"
:class="{active: showTimelineView, 'btn-outline-light': lightButtons, 'btn-outline-dark': !lightButtons}"
:aria-pressed="showTimelineView"
@click="showTimelineView = !showTimelineView"
:title="(showTimelineView ? _('Hide timeline') : _('Show timeline'))">
<i class="far fa-chart-bar" :title="_('Timeline')"></i>
</button>
</li>
<li class="nav-item dropdown" v-if="localesAreSet">
<button
class="btn btn-sm dropdown-toggle"
:class="{'btn-outline-light': lightButtons, 'btn-outline-dark': !lightButtons}"
type="button"
id="locale-dropdown"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
:title="_('Select language')">
<i class="fas fa-globe-africa" :title="_('Language')"></i>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="locale-dropdown">
<a
class="dropdown-item"
v-for="(locPath, key) in config.allLocales"
:key="key"
:href="locPath + (currentSnapshot ? currentSnapshot.id : '*') + '/' + config.url">
{{ key }}
</a>
</div> </div>
<div class="toggles">
<span class="toggle" :class="{expanded: showFullView}" @click="showFullView = !showFullView" :title="(showTimelineView ? _('show calendar'):_('hide calendar'))">
<img src="/static/calendar-icon.png" />
</span>
<span class="toggle" :class="{expanded: showTimelineView}" @click="showTimelineView = !showTimelineView" :title="(showTimelineView ? _('show timeline'):_('hide timeline'))">
<img src="/static/timeline-icon.png" />
</span>
<ul class="lang-select" role="listbox" :aria-activedescendant="config.locale"
:aria-labelledby="_('Language select')">
<li v-for="(locPath, key) in config.allLocales" role="option" :id="key">
<a :href="locPath + (currentSnapshot ? currentSnapshot.id : '*') + '/' + config.url">{{ key }}</a>
</li> </li>
</ul> </ul>
</div> </div>
</nav>
<!-- Capture title and date -->
<nav
class="navbar navbar-light justify-content-center title-nav fixed-top"
id="second-navbar"
:style="navbarStyle">
<span class="hidden" v-if="!currentSnapshot">&nbsp;</span>
<span v-if="currentSnapshot">
<span class="strong mr-1">
{{_('Current Capture')}}:
<span class="ml-1" v-if="config.title">
{{ config.title }}
</span>
</span>
<span class="mr-1" v-if="config.title">,</span>
{{currentSnapshot.getTimeDateFormatted()}}
</span>
</nav>
<!-- Timeline -->
<div class="card border-top-0 border-left-0 border-right-0 timeline-wrap">
<div class="card-body" v-if="currentPeriod && showTimelineView">
<div class="row">
<div class="col col-12">
<TimelineBreadcrumbs
:period="currentPeriod"
@goto-period="gotoPeriod"
></TimelineBreadcrumbs>
</div> </div>
<div class="col col-12 mt-2">
<Timeline <Timeline
v-if="currentPeriod && showTimelineView"
:period="currentPeriod" :period="currentPeriod"
:highlight="timelineHighlight" :highlight="timelineHighlight"
:current-snapshot="currentSnapshot" :current-snapshot="currentSnapshot"
@ -40,21 +130,20 @@
</div> </div>
</div> </div>
</div> </div>
<div class="snapshot-title">
<form @submit="gotoUrl">
<input id="theurl" type="text" :value="config.url"></input>
</form>
<div v-if="currentSnapshot && !showFullView">
<span v-if="config.title">{{ config.title }}</span>
{{_('Current Capture')}}: {{currentSnapshot.getTimeDateFormatted()}}
</div> </div>
</div>
<CalendarYear v-if="showFullView && currentPeriod && currentPeriod.children.length" <!-- Calendar -->
<div class="card border-0" v-if="currentPeriod && showFullView && currentPeriod.children.length">
<div class="card-body">
<CalendarYear
:period="currentPeriod" :period="currentPeriod"
:current-snapshot="currentSnapshot" :current-snapshot="currentSnapshot"
@goto-period="gotoPeriod"> @goto-period="gotoPeriod">
</CalendarYear> </CalendarYear>
</div> </div>
</div>
</div>
</template> </template>
<script> <script>
@ -73,13 +162,15 @@ export default {
snapshots: [], snapshots: [],
currentPeriod: null, currentPeriod: null,
currentSnapshot: null, currentSnapshot: null,
currentSnapshotIndex: null,
msgs: [], msgs: [],
showFullView: true, showFullView: true,
showTimelineView: true, showTimelineView: true,
maxTimelineZoomLevel: PywbPeriod.Type.day, maxTimelineZoomLevel: PywbPeriod.Type.day,
config: { config: {
title: "", title: "",
initialView: {} initialView: {},
allLocales: {}
}, },
timelineHighlight: false, timelineHighlight: false,
locales: [], locales: [],
@ -87,11 +178,47 @@ export default {
}, },
components: {Timeline, TimelineBreadcrumbs, CalendarYear}, components: {Timeline, TimelineBreadcrumbs, CalendarYear},
mounted: function() { mounted: function() {
// add empty unload event listener to make this page bfcache ineligible.
// bfcache otherwises prevent the query template from reloading as expected
// when the user navigates there via browser back/forward buttons
addEventListener('unload', (event) => { });
}, },
computed: { computed: {
sessionStorageUrlKey() { sessionStorageUrlKey() {
// remove http(s), www and trailing slash // remove http(s), www and trailing slash
return 'zoom__' + this.config.url.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, ''); return 'zoom__' + this.config.url.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '');
},
localesAreSet() {
return Object.entries(this.config.allLocales).length > 0;
},
navbarStyle() {
return {
'--navbar-background': `#${this.config.navbarBackground}`,
'--navbar-color': `#${this.config.navbarColor}`
}
},
lightButtons() {
return !!this.config.navbarLightButtons;
},
previousSnapshot() {
if (!this.currentSnapshotIndex) {
return null;
}
if (this.currentSnapshotIndex > 0) {
return this.snapshots[this.currentSnapshotIndex - 1];
}
return null;
},
nextSnapshot() {
if (this.currentSnapshotIndex == null) {
return null;
}
if (
(this.currentSnapshotIndex >= 0)
&& (this.currentSnapshotIndex !== this.snapshots.length - 1)) {
return this.snapshots[this.currentSnapshotIndex + 1];
}
return null;
} }
}, },
methods: { methods: {
@ -124,10 +251,13 @@ export default {
gotoSnapshot(snapshot, fromPeriod, reloadIFrame=false) { gotoSnapshot(snapshot, fromPeriod, reloadIFrame=false) {
this.currentSnapshot = snapshot; this.currentSnapshot = snapshot;
const isCurrentSnapshot = (snapshotInArray) => snapshotInArray.id == snapshot.id && snapshotInArray.url == snapshot.url;
this.currentSnapshotIndex = this.snapshots.findIndex(isCurrentSnapshot);
// if the current period doesn't match the current snapshot, update it // if the current period doesn't match the current snapshot, update it
if (fromPeriod && !this.currentPeriod.contains(fromPeriod)) { if (!this.currentPeriod || (fromPeriod && !this.currentPeriod.contains(fromPeriod))) {
const fromPeriodAtMaxZoomLevel = fromPeriod.get(this.maxTimelineZoomLevel); const fromPeriodAtMaxZoomLevel = fromPeriod.get(this.maxTimelineZoomLevel);
if (fromPeriodAtMaxZoomLevel !== this.currentPeriod) { if (!this.currentPeriod || fromPeriodAtMaxZoomLevel !== this.currentPeriod) {
this.currentPeriod = fromPeriodAtMaxZoomLevel; this.currentPeriod = fromPeriodAtMaxZoomLevel;
} }
} }
@ -138,7 +268,15 @@ export default {
if (reloadIFrame !== false) { if (reloadIFrame !== false) {
this.$emit("show-snapshot", snapshot); this.$emit("show-snapshot", snapshot);
} }
this.showFullView = false; this.hideBannerUtilities();
},
gotoPreviousSnapshot() {
let periodToChangeTo = this.currentPeriod.findByFullId(this.previousSnapshot.getFullId());
this.gotoPeriod(periodToChangeTo, false /* onlyZoomToPeriod */);
},
gotoNextSnapshot() {
let periodToChangeTo = this.currentPeriod.findByFullId(this.nextSnapshot.getFullId());
this.gotoPeriod(periodToChangeTo, false /* onlyZoomToPeriod */);
}, },
gotoUrl(event) { gotoUrl(event) {
event.preventDefault(); event.preventDefault();
@ -176,130 +314,140 @@ export default {
// convert to snapshot object to support proper rendering of time/date // convert to snapshot object to support proper rendering of time/date
const snapshot = new PywbSnapshot(view, 0); const snapshot = new PywbSnapshot(view, 0);
// set config current URL and title
this.config.url = view.url; this.config.url = view.url;
this.config.title = view.title;
this.gotoSnapshot(snapshot); let periodToChangeTo = this.currentPeriod.findByFullId(snapshot.getFullId());
this.gotoPeriod(periodToChangeTo, false /* onlyZoomToPeriod */);
},
setTimelineView() {
this.showTimelineView = !this.showTimelineView;
if (this.showTimelineView === true) {
this.showFullView = false;
}
},
hideBannerUtilities() {
this.showFullView = false;
this.showTimelineView = false;
},
updateTitle(title) {
this.config.title = title;
} }
} }
}; };
</script> </script>
<style> <style>
body {
padding-top: 89px !important;
}
.app { .app {
font-family: Calibri, Arial, sans-serif; font-family: Calibri, Arial, sans-serif;
border-bottom: 1px solid lightcoral; /*border-bottom: 1px solid lightcoral;*/
width: 100%; width: 100%;
} }
.app.expanded { .app.expanded {
height: 150px; height: 130px;
} }
.full-view { .full-view {
/*position: fixed;*/ /*position: fixed;*/
/*top: 150px;*/ /*top: 150px;*/
left: 0; left: 0;
} }
.navbar {
background-color: var(--navbar-background);
color: var(--navbar-color);
}
.top-navbar {
z-index: 90;
padding: 2px 16px 0 16px;
}
.top-navbar span.navbar-toggler-icon {
margin: .25rem !important;
}
#logo-img {
max-height: 40px;
}
.title-nav {
margin-top: 50px;
z-index: 80;
}
#secondNavbar {
height: 24px !important;
}
#navbarCollapse {
justify-content: right;
}
#navbarCollapse ul#toggles {
display: flex;
align-content: center;
}
#navbarCollapse:not(.show) ul#toggles li:not(:first-child) {
margin-left: .25rem;
}
#navbarCollapse.show {
padding-bottom: 1em;
}
#navbarCollapse.show ul#toggles li {
margin-top: 5px;
}
#navbarCollapse.show ul#toggles li {
margin-left: 0px;
}
.iframe iframe { .iframe iframe {
width: 100%; width: 100%;
height: 80vh; height: 80vh;
} }
.logo { #searchdiv {
margin-right: 30px; height: 31px;
width: 180px;
} }
.banner { #theurl {
width: 100%; width: 250px;
max-width: 1200px; /* limit width */
position: relative;
} }
.banner .line { @media (min-width: 576px) {
display: flex; #theurl {
justify-content: flex-start; width: 350px;
} }
}
.banner .logo { @media (min-width: 768px) {
flex-shrink: initial; #theurl {
/* for any content/image inside the logo container */ width: 500px;
}
}
@media (min-width: 992px) {
#theurl {
width: 600px;
}
}
@media (min-width: 1200px) {
#theurl {
width: 900px;
}
}
#toggles {
align-items: center;
}
.breadcrumb-row {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.banner .logo img { div.timeline-wrap div.card {
flex-shrink: 1; margin-top: 55px;
} }
div.timeline-wrap div.card-body {
.banner .timeline-wrap { display: flex;
flex-grow: 2; align-items: center;
overflow-x: hidden; justify-content: center;
text-align: left;
margin: 0 25px;
position: relative;
} }
div.timeline-wrap div.card-body div.row {
.timeline-wrap .line .breadcrumbs-wrap { width: 100%;
display: inline-block; align-items: center;
flex-grow: 1; justify-content: center;
} }
.timeline-wrap .line .toggles { .strong {
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-weight: bold;
font-size: 16px;
} }
#theurl { .hidden {
width: 400px; color: var(--navbar-background);
} }
ul.lang-select {
display: inline-block;
list-style-type: none;
margin: 0;
padding: 0 24px 0 8px;
}
ul.lang-select li {
display: inline-block;
padding-left: 6px;
font-weight: bold;
font-size: smaller;
}
ul.lang-select li:not(:last-child):after {
content: ' / ';
}
ul.lang-select a:link,
ul.lang-select a:visited,
ul.lang-select a:active {
text-decoration: none;
}
ul.lang-select a:hover {
text-decoration: underline;
}
</style> </style>

View File

@ -9,14 +9,13 @@
text-align: center; text-align: center;
vertical-align: top; vertical-align: top;
box-sizing: content-box; box-sizing: content-box;
border-radius: 10px;
} }
.calendar-month:hover { .calendar-month:hover {
background-color: #eeeeee; background-color: #eeeeee;
border-radius: 10px;
} }
.calendar-month.current { .calendar-month.current {
background-color: #fff7ce; background-color: #fff7ce;
border-radius: 5px;
} }
.calendar-month.contains-current-snapshot { .calendar-month.contains-current-snapshot {
border: solid 1px red; border: solid 1px red;
@ -95,7 +94,7 @@
<h3>{{getLongMonthName(month.id)}} <span v-if="month.snapshotCount">({{ month.snapshotCount }})</span></h3> <h3>{{getLongMonthName(month.id)}} <span v-if="month.snapshotCount">({{ month.snapshotCount }})</span></h3>
<div v-if="month.snapshotCount"> <div v-if="month.snapshotCount">
<span v-for="(dayInitial) in dayInitials" class="day" :style="dayStyle">{{dayInitial}}</span><br/> <span v-for="(dayInitial) in dayInitials" class="day" :style="dayStyle">{{dayInitial}}</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">{{ $root._(day.snapshotCount !== 1 ? '{count} captures':'{count} capture', {count: day.snapshotCount}) }}</span></template><template v-else v-html="'&nbsp;'"></template></span></span> <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)" @keyup.13="gotoDay(day, $event)"><template v-if="day"><span class="size" v-if="day.snapshotCount" :style="getDayCountCircleStyle(day.snapshotCount)" tabindex="0"> </span><span class="day-id">{{day.id}}</span><span v-if="day.snapshotCount" class="count">{{ $root._(day.snapshotCount !== 1 ? '{count} captures':'{count} capture', {count: day.snapshotCount}) }}</span></template><template v-else v-html="'&nbsp;'"></template></span></span>
</div> </div>
<div v-else class="empty">{{ _('no captures') }}</div> <div v-else class="empty">{{ _('no captures') }}</div>
</div> </div>

View File

@ -1,29 +1,22 @@
<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> <template>
<div class="full-view"> <div class="full-view">
<h2>{{year.id}} ({{ $root._(year.snapshotCount !== 1 ? '{count} captures':'{count} capture', {count: year.snapshotCount}) }})</h2> <h2>
<i
class="fas fa-arrow-left year-arrow"
@click="gotoPreviousYear"
@keyup.enter="gotoPreviousYear"
v-if="previousYear"
tabindex="0"></i>
<span class="mx-1">
{{year.id}} ({{ $root._(year.snapshotCount !== 1 ? '{count} captures':'{count} capture', {count: year.snapshotCount}) }})
</span>
<i
class="fas fa-arrow-right year-arrow"
@click="gotoNextYear"
@keyup.enter="gotoNextYear"
v-if="nextYear"
tabindex="0"></i>
</h2>
<div class="months"> <div class="months">
<CalendarMonth <CalendarMonth
v-for="month in year.children" v-for="month in year.children"
@ -36,7 +29,10 @@
@show-day-timeline="setCurrentTimeline" @show-day-timeline="setCurrentTimeline"
></CalendarMonth> ></CalendarMonth>
</div> </div>
<Tooltip :position="currentTimelinePos" v-if="currentTimelinePeriod" ref="timelineLinearTooltip"> <Tooltip
:position="currentTimelinePos"
v-if="currentTimelinePeriod"
ref="timelineLinearTooltip">
<TimelineLinear <TimelineLinear
:period="currentTimelinePeriod" :period="currentTimelinePeriod"
:current-snapshot="containsCurrentSnapshot ? currentSnapshot : null" :current-snapshot="containsCurrentSnapshot ? currentSnapshot : null"
@ -86,6 +82,17 @@ export default {
} }
return year; return year;
}, },
currentYearIndex() {
if (this.year.parent) {
return this.year.parent.children.findIndex(year => year.fullId === this.year.fullId);
}
},
previousYear() {
return this.year.getPrevious();
},
nextYear() {
return this.year.getNext();
},
currentMonth() { // the month that the timeline period is in currentMonth() { // the month that the timeline period is in
let month = null; let month = null;
if (this.period.type === PywbPeriod.Type.month) { if (this.period.type === PywbPeriod.Type.month) {
@ -101,6 +108,12 @@ export default {
} }
}, },
methods: { methods: {
gotoPreviousYear() {
this.gotoPeriod(this.previousYear, true /* changeYearOnly */);
},
gotoNextYear() {
this.gotoPeriod(this.nextYear, true /* changeYearOnly */);
},
resetCurrentTimeline(event) { resetCurrentTimeline(event) {
if (event && this.$refs.timelineLinearTooltip) { if (event && this.$refs.timelineLinearTooltip) {
let el = event.target; let el = event.target;
@ -122,12 +135,18 @@ export default {
if (!day) { if (!day) {
return; return;
} }
if (event.code === "Enter") {
let middleXPos = (window.innerWidth / 2) - 60;
this.currentTimelinePos = `${middleXPos},200`;
} else {
this.currentTimelinePos = `${event.x},${event.y}`; this.currentTimelinePos = `${event.x},${event.y}`;
}
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
}, },
gotoPeriod(period) { gotoPeriod(period, changeYearOnly=false) {
if (period.snapshot || period.snapshotPeriod) { if (period.snapshot || period.snapshotPeriod || changeYearOnly) {
this.$emit('goto-period', period); this.$emit('goto-period', period);
} else { } else {
this.currentTimelinePeriod = period; this.currentTimelinePeriod = period;
@ -138,3 +157,27 @@ export default {
}; };
</script> </script>
<style scoped>
.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;
}
.year-arrow:hover {
cursor: pointer;
}
</style>

View File

@ -58,7 +58,7 @@ export default {
} }
.pywb-loading-spinner-mask { .pywb-loading-spinner-mask {
position: fixed; position: fixed;
top: 0; top: 10px;
left: 0; left: 0;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;

View File

@ -16,7 +16,12 @@
</div> </div>
</template> </template>
</div> </div>
<div v-html="'&#x25C0;'" class="arrow previous" :class="{disabled: isScrollZero && !previousPeriod}" @click="scrollPrev" @dblclick.stop.prevent></div> <div v-html="'&#x25C0;'"
class="arrow previous"
:class="{disabled: isScrollZero && !previousPeriod}"
@click="scrollPrev"
@keyup.enter="scrollPrev"
@dblclick.stop.prevent tabindex="0"></div>
<div class="scroll" ref="periodScroll" :class="{highlight: highlight}"> <div class="scroll" ref="periodScroll" :class="{highlight: highlight}">
<div class="periods" ref="periods"> <div class="periods" ref="periods">
<div v-for="subPeriod in period.children" <div v-for="subPeriod in period.children"
@ -31,16 +36,20 @@
:style="{height: getHistoLineHeight(histoPeriod.snapshotCount)}" :style="{height: getHistoLineHeight(histoPeriod.snapshotCount)}"
:class="{'has-single-snapshot': histoPeriod.snapshotCount === 1, 'contains-current-snapshot': containsCurrentSnapshot(histoPeriod)}" :class="{'has-single-snapshot': histoPeriod.snapshotCount === 1, 'contains-current-snapshot': containsCurrentSnapshot(histoPeriod)}"
@click="changePeriod(histoPeriod, $event)" @click="changePeriod(histoPeriod, $event)"
@keyup.enter="changePeriod(histoPeriod, $event)"
@mouseover="setTooltipPeriod(histoPeriod, $event)" @mouseover="setTooltipPeriod(histoPeriod, $event)"
@mouseout="setTooltipPeriod(null, $event)" @mouseout="setTooltipPeriod(null, $event)"
tabindex="0"
> >
</div> </div>
</div> </div>
<div class="inner" <div class="inner"
:class="{'has-single-snapshot': subPeriod.snapshotCount === 1}" :class="{'has-single-snapshot': subPeriod.snapshotCount === 1}"
@click="changePeriod(subPeriod, $event)" @click="changePeriod(subPeriod, $event)"
@keyup.enter="changePeriod(histoPeriod, $event)"
@mouseover="setTooltipPeriod(subPeriod, $event)" @mouseover="setTooltipPeriod(subPeriod, $event)"
@mouseout="setTooltipPeriod(null, $event)" @mouseout="setTooltipPeriod(null, $event)"
tabindex="0"
> >
<div class="label"> <div class="label">
{{subPeriod.getReadableId()}} {{subPeriod.getReadableId()}}
@ -49,7 +58,13 @@
</div> </div>
</div> </div>
</div> </div>
<div v-html="'&#x25B6;'" class="arrow next" :class="{disabled: isScrollMax && !nextPeriod}" @click="scrollNext" @dblclick.stop.prevent></div> <div
v-html="'&#x25B6;'"
class="arrow next"
:class="{disabled: isScrollMax && !nextPeriod}"
@click="scrollNext"
@keyup.enter="scrollNext"
@dblclick.stop.prevent tabindex="0"></div>
</div> </div>
</template> </template>

View File

@ -2,13 +2,23 @@
<div class="breadcrumbs"> <div class="breadcrumbs">
<template v-if="parents.length"> <template v-if="parents.length">
<span class="item"> <span class="item">
<span class="goto" @click="changePeriod(parents[0])" :title="getPeriodZoomOutText(parents[0])"> <span
class="goto"
@click="changePeriod(parents[0])"
@keyup.enter="changePeriod(parents[0])"
:title="getPeriodZoomOutText(parents[0])"
tabindex="1">
<img src="/static/zoom-out-icon-333316.png" /> {{parents[0].getReadableId(true)}} <img src="/static/zoom-out-icon-333316.png" /> {{parents[0].getReadableId(true)}}
</span> </span>
</span> </span>
&gt; &gt;
<span v-for="(parent,i) in parents" :key="parent.id" class="item" v-if="i > 0"> <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)"> <span
class="goto"
@click="changePeriod(parent)"
@keyup.enter="changePeriod(parent)"
:title="getPeriodZoomOutText(parent)"
tabindex="1">
{{parent.getReadableId(true)}} {{parent.getReadableId(true)}}
</span> </span>
</span> </span>

View File

@ -1,17 +1,23 @@
<template> <template>
<div class="timeline-linear"> <div class="timeline-linear">
<div class="title"> <div class="title">
<div>{{ period.getFullReadableId() }}</div> <div>{{ period.getFullReadableId() }}</div>
<div>{{ $root._(period.snapshotCount !== 1 ? '{count} captures':'{count} capture', {count: period.snapshotCount}) }}</div> <div>{{ $root._(period.snapshotCount !== 1 ? '{count} captures':'{count} capture', {count: period.snapshotCount}) }}</div>
</div> </div>
<div class="list"> <div class="list">
<div v-for="period in snapshotPeriods"> <div v-for="snapshotPeriod in snapshotPeriods">
<a :href="$root.config.prefix + period.id + '/' + $root.config.url" class="link" >{{period.snapshot.getTimeFormatted()}}</a> <span
@click="changePeriod(snapshotPeriod)"
@keyup.enter="changePeriod(snapshotPeriod)"
class="link"
tabindex="1">
{{ snapshotPeriod.snapshot.getTimeFormatted() }}
</span>
<span v-if="isCurrentSnapshot(period)" class="current">{{$root._('current')}}</span> <span v-if="isCurrentSnapshot(period)" class="current">{{$root._('current')}}</span>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
@ -29,8 +35,14 @@ export default {
}, },
methods: { methods: {
isCurrentSnapshot(period) { isCurrentSnapshot(period) {
if (!!this.currentSnapshot && !!period.snapshot) {
return this.currentSnapshot && this.currentSnapshot.id === period.snapshot.id; return this.currentSnapshot && this.currentSnapshot.id === period.snapshot.id;
} }
return false;
},
changePeriod(period) {
this.$emit("goto-period", period);
}
} }
} }
</script> </script>
@ -42,6 +54,7 @@ export default {
background-color: white; background-color: white;
border: 1px solid gray; border: 1px solid gray;
border-radius: 5px; border-radius: 5px;
z-index: 1100;
} }
.timeline-linear .list { .timeline-linear .list {
max-height: 80vh; max-height: 80vh;

View File

@ -50,7 +50,7 @@ export default {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
z-index: 40; z-index: 100;
background-color: white; background-color: white;
border: 1px solid grey; border: 1px solid grey;
border-radius: 5px; border-radius: 5px;

View File

@ -7,20 +7,23 @@ import Vue from "vue/dist/vue.esm.browser";
// =========================================================================== // ===========================================================================
export function main(staticPrefix, url, prefix, timestamp, logoUrl, locale, allLocales, i18nStrings) { export function main(staticPrefix, url, prefix, timestamp, logoUrl, navbarBackground, navbarColor, navbarLightButtons, locale, allLocales, i18nStrings) {
PywbI18N.init(locale, i18nStrings); PywbI18N.init(locale, i18nStrings);
new CDXLoader(staticPrefix, url, prefix, timestamp, logoUrl, allLocales); new CDXLoader(staticPrefix, url, prefix, timestamp, logoUrl, navbarBackground, navbarColor, navbarLightButtons, allLocales);
} }
// =========================================================================== // ===========================================================================
class CDXLoader { class CDXLoader {
constructor(staticPrefix, url, prefix, timestamp, logoUrl, allLocales) { constructor(staticPrefix, url, prefix, timestamp, logoUrl, navbarBackground, navbarColor, navbarLightButtons, allLocales) {
this.loadingSpinner = null; this.loadingSpinner = null;
this.loaded = false; this.loaded = false;
this.opts = {}; this.opts = {};
this.prefix = prefix; this.prefix = prefix;
this.staticPrefix = staticPrefix; this.staticPrefix = staticPrefix;
this.logoUrl = logoUrl; this.logoUrl = logoUrl;
this.navbarBackground = navbarBackground;
this.navbarColor = navbarColor;
this.navbarLightButtons = navbarLightButtons
this.isReplay = (timestamp !== undefined); this.isReplay = (timestamp !== undefined);
@ -56,7 +59,7 @@ class CDXLoader {
const logoImg = this.staticPrefix + "/" + (this.logoUrl ? this.logoUrl : "pywb-logo-sm.png"); const logoImg = this.staticPrefix + "/" + (this.logoUrl ? this.logoUrl : "pywb-logo-sm.png");
this.app = this.initApp({logoImg, url, allLocales}); this.app = this.initApp({logoImg, navbarBackground, navbarColor, navbarLightButtons, url, allLocales});
this.loadCDX(queryURL).then((cdxList) => { this.loadCDX(queryURL).then((cdxList) => {
this.setAppData(cdxList, timestamp ? {url, timestamp}:null); this.setAppData(cdxList, timestamp ? {url, timestamp}:null);
}); });
@ -107,6 +110,7 @@ class CDXLoader {
this.app.setData(new PywbData(cdxList)); this.app.setData(new PywbData(cdxList));
if (snapshot) { if (snapshot) {
this.app.hideBannerUtilities();
this.app.setSnapshot(snapshot); this.app.setSnapshot(snapshot);
} }
} }
@ -180,6 +184,10 @@ class VueBannerWrapper
if (type === "load" || type === "replace-url") { if (type === "load" || type === "replace-url") {
const surt = this.getSurt(event.data.url); const surt = this.getSurt(event.data.url);
if (event.data.title) {
this.loader.app.updateTitle(event.data.title);
}
if (surt !== this.lastSurt) { if (surt !== this.lastSurt) {
this.loader.updateSnapshot(event.data.url, event.data.ts); this.loader.updateSnapshot(event.data.url, event.data.ts);
this.lastSurt = surt; this.lastSurt = surt;

View File

@ -493,14 +493,14 @@ class TestWbIntegration(BaseConfigTest):
assert 'The url <b>http://not-exist.example.com/path?A=B</b> could not be found in this collection.' in resp.text assert 'The url <b>http://not-exist.example.com/path?A=B</b> could not be found in this collection.' in resp.text
def test_static_content(self): def test_static_content(self):
resp = self.testapp.get('/static/default_banner.css') resp = self.testapp.get('/static/vue_banner.css')
assert resp.status_int == 200 assert resp.status_int == 200
assert resp.content_type == 'text/css' assert resp.content_type == 'text/css'
assert resp.content_length > 0 assert resp.content_length > 0
def test_static_content_filewrapper(self): def test_static_content_filewrapper(self):
from wsgiref.util import FileWrapper from wsgiref.util import FileWrapper
resp = self.testapp.get('/static/default_banner.css', extra_environ = {'wsgi.file_wrapper': FileWrapper}) resp = self.testapp.get('/static/vue_banner.css', extra_environ = {'wsgi.file_wrapper': FileWrapper})
assert resp.status_int == 200 assert resp.status_int == 200
assert resp.content_type == 'text/css' assert resp.content_type == 'text/css'
assert resp.content_length > 0 assert resp.content_length > 0

View File

@ -27,7 +27,7 @@ class TestPrefixedDeploy(BaseConfigTest):
resp = self.get('/prefix/pywb/*/iana.org') resp = self.get('/prefix/pywb/*/iana.org')
self._assert_basic_html(resp) self._assert_basic_html(resp)
assert '/prefix/static/query.js' in resp.text assert '/prefix/static/vue/vueui.js' in resp.text
def test_replay_content(self, fmod): def test_replay_content(self, fmod):
resp = self.get('/prefix/pywb/20140127171238{0}/http://www.iana.org/', fmod) resp = self.get('/prefix/pywb/20140127171238{0}/http://www.iana.org/', fmod)
@ -35,14 +35,14 @@ class TestPrefixedDeploy(BaseConfigTest):
assert '"20140127171238"' in resp.text, resp.text assert '"20140127171238"' in resp.text, resp.text
assert "'http://localhost:80/prefix/static/wombat.js'" in resp.text assert "'http://localhost:80/prefix/static/wombat.js'" in resp.text
assert "'http://localhost:80/prefix/static/default_banner.js'" in resp.text assert "http://localhost:80/prefix/static/vue/vueui.js" in resp.text
assert '"http://localhost:80/prefix/static/"' in resp.text assert '"http://localhost:80/prefix/static/"' in resp.text
assert '"http://localhost:80/prefix/pywb/"' in resp.text assert '"http://localhost:80/prefix/pywb/"' in resp.text
assert 'WBWombatInit' in resp.text, resp.text assert 'WBWombatInit' in resp.text, resp.text
assert '"/prefix/pywb/20140127171238{0}/http://www.iana.org/time-zones"'.format(fmod) in resp.text, resp.text assert '"/prefix/pywb/20140127171238{0}/http://www.iana.org/time-zones"'.format(fmod) in resp.text, resp.text
def test_static_content(self): def test_static_content(self):
resp = self.get('/prefix/static/default_banner.css') resp = self.get('/prefix/static/vue_banner.css')
assert resp.status_int == 200 assert resp.status_int == 200
assert resp.content_type == 'text/css' assert resp.content_type == 'text/css'
assert resp.content_length > 0 assert resp.content_length > 0

View File

@ -1,3 +1,4 @@
from pywb.warcserver.test.testutils import BaseTestClass, TempDirTests, HttpBinLiveTests from pywb.warcserver.test.testutils import BaseTestClass, TempDirTests, HttpBinLiveTests
from .base_config_test import CollsDirMixin from .base_config_test import CollsDirMixin
@ -116,7 +117,7 @@ class TestProxy(BaseTestProxy):
assert 'wbinfo.enable_auto_fetch = false;' in res.text assert 'wbinfo.enable_auto_fetch = false;' in res.text
# banner # banner
assert 'default_banner.js' in res.text assert 'vueui.js' in res.text
# no redirect check # no redirect check
assert 'window == window.top' not in res.text assert 'window == window.top' not in res.text
@ -147,7 +148,7 @@ class TestProxyDefaultDate(BaseTestProxy):
assert 'wbinfo.enable_auto_fetch = false;' in res.text assert 'wbinfo.enable_auto_fetch = false;' in res.text
# banner # banner
assert 'default_banner.js' in res.text assert 'vueui.js' in res.text
# no redirect check # no redirect check
assert 'window == window.top' not in res.text assert 'window == window.top' not in res.text
@ -307,7 +308,7 @@ class TestProxyNoBanner(BaseTestProxy):
assert 'WB Insert' in res.text assert 'WB Insert' in res.text
# no banner # no banner
assert 'default_banner.js' not in res.text assert 'vueui.js' not in res.text
# no wombat.js and wombatProxyMode.js # no wombat.js and wombatProxyMode.js
assert 'wombat.js' not in res.text assert 'wombat.js' not in res.text
@ -341,7 +342,7 @@ class TestProxyNoHeadInsert(BaseTestProxy):
assert 'WB Insert' not in res.text assert 'WB Insert' not in res.text
# no banner # no banner
assert 'default_banner.js' not in res.text assert 'vueui.js' not in res.text
# no wombat.js and wombatProxyMode.js # no wombat.js and wombatProxyMode.js
assert 'wombat.js' not in res.text assert 'wombat.js' not in res.text