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