Skip to content

Commit

Permalink
added toc dropup
Browse files Browse the repository at this point in the history
  • Loading branch information
heropj committed Feb 9, 2025
1 parent 47db0e7 commit fe16526
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 0 deletions.
29 changes: 29 additions & 0 deletions www/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,35 @@ iframe._invert, iframe._mwInvert {
font-weight: bold;
}

.btn-group.btn-block {
padding-left: 5%;
}

.dropdown-menu.flex-container {
overflow-y:auto;
padding: 2px 4px;
position: absolute;
margin: 0 !important;
}

/* to fix its alignment with other buttons */
.dropup button{
padding-top: 12px;
}
@media (max-width:768px) {
.dropup button{
padding-top: 0px;
}
}

#ToCList{
line-height: 1.7;
}

#ToCList a {
display: block;
}

.container {
margin-bottom: 1.5em;
}
Expand Down
7 changes: 7 additions & 0 deletions www/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,13 @@ <h3 data-i18n="configure-expert-settings-title">Expert settings</h3>
</div>
</div>
<div id="navigationButtons" class="btn-group btn-block">
<div class="dropup">
<button class="btn btn-lg dropdown-toggle col-xs-4" id="dropup" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true" style="font-size: 14px;">
<i class="fas fa-list-alt fa-lg"></i>
</button>
<ul id="ToCList" class="dropdown-menu flex-container"></ul>
</div>
<a href="#" data-i18n-tip="home" class="btn btn-lg" id="btnHomeBottom" title="Home"><i class="fas fa-home"></i></a>
<a href="#" class="btn btn-lg" data-i18n-tip="home-btn-back" id="btnBack" title="Back"><i class="fas fa-arrow-left"></i></a>
<a href="#" class="btn btn-lg" data-i18n-tip="home-btn-forward" id="btnForward" title="Forward"><i class="fas fa-arrow-right"></i></a>
Expand Down
109 changes: 109 additions & 0 deletions www/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3228,6 +3228,115 @@ 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 iframe = document.getElementById('articleContent');
var ToCList = document.getElementById('ToCList');

// Function to close TOC
function closeTOC () {
ToCList.style.display = 'none';
}
// close TOC when 'click' is not on dropup or TOCList
iframe.addEventListener('load', function () {
var innerDoc = iframe.contentDocument || iframe.contentWindow.document;
// Add click event listener inside the iframe
innerDoc.addEventListener('click', function () {
closeTOC(); // Close TOC when clicking inside the iframe
});
});
document.addEventListener('click', function (event) {
if (!dropup.contains(event.target) && !ToCList.contains(event.target)) {
closeTOC();
}
});

dropup.addEventListener('click', function () {
const isVisible = getComputedStyle(ToCList).display !== 'none';
if (isVisible) {
ToCList.style.display = 'none';
} else {
setupTableOfContents();
ToCList.style.display = 'flex';
ToCList.style.flexDirection = 'column';
}
});

// 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 = '';
headings.forEach(function (heading) {
if (/^h1$/i.test(heading.tagName)) {
dropupHtml += '<li style="font-size:' + 18 + 'px;"><a style="color: black;" href="#" data-heading-id="' + heading.id + '">' + heading.textContent + '</a></li>';
} else if (/^h2$/i.test(heading.tagName)) {
dropupHtml += '<li style="margin-top:6px;margin-left:6px;font-size:' + 16 + 'px;"><a style="color: black;" href="#" data-heading-id="' + heading.id + '">' + heading.textContent + '</a></li>';
} else if (/^h3$/i.test(heading.tagName)) {
dropupHtml += '<li style="margin-left:12px;font-weight:350;font-size:' + 14 + 'px;"><a style="color: black;" href="#" data-heading-id="' + heading.id + '">' + heading.textContent + '</a></li>';
} else if (/^h4$/i.test(heading.tagName)) {
dropupHtml += '<li style="margin-left:16px;font-weight:300;font-size:' + 12 + 'px;"><a style="color: black;" href="#" data-heading-id="' + heading.id + '">' + heading.textContent + '</a></li>';
}
// 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.innerHTML = dropupHtml;
Array.from(ToCList.getElementsByTagName('a'))
.forEach(function (listElement) {
listElement.addEventListener('click', function () {
var sectionEle = innerDoc.getElementById(this.dataset.headingId)

var sectionsToOpen = getParentSections(sectionEle); // get all parents which are 'section' or 'details'
openSection(sectionsToOpen); // open all parents
// why..? because if the section is inside a details element, it will be closed by default

sectionEle.scrollIntoView();

// highlighting the section
sectionEle.style.backgroundColor = '#bdd1e5';
setTimeout(function () {
sectionEle.style.backgroundColor = '';
}, 400);
sectionEle.style.transition = 'background-color 300ms ease-out';
});
});
}

// get all parent elements which are 'section' or 'details'
function getParentSections (element) {
const parents = [];
let currentElement = element;
while (currentElement) {
if (currentElement.matches('section, details')) {
parents.push(currentElement);
}
currentElement = currentElement.parentElement;
}
return parents;
};

// Function to open a specific section and all its parent sections
function openSection (sectionsToOpen) {
if (!sectionsToOpen) return;
sectionsToOpen.forEach(section => {
if (section.tagName === 'DETAILS') {
section.setAttribute('open', '');
} else if (section.tagName === 'SECTION') {
section.style.display = '';
Array.from(section.children).forEach(child => {
if (!/SUMMARY|H\d/.test(child.tagName)) {
child.style.display = '';
}
});
}
});
};

/**
* Extracts the content of the given article pathname, or a downloadable file, from the ZIM
*
Expand Down
38 changes: 38 additions & 0 deletions www/js/lib/uiUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,43 @@ 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 = {};

if (element.id) {
obj.id = element.id;
} else {
// generating custom id if id attribute is not present in element
var generatedId = element.textContent
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
obj.id = 'pph-' + i + '-' + generatedId;
element.id = obj.id; // to target the element
}
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
*
Expand Down Expand Up @@ -1083,6 +1120,7 @@ export default {
determineCanvasElementsWorkaround: determineCanvasElementsWorkaround,
replaceCSSLinkWithInlineCSS: replaceCSSLinkWithInlineCSS,
deriveZimUrlFromRelativeUrl: deriveZimUrlFromRelativeUrl,
TOC: TableOfContents,
removeUrlParameters: removeUrlParameters,
displayActiveContentWarning: displayActiveContentWarning,
displayFileDownloadAlert: displayFileDownloadAlert,
Expand Down

0 comments on commit fe16526

Please # to comment.