mirror of
https://github.com/kiwix/kiwix-js.git
synced 2025-09-08 23:00:53 -04:00
Both MV2 and MV3 are built for Chromium. Only MV2 for Firefox for now (until Service Workers are supported as backgroundscript.js).
This commit is contained in:
parent
bdfe50562c
commit
bd7393e921
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
/* 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')
|
||||
});
|
||||
}
|
||||
genericBrowser.browserAction.onClicked.addListener(function () {
|
||||
var newURL = chrome.runtime.getURL('www/index.html');
|
||||
chrome.tabs.create({ url: newURL });
|
||||
});
|
@ -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
|
||||
}
|
||||
|
47
manifest.v2.json
Normal file
47
manifest.v2.json
Normal file
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
'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<Response>} 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<Response>} 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<Array>} 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;
|
||||
});
|
||||
}
|
||||
|
123
www/js/app.js
123
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('<p>We will now attempt to reload the app to apply the new setting.</p>' +
|
||||
'<p>(If you cancel, then the setting will only be applied when you next start the app.)</p>', '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 ? '<p>To enable the Service Worker, we ' :
|
||||
('<p>We shall attempt to switch you to ServiceWorker mode (this is now the default). ' +
|
||||
var message = params.defaultModeChangeAlertDisplayed
|
||||
? '<p>To enable the Service Worker, we '
|
||||
: ('<p>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.</p><p>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<storages.length; i++) {
|
||||
for (var i = 0; i < storages.length; i++) {
|
||||
var storage = storages[i];
|
||||
if (selectedStorageName === storage.storageName) {
|
||||
// We found the selected storage
|
||||
@ -1255,9 +1246,9 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
||||
if (storages.length === 1) {
|
||||
selectedStorage = storages[0];
|
||||
} else {
|
||||
uiUtil.systemAlert('Something weird happened with the DeviceStorage API : found a directory without prefix : '
|
||||
+ archiveDirectory + ', but there were ' + storages.length
|
||||
+ ' storages found with getDeviceStorages instead of 1', 'Error: unprefixed directory');
|
||||
uiUtil.systemAlert('Something weird happened with the DeviceStorage API : found a directory without prefix : ' +
|
||||
archiveDirectory + ', but there were ' + storages.length +
|
||||
' storages found with getDeviceStorages instead of 1', 'Error: unprefixed directory');
|
||||
}
|
||||
}
|
||||
resetCssCache();
|
||||
@ -1269,7 +1260,6 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
||||
// callbackError which is called in case of an error
|
||||
uiUtil.systemAlert(message, label);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1321,7 +1311,6 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
||||
function handleIframeDrop (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
}
|
||||
|
||||
function handleFileDrop (packet) {
|
||||
@ -1390,7 +1379,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
||||
request.response.name = url;
|
||||
resolve(request.response);
|
||||
} else {
|
||||
reject('HTTP status ' + request.status + ' when reading ' + url);
|
||||
reject(new Error('HTTP status ' + request.status + ' when reading ' + url));
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -1444,9 +1433,9 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
||||
appstate.search.status = 'cancelled';
|
||||
// Initiate a new search object and point appstate.search to it (the zimArchive search object will continue to point to the old object)
|
||||
// DEV: Technical explanation: the appstate.search is a pointer to an underlying object assigned in memory, and we are here defining a new object
|
||||
// in memory {'prefix': prefix, 'status': 'init', .....}, and pointing appstate.search to it; the old search object that was passed to selectedArchive
|
||||
// in memory {prefix: prefix, status: 'init', .....}, and pointing appstate.search to it; the old search object that was passed to selectedArchive
|
||||
// (zimArchive.js) continues to exist in the scope of the functions initiated by the previous search until all Promises have returned
|
||||
appstate.search = {prefix: prefix, status: 'init', type: '', size: params.maxSearchResultsSize};
|
||||
appstate.search = { prefix: prefix, status: 'init', type: '', size: params.maxSearchResultsSize };
|
||||
var activeContent = document.getElementById('activeContent');
|
||||
if (activeContent) activeContent.style.display = 'none';
|
||||
selectedArchive.findDirEntriesWithPrefix(appstate.search, populateListOfArticles);
|
||||
@ -1479,7 +1468,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
|
||||
} else if (nbDirEntry >= 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.'
|
||||
+ '<br/><br/>The easiest way to run it is to download and run it as a browser extension (from the vendor store).'
|
||||
+ '<br/><br/>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/...)'
|
||||
+ "<br/><br/>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.' +
|
||||
'<br/><br/>The easiest way to run it is to download and run it as a browser extension (from the vendor store).' +
|
||||
'<br/><br/>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/...)' +
|
||||
"<br/><br/>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 <html> 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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
'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<Boolean>} 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 <link> syntax with href="data:text/css..." or href="blob:..."
|
||||
* So we replace the tag with a <style type="text/css">...</style>
|
||||
* 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 = '<strong>Download</strong> 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 <fully>)
|
||||
* 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 <theme> 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 += '</p><p style="word-break:break-all;">' + clickedAnchor.href + '</p>';
|
||||
systemAlert(message, 'Opening external link', true).then(function (response) {
|
||||
if (response) {
|
||||
if (!target)
|
||||
if (!target) {
|
||||
target = '_blank';
|
||||
}
|
||||
window.open(clickedAnchor.href, target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds the closest <a> or <area> enclosing tag of an element.
|
||||
* Returns undefined if there isn't any.
|
||||
*
|
||||
*
|
||||
* @param {Element} element
|
||||
* @returns {Element} closest enclosing anchor tag (if any)
|
||||
*/
|
||||
function closestAnchorEnclosingElement(element) {
|
||||
function closestAnchorEnclosingElement (element) {
|
||||
if (Element.prototype.closest) {
|
||||
// Recent browsers support that natively. See https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
|
||||
return element.closest('a,area');
|
||||
|
Loading…
x
Reference in New Issue
Block a user