• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2006, 2007, 2008 Apple Inc.  All rights reserved.
3 * Copyright (C) 2007 Matt Lilek (pewtermoose@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 */
37WebInspector.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
129WebInspector.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
139WebInspector.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
147WebInspector.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
157WebInspector.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
167WebInspector.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 */
510WebInspector.Searchable = function()
511{
512}
513
514WebInspector.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