/**
* 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', 'cache', 'images', 'settingsStore', 'transformStyles', 'kiwixServe'],
function ($, zimArchiveLoader, uiUtil, util, cache, images, settingsStore, transformStyles, kiwixServe) {
/**
* 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;
// Define global state:
// Placeholders for the article container, the article window and the article DOM
var articleContainer = document.getElementById('articleContent');
var articleWindow = articleContainer.contentWindow;
var articleDocument;
/**
* @type ZIMArchive
*/
appstate.selectedArchive = null;
// An object to hold the current search and its state (allows cancellation of search across modules)
appstate['search'] = {
'prefix': '', // A field to hold the original search string
'status': '', // The status of the search: ''|'init'|'interim'|'cancelled'|'complete'
'type': '' // The type of the search: 'basic'|'full' (set automatically in search algorithm)
};
// A parameter to determine the Settings Store API in use (we need to nullify before testing
// because params.storeType is also set in a preliminary way in init.js)
params['storeType'] = null;
params['storeType'] = settingsStore.getBestAvailableStorageAPI();
// Test caching capability
cache.test(function(){});
// Unique identifier of the article expected to be displayed
var expectedArticleURLToBeDisplayed = "";
// Check if we have managed to switch to PWA mode (if running UWP app)
// DEV: we do this in init.js, but sometimes it doesn't seem to register, so we do it again once the app has fully launched
if (/UWP\|PWA/.test(params.appType) && /^http/i.test(window.location.protocol)) {
// We are in a PWA, so signal success
params.localUWPSettings.PWA_launch = 'success';
}
/**
* Resize the IFrame height, so that it fills the whole available height in the window
*/
function resizeIFrame() {
var scrollbox = document.getElementById('scrollbox');
var header = document.getElementById('top');
var iframe = document.getElementById('articleContent');
var navbarHeight = document.getElementById('navbar').getBoundingClientRect().height;
// Reset any hidden headers and footers and iframe shift
header.style.zIndex = 1;
header.style.transform = 'translateY(0)';
document.getElementById('footer').style.transform = 'translateY(0)';
iframe.style.transform = 'translateY(-1px)';
// iframe.style.height = window.innerHeight + 'px';
// DEV: if we set the iframe with clientHeight, then it takes into account any zoom
iframe.style.height = document.documentElement.clientHeight + 'px';
//Re-enable top-level scrolling
scrollbox.style.height = window.innerHeight - navbarHeight + 'px';
if (iframe.style.display !== "none" && document.getElementById("prefix") !== document.activeElement) {
scrollbox.style.height = 0;
}
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');
}
checkToolbar();
}
$(document).ready(function() {
resizeIFrame();
// uiUtil.initTouchZoom();
});
$(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
if (params.navButtonsPos === 'top') {
// User has requested navigation buttons should be at top, so we need to swap them
var btnBack = document.getElementById('btnBack');
var btnBackAlt = document.getElementById('btnBackAlt');
btnBack.id = 'btnBackAlt';
btnBackAlt.id = 'btnBack';
btnBackAlt.style.display = 'inline';
btnBack.style.display = 'none';
var btnForward = document.getElementById('btnForward');
var btnForwardAlt = document.getElementById('btnForwardAlt');
btnForward.id = 'btnForwardAlt';
btnForwardAlt.id = 'btnForward';
btnForwardAlt.style.display = 'inline';
btnForward.style.display = 'none';
var btnRandom = document.getElementById('btnRandomArticle');
var btnRandomAlt = document.getElementById('btnRandomArticleAlt');
btnRandom.id = 'btnRandomArticleAlt';
btnRandomAlt.id = 'btnRandomArticle';
btnRandom.style.display = 'none';
btnRandomAlt.style.display = 'inline';
}
var searchArticlesFocused = false;
document.getElementById('searchArticles').addEventListener('click', function () {
var prefix = document.getElementById('prefix').value;
// Do not initiate the same search if it is already in progress
if (appstate.search.prefix === prefix && !/^(cancelled|complete)$/.test(appstate.search.status)) return;
$("#welcomeText").hide();
$('.alert').hide();
$("#searchingArticles").show();
pushBrowserHistoryState(null, prefix);
// Initiate the search
searchDirEntriesFromPrefix(prefix);
clearFindInArticle();
//Re-enable top-level scrolling
document.getElementById('scrollbox').style.height = window.innerHeight - document.getElementById('top').getBoundingClientRect().height + 'px';
if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) {
$('#navbarToggle').click();
}
// This flag is set to true in the mousedown event below
searchArticlesFocused = false;
});
document.getElementById('formArticleSearch').addEventListener('submit', function () {
document.getElementById('searchArticles').click();
});
// Handle keyboard events in the prefix (article search) field
var keyPressHandled = false;
$('#prefix').on('keydown', function (e) {
// If user presses Escape...
// IE11 returns "Esc" and the other browsers "Escape"; regex below matches both
if (/^Esc/.test(e.key)) {
// Hide the article list
e.preventDefault();
e.stopPropagation();
$('#articleListWithHeader').hide();
$('#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(window, 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(window, 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 (appstate.selectedArchive !== null && appstate.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('scrollbox').style.position = 'absolute';
document.getElementById('scrollbox').style.height = window.innerHeight - document.getElementById('top').getBoundingClientRect().height + 'px';
});
// Hide the search results if user moves out of prefix field
document.getElementById('prefix').addEventListener('blur', function () {
if (!searchArticlesFocused) {
appstate.search.status = 'cancelled';
}
// We need to wait one tick for the activeElement to receive focus
setTimeout(function () {
if (!(/^articleList|searchSyntaxLink/.test(document.activeElement.id)
|| /^list-group/.test(document.activeElement.className))) {
document.getElementById('scrollbox').style.height = 0;
document.getElementById('articleListWithHeader').style.display = 'none';
appstate.tempPrefix = '';
document.getElementById('searchingArticles').style.display = 'none';
}
}, 1);
});
//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 = settingsStore.getItem('cssSource') || "auto";
params.cssTheme = settingsStore.getItem('cssTheme') || "light";
//params.contentInjectionMode = settingsStore.getItem('contentInjectionMode');
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();
switchCSSTheme();
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);
// if (typeof window.nw !== 'undefined' || typeof window.fs === 'undefined') {
window.frames[0].frameElement.contentWindow.print();
// } else {
// // We are in an Electron app and need to use export to browser to print
// params.preloadingAllImages = false;
// // Add a window.print() script to the html
// document.getElementById('articleContent').contentDocument.head.innerHTML +=
// '\n';
});
// Older math blocks
htmlArticle = htmlArticle.replace(/
]+?math-fallback-image)[^>]*?alt\s*=\s*(['"])((?:[^"']|(?!\1)[\s\S])+)[^>]+>/ig,
function (p0, p1, math) {
// Remove any rogue ampersands in MathJax due to double escaping (by Wikipedia)
math = math.replace(/&/g, '&');
// Change any mbox commands to fbox (because KaTeX doesn't support mbox)
math = math.replace(/mbox{/g, 'fbox{');
return '';
});
}
params.containsMathTex = params.useMathJax ? /