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