Home | History | Annotate | Download | only in front_end
      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  */
     35 WebInspector.SearchController = function()
     36 {
     37     this._element = document.createElement("table");
     38     this._element.className = "toolbar-search";
     39     this._element.cellSpacing = 0;
     40 
     41     this._firstRowElement = this._element.createChild("tr");
     42     this._secondRowElement = this._element.createChild("tr", "hidden");
     43 
     44     // Column 1
     45     var searchControlElementColumn = this._firstRowElement.createChild("td");
     46     this._searchControlElement = searchControlElementColumn.createChild("span", "toolbar-search-control");
     47     this._searchInputElement = this._searchControlElement.createChild("input", "search-replace");
     48     this._searchInputElement.id = "search-input-field";
     49 
     50     this._matchesElement = this._searchControlElement.createChild("label", "search-results-matches");
     51     this._matchesElement.setAttribute("for", "search-input-field");
     52 
     53     this._searchNavigationElement = this._searchControlElement.createChild("div", "toolbar-search-navigation-controls");
     54     this._toggleFilterUI(false);
     55 
     56     this._searchNavigationPrevElement = this._searchNavigationElement.createChild("div", "toolbar-search-navigation toolbar-search-navigation-prev");
     57     this._searchNavigationPrevElement.addEventListener("click", this._onPrevButtonSearch.bind(this), false);
     58     this._searchNavigationPrevElement.title = WebInspector.UIString("Search Previous");
     59 
     60     this._searchNavigationNextElement = this._searchNavigationElement.createChild("div", "toolbar-search-navigation toolbar-search-navigation-next");
     61     this._searchNavigationNextElement.addEventListener("click", this._onNextButtonSearch.bind(this), false);
     62     this._searchNavigationNextElement.title = WebInspector.UIString("Search Next");
     63 
     64     this._searchInputElement.addEventListener("mousedown", this._onSearchFieldManualFocus.bind(this), false); // when the search field is manually selected
     65     this._searchInputElement.addEventListener("keydown", this._onKeyDown.bind(this), true);
     66     this._searchInputElement.addEventListener("input", this._onInput.bind(this), false);
     67 
     68     this._replaceInputElement = this._secondRowElement.createChild("td").createChild("input", "search-replace toolbar-replace-control");
     69     this._replaceInputElement.addEventListener("keydown", this._onKeyDown.bind(this), true);
     70     this._replaceInputElement.placeholder = WebInspector.UIString("Replace");
     71 
     72     // Column 2
     73     this._findButtonElement = this._firstRowElement.createChild("td").createChild("button", "hidden");
     74     this._findButtonElement.textContent = WebInspector.UIString("Find");
     75     this._findButtonElement.tabIndex = -1;
     76     this._findButtonElement.addEventListener("click", this._onNextButtonSearch.bind(this), false);
     77 
     78     this._replaceButtonElement = this._secondRowElement.createChild("td").createChild("button");
     79     this._replaceButtonElement.textContent = WebInspector.UIString("Replace");
     80     this._replaceButtonElement.disabled = true;
     81     this._replaceButtonElement.tabIndex = -1;
     82     this._replaceButtonElement.addEventListener("click", this._replace.bind(this), false);
     83 
     84     // Column 3
     85     this._prevButtonElement = this._firstRowElement.createChild("td").createChild("button", "hidden");
     86     this._prevButtonElement.textContent = WebInspector.UIString("Previous");
     87     this._prevButtonElement.disabled = true;
     88     this._prevButtonElement.tabIndex = -1;
     89     this._prevButtonElement.addEventListener("click", this._onPrevButtonSearch.bind(this), false);
     90 
     91     this._replaceAllButtonElement = this._secondRowElement.createChild("td").createChild("button");
     92     this._replaceAllButtonElement.textContent = WebInspector.UIString("Replace All");
     93     this._replaceAllButtonElement.addEventListener("click", this._replaceAll.bind(this), false);
     94 
     95     // Column 4
     96     this._replaceElement = this._firstRowElement.createChild("td").createChild("span");
     97 
     98     this._replaceCheckboxElement = this._replaceElement.createChild("input");
     99     this._replaceCheckboxElement.type = "checkbox";
    100     this._replaceCheckboxElement.id = "search-replace-trigger";
    101     this._replaceCheckboxElement.addEventListener("click", this._updateSecondRowVisibility.bind(this), false);
    102 
    103     this._replaceLabelElement = this._replaceElement.createChild("label");
    104     this._replaceLabelElement.textContent = WebInspector.UIString("Replace");
    105     this._replaceLabelElement.setAttribute("for", "search-replace-trigger");
    106 
    107     // Column 5
    108     this._filterCheckboxContainer = this._firstRowElement.createChild("td").createChild("label");
    109     this._filterCheckboxContainer.setAttribute("for", "filter-trigger");
    110 
    111     this._filterCheckboxElement = this._filterCheckboxContainer.createChild("input");
    112     this._filterCheckboxElement.type = "checkbox";
    113     this._filterCheckboxElement.id = "filter-trigger";
    114     this._filterCheckboxElement.addEventListener("click", this._filterCheckboxClick.bind(this), false);
    115 
    116     this._filterCheckboxContainer.createTextChild(WebInspector.UIString("Filter"));
    117 
    118     // Column 6
    119     var cancelButtonElement = this._firstRowElement.createChild("td").createChild("button");
    120     cancelButtonElement.textContent = WebInspector.UIString("Cancel");
    121     cancelButtonElement.tabIndex = -1;
    122     cancelButtonElement.addEventListener("click", this.closeSearch.bind(this), false);
    123 }
    124 
    125 WebInspector.SearchController.prototype = {
    126     /**
    127      * @param {number} matches
    128      * @param {WebInspector.Searchable} provider
    129      */
    130     updateSearchMatchesCount: function(matches, provider)
    131     {
    132         provider.currentSearchMatches = matches;
    133 
    134         if (provider === this._searchProvider)
    135             this._updateSearchMatchesCountAndCurrentMatchIndex(provider.currentQuery ? matches : 0, -1);
    136     },
    137 
    138     /**
    139      * @param {number} currentMatchIndex
    140      * @param {WebInspector.Searchable} provider
    141      */
    142     updateCurrentMatchIndex: function(currentMatchIndex, provider)
    143     {
    144         if (provider === this._searchProvider)
    145             this._updateSearchMatchesCountAndCurrentMatchIndex(provider.currentSearchMatches, currentMatchIndex);
    146     },
    147 
    148     isSearchVisible: function()
    149     {
    150         return this._searchIsVisible;
    151     },
    152 
    153     closeSearch: function()
    154     {
    155         this.cancelSearch();
    156         WebInspector.setCurrentFocusElement(WebInspector.previousFocusElement());
    157     },
    158 
    159     cancelSearch: function()
    160     {
    161         if (!this._searchIsVisible)
    162             return;
    163         if (this._filterCheckboxElement.checked) {
    164             this._filterCheckboxElement.checked = false;
    165             this._toggleFilterUI(false);
    166             this.resetFilter();
    167         } else
    168             this.resetSearch();
    169         delete this._searchIsVisible;
    170         this._searchHost.setFooterElement(null);
    171         this.resetSearch();
    172         delete this._searchHost;
    173         delete this._searchProvider;
    174     },
    175 
    176     resetSearch: function()
    177     {
    178         this._clearSearch();
    179         this._updateReplaceVisibility();
    180         this._matchesElement.textContent = "";
    181     },
    182 
    183     /**
    184      * @param {Event} event
    185      * @return {boolean}
    186      */
    187     handleShortcut: function(event)
    188     {
    189         var isMac = WebInspector.isMac();
    190 
    191         switch (event.keyIdentifier) {
    192             case "U+0046": // F key
    193                 if (isMac)
    194                     var isFindKey = event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey;
    195                 else
    196                     var isFindKey = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey;
    197 
    198                 if (isFindKey) {
    199                     this.showSearchField();
    200                     event.consume(true);
    201                     return true;
    202                 }
    203                 break;
    204 
    205             case "F3":
    206                 if (!isMac) {
    207                     this.showSearchField();
    208                     event.consume(true);
    209                     return true;
    210                 }
    211                 break;
    212 
    213             case "U+0047": // G key
    214                 if (isMac && event.metaKey && !event.ctrlKey && !event.altKey && this._searchHost) {
    215                     if (event.shiftKey)
    216                         this._searchProvider.jumpToPreviousSearchResult();
    217                     else
    218                         this._searchProvider.jumpToNextSearchResult();
    219                     event.consume(true);
    220                     return true;
    221                 }
    222                 break;
    223         }
    224         return false;
    225     },
    226 
    227     /**
    228      * @param {boolean} enabled
    229      */
    230     _updateSearchNavigationButtonState: function(enabled)
    231     {
    232         this._replaceButtonElement.disabled = !enabled;
    233         this._prevButtonElement.disabled = !enabled;
    234         if (enabled) {
    235             this._searchNavigationPrevElement.addStyleClass("enabled");
    236             this._searchNavigationNextElement.addStyleClass("enabled");
    237         } else {
    238             this._searchNavigationPrevElement.removeStyleClass("enabled");
    239             this._searchNavigationNextElement.removeStyleClass("enabled");
    240         }
    241     },
    242 
    243     /**
    244      * @param {number} matches
    245      * @param {number} currentMatchIndex
    246      */
    247     _updateSearchMatchesCountAndCurrentMatchIndex: function(matches, currentMatchIndex)
    248     {
    249         if (!this._currentQuery)
    250             this._matchesElement.textContent = "";
    251         else if (matches === 0 || currentMatchIndex >= 0)
    252             this._matchesElement.textContent = WebInspector.UIString("%d of %d", currentMatchIndex + 1, matches);
    253         else if (matches === 1)
    254             this._matchesElement.textContent = WebInspector.UIString("1 match");
    255         else
    256             this._matchesElement.textContent = WebInspector.UIString("%d matches", matches);
    257         this._updateSearchNavigationButtonState(matches > 0);
    258     },
    259 
    260     showSearchField: function()
    261     {
    262         if (this._searchIsVisible)
    263             this.cancelSearch();
    264 
    265         if (WebInspector.drawer.element.isAncestor(document.activeElement) && WebInspector.drawer.getSearchProvider())
    266             this._searchHost = WebInspector.drawer;
    267         else
    268             this._searchHost = WebInspector.inspectorView;
    269 
    270         this._searchProvider = this._searchHost.getSearchProvider();
    271         this._searchHost.setFooterElement(this._element);
    272 
    273         this._updateReplaceVisibility();
    274         this._updateFilterVisibility();
    275         if (WebInspector.currentFocusElement() !== this._searchInputElement) {
    276             var selection = window.getSelection();
    277             if (selection.rangeCount) {
    278                 var queryCandidate = selection.toString().replace(/\r?\n.*/, "");
    279                 if (queryCandidate)
    280                     this._searchInputElement.value = queryCandidate;
    281             }
    282         }
    283         this._performSearch(false, false);
    284         this._searchInputElement.focus();
    285         this._searchInputElement.select();
    286         this._searchIsVisible = true;
    287     },
    288 
    289     /**
    290      * @param {boolean} filter
    291      */
    292     _toggleFilterUI: function(filter)
    293     {
    294         this._matchesElement.enableStyleClass("hidden", filter);
    295         this._searchNavigationElement.enableStyleClass("hidden", filter);
    296         this._searchInputElement.placeholder = filter ? WebInspector.UIString("Filter") : WebInspector.UIString("Find");
    297     },
    298 
    299     _updateFilterVisibility: function()
    300     {
    301         if (this._searchProvider.canFilter())
    302             this._filterCheckboxContainer.removeStyleClass("hidden");
    303         else
    304             this._filterCheckboxContainer.addStyleClass("hidden");
    305     },
    306 
    307     _updateReplaceVisibility: function()
    308     {
    309         if (!this._searchProvider)
    310             return;
    311 
    312         if (this._searchProvider.canSearchAndReplace())
    313             this._replaceElement.removeStyleClass("hidden");
    314         else {
    315             this._replaceElement.addStyleClass("hidden");
    316             this._replaceCheckboxElement.checked = false;
    317             this._updateSecondRowVisibility();
    318         }
    319     },
    320 
    321     /**
    322      * @param {Event} event
    323      */
    324     _onSearchFieldManualFocus: function(event)
    325     {
    326         WebInspector.setCurrentFocusElement(event.target);
    327     },
    328 
    329     /**
    330      * @param {KeyboardEvent} event
    331      */
    332     _onKeyDown: function(event)
    333     {
    334         if (isEnterKey(event)) {
    335             if (event.target === this._searchInputElement) {
    336                 // FIXME: This won't start backwards search with Shift+Enter correctly.
    337                 if (!this._currentQuery)
    338                     this._performSearch(true, true);
    339                 else
    340                     this._jumpToNextSearchResult(event.shiftKey);
    341             } else if (event.target === this._replaceInputElement)
    342                 this._replace();
    343         }
    344     },
    345 
    346     /**
    347      * @param {boolean=} isBackwardSearch
    348      */
    349     _jumpToNextSearchResult: function(isBackwardSearch)
    350     {
    351         if (!this._currentQuery || !this._searchNavigationPrevElement.hasStyleClass("enabled"))
    352             return;
    353 
    354         if (isBackwardSearch)
    355             this._searchProvider.jumpToPreviousSearchResult();
    356         else
    357             this._searchProvider.jumpToNextSearchResult();
    358     },
    359 
    360     _onNextButtonSearch: function(event)
    361     {
    362         if (!this._searchNavigationNextElement.hasStyleClass("enabled"))
    363             return;
    364         // Simulate next search on search-navigation-button click.
    365         this._jumpToNextSearchResult();
    366         this._searchInputElement.focus();
    367     },
    368 
    369     _onPrevButtonSearch: function(event)
    370     {
    371         if (!this._searchNavigationPrevElement.hasStyleClass("enabled"))
    372             return;
    373         // Simulate previous search on search-navigation-button click.
    374         this._jumpToNextSearchResult(true);
    375         this._searchInputElement.focus();
    376     },
    377 
    378     _clearSearch: function()
    379     {
    380         delete this._currentQuery;
    381         if (this._searchHost){
    382             var searchProvider = this._searchHost.getSearchProvider();
    383             if (searchProvider && !!searchProvider.currentQuery) {
    384                 delete searchProvider.currentQuery;
    385                 searchProvider.searchCanceled();
    386             }
    387         }
    388         this._updateSearchMatchesCountAndCurrentMatchIndex(0, -1);
    389     },
    390 
    391     /**
    392      * @param {boolean} forceSearch
    393      * @param {boolean} shouldJump
    394      */
    395     _performSearch: function(forceSearch, shouldJump)
    396     {
    397         var query = this._searchInputElement.value;
    398         var minimalSearchQuerySize = this._searchProvider.minimalSearchQuerySize();
    399         if (!query || !this._searchProvider || (!forceSearch && query.length < minimalSearchQuerySize && !this._currentQuery)) {
    400             this._clearSearch();
    401             return;
    402         }
    403 
    404         this._currentQuery = query;
    405         this._searchProvider.currentQuery = query;
    406         this._searchProvider.performSearch(query, shouldJump);
    407     },
    408 
    409     _updateSecondRowVisibility: function()
    410     {
    411         if (!this._searchIsVisible || !this._searchHost)
    412             return;
    413         if (this._replaceCheckboxElement.checked) {
    414             this._element.addStyleClass("toolbar-search-replace");
    415             this._secondRowElement.removeStyleClass("hidden");
    416             this._prevButtonElement.removeStyleClass("hidden");
    417             this._findButtonElement.removeStyleClass("hidden");
    418             this._replaceCheckboxElement.tabIndex = -1;
    419             this._replaceInputElement.focus();
    420         } else {
    421             this._element.removeStyleClass("toolbar-search-replace");
    422             this._secondRowElement.addStyleClass("hidden");
    423             this._prevButtonElement.addStyleClass("hidden");
    424             this._findButtonElement.addStyleClass("hidden");
    425             this._replaceCheckboxElement.tabIndex = 0;
    426             this._searchInputElement.focus();
    427         }
    428         this._searchHost.setFooterElement(this._element);
    429     },
    430 
    431     _replace: function()
    432     {
    433         this._searchProvider.replaceSelectionWith(this._replaceInputElement.value);
    434         delete this._currentQuery;
    435         this._performSearch(true, true);
    436     },
    437 
    438     _replaceAll: function()
    439     {
    440         this._searchProvider.replaceAllWith(this._searchInputElement.value, this._replaceInputElement.value);
    441     },
    442 
    443     _filterCheckboxClick: function()
    444     {
    445         this._searchInputElement.focus();
    446         this._searchInputElement.select();
    447 
    448         if (this._filterCheckboxElement.checked) {
    449             this._toggleFilterUI(true);
    450             this.resetSearch();
    451             this._performFilter(this._searchInputElement.value);
    452         } else {
    453             this._toggleFilterUI(false);
    454             this.resetFilter();
    455             this._performSearch(false, false);
    456         }
    457     },
    458 
    459     /**
    460      * @param {string} query
    461      */
    462     _performFilter: function(query)
    463     {
    464         this._searchProvider.performFilter(query);
    465     },
    466 
    467     _onInput: function(event)
    468     {
    469         if (this._filterCheckboxElement.checked)
    470             this._performFilter(event.target.value);
    471         else
    472             this._performSearch(false, true);
    473     },
    474 
    475     resetFilter: function()
    476     {
    477         this._performFilter("");
    478     }
    479 }
    480 
    481 /**
    482  * @type {?WebInspector.SearchController}
    483  */
    484 WebInspector.searchController = null;
    485 
    486 /**
    487  * @interface
    488  */
    489 WebInspector.Searchable = function()
    490 {
    491 }
    492 
    493 WebInspector.Searchable.prototype = {
    494     /**
    495      * @return {boolean}
    496      */
    497     canSearchAndReplace: function() { },
    498 
    499     /**
    500      * @return {boolean}
    501      */
    502     canFilter: function() { },
    503 
    504     searchCanceled: function() { },
    505 
    506     /**
    507      * @param {string} query
    508      * @param {boolean} shouldJump
    509      * @param {WebInspector.Searchable=} self
    510      */
    511     performSearch: function(query, shouldJump, self) { },
    512 
    513     /**
    514      * @return {number}
    515      */
    516     minimalSearchQuerySize: function() { },
    517 
    518     /**
    519      * @param {WebInspector.Searchable=} self
    520      */
    521     jumpToNextSearchResult: function(self) { },
    522 
    523     /**
    524      * @param {WebInspector.Searchable=} self
    525      */
    526     jumpToPreviousSearchResult: function(self) { },
    527 }
    528