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