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