Skip to content

Commit

Permalink
Support epub and other downloads from ZIM in jQuery mode #439 (#462)
Browse files Browse the repository at this point in the history
Closes #439.
Jaifroid authored Mar 3, 2019
1 parent d1d5d24 commit dd0d55b
Showing 3 changed files with 98 additions and 8 deletions.
2 changes: 2 additions & 0 deletions www/index.html
Original file line number Diff line number Diff line change
@@ -283,6 +283,8 @@ <h3>Expert settings</h3>
<iframe id="articleContent" class="articleIFrame" src="article.html"></iframe>
</article>
<footer>
<!-- Bootstrap alert box -->
<div id="alertBoxFooter"></div>
<div id="navigationButtons" class="btn-toolbar">
<div class="btn-group btn-group-justified">
<a class="btn btn-lg" id="btnHomeBottom" title="Home"><span class="glyphicon glyphicon-home"></span></a>
45 changes: 38 additions & 7 deletions www/js/app.js
Original file line number Diff line number Diff line change
@@ -78,6 +78,7 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies','abstractFiles
// Define behavior of HTML elements
$('#searchArticles').on('click', function(e) {
$("#welcomeText").hide();
$('.alert').hide();
$("#searchingArticles").show();
pushBrowserHistoryState(null, $('#prefix').val());
searchDirEntriesFromPrefix($('#prefix').val());
@@ -857,6 +858,11 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies','abstractFiles
// common in unsupported packaged UIs, e.g. PhET ZIMs.
var regexpActiveContent = /<script\b(?:(?![^>]+src\b)|(?=[^>]+src\b=["'][^"']+?app\.js))(?!>[^<]+(?:importScript\(\)|toggleOpenSection))(?![^>]+type\s*=\s*["'](?:math\/|[^"']*?math))/i;

// DEV: The regex below matches ZIM links (anchor hrefs) that should have the html5 "donwnload" attribute added to
// the link. This is currently the case for epub and pdf files in Project Gutenberg ZIMs -- add any further types you need
// to support to this regex. The "zip" has been added here as an example of how to support further filetypes
var regexpDownloadLinks = /^.*?\.epub($|\?)|^.*?\.pdf($|\?)|^.*?\.zip($|\?)/i;

// Cache for CSS styles contained in ZIM.
// It significantly speeds up subsequent page display. See kiwix-js issue #335
var cssCache = new Map();
@@ -890,6 +896,9 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies','abstractFiles

// Tell jQuery we're removing the iframe document: clears jQuery cache and prevents memory leaks [kiwix-js #361]
$('#articleContent').contents().remove();

// Remove from DOM any download alert box that was activated in uiUtil.displayFileDownloadAlert function
$('#downloadAlert').alert('close');

var iframeArticleContent = document.getElementById('articleContent');

@@ -961,12 +970,26 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies','abstractFiles
// It's an external URL : we should open it in a new tab
this.target = "_blank";
} else {
// It's a link to another article
// Add an onclick event to go to this article
// It's a link to an article or file in the ZIM
var decodedURL = decodeURIComponent(zimUrl);
var contentType;
var downloadAttrValue;
// Some file types need to be downloaded rather than displayed (e.g. *.epub)
// The HTML download attribute can be Boolean or a string representing the specified filename for saving the file
// For Boolean values, getAttribute can return any of the following: download="" download="download" download="true"
// So we need to test hasAttribute first: see https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute
// However, we cannot rely on the download attribute having been set, so we also need to test for known download file types
var isDownloadableLink = this.hasAttribute('download') || regexpDownloadLinks.test(href);
if (isDownloadableLink) {
downloadAttrValue = this.getAttribute('download');
// Normalize the value to a true Boolean or a filename string or true if there is no download attribute
downloadAttrValue = /^(download|true|\s*)$/i.test(downloadAttrValue) || downloadAttrValue || true;
contentType = this.getAttribute('type');
}
// Add an onclick event to extract this article or file from the ZIM
// instead of following the link
$(this).on('click', function (e) {
var decodedURL = decodeURIComponent(zimUrl);
goToArticle(decodedURL);
goToArticle(decodedURL, downloadAttrValue, contentType);
return false;
});
}
@@ -1159,16 +1182,24 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies','abstractFiles


/**
* Replace article content with the one of the given title
* @param {String} title
* Extracts the content of the given article title, or a downloadable file, from the ZIM
*
* @param {String} title The path and filename to the article or file to be extracted
* @param {Boolean|String} download A Bolean value that will trigger download of title, or the filename that should
* be used to save the file in local FS (in HTML5 spec, a string value for the download attribute is optional)
* @param {String} contentType The mimetype of the downloadable file, if known
*/
function goToArticle(title) {
function goToArticle(title, download, contentType) {
$("#searchingArticles").show();
title = uiUtil.removeUrlParameters(title);
selectedArchive.getDirEntryByTitle(title).then(function(dirEntry) {
if (dirEntry === null || dirEntry === undefined) {
$("#searchingArticles").hide();
alert("Article with title " + title + " not found in the archive");
} else if (download) {
selectedArchive.readBinaryFile(dirEntry, function (fileDirEntry, content) {
uiUtil.displayFileDownloadAlert(title, download, contentType, content);
});
} else {
params.isLandingPage = false;
$('#activeContent').alert('close');
59 changes: 58 additions & 1 deletion www/js/lib/uiUtil.js
Original file line number Diff line number Diff line change
@@ -119,13 +119,70 @@ define([], function() {
});
}

/**
* Displays a Bootstrap alert box at the foot of the page to enable saving the content of the given title to the device's filesystem
* and initiates download/save process if this is supported by the OS or Browser
*
* @param {String} title The path and filename to the file to be extracted
* @param {Boolean|String} download A Bolean value that will trigger download of title, or the filename that should
* be used to save the file in local FS
* @param {String} contentType The mimetype of the downloadable file, if known
* @param {Uint8Array} content The binary-format content of the downloadable file
*/
function displayFileDownloadAlert(title, download, contentType, content) {
// We have to create the alert box in code, because Bootstrap removes it completely from the DOM when the user dismisses it
document.getElementById('alertBoxFooter').innerHTML =
'<div id="downloadAlert" class="alert alert-info alert-dismissible">' +
' <a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>' +
' <span id="alertMessage"></span>' +
'</div>';
// Download code adapted from https://stackoverflow.com/a/19230668/9727685
if (!contentType) {
// DEV: Add more contentTypes here for downloadable files
if (/\.epub$/.test(title)) contentType = 'application/epub+zip';
if (/\.pdf$/.test(title)) contentType = 'application/pdf';
if (/\.zip$/.test(title)) contentType = 'application/zip';
}
// Set default contentType if there has been no match
if (!contentType) contentType = 'application/octet-stream';
var a = document.createElement('a');
var blob = new Blob([content], { 'type': contentType });
// If the filename to use for saving has not been specified, construct it from title
var filename = download === true ? title.replace(/^.*\/([^\/]+)$/, '$1') : download;
// Make filename safe
filename = filename.replace(/[\/\\:*?"<>|]/g, '_');
a.href = window.URL.createObjectURL(blob);
a.target = '_blank';
a.type = contentType;
a.download = filename;
a.classList.add('alert-link');
a.innerHTML = filename;
var alertMessage = document.getElementById('alertMessage');
alertMessage.innerHTML = '<strong>Download</strong> If the download does not start, please tap the following link: ';
// We have to add the anchor to a UI element for Firefox to be able to click it programmatically: see https://stackoverflow.com/a/27280611/9727685
alertMessage.appendChild(a);
try { a.click(); }
catch (err) {
// If the click fails, user may be able to download by manually clicking the link
// But for IE11 we need to force use of the saveBlob method with the onclick event
if (window.navigator && window.navigator.msSaveBlob) {
a.addEventListener('click', function(e) {
window.navigator.msSaveBlob(blob, filename);
e.preventDefault();
});
}
}
$("#searchingArticles").hide();
}

/**
* Functions and classes exposed by this module
*/
return {
feedNodeWithBlob: feedNodeWithBlob,
replaceCSSLinkWithInlineCSS: replaceCSSLinkWithInlineCSS,
removeUrlParameters: removeUrlParameters,
displayActiveContentWarning: displayActiveContentWarning
displayActiveContentWarning: displayActiveContentWarning,
displayFileDownloadAlert: displayFileDownloadAlert
};
});

0 comments on commit dd0d55b

Please sign in to comment.