412 lines
10 KiB
JavaScript
Executable File
412 lines
10 KiB
JavaScript
Executable File
window.addEventListener('load', init);
|
|
window.addEventListener('resize', reactToResizing);
|
|
window.addEventListener('hashchange', resumeOrGoToStart);
|
|
|
|
let slides;
|
|
let currentSlide;
|
|
|
|
let topicList;
|
|
let topicLinks;
|
|
let topicListSearch;
|
|
|
|
let pageNumberContainer;
|
|
|
|
const activeTopicLinkClass = 'active-topic-link';
|
|
const notMatchingSearchTermsClass = 'not-matching-search-terms';
|
|
const topicListSearchClass = 'topic-list-search';
|
|
|
|
function init() {
|
|
slides = Array.from(document.querySelectorAll('article'));
|
|
for (let i = 0; i < slides.length; i++) {
|
|
slides[i].id = `slide${i}`;
|
|
}
|
|
topicList = createTopicList();
|
|
pageNumberContainer = document.getElementById('page-number-container');
|
|
resumeOrGoToStart();
|
|
document.addEventListener('keydown', onKeyPressed);
|
|
}
|
|
|
|
function resumeOrGoToStart() {
|
|
let urlParts = window.location.href.split('#');
|
|
if (urlParts.length > 1) {
|
|
goToSlide(parseInt(urlParts[1].replace('slide', '')))
|
|
} else {
|
|
goToSlide(0)
|
|
}
|
|
}
|
|
|
|
function goToSlide(index) {
|
|
if (index >= 0 && index < slides.length) {
|
|
currentSlide = index;
|
|
window.location.href = window.location.href.split('#')[0] + `#slide${index}`;
|
|
|
|
let oldActiveTopicLinks = topicList.querySelectorAll(`.${activeTopicLinkClass}`);
|
|
[...oldActiveTopicLinks].forEach(link => {
|
|
link.classList.remove(activeTopicLinkClass);
|
|
});
|
|
|
|
let newActiveTopicLinks = topicList.querySelectorAll(`[data-slide*=";slide${index};"]`);
|
|
[...newActiveTopicLinks].forEach(link => {
|
|
link.classList.add(activeTopicLinkClass);
|
|
})
|
|
|
|
if (pageNumberContainer) {
|
|
pageNumberContainer.textContent = index;
|
|
}
|
|
}
|
|
}
|
|
|
|
function goToPreviousSlide() {
|
|
if (!showPreviousFragment()) {
|
|
goToSlide(currentSlide - 1);
|
|
}
|
|
}
|
|
|
|
function goToNextSlide() {
|
|
if (!showNextFragment()) {
|
|
goToSlide(currentSlide + 1);
|
|
}
|
|
}
|
|
|
|
function goToFirstSlide() {
|
|
goToSlide(0);
|
|
}
|
|
|
|
function goToLastSlide() {
|
|
goToSlide(slides.length - 1);
|
|
}
|
|
|
|
function onKeyPressed(event) {
|
|
switch (event.keyCode) {
|
|
case 34: // page down
|
|
case 40: // arrow down
|
|
case 39: // arrow right
|
|
goToNextSlide();
|
|
break;
|
|
case 33: // page up
|
|
case 38: // arrow up
|
|
case 37: // arrow left
|
|
goToPreviousSlide();
|
|
break;
|
|
case 36: // pos1
|
|
goToFirstSlide();
|
|
break;
|
|
case 35: // end
|
|
goToLastSlide();
|
|
break;
|
|
case 17: // ctrl
|
|
toggleTopicList();
|
|
break;
|
|
case 27: // esc
|
|
hideTopicList();
|
|
hideHelpMenu();
|
|
break;
|
|
case 72: // H
|
|
onHPressed();
|
|
break;
|
|
case 80: // P
|
|
onPPressed();
|
|
break;
|
|
}
|
|
}
|
|
|
|
function onHPressed() {
|
|
if (!(document.activeElement === topicListSearch))
|
|
toggleHelpMenu();
|
|
}
|
|
|
|
function onPPressed() {
|
|
if (!(document.activeElement === topicListSearch))
|
|
togglePointer();
|
|
}
|
|
|
|
function showNextFragment() {
|
|
let fragments = [...slides[currentSlide].querySelectorAll('.fragment')]
|
|
let visible = [...slides[currentSlide].querySelectorAll('.fragment.visible')]
|
|
|
|
if (fragments.length == visible.length) {
|
|
return false;
|
|
} else {
|
|
fragments[visible.length].classList.add('visible')
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function showPreviousFragment() {
|
|
let visible = [...slides[currentSlide].querySelectorAll('.fragment.visible')]
|
|
|
|
if (visible.length == 0) {
|
|
return false;
|
|
} else {
|
|
visible[visible.length - 1].classList.remove('visible')
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Store for the headline nodes of each chapter.
|
|
*/
|
|
class HeadlineStore {
|
|
constructor() {
|
|
this.topLevelChildren = [];
|
|
}
|
|
|
|
/**
|
|
* Add a new root node, i.e. the headline of a new chapter.
|
|
* @param {Node} node the level-1 headline node
|
|
*/
|
|
add(node) {
|
|
this.topLevelChildren.push(node);
|
|
}
|
|
|
|
/**
|
|
* Generate a HTML list of all headline nodes stored in this structure.
|
|
*/
|
|
getHTML() {
|
|
let resultHTML = '<ul>'
|
|
|
|
resultHTML += this.topLevelChildren
|
|
.map(root => root.getHTML())
|
|
.join('');
|
|
|
|
return resultHTML + '</ul>'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Data structure storing all headlines for the topic list.
|
|
* Each node knows its parents and children.
|
|
*/
|
|
class Node {
|
|
constructor(title, level, father, slide) {
|
|
this.children = [];
|
|
this.slidesWithSameTitle = [];
|
|
this.title = title;
|
|
this.level = level;
|
|
this.father = father;
|
|
this.slide = slide;
|
|
}
|
|
|
|
/**
|
|
* Return the child that was added last among all childen.
|
|
*/
|
|
getLastChild() {
|
|
return this.children[this.children.length - 1];
|
|
}
|
|
|
|
/**
|
|
* Add a child node to this headline if the title of the new child node is
|
|
* different from the title of the node that was added last.
|
|
* This way, headlines are not added twice if they appear consecutively.
|
|
* @param {Node} node a subtitle node
|
|
* @return true, if the node was added, false otherwise
|
|
*/
|
|
addChildNode(node) {
|
|
if (!this.lastChildHasTitle(node.title)) {
|
|
this.children.push(node);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Each headline can have more than one slide related to it.
|
|
* Consecutive headlines appear only ones in a dictionary, but they are still
|
|
* related to all slides. If there are three slides with title »Example« in a
|
|
* row, the node will have slide1 as its child and slide2 and slide3 as
|
|
* entries to the slidesWithSameTitle array.
|
|
* The goal of this is to ultimately highlight the node in the topic list not
|
|
* only when slide1 is active, but also for slide2 and slide3.
|
|
* @param {Integer} slide the ID of a related slide
|
|
*/
|
|
addSlideWithSameTitle(slide) {
|
|
this.slidesWithSameTitle.push(slide);
|
|
}
|
|
|
|
/**
|
|
* Whether or not this node's last child has the given title.
|
|
* @param {String} title the slide's title
|
|
*/
|
|
lastChildHasTitle(title) {
|
|
let len = this.children.length;
|
|
return len > 0 && this.children[len - 1].title == title;
|
|
}
|
|
|
|
/**
|
|
* Generate the HTML anchor content for this node (without any children).
|
|
*/
|
|
getLinkHTML() {
|
|
return `<a data-slide=";slide${this.slide};${
|
|
// Slide IDs have to be surrounded by »;«, otherwise slide10 and slide100
|
|
// would both be highlighted when slide10 is active.
|
|
this.slidesWithSameTitle.map(slide => `slide${slide}`).join(';')
|
|
};" href="javascript:goToSlide(${this.slide})" class="topic-link">${this.title}</a>`
|
|
}
|
|
|
|
/**
|
|
* Generate a list item for the topic list with a sublist of all child nodes.
|
|
*/
|
|
getHTML() {
|
|
let resultHTML = `<li>${this.getLinkHTML()}`;
|
|
|
|
if (this.children.length > 0) {
|
|
resultHTML +=
|
|
`<ul>${this.children.map(child => child.getHTML()).join('')}</ul>`
|
|
}
|
|
|
|
return resultHTML + '</li>';
|
|
}
|
|
}
|
|
|
|
function getTopicListContent() {
|
|
let results = [...document.querySelectorAll('section h1, section h2, section h3, section h4, section h5, section h6')];
|
|
let resultHTML = `<input type="text" class="${topicListSearchClass}"/>`;
|
|
let currentLevel = 1;
|
|
|
|
headlineStore = new HeadlineStore();
|
|
currentNode = null;
|
|
|
|
results.forEach(element => {
|
|
let node = element.nodeName;
|
|
let level = parseInt(node[node.length - 1]);
|
|
let title = element.textContent;
|
|
|
|
let parentElement = element.parentElement
|
|
while (parentElement.nodeName.toLowerCase() != 'article') {
|
|
parentElement = parentElement.parentElement;
|
|
}
|
|
|
|
let slide = parseInt(parentElement.id.replace('slide', ''));
|
|
|
|
if (level == 1) {
|
|
// first chapter
|
|
let rootNode = new Node(title, level, null, slide);
|
|
headlineStore.add(rootNode)
|
|
currentNode = rootNode;
|
|
} else if (level > currentLevel) {
|
|
// one or more levels down
|
|
let childNode = new Node(title, level, currentNode, slide);
|
|
if (currentNode.addChildNode(childNode)) {
|
|
currentNode = childNode;
|
|
} else {
|
|
currentNode = currentNode.getLastChild();
|
|
currentNode.addSlideWithSameTitle(slide)
|
|
}
|
|
} else if (level == currentLevel) {
|
|
// same level
|
|
let parentNode = currentNode.father;
|
|
let siblingNode = new Node(title, level, parentNode, slide);
|
|
if (parentNode.addChildNode(siblingNode)) {
|
|
currentNode = siblingNode;
|
|
} else {
|
|
currentNode = parentNode.getLastChild();
|
|
currentNode.addSlideWithSameTitle(slide)
|
|
}
|
|
} else if (level < currentLevel) {
|
|
// one or more levels up
|
|
let parentNode = currentNode
|
|
while (parentNode.level >= level) {
|
|
parentNode = parentNode.father;
|
|
}
|
|
let siblingNode = new Node(title, level, parentNode, slide);
|
|
if (parentNode.addChildNode(siblingNode)) {
|
|
currentNode = siblingNode;
|
|
} else {
|
|
currentNode = parentNode.getLastChild();
|
|
}
|
|
}
|
|
|
|
currentLevel = level;
|
|
});
|
|
|
|
return resultHTML + headlineStore.getHTML();
|
|
}
|
|
|
|
function createTopicList() {
|
|
let topicList = document.createElement(`div`);
|
|
topicList.innerHTML = getTopicListContent();
|
|
topicList.classList.add('topic-list')
|
|
document.body.appendChild(topicList);
|
|
topicList.style.position = 'fixed';
|
|
topicList.style.top = '4rem';
|
|
topicList.style.left = '50%';
|
|
topicList.style.transform = 'translateX(-50%)';
|
|
topicList.style.backgroundColor = 'white';
|
|
topicList.style.padding = '2rem';
|
|
topicList.style.width = '800px';
|
|
topicList.style.maxHeight = 'calc(100vh - 8rem)';
|
|
topicList.style.boxSizing = 'border-box';
|
|
topicList.style.visibility = 'hidden';
|
|
topicList.style.overflowY = 'scroll';
|
|
|
|
topicLinks = [...topicList.querySelectorAll('a')]
|
|
topicLinks.forEach(link => {
|
|
link.addEventListener('click', hideTopicList);
|
|
});
|
|
|
|
topicListSearch = topicList.querySelector(`.${topicListSearchClass}`);
|
|
topicListSearch.addEventListener('input', onTopicListSearchInput);
|
|
|
|
return topicList;
|
|
}
|
|
|
|
function onTopicListSearchInput(e) {
|
|
let searchTerms = topicListSearch.value.toLowerCase();
|
|
topicLinks.forEach(link => {
|
|
if (link.textContent.toLowerCase().includes(searchTerms) || searchTerms.length == 0) {
|
|
link.classList.remove(notMatchingSearchTermsClass);
|
|
link.removeAttribute('tabindex');
|
|
} else {
|
|
link.tabIndex = -1;
|
|
link.classList.add(notMatchingSearchTermsClass);
|
|
}
|
|
})
|
|
}
|
|
|
|
let lastCtrlPress = null;
|
|
|
|
function toggleTopicList() {
|
|
if (lastCtrlPress == null) {
|
|
lastCtrlPress = Date.now();
|
|
} else {
|
|
let now = Date.now();
|
|
if (now - lastCtrlPress < 400) {
|
|
lastCtrlPress = null;
|
|
if (topicList.style.visibility === 'visible') {
|
|
hideTopicList();
|
|
} else {
|
|
hideHelpMenu();
|
|
topicListSearch.value = '';
|
|
onTopicListSearchInput();
|
|
topicList.style.visibility = 'visible';
|
|
topicListSearch.focus();
|
|
}
|
|
} else {
|
|
lastCtrlPress = Date.now();
|
|
}
|
|
}
|
|
}
|
|
|
|
function hideTopicList() {
|
|
if (topicList.style.visibility === 'visible') {
|
|
topicList.style.visibility = 'hidden';
|
|
}
|
|
}
|
|
|
|
function reactToResizing() {
|
|
goToSlide(currentSlide);
|
|
}
|
|
|
|
function toggleHelpMenu() {
|
|
hideTopicList();
|
|
document.getElementById('help-menu-toggle').click();
|
|
}
|
|
|
|
function togglePointer() {
|
|
document.body.classList.toggle('laser-pointer-active');
|
|
}
|
|
|
|
function hideHelpMenu() {
|
|
document.getElementById('help-menu-checkbox').checked = false;
|
|
} |