diff --git a/service-worker.js b/service-worker.js
index 90c2b4c7..a41bfbc3 100644
--- a/service-worker.js
+++ b/service-worker.js
@@ -1,4 +1,4 @@
-/**
+/**
* service-worker.js : Service Worker implementation,
* in order to capture the HTTP requests made by an article, and respond with the
* corresponding content, coming from the archive
@@ -23,9 +23,14 @@
*/
'use strict';
+/**
+ * A global Boolean that governs whether images are displayed
+ * app.js can alter this variable via messaging
+ */
+var imageDisplay;
+
self.addEventListener('install', function(event) {
event.waitUntil(self.skipWaiting());
- console.log("ServiceWorker installed");
});
self.addEventListener('activate', function(event) {
@@ -33,7 +38,6 @@ self.addEventListener('activate', function(event) {
// without the need to reload the page.
// See https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim
event.waitUntil(self.clients.claim());
- console.log("ServiceWorker activated");
});
var regexpRemoveUrlParameters = new RegExp(/([^?#]+)[?#].*$/);
@@ -53,85 +57,54 @@ var regexpRemoveUrlParameters = new RegExp(/([^?#]+)[?#].*$/);
function removeUrlParameters(url) {
return url.replace(regexpRemoveUrlParameters, "$1");
}
-
-console.log("ServiceWorker startup");
var outgoingMessagePort = null;
var fetchCaptureEnabled = false;
self.addEventListener('fetch', fetchEventListener);
-console.log('fetchEventListener set');
self.addEventListener('message', function (event) {
if (event.data.action === 'init') {
- console.log('Init message received', event.data);
+ // On 'init' message, we initialize the outgoingMessagePort and enable the fetchEventListener
outgoingMessagePort = event.ports[0];
- console.log('outgoingMessagePort initialized', outgoingMessagePort);
fetchCaptureEnabled = true;
- console.log('fetchEventListener enabled');
}
if (event.data.action === 'disable') {
- console.log('Disable message received');
+ // On 'disable' message, we delete the outgoingMessagePort and disable the fetchEventListener
outgoingMessagePort = null;
- console.log('outgoingMessagePort deleted');
fetchCaptureEnabled = false;
- console.log('fetchEventListener disabled');
}
});
-// TODO : this way to recognize content types is temporary
-// It must be replaced by reading the actual MIME-Type from the backend
-var regexpJPEG = new RegExp(/\.jpe?g$/i);
-var regexpPNG = new RegExp(/\.png$/i);
-var regexpJS = new RegExp(/\.js/i);
-var regexpCSS = new RegExp(/\.css$/i);
-var regexpSVG = new RegExp(/\.svg$/i);
-
-// Pattern for ZIM file namespace - see http://www.openzim.org/wiki/ZIM_file_format#Namespaces
+// Pattern for ZIM file namespace - see https://wiki.openzim.org/wiki/ZIM_file_format#Namespaces
var regexpZIMUrlWithNamespace = new RegExp(/(?:^|\/)([-ABIJMUVWX])\/(.+)/);
function fetchEventListener(event) {
if (fetchCaptureEnabled) {
- console.log('ServiceWorker handling fetch event for : ' + event.request.url);
-
if (regexpZIMUrlWithNamespace.test(event.request.url)) {
+ // The ServiceWorker will handle this request
- console.log('Asking app.js for a content', event.request.url);
+ // If the user has disabled the display of images, and the browser wants an image, respond with empty SVG
+ // A URL with "?kiwix-display" query string acts as a passthrough so that the regex will not match and
+ // the image will be fetched by app.js
+ // DEV: If you need to hide more image types, add them to regex below and also edit equivalent regex in app.js
+ if (imageDisplay !== 'all' && /(^|\/)[IJ]\/.*\.(jpe?g|png|svg|gif)($|[?#])(?!kiwix-display)/i.test(event.request.url)) {
+ var svgResponse;
+ if (imageDisplay === 'manual')
+ svgResponse = "";
+ else
+ svgResponse = "";
+ event.respondWith(new Response(svgResponse, { headers: { 'Content-Type': 'image/svg+xml' } }));
+ return;
+ }
+
+ // Let's ask app.js for that content
event.respondWith(new Promise(function(resolve, reject) {
var nameSpace;
var title;
var titleWithNameSpace;
- var contentType;
var regexpResult = regexpZIMUrlWithNamespace.exec(event.request.url);
- nameSpace = regexpResult[1];
- title = regexpResult[2];
-
- // The namespace defines the type of content. See http://www.openzim.org/wiki/ZIM_file_format#Namespaces
- // TODO : read the contentType from the ZIM file instead of hard-coding it here
- if (nameSpace === 'A') {
- console.log("It's an article : " + title);
- contentType = 'text/html';
- }
- else if (nameSpace === 'I' || nameSpace === 'J') {
- console.log("It's an image : " + title);
- if (regexpJPEG.test(title)) {
- contentType = 'image/jpeg';
- }
- else if (regexpPNG.test(title)) {
- contentType = 'image/png';
- }
- else if (regexpSVG.test(title)) {
- contentType = 'image/svg+xml';
- }
- }
- else if (nameSpace === '-') {
- console.log("It's a layout dependency : " + title);
- if (regexpJS.test(title)) {
- contentType = 'text/javascript';
- }
- else if (regexpCSS.test(title)) {
- contentType = 'text/css';
- }
- }
+ nameSpace = regexpResult[1];
+ title = regexpResult[2];
// We need to remove the potential parameters in the URL
title = removeUrlParameters(decodeURIComponent(title));
@@ -142,31 +115,44 @@ function fetchEventListener(event) {
var messageChannel = new MessageChannel();
messageChannel.port1.onmessage = function(event) {
if (event.data.action === 'giveContent') {
- console.log('content message received for ' + titleWithNameSpace, event.data);
+ // Content received from app.js
+ var contentLength = event.data.content ? event.data.content.byteLength : null;
+ var contentType = event.data.mimetype;
+ // Set the imageDisplay variable if it has been sent in the event data
+ imageDisplay = typeof event.data.imageDisplay !== 'undefined' ?
+ event.data.imageDisplay : imageDisplay;
+ var headers = new Headers ();
+ if (contentLength) headers.set('Content-Length', contentLength);
+ if (contentType) headers.set('Content-Type', contentType);
+ // Test if the content is a video or audio file
+ // See kiwix-js #519 and openzim/zimwriterfs #113 for why we test for invalid types like "mp4" or "webm" (without "video/")
+ // The full list of types produced by zimwriterfs is in https://github.com/openzim/zimwriterfs/blob/master/src/tools.cpp
+ if (contentLength >= 1 && /^(video|audio)|(^|\/)(mp4|webm|og[gmv]|mpeg)$/i.test(contentType)) {
+ // In case of a video (at least), Chrome and Edge need these HTTP headers else seeking doesn't work
+ // (even if we always send all the video content, not the requested range, until the backend supports it)
+ headers.set('Accept-Ranges', 'bytes');
+ headers.set('Content-Range', 'bytes 0-' + (contentLength-1) + '/' + contentLength);
+ }
var responseInit = {
status: 200,
statusText: 'OK',
- headers: {
- 'Content-Type': contentType
- }
+ headers: headers
};
var httpResponse = new Response(event.data.content, responseInit);
- console.log('ServiceWorker responding to the HTTP request for ' + titleWithNameSpace + ' (size=' + event.data.content.length + ' octets)' , httpResponse);
+ // Let's send the content back from the ServiceWorker
resolve(httpResponse);
}
else if (event.data.action === 'sendRedirect') {
resolve(Response.redirect(event.data.redirectUrl));
}
else {
- console.log('Invalid message received from app.js for ' + titleWithNameSpace, event.data);
+ console.error('Invalid message received from app.js for ' + titleWithNameSpace, event.data);
reject(event.data);
}
};
- console.log('Eventlistener added to listen for an answer to ' + titleWithNameSpace);
outgoingMessagePort.postMessage({'action': 'askForContent', 'title': titleWithNameSpace}, [messageChannel.port2]);
- console.log('Message sent to app.js through outgoingMessagePort');
}));
}
// If event.respondWith() isn't called because this wasn't a request that we want to handle,
diff --git a/www/css/app.css b/www/css/app.css
index d06499d1..0392ef09 100644
--- a/www/css/app.css
+++ b/www/css/app.css
@@ -97,7 +97,7 @@ div:not(.panel-success, .alert-message) {
background: lightblue;
}
-#articleList a:hover, #articleList a.hover {
+#articleList a:hover, #articleList a.hover, .backgroundLightBlue {
background: lightblue;
}
diff --git a/www/index.html b/www/index.html
index 437be675..3eb671b8 100644
--- a/www/index.html
+++ b/www/index.html
@@ -630,18 +630,23 @@
-
Content injection mode: do not touch unless you know what you're doing!
+
Content injection mode
+
+
+
+
API Status
+
diff --git a/www/js/app.js b/www/js/app.js
index 99c7fc0d..789f1ff4 100644
--- a/www/js/app.js
+++ b/www/js/app.js
@@ -26,14 +26,22 @@
// This uses require.js to structure javascript:
// http://requirejs.org/docs/api.html#define
-define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module', 'transformStyles', 'kiwixServe'],
- function ($, zimArchiveLoader, util, uiUtil, cookies, q, module, transformStyles, kiwixServe) {
+define(['jquery', 'zimArchiveLoader', 'uiUtil', 'images', 'cookies', 'abstractFilesystemAccess', 'q', 'transformStyles', 'kiwixServe'],
+ function ($, zimArchiveLoader, uiUtil, images, cookies, abstractFilesystemAccess, q, transformStyles, kiwixServe) {
/**
* Maximum number of articles to display in a search
* @type Integer
*/
var MAX_SEARCH_RESULT_SIZE = params.results; //This value is controlled in init.js, as are all parameters
+ /**
+ * The delay (in milliseconds) between two "keepalive" messages
+ * sent to the ServiceWorker (so that it is not stopped by
+ * the browser, and keeps the MessageChannel to communicate
+ * with the application)
+ * @type Integer
+ */
+ var DELAY_BETWEEN_KEEPALIVE_SERVICEWORKER = 30000;
/**
* @type ZIMArchive
@@ -1117,11 +1125,15 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
* Displays or refreshes the API status shown to the user
*/
function refreshAPIStatus() {
+ var apiStatusPanel = document.getElementById('apiStatusDiv');
+ apiStatusPanel.classList.remove('panel-success', 'panel-warning');
+ var apiPanelClass = 'panel-success';
if (isMessageChannelAvailable()) {
$('#messageChannelStatus').html("MessageChannel API available");
$('#messageChannelStatus').removeClass("apiAvailable apiUnavailable")
.addClass("apiAvailable");
} else {
+ apiPanelClass = 'panel-warning';
$('#messageChannelStatus').html("MessageChannel API unavailable");
$('#messageChannelStatus').removeClass("apiAvailable apiUnavailable")
.addClass("apiUnavailable");
@@ -1132,15 +1144,18 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
$('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable")
.addClass("apiAvailable");
} else {
+ apiPanelClass = 'panel-warning';
$('#serviceWorkerStatus').html("ServiceWorker API available, but not registered");
$('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable")
.addClass("apiUnavailable");
}
} else {
+ apiPanelClass = 'panel-warning';
$('#serviceWorkerStatus').html("ServiceWorker API unavailable");
$('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable")
.addClass("apiUnavailable");
}
+ apiStatusPanel.classList.add(apiPanelClass);
}
var contentInjectionMode;
@@ -1160,7 +1175,6 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
// Send the init message to the ServiceWorker, with this MessageChannel as a parameter
navigator.serviceWorker.controller.postMessage({'action': 'init'}, [tmpMessageChannel.port2]);
messageChannel = tmpMessageChannel;
- console.log("init message sent to ServiceWorker");
// Schedule to do it again regularly to keep the 2-way communication alive.
// See https://github.com/kiwix/kiwix-js/issues/145 to understand why
clearTimeout(keepAliveServiceWorkerHandle);
@@ -1201,7 +1215,7 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
if (!isServiceWorkerReady()) {
$('#serviceWorkerStatus').html("ServiceWorker API available : trying to register it...");
navigator.serviceWorker.register('../service-worker.js').then(function (reg) {
- console.log('serviceWorker registered', reg);
+ // The ServiceWorker is registered
serviceWorkerRegistration = reg;
refreshAPIStatus();
@@ -1210,6 +1224,8 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
var serviceWorker = reg.installing || reg.waiting || reg.active;
serviceWorker.addEventListener('statechange', function (statechangeevent) {
if (statechangeevent.target.state === 'activated') {
+ // Remove any jQuery hooks from a previous jQuery session
+ $('#articleContent').contents().remove();
// Create the MessageChannel
// and send the 'init' message to the ServiceWorker
initOrKeepAliveServiceWorker();
@@ -1225,45 +1241,33 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
}, function (err) {
console.error('error while registering serviceWorker', err);
refreshAPIStatus();
+ var message = "The ServiceWorker could not be properly registered. Switching back to jQuery mode. Error message : " + err;
+ var protocol = window.location.protocol;
+ if (protocol === 'moz-extension:') {
+ message += "\n\nYou seem to be using kiwix-js through a Firefox extension : ServiceWorkers are disabled by Mozilla in extensions.";
+ message += "\nPlease vote for https://bugzilla.mozilla.org/show_bug.cgi?id=1344561 so that some future Firefox versions support it";
+ }
+ else if (protocol === 'file:') {
+ message += "\n\nYou seem to be opening kiwix-js with the file:// protocol. You should open it through a web server : either through a local one (http://localhost/...) or through a remote one (but you need SSL : https://webserver/...)";
+ }
+ uiUtil.systemAlert(message);
+ setContentInjectionMode("jquery");
+ return;
});
} else {
+ // We need to set this variable earlier else the ServiceWorker does not get reactivated
+ contentInjectionMode = value;
initOrKeepAliveServiceWorker();
}
}
$('input:radio[name=contentInjectionMode]').prop('checked', false);
$('input:radio[name=contentInjectionMode]').filter('[value="' + value + '"]').prop('checked', true);
contentInjectionMode = value;
+ images.setContentInjectionMode(contentInjectionMode);
// Save the value in a cookie, so that to be able to keep it after a reload/restart
cookies.setItem('lastContentInjectionMode', value, Infinity);
}
- /**
- * If the ServiceWorker mode is selected, warn the user before activating it
- * @param chosenContentInjectionMode The mode that the user has chosen
- */
- function checkWarnServiceWorkerMode(chosenContentInjectionMode) {
- if (chosenContentInjectionMode === 'serviceworker' && !cookies.hasItem("warnedServiceWorkerMode")) {
- // The user selected the "serviceworker" mode, which is still unstable
- // So let's display a warning to the user
-
- // If the focus is on the search field, we have to move it,
- // else the keyboard hides the message
- if ($("#prefix").is(":focus")) {
- $("#searchArticles").focus();
- }
- if (confirm("The 'Service Worker' mode is still UNSTABLE for now." +
- " It happens that the application needs to be reinstalled (or the ServiceWorker manually removed)." +
- " Please confirm with OK that you're ready to face this kind of bugs, or click Cancel to stay in 'jQuery' mode.")) {
- // We will not display this warning again for one day
- cookies.setItem("warnedServiceWorkerMode", true, 86400);
- return true;
- } else {
- return false;
- }
- }
- return true;
- }
-
// At launch, we try to set the last content injection mode (stored in a cookie)
var lastContentInjectionMode = cookies.getItem('lastContentInjectionMode');
if (lastContentInjectionMode) {
@@ -1313,7 +1317,6 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
* @type Array.
*/
var storages = [];
- //var storages = [appFolder.path]; //UWP @UWP
function searchForArchivesInPreferencesOrStorage() {
// First see if the list of archives is stored in the cookie
var listOfArchivesFromCookie = cookies.getItem("listOfArchives");
@@ -1600,16 +1603,16 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
function displayFileSelect() {
document.getElementById('openLocalFiles').style.display = 'block';
// Set the main drop zone
- scrollBoxDropZone.addEventListener('dragover', handleGlobalDragover, false);
- scrollBoxDropZone.addEventListener('dragleave', function (e) {
+ scrollBoxDropZone.addEventListener('dragover', handleGlobalDragover);
+ scrollBoxDropZone.addEventListener('dragleave', function(e) {
configDropZone.style.border = '';
});
// Also set a global drop zone (allows us to ensure Config is always displayed for the file drop)
globalDropZone.addEventListener('dragover', function (e) {
e.preventDefault();
- if (document.getElementById('configuration').style.display === 'none') document.getElementById('btnConfigure').click();
+ if (configDropZone.style.display === 'none') document.getElementById('btnConfigure').click();
e.dataTransfer.dropEffect = 'link';
- }, false);
+ });
globalDropZone.addEventListener('drop', handleFileDrop);
// This handles use of the file picker
document.getElementById('archiveFiles').addEventListener('change', setLocalArchiveFromFileSelect);
@@ -1825,11 +1828,12 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
request.responseType = "blob";
request.onreadystatechange = function () {
if (request.readyState === XMLHttpRequest.DONE) {
- if (request.status >= 200 && request.status < 300 || request.status === 0) {
+ if ((request.status >= 200 && request.status < 300) || request.status === 0) {
// Hack to make this look similar to a file
request.response.name = url;
deferred.resolve(request.response);
- } else {
+ }
+ else {
deferred.reject("HTTP status " + request.status + " when reading " + url);
}
}
@@ -2077,9 +2081,9 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module'
function findDirEntryFromDirEntryIdAndLaunchArticleRead(dirEntryId) {
if (selectedArchive.isReady()) {
var dirEntry = selectedArchive.parseDirEntryId(dirEntryId);
- // Remove focus from search field to hide keyboard
- document.getElementById('searchArticles').focus();
- document.getElementById('searchingArticles').style.display = 'block';
+ // Remove focus from search field to hide keyboard and to allow navigation keys to be used
+ document.getElementById('articleContent').contentWindow.focus();
+ $("#searchingArticles").show();
if (dirEntry.isRedirect()) {
selectedArchive.resolveRedirect(dirEntry, readArticle);
} else {
diff --git a/www/js/lib/images.js b/www/js/lib/images.js
new file mode 100644
index 00000000..3abe0739
--- /dev/null
+++ b/www/js/lib/images.js
@@ -0,0 +1,183 @@
+/**
+ * images.js : Functions for the processing of images
+ *
+ * Copyright 2013-2019 Mossroy and contributors
+ * License GPL v3:
+ *
+ * This file is part of Kiwix.
+ *
+ * Kiwix 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.
+ *
+ * Kiwix 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 Kiwix (file LICENSE-GPLv3.txt). If not, see
+ */
+'use strict';
+define(['uiUtil', 'cookies'], function(uiUtil, cookies) {
+
+ /**
+ * Declare a module-specific variable defining the contentInjectionMode. Its value may be
+ * changed in setContentInjectionMode()
+ */
+ var contentInjectionMode = cookies.getItem('lastContentInjectionMode');
+
+ /**
+ * Iterates over an array or collection of image nodes, extracting the image data from the ZIM
+ * and inserting a BLOB URL to each image in the image's src attribute
+ *
+ * @param {Object} images An array or collection of DOM image nodes
+ * @param {Object} selectedArchive The ZIM archive picked by the user
+ */
+ function extractImages(images, selectedArchive) {
+ Array.prototype.slice.call(images).forEach(function (image) {
+ var imageUrl = image.getAttribute('data-kiwixurl');
+ if (!imageUrl) return;
+ image.removeAttribute('data-kiwixurl');
+ var title = decodeURIComponent(imageUrl);
+ if (contentInjectionMode === 'serviceworker') {
+ image.src = imageUrl + '?kiwix-display';
+ } else {
+ selectedArchive.getDirEntryByTitle(title).then(function (dirEntry) {
+ return selectedArchive.readBinaryFile(dirEntry, function (fileDirEntry, content) {
+ image.style.background = '';
+ var mimetype = dirEntry.getMimetype();
+ uiUtil.feedNodeWithBlob(image, 'src', content, mimetype);
+ });
+ }).fail(function (e) {
+ console.error('Could not find DirEntry for image: ' + title, e);
+ });
+ }
+ });
+ }
+
+ /**
+ * Iterates over an array or collection of image nodes, preparing each node for manual image
+ * extraction when user taps the indicated area
+ *
+ * @param {Object} images An array or collection of DOM image nodes
+ * @param {Object} selectedArchive The ZIM archive picked by the user
+ */
+ function setupManualImageExtraction(images, selectedArchive) {
+ Array.prototype.slice.call(images).forEach(function (image) {
+ var originalHeight = image.getAttribute('height') || '';
+ //Ensure 36px clickable image height so user can request images by tapping
+ image.height = '36';
+ if (contentInjectionMode ==='jquery') {
+ image.src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E";
+ image.style.background = 'lightblue';
+ }
+ image.dataset.kiwixheight = originalHeight;
+ image.addEventListener('click', function (e) {
+ // If the image clicked on hasn't been extracted yet, cancel event bubbling, so that we don't navigate
+ // away from the article if the image is hyperlinked
+ if (image.dataset.kiwixurl) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ var visibleImages = queueImages(images);
+ visibleImages.forEach(function (image) {
+ if (image.dataset.kiwixheight) image.height = image.dataset.kiwixheight;
+ else image.removeAttribute('height');
+ // Line below provides a visual indication to users of slow browsers that their click has been registered and
+ // images are being fetched; this is not necessary in SW mode because SW is only supported by faster browsers
+ if (contentInjectionMode ==='jquery') image.style.background = 'lightgray';
+ });
+ extractImages(visibleImages, selectedArchive);
+ });
+ });
+ }
+
+ /**
+ * Sorts an array or collection of image nodes, returning a list of those that are inside the visible viewport
+ *
+ * @param {Object} images An array or collection of DOM image nodes
+ * @returns {Array} An array of image nodes that are within the visible viewport
+ */
+ function queueImages(images) {
+ var visibleImages = [];
+ for (var i = 0, l = images.length; i < l; i++) {
+ if (!images[i].dataset.kiwixurl) continue;
+ if (uiUtil.isElementInView(images[i])) {
+ visibleImages.push(images[i]);
+ }
+ }
+ return visibleImages;
+ }
+
+ /**
+ * Prepares an array or collection of image nodes that have been disabled in Service Worker for manual extraction
+ *
+ * @param {Object} images An array or collection of DOM image nodes
+ * @param {String} displayType If 'progressive', lazyLoad will be used; if 'manual', setupManualImageExtraction will be used
+ */
+ function prepareImagesServiceWorker (images, displayType) {
+ var zimImages = [];
+ for (var i = 0, l = images.length; i < l; i++) {
+ // DEV: make sure list of file types here is the same as the list in Service Worker code
+ if (/(^|\/)[IJ]\/.*\.(jpe?g|png|svg|gif)($|[?#])/i.test(images[i].src)) {
+ images[i].dataset.kiwixurl = images[i].getAttribute('src');
+ zimImages.push(images[i]);
+ }
+ }
+ if (displayType === 'manual') {
+ setupManualImageExtraction(zimImages);
+ } else {
+ lazyLoad(zimImages);
+ }
+ }
+
+ /**
+ * Processes an array or collection of image nodes so that they will be lazy loaded (progressive extraction)
+ *
+ * @param {Object} images And array or collection of DOM image nodes which will be processed for
+ * progressive image extraction
+ * @param {Object} selectedArchive The archive picked by the user
+ */
+ function lazyLoad(images, selectedArchive) {
+ var queue;
+ var getImages = function() {
+ queue = queueImages(images);
+ extractImages(queue, selectedArchive);
+ };
+ getImages();
+ // Sometimes the page hasn't been rendered when getImages() is run, especially in Firefox, so run again after delay
+ setTimeout(getImages, 700);
+ if (queue.length === images.length) return;
+ // There are images remaining, so set up an event listener to load more images once user has stopped scrolling the iframe
+ var iframe = document.getElementById('articleContent');
+ var iframeWindow = iframe.contentWindow;
+ iframeWindow.addEventListener('scroll', function() {
+ clearTimeout(timer);
+ // Waits for a period after scroll start to start checking for images
+ var timer = setTimeout(getImages, 600);
+ });
+ }
+
+ /**
+ * A utility to set the contentInjectionmode in this module
+ * It should be called when the user changes the contentInjectionMode
+ *
+ * @param {String} injectionMode The contentInjectionMode selected by the user
+ */
+ function setContentInjectionMode(injectionMode) {
+ contentInjectionMode = injectionMode;
+ }
+
+ /**
+ * Functions and classes exposed by this module
+ */
+ return {
+ extractImages: extractImages,
+ setupManualImageExtraction: setupManualImageExtraction,
+ prepareImagesServiceWorker: prepareImagesServiceWorker,
+ lazyLoad: lazyLoad,
+ setContentInjectionMode: setContentInjectionMode
+ };
+});