on d943c20 help sign in
urbit.org/static/scripts.js
323 lines 9.5 kB history raw
1// toggling of navigation menus
2var toggleMainNav = document.getElementById('js-main-nav-toggle');
3if (toggleMainNav !== null) {
4 toggleMainNav.onclick = function() {
5 document.body.classList.toggle('has-active-main-nav');
6 }
7}
9var toggleContentNav = document.getElementById('js-content-nav-toggle');
10if (toggleContentNav !== null) {
11 toggleContentNav.onclick = function() {
12 document.body.classList.toggle('has-active-content-nav');
13 }
16function initHotKeys() {
18 var bodyEl = document.body;
19 var searchInput = document.getElementById('search');
21 document.addEventListener('keydown', function (event) {
22 if (event.defaultPrevented) {
23 return;
24 }
26 var key = event.key || event.keyCode;
28 // ESC to close search window
29 if (key === 'Escape' || key === 'Esc' || key === 27) {
30 document.body.classList.remove('has-active-search-window');
31 console.log('esc');
32 }
34 // / to close toggle window
35 if (key === '/' || key === 'Period' || key === 58) {
36 event.preventDefault();
37 document.body.classList.toggle('has-active-search-window');
38 if (bodyEl.classList.contains('has-active-search-window')) {
39 searchInput.focus();
40 }
41 }
43 //when search is active TAB puts search input into focus
44 if (bodyEl.classList.contains('has-active-search-window')) {
45 if (key === 'Tab' || key === 'Tab' || key === 9) {
46 searchInput.focus();
47 }
48 }
49 });
50};
53// search functionality
54function debounce(func, wait) {
55 var timeout;
57 return function () {
58 var context = this;
59 var args = arguments;
60 clearTimeout(timeout);
62 timeout = setTimeout(function () {
63 timeout = null;
64 func.apply(context, args);
65 }, wait);
66 };
69// The strategy is as follows:
70// First, assign a value to each word in the document:
71// Words that correspond to search terms (stemmer aware): 40
72// Normal words: 2
73// First word in a sentence: 8
74// Then use a sliding window with a constant number of words and count the
75// sum of the values of the words within the window. Then use the window that got the
76// maximum sum. If there are multiple maximas, then get the last one.
77// Enclose the terms in <b>.
78function makeTeaser(body, terms) {
79 var TERM_WEIGHT = 40;
80 var NORMAL_WORD_WEIGHT = 2;
81 var FIRST_WORD_WEIGHT = 8;
82 //var TEASER_MAX_WORDS = 17;
83 var TEASER_MAX_CHARS = 63;
85 var stemmedTerms = terms.map(function (w) {
86 return elasticlunr.stemmer(w.toLowerCase());
87 });
89 var termFound = false;
90 var index = 0;
91 var weighted = []; // contains elements of ["word", weight, index_in_document]
93 // split in sentences, then words
94 var sentences = body.toLowerCase().split(". ");
96 for (var i in sentences) {
97 var words = sentences[i].split(" ");
98 var value = FIRST_WORD_WEIGHT;
100 for (var j in words) {
101 var word = words[j];
103 if (word.length > 0) {
104 for (var k in stemmedTerms) {
105 if (elasticlunr.stemmer(word).startsWith(stemmedTerms[k])) {
106 value = TERM_WEIGHT;
107 termFound = true;
108 }
109 }
110 weighted.push([word, value, index]);
111 value = NORMAL_WORD_WEIGHT;
112 }
114 index += word.length;
115 index += 1; // ' ' or '.' if last word in sentence
116 }
118 index += 1; // because we split at a two-char boundary '. '
119 }
121 if (weighted.length === 0) {
122 return body;
123 }
125 var windowWeights = [];
126 var windowSize = 0;
128 // loop over words (weighted) till max chars is reached adding a counter to windowSize every time a new word has passed
129 var sumChars = 0;
130 for (var i = 0; i < weighted.length; i++) {
131 sumChars += weighted[i][0].length;
132 if (sumChars <= TEASER_MAX_CHARS) {
133 windowSize++;
134 }
135 }
137 // We add a window with all the weights first
138 var curSum = 0;
139 for (var i = 0; i < windowSize; i++) {
140 curSum += weighted[i][1];
141 }
142 windowWeights.push(curSum);
144 for (var i = 0; i < weighted.length - windowSize; i++) {
145 curSum -= weighted[i][1];
146 curSum += weighted[i + windowSize][1];
147 windowWeights.push(curSum);
148 }
150 // If we didn't find the term, just pick the first window
151 var maxSumIndex = 0;
152 if (termFound) {
153 var maxFound = 0;
154 // backwards
155 for (var i = windowWeights.length - 1; i >= 0; i--) {
156 if (windowWeights[i] > maxFound) {
157 maxFound = windowWeights[i];
158 maxSumIndex = i;
159 }
160 }
161 }
163 var teaser = [];
164 var startIndex = weighted[maxSumIndex][2];
165 for (var i = maxSumIndex; i < maxSumIndex + windowSize; i++) {
166 var word = weighted[i];
167 if (startIndex < word[2]) {
168 // missing text from index to start of `word`
169 teaser.push(body.substring(startIndex, word[2]));
170 startIndex = word[2];
171 }
173 startIndex = word[2] + word[0].length;
174 teaser.push(body.substring(word[2], startIndex));
176 }
177 teaser.push("…");
178 return teaser.join("");
181function formatSearchResultTitle(item) {
182 var pathArray = item.ref.split( '/' );
184 // first directory is in 4th element of split path array
185 var firstPartTitle = pathArray[3];
187 // last directory is in 2nd to last element of array
188 var lastPartTitle = item.doc.title;
190 var fullTitle = firstPartTitle + '<span class="gray90 inline-block pl2">' + ' / ' + lastPartTitle + '</span>';
192 return fullTitle;
195function formatSearchResultItem(item, terms) {
196 console.log(item);
197 var li = document.createElement("li");
198 var createA = document.createElement("a");
199 li.appendChild(createA);
200 var teaserTitle = formatSearchResultTitle(item);
201 li.classList.add("search-results__item");
202 li.classList.add("ph3");
203 var hrefA = item.ref;
204 createA.setAttribute('href', hrefA);
205 createA.setAttribute('class','no-underline block pl6 pv3');
206 createA.innerHTML = `<span class="fs45 capitalize">${teaserTitle}</span>`;
207 createA.innerHTML += `<span class='pr7 fs5 none float-right'>→</span>`;
208 createA.innerHTML += `<div class="fs35 truncate mr10">${makeTeaser(item.doc.body, terms)}</div>`;
209 return li;
212function initSearch() {
213 var searchInput = document.getElementById("search");
214 var searchForm = document.getElementById("search-form");
215 var inputReset = searchForm.nextElementSibling;
217 if (!searchInput) {
218 return;
219 }
221 var searchResults = document.querySelector(".search-results");
222 var searchResultsHeader = document.querySelector(".search-results__header");
223 var searchResultsItems = document.querySelector(".search-results__items");
224 var MAX_ITEMS = 200;
225 var options = {
226 bool: "AND",
227 expand: true, // turn on partial word search
228 fields: {
229 title: {boost: 2},
230 body: {boost: 1},
231 }
232 };
233 var currentTerm = "";
234 var index = elasticlunr.Index.load(window.searchIndex);
235 // toggle search window
236 var toggleSearchWindow = document.getElementById('js-search-window-toggle');
237 var bodyEl = document.body;
238 var searchOverlay = document.getElementById('search-overlay');
239 var searchWindow = document.getElementById('search-window');
240 var searchActive = bodyEl.classList.contains('has-active-search-window');
242 if (toggleSearchWindow !== null) {
243 toggleSearchWindow.onclick = function() {
244 inputReset.style.display="none";
246 //activate search
247 bodyEl.classList.toggle('has-active-search-window');
248 //put input in focus when activating search
249 searchInput.focus();
251 searchInput.value = "";
252 searchResultsHeader.value = "";
253 searchResults.style.display = searchInput.value.trim() === "" ? "none" : "block";
254 }
255 searchOverlay.addEventListener('click', function(e){
256 if (!searchWindow.contains(e.target)){
257 bodyEl.classList.toggle('has-active-search-window');
258 }
259 })
260 }
263 searchInput.addEventListener("keyup", debounce(function() {
264 inputReset.style.display="block";
266 searchResultsHeader.value = "";
267 var term = searchInput.value.trim();
268 /*
269 //removed to fix: a state where there’s a valid search term, but no results
271 if (term === currentTerm || !index) {
272 return;
273 }*/
275 searchResults.style.display = term === "" ? "none" : "block";
276 searchResults.style.overflowY = term === "" ? "hidden" : "scroll";
277 searchResultsItems.innerHTML = "";
278 if (term === "") {
279 inputReset.style.display="none";
280 return;
281 }
283 var results = index.search(term, options).filter(function (r) {
284 return r.doc.body !== "";
285 });
287 if (results.length === 0) {
288 searchResultsHeader.innerText = `No search results for '${term}'.`;
289 return;
290 }
292 currentTerm = term;
293 searchResultsHeader.innerText = `${results.length} search results for '${term}':`;
294 for (var i = 0; i < Math.min(results.length, MAX_ITEMS); i++) {
295 if (!results[i].doc.body) {
296 continue;
297 }
298 searchResultsItems.appendChild(formatSearchResultItem(results[i], term.split(" ")));
299 }
300 }, 150));
302 // reset input button
303 inputReset.onclick=function(){
304 document.getElementById('search-form').reset();
305 }
306 searchForm.addEventListener("reset", function(event){
307 var e = document.createEvent('KeyboardEvent');
308 e.initEvent("keyup", false, true);
309 searchInput.dispatchEvent(e);
310 });
314if (document.readyState === "complete" ||
315 (document.readyState !== "loading" && !document.documentElement.doScroll)
316) {
317 initToggleSearchWindow();
318} else {
319 document.addEventListener("DOMContentLoaded", function () {
320 initSearch();
321 initHotKeys();
322 });