4 const menuButton = document.getElementById('menu-button');
5 const menuDropDown = document.querySelector('#header .main-nav');
7 menuButton.addEventListener('click', function(event) {
8 menuDropDown.classList.toggle('showing');
9 event.stopPropagation();
12 document.body.addEventListener('click', function(event) {
13 const isShown = menuDropDown.classList.contains('showing');
15 menuDropDown.classList.remove('showing');
16 event.stopPropagation();
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)
27 function videoClick() {
28 if (typeof InstallTrigger !== 'undefined') return;
29 this.paused ? this.play() : this.pause();
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;
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]);
45 for (let child of children) {
46 if (typeof child === 'string') {
47 child = new Text(child);
55 const searchForm = document.getElementById('site-search-form');
56 const searchInput = document.getElementById('site-search-input');
57 const searchDialog = searchForm.querySelector('dialog');
59 async function runSearch() {
60 const searchTerm = searchInput.value;
64 const resp = await fetch(`/search.php?query=${encodeURIComponent(searchTerm)}`);
65 pages = await resp.json();
67 searchDialog.innerHTML = '<strong class="search-category-title">Failed to load search results</strong>';
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;
80 // Categorizes pages to display
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: []},
87 const categoryNames = Object.keys(categorised);
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;
98 (pageCategory || categorised.other).pages.push(page);
101 const categoryResults = categoryNames.map(name => {
102 const category = categorised[name];
103 if (category.pages.length === 0) {
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]);
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);
118 while (searchDialog.firstChild) {
119 searchDialog.removeChild(searchDialog.firstChild);
122 searchDialog.append(results);
126 function showSearchDialog() {
127 if (searchDialog.open) {
133 const clickListener = e => {
134 if(!e.target.closest('dialog')) {
139 const escListener = e => {
140 if (e.key === 'Escape') {
145 const mouseLeaveListener = e => {
149 const closeListener = () => {
150 searchDialog.close();
151 document.removeEventListener('click', clickListener);
152 document.removeEventListener('keydown', escListener);
153 searchForm.removeEventListener('mouseleave', mouseLeaveListener);
156 document.addEventListener('click', clickListener);
157 document.addEventListener('keydown', escListener);
158 searchForm.addEventListener('mouseleave', mouseLeaveListener);
161 function showSearchLoading() {
162 searchDialog.innerHTML = `<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>`;
166 searchForm.addEventListener('submit', event => {
167 event.preventDefault();
172 searchInput.addEventListener('input', event => {
173 const termLength = searchInput.value.length;
174 if (termLength === 0) {
175 searchDialog.close();
176 } else if (termLength > 2) {
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 => {
190 e.target.textContent = email;
191 e.target.href = 'mailto:' + email;