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