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