diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index fbec64b0..e3c53898 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -17,6 +17,8 @@ module.exports = {
'no-extra-parens': 1,
'no-unused-expressions': 1,
'no-unused-vars': 1,
- 'n/no-callback-literal': 0
+ 'n/no-callback-literal': 0,
+ 'object-shorthand': 0,
+ 'multiline-ternary': 0
}
}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 28df2656..6af9a900 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -19,11 +19,11 @@ Please follow these guidelines when contributing:
- be sure to test your fix in both "JQuery" mode and "Service Worker" mode (see Configuration);
- run the Unit tests (see below) in at least the above browsers.
-If all the tests are working fine, you can finally test the extension versions, like this:
+If all the tests are working fine, you can finally test the extension versions. Plese note that we are using Manifest V3 for the Chromium extensions, and Manifest V2
+for the Firefox extension, so there are different instructions for the two browser families:
- - Remove the '-WIP' from the version key from the manifest.json file present in the root of this repo;
- In Chromium, you can install the extension by loading the root folder with Extensions -> Load Unpacked (with Developer Mode turned ON) -> select the root folder of the repository;
- - In Firefox, you can load an extension with Manage Your Extensions -> Debug Add-ons -> Load Temporary Add-on, and then pick any file in the repository.
+ - In Firefox, you need to rename manifest.json to manifest.v3.json, and then rename manifest.v2.json to manifest.json. Then you can load the extension with Manage Your Extensions -> Debug Add-ons -> Load Temporary Add-on, and then pick any file in the repository.
If your feature works and tests are passing, make a PR, describe the testing you have done, and ask for a code review.
diff --git a/webextension/backgroundscript.js b/backgroundscript.js
similarity index 79%
rename from webextension/backgroundscript.js
rename to backgroundscript.js
index 8de1fe83..bc0b3216 100644
--- a/webextension/backgroundscript.js
+++ b/backgroundscript.js
@@ -1,5 +1,5 @@
/**
- * backgroundscript.js: Background script for the WebExtension
+ * backgroundscript.js: Background script for the WebExtension Manifest V2
*
* Copyright 2017 Mossroy and contributors
* License GPL v3:
@@ -20,22 +20,20 @@
* along with Kiwix (file LICENSE-GPLv3.txt). If not, see
*/
+/* global chrome, browser */
+
// In order to work on both Firefox and Chromium/Chrome (and derivatives).
// browser and chrome variables expose almost the same APIs
var genericBrowser;
if (typeof browser !== 'undefined') {
// Firefox
genericBrowser = browser;
-}
-else {
+} else {
// Chromium/Chrome
genericBrowser = chrome;
}
-genericBrowser.browserAction.onClicked.addListener(handleClick);
-
-function handleClick(event) {
- genericBrowser.tabs.create({
- url: genericBrowser.runtime.getURL('/www/index.html')
- });
-}
\ No newline at end of file
+genericBrowser.browserAction.onClicked.addListener(function () {
+ var newURL = chrome.runtime.getURL('www/index.html');
+ chrome.tabs.create({ url: newURL });
+});
diff --git a/manifest.json b/manifest.json
index a7a76ca2..35af2116 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,9 +1,9 @@
{
- "manifest_version": 2,
+ "manifest_version": 3,
"name": "Kiwix",
"version": "3.8.1",
- "description": "Kiwix : offline Wikipedia reader",
+ "description": "Kiwix Offline Browser",
"icons": {
"16": "www/img/icons/kiwix-16.png",
@@ -16,7 +16,7 @@
"128": "www/img/icons/kiwix-128.png"
},
- "browser_action": {
+ "action": {
"default_icon": {
"16": "www/img/icons/kiwix-16.png",
"19": "www/img/icons/kiwix-19.png",
@@ -26,22 +26,24 @@
},
"default_title": "Kiwix"
},
-
- "applications": {
- "gecko": {
- "id": "kiwix-html5-unlisted@kiwix.org"
- }
- },
-
- "web_accessible_resources": ["www/index.html"],
"background": {
- "scripts": ["webextension/backgroundscript.js"]
+ "service_worker": "service-worker.js"
},
- "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
+ "permissions": ["storage", "activeTab", "scripting"],
- "author": "mossroy",
+ "content_security_policy": {
+ "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';",
+ "sandbox": "sandbox allow-scripts allow-downloads allow-forms allow-popups allow-modals; script-src 'self' 'unsafe-inline' 'unsafe-eval'; child-src 'self';"
+ },
+
+ "web_accessible_resources": [{
+ "resources": ["www/index.html", "www/article.html"],
+ "matches": ["https://*.kiwix.org/*"]
+ }],
+
+ "author": "Kiwix",
"homepage_url": "https://www.kiwix.org",
"offline_enabled": true
}
diff --git a/manifest.v2.json b/manifest.v2.json
new file mode 100644
index 00000000..3b6fb9bf
--- /dev/null
+++ b/manifest.v2.json
@@ -0,0 +1,47 @@
+{
+ "manifest_version": 2,
+ "name": "Kiwix",
+ "version": "3.8.1",
+
+ "description": "Kiwix : offline Wikipedia reader",
+
+ "icons": {
+ "16": "www/img/icons/kiwix-16.png",
+ "19": "www/img/icons/kiwix-19.png",
+ "32": "www/img/icons/kiwix-32.png",
+ "38": "www/img/icons/kiwix-38.png",
+ "48": "www/img/icons/kiwix-48.png",
+ "64": "www/img/icons/kiwix-64.png",
+ "90": "www/img/icons/kiwix-90.png",
+ "128": "www/img/icons/kiwix-128.png"
+ },
+
+ "browser_action": {
+ "default_icon": {
+ "16": "www/img/icons/kiwix-16.png",
+ "19": "www/img/icons/kiwix-19.png",
+ "32": "www/img/icons/kiwix-32.png",
+ "38": "www/img/icons/kiwix-38.png",
+ "64": "www/img/icons/kiwix-64.png"
+ },
+ "default_title": "Kiwix"
+ },
+
+ "applications": {
+ "gecko": {
+ "id": "kiwix-html5-unlisted@kiwix.org"
+ }
+ },
+
+ "web_accessible_resources": ["www/index.html"],
+
+ "background": {
+ "scripts": ["backgroundscript.js"]
+ },
+
+ "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
+
+ "author": "mossroy",
+ "homepage_url": "https://www.kiwix.org",
+ "offline_enabled": true
+}
diff --git a/scripts/create_all_packages.sh b/scripts/create_all_packages.sh
index 8bd0c36c..e99916e5 100755
--- a/scripts/create_all_packages.sh
+++ b/scripts/create_all_packages.sh
@@ -55,7 +55,7 @@ fi
# Copy only the necessary files in a temporary directory
mkdir -p tmp
rm -rf tmp/*
-cp -r www webextension manifest.json manifest.webapp LICENSE-GPLv3.txt service-worker.js README.md tmp/
+cp -r www manifest.json manifest.v2.json manifest.webapp LICENSE-GPLv3.txt service-worker.js README.md tmp/
# Remove unwanted files
rm -f tmp/www/js/lib/libzim-*dev.*
@@ -64,8 +64,10 @@ rm -f tmp/www/js/lib/libzim-*dev.*
regexpNumericVersion='^[0-9\.]+$'
if [[ $VERSION =~ $regexpNumericVersion ]] ; then
sed -i -e "s/$VERSION_TO_REPLACE/$VERSION/" tmp/manifest.json
+ sed -i -e "s/$VERSION_TO_REPLACE/$VERSION/" tmp/manifest.v2.json
else
sed -i -e "s/$VERSION_TO_REPLACE/$MAJOR_NUMERIC_VERSION/" tmp/manifest.json
+ sed -i -e "s/$VERSION_TO_REPLACE/$MAJOR_NUMERIC_VERSION/" tmp/manifest.v2.json
fi
sed -i -e "s/$VERSION_TO_REPLACE/$VERSION/" tmp/manifest.webapp
sed -i -e "s/$VERSION_TO_REPLACE/$VERSION/" tmp/service-worker.js
@@ -73,17 +75,26 @@ sed -i -e "s/$VERSION_TO_REPLACE/$VERSION/" tmp/www/js/app.js
mkdir -p build
rm -rf build/*
-# Package for Chromium/Chrome
-scripts/package_chrome_extension.sh $DRYRUN $TAG -v $VERSION
+# Package for Chromium/Chrome with Manifest V3
+scripts/package_chrome_extension.sh -m 3 $DRYRUN $TAG -v $VERSION
+# Package for Chromium/Chrome with Manifest V2
+cp backgroundscript.js tmp/
+rm tmp/manifest.json
+mv tmp/manifest.v2.json tmp/manifest.json
+scripts/package_chrome_extension.sh -m 2 $DRYRUN $TAG -v $VERSION
+
# Package for Firefox and Firefox OS
# We have to put a unique version string inside the manifest.json (which Chrome might not have accepted)
-# So we take the original manifest again, and replace the version inside it again
-cp manifest.json tmp/
+# So we take the original manifest v2 again, and replace the version inside it again
+cp manifest.v2.json tmp/manifest.json
sed -i -e "s/$VERSION_TO_REPLACE/$VERSION_FOR_MOZILLA_MANIFEST/" tmp/manifest.json
+echo ""
scripts/package_firefox_extension.sh $DRYRUN $TAG -v $VERSION
+echo ""
scripts/package_firefoxos_app.sh $DRYRUN $TAG -v $VERSION
cp -f ubuntu_touch/* tmp/
sed -i -e "s/$VERSION_TO_REPLACE/$VERSION/" tmp/manifest.json
+echo ""
scripts/package_ubuntu_touch_app.sh $DRYRUN $TAG -v $VERSION
# Change permissions on source files to match those expected by the server
diff --git a/scripts/package_chrome_extension.sh b/scripts/package_chrome_extension.sh
index 5d0e7c60..5f2e5bc3 100755
--- a/scripts/package_chrome_extension.sh
+++ b/scripts/package_chrome_extension.sh
@@ -3,19 +3,23 @@ BASEDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"/..
cd "$BASEDIR"
# Reading arguments
-while getopts tdv: option; do
+while getopts m:tdv: option; do
case "${option}" in
+ m) MV=$OPTARG;; # Optionally indicates the manifest version we're using (2 or 3); if present, the version will be added to filename
t) TAG="-t";; # Indicates that we're releasing a public version from a tag
d) DRYRUN="-d";; # Indicates a dryrun test, that does not modify anything on the network
v) VERSION=${OPTARG};;
esac
done
-
+if [ -n $MV ]; then
+ echo -e "\nManifest version requested: $MV"
+ VERSION="MV$MV-$VERSION"
+fi
echo "Packaging unsigned Chrome extension, version $VERSION"
cd tmp
-zip -r ../build/kiwix-chrome-unsigned-extension-$VERSION.zip www webextension manifest.json LICENSE-GPLv3.txt service-worker.js README.md
+zip -r ../build/kiwix-chrome-unsigned-extension-$VERSION.zip www manifest.json LICENSE-GPLv3.txt service-worker.js README.md
cd ..
-if [ "${TAG}zz" == "zz" ]; then
+if [ -z $TAG ]; then
# Package the extension with Chrome or Chromium, if we're not packaging a public version
if hash chromium-browser 2>/dev/null
then
@@ -28,6 +32,7 @@ if [ "${TAG}zz" == "zz" ]; then
echo "Signing the extension for $CHROME_BIN, version $VERSION"
$CHROME_BIN --no-sandbox --pack-extension=tmp --pack-extension-key=./scripts/kiwix-html5.pem
mv tmp.crx build/kiwix-chrome-signed-extension-$VERSION.crx
+ ls -l build/kiwix-chrome-signed-extension-$VERSION.crx
else
echo "This unsigned extension must be manually uploaded to Google to be signed and distributed from their store"
fi
diff --git a/service-worker.js b/service-worker.js
index 22a9aa56..15c94170 100644
--- a/service-worker.js
+++ b/service-worker.js
@@ -2,27 +2,29 @@
* 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
- *
+ *
* Copyright 2022 Mossroy, Jaifroid 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';
+/* global chrome */
+
/**
* App version number - ENSURE IT MATCHES VALUE IN app.js
* DEV: Changing this will cause the browser to recognize that the Service Worker has changed, and it will
@@ -60,10 +62,9 @@ var useAssetsCache = true;
* This is an expert setting in Configuration
* @type {Boolean}
*/
- var useAppCache = true;
+var useAppCache = true;
-
-/**
+/**
* A regular expression that matches the Content-Types of assets that may be stored in ASSETS_CACHE
* Add any further Content-Types you wish to cache to the regexp, separated by '|'
* @type {RegExp}
@@ -78,7 +79,7 @@ var regexpCachedContentTypes = /text\/css|text\/javascript|application\/javascri
*/
var regexpExcludedURLSchema = /^(?:file|chrome-extension|example-extension):/i;
-/**
+/**
* Pattern for ZIM file namespace: see https://wiki.openzim.org/wiki/ZIM_file_format#Namespaces
* In our case, there is also the ZIM file name used as a prefix in the URL
* @type {RegExp}
@@ -91,7 +92,7 @@ const regexpZIMUrlWithNamespace = /(?:^|\/)([^/]+\/)([-ABCIJMUVWX])\/(.+)/;
* See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
* But, in our case, we send a header to tell the browser we only accept the bytes unit.
* I did not see multiple ranges asked by a browser.
- *
+ *
* @type {RegExp}
*/
const regexpByteRangeHeader = /^\s*bytes=(\d+)-/;
@@ -99,74 +100,84 @@ const regexpByteRangeHeader = /^\s*bytes=(\d+)-/;
/**
* The list of files that the app needs in order to run entirely from offline code
*/
-let precacheFiles = [
- ".", // This caches the redirect to www/index.html, in case a user launches the app from its root directory
- "manifest.json",
- "service-worker.js",
- "www/css/app.css",
- "www/css/bootstrap.css",
- "www/css/kiwixJS_invert.css",
- "www/css/kiwixJS_mwInvert.css",
- "www/css/transition.css",
- "www/img/icons/kiwix-256.png",
- "www/img/icons/kiwix-32.png",
- "www/img/icons/kiwix-60.png",
- "www/img/spinner.gif",
- "www/img/Icon_External_Link.png",
- "www/index.html",
- "www/article.html",
- "www/main.html",
- "www/js/app.js",
- "www/js/init.js",
- "www/js/lib/abstractFilesystemAccess.js",
- "www/js/lib/arrayFromPolyfill.js",
- "www/js/lib/bootstrap.bundle.js",
- "www/js/lib/filecache.js",
- "www/js/lib/jquery-3.7.0.slim.min.js",
- "www/js/lib/promisePolyfill.js",
- "www/js/lib/require.js",
- "www/js/lib/settingsStore.js",
- "www/js/lib/uiUtil.js",
- "www/js/lib/utf8.js",
- "www/js/lib/util.js",
- "www/js/lib/xzdec_wrapper.js",
- "www/js/lib/zstddec_wrapper.js",
- "www/js/lib/zimArchive.js",
- "www/js/lib/zimArchiveLoader.js",
- "www/js/lib/zimDirEntry.js",
- "www/js/lib/zimfile.js",
- "www/js/lib/fontawesome/fontawesome.js",
- "www/js/lib/fontawesome/solid.js"
+const precacheFiles = [
+ '.', // This caches the redirect to www/index.html, in case a user launches the app from its root directory
+ 'manifest.json',
+ 'service-worker.js',
+ 'www/css/app.css',
+ 'www/css/bootstrap.css',
+ 'www/css/kiwixJS_invert.css',
+ 'www/css/kiwixJS_mwInvert.css',
+ 'www/css/transition.css',
+ 'www/img/icons/kiwix-256.png',
+ 'www/img/icons/kiwix-32.png',
+ 'www/img/icons/kiwix-60.png',
+ 'www/img/spinner.gif',
+ 'www/img/Icon_External_Link.png',
+ 'www/index.html',
+ 'www/article.html',
+ 'www/main.html',
+ 'www/js/app.js',
+ 'www/js/init.js',
+ 'www/js/lib/abstractFilesystemAccess.js',
+ 'www/js/lib/arrayFromPolyfill.js',
+ 'www/js/lib/bootstrap.bundle.js',
+ 'www/js/lib/filecache.js',
+ 'www/js/lib/jquery-3.7.0.slim.min.js',
+ 'www/js/lib/promisePolyfill.js',
+ 'www/js/lib/require.js',
+ 'www/js/lib/settingsStore.js',
+ 'www/js/lib/uiUtil.js',
+ 'www/js/lib/utf8.js',
+ 'www/js/lib/util.js',
+ 'www/js/lib/xzdec_wrapper.js',
+ 'www/js/lib/zstddec_wrapper.js',
+ 'www/js/lib/zimArchive.js',
+ 'www/js/lib/zimArchiveLoader.js',
+ 'www/js/lib/zimDirEntry.js',
+ 'www/js/lib/zimfile.js',
+ 'www/js/lib/fontawesome/fontawesome.js',
+ 'www/js/lib/fontawesome/solid.js'
];
if ('WebAssembly' in self) {
- precacheFiles.push(
- "www/js/lib/xzdec-wasm.js",
- "www/js/lib/xzdec-wasm.wasm",
- "www/js/lib/zstddec-wasm.js",
- "www/js/lib/zstddec-wasm.wasm",
- "www/js/lib/libzim-wasm.js",
- "www/js/lib/libzim-wasm.wasm"
- );
+ precacheFiles.push(
+ 'www/js/lib/xzdec-wasm.js',
+ 'www/js/lib/xzdec-wasm.wasm',
+ 'www/js/lib/zstddec-wasm.js',
+ 'www/js/lib/zstddec-wasm.wasm',
+ 'www/js/lib/libzim-wasm.js',
+ 'www/js/lib/libzim-wasm.wasm'
+ );
} else {
- precacheFiles.push(
- "www/js/lib/xzdec-asm.js",
- "www/js/lib/zstddec-asm.js",
- "www/js/lib/libzim-asm.js"
- );
+ precacheFiles.push(
+ 'www/js/lib/xzdec-asm.js',
+ 'www/js/lib/zstddec-asm.js',
+ 'www/js/lib/libzim-asm.js'
+ );
+}
+
+/**
+ * If we're in a Chromium extension, add a listener to launch the tab when the icon is clicked
+ */
+if (typeof chrome !== 'undefined' && chrome.action) {
+ chrome.action.onClicked.addListener(function () {
+ var newURL = chrome.runtime.getURL('www/index.html');
+ chrome.tabs.create({ url: newURL });
+ });
}
// Process install event
-self.addEventListener("install", function (event) {
- console.debug("[SW] Install Event processing");
+self.addEventListener('install', function (event) {
+ console.debug('[SW] Install Event processing');
// DEV: We can't skip waiting because too many params are loaded at an early stage from the old file before the new one can activate...
// self.skipWaiting();
// We try to circumvent the browser's cache by adding a header to the Request, and it ensures all files are explicitly versioned
var requests = precacheFiles.map(function (urlPath) {
return new Request(urlPath + '?v' + appVersion, { cache: 'no-cache' });
});
- if (!regexpExcludedURLSchema.test(requests[0].url)) event.waitUntil(
- caches.open(APP_CACHE).then(function (cache) {
+ if (!regexpExcludedURLSchema.test(requests[0].url)) {
+ event.waitUntil(caches.open(APP_CACHE).then(function (cache) {
return Promise.all(
requests.map(function (request) {
return fetch(request).then(function (response) {
@@ -178,8 +189,8 @@ self.addEventListener("install", function (event) {
});
})
);
- })
- );
+ }));
+ }
});
// Allow sw to control current page
@@ -211,7 +222,7 @@ let fetchCaptureEnabled = false;
*/
self.addEventListener('fetch', function (event) {
// Only cache GET requests
- if (event.request.method !== "GET") return;
+ if (event.request.method !== 'GET') return;
var rqUrl = event.request.url;
var urlObject = new URL(rqUrl);
// Test the URL with parameters removed
@@ -225,13 +236,13 @@ self.addEventListener('fetch', function (event) {
event.respondWith(
// First see if the content is in the cache
fromCache(cache, rqUrl).then(function (response) {
- // The response was found in the cache so we respond with it
+ // The response was found in the cache so we respond with it
return response;
}, function () {
// The response was not found in the cache so we look for it in the ZIM
// and add it to the cache if it is an asset type (css or js)
if (cache === ASSETS_CACHE && regexpZIMUrlWithNamespace.test(strippedUrl)) {
- let range = event.request.headers.get('range');
+ const range = event.request.headers.get('range');
return fetchUrlFromZIM(urlObject, range).then(function (response) {
// Add css or js assets to ASSETS_CACHE (or update their cache entries) unless the URL schema is not supported
if (regexpCachedContentTypes.test(response.headers.get('Content-Type')) &&
@@ -252,7 +263,7 @@ self.addEventListener('fetch', function (event) {
}
return response;
}).catch(function (error) {
- console.debug("[SW] Network request failed and no cache.", error);
+ console.debug('[SW] Network request failed and no cache.', error);
});
}
})
@@ -262,7 +273,7 @@ self.addEventListener('fetch', function (event) {
/**
* Handle custom commands sent from app.js
*/
- self.addEventListener('message', function (event) {
+self.addEventListener('message', function (event) {
if (event.data.action) {
if (event.data.action === 'init') {
// On 'init' message, we initialize the outgoingMessagePort and enable the fetchEventListener
@@ -287,7 +298,7 @@ self.addEventListener('fetch', function (event) {
if (useAppCache !== oldValue) console.debug('[SW] Use of appCache was switched to: ' + useAppCache);
}
if (event.data.action === 'getCacheNames') {
- event.ports[0].postMessage({ 'app': APP_CACHE, 'assets': ASSETS_CACHE });
+ event.ports[0].postMessage({ app: APP_CACHE, assets: ASSETS_CACHE });
}
if (event.data.action.checkCache) {
// Checks and returns the caching strategy: checkCache key should contain a sample URL string to test
@@ -300,12 +311,12 @@ self.addEventListener('fetch', function (event) {
/**
* Handles URLs that need to be extracted from the ZIM archive
- *
+ *
* @param {URL} urlObject The URL object to be processed for extraction from the ZIM
* @param {String} range Optional byte range string
* @returns {Promise} A Promise for the Response, or rejects with the invalid message port data
*/
-function fetchUrlFromZIM(urlObject, range) {
+function fetchUrlFromZIM (urlObject, range) {
return new Promise(function (resolve, reject) {
// Note that titles may contain bare question marks or hashes, so we must use only the pathname without any URL parameters.
// Be sure that you haven't encoded any querystring along with the URL.
@@ -330,14 +341,14 @@ function fetchUrlFromZIM(urlObject, range) {
headers.set('Content-Security-Policy', "default-src 'self' data: blob: about: chrome-extension: https://moz-extension.kiwix.org https://kiwix.github.io 'unsafe-inline' 'unsafe-eval'; sandbox allow-scripts allow-same-origin allow-modals allow-popups allow-forms allow-downloads;");
headers.set('Referrer-Policy', 'no-referrer');
if (contentType) headers.set('Content-Type', contentType);
-
+
// Test if the content is a video or audio file. In this case, Chrome & Edge need us to support ranges.
// 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)) {
headers.set('Accept-Ranges', 'bytes');
}
-
+
var slicedData = msgPortEvent.data.content;
if (range) {
// The browser asks for a range of bytes (usually for a video or audio stream)
@@ -347,22 +358,22 @@ function fetchUrlFromZIM(urlObject, range) {
// So it's probably better to send all we have: hopefully it will avoid some subsequent requests of
// the browser to get the following chunks (which would trigger some other complete reads in the ZIM file)
// This might be improved in the future with the libzim wasm backend, that should be able to handle ranges.
- let partsOfRangeHeader = regexpByteRangeHeader.exec(range);
- let begin = partsOfRangeHeader[1];
- let end = contentLength - 1;
+ const partsOfRangeHeader = regexpByteRangeHeader.exec(range);
+ const begin = partsOfRangeHeader[1];
+ const end = contentLength - 1;
slicedData = slicedData.slice(begin);
-
+
headers.set('Content-Range', 'bytes ' + begin + '-' + end + '/' + contentLength);
headers.set('Content-Length', end - begin + 1);
}
-
+
var responseInit = {
// HTTP status is usually 200, but has to bee 206 when partial content (range) is sent
status: range ? 206 : 200,
statusText: 'OK',
- headers: headers
+ headers
};
-
+
var httpResponse = new Response(slicedData, responseInit);
// Let's send the content back from the ServiceWorker
@@ -374,8 +385,8 @@ function fetchUrlFromZIM(urlObject, range) {
}
};
outgoingMessagePort.postMessage({
- 'action': 'askForContent',
- 'title': titleWithNameSpace
+ action: 'askForContent',
+ title: titleWithNameSpace
}, [messageChannel.port2]);
});
}
@@ -386,12 +397,16 @@ function fetchUrlFromZIM(urlObject, range) {
* @param {String} requestUrl The Request URL to fulfill from cache
* @returns {Promise} A Promise for the cached Response, or rejects with strings 'disabled' or 'no-match'
*/
-function fromCache(cache, requestUrl) {
+function fromCache (cache, requestUrl) {
// Prevents use of Cache API if user has disabled it
- if (!(useAppCache && cache === APP_CACHE || useAssetsCache && cache === ASSETS_CACHE)) return Promise.reject('disabled');
+ if (!(useAppCache && cache === APP_CACHE || useAssetsCache && cache === ASSETS_CACHE)) {
+ return Promise.reject(new Error('disabled'));
+ }
return caches.open(cache).then(function (cacheObj) {
return cacheObj.match(requestUrl).then(function (matching) {
- if (!matching || matching.status === 404) return Promise.reject('no-match');
+ if (!matching || matching.status === 404) {
+ return Promise.reject(new Error('no-match'));
+ }
console.debug('[SW] Supplying ' + requestUrl + ' from ' + cache + '...');
return matching;
});
@@ -405,10 +420,11 @@ function fromCache(cache, requestUrl) {
* @param {Response} response The Response received from the server/ZIM
* @returns {Promise} A Promise for the update action
*/
-function updateCache(cache, request, response) {
+function updateCache (cache, request, response) {
// Prevents use of Cache API if user has disabled it
- if (!response.ok || !(useAppCache && cache === APP_CACHE || useAssetsCache && cache === ASSETS_CACHE))
+ if (!response.ok || !(useAppCache && cache === APP_CACHE || useAssetsCache && cache === ASSETS_CACHE)) {
return Promise.resolve();
+ }
return caches.open(cache).then(function (cacheObj) {
console.debug('[SW] Adding ' + (request.url || request) + ' to ' + cache + '...');
return cacheObj.put(request, response);
@@ -421,16 +437,16 @@ function updateCache(cache, request, response) {
* @param {String} url A URL to test against excludedURLSchema
* @returns {Promise} A Promise for an array of format [cacheType, cacheDescription, assetCount]
*/
-function testCacheAndCountAssets(url) {
+function testCacheAndCountAssets (url) {
if (regexpExcludedURLSchema.test(url)) return Promise.resolve(['custom', 'custom', 'Custom', '-']);
if (!useAssetsCache) return Promise.resolve(['none', 'none', 'None', 0]);
return caches.open(ASSETS_CACHE).then(function (cache) {
return cache.keys().then(function (keys) {
return ['cacheAPI', ASSETS_CACHE, 'Cache API', keys.length];
- }).catch(function(err) {
+ }).catch(function (err) {
return err;
});
- }).catch(function(err) {
+ }).catch(function (err) {
return err;
});
}
diff --git a/www/js/app.js b/www/js/app.js
index 9bbc7ccd..84020f1e 100644
--- a/www/js/app.js
+++ b/www/js/app.js
@@ -30,9 +30,8 @@
// This uses require.js to structure javascript:
// http://requirejs.org/docs/api.html#define
-define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesystemAccess'],
+define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore', 'abstractFilesystemAccess'],
function ($, zimArchiveLoader, uiUtil, settingsStore, abstractFilesystemAccess) {
-
/**
* 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)
@@ -108,7 +107,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
(isServiceWorkerAvailable() ? 'serviceworker' : 'jquery');
// A parameter to circumvent anti-fingerprinting technology in browsers that do not support WebP natively by substituting images
// directly with the canvas elements produced by the WebP polyfill [kiwix-js #835]. NB This is only currently used in jQuery mode.
- params['useCanvasElementsForWebpTranscoding']; // Value is determined in uiUtil.determineCanvasElementsWorkaround(), called when setting the content injection mode
+ params['useCanvasElementsForWebpTranscoding'] = null; // Value is determined in uiUtil.determineCanvasElementsWorkaround(), called when setting the content injection mode
// An object to hold the current search and its state (allows cancellation of search across modules)
appstate['search'] = {
@@ -182,11 +181,11 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
document.getElementById('bypassAppCacheCheck').checked = !params.appCache;
document.getElementById('appVersion').textContent = 'Kiwix ' + params.appVersion;
// We check here if we have to warn the user that we switched to ServiceWorkerMode
- // This is only needed if the ServiceWorker mode is available, or we are in a Firefox Extension that supports Service Workers
+ // This is only needed if the ServiceWorker mode is available, or we are in an Extension that supports Service Workers
// outside of the extension environment, AND the user's settings are stuck on jQuery mode, AND the user has not already been
// alerted about the switch to ServiceWorker mode by default
- if ((isServiceWorkerAvailable() || isMessageChannelAvailable() && /^moz-extension:/i.test(window.location.protocol))
- && params.contentInjectionMode === 'jquery' && !params.defaultModeChangeAlertDisplayed) {
+ if ((isServiceWorkerAvailable() || isMessageChannelAvailable() && /^(moz|chrome)-extension:/i.test(window.location.protocol)) &&
+ params.contentInjectionMode === 'jquery' && !params.defaultModeChangeAlertDisplayed) {
// Attempt to upgrade user to ServiceWorker mode
params.contentInjectionMode = 'serviceworker';
} else if (params.contentInjectionMode === 'serviceworker') {
@@ -329,16 +328,12 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
$('#prefix').on('keyup', function (e) {
if (selectedArchive !== null && selectedArchive.isReady()) {
// Prevent processing by keyup event if we already handled the keypress in keydown event
- if (keyPressHandled)
- keyPressHandled = false;
- else
- onKeyUpPrefix(e);
+ if (keyPressHandled) { keyPressHandled = false; } else { onKeyUpPrefix(e); }
}
});
// Restore the search results if user goes back into prefix field
$('#prefix').on('focus', function () {
- if (document.getElementById('prefix').value !== '')
- document.getElementById('articleListWithHeader').style.display = '';
+ if (document.getElementById('prefix').value !== '') { document.getElementById('articleListWithHeader').style.display = ''; }
});
// Hide the search results if user moves out of prefix field
$('#prefix').on('blur', function () {
@@ -374,7 +369,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
});
$('#btnTop').on('click', function () {
var articleContent = document.getElementById('articleContent');
- articleContent.contentWindow.scrollTo({top: 0, behavior: 'smooth'});
+ articleContent.contentWindow.scrollTo({ top: 0, behavior: 'smooth' });
// We return true, so that the link to #top is still triggered (useful in the About section)
return true;
});
@@ -401,7 +396,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
document.getElementById('prefix').value = '';
document.getElementById('prefix').focus();
var articleList = document.getElementById('articleList');
- var articleListHeaderMessage = document.getElementById('articleListHeaderMessage');
+ var articleListHeaderMessage = document.getElementById('articleListHeaderMessage');
while (articleList.firstChild) articleList.removeChild(articleList.firstChild);
while (articleListHeaderMessage.firstChild) articleListHeaderMessage.removeChild(articleListHeaderMessage.firstChild);
document.getElementById('searchingArticles').style.display = 'none';
@@ -505,7 +500,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
refreshCacheStatus();
});
document.getElementById('disableDragAndDropCheck').addEventListener('change', function () {
- params.disableDragAndDrop = this.checked ? true : false;
+ params.disableDragAndDrop = !!this.checked;
settingsStore.setItem('disableDragAndDrop', params.disableDragAndDrop, Infinity);
uiUtil.systemAlert('
We will now attempt to reload the app to apply the new setting.
' +
'
(If you cancel, then the setting will only be applied when you next start the app.)
', 'Reload app', true).then(function (result) {
@@ -515,20 +510,20 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
});
});
$('input:checkbox[name=hideActiveContentWarning]').on('change', function () {
- params.hideActiveContentWarning = this.checked ? true : false;
+ params.hideActiveContentWarning = !!this.checked;
settingsStore.setItem('hideActiveContentWarning', params.hideActiveContentWarning, Infinity);
});
$('input:checkbox[name=showUIAnimations]').on('change', function () {
- params.showUIAnimations = this.checked ? true : false;
+ params.showUIAnimations = !!this.checked;
settingsStore.setItem('showUIAnimations', params.showUIAnimations, Infinity);
});
$('input:checkbox[name=useHomeKeyToFocusSearchBar]').on('change', function () {
- params.useHomeKeyToFocusSearchBar = this.checked ? true : false;
+ params.useHomeKeyToFocusSearchBar = !!this.checked;
settingsStore.setItem('useHomeKeyToFocusSearchBar', params.useHomeKeyToFocusSearchBar, Infinity);
switchHomeKeyToFocusSearchBar();
});
$('input:checkbox[name=openExternalLinksInNewTabs]').on('change', function () {
- params.openExternalLinksInNewTabs = this.checked ? true : false;
+ params.openExternalLinksInNewTabs = !!this.checked;
settingsStore.setItem('openExternalLinksInNewTabs', params.openExternalLinksInNewTabs, Infinity);
});
document.getElementById('appThemeSelect').addEventListener('change', function (e) {
@@ -578,12 +573,11 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
uiUtil.checkUpdateStatus(appstate);
}, 10000);
-
// Adds an event listener to kiwix logo and bottom navigation bar which gets triggered when these elements are dragged.
// Returning false prevents their dragging (which can cause some unexpected behavior)
// Doing that in javascript is the only way to make it cross-browser compatible
- document.getElementById('kiwixLogo').ondragstart=function () {return false;}
- document.getElementById('navigationButtons').ondragstart=function () {return false;}
+ document.getElementById('kiwixLogo').ondragstart = function () { return false; }
+ document.getElementById('navigationButtons').ondragstart = function () { return false; }
// focus search bar (#prefix) if Home key is pressed
function focusPrefixOnHomeKey (event) {
@@ -592,7 +586,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
// wait to prevent interference with scrolling (default action)
setTimeout(function () {
document.getElementById('prefix').focus();
- },0);
+ }, 0);
}
}
// switch on/off the feature to use Home Key to focus search bar
@@ -602,8 +596,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
var isIframeAccessible = true;
try {
iframeContentWindow.removeEventListener('keydown', focusPrefixOnHomeKey);
- }
- catch (err) {
+ } catch (err) {
console.error('The iframe is probably not accessible', err);
isIframeAccessible = false;
}
@@ -615,10 +608,8 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
// only for initial empty iFrame loaded using `src` attribute
// in any other case listener gets removed on reloading of iFrame content
iframeContentWindow.addEventListener('keydown', focusPrefixOnHomeKey);
- }
- // when the feature is not active
- else {
- // remove event listener for window(outside iframe)
+ } else {
+ // When the feature is not active, remove event listener for window (outside iframe)
window.removeEventListener('keydown', focusPrefixOnHomeKey);
// if feature is deactivated and no zim content is loaded yet
iframeContentWindow.removeEventListener('keydown', focusPrefixOnHomeKey);
@@ -710,7 +701,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
}
apiName = params.decompressorAPI.errorStatus || apiName || 'Not initialized';
// innerHTML is used here because the API name may contain HTML entities like
- decompAPIStatusDiv.innerHTML = 'Decompressor API: ' + apiName ;
+ decompAPIStatusDiv.innerHTML = 'Decompressor API: ' + apiName;
// Update Search Provider
uiUtil.reportSearchProviderToAPIStatusPanel(params.searchProvider);
// Update PWA origin
@@ -859,7 +850,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
uriParams += '&appTheme=' + params.appTheme;
uriParams += '&showUIAnimations=' + params.showUIAnimations;
window.location.href = params.referrerExtensionURL + '/www/index.html' + uriParams;
- 'Beam me down, Scotty!';
+ console.log('Beam me down, Scotty!');
};
uiUtil.systemAlert(message, 'Warning!', true).then(function (response) {
if (response) {
@@ -1018,8 +1009,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
try {
var dummyMessageChannel = new MessageChannel();
if (dummyMessageChannel) return true;
- }
- catch (e) {
+ } catch (e) {
return false;
}
return false;
@@ -1039,8 +1029,9 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
// DEV: See explanation below for why we access localStorage directly here
var PWASuccessfullyLaunched = localStorage.getItem(params.keyPrefix + 'PWA_launch') === 'success';
var allowInternetAccess = settingsStore.getItem('allowInternetAccess') === 'true';
- var message = params.defaultModeChangeAlertDisplayed ? '
To enable the Service Worker, we ' :
- ('
We shall attempt to switch you to ServiceWorker mode (this is now the default). ' +
+ var message = params.defaultModeChangeAlertDisplayed
+ ? '
To enable the Service Worker, we '
+ : ('
We shall attempt to switch you to ServiceWorker mode (this is now the default). ' +
'It supports more types of ZIM archives and is much more robust.
We ');
message += 'need one-time access to our secure server so that the app can re-launch as a Progressive Web App (PWA). ' +
'If available, the PWA will work offline, but will auto-update periodically when online as per the ' +
@@ -1063,7 +1054,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
// regarding the location of the key to be able to retrieve it in init.js before settingsStore is initialized
localStorage.setItem(params.keyPrefix + 'PWA_launch', 'fail');
window.location.href = params.PWAServer + 'www/index.html' + uriParams;
- 'Beam me up, Scotty!';
+ console.log('Beam me up, Scotty!');
};
var checkPWAIsOnline = function () {
uiUtil.spinnerDisplay(true, 'Checking server access...');
@@ -1148,7 +1139,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
} else {
// If DeviceStorage is not available, we display the file select components
displayFileSelect();
- if (document.getElementById('archiveFiles').files && document.getElementById('archiveFiles').files.length>0) {
+ if (document.getElementById('archiveFiles').files && document.getElementById('archiveFiles').files.length > 0) {
// Archive files are already selected,
setLocalArchiveFromFileSelect();
} else {
@@ -1169,7 +1160,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
document.getElementById('articleListWithHeader').style.display = 'none';
$('#articleContent').contents().empty();
- if (title && !(''===title)) {
+ if (title && !(title === '')) {
goToArticle(title);
} else if (titleSearch && titleSearch !== '') {
document.getElementById('prefix').value = titleSearch;
@@ -1207,7 +1198,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
var lastSelectedArchive = settingsStore.getItem('lastSelectedArchive');
if (lastSelectedArchive !== null && lastSelectedArchive !== undefined && lastSelectedArchive !== '') {
// Attempt to select the corresponding item in the list, if it exists
- if ($("#archiveList option[value='"+lastSelectedArchive+"']").length > 0) {
+ if ($("#archiveList option[value='" + lastSelectedArchive + "']").length > 0) {
document.getElementById('archiveList').value = lastSelectedArchive;
}
}
@@ -1236,9 +1227,9 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
var regexpStorageName = /^\/([^/]+)\//;
var regexpResults = regexpStorageName.exec(archiveDirectory);
var selectedStorage = null;
- if (regexpResults && regexpResults.length>0) {
+ if (regexpResults && regexpResults.length > 0) {
var selectedStorageName = regexpResults[1];
- for (var i=0; i= params.maxSearchResultsSize) {
message = 'First ' + params.maxSearchResultsSize + ' articles found (refine your search).';
} else {
- message = 'Finished. ' + (nbDirEntry ? nbDirEntry : 'No') + ' articles found' + (
+ message = 'Finished. ' + (nbDirEntry || 'No') + ' articles found' + (
reportingSearch.type === 'basic' ? ': try fewer words for full search.' : '.'
);
}
@@ -1608,8 +1597,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
if (iframeArticleContent.contentWindow) {
// Configure home key press to focus #prefix only if the feature is in active state
- if (params.useHomeKeyToFocusSearchBar)
- iframeArticleContent.contentWindow.addEventListener('keydown', focusPrefixOnHomeKey);
+ if (params.useHomeKeyToFocusSearchBar) { iframeArticleContent.contentWindow.addEventListener('keydown', focusPrefixOnHomeKey); }
if (params.openExternalLinksInNewTabs) {
// Add event listener to iframe window to check for links to external resources
iframeArticleContent.contentWindow.addEventListener('click', function (event) {
@@ -1635,7 +1623,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
// remove eventListener to avoid memory leaks
iframeArticleContent.contentWindow.removeEventListener('keydown', focusPrefixOnHomeKey);
var articleList = document.getElementById('articleList');
- var articleListHeaderMessage = document.getElementById('articleListHeaderMessage');
+ var articleListHeaderMessage = document.getElementById('articleListHeaderMessage');
while (articleList.firstChild) articleList.removeChild(articleList.firstChild);
while (articleListHeaderMessage.firstChild) articleListHeaderMessage.removeChild(articleListHeaderMessage.firstChild);
document.getElementById('articleListWithHeader').style.display = 'none';
@@ -1792,7 +1780,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
iframeArticleContent.onload = function () {
iframeArticleContent.onload = function () {};
var articleList = document.getElementById('articleList');
- var articleListHeaderMessage = document.getElementById('articleListHeaderMessage');
+ var articleListHeaderMessage = document.getElementById('articleListHeaderMessage');
while (articleList.firstChild) articleList.removeChild(articleList.firstChild);
while (articleListHeaderMessage.firstChild) articleListHeaderMessage.removeChild(articleListHeaderMessage.firstChild);
document.getElementById('articleListWithHeader').style.display = 'none';
@@ -1800,10 +1788,10 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
var iframeContentDocument = iframeArticleContent.contentDocument;
if (!iframeContentDocument && window.location.protocol === 'file:') {
- uiUtil.systemAlert('You seem to be opening kiwix-js with the file:// protocol, which is blocked by your browser for security reasons.'
- + '
The easiest way to run it is to download and run it as a browser extension (from the vendor store).'
- + '
Else you can open it through a web server : either through a local one (http://localhost/...) or through a remote one (but you need SSL : https://webserver/...)'
- + "
Another option is to force your browser to accept that (but you'll open a security breach) : on Chrome, you can start it with --allow-file-access-from-files command-line argument; on Firefox, you can set privacy.file_unique_origin to false in about:config");
+ uiUtil.systemAlert('You seem to be opening kiwix-js with the file:// protocol, which is blocked by your browser for security reasons.' +
+ '
The easiest way to run it is to download and run it as a browser extension (from the vendor store).' +
+ '
Else you can open it through a web server : either through a local one (http://localhost/...) or through a remote one (but you need SSL : https://webserver/...)' +
+ "
Another option is to force your browser to accept that (but you'll open a security breach) : on Chrome, you can start it with --allow-file-access-from-files command-line argument; on Firefox, you can set privacy.file_unique_origin to false in about:config");
return;
}
@@ -1816,9 +1804,11 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
docBody = docBody ? docBody[0] : null;
if (docBody) {
// Add any missing classes stripped from the tag
- if (htmlCSS) htmlCSS.forEach(function (cl) {
+ if (htmlCSS) {
+ htmlCSS.forEach(function (cl) {
docBody.classList.add(cl);
});
+}
// Deflect drag-and-drop of ZIM file on the iframe to Config
docBody.addEventListener('dragover', handleIframeDragover);
docBody.addEventListener('drop', handleIframeDrop);
@@ -1846,8 +1836,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
}
if (iframeArticleContent.contentWindow) {
// Configure home key press to focus #prefix only if the feature is in active state
- if (params.useHomeKeyToFocusSearchBar)
- iframeArticleContent.contentWindow.addEventListener('keydown', focusPrefixOnHomeKey);
+ if (params.useHomeKeyToFocusSearchBar) { iframeArticleContent.contentWindow.addEventListener('keydown', focusPrefixOnHomeKey); }
// when unloaded remove eventListener to avoid memory leaks
iframeArticleContent.contentWindow.onunload = function () {
iframeArticleContent.contentWindow.removeEventListener('keydown', focusPrefixOnHomeKey);
@@ -1998,7 +1987,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
selectedArchive.getDirEntryByPath(url).then(function (dirEntry) {
if (!dirEntry) {
cssCache.set(url, ''); // Prevent repeated lookups of this unfindable asset
- throw 'DirEntry ' + typeof dirEntry;
+ throw new Error('DirEntry ' + typeof dirEntry);
}
var mimetype = dirEntry.getMimetype();
var readFile = /^text\//i.test(mimetype) ? selectedArchive.readUtf8File : selectedArchive.readBinaryFile;
@@ -2069,7 +2058,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
var source = mediaSource.getAttribute('src');
source = source ? uiUtil.deriveZimUrlFromRelativeUrl(source, baseUrl) : null;
// We have to exempt text tracks from using deriveZimUrlFromRelativeurl due to a bug in Firefox [kiwix-js #496]
- source = source ? source : decodeURIComponent(mediaSource.dataset.kiwixurl);
+ source = source || decodeURIComponent(mediaSource.dataset.kiwixurl);
if (!source || !regexpZIMUrlWithNamespace.test(source)) {
if (source) console.error('No usable media source was found for: ' + source);
return;
@@ -2100,7 +2089,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
if (params.assetsCache && /\.css$|\.js$/i.test(title)) {
var cacheBlock = document.getElementById('cachingAssets');
cacheBlock.style.display = 'block';
- title = title.replace(/[^/]+\//g, '').substring(0,18);
+ title = title.replace(/[^/]+\//g, '').substring(0, 18);
cacheBlock.textContent = 'Caching ' + title + '...';
}
}
@@ -2115,13 +2104,13 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
var stateObj = {};
var urlParameters;
var stateLabel;
- if (title && !(''===title)) {
+ if (title && !(title === '')) {
// Prevents creating a double history for the same page
if (history.state && history.state.title === title) return;
stateObj.title = title;
urlParameters = '?title=' + title;
stateLabel = 'Wikipedia Article : ' + title;
- } else if (titleSearch && !(''===titleSearch)) {
+ } else if (titleSearch && !(titleSearch === '')) {
stateObj.titleSearch = titleSearch;
urlParameters = '?titleSearch=' + titleSearch;
stateLabel = 'Wikipedia search : ' + titleSearch;
@@ -2131,7 +2120,6 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
window.history.pushState(stateObj, stateLabel, urlParameters);
}
-
/**
* Extracts the content of the given article pathname, or a downloadable file, from the ZIM
*
@@ -2226,5 +2214,4 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
}
});
}
-
});
diff --git a/www/js/lib/uiUtil.js b/www/js/lib/uiUtil.js
index 739e023c..0883b53e 100644
--- a/www/js/lib/uiUtil.js
+++ b/www/js/lib/uiUtil.js
@@ -1,26 +1,30 @@
/**
* uiUtil.js : Utility functions for the User Interface
- *
+ *
* Copyright 2013-2020 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';
+/* eslint-disable no-global-assign */
+/* global $, define, webpMachine, webpHero, params */
+
// DEV: Put your RequireJS definition in the rqDef array below, and any function exports in the function parenthesis of the define statement
// We need to do it this way in order to load WebP polyfills conditionally. The WebP polyfills are only needed by a few old browsers, so loading them
// only if needed saves approximately 1MB of memory.
@@ -31,27 +35,26 @@ if (webpMachine) {
rqDef.push('webpHeroBundle');
}
-define(rqDef, function(settingsStore, util) {
-
+define(rqDef, function (settingsStore, util) {
/**
* Displays a Bootstrap alert or confirm dialog box depending on the options provided
- *
- * @param {String} message The alert message(can be formatted using HTML) to display in the body of the modal.
+ *
+ * @param {String} message The alert message(can be formatted using HTML) 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 {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) {
+ function systemAlert (message, label, isConfirm, declineConfirmLabel, approveConfirmLabel, closeMessageLabel) {
declineConfirmLabel = declineConfirmLabel || 'Cancel';
approveConfirmLabel = approveConfirmLabel || 'Confirm';
closeMessageLabel = closeMessageLabel || 'Okay';
label = label || (isConfirm ? 'Confirmation' : 'Message');
return util.PromiseQueue.enqueue(function () {
return new Promise(function (resolve, reject) {
- if (!message) reject('Missing body message');
+ if (!message) reject(new Error('Missing body message'));
// Set the text to the modal and its buttons
document.getElementById('approveConfirm').textContent = approveConfirmLabel;
document.getElementById('declineConfirm').textContent = declineConfirmLabel;
@@ -86,10 +89,10 @@ define(rqDef, function(settingsStore, util) {
modal.classList.remove('show');
modal.style.display = 'none';
backdrop.classList.remove('show');
- if(Array.from(document.body.children).indexOf(backdrop)>=0){
+ if (Array.from(document.body.children).indexOf(backdrop) >= 0) {
document.body.removeChild(backdrop);
}
- //remove event listeners
+ // remove event listeners
document.getElementById('modalCloseBtn').removeEventListener('click', close);
document.getElementById('declineConfirm').removeEventListener('click', close);
document.getElementById('closeMessage').removeEventListener('click', close);
@@ -132,7 +135,7 @@ define(rqDef, function(settingsStore, util) {
document.getElementById('declineConfirm').addEventListener('click', close);
document.getElementById('closeMessage').addEventListener('click', close);
document.getElementById('approveConfirm').addEventListener('click', closeConfirm);
-
+
modal.addEventListener('click', close);
document.getElementsByClassName('modal-dialog')[0].addEventListener('click', stopOutsideModalClick);
@@ -142,20 +145,20 @@ define(rqDef, function(settingsStore, util) {
});
});
}
-
+
/**
* Creates a data: URI from the given content
* The given attribute of the DOM node (nodeAttribute) is then set to this URI
- *
+ *
* This is used to inject images (and other dependencies) into the article DOM
- *
+ *
* @param {Object} node The node to which the URI should be added
* @param {String} nodeAttribute The attribute to set to the URI
* @param {Uint8Array} content The binary content to convert to a URI
* @param {String} mimeType The MIME type of the content
* @param {Function} callback An optional function to call to start processing the next item
*/
- function feedNodeWithDataURI(node, nodeAttribute, content, mimeType, callback) {
+ function feedNodeWithDataURI (node, nodeAttribute, content, mimeType, callback) {
// Decode WebP data if the browser does not support WebP and the mimeType is webp
if (webpMachine && /image\/webp/i.test(mimeType)) {
// If we're dealing with a dataURI, first convert to Uint8Array
@@ -200,9 +203,9 @@ define(rqDef, function(settingsStore, util) {
* Determines whether the Canvas Elements Workaround for decoding WebP images is needed, and sets UI accordingly.
* This also sets a global app parameter (useCanvasElementsForWebpTranscoding) that determines whether the workaround will be used in jQuery mode.
* Note that the workaround will never be used in Service Worker mode, but we still need to determine it in case the user switches modes.
- * @returns {Boolean} A value to indicate the browser's capability (whether it requires the workaround or not)
+ * @returns {Boolean} A value to indicate the browser's capability (whether it requires the workaround or not)
*/
- function determineCanvasElementsWorkaround() {
+ function determineCanvasElementsWorkaround () {
var userPreference = settingsStore.getItem('useCanvasElementsForWebpTranscoding') !== 'false';
// Determine whether the browser is able to read canvas data correctly
var browserRequiresWorkaround = webpMachine && webpHero && !webpHero.detectCanvasReadingSupport();
@@ -218,18 +221,18 @@ define(rqDef, function(settingsStore, util) {
useCanvasElementsCheck.checked = userPreference;
}
params.useCanvasElementsForWebpTranscoding = browserRequiresWorkaround ? userPreference : false;
- // Return the determined browser capability (which may be different from the user's preference) in case caller wants this
+ // Return the determined browser capability (which may be different from the user's preference) in case caller wants this
return browserRequiresWorkaround;
}
/**
* Replace the given CSS link (from the DOM) with an inline CSS of the given content
- *
+ *
* Due to CSP, Firefox OS does not accept syntax with href="data:text/css..." or href="blob:..."
* So we replace the tag with a
* while copying some attributes of the original tag
* Cf http://jonraasch.com/blog/javascript-style-node
- *
+ *
* @param {Element} link The original link node from the DOM
* @param {String} cssContent The content to insert as an inline stylesheet
*/
@@ -251,13 +254,13 @@ define(rqDef, function(settingsStore, util) {
}
link.parentNode.replaceChild(cssElement, link);
}
-
+
/**
* Removes parameters and anchors from a URL
* @param {type} url The URL to be processed
* @returns {String} The same URL without its parameters and anchors
*/
- function removeUrlParameters(url) {
+ function removeUrlParameters (url) {
// Remove any querystring
var strippedUrl = url.replace(/\?[^?]*$/, '');
// Remove any anchor parameters - note that we are deliberately excluding entity references, e.g. '''.
@@ -267,13 +270,13 @@ define(rqDef, function(settingsStore, util) {
/**
* 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) {
+ 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) {
@@ -294,16 +297,16 @@ define(rqDef, function(settingsStore, util) {
* Displays a Bootstrap warning alert with information about how to access content in a ZIM with unsupported active UI
*/
var activeContentWarningSetup = false;
- function displayActiveContentWarning() {
+ function displayActiveContentWarning () {
var alertActiveContent = document.getElementById('activeContent');
alertActiveContent.style.display = '';
if (!activeContentWarningSetup) {
// We are setting up the active content warning for the first time
activeContentWarningSetup = true;
- alertActiveContent.querySelector('button[data-hide]').addEventListener('click', function() {
+ alertActiveContent.querySelector('button[data-hide]').addEventListener('click', function () {
alertActiveContent.style.display = 'none';
});
- ['swModeLink', 'stop'].forEach(function(id) {
+ ['swModeLink', '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
document.getElementById(id).addEventListener('click', function () {
@@ -328,7 +331,7 @@ define(rqDef, function(settingsStore, util) {
/**
* 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
@@ -336,23 +339,25 @@ define(rqDef, function(settingsStore, util) {
* @param {Uint8Array} content The binary-format content of the downloadable file
*/
var downloadAlertSetup = false;
- function displayFileDownloadAlert(title, download, contentType, content) {
+ function displayFileDownloadAlert (title, download, contentType, content) {
var downloadAlert = document.getElementById('downloadAlert');
downloadAlert.style.display = 'block';
- if (!downloadAlertSetup) downloadAlert.querySelector('button[data-hide]').addEventListener('click', function() {
- // We are setting up the alert for the first time
- downloadAlert.style.display = 'none';
- });
+ if (!downloadAlertSetup) {
+ downloadAlert.querySelector('button[data-hide]').addEventListener('click', function () {
+ // We are setting up the alert for the first time
+ downloadAlert.style.display = 'none';
+ });
+ }
downloadAlertSetup = true;
- // Download code adapted from https://stackoverflow.com/a/19230668/9727685
+ // Download code adapted from https://stackoverflow.com/a/19230668/9727685
// Set default contentType if none was provided
if (!contentType) contentType = 'application/octet-stream';
var a = document.createElement('a');
- var blob = new Blob([content], { 'type': contentType });
+ 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;
+ var filename = download === true ? title.replace(/^.*\/([^/]+)$/, '$1') : download;
// Make filename safe
- filename = filename.replace(/[\/\\:*?"<>|]/g, '_');
+ filename = filename.replace(/[/\\:*?"<>|]/g, '_');
a.href = window.URL.createObjectURL(blob);
a.target = '_blank';
a.type = contentType;
@@ -360,16 +365,17 @@ define(rqDef, function(settingsStore, util) {
a.classList.add('alert-link');
a.textContent = filename;
var alertMessage = document.getElementById('alertMessage');
- //innerHTML required as it has HTML tags
+ // innerHTML required as it has HTML tags
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(); }
- catch (err) {
+ try {
+ a.click();
+ } 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
+ // 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) {
+ a.addEventListener('click', function (e) {
window.navigator.msSaveBlob(blob, filename);
e.preventDefault();
});
@@ -382,7 +388,7 @@ define(rqDef, function(settingsStore, util) {
* Check for update of Service Worker (PWA) and display information to user
*/
var updateAlert = document.getElementById('updateAlert');
- function checkUpdateStatus(appstate) {
+ function checkUpdateStatus (appstate) {
if ('serviceWorker' in navigator && !appstate.pwaUpdateNeeded) {
settingsStore.getCacheNames(function (cacheNames) {
if (cacheNames && !cacheNames.error) {
@@ -406,9 +412,11 @@ define(rqDef, function(settingsStore, util) {
});
}
}
- if (updateAlert) updateAlert.querySelector('button[data-hide]').addEventListener('click', function () {
- updateAlert.style.display = 'none';
- });
+ if (updateAlert) {
+ updateAlert.querySelector('button[data-hide]').addEventListener('click', function () {
+ updateAlert.style.display = 'none';
+ });
+ }
/**
* Checks if a server is accessible by attempting to load a test image from the server
@@ -416,7 +424,7 @@ define(rqDef, function(settingsStore, util) {
* @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) {
+ function checkServerIsAccessible (imageSrc, onSuccess, onError) {
var image = new Image();
image.onload = onSuccess;
image.onerror = onError;
@@ -425,10 +433,10 @@ define(rqDef, function(settingsStore, util) {
/**
* Show or hide the spinner together with a message
- * @param {Boolean} show True to show the spinner, false to hide it
- * @param {String} message A message to display, or hide the message if null
+ * @param {Boolean} show True to show the spinner, false to hide it
+ * @param {String} message A message to display, or hide the message if null
*/
- function spinnerDisplay(show, message) {
+ function spinnerDisplay (show, message) {
var searchingArticles = document.getElementById('searchingArticles');
var spinnerMessage = document.getElementById('cachingAssets');
if (show) searchingArticles.style.display = 'block';
@@ -444,25 +452,26 @@ define(rqDef, function(settingsStore, util) {
/**
* Checks whether an element is partially or fully inside the current viewport
- *
+ *
* @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 {Boolean} fully If true, checks that the entire element is inside the viewport;
* if false, checks whether any part of the element is inside the viewport
* @returns {Boolean} True if the element is fully or partially (depending on the value of )
* inside the current viewport
*/
- function isElementInView(el, fully) {
+ function isElementInView (el, fully) {
var rect = el.getBoundingClientRect();
- if (fully)
+ if (fully) {
return rect.top > 0 && rect.bottom < window.innerHeight && rect.left > 0 && rect.right < window.innerWidth;
- else
+ } else {
return rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
+ }
}
/**
* Removes the animation effect between various sections
*/
- function removeAnimationClasses() {
+ function removeAnimationClasses () {
var configuration = document.getElementById('configuration');
configuration.classList.remove('slideIn_L');
configuration.classList.remove('slideIn_R');
@@ -473,15 +482,15 @@ define(rqDef, function(settingsStore, util) {
document.getElementById('articleContent').classList.remove('slideIn_R');
document.getElementById('articleContent').classList.remove('slideOut_L');
}
-
+
/**
* Adds the slide animation between different sections
- *
+ *
* @param {String} section It takes the name of the section to which the animation is to be added
- *
+ *
*/
- function applyAnimationToSection(section) {
- if (section == 'home') {
+ function applyAnimationToSection (section) {
+ if (section === 'home') {
if (!$('#configuration').is(':hidden')) {
document.getElementById('configuration').classList.add('slideOut_R');
setTimeout(function () {
@@ -498,7 +507,7 @@ define(rqDef, function(settingsStore, util) {
setTimeout(function () {
document.getElementById('articleContent').style.display = '';
}, 300);
- } else if (section == 'config') {
+ } else if (section === 'config') {
if (!$('#about').is(':hidden')) {
$('#about').addClass('slideOut_R');
$('#configuration').addClass('slideIn_R');
@@ -515,7 +524,7 @@ define(rqDef, function(settingsStore, util) {
setTimeout(function () {
document.getElementById('configuration').style.display = '';
}, 300);
- } else if (section == 'about') {
+ } else if (section === 'about') {
if (!$('#configuration').is(':hidden')) {
document.getElementById('configuration').classList.add('slideOut_L');
setTimeout(function () {
@@ -537,17 +546,17 @@ define(rqDef, function(settingsStore, util) {
/**
* Applies the requested app and content theme
- *
+ *
* A string consists of two parts, the appTheme (theme to apply to the app shell only), and an optional
* contentTheme beginning with an underscore: e.g. 'dark_invert' = 'dark' (appTheme) + '_invert' (contentTheme)
* Current themes are: light, dark, dark_invert, dark_mwInvert but code below is written for extensibility
* For each appTheme (except the default 'light'), a corresponding set of rules must be present in app.css
* For each contentTheme, a stylesheet must be provided in www/css that is named 'kiwixJS' + contentTheme
* A rule may additionally be needed in app.css for full implementation of contentTheme
- *
+ *
* @param {String} theme The theme to apply (light|dark[_invert|_mwInvert]|auto[_invert|_mwInvert])
*/
- function applyAppTheme(theme) {
+ function applyAppTheme (theme) {
var darkPreference = window.matchMedia('(prefers-color-scheme:dark)');
// Resolve the app theme from the matchMedia preference (for auto themes) or from the theme string
var appTheme = /^auto/.test(theme) ? darkPreference.matches ? 'dark' : 'light' : theme.replace(/_.*$/, '');
@@ -583,7 +592,7 @@ define(rqDef, function(settingsStore, util) {
// Hide any previously displayed description for auto themes
var oldDescription = document.getElementById('kiwix-auto-description');
if (oldDescription) oldDescription.style.display = 'none';
- // Show description for auto themes
+ // Show description for auto themes
var description = document.getElementById('kiwix-' + theme.replace(/_.*$/, '') + '-description');
if (description) description.style.display = 'block';
// If there is no ContentTheme or we are applying a different ContentTheme, remove any previously applied ContentTheme
@@ -613,21 +622,21 @@ define(rqDef, function(settingsStore, util) {
// If we are in Config and a real document has been loaded already, expose return link so user can see the result of the change
// DEV: The Placeholder string below matches the dummy article.html that is loaded before any articles are loaded
if (document.getElementById('liConfigureNav').classList.contains('active') && doc &&
- doc.title !== "Placeholder for injecting an article into the iframe") {
+ doc.title !== 'Placeholder for injecting an article into the iframe') {
showReturnLink();
}
}
// Displays the return link and handles click event. Called by applyAppTheme()
- function showReturnLink() {
+ function showReturnLink () {
var viewArticle = document.getElementById('viewArticle');
viewArticle.style.display = 'block';
- viewArticle.addEventListener('click', function(e) {
+ viewArticle.addEventListener('click', function (e) {
e.preventDefault();
document.getElementById('liConfigureNav').classList.remove('active');
document.getElementById('liHomeNav').classList.add('active');
removeAnimationClasses();
- if (params.showUIAnimations) {
+ if (params.showUIAnimations) {
applyAnimationToSection('home');
} else {
document.getElementById('configuration').style.display = 'none';
@@ -641,7 +650,7 @@ define(rqDef, function(settingsStore, util) {
// 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) {
+ 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!';
@@ -652,25 +661,25 @@ define(rqDef, function(settingsStore, util) {
}
// Reports the search provider to the API Status Panel
- function reportSearchProviderToAPIStatusPanel(provider) {
+ function reportSearchProviderToAPIStatusPanel (provider) {
var providerAPI = document.getElementById('searchProviderStatus');
if (providerAPI) { // NB we need this so that tests don't fail
- providerAPI.textContent = 'Search Provider: ' + (/^fulltext/.test(provider) ? 'Title + Xapian [' + provider + ']' :
- /^title/.test(provider) ? 'Title only [' + provider + ']' : 'Not initialized');
+ providerAPI.textContent = 'Search Provider: ' + (/^fulltext/.test(provider) ? 'Title + Xapian [' + provider + ']'
+ : /^title/.test(provider) ? 'Title only [' + provider + ']' : 'Not initialized');
providerAPI.className = /^fulltext/.test(provider) ? 'apiAvailable' : !/ERROR/.test(provider) ? 'apiUnavailable' : 'apiBroken';
}
}
// If global variable webpMachine is true (set in init.js), then we need to initialize the WebP Polyfill
- if (webpMachine) webpMachine = new webpHero.WebpMachine({useCanvasElements: true});
-
+ if (webpMachine) webpMachine = new webpHero.WebpMachine({ useCanvasElements: true });
+
/**
* Warn the user that he/she clicked on an external link, and open it in a new tab
- *
+ *
* @param {Event} event the click event (on an anchor) to handle
* @param {Element} clickedAnchor the DOM anchor that has been clicked (optional, defaults to event.target)
*/
- function warnAndOpenExternalLinkInNewTab(event, clickedAnchor) {
+ function warnAndOpenExternalLinkInNewTab (event, clickedAnchor) {
event.preventDefault();
event.stopPropagation();
if (!clickedAnchor) clickedAnchor = event.target;
@@ -682,21 +691,22 @@ define(rqDef, function(settingsStore, util) {
message += '