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 /* 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 272 return result; 273 274}; 275 276// ----------------------------------------------------------------------------- 277 278/** 279 * Embed the TOC in the sidebar. Replace an elmenent with the TOC. 280 * 281 * @param {HTMLElement} target An HTML element to embed the TOC. 282 * @param {HTMLElement} toc A TOC HTML element. 283 */ 284ReadtheDokus.prototype._embedToc = function(target, toc) 285{ 286 287 if (target && toc) 288 { 289 target.parentNode.parentNode.appendChild(toc); 290 target.parentNode.style.display = "none"; 291 } 292 293}; 294 295// ----------------------------------------------------------------------------- 296 297/** 298 * Show the TOC on the current position. 299 * 300 * @param {HTMLElement} toc A TOC HTML element. 301 */ 302ReadtheDokus.prototype._showToc = function(toc) 303{ 304 305 if (toc) 306 { 307 this._toc.parentNode.style.display = "block"; 308 } 309 310}; 311 312// ----------------------------------------------------------------------------- 313 314/** 315 * Initialize the TOC menu. 316 * 317 * @param {HTMLElement} toc A TOC HTML element. 318 */ 319ReadtheDokus.prototype._initToc = function(toc) 320{ 321 322 if (toc) 323 { 324 this._installTocSelectHandler(); 325 this._installTocMenuHandler(); 326 this._installTocJumpHandler(); 327 } 328 329}; 330 331// ----------------------------------------------------------------------------- 332 333/** 334 * Install a click handler to highlight and expand a TOC menu item. 335 */ 336ReadtheDokus.prototype._installTocSelectHandler = function() 337{ 338 339 var list = this._toc.querySelectorAll(".level1 div.li"); 340 var nodes = Array.prototype.slice.call(list, 0); 341 nodes.forEach(function(elem) { 342 elem.addEventListener("click", function() { 343 // Get the level2 parent element 344 let p = this._getParent(elem, "level2"); 345 346 // Remove all "current" class 347 var list2 = this._toc.querySelectorAll(".current"); 348 var nodes2 = Array.prototype.slice.call(list2, 0); 349 nodes2.forEach(function(elem) { 350 elem.classList.remove("current"); 351 }); 352 353 // Add "current" class to the clicked item and its level2 parent 354 if (p) 355 { 356 p.parentNode.classList.add("current"); 357 p.classList.add("current"); 358 elem.classList.add("current"); 359 elem.scrollIntoView(true); 360 } 361 362 // Expand the item 363 this.expandTocMenu(elem); 364 365 // Fold all the other level2 items 366 var list3 = this._toc.querySelectorAll(".level2 > div.li.expandable"); 367 var nodes3 = Array.prototype.slice.call(list3, 0); 368 nodes3.forEach(function(item) { 369 if (item != p) 370 { 371 this.collapseTocMenu(item); 372 } 373 }.bind(this)); 374 }.bind(this)); 375 }.bind(this)); 376 377}; 378 379// ----------------------------------------------------------------------------- 380 381/** 382 * Install a click handler to expand/collapse a TOC menu item. 383 */ 384ReadtheDokus.prototype._installTocMenuHandler = function() 385{ 386 387 // Search for TOC menu items which have children 388 var list = this._toc.querySelectorAll("div.li"); 389 var nodes = Array.prototype.slice.call(list, 0); 390 nodes.forEach(function(elem) { 391 if (elem.parentNode.querySelector(".toc")) 392 { 393 elem.classList.add("expandable"); 394 395 // Insert +/- fontawesome icon and image 396 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>'); 397 398 // Install click handler 399 elem.children[0].children[0].addEventListener("click", function(e) { 400 this.toggleTocMenu(elem); 401 402 e.stopPropagation(); 403 e.preventDefault(); 404 }.bind(this)); 405 406 // Only level1 menu items are open at start 407 if (!elem.parentNode.classList.contains("level1")) 408 { 409 this.collapseTocMenu(elem); 410 } 411 } 412 413 // Install a click handler to move an clicked item to top 414 elem.addEventListener("click", function() { 415 elem.scrollIntoView(true); 416 }); 417 }.bind(this)); 418 419}; 420 421// ----------------------------------------------------------------------------- 422 423/** 424 * Install a click handler to jump to the anchor taking the fixed header height into account. 425 */ 426ReadtheDokus.prototype._installTocJumpHandler = function() 427{ 428 429 var headerHeight = this._header.offsetHeight; 430 var list = this._toc.querySelectorAll('a[href*="#"]'); 431 var nodes = Array.prototype.slice.call(list, 0); 432 nodes.forEach(function(elem){ 433 elem.addEventListener("click", function(e) { 434 var hash = elem.getAttribute("href"); 435 var target = document.querySelector(hash); 436 if (target) 437 { 438 if (dokus.getMediaQuery() == "sp") 439 { 440 this.hideSidebar(); 441 } 442 443 var top = target.getBoundingClientRect().top; 444 window.scrollTo(0, window.pageYOffset + top - headerHeight); 445 } 446 447 e.preventDefault(); 448 return false; 449 }.bind(this)); 450 }.bind(this)); 451 452}; 453 454// ----------------------------------------------------------------------------- 455 456/** 457 * Return a specified level parent TOC item of the specified element. 458 * 459 * @param {HTMLElement} elem An HTML element. 460 * @param {String} level A depth level. 461 */ 462ReadtheDokus.prototype._getParent = function(elem, level) 463{ 464 465 let current = elem.parentNode; 466 467 while (current && !current.classList.contains("level1")) 468 { 469 if (current.classList.contains(level)) 470 { 471 return current.children[0]; 472 } 473 474 current = current.parentNode.parentNode; 475 } 476 477 return null; 478 479}; 480 481// ----------------------------------------------------------------------------- 482 483/** 484 * Initialize the mobile header. Add an click event listener to toggle the sidebar. 485 */ 486ReadtheDokus.prototype._initMobileHeader = function() 487{ 488 489 // Add click event handler for mobile menu 490 document.getElementById("btn-mobilemenu").addEventListener("click", function(){ 491 this.toggleSidebar(); 492 }.bind(this)); 493 494}; 495 496// ----------------------------------------------------------------------------- 497 498/** 499 * Initialize previous/next page buttons. Show/hide buttons depending on the current page index. 500 */ 501ReadtheDokus.prototype._initPageButtons = function() 502{ 503 504 // Get current page (remove hash) 505 this._currentPage = window.location.href.replace(/#.*$/, ""); 506 507 // Get current page index 508 this._currentPageIndex = this._pages.indexOf(this._currentPage); 509 510 // Show prev button 511 if (this._currentPageIndex > 0) 512 { 513 document.getElementById("btn-prevpage").classList.remove("invisible"); 514 document.getElementById("btn-prevpage").href = this._pages[this._currentPageIndex - 1]; 515 } 516 517 // Show next button 518 if (this._currentPageIndex > -1 && this._currentPageIndex < this._pages.length - 1) 519 { 520 document.getElementById("btn-nextpage").classList.remove("invisible"); 521 document.getElementById("btn-nextpage").href = this._pages[this._currentPageIndex + 1]; 522 } 523 524}; 525