]+>\s+This article is issued from)/i, '$1class="copyLeft" $2');
// Remove openInTab div (we can't do this using DOM methods because it aborts code spawned from onclick event)
innerDocument.body.innerHTML = innerDocument.body.innerHTML.replace(/
))+<\/div>\s*/, '');
// Using @media print on images doesn't get rid of them all, so use brute force
if (!document.getElementById("printImageCheck").checked)
innerDocument.body.innerHTML = innerDocument.body.innerHTML.replace(/
![]()
]*>\s*/ig, '');
var printOptions = innerDocument.getElementById("printOptions");
//If there is no printOptions style block in the iframe, create it
if (!printOptions) {
var printStyle = innerDocument.createElement("style");
printStyle.id = "printOptions";
innerDocument.head.appendChild(printStyle);
printOptions = innerDocument.getElementById("printOptions");
}
var printStyleInnerHTML = "@media print { ";
printStyleInnerHTML += document.getElementById("printNavBoxCheck").checked ? "" : ".navbox, .vertical-navbox { display: none; } ";
printStyleInnerHTML += document.getElementById("printEndNoteCheck").checked ? "" : ".reflist, div[class*=references], .zimReferences, .zimSources { display: none; } ";
printStyleInnerHTML += document.getElementById("externalLinkCheck").checked ? "" : ".externalLinks, .furtherReading { display: none; } ";
printStyleInnerHTML += document.getElementById("seeAlsoLinkCheck").checked ? "" : ".seeAlso { display: none; } ";
printStyleInnerHTML += document.getElementById("printInfoboxCheck").checked ? "" : ".mw-stack, .infobox, .infobox_v2, .infobox_v3, .qbRight, .qbRightDiv, .wv-quickbar, .wikitable { display: none; } ";
// printStyleInnerHTML += document.getElementById("printImageCheck").checked ? "" : "img, .gallery { display: none; } ";
printStyleInnerHTML += ".copyLeft { display: none } ";
printStyleInnerHTML += ".map-pin { display: none } ";
printStyleInnerHTML += ".external { padding-right: 0 !important } ";
var sliderVal = document.getElementById("documentZoomSlider").value;
sliderVal = ~~sliderVal;
sliderVal = Math.floor(sliderVal * (Math.max(window.screen.width, window.screen.height) / 1440));
printStyleInnerHTML += "body { font-size: " + sliderVal + "% !important; } ";
printStyleInnerHTML += "}";
printOptions.innerHTML = printStyleInnerHTML;
}
function downloadBlobUWP(blob, filename, message) {
// Copy BLOB to downloads folder and launch from there in Edge
// First create an empty file in the folder
Windows.Storage.DownloadsFolder.createFileAsync(filename, Windows.Storage.CreationCollisionOption.generateUniqueName)
.then(function (file) {
// Open the returned dummy file in order to copy the data into it
file.openAsync(Windows.Storage.FileAccessMode.readWrite).then(function (output) {
// Get the InputStream stream from the blob object
var input = blob.msDetachStream();
// Copy the stream from the blob to the File stream
Windows.Storage.Streams.RandomAccessStream.copyAsync(input, output).then(function () {
output.flushAsync().done(function () {
input.close();
output.close();
// Finally, tell the system to open the file if it's not a subtitle file
if (!/\.(?:ttml|ssa|ass|srt|idx|sub|vtt)$/i.test(filename)) Windows.System.Launcher.launchFileAsync(file);
if (file.isAvailable) {
var fileLink = file.path.replace(/\\/g, '/');
fileLink = fileLink.replace(/^([^:]+:\/(?:[^/]+\/)*)(.*)/, function (p0, p1, p2) {
return 'file:///' + p1 + encodeURIComponent(p2);
});
if (message) message.innerHTML = '
Download: Your file was saved as
' + file.path + '';
//window.open(fileLink, null, "msHideView=no");
}
});
});
});
});
}
/**
* Derives the URL.pathname from a relative or semi-relative URL using the given base ZIM URL
*
* @param {String} url The (URI-encoded) URL to convert (e.g. "Einstein", "../Einstein",
* "../../I/im%C3%A1gen.png", "-/s/style.css", "/A/Einstein.html", "../static/bootstrap/css/bootstrap.min.css")
* @param {String} base The base ZIM URL of the currently loaded article (e.g. "A/", "A/subdir1/subdir2/", "C/Singapore/")
* @returns {String} The derived ZIM URL in decoded form (e.g. "A/Einstein", "I/imágen.png", "C/")
*/
function deriveZimUrlFromRelativeUrl(url, base) {
// We use a dummy domain because URL API requires a valid URI
var dummy = 'http://d/';
var deriveZimUrl = function (url, base) {
if (typeof URL === 'function') return new URL(url, base);
// IE11 lacks URL API: workaround adapted from https://stackoverflow.com/a/28183162/9727685
var d = document.implementation.createHTMLDocument('t');
d.head.innerHTML = '
';
var a = d.createElement('a');
a.href = url;
return { pathname: a.href.replace(dummy, '') };
};
var zimUrl = deriveZimUrl(url, dummy + base);
return decodeURIComponent(zimUrl.pathname.replace(/^\//, ''));
}
/**
* Walk up the DOM tree to find the closest element where the tagname matches the supplied regular expression
*
* @param {Element} el The starting DOM element
* @param {RegExp} rgx A regular expression to match the element's tagname
* @returns {Element|null} The matching element or null if no match was found
*/
function getClosestMatchForTagname(el, rgx) {
do {
if (rgx.test(el.tagName)) return el;
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === 1);
return null;
}
/**
* Displays a Bootstrap warning alert with information about how to access content in a ZIM with unsupported active UI
*/
function displayActiveContentWarning() {
// We have to add the alert box in code, because Bootstrap removes it completely from the DOM when the user dismisses it
var alertHTML =
'
' +
'
×' +
'
Unable to display active content: To use Archive Index
type a space in the box above, or else ' +
'
switch to Service Worker mode ' +
'if your platform supports it. [
Permanently hide]' +
'
';
if (params.contentInjectionMode === 'serviceworker' && (params.manipulateImages || params.displayHiddenBlockElements || params.allowHTMLExtraction)) {
alertHTML =
'
' +
'
×' +
'
Active content may be disrupted: Please ' + (params.displayHiddenBlockElements ?
'
disable Display hidden block elements ' :
params.manipulateImages ? '
disable Image manipulation ' : '') +
(params.allowHTMLExtraction ? (params.displayHiddenBlockElements || params.manipulateImages ? 'and ' : '') +
'disable Breakout link ' : '') + 'for this content to work properly. To use Archive Index
type a space ' +
'in the box above. [
Permanently hide]' +
'
';
}
var alertBoxHeader = document.getElementById('alertBoxHeader');
alertBoxHeader.innerHTML = alertHTML;
alertBoxHeader.style.display = 'block';
['swModeLink', 'imModeLink', 'hbeModeLink', 'stop'].forEach(function(id) {
// Define event listeners for both hyperlinks in alert box: these take the user to the Config tab and highlight
// the options that the user needs to select
var modeLink = document.getElementById(id);
if (modeLink) modeLink.addEventListener('click', function () {
var elementID = id === 'stop' ? 'hideActiveContentWarningCheck' :
id === 'swModeLink' ? 'serviceworkerModeRadio' :
id === 'imModeLink' ? 'manipulateImagesCheck' : 'displayHiddenBlockElementsCheck';
var thisLabel = document.getElementById(elementID).parentNode;
thisLabel.style.borderColor = 'red';
thisLabel.style.borderStyle = 'solid';
var btnHome = document.getElementById('btnHome');
[thisLabel, btnHome].forEach(function (ele) {
// Define event listeners to cancel the highlighting both on the highlighted element and on the Home tab
ele.addEventListener('mousedown', function () {
thisLabel.style.borderColor = '';
thisLabel.style.borderStyle = '';
});
});
alertBoxHeader.style.display = 'none';
document.getElementById('btnConfigure').click();
});
});
}
/**
* Displays a Bootstrap alert box at the foot of the page to enable saving the content of the given title to the device's filesystem
* and initiates download/save process if this is supported by the OS or Browser
*
* @param {String} title The path and filename to the file to be extracted
* @param {Boolean|String} download A Bolean value that will trigger download of title, or the filename that should
* be used to save the file in local FS
* @param {String} contentType The mimetype of the downloadable file, if known
* @param {Uint8Array} content The binary-format content of the downloadable file
* @param {Boolean} autoDismiss If true, dismiss the alert programmatically
*/
function displayFileDownloadAlert(title, download, contentType, content, autoDismiss) {
// We have to create the alert box in code, because Bootstrap removes it completely from the DOM when the user dismisses it
document.getElementById('alertBoxFooter').innerHTML =
'
';
// Download code adapted from https://stackoverflow.com/a/19230668/9727685
if (!contentType) {
// DEV: Add more contentTypes here for downloadable files
if (/\.epub$/.test(title)) contentType = 'application/epub+zip';
if (/\.pdf$/.test(title)) contentType = 'application/pdf';
if (/\.zip$/.test(title)) contentType = 'application/zip';
}
// Set default contentType if there has been no match
if (!contentType) contentType = 'application/octet-stream';
var a = document.createElement('a');
var blob = new Blob([content], { 'type': contentType });
// If the filename to use for saving has not been specified, construct it from title
var filename = download === true ? title.replace(/^.*\/([^\/]+)$/, '$1') : download;
// Make filename safe
filename = filename.replace(/[\/\\:*?"<>|]/g, '_');
a.href = window.URL.createObjectURL(blob);
a.target = '_blank';
a.type = contentType;
// if (typeof window.fs === 'undefined') a.download = filename;
a.classList.add('alert-link');
a.innerHTML = filename;
var alertMessage = document.getElementById('alertMessage');
alertMessage.innerHTML = '
Download If the download does not start, please tap the following link: ';
// We have to add the anchor to a UI element for Firefox to be able to click it programmatically: see https://stackoverflow.com/a/27280611/9727685
alertMessage.appendChild(a);
try {
a.click();
// Following line should run only if there was no error, leaving the alert showing in case of error
if (autoDismiss) $('#downloadAlert').alert('close');
return;
}
catch (err) {
// Edge will error out unless there is a download added but Chrome works better without the attribute
a.download = filename;
}
try {
a.click();
// Following line should run only if there was no error, leaving the alert showing in case of error
if (autoDismiss) $('#downloadAlert').alert('close');
}
catch (err) {
// If the click fails, user may be able to download by manually clicking the link
// But for IE11 we need to force use of the saveBlob method with the onclick event
if (window.navigator && window.navigator.msSaveBlob) {
a.addEventListener('click', function (e) {
window.navigator.msSaveBlob(blob, filename);
e.preventDefault();
});
} else {
// And try to launch through UWP download
if (Windows && Windows.Storage) {
downloadBlobUWP(blob, filename, alertMessage);
if (autoDismiss) $('#downloadAlert').alert('close');
} else {
// Last gasp attempt to open automatically
window.open(a.href);
}
}
}
}
/**
* Initiates XMLHttpRequest
* Can be used for loading local files; CSP may restrict access to remote files due to CORS
*
* @param {URL} url The Uniform Resource Locator to be read
* @param {String} responseType The response type to return (arraybuffer|blob|document|json|text);
* (passing an empty or null string defaults to text)
* @param {Function} callback The function to call with the result: data, mimetype, and status or error code
*/
function XHR(url, responseType, callback) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function (e) {
if (this.readyState == 4) {
callback(this.response, this.response.type, this.status);
}
};
var err = false;
try {
xhr.open('GET', url, true);
if (responseType) xhr.responseType = responseType;
}
catch (e) {
console.log("Exception during GET request: " + e);
err = true;
}
if (!err) {
xhr.send();
} else {
callback("Error", null, 500);
}
}
/**
* Inserts a link to break the article out to a new browser tab
*
* @param {String} mode The app mode to use for the breakoutLink icon (light|dark)
*/
function insertBreakoutLink(mode) {
var desc = "Open article in new tab or window";
var iframe = document.getElementById('articleContent').contentDocument;
// This code provides an absolute link, removing the file and any query string from href (we need this because of SW mode)
var prefix = (window.location.protocol + '//' + window.location.host + window.location.pathname).replace(/\/[^/]*$/, '');
var div = document.createElement('div');
div.style.cssText = 'top: 10px; right: 25px; position: relative; z-index: 2; float: right;';
div.id = "openInTab";
div.innerHTML = '
 + ')
';
iframe.body.insertBefore(div, iframe.body.firstChild);
var openInTab = iframe.getElementById('openInTab');
// Have to use jQuery here becasue e.preventDefault is not working properly in some browsers
$(openInTab).on('click', function() {
itemsCount = false;
params.preloadingAllImages = false;
extractHTML();
return false;
});
}
/**
* Extracts self-contained HTML from the iframe DOM, transforming BLOB references to dataURIs
*/
function extractHTML() {
if (params.preloadingAllImages !== true) {
params.preloadAllImages();
return;
}
var iframe = document.getElementById('articleContent').contentDocument;
// Store the html for the head section, to restore later (in SW mode, stylesheets will be transformed to dataURI links,
// which only work from file:/// URL, due to CORS, so they have to be restored)
var headHtml = iframe.head.innerHTML;
var title = iframe.title;
if (itemsCount === false) {
// Establish the source items that need to be extracted to self-contained URIs
// DEV: Add any further sources to the querySelector below
var items = iframe.querySelectorAll('img[src],link[href][rel="stylesheet"]');
itemsCount = items.length;
Array.prototype.slice.call(items).forEach(function (item) {
// Extract the BLOB itself from the URL (even if it's a blob: URL)
var itemUrl = item.href || item.src;
XHR(itemUrl, 'blob', function (response, mimetype, status) {
if (status == 500) {
itemsCount--;
return;
}
// Pure SVG images may be missing the mimetype
if (!mimetype) mimetype = /\.svg$/i.test(itemUrl) ? 'image/svg+xml' : '';
// Now read the data from the extracted blob
var myReader = new FileReader();
myReader.addEventListener("loadend", function () {
if (myReader.result) {
var dataURL = myReader.result.replace(/data:;/, 'data:' + mimetype + ';');
if (item.href) {
try { item.href = dataURL; }
catch (err) { null; }
}
if (item.src) {
try { item.src = dataURL; }
catch (err) { null; }
}
}
itemsCount--;
if (itemsCount === 0) extractHTML();
});
//Start the reading process.
myReader.readAsDataURL(response);
});
});
}
if (itemsCount > 0) return; //Ensures function stops if we are still extracting images or css
// Construct filename (forbidden characters will be removed in the download function)
var filename = title.replace(/(\.html?)*$/i, '.html');
var html = iframe.documentElement.outerHTML;
// Remove openInTab div (we can't do this using DOM methods because it aborts code spawned from onclick event)
html = html.replace(/
))+<\/div>\s*/, '');
var blob = new Blob([html], { type: 'text/html' });
// We can't use window.open() because pop-up blockers block it, so use explicit BLOB download
displayFileDownloadAlert(title, filename, 'text/html', blob, true);
// Restore original head section (to restore any transformed stylesheets)
iframe.head.innerHTML = headHtml;
itemsCount = false;
params.preloadingAllImages = false;
clearSpinner();
}
/**
* Displays a Bootstrap alert or confirm dialog box depending on the options provided
* Credit to @gaurav7019 for this code (slightly adapted here) - see Kiwix JS #804
*
* @param {String} message The alert message to display in the body of the modal
* @param {String} label The modal's label or title which appears in the header (optional, Default = "Confirmation" or "Message")
* @param {Boolean} isConfirm If true, the modal will be a confirm dialog box, otherwise it will be a simple alert message
* @param {String} declineConfirmLabel The text to display on the decline confirmation button (optional, Default = "Cancel")
* @param {String} approveConfirmLabel The text to display on the approve confirmation button (optional, Default = "Confirm")
* @param {String} closeMessageLabel The text to display on the close alert message button (optional, Default = "Okay")
* @returns {Promise} A promise which resolves to true if the user clicked Confirm, false if the user clicked Cancel/Okay, backdrop or the cross(x) button
*/
function systemAlert(message, label, isConfirm, declineConfirmLabel, approveConfirmLabel, closeMessageLabel) {
declineConfirmLabel = declineConfirmLabel || "Cancel";
approveConfirmLabel = approveConfirmLabel || "Confirm";
closeMessageLabel = closeMessageLabel || "OK";
label = label || (isConfirm ? "Confirmation" : "Message");
return new Promise(function (resolve, reject) {
if (!message) reject("Missing body message");
// Set the text to the modal and it's buttons
document.getElementById("approveConfirm").textContent = approveConfirmLabel;
document.getElementById("declineConfirm").textContent = declineConfirmLabel;
document.getElementById("closeMessage").textContent = closeMessageLabel;
document.getElementById("modalLabel").textContent = label;
document.getElementById("modalText").textContent = message;
// Display buttons acc to the type of alert
document.getElementById("approveConfirm").style.display = isConfirm ? "inline" : "none";
document.getElementById("declineConfirm").style.display = isConfirm ? "inline" : "none";
document.getElementById("closeMessage").style.display = isConfirm ? "none" : "inline";
// Display the modal
$("#alertModal").modal("show");
// When hide model is called, resolve promise with true if hidden using approve button, false otherwise
$("#alertModal").on("hide.bs.modal", function () {
const closeSource = document.activeElement;
if (closeSource.id === "approveConfirm") {
resolve(true);
} else {
resolve(false);
}
});
});
}
/**
* Checks if a server is accessible by attempting to load a test image from the server
*
* @param {String} imageSrc The full URI of the image
* @param {any} onSuccess A function to call if the image can be loaded
* @param {any} onError A function to call if the image cannot be loaded
*/
function checkServerIsAccessible(imageSrc, onSuccess, onError) {
var image = new Image();
image.onload = onSuccess;
image.onerror = onError;
image.src = imageSrc;
}
/**
* Checks whether an element is partially or fully inside the current viewport, and adds the rect.top value to element.top
*
* @param {Window} area The Window to check
* @param {Element} el The DOM element for which to check visibility
* @param {Boolean} fully If true, checks that the entire element is inside the viewport
* @param {Integer} offset An additional bottom (+) or top (-) margin to include in the search window
* @returns {Boolean} True if the element is fully or partially inside the current viewport
*/
function isElementInView(area, el, fully, offset) {
offset = offset || 0;
var rect = el.getBoundingClientRect();
el.top = rect.top;
//console.log(el.dataset.kiwixurl + ': ' + rect.top);
if (fully)
return rect.top > 0 + (offset < 0 ? offset : 0) && rect.bottom < area.innerHeight + (offset > 0 ? offset : 0) && rect.left > 0 && rect.right < area.innerWidth;
else
return rect.top < area.innerHeight + (offset > 0 ? offset : 0) && rect.bottom > 0 + (offset < 0 ? offset : 0) && rect.left < area.innerWidth && rect.right > 0;
}
/**
* Initiates pointer touch events on the given element in order to set the zoom level
*
* @param {Element} element The element to which the pointer events should be attached
* @param {Node} container The node to which the pointer events should be applied, if different
*/
function initTouchZoom(element, container) {
container = container || element;
// Global vars to cache event state
appstate.evCache = new Array();
appstate.prevDiff = -1;
// Set initial element transforms
var contentWin = element.ownerDocument.defaultView || element.ownerDocument.parentWindow;
container.style.transformOrigin = 'left top'; // DEV: To support RTL languages, this should be 'right top'
appstate.windowScale = 1;
appstate.sessionScale = 1;
appstate.startVector = null;
// Install event handlers for the pointer target
element.onpointerdown = pointerdown_handler;
element.onpointermove = function(event) {
pointermove_handler(event, container, contentWin);
};
// Use same handler for pointer{up,cancel,out,leave} events since
// the semantics for these events - in this app - are the same.
element.onpointerup = pointerup_handler;
element.onpointercancel = pointerup_handler;
element.onpointerout = pointerup_handler;
element.onpointerleave = pointerup_handler;
}
function pointerdown_handler(ev) {
// The pointerdown event signals the start of a touch interaction.
// This event is cached to support 2-finger gestures
appstate.evCache.push(ev);
// console.debug('pointerDown', ev);
}
function pointermove_handler(ev, cont, win) {
// This function implements a 2-pointer horizontal pinch/zoom gesture.
// console.debug('pointerMove', ev);
// Find this event in the cache and update its record with this event
for (var i = 0; i < appstate.evCache.length; i++) {
if (ev.pointerId == appstate.evCache[i].pointerId) {
appstate.evCache[i] = ev;
break;
}
}
// If two pointers are down, check for pinch gestures
if (appstate.evCache.length == 2) {
ev.preventDefault();
// Calculate the distance between the two pointers
var x0 = appstate.evCache[0].clientX;
var y0 = appstate.evCache[0].clientY;
var x1 = appstate.evCache[1].clientX;
var y1 = appstate.evCache[1].clientY;
var curDiff = Math.abs(Math.sqrt(Math.pow(x1 - x0, 2) + Math.pow(y1 - y0, 2)));
// console.debug('Current difference: ' + curDiff);
if (appstate.prevDiff > 0) {
if (appstate.startVector === null) {
appstate.startVector = curDiff;
appstate.scrollXStart = win.scrollX / appstate.windowScale;
appstate.scrollYStart = win.scrollY / appstate.windowScale;
// console.debug('scrollXStart: ' + appstate.scrollXStart);
}
appstate.windowScale = appstate.sessionScale * curDiff / appstate.startVector;
// console.debug('winScrollY: ' + win.scrollY);
// console.debug('winScrollX: ' + win.scrollX);
// console.debug('x0x1 mean: ' + (x0 + x1)/2);
// console.debug('y0y1 mean: ' + (y0 + y1)/2);
cont.style.transform = 'scale(' + appstate.windowScale + ')';
win.scrollTo(appstate.scrollXStart * appstate.windowScale, appstate.scrollYStart * appstate.windowScale);
}
// Cache the distance for the next move event
appstate.prevDiff = curDiff;
}
}
function pointerup_handler(ev) {
// console.debug(ev.type, ev);
// Remove this pointer from the cache
remove_event(ev);
// If the number of pointers down is less than two then reset diff tracker
if (appstate.evCache.length < 2) {
appstate.prevDiff = -1;
}
}
function remove_event(ev) {
// Remove this event from the target's cache
for (var i = 0; i < appstate.evCache.length; i++) {
if (appstate.evCache[i].pointerId == ev.pointerId) {
appstate.evCache.splice(i, 1);
break;
}
}
appstate.startVector = null;
appstate.sessionScale = appstate.windowScale;
}
// Reports an error in loading one of the ASM or WASM machines to the UI API Status Panel
// This can't be done in app.js because the error occurs after the API panel is first displayed
function reportAssemblerErrorToAPIStatusPanel(decoderType, error, assemblerMachineType) {
console.error('Could not instantiate any ' + decoderType + ' decoder!', error);
params.decompressorAPI.assemblerMachineType = assemblerMachineType;
params.decompressorAPI.errorStatus = 'Error loading ' + decoderType + ' decompressor!';
var decompAPI = document.getElementById('decompressorAPIStatus');
decompAPI.innerHTML = 'Decompressor API: ' + params.decompressorAPI.errorStatus;
decompAPI.className = 'apiBroken';
document.getElementById('apiStatusDiv').className = 'panel panel-danger';
}
// If global variable webpMachine is true (set in init.js), then we need to initialize the WebP Polyfill
if (webpMachine) webpMachine = new webpHero.WebpMachine();
/**
* Functions and classes exposed by this module
*/
return {
systemAlert: systemAlert,
feedNodeWithBlob: feedNodeWithBlob,
deriveZimUrlFromRelativeUrl: deriveZimUrlFromRelativeUrl,
getClosestMatchForTagname: getClosestMatchForTagname,
removeUrlParameters: removeUrlParameters,
toc: TableOfContents,
isElementInView: isElementInView,
makeReturnLink: makeReturnLink,
pollSpinner: pollSpinner,
clearSpinner: clearSpinner,
XHR: XHR,
printCustomElements: printCustomElements,
downloadBlobUWP: downloadBlobUWP,
displayActiveContentWarning: displayActiveContentWarning,
displayFileDownloadAlert: displayFileDownloadAlert,
insertBreakoutLink: insertBreakoutLink,
extractHTML: extractHTML,
checkServerIsAccessible: checkServerIsAccessible,
initTouchZoom: initTouchZoom,
reportAssemblerErrorToAPIStatusPanel: reportAssemblerErrorToAPIStatusPanel
};
});