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