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