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