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.VBox}
35 * @param {!WebInspector.Searchable} searchable
36 */
37WebInspector.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
127WebInspector.SearchableView._lastUniqueId = 0;
128
129/**
130 * @return {!Array.<!WebInspector.KeyboardShortcut.Descriptor>}
131 */
132WebInspector.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 */
145WebInspector.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 */
156WebInspector.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 */
169WebInspector.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
179WebInspector.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 */
553WebInspector.Searchable = function()
554{
555}
556
557WebInspector.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 */
575WebInspector.Replaceable = function()
576{
577}
578
579WebInspector.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