Web Inspector: Remove SidebarPanel show/hide and added/removed
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / SearchSidebarPanel.js
1 /*
2  * Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.SearchSidebarPanel = class SearchSidebarPanel extends WI.NavigationSidebarPanel
27 {
28     constructor(contentBrowser)
29     {
30         super("search", WI.UIString("Search"), true, true);
31
32         this.contentBrowser = contentBrowser;
33
34         var searchElement = document.createElement("div");
35         searchElement.classList.add("search-bar");
36         this.element.appendChild(searchElement);
37
38         this._inputElement = document.createElement("input");
39         this._inputElement.type = "search";
40         this._inputElement.spellcheck = false;
41         this._inputElement.addEventListener("search", this._searchFieldChanged.bind(this));
42         this._inputElement.addEventListener("input", this._searchFieldInput.bind(this));
43         this._inputElement.setAttribute("results", 5);
44         this._inputElement.setAttribute("autosave", "inspector-search-autosave");
45         this._inputElement.setAttribute("placeholder", WI.UIString("Search Resource Content"));
46         searchElement.appendChild(this._inputElement);
47
48         this._searchQuerySetting = new WI.Setting("search-sidebar-query", "");
49         this._inputElement.value = this._searchQuerySetting.value;
50
51         WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
52
53         this.contentTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._treeSelectionDidChange, this);
54     }
55
56     // Public
57
58     closed()
59     {
60         super.closed();
61
62         WI.Frame.removeEventListener(null, null, this);
63     }
64
65     focusSearchField(performSearch)
66     {
67         if (!this.parentSidebar)
68             return;
69
70         this.parentSidebar.selectedSidebarPanel = this;
71         this.parentSidebar.collapsed = false;
72
73         this._inputElement.select();
74
75         if (performSearch)
76             this.performSearch(this._inputElement.value);
77     }
78
79     performSearch(searchQuery)
80     {
81         // Before performing a new search, clear the old search.
82         this.contentTreeOutline.removeChildren();
83         this.contentBrowser.contentViewContainer.closeAllContentViews();
84
85         this._inputElement.value = searchQuery;
86         this._searchQuerySetting.value = searchQuery;
87
88         this.hideEmptyContentPlaceholder();
89
90         this.element.classList.remove("changed");
91         if (this._changedBanner)
92             this._changedBanner.remove();
93
94         searchQuery = searchQuery.trim();
95         if (!searchQuery.length)
96             return;
97
98         // FIXME: Provide UI to toggle regex and case sensitive searches.
99         var isCaseSensitive = false;
100         var isRegex = false;
101
102         var updateEmptyContentPlaceholderTimeout = null;
103
104         function createTreeElementForMatchObject(matchObject, parentTreeElement)
105         {
106             let matchTreeElement = new WI.SearchResultTreeElement(matchObject);
107             matchTreeElement.addEventListener(WI.TreeElement.Event.DoubleClick, this._treeElementDoubleClick, this);
108
109             parentTreeElement.appendChild(matchTreeElement);
110
111             if (!this.contentTreeOutline.selectedTreeElement)
112                 matchTreeElement.revealAndSelect(false, true);
113         }
114
115         function updateEmptyContentPlaceholderSoon()
116         {
117             if (updateEmptyContentPlaceholderTimeout)
118                 return;
119             updateEmptyContentPlaceholderTimeout = setTimeout(updateEmptyContentPlaceholder.bind(this), 100);
120         }
121
122         function updateEmptyContentPlaceholder()
123         {
124             if (updateEmptyContentPlaceholderTimeout) {
125                 clearTimeout(updateEmptyContentPlaceholderTimeout);
126                 updateEmptyContentPlaceholderTimeout = null;
127             }
128
129             this.updateEmptyContentPlaceholder(WI.UIString("No Search Results"));
130         }
131
132         function forEachMatch(searchQuery, lineContent, callback)
133         {
134             var lineMatch;
135             var searchRegex = new RegExp(searchQuery.escapeForRegExp(), "gi");
136             while ((searchRegex.lastIndex < lineContent.length) && (lineMatch = searchRegex.exec(lineContent)))
137                 callback(lineMatch, searchRegex.lastIndex);
138         }
139
140         function resourcesCallback(error, result)
141         {
142             updateEmptyContentPlaceholderSoon.call(this);
143
144             if (error)
145                 return;
146
147             function resourceCallback(frameId, url, error, resourceMatches)
148             {
149                 updateEmptyContentPlaceholderSoon.call(this);
150
151                 if (error || !resourceMatches || !resourceMatches.length)
152                     return;
153
154                 var frame = WI.frameResourceManager.frameForIdentifier(frameId);
155                 if (!frame)
156                     return;
157
158                 var resource = frame.url === url ? frame.mainResource : frame.resourceForURL(url);
159                 if (!resource)
160                     return;
161
162                 var resourceTreeElement = this._searchTreeElementForResource(resource);
163
164                 for (var i = 0; i < resourceMatches.length; ++i) {
165                     var match = resourceMatches[i];
166                     forEachMatch(searchQuery, match.lineContent, (lineMatch, lastIndex) => {
167                         var matchObject = new WI.SourceCodeSearchMatchObject(resource, match.lineContent, searchQuery, new WI.TextRange(match.lineNumber, lineMatch.index, match.lineNumber, lastIndex));
168                         createTreeElementForMatchObject.call(this, matchObject, resourceTreeElement);
169                     });
170                 }
171
172                 updateEmptyContentPlaceholder.call(this);
173             }
174
175             for (var i = 0; i < result.length; ++i) {
176                 var searchResult = result[i];
177                 if (!searchResult.url || !searchResult.frameId)
178                     continue;
179
180                 // COMPATIBILITY (iOS 9): Page.searchInResources did not have the optional requestId parameter.
181                 PageAgent.searchInResource(searchResult.frameId, searchResult.url, searchQuery, isCaseSensitive, isRegex, searchResult.requestId, resourceCallback.bind(this, searchResult.frameId, searchResult.url));
182             }
183
184             let promises = [
185                 WI.Frame.awaitEvent(WI.Frame.Event.ResourceWasAdded),
186                 WI.Target.awaitEvent(WI.Target.Event.ResourceAdded)
187             ];
188             Promise.race(promises).then(this._contentChanged.bind(this));
189         }
190
191         function searchScripts(scriptsToSearch)
192         {
193             updateEmptyContentPlaceholderSoon.call(this);
194
195             if (!scriptsToSearch.length)
196                 return;
197
198             function scriptCallback(script, error, scriptMatches)
199             {
200                 updateEmptyContentPlaceholderSoon.call(this);
201
202                 if (error || !scriptMatches || !scriptMatches.length)
203                     return;
204
205                 var scriptTreeElement = this._searchTreeElementForScript(script);
206
207                 for (var i = 0; i < scriptMatches.length; ++i) {
208                     var match = scriptMatches[i];
209                     forEachMatch(searchQuery, match.lineContent, (lineMatch, lastIndex) => {
210                         var matchObject = new WI.SourceCodeSearchMatchObject(script, match.lineContent, searchQuery, new WI.TextRange(match.lineNumber, lineMatch.index, match.lineNumber, lastIndex));
211                         createTreeElementForMatchObject.call(this, matchObject, scriptTreeElement);
212                     });
213                 }
214
215                 updateEmptyContentPlaceholder.call(this);
216             }
217
218             for (let script of scriptsToSearch)
219                 script.target.DebuggerAgent.searchInContent(script.id, searchQuery, isCaseSensitive, isRegex, scriptCallback.bind(this, script));
220         }
221
222         function domCallback(error, searchId, resultsCount)
223         {
224             updateEmptyContentPlaceholderSoon.call(this);
225
226             if (error || !resultsCount)
227                 return;
228
229             console.assert(searchId);
230
231             this._domSearchIdentifier = searchId;
232
233             function domSearchResults(error, nodeIds)
234             {
235                 updateEmptyContentPlaceholderSoon.call(this);
236
237                 if (error)
238                     return;
239
240                 for (var i = 0; i < nodeIds.length; ++i) {
241                     // If someone started a new search, then return early and stop showing seach results from the old query.
242                     if (this._domSearchIdentifier !== searchId)
243                         return;
244
245                     var domNode = WI.domTreeManager.nodeForId(nodeIds[i]);
246                     if (!domNode || !domNode.ownerDocument)
247                         continue;
248
249                     // We do not display the document node when the search query is "/". We don't have anything to display in the content view for it.
250                     if (domNode.nodeType() === Node.DOCUMENT_NODE)
251                         continue;
252
253                     // FIXME: This should use a frame to do resourceForURL, but DOMAgent does not provide a frameId.
254                     var resource = WI.frameResourceManager.resourceForURL(domNode.ownerDocument.documentURL);
255                     if (!resource)
256                         continue;
257
258                     var resourceTreeElement = this._searchTreeElementForResource(resource);
259                     var domNodeTitle = WI.DOMSearchMatchObject.titleForDOMNode(domNode);
260
261                     // Textual matches.
262                     var didFindTextualMatch = false;
263                     forEachMatch(searchQuery, domNodeTitle, (lineMatch, lastIndex) => {
264                         var matchObject = new WI.DOMSearchMatchObject(resource, domNode, domNodeTitle, searchQuery, new WI.TextRange(0, lineMatch.index, 0, lastIndex));
265                         createTreeElementForMatchObject.call(this, matchObject, resourceTreeElement);
266                         didFindTextualMatch = true;
267                     });
268
269                     // Non-textual matches are CSS Selector or XPath matches. In such cases, display the node entirely highlighted.
270                     if (!didFindTextualMatch) {
271                         var matchObject = new WI.DOMSearchMatchObject(resource, domNode, domNodeTitle, domNodeTitle, new WI.TextRange(0, 0, 0, domNodeTitle.length));
272                         createTreeElementForMatchObject.call(this, matchObject, resourceTreeElement);
273                     }
274
275                     updateEmptyContentPlaceholder.call(this);
276                 }
277             }
278
279             DOMAgent.getSearchResults(searchId, 0, resultsCount, domSearchResults.bind(this));
280         }
281
282         if (window.DOMAgent)
283             WI.domTreeManager.ensureDocument();
284
285         if (window.PageAgent)
286             PageAgent.searchInResources(searchQuery, isCaseSensitive, isRegex, resourcesCallback.bind(this));
287
288         setTimeout(searchScripts.bind(this, WI.debuggerManager.searchableScripts), 0);
289
290         if (window.DOMAgent) {
291             if (this._domSearchIdentifier) {
292                 DOMAgent.discardSearchResults(this._domSearchIdentifier);
293                 this._domSearchIdentifier = undefined;
294             }
295
296             DOMAgent.performSearch(searchQuery, domCallback.bind(this));
297         }
298
299         // FIXME: Resource search should work in JSContext inspection.
300         // <https://webkit.org/b/131252> Web Inspector: JSContext inspection Resource search does not work
301         if (!window.DOMAgent && !window.PageAgent)
302             updateEmptyContentPlaceholderSoon.call(this);
303     }
304
305     // Private
306
307     _searchFieldChanged(event)
308     {
309         this.performSearch(event.target.value);
310     }
311
312     _searchFieldInput(event)
313     {
314         // If the search field is cleared, immediately clear the search results tree outline.
315         if (!event.target.value.length)
316             this.performSearch("");
317     }
318
319     _searchTreeElementForResource(resource)
320     {
321         var resourceTreeElement = this.contentTreeOutline.getCachedTreeElement(resource);
322         if (!resourceTreeElement) {
323             resourceTreeElement = new WI.ResourceTreeElement(resource);
324             resourceTreeElement.hasChildren = true;
325             resourceTreeElement.expand();
326
327             this.contentTreeOutline.appendChild(resourceTreeElement);
328         }
329
330         return resourceTreeElement;
331     }
332
333     _searchTreeElementForScript(script)
334     {
335         var scriptTreeElement = this.contentTreeOutline.getCachedTreeElement(script);
336         if (!scriptTreeElement) {
337             scriptTreeElement = new WI.ScriptTreeElement(script);
338             scriptTreeElement.hasChildren = true;
339             scriptTreeElement.expand();
340
341             this.contentTreeOutline.appendChild(scriptTreeElement);
342         }
343
344         return scriptTreeElement;
345     }
346
347     _mainResourceDidChange(event)
348     {
349         if (!event.target.isMainFrame())
350             return;
351
352         if (this._delayedSearchTimeout) {
353             clearTimeout(this._delayedSearchTimeout);
354             this._delayedSearchTimeout = undefined;
355         }
356
357         this.contentTreeOutline.removeChildren();
358         this.contentBrowser.contentViewContainer.closeAllContentViews();
359
360         if (this.visible) {
361             const performSearch = true;
362             this.focusSearchField(performSearch);
363         }
364     }
365
366     _treeSelectionDidChange(event)
367     {
368         if (!this.visible)
369             return;
370
371         let treeElement = event.data.selectedElement;
372         if (!treeElement || treeElement instanceof WI.FolderTreeElement)
373             return;
374
375         const options = {
376             ignoreNetworkTab: true,
377         };
378
379         if (treeElement instanceof WI.ResourceTreeElement || treeElement instanceof WI.ScriptTreeElement) {
380             const cookie = null;
381             WI.showRepresentedObject(treeElement.representedObject, cookie, options);
382             return;
383         }
384
385         console.assert(treeElement instanceof WI.SearchResultTreeElement);
386         if (!(treeElement instanceof WI.SearchResultTreeElement))
387             return;
388
389         if (treeElement.representedObject instanceof WI.DOMSearchMatchObject)
390             WI.showMainFrameDOMTree(treeElement.representedObject.domNode);
391         else if (treeElement.representedObject instanceof WI.SourceCodeSearchMatchObject)
392             WI.showOriginalOrFormattedSourceCodeTextRange(treeElement.representedObject.sourceCodeTextRange, options);
393     }
394
395     _treeElementDoubleClick(event)
396     {
397         let treeElement = event.target;
398         if (!treeElement)
399             return;
400
401         if (treeElement.representedObject instanceof WI.DOMSearchMatchObject) {
402             WI.showMainFrameDOMTree(treeElement.representedObject.domNode, {
403                 ignoreSearchTab: true,
404             });
405         } else if (treeElement.representedObject instanceof WI.SourceCodeSearchMatchObject) {
406             WI.showOriginalOrFormattedSourceCodeTextRange(treeElement.representedObject.sourceCodeTextRange, {
407                 ignoreNetworkTab: true,
408                 ignoreSearchTab: true,
409             });
410         }
411     }
412
413     _contentChanged(event)
414     {
415         this.element.classList.add("changed");
416
417         if (!this._changedBanner) {
418             this._changedBanner = document.createElement("div");
419             this._changedBanner.classList.add("banner");
420             this._changedBanner.append(WI.UIString("The page's content has changed"), document.createElement("br"));
421
422             let performSearchLink = this._changedBanner.appendChild(document.createElement("a"));
423             performSearchLink.textContent = WI.UIString("Search Again");
424             performSearchLink.addEventListener("click", () => {
425                 const performSearch = true;
426                 this.focusSearchField(performSearch);
427             });
428         }
429
430         this.element.appendChild(this._changedBanner);
431     }
432 };