1// ----------------------------------------------------------------------------- 2// Constructor 3// ----------------------------------------------------------------------------- 4 5/** 6 * Constructor. 7 */ 8function ReadtheDokus() 9{ 10 11 this._pages; 12 this._options; 13 this._toc = document.getElementById("dw__toc"); 14 this._header = document.querySelector("header"); 15 this._sidebar = document.querySelector("#dokuwiki__aside"); 16 this._delimiter = ( window.location.search.indexOf(":") > -1 ? ":" : "/"); 17 18} 19 20// ----------------------------------------------------------------------------- 21// Methods 22// ----------------------------------------------------------------------------- 23 24/** 25 * Run the application. 26 * 27 * @param {Object} options Options. Has following keys: 28 * "selector": selector to the sidebar links. 29 */ 30ReadtheDokus.prototype.run = function(options) 31{ 32 33 this._options = options || {}; 34 35 // Init 36 this._initToc(this._toc); 37 this._initMobileHeader(); 38 var placeHolder = this.safeGet(LANG, "template.readthedokus.searchform_placeholder"); 39 if (placeHolder) { 40 this._sidebar.querySelector("#sidebarheader #qsearch__in").setAttribute("placeholder", placeHolder); 41 } 42 document.body.setAttribute("data-contentlang", document.body.getAttribute("data-id").split(":")[0]); 43 44 // Embed TOC 45 var selector = options && options["linkSelector"] || ""; 46 this.embedTOC(selector); 47 48 // Collect page links 49 this.refreshPageLinks(selector); 50 51 // Anchor jump 52 if (document.location.hash) 53 { 54 var style; 55 var hash = decodeURI(document.location.hash); 56 var elem = document.querySelector(hash); 57 if (elem) 58 { 59 // Hide the jump target element first to prevent the default scroll. 60 style = elem.style.display; 61 elem.style.display = "none"; 62 63 setTimeout(function(){ 64 elem.style.display = style; 65 this._jumpToAnchor(hash); 66 }.bind(this), 0); 67 68 // Select the item in TOC 69 var sidebarItem = document.querySelector("#sidebar #dw__toc a[href='" + hash + "']"); 70 if (sidebarItem) 71 { 72 this.expandParentTocMenu(sidebarItem); 73 sidebarItem.click(); 74 } 75 } 76 } 77 78}; 79 80// ----------------------------------------------------------------------------- 81 82/** 83 * Embed the TOC in the sidebar. Replace an elmenent with the TOC. 84 * 85 * @param {String} selector A selector to the sidebar links. 86 */ 87ReadtheDokus.prototype.embedTOC = function(selector) 88{ 89 90 var isFound = false; 91 var sidebarItem; 92 93 if (JSINFO["ACT"] == "show") 94 { 95 this._enumSidebarLinks(selector, function(elem) { 96 console.log("@@@", elem); 97 // Embed TOC if the current page id matches to the sidebar link 98 if (!isFound && (elem.getAttribute("data-wiki-id") === JSINFO["id"] || elem.getAttribute("data-wiki-id") === (JSINFO["id"] + ":"))) 99 { 100 if (elem && this._toc) 101 { 102 // Embed 103 elem.parentNode.appendChild(this._toc); 104 elem.style.display = "none"; 105 } 106 107 isFound = true; 108 sidebarItem = elem; 109 } 110 }.bind(this)); 111 } 112 113 // Show TOC on top of sidebar if matching item was not found in the sidebar 114 if (!isFound && this._toc ) 115 { 116 this._toc.parentNode.style.display = "block"; 117 } 118 119 // Scroll the TOC to the top 120 if (this._toc) 121 { 122 this._toc.scrollIntoView(true); 123 } 124 else if (sidebarItem) 125 { 126 sidebarItem.scrollIntoView(true); 127 } 128 129}; 130 131// ----------------------------------------------------------------------------- 132 133/** 134 * Re-collect page links from the side bar and refresh next/prev buttons. 135 * 136 * @param {String} selector A selector to the sidebar links. 137 */ 138ReadtheDokus.prototype.refreshPageLinks = function(selector) 139{ 140 141 this._pages = []; 142 143 // Collect page links 144 this._enumSidebarLinks(selector, function(elem) { 145 this._pages.push(elem.href); 146 }.bind(this)); 147 148 // Get start page 149 if (this._pages.length > 0) 150 { 151 var startPage = this._getStartPage(this._pages[0], this._delimiter); 152 this._pages.unshift(startPage); 153 /* 154 var list = document.querySelectorAll("#sidebarheader > div.home > a, #pageheader .breadcrumbs > .home > a"); 155 var nodes = Array.prototype.slice.call(list, 0); 156 nodes.forEach(function(elem) { 157 elem.href = this._startPage; 158 }.bind(this)); 159 */ 160 } 161 162 // Init next/prev buttons 163 this._initPageButtons(); 164 165}; 166 167// ----------------------------------------------------------------------------- 168 169/** 170 * Return the media query value depending on the current screen size. 171 * 172 * @return {String} "pc" for PC, "tb" for Tablets, or "sp" for Smartphones. 173 */ 174ReadtheDokus.prototype.getMediaQuery = function() 175{ 176 177 return getComputedStyle(document.querySelector("#__media_query")).getPropertyValue("--media-query").trim() || getComputedStyle(document.querySelector("#__media_query"))["-media-query"].trim(); 178 179}; 180 181// ----------------------------------------------------------------------------- 182 183/** 184 * Toggle a TOC menu item. 185 * 186 * @param {HTMLElement} elem A TOC item element. 187 */ 188ReadtheDokus.prototype.toggleTocMenu = function(elem) 189{ 190 191 var invisible = elem.parentNode.querySelector(".toc").classList.contains("invisible"); 192 if (invisible) 193 { 194 this.expandTocMenu(elem); 195 } 196 else 197 { 198 this.collapseTocMenu(elem); 199 } 200 201}; 202 203// ----------------------------------------------------------------------------- 204 205/** 206 * Expand a TOC menu item. 207 * 208 * @param {HTMLElement} elem A toc item element. 209 */ 210ReadtheDokus.prototype.expandTocMenu = function(elem) 211{ 212 213 if (elem && elem.classList.contains("expandable")) 214 { 215 elem.parentNode.querySelector(".toc").classList.remove("invisible"); 216 217 var i = elem.children[0].children[0].children[0]; 218 i.classList.remove("fa-plus-square"); 219 i.classList.add("fa-minus-square"); 220 221 var img = elem.children[0].children[0].children[1]; 222 img.classList.remove("plus"); 223 img.classList.add("minus"); 224 img.src= DOKU_BASE + "lib/images/minus.gif"; 225 } 226 227}; 228 229// ----------------------------------------------------------------------------- 230 231/** 232 * Expand all the parent TOC menu of the specified element. 233 * 234 * @param {HTMLElement} elem A toc item element. 235 */ 236ReadtheDokus.prototype.expandParentTocMenu = function(elem) 237{ 238 239 var p = elem.parentNode; 240 while (p) 241 { 242 if (p.classList.contains("toc")) 243 { 244 if (p.previousElementSibling && p.previousElementSibling.classList.contains("expandable")) 245 { 246 this.expandTocMenu(p.previousElementSibling); 247 } 248 } 249 250 if (p.id === "dw__toc") 251 { 252 break; 253 } 254 255 p = p.parentNode; 256 } 257 258}; 259 260// ----------------------------------------------------------------------------- 261 262/** 263 * Collapse a TOC menu item. 264 * 265 * @param {HTMLElement} elem A toc item element. 266 */ 267ReadtheDokus.prototype.collapseTocMenu = function(elem) 268{ 269 270 if (elem && elem.classList.contains("expandable")) 271 { 272 elem.parentNode.querySelector(".toc").classList.add("invisible"); 273 274 var i = elem.children[0].children[0].children[0]; 275 i.classList.remove("fa-minus-square"); 276 i.classList.add("fa-plus-square"); 277 278 var img = elem.children[0].children[0].children[1]; 279 img.classList.remove("minus"); 280 img.classList.add("plus"); 281 img.src=DOKU_BASE + "lib/images/plus.gif"; 282 } 283 284}; 285// ----------------------------------------------------------------------------- 286 287/** 288 * Toggle the sidebar. 289 */ 290ReadtheDokus.prototype.toggleSidebar = function() 291{ 292 293 var mq = this.getMediaQuery(); 294 if (mq == "pc" || mq == "tb") 295 { 296 document.querySelector("#dokuwiki__site").classList.toggle("showSidebar"); 297 } 298 else 299 { 300 document.querySelector("#dokuwiki__site").classList.toggle("showSidebarSP"); 301 } 302 303}; 304 305// ----------------------------------------------------------------------------- 306 307/** 308 * Show the sidebar. 309 */ 310ReadtheDokus.prototype.showSidebar = function() 311{ 312 313 var mq = this.getMediaQuery(); 314 if (mq == "pc" || mq == "tb") 315 { 316 document.querySelector("#dokuwiki__site").classList.add("showSidebar"); 317 } 318 else 319 { 320 document.querySelector("#dokuwiki__site").classList.add("showSidebarSP"); 321 } 322 323}; 324 325// ----------------------------------------------------------------------------- 326 327/** 328 * Hide the sidebar. 329 */ 330ReadtheDokus.prototype.hideSidebar = function() 331{ 332 333 if (dokus.getMediaQuery() == "pc" || dokus.getMediaQuery() == "tb") 334 { 335 document.querySelector("#dokuwiki__site").classList.remove("showSidebar"); 336 } 337 else 338 { 339 document.querySelector("#dokuwiki__site").classList.remove("showSidebarSP"); 340 } 341 342}; 343 344// ----------------------------------------------------------------------------- 345// Privates 346// ----------------------------------------------------------------------------- 347 348/** 349 * Enumerates the sidebar links and call the callback function on each item. 350 * 351 * @param {String} selector A selector to select link nodes. 352 * @param {Function} callback A callback function. 353 */ 354ReadtheDokus.prototype._enumSidebarLinks = function(selector, callback) 355{ 356 357 callback = ( typeof callback === "function" ? callback : function(){} ); 358 selector = selector || ".aside > #sidebar a[data-wiki-id]"; 359 var links = this._sidebar.querySelectorAll(selector); 360 var nodes = Array.prototype.slice.call(links, 0); 361 362 nodes.forEach(function(elem) { 363 callback(elem); 364 }); 365 366}; 367// ----------------------------------------------------------------------------- 368 369/** 370 * Build and return the start page id. 371 * 372 * @param {String} basePage A base page for the start page. 373 * @param {String} delimiter An id delimiter char. 374 */ 375ReadtheDokus.prototype._getStartPage = function(basePage, delimiter) 376{ 377 378 var result = document.querySelector("#sidebarheader > div.home > a").href; 379 380 /* 381 if (basePage && delimiter) 382 { 383 var re = new RegExp("\\" + delimiter + "[^\\" + delimiter + "]*[^\\" + delimiter + "]*$"); 384 result = basePage.replace(re, "").replace(re, "") + delimiter + "start"; 385 386 } 387 */ 388 389 return result; 390 391}; 392 393// ----------------------------------------------------------------------------- 394 395/** 396 * Initialize the TOC menu. 397 * 398 * @param {HTMLElement} toc A TOC HTML element. 399 */ 400ReadtheDokus.prototype._initToc = function(toc) 401{ 402 403 if (toc) 404 { 405 this._installTocSelectHandler(); 406 this._installTocMenuHandler(); 407 this._installTocJumpHandler(); 408 } 409 410}; 411 412// ----------------------------------------------------------------------------- 413 414/** 415 * Install a click handler to highlight and expand a TOC menu item. 416 */ 417ReadtheDokus.prototype._installTocSelectHandler = function() 418{ 419 420 var list = this._toc.querySelectorAll(".level1 div.li"); 421 var nodes = Array.prototype.slice.call(list, 0); 422 nodes.forEach(function(elem) { 423 elem.addEventListener("click", function(e) { 424 // Get the level2 parent element 425 let p = this._getParent(elem, "level2"); 426 427 // Remove all "current" class 428 var list2 = this._toc.querySelectorAll(".current"); 429 var nodes2 = Array.prototype.slice.call(list2, 0); 430 nodes2.forEach(function(elem) { 431 elem.classList.remove("current"); 432 }); 433 434 // Add "current" class to the clicked item and its level2 parent 435 if (p) 436 { 437 p.parentNode.classList.add("current"); 438 p.classList.add("current"); 439 elem.classList.add("current"); 440 elem.scrollIntoView(true); 441 } 442 443 // Expand the item 444 this.expandTocMenu(elem); 445 446 // Fold all the other level2 items 447 var list3 = this._toc.querySelectorAll(".level2 > div.li.expandable"); 448 var nodes3 = Array.prototype.slice.call(list3, 0); 449 nodes3.forEach(function(item) { 450 if (item != p) 451 { 452 this.collapseTocMenu(item); 453 } 454 }.bind(this)); 455 456 e.stopPropagation(); 457 }.bind(this)); 458 }.bind(this)); 459 460}; 461 462// ----------------------------------------------------------------------------- 463 464/** 465 * Install a click handler to expand/collapse a TOC menu item. 466 */ 467ReadtheDokus.prototype._installTocMenuHandler = function() 468{ 469 470 // Search for TOC menu items which have children 471 var list = this._toc.querySelectorAll("div.li"); 472 var nodes = Array.prototype.slice.call(list, 0); 473 nodes.forEach(function(elem) { 474 if (elem.parentNode.querySelector(".toc")) 475 { 476 elem.classList.add("expandable"); 477 478 // Insert +/- fontawesome icon and image 479 elem.children[0].insertAdjacentHTML("afterbegin", '<div class="btn-expand"><i class="far fa-minus-square"></i><img class="minus" src="' + DOKU_BASE + 'lib/images/minus.gif" alt="−"></div>'); 480 481 // Install click handler 482 elem.children[0].children[0].addEventListener("click", function(e) { 483 this.toggleTocMenu(elem); 484 485 e.stopPropagation(); 486 e.preventDefault(); 487 }.bind(this)); 488 489 // Only level1 menu items are open at start 490 if (!elem.parentNode.classList.contains("level1")) 491 { 492 this.collapseTocMenu(elem); 493 } 494 } 495 496 // Install a click handler to move an clicked item to top 497 elem.addEventListener("click", function() { 498 elem.scrollIntoView(true); 499 }); 500 }.bind(this)); 501 502}; 503 504// ----------------------------------------------------------------------------- 505 506/** 507 * Install a click handler to jump to the anchor taking the fixed header height into account. 508 */ 509ReadtheDokus.prototype._installTocJumpHandler = function() 510{ 511 512 var list = document.querySelectorAll('a[href*="#"]'); 513 var nodes = Array.prototype.slice.call(list, 0); 514 nodes.forEach(function(elem){ 515 elem.addEventListener("click", function(e) { 516 var href = elem.getAttribute("href"); 517 var hash; 518 519 // Do anchor jump if the link destination is in the same page 520 if (href.substring(0,1) == "#" || elem.getAttribute("title") == document.body.getAttribute("data-id")) 521 { 522 var index = href.indexOf("#"); 523 hash = href.substring(index); 524 this._jumpToAnchor(hash); 525 e.preventDefault(); 526 return false; 527 } 528 }.bind(this)); 529 }.bind(this)); 530 531}; 532 533// ----------------------------------------------------------------------------- 534 535/** 536 * Jump to an anchor taking the header height in account. 537 * 538 * @param {HTMLElement} elem An HTML element. 539 * @param {String} level A depth level. 540 */ 541ReadtheDokus.prototype._jumpToAnchor = function(hash) 542{ 543 544 var target = document.querySelector(hash); 545 if (target) 546 { 547 // Jump to Anchor 548 location.href = hash; 549 550 // Get Header Height 551 var headerHeight = this._header.offsetHeight; 552 if (dokus.getMediaQuery() == "sp") 553 { 554 this.hideSidebar(); 555 } 556 557 // Scroll to the position of Target Element + Header Height 558 var top = target.getBoundingClientRect().top; 559 window.scrollTo(0, window.pageYOffset + top - headerHeight); 560 } 561 562}; 563 564// ----------------------------------------------------------------------------- 565 566/** 567 * Return a specified level parent TOC item of the specified element. 568 * 569 * @param {HTMLElement} elem An HTML element. 570 * @param {String} level A depth level. 571 */ 572ReadtheDokus.prototype._getParent = function(elem, level) 573{ 574 575 let current = elem.parentNode; 576 577 while (current && !current.classList.contains("level1")) 578 { 579 if (current.classList.contains(level)) 580 { 581 return current.children[0]; 582 } 583 584 current = current.parentNode.parentNode; 585 } 586 587 return null; 588 589}; 590 591// ----------------------------------------------------------------------------- 592 593/** 594 * Initialize the mobile header. Add a click event listener to toggle the sidebar. 595 */ 596ReadtheDokus.prototype._initMobileHeader = function() 597{ 598 599 // Add click event handler for mobile menu 600 document.getElementById("btn-mobilemenu").addEventListener("click", function(){ 601 this.toggleSidebar(); 602 }.bind(this)); 603 604}; 605 606// ----------------------------------------------------------------------------- 607 608/** 609 * Initialize previous/next page buttons. Show/hide buttons depending on the current page index. 610 */ 611ReadtheDokus.prototype._initPageButtons = function() 612{ 613 614 // Get current page (remove hash) and index 615 var currentPage = window.location.href.replace(/#.*$/, ""); 616 var currentPageIndex = this._pages.indexOf(currentPage); 617 618 // Show prev button 619 if (currentPageIndex > 0) 620 { 621 document.getElementById("btn-prevpage").classList.remove("invisible"); 622 document.getElementById("btn-prevpage").href = this._pages[currentPageIndex - 1]; 623 } 624 625 // Show next button 626 if (currentPageIndex > -1 && currentPageIndex < this._pages.length - 1) 627 { 628 document.getElementById("btn-nextpage").classList.remove("invisible"); 629 document.getElementById("btn-nextpage").href = this._pages[currentPageIndex + 1]; 630 } 631 632}; 633 634// ----------------------------------------------------------------------------- 635 636/** 637 * Get an value from object. Return default value when specified key is not available. 638 * 639 * @param {Object} store Object that holds keys/values. 640 * @param {String} key Key to get. 641 * @param {Object} defaultValue Value returned when key is not found. 642 * 643 * @return {*} Value. 644 */ 645ReadtheDokus.prototype.safeGet = function(store, key, defaultValue) 646{ 647 648 let current = store; 649 let found = true; 650 651 let keys = key.split("."); 652 for (let i = 0; i < keys.length; i++) 653 { 654 if (current !== null && typeof current === "object" && keys[i] in current) 655 { 656 current = current[keys[i]]; 657 } 658 else 659 { 660 found = false; 661 break; 662 } 663 } 664 665 return ( found ? current : defaultValue); 666 667}; 668