Home | History | Annotate | Download | only in components
      1 /*
      2  * Copyright (C) 2006, 2007, 2008 Apple Inc.  All rights reserved.
      3  * Copyright (C) 2007 Matt Lilek (pewtermoose (at) gmail.com).
      4  * Copyright (C) 2009 Joseph Pecoraro
      5  * Copyright (C) 2011 Google Inc. All rights reserved.
      6  *
      7  * Redistribution and use in source and binary forms, with or without
      8  * modification, are permitted provided that the following conditions
      9  * are met:
     10  *
     11  * 1.  Redistributions of source code must retain the above copyright
     12  *     notice, this list of conditions and the following disclaimer.
     13  * 2.  Redistributions in binary form must reproduce the above copyright
     14  *     notice, this list of conditions and the following disclaimer in the
     15  *     documentation and/or other materials provided with the distribution.
     16  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
     17  *     its contributors may be used to endorse or promote products derived
     18  *     from this software without specific prior written permission.
     19  *
     20  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
     21  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     22  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     23  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
     24  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
     25  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
     26  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
     27  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
     29  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     30  */
     31 
     32 /**
     33  * @constructor
     34  * @extends {WebInspector.VBox}
     35  * @param {!WebInspector.Searchable} searchable
     36  */
     37 WebInspector.SearchableView = function(searchable)
     38 {
     39     WebInspector.VBox.call(this);
     40 
     41     this._searchProvider = searchable;
     42     this.element.addEventListener("keydown", this._onKeyDown.bind(this), false);
     43 
     44     this._footerElementContainer = this.element.createChild("div", "search-bar status-bar hidden");
     45     this._footerElementContainer.style.order = 100;
     46 
     47     this._footerElement = this._footerElementContainer.createChild("table", "toolbar-search");
     48     this._footerElement.cellSpacing = 0;
     49 
     50     this._firstRowElement = this._footerElement.createChild("tr");
     51     this._secondRowElement = this._footerElement.createChild("tr", "hidden");
     52 
     53     // Column 1
     54     var searchControlElementColumn = this._firstRowElement.createChild("td");
     55     this._searchControlElement = searchControlElementColumn.createChild("span", "toolbar-search-control");
     56     this._searchInputElement = this._searchControlElement.createChild("input", "search-replace");
     57     this._searchInputElement.id = "search-input-field";
     58     this._searchInputElement.placeholder = WebInspector.UIString("Find");
     59 
     60     this._matchesElement = this._searchControlElement.createChild("label", "search-results-matches");
     61     this._matchesElement.setAttribute("for", "search-input-field");
     62 
     63     this._searchNavigationElement = this._searchControlElement.createChild("div", "toolbar-search-navigation-controls");
     64 
     65     this._searchNavigationPrevElement = this._searchNavigationElement.createChild("div", "toolbar-search-navigation toolbar-search-navigation-prev");
     66     this._searchNavigationPrevElement.addEventListener("click", this._onPrevButtonSearch.bind(this), false);
     67     this._searchNavigationPrevElement.title = WebInspector.UIString("Search Previous");
     68 
     69     this._searchNavigationNextElement = this._searchNavigationElement.createChild("div", "toolbar-search-navigation toolbar-search-navigation-next");
     70     this._searchNavigationNextElement.addEventListener("click", this._onNextButtonSearch.bind(this), false);
     71     this._searchNavigationNextElement.title = WebInspector.UIString("Search Next");
     72 
     73     this._searchInputElement.addEventListener("mousedown", this._onSearchFieldManualFocus.bind(this), false); // when the search field is manually selected
     74     this._searchInputElement.addEventListener("keydown", this._onSearchKeyDown.bind(this), true);
     75     this._searchInputElement.addEventListener("input", this._onInput.bind(this), false);
     76 
     77     this._replaceInputElement = this._secondRowElement.createChild("td").createChild("input", "search-replace toolbar-replace-control");
     78     this._replaceInputElement.addEventListener("keydown", this._onReplaceKeyDown.bind(this), true);
     79     this._replaceInputElement.placeholder = WebInspector.UIString("Replace");
     80 
     81     // Column 2
     82     this._findButtonElement = this._firstRowElement.createChild("td").createChild("button", "hidden");
     83     this._findButtonElement.textContent = WebInspector.UIString("Find");
     84     this._findButtonElement.tabIndex = -1;
     85     this._findButtonElement.addEventListener("click", this._onFindClick.bind(this), false);
     86 
     87     this._replaceButtonElement = this._secondRowElement.createChild("td").createChild("button");
     88     this._replaceButtonElement.textContent = WebInspector.UIString("Replace");
     89     this._replaceButtonElement.disabled = true;
     90     this._replaceButtonElement.tabIndex = -1;
     91     this._replaceButtonElement.addEventListener("click", this._replace.bind(this), false);
     92 
     93     // Column 3
     94     this._prevButtonElement = this._firstRowElement.createChild("td").createChild("button", "hidden");
     95     this._prevButtonElement.textContent = WebInspector.UIString("Previous");
     96     this._prevButtonElement.tabIndex = -1;
     97     this._prevButtonElement.addEventListener("click", this._onPreviousClick.bind(this), false);
     98 
     99     this._replaceAllButtonElement = this._secondRowElement.createChild("td").createChild("button");
    100     this._replaceAllButtonElement.textContent = WebInspector.UIString("Replace All");
    101     this._replaceAllButtonElement.addEventListener("click", this._replaceAll.bind(this), false);
    102 
    103     // Column 4
    104     this._replaceElement = this._firstRowElement.createChild("td").createChild("span");
    105 
    106     this._replaceCheckboxElement = this._replaceElement.createChild("input");
    107     this._replaceCheckboxElement.type = "checkbox";
    108     this._uniqueId = ++WebInspector.SearchableView._lastUniqueId;
    109     var replaceCheckboxId = "search-replace-trigger" + this._uniqueId;
    110     this._replaceCheckboxElement.id = replaceCheckboxId;
    111     this._replaceCheckboxElement.addEventListener("change", this._updateSecondRowVisibility.bind(this), false);
    112 
    113     this._replaceLabelElement = this._replaceElement.createChild("label");
    114     this._replaceLabelElement.textContent = WebInspector.UIString("Replace");
    115     this._replaceLabelElement.setAttribute("for", replaceCheckboxId);
    116 
    117     // Column 5
    118     var cancelButtonElement = this._firstRowElement.createChild("td").createChild("button");
    119     cancelButtonElement.textContent = WebInspector.UIString("Cancel");
    120     cancelButtonElement.tabIndex = -1;
    121     cancelButtonElement.addEventListener("click", this.closeSearch.bind(this), false);
    122     this._minimalSearchQuerySize = 3;
    123 
    124     this._registerShortcuts();
    125 }
    126 
    127 WebInspector.SearchableView._lastUniqueId = 0;
    128 
    129 /**
    130  * @return {!Array.<!WebInspector.KeyboardShortcut.Descriptor>}
    131  */
    132 WebInspector.SearchableView.findShortcuts = function()
    133 {
    134     if (WebInspector.SearchableView._findShortcuts)
    135         return WebInspector.SearchableView._findShortcuts;
    136     WebInspector.SearchableView._findShortcuts = [WebInspector.KeyboardShortcut.makeDescriptor("f", WebInspector.KeyboardShortcut.Modifiers.CtrlOrMeta)];
    137     if (!WebInspector.isMac())
    138         WebInspector.SearchableView._findShortcuts.push(WebInspector.KeyboardShortcut.makeDescriptor(WebInspector.KeyboardShortcut.Keys.F3));
    139     return WebInspector.SearchableView._findShortcuts;
    140 }
    141 
    142 /**
    143  * @return {!Array.<!WebInspector.KeyboardShortcut.Descriptor>}
    144  */
    145 WebInspector.SearchableView.cancelSearchShortcuts = function()
    146 {
    147     if (WebInspector.SearchableView._cancelSearchShortcuts)
    148         return WebInspector.SearchableView._cancelSearchShortcuts;
    149     WebInspector.SearchableView._cancelSearchShortcuts = [WebInspector.KeyboardShortcut.makeDescriptor(WebInspector.KeyboardShortcut.Keys.Esc)];
    150     return WebInspector.SearchableView._cancelSearchShortcuts;
    151 }
    152 
    153 /**
    154  * @return {!Array.<!WebInspector.KeyboardShortcut.Descriptor>}
    155  */
    156 WebInspector.SearchableView.findNextShortcut = function()
    157 {
    158     if (WebInspector.SearchableView._findNextShortcut)
    159         return WebInspector.SearchableView._findNextShortcut;
    160     WebInspector.SearchableView._findNextShortcut = [];
    161     if (WebInspector.isMac())
    162         WebInspector.SearchableView._findNextShortcut.push(WebInspector.KeyboardShortcut.makeDescriptor("g", WebInspector.KeyboardShortcut.Modifiers.Meta));
    163     return WebInspector.SearchableView._findNextShortcut;
    164 }
    165 
    166 /**
    167  * @return {!Array.<!WebInspector.KeyboardShortcut.Descriptor>}
    168  */
    169 WebInspector.SearchableView.findPreviousShortcuts = function()
    170 {
    171     if (WebInspector.SearchableView._findPreviousShortcuts)
    172         return WebInspector.SearchableView._findPreviousShortcuts;
    173     WebInspector.SearchableView._findPreviousShortcuts = [];
    174     if (WebInspector.isMac())
    175         WebInspector.SearchableView._findPreviousShortcuts.push(WebInspector.KeyboardShortcut.makeDescriptor("g", WebInspector.KeyboardShortcut.Modifiers.Meta | WebInspector.KeyboardShortcut.Modifiers.Shift));
    176     return WebInspector.SearchableView._findPreviousShortcuts;
    177 }
    178 
    179 WebInspector.SearchableView.prototype = {
    180     /**
    181      * @return {!Element}
    182      */
    183     defaultFocusedElement: function()
    184     {
    185         var children = this.children();
    186         for (var i = 0; i < children.length; ++i) {
    187             var element = children[i].defaultFocusedElement();
    188             if (element)
    189                 return element;
    190         }
    191         return WebInspector.View.prototype.defaultFocusedElement.call(this);
    192     },
    193 
    194     /**
    195      * @param {!Event} event
    196      */
    197     _onKeyDown: function(event)
    198     {
    199         var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(/**@type {!KeyboardEvent}*/(event));
    200         var handler = this._shortcuts[shortcutKey];
    201         if (handler && handler(event))
    202             event.consume(true);
    203     },
    204 
    205     _registerShortcuts: function()
    206     {
    207         this._shortcuts = {};
    208 
    209         /**
    210          * @param {!Array.<!WebInspector.KeyboardShortcut.Descriptor>} shortcuts
    211          * @param {function()} handler
    212          * @this {WebInspector.SearchableView}
    213          */
    214         function register(shortcuts, handler)
    215         {
    216             for (var i = 0; i < shortcuts.length; ++i)
    217                 this._shortcuts[shortcuts[i].key] = handler;
    218         }
    219 
    220         register.call(this, WebInspector.SearchableView.findShortcuts(), this.handleFindShortcut.bind(this));
    221         register.call(this, WebInspector.SearchableView.cancelSearchShortcuts(), this.handleCancelSearchShortcut.bind(this));
    222         register.call(this, WebInspector.SearchableView.findNextShortcut(), this.handleFindNextShortcut.bind(this));
    223         register.call(this, WebInspector.SearchableView.findPreviousShortcuts(), this.handleFindPreviousShortcut.bind(this));
    224     },
    225 
    226     /**
    227      * @param {number} minimalSearchQuerySize
    228      */
    229     setMinimalSearchQuerySize: function(minimalSearchQuerySize)
    230     {
    231         this._minimalSearchQuerySize = minimalSearchQuerySize;
    232     },
    233 
    234     /**
    235      * @param {boolean} replaceable
    236      */
    237     setReplaceable: function(replaceable)
    238     {
    239         this._replaceable = replaceable;
    240     },
    241 
    242     /**
    243      * @param {number} matches
    244      */
    245     updateSearchMatchesCount: function(matches)
    246     {
    247         this._searchProvider.currentSearchMatches = matches;
    248         this._updateSearchMatchesCountAndCurrentMatchIndex(this._searchProvider.currentQuery ? matches : 0, -1);
    249     },
    250 
    251     /**
    252      * @param {number} currentMatchIndex
    253      */
    254     updateCurrentMatchIndex: function(currentMatchIndex)
    255     {
    256         this._updateSearchMatchesCountAndCurrentMatchIndex(this._searchProvider.currentSearchMatches, currentMatchIndex);
    257     },
    258 
    259     /**
    260      * @return {boolean}
    261      */
    262     isSearchVisible: function()
    263     {
    264         return this._searchIsVisible;
    265     },
    266 
    267     closeSearch: function()
    268     {
    269         this.cancelSearch();
    270         if (WebInspector.currentFocusElement().isDescendant(this._footerElementContainer))
    271             this.focus();
    272     },
    273 
    274     _toggleSearchBar: function(toggled)
    275     {
    276         this._footerElementContainer.classList.toggle("hidden", !toggled);
    277         this.doResize();
    278     },
    279 
    280     cancelSearch: function()
    281     {
    282         if (!this._searchIsVisible)
    283             return;
    284         this.resetSearch();
    285         delete this._searchIsVisible;
    286         this._toggleSearchBar(false);
    287     },
    288 
    289     resetSearch: function()
    290     {
    291         this._clearSearch();
    292         this._updateReplaceVisibility();
    293         this._matchesElement.textContent = "";
    294     },
    295 
    296     /**
    297      * @return {boolean}
    298      */
    299     handleFindNextShortcut: function()
    300     {
    301         if (!this._searchIsVisible)
    302             return false;
    303         this._searchProvider.jumpToNextSearchResult();
    304         return true;
    305     },
    306 
    307     /**
    308      * @return {boolean}
    309      */
    310     handleFindPreviousShortcut: function()
    311     {
    312         if (!this._searchIsVisible)
    313             return false;
    314         this._searchProvider.jumpToPreviousSearchResult();
    315         return true;
    316     },
    317 
    318     /**
    319      * @return {boolean}
    320      */
    321     handleFindShortcut: function()
    322     {
    323         this.showSearchField();
    324         return true;
    325     },
    326 
    327     /**
    328      * @return {boolean}
    329      */
    330     handleCancelSearchShortcut: function()
    331     {
    332         if (!this._searchIsVisible)
    333             return false;
    334         this.closeSearch();
    335         return true;
    336     },
    337 
    338     /**
    339      * @param {boolean} enabled
    340      */
    341     _updateSearchNavigationButtonState: function(enabled)
    342     {
    343         this._replaceButtonElement.disabled = !enabled;
    344         if (enabled) {
    345             this._searchNavigationPrevElement.classList.add("enabled");
    346             this._searchNavigationNextElement.classList.add("enabled");
    347         } else {
    348             this._searchNavigationPrevElement.classList.remove("enabled");
    349             this._searchNavigationNextElement.classList.remove("enabled");
    350         }
    351     },
    352 
    353     /**
    354      * @param {number} matches
    355      * @param {number} currentMatchIndex
    356      */
    357     _updateSearchMatchesCountAndCurrentMatchIndex: function(matches, currentMatchIndex)
    358     {
    359         if (!this._currentQuery)
    360             this._matchesElement.textContent = "";
    361         else if (matches === 0 || currentMatchIndex >= 0)
    362             this._matchesElement.textContent = WebInspector.UIString("%d of %d", currentMatchIndex + 1, matches);
    363         else if (matches === 1)
    364             this._matchesElement.textContent = WebInspector.UIString("1 match");
    365         else
    366             this._matchesElement.textContent = WebInspector.UIString("%d matches", matches);
    367         this._updateSearchNavigationButtonState(matches > 0);
    368     },
    369 
    370     showSearchField: function()
    371     {
    372         if (this._searchIsVisible)
    373             this.cancelSearch();
    374 
    375         var queryCandidate;
    376         if (WebInspector.currentFocusElement() !== this._searchInputElement) {
    377             var selection = window.getSelection();
    378             if (selection.rangeCount)
    379                 queryCandidate = selection.toString().replace(/\r?\n.*/, "");
    380         }
    381 
    382         this._toggleSearchBar(true);
    383         this._updateReplaceVisibility();
    384         if (queryCandidate)
    385             this._searchInputElement.value = queryCandidate;
    386         this._performSearch(false, false);
    387         this._searchInputElement.focus();
    388         this._searchInputElement.select();
    389         this._searchIsVisible = true;
    390     },
    391 
    392     _updateReplaceVisibility: function()
    393     {
    394         this._replaceElement.classList.toggle("hidden", !this._replaceable);
    395         if (!this._replaceable) {
    396             this._replaceCheckboxElement.checked = false;
    397             this._updateSecondRowVisibility();
    398         }
    399     },
    400 
    401     /**
    402      * @param {!Event} event
    403      */
    404     _onSearchFieldManualFocus: function(event)
    405     {
    406         WebInspector.setCurrentFocusElement(event.target);
    407     },
    408 
    409     /**
    410      * @param {!Event} event
    411      */
    412     _onSearchKeyDown: function(event)
    413     {
    414         if (!isEnterKey(event))
    415             return;
    416 
    417         if (!this._currentQuery)
    418             this._performSearch(true, true, event.shiftKey);
    419         else
    420             this._jumpToNextSearchResult(event.shiftKey);
    421     },
    422 
    423     /**
    424      * @param {!Event} event
    425      */
    426     _onReplaceKeyDown: function(event)
    427     {
    428         if (isEnterKey(event))
    429             this._replace();
    430     },
    431 
    432     /**
    433      * @param {boolean=} isBackwardSearch
    434      */
    435     _jumpToNextSearchResult: function(isBackwardSearch)
    436     {
    437         if (!this._currentQuery || !this._searchNavigationPrevElement.classList.contains("enabled"))
    438             return;
    439 
    440         if (isBackwardSearch)
    441             this._searchProvider.jumpToPreviousSearchResult();
    442         else
    443             this._searchProvider.jumpToNextSearchResult();
    444     },
    445 
    446     _onNextButtonSearch: function(event)
    447     {
    448         if (!this._searchNavigationNextElement.classList.contains("enabled"))
    449             return;
    450         this._jumpToNextSearchResult();
    451         this._searchInputElement.focus();
    452     },
    453 
    454     _onPrevButtonSearch: function(event)
    455     {
    456         if (!this._searchNavigationPrevElement.classList.contains("enabled"))
    457             return;
    458         this._jumpToNextSearchResult(true);
    459         this._searchInputElement.focus();
    460     },
    461 
    462     _onFindClick: function(event)
    463     {
    464         if (!this._currentQuery)
    465             this._performSearch(true, true);
    466         else
    467             this._jumpToNextSearchResult();
    468         this._searchInputElement.focus();
    469     },
    470 
    471     _onPreviousClick: function(event)
    472     {
    473         if (!this._currentQuery)
    474             this._performSearch(true, true, true);
    475         else
    476             this._jumpToNextSearchResult(true);
    477         this._searchInputElement.focus();
    478     },
    479 
    480     _clearSearch: function()
    481     {
    482         delete this._currentQuery;
    483         if (!!this._searchProvider.currentQuery) {
    484             delete this._searchProvider.currentQuery;
    485             this._searchProvider.searchCanceled();
    486         }
    487         this._updateSearchMatchesCountAndCurrentMatchIndex(0, -1);
    488     },
    489 
    490     /**
    491      * @param {boolean} forceSearch
    492      * @param {boolean} shouldJump
    493      * @param {boolean=} jumpBackwards
    494      */
    495     _performSearch: function(forceSearch, shouldJump, jumpBackwards)
    496     {
    497         var query = this._searchInputElement.value;
    498         if (!query || (!forceSearch && query.length < this._minimalSearchQuerySize && !this._currentQuery)) {
    499             this._clearSearch();
    500             return;
    501         }
    502 
    503         this._currentQuery = query;
    504         this._searchProvider.currentQuery = query;
    505         this._searchProvider.performSearch(query, shouldJump, jumpBackwards);
    506     },
    507 
    508     _updateSecondRowVisibility: function()
    509     {
    510         var secondRowVisible = this._replaceCheckboxElement.checked;
    511         this._footerElementContainer.classList.toggle("replaceable", secondRowVisible);
    512         this._footerElement.classList.toggle("toolbar-search-replace", secondRowVisible);
    513         this._secondRowElement.classList.toggle("hidden", !secondRowVisible);
    514         this._prevButtonElement.classList.toggle("hidden", !secondRowVisible);
    515         this._findButtonElement.classList.toggle("hidden", !secondRowVisible);
    516         this._replaceCheckboxElement.tabIndex = secondRowVisible ? -1 : 0;
    517 
    518         if (secondRowVisible)
    519             this._replaceInputElement.focus();
    520         else
    521             this._searchInputElement.focus();
    522         this.doResize();
    523     },
    524 
    525     _replace: function()
    526     {
    527         /** @type {!WebInspector.Replaceable} */ (this._searchProvider).replaceSelectionWith(this._replaceInputElement.value);
    528         delete this._currentQuery;
    529         this._performSearch(true, true);
    530     },
    531 
    532     _replaceAll: function()
    533     {
    534         /** @type {!WebInspector.Replaceable} */ (this._searchProvider).replaceAllWith(this._searchInputElement.value, this._replaceInputElement.value);
    535     },
    536 
    537     _onInput: function(event)
    538     {
    539         this._onValueChanged();
    540     },
    541 
    542     _onValueChanged: function()
    543     {
    544         this._performSearch(false, true);
    545     },
    546 
    547     __proto__: WebInspector.VBox.prototype
    548 }
    549 
    550 /**
    551  * @interface
    552  */
    553 WebInspector.Searchable = function()
    554 {
    555 }
    556 
    557 WebInspector.Searchable.prototype = {
    558     searchCanceled: function() { },
    559 
    560     /**
    561      * @param {string} query
    562      * @param {boolean} shouldJump
    563      * @param {boolean=} jumpBackwards
    564      */
    565     performSearch: function(query, shouldJump, jumpBackwards) { },
    566 
    567     jumpToNextSearchResult: function() { },
    568 
    569     jumpToPreviousSearchResult: function() { }
    570 }
    571 
    572 /**
    573  * @interface
    574  */
    575 WebInspector.Replaceable = function()
    576 {
    577 }
    578 
    579 WebInspector.Replaceable.prototype = {
    580     /**
    581      * @param {string} text
    582      */
    583     replaceSelectionWith: function(text) { },
    584 
    585     /**
    586      * @param {string} query
    587      * @param {string} replacement
    588      */
    589     replaceAllWith: function(query, replacement) { }
    590 }
    591