diff --git a/config.yaml b/config.yaml
index 2344eecd..236e7678 100644
--- a/config.yaml
+++ b/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
diff --git a/pywb/apps/frontendapp.py b/pywb/apps/frontendapp.py
index b29e612c..4ef38b20 100644
--- a/pywb/apps/frontendapp.py
+++ b/pywb/apps/frontendapp.py
@@ -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')
diff --git a/pywb/static/default_banner.css b/pywb/static/default_banner.css
deleted file mode 100644
index 302b7848..00000000
--- a/pywb/static/default_banner.css
+++ /dev/null
@@ -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;
- }
-}
\ No newline at end of file
diff --git a/pywb/static/default_banner.js b/pywb/static/default_banner.js
deleted file mode 100644
index 15e1ee27..00000000
--- a/pywb/static/default_banner.js
+++ /dev/null
@@ -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 .
-
-*/
-
-// 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 += "";
- logoContents += "";
-
- 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 = " " +window.banner_info.calendarLabel + "";
- 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 = "" + capture_str + "";
-
- capture_str += "";
-
- if (is_live) {
- title_str = window.banner_info.liveMsg + " " + title_str;
- capture_str += "" + window.banner_info.liveMsg + " ";
- }
-
- capture_str += this.ts_to_date(ts, window.banner_info.is_gmt);
- capture_str += "";
-
- 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();
- }
- }
-
-})();
diff --git a/pywb/static/query.js b/pywb/static/query.js
deleted file mode 100644
index cae02cae..00000000
--- a/pywb/static/query.js
+++ /dev/null
@@ -1,1086 +0,0 @@
-var colon = ':';
-var usingWorkerStrat =
- typeof window.Worker === 'function' &&
- typeof window.fetch === 'function' &&
- typeof window.TextDecoder === 'function' &&
- typeof window.ReadableStream === 'function';
-
-var first = 1;
-
-/**
- * Class responsible for rendering the results of the query. If the query is considered regular
- * (no filter or match type query modifiers) the calender view is rendered otherwise the advanced search is rendered
- * @param {Object} init - Initialization info for rendering the results of the query
- */
-function RenderCalendar(init) {
- if (!(this instanceof RenderCalendar)) return new RenderCalendar(init);
- this.prefix = init.prefix;
- this.cdxURL = this.prefix + 'cdx';
- this.staticPrefix = init.staticPrefix;
- this.queryInfo = {
- calView: true,
- complete: false,
- cdxQueryURL: null,
- url: null,
- hasFilter: false,
- searchParams: {
- present: false,
- includeInURL: false
- }
- };
- // references to the DOM structure elements that contain the to be rendered markup
- this.containers = {
- numCaptures: null,
- updatesSpinner: null,
- yearsListDiv: null,
- monthsListDiv: null,
- daysListDiv: null,
- advancedResultsList: null,
- countTextNode: null,
- versionsTextNode: null
- };
- // ids of the to be created dom elements that are important to rendering the query results
- this.domStructureIds = {
- queryInfoId: 'display-query-type-info',
- capturesMount: 'captures',
- numCaptures: 'num-captures',
- updatesSpinner: 'still-updating-spinner'
- };
- // memoized last active year month day tab information
- this.lastActiveYMD = {};
- this.lastActiveDaysTab = null;
- // regular expressing for checking the URL when no query params are present
- this.starQueries = {
- regularStr: '/*/',
- dtFromTo: /\/([^-]+)-([^/]+)\/(.+)/i,
- dtFrom: /\/([^/]+)\/(.+)/i
- };
- // regex for extracting the filter constraints and filter mods to human explanation
- this.filterRE = /filter([^a-z]+)([a-z]+):(.+)/i;
- this.filterMods = {
- '=': 'Contains',
- '==': 'Matches Exactly',
- '=~': 'Matches Regex',
- '=!': 'Does Not Contains',
- '=!=': 'Is Not',
- '=!~': 'Does Not Begins With'
- };
- this.text = init.text;
- this.versionString = null;
-}
-
-/**
- * Initializes the rendering process and checks the our locations URL to determine which
- * result view to render (calendar or advanced)
- * @return {void}
- */
-RenderCalendar.prototype.init = function() {
- // strip the prefix from our locations URL leaving us with /dt-info/url or /*?...
- var queryPart = location.href.substring(this.prefix.length - 1);
-
- // check for from the search interface
- if (queryPart.indexOf('/*?url=') === 0) {
- // the query info came from the collections query interface and since we are
- // mimicking the cdx api here we need to parse our locations URL using the
- // WHATWG URL Standard since location.search is un-helpful here
- var searchURL = new URL(location.href);
- this.queryInfo.url = searchURL.searchParams.get('url');
- var haveMatchType = searchURL.searchParams.has('matchType');
- var haveFilter = searchURL.searchParams.has('filter');
- if (!haveMatchType) {
- // be extra sure the user did not input a URL that includes the match type
- this.determineViewFromQInfoURL();
- } else {
- this.queryInfo.searchParams.matchType = searchURL.searchParams.get(
- 'matchType'
- );
- }
- if (this.queryInfo.calView) {
- this.queryInfo.calView = !(haveMatchType || haveFilter);
- }
- this.queryInfo.searchParams.from = searchURL.searchParams.get('from');
- this.queryInfo.searchParams.to = searchURL.searchParams.get('to');
- this.queryInfo.searchParams.present = true;
- this.queryInfo.rawSearch = location.search;
- this.queryInfo.hasFilter = haveFilter;
- return this.makeCDXRequest();
- }
-
- // check for /*/URL
- if (queryPart.indexOf(this.starQueries.regularStr) === 0) {
- this.queryInfo.url = queryPart.substring(
- queryPart.indexOf(this.starQueries.regularStr) +
- this.starQueries.regularStr.length
- );
- this.determineViewFromQInfoURL();
- return this.makeCDXRequest();
- }
-
- // check for /fromDT*-toDT*/url*
- var maybeDTStarQ = this.maybeExtractDTStarQuery(queryPart);
- if (maybeDTStarQ) {
- this.queryInfo.searchParams.present = true;
- this.queryInfo.searchParams.includeInURL = true;
- this.queryInfo.searchParams.from = maybeDTStarQ.from;
- this.queryInfo.searchParams.to = maybeDTStarQ.to;
- this.queryInfo.url = maybeDTStarQ.url;
- this.determineViewFromQInfoURL();
- return this.makeCDXRequest();
- }
-};
-
-/**
- * Determines the match type from the query URL if it includes the shortcuts
- * @return {void}
- */
-RenderCalendar.prototype.determineViewFromQInfoURL = function() {
- var purl;
- var domainMatch =
- this.queryInfo.url.charAt(this.queryInfo.url.length - 1) === '*';
-
- try {
- // if parsing the query url via the WHATWG URL class fails (no scheme)
- // we are probably not rendering the date calendar
- purl = new URL(this.queryInfo.url);
- } catch (e) {}
-
- if (!purl) {
- // since purl is null we know we do not hav a valid URL and need to check
- // for the match type cases and if one is present update queryInfo
- var prefixMatch = this.queryInfo.url.substring(0, 2) === '*.';
- return this.updateMatchType(prefixMatch, domainMatch);
- }
-
- if (purl.search) {
- // the URL we are querying with has some search params
- // in this case we know we can not check for match type domain because
- // http://example.com?it=* is valid and the * here is for the search param
- return;
- }
-
- // there are no search params and may have http://example.com[*|/*]
- // indicating we are operating under match type domain
- return this.updateMatchType(null, domainMatch);
-};
-
-/**
- * Updates the queryInfo's searchParams property to reflect if CDX query is
- * using a match type depending on the supplied T/falsy mPrefix and mDomain values
- * @param {?boolean} prefixMatch - T/falsy indicating if the match type prefix condition is satisfied
- * @param {?boolean} domainMatch - T/falsy indicating if the match type domain condition is satisfied
- * @return {void}
- */
-RenderCalendar.prototype.updateMatchType = function(prefixMatch, domainMatch) {
- // if mPrefix && mDomain something weird is up and we got *.xyz.com[*|/*]
- // default to prefix just to be safe since we know we got that for sure
- if ((prefixMatch && domainMatch) || prefixMatch) {
- this.queryInfo.searchParams.present = true;
- this.queryInfo.searchParams.matchType = 'prefix';
- this.queryInfo.calView = false;
- return;
- }
- if (domainMatch) {
- this.queryInfo.searchParams.present = true;
- this.queryInfo.searchParams.matchType = 'domain';
- this.queryInfo.calView = false;
- }
-};
-
-/**
- * Extracts the datetime information and url from the query part of our
- * locations URL if it exists otherwise returns null
- * @param {string} queryPart
- * @return {?{from: string, to?: string, url: string}}
- */
-RenderCalendar.prototype.maybeExtractDTStarQuery = function(queryPart) {
- // check /from-to/url first
- var match = this.starQueries.dtFromTo.exec(queryPart);
- if (match) return { from: match[1], to: match[2], url: match[3] };
- // lastly check /from/url first
- match = this.starQueries.dtFrom.exec(queryPart);
- if (match) return { from: match[1], url: match[2] };
- return null;
-};
-
-/**
- * Constructs and returns the URL for the CDX query. Also updates the
- * queryInfo's cdxQueryURL property with the constructed URL.
- * @return {string}
- */
-RenderCalendar.prototype.makeCDXQueryURL = function() {
- var queryURL;
- if (this.queryInfo.rawSearch) {
- queryURL = this.cdxURL + this.queryInfo.rawSearch + '&output=json';
- } else {
- queryURL =
- this.cdxURL +
- '?output=json&url=' +
- // ensure that any query params in the search URL are not considered
- // apart of the full query URL
- encodeURIComponent(this.queryInfo.url);
- }
- var querySearchParams = this.queryInfo.searchParams;
- if (querySearchParams.present && querySearchParams.includeInURL) {
- if (querySearchParams.from) {
- queryURL += '&from=' + querySearchParams.from.trim();
- }
- if (querySearchParams.to) {
- queryURL += '&to=' + querySearchParams.to.trim();
- }
- }
- this.queryInfo.cdxQueryURL = queryURL;
- return queryURL;
-};
-
-/**
- * Performs the query by requesting the CDX information from the cdx server.
- * How the results are received from the CDX API is done by of two strategies: using a web worker or in the main thread.
- * This is due to the fact that the query results can be LARGE and extracting the results in the main thread is not
- * preformat and will lock up the browser. The determination for if we can use the web worker strategy is
- * by checking if the browser has the symbols Worker, fetch, TextDecoder, and ReadableStream defined and are functions.
- * If we can use the web worker strategy a worker is created and updates the result view as it sends us the queries
- * cdx info. Otherwise the query is executed in the main thread and the results are both parsed and rendered in the
- * main thread.
- * @return {void}
- */
-RenderCalendar.prototype.makeCDXRequest = function() {
- // if we are rendering the calendar view (regular result view) we need memoizedYMDT to be an object otherwise nothing
- var memoizedYMDT = this.queryInfo.calView ? {} : null;
- var renderCal = this;
- // initialized the dom structure
- this.createContainers();
- if (usingWorkerStrat) {
- // execute the query and render the results using the query worker
- var queryWorker = new window.Worker(this.staticPrefix + '/queryWorker.js');
- var cdxRecordMsg = 'cdxRecord';
- var done = 'finished';
- var months = this.text.months;
-
- queryWorker.onmessage = function(msg) {
- var data = msg.data;
- var terminate = false;
- if (data.type === cdxRecordMsg) {
- data.timeInfo.month = months[data.timeInfo.month];
-
- // render the results sent to us from the worker
- renderCal.displayedCountStr(
- data.recordCount,
- data.recordCountFormatted
- );
- if (renderCal.queryInfo.calView) {
- renderCal.renderDateCalPart(
- memoizedYMDT,
- data.record,
- data.timeInfo,
- data.recordCount === first
- );
- } else {
- renderCal.renderAdvancedSearchPart(data.record);
- }
- } else if (data.type === done) {
- // the worker has consumed the entirety of the response body
- terminate = true;
- // if there were no results we need to inform the user
- renderCal.displayedCountStr(
- data.recordCount,
- data.recordCountFormatted
- );
- }
- if (terminate) {
- queryWorker.terminate();
- var spinner = document.getElementById(
- renderCal.domStructureIds.updatesSpinner
- );
- if (spinner && spinner.parentNode) {
- spinner.parentNode.removeChild(spinner);
- }
- }
- };
- queryWorker.postMessage({
- type: 'query',
- queryURL: this.makeCDXQueryURL()
- });
- return;
- }
- // main thread processing
- $.ajax(this.makeCDXQueryURL(), {
- dataType: 'text',
- success: function(data) {
- var cdxLines = data ? data.trim().split('\n') : [];
- if (cdxLines.length === 0) {
- renderCal.displayedCountStr(0, '0');
- return;
- }
- var numCdxEntries = cdxLines.length;
- renderCal.displayedCountStr(
- numCdxEntries,
- numCdxEntries.toLocaleString()
- );
- for (var i = 0; i < numCdxEntries; ++i) {
- var cdxObj = JSON.parse(cdxLines[i]);
- if (renderCal.queryInfo.calView) {
- renderCal.renderDateCalPart(
- memoizedYMDT,
- cdxObj,
- renderCal.makeTimeInfo(cdxObj),
- i === 0
- );
- } else {
- renderCal.renderAdvancedSearchPart(cdxObj);
- }
- }
- var spinner = document.getElementById(
- renderCal.domStructureIds.updatesSpinner
- );
- if (spinner && spinner.parentNode) {
- spinner.parentNode.removeChild(spinner);
- }
- }
- });
-};
-
-/**
- * Creates the DOM structure required for rendering the query results based on if we are rendering the
- * calendar view or the advanced query view and populates the containers object
- */
-RenderCalendar.prototype.createContainers = function() {
- var queryResults = document.getElementById(
- this.domStructureIds.capturesMount
- );
- var queryInfo = document.getElementById(this.domStructureIds.queryInfoId);
- var renderCal = this;
- if (this.queryInfo.calView) {
- // create regulars count structure
- this.createAndAddElementTo(queryInfo, {
- tag: 'h3',
- children: [
- {
- tag: 'i',
- className: 'fas fa-spinner fa-pulse mr-2',
- id: this.domStructureIds.updatesSpinner
- },
- {
- tag: 'b',
- child: {
- tag: 'textNode',
- value: '',
- ref: function(refToElem) {
- renderCal.containers.countTextNode = refToElem;
- }
- }
- },
- { tag: 'textNode', value: ' ' },
- {
- tag: 'textNode',
- value: '',
- ref: function(refToElem) {
- renderCal.containers.versionsTextNode = refToElem;
- }
- },
- { tag: 'b', innerText: ' ' + this.queryInfo.url }
- ]
- });
- // create the row that will hold the results of the regular query
- this.createAndAddElementTo(queryResults, {
- tag: 'div',
- className: 'row q-row',
- children: [
- // years column and initial display structure
- {
- tag: 'div',
- className: 'col-12 col-sm-2 pr-1 pl-1 h-100',
- child: {
- tag: 'div',
- className: 'list-group h-100 auto-overflow',
- id: 'year-tab-list',
- attributes: { role: 'tablist' },
- ref: function(refToElem) {
- renderCal.containers.yearsListDiv = refToElem;
- }
- }
- },
- // months initial structure
- {
- tag: 'div',
- className: 'col-12 mt-2 col-sm-2 mt-sm-0 pr-1 pl-1 h-100',
- child: {
- tag: 'div',
- className: 'tab-content h-100',
- id: 'year-month-tab-list',
- ref: function(refToElem) {
- renderCal.containers.monthsListDiv = refToElem;
- }
- }
- },
- // days initial structure
- {
- tag: 'div',
- className: 'col-12 mt-3 mb-3 pr-1 mt-sm-0 mb-sm-0 pr-sm-0 col-sm-8 pl-1 h-100',
- child: {
- tag: 'div',
- className: 'tab-content h-100',
- id: 'year-month-day-tab-list',
- ref: function(refToElem) {
- renderCal.containers.daysListDiv = refToElem;
- }
- }
- }
- ]
- });
- this.containers.updatesSpinner = document.getElementById(
- this.domStructureIds.updatesSpinner
- );
- return;
- }
- // create the advanced results query info DOM structure
- var forString = ' for ';
- var forElems;
-
- if (this.queryInfo.searchParams.matchType) {
- forString = ' ' + this.text.matching + ' ';
- forElems = [
- { tag: 'b', innerText: this.queryInfo.url },
- { tag: 'textNode', value: ' ' + this.text.by + ' ' },
- { tag: 'b', innerText: this.text.types[this.queryInfo.searchParams.matchType] }
- ];
- } else {
- forElems = [{ tag: 'b', innerText: this.queryInfo.url }];
- }
- this.createAndAddElementTo(queryInfo, {
- tag: 'div',
- className: 'col-12',
- child: {
- tag: 'h3',
- className: 'text-center',
- children: [
- {
- tag: 'i',
- className: 'fas fa-spinner fa-pulse mr-2',
- id: this.domStructureIds.updatesSpinner
- },
- {
- tag: 'b',
- child: {
- tag: 'textNode',
- value: '',
- ref: function(refToElem) {
- renderCal.containers.countTextNode = refToElem;
- }
- }
- },
- { tag: 'textNode', value: ' ' },
- {
- tag: 'textNode',
- value: '',
- ref: function(refToElem) {
- renderCal.containers.versionsTextNode = refToElem;
- }
- },
- { tag: 'textNode', value: forString }
- ].concat(forElems)
- }
- });
- // next we will display only the URL or the URL + match type or the URL + date range
- // if the executed query contained filters display the humanized version of them
-
- if (this.queryInfo.searchParams.from || this.queryInfo.searchParams.to) {
- this.createAndAddElementTo(queryInfo, {
- tag: 'div',
- className: 'col-6 align-self-center',
- child: {
- tag: 'p',
- className: 'lead text-center',
- child: { tag: 'textNode', value: ' ' + this.niceDateRange() }
- }
- });
- }
-
- if (this.queryInfo.hasFilter) {
- this.createAndAddElementTo(queryInfo, {
- tag: 'div',
- className: 'col-6',
- children: [
- {
- tag: 'p',
- className: 'text-center mb-0 mt-1',
- innerText: 'Filtering by'
- },
- {
- tag: 'ul',
- className: 'list-group auto-overflow',
- style: 'max-height: 150px',
- children: this.niceFilterDisplay()
- }
- ]
- });
- }
- // create the advanced results DOM structure
- this.containers.advancedResultsList = this.createAndAddElementTo(
- queryResults,
- {
- tag: 'div',
- className: 'row q-row',
- child: {
- tag: 'ul',
- className: 'list-group h-100 auto-overflow w-100'
- }
- }
- ).firstElementChild;
-};
-
-/**
- * Updates the calendar view with the supplied cdx information
- * @param {Object} memoizedYMDT - Object containing the counts for the captures
- * @param {Object} cdxObj - CDX object for this capture
- * @param {Object} timeInfo - Object containing the date time information for this capture
- * @param {boolean} active - Should we add the active classes to the markup rendered here
- */
-RenderCalendar.prototype.renderDateCalPart = function(
- memoizedYMDT,
- cdxObj,
- timeInfo,
- active
-) {
- if (memoizedYMDT[timeInfo.year] == null) {
- // we have not seen this year month day before
- // create the year month day structure (occurs once per result year)
-
- var timeVal = {};
- timeVal[timeInfo.time] = 1;
-
- var dayVal = {};
- dayVal[timeInfo.day] = timeVal;
-
- var monthVal = {};
- monthVal[timeInfo.month] = dayVal;
-
- memoizedYMDT[timeInfo.year] = monthVal;
-
- this.addRegYearListItem(timeInfo, active);
- this.addRegYearMonthListItem(timeInfo, active);
- return this.addRegYearMonthDayListItem(cdxObj, timeInfo, 1, active);
- } else if (memoizedYMDT[timeInfo.year][timeInfo.month] == null) {
- // we have seen the year before but not the month and day
- // create the month day structure (occurs for every new month encountered for an existing year)
- var timeVal = {};
- timeVal[timeInfo.time] = 1;
-
- var dayVal = {};
- dayVal[timeInfo.day] = timeVal;
-
- memoizedYMDT[timeInfo.year][timeInfo.month] = dayVal;
-
- this.addRegYearMonthListItem(timeInfo, active);
- return this.addRegYearMonthDayListItem(cdxObj, timeInfo, 1, active);
- }
- // in cases 1 & 2 a new day at time entry is added otherwise only the captures count is updated (case 3)
- var count = 1;
- var month = memoizedYMDT[timeInfo.year][timeInfo.month];
- if (month[timeInfo.day] == null) {
- // never seen this day (case 1)
- var timeVal = {};
- timeVal[timeInfo.time] = count;
-
- month[timeInfo.day] = timeVal;
- } else if (month[timeInfo.day][timeInfo.time] == null) {
- // we have seen this day before but not at this time (case 2)
- month[timeInfo.day][timeInfo.time] = count;
- } else {
- // we have seen this day at time before so just increment the captures count (case 3)
- count = month[timeInfo.day][timeInfo.time] + 1;
- month[timeInfo.day][timeInfo.time] = count;
- }
- this.addRegYearMonthDayListItem(cdxObj, timeInfo, count, active);
-};
-
-/**
- * Updates the advanced view with the supplied cdx information
- * @param {Object} cdxObj - CDX object for this capture
- */
-RenderCalendar.prototype.renderAdvancedSearchPart = function(cdxObj) {
- // display the URL of the result
- var displayedInfo = [
- {
- tag: 'small',
- innerText: this.text.dateTime + this.tsToDate(cdxObj.timestamp)
- }
- ];
- // display additional information about the result under the URL
- if (cdxObj.mime) {
- displayedInfo.push({
- tag: 'small',
- innerText: this.text.mimeType + cdxObj.mime
- });
- }
- if (cdxObj.status) {
- displayedInfo.push({
- tag: 'small',
- innerText: this.text.httpStatus + cdxObj.status
- });
- }
- displayedInfo.push({
- tag: 'a',
- attributes: {
- href: this.prefix + '*' + '/' + cdxObj.url,
- target: '_blank'
- },
- child: { tag: 'small', innerText: this.text.viewAllCaptures }
- });
- this.createAndAddElementTo(this.containers.advancedResultsList, {
- tag: 'li',
- className: 'list-group-item flex-column align-items-start w-100',
- children: [
- {
- tag: 'a',
- className: 'w-100 text-small long-text',
- attributes: {
- href: this.prefix + cdxObj.timestamp + '/' + cdxObj.url,
- target: '_blank'
- },
- innerText: cdxObj.url
- },
- {
- tag: 'div',
- className: 'pt-1 d-flex w-100 justify-content-between',
- children: displayedInfo
- }
- ]
- });
-};
-
-/**
- * Creates and adds a year entry for the calendar view
- * @param {Object} timeInfo - Object containing the date time information for this capture
- * @param {boolean} active - Should we add the active classes to the markup rendered here
- */
-RenderCalendar.prototype.addRegYearListItem = function(timeInfo, active) {
- // adds an year to div[id=year-tab-list]
- var renderCal = this;
- var year = timeInfo.year;
- this.createAndAddElementTo(this.containers.yearsListDiv, {
- tag: 'a',
- className:
- 'list-group-item list-group-item-action' + (active ? ' active' : ''),
- innerText: year,
- attributes: { role: 'tab', href: '#' + this.displayMonthsId(year) },
- dataset: { toggle: 'list' },
- events: {
- // add a click listener to ensure we display the correct year month days combination
- // when choosing a new year or month
- click: function() {
- renderCal.ensureCorrectActive(year);
- }
- }
- });
-};
-
-/**
- * Adds a years months to it's month list. If the years month list was not previously created it is created.
- * @param {Object} timeInfo - Object containing the date time information for this capture
- * @param {boolean} active - Should we add the active classes to the markup rendered here
- */
-RenderCalendar.prototype.addRegYearMonthListItem = function(timeInfo, active) {
- var yeaMonthsId = this.displayMonthsId(timeInfo.year); // _year-Display-Month
- var yearMonthListId = this.displayMonthsListId(timeInfo.year); // _year-Month-Display-List
- var yml = document.getElementById(yearMonthListId);
- if (yml == null) {
- /*
- we have not created this years month list before before
- so we must create a tab pane for it and month list
-
- div[id=_year-Display-Months, role=tabpanel].h-100.tab-pane
- div[id=_year-Display-Months-List, role=tablist].list-group.h-100.auto-overflow
- */
- yml = this.createAndAddElementTo(this.containers.monthsListDiv, {
- tag: 'div',
- className: 'h-100 tab-pane' + (active ? ' active show' : ''),
- id: yeaMonthsId,
- attributes: { role: 'tabpanel' },
- child: {
- tag: 'div',
- className: 'list-group h-100 auto-overflow',
- id: yearMonthListId,
- attributes: { role: 'tablist' }
- }
- }).firstElementChild;
- }
- var renderCal = this;
- var showDaysId = this.displayYearMonthDaysId(timeInfo.year, timeInfo.month);
- var year = timeInfo.year;
- // adds an years month to div[id=_year-Display-Months-List]
- this.createAndAddElementTo(yml, {
- tag: 'a',
- className:
- 'list-group-item list-group-item-action' + (active ? ' active' : ''),
- innerText: timeInfo.month,
- attributes: {
- role: 'tab',
- href: '#' + showDaysId // _year-month-Display-Days
- },
- events: {
- click: function() {
- renderCal.lastActiveYMD[year] = {
- month: yeaMonthsId,
- days: showDaysId
- };
- renderCal.lastActiveDaysTab = document.getElementById(showDaysId);
- }
- },
- dataset: { toggle: 'list' }
- });
-};
-
-/**
- * Updates the number of captures for a day at time for a year month. If the years month month day at time list
- * was not previously created it is created. If the day was not present in it's years months days list it is created.
- * @param {Object} cdxObj - CDX object for this capture
- * @param {Object} timeInfo - Object containing the date time information for this capture
- * @param {number} numCaptures - How many captures do we have currently for this URL
- * @param {boolean} active - Should we add the active classes to the markup rendered here
- */
-RenderCalendar.prototype.addRegYearMonthDayListItem = function(
- cdxObj,
- timeInfo,
- numCaptures,
- active
-) {
- var yearMonthDaysListId = this.displayYearMonthDaysListId(
- timeInfo.year,
- timeInfo.month
- ); // _year-month-Display-Days-List
- var ymlDL = document.getElementById(yearMonthDaysListId);
- if (ymlDL == null) {
- /*
- we have not created this years months day tab and list before before
- so we must create a tab pane for it and days list
-
- div[id=_year-month-Display-Days, role=tabpanel].h-100.tab-pane.active.show
- ul[id=_year-month-Display-Days-List].list-group.h-100.auto-overflow
- */
- ymlDL = this.createAndAddElementTo(this.containers.daysListDiv, {
- tag: 'div',
- className: 'h-100 tab-pane' + (active ? ' active show' : ''),
- id: this.displayYearMonthDaysId(timeInfo.year, timeInfo.month),
- attributes: { role: 'tabpanel' },
- child: {
- tag: 'ul',
- className: 'list-group h-100 auto-overflow',
- id: yearMonthDaysListId
- }
- }).firstElementChild;
- }
- // retrieve the existing days num captures display element
- var existingDayId = 'count_' + cdxObj.timestamp;
- var existingDayCount = document.getElementById(existingDayId);
- if (existingDayCount == null) {
- /*
- it does not exist so we have not displayed this day before
-
- li.list-group-item
- a[href="replay url"]
- span[id=count_ts].badge.badge-info.badge-pill.float-right
- */
- const options = {
- dateStyle: 'long',
- timeStyle: 'medium',
- };
- var dateTimeString = this.tsToDate(cdxObj.timestamp, false, options);
- this.createAndAddElementTo(ymlDL, {
- tag: 'li',
- className: 'list-group-item',
- children: [
- {
- tag: 'a',
- attributes: {
- href: this.prefix + cdxObj.timestamp + '/' + cdxObj.url,
- target: '_blank'
- },
- innerText: dateTimeString
- },
- {
- tag: 'span',
- id: existingDayId,
- className: 'badge badge-info badge-pill float-right',
- ref: function(refToElem) {
- existingDayCount = refToElem;
- }
- }
- ]
- });
- if (active) {
- // populate the lastActives with the selected correct year month days combination
- var daysID = this.displayYearMonthDaysId(timeInfo.year, timeInfo.month);
- this.lastActiveYMD[timeInfo.year] = {
- month: this.displayMonthsId(timeInfo.year),
- days: daysID
- };
- this.lastActiveDaysTab = document.getElementById(daysID);
- }
- }
- existingDayCount.innerText = numCaptures + '';
-};
-
-/**
- * Ensures that we display the correct year month days combination when switching year's.
- * Updates the lastActiveDaysTab property.
- * @param {string} year - The year to be shown
- */
-RenderCalendar.prototype.ensureCorrectActive = function(year) {
- // un-activates currently active year month day display tab
- if (this.lastActiveDaysTab != null) {
- this.lastActiveDaysTab.classList.remove('active', 'show');
- }
- // activates the last year month day display tab for the current year chosen
- if (this.lastActiveYMD[year] && this.lastActiveYMD[year].days) {
- this.lastActiveDaysTab = document.getElementById(
- this.lastActiveYMD[year].days
- );
- this.lastActiveDaysTab.classList.add('active', 'show');
- }
-};
-
-/**
- * Creates a DOM element(s) based on the supplied creation options.
- * Creation options:
- * - tag: The name of the tag to be created (supplied to doc.createElement) or 'textNode' to create a text
- * node using doc.createTextNode [required]
- * - value: A string used as the contents for the to be created text node (only used if tag == 'textNode')
- * - id: An string used to set the `id` of the element
- * - className: An string of CSS class names used to set the elements `className` property
- * - style: An string containing style definition(s) used to set the elements `style` property
- * - innerText: An string used to set the elements `innerText` property
- * - innerHTML: An string used to set the elements `innerHTML` property
- * - attributes: An object containing string key value pairs that will be used to set the elements attributes
- * using element.setAttribute(key, value)
- * - dataset: An object containing string key value pairs that will be used to set the elements attributes
- * using element.dataset[key] = value
- * - events: An object containing string keys and function value pairs that will be used to add event listeners
- * on the element using addEventListener.addEventListener(key, value)
- * - child: An object (same properties as the creationOptions parameter for this function) that represent a child
- * element of the element created using this function, added via element.appendChild once created
- * - children: An array of objects (same properties as the creationOptions parameter for this function) that
- * represent a child element of the element created using this function, added via element.appendChild once created
- * - ref: An function that if supplied will be called with the created element as the only argument
- * @param {Object} creationOptions - Options for new element creation
- * @return {HTMLElement|Node} - The newly created element
- */
-RenderCalendar.prototype.createElement = function(creationOptions) {
- if (creationOptions.tag === 'textNode') {
- var tn = document.createTextNode(creationOptions.value);
- if (creationOptions.ref) {
- creationOptions.ref(tn);
- }
- return tn;
- }
- var elem = document.createElement(creationOptions.tag);
- if (creationOptions.id) {
- elem.id = creationOptions.id;
- }
- if (creationOptions.className) {
- elem.className = creationOptions.className;
- }
- if (creationOptions.style) {
- elem.style = creationOptions.style;
- }
- if (creationOptions.innerText) {
- elem.innerText = creationOptions.innerText;
- }
- if (creationOptions.innerHTML) {
- elem.innerHTML = creationOptions.innerHTML;
- }
- if (creationOptions.attributes) {
- var atts = creationOptions.attributes;
- for (var attributeName in atts) {
- elem.setAttribute(attributeName, atts[attributeName]);
- }
- }
- if (creationOptions.dataset) {
- var dataset = creationOptions.dataset;
- for (var dataName in dataset) {
- elem.dataset[dataName] = dataset[dataName];
- }
- }
- if (creationOptions.events) {
- var events = creationOptions.events;
- for (var eventName in events) {
- elem.addEventListener(eventName, events[eventName]);
- }
- }
- if (creationOptions.child) {
- elem.appendChild(this.createElement(creationOptions.child));
- }
- if (creationOptions.children) {
- var kids = creationOptions.children;
- for (var i = 0; i < kids.length; i++) {
- elem.appendChild(this.createElement(kids[i]));
- }
- }
- if (creationOptions.ref) {
- creationOptions.ref(elem);
- }
- return elem;
-};
-
-/**
- * Adds the newly created element to the supplied existing element.
- * @param {Node} addTo - The DOM node the newly created element will be added to via addTo.appendChild
- * @param {Object} creationOptions - Options for creating the new element.
- * See the description of {@link createElement} for more information.
- * @return {HTMLElement|Node} - The newly created element
- */
-RenderCalendar.prototype.createAndAddElementTo = function(
- addTo,
- creationOptions
-) {
- var created = this.createElement(creationOptions);
- addTo.appendChild(created);
- return created;
-};
-
-/**
- * Returns element creation options for the humanized filtering options of the advanced view
- * @returns {Array