| /* |
| * Copyright (C) 2013, 2015 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| WI.SearchSidebarPanel = class SearchSidebarPanel extends WI.NavigationSidebarPanel |
| { |
| constructor() |
| { |
| super("search", WI.UIString("Search"), true, true); |
| |
| this._searchInputSettings = WI.SearchUtilities.createSettings("search-sidebar"); |
| for (let setting of Object.values(this._searchInputSettings)) { |
| setting.addEventListener(WI.Setting.Event.Changed, function(event) { |
| this.focusSearchField(true); |
| }, this); |
| } |
| |
| this._inputContainer = this.element.appendChild(document.createElement("div")); |
| this._inputContainer.classList.add("search-bar"); |
| |
| this._inputElement = this._inputContainer.appendChild(document.createElement("input")); |
| this._inputElement.type = "search"; |
| this._inputElement.spellcheck = false; |
| this._inputElement.addEventListener("search", this._searchFieldChanged.bind(this)); |
| this._inputElement.addEventListener("input", this._searchFieldInput.bind(this)); |
| this._inputElement.setAttribute("results", 5); |
| this._inputElement.setAttribute("autosave", "inspector-search-autosave"); |
| this._inputElement.setAttribute("placeholder", WI.UIString("Search Resource Content")); |
| |
| this._inputContainer.appendChild(WI.SearchUtilities.createSettingsButton(this._searchInputSettings)); |
| |
| this._searchQuerySetting = new WI.Setting("search-sidebar-query", ""); |
| this._inputElement.value = this._searchQuerySetting.value; |
| |
| WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this); |
| |
| this.contentTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._treeSelectionDidChange, this); |
| } |
| |
| // Public |
| |
| showDefaultContentView() |
| { |
| let contentView = new WI.ContentView; |
| |
| let contentPlaceholder = WI.createMessageTextView(this._searchQuerySetting.value ? WI.UIString("No search results") : WI.UIString("No search string")); |
| contentView.element.appendChild(contentPlaceholder); |
| |
| let searchNavigationItem = new WI.ButtonNavigationItem("search", WI.UIString("Search Resource Content"), "Images/Search.svg", 15, 15); |
| searchNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleDefaultContentViewSearchNavigationItemClicked, this); |
| |
| let importHelpElement = WI.createNavigationItemHelp(WI.UIString("Press %s to see recent searches."), searchNavigationItem); |
| contentPlaceholder.appendChild(importHelpElement); |
| |
| this.contentBrowser.showContentView(contentView); |
| } |
| |
| closed() |
| { |
| super.closed(); |
| |
| WI.Frame.removeEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this); |
| } |
| |
| focusSearchField(performSearch) |
| { |
| if (!this.parentSidebar) |
| return; |
| |
| this.parentSidebar.selectedSidebarPanel = this; |
| this.parentSidebar.collapsed = false; |
| |
| this._inputElement.select(); |
| |
| if (performSearch) |
| this.performSearch(this._inputElement.value, {omitFocus: true}); |
| } |
| |
| performSearch(searchQuery, {omitFocus} = {}) |
| { |
| this._inputElement.value = searchQuery; |
| this._searchQuerySetting.value = searchQuery; |
| |
| this.element.classList.remove("changed"); |
| if (this._changedBanner) |
| this._changedBanner.remove(); |
| |
| if (!searchQuery.length) { |
| this._inputContainer.classList.remove("invalid"); |
| this.hideEmptyContentPlaceholder(); |
| this.showDefaultContentView(); |
| return; |
| } |
| |
| let isCaseSensitive = !!this._searchInputSettings.caseSensitive.value; |
| let isRegex = !!this._searchInputSettings.regularExpression.value; |
| let searchRegex = WI.SearchUtilities.searchRegExpForString(searchQuery, { |
| caseSensitive: isCaseSensitive, |
| regularExpression: isRegex, |
| }); |
| this._inputContainer.classList.toggle("invalid", !searchRegex); |
| if (!searchRegex) |
| return; |
| |
| this.hideEmptyContentPlaceholder(); |
| |
| // Before performing a new search, clear the old search. |
| this.contentTreeOutline.removeChildren(); |
| this.contentBrowser.contentViewContainer.closeAllContentViews(); |
| |
| let createSearchingPlaceholder = () => { |
| let searchingPlaceholder = WI.createMessageTextView(""); |
| String.format(WI.UIString("Searching %s"), [(new WI.IndeterminateProgressSpinner).element], String.standardFormatters, searchingPlaceholder, (a, b) => { |
| a.append(b); |
| return a; |
| }); |
| this.updateEmptyContentPlaceholder(searchingPlaceholder); |
| }; |
| |
| if (!WI.targetsAvailable() && WI.sharedApp.isWebDebuggable()) { |
| createSearchingPlaceholder(); |
| WI.whenTargetsAvailable().then(() => { |
| if (this._searchQuerySetting.value === searchQuery) |
| this.performSearch(searchQuery, {omitFocus}); |
| }); |
| return; |
| } |
| |
| let target = WI.assumingMainTarget(); |
| |
| let promiseCount = 0; |
| let countPromise = async (promise, callback) => { |
| ++promiseCount; |
| if (promiseCount === 1) |
| createSearchingPlaceholder(); |
| |
| let value = await promise; |
| |
| if (callback) |
| callback(value); |
| |
| --promiseCount; |
| console.assert(promiseCount >= 0); |
| if (promiseCount === 0) { |
| this.updateEmptyContentPlaceholder(WI.UIString("No Search Results")); |
| |
| if (!this.contentTreeOutline.children.length) |
| this.showDefaultContentView(); |
| } |
| }; |
| |
| function createTreeElementForMatchObject(matchObject, parentTreeElement) |
| { |
| let matchTreeElement = new WI.SearchResultTreeElement(matchObject); |
| matchTreeElement.addEventListener(WI.TreeElement.Event.DoubleClick, this._treeElementDoubleClick, this); |
| |
| parentTreeElement.appendChild(matchTreeElement); |
| |
| if (!this.contentTreeOutline.selectedTreeElement) { |
| const selectedByUser = true; |
| matchTreeElement.revealAndSelect(omitFocus ?? false, selectedByUser); |
| } |
| } |
| |
| function forEachMatch(lineContent, callback) |
| { |
| var lineMatch; |
| while ((searchRegex.lastIndex < lineContent.length) && (lineMatch = searchRegex.exec(lineContent))) |
| callback(lineMatch, searchRegex.lastIndex); |
| } |
| |
| let resourceCallback = (frameId, url, {result}) => { |
| if (!result || !result.length) |
| return; |
| |
| var frame = WI.networkManager.frameForIdentifier(frameId); |
| if (!frame) |
| return; |
| |
| let resource = frame.url === url ? frame.mainResource : frame.resourcesForURL(url).firstValue; |
| if (!resource) |
| return; |
| |
| var resourceTreeElement = this._searchTreeElementForResource(resource); |
| |
| for (var i = 0; i < result.length; ++i) { |
| var match = result[i]; |
| forEachMatch(match.lineContent, (lineMatch, lastIndex) => { |
| var matchObject = new WI.SourceCodeSearchMatchObject(resource, match.lineContent, searchQuery, new WI.TextRange(match.lineNumber, lineMatch.index, match.lineNumber, lastIndex)); |
| createTreeElementForMatchObject.call(this, matchObject, resourceTreeElement); |
| }); |
| } |
| |
| if (!resourceTreeElement.children.length) |
| this.contentTreeOutline.removeChild(resourceTreeElement); |
| }; |
| |
| let resourcesCallback = ({result}) => { |
| let preventDuplicates = new Set; |
| |
| for (let searchResult of result) { |
| if (!searchResult.url || !searchResult.frameId) |
| continue; |
| |
| // FIXME: Backend sometimes searches files twice. |
| // <https://webkit.org/b/188287> Web Inspector: [Backend] Page.searchInResources sometimes returns duplicate results for a resource |
| // Note we will still want this to fix legacy backends. |
| let key = searchResult.frameId + ":" + searchResult.url; |
| if (preventDuplicates.has(key)) |
| continue; |
| preventDuplicates.add(key); |
| |
| countPromise(target.PageAgent.searchInResource(searchResult.frameId, searchResult.url, searchQuery, isCaseSensitive, isRegex, searchResult.requestId), resourceCallback.bind(this, searchResult.frameId, searchResult.url)); |
| } |
| |
| let promises = [ |
| WI.Frame.awaitEvent(WI.Frame.Event.ResourceWasAdded, this), |
| WI.Target.awaitEvent(WI.Target.Event.ResourceAdded, this), |
| ]; |
| Promise.race(promises).then(this._contentChanged.bind(this)); |
| }; |
| |
| let scriptCallback = (script, {result}) => { |
| if (!result || !result.length) |
| return; |
| |
| var scriptTreeElement = this._searchTreeElementForScript(script); |
| |
| for (let match of result) { |
| forEachMatch(match.lineContent, (lineMatch, lastIndex) => { |
| var matchObject = new WI.SourceCodeSearchMatchObject(script, match.lineContent, searchQuery, new WI.TextRange(match.lineNumber, lineMatch.index, match.lineNumber, lastIndex)); |
| createTreeElementForMatchObject.call(this, matchObject, scriptTreeElement); |
| }); |
| } |
| |
| if (!scriptTreeElement.children.length) |
| this.contentTreeOutline.removeChild(scriptTreeElement); |
| }; |
| |
| let searchScripts = (scriptsToSearch) => { |
| if (!scriptsToSearch.length) |
| return; |
| |
| for (let script of scriptsToSearch) |
| countPromise(script.target.DebuggerAgent.searchInContent(script.id, searchQuery, isCaseSensitive, isRegex), scriptCallback.bind(this, script)); |
| }; |
| |
| let domCallback = ({searchId, resultCount}) => { |
| if (!resultCount) |
| return; |
| |
| console.assert(searchId); |
| |
| this._domSearchIdentifier = searchId; |
| |
| let domSearchResults = ({nodeIds}) => { |
| // If someone started a new search, then return early and stop showing search results from the old query. |
| if (this._domSearchIdentifier !== searchId) |
| return; |
| |
| for (let nodeId of nodeIds) { |
| let domNode = WI.domManager.nodeForId(nodeId); |
| if (!domNode || !domNode.ownerDocument) |
| continue; |
| |
| // 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. |
| if (domNode.nodeType() === Node.DOCUMENT_NODE) |
| continue; |
| |
| // FIXME: This should use a frame to do resourceForURL, but DOMAgent does not provide a frameId. |
| let resource = WI.networkManager.resourcesForURL(domNode.ownerDocument.documentURL).firstValue; |
| if (!resource) |
| continue; |
| |
| var resourceTreeElement = this._searchTreeElementForResource(resource); |
| var domNodeTitle = WI.DOMSearchMatchObject.titleForDOMNode(domNode); |
| |
| // Textual matches. |
| var didFindTextualMatch = false; |
| forEachMatch(domNodeTitle, (lineMatch, lastIndex) => { |
| var matchObject = new WI.DOMSearchMatchObject(resource, domNode, domNodeTitle, searchQuery, new WI.TextRange(0, lineMatch.index, 0, lastIndex)); |
| createTreeElementForMatchObject.call(this, matchObject, resourceTreeElement); |
| didFindTextualMatch = true; |
| }); |
| |
| // Non-textual matches are CSS Selector or XPath matches. In such cases, display the node entirely highlighted. |
| if (!didFindTextualMatch) { |
| var matchObject = new WI.DOMSearchMatchObject(resource, domNode, domNodeTitle, domNodeTitle, new WI.TextRange(0, 0, 0, domNodeTitle.length)); |
| createTreeElementForMatchObject.call(this, matchObject, resourceTreeElement); |
| } |
| |
| if (!resourceTreeElement.children.length) |
| this.contentTreeOutline.removeChild(resourceTreeElement); |
| |
| } |
| }; |
| |
| countPromise(target.DOMAgent.getSearchResults(searchId, 0, resultCount), domSearchResults); |
| }; |
| |
| WI.domManager.ensureDocument(); |
| |
| if (target.hasCommand("Page.searchInResources")) |
| countPromise(target.PageAgent.searchInResources(searchQuery, isCaseSensitive, isRegex), resourcesCallback); |
| |
| setTimeout(searchScripts.bind(this, WI.debuggerManager.searchableScripts), 0); |
| |
| if (target.hasDomain("DOM")) { |
| if (this._domSearchIdentifier) { |
| target.DOMAgent.discardSearchResults(this._domSearchIdentifier); |
| this._domSearchIdentifier = undefined; |
| } |
| |
| let commandArguments = { |
| query: searchQuery, |
| caseSensitive: isCaseSensitive, |
| }; |
| countPromise(target.DOMAgent.performSearch.invoke(commandArguments), domCallback); |
| } |
| |
| // FIXME: Resource search should work with Local Overrides if enabled. |
| |
| // FIXME: Resource search should work in JSContext inspection. |
| // <https://webkit.org/b/131252> Web Inspector: JSContext inspection Resource search does not work |
| } |
| |
| // Private |
| |
| _searchFieldChanged(event) |
| { |
| this.performSearch(event.target.value); |
| } |
| |
| _searchFieldInput(event) |
| { |
| // If the search field is cleared, immediately clear the search results tree outline. |
| if (!event.target.value.length) |
| this.performSearch(""); |
| } |
| |
| _searchTreeElementForResource(resource) |
| { |
| var resourceTreeElement = this.contentTreeOutline.getCachedTreeElement(resource); |
| if (!resourceTreeElement) { |
| resourceTreeElement = new WI.ResourceTreeElement(resource); |
| resourceTreeElement.hasChildren = true; |
| resourceTreeElement.expand(); |
| |
| this.contentTreeOutline.appendChild(resourceTreeElement); |
| } |
| |
| return resourceTreeElement; |
| } |
| |
| _searchTreeElementForScript(script) |
| { |
| var scriptTreeElement = this.contentTreeOutline.getCachedTreeElement(script); |
| if (!scriptTreeElement) { |
| scriptTreeElement = new WI.ScriptTreeElement(script); |
| scriptTreeElement.hasChildren = true; |
| scriptTreeElement.expand(); |
| |
| this.contentTreeOutline.appendChild(scriptTreeElement); |
| } |
| |
| return scriptTreeElement; |
| } |
| |
| _mainResourceDidChange(event) |
| { |
| if (!event.target.isMainFrame()) |
| return; |
| |
| if (this._delayedSearchTimeout) { |
| clearTimeout(this._delayedSearchTimeout); |
| this._delayedSearchTimeout = undefined; |
| } |
| |
| this.contentTreeOutline.removeChildren(); |
| this.contentBrowser.contentViewContainer.closeAllContentViews(); |
| |
| if (this.visible) { |
| const performSearch = true; |
| this.focusSearchField(performSearch); |
| } |
| } |
| |
| _treeSelectionDidChange(event) |
| { |
| if (!this.selected) |
| return; |
| |
| let treeElement = this.contentTreeOutline.selectedTreeElement; |
| if (!treeElement || treeElement instanceof WI.FolderTreeElement) |
| return; |
| |
| const options = { |
| ignoreNetworkTab: true, |
| }; |
| |
| if (treeElement instanceof WI.ResourceTreeElement || treeElement instanceof WI.ScriptTreeElement) { |
| const cookie = null; |
| WI.showRepresentedObject(treeElement.representedObject, cookie, options); |
| return; |
| } |
| |
| console.assert(treeElement instanceof WI.SearchResultTreeElement); |
| if (!(treeElement instanceof WI.SearchResultTreeElement)) |
| return; |
| |
| if (treeElement.representedObject instanceof WI.DOMSearchMatchObject) |
| WI.showMainFrameDOMTree(treeElement.representedObject.domNode); |
| else if (treeElement.representedObject instanceof WI.SourceCodeSearchMatchObject) |
| WI.showOriginalOrFormattedSourceCodeTextRange(treeElement.representedObject.sourceCodeTextRange, options); |
| } |
| |
| _treeElementDoubleClick(event) |
| { |
| let treeElement = event.target; |
| if (!treeElement) |
| return; |
| |
| if (treeElement.representedObject instanceof WI.DOMSearchMatchObject) { |
| WI.showMainFrameDOMTree(treeElement.representedObject.domNode, { |
| ignoreSearchTab: true, |
| }); |
| } else if (treeElement.representedObject instanceof WI.SourceCodeSearchMatchObject) { |
| WI.showOriginalOrFormattedSourceCodeTextRange(treeElement.representedObject.sourceCodeTextRange, { |
| ignoreNetworkTab: true, |
| ignoreSearchTab: true, |
| }); |
| } |
| } |
| |
| _contentChanged(event) |
| { |
| this.element.classList.add("changed"); |
| |
| if (!this._changedBanner) { |
| this._changedBanner = document.createElement("div"); |
| this._changedBanner.classList.add("banner"); |
| this._changedBanner.append(WI.UIString("The page's content has changed"), document.createElement("br")); |
| |
| let performSearchLink = this._changedBanner.appendChild(document.createElement("a")); |
| performSearchLink.textContent = WI.UIString("Search Again"); |
| performSearchLink.addEventListener("click", () => { |
| const performSearch = true; |
| this.focusSearchField(performSearch); |
| }); |
| } |
| |
| this.element.appendChild(this._changedBanner); |
| } |
| |
| _handleDefaultContentViewSearchNavigationItemClicked(event) |
| { |
| this.focusSearchField(); |
| } |
| }; |