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