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:
parent
ff7783aa74
commit
c28941a0b6
23
config.yaml
23
config.yaml
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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'> " +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 + " </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();
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
1086
pywb/static/query.js
1086
pywb/static/query.js
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -1,11 +1,4 @@
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#wb_iframe_div, #replay_iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
6
pywb/templates/bootstrap_jquery.html
Normal file
6
pywb/templates/bootstrap_jquery.html
Normal 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>
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
@ -1,4 +1,4 @@
|
||||
__version__ = '2.7.0b1'
|
||||
__version__ = '2.7.0b2'
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(__version__)
|
||||
|
@ -1,36 +1,126 @@
|
||||
<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="' '"></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>
|
||||
|
||||
<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>
|
||||
</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"> </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
|
||||
v-if="currentPeriod && showTimelineView"
|
||||
:period="currentPeriod"
|
||||
:highlight="timelineHighlight"
|
||||
:current-snapshot="currentSnapshot"
|
||||
@ -40,21 +130,20 @@
|
||||
</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>
|
||||
<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"
|
||||
:current-snapshot="currentSnapshot"
|
||||
@goto-period="gotoPeriod">
|
||||
</CalendarYear>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -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>
|
||||
|
@ -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;
|
||||
@ -95,7 +94,7 @@
|
||||
<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="' '"></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="' '"></template></span></span>
|
||||
</div>
|
||||
<div v-else class="empty">{{ _('no captures') }}</div>
|
||||
</div>
|
||||
|
@ -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>
|
||||
<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">
|
||||
<CalendarMonth
|
||||
v-for="month in year.children"
|
||||
@ -36,7 +29,10 @@
|
||||
@show-day-timeline="setCurrentTimeline"
|
||||
></CalendarMonth>
|
||||
</div>
|
||||
<Tooltip :position="currentTimelinePos" v-if="currentTimelinePeriod" ref="timelineLinearTooltip">
|
||||
<Tooltip
|
||||
:position="currentTimelinePos"
|
||||
v-if="currentTimelinePeriod"
|
||||
ref="timelineLinearTooltip">
|
||||
<TimelineLinear
|
||||
:period="currentTimelinePeriod"
|
||||
:current-snapshot="containsCurrentSnapshot ? currentSnapshot : null"
|
||||
@ -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) {
|
||||
@ -101,6 +108,12 @@ export default {
|
||||
}
|
||||
},
|
||||
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;
|
||||
}
|
||||
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>
|
||||
|
@ -58,7 +58,7 @@ export default {
|
||||
}
|
||||
.pywb-loading-spinner-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
top: 10px;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
@ -16,7 +16,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-html="'◀'" class="arrow previous" :class="{disabled: isScrollZero && !previousPeriod}" @click="scrollPrev" @dblclick.stop.prevent></div>
|
||||
<div v-html="'◀'"
|
||||
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="'▶'" class="arrow next" :class="{disabled: isScrollMax && !nextPeriod}" @click="scrollNext" @dblclick.stop.prevent></div>
|
||||
<div
|
||||
v-html="'▶'"
|
||||
class="arrow next"
|
||||
:class="{disabled: isScrollMax && !nextPeriod}"
|
||||
@click="scrollNext"
|
||||
@keyup.enter="scrollNext"
|
||||
@dblclick.stop.prevent tabindex="0"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -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>
|
||||
>
|
||||
<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>
|
||||
|
@ -6,8 +6,14 @@
|
||||
</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>
|
||||
<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>
|
||||
@ -29,8 +35,14 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
isCurrentSnapshot(period) {
|
||||
if (!!this.currentSnapshot && !!period.snapshot) {
|
||||
return this.currentSnapshot && this.currentSnapshot.id === period.snapshot.id;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
changePeriod(period) {
|
||||
this.$emit("goto-period", period);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user