diff --git a/www/css/app.css b/www/css/app.css
index 3c7e365ca..9f019949c 100644
--- a/www/css/app.css
+++ b/www/css/app.css
@@ -210,6 +210,20 @@ iframe._invert, iframe._mwInvert {
height: 50px;
}
+.btn-group.btn-block {
+ padding-left: 5%;
+}
+
+.dropdown-menu.flex-container {
+ overflow-y:auto;
+ padding-top: 10px;
+ padding-bottom: 10px;
+ padding-right: 10px;
+ padding-left: 10px;
+ position: absolute;
+ left: 10px;
+}
+
.status {
position: absolute;
top: 50%;
diff --git a/www/index.html b/www/index.html
index e25b8642a..543f56607 100644
--- a/www/index.html
+++ b/www/index.html
@@ -788,8 +788,14 @@
Expert settings
-
-
+
+
+
diff --git a/www/js/app.js b/www/js/app.js
index 03ab1aea9..a91c084a3 100644
--- a/www/js/app.js
+++ b/www/js/app.js
@@ -3061,6 +3061,67 @@ function pushBrowserHistoryState (title, titleSearch) {
window.history.pushState(stateObj, stateLabel, urlParameters);
}
+// Setup table of contents and display the list when the dropup button is clicked
+var dropup = document.getElementById('dropup');
+dropup.setAttribute('tabindex', '0');
+var ToCList = document.getElementById('ToCList');
+dropup.addEventListener('click', function () {
+ if (ToCList.style.display !== 'none') {
+ ToCList.style.display = 'none';
+ } else {
+ setupTableOfContents();
+ ToCList.style.display = 'block';
+ dropup.style.fontSize = '14px';
+ }
+});
+
+dropup.addEventListener('blur', function () {
+ setTimeout(() => {
+ if (ToCList.style.display === 'block') ToCList.style.display = 'none';
+ }, 200);
+});
+
+// Inject table of contents list into dropup element and scroll selection into view
+function setupTableOfContents () {
+ var iframe = document.getElementById('articleContent');
+ var innerDoc = iframe.contentDocument;
+ var tableOfContents = new uiUtil.TOC(innerDoc);
+ var headings = tableOfContents.getHeadingObjects();
+
+ var dropupHtml = '';
+ params.relativeFontSize = 100;
+ headings.forEach(function (heading) {
+ if (/^h1$/i.test(heading.tagName)) {
+ dropupHtml += '
' + heading.textContent + '';
+ } else if (/^h2$/i.test(heading.tagName)) {
+ dropupHtml += '
' + heading.textContent + '';
+ } else if (/^h3$/i.test(heading.tagName)) {
+ dropupHtml += '
' + heading.textContent + '';
+ } else if (/^h4$/i.test(heading.tagName)) {
+ dropupHtml += '
' + heading.textContent + '';
+ }
+ // Skip smaller headings (if there are any) to avoid making list too long
+ });
+ var ToCList = document.getElementById('ToCList');
+ ToCList.style.maxHeight = ~~(window.innerHeight * 0.75) + 'px';
+ ToCList.style.marginLeft = '-5% !important';
+ ToCList.innerHTML = dropupHtml;
+ Array.prototype.slice.call(ToCList.getElementsByTagName('a')).forEach(function (listElement) {
+ listElement.addEventListener('click', function () {
+ var sectionEle = innerDoc.getElementById(this.dataset.headingId);
+ // Scroll to element
+ sectionEle.scrollIntoView();
+ // Scrolling up then down ensures that the toolbars show according to user settings
+ iframe.contentWindow.scrollBy(0, -5);
+ setTimeout(function () {
+ iframe.contentWindow.scrollBy(0, 5);
+ iframe.contentWindow.focus();
+ }, 150);
+ ToCList.style.display = 'none';
+ });
+ });
+}
+
/**
* Extracts the content of the given article pathname, or a downloadable file, from the ZIM
*
diff --git a/www/js/lib/uiUtil.js b/www/js/lib/uiUtil.js
index 20a2b8a82..021137ea6 100644
--- a/www/js/lib/uiUtil.js
+++ b/www/js/lib/uiUtil.js
@@ -172,6 +172,32 @@ function slideAway (e) {
}
}
+/*
+ * Returns a list of headings from an article
+ * @param {String} the page for which table of cotents needs to be listed
+ * @returns {List} a list of all headings as objects
+*/
+function TableOfContents (articleDoc) {
+ this.doc = articleDoc;
+ this.headings = this.doc.querySelectorAll('h1, h2, h3, h4, h5, h6');
+
+ this.getHeadingObjects = function () {
+ var headings = [];
+ for (var i = 0; i < this.headings.length; i++) {
+ var element = this.headings[i];
+ var obj = {};
+ obj.id = element.id;
+ var objectId = element.innerHTML.match(/\bid\s*=\s*["']\s*([^"']+?)\s*["']/i);
+ obj.id = obj.id ? obj.id : objectId && objectId.length > 1 ? objectId[1] : '';
+ obj.index = i;
+ obj.textContent = element.textContent;
+ obj.tagName = element.tagName;
+ headings.push(obj);
+ }
+ return headings;
+ };
+}
+
/**
* Displays a Bootstrap alert or confirm dialog box depending on the options provided
*
@@ -1065,6 +1091,7 @@ export default {
determineCanvasElementsWorkaround: determineCanvasElementsWorkaround,
replaceCSSLinkWithInlineCSS: replaceCSSLinkWithInlineCSS,
deriveZimUrlFromRelativeUrl: deriveZimUrlFromRelativeUrl,
+ TOC: TableOfContents,
removeUrlParameters: removeUrlParameters,
displayActiveContentWarning: displayActiveContentWarning,
displayFileDownloadAlert: displayFileDownloadAlert,