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