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