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