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

View File

@ -432,7 +432,8 @@ class FrontEndApp(object):
return WbResponse.bin_stream(StreamIter(res.raw),
content_type=content_type,
status=status_line)
status=status_line,
headers=[("Cache-Control", "max-age=86400, must-revalidate")])
except Exception as e:
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 {
width: 100%;
height: 100%;
}

View File

@ -22,23 +22,11 @@ window.banner_info = {
logoImg: "{{ ui.logo }}"
};
</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>
<link rel='stylesheet' href='{{ static_prefix }}/vue_banner.css'/>
<script src="{{ static_prefix }}/vue/vueui.js"></script>
{% endif %}
{% endif %}
<link rel="stylesheet" href='{{ static_prefix }}/vue_banner.css'/>
{% include 'bootstrap_jquery.html' ignore missing %}
{% endautoescape %}
{% endif %}

View File

@ -6,13 +6,7 @@
<title>{% block title %}{% endblock %}</title>
<!-- jquery and bootstrap dependencies query view -->
<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>
{% include 'bootstrap_jquery.html' ignore missing %}
{% block head %}
{% 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 }}
{% if ui.vue_timeline_banner %}
{% include 'vue_loc.html' %}
{% endif %}
</head>
<body style="margin: 0px; padding: 0px;">
{% if ui.vue_timeline_banner %}
<div id="app" style="width: 100%; height: 200px"></div>
<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);
</script>
{% endif %}
<div id="wb_iframe_div">
<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 %}
{{ 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 }}/vue/vueui.js"></script>
{% include 'vue_loc.html' %}
{% endif %}
{% endblock %}
{% 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>
{% endif %}
<script>
var text = {
@ -75,17 +52,9 @@
},
};
{% if not ui.vue_calendar_ui %}
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') }}",
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') }}",
allLocales, i18nStrings);
{% endif %}
</script>
{% endblock %}

View File

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

View File

@ -1,59 +1,148 @@
<template>
<div class="app" :class="{expanded: showTimelineView}" data-app="webrecorder-replay-app">
<div class="banner">
<div class="line">
<div class="logo"><a href="/"><img :src="config.logoImg"/></a></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="'&nbsp;'"></span><!-- for spacing -->
<div class="app" :class="{expanded: showTimelineView || showFullView }" data-app="webrecorder-replay-app">
<!-- Top navbar -->
<nav
class="navbar navbar-light navbar-expand-lg fixed-top top-navbar justify-content-center"
:style="navbarStyle">
<a class="navbar-brand flex-grow-1 my-1" href="/">
<img :src="config.logoImg" id="logo-img" alt="_('pywb logo')">
</a>
<div class="flex-grow-1 d-flex" id="searchdiv">
<form class="form-inline my-2 my-md-0 mx-lg-auto" @submit="gotoUrl">
<input id="theurl" type="text" :value="config.url" height="31"></input>
</form>
</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>
</li>
</ul>
</div>
</nav>
<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>
</ul>
</div>
<!-- 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 class="col col-12 mt-2">
<Timeline
:period="currentPeriod"
:highlight="timelineHighlight"
:current-snapshot="currentSnapshot"
:max-zoom-level="maxTimelineZoomLevel"
@goto-period="gotoPeriod"
></Timeline>
</div>
<Timeline
v-if="currentPeriod && showTimelineView"
:period="currentPeriod"
:highlight="timelineHighlight"
:current-snapshot="currentSnapshot"
:max-zoom-level="maxTimelineZoomLevel"
@goto-period="gotoPeriod"
></Timeline>
</div>
</div>
</div>
<!-- Calendar -->
<div class="card border-0" v-if="currentPeriod && showFullView && currentPeriod.children.length">
<div class="card-body">
<CalendarYear
:period="currentPeriod"
:current-snapshot="currentSnapshot"
@goto-period="gotoPeriod">
</CalendarYear>
</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>
<CalendarYear v-if="showFullView && currentPeriod && currentPeriod.children.length"
:period="currentPeriod"
:current-snapshot="currentSnapshot"
@goto-period="gotoPeriod">
</CalendarYear>
</div>
</template>
@ -73,13 +162,15 @@ export default {
snapshots: [],
currentPeriod: null,
currentSnapshot: null,
currentSnapshotIndex: null,
msgs: [],
showFullView: true,
showTimelineView: true,
maxTimelineZoomLevel: PywbPeriod.Type.day,
config: {
title: "",
initialView: {}
initialView: {},
allLocales: {}
},
timelineHighlight: false,
locales: [],
@ -87,11 +178,47 @@ export default {
},
components: {Timeline, TimelineBreadcrumbs, CalendarYear},
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: {
sessionStorageUrlKey() {
// remove http(s), www and trailing slash
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: {
@ -124,10 +251,13 @@ export default {
gotoSnapshot(snapshot, fromPeriod, reloadIFrame=false) {
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 (fromPeriod && !this.currentPeriod.contains(fromPeriod)) {
if (!this.currentPeriod || (fromPeriod && !this.currentPeriod.contains(fromPeriod))) {
const fromPeriodAtMaxZoomLevel = fromPeriod.get(this.maxTimelineZoomLevel);
if (fromPeriodAtMaxZoomLevel !== this.currentPeriod) {
if (!this.currentPeriod || fromPeriodAtMaxZoomLevel !== this.currentPeriod) {
this.currentPeriod = fromPeriodAtMaxZoomLevel;
}
}
@ -138,7 +268,15 @@ export default {
if (reloadIFrame !== false) {
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) {
event.preventDefault();
@ -176,130 +314,140 @@ export default {
// convert to snapshot object to support proper rendering of time/date
const snapshot = new PywbSnapshot(view, 0);
// set config current URL and title
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>
<style>
body {
padding-top: 89px !important;
}
.app {
font-family: Calibri, Arial, sans-serif;
border-bottom: 1px solid lightcoral;
/*border-bottom: 1px solid lightcoral;*/
width: 100%;
}
.app.expanded {
height: 150px;
height: 130px;
}
.full-view {
/*position: fixed;*/
/*top: 150px;*/
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 {
width: 100%;
height: 80vh;
}
.logo {
margin-right: 30px;
width: 180px;
#searchdiv {
height: 31px;
}
.banner {
width: 100%;
max-width: 1200px; /* limit width */
position: relative;
#theurl {
width: 250px;
}
.banner .line {
display: flex;
justify-content: flex-start;
@media (min-width: 576px) {
#theurl {
width: 350px;
}
}
.banner .logo {
flex-shrink: initial;
/* for any content/image inside the logo container */
@media (min-width: 768px) {
#theurl {
width: 500px;
}
}
@media (min-width: 992px) {
#theurl {
width: 600px;
}
}
@media (min-width: 1200px) {
#theurl {
width: 900px;
}
}
#toggles {
align-items: center;
}
.breadcrumb-row {
display: flex;
align-items: center;
justify-content: center;
}
.banner .logo img {
flex-shrink: 1;
div.timeline-wrap div.card {
margin-top: 55px;
}
.banner .timeline-wrap {
flex-grow: 2;
overflow-x: hidden;
text-align: left;
margin: 0 25px;
position: relative;
div.timeline-wrap div.card-body {
display: flex;
align-items: center;
justify-content: center;
}
.timeline-wrap .line .breadcrumbs-wrap {
display: inline-block;
flex-grow: 1;
div.timeline-wrap div.card-body div.row {
width: 100%;
align-items: center;
justify-content: center;
}
.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;
.strong {
font-weight: bold;
font-size: 16px;
}
#theurl {
width: 400px;
.hidden {
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>

View File

@ -9,14 +9,13 @@
text-align: center;
vertical-align: top;
box-sizing: content-box;
border-radius: 10px;
}
.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;
@ -91,14 +90,14 @@
</style>
<template>
<div class="calendar-month" :class="{current: isCurrent, 'contains-current-snapshot': containsCurrentSnapshot}">
<h3>{{getLongMonthName(month.id)}} <span v-if="month.snapshotCount">({{ month.snapshotCount }})</span></h3>
<div v-if="month.snapshotCount">
<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>
</div>
<div v-else class="empty">{{ _('no captures') }}</div>
<div class="calendar-month" :class="{current: isCurrent, 'contains-current-snapshot': containsCurrentSnapshot}">
<h3>{{getLongMonthName(month.id)}} <span v-if="month.snapshotCount">({{ month.snapshotCount }})</span></h3>
<div v-if="month.snapshotCount">
<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)" @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 v-else class="empty">{{ _('no captures') }}</div>
</div>
</template>
<script>

View File

@ -1,49 +1,45 @@
<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}} ({{ $root._(year.snapshotCount !== 1 ? '{count} captures':'{count} capture', {count: year.snapshotCount}) }})</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 class="full-view">
<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">
<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>
@ -86,6 +82,17 @@ export default {
}
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
let month = null;
if (this.period.type === PywbPeriod.Type.month) {
@ -97,10 +104,16 @@ export default {
},
containsCurrentSnapshot() {
return this.currentSnapshot &&
this.year.contains(this.currentSnapshot);
this.year.contains(this.currentSnapshot);
}
},
methods: {
gotoPreviousYear() {
this.gotoPeriod(this.previousYear, true /* changeYearOnly */);
},
gotoNextYear() {
this.gotoPeriod(this.nextYear, true /* changeYearOnly */);
},
resetCurrentTimeline(event) {
if (event && this.$refs.timelineLinearTooltip) {
let el = event.target;
@ -122,12 +135,18 @@ export default {
if (!day) {
return;
}
this.currentTimelinePos = `${event.x},${event.y}`;
if (event.code === "Enter") {
let middleXPos = (window.innerWidth / 2) - 60;
this.currentTimelinePos = `${middleXPos},200`;
} else {
this.currentTimelinePos = `${event.x},${event.y}`;
}
event.stopPropagation();
event.preventDefault();
},
gotoPeriod(period) {
if (period.snapshot || period.snapshotPeriod) {
gotoPeriod(period, changeYearOnly=false) {
if (period.snapshot || period.snapshotPeriod || changeYearOnly) {
this.$emit('goto-period', period);
} else {
this.currentTimelinePeriod = period;
@ -138,3 +157,27 @@ export default {
};
</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 {
position: fixed;
top: 0;
top: 10px;
left: 0;
width: 100vw;
height: 100vh;

View File

@ -16,7 +16,12 @@
</div>
</template>
</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="periods" ref="periods">
<div v-for="subPeriod in period.children"
@ -31,16 +36,20 @@
:style="{height: getHistoLineHeight(histoPeriod.snapshotCount)}"
:class="{'has-single-snapshot': histoPeriod.snapshotCount === 1, 'contains-current-snapshot': containsCurrentSnapshot(histoPeriod)}"
@click="changePeriod(histoPeriod, $event)"
@keyup.enter="changePeriod(histoPeriod, $event)"
@mouseover="setTooltipPeriod(histoPeriod, $event)"
@mouseout="setTooltipPeriod(null, $event)"
tabindex="0"
>
</div>
</div>
<div class="inner"
:class="{'has-single-snapshot': subPeriod.snapshotCount === 1}"
@click="changePeriod(subPeriod, $event)"
@keyup.enter="changePeriod(histoPeriod, $event)"
@mouseover="setTooltipPeriod(subPeriod, $event)"
@mouseout="setTooltipPeriod(null, $event)"
tabindex="0"
>
<div class="label">
{{subPeriod.getReadableId()}}
@ -49,7 +58,13 @@
</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>
</template>

View File

@ -2,13 +2,23 @@
<div class="breadcrumbs">
<template v-if="parents.length">
<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)}}
</span>
</span>
&gt;
<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)}}
</span>
</span>

View File

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

View File

@ -50,7 +50,7 @@ export default {
position: fixed;
top: 0;
left: 0;
z-index: 40;
z-index: 100;
background-color: white;
border: 1px solid grey;
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);
new CDXLoader(staticPrefix, url, prefix, timestamp, logoUrl, allLocales);
new CDXLoader(staticPrefix, url, prefix, timestamp, logoUrl, navbarBackground, navbarColor, navbarLightButtons, allLocales);
}
// ===========================================================================
class CDXLoader {
constructor(staticPrefix, url, prefix, timestamp, logoUrl, allLocales) {
constructor(staticPrefix, url, prefix, timestamp, logoUrl, navbarBackground, navbarColor, navbarLightButtons, allLocales) {
this.loadingSpinner = null;
this.loaded = false;
this.opts = {};
this.prefix = prefix;
this.staticPrefix = staticPrefix;
this.logoUrl = logoUrl;
this.navbarBackground = navbarBackground;
this.navbarColor = navbarColor;
this.navbarLightButtons = navbarLightButtons
this.isReplay = (timestamp !== undefined);
@ -56,7 +59,7 @@ class CDXLoader {
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.setAppData(cdxList, timestamp ? {url, timestamp}:null);
});
@ -107,6 +110,7 @@ class CDXLoader {
this.app.setData(new PywbData(cdxList));
if (snapshot) {
this.app.hideBannerUtilities();
this.app.setSnapshot(snapshot);
}
}
@ -180,6 +184,10 @@ class VueBannerWrapper
if (type === "load" || type === "replace-url") {
const surt = this.getSurt(event.data.url);
if (event.data.title) {
this.loader.app.updateTitle(event.data.title);
}
if (surt !== this.lastSurt) {
this.loader.updateSnapshot(event.data.url, event.data.ts);
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
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.content_type == 'text/css'
assert resp.content_length > 0
def test_static_content_filewrapper(self):
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.content_type == 'text/css'
assert resp.content_length > 0

View File

@ -27,7 +27,7 @@ class TestPrefixedDeploy(BaseConfigTest):
resp = self.get('/prefix/pywb/*/iana.org')
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):
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 "'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/pywb/"' in 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
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.content_type == 'text/css'
assert resp.content_length > 0

View File

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