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