Port some changes from Kiwix JS

Former-commit-id: c104efb632b0d23bd7376e277f22e169a2814e55 [formerly a5dfd669c5676262a410c7bd5a7351cc4bdc6318]
Former-commit-id: e3fd809660f7229c985727d21d411bc5d4197b76
This commit is contained in:
Jaifroid 2019-06-08 15:01:36 +01:00
parent 13b106bdbe
commit c3d8dc4643
5 changed files with 286 additions and 108 deletions

View File

@ -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 = "<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'><rect width='1' height='1' style='fill:lightblue'/></svg>";
else
svgResponse = "<svg xmlns='http://www.w3.org/2000/svg'/>";
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,

View File

@ -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;
}

View File

@ -630,18 +630,23 @@
</div>
</div>
<div class="panel panel-danger" id="contentInjectionModeDiv">
<div class="panel-heading">Content injection mode: do not touch unless you know what you're doing!</div>
<div class="panel-heading">Content injection mode</div>
<div class="panel-body">
<label class="radio">
<input type="radio" name="contentInjectionMode" value="jquery" id="jQueryModeRadio" checked>
<span class="radiobtn"></span>
<b>JQuery</b> (slow and memory hungry, but safer)
<strong>JQuery</strong> (stable and safe)
</label>
<label class="radio">
<input type="radio" name="contentInjectionMode" value="serviceworker" id="serviceworkerModeRadio">
<span class="radiobtn"></span>
<b>ServiceWorker</b> (faster but unstable, and not supported by all platforms)
<strong>ServiceWorker</strong> (not available on all platforms, but supports more ZIM files)
</label>
</div>
</div>
<div class="panel panel-warning" id="apiStatusDiv">
<div class="panel-heading">API Status</div>
<div class="panel-body">
<div id="serviceWorkerStatus"></div>
<div id="messageChannelStatus"></div>
</div>

View File

@ -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.<StorageFirefoxOS>
*/
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 {

183
www/js/lib/images.js Normal file
View File

@ -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 <http://www.gnu.org/licenses/>
*/
'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
};
});