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
32WebInspector.SearchController = function()
33{
34    this.element = document.getElementById("search");
35    this._matchesElement = document.getElementById("search-results-matches");
36    this._toolbarLabelElement = document.getElementById("search-toolbar-label");
37
38    this.element.addEventListener("search", this._onSearch.bind(this), false); // when the search is emptied
39    this.element.addEventListener("mousedown", this._onSearchFieldManualFocus.bind(this), false); // when the search field is manually selected
40    this.element.addEventListener("keydown", this._onKeyDown.bind(this), true);
41}
42
43WebInspector.SearchController.prototype = {
44    updateSearchMatchesCount: function(matches, panel)
45    {
46        if (!panel)
47            panel = WebInspector.currentPanel;
48
49        panel.currentSearchMatches = matches;
50
51        if (panel === WebInspector.currentPanel)
52            this._updateSearchMatchesCount(WebInspector.currentPanel.currentQuery && matches);
53    },
54
55    updateSearchLabel: function()
56    {
57        var panelName = WebInspector.currentPanel && WebInspector.currentPanel.toolbarItemLabel;
58        if (!panelName)
59            return;
60        var newLabel = WebInspector.UIString("Search %s", panelName);
61        if (WebInspector.attached)
62            this.element.setAttribute("placeholder", newLabel);
63        else {
64            this.element.removeAttribute("placeholder");
65            this._toolbarLabelElement.textContent = newLabel;
66        }
67    },
68
69    cancelSearch: function()
70    {
71        this.element.value = "";
72        this._performSearch("");
73    },
74
75    handleShortcut: function(event)
76    {
77        var isMac = WebInspector.isMac();
78
79        switch (event.keyIdentifier) {
80            case "U+0046": // F key
81                if (isMac)
82                    var isFindKey = event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey;
83                else
84                    var isFindKey = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey;
85
86                if (isFindKey) {
87                    this.focusSearchField();
88                    event.handled = true;
89                }
90                break;
91
92
93            case "F3":
94                if (!isMac) {
95                    this.focusSearchField();
96                    event.handled = true;
97                }
98                break;
99
100            case "U+0047": // G key
101                var currentPanel = WebInspector.currentPanel;
102
103                if (isMac && event.metaKey && !event.ctrlKey && !event.altKey) {
104                    if (event.shiftKey) {
105                        if (currentPanel.jumpToPreviousSearchResult)
106                            currentPanel.jumpToPreviousSearchResult();
107                    } else if (currentPanel.jumpToNextSearchResult)
108                        currentPanel.jumpToNextSearchResult();
109                    event.handled = true;
110                }
111                break;
112        }
113    },
114
115    activePanelChanged: function()
116    {
117        this.updateSearchLabel();
118
119        if (!this._currentQuery)
120            return;
121
122        panel = WebInspector.currentPanel;
123        if (panel.performSearch) {
124            function performPanelSearch()
125            {
126                this._updateSearchMatchesCount();
127
128                panel.currentQuery = this._currentQuery;
129                panel.performSearch(this._currentQuery);
130            }
131
132            // Perform the search on a timeout so the panel switches fast.
133            setTimeout(performPanelSearch.bind(this), 0);
134        } else {
135            // Update to show Not found for panels that can't be searched.
136            this._updateSearchMatchesCount();
137        }
138    },
139
140    _updateSearchMatchesCount: function(matches)
141    {
142        if (matches == null) {
143            this._matchesElement.addStyleClass("hidden");
144            return;
145        }
146
147        if (matches) {
148            if (matches === 1)
149                var matchesString = WebInspector.UIString("1 match");
150            else
151                var matchesString = WebInspector.UIString("%d matches", matches);
152        } else
153            var matchesString = WebInspector.UIString("Not Found");
154
155        this._matchesElement.removeStyleClass("hidden");
156        this._matchesElement.textContent = matchesString;
157        WebInspector.toolbar.resize();
158    },
159
160    focusSearchField: function()
161    {
162        this.element.focus();
163        this.element.select();
164    },
165
166    _onSearchFieldManualFocus: function(event)
167    {
168        WebInspector.currentFocusElement = event.target;
169    },
170
171    _onKeyDown: function(event)
172    {
173        // Escape Key will clear the field and clear the search results
174        if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code) {
175            // If focus belongs here and text is empty - nothing to do, return unhandled.
176            // When search was selected manually and is currently blank, we'd like Esc stay unhandled
177            // and hit console drawer handler.
178            if (event.target.value === "" && this.currentFocusElement === this.previousFocusElement)
179                return;
180            event.preventDefault();
181            event.stopPropagation();
182
183            this.cancelSearch(event);
184            WebInspector.currentFocusElement = WebInspector.previousFocusElement;
185            if (WebInspector.currentFocusElement === event.target)
186                WebInspector.currentFocusElement.currentFocusElement.select();
187            return false;
188        }
189
190        if (!isEnterKey(event))
191            return false;
192
193        // Select all of the text so the user can easily type an entirely new query.
194        event.target.select();
195
196        // Only call performSearch if the Enter key was pressed. Otherwise the search
197        // performance is poor because of searching on every key. The search field has
198        // the incremental attribute set, so we still get incremental searches.
199        this._onSearch(event);
200
201        // Call preventDefault since this was the Enter key. This prevents a "search" event
202        // from firing for key down. This stops performSearch from being called twice in a row.
203        event.preventDefault();
204    },
205
206    _onSearch: function(event)
207    {
208        var forceSearch = event.keyIdentifier === "Enter";
209        this._performSearch(event.target.value, forceSearch, event.shiftKey, false);
210    },
211
212    _performSearch: function(query, forceSearch, isBackwardSearch, repeatSearch)
213    {
214        var isShortSearch = (query.length < 3);
215
216        // Clear a leftover short search flag due to a non-conflicting forced search.
217        if (isShortSearch && this._shortSearchWasForcedByKeyEvent && this._currentQuery !== query)
218            delete this._shortSearchWasForcedByKeyEvent;
219
220        // Indicate this was a forced search on a short query.
221        if (isShortSearch && forceSearch)
222            this._shortSearchWasForcedByKeyEvent = true;
223
224        if (!query || !query.length || (!forceSearch && isShortSearch)) {
225            // Prevent clobbering a short search forced by the user.
226            if (this._shortSearchWasForcedByKeyEvent) {
227                delete this._shortSearchWasForcedByKeyEvent;
228                return;
229            }
230
231            delete this._currentQuery;
232
233            for (var panelName in WebInspector.panels) {
234                var panel = WebInspector.panels[panelName];
235                var hadCurrentQuery = !!panel.currentQuery;
236                delete panel.currentQuery;
237                if (hadCurrentQuery && panel.searchCanceled)
238                    panel.searchCanceled();
239            }
240
241            this._updateSearchMatchesCount();
242
243            return;
244        }
245
246        var currentPanel = WebInspector.currentPanel;
247        if (!repeatSearch && query === currentPanel.currentQuery && currentPanel.currentQuery === this._currentQuery) {
248            // When this is the same query and a forced search, jump to the next
249            // search result for a good user experience.
250            if (forceSearch) {
251                if (!isBackwardSearch && currentPanel.jumpToNextSearchResult)
252                    currentPanel.jumpToNextSearchResult();
253                else if (isBackwardSearch && currentPanel.jumpToPreviousSearchResult)
254                    currentPanel.jumpToPreviousSearchResult();
255            }
256            return;
257        }
258
259        this._currentQuery = query;
260
261        this._updateSearchMatchesCount();
262
263        if (!currentPanel.performSearch)
264            return;
265
266        currentPanel.currentQuery = query;
267        currentPanel.performSearch(query);
268    }
269}
270