| 284 | | |
| 285 | | |
| 286 | | |
| 287 | | /* ________________________________________________________________________________________________________ |
| 288 | | |
| 289 | | Class: SearchPanel |
| 290 | | ________________________________________________________________________________________________________ |
| 291 | | |
| 292 | | A class handling everything associated with the search panel. |
| 293 | | |
| 294 | | Parameters: |
| 295 | | |
| 296 | | name - The name of the global variable that will be storing this instance. Is needed to be able to set timeouts. |
| 297 | | mode - The mode the search is going to work in. Pass <NaturalDocs::Builder::Base->CommandLineOption()>, so the |
| 298 | | value will be something like "HTML" or "FramedHTML". |
| 299 | | |
| 300 | | ________________________________________________________________________________________________________ |
| 301 | | */ |
| 302 | | |
| 303 | | |
| 304 | | function SearchPanel(name, mode, resultsPath) |
| 305 | | { |
| 306 | | if (!name || !mode || !resultsPath) |
| 307 | | { alert("Incorrect parameters to SearchPanel."); }; |
| 308 | | |
| 309 | | |
| 310 | | // Group: Variables |
| 311 | | // ________________________________________________________________________ |
| 312 | | |
| 313 | | /* |
| 314 | | var: name |
| 315 | | The name of the global variable that will be storing this instance of the class. |
| 316 | | */ |
| 317 | | this.name = name; |
| 318 | | |
| 319 | | /* |
| 320 | | var: mode |
| 321 | | The mode the search is going to work in, such as "HTML" or "FramedHTML". |
| 322 | | */ |
| 323 | | this.mode = mode; |
| 324 | | |
| 325 | | /* |
| 326 | | var: resultsPath |
| 327 | | The relative path from the current HTML page to the results page directory. |
| 328 | | */ |
| 329 | | this.resultsPath = resultsPath; |
| 330 | | |
| 331 | | /* |
| 332 | | var: keyTimeout |
| 333 | | The timeout used between a keystroke and when a search is performed. |
| 334 | | */ |
| 335 | | this.keyTimeout = 0; |
| 336 | | |
| 337 | | /* |
| 338 | | var: keyTimeoutLength |
| 339 | | The length of <keyTimeout> in thousandths of a second. |
| 340 | | */ |
| 341 | | this.keyTimeoutLength = 500; |
| 342 | | |
| 343 | | /* |
| 344 | | var: lastSearchValue |
| 345 | | The last search string executed, or an empty string if none. |
| 346 | | */ |
| 347 | | this.lastSearchValue = ""; |
| 348 | | |
| 349 | | /* |
| 350 | | var: lastResultsPage |
| 351 | | The last results page. The value is only relevant if <lastSearchValue> is set. |
| 352 | | */ |
| 353 | | this.lastResultsPage = ""; |
| 354 | | |
| 355 | | /* |
| 356 | | var: deactivateTimeout |
| 357 | | |
| 358 | | The timeout used between when a control is deactivated and when the entire panel is deactivated. Is necessary |
| 359 | | because a control may be deactivated in favor of another control in the same panel, in which case it should stay |
| 360 | | active. |
| 361 | | */ |
| 362 | | this.deactivateTimout = 0; |
| 363 | | |
| 364 | | /* |
| 365 | | var: deactivateTimeoutLength |
| 366 | | The length of <deactivateTimeout> in thousandths of a second. |
| 367 | | */ |
| 368 | | this.deactivateTimeoutLength = 200; |
| 369 | | |
| 370 | | |
| 371 | | |
| 372 | | |
| 373 | | // Group: DOM Elements |
| 374 | | // ________________________________________________________________________ |
| 375 | | |
| 376 | | |
| 377 | | // Function: DOMSearchField |
| 378 | | this.DOMSearchField = function() |
| 379 | | { return document.getElementById("MSearchField"); }; |
| 380 | | |
| 381 | | // Function: DOMSearchType |
| 382 | | this.DOMSearchType = function() |
| 383 | | { return document.getElementById("MSearchType"); }; |
| 384 | | |
| 385 | | // Function: DOMPopupSearchResults |
| 386 | | this.DOMPopupSearchResults = function() |
| 387 | | { return document.getElementById("MSearchResults"); }; |
| 388 | | |
| 389 | | // Function: DOMPopupSearchResultsWindow |
| 390 | | this.DOMPopupSearchResultsWindow = function() |
| 391 | | { return document.getElementById("MSearchResultsWindow"); }; |
| 392 | | |
| 393 | | // Function: DOMSearchPanel |
| 394 | | this.DOMSearchPanel = function() |
| 395 | | { return document.getElementById("MSearchPanel"); }; |
| 396 | | |
| 397 | | |
| 398 | | |
| 399 | | |
| 400 | | // Group: Event Handlers |
| 401 | | // ________________________________________________________________________ |
| 402 | | |
| 403 | | |
| 404 | | /* |
| 405 | | Function: OnSearchFieldFocus |
| 406 | | Called when focus is added or removed from the search field. |
| 407 | | */ |
| 408 | | this.OnSearchFieldFocus = function(isActive) |
| 409 | | { |
| 410 | | this.Activate(isActive); |
| 411 | | }; |
| 412 | | |
| 413 | | |
| 414 | | /* |
| 415 | | Function: OnSearchFieldChange |
| 416 | | Called when the content of the search field is changed. |
| 417 | | */ |
| 418 | | this.OnSearchFieldChange = function() |
| 419 | | { |
| 420 | | if (this.keyTimeout) |
| 421 | | { |
| 422 | | clearTimeout(this.keyTimeout); |
| 423 | | this.keyTimeout = 0; |
| 424 | | }; |
| 425 | | |
| 426 | | var searchValue = this.DOMSearchField().value.replace(/ +/g, ""); |
| 427 | | |
| 428 | | if (searchValue != this.lastSearchValue) |
| 429 | | { |
| 430 | | if (searchValue != "") |
| 431 | | { |
| 432 | | this.keyTimeout = setTimeout(this.name + ".Search()", this.keyTimeoutLength); |
| 433 | | } |
| 434 | | else |
| 435 | | { |
| 436 | | if (this.mode == "HTML") |
| 437 | | { this.DOMPopupSearchResultsWindow().style.display = "none"; }; |
| 438 | | this.lastSearchValue = ""; |
| 439 | | }; |
| 440 | | }; |
| 441 | | }; |
| 442 | | |
| 443 | | |
| 444 | | /* |
| 445 | | Function: OnSearchTypeFocus |
| 446 | | Called when focus is added or removed from the search type. |
| 447 | | */ |
| 448 | | this.OnSearchTypeFocus = function(isActive) |
| 449 | | { |
| 450 | | this.Activate(isActive); |
| 451 | | }; |
| 452 | | |
| 453 | | |
| 454 | | /* |
| 455 | | Function: OnSearchTypeChange |
| 456 | | Called when the search type is changed. |
| 457 | | */ |
| 458 | | this.OnSearchTypeChange = function() |
| 459 | | { |
| 460 | | var searchValue = this.DOMSearchField().value.replace(/ +/g, ""); |
| 461 | | |
| 462 | | if (searchValue != "") |
| 463 | | { |
| 464 | | this.Search(); |
| 465 | | }; |
| 466 | | }; |
| 467 | | |
| 468 | | |
| 469 | | |
| 470 | | // Group: Action Functions |
| 471 | | // ________________________________________________________________________ |
| 472 | | |
| 473 | | |
| 474 | | /* |
| 475 | | Function: CloseResultsWindow |
| 476 | | Closes the results window. |
| 477 | | */ |
| 478 | | this.CloseResultsWindow = function() |
| 479 | | { |
| 480 | | this.DOMPopupSearchResultsWindow().style.display = "none"; |
| 481 | | this.Activate(false, true); |
| 482 | | }; |
| 483 | | |
| 484 | | |
| 485 | | /* |
| 486 | | Function: Search |
| 487 | | Performs a search. |
| 488 | | */ |
| 489 | | this.Search = function() |
| 490 | | { |
| 491 | | this.keyTimeout = 0; |
| 492 | | |
| 493 | | var searchValue = this.DOMSearchField().value.replace(/^ +/, ""); |
| 494 | | var searchTopic = this.DOMSearchType().value; |
| 495 | | |
| 496 | | var pageExtension = searchValue.substr(0,1); |
| 497 | | |
| 498 | | if (pageExtension.match(/^[a-z]/i)) |
| 499 | | { pageExtension = pageExtension.toUpperCase(); } |
| 500 | | else if (pageExtension.match(/^[0-9]/)) |
| 501 | | { pageExtension = 'Numbers'; } |
| 502 | | else |
| 503 | | { pageExtension = "Symbols"; }; |
| 504 | | |
| 505 | | var resultsPage; |
| 506 | | var resultsPageWithSearch; |
| 507 | | var hasResultsPage; |
| 508 | | |
| 509 | | // indexSectionsWithContent is defined in searchdata.js |
| 510 | | if (indexSectionsWithContent[searchTopic][pageExtension] == true) |
| 511 | | { |
| 512 | | resultsPage = this.resultsPath + '/' + searchTopic + pageExtension + '.html'; |
| 513 | | resultsPageWithSearch = resultsPage+'?'+escape(searchValue); |
| 514 | | hasResultsPage = true; |
| 515 | | } |
| 516 | | else |
| 517 | | { |
| 518 | | resultsPage = this.resultsPath + '/NoResults.html'; |
| 519 | | resultsPageWithSearch = resultsPage; |
| 520 | | hasResultsPage = false; |
| 521 | | }; |
| 522 | | |
| 523 | | var resultsFrame; |
| 524 | | if (this.mode == "HTML") |
| 525 | | { resultsFrame = window.frames.MSearchResults; } |
| 526 | | else if (this.mode == "FramedHTML") |
| 527 | | { resultsFrame = window.top.frames['Content']; }; |
| 528 | | |
| 529 | | |
| 530 | | if (resultsPage != this.lastResultsPage || |
| 531 | | |
| 532 | | // Bug in IE. If everything becomes hidden in a run, none of them will be able to be reshown in the next for some |
| 533 | | // reason. It counts the right number of results, and you can even read the display as "block" after setting it, but it |
| 534 | | // just doesn't work in IE 6 or IE 7. So if we're on the right page but the previous search had no results, reload the |
| 535 | | // page anyway to get around the bug. |
| 536 | | (browserType == "IE" && hasResultsPage && resultsFrame.searchResults.lastMatchCount == 0) ) |
| 537 | | |
| 538 | | { |
| 539 | | resultsFrame.location.href = resultsPageWithSearch; |
| 540 | | } |
| 541 | | |
| 542 | | // So if the results page is right and there's no IE bug, reperform the search on the existing page. We have to check if there |
| 543 | | // are results because NoResults.html doesn't have any JavaScript, and it would be useless to do anything on that page even |
| 544 | | // if it did. |
| 545 | | else if (hasResultsPage) |
| 546 | | { |
| 547 | | // We need to check if this exists in case the frame is present but didn't finish loading. |
| 548 | | if (resultsFrame.searchResults) |
| 549 | | { resultsFrame.searchResults.Search(searchValue); } |
| 550 | | |
| 551 | | // Otherwise just reload instead of waiting. |
| 552 | | else |
| 553 | | { resultsFrame.location.href = resultsPageWithSearch; }; |
| 554 | | }; |
| 555 | | |
| 556 | | |
| 557 | | var domPopupSearchResultsWindow = this.DOMPopupSearchResultsWindow(); |
| 558 | | |
| 559 | | if (this.mode == "HTML" && domPopupSearchResultsWindow.style.display != "block") |
| 560 | | { |
| 561 | | var domSearchType = this.DOMSearchType(); |
| 562 | | |
| 563 | | var left = GetXPosition(domSearchType); |
| 564 | | var top = GetYPosition(domSearchType) + domSearchType.offsetHeight; |
| 565 | | |
| 566 | | MoveToPosition(domPopupSearchResultsWindow, left, top); |
| 567 | | domPopupSearchResultsWindow.style.display = 'block'; |
| 568 | | }; |
| 569 | | |
| 570 | | |
| 571 | | this.lastSearchValue = searchValue; |
| 572 | | this.lastResultsPage = resultsPage; |
| 573 | | }; |
| 574 | | |
| 575 | | |
| 576 | | |
| 577 | | // Group: Activation Functions |
| 578 | | // Functions that handle whether the entire panel is active or not. |
| 579 | | // ________________________________________________________________________ |
| 580 | | |
| 581 | | |
| 582 | | /* |
| 583 | | Function: Activate |
| 584 | | |
| 585 | | Activates or deactivates the search panel, resetting things to their default values if necessary. You can call this on every |
| 586 | | control's OnBlur() and it will handle not deactivating the entire panel when focus is just switching between them transparently. |
| 587 | | |
| 588 | | Parameters: |
| 589 | | |
| 590 | | isActive - Whether you're activating or deactivating the panel. |
| 591 | | ignoreDeactivateDelay - Set if you're positive the action will deactivate the panel and thus want to skip the delay. |
| 592 | | */ |
| 593 | | this.Activate = function(isActive, ignoreDeactivateDelay) |
| 594 | | { |
| 595 | | // We want to ignore isActive being false while the results window is open. |
| 596 | | if (isActive || (this.mode == "HTML" && this.DOMPopupSearchResultsWindow().style.display == "block")) |
| 597 | | { |
| 598 | | if (this.inactivateTimeout) |
| 599 | | { |
| 600 | | clearTimeout(this.inactivateTimeout); |
| 601 | | this.inactivateTimeout = 0; |
| 602 | | }; |
| 603 | | |
| 604 | | this.DOMSearchPanel().className = 'MSearchPanelActive'; |
| 605 | | |
| 606 | | var searchField = this.DOMSearchField(); |
| 607 | | |
| 608 | | if (searchField.value == 'Search') |
| 609 | | { searchField.value = ""; } |
| 610 | | } |
| 611 | | else if (!ignoreDeactivateDelay) |
| 612 | | { |
| 613 | | this.inactivateTimeout = setTimeout(this.name + ".InactivateAfterTimeout()", this.inactivateTimeoutLength); |
| 614 | | } |
| 615 | | else |
| 616 | | { |
| 617 | | this.InactivateAfterTimeout(); |
| 618 | | }; |
| 619 | | }; |
| 620 | | |
| 621 | | |
| 622 | | /* |
| 623 | | Function: InactivateAfterTimeout |
| 624 | | |
| 625 | | Called by <inactivateTimeout>, which is set by <Activate()>. Inactivation occurs on a timeout because a control may |
| 626 | | receive OnBlur() when focus is really transferring to another control in the search panel. In this case we don't want to |
| 627 | | actually deactivate the panel because not only would that cause a visible flicker but it could also reset the search value. |
| 628 | | So by doing it on a timeout instead, there's a short period where the second control's OnFocus() can cancel the deactivation. |
| 629 | | */ |
| 630 | | this.InactivateAfterTimeout = function() |
| 631 | | { |
| 632 | | this.inactivateTimeout = 0; |
| 633 | | |
| 634 | | this.DOMSearchPanel().className = 'MSearchPanelInactive'; |
| 635 | | this.DOMSearchField().value = "Search"; |
| 636 | | }; |
| 637 | | }; |
| 638 | | |
| 639 | | |
| 640 | | |
| 641 | | |
| 642 | | /* ________________________________________________________________________________________________________ |
| 643 | | |
| 644 | | Class: SearchResults |
| 645 | | _________________________________________________________________________________________________________ |
| 646 | | |
| 647 | | The class that handles everything on the search results page. |
| 648 | | _________________________________________________________________________________________________________ |
| 649 | | */ |
| 650 | | |
| 651 | | |
| 652 | | function SearchResults(name, mode) |
| 653 | | { |
| 654 | | /* |
| 655 | | var: mode |
| 656 | | The mode the search is going to work in, such as "HTML" or "FramedHTML". |
| 657 | | */ |
| 658 | | this.mode = mode; |
| 659 | | |
| 660 | | /* |
| 661 | | var: lastMatchCount |
| 662 | | The number of matches from the last run of <Search()>. |
| 663 | | */ |
| 664 | | this.lastMatchCount = 0; |
| 665 | | |
| 666 | | |
| 667 | | /* |
| 668 | | Function: Toggle |
| 669 | | Toggles the visibility of the passed element ID. |
| 670 | | */ |
| 671 | | this.Toggle = function(id) |
| 672 | | { |
| 673 | | if (this.mode == "FramedHTML") |
| 674 | | { return; }; |
| 675 | | |
| 676 | | var parentElement = document.getElementById(id); |
| 677 | | |
| 678 | | var element = parentElement.firstChild; |
| 679 | | |
| 680 | | while (element && element != parentElement) |
| 681 | | { |
| 682 | | if (element.nodeName == 'DIV' && element.className == 'ISubIndex') |
| 683 | | { |
| 684 | | if (element.style.display == 'block') |
| 685 | | { element.style.display = "none"; } |
| 686 | | else |
| 687 | | { element.style.display = 'block'; } |
| 688 | | }; |
| 689 | | |
| 690 | | if (element.nodeName == 'DIV' && element.hasChildNodes()) |
| 691 | | { element = element.firstChild; } |
| 692 | | else if (element.nextSibling) |
| 693 | | { element = element.nextSibling; } |
| 694 | | else |
| 695 | | { |
| 696 | | do |
| 697 | | { |
| 698 | | element = element.parentNode; |
| 699 | | } |
| 700 | | while (element && element != parentElement && !element.nextSibling); |
| 701 | | |
| 702 | | if (element && element != parentElement) |
| 703 | | { element = element.nextSibling; }; |
| 704 | | }; |
| 705 | | }; |
| 706 | | }; |
| 707 | | |
| 708 | | |
| 709 | | /* |
| 710 | | Function: Search |
| 711 | | |
| 712 | | Searches for the passed string. If there is no parameter, it takes it from the URL query. |
| 713 | | |
| 714 | | Always returns true, since other documents may try to call it and that may or may not be possible. |
| 715 | | */ |
| 716 | | this.Search = function(search) |
| 717 | | { |
| 718 | | if (!search) |
| 719 | | { |
| 720 | | search = window.location.search; |
| 721 | | search = search.substring(1); // Remove the leading ? |
| 722 | | search = unescape(search); |
| 723 | | }; |
| 724 | | |
| 725 | | search = search.replace(/^ +/, ""); |
| 726 | | search = search.replace(/ +$/, ""); |
| 727 | | search = search.toLowerCase(); |
| 728 | | |
| 729 | | if (search.match(/[^a-z0-9]/)) // Just a little speedup so it doesn't have to go through the below unnecessarily. |
| 730 | | { |
| 731 | | search = search.replace(/\_/g, "_und"); |
| 732 | | search = search.replace(/\ +/gi, "_spc"); |
| 733 | | search = search.replace(/\~/g, "_til"); |
| 734 | | search = search.replace(/\!/g, "_exc"); |
| 735 | | search = search.replace(/\@/g, "_att"); |
| 736 | | search = search.replace(/\#/g, "_num"); |
| 737 | | search = search.replace(/\$/g, "_dol"); |
| 738 | | search = search.replace(/\%/g, "_pct"); |
| 739 | | search = search.replace(/\^/g, "_car"); |
| 740 | | search = search.replace(/\&/g, "_amp"); |
| 741 | | search = search.replace(/\*/g, "_ast"); |
| 742 | | search = search.replace(/\(/g, "_lpa"); |
| 743 | | search = search.replace(/\)/g, "_rpa"); |
| 744 | | search = search.replace(/\-/g, "_min"); |
| 745 | | search = search.replace(/\+/g, "_plu"); |
| 746 | | search = search.replace(/\=/g, "_equ"); |
| 747 | | search = search.replace(/\{/g, "_lbc"); |
| 748 | | search = search.replace(/\}/g, "_rbc"); |
| 749 | | search = search.replace(/\[/g, "_lbk"); |
| 750 | | search = search.replace(/\]/g, "_rbk"); |
| 751 | | search = search.replace(/\:/g, "_col"); |
| 752 | | search = search.replace(/\;/g, "_sco"); |
| 753 | | search = search.replace(/\"/g, "_quo" |