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