]> BookStack Code Mirror - website/blob - themes/bookstack/static/js/script.js
Updated hacks
[website] / themes / bookstack / static / js / script.js
1
2 // Mobile menu
3
4 const menuButton = document.getElementById('menu-button');
5 const menuDropDown = document.querySelector('#header .main-nav');
6
7 menuButton.addEventListener('click', function(event) {
8   menuDropDown.classList.toggle('showing');
9   event.stopPropagation();
10 });
11
12 document.body.addEventListener('click', function(event) {
13   const isShown = menuDropDown.classList.contains('showing');
14   if (isShown) {
15     menuDropDown.classList.remove('showing');
16     event.stopPropagation();  
17   }
18 });
19
20
21 // Handle video click to play
22 const videos = document.querySelectorAll('video');
23 for (let i = 0; i < videos.length; i++) {
24     videos[i].addEventListener('click', videoClick)
25 }
26
27 function videoClick() {
28     if (typeof InstallTrigger !== 'undefined') return;
29     this.paused ? this.play() : this.pause();
30 }
31
32 // Header double click URL reference
33 document.body.addEventListener('dblclick', event => {
34   const isHeader = event.target.matches('h1, h2, h3, h4, h5, h6');
35   if (isHeader && event.target.id) {
36     window.location.hash = event.target.id;
37   }
38 });
39
40 function el(tag, attributes = {}, children = []) {
41   const elem = document.createElement(tag);
42   for (const attr of Object.keys(attributes)) {
43     elem.setAttribute(attr, attributes[attr]);
44   }
45   for (let child of children) {
46     if (typeof child === 'string') {
47       child = new Text(child);
48     }
49     elem.append(child);
50   }
51   return elem;
52 }
53
54 // Site search
55 const searchForm = document.getElementById('site-search-form');
56 const searchInput = document.getElementById('site-search-input');
57 const searchDialog = searchForm.querySelector('dialog');
58
59 async function runSearch() {
60   const searchTerm = searchInput.value.toLowerCase();
61
62   let pages = [];
63   try {
64     pages = await window.webidx.search({
65       dbfile:'/search.db',
66       query: searchTerm,
67     });
68   } catch (error) {
69     searchDialog.innerHTML = '<strong class="search-category-title">Failed to load search results</strong>';
70     console.error(error);
71     return;
72   }
73
74   // Sort pages to prioritise those with word in title
75   pages.sort((a, b) => {
76     const aScore = (a.url.includes(searchTerm) || a.title.toLowerCase().includes(searchTerm)) ? 1 : 0;
77     const bScore = (b.url.includes(searchTerm) || b.title.toLowerCase().includes(searchTerm)) ? 1 : 0;
78     return bScore - aScore;
79   });
80
81   // Categorizes pages to display
82   const categorised = {
83     docs: {title: 'Documentation', filter: '/docs/', pages: []},
84     hacks: {title: 'Hacks', filter: '/hacks/', pages: []}, 
85     blog: {title: 'From the blog', filter: '/blog/', pages: []}, 
86     other: {title: 'Site Pages', filter: '', pages: []},
87   };
88   const categoryNames = Object.keys(categorised);
89
90   for (const page of pages) {
91     for (const categoryName of categoryNames) {
92       const category = categorised[categoryName];
93       if (page.url.includes(category.filter)) {
94         category.pages.push(page);
95         break;
96       }
97     }
98   }
99
100   const categoryResults = categoryNames.map(name => {
101     const category = categorised[name];
102     if (category.pages.length === 0) {
103       return null;
104     }
105     return el('div', {}, [
106       el('strong', {class: 'search-category-title'}, [category.title]),
107       el('div', {}, category.pages.slice(0, 5).map(page => {
108         return el('a', {href: page.url}, [page.title]);
109       })),
110     ]);
111   }).filter(Boolean);
112
113   const emptyResult = el('strong', {class: 'search-category-title'}, [el('em', {}, 'No results found')]);
114   const resultList = categoryResults.length ? categoryResults : [emptyResult];
115   const results = el('div', {}, resultList);
116
117   for (const child of searchDialog.children) {
118     child.remove();
119   }
120   
121   searchDialog.append(results);
122   showSearchDialog();
123 }
124
125 function showSearchDialog() {
126   if (searchDialog.open) {
127     return;
128   }
129   searchDialog.show();
130   searchInput.focus();
131   
132   const clickListener = e => {
133     if(!e.target.closest('dialog')) {
134       closeListener();
135     }
136   };
137
138   const escListener = e => {
139     if (e.key === 'Escape') {
140         closeListener();
141     }
142   };
143
144   const mouseLeaveListener = e => {
145     closeListener();
146   }
147
148   const closeListener = () => {
149     searchDialog.close();
150     document.removeEventListener('click', clickListener);
151     document.removeEventListener('keydown', escListener);
152     searchForm.removeEventListener('mouseleave', mouseLeaveListener);
153   };
154
155   document.addEventListener('click', clickListener);
156   document.addEventListener('keydown', escListener);
157   searchForm.addEventListener('mouseleave', mouseLeaveListener);
158 }
159
160 function showSearchLoading() {
161   searchDialog.innerHTML = `<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>`;
162   showSearchDialog();
163 }
164
165 searchForm.addEventListener('submit', event => {
166   event.preventDefault();
167   showSearchLoading();
168   runSearch();
169 });
170
171 searchInput.addEventListener('input', event => {
172   const termLength = searchInput.value.length;
173   if (termLength === 0) {
174     searchDialog.close();
175   } else if (termLength > 2) {
176     showSearchLoading();
177     runSearch();
178   }
179 });
180
181
182 // Email display
183 const emailDisplayLinks = document.querySelectorAll('a.email-display');
184 const eb64 = 'ZW1haWxAYm9v' + 'a3N0YWNrYXBwLmNvbQ==';
185 for (const link of emailDisplayLinks) {
186   const email = atob(eb64);
187   link.addEventListener('click', e => {
188     e.preventDefault();
189     e.target.textContent = email;
190     e.target.href = 'mailto:' + email;
191   });
192 }