/** * app.js : User Interface implementation * This file handles the interaction between the application and the user * * Copyright 2013-2014 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'; // This uses require.js to structure javascript: // http://requirejs.org/docs/api.html#define define(['jquery', 'zimArchiveLoader', 'uiUtil', 'util', 'utf8', 'images', 'cookies', 'q', 'transformStyles', 'kiwixServe'], function ($, zimArchiveLoader, uiUtil, util, utf8, images, cookies, q, transformStyles, kiwixServe) { /** * Maximum number of articles to display in a search * @type Integer */ var MAX_SEARCH_RESULT_SIZE = params.results; //This value is controlled in init.js, as are all parameters /** * The delay (in milliseconds) between two "keepalive" messages * sent to the ServiceWorker (so that it is not stopped by * the browser, and keeps the MessageChannel to communicate * with the application) * @type Integer */ var DELAY_BETWEEN_KEEPALIVE_SERVICEWORKER = 30000; /** * @type ZIMArchive */ state.selectedArchive = null; // Define globalDropZone (universal drop area) and configDropZone (highlighting area on Config page) var globalDropZone = document.getElementById('search-article'); var scrollBoxDropZone = document.getElementById('scrollbox'); var configDropZone = document.getElementById('configuration'); /** * Resize the IFrame height, so that it fills the whole available height in the window */ function resizeIFrame() { var height = $(window).outerHeight() //- $("#top").outerHeight(true) // TODO : this 5 should be dynamically computed, and not hard-coded //- 5; + 10; //Try adding extra space to get pesky x-scrollbar out of way $(".articleIFrame").css("height", height + "px"); if (params.hideToolbar && document.getElementById('articleContent').style.display == "none") document.getElementById('scrollbox').style.height = height + "px"; var ToCList = document.getElementById('ToCList'); if (typeof ToCList !== "undefined") { ToCList.style.maxHeight = ~~(window.innerHeight * 0.75) + 'px'; ToCList.style.marginLeft = ~~(window.innerWidth / 2) - ~~(window.innerWidth * 0.16) + 'px'; } if (window.outerWidth <= 470) { document.getElementById('dropup').classList.remove('col-xs-4'); document.getElementById('dropup').classList.add('col-xs-3'); //var colXS2 = document.querySelectorAll('.col-xs-2'); //if (colXS2.length && window.outerWidth <= 360) { // for (var i = 0; i < colXS2.length; i++) { // colXS2[i].classList.remove('col-xs-2'); // colXS2[i].classList.add('col-xs-1'); // } if (window.outerWidth <= 360) { //document.getElementById('btnHomeBottom').classList.remove('col-xs-2'); //document.getElementById('btnHomeBottom').classList.add('col-xs-1'); document.getElementById('btnTop').classList.remove('col-xs-2'); document.getElementById('btnTop').classList.add('col-xs-1'); //} else if (window.outerWidth > 360 && !colXS2.length) { } else { //document.getElementById('btnHomeBottom').classList.remove('col-xs-1'); //document.getElementById('btnHomeBottom').classList.add('col-xs-2'); document.getElementById('btnTop').classList.remove('col-xs-1'); document.getElementById('btnTop').classList.add('col-xs-2'); } } else { document.getElementById('dropup').classList.remove('col-xs-3'); document.getElementById('dropup').classList.add('col-xs-4'); } } $(document).ready(resizeIFrame); $(window).resize(function() { resizeIFrame; // We need to load any images exposed by the resize var scrollFunc = document.getElementById('articleContent').contentWindow; scrollFunc = scrollFunc ? scrollFunc.onscroll : null; if (scrollFunc) scrollFunc(); }); // Define behavior of HTML elements document.getElementById('searchArticles').addEventListener('click', function () { $("#welcomeText").hide(); $('.alert').hide(); document.getElementById('searchingArticles').style.display = 'block'; pushBrowserHistoryState(null, $('#prefix').val()); searchDirEntriesFromPrefix($('#prefix').val()); clearFindInArticle(); //Re-enable top-level scrolling document.getElementById('top').style.position = "relative"; document.getElementById('scrollbox').style.position = "fixed"; document.getElementById('scrollbox').style.height = window.innerHeight + "px"; //document.getElementById('search-article').style.overflow = "auto"; if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) { $('#navbarToggle').click(); } }); document.getElementById('formArticleSearch').addEventListener('submit', function () { document.getElementById("searchArticles").click(); }); var keyPressHandled = false; $('#prefix').on('keydown', function (e) { if (/^Esc/.test(e.key)) { // Hide the article list e.preventDefault(); e.stopPropagation(); $('#articleListWithHeader').hide(); document.getElementById('articleContent').style.position = 'fixed'; $('#articleContent').focus(); $("#myModal").modal('hide'); // This is in case the modal box is showing with an index search keyPressHandled = true; } // Arrow-key selection code adapted from https://stackoverflow.com/a/14747926/9727685 // IE11 produces "Down" instead of "ArrowDown" and "Up" instead of "ArrowUp" if (/^((Arrow)?Down|(Arrow)?Up|Enter)$/.test(e.key)) { // User pressed Down arrow or Up arrow or Enter e.preventDefault(); e.stopPropagation(); // This is needed to prevent processing in the keyup event : https://stackoverflow.com/questions/9951274 keyPressHandled = true; var activeElement = document.querySelector("#articleList .hover") || document.querySelector("#articleList a"); if (!activeElement) return; // If user presses Enter, read the dirEntry if (/Enter/.test(e.key)) { if (activeElement.classList.contains('hover')) { var dirEntryId = activeElement.getAttribute('dirEntryId'); findDirEntryFromDirEntryIdAndLaunchArticleRead(dirEntryId); return; } } // If user presses ArrowDown... // (NB selection is limited to five possibilities by regex above) if (/Down/.test(e.key)) { if (activeElement.classList.contains('hover')) { activeElement.classList.remove('hover'); activeElement = activeElement.nextElementSibling || activeElement; var nextElement = activeElement.nextElementSibling || activeElement; if (!uiUtil.isElementInView(nextElement, true)) nextElement.scrollIntoView(false); } } // If user presses ArrowUp... if (/Up/.test(e.key)) { activeElement.classList.remove('hover'); activeElement = activeElement.previousElementSibling || activeElement; var previousElement = activeElement.previousElementSibling || activeElement; if (!uiUtil.isElementInView(previousElement, true)) previousElement.scrollIntoView(); if (previousElement === activeElement) { document.getElementById('articleListWithHeader').scrollIntoView(); document.getElementById('top').scrollIntoView(); } } activeElement.classList.add('hover'); } }); // Search for titles as user types characters $('#prefix').on('keyup', function (e) { if (state.selectedArchive !== null && state.selectedArchive.isReady()) { // Prevent processing by keyup event if we already handled the keypress in keydown event if (keyPressHandled) keyPressHandled = false; else onKeyUpPrefix(e); } }); // Restore the search results if user goes back into prefix field $('#prefix').on('focus', function (e) { var prefixVal = $('#prefix').val(); if (/^\s/.test(prefixVal)) { // If user had previously had the archive index open, clear it document.getElementById('prefix').value = ''; } else if (prefixVal !== '') { $('#articleListWithHeader').show(); document.getElementById('articleContent').style.position = 'static'; } }); // Hide the search resutls if user moves out of prefix field $('#prefix').on('blur', function() { $('#articleListWithHeader').hide(); document.getElementById('articleContent').style.position = 'fixed'; }); //Add keyboard shortcuts window.addEventListener('keyup', function (e) { var e = e || window.event; //Alt-F for search in article, also patches Ctrl-F for apps that do not have access to browser search if ((e.ctrlKey || e.altKey) && e.which == 70) { document.getElementById('findText').click(); } }); window.addEventListener('keydown', function (e) { //Ctrl-P to patch printing support, so iframe gets printed if (e.ctrlKey && e.which == 80) { e.stopPropagation(); e.preventDefault(); printIntercept(); } }, true); //Set up listeners for print dialogues //$("#printModal").off('hide.bs.modal'); $("#printModal").on('hide.bs.modal', function () { //Restore temporarily changed values params.cssSource = cookies.getItem('cssSource') || "auto"; if (document.activeElement.id != "confirm-print-continue") { //User cancelled if (params.printInterception) { printCleanup(); return; } //We don't need a radical cleanup because there was no printIntercept removePageMaxWidth(); setTab(); return; } uiUtil.printCustomElements(); document.getElementById("alert-content").innerHTML = "Document will now reload to restore the DOM after printing..."; $("#alertModal").off('hide.bs.modal'); $("#alertModal").on('hide.bs.modal', function () { printCleanup(); }); $("#alertModal").modal({ backdrop: "static", keyboard: true }); //innerDocument.execCommand("print", false, null); window.frames[0].frameElement.contentWindow.print(); }); document.getElementById('printDesktopCheck').addEventListener('click', function (e) { //Reload article if user wants to print a different style params.cssSource = e.target.checked ? "desktop" : "mobile"; params.printIntercept = true; params.printInterception = false; var btnContinue = document.getElementById('confirm-print-continue'); var btnCancel = document.getElementById('confirm-print-cancel'); btnCancel.disabled = true; btnContinue.disabled = true; btnContinue.innerHTML = "Please wait"; goToArticle(decodeURIComponent(params.lastPageVisit.replace(/@kiwixKey@.+/, ""))); }); function printCleanup() { params.printIntercept = false; params.printInterception = false; goToArticle(decodeURIComponent(params.lastPageVisit.replace(/@kiwixKey@.+/, ""))); setTimeout(function () { //Restore temporarily changed value after page has reloaded params.rememberLastPage = cookies.getItem('rememberLastPage') == "false" ? false : true; if (!params.rememberLastPage) { cookies.setItem('lastPageVisit', "", Infinity); if (typeof Storage !== "undefined") { try { localStorage.setItem('lastPageHTML', ""); } catch (err) { console.log("localStorage not supported: " + err); } } } }, 5000); } //End of listeners for print dialogues function printIntercept() { params.printInterception = params.printIntercept; params.printIntercept = false; document.getElementById('btnAbout').classList.add('active'); var btnContinue = document.getElementById('confirm-print-continue'); var btnCancel = document.getElementById('confirm-print-cancel'); btnCancel.disabled = false; btnContinue.disabled = false; btnContinue.innerHTML = "Continue"; var printModalContent = document.getElementById('print-modal-content'); printModalContent.classList.remove('dark'); var determinedTheme = params.cssUITheme; determinedTheme = determinedTheme == 'auto' ? cssUIThemeGetOrSet('auto', true) : determinedTheme; if (determinedTheme != "light") { printModalContent.classList.add('dark'); } //If document is in wrong style, reload it var innerDoc = window.frames[0].frameElement.contentDocument; var styleIsDesktop = !/\bhref\s*=\s*["'][^"']*?(?:minerva|mobile)/i.test(innerDoc.head.innerHTML); if (styleIsDesktop != document.getElementById("printDesktopCheck").checked) { //We need to reload the document because it doesn't match the requested style params.cssSource = styleIsDesktop ? "mobile" : "desktop"; params.rememberLastPage = true; //Re-enable caching to speed up reloading of page params.printIntercept = true; params.printInterception = false; btnCancel.disabled = true; btnContinue.disabled = true; btnContinue.innerHTML = "Please wait"; $("#printModal").modal({ backdrop: "static", keyboard: true }); goToArticle(decodeURIComponent(params.lastPageVisit.replace(/@kiwixKey@.+/, ""))); return; } //Pre-load all images in case user wants to print them if (params.imageDisplay) { document.getElementById("printImageCheck").disabled = false; btnCancel.disabled = true; btnContinue.disabled = true; btnContinue.innerHTML = "Loading images..."; //Callback for when all images are loaded params.printImagesLoaded = function() { // Images have finished loading, so enable buttons btnCancel.disabled = false; btnContinue.disabled = false; btnContinue.innerHTML = "Continue"; }; images.prepareImagesJQuery(printIntercept); } else { document.getElementById("printImageCheck").checked = false; document.getElementById("printImageCheck").disabled = true; } //Remove max page-width restriction if (params.removePageMaxWidth !== true) { var tempPageMaxWidth = params.removePageMaxWidth; params.removePageMaxWidth = true; removePageMaxWidth(); params.removePageMaxWidth = tempPageMaxWidth; } $("#printModal").modal({ backdrop: "static", keyboard: true }); } //Establish some variables with global scope var firstRun = false; var localSearch = {}; function clearFindInArticle() { if (document.getElementById('row2').style.display == "none") return; if (typeof localSearch !== "undefined" && localSearch.remove) { localSearch.remove(); } document.getElementById('findInArticle').value = ""; document.getElementById('matches').innerHTML = "Full: 0"; document.getElementById('partial').innerHTML = "Partial: 0"; document.getElementById('row2').style.display = "none"; document.getElementById('findText').classList.remove("active"); } document.getElementById('findText').addEventListener('click', function () { var searchDiv = document.getElementById('row2'); if (searchDiv.style.display != "none") { setTab(); return; } var findInArticle = null; var innerDocument = window.frames[0].frameElement.contentDocument; innerDocument = innerDocument ? innerDocument.body : null; if (!innerDocument || innerDocument.innerHTML.length < 10) return; setTab('findText'); findInArticle = document.getElementById('findInArticle'); searchDiv.style.display = "block"; findInArticle.focus(); localSearch = new util.Hilitor(innerDocument); //TODO: MatchType should be language specific var timer = null; findInArticle.addEventListener('keyup', function (e) { //If user pressed Alt-F or Ctrl-F, exit if ((e.altKey || e.ctrlKey) && e.which == 70) return; var val = this.value; //If user pressed enter / return key if (val && e.which == 13) { localSearch.scrollFrom = localSearch.scrollToFullMatch(val, localSearch.scrollFrom); return; } //If value hasn't changed, exit if (val == localSearch.lastScrollValue) return; findInArticleKeyup(val); }); var findInArticleKeyup = function (val) { // Use a timeout, so that very quick typing does not cause a lot of overhead if (window.timeoutFIAKeyup) { window.clearTimeout(window.timeoutFIAKeyup); } window.timeoutFIAKeyup = window.setTimeout(function () { findInArticleInitiate(val); }, 500); }; var findInArticleInitiate = function (val) { //Ensure nothing happens if only one or two ASCII values have been entered (search is not specific enough) //if no value has been entered (clears highlighting if user deletes all values in search field) if (!/^\s*[A-Za-z\s]{1,2}$/.test(val)) { localSearch.scrollFrom = 0; localSearch.lastScrollValue = val; localSearch.setMatchType('open'); //Change matchType to 'left' if we are dealing with an ASCII language and a space has been typed if (/\s/.test(val) && /(?:^|[\s\b])[A-Za-z]+(?:[\b\s]|$)/.test(val)) localSearch.setMatchType('left'); localSearch.apply(val); if (val.length) { var fullTotal = localSearch.countFullMatches(val); var partialTotal = localSearch.countPartialMatches(); fullTotal = fullTotal > partialTotal ? partialTotal : fullTotal; document.getElementById('matches').innerHTML = 'Full: ' + fullTotal + ''; document.getElementById('partial').innerHTML = "Partial: " + partialTotal; document.getElementById('scrollLink').addEventListener('click', function () { localSearch.scrollFrom = localSearch.scrollToFullMatch(val, localSearch.scrollFrom); }); //Auto-scroll: TODO - consider making this an option localSearch.scrollFrom = localSearch.scrollToFullMatch(val, localSearch.scrollFrom); } else { document.getElementById('matches').innerHTML = "Full: 0"; document.getElementById('partial').innerHTML = "Partial: 0"; } } }; }); document.getElementById('btnRandomArticle').addEventListener('click', function () { setTab('btnRandomArticle'); //Re-enable top-level scrolling document.getElementById('top').style.position = "relative"; document.getElementById('scrollbox').style.position = "fixed"; document.getElementById('scrollbox').style.height = window.innerHeight + "px"; goToRandomArticle(); }); document.getElementById('btnRescanDeviceStorage').addEventListener("click", function () { var returnDivs = document.getElementsByClassName("returntoArticle"); for (var i = 0; i < returnDivs.length; i++) { returnDivs[i].innerHTML = ""; } params.rescan = true; //Reload any ZIM files in local storage (whcih the usar can't otherwise select with the filepicker) loadPackagedArchive(); if (storages.length) { searchForArchivesInStorage(); } else { displayFileSelect(); } }); // Bottom bar : // @TODO Since bottom bar now hidden in Settings and About the returntoArticle code cannot be accessed; // consider adding it to top home button instead document.getElementById('btnBack').addEventListener('click', function () { if (document.getElementById('articleContent').style.display == "none") { document.getElementById('returntoArticle').click(); return; } clearFindInArticle(); history.back(); return; }); document.getElementById('btnForward').addEventListener('click', function () { clearFindInArticle(); history.forward(); }); document.getElementById('articleContent').contentDocument.body.style.fontSize = params.relativeFontSize + "%"; document.getElementById('btnZoomin').addEventListener('click', function () { params.relativeFontSize += 5; var doc = document.getElementById('articleContent').contentDocument; doc.body.style.fontSize = /-\/static\/main\.css/.test(doc.head.innerHTML) ? params.relativeFontSize * 1.5 + "%" : params.relativeFontSize + "%"; document.getElementById('lblZoom').innerHTML = params.relativeFontSize + "%"; document.getElementById('lblZoom').style = "position:absolute;right: " + window.innerWidth / 3 + "px;bottom:5px;z-index:50;"; setTimeout(function () { document.getElementById('lblZoom').innerHTML = ""; }, 1000); cookies.setItem('relativeFontSize', params.relativeFontSize, Infinity); }); document.getElementById('btnZoomout').addEventListener('click', function () { params.relativeFontSize -= 5; var doc = document.getElementById('articleContent').contentDocument; doc.body.style.fontSize = /-\/static\/main\.css/.test(doc.head.innerHTML) ? params.relativeFontSize * 1.5 + "%" : params.relativeFontSize + "%"; document.getElementById('lblZoom').innerHTML = params.relativeFontSize + "%"; document.getElementById('lblZoom').style = "position:absolute;left: " + window.innerWidth / 3 + "px;bottom:5px;z-index:50;"; setTimeout(function () { document.getElementById('lblZoom').innerHTML = ""; }, 1000); cookies.setItem('relativeFontSize', params.relativeFontSize, Infinity); }); setRelativeUIFontSize(params.relativeUIFontSize); document.getElementById('relativeUIFontSizeSlider').addEventListener('change', function () { setRelativeUIFontSize(this.value); }); function setRelativeUIFontSize(value) { value = ~~value; document.getElementById('spinnerVal').innerHTML = value + "%"; document.getElementById('search-article').style.fontSize = value + "%"; document.getElementById('relativeUIFontSizeSlider').value = value; var forms = document.querySelectorAll('.form-control'); for (var i = 0; i < forms.length; i++) { forms[i].style.fontSize = ~~(value * 14 / 100) + "px"; } var buttons = document.getElementsByClassName('btn'); for (var i = 0; i < buttons.length; i++) { buttons[i].style.fontSize = buttons[i].id == "reloadPackagedArchive" ? ~~(value * 10 / 100) + "px" : ~~(value * 14 / 100) + "px"; } var heads = document.querySelectorAll("h1, h2, h3, h4"); for (var i = 0; i < heads.length; i++) { var multiplier = 1; var head = heads[i].tagName; multiplier = head == "H4" ? 1.4 : head == "H3" ? 1.9 : head == "H2" ? 2.3 : head == "H1" ? 2.8 : multiplier; heads[i].style.fontSize = ~~(value * 0.14 * multiplier) + "px"; } document.getElementById('displaySettingsDiv').scrollIntoView(); //document.getElementById('prefix').style.height = ~~(value * 14 / 100) * 1.4285 + 14 + "px"; if (value != params.relativeUIFontSize) { params.relativeUIFontSize = value; cookies.setItem('relativeUIFontSize', value, Infinity); } } document.getElementById('btnHomeBottom').addEventListener('click', function () { document.getElementById('btnHome').click(); }); document.getElementById('btnTop').addEventListener('click', function () { //Ensures toolbar is shown after hidden var thisdoc = document.getElementById('top'); if (params.hideToolbar && thisdoc.style.zIndex == "0") { params.hideToolbar = false; checkToolbar(); params.hideToolbar = true; return; } document.getElementById('articleContent').contentWindow.scrollTo({ top: 0, behavior: 'smooth' }); document.getElementById('search-article').scrollTop = 0; }); // Top menu : document.getElementById('btnHome').addEventListener('click', function () { setTab('btnHome'); $('#articleContent').hide(); $('#articleContent').contents().empty(); $('#searchingArticles').hide(); $('#welcomeText').show(); if (state.selectedArchive !== null && state.selectedArchive.isReady()) { $('#welcomeText').hide(); goToMainArticle(); } }); function setTab(activeBtn) { // Highlight the selected section in the navbar $('#liHomeNav').attr("class", "active"); $('#liConfigureNav').attr("class", ""); $('#liAboutNav').attr("class", ""); if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) { $('#navbarToggle').click(); } setActiveBtn(activeBtn); var btnAbout = document.getElementById('btnAbout'); if (!activeBtn || activeBtn == "btnHome" || activeBtn == "findText") { btnAbout.innerHTML = ''; btnAbout.title = 'Ctrl-P: Print'; } else { btnAbout.innerHTML = ''; btnAbout.title = 'About'; } clearFindInArticle(); //Re-enable bottom toolbar display document.getElementById('footer').style.display = "block"; //Re-enable top-level scrolling document.getElementById('top').style.position = "relative"; document.getElementById('scrollbox').style.position = "fixed"; document.getElementById('scrollbox').style.height = window.innerHeight + "px"; document.getElementById('articleContent').style.position = "fixed"; //Use the "light" navbar if the content is "light" (otherwise it looks shite....) var determinedTheme = cssUIThemeGetOrSet(params.cssUITheme); var determinedWikiTheme = params.cssTheme == 'auto' ? determinedTheme : params.cssTheme == 'inverted' ? 'dark' : params.cssTheme; if (determinedWikiTheme != determinedTheme) { if (determinedWikiTheme == "light" && (!activeBtn || activeBtn == "btnHome" || activeBtn == "findText") || determinedWikiTheme == "dark" && activeBtn && activeBtn != "btnHome" && activeBtn != "findText") { cssUIThemeGetOrSet("light"); } else { cssUIThemeGetOrSet("dark"); } } else { cssUIThemeGetOrSet(determinedTheme); } if (typeof Windows !== 'undefined') { document.getElementById('openLocalFiles').style.display = params.rescan ? "block" : "none"; } document.getElementById('libraryArea').style.borderColor = ''; document.getElementById('libraryArea').style.borderStyle = ''; var currentArchive = document.getElementById('currentArchive'); if (params.packagedFile && params.storedFile && params.storedFile != params.packagedFile) { currentArchive.innerHTML = "Currently loaded archive: " + params.storedFile.replace(/\.zim$/i, "") + ""; currentArchive.style.display = "block"; document.getElementById('downloadLinksText').style.display = "none"; document.getElementById('moreInfo').style.display = "none"; } if (params.storedFile && params.storedFile == params.packagedFile) { document.getElementById('downloadLinksText').style.display = "block"; currentArchive.style.display = "none"; } //else { // document.getElementById('rescanStorage').style.display = "none"; //} // Show the selected content in the page $('#about').hide(); $('#configuration').hide(); $('#formArticleSearch').show(); $('#articleContent').show(); $("#articleList").empty(); $('#articleListHeaderMessage').empty(); $('#articleListWithHeader').hide(); $("#prefix").val(""); $("#searchingArticles").hide(); $("#welcomeText").hide(); } function setActiveBtn(activeBtn) { document.getElementById('btnHome').classList.remove("active"); document.getElementById('btnRandomArticle').classList.remove("active"); document.getElementById('btnConfigure').classList.remove("active"); document.getElementById('btnAbout').classList.remove("active"); if (activeBtn) { var activeID = document.getElementById(activeBtn); if (activeID) activeID.classList.add("active"); } } document.getElementById('btnConfigure').addEventListener('click', function () { var searchDiv = document.getElementById('configuration'); if (searchDiv.style.display != 'none') { setTab(); if (params.themeChanged) { params.themeChanged = false; if (history.state !== null) { var thisURL = decodeURIComponent(history.state.title); goToArticle(thisURL); } } return; } setTab('btnConfigure'); // Highlight the selected section in the navbar $('#liHomeNav').attr("class", ""); $('#liConfigureNav').attr("class", "active"); $('#liAboutNav').attr("class", ""); if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) { $('#navbarToggle').click(); } //Hide footer toolbar document.getElementById('footer').style.display = "none"; // Show the selected content in the page $('#configuration').show(); $('#articleContent').hide(); $('.alert').hide(); $('#downloadLinks').hide(); $('#serverResponse').hide(); $('#myModal').hide(); refreshAPIStatus(); //Re-enable top-level scrolling document.getElementById('top').style.position = "relative"; document.getElementById('scrollbox').style.position = "fixed"; document.getElementById('scrollbox').style.height = document.documentElement.clientHeight + "px"; document.getElementById('search-article').style.overflowY = "auto"; if (document.getElementById('openLocalFiles').style.display == "none") { document.getElementById('rescanStorage').style.display = "block"; } //If user hadn't previously picked a folder or a file, resort to the local storage folder (UWP functionality) if (params.localStorage && !params.pickedFolder && !params.pickedFile) { params.pickedFolder = params.localStorage; } if (typeof Windows === 'undefined') { //If not UWP, display legacy File Select document.getElementById('archiveFile').style.display = "none"; document.getElementById('archiveFiles').style.display = "none"; document.getElementById('UWPInstructions').style.display = "none"; document.getElementById('archivesFound').style.display = "none"; document.getElementById('chooseArchiveFromLocalStorage').style.display = "block"; document.getElementById('instructions').style.display = "block"; document.getElementById('archiveFilesLegacy').style.display = "inline"; document.getElementById('archiveFilesLegacy').addEventListener('change', setLocalArchiveFromFileSelect); } }); document.getElementById('btnAbout').addEventListener('click', function () { var btnAboutElement = document.getElementById('btnAbout'); if (/glyphicon-print/.test(btnAboutElement.innerHTML)) { printIntercept(); return; } //Check if we're 'unclicking' the button var searchDiv = document.getElementById('about'); if (searchDiv.style.display != 'none') { setTab(); return; } // Highlight the selected section in the navbar $('#liHomeNav').attr("class", ""); $('#liConfigureNav').attr("class", ""); $('#liAboutNav').attr("class", "active"); if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) { $('#navbarToggle').click(); } setTab('btnAbout'); //Hide footer toolbar document.getElementById('footer').style.display = "none"; // Show the selected content in the page $('#about').show(); $('#articleContent').hide(); $('.alert').hide(); //Re-enable top-level scrolling document.getElementById('top').style.position = "relative"; document.getElementById('scrollbox').style.position = "fixed"; document.getElementById('scrollbox').style.height = document.documentElement.clientHeight + "px"; document.getElementById('search-article').style.overflowY = "auto"; }); // Two event listeners are needed because the archive list doesn't "change" if there is only one element in it // (also note that keyboard users might not click) var selectFired = false; document.getElementById('archiveList').addEventListener('change', function () { this.click(); }); document.getElementById('archiveList').addEventListener('click', function () { if (selectFired) return; // If nothing was selected, user will have to click again // (NB this.selectedIndex will be -1 if no value has been selected) if (!~this.selectedIndex) return; selectFired = true; $('#openLocalFiles').hide(); // Void any previous picked file to prevent it launching if (params.pickedFile && params.pickedFile.name !== this.options[this.selectedIndex].value) { params.pickedFile = ''; } setLocalArchiveFromArchiveList(); setTimeout(function () { selectFired = false; }, 0); }); document.getElementById('archiveFile').addEventListener('click', function () { if (typeof Windows !== 'undefined' && typeof Windows.Storage !== 'undefined') { //UWP FilePicker pickFileUWP(); } else { //@TODO enable and provide classic filepicker } }); document.getElementById('archiveFiles').addEventListener('click', function () { if (typeof Windows !== 'undefined' && typeof Windows.Storage !== 'undefined') { //UWP FolderPicker pickFolderUWP(); } else { //@TODO hide folderpicker } }); document.getElementById('downloadTrigger').addEventListener('click', function () { kiwixServe.requestXhttpData(params.kiwixDownloadLink); }); $('input:radio[name=contentInjectionMode]').on('change', function (e) { var returnDivs = document.getElementsByClassName("returntoArticle"); for (var i = 0; i < returnDivs.length; i++) { returnDivs[i].innerHTML = ""; } // Do the necessary to enable or disable the Service Worker setContentInjectionMode(this.value); }); $('input:checkbox[name=allowInternetAccess]').on('change', function (e) { params.allowInternetAccess = this.checked ? true : false; document.getElementById('serverResponse').style.display = "none"; if (!this.checked) { document.getElementById('downloadLinks').style.display = "none"; } //NB do not store this value in a cookie -- should be enabled by the user on a per-session basis only }); $('input:checkbox[name=cssCacheMode]').on('change', function (e) { params.cssCache = this.checked ? true : false; cookies.setItem('cssCache', params.cssCache, Infinity); params.themeChanged = true; }); $('input:checkbox[name=imageDisplayMode]').on('change', function (e) { params.imageDisplay = this.checked ? true : false; params.imageDisplayMode = this.checked ? 'progressive' : 'manual'; params.themeChanged = params.imageDisplay; //Only reload page if user asked for all images to be displayed cookies.setItem('imageDisplay', params.imageDisplay, Infinity); }); $('input:checkbox[name=hideActiveContentWarning]').on('change', function (e) { params.hideActiveContentWarning = this.checked ? true : false; cookies.setItem('hideActiveContentWarning', params.hideActiveContentWarning, Infinity); }); $('input:checkbox[name=allowHTMLExtraction]').on('change', function (e) { params.allowHTMLExtraction = this.checked ? true : false; cookies.setItem('allowHTMLExtraction', params.allowHTMLExtraction, Infinity); }); $('input:text[name=alphaChar]').on('change', function (e) { params.alphaChar = this.value.length == 1 ? this.value : params.alphaChar; this.value = params.alphaChar; cookies.setItem('alphaChar', params.alphaChar, Infinity); }); $('input:text[name=omegaChar]').on('change', function (e) { params.omegaChar = this.value.length == 1 ? this.value : params.omegaChar; this.value = params.omegaChar; cookies.setItem('omegaChar', params.omegaChar, Infinity); }); $('input:checkbox[name=hideToolbar]').on('change', function (e) { params.hideToolbar = this.checked ? true : false; cookies.setItem('hideToolbar', params.hideToolbar, Infinity); //checkToolbar(); }); // Set up hook into Windows ViewManagement uiSettings if needed var uiSettings = null; initializeUISettings(); function initializeUISettings() { var checkAuto = params.cssUITheme == 'auto' || params.cssTheme == 'auto'; if (checkAuto && typeof Windows !== 'undefined' && Windows.UI && Windows.UI.ViewManagement) { uiSettings = new Windows.UI.ViewManagement.UISettings(); uiSettings.oncolorvalueschanged = function () { if (params.cssUITheme == 'auto') cssUIThemeGetOrSet('auto'); if (params.cssTheme == 'auto') switchCSSTheme(); }; } } // Code below is needed on startup to show or hide the inverted dark theme checkbox; // similar code also runs in switchCSSTheme(), but that is not evoked on startup if (params.cssTheme == 'auto') document.getElementById('darkInvert').style.display = cssUIThemeGetOrSet('auto', true) == 'light' ? 'none' : 'inline'; document.getElementById('cssUIDarkThemeCheck').addEventListener('click', function () { //This code implements a tri-state checkbox if (this.readOnly) this.checked = this.readOnly = false; else if (!this.checked) this.readOnly = this.indeterminate = true; //Code below shows how to invert the order //if (this.readOnly) { this.checked = true; this.readOnly = false; } //else if (this.checked) this.readOnly = this.indeterminate = true; params.cssUITheme = this.indeterminate ? "auto" : this.checked ? 'dark' : 'light'; if (!uiSettings) initializeUISettings(); cookies.setItem('cssUITheme', params.cssUITheme, Infinity); document.getElementById('cssUIDarkThemeState').innerHTML = params.cssUITheme; cssUIThemeGetOrSet(params.cssUITheme); //Make subsequent check valid if params.cssTheme is "invert" rather than "dark" if (params.cssUITheme != params.cssTheme) $('#cssWikiDarkThemeCheck').click(); }); document.getElementById('cssWikiDarkThemeCheck').addEventListener('click', function () { if (this.readOnly) this.checked = this.readOnly = false; else if (!this.checked) this.readOnly = this.indeterminate = true; params.cssTheme = this.indeterminate ? "auto" : this.checked ? 'dark' : 'light'; if (!uiSettings) initializeUISettings(); var determinedValue = params.cssTheme; if (params.cssTheme == "auto") determinedValue = cssUIThemeGetOrSet('auto', true); if (determinedValue == "light") document.getElementById('footer').classList.remove("darkfooter"); if (params.cssTheme == "light") document.getElementById('cssWikiDarkThemeInvertCheck').checked = false; if (determinedValue == "dark") document.getElementById('footer').classList.add("darkfooter"); document.getElementById('darkInvert').style.display = determinedValue == 'light' ? 'none' : 'inline'; params.cssTheme = document.getElementById('cssWikiDarkThemeInvertCheck').checked && determinedValue == 'dark' ? 'invert' : params.cssTheme; document.getElementById('cssWikiDarkThemeState').innerHTML = params.cssTheme; cookies.setItem('cssTheme', params.cssTheme, Infinity); switchCSSTheme(); }); $('input:checkbox[name=cssWikiDarkThemeInvert]').on('change', function (e) { if (this.checked) { params.cssTheme = 'invert'; } else { var darkThemeCheckbox = document.getElementById('cssWikiDarkThemeCheck'); params.cssTheme = darkThemeCheckbox.indeterminate ? 'auto' : darkThemeCheckbox.checked ? 'dark' : 'light'; } cookies.setItem('cssTheme', params.cssTheme, Infinity); switchCSSTheme(); }); function cssUIThemeGetOrSet(value, getOnly) { if (value == 'auto') { if (uiSettings) { // We need to check the system theme // Value 0 below is the 'background' constant in array Windows.UI.ViewManagement.UIColorType var colour = uiSettings.getColorValue(0); value = (colour.b + colour.g + colour.r) <= 382 ? 'dark' : 'light'; } else { // There is no system default, so use light, as it is what most people will expect value = 'light'; } } if (getOnly) return value; var elements; if (value == 'dark') { document.getElementsByTagName('body')[0].classList.add("dark"); document.getElementById('archiveFilesLegacy').classList.add("dark"); document.getElementById('footer').classList.add("darkfooter"); document.getElementById('archiveFilesLegacy').classList.remove("btn"); document.getElementById('findInArticle').classList.add("dark"); document.getElementById('prefix').classList.add("dark"); elements = document.querySelectorAll(".settings"); for (var i = 0; i < elements.length; i++) { elements[i].style.border = "1px solid darkgray"; } document.getElementById('kiwixIcon').src = /wikivoyage/i.test(params.storedFile) ? "img/icons/wikivoyage-white-32.png" : /medicine/i.test(params.storedFile) ? "img/icons/wikimed-lightblue-32.png" : "img/icons/kiwix-32.png"; if (/wikivoyage/i.test(params.storedFile)) document.getElementById('kiwixIconAbout').src = "img/icons/wikivoyage-90-white.png"; } if (value == 'light') { document.getElementsByTagName('body')[0].classList.remove("dark"); document.getElementById('search-article').classList.remove("dark"); document.getElementById('footer').classList.remove("darkfooter"); document.getElementById('archiveFilesLegacy').classList.remove("dark"); document.getElementById('archiveFilesLegacy').classList.add("btn"); document.getElementById('findInArticle').classList.remove("dark"); document.getElementById('prefix').classList.remove("dark"); elements = document.querySelectorAll(".settings"); for (var i = 0; i < elements.length; i++) { elements[i].style.border = "1px solid black"; } document.getElementById('kiwixIcon').src = /wikivoyage/i.test(params.storedFile) ? "img/icons/wikivoyage-black-32.png" : /medicine/i.test(params.storedFile) ? "img/icons/wikimed-blue-32.png" : "img/icons/kiwix-blue-32.png"; if (/wikivoyage/i.test(params.packagedFile)) document.getElementById('kiwixIconAbout').src = "img/icons/wikivoyage-90.png"; } return value; } function switchCSSTheme() { var doc = window.frames[0].frameElement.contentDocument; var treePath = decodeURIComponent(params.lastPageVisit).replace(/[^/]+\/(?:[^/]+$)?/g, "../"); //If something went wrong, use the page reload method if (!treePath) { params.themeChanged = true; return; } var styleSheets = doc.getElementsByTagName("link"); //Remove any dark theme, as we don't know whether user switched from light to dark or from inverted to dark, etc. for (var i = styleSheets.length - 1; i > -1; i--) { if (~styleSheets[i].href.search(/\/style-dark/)) { styleSheets[i].disabled = true; styleSheets[i].parentNode.removeChild(styleSheets[i]); } } var determinedWikiTheme = params.cssTheme == 'auto' ? cssUIThemeGetOrSet('auto', true) : params.cssTheme; var breakoutLink = doc.getElementById('breakoutLink'); if (determinedWikiTheme != "light") { var link = doc.createElement("link"); link.setAttribute("rel", "stylesheet"); link.setAttribute("type", "text/css"); link.setAttribute("href", determinedWikiTheme == "dark" ? "-/s/style-dark.css" : "-/s/style-dark-invert.css"); doc.head.appendChild(link); if (breakoutLink) breakoutLink.src = 'img/icons/new_window_lb.svg'; } else { if (breakoutLink) breakoutLink.src = 'img/icons/new_window.svg'; } document.getElementById('darkInvert').style.display = determinedWikiTheme == 'light' ? 'none' : 'inline'; } $('input:checkbox[name=rememberLastPage]').on('change', function (e) { if (params.rememberLastPage && this.checked) document.getElementById('rememberLastPageCheck').checked = true; params.rememberLastPage = this.checked ? true : false; cookies.setItem('rememberLastPage', params.rememberLastPage, Infinity); if (!params.rememberLastPage) { cookies.setItem('lastPageVisit', "", Infinity); //Clear localStorage if (typeof Storage !== "undefined") { try { localStorage.setItem('lastPageHTML', ""); localStorage.clear(); } catch (err) { console.log("localStorage not supported: " + err); } } } }); $('input:radio[name=cssInjectionMode]').on('click', function (e) { params.cssSource = this.value; cookies.setItem('cssSource', params.cssSource, Infinity); params.themeChanged = true; }); document.getElementById('removePageMaxWidthCheck').addEventListener('click', function () { //This code implements a tri-state checkbox if (this.readOnly) this.checked = this.readOnly = false; else if (!this.checked) this.readOnly = this.indeterminate = true; params.removePageMaxWidth = this.indeterminate ? "auto" : this.checked; document.getElementById('pageMaxWidthState').innerHTML = (params.removePageMaxWidth == "auto" ? "auto" : params.removePageMaxWidth ? "always" : "never"); cookies.setItem('removePageMaxWidth', params.removePageMaxWidth, Infinity); removePageMaxWidth(); }); function removePageMaxWidth() { var doc = window.frames[0].frameElement.contentDocument; var zimType = /\bhref\s*=\s*["'][^"']*?(?:minerva|mobile)/i.test(doc.head.innerHTML) ? "mobile" : "desktop"; var idArray = ["content", "bodyContent"]; for (var i = 0; i < idArray.length; i++) { var contentElement = doc.getElementById(idArray[i]); if (!contentElement) continue; var docStyle = contentElement.style; if (!docStyle) continue; if (contentElement.className == "mw-body") { docStyle.padding = "1em"; docStyle.border = "1px solid #a7d7f9"; } if (params.removePageMaxWidth == "auto") { docStyle.maxWidth = zimType == "desktop" ? "100%" : "55.8em"; docStyle.cssText = docStyle.cssText.replace(/(max-width[^;]+)/i, "$1 !important"); docStyle.border = "0"; } else { docStyle.maxWidth = params.removePageMaxWidth ? "100%" : "55.8em"; docStyle.cssText = docStyle.cssText.replace(/(max-width[^;]+)/i, "$1 !important"); if (params.removePageMaxWidth || zimType == "mobile") docStyle.border = "0"; } docStyle.margin = "0 auto"; } } $('input:radio[name=useMathJax]').on('click', function (e) { params.useMathJax = /true/i.test(this.value); cookies.setItem('useMathJax', params.useMathJax, Infinity); params.themeChanged = true; }); document.getElementById('otherLangs').addEventListener('click', function () { if (!params.showFileSelectors) document.getElementById('displayFileSelectorsCheck').click(); var library = document.getElementById('libraryArea'); library.style.borderColor = 'red'; library.style.borderStyle = 'solid'; document.getElementById('downloadTrigger').addEventListener('mousedown', function () { library.style.borderColor = ''; library.style.borderStyle = ''; }); }); $('input:checkbox[name=displayFileSelectors]').on('change', function (e) { params.showFileSelectors = this.checked ? true : false; document.getElementById('rescanStorage').style.display = "block"; document.getElementById('openLocalFiles').style.display = "none"; document.getElementById('hideFileSelectors').style.display = params.showFileSelectors ? "block" : "none"; document.getElementById('downloadLinksText').style.display = params.showFileSelectors ? "none" : "inline"; document.getElementById('moreInfo').style.display = params.showFileSelectors ? "none" : "inline"; if (params.packagedFile && params.storedFile && params.storedFile != params.packagedFile) { var currentArchive = document.getElementById('currentArchive'); currentArchive.innerHTML = "Currently loaded archive: " + params.storedFile.replace(/\.zim$/i, "") + ""; currentArchive.style.display = params.showFileSelectors ? "none" : "block"; document.getElementById('downloadLinksText').style.display = params.showFileSelectors ? "none" : "block"; } cookies.setItem('showFileSelectors', params.showFileSelectors, Infinity); if (params.showFileSelectors) document.getElementById('configuration').scrollIntoView(); }); function checkToolbar() { var thisdoc = document.getElementById('top'); var scrollbox = document.getElementById('scrollbox'); if (!params.hideToolbar) { thisdoc.style.display = "block"; thisdoc.style.position = "fixed"; thisdoc.style.zIndex = "1"; scrollbox.style.position = "relative"; scrollbox.style.height = ~~document.getElementById('top').getBoundingClientRect().height + "px"; //Cannot be larger or else on Windows Mobile (at least) and probably other mobile, the top bar gets covered by iframe resizeIFrame(); return; } thisdoc.style.position = "relative"; thisdoc.style.zIndex = "0"; scrollbox.style.position = "fixed"; resizeIFrame(); if (typeof tryHideToolber !== "undefined") window.frames[0].removeEventListener('scroll', tryHideToolbar); var tryHideToolbar = function () { hideToolbar(); }; window.frames[0].addEventListener('scroll', tryHideToolbar, true); function hideToolbar(lastypos) { if (!params.hideToolbar) return; //Don't hide toolbar if search is open if (document.getElementById('row2').style.display != "none") return; var ypos = window.frames[0].frameElement.contentDocument.body.scrollTop; var thisdoc = document.getElementById('top'); //Immediately hide toolbar if not at top if (params.hideToolbar && ypos) { thisdoc.style.display = "none"; scrollbox.style.position = "fixed"; thisdoc.style.zIndex = "0"; } //As function runs on start of scroll, give 0.25s to find out if user has stopped scrolling if (typeof lastypos !== "undefined" && lastypos == ypos) { //We've stropped scrolling, do we need to re-enable? if (!ypos) { params.hideToolbar = false; checkToolbar(); params.hideToolbar = true; } } else { var wait = setTimeout(function () { clearTimeout(wait); hideToolbar(ypos); }, 250, ypos); } } } $(document).ready(function (e) { // Set initial behaviour (see also init.js) cssUIThemeGetOrSet(params.cssUITheme); //@TODO - this is initialization code, and should be in init.js (withoug jQuery) $('input:radio[name=cssInjectionMode]').filter('[value="' + params.cssSource + '"]').prop('checked', true); //DEV this hides file selectors if it is a packaged file -- add your own packaged file test to regex below if (/wikivoyage|medicine/i.test(params.fileVersion)) { document.getElementById('packagedAppFileSelectors').style.display = "block"; document.getElementById('hideFileSelectors').style.display = "none"; //document.getElementById('downloadLinksText').style.display = "none"; if (params.showFileSelectors) { document.getElementById('hideFileSelectors').style.display = "block"; document.getElementById('downloadLinksText').style.display = "inline"; } } //Code below triggers display of modal info box if app is run for the first time, or it has been upgraded to new version if (cookies.getItem('version') != params.version) { firstRun = true; $('#myModal').modal({ backdrop: "static" }); cookies.setItem('version', params.version, Infinity); } }); /** * Displays or refreshes the API status shown to the user */ function refreshAPIStatus() { var apiStatusPanel = document.getElementById('apiStatusDiv'); apiStatusPanel.classList.remove('panel-success', 'panel-warning'); var apiPanelClass = 'panel-success'; if (isMessageChannelAvailable()) { $('#messageChannelStatus').html("MessageChannel API available"); $('#messageChannelStatus').removeClass("apiAvailable apiUnavailable") .addClass("apiAvailable"); } else { apiPanelClass = 'panel-warning'; $('#messageChannelStatus').html("MessageChannel API unavailable"); $('#messageChannelStatus').removeClass("apiAvailable apiUnavailable") .addClass("apiUnavailable"); } if (isServiceWorkerAvailable()) { if (isServiceWorkerReady()) { $('#serviceWorkerStatus').html("ServiceWorker API available, and registered"); $('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable") .addClass("apiAvailable"); } else { apiPanelClass = 'panel-warning'; $('#serviceWorkerStatus').html("ServiceWorker API available, but not registered"); $('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable") .addClass("apiUnavailable"); } } else { apiPanelClass = 'panel-warning'; $('#serviceWorkerStatus').html("ServiceWorker API unavailable"); $('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable") .addClass("apiUnavailable"); } apiStatusPanel.classList.add(apiPanelClass); } var keepAliveServiceWorkerHandle; /** * Send an 'init' message to the ServiceWorker with a new MessageChannel * to initialize it, or to keep it alive. * This MessageChannel allows a 2-way communication between the ServiceWorker * and the application */ function initOrKeepAliveServiceWorker() { if (params.contentInjectionMode === 'serviceworker') { // Create a new messageChannel var tmpMessageChannel = new MessageChannel(); tmpMessageChannel.port1.onmessage = handleMessageChannelMessage; // Send the init message to the ServiceWorker, with this MessageChannel as a parameter navigator.serviceWorker.controller.postMessage({'action': 'init'}, [tmpMessageChannel.port2]); messageChannel = tmpMessageChannel; // Schedule to do it again regularly to keep the 2-way communication alive. // See https://github.com/kiwix/kiwix-js/issues/145 to understand why clearTimeout(keepAliveServiceWorkerHandle); keepAliveServiceWorkerHandle = setTimeout(initOrKeepAliveServiceWorker, DELAY_BETWEEN_KEEPALIVE_SERVICEWORKER, false); } } /** * Sets the given injection mode. * This involves registering (or re-enabling) the Service Worker if necessary * It also refreshes the API status for the user afterwards. * * @param {String} value The chosen content injection mode : 'jquery' or 'serviceworker' */ function setContentInjectionMode(value) { if (value === 'jquery') { if (isServiceWorkerReady()) { // We need to disable the ServiceWorker // Unregistering it does not seem to work as expected : the ServiceWorker // is indeed unregistered but still active... // So we have to disable it manually (even if it's still registered and active) navigator.serviceWorker.controller.postMessage({'action': 'disable'}); messageChannel = null; } refreshAPIStatus(); } else if (value === 'serviceworker') { if (!isServiceWorkerAvailable()) { uiUtil.systemAlert("The ServiceWorker API is not available on your device. Falling back to JQuery mode"); setContentInjectionMode('jquery'); return; } if (!isMessageChannelAvailable()) { uiUtil.systemAlert("The MessageChannel API is not available on your device. Falling back to JQuery mode"); setContentInjectionMode('jquery'); return; } if (!isServiceWorkerReady()) { $('#serviceWorkerStatus').html("ServiceWorker API available : trying to register it..."); navigator.serviceWorker.register('../service-worker.js').then(function (reg) { // The ServiceWorker is registered serviceWorkerRegistration = reg; refreshAPIStatus(); // We need to wait for the ServiceWorker to be activated // before sending the first init message var serviceWorker = reg.installing || reg.waiting || reg.active; serviceWorker.addEventListener('statechange', function (statechangeevent) { if (statechangeevent.target.state === 'activated') { // Remove any jQuery hooks from a previous jQuery session $('#articleContent').contents().remove(); // Create the MessageChannel // and send the 'init' message to the ServiceWorker initOrKeepAliveServiceWorker(); } }); if (serviceWorker.state === 'activated') { // Even if the ServiceWorker is already activated, // We need to re-create the MessageChannel // and send the 'init' message to the ServiceWorker // in case it has been stopped and lost its context initOrKeepAliveServiceWorker(); } }, function (err) { console.error('error while registering serviceWorker', err); refreshAPIStatus(); var message = "The ServiceWorker could not be properly registered. Switching back to jQuery mode. Error message : " + err; var protocol = window.location.protocol; if (protocol === 'moz-extension:') { message += "\n\nYou seem to be using kiwix-js through a Firefox extension : ServiceWorkers are disabled by Mozilla in extensions."; message += "\nPlease vote for https://bugzilla.mozilla.org/show_bug.cgi?id=1344561 so that some future Firefox versions support it"; } else if (protocol === 'file:') { message += "\n\nYou seem to be opening kiwix-js with the file:// protocol. You should open it through a web server : either through a local one (http://localhost/...) or through a remote one (but you need SSL : https://webserver/...)"; } uiUtil.systemAlert(message); setContentInjectionMode("jquery"); return; }); } else { // We need to set this variable earlier else the ServiceWorker does not get reactivated params.contentInjectionMode = value; initOrKeepAliveServiceWorker(); } } $('input:radio[name=contentInjectionMode]').prop('checked', false); $('input:radio[name=contentInjectionMode]').filter('[value="' + value + '"]').prop('checked', true); params.contentInjectionMode = value; // Save the value in a cookie, so that to be able to keep it after a reload/restart cookies.setItem('lastContentInjectionMode', value, Infinity); } // At launch, we try to set the last content injection mode (stored in a cookie) var lastContentInjectionMode = cookies.getItem('lastContentInjectionMode'); if (lastContentInjectionMode) { setContentInjectionMode(lastContentInjectionMode); } else { setContentInjectionMode('jquery'); } var serviceWorkerRegistration = null; /** * Tells if the ServiceWorker API is available * https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker * @returns {Boolean} */ function isServiceWorkerAvailable() { return 'serviceWorker' in navigator; } /** * Tells if the MessageChannel API is available * https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel * @returns {Boolean} */ function isMessageChannelAvailable() { try { var dummyMessageChannel = new MessageChannel(); if (dummyMessageChannel) return true; } catch (e) { return false; } return false; } /** * Tells if the ServiceWorker is registered, and ready to capture HTTP requests * and inject content in articles. * @returns {Boolean} */ function isServiceWorkerReady() { // Return true if the serviceWorkerRegistration is not null and not undefined return serviceWorkerRegistration; } /** * * @type Array. */ var storages = []; function searchForArchivesInPreferencesOrStorage() { // First see if the list of archives is stored in the cookie var listOfArchivesFromCookie = cookies.getItem("listOfArchives"); if (listOfArchivesFromCookie !== null && listOfArchivesFromCookie !== undefined && listOfArchivesFromCookie !== "") { var directories = listOfArchivesFromCookie.split('|'); populateDropDownListOfArchives(directories); } else { if (storages.length || params.localStorage) { searchForArchivesInStorage(); } else { displayFileSelect(); if (document.getElementById('archiveFiles').files && document.getElementById('archiveFiles').files.length > 0) { // Archive files are already selected, setLocalArchiveFromFileSelect(); } else { document.getElementById('btnConfigure').click(); } } } } function searchForArchivesInStorage() { // If DeviceStorage is available, we look for archives in it document.getElementById('btnConfigure').click(); $('#scanningForArchives').show(); if (params.localStorage && typeof Windows !== 'undefined' && typeof Windows.Storage !== 'undefined') { scanUWPFolderforArchives(params.localStorage); } else { zimArchiveLoader.scanForArchives(storages, populateDropDownListOfArchives); } } // @STORAGE AUTOLOAD STARTS HERE if ($.isFunction(navigator.getDeviceStorages)) { // The method getDeviceStorages is available (FxOS>=1.1) storages = $.map(navigator.getDeviceStorages("sdcard"), function (s) { return new abstractFilesystemAccess.StorageFirefoxOS(s); }); } if (storages !== null && storages.length > 0 || typeof Windows !== 'undefined' && typeof Windows.Storage !== 'undefined') { // Make a fake first access to device storage, in order to ask the user for confirmation if necessary. // This way, it is only done once at this moment, instead of being done several times in callbacks // After that, we can start looking for archives //storages[0].get("fake-file-to-read").then(searchForArchivesInPreferencesOrStorage, if (!params.pickedFile) { searchForArchivesInPreferencesOrStorage(); } else { processPickedFileUWP(params.pickedFile); } } else { // If DeviceStorage is not available, we display the file select components displayFileSelect(); if (document.getElementById('archiveFiles').files && document.getElementById('archiveFiles').files.length > 0) { // Archive files are already selected, setLocalArchiveFromFileSelect(); } else { document.getElementById('btnConfigure').click(); } } // Display the article when the user goes back in the browser history window.onpopstate = function (event) { if (event.state) { var title = event.state.title; var titleSearch = event.state.titleSearch; $('#prefix').val(""); $("#welcomeText").hide(); if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) { $('#navbarToggle').click(); } $('#searchingArticles').hide(); $('#configuration').hide(); $('#articleListWithHeader').hide(); $('#articleContent').contents().empty(); if (title && !("" === title)) { goToArticle(title); } else if (titleSearch && !("" === titleSearch)) { $('#prefix').val(titleSearch); searchDirEntriesFromPrefix($('#prefix').val()); } } }; /** * Populate the drop-down list of archives with the given list * @param {Array.} archiveDirectories */ function populateDropDownListOfArchives(archiveDirectories) { $('#scanningForArchives').hide(); $('#chooseArchiveFromLocalStorage').show(); document.getElementById('rescanStorage').style.display = params.rescan ? "none" : "block"; document.getElementById('openLocalFiles').style.display = params.rescan ? "block" : "none"; var comboArchiveList = document.getElementById('archiveList'); comboArchiveList.options.length = 0; for (var i = 0; i < archiveDirectories.length; i++) { var archiveDirectory = archiveDirectories[i]; if (archiveDirectory === "/") { uiUtil.systemAlert("It looks like you have put some archive files at the root of your sdcard (or internal storage). Please move them in a subdirectory"); } else { comboArchiveList.options[i] = new Option(archiveDirectory, archiveDirectory); } } // Store the list of archives in a cookie, to avoid rescanning at each start cookies.setItem("listOfArchives", archiveDirectories.join('|'), Infinity); comboArchiveList.size = comboArchiveList.length; //Kiwix-Js-Windows #23 - remove dropdown caret if only one archive if (comboArchiveList.length > 1) comboArchiveList.removeAttribute("multiple"); if (comboArchiveList.length == 1) comboArchiveList.setAttribute("multiple", "1"); if (comboArchiveList.options.length > 0) { var plural = comboArchiveList.length > 1 ? "s" : ""; document.getElementById('archiveNumber').innerHTML = '' + comboArchiveList.length + ' Archive' + plural + ' found in selected location (tap "Select storage" to change)'; // If we're doing a rescan, then don't attempt to jump to the last selected archive, but leave selectors open var lastSelectedArchive = params.rescan ? '' : params.storedFile; if (lastSelectedArchive !== null && lastSelectedArchive !== undefined && lastSelectedArchive !== "") { // || comboArchiveList.options.length == 1 // Either we have previously chosen a file, // or there is only one file // Attempt to select the corresponding item in the list, if it exists var success = false; if ($("#archiveList option[value='" + lastSelectedArchive + "']").length > 0) { $("#archiveList").val(lastSelectedArchive); success = true; cookies.setItem("lastSelectedArchive", lastSelectedArchive, Infinity); } // Set the localArchive as the last selected (if none has been selected previously, wait for user input) //if (success || comboArchiveList.options.length == 1) { if (success) { setLocalArchiveFromArchiveList(); } else { // We can't find lastSelectedArchive in the archive list, so let's ask the user to pick it //uiUtil.systemAlert(lastSelectedArchive + ' could not be found in the known list of archives!'); document.getElementById('alert-content').innerHTML = '

We could not find the archive ' + lastSelectedArchive + '!

Please select its location...

'; $('#alertModal').off('hide.bs.modal'); $('#alertModal').on('hide.bs.modal', function () { displayFileSelect(); }); $('#alertModal').modal({ backdrop: 'static', keyboard: true }); if (document.getElementById('configuration').style.display == 'none') document.getElementById('btnConfigure').click(); } } } else { uiUtil.systemAlert("Welcome to Kiwix! This application needs at least a ZIM file in your SD-card (or internal storage). Please download one and put it on the device (see About section). Also check that your device is not connected to a computer through USB device storage (which often locks the SD-card content)"); $("#btnAbout").click(); var isAndroid = navigator.userAgent.indexOf("Android") !== -1; if (isAndroid) { alert("You seem to be using an Android device. Be aware that there is a bug on Firefox, that prevents finding Wikipedia archives in a SD-card (at least on some devices. See about section). Please put the archive in the internal storage if the application can't find it."); } } } /** * Sets the localArchive from the selected archive in the drop-down list */ function setLocalArchiveFromArchiveList(archiveDirectory) { params.rescan = false; archiveDirectory = archiveDirectory || $('#archiveList').val(); if (archiveDirectory && archiveDirectory.length > 0) { // Now, try to find which DeviceStorage has been selected by the user // It is the prefix of the archive directory var regexpStorageName = /^\/([^\/]+)\//; var regexpResults = regexpStorageName.exec(archiveDirectory); var selectedStorage = null; if (regexpResults && regexpResults.length > 0) { var selectedStorageName = regexpResults[1]; for (var i = 0; i < storages.length; i++) { var storage = storages[i]; if (selectedStorageName === storage.storageName) { // We found the selected storage selectedStorage = storage; } } if (selectedStorage === null) { uiUtil.systemAlert("Unable to find which device storage corresponds to directory " + archiveDirectory); } } else { // This happens when the archiveDirectory is not prefixed by the name of the storage // (in the Simulator, or with FxOs 1.0, or probably on devices that only have one device storage) // In this case, we use the first storage of the list (there should be only one) if (storages.length === 1) { selectedStorage = storages[0]; } else { //IT'S NOT FREAKIN FFOS!!!!!!!!!! //Patched for UWP support: if (!params.pickedFile && params.pickedFolder && typeof MSApp !== 'undefined') { var query = params.pickedFolder.createFileQuery(); query.getFilesAsync().done(function (files) { var file; if (files) { for (var i = 0; i < files.length; i++) { if (files[i].name == archiveDirectory) { file = files[i]; break; } } if (file) { var fileset = []; if (/\.zim\w\w$/i.test(file.name)) { var genericFileName = file.name.replace(/(.*)\.zim\w\w$/i, "$1"); var testFileName = new RegExp(genericFileName + '\\.zim\\w\\w$'); for (var i = 0; i < files.length; i++) { if (testFileName.test(files[i].name)) { //This converts a UWP storage file object into a standard JavaScript web file object fileset.push(MSApp.createFileFromStorageFile(files[i])); } } } else { //This converts a UWP storage file object into a standard JavaScript web file object fileset = [MSApp.createFileFromStorageFile(file)]; } } } if (fileset && fileset.length) { setLocalArchiveFromFileList(fileset); } else { console.error("The picked file could not be found in the selected folder!"); var archiveList = []; for (var i = 0; i < files.length; i++) { if (/\.zima?a?$/i.test(files[i].name)) { archiveList.push(files[i].name); } } populateDropDownListOfArchives(archiveList); document.getElementById('btnConfigure').click(); } }); return; } else { //Check if user previously picked a specific file rather than a folder if (params.pickedFile && typeof MSApp !== 'undefined') { try { selectedStorage = MSApp.createFileFromStorageFile(params.pickedFile); setLocalArchiveFromFileList([selectedStorage]); return; } catch (err){ // Probably user has moved or deleted the previously selected file uiUtil.systemAlert("The previously picked archive can no longer be found!"); console.error("Picked archive not found: " + err); } } } //There was no picked file or folder, so we'll try setting the default localStorage //if (!params.pickedFolder) { //This gets called, for example, if the picked folder or picked file are in FutureAccessList but now are //no longer accessible. There will be a (handled) error in cosole log, and params.pickedFolder and params.pickedFile will be blank params.rescan = true; if (params.localStorage) { scanUWPFolderforArchives(params.localStorage); } else { document.getElementById('btnConfigure').click(); } return; //} } } // Reset the cssDirEntryCache and cssBlobCache. Must be done when archive changes. if (cssBlobCache) cssBlobCache = new Map(); //if (cssDirEntryCache) // cssDirEntryCache = new Map(); state.selectedArchive = zimArchiveLoader.loadArchiveFromDeviceStorage(selectedStorage, archiveDirectory, function (archive) { cookies.setItem("lastSelectedArchive", archiveDirectory, Infinity); // The archive is set : go back to home page to start searching if (params.rescan) { document.getElementById('btnConfigure').click(); params.rescan = false; } else { $('#openLocalFiles').hide(); document.getElementById('btnHome').click(); } }); } } /** * Displays the zone to select files from the archive */ function displayFileSelect() { document.getElementById('openLocalFiles').style.display = 'block'; // Set the main drop zone scrollBoxDropZone.addEventListener('dragover', handleGlobalDragover); scrollBoxDropZone.addEventListener('dragleave', function(e) { configDropZone.style.border = ''; }); // Also set a global drop zone (allows us to ensure Config is always displayed for the file drop) globalDropZone.addEventListener('dragover', function (e) { e.preventDefault(); if (configDropZone.style.display === 'none') document.getElementById('btnConfigure').click(); e.dataTransfer.dropEffect = 'link'; }); globalDropZone.addEventListener('drop', handleFileDrop); // This handles use of the file picker document.getElementById('archiveFiles').addEventListener('change', setLocalArchiveFromFileSelect); } function handleGlobalDragover(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'link'; configDropZone.style.border = '3px dotted red'; } function handleIframeDragover(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'link'; document.getElementById('btnConfigure').click(); } function handleIframeDrop(e) { e.stopPropagation(); e.preventDefault(); return; } function handleFileDrop(packet) { packet.stopPropagation(); packet.preventDefault(); configDropZone.style.border = ''; var files = packet.dataTransfer.files; document.getElementById('openLocalFiles').style.display = 'none'; setLocalArchiveFromFileList(files); // This clears the display of any previously picked archive in the file selector document.getElementById('archiveFiles').value = null; } function pickFileUWP() { //Support UWP FilePicker [kiwix-js-windows #3] // Create the picker object and set options var filePicker = new Windows.Storage.Pickers.FileOpenPicker; filePicker.suggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.downloads; // Filter folder contents filePicker.fileTypeFilter.replaceAll([".zim"]); filePicker.pickSingleFileAsync().then(processPickedFileUWP); } function processPickedFileUWP(file) { if (file) { // Cache file so the contents can be accessed at a later time Windows.Storage.AccessCache.StorageApplicationPermissions.futureAccessList.addOrReplace(params.falFileToken, file); params.pickedFile = file; if (params.pickedFolder) Windows.Storage.AccessCache.StorageApplicationPermissions.futureAccessList.remove(params.falFolderToken); params.pickedFolder = ""; cookies.setItem("lastSelectedArchive", file.name, Infinity); params.storedFile = file.name; // Since we've explicitly picked a file, we should jump to it params.rescan = false; document.getElementById('openLocalFiles').style.display = "none"; populateDropDownListOfArchives([file.name]); } else { // The picker was dismissed with no selected file console.log("User closed folder picker without picking a file"); } } function pickFolderUWP() { //Support UWP FilePicker [kiwix-js-windows #3] var folderPicker = new Windows.Storage.Pickers.FolderPicker; folderPicker.suggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.downloads; folderPicker.fileTypeFilter.replaceAll([".zim", ".dat", ".idx", ".txt", ".zimaa"]); folderPicker.pickSingleFolderAsync().done(function (folder) { if (folder) { scanUWPFolderforArchives(folder); } }); } function scanUWPFolderforArchives(folder) { if (folder) { // Application now has read/write access to all contents in the picked folder (including sub-folder contents) // Cache folder so the contents can be accessed at a later time Windows.Storage.AccessCache.StorageApplicationPermissions.futureAccessList.addOrReplace(params.falFolderToken, folder); params.pickedFolder = folder; // Query the folder. var query = folder.createFileQuery(); query.getFilesAsync().done(function (files) { // Display file list var archiveDisplay = document.getElementById('chooseArchiveFromLocalStorage'); if (files) { var archiveList = []; files.forEach(function (file) { if (file.fileType == ".zim" || file.fileType == ".zimaa") { archiveList.push(file.name); } }); if (archiveList.length) { document.getElementById('noZIMFound').style.display = "none"; populateDropDownListOfArchives(archiveList); return; } } archiveDisplay.style.display = "inline"; document.getElementById('noZIMFound').style.display = "block"; document.getElementById('openLocalFiles').style.display = "none"; document.getElementById('rescanStorage').style.display = "block"; document.getElementById('archiveList').options.length = 0; document.getElementById('archiveList').size = 0; document.getElementById('archiveNumber').innerHTML = '0 Archives found in local storage (tap "Select storage" to select an archive location)'; params.pickedFolder = ""; Windows.Storage.AccessCache.StorageApplicationPermissions.futureAccessList.remove(params.falFolderToken); return; }); } else { // The picker was dismissed with no selected file console.log("User closed folder picker without picking a file"); } } function setLocalArchiveFromFileList(files) { // Check for usable file types for (var i = files.length; i--;) { // DEV: you can support other file types by adding (e.g.) '|dat|idx' after 'zim\w{0,2}' if (!/\.(?:zim\w{0,2})$/i.test(files[i].name)) { uiUtil.systemAlert("One or more files does not appear to be a ZIM file!"); return; } } // Check that user hasn't picked just part of split ZIM if (files.length == 1 && /\.zim\w\w/i.test(files[0].name)) { document.getElementById('alert-content').innerHTML = '

You have picked only part of a split archive!

Please select its folder in Config, or drag and drop all of its parts into Config.

'; $('#alertModal').off('hide.bs.modal'); $('#alertModal').on('hide.bs.modal', function () { if (document.getElementById('configuration').style.display == 'none') document.getElementById('btnConfigure').click(); displayFileSelect(); }); $('#alertModal').modal({ backdrop: 'static', keyboard: true }); } // If the file name is already in the archive list, try to select it in the list var listOfArchives = document.getElementById('archiveList'); if (listOfArchives) listOfArchives.value = files[0].name; // Reset the cssDirEntryCache and cssBlobCache. Must be done when archive changes. if (cssBlobCache) cssBlobCache = new Map(); //if (cssDirEntryCache) // cssDirEntryCache = new Map(); state.selectedArchive = zimArchiveLoader.loadArchiveFromFiles(files, function (archive) { // The archive is set : go back to home page to start searching params.storedFile = archive._file._files[0].name; cookies.setItem("lastSelectedArchive", params.storedFile, Infinity); var reloadLink = document.getElementById("reloadPackagedArchive"); if (reloadLink) { if (params.packagedFile != params.storedFile) { reloadLink.style.display = "inline"; reloadLink.removeEventListener("click", loadPackagedArchive); reloadLink.addEventListener("click", loadPackagedArchive); document.getElementById("moreInfo").style.display = "none"; } else { reloadLink.style.display = "none"; document.getElementById('currentArchive').style.display = "none"; document.getElementById("moreInfo").style.display = "inline"; } } //This ensures the correct icon is set for the newly loaded archive cssUIThemeGetOrSet(params.cssUITheme); if (params.rescan) { document.getElementById('btnConfigure').click(); document.getElementById('btnConfigure').click(); params.rescan = false; } else { $('#openLocalFiles').hide(); if (params.rememberLastPage && ~params.lastPageVisit.indexOf(params.storedFile)) { var lastPage = decodeURIComponent(params.lastPageVisit.replace(/@kiwixKey@.+/, "")); goToArticle(lastPage); } else { // The archive has changed, so we must blank the last page in case the Home page of the new archive // has the same title as the previous archive (possible if it is, for example, "index") params.lastPageVisit = ""; document.getElementById('btnHome').click(); } } }); } function loadPackagedArchive() { // Reload any ZIM files in local storage (whcih the user can't otherwise select with the filepicker) if (params.localStorage) { params.storedFile = params.packagedFile || ''; params.pickedFolder = params.localStorage; scanUWPFolderforArchives(params.localStorage); if (!params.rescan) setLocalArchiveFromArchiveList(params.storedFile); } } /** * Sets the localArchive from the File selects populated by user */ function setLocalArchiveFromFileSelect() { setLocalArchiveFromFileList(document.getElementById('archiveFilesLegacy').files); } /** * Reads a remote archive with given URL, and returns the response in a Promise. * This function is used by setRemoteArchives below, for UI tests * * @param url The URL of the archive to read * @returns {Promise} */ function readRemoteArchive(url) { var deferred = q.defer(); var request = new XMLHttpRequest(); request.open("GET", url, true); request.responseType = "blob"; request.onreadystatechange = function () { if (request.readyState === XMLHttpRequest.DONE) { if ((request.status >= 200 && request.status < 300) || request.status === 0) { // Hack to make this look similar to a file request.response.name = url; deferred.resolve(request.response); } else { deferred.reject("HTTP status " + request.status + " when reading " + url); } } }; request.onabort = function (e) { deferred.reject(e); }; request.send(null); return deferred.promise; } /** * This is used in the testing interface to inject remote archives */ window.setRemoteArchives = function () { var readRequests = []; var i; for (i = 0; i < arguments.length; i++) { readRequests[i] = readRemoteArchive(arguments[i]); } return q.all(readRequests).then(function (arrayOfArchives) { setLocalArchiveFromFileList(arrayOfArchives); }); }; /** * Handle key input in the prefix input zone * @param {Event} evt */ function onKeyUpPrefix(evt) { // Use a timeout, so that very quick typing does not cause a lot of overhead // It is also necessary for the words suggestions to work inside Firefox OS if (window.timeoutKeyUpPrefix) { window.clearTimeout(window.timeoutKeyUpPrefix); } window.timeoutKeyUpPrefix = window.setTimeout(function () { var prefix = $("#prefix").val(); if (prefix && prefix.length > 0) { document.getElementById('searchArticles').click(); } }, 500); } /** * Search the index for DirEntries with title that start with the given prefix (implemented * with a binary search inside the index file) * @param {String} prefix */ function searchDirEntriesFromPrefix(prefix) { if (state.selectedArchive !== null && state.selectedArchive.isReady()) { $('#activeContent').alert('close'); if (!prefix || /^\s/.test(prefix)) { var sel = prefix ? prefix.replace(/^\s(.*)/, '$1') : ''; if (sel.length) { sel = sel.replace(/^(.)(.*)/, function(p0, p1, p2) { return p1.toUpperCase() + p2; }); } showZIMIndex(null, sel); } else { state.selectedArchive.findDirEntriesWithPrefix(prefix.trim(), MAX_SEARCH_RESULT_SIZE, populateListOfArticles); } } else { $('#searchingArticles').hide(); // We have to remove the focus from the search field, // so that the keyboard does not stay above the message $('#searchArticles').focus(); uiUtil.systemAlert("Archive not set : please select an archive"); document.getElementById('btnConfigure').click(); } } /** * Extracts and displays in htmlArticle the first MAX_SEARCH_RESULT_SIZE articles beginning with start * @param {String} start Optional index number to begin the list with * @param {String} prefix Optional search prefix from which to start an alphabetical search */ function showZIMIndex(start, prefix) { // If we're searching by title index number (other than 0 or null), we should ignore any prefix if (isNaN(start)) { prefix = prefix || ''; } else { prefix = start > 0 ? '' : prefix; } if (state.selectedArchive !== null && state.selectedArchive.isReady()) { state.selectedArchive.findDirEntriesWithPrefixCaseSensitive(prefix, MAX_SEARCH_RESULT_SIZE, function(dirEntryArray, nextStart) { var docBody = document.getElementById('largeModal'); var newHtml = ""; for (var i = 0; i < dirEntryArray.length; i++) { var dirEntry = dirEntryArray[i]; newHtml += "\n" + (dirEntry.getTitleOrUrl()) + ""; } start = start ? start : 0; var back = start ? '<< Previous ' + MAX_SEARCH_RESULT_SIZE + '' : ''; var next = dirEntryArray.length === MAX_SEARCH_RESULT_SIZE ? 'Next ' + MAX_SEARCH_RESULT_SIZE + ' >>' : ''; var backNext = back ? next ? back + ' | ' + next : back : next; backNext = '
' + backNext + '
\n'; var alphaSelector = []; // Set up the alphabetic selector var lower = params.alphaChar.charCodeAt(); var upper = params.omegaChar.charCodeAt(); if (upper <= lower) { alphaSelector.push('PLEASE SELECT VALID START AND END ALPHABET CHARACTERS IN CONFIGURATION'); } else { for (i = lower; i <= upper; i++) { var char = String.fromCharCode(i); alphaSelector.push('' + char + ''); } } // Add selectors for diacritics, etc. for Roman alphabet if (params.alphaChar === 'A' && params.omegaChar == 'Z') { alphaSelector.push('¡¿ÀÑ'); alphaSelector.unshift('!#123'); // Add way of selecting a non-Roman alphabet var switchAlphaButton = document.getElementById('extraModalFooterContent'); // Don't re-add button and event listeners if they already exist if (!/button/.test(switchAlphaButton.innerHTML)) { switchAlphaButton.innerHTML = ''; switchAlphaButton.addEventListener('click', function () { var alphaLabel = document.getElementById('alphaCharTxt').parentNode; alphaLabel.style.borderColor = 'red'; alphaLabel.style.borderStyle = 'solid'; alphaLabel.addEventListener('mousedown', function () { this.style.borderColor = ''; this.style.borderStyle = ''; }); $('#myModal').modal('hide'); document.getElementById('btnConfigure').click(); window.location.href = "#displaySettingsDiv"; }); } } // Add diacritics for Greek alphabet if (params.alphaChar === 'Α' && params.omegaChar == 'Ω') { alphaSelector.push('ΪΫά'); alphaSelector.unshift('ΆΈΉ'); } var alphaString = '
[ ' + alphaSelector.join(' | \n') + ' ]
\n'; var closeButton = ''; docBody.innerHTML = closeButton + '
\n

\n' + alphaString + '
' + backNext + '
\n' + '

ZIM Archive Index

\n' + '
' + newHtml + '\n
\n' + '
\n' + backNext + '

' + alphaString + '
\n'; var indexEntries = docBody.querySelectorAll('.list-group-item'); $(indexEntries).on('click', function (event) { $("#myModal").modal('hide'); handleTitleClick(event); return false; }); var continueAnchors = docBody.querySelectorAll('.continueAnchor'); $(continueAnchors).on('click', function(e) { document.getElementById('prefix').value = ''; var start = ~~this.dataset.start; showZIMIndex(start); return false; }); alphaSelector = docBody.querySelectorAll('.alphaSelector'); $(alphaSelector).on('click', function(e) { var char = this.dataset.sel; document.getElementById('prefix').value = ' ' + char; showZIMIndex(null, char); return false; }); $('#searchingArticles').hide(); $('#articleListWithHeader').hide(); var modalTheme = document.getElementById('modalTheme'); modalTheme.classList.remove('dark'); var determinedTheme = params.cssUITheme == 'auto' ? cssUIThemeGetOrSet('auto', true) : params.cssUITheme; if (determinedTheme === 'dark') modalTheme.classList.add('dark'); $('#myModal').modal({ backdrop: "static" }); //if ($('#prefix').is(":focus")) { // // This removes the focus from prefix // document.getElementById('findText').click(); // document.getElementById('findText').click(); //} }, start); } } /** * Display the list of articles with the given array of DirEntry * @param {Array} dirEntryArray The array of dirEntries returned from the binary search */ function populateListOfArticles(dirEntryArray) { var articleListHeaderMessageDiv = $('#articleListHeaderMessage'); var nbDirEntry = dirEntryArray ? dirEntryArray.length : 0; var message; if (nbDirEntry >= MAX_SEARCH_RESULT_SIZE) { message = 'First ' + MAX_SEARCH_RESULT_SIZE + ' articles below (refine your search).'; } else { message = nbDirEntry + ' articles found.'; } if (nbDirEntry === 0) { message = 'No articles found.'; } articleListHeaderMessageDiv.html(message); var articleListDiv = document.getElementById('articleList'); var articleListDivHtml = ''; var listLength = dirEntryArray.length < MAX_SEARCH_RESULT_SIZE ? dirEntryArray.length : MAX_SEARCH_RESULT_SIZE; for (var i = 0; i < listLength; i++) { var dirEntry = dirEntryArray[i]; articleListDivHtml += '' + dirEntry.getTitleOrUrl() + ''; } articleListDiv.innerHTML = articleListDivHtml; // Needed so that results show on top of article document.getElementById('articleContent').style.position = 'static'; // We have to use mousedown below instead of click as otherwise the prefix blur event fires first // and prevents this event from firing; note that touch also triggers mousedown $('#articleList a').on('mousedown', function (e) { handleTitleClick(e); return false; }); $('#searchingArticles').hide(); $('#articleListWithHeader').show(); } /** * Handles the click on the title of an article in search results * @param {Event} event * @returns {Boolean} */ function handleTitleClick(event) { var dirEntryId = event.target.getAttribute("dirEntryId"); findDirEntryFromDirEntryIdAndLaunchArticleRead(dirEntryId); return false; } /** * Creates an instance of DirEntry from given dirEntryId (including resolving redirects), * and call the function to read the corresponding article * @param {String} dirEntryId */ function findDirEntryFromDirEntryIdAndLaunchArticleRead(dirEntryId) { if (state.selectedArchive.isReady()) { var dirEntry = state.selectedArchive.parseDirEntryId(dirEntryId); // Remove focus from search field to hide keyboard and to allow navigation keys to be used document.getElementById('articleContent').contentWindow.focus(); $("#searchingArticles").show(); if (dirEntry.isRedirect()) { state.selectedArchive.resolveRedirect(dirEntry, readArticle); } else { params.isLandingPage = false; readArticle(dirEntry); } } else { uiUtil.systemAlert('Data files not set'); } } /** * Read the article corresponding to the given dirEntry * @param {DirEntry} dirEntry The directory entry of the article to read */ function readArticle(dirEntry) { if (params.contentInjectionMode === 'serviceworker') { // In ServiceWorker mode, we simply set the iframe src. // (reading the backend is handled by the ServiceWorker itself) // We will need the encoded URL on article load so that we can set the iframe's src correctly, // but we must not encode the '/' character or else relative links may fail [kiwix-js #498] var encodedUrl = dirEntry.url.replace(/[^/]+/g, function (matchedSubstring) { return encodeURIComponent(matchedSubstring); }); var iframeArticleContent = document.getElementById('articleContent'); iframeArticleContent.onload = function () { // The iframe is empty, show spinner on load of landing page $("#searchingArticles").show(); $("#articleList").empty(); $('#articleListHeaderMessage').empty(); $('#articleListWithHeader').hide(); $("#prefix").val(""); iframeArticleContent.onload = function () { // The content is fully loaded by the browser : we can hide the spinner $("#searchingArticles").hide(); // Deflect drag-and-drop of ZIM file on the iframe to Config var doc = iframeArticleContent.contentDocument ? iframeArticleContent.contentDocument.documentElement : null; var docBody = doc ? doc.getElementsByTagName('body') : null; docBody = docBody ? docBody[0] : null; if (docBody) { docBody.addEventListener('dragover', handleIframeDragover); docBody.addEventListener('drop', handleIframeDrop); } if (/manual|progressive/.test(params.imageDisplayMode)) images.prepareImagesServiceWorker(); if (iframeArticleContent.contentWindow) iframeArticleContent.contentWindow.onunload = function () { $("#searchingArticles").show(); }; if (params.allowHTMLExtraction) { var determinedTheme = params.cssTheme == 'auto' ? cssUIThemeGetOrSet('auto') : params.cssTheme; uiUtil.insertBreakoutLink(determinedTheme); } }; // We put the ZIM filename as a prefix in the URL, so that browser caches are separate for each ZIM file iframeArticleContent.src = "../" + state.selectedArchive._file._files[0].name + "/" + dirEntry.namespace + "/" + encodedUrl; // Display the iframe content $("#articleContent").show(); }; iframeArticleContent.src = "article.html"; } else { // In jQuery mode, we read the article content in the backend and manually insert it in the iframe if (dirEntry.isRedirect()) { state.selectedArchive.resolveRedirect(dirEntry, readArticle); } else { //TESTING// console.log("Initiating HTML load..."); console.time("Time to HTML load"); console.log("Initiating Document Ready timer..."); console.time("Time to Document Ready"); //Set startup cookie to guard against boot loop //Cookie will signal failure until article is fully loaded document.cookie = 'lastPageLoad=failed;expires=Fri, 31 Dec 9999 23:59:59 GMT'; //Void the localSearch variable to prevent invalid DOM references remainining [kiwix-js-windows #56] localSearch = {}; //Load cached start page if it exists and we have loaded the packaged file var htmlContent = 0; if (params.cachedStartPage && ~state.selectedArchive._file._files[0].name.indexOf(params.packagedFile) && params.isLandingPage) { htmlContent = -1; // DEV: You should deal with the rare possibility that the cachedStartPage is not in the same namespace as the main page dirEntry... // Ideally include the namespace in params.cachedStartPage and adjust/test code (not hard) uiUtil.XHR(dirEntry.namespace + '/' + encodeURIComponent(params.cachedStartPage), 'text', function (responseTxt, status) { htmlContent = /]*>/.test(responseTxt) ? responseTxt : 0; if (htmlContent) { console.log('Article retrieved from storage cache...'); // Alter the dirEntry url and title parameters in case we are overriding the start page dirEntry.url = params.cachedStartPage; var title = htmlContent.match(/]*>((?:[^<]|<(?!\/title))+)/); dirEntry.title = title ? title[1] : dirEntry.title; displayArticleInForm(dirEntry, htmlContent); } else { document.getElementById('searchingArticles').style.display = 'block'; state.selectedArchive.readUtf8File(dirEntry, displayArticleInForm); } }); } //Load lastPageVisit if it is the currently requested page if (!htmlContent) { if (params.rememberLastPage && typeof Storage !== "undefined" && dirEntry.namespace + '/' + dirEntry.url == decodeURIComponent(params.lastPageVisit.replace(/@kiwixKey@.+/, ""))) { try { htmlContent = localStorage.getItem('lastPageHTML'); } catch (err) { console.log("localStorage not supported: " + err); } } if (/]*>/.test(htmlContent)) { console.log("Fast article retrieval from localStorage..."); setTimeout(function () { displayArticleInForm(dirEntry, htmlContent); }, 100); } else { state.selectedArchive.readUtf8File(dirEntry, displayArticleInForm); } } } } } var messageChannel; /** * Function that handles a message of the messageChannel. * It tries to read the content in the backend, and sends it back to the ServiceWorker * * @param {Event} event The event object of the message channel */ function handleMessageChannelMessage(event) { if (event.data.error) { console.error("Error in MessageChannel", event.data.error); reject(event.data.error); } else { // We received a message from the ServiceWorker if (event.data.action === "askForContent") { // The ServiceWorker asks for some content var title = event.data.title; var messagePort = event.ports[0]; var readFile = function (dirEntry) { if (dirEntry === null) { console.error("Title " + title + " not found in archive."); messagePort.postMessage({ 'action': 'giveContent', 'title': title, 'content': '' }); } else if (dirEntry.isRedirect()) { state.selectedArchive.resolveRedirect(dirEntry, function (resolvedDirEntry) { var redirectURL = resolvedDirEntry.namespace + "/" + resolvedDirEntry.url; // Ask the ServiceWork to send an HTTP redirect to the browser. // We could send the final content directly, but it is necessary to let the browser know in which directory it ends up. // Else, if the redirect URL is in a different directory than the original URL, // the relative links in the HTML content would fail. See #312 messagePort.postMessage({ 'action': 'sendRedirect', 'title': title, 'redirectUrl': redirectURL }); }); } else { // Let's read the content in the ZIM file state.selectedArchive.readBinaryFile(dirEntry, function (fileDirEntry, content) { var mimetype = fileDirEntry.getMimetype(); // Let's send the content to the ServiceWorker var message = { 'action': 'giveContent', 'title': title, 'mimetype': mimetype, 'imageDisplay': params.imageDisplayMode }; if (mimetype === 'text/html') { content = utf8.parse(content); // Add DOCTYPE to prevent quirks mode if missing (quirks mode prevents katex from running, and is incompatible with jQuery) message.content = !/^\s*(?:\n' + content : content; messagePort.postMessage(message); } else { message.content = content.buffer; messagePort.postMessage(message, [content.buffer]); } }); } }; state.selectedArchive.getDirEntryByTitle(title).then(readFile).fail(function () { messagePort.postMessage({ 'action': 'giveContent', 'title': title, 'content': new UInt8Array() }); }); } else { console.error("Invalid message received", event.data); } } } // Compile some regular expressions needed to modify links // Pattern to find the path in a url var regexpPath = /^(.*\/)[^\/]+$/; // Pattern to find a ZIM URL (with its namespace) - see https://wiki.openzim.org/wiki/ZIM_file_format#Namespaces var regexpZIMUrlWithNamespace = /^[.\/]*([-ABIJMUVWX]\/.+)$/; // Regex below finds images, scripts, and stylesheets with ZIM-type metadata and image namespaces [kiwix-js #378] // It first searches for ]*?\s)(?:src|href)(\s*=\s*["'])(?:\.\.\/|\/)+(?=[-IJ]\/)/ig; // Regex below tests the html of an article for active content [kiwix-js #466] // It inspects every