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