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