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