window.addEventListener('load', init); window.addEventListener('resize', reactToResizing); let slides; let currentSlide; let topicList; let topicLinks; let topicListSearch; 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(); 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); }) } } 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(); break; } } 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 = '' } } /** * 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 `${this.title}` } /** * Generate a list item for the topic list with a sublist of all child nodes. */ getHTML() { let resultHTML = `
  • ${this.getLinkHTML()}`; if (this.children.length > 0) { resultHTML += `` } return resultHTML + '
  • '; } } function getTopicListContent() { let results = [...document.querySelectorAll('h1, h2, h3, h4, h5, h6')]; let resultHTML = ``; 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 { 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); }