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