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