xref: /template/readthedokus/js/readthedokus.js (revision 8835d2d61b26a2491235f551fe6c13d9ecf249a3)
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 level2 parent
342			let p = this._getParent(elem, "level2");
343
344			// Remove all current
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			// Set current to this and 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
361			this.expandTocMenu(elem);
362
363			// Fold 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 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