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