xref: /template/readthedokus/js/readthedokus.js (revision 4c373bb242b3a9aa41c9513b4114c7b9257317b1)
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