]> BookStack Code Mirror - website/blob - themes/bookstack/static/js/script.js
385a74a9fab1abe3b76416f77753a50146f35db1
[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;
61
62   let pages = [];
63   try {
64     const resp = await fetch(`/search.php?query=${encodeURIComponent(searchTerm)}`);
65     pages = await resp.json();
66   } catch (error) {
67     searchDialog.innerHTML = '<strong class="search-category-title">Failed to load search results</strong>';
68     console.error(error);
69     return;
70   }
71
72   // Sort pages to prioritise those with word in title
73   const lowerSearchTerm = searchTerm.toLowerCase();
74   pages.sort((a, b) => {
75     const aScore = (a.url.includes(lowerSearchTerm) || a.title.toLowerCase().includes(lowerSearchTerm)) ? 1 : 0;
76     const bScore = (b.url.includes(lowerSearchTerm) || b.title.toLowerCase().includes(lowerSearchTerm)) ? 1 : 0;
77     return bScore - aScore;
78   });
79
80   // Categorizes pages to display
81   const categorised = {
82     docs: {title: 'Documentation', filter: '/docs/', pages: []},
83     hacks: {title: 'Hacks', filter: '/hacks/', pages: []}, 
84     blog: {title: 'From the blog', filter: '/blog/', pages: []}, 
85     other: {title: 'Site Pages', filter: '', pages: []},
86   };
87   const categoryNames = Object.keys(categorised);
88
89   for (const page of pages) {
90     let pageCategory = null;
91     for (const categoryName of categoryNames) {
92       const category = categorised[categoryName];
93       if (page.url.startsWith(category.filter)) {
94         pageCategory = category;
95         break;
96       }
97     }
98     (pageCategory || categorised.other).pages.push(page);
99   }
100
101   const categoryResults = categoryNames.map(name => {
102     const category = categorised[name];
103     if (category.pages.length === 0) {
104       return null;
105     }
106     return el('div', {}, [
107       el('strong', {class: 'search-category-title'}, [category.title]),
108       el('div', {}, category.pages.slice(0, 5).map(page => {
109         return el('a', {href: page.url}, [page.title]);
110       })),
111     ]);
112   }).filter(Boolean);
113
114   const emptyResult = el('strong', {class: 'search-category-title'}, [el('em', {}, 'No results found')]);
115   const resultList = categoryResults.length ? categoryResults : [emptyResult];
116   const results = el('div', {}, resultList);
117
118   while (searchDialog.firstChild) {
119     searchDialog.removeChild(searchDialog.firstChild);
120   }
121   
122   searchDialog.append(results);
123   showSearchDialog();
124 }
125
126 function showSearchDialog() {
127   if (searchDialog.open) {
128     return;
129   }
130   searchDialog.show();
131   searchInput.focus();
132   
133   const clickListener = e => {
134     if(!e.target.closest('dialog')) {
135       closeListener();
136     }
137   };
138
139   const escListener = e => {
140     if (e.key === 'Escape') {
141         closeListener();
142     }
143   };
144
145   const mouseLeaveListener = e => {
146     closeListener();
147   }
148
149   const closeListener = () => {
150     searchDialog.close();
151     document.removeEventListener('click', clickListener);
152     document.removeEventListener('keydown', escListener);
153     searchForm.removeEventListener('mouseleave', mouseLeaveListener);
154   };
155
156   document.addEventListener('click', clickListener);
157   document.addEventListener('keydown', escListener);
158   searchForm.addEventListener('mouseleave', mouseLeaveListener);
159 }
160
161 function showSearchLoading() {
162   searchDialog.innerHTML = `<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>`;
163   showSearchDialog();
164 }
165
166 searchForm.addEventListener('submit', event => {
167   event.preventDefault();
168   showSearchLoading();
169   runSearch();
170 });
171
172 searchInput.addEventListener('input', event => {
173   const termLength = searchInput.value.length;
174   if (termLength === 0) {
175     searchDialog.close();
176   } else if (termLength > 2) {
177     showSearchLoading();
178     runSearch();
179   }
180 });
181
182
183 // Email display
184 const emailDisplayLinks = document.querySelectorAll('a.email-display');
185 const eb64 = 'ZW1haWxAYm9v' + 'a3N0YWNrYXBwLmNvbQ==';
186 for (const link of emailDisplayLinks) {
187   const email = atob(eb64);
188   link.addEventListener('click', e => {
189     e.preventDefault();
190     e.target.textContent = email;
191     e.target.href = 'mailto:' + email;
192   });
193 }