1/* 2 @licstart The following is the entire license notice for the JavaScript code in this file. 3 4 The MIT License (MIT) 5 6 Copyright (C) 1997-2020 by Dimitri van Heesch 7 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: 13 14 The above copyright notice and this permission notice shall be included in all copies or 15 substantial portions of the Software. 16 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. 22 23 @licend The above is the entire license notice for the JavaScript code in this file 24 */ 25const SEARCH_COOKIE_NAME = ''+'search_grp'; 26 27const searchResults = new SearchResults(); 28 29/* A class handling everything associated with the search panel. 30 31 Parameters: 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 35*/ 36function SearchBox(name, resultsPath, extension) { 37 if (!name || !resultsPath) { alert("Missing parameters to SearchBox."); } 38 if (!extension || extension == "") { extension = ".html"; } 39 40 function getXPos(item) { 41 let x = 0; 42 if (item.offsetWidth) { 43 while (item && item!=document.body) { 44 x += item.offsetLeft; 45 item = item.offsetParent; 46 } 47 } 48 return x; 49 } 50 51 function getYPos(item) { 52 let y = 0; 53 if (item.offsetWidth) { 54 while (item && item!=document.body) { 55 y += item.offsetTop; 56 item = item.offsetParent; 57 } 58 } 59 return y; 60 } 61 62 // ---------- Instance variables 63 this.name = name; 64 this.resultsPath = resultsPath; 65 this.keyTimeout = 0; 66 this.keyTimeoutLength = 500; 67 this.closeSelectionTimeout = 300; 68 this.lastSearchValue = ""; 69 this.lastResultsPage = ""; 70 this.hideTimeout = 0; 71 this.searchIndex = 0; 72 this.searchActive = false; 73 this.extension = extension; 74 75 // ----------- DOM Elements 76 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"); 84 85 // ------------ Event Handlers 86 87 // Called when focus is added or removed from the search field. 88 this.OnSearchFieldFocus = function(isActive) { 89 this.Activate(isActive); 90 } 91 92 this.OnSearchSelectShow = function() { 93 const searchSelectWindow = this.DOMSearchSelectWindow(); 94 const searchField = this.DOMSearchSelect(); 95 96 const left = getXPos(searchField); 97 const top = getYPos(searchField) + searchField.offsetHeight; 98 99 // show search selection popup 100 searchSelectWindow.style.display='block'; 101 searchSelectWindow.style.left = left + 'px'; 102 searchSelectWindow.style.top = top + 'px'; 103 104 // stop selection hide timer 105 if (this.hideTimeout) { 106 clearTimeout(this.hideTimeout); 107 this.hideTimeout=0; 108 } 109 return false; // to avoid "image drag" default event 110 } 111 112 this.OnSearchSelectHide = function() { 113 this.hideTimeout = setTimeout(this.CloseSelectionWindow.bind(this), 114 this.closeSelectionTimeout); 115 } 116 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); 121 this.keyTimeout = 0; 122 } 123 124 const e = evt ? evt : window.event; // for IE 125 if (e.keyCode==40 || e.keyCode==13) { 126 if (e.shiftKey==1) { 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') { 132 child.focus(); 133 return; 134 } 135 } 136 return; 137 } else { 138 const elem = searchResults.NavNext(0); 139 if (elem) elem.focus(); 140 } 141 } else if (e.keyCode==27) { // Escape out of the search field 142 e.stopPropagation(); 143 this.DOMSearchField().blur(); 144 this.DOMPopupSearchResultsWindow().style.display = 'none'; 145 this.DOMSearchClose().style.display = 'none'; 146 this.lastSearchValue = ''; 147 this.Activate(false); 148 return; 149 } 150 151 // strip whitespaces 152 const searchValue = this.DOMSearchField().value.replace(/ +/g, ""); 153 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 = ''; 162 } 163 } 164 } 165 166 this.SelectItemCount = function() { 167 let count=0; 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') { 172 count++; 173 } 174 } 175 return count; 176 } 177 178 this.GetSelectionIdByName = function(name) { 179 let j=0; 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) { 185 return j; 186 } 187 j++; 188 } 189 } 190 return 0; 191 } 192 193 this.SelectItemSet = function(id) { 194 let j=0; 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; 200 if (j==id) { 201 node.innerHTML='•'; 202 Cookie.writeSetting(SEARCH_COOKIE_NAME, child.childNodes[1].nodeValue, 0) 203 } else { 204 node.innerHTML=' '; 205 } 206 j++; 207 } 208 } 209 } 210 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 218 this.Search(); 219 } 220 } 221 222 this.OnSearchSelectKey = function(evt) { 223 const e = (evt) ? evt : window.event; // for IE 224 if (e.keyCode==40 && this.searchIndex<this.SelectItemCount()) { // Down 225 this.searchIndex++; 226 this.OnSelectItem(this.searchIndex); 227 } else if (e.keyCode==38 && this.searchIndex>0) { // Up 228 this.searchIndex--; 229 this.OnSelectItem(this.searchIndex); 230 } else if (e.keyCode==13 || e.keyCode==27) { 231 e.stopPropagation(); 232 this.OnSelectItem(this.searchIndex); 233 this.CloseSelectionWindow(); 234 this.DOMSearchField().focus(); 235 } 236 return false; 237 } 238 239 // --------- Actions 240 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); 246 } 247 248 this.CloseSelectionWindow = function() { 249 this.DOMSearchSelectWindow().style.display = 'none'; 250 } 251 252 // Performs a search. 253 this.Search = function() { 254 this.keyTimeout = 0; 255 256 // strip leading whitespace 257 const searchValue = this.DOMSearchField().value.replace(/^ +/, ""); 258 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); 263 } 264 265 let jsFile; 266 let idx = indexSectionsWithContent[this.searchIndex].indexOf(idxChar); 267 if (idx!=-1) { 268 const hexCode=idx.toString(16); 269 jsFile = this.resultsPath + indexSectionNames[this.searchIndex] + '_' + hexCode + '.js'; 270 } 271 272 const loadJS = function(url, impl, loc) { 273 const scriptTag = document.createElement('script'); 274 scriptTag.src = url; 275 scriptTag.onload = impl; 276 scriptTag.onreadystatechange = impl; 277 loc.appendChild(scriptTag); 278 } 279 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; 285 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"; 291 } 292 293 if (idx!=-1) { 294 searchResults.Search(searchValue); 295 } else { // no file with search results => force empty search results 296 searchResults.Search('===='); 297 } 298 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; 307 let width = 300; 308 if (left<10) left=10; 309 if (width+left+8>maxWidth) width=maxWidth-left-8; 310 let height = 400; 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'; 316 } 317 } 318 319 if (jsFile) { 320 loadJS(jsFile, handleResults, this.DOMPopupSearchResultsWindow()); 321 } else { 322 handleResults(); 323 } 324 325 this.lastSearchValue = searchValue; 326 } 327 328 // -------- Activation Functions 329 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' 335 ) { 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 = ''; 344 } 345 } 346} 347 348// ----------------------------------------------------------------------- 349 350// The class that handles everything on the search results page. 351function SearchResults() { 352 353 function convertToId(search) { 354 let result = ''; 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]/)) { 359 result+=c; 360 } else if (cn<16) { 361 result+="_0"+cn.toString(16); 362 } else { 363 result+="_"+cn.toString(16); 364 } 365 } 366 return result; 367 } 368 369 // The number of matches from the last run of <Search()>. 370 this.lastMatchCount = 0; 371 this.lastKey = 0; 372 this.repeatOn = false; 373 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; 378 379 while (element && element!=parentElement) { 380 if (element.nodeName.toLowerCase() == 'div' && element.className == 'SRChildren') { 381 return element; 382 } 383 384 if (element.nodeName.toLowerCase() == 'div' && element.hasChildNodes()) { 385 element = element.firstChild; 386 } else if (element.nextSibling) { 387 element = element.nextSibling; 388 } else { 389 do { 390 element = element.parentNode; 391 } 392 while (element && element!=parentElement && !element.nextSibling); 393 394 if (element && element!=parentElement) { 395 element = element.nextSibling; 396 } 397 } 398 } 399 } 400 401 this.Toggle = function(id) { 402 const element = this.FindChildElement(id); 403 if (element) { 404 if (element.style.display == 'block') { 405 element.style.display = 'none'; 406 } else { 407 element.style.display = 'block'; 408 } 409 } 410 } 411 412 // Searches for the passed string. If there is no parameter, 413 // it takes it from the URL query. 414 // 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); 422 } 423 424 search = search.replace(/^ +/, ""); // strip leading spaces 425 search = search.replace(/ +$/, ""); // strip trailing spaces 426 search = search.toLowerCase(); 427 search = convertToId(search); 428 429 const resultRows = document.getElementsByTagName("div"); 430 let matches = 0; 431 432 let i = 0; 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_' 438 439 if (search.length<=rowMatchName.length && 440 rowMatchName.substr(0, search.length)==search) { 441 row.style.display = 'block'; 442 matches++; 443 } else { 444 row.style.display = 'none'; 445 } 446 } 447 i++; 448 } 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'; 454 } 455 this.lastMatchCount = matches; 456 return true; 457 } 458 459 // return the first item with index index or higher that is visible 460 this.NavNext = function(index) { 461 let focusItem; 462 for (;;) { 463 const focusName = 'Item'+index; 464 focusItem = document.getElementById(focusName); 465 if (focusItem && focusItem.parentNode.parentNode.style.display=='block') { 466 break; 467 } else if (!focusItem) { // last element 468 break; 469 } 470 focusItem=null; 471 index++; 472 } 473 return focusItem; 474 } 475 476 this.NavPrev = function(index) { 477 let focusItem; 478 for (;;) { 479 const focusName = 'Item'+index; 480 focusItem = document.getElementById(focusName); 481 if (focusItem && focusItem.parentNode.parentNode.style.display=='block') { 482 break; 483 } else if (!focusItem) { // last element 484 break; 485 } 486 focusItem=null; 487 index--; 488 } 489 return focusItem; 490 } 491 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 500 } 501 } else if (e.type == "keyup") { 502 this.lastKey = 0; 503 this.repeatOn = false; 504 } 505 return this.lastKey!=0; 506 } 507 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; 512 513 if (this.lastKey==38) { // Up 514 const newIndex = itemIndex-1; 515 let focusItem = this.NavPrev(newIndex); 516 if (focusItem) { 517 let child = this.FindChildElement(focusItem.parentNode.parentNode.id); 518 if (child && child.style.display == 'block') { // children visible 519 let n=0; 520 let tmpElem; 521 for (;;) { // search for last child 522 tmpElem = document.getElementById('Item'+newIndex+'_c'+n); 523 if (tmpElem) { 524 focusItem = tmpElem; 525 } else { // found it! 526 break; 527 } 528 n++; 529 } 530 } 531 } 532 if (focusItem) { 533 focusItem.focus(); 534 } else { // return focus to search field 535 document.getElementById("MSearchField").focus(); 536 } 537 } else if (this.lastKey==40) { // Down 538 const newIndex = itemIndex+1; 539 let focusItem; 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'); 544 } 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 556 e.stopPropagation(); 557 searchBox.CloseResultsWindow(); 558 document.getElementById("MSearchField").focus(); 559 } else if (this.lastKey==13) { // Enter 560 return true; 561 } 562 return false; 563 } 564 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; 569 570 if (this.lastKey==38) { // Up 571 if (childIndex>0) { 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(); 576 } 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); 582 } 583 if (elem) { 584 elem.focus(); 585 } 586 } else if (this.lastKey==27) { // Escape 587 e.stopPropagation(); 588 searchBox.CloseResultsWindow(); 589 document.getElementById("MSearchField").focus(); 590 } else if (this.lastKey==13) { // Enter 591 return true; 592 } 593 return false; 594 } 595} 596 597function createResults(resultsPath) { 598 599 function setKeyActions(elem,action) { 600 elem.setAttribute('onkeydown',action); 601 elem.setAttribute('onkeypress',action); 602 elem.setAttribute('onkeyup',action); 603 } 604 605 function setClassAttr(elem,attr) { 606 elem.setAttribute('class',attr); 607 elem.setAttribute('className',attr); 608 } 609 610 const results = document.getElementById("SRResults"); 611 results.innerHTML = ''; 612 searchData.forEach((elem,index) => { 613 const id = elem[0]; 614 const srResult = document.createElement('div'); 615 srResult.setAttribute('id','SR_'+id); 616 setClassAttr(srResult,'SRResult'); 617 const srEntry = document.createElement('div'); 618 setClassAttr(srEntry,'SREntry'); 619 const srLink = document.createElement('a'); 620 srLink.setAttribute('id','Item'+index); 621 setKeyActions(srLink,'return searchResults.Nav(event,'+index+')'); 622 setClassAttr(srLink,'SRSymbol'); 623 srLink.innerHTML = elem[1][0]; 624 srEntry.appendChild(srLink); 625 if (elem[1].length==2) { // single result 626 srLink.setAttribute('href',resultsPath+elem[1][1][0]); 627 srLink.setAttribute('onclick','searchBox.CloseResultsWindow()'); 628 if (elem[1][1][1]) { 629 srLink.setAttribute('target','_parent'); 630 } else { 631 srLink.setAttribute('target','_blank'); 632 } 633 const srScope = document.createElement('span'); 634 setClassAttr(srScope,'SRScope'); 635 srScope.innerHTML = elem[1][1][2]; 636 srEntry.appendChild(srScope); 637 } else { // multiple results 638 srLink.setAttribute('href','javascript:searchResults.Toggle("SR_'+id+'")'); 639 const srChildren = document.createElement('div'); 640 setClassAttr(srChildren,'SRChildren'); 641 for (let c=0; c<elem[1].length-1; c++) { 642 const srChild = document.createElement('a'); 643 srChild.setAttribute('id','Item'+index+'_c'+c); 644 setKeyActions(srChild,'return searchResults.NavChild(event,'+index+','+c+')'); 645 setClassAttr(srChild,'SRScope'); 646 srChild.setAttribute('href',resultsPath+elem[1][c+1][0]); 647 srChild.setAttribute('onclick','searchBox.CloseResultsWindow()'); 648 if (elem[1][c+1][1]) { 649 srChild.setAttribute('target','_parent'); 650 } else { 651 srChild.setAttribute('target','_blank'); 652 } 653 srChild.innerHTML = elem[1][c+1][2]; 654 srChildren.appendChild(srChild); 655 } 656 srEntry.appendChild(srChildren); 657 } 658 srResult.appendChild(srEntry); 659 results.appendChild(srResult); 660 }); 661} 662 663function init_search() { 664 const results = document.getElementById("MSearchSelectWindow"); 665 666 results.tabIndex=0; 667 for (let key in indexSectionLabels) { 668 const link = document.createElement('a'); 669 link.setAttribute('class','SelectItem'); 670 link.setAttribute('onclick','searchBox.OnSelectItem('+key+')'); 671 link.href='javascript:void(0)'; 672 link.innerHTML='<span class="SelectionMark"> </span>'+indexSectionLabels[key]; 673 results.appendChild(link); 674 } 675 676 const input = document.getElementById("MSearchSelect"); 677 const searchSelectWindow = document.getElementById("MSearchSelectWindow"); 678 input.tabIndex=0; 679 input.addEventListener("keydown", function(event) { 680 if (event.keyCode==13 || event.keyCode==40) { 681 event.preventDefault(); 682 if (searchSelectWindow.style.display == 'block') { 683 searchBox.CloseSelectionWindow(); 684 } else { 685 searchBox.OnSearchSelectShow(); 686 searchBox.DOMSearchSelectWindow().focus(); 687 } 688 } 689 }); 690 const name = Cookie.readSetting(SEARCH_COOKIE_NAME,0); 691 const id = searchBox.GetSelectionIdByName(name); 692 searchBox.OnSelectItem(id); 693} 694/* @license-end */ 695