mirror of
https://github.com/kiwix/kiwix-js-pwa.git
synced 2025-08-03 19:38:36 -04:00

Former-commit-id: 304fb75c0d055ac1b43e614e67598d5ca3633aad [formerly 46c1a05f68c964b9a71f017fcdf24192dd476757] Former-commit-id: 6c5bc8226d12ca52a55f64741bee166785979187
1086 lines
49 KiB
JavaScript
1086 lines
49 KiB
JavaScript
/**
|
|
* 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 <http://www.gnu.org/licenses/>
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
// This uses require.js to structure javascript:
|
|
// http://requirejs.org/docs/api.html#define
|
|
|
|
define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies','abstractFilesystemAccess', 'module'],
|
|
function($, zimArchiveLoader, util, uiUtil, cookies, abstractFilesystemAccess, module) {
|
|
|
|
/**
|
|
* Maximum number of articles to display in a search
|
|
* @type Integer
|
|
*/
|
|
var MAX_SEARCH_RESULT_SIZE = module.config().results; //This is set in init.js
|
|
|
|
/**
|
|
* @type ZIMArchive
|
|
*/
|
|
var selectedArchive = null;
|
|
|
|
/**
|
|
* Resize the IFrame height, so that it fills the whole available height in the window
|
|
*/
|
|
function resizeIFrame() {
|
|
var height = $(window).outerHeight()
|
|
- $("#top").outerHeight(true)
|
|
- $("#articleListWithHeader").outerHeight(true)
|
|
// TODO : this 5 should be dynamically computed, and not hard-coded
|
|
- 5;
|
|
$(".articleIFrame").css("height", height + "px");
|
|
}
|
|
$(document).ready(resizeIFrame);
|
|
$(window).resize(resizeIFrame);
|
|
|
|
// Define behavior of HTML elements
|
|
$('#searchArticles').on('click', function(e) {
|
|
pushBrowserHistoryState(null, $('#prefix').val());
|
|
searchDirEntriesFromPrefix($('#prefix').val());
|
|
$("#welcomeText").hide();
|
|
$("#readingArticle").hide();
|
|
$("#articleContent").hide();
|
|
if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) {
|
|
$('#navbarToggle').click();
|
|
}
|
|
});
|
|
$('#formArticleSearch').on('submit', function(e) {
|
|
document.getElementById("searchArticles").click();
|
|
return false;
|
|
});
|
|
$('#prefix').on('keyup', function(e) {
|
|
if (selectedArchive !== null && selectedArchive.isReady()) {
|
|
onKeyUpPrefix(e);
|
|
}
|
|
});
|
|
$("#btnRandomArticle").on("click", function(e) {
|
|
$('#prefix').val("");
|
|
goToRandomArticle();
|
|
$("#welcomeText").hide();
|
|
$('#articleList').hide();
|
|
$('#articleListHeaderMessage').hide();
|
|
$("#readingArticle").hide();
|
|
$('#searchingForArticles').hide();
|
|
if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) {
|
|
$('#navbarToggle').click();
|
|
}
|
|
});
|
|
|
|
$('#btnRescanDeviceStorage').on("click", function(e) {
|
|
searchForArchivesInStorage();
|
|
});
|
|
// Bottom bar :
|
|
$('#btnBack').on('click', function(e) {
|
|
history.back();
|
|
return false;
|
|
});
|
|
$('#btnForward').on('click', function(e) {
|
|
history.forward();
|
|
return false;
|
|
});
|
|
$('#btnHomeBottom').on('click', function(e) {
|
|
$('#btnHome').click();
|
|
return false;
|
|
});
|
|
$('#btnTop').on('click', function(e) {
|
|
$("#articleContent").contents().scrollTop(0);
|
|
// We return true, so that the link to #top is still triggered (useful in the About section)
|
|
return true;
|
|
});
|
|
// Top menu :
|
|
$('#btnHome').on('click', function(e) {
|
|
// 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();
|
|
}
|
|
// Show the selected content in the page
|
|
$('#about').hide();
|
|
$('#configuration').hide();
|
|
$('#formArticleSearch').show();
|
|
$("#welcomeText").show();
|
|
$('#articleList').show();
|
|
$('#articleListHeaderMessage').show();
|
|
$('#articleContent').show();
|
|
// Give the focus to the search field, and clean up the page contents
|
|
$("#prefix").val("");
|
|
$('#prefix').focus();
|
|
$("#articleList").empty();
|
|
$('#articleListHeaderMessage').empty();
|
|
$("#readingArticle").hide();
|
|
$("#articleContent").hide();
|
|
$("#articleContent").contents().empty();
|
|
$('#searchingForArticles').hide();
|
|
if (selectedArchive !== null && selectedArchive.isReady()) {
|
|
$("#welcomeText").hide();
|
|
goToMainArticle();
|
|
}
|
|
return false;
|
|
});
|
|
$('#btnConfigure').on('click', function(e) {
|
|
// 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();
|
|
}
|
|
// Show the selected content in the page
|
|
$('#about').hide();
|
|
$('#configuration').show();
|
|
$('#formArticleSearch').hide();
|
|
$("#welcomeText").hide();
|
|
$('#articleList').hide();
|
|
$('#articleListHeaderMessage').hide();
|
|
$("#readingArticle").hide();
|
|
$("#articleContent").hide();
|
|
$('#articleContent').hide();
|
|
$('#searchingForArticles').hide();
|
|
refreshAPIStatus();
|
|
return false;
|
|
});
|
|
$('#btnAbout').on('click', function(e) {
|
|
// 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();
|
|
}
|
|
// Show the selected content in the page
|
|
$('#about').show();
|
|
$('#configuration').hide();
|
|
$('#formArticleSearch').hide();
|
|
$("#welcomeText").hide();
|
|
$('#articleList').hide();
|
|
$('#articleListHeaderMessage').hide();
|
|
$("#readingArticle").hide();
|
|
$("#articleContent").hide();
|
|
$('#articleContent').hide();
|
|
$('#searchingForArticles').hide();
|
|
return false;
|
|
});
|
|
$('input:radio[name=contentInjectionMode]').on('change', function(e) {
|
|
if (checkWarnServiceWorkerMode(this.value)) {
|
|
// Do the necessary to enable or disable the Service Worker
|
|
setContentInjectionMode(this.value);
|
|
}
|
|
else {
|
|
setContentInjectionMode('jquery');
|
|
}
|
|
});
|
|
$('input:radio[name=cssInjectionMode]').on('change', function (e) {
|
|
params['cssSource'] = this.value;
|
|
});
|
|
|
|
/**
|
|
* Displays of refreshes the API status shown to the user
|
|
*/
|
|
function refreshAPIStatus() {
|
|
if (isMessageChannelAvailable()) {
|
|
$('#messageChannelStatus').html("MessageChannel API available");
|
|
$('#messageChannelStatus').removeClass("apiAvailable apiUnavailable")
|
|
.addClass("apiAvailable");
|
|
} else {
|
|
$('#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 {
|
|
$('#serviceWorkerStatus').html("ServiceWorker API available, but not registered");
|
|
$('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable")
|
|
.addClass("apiUnavailable");
|
|
}
|
|
} else {
|
|
$('#serviceWorkerStatus').html("ServiceWorker API unavailable");
|
|
$('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable")
|
|
.addClass("apiUnavailable");
|
|
}
|
|
}
|
|
|
|
var contentInjectionMode;
|
|
|
|
/**
|
|
* 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()) {
|
|
alert("The ServiceWorker API is not available on your device. Falling back to JQuery mode");
|
|
setContentInjectionMode('jquery');
|
|
return;
|
|
}
|
|
if (!isMessageChannelAvailable()) {
|
|
alert("The MessageChannel API is not available on your device. Falling back to JQuery mode");
|
|
setContentInjectionMode('jquery');
|
|
return;
|
|
}
|
|
|
|
if (!messageChannel) {
|
|
// Let's create the messageChannel for the 2-way communication
|
|
// with the Service Worker
|
|
messageChannel = new MessageChannel();
|
|
messageChannel.port1.onmessage = handleMessageChannelMessage;
|
|
}
|
|
|
|
if (!isServiceWorkerReady()) {
|
|
$('#serviceWorkerStatus').html("ServiceWorker API available : trying to register it...");
|
|
navigator.serviceWorker.register('../service-worker.js').then(function (reg) {
|
|
console.log('serviceWorker registered', reg);
|
|
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') {
|
|
console.log("try to post an init message to ServiceWorker");
|
|
navigator.serviceWorker.controller.postMessage({'action': 'init'}, [messageChannel.port2]);
|
|
console.log("init message sent to ServiceWorker");
|
|
}
|
|
});
|
|
}, function (err) {
|
|
console.error('error while registering serviceWorker', err);
|
|
refreshAPIStatus();
|
|
});
|
|
} else {
|
|
console.log("try to re-post an init message to ServiceWorker, to re-enable it in case it was disabled");
|
|
navigator.serviceWorker.controller.postMessage({'action': 'init'}, [messageChannel.port2]);
|
|
console.log("init message sent to ServiceWorker");
|
|
}
|
|
}
|
|
$('input:radio[name=contentInjectionMode]').prop('checked', false);
|
|
$('input:radio[name=contentInjectionMode]').filter('[value="' + value + '"]').prop('checked', true);
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* If the ServiceWorker mode is selected, warn the user before activating it
|
|
* @param chosenContentInjectionMode The mode that the user has chosen
|
|
*/
|
|
function checkWarnServiceWorkerMode(chosenContentInjectionMode) {
|
|
if (chosenContentInjectionMode === 'serviceworker' && !cookies.hasItem("warnedServiceWorkerMode")) {
|
|
// The user selected the "serviceworker" mode, which is still unstable
|
|
// So let's display a warning to the user
|
|
|
|
// If the focus is on the search field, we have to move it,
|
|
// else the keyboard hides the message
|
|
if ($("#prefix").is(":focus")) {
|
|
$("searchArticles").focus();
|
|
}
|
|
if (confirm("The 'Service Worker' mode is still UNSTABLE for now."
|
|
+ " It happens that the application needs to be reinstalled (or the ServiceWorker manually removed)."
|
|
+ " Please confirm with OK that you're ready to face this kind of bugs, or click Cancel to stay in 'jQuery' mode.")) {
|
|
// We will not display this warning again for one day
|
|
cookies.setItem("warnedServiceWorkerMode", true, 86400);
|
|
return true;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// At launch, we try to set the last content injection mode (stored in a cookie)
|
|
var lastContentInjectionMode = cookies.getItem('lastContentInjectionMode');
|
|
if (lastContentInjectionMode) {
|
|
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.<StorageFirefoxOS>
|
|
*/
|
|
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 {
|
|
searchForArchivesInStorage();
|
|
}
|
|
}
|
|
function searchForArchivesInStorage() {
|
|
// If DeviceStorage is available, we look for archives in it
|
|
$("#btnConfigure").click();
|
|
$('#scanningForArchives').show();
|
|
zimArchiveLoader.scanForArchives(storages, populateDropDownListOfArchives);
|
|
}
|
|
|
|
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) {
|
|
// 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,
|
|
searchForArchivesInPreferencesOrStorage);
|
|
}
|
|
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 {
|
|
$("#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();
|
|
$("#readingArticle").hide();
|
|
if ($('#navbarToggle').is(":visible") && $('#liHomeNav').is(':visible')) {
|
|
$('#navbarToggle').click();
|
|
}
|
|
$('#searchingForArticles').hide();
|
|
$('#configuration').hide();
|
|
$('#articleList').hide();
|
|
$('#articleListHeaderMessage').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.<String>} archiveDirectories
|
|
*/
|
|
function populateDropDownListOfArchives(archiveDirectories) {
|
|
$('#scanningForArchives').hide();
|
|
$('#chooseArchiveFromLocalStorage').show();
|
|
var comboArchiveList = document.getElementById('archiveList');
|
|
comboArchiveList.options.length = 0;
|
|
for (var i = 0; i < archiveDirectories.length; i++) {
|
|
var archiveDirectory = archiveDirectories[i];
|
|
if (archiveDirectory === "/") {
|
|
alert("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);
|
|
|
|
$('#archiveList').on('change', setLocalArchiveFromArchiveList);
|
|
if (comboArchiveList.options.length > 0) {
|
|
var lastSelectedArchive = cookies.getItem("lastSelectedArchive");
|
|
if (lastSelectedArchive !== null && lastSelectedArchive !== undefined && lastSelectedArchive !== "") {
|
|
// Attempt to select the corresponding item in the list, if it exists
|
|
if ($("#archiveList option[value='"+lastSelectedArchive+"']").length > 0) {
|
|
$("#archiveList").val(lastSelectedArchive);
|
|
}
|
|
}
|
|
// Set the localArchive as the last selected (or the first one if it has never been selected)
|
|
setLocalArchiveFromArchiveList();
|
|
}
|
|
else {
|
|
alert("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() {
|
|
var 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) {
|
|
alert("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 {
|
|
alert("Something weird happened with the DeviceStorage API : found a directory without prefix : "
|
|
+ archiveDirectory + ", but there were " + storages.length
|
|
+ " storages found with getDeviceStorages instead of 1");
|
|
}
|
|
}
|
|
selectedArchive = zimArchiveLoader.loadArchiveFromDeviceStorage(selectedStorage, archiveDirectory, function (archive) {
|
|
cookies.setItem("lastSelectedArchive", archiveDirectory, Infinity);
|
|
// The archive is set : go back to home page to start searching
|
|
$("#btnHome").click();
|
|
});
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Displays the zone to select files from the archive
|
|
*/
|
|
function displayFileSelect() {
|
|
$('#openLocalFiles').show();
|
|
$('#archiveFiles').on('change', setLocalArchiveFromFileSelect);
|
|
}
|
|
|
|
function setLocalArchiveFromFileList(files) {
|
|
selectedArchive = zimArchiveLoader.loadArchiveFromFiles(files, function (archive) {
|
|
// The archive is set : go back to home page to start searching
|
|
$("#btnHome").click();
|
|
});
|
|
}
|
|
/**
|
|
* Sets the localArchive from the File selects populated by user
|
|
*/
|
|
function setLocalArchiveFromFileSelect() {
|
|
setLocalArchiveFromFileList(document.getElementById('archiveFiles').files);
|
|
}
|
|
|
|
/**
|
|
* This is used in the testing interface to inject a remote archive.
|
|
*/
|
|
window.setRemoteArchive = function(url) {
|
|
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;
|
|
setLocalArchiveFromFileList([request.response]);
|
|
}
|
|
}
|
|
};
|
|
request.send(null);
|
|
};
|
|
|
|
/**
|
|
* 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) {
|
|
$('#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) {
|
|
$('#searchingForArticles').show();
|
|
$('#configuration').hide();
|
|
$('#articleContent').contents().empty();
|
|
if (selectedArchive !== null && selectedArchive.isReady()) {
|
|
selectedArchive.findDirEntriesWithPrefix(prefix.trim(), MAX_SEARCH_RESULT_SIZE, populateListOfArticles);
|
|
} else {
|
|
$('#searchingForArticles').hide();
|
|
// We have to remove the focus from the search field,
|
|
// so that the keyboard does not stay above the message
|
|
$("#searchArticles").focus();
|
|
alert("Archive not set : please select an archive");
|
|
$("#btnConfigure").click();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Display the list of articles with the given array of DirEntry
|
|
* @param {Array.<DirEntry>} dirEntryArray
|
|
* @param {Integer} maxArticles
|
|
*/
|
|
function populateListOfArticles(dirEntryArray, maxArticles) {
|
|
var articleListHeaderMessageDiv = $('#articleListHeaderMessage');
|
|
var nbDirEntry = 0;
|
|
if (dirEntryArray) {
|
|
nbDirEntry = dirEntryArray.length;
|
|
}
|
|
|
|
var message;
|
|
if (maxArticles >= 0 && nbDirEntry >= maxArticles) {
|
|
message = maxArticles + " first articles below (refine your search).";
|
|
}
|
|
else {
|
|
message = nbDirEntry + " articles found.";
|
|
}
|
|
if (nbDirEntry === 0) {
|
|
message = "No articles found.";
|
|
}
|
|
|
|
articleListHeaderMessageDiv.html(message);
|
|
|
|
|
|
var articleListDiv = $('#articleList');
|
|
var articleListDivHtml = "";
|
|
for (var i = 0; i < dirEntryArray.length; i++) {
|
|
var dirEntry = dirEntryArray[i];
|
|
|
|
articleListDivHtml += "<a href='#' dirEntryId='" + dirEntry.toStringId().replace(/'/g,"'")
|
|
+ "' class='list-group-item'>" + dirEntry.title + "</a>";
|
|
}
|
|
articleListDiv.html(articleListDivHtml);
|
|
$("#articleList a").on("click",handleTitleClick);
|
|
$('#searchingForArticles').hide();
|
|
$('#articleList').show();
|
|
$('#articleListHeaderMessage').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");
|
|
$("#articleList").empty();
|
|
$('#articleListHeaderMessage').empty();
|
|
$("#prefix").val("");
|
|
findDirEntryFromDirEntryIdAndLaunchArticleRead(dirEntryId);
|
|
var dirEntry = selectedArchive.parseDirEntryId(dirEntryId);
|
|
pushBrowserHistoryState(dirEntry.url);
|
|
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 (selectedArchive.isReady()) {
|
|
var dirEntry = selectedArchive.parseDirEntryId(dirEntryId);
|
|
$("#articleName").html(dirEntry.title);
|
|
$("#readingArticle").show();
|
|
$("#articleContent").contents().html("");
|
|
if (dirEntry.isRedirect()) {
|
|
selectedArchive.resolveRedirect(dirEntry, readArticle);
|
|
}
|
|
else {
|
|
readArticle(dirEntry);
|
|
}
|
|
}
|
|
else {
|
|
alert("Data files not set");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read the article corresponding to the given dirEntry
|
|
* @param {DirEntry} dirEntry
|
|
*/
|
|
function readArticle(dirEntry) {
|
|
if (dirEntry.isRedirect()) {
|
|
selectedArchive.resolveRedirect(dirEntry, readArticle);
|
|
}
|
|
else {
|
|
selectedArchive.readArticle(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
|
|
*/
|
|
function handleMessageChannelMessage(event) {
|
|
if (event.data.error) {
|
|
console.error("Error in MessageChannel", event.data.error);
|
|
reject(event.data.error);
|
|
} else {
|
|
console.log("the ServiceWorker sent a message on port1", event.data);
|
|
if (event.data.action === "askForContent") {
|
|
console.log("we are asked for a content : let's try to answer to this message");
|
|
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()) {
|
|
selectedArchive.resolveRedirect(dirEntry, readFile);
|
|
} else {
|
|
console.log("Reading binary file...");
|
|
selectedArchive.readBinaryFile(dirEntry, function(readableTitle, content) {
|
|
messagePort.postMessage({'action': 'giveContent', 'title' : title, 'content': content});
|
|
console.log("content sent to ServiceWorker");
|
|
});
|
|
}
|
|
};
|
|
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
|
|
var regexpImageLink = /^.?\/?[^:]+:(.*)/;
|
|
var regexpPath = /^(.*\/)[^\/]+$/;
|
|
// These regular expressions match both relative and absolute URLs
|
|
// Since late 2014, all ZIM files should use relative URLs
|
|
var regexpImageUrl = /^(?:\.\.\/|\/)+(I\/.*)$/;
|
|
var regexpMetadataUrl = /^(?:\.\.\/|\/)+(-\/.*)$/;
|
|
// This regular expression matches the href of all <link> tags containing rel="stylesheet" in raw HTML
|
|
var regexpSheetHref = /(<link\s+(?=[^>]*rel\s*=\s*["']stylesheet)[^>]*href\s*=\s*["'])([^"']+)(["'][^>]*>)/ig;
|
|
|
|
|
|
/**
|
|
* Display the the given HTML article in the web page,
|
|
* and convert links to javascript calls
|
|
* NB : in some error cases, the given title can be null, and the htmlArticle contains the error message
|
|
* @param {DirEntry} dirEntry
|
|
* @param {String} htmlArticle
|
|
*/
|
|
function displayArticleInForm(dirEntry, htmlArticle) {
|
|
// Display the article inside the web page.
|
|
|
|
//Fast-replace img src with data-kiwixsrc and hide image [kiwix-js #272]
|
|
htmlArticle = htmlArticle.replace(/(<img\s+[^>]*\b)src(\s*=)/ig,
|
|
"$1style=\"display: none;\" onload=\"this.style.display='inline'\" data-kiwixsrc$2");
|
|
|
|
//Preload stylesheets [kiwix-js @149]
|
|
//Set up blobArray of promises
|
|
var cssArray = htmlArticle.match(regexpSheetHref);
|
|
var blobArray = [];
|
|
cssSource = params['cssSource'];
|
|
getBLOB(cssArray);
|
|
|
|
//Extract CSS URLs from given array of links
|
|
function getBLOB(arr) {
|
|
if (cssSource == "desktop" && (!arr.join().match(/-\/s\/style\.css/i))) {
|
|
arr.push('<link href="../-/s/style.css" rel="stylesheet">'); //Insert the standard desktop style
|
|
}
|
|
for (var i = 0; i < arr.length; i++) {
|
|
var linkArray = regexpSheetHref.exec(arr[i]);
|
|
regexpSheetHref.lastIndex = 0; //Reset start position for next loop
|
|
if (regexpMetadataUrl.test(linkArray[2])) { //It's a CSS file contained in ZIM
|
|
var zimLink = decodeURIComponent(uiUtil.removeUrlParameters(linkArray[2]));
|
|
//If this is a standard Wikipedia css use stylesheet cached in the filesystem...
|
|
if ((cssSource != "zimfile") &&
|
|
(zimLink.match(/-\/s\/style\.css/i) ||
|
|
zimLink.match(/-\/s\/css_modules\/mediawiki\.toc\.css/i) ||
|
|
zimLink.match(/-\/s\/css_modules\/ext\.cite\.styles\.css/i) ||
|
|
zimLink.match(/-\/s\/css_modules\/ext\.timeline\.styles\.css/i) ||
|
|
zimLink.match(/-\/s\/css_modules\/ext\.scribunto\.logs\.css/i) ||
|
|
zimLink.match(/-\/s\/css_modules\/mediawiki\.page\.gallery\.styles\.css/i) ||
|
|
zimLink.match(/-\/s\/css_modules\/ext\.cite\.a11y\.css/i) ||
|
|
zimLink.match(/-\/s\/css_modules\/content\.parsoid\.css/i) ||
|
|
zimLink.match(/-\/s\/css_modules\/inserted_style_mobile\.css/i) ||
|
|
zimLink.match(/-\/s\/css_modules\/mobile\.css/i) ||
|
|
zimLink.match(/-\/s\/css_modules\/skins\.minerva\.base\.reset\|skins\.minerva\.content\.styles\|ext\.cite\.style\|mediawiki\.page\.gallery\.styles\|mobile\.app\.pagestyles\.android\|mediawiki\.skinning\.content\.parsoid\.css/i)
|
|
)) {
|
|
if (cssSource == "desktop" && zimLink.match(/minerva|mobile|parsoid/)) { //If user selected desktop style and the ZIM is formatted for mobile...
|
|
zimLink = "#"; //Void the style
|
|
}
|
|
if ((cssSource == "mobile") || (zimLink.match(/minerva/))) { //If user has selected mobile display mode or mobile is built into ZIM, substitute main stylesheet
|
|
zimLink = zimLink.match(/(-\/s\/style\.css)|(minerva)/i) ? "../-/s/style-mobile.css" : zimLink;
|
|
}
|
|
blobArray[i] = zimLink.replace(/\|/ig, "_"); //Replace "|" with "_" (legacy for some stylesheets with pipes in filename)
|
|
console.log("Matched #" + i + " [" + blobArray[i] + "] from local filesystem");
|
|
injectCSS();
|
|
} else { //Try to get the stylesheet from the ZIM file
|
|
var linkURL = zimLink.match(regexpMetadataUrl)[1];
|
|
console.log("Attempting to resolve CSS link #" + i + " [" + linkURL + "] from ZIM file..." +
|
|
"\n(Consider adding file #" + i + " to the local filesystem)");
|
|
resolveCSS(linkURL, i); //Pass link and index
|
|
}
|
|
} else {
|
|
blobArray[i] = linkArray[2]; //If CSS not in ZIM, store URL in blobArray
|
|
injectCSS(); //Ensure this is called even if none of CSS links are in ZIM
|
|
}
|
|
}
|
|
}
|
|
|
|
function resolveCSS(title, index) {
|
|
selectedArchive.getDirEntryByTitle(title).then(
|
|
function (dirEntry) {
|
|
selectedArchive.readBinaryFile(dirEntry, function (readableTitle, content) {
|
|
//var cssContent = util.uintToString(content); //Uncomment this line and break on next to capture cssContent for local filesystem cache
|
|
var cssBlob = new Blob([content], { type: 'text/css' });
|
|
var newURL = URL.createObjectURL(cssBlob);
|
|
blobArray[index] = newURL;
|
|
injectCSS(); //Don't move this: it must run within .then function to pass correct values
|
|
});
|
|
}).fail(function (e) {
|
|
console.error("could not find DirEntry for CSS : " + title, e);
|
|
blobArray[index] = title;
|
|
injectCSS();
|
|
});
|
|
}
|
|
|
|
function injectCSS() {
|
|
if (blobArray.length === cssArray.length) { //If all promised values have been obtained
|
|
for (var i in cssArray) {
|
|
cssArray[i] = cssArray[i].replace(/(href\s*=\s*["'])([^"']+)/ig, "$1" + blobArray[i]);
|
|
}
|
|
htmlArticle = htmlArticle.replace(regexpSheetHref, ""); //Void existing stylesheets
|
|
var cssArray$ = "\r\n" + cssArray.join("\r\n") + "\r\n";
|
|
if (cssSource == "mobile") { //If user has selected mobile display mode, insert extra stylesheets
|
|
cssArray$ += cssArray$.match(/-\/s\/css_modules\/content\.parsoid\.css/i) ? "" : '<link href="../-/s/css_modules/content.parsoid.css" rel="stylesheet" type="text/css">\r\n';
|
|
cssArray$ += cssArray$.match(/-\/s\/css_modules\/mobile\.css/i) ? "" : '<link href="../-/s/css_modules/mobile.css" rel="stylesheet" type="text/css">\r\n';
|
|
cssArray$ += cssArray$.match(/-\/s\/css_modules\/inserted_style_mobile\.css/i) ? "" : '<link href="../-/s/css_modules/inserted_style_mobile.css" rel="stylesheet" type="text/css">\r\n';
|
|
htmlArticle = htmlArticle.replace(/class\s*=\s*["']\s*thumb\s+tright\s*["']\s*/ig, 'style="float: right; clear: right; margin-left: 1.4em;"');
|
|
htmlArticle = htmlArticle.replace(/class\s*=\s*["']\s*thumb\s+tleft\s*["']\s*/ig, 'style="float: left; clear: left; margin-right: 1.4em;"');
|
|
htmlArticle = htmlArticle.replace(/class\s*=\s*["']\s*thumbcaption\s*["']\s*/ig, 'style="margin: 0.5em 0 0; font-size: 0.8em; line-height: 1.5; padding: 0 !important; color: #54595d; width: auto !important;"');
|
|
htmlArticle = htmlArticle.replace(/(table\s+(?=[^>]*class\s*=\s*["'][^"']*infobox)[^>]*style\s*=\s*["'][^"']+[^;'"]);?\s*["']/ig, '$1; position: relative; border: 1px solid #eaecf0; text-align: left; background-color: #f8f9fa;"');
|
|
}
|
|
if (cssSource == "desktop") { //If user has selected desktop display mode...
|
|
htmlArticle = htmlArticle.replace(/class\s*=\s*["']\s*mw-body\s*["']\s*/ig, 'style="background-color: white; padding: 1em; border-width: 0px; max-width: 55.8em; margin: 0 auto 0 auto;"');
|
|
htmlArticle = htmlArticle.replace(/<h1\s*[^>]+titleHeading[^>]+>\s*<\/h1>\s*/ig, ""); //Void empty header title
|
|
}
|
|
htmlArticle = htmlArticle.replace(/\s*(<\/head>)/i, cssArray$ + "$1");
|
|
console.log("All CSS resolved");
|
|
injectHTML(htmlArticle); //This passes the revised HTML to the image and JS subroutine...
|
|
} else {
|
|
//console.log("Waiting for " + (cssArray.length - blobArray.length) + " out of " + cssArray.length + " to resolve...")
|
|
}
|
|
}
|
|
//End of preload stylesheets code
|
|
|
|
function injectHTML(htmlContent) {
|
|
$("#readingArticle").hide();
|
|
$("#articleContent").show();
|
|
// Scroll the iframe to its top
|
|
$("#articleContent").contents().scrollTop(0);
|
|
$('#articleContent').contents().find('body').html(htmlContent);
|
|
|
|
// If the ServiceWorker is not useable, we need to fallback to parse the DOM
|
|
// to inject math images, and replace some links with javascript calls
|
|
if (contentInjectionMode === 'jquery') {
|
|
|
|
// Convert links into javascript calls
|
|
$('#articleContent').contents().find('body').find('a').each(function () {
|
|
// Store current link's url
|
|
var url = $(this).attr("href");
|
|
if (url === null || url === undefined) {
|
|
return;
|
|
}
|
|
var lowerCaseUrl = url.toLowerCase();
|
|
var cssClass = $(this).attr("class");
|
|
|
|
if (cssClass === "new") {
|
|
// It's a link to a missing article : display a message
|
|
$(this).on('click', function (e) {
|
|
alert("Missing article in Wikipedia");
|
|
return false;
|
|
});
|
|
}
|
|
else if (url.slice(0, 1) === "#") {
|
|
// It's an anchor link : do nothing
|
|
}
|
|
else if (url.substring(0, 4) === "http") {
|
|
// It's an external link : open in a new tab
|
|
$(this).attr("target", "_blank");
|
|
}
|
|
else if (url.match(regexpImageLink)
|
|
&& (util.endsWith(lowerCaseUrl, ".png")
|
|
|| util.endsWith(lowerCaseUrl, ".svg")
|
|
|| util.endsWith(lowerCaseUrl, ".jpg")
|
|
|| util.endsWith(lowerCaseUrl, ".jpeg"))) {
|
|
// It's a link to a file of Wikipedia : change the URL to the online version and open in a new tab
|
|
var onlineWikipediaUrl = url.replace(regexpImageLink, "https://" + selectedArchive._language + ".wikipedia.org/wiki/File:$1");
|
|
$(this).attr("href", onlineWikipediaUrl);
|
|
$(this).attr("target", "_blank");
|
|
}
|
|
else {
|
|
// It's a link to another article
|
|
// Add an onclick event to go to this article
|
|
// instead of following the link
|
|
|
|
if (url.substring(0, 2) === "./") {
|
|
url = url.substring(2);
|
|
}
|
|
// Remove the initial slash if it's an absolute URL
|
|
else if (url.substring(0, 1) === "/") {
|
|
url = url.substring(1);
|
|
}
|
|
$(this).on('click', function (e) {
|
|
var decodedURL = decodeURIComponent(url);
|
|
pushBrowserHistoryState(decodedURL);
|
|
goToArticle(decodedURL);
|
|
return false;
|
|
});
|
|
}
|
|
});
|
|
|
|
// Load images
|
|
$('#articleContent').contents().find('body').find('img').each(function () {
|
|
var image = $(this);
|
|
// It's a standard image contained in the ZIM file
|
|
// We try to find its name (from an absolute or relative URL)
|
|
var imageMatch = image.attr('data-kiwixsrc').match(regexpImageUrl); //kiwix-js #272
|
|
if (imageMatch) {
|
|
var title = decodeURIComponent(imageMatch[1]);
|
|
selectedArchive.getDirEntryByTitle(title).then(function (dirEntry) {
|
|
selectedArchive.readBinaryFile(dirEntry, function (readableTitle, content) {
|
|
// TODO : use the complete MIME-type of the image (as read from the ZIM file)
|
|
uiUtil.feedNodeWithBlob(image, 'src', content, 'image');
|
|
});
|
|
}).fail(function (e) {
|
|
console.error("could not find DirEntry for image:" + title, e);
|
|
});
|
|
}
|
|
});
|
|
|
|
/*/ Load Javascript content
|
|
$('#articleContent').contents().find('script').each(function () {
|
|
var script = $(this);
|
|
// We try to find its name (from an absolute or relative URL)
|
|
if (script) { var srcMatch = script.attr("src").match(regexpMetadataUrl) }
|
|
// TODO check that the type of the script is text/javascript or application/javascript
|
|
if (srcMatch) {
|
|
// It's a Javascript file contained in the ZIM file
|
|
var title = uiUtil.removeUrlParameters(decodeURIComponent(srcMatch[1]));
|
|
selectedArchive.getDirEntryByTitle(title).then(function (dirEntry) {
|
|
if (dirEntry === null)
|
|
console.log("Error: js file not found: " + title);
|
|
else
|
|
selectedArchive.readBinaryFile(dirEntry, function (readableTitle, content) {
|
|
// TODO : I have to disable javascript for now
|
|
// var jsContent = encodeURIComponent(util.uintToString(content));
|
|
//script.attr("src", 'data:text/javascript;charset=UTF-8,' + jsContent);
|
|
});
|
|
}).fail(function (e) {
|
|
console.error("could not find DirEntry for javascript : " + title, e);
|
|
});
|
|
}
|
|
});*/
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Changes the URL of the browser page, so that the user might go back to it
|
|
*
|
|
* @param {String} title
|
|
* @param {String} titleSearch
|
|
*/
|
|
function pushBrowserHistoryState(title, titleSearch) {
|
|
var stateObj = {};
|
|
var urlParameters;
|
|
var stateLabel;
|
|
if (title && !(""===title)) {
|
|
stateObj.title = title;
|
|
urlParameters = "?title=" + title;
|
|
stateLabel = "Wikipedia Article : " + title;
|
|
}
|
|
else if (titleSearch && !(""===titleSearch)) {
|
|
stateObj.titleSearch = titleSearch;
|
|
urlParameters = "?titleSearch=" + titleSearch;
|
|
stateLabel = "Wikipedia search : " + titleSearch;
|
|
}
|
|
else {
|
|
return;
|
|
}
|
|
window.history.pushState(stateObj, stateLabel, urlParameters);
|
|
}
|
|
|
|
|
|
/**
|
|
* Replace article content with the one of the given title
|
|
* @param {String} title
|
|
*/
|
|
function goToArticle(title) {
|
|
selectedArchive.getDirEntryByTitle(title).then(function(dirEntry) {
|
|
if (dirEntry === null || dirEntry === undefined) {
|
|
$("#readingArticle").hide();
|
|
alert("Article with title " + title + " not found in the archive");
|
|
}
|
|
else {
|
|
$("#articleName").html(title);
|
|
$("#readingArticle").show();
|
|
$('#articleContent').contents().find('body').html("");
|
|
readArticle(dirEntry);
|
|
}
|
|
}).fail(function() { alert("Error reading article with title " + title); });
|
|
}
|
|
|
|
function goToRandomArticle() {
|
|
selectedArchive.getRandomDirEntry(function(dirEntry) {
|
|
if (dirEntry === null || dirEntry === undefined) {
|
|
alert("Error finding random article.");
|
|
}
|
|
else {
|
|
if (dirEntry.namespace === 'A') {
|
|
$("#articleName").html(dirEntry.title);
|
|
pushBrowserHistoryState(dirEntry.url);
|
|
$("#readingArticle").show();
|
|
$('#articleContent').contents().find('body').html("");
|
|
readArticle(dirEntry);
|
|
}
|
|
else {
|
|
// If the random title search did not end up on an article,
|
|
// we try again, until we find one
|
|
goToRandomArticle();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function goToMainArticle() {
|
|
selectedArchive.getMainPageDirEntry(function(dirEntry) {
|
|
if (dirEntry === null || dirEntry === undefined) {
|
|
console.error("Error finding main article.");
|
|
}
|
|
else {
|
|
if (dirEntry.namespace === 'A') {
|
|
$("#articleName").html(dirEntry.title);
|
|
pushBrowserHistoryState(dirEntry.url);
|
|
$("#readingArticle").show();
|
|
$('#articleContent').contents().find('body').html("");
|
|
readArticle(dirEntry);
|
|
}
|
|
else {
|
|
console.error("The main page of this archive does not seem to be an article");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|