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