1 2/***************************************************************** 3 * @author: Martijn De Jongh (Martino), [email protected] 4 * https://github.com/Martinomagnifico 5 * 6 * Simplemenu.js for Reveal.js 7 * Version 2.0.1 8 * 9 * @license 10 * MIT licensed 11 * 12 * Thanks to: 13 * - Hakim El Hattab, Reveal.js 14 ******************************************************************/ 15 16 17 18(function (global, factory) { 19 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 20 typeof define === 'function' && define.amd ? define(factory) : 21 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Simplemenu = factory()); 22})(this, (function () { 'use strict'; 23 24 const Plugin = () => { 25 let options = {}; 26 const vars = {}; 27 const sections = {}; 28 const mainArray = []; 29 let autoListItems = []; 30 let manualListItems = []; 31 32 const debugLog = text => { 33 if (options.debug) console.log(text); 34 }; 35 36 const isObject = item => { 37 return item && typeof item === 'object' && !Array.isArray(item); 38 }; 39 40 const mergeDeep = function (target) { 41 for (var _len = arguments.length, sources = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 42 sources[_key - 1] = arguments[_key]; 43 } 44 45 if (!sources.length) return target; 46 const source = sources.shift(); 47 48 if (isObject(target) && isObject(source)) { 49 for (const key in source) { 50 if (isObject(source[key])) { 51 if (!target[key]) Object.assign(target, { 52 [key]: {} 53 }); 54 mergeDeep(target[key], source[key]); 55 } else { 56 Object.assign(target, { 57 [key]: source[key] 58 }); 59 } 60 } 61 } 62 63 return mergeDeep(target, ...sources); 64 }; 65 66 const selectionArray = (container, selectors) => { 67 let selections = container.querySelectorAll(selectors); 68 let selectionarray = Array.prototype.slice.call(selections); 69 return selectionarray; 70 }; 71 72 const pluginPath = filename => { 73 let path; 74 let pluginScript = document.querySelector(`script[src$="${filename}"]`); 75 76 if (pluginScript) { 77 path = pluginScript.getAttribute("src").slice(0, -1 * filename.length); 78 } else { 79 path = (typeof document === 'undefined' && typeof location === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : typeof document === 'undefined' ? location.href : (document.currentScript && document.currentScript.src || new URL('simplemenu.js', document.baseURI).href)).slice(0, (typeof document === 'undefined' && typeof location === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : typeof document === 'undefined' ? location.href : (document.currentScript && document.currentScript.src || new URL('simplemenu.js', document.baseURI).href)).lastIndexOf('/') + 1); 80 } 81 82 return path; 83 }; 84 85 const isBefore = (a, b) => { 86 var all = document.getElementsByTagName('*'); 87 88 for (var i = 0; i < all.length; ++i) { 89 if (all[i] === a) return true;else if (all[i] === b) return false; 90 } 91 }; 92 93 const isStack = section => { 94 let isStack = false; 95 96 for (let i = 0; i < section.childNodes.length; i++) { 97 if (section.childNodes[i].tagName == "SECTION") { 98 isStack = true; 99 break; 100 } 101 } 102 103 return isStack; 104 }; 105 106 const createNode = thehtml => { 107 const fragment = document.createRange().createContextualFragment(thehtml); 108 return fragment.firstElementChild; 109 }; 110 111 const loadStyle = (url, type, callback) => { 112 let head = document.querySelector('head'); 113 let style = document.createElement('link'); 114 style.rel = 'stylesheet'; 115 style.href = url; 116 117 let finish = () => { 118 if (typeof callback === 'function') { 119 callback.call(); 120 callback = null; 121 } 122 }; 123 124 style.onload = finish; 125 126 style.onreadystatechange = function () { 127 if (this.readyState === 'loaded') { 128 finish(); 129 } 130 }; 131 132 head.appendChild(style); 133 }; 134 135 const checkOccurrence = (array, element) => { 136 let counter = 0; 137 138 for (let i = 0; i <= array.length; i++) { 139 if (array[i] == element) { 140 counter++; 141 } 142 } 143 144 return counter; 145 }; 146 147 const menuArray = () => { 148 const matchString = vars.matchString; 149 let menulist = selectionArray(vars.viewport, `.${options.menuclass}`) ? selectionArray(vars.viewport, `.${options.menuclass}`) : []; 150 let automenus = []; 151 let manualmenus = []; 152 153 if (menulist.length) { 154 menulist.forEach(menu => { 155 if (menu.getElementsByTagName('li').length < 1) { 156 menu.setAttribute('data-simplemenu-auto', ''); 157 automenus.push(menu); 158 } else { 159 if (options.selectby == "data-name" || options.selectby == "name") { 160 let existingListItems = selectionArray(menu, `.${options.menuclass} ${options.activeelement}`);
161 existingListItems.forEach(listItem => { 162 if (!listItem.dataset[matchString]) { 163 let content = listItem.textContent || listItem.querySelector('a').textContent; 164 listItem.setAttribute(`data-${matchString}`, content); 165 } 166 }); 167 } 168 169 manualmenus.push(menu); 170 } 171 }); 172 return { 173 automenus: automenus, 174 manualmenus: manualmenus 175 }; 176 } else { 177 return false; 178 } 179 }; 180 181 const setScale = revealScale => { 182 let totalScale = revealScale * vars.userScale; 183 vars.viewport.style.setProperty('--simplemenu-scale', totalScale.toFixed(3)); 184 }; 185 186 const moveRevealUI = (curUiEl, newUiEl) => { 187 let newUiElClassList = newUiEl.classList; 188 newUiEl.parentNode.replaceChild(curUiEl, newUiEl); 189 curUiEl.classList = newUiElClassList; 190 }; 191 192 const getRevealUI = () => { 193 let revealUIs = ['controls', 'slide-number']; 194 revealUIs.forEach(uielement => { 195 let curUiEl = vars.deck.getRevealElement().querySelector(`.reveal > .${uielement}`); 196 let newUiEl = vars.deck.getRevealElement().querySelector(`.reveal > * .${uielement}`); 197 198 if (curUiEl && newUiEl) { 199 moveRevealUI(curUiEl, newUiEl); 200 } 201 }); 202 }; 203 204 function copyDataAttributes(source, target) { 205 [...source.attributes].filter(attr => attr.nodeName.indexOf('data') > -1).forEach(attr => { 206 target.setAttribute(attr.nodeName, attr.nodeValue); 207 }); 208 } 209 210 const prepareSlides = () => { 211 debugLog("Preparing slides"); 212 sections.all = selectionArray(vars.viewport, "section"); 213 sections.all.forEach(section => { 214 // In Markdown environments, setting a data-name of a stack is not directly possible. 215 // Satting a data-stack-name on the first child solves this. 216 if (!section.parentNode.dataset.name && section.dataset && section.dataset.stackName) { 217 section.parentNode.dataset.name = section.dataset.stackName; 218 } // If a section has a data-sm='none', it will also remove the data-name. 219 220 221 if (section.dataset && section.dataset[vars.matchString] && section.dataset[vars.matchString] == "false" && section.dataset.name) { 222 delete section.dataset.name; 223 } 224 }); // Get all of the kinds of sections 225 226 sections.top = sections.all.filter(section => section.parentNode.classList.contains('slides') && !(section.dataset[vars.matchString] && section.dataset[vars.matchString] == "false")); 227 sections.named = sections.top.filter(section => section.dataset.name || section.getAttribute('name')); 228 sections.namedvisible = sections.named.filter(section => section.dataset.visibility != "hidden"); // Go through all the named sections 229 230 let namedsectionMatches = []; 231 sections.named.forEach(namedsection => { 232 // The 'name' attribute is also allowed. 233 let matchName = namedsection.dataset.name || namedsection.getAttribute('name'); // Named sections can have the same name, but should then be differentiated. 234 235 namedsectionMatches.push(matchName); 236 let dupsBefore = matchName && checkOccurrence(namedsectionMatches, matchName) > 1 ? `-${checkOccurrence(namedsectionMatches, matchName)}` : null; 237 let match = dupsBefore ? matchName + dupsBefore : matchName; // We set the name of the match as a data-attribute 238 239 namedsection.setAttribute(`data-${vars.matchString}`, match); // If the (named) section is not a stack and does not have an ID, we need to give it one. 240 241 if (!isStack(namedsection) && !namedsection.id) { 242 // Note: Quarto will already have assigned an ID, but it may also have been done manually. 243 namedsection.id = match.toLowerCase().replace(/\W/g, ''); 244 } else if (isStack(namedsection)) { 245 // Find the first (visible) section inside a stack. 246 let allsects = selectionArray(namedsection, `section`); 247 let allVisibleSects = allsects.filter(section => section.dataset.visibility != "hidden"); 248 let firstChildSection = allVisibleSects[0]; 249 250 if (firstChildSection && !firstChildSection.id) { 251 firstChildSection.id = match.toLowerCase().replace(/\W/g, ''); 252 253 if (namedsection.id == firstChildSection.id) { 254 namedsection.removeAttribute('id'); 255 } 256 } 257 } 258 }); 259 let currentMatch = null; 260 let currentid = null; // Get all the sections that are actually slides 261 262 sections.regular = sections.all.filter(section => !isStack(section) && section.dataset.visibility != "hidden"); // Go through all the sections 263 264 sections.regular.forEach((section, i) => { 265 // Filling an array with the needed comparison information 266 let isChildSection = isStack(section.parentNode) && section.parentNode.tagName == "SECTION"; 267 let theSection = isChildSection ? section.parentNode : section; 268 let dataname = theSection.dataset.name; 269 let name = theSection.getAttribute(`name`); 270 let dataintl = theSection.getAttribute(vars.langattribute) ? theSection.getAttribute(vars.langattribute) : null; 271 let parentid = section.parentNode.id ? section.parentNode.id : null; 272 let id = section.id ? section.id : isChildSection ? parentid : null; 273 let match = theSection.dataset[vars.matchString]; 274 275 if (match) { 276 currentMatch = match; 277 } 278 279 if (id || match == "false") { 280 currentid = i; 281 } 282 283 if (options.flat) { 284 if (match != "false") { 285 match = currentMatch; 286 } 287 } 288 289 if (match == "false") { 290 match = null; 291 } 292 293 if (dataname == "false") { 294 dataname = null; 295 } 296 297 let sectionObject = { 298 index: i, 299 ...(section && { 300 section 301 }),
302 ...(dataname && { 303 dataname 304 }), 305 ...(name && { 306 name 307 }), 308 ...(id && { 309 id 310 }), 311 ...(match && { 312 match 313 }), 314 ...(currentid && { 315 currentid 316 }), 317 ...(dataintl && { 318 dataintl 319 }) 320 }; 321 mainArray.push(sectionObject); 322 }); 323 }; 324 325 const prepareMenubars = () => { 326 debugLog("Preparing menubars"); 327 let menubars = selectionArray(vars.viewport, `.${options.menubarclass}`) ? selectionArray(vars.viewport, `.${options.menubarclass}`) : []; 328 329 if (options.barhtml.header) { 330 // Generate header menubar 331 let bar = createNode(options.barhtml.header); 332 menubars.push(bar); 333 vars.slides.before(bar); 334 } 335 336 if (options.barhtml.footer) { 337 // Generate footer menubar 338 let bar = createNode(options.barhtml.footer); 339 menubars.push(bar); 340 vars.slides.after(bar); 341 } 342 343 if (menubars.length) { 344 // If menubar (pre-existing or just added): 345 setScale(vars.deck.getScale()); 346 menubars.forEach((menubar, i) => { 347 let barLocation = isBefore(menubar, vars.slides) ? "top" : "bottom"; 348 menubar.classList.add(barLocation); 349 350 if (!menubar.id) { 351 menubar.id = `${options.menubarclass}${barLocation}`; 352 } 353 354 menubar.classList.add("ready"); 355 }); 356 vars.menubars = menubars; 357 } else { 358 console.log("There are no menubars. You can still use Simplemenu to populate empty menus like in an Agenda or Table Of Contents."); 359 } 360 }; 361 362 const prepareMenus = () => { 363 debugLog("Preparing menus"); 364 let menus = menuArray(); 365 366 if (!menus || menus && !menus.automenus) { 367 console.log("There are no menus. Please add one or more menus manually or through the 'barhtml' option."); 368 return; 369 } 370 371 if (menus.automenus.length >= 1 && sections.namedvisible.length >= 1) { 372 // There are empty menus. Autofill them. 373 let idArray = []; 374 const autoMenuLinks = sections.namedvisible.map(section => { 375 let match = section.dataset[vars.matchString]; 376 let name = section.dataset.name || section.getAttribute(`name`) || section.id; 377 let id = section.id || name.toLowerCase().replace(/\W/g, ''); 378 idArray.push(id); 379 380 if (vars.quarto) { 381 id = mainArray.find(item => item.match === match).id; 382 } 383 384 let duplicatesBefore = checkOccurrence(idArray, id) > 1 ? `-${checkOccurrence(idArray, id)}` : ''; 385 let href = vars.quarto ? id : id + duplicatesBefore; 386 let smmatchString = ` data-${vars.matchString}="${match}"`; 387 let nameString = section.getAttribute(`name`) ? ` name="${section.getAttribute(`name`)}"` : ''; 388 let intlString = section.getAttribute(vars.langattribute) ? ` ${vars.langattribute}="${section.getAttribute(vars.langattribute)}"` : ''; 389 return `<li><a href="#/${href}"${intlString}${nameString}${smmatchString}>${name}</a></li>`; 390 }).reduce((combinedHTML, itemHTML) => { 391 let orderedHTML = vars.rtl ? itemHTML + combinedHTML : combinedHTML + itemHTML; 392 return orderedHTML; 393 }); 394 menus.automenus.forEach(automenu => { 395 automenu.innerHTML = autoMenuLinks; 396 }); 397 autoListItems = menus.automenus.map(menu => Array.from(menu.querySelectorAll(options.activeelement))).flat(); 398 } 399 400 if (menus.manualmenus.length >= 1) { 401 // There are pre-existing menus. Fix link to ID if needed. 402 // Only get the listitems 403 manualListItems = menus.manualmenus.map(menu => Array.from(menu.querySelectorAll(options.activeelement))).flat(); 404 manualListItems.forEach(listItem => { 405 // Get the anchorlinks 406 let linker = listItem.tagName == "a" ? listItem : listItem.querySelector('a'); 407 let linkhref = linker.getAttribute('href'); 408 409 if (linkhref === "#") { 410 let newLink = listItem.dataset[vars.matchString].toLowerCase().replace(/\W/g, ''); 411 linker.href = `#/${newLink}`; 412 } 413 }); 414 } 415 }; 416 417 const preparePrint = () => {
418 const urlParams = new URLSearchParams(window.location.search); 419 const hasPrintParam = urlParams.has('print-pdf'); 420 421 if (hasPrintParam) { 422 mainArray.forEach(item => { 423 let printSection = item.section; 424 let datainfo = document.createElement("div"); 425 datainfo.classList.add("datainfo"); 426 copyDataAttributes(printSection, datainfo); 427 let moreData = ['match', 'name', 'dataname', 'currentid', 'id', 'dataintl']; 428 moreData.forEach(moreDataItem => { 429 if (item[moreDataItem]) { 430 datainfo.dataset[moreDataItem] = item[moreDataItem]; 431 } 432 }); 433 printSection.appendChild(datainfo); 434 }); 435 } 436 }; 437 438 const prepare = resolve => { 439 prepareSlides(); 440 prepareMenubars(); 441 prepareMenus(); 442 preparePrint(); 443 return setTimeout(resolve, 0); 444 }; 445 446 const compare = (listItem, section) => { 447 let menukind = listItem.parentNode.hasAttribute('data-simplemenu-auto') ? "auto" : "manual"; 448 let sectionmatch = section.match ? section.match : null; 449 450 if (menukind == "manual") { 451 if (options.selectby == "id") { 452 sectionmatch = section.id ? section.id : section.currentid ? mainArray[section.currentid].id : null; 453 } else if (options.selectby == "name") { 454 sectionmatch = section.name; 455 } else { 456 sectionmatch = section.dataname; 457 } 458 } 459 460 if (sectionmatch) { 461 let menumatch = listItem.dataset[vars.matchString] || listItem.querySelector('a').dataset[vars.matchString]; 462 463 if (options.selectby == "id" && menukind == "manual") { 464 let href = listItem.href || listItem.querySelector('a').href; 465 let lastHref = href ? href.substring(href.lastIndexOf("/") + 1) : ''; 466 menumatch = lastHref; 467 } 468 469 if (options.selectby == "data-name" && menukind == "manual") { 470 sectionmatch = section.dataname ? section.dataname : null; 471 } 472 473 if (menumatch && menumatch == sectionmatch) { 474 listItem.classList.add(options.activeclass); 475 } else { 476 listItem.classList.remove(options.activeclass); 477 } 478 } else { 479 listItem.classList.remove(options.activeclass); 480 } 481 }; 482 483 const checkSlidesNormal = event => { 484 const index = sections.regular.indexOf(event.currentSlide); 485 let section = mainArray[index]; 486 autoListItems.filter(listItem => { 487 compare(listItem, section); 488 }); 489 manualListItems.filter(listItem => { 490 compare(listItem, section); 491 }); 492 }; 493 494 const checkSlidesPDF = event => { 495 let pdfPages = selectionArray(vars.viewport, '.slides .pdf-page'); // Check if any menubar has a slide number 496 497 let anyMenubarHasSlidenumber = false; 498 499 if (vars.menubars) { 500 vars.menubars.forEach(menubar => { 501 anyMenubarHasSlidenumber = !!menubar.getElementsByClassName("slide-number"); 502 }); 503 } 504 505 pdfPages.forEach(pdfPage => { 506 // The original section has gone, so we rebuild it with the saved data-attributes 507 let datainfo = pdfPage.getElementsByClassName("datainfo")[0]; 508 let section = {}; 509 section.name = datainfo.dataset.name; 510 section.dataname = datainfo.dataset.dataname; 511 section.currentid = datainfo.dataset.currentid; 512 section.match = datainfo.dataset.match; 513 section.id = datainfo.dataset.id; 514 515 if (datainfo.dataset.state) { 516 let newClasses = datainfo.dataset.state.split(" "); 517 newClasses.forEach(newClass => { 518 pdfPage.classList.add(newClass); 519 let vp = vars.deck.getRevealElement().closest(".reveal-viewport"); 520 vp.classList.remove(newClass); 521 }); 522 } // If any menubar has a slide number, turn the original one on this slide off 523 524 525 if (anyMenubarHasSlidenumber && pdfPage.getElementsByClassName("slide-number").length > 0) { 526 pdfPage.getElementsByClassName("slide-number")[0].style.display = "none"; 527 } 528 529 if (vars.menubars) { 530 vars.menubars.forEach(menubar => { 531 let bar = menubar.cloneNode(true); 532 pdfPage.appendChild(bar); 533 let listItems = selectionArray(bar, `.${options.menuclass} ${options.activeelement}`);
534 listItems.forEach(listItem => { 535 compare(listItem, section); 536 }); // If there is a slidenumber in the menu, 537 538 let newSN = pdfPage.querySelector(`.${options.menubarclass} .slide-number`); 539 let oldSN = pdfPage.querySelector(`:scope > .slide-number`); 540 541 if (newSN && oldSN) { 542 // ...then fill it with the current (total) slidenumber. 543 newSN.textContent = oldSN.textContent; 544 } 545 }); 546 } 547 }); 548 549 if (vars.menubars) { 550 vars.menubars.forEach(menubar => { 551 menubar.parentNode.removeChild(menubar); 552 }); 553 } 554 }; 555 556 const chapterize = event => { 557 if (event && event.type == "ready") { 558 debugLog(mainArray); 559 getRevealUI(); 560 } 561 562 if (event && (event.type == "ready" || event.type == "slidechanged")) { 563 checkSlidesNormal(event); 564 } 565 566 if (event && event.type == "pdf-ready") { 567 checkSlidesPDF(); 568 } 569 }; 570 571 const simpleMenu = (deck, options, es5Filename) => { 572 deck.configure({ 573 hash: true 574 }); 575 vars.deck = deck; 576 vars.viewport = deck.getRevealElement().tagName == "BODY" ? document : deck.getRevealElement(); 577 vars.slides = deck.getSlidesElement(); 578 vars.langattribute = deck.getConfig().internation ? deck.getConfig().internation.langattribute ? deck.getConfig().internation.langattribute : "data-i18n" : false; 579 vars.rtl = deck.getConfig().rtl; 580 vars.quarto = document.querySelector('[name=generator]') && document.querySelector('[name=generator]').content.includes("quarto") ? true : false; 581 vars.matchString = "sm"; 582 vars.userScale = options.scale; 583 deck.addEventListener('ready', chapterize, false); 584 deck.addEventListener('slidechanged', chapterize, false); 585 deck.addEventListener('pdf-ready', chapterize, false); 586 deck.addEventListener('resize', _ref => { 587 let { 588 scale 589 } = _ref; 590 return setScale(scale); 591 }, false); 592 const SimplemenuStylePath = options.csspath ? options.csspath : `${pluginPath(es5Filename)}simplemenu.css` || 'plugin/simplemenu/simplemenu.css'; 593 return new Promise(resolve => { 594 if (options.csspath === false) { 595 return prepare(resolve); 596 } else { 597 loadStyle(SimplemenuStylePath, 'stylesheet', async () => prepare(resolve)); 598 } 599 }); 600 }; 601 602 const init = deck => { 603 let defaultOptions = { 604 menubarclass: 'menubar', 605 menuclass: 'menu', 606 activeclass: 'active', 607 activeelement: 'li', 608 selectby: 'id', 609 barhtml: { 610 header: '', 611 footer: '' 612 }, 613 flat: false, 614 scale: 0.67, 615 csspath: '' 616 }; 617 options = deck.getConfig().simplemenu || {}; 618 options = mergeDeep(defaultOptions, options); 619 let wronginputs = false; 620 let warning = ''; 621 622 if (options.selectby !== "id" && options.selectby !== "data-name" && options.selectby !== "name") { 623 wronginputs = true; 624 warning = 'The selectby option can be only "id", "data-name" or "name".'; 625 } 626 627 if (wronginputs) { 628 console.log('Simplemenu did not load:'); 629 console.log(warning); 630 return false; 631 } 632 633 return simpleMenu(deck, options, "simplemenu.js"); 634 }; 635 636 return { 637 id: 'simplemenu', 638 init: init 639 }; 640 }; 641 642 return Plugin; 643 644}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.