mirror of
https://github.com/kiwix/kiwix-js.git
synced 2025-09-22 03:52:21 -04:00
Merge pull request #142 from mossroy/jquery-fallback
JQuery fallback and injection of stylesheets
This commit is contained in:
commit
21ce7b14d3
@ -25,32 +25,6 @@
|
||||
// TODO : remove requirejs if it's really useless here
|
||||
importScripts('./www/js/lib/require.js');
|
||||
|
||||
/**
|
||||
* From https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript
|
||||
*/
|
||||
function b64toBlob(b64Data, contentType, sliceSize) {
|
||||
contentType = contentType || '';
|
||||
sliceSize = sliceSize || 512;
|
||||
|
||||
var byteCharacters = atob(b64Data);
|
||||
var byteArrays = [];
|
||||
|
||||
for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
||||
var slice = byteCharacters.slice(offset, offset + sliceSize);
|
||||
|
||||
var byteNumbers = new Array(slice.length);
|
||||
for (var i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i);
|
||||
}
|
||||
|
||||
var byteArray = new Uint8Array(byteNumbers);
|
||||
|
||||
byteArrays.push(byteArray);
|
||||
}
|
||||
|
||||
var blob = new Blob(byteArrays, {type: contentType});
|
||||
return blob;
|
||||
}
|
||||
|
||||
self.addEventListener('install', function(event) {
|
||||
event.waitUntil(self.skipWaiting());
|
||||
@ -81,6 +55,15 @@ function(util) {
|
||||
console.log('Init message received', event.data);
|
||||
outgoingMessagePort = event.ports[0];
|
||||
console.log('outgoingMessagePort initialized', outgoingMessagePort);
|
||||
self.addEventListener('fetch', fetchEventListener);
|
||||
console.log('fetchEventListener enabled');
|
||||
}
|
||||
if (event.data.action === 'disable') {
|
||||
console.log('Disable message received');
|
||||
outgoingMessagePort = null;
|
||||
console.log('outgoingMessagePort deleted');
|
||||
self.removeEventListener('fetch', fetchEventListener);
|
||||
console.log('fetchEventListener removed');
|
||||
}
|
||||
});
|
||||
|
||||
@ -91,21 +74,35 @@ function(util) {
|
||||
var regexpJS = new RegExp(/\.js/i);
|
||||
var regexpCSS = new RegExp(/\.css$/i);
|
||||
|
||||
var regexpContentUrl = new RegExp(/\/(.)\/(.*[^\/]+)$/);
|
||||
var regexpContentUrlWithNamespace = new RegExp(/\/(.)\/(.*[^\/]+)$/);
|
||||
var regexpContentUrlWithoutNamespace = new RegExp(/^([^\/]+)$/);
|
||||
var regexpDummyArticle = new RegExp(/dummyArticle\.html$/);
|
||||
|
||||
self.addEventListener('fetch', function(event) {
|
||||
|
||||
function fetchEventListener(event) {
|
||||
console.log('ServiceWorker handling fetch event for : ' + event.request.url);
|
||||
|
||||
|
||||
// TODO handle the dummy article more properly
|
||||
if (regexpContentUrl.test(event.request.url) && !regexpDummyArticle.test(event.request.url)) {
|
||||
|
||||
if ((regexpContentUrlWithNamespace.test(event.request.url)
|
||||
|| regexpContentUrlWithoutNamespace.test(event.request.url))
|
||||
&& !regexpDummyArticle.test(event.request.url)) {
|
||||
|
||||
console.log('Asking app.js for a content', event.request.url);
|
||||
event.respondWith(new Promise(function(resolve, reject) {
|
||||
var regexpResult = regexpContentUrl.exec(event.request.url);
|
||||
var nameSpace = regexpResult[1];
|
||||
var titleName = regexpResult[2];
|
||||
var nameSpace;
|
||||
var titleName;
|
||||
var titleNameWithNameSpace;
|
||||
var contentType;
|
||||
if (regexpContentUrlWithoutNamespace.test(event.request.url)) {
|
||||
// When the request URL is in the same folder,
|
||||
// it means it's a link to an article (namespace A)
|
||||
var regexpResult = regexpContentUrlWithoutNamespace.exec(event.request.url);
|
||||
nameSpace = 'A';
|
||||
titleName = regexpResult[1];
|
||||
} else {
|
||||
var regexpResult = regexpContentUrlWithNamespace.exec(event.request.url);
|
||||
nameSpace = regexpResult[1];
|
||||
titleName = regexpResult[2];
|
||||
}
|
||||
|
||||
// The namespace defines the type of content. See http://www.openzim.org/wiki/ZIM_file_format#Namespaces
|
||||
// TODO : read the contentType from the ZIM file instead of hard-coding it here
|
||||
@ -126,17 +123,6 @@ function(util) {
|
||||
console.log("It's a layout dependency : " + titleName);
|
||||
if (regexpJS.test(titleName)) {
|
||||
contentType = 'text/javascript';
|
||||
}
|
||||
else if (regexpCSS.test(titleName)) {
|
||||
contentType = 'image/css';
|
||||
}
|
||||
}
|
||||
|
||||
// Let's instanciate a new messageChannel, to allow app.s to give us the content
|
||||
var messageChannel = new MessageChannel();
|
||||
messageChannel.port1.onmessage = function(event) {
|
||||
if (event.data.action === 'giveContent') {
|
||||
console.log('content message received for ' + titleName, event.data);
|
||||
var responseInit = {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
@ -145,23 +131,51 @@ function(util) {
|
||||
}
|
||||
};
|
||||
|
||||
var httpResponse = new Response(';', responseInit);
|
||||
|
||||
// TODO : temporary before the backend actually sends a proper content
|
||||
resolve(httpResponse);
|
||||
return;
|
||||
}
|
||||
else if (regexpCSS.test(titleName)) {
|
||||
contentType = 'text/css';
|
||||
}
|
||||
}
|
||||
|
||||
// We need to remove the potential parameters in the URL
|
||||
titleName = util.removeUrlParameters(titleName);
|
||||
|
||||
titleNameWithNameSpace = nameSpace + '/' + titleName;
|
||||
|
||||
// Let's instanciate a new messageChannel, to allow app.s to give us the content
|
||||
var messageChannel = new MessageChannel();
|
||||
messageChannel.port1.onmessage = function(event) {
|
||||
if (event.data.action === 'giveContent') {
|
||||
console.log('content message received for ' + titleNameWithNameSpace, event.data);
|
||||
var responseInit = {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {
|
||||
'Content-Type': contentType
|
||||
}
|
||||
};
|
||||
|
||||
var httpResponse = new Response(event.data.content, responseInit);
|
||||
|
||||
console.log('ServiceWorker responding to the HTTP request for ' + titleName + ' (size=' + event.data.content.length + ' octets)' , httpResponse);
|
||||
console.log('ServiceWorker responding to the HTTP request for ' + titleNameWithNameSpace + ' (size=' + event.data.content.length + ' octets)' , httpResponse);
|
||||
resolve(httpResponse);
|
||||
}
|
||||
else {
|
||||
console.log('Invalid message received from app.js for ' + titleName, event.data);
|
||||
console.log('Invalid message received from app.js for ' + titleNameWithNameSpace, event.data);
|
||||
reject(event.data);
|
||||
}
|
||||
};
|
||||
console.log('Eventlistener added to listen for an answer to ' + titleName);
|
||||
outgoingMessagePort.postMessage({'action': 'askForContent', 'titleName': titleName}, [messageChannel.port2]);
|
||||
console.log('Eventlistener added to listen for an answer to ' + titleNameWithNameSpace);
|
||||
outgoingMessagePort.postMessage({'action': 'askForContent', 'titleName': titleNameWithNameSpace}, [messageChannel.port2]);
|
||||
console.log('Message sent to app.js through outgoingMessagePort');
|
||||
}));
|
||||
}
|
||||
// If event.respondWith() isn't called because this wasn't a request that we want to handle,
|
||||
// then the default request/response behavior will automatically be used.
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
@ -531,7 +531,7 @@ define(['jquery', 'title', 'archive', 'zimArchive', 'zimDirEntry', 'util', 'geom
|
||||
});
|
||||
});
|
||||
});
|
||||
asyncTest("Image 's/style.css' can be loaded", function() {
|
||||
asyncTest("Stylesheet 's/style.css' can be loaded", function() {
|
||||
expect(4);
|
||||
localZimArchive.getTitleByName("-/s/style.css").then(function(title) {
|
||||
ok(title !== null, "Title found");
|
||||
|
@ -207,6 +207,12 @@
|
||||
<br /> Please select the archive you want to use : <select id="archiveList" class="form-control"></select>
|
||||
<br /> Click <a id="btnRescanDeviceStorage">here</a> to rescan your SD Cards and internal memory
|
||||
</div>
|
||||
<div id="contentInjectionModeDiv">
|
||||
Content injection mode : <br/>
|
||||
<input type="radio" name="contentInjectionMode" value="jquery" id="jQueryModeRadio" checked><label for="jQueryModeRadio">JQuery</label>
|
||||
<br>
|
||||
<input type="radio" name="contentInjectionMode" value="serviceworker" id="serviceworkerModeRadio"><label for="serviceworkerModeRadio">ServiceWorker</label>
|
||||
</div>
|
||||
<div id="serviceWorkerStatus"></div>
|
||||
<div id="messageChannelStatus"></div>
|
||||
<br />
|
||||
|
315
www/js/app.js
315
www/js/app.js
@ -227,24 +227,7 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction
|
||||
$('#geolocationProgress').hide();
|
||||
$('#articleContent').hide();
|
||||
$('#searchingForTitles').hide();
|
||||
// Refresh the ServiceWorker status (in case it has been unregistered)
|
||||
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");
|
||||
}
|
||||
refreshAPIStatus();
|
||||
return false;
|
||||
});
|
||||
$('#btnAbout').on('click', function(e) {
|
||||
@ -271,6 +254,137 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction
|
||||
$('#searchingForTitles').hide();
|
||||
return false;
|
||||
});
|
||||
$('input:radio[name=contentInjectionMode]').on('change', function(e) {
|
||||
// Do the necessary to enable or disable the Service Worker
|
||||
setContentInjectionMode(this.value);
|
||||
checkSelectedArchiveCompatibilityWithInjectionMode();
|
||||
});
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the archive selected by the user is compatible
|
||||
* with the injection mode, and warn the user if it's not
|
||||
* @returns {Boolean} true if they're compatible
|
||||
*/
|
||||
function checkSelectedArchiveCompatibilityWithInjectionMode() {
|
||||
if (selectedArchive && selectedArchive.needsWikimediaCSS() && contentInjectionMode === 'serviceworker') {
|
||||
alert('You seem to want to use ServiceWorker mode for an Evopedia archive : this is not supported. Please use the JQuery mode or use a ZIM file');
|
||||
$("#btnConfigure").click();
|
||||
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;
|
||||
|
||||
@ -305,42 +419,10 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
function isServiceWorkerReady() {
|
||||
return (serviceWorkerRegistration !== null);
|
||||
// Return true if the serviceWorkerRegistration is not null and not undefined
|
||||
return (serviceWorkerRegistration);
|
||||
}
|
||||
|
||||
if (isServiceWorkerAvailable()) {
|
||||
$('#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;
|
||||
$('#serviceWorkerStatus').html("ServiceWorker API available, and registered");
|
||||
$('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable")
|
||||
.addClass("apiAvailable");
|
||||
}, function(err) {
|
||||
console.error('error while registering serviceWorker', err);
|
||||
$('#serviceWorkerStatus').html("ServiceWorker API available, but unable to register : " + err);
|
||||
$('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable")
|
||||
.addClass("apiUnavailable");
|
||||
});
|
||||
}
|
||||
else {
|
||||
console.log("serviceWorker API not available");
|
||||
$('#serviceWorkerStatus').html("ServiceWorker API unavailable");
|
||||
$('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable")
|
||||
.addClass("apiUnavailable");
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
||||
// Detect if DeviceStorage is available
|
||||
/**
|
||||
*
|
||||
* @type Array.<StorageFirefoxOS|StoragePhoneGap>
|
||||
@ -532,8 +614,10 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction
|
||||
}
|
||||
selectedArchive = backend.loadArchiveFromDeviceStorage(selectedStorage, archiveDirectory);
|
||||
cookies.setItem("lastSelectedArchive", archiveDirectory, Infinity);
|
||||
// The archive is set : go back to home page to start searching
|
||||
$("#btnHome").click();
|
||||
if (checkSelectedArchiveCompatibilityWithInjectionMode()) {
|
||||
// The archive is set : go back to home page to start searching
|
||||
$("#btnHome").click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -550,8 +634,10 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction
|
||||
*/
|
||||
function setLocalArchiveFromFileSelect() {
|
||||
selectedArchive = backend.loadArchiveFromFiles(document.getElementById('archiveFiles').files);
|
||||
// The archive is set : go back to home page to start searching
|
||||
$("#btnHome").click();
|
||||
if (checkSelectedArchiveCompatibilityWithInjectionMode()) {
|
||||
// The archive is set : go back to home page to start searching
|
||||
$("#btnHome").click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -701,13 +787,13 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction
|
||||
$('#titleListHeaderMessage').empty();
|
||||
$('#suggestEnlargeMaxDistance').hide();
|
||||
$('#suggestReduceMaxDistance').hide();
|
||||
$("#prefix").val("");
|
||||
findTitleFromTitleIdAndLaunchArticleRead(titleId);
|
||||
var title = selectedArchive.parseTitleId(titleId);
|
||||
pushBrowserHistoryState(title.name());
|
||||
$("#prefix").val("");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Creates an instance of title from given titleId (including resolving redirects),
|
||||
@ -745,13 +831,7 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction
|
||||
}
|
||||
}
|
||||
|
||||
if (isMessageChannelAvailable()) {
|
||||
// Let's instanciate the messageChannel where the ServiceWorker can ask for contents
|
||||
// NB : note that we use the var keyword here (and not let),
|
||||
// so that the scope of the variable is the whole file
|
||||
var messageChannel = new MessageChannel();
|
||||
messageChannel.port1.onmessage = handleMessageChannelMessage;
|
||||
}
|
||||
var messageChannel;
|
||||
|
||||
/**
|
||||
* Function that handles a message of the messageChannel.
|
||||
@ -797,6 +877,8 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction
|
||||
var regexpImageLink = /^.?\/?[^:]+:(.*)/;
|
||||
var regexpMathImageUrl = /^\/math.*\/([0-9a-f]{32})\.png$/;
|
||||
var regexpPath = /^(.*\/)[^\/]+$/;
|
||||
var regexpImageUrl = /^\.\.\/(I\/.*)$/;
|
||||
var regexpMetadataUrl = /^\.\.\/(-\/.*)$/;
|
||||
|
||||
/**
|
||||
* Display the the given HTML article in the web page,
|
||||
@ -810,25 +892,6 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction
|
||||
$("#articleContent").show();
|
||||
// Scroll the iframe to its top
|
||||
$("#articleContent").contents().scrollTop(0);
|
||||
|
||||
if (isServiceWorkerReady()) {
|
||||
// TODO : We do not use Service Workers on Evopedia archives, for now
|
||||
// Maybe it would be worth trying to enable them in the future?
|
||||
if (selectedArchive.needsWikimediaCSS()) {
|
||||
// Let's unregister the ServiceWorker
|
||||
serviceWorkerRegistration.unregister().then(function() {serviceWorkerRegistration = null;});
|
||||
}
|
||||
else {
|
||||
// TODO : for testing : this initialization should be done earlier,
|
||||
// as soon as the ServiceWorker is ready.
|
||||
// This can probably been done by listening to state change :
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange
|
||||
console.log("try to post an init message to ServiceWorker");
|
||||
console.log("messageChannel :", messageChannel);
|
||||
navigator.serviceWorker.controller.postMessage({'action': 'init'}, [messageChannel.port2]);
|
||||
console.log("init message sent to ServiceWorker");
|
||||
}
|
||||
}
|
||||
|
||||
// Apply Mediawiki CSS only when it's an Evopedia archive
|
||||
if (selectedArchive.needsWikimediaCSS() === true) {
|
||||
@ -837,17 +900,14 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction
|
||||
var currentPath = regexpPath.exec(currentHref)[1];
|
||||
$('#articleContent').contents().find('head').append("<link rel='stylesheet' type='text/css' href='" + currentPath + "css/mediawiki-main.css' id='mediawiki-stylesheet' />");
|
||||
}
|
||||
else {
|
||||
// TODO temporary test to inject CSS inside the iframe
|
||||
$('#articleContent').contents().find('head').empty();
|
||||
$('#articleContent').contents().find('head').append("<link rel='stylesheet' href='data:text/css;charset=UTF-8," + encodeURIComponent("body {background: #E9E9E9;}") + "' />");
|
||||
}
|
||||
|
||||
// Display the article inside the web page.
|
||||
$('#articleContent').contents().find('body').html(htmlArticle);
|
||||
|
||||
|
||||
// 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 (selectedArchive.needsWikimediaCSS() || !isServiceWorkerReady() || !isMessageChannelAvailable()) {
|
||||
if (contentInjectionMode === 'jquery') {
|
||||
|
||||
// Convert links into javascript calls
|
||||
$('#articleContent').contents().find('body').find('a').each(function() {
|
||||
@ -906,18 +966,71 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load math images
|
||||
$('#articleContent').contents().find('body').find('img').each(function() {
|
||||
var image = $(this);
|
||||
var m = image.attr("src").match(regexpMathImageUrl);
|
||||
if (m) {
|
||||
selectedArchive.loadMathImage(m[1], function(data) {
|
||||
image.attr("src", 'data:image/png;base64,' + data);
|
||||
});
|
||||
}
|
||||
});
|
||||
// Load images
|
||||
$('#articleContent').contents().find('body').find('img').each(function() {
|
||||
var image = $(this);
|
||||
var m = image.attr("src").match(regexpMathImageUrl);
|
||||
if (m) {
|
||||
// It's a math image (Evopedia archive)
|
||||
selectedArchive.loadMathImage(m[1], function(data) {
|
||||
image.attr("src", 'data:image/png;base64,' + data);
|
||||
});
|
||||
} else {
|
||||
// It's a standard image contained in the ZIM file
|
||||
var imageMatch = image.attr("src").match(regexpImageUrl);
|
||||
if (imageMatch) {
|
||||
selectedArchive.getTitleByName(imageMatch[1]).then(function(title) {
|
||||
selectedArchive.readBinaryFile(title, function (readableTitleName, content) {
|
||||
// TODO : add the complete MIME-type of the image (as read from the ZIM file)
|
||||
image.attr("src", 'data:image;base64,' + util.uint8ArrayToBase64(content));
|
||||
});
|
||||
}).fail(function () {
|
||||
console.error("could not find title for image:" + imageMatch[1]);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Load CSS content
|
||||
$('#articleContent').contents().find('link[rel=stylesheet]').each(function() {
|
||||
var link = $(this);
|
||||
var hrefMatch = link.attr("href").match(regexpMetadataUrl);
|
||||
if (hrefMatch) {
|
||||
// It's a CSS file contained in the ZIM file
|
||||
var titleName = util.removeUrlParameters(hrefMatch[1]);
|
||||
selectedArchive.getTitleByName(titleName).then(function(title) {
|
||||
selectedArchive.readBinaryFile(title, function (readableTitleName, content) {
|
||||
var cssContent = encodeURIComponent(util.uintToString(content));
|
||||
link.attr("href", 'data:text/css;charset=UTF-8,' + cssContent);
|
||||
});
|
||||
}).fail(function () {
|
||||
console.error("could not find title for CSS : " + hrefMatch[1]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Load Javascript content
|
||||
$('#articleContent').contents().find('script').each(function() {
|
||||
var script = $(this);
|
||||
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 titleName = util.removeUrlParameters(srcMatch[1]);
|
||||
selectedArchive.getTitleByName(titleName).then(function(title) {
|
||||
selectedArchive.readBinaryFile(title, function (readableTitleName, 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 () {
|
||||
console.error("could not find title for javascript : " + srcMatch[1]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -159,6 +159,64 @@ define(['q'], function(q) {
|
||||
return mid;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a Base64 Content to a Blob
|
||||
* From https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript
|
||||
*
|
||||
* @param {String} b64Data Base64-encoded data
|
||||
* @param {String} contentType
|
||||
* @param {Integer} sliceSize
|
||||
* @returns {Blob}
|
||||
*/
|
||||
function b64toBlob(b64Data, contentType, sliceSize) {
|
||||
contentType = contentType || '';
|
||||
sliceSize = sliceSize || 512;
|
||||
|
||||
var byteCharacters = atob(b64Data);
|
||||
var byteArrays = [];
|
||||
|
||||
for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
||||
var slice = byteCharacters.slice(offset, offset + sliceSize);
|
||||
|
||||
var byteNumbers = new Array(slice.length);
|
||||
for (var i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i);
|
||||
}
|
||||
|
||||
var byteArray = new Uint8Array(byteNumbers);
|
||||
|
||||
byteArrays.push(byteArray);
|
||||
}
|
||||
|
||||
var blob = new Blob(byteArrays, {type: contentType});
|
||||
return blob;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a UInt Array to a UTF-8 encoded string
|
||||
* source : http://michael-rushanan.blogspot.de/2014/03/javascript-uint8array-hacks-and-cheat.html
|
||||
*
|
||||
* @param {UIntArray} uintArray
|
||||
* @returns {String}
|
||||
*/
|
||||
function uintToString(uintArray) {
|
||||
var s = '';
|
||||
for (var i = 0; i < uintArray.length; i++) {
|
||||
s += String.fromCharCode(uintArray[i]);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
var regexpRemoveUrlParameters = new RegExp(/([^\?]+)\?.*$/);
|
||||
|
||||
function removeUrlParameters(url) {
|
||||
if (regexpRemoveUrlParameters.test(url)) {
|
||||
return regexpRemoveUrlParameters.exec(url)[1];
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Functions and classes exposed by this module
|
||||
@ -171,6 +229,9 @@ define(['q'], function(q) {
|
||||
uint8ArrayToHex : uint8ArrayToHex,
|
||||
uint8ArrayToBase64 : uint8ArrayToBase64,
|
||||
readFileSlice : readFileSlice,
|
||||
binarySearch: binarySearch
|
||||
binarySearch: binarySearch,
|
||||
b64toBlob: b64toBlob,
|
||||
uintToString: uintToString,
|
||||
removeUrlParameters: removeUrlParameters
|
||||
};
|
||||
});
|
||||
|
@ -200,6 +200,8 @@ define(['zimfile', 'zimDirEntry', 'util', 'utf8'],
|
||||
callback(title.name(), data);
|
||||
});
|
||||
};
|
||||
|
||||
var regexpTitleNameWithoutNameSpace = /^[^\/]+$/;
|
||||
|
||||
/**
|
||||
* Searches a title (article / page) by name.
|
||||
@ -208,6 +210,10 @@ define(['zimfile', 'zimDirEntry', 'util', 'utf8'],
|
||||
*/
|
||||
ZIMArchive.prototype.getTitleByName = function(titleName) {
|
||||
var that = this;
|
||||
// If no namespace is mentioned, it's an article, and we have to add it
|
||||
if (regexpTitleNameWithoutNameSpace.test(titleName)) {
|
||||
titleName= "A/" + titleName;
|
||||
}
|
||||
return util.binarySearch(0, this._file.articleCount, function(i) {
|
||||
return that._file.dirEntryByUrlIndex(i).then(function(dirEntry) {
|
||||
var url = dirEntry.namespace + "/" + dirEntry.url;
|
||||
|
Loading…
x
Reference in New Issue
Block a user