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