2 @licstart The following is the entire license notice for the JavaScript code in this file.
6 Copyright (C) 1997-2020 by Dimitri van Heesch
8 Permission is hereby granted, free of charge, to any person obtaining a copy of this software
9 and associated documentation files (the "Software"), to deal in the Software without restriction,
10 including without limitation the rights to use, copy, modify, merge, publish, distribute,
11 sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
12 furnished to do so, subject to the following conditions:
14 The above copyright notice and this permission notice shall be included in all copies or
15 substantial portions of the Software.
17 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
18 BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
20 DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 @licend The above is the entire license notice for the JavaScript code in this file
25const SEARCH_COOKIE_NAME = ''+'search_grp';
27const searchResults = new SearchResults();
29/* A class handling everything associated with the search panel.
32 name - The name of the global variable that will be
33 storing this instance. Is needed to be able to set timeouts.
34 resultPath - path to use for external files
36function SearchBox(name, resultsPath, extension) {
37 if (!name || !resultsPath) { alert("Missing parameters to SearchBox."); }
38 if (!extension || extension == "") { extension = ".html"; }
40 function getXPos(item) {
42 if (item.offsetWidth) {
43 while (item && item!=document.body) {
45 item = item.offsetParent;
51 function getYPos(item) {
53 if (item.offsetWidth) {
54 while (item && item!=document.body) {
56 item = item.offsetParent;
62 // ---------- Instance variables
64 this.resultsPath = resultsPath;
66 this.keyTimeoutLength = 500;
67 this.closeSelectionTimeout = 300;
68 this.lastSearchValue = "";
69 this.lastResultsPage = "";
72 this.searchActive = false;
73 this.extension = extension;
75 // ----------- DOM Elements
77 this.DOMSearchField = () => document.getElementById("MSearchField");
78 this.DOMSearchSelect = () => document.getElementById("MSearchSelect");
79 this.DOMSearchSelectWindow = () => document.getElementById("MSearchSelectWindow");
80 this.DOMPopupSearchResults = () => document.getElementById("MSearchResults");
81 this.DOMPopupSearchResultsWindow = () => document.getElementById("MSearchResultsWindow");
82 this.DOMSearchClose = () => document.getElementById("MSearchClose");
83 this.DOMSearchBox = () => document.getElementById("MSearchBox");
85 // ------------ Event Handlers
87 // Called when focus is added or removed from the search field.
88 this.OnSearchFieldFocus = function(isActive) {
89 this.Activate(isActive);
92 this.OnSearchSelectShow = function() {
93 const searchSelectWindow = this.DOMSearchSelectWindow();
94 const searchField = this.DOMSearchSelect();
96 const left = getXPos(searchField);
97 const top = getYPos(searchField) + searchField.offsetHeight;
99 // show search selection popup
100 searchSelectWindow.style.display='block';
101 searchSelectWindow.style.left = left + 'px';
102 searchSelectWindow.style.top = top + 'px';
104 // stop selection hide timer
105 if (this.hideTimeout) {
106 clearTimeout(this.hideTimeout);
109 return false; // to avoid "image drag" default event
112 this.OnSearchSelectHide = function() {
113 this.hideTimeout = setTimeout(this.CloseSelectionWindow.bind(this),
114 this.closeSelectionTimeout);
117 // Called when the content of the search field is changed.
118 this.OnSearchFieldChange = function(evt) {
119 if (this.keyTimeout) { // kill running timer
120 clearTimeout(this.keyTimeout);
124 const e = evt ? evt : window.event; // for IE
125 if (e.keyCode==40 || e.keyCode==13) {
127 this.OnSearchSelectShow();
128 const win=this.DOMSearchSelectWindow();
129 for (let i=0;i<win.childNodes.length;i++) {
130 const child = win.childNodes[i]; // get span within a
131 if (child.className=='SelectItem') {
138 const elem = searchResults.NavNext(0);
139 if (elem) elem.focus();
141 } else if (e.keyCode==27) { // Escape out of the search field
143 this.DOMSearchField().blur();
144 this.DOMPopupSearchResultsWindow().style.display = 'none';
145 this.DOMSearchClose().style.display = 'none';
146 this.lastSearchValue = '';
147 this.Activate(false);
152 const searchValue = this.DOMSearchField().value.replace(/ +/g, "");
154 if (searchValue != this.lastSearchValue) { // search value has changed
155 if (searchValue != "") { // non-empty search
156 // set timer for search update
157 this.keyTimeout = setTimeout(this.Search.bind(this), this.keyTimeoutLength);
158 } else { // empty search field
159 this.DOMPopupSearchResultsWindow().style.display = 'none';
160 this.DOMSearchClose().style.display = 'none';
161 this.lastSearchValue = '';
166 this.SelectItemCount = function() {
168 const win=this.DOMSearchSelectWindow();
169 for (let i=0;i<win.childNodes.length;i++) {
170 const child = win.childNodes[i]; // get span within a
171 if (child.className=='SelectItem') {
178 this.GetSelectionIdByName = function(name) {
180 const win=this.DOMSearchSelectWindow();
181 for (let i=0;i<win.childNodes.length;i++) {
182 const child = win.childNodes[i];
183 if (child.className=='SelectItem') {
184 if (child.childNodes[1].nodeValue==name) {
193 this.SelectItemSet = function(id) {
195 const win=this.DOMSearchSelectWindow();
196 for (let i=0;i<win.childNodes.length;i++) {
197 const child = win.childNodes[i]; // get span within a
198 if (child.className=='SelectItem') {
199 const node = child.firstChild;
201 node.innerHTML='•';
202 Cookie.writeSetting(SEARCH_COOKIE_NAME, child.childNodes[1].nodeValue, 0)
204 node.innerHTML=' ';
211 // Called when an search filter selection is made.
212 // set item with index id as the active item
213 this.OnSelectItem = function(id) {
214 this.searchIndex = id;
215 this.SelectItemSet(id);
216 const searchValue = this.DOMSearchField().value.replace(/ +/g, "");
217 if (searchValue!="" && this.searchActive) { // something was found -> do a search
222 this.OnSearchSelectKey = function(evt) {
223 const e = (evt) ? evt : window.event; // for IE
224 if (e.keyCode==40 && this.searchIndex<this.SelectItemCount()) { // Down
226 this.OnSelectItem(this.searchIndex);
227 } else if (e.keyCode==38 && this.searchIndex>0) { // Up
229 this.OnSelectItem(this.searchIndex);
230 } else if (e.keyCode==13 || e.keyCode==27) {
232 this.OnSelectItem(this.searchIndex);
233 this.CloseSelectionWindow();
234 this.DOMSearchField().focus();
241 // Closes the results window.
242 this.CloseResultsWindow = function() {
243 this.DOMPopupSearchResultsWindow().style.display = 'none';
244 this.DOMSearchClose().style.display = 'none';
245 this.Activate(false);
248 this.CloseSelectionWindow = function() {
249 this.DOMSearchSelectWindow().style.display = 'none';
252 // Performs a search.
253 this.Search = function() {
256 // strip leading whitespace
257 const searchValue = this.DOMSearchField().value.replace(/^ +/, "");
259 const code = searchValue.toLowerCase().charCodeAt(0);
260 let idxChar = searchValue.substr(0, 1).toLowerCase();
261 if ( 0xD800 <= code && code <= 0xDBFF && searchValue > 1) { // surrogate pair
262 idxChar = searchValue.substr(0, 2);
266 let idx = indexSectionsWithContent[this.searchIndex].indexOf(idxChar);
268 const hexCode=idx.toString(16);
269 jsFile = this.resultsPath + indexSectionNames[this.searchIndex] + '_' + hexCode + '.js';
272 const loadJS = function(url, impl, loc) {
273 const scriptTag = document.createElement('script');
275 scriptTag.onload = impl;
276 scriptTag.onreadystatechange = impl;
277 loc.appendChild(scriptTag);
280 const domPopupSearchResultsWindow = this.DOMPopupSearchResultsWindow();
281 const domSearchBox = this.DOMSearchBox();
282 const domPopupSearchResults = this.DOMPopupSearchResults();
283 const domSearchClose = this.DOMSearchClose();
284 const resultsPath = this.resultsPath;
286 const handleResults = function() {
287 document.getElementById("Loading").style.display="none";
288 if (typeof searchData !== 'undefined') {
289 createResults(resultsPath);
290 document.getElementById("NoMatches").style.display="none";
294 searchResults.Search(searchValue);
295 } else { // no file with search results => force empty search results
296 searchResults.Search('====');
299 if (domPopupSearchResultsWindow.style.display!='block') {
300 domSearchClose.style.display = 'inline-block';
301 let left = getXPos(domSearchBox) + 150;
302 let top = getYPos(domSearchBox) + 20;
303 domPopupSearchResultsWindow.style.display = 'block';
304 left -= domPopupSearchResults.offsetWidth;
305 const maxWidth = document.body.clientWidth;
306 const maxHeight = document.body.clientHeight;
308 if (left<10) left=10;
309 if (width+left+8>maxWidth) width=maxWidth-left-8;
311 if (height+top+8>maxHeight) height=maxHeight-top-8;
312 domPopupSearchResultsWindow.style.top = top + 'px';
313 domPopupSearchResultsWindow.style.left = left + 'px';
314 domPopupSearchResultsWindow.style.width = width + 'px';
315 domPopupSearchResultsWindow.style.height = height + 'px';
320 loadJS(jsFile, handleResults, this.DOMPopupSearchResultsWindow());
325 this.lastSearchValue = searchValue;
328 // -------- Activation Functions
330 // Activates or deactivates the search panel, resetting things to
331 // their default values if necessary.
332 this.Activate = function(isActive) {
333 if (isActive || // open it
334 this.DOMPopupSearchResultsWindow().style.display == 'block'
336 this.DOMSearchBox().className = 'MSearchBoxActive';
337 this.searchActive = true;
338 } else if (!isActive) { // directly remove the panel
339 this.DOMSearchBox().className = 'MSearchBoxInactive';
340 this.searchActive = false;
341 this.lastSearchValue = ''
342 this.lastResultsPage = '';
343 this.DOMSearchField().value = '';
348// -----------------------------------------------------------------------
350// The class that handles everything on the search results page.
351function SearchResults() {
353 function convertToId(search) {
355 for (let i=0;i<search.length;i++) {
356 const c = search.charAt(i);
357 const cn = c.charCodeAt(0);
358 if (c.match(/[a-z0-9\u0080-\uFFFF]/)) {
361 result+="_0"+cn.toString(16);
363 result+="_"+cn.toString(16);
369 // The number of matches from the last run of <Search()>.
370 this.lastMatchCount = 0;
372 this.repeatOn = false;
374 // Toggles the visibility of the passed element ID.
375 this.FindChildElement = function(id) {
376 const parentElement = document.getElementById(id);
377 let element = parentElement.firstChild;
379 while (element && element!=parentElement) {
380 if (element.nodeName.toLowerCase() == 'div' && element.className == 'SRChildren') {
384 if (element.nodeName.toLowerCase() == 'div' && element.hasChildNodes()) {
385 element = element.firstChild;
386 } else if (element.nextSibling) {
387 element = element.nextSibling;
390 element = element.parentNode;
392 while (element && element!=parentElement && !element.nextSibling);
394 if (element && element!=parentElement) {
395 element = element.nextSibling;
401 this.Toggle = function(id) {
402 const element = this.FindChildElement(id);
404 if (element.style.display == 'block') {
405 element.style.display = 'none';
407 element.style.display = 'block';
412 // Searches for the passed string. If there is no parameter,
413 // it takes it from the URL query.
415 // Always returns true, since other documents may try to call it
416 // and that may or may not be possible.
417 this.Search = function(search) {
418 if (!search) { // get search word from URL
419 search = window.location.search;
420 search = search.substring(1); // Remove the leading '?'
421 search = unescape(search);
424 search = search.replace(/^ +/, ""); // strip leading spaces
425 search = search.replace(/ +$/, ""); // strip trailing spaces
426 search = search.toLowerCase();
427 search = convertToId(search);
429 const resultRows = document.getElementsByTagName("div");
433 while (i < resultRows.length) {
434 const row = resultRows.item(i);
435 if (row.className == "SRResult") {
436 let rowMatchName = row.id.toLowerCase();
437 rowMatchName = rowMatchName.replace(/^sr\d*_/, ''); // strip 'sr123_'
439 if (search.length<=rowMatchName.length &&
440 rowMatchName.substr(0, search.length)==search) {
441 row.style.display = 'block';
444 row.style.display = 'none';
449 document.getElementById("Searching").style.display='none';
450 if (matches == 0) { // no results
451 document.getElementById("NoMatches").style.display='block';
452 } else { // at least one result
453 document.getElementById("NoMatches").style.display='none';
455 this.lastMatchCount = matches;
459 // return the first item with index index or higher that is visible
460 this.NavNext = function(index) {
463 const focusName = 'Item'+index;
464 focusItem = document.getElementById(focusName);
465 if (focusItem && focusItem.parentNode.parentNode.style.display=='block') {
467 } else if (!focusItem) { // last element
476 this.NavPrev = function(index) {
479 const focusName = 'Item'+index;
480 focusItem = document.getElementById(focusName);
481 if (focusItem && focusItem.parentNode.parentNode.style.display=='block') {
483 } else if (!focusItem) { // last element
492 this.ProcessKeys = function(e) {
493 if (e.type == "keydown") {
494 this.repeatOn = false;
495 this.lastKey = e.keyCode;
496 } else if (e.type == "keypress") {
497 if (!this.repeatOn) {
498 if (this.lastKey) this.repeatOn = true;
499 return false; // ignore first keypress after keydown
501 } else if (e.type == "keyup") {
503 this.repeatOn = false;
505 return this.lastKey!=0;
508 this.Nav = function(evt,itemIndex) {
509 const e = (evt) ? evt : window.event; // for IE
510 if (e.keyCode==13) return true;
511 if (!this.ProcessKeys(e)) return false;
513 if (this.lastKey==38) { // Up
514 const newIndex = itemIndex-1;
515 let focusItem = this.NavPrev(newIndex);
517 let child = this.FindChildElement(focusItem.parentNode.parentNode.id);
518 if (child && child.style.display == 'block') { // children visible
521 for (;;) { // search for last child
522 tmpElem = document.getElementById('Item'+newIndex+'_c'+n);
525 } else { // found it!
534 } else { // return focus to search field
535 document.getElementById("MSearchField").focus();
537 } else if (this.lastKey==40) { // Down
538 const newIndex = itemIndex+1;
540 const item = document.getElementById('Item'+itemIndex);
541 const elem = this.FindChildElement(item.parentNode.parentNode.id);
542 if (elem && elem.style.display == 'block') { // children visible
543 focusItem = document.getElementById('Item'+itemIndex+'_c0');
545 if (!focusItem) focusItem = this.NavNext(newIndex);
546 if (focusItem) focusItem.focus();
547 } else if (this.lastKey==39) { // Right
548 const item = document.getElementById('Item'+itemIndex);
549 const elem = this.FindChildElement(item.parentNode.parentNode.id);
550 if (elem) elem.style.display = 'block';
551 } else if (this.lastKey==37) { // Left
552 const item = document.getElementById('Item'+itemIndex);
553 const elem = this.FindChildElement(item.parentNode.parentNode.id);
554 if (elem) elem.style.display = 'none';
555 } else if (this.lastKey==27) { // Escape
557 searchBox.CloseResultsWindow();
558 document.getElementById("MSearchField").focus();
559 } else if (this.lastKey==13) { // Enter
565 this.NavChild = function(evt,itemIndex,childIndex) {
566 const e = (evt) ? evt : window.event; // for IE
567 if (e.keyCode==13) return true;
568 if (!this.ProcessKeys(e)) return false;
570 if (this.lastKey==38) { // Up
572 const newIndex = childIndex-1;
573 document.getElementById('Item'+itemIndex+'_c'+newIndex).focus();
574 } else { // already at first child, jump to parent
575 document.getElementById('Item'+itemIndex).focus();
577 } else if (this.lastKey==40) { // Down
578 const newIndex = childIndex+1;
579 let elem = document.getElementById('Item'+itemIndex+'_c'+newIndex);
580 if (!elem) { // last child, jump to parent next parent
581 elem = this.NavNext(itemIndex+1);
586 } else if (this.lastKey==27) { // Escape
588 searchBox.CloseResultsWindow();
589 document.getElementById("MSearchField").focus();
590 } else if (this.lastKey==13) { // Enter
597function createResults(resultsPath) {
599 function setKeyActions(elem,action) {
600 elem.setAttribute('onkeydown',action);
601 elem.setAttribute('onkeypress',action);
602 elem.setAttribute('onkeyup',action);
605 function setClassAttr(elem,attr) {
606 elem.setAttribute('class',attr);
607 elem.setAttribute('className',attr);
610 const decodeHtml = (html) => {
611 const txt = document.createElement("textarea");
612 txt.innerHTML = html;
616 const results = document.getElementById("SRResults");
617 results.innerHTML = '';
618 searchData.forEach((elem,index) => {
620 const srResult = document.createElement('div');
621 srResult.setAttribute('id','SR_'+id);
622 setClassAttr(srResult,'SRResult');
623 const srEntry = document.createElement('div');
624 setClassAttr(srEntry,'SREntry');
625 const srLink = document.createElement('a');
626 srLink.setAttribute('id','Item'+index);
627 setKeyActions(srLink,'return searchResults.Nav(event,'+index+')');
628 setClassAttr(srLink,'SRSymbol');
629 srLink.innerHTML = decodeHtml(elem[1][0]);
630 srEntry.appendChild(srLink);
631 if (elem[1].length==2) { // single result
632 if (elem[1][1][0].startsWith('http://') || elem[1][1][0].startsWith('https://')) { // absolute path
633 srLink.setAttribute('href',elem[1][1][0]);
634 } else { // relative path
635 srLink.setAttribute('href',resultsPath+elem[1][1][0]);
637 srLink.setAttribute('onclick','searchBox.CloseResultsWindow()');
639 srLink.setAttribute('target','_parent');
641 srLink.setAttribute('target','_blank');
643 const srScope = document.createElement('span');
644 setClassAttr(srScope,'SRScope');
645 srScope.innerHTML = decodeHtml(elem[1][1][2]);
646 srEntry.appendChild(srScope);
647 } else { // multiple results
648 srLink.setAttribute('href','javascript:searchResults.Toggle("SR_'+id+'")');
649 const srChildren = document.createElement('div');
650 setClassAttr(srChildren,'SRChildren');
651 for (let c=0; c<elem[1].length-1; c++) {
652 const srChild = document.createElement('a');
653 srChild.setAttribute('id','Item'+index+'_c'+c);
654 setKeyActions(srChild,'return searchResults.NavChild(event,'+index+','+c+')');
655 setClassAttr(srChild,'SRScope');
656 if (elem[1][c+1][0].startsWith('http://') || elem[1][c+1][0].startsWith('https://')) { // absolute path
657 srChild.setAttribute('href',elem[1][c+1][0]);
658 } else { // relative path
659 srChild.setAttribute('href',resultsPath+elem[1][c+1][0]);
661 srChild.setAttribute('onclick','searchBox.CloseResultsWindow()');
662 if (elem[1][c+1][1]) {
663 srChild.setAttribute('target','_parent');
665 srChild.setAttribute('target','_blank');
667 srChild.innerHTML = decodeHtml(elem[1][c+1][2]);
668 srChildren.appendChild(srChild);
670 srEntry.appendChild(srChildren);
672 srResult.appendChild(srEntry);
673 results.appendChild(srResult);
677function init_search() {
678 const results = document.getElementById("MSearchSelectWindow");
681 for (let key in indexSectionLabels) {
682 const link = document.createElement('a');
683 link.setAttribute('class','SelectItem');
684 link.setAttribute('onclick','searchBox.OnSelectItem('+key+')');
685 link.href='javascript:void(0)';
686 link.innerHTML='<span class="SelectionMark"> </span>'+indexSectionLabels[key];
687 results.appendChild(link);
690 const input = document.getElementById("MSearchSelect");
691 const searchSelectWindow = document.getElementById("MSearchSelectWindow");
693 input.addEventListener("keydown", function(event) {
694 if (event.keyCode==13 || event.keyCode==40) {
695 event.preventDefault();
696 if (searchSelectWindow.style.display == 'block') {
697 searchBox.CloseSelectionWindow();
699 searchBox.OnSearchSelectShow();
700 searchBox.DOMSearchSelectWindow().focus();
704 const name = Cookie.readSetting(SEARCH_COOKIE_NAME,0);
705 const id = searchBox.GetSelectionIdByName(name);
706 searchBox.OnSelectItem(id);