Jaifroid c9bf00b070 Prevent display of unnecessary 404s while loading images
Former-commit-id: 6a28f20ac8d0178c0702b5fce502040926a405d8 [formerly 49c72f554f7cc7cfc559884c6beb5a47e2bf94da]
Former-commit-id: 690a04140e95a7b460d4af0a4d927238d09bfd36
2017-06-25 18:53:33 +01:00

1041 lines
44 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'],
function($, zimArchiveLoader, util, uiUtil, cookies, abstractFilesystemAccess) {
/*/ Disable any eval() call in jQuery : it's disabled by CSP in any packaged application
// It happens on some wiktionary archives, because there is some javascript inside the html article
// Cf http://forum.jquery.com/topic/jquery-ajax-disable-script-eval
jQuery.globalEval = function (code) {
// jQuery believes the javascript has been executed, but we did nothing
// In any case, that would have been blocked by CSP for package applications
console.log("jQuery tried to run some javascript with eval(), which is not allowed in packaged applications");
}; */
/**
* Maximum number of articles to display in a search
* @type Integer
*/
//var MAX_SEARCH_RESULT_SIZE = 50;
var MAX_SEARCH_RESULT_SIZE = 20; //GK - speed up search
/**
* @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');
}
});
/**
* 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,"&apos;")
+ "' 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 = /^(?:\.\.\/|\/)+(-\/.*)$/;
/**
* 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) {
$("#readingArticle").hide();
$("#articleContent").show();
// Scroll the iframe to its top
$("#articleContent").contents().scrollTop(0);
// Display the article inside the web page.
var $body = $(htmlArticle);
$body.find('img').each(function(){
var image = $(this);
// Prevents unnecessary 404's being produced when iframe loads images
$(image).attr("data-src", $(image).attr("src"));
$(image).removeAttr("src");
$(image).attr("data-height", $(image).attr("height"));
$(image).removeAttr("height");
$(image).attr("data-width", $(image).attr("width"));
$(image).removeAttr("width");
//Restore image height and size on image load
$(image).on("load", function (e) {
this.width = $(image).attr("data-width");
this.height = $(image).attr("data-height");
});
});
$('#articleContent').contents().find('body').html($body);
// 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-src').match(regexpImageUrl);
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 CSS content
$('#articleContent').contents().find('link[rel=stylesheet]').each(function() {
var link = $(this);
// We try to find its name (from an absolute or relative URL)
var hrefMatch = link.attr("href").match(regexpMetadataUrl);
if (hrefMatch) {
// It's a CSS file contained in the ZIM file
var title = uiUtil.removeUrlParameters(decodeURIComponent(hrefMatch[1]));
selectedArchive.getDirEntryByTitle(title).then(function(dirEntry) {
selectedArchive.readBinaryFile(dirEntry, function (readableTitle, content) {
var cssContent = util.uintToString(content);
// For some reason, Firefox OS does not accept the syntax <link rel="stylesheet" href="data:text/css,...">
// So we replace the tag with a <style type="text/css">...</style>
// while copying some attributes of the original tag
// Cf http://jonraasch.com/blog/javascript-style-node
var cssElement = document.createElement('style');
cssElement.type = 'text/css';
if (cssElement.styleSheet) {
cssElement.styleSheet.cssText = cssContent;
} else {
cssElement.appendChild(document.createTextNode(cssContent));
}
var mediaAttributeValue = link.attr('media');
if (mediaAttributeValue) {
cssElement.media = mediaAttributeValue;
}
var disabledAttributeValue = link.attr('media');
if (disabledAttributeValue) {
cssElement.disabled = disabledAttributeValue;
}
link.replaceWith(cssElement);
});
}).fail(function (e) {
console.error("could not find DirEntry for CSS : " + 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)
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");
}
}
});
}
});