diff --git a/pywb/static/default_banner.js b/pywb/static/default_banner.js
index 640c6d70..5def7ff2 100644
--- a/pywb/static/default_banner.js
+++ b/pywb/static/default_banner.js
@@ -20,8 +20,134 @@ This file is part of pywb, https://github.com/webrecorder/pywb
// Creates the default pywb banner.
-(function() {
- function ts_to_date(ts, is_gmt) {
+(function () {
+ if (window.top !== window) {
+ return;
+ }
+
+ /**
+ * 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.loadingId = 'bannerLoading';
+ this.onMessage = this.onMessage.bind(this);
+ }
+
+ // Functions required to be exposed by all banners
+
+ /**
+ * @desc Initialize (display) the banner
+ */
+ DefaultBanner.prototype.init = function () {
+ if (window.wbinfo) {
+ this.createBanner('_wb_plain_banner');
+ this.set_banner(
+ window.wbinfo.url,
+ window.wbinfo.timestamp,
+ window.wbinfo.is_live,
+ window.wbinfo.is_framed ? "" : document.title
+ );
+ } else {
+ this.createBanner('_wb_frame_top_banner');
+ }
+ };
+
+ /**
+ * @desc Called by ContentFrame to detect if the banner is still showing
+ * that the page is loading
+ * @returns {boolean}
+ */
+ DefaultBanner.prototype.stillIndicatesLoading = function () {
+ return document.getElementById(this.loadingId) != null;
+ };
+
+ /**
+ * @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 Creates the underlying HTML elements comprising the banner
+ * @param {string} bid - The id for the banner
+ */
+ DefaultBanner.prototype.createBanner = function (bid) {
+ this.banner = document.createElement("wb_div", true);
+ this.banner.setAttribute("id", bid);
+ this.banner.setAttribute("lang", "en");
+ this.captureInfo = document.createElement('span');
+ this.captureInfo.innerHTML = 'Loading...';
+ this.captureInfo.id = '_wb_capture_info';
+ this.banner.appendChild(this.captureInfo);
+ document.body.insertBefore(this.banner, 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 "";
}
@@ -31,11 +157,11 @@ This file is part of pywb, https://github.com/webrecorder/pywb
}
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");
+ 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);
@@ -44,22 +170,16 @@ This file is part of pywb, https://github.com/webrecorder/pywb
} else {
return date.toLocaleString();
}
- }
+ };
- function init(bid) {
- var banner = document.createElement("wb_div", true);
-
- banner.setAttribute("id", bid);
- banner.setAttribute("lang", "en");
-
- var text = "";
- text += "Loading...";
-
- banner.innerHTML = text;
- document.body.insertBefore(banner, document.body.firstChild);
- }
-
- function set_banner(url, ts, is_live, title) {
+ /**
+ * @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;
@@ -67,11 +187,11 @@ This file is part of pywb, https://github.com/webrecorder/pywb
return;
}
- var date_str = ts_to_date(ts, true);
+ var date_str = this.ts_to_date(ts, true);
if (title) {
capture_str = title;
- } else {
+ } else {
capture_str = url;
}
@@ -88,69 +208,12 @@ This file is part of pywb, https://github.com/webrecorder/pywb
title_str += " (" + date_str + ")";
capture_str += date_str;
-
- document.querySelector("#_wb_capture_info").innerHTML = capture_str;
+ this.captureInfo.innerHTML = capture_str;
window.document.title = title_str;
- }
-
- if (window.top != window) {
- return;
- }
-
- var last_state = {};
-
- window.addEventListener("load", function() {
- if (window.wbinfo) {
- init("_wb_plain_banner");
-
- set_banner(window.wbinfo.url,
- window.wbinfo.timestamp,
- window.wbinfo.is_live,
- window.wbinfo.is_framed ? "" : document.title);
- } else {
- init("_wb_frame_top_banner");
-
- var state;
- var title = "";
-
- window.addEventListener("message", function(event) {
- var type = event.data.wb_type;
-
- if (type == "load" || type == "replace-url") {
- state = event.data;
- last_state = state;
- title = event.data.title || title;
- } else if (type == "title") {
- state = last_state;
- title = event.data.title;
- } else {
- return;
- }
-
- // favicon update
- if (type === 'load') {
- var head = document.querySelector('head');
- var oldLink = document.querySelectorAll("link[rel*='icon']");
- for (var i = 0; i < oldLink.length; i++) {
- head.removeChild(oldLink[i]);
- }
-
- if (state.icons) {
- for (var i = 0; i < state.icons.length; i++) {
- var icon = state.icons[i];
- var link = document.createElement('link');
- link.rel = icon.rel;
- link.href = icon.href;
- head.appendChild(link);
- }
- }
- }
-
- set_banner(state.url, state.ts, state.is_live, title);
- });
- }
- });
+ };
+ // all banners will expose themselves by adding themselves as WBBanner on window
+ window.WBBanner = new DefaultBanner();
})();
diff --git a/pywb/static/wb_frame.js b/pywb/static/wb_frame.js
index d743e658..e72f2c26 100644
--- a/pywb/static/wb_frame.js
+++ b/pywb/static/wb_frame.js
@@ -18,159 +18,250 @@ This file is part of pywb, https://github.com/webrecorder/pywb
*/
+/**
+ * @param {Object} content_info - Information about the contents to be replayed
+ */
function ContentFrame(content_info) {
+ if (!(this instanceof ContentFrame)) return new ContentFrame(content_info);
this.last_inner_hash = window.location.hash;
this.last_url = content_info.url;
this.last_ts = content_info.request_ts;
-
- this.init_iframe = function() {
- if (typeof(content_info.iframe) === "string") {
- this.iframe = document.querySelector(content_info.iframe);
- } else {
- this.iframe = content_info.iframe;
- }
-
- if (!this.iframe) {
- console.warn("no iframe found " + content_info.iframe + " found");
- return;
- }
-
- this.extract_prefix();
-
- this.load_url(content_info.url, content_info.request_ts);
- }
-
- this.extract_prefix = function() {
- this.app_prefix = content_info.app_prefix || content_info.prefix;
- this.content_prefix = content_info.content_prefix || content_info.prefix;
-
- if (this.app_prefix && this.content_prefix) {
- return;
- }
-
- var inx = window.location.href.indexOf(content_info.url);
-
- if (inx < 0) {
- inx = window.location.href.indexOf("/http") + 1;
- if (inx <= 0) {
- inx = window.location.href.indexOf("///") + 1;
- if (inx <= 0) {
- console.warn("No Prefix Found!");
- }
- }
- }
-
- this.prefix = window.location.href.substr(0, inx);
-
- this.app_prefix = this.app_prefix || this.prefix;
- this.content_prefix = this.content_prefix || this.prefix;
- }
-
-
- this.make_url = function(url, ts, content_url) {
- var mod, prefix;
-
- if (content_url) {
- mod = "mp_";
- prefix = this.content_prefix;
- } else {
- mod = "";
- prefix = this.app_prefix;
- }
-
- if (ts || mod) {
- mod += "/";
- }
-
- if (ts) {
- return prefix + ts + mod + url;
- } else {
- return prefix + mod + url;
- }
- }
-
- this.handle_event = function(event) {
- var frame_win = this.iframe.contentWindow;
- if (event.source == window.parent) {
- // Pass to replay frame
- frame_win.postMessage(event.data, "*");
- } else if (event.source == frame_win) {
-
- // Check if iframe url change message
- if (typeof(event.data) == "object" && event.data["wb_type"]) {
- this.handle_message(event.data);
-
- } else {
- // Pass to parent
- window.parent.postMessage(event.data, "*");
- }
- }
- }
-
- this.handle_message = function(state) {
- var type = state.wb_type;
-
- if (type == "load" || type == "replace-url") {
- this.set_url(state);
- } else if (type == "hashchange") {
- this.inner_hash_changed(state);
- }
- }
-
- this.set_url = function(state) {
- if (state.url && (state.url != this.last_url || state.request_ts != this.last_ts)) {
- var new_url = this.make_url(state.url, state.request_ts, false);
-
- window.history.replaceState(state, "", new_url);
-
- this.last_url = state.url;
- this.last_ts = state.request_ts;
- }
- }
-
- this.load_url = function(newUrl, newTs) {
- this.iframe.src = this.make_url(newUrl, newTs, true);
- }
-
- this.inner_hash_changed = function(state) {
- if (window.location.hash != state.hash) {
- window.location.hash = state.hash;
- }
- this.last_inner_hash = state.hash;
- }
-
- this.outer_hash_changed = function(event) {
- if (window.location.hash == this.last_inner_hash) {
- return;
- }
-
- if (this.iframe) {
- var message = {"wb_type": "outer_hashchange", "hash": window.location.hash}
-
- this.iframe.contentWindow.postMessage(message, "*", undefined, true);
- }
- }
-
- this.close = function() {
- window.removeEventListener("hashchange", this.outer_hash_changed);
- window.removeEventListener("message", this.handle_event);
- }
-
+ this.content_info = content_info;
// bind event callbacks
this.outer_hash_changed = this.outer_hash_changed.bind(this);
this.handle_event = this.handle_event.bind(this);
+ this.wbBanner = null;
+ this.checkBannerToId = null;
- window.addEventListener("hashchange", this.outer_hash_changed, false);
- window.addEventListener("message", this.handle_event);
+ window.addEventListener('hashchange', this.outer_hash_changed, false);
+ window.addEventListener('message', this.handle_event);
- if (document.readyState === "complete") {
+ if (document.readyState === 'complete') {
this.init_iframe();
} else {
- document.addEventListener("DOMContentLoaded", this.init_iframe.bind(this), { once: true });
+ document.addEventListener('DOMContentLoaded', this.init_iframe.bind(this), {once: true});
}
- window.__WB_pmw = function(win) {
+ window.__WB_pmw = function (win) {
this.pm_source = win;
return this;
- }
+ };
}
+
+/**
+ * @desc Initializes the replay iframe. If a banner exists (exposed on window as WBBanner)
+ * then the init function of the banner is called.
+ */
+ContentFrame.prototype.init_iframe = function () {
+ if (typeof (this.content_info.iframe) === 'string') {
+ this.iframe = document.querySelector(this.content_info.iframe);
+ } else {
+ this.iframe = this.content_info.iframe;
+ }
+
+ if (!this.iframe) {
+ console.warn('no iframe found ' + this.content_info.iframe + ' found');
+ return;
+ }
+
+ this.extract_prefix();
+ if (window.WBBanner) {
+ this.wbBanner = window.WBBanner;
+ this.wbBanner.init();
+ }
+ this.load_url(this.content_info.url, this.content_info.request_ts);
+};
+
+/**
+ * @desc Initializes the prefixes used to load the pages to be replayed
+ */
+ContentFrame.prototype.extract_prefix = function () {
+ this.app_prefix = this.content_info.app_prefix || this.content_info.prefix;
+ this.content_prefix = this.content_info.content_prefix || this.content_info.prefix;
+
+ if (this.app_prefix && this.content_prefix) {
+ return;
+ }
+
+ var inx = window.location.href.indexOf(this.content_info.url);
+
+ if (inx < 0) {
+ inx = window.location.href.indexOf('/http') + 1;
+ if (inx <= 0) {
+ inx = window.location.href.indexOf('///') + 1;
+ if (inx <= 0) {
+ console.warn('No Prefix Found!');
+ }
+ }
+ }
+
+ this.prefix = window.location.href.substr(0, inx);
+
+ this.app_prefix = this.app_prefix || this.prefix;
+ this.content_prefix = this.content_prefix || this.prefix;
+};
+
+/**
+ * @desc Returns an absolute URL (with correct prefix and replay modifier) given
+ * the replayed pages URL and optional timestamp and content_url
+ * @param {string} url - The URL of the replayed page
+ * @param {?string} ts - The timestamp of the replayed page
+ * @param {?boolean} content_url - Is the abs URL to be constructed using the content_prefix or app_prefix
+ * @returns {string}
+ */
+ContentFrame.prototype.make_url = function (url, ts, content_url) {
+ var mod, prefix;
+
+ if (content_url) {
+ mod = 'mp_';
+ prefix = this.content_prefix;
+ } else {
+ mod = '';
+ prefix = this.app_prefix;
+ }
+
+ if (ts || mod) {
+ mod += '/';
+ }
+
+ if (ts) {
+ return prefix + ts + mod + url;
+ } else {
+ return prefix + mod + url;
+ }
+};
+
+/**
+ * @desc Handles and routes all messages received from the replay iframe.
+ * @param {MessageEvent} event - A message event potentially containing a message from the replay iframe
+ */
+ContentFrame.prototype.handle_event = function (event) {
+ var frame_win = this.iframe.contentWindow;
+ if (event.source === window.parent) {
+ // Pass to replay frame
+ frame_win.postMessage(event.data, '*');
+ } else if (event.source === frame_win) {
+ // Check if iframe url change message
+ if (typeof (event.data) === 'object' && event.data['wb_type']) {
+ this.handle_message(event);
+ } else {
+ // Pass to parent
+ window.parent.postMessage(event.data, '*');
+ }
+ }
+};
+
+/**
+ * @desc Handles messages intended for the content frame (indicated by data.wb_type). If a banner
+ * is exposed, calls the onMessage function of the exposed banner.
+ * @param {MessageEvent} event - The message event containing a message from the replay iframe
+ */
+ContentFrame.prototype.handle_message = function (event) {
+ if (this.wbBanner) {
+ this.wbBanner.onMessage(event);
+ }
+ var state = event.data;
+ var type = state.wb_type;
+
+ if (type === 'load' || type === 'replace-url') {
+ this.set_url(state);
+ } else if (type === 'hashchange') {
+ this.inner_hash_changed(state);
+ }
+};
+
+/**
+ * @desc Updates the URL of the top frame
+ * @param {Object} state - The contents of a message rreceived from the replay iframe
+ */
+ContentFrame.prototype.set_url = function (state) {
+ if (state.url && (state.url !== this.last_url || state.request_ts !== this.last_ts)) {
+ var new_url = this.make_url(state.url, state.request_ts, false);
+
+ window.history.replaceState(state, '', new_url);
+
+ this.last_url = state.url;
+ this.last_ts = state.request_ts;
+ }
+};
+
+/**
+ * @desc Checks to see if the banner is still indicating the replay iframe is still loading
+ * 2 seconds after the load event is fired by the replay iframe. If the banner is still
+ * indicating the replayed page is loading. Updates the displayed information using
+ * newURL and newTS
+ * @param {string} newUrl - The new URL of the replay iframe
+ * @param {?string} newTs - The new timestamp of the replay iframe. Is falsy if
+ * operating in live mode
+ */
+ContentFrame.prototype.initBannerUpdateCheck = function (newUrl, newTs) {
+ if (!this.wbBanner) return;
+ var contentFrame = this;
+ var replayIframeLoaded = function () {
+ contentFrame.iframe.removeEventListener('load', replayIframeLoaded);
+ contentFrame.checkBannerToId = setTimeout(function () {
+ contentFrame.checkBannerToId = null;
+ if (contentFrame.wbBanner.stillIndicatesLoading()) {
+ contentFrame.wbBanner.updateCaptureInfo(
+ newUrl,
+ newTs,
+ contentFrame.content_prefix.indexOf('/live') !== -1
+ );
+ }
+ }, 2000);
+ };
+ if (this.checkBannerToId) {
+ clearTimeout(this.checkBannerToId);
+ }
+ this.iframe.addEventListener('load', replayIframeLoaded);
+};
+
+
+/**
+ * @desc Navigates the replay iframe to a newURL and if a banner is exposed
+ * the initBannerUpdateCheck function is called.
+ * @param {string} newUrl - The new URL of the replay iframe
+ * @param {?string} newTs - The new timestamp of the replay iframe. Is falsy if
+ * operating in live mode
+ */
+ContentFrame.prototype.load_url = function (newUrl, newTs) {
+ this.iframe.src = this.make_url(newUrl, newTs, true);
+ if (this.wbBanner) {
+ this.initBannerUpdateCheck(newUrl, newTs);
+ }
+};
+
+/**
+ * @desc Updates this frames hash to the one inside the replay iframe
+ * @param {Object} state - The contents of message received from the replay iframe
+ */
+ContentFrame.prototype.inner_hash_changed = function (state) {
+ if (window.location.hash !== state.hash) {
+ window.location.hash = state.hash;
+ }
+ this.last_inner_hash = state.hash;
+};
+
+/**
+ * @desc Updates the hash of the replay iframe on a hash change in this frame
+ * @param event
+ */
+ContentFrame.prototype.outer_hash_changed = function (event) {
+ if (window.location.hash === this.last_inner_hash) {
+ return;
+ }
+
+ if (this.iframe) {
+ var message = {'wb_type': 'outer_hashchange', 'hash': window.location.hash};
+
+ this.iframe.contentWindow.postMessage(message, '*', undefined, true);
+ }
+};
+
+/**
+ * @desc Cleans up any event listeners added by the content frame
+ */
+ContentFrame.prototype.close = function () {
+ window.removeEventListener('hashchange', this.outer_hash_changed);
+ window.removeEventListener('message', this.handle_event);
+};