diff options
Diffstat (limited to 'WebCore/inspector/front-end/ElementsPanel.js')
-rw-r--r-- | WebCore/inspector/front-end/ElementsPanel.js | 434 |
1 files changed, 357 insertions, 77 deletions
diff --git a/WebCore/inspector/front-end/ElementsPanel.js b/WebCore/inspector/front-end/ElementsPanel.js index 1748159..ffa0000 100644 --- a/WebCore/inspector/front-end/ElementsPanel.js +++ b/WebCore/inspector/front-end/ElementsPanel.js @@ -61,8 +61,7 @@ WebInspector.ElementsPanel = function() InspectorController.toggleNodeSearch(); this.panel.nodeSearchButton.removeStyleClass("toggled-on"); } - if (this._focusedDOMNode) - InspectorController.addInspectedNode(this._focusedDOMNode.id, function() {}); + WebInspector.console.addInspectedNode(this._focusedDOMNode); }; this.contentElement.appendChild(this.treeOutline.element); @@ -98,7 +97,10 @@ WebInspector.ElementsPanel = function() this.sidebarResizeElement.className = "sidebar-resizer-vertical"; this.sidebarResizeElement.addEventListener("mousedown", this.rightSidebarResizerDragStart.bind(this), false); - this.nodeSearchButton = new WebInspector.StatusBarButton(WebInspector.UIString("Select an element in the page to inspect it."), "node-search-status-bar-item"); + this.nodeSearchButton = this.createStatusBarButton(); + this.nodeSearchButton.title = WebInspector.UIString("Select an element in the page to inspect it."); + this.nodeSearchButton.id = "node-search-status-bar-item"; + this.nodeSearchButton.className = "status-bar-item"; this.nodeSearchButton.addEventListener("click", this._nodeSearchButtonClicked.bind(this), false); this.searchingForNode = false; @@ -107,7 +109,13 @@ WebInspector.ElementsPanel = function() this.element.appendChild(this.sidebarElement); this.element.appendChild(this.sidebarResizeElement); - this._changedStyles = {}; + this._mutationMonitoredWindows = []; + this._nodeInsertedEventListener = InspectorController.wrapCallback(this._nodeInserted.bind(this)); + this._nodeRemovedEventListener = InspectorController.wrapCallback(this._nodeRemoved.bind(this)); + this._contentLoadedEventListener = InspectorController.wrapCallback(this._contentLoaded.bind(this)); + + this.stylesheet = null; + this.styles = {}; this.reset(); } @@ -122,7 +130,7 @@ WebInspector.ElementsPanel.prototype = { get statusBarItems() { - return [this.nodeSearchButton.element, this.crumbsElement]; + return [this.nodeSearchButton, this.crumbsElement]; }, updateStatusBarItems: function() @@ -148,7 +156,7 @@ WebInspector.ElementsPanel.prototype = { if (InspectorController.searchingForNode()) { InspectorController.toggleNodeSearch(); - this.nodeSearchButton.toggled = false; + this.nodeSearchButton.removeStyleClass("toggled-on"); } }, @@ -167,27 +175,40 @@ WebInspector.ElementsPanel.prototype = { if (InspectorController.searchingForNode()) { InspectorController.toggleNodeSearch(); - this.nodeSearchButton.toggled = false; + this.nodeSearchButton.removeStyleClass("toggled-on"); } this.recentlyModifiedNodes = []; + this.unregisterAllMutationEventListeners(); delete this.currentQuery; this.searchCanceled(); - var inspectedWindow = WebInspector.domAgent.inspectedWindow; - if (!inspectedWindow || !inspectedWindow.document || !inspectedWindow.document.firstChild) + var inspectedWindow = Preferences.useDOMAgent ? WebInspector.domAgent.inspectedWindow : InspectorController.inspectedWindow(); + if (!inspectedWindow || !inspectedWindow.document) return; + if (!inspectedWindow.document.firstChild) { + function contentLoaded() + { + inspectedWindow.document.removeEventListener("DOMContentLoaded", contentLoadedCallback, false); + + this.reset(); + } + + var contentLoadedCallback = InspectorController.wrapCallback(contentLoaded.bind(this)); + inspectedWindow.document.addEventListener("DOMContentLoaded", contentLoadedCallback, false); + return; + } + // If the window isn't visible, return early so the DOM tree isn't built // and mutation event listeners are not added. if (!InspectorController.isWindowVisible()) return; - var inspectedRootDocument = inspectedWindow.document; - inspectedRootDocument.addEventListener("DOMNodeInserted", this._nodeInserted.bind(this)); - inspectedRootDocument.addEventListener("DOMNodeRemoved", this._nodeRemoved.bind(this)); + this.registerMutationEventListeners(inspectedWindow); + var inspectedRootDocument = inspectedWindow.document; this.rootDOMNode = inspectedRootDocument; var canidateFocusNode = inspectedRootDocument.body || inspectedRootDocument.documentElement; @@ -201,11 +222,19 @@ WebInspector.ElementsPanel.prototype = { } }, + includedInSearchResultsPropertyName: "__includedInInspectorSearchResults", + searchCanceled: function() { if (this._searchResults) { + const searchResultsProperty = this.includedInSearchResultsPropertyName; for (var i = 0; i < this._searchResults.length; ++i) { - var treeElement = this.treeOutline.findTreeElement(this._searchResults[i]); + var node = this._searchResults[i]; + + // Remove the searchResultsProperty since there might be an unfinished search. + delete node[searchResultsProperty]; + + var treeElement = this.treeOutline.findTreeElement(node); if (treeElement) treeElement.highlighted = false; } @@ -213,9 +242,13 @@ WebInspector.ElementsPanel.prototype = { WebInspector.updateSearchMatchesCount(0, this); + if (this._currentSearchChunkIntervalIdentifier) { + clearInterval(this._currentSearchChunkIntervalIdentifier); + delete this._currentSearchChunkIntervalIdentifier; + } + this._currentSearchResultIndex = 0; this._searchResults = []; - InspectorController.searchCanceled(function() {}); }, performSearch: function(query) @@ -227,56 +260,241 @@ WebInspector.ElementsPanel.prototype = { if (!whitespaceTrimmedQuery.length) return; - this._updatedMatchCountOnce = false; - this._matchesCountUpdateTimeout = null; + var tagNameQuery = whitespaceTrimmedQuery; + var attributeNameQuery = whitespaceTrimmedQuery; + var startTagFound = (tagNameQuery.indexOf("<") === 0); + var endTagFound = (tagNameQuery.lastIndexOf(">") === (tagNameQuery.length - 1)); - InspectorController.performSearch(whitespaceTrimmedQuery, function() {}); - }, + if (startTagFound || endTagFound) { + var tagNameQueryLength = tagNameQuery.length; + tagNameQuery = tagNameQuery.substring((startTagFound ? 1 : 0), (endTagFound ? (tagNameQueryLength - 1) : tagNameQueryLength)); + } - _updateMatchesCount: function() - { - WebInspector.updateSearchMatchesCount(this._searchResults.length, this); - this._matchesCountUpdateTimeout = null; - this._updatedMatchCountOnce = true; - }, + // Check the tagNameQuery is it is a possibly valid tag name. + if (!/^[a-zA-Z0-9\-_:]+$/.test(tagNameQuery)) + tagNameQuery = null; - _updateMatchesCountSoon: function() - { - if (!this._updatedMatchCountOnce) - return this._updateMatchesCount(); - if (this._matchesCountUpdateTimeout) - return; - // Update the matches count every half-second so it doesn't feel twitchy. - this._matchesCountUpdateTimeout = setTimeout(this._updateMatchesCount.bind(this), 500); - }, + // Check the attributeNameQuery is it is a possibly valid tag name. + if (!/^[a-zA-Z0-9\-_:]+$/.test(attributeNameQuery)) + attributeNameQuery = null; - addNodesToSearchResult: function(nodeIds) - { - if (!nodeIds) - return; + const escapedQuery = query.escapeCharacters("'"); + const escapedTagNameQuery = (tagNameQuery ? tagNameQuery.escapeCharacters("'") : null); + const escapedWhitespaceTrimmedQuery = whitespaceTrimmedQuery.escapeCharacters("'"); + const searchResultsProperty = this.includedInSearchResultsPropertyName; - var nodeIdsArray = nodeIds.split(","); - for (var i = 0; i < nodeIdsArray.length; ++i) { - var nodeId = nodeIdsArray[i]; - var node = WebInspector.domAgent.nodeForId(nodeId); - if (!node) - continue; + var updatedMatchCountOnce = false; + var matchesCountUpdateTimeout = null; - if (!this._searchResults.length) { - this._currentSearchResultIndex = 0; - this.focusedDOMNode = node; + function updateMatchesCount() + { + WebInspector.updateSearchMatchesCount(this._searchResults.length, this); + matchesCountUpdateTimeout = null; + updatedMatchCountOnce = true; + } + + function updateMatchesCountSoon() + { + if (!updatedMatchCountOnce) + return updateMatchesCount.call(this); + if (matchesCountUpdateTimeout) + return; + // Update the matches count every half-second so it doesn't feel twitchy. + matchesCountUpdateTimeout = setTimeout(updateMatchesCount.bind(this), 500); + } + + function addNodesToResults(nodes, length, getItem) + { + if (!length) + return; + + for (var i = 0; i < length; ++i) { + var node = getItem.call(nodes, i); + // Skip this node if it already has the property. + if (searchResultsProperty in node) + continue; + + if (!this._searchResults.length) { + this._currentSearchResultIndex = 0; + this.focusedDOMNode = node; + } + + node[searchResultsProperty] = true; + this._searchResults.push(node); + + // Highlight the tree element to show it matched the search. + // FIXME: highlight the substrings in text nodes and attributes. + var treeElement = this.treeOutline.findTreeElement(node); + if (treeElement) + treeElement.highlighted = true; } - this._searchResults.push(node); + updateMatchesCountSoon.call(this); + } + + function matchExactItems(doc) + { + matchExactId.call(this, doc); + matchExactClassNames.call(this, doc); + matchExactTagNames.call(this, doc); + matchExactAttributeNames.call(this, doc); + } + + function matchExactId(doc) + { + const result = doc.__proto__.getElementById.call(doc, whitespaceTrimmedQuery); + addNodesToResults.call(this, result, (result ? 1 : 0), function() { return this }); + } + + function matchExactClassNames(doc) + { + const result = doc.__proto__.getElementsByClassName.call(doc, whitespaceTrimmedQuery); + addNodesToResults.call(this, result, result.length, result.item); + } + + function matchExactTagNames(doc) + { + if (!tagNameQuery) + return; + const result = doc.__proto__.getElementsByTagName.call(doc, tagNameQuery); + addNodesToResults.call(this, result, result.length, result.item); + } + + function matchExactAttributeNames(doc) + { + if (!attributeNameQuery) + return; + const result = doc.__proto__.querySelectorAll.call(doc, "[" + attributeNameQuery + "]"); + addNodesToResults.call(this, result, result.length, result.item); + } + + function matchPartialTagNames(doc) + { + if (!tagNameQuery) + return; + const result = doc.__proto__.evaluate.call(doc, "//*[contains(name(), '" + escapedTagNameQuery + "')]", doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE); + addNodesToResults.call(this, result, result.snapshotLength, result.snapshotItem); + } - // Highlight the tree element to show it matched the search. - // FIXME: highlight the substrings in text nodes and attributes. - var treeElement = this.treeOutline.findTreeElement(node); - if (treeElement) - treeElement.highlighted = true; + function matchStartOfTagNames(doc) + { + if (!tagNameQuery) + return; + const result = doc.__proto__.evaluate.call(doc, "//*[starts-with(name(), '" + escapedTagNameQuery + "')]", doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE); + addNodesToResults.call(this, result, result.snapshotLength, result.snapshotItem); } - this._updateMatchesCountSoon(); + function matchPartialTagNamesAndAttributeValues(doc) + { + if (!tagNameQuery) { + matchPartialAttributeValues.call(this, doc); + return; + } + + const result = doc.__proto__.evaluate.call(doc, "//*[contains(name(), '" + escapedTagNameQuery + "') or contains(@*, '" + escapedQuery + "')]", doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE); + addNodesToResults.call(this, result, result.snapshotLength, result.snapshotItem); + } + + function matchPartialAttributeValues(doc) + { + const result = doc.__proto__.evaluate.call(doc, "//*[contains(@*, '" + escapedQuery + "')]", doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE); + addNodesToResults.call(this, result, result.snapshotLength, result.snapshotItem); + } + + function matchStyleSelector(doc) + { + const result = doc.__proto__.querySelectorAll.call(doc, whitespaceTrimmedQuery); + addNodesToResults.call(this, result, result.length, result.item); + } + + function matchPlainText(doc) + { + const result = doc.__proto__.evaluate.call(doc, "//text()[contains(., '" + escapedQuery + "')] | //comment()[contains(., '" + escapedQuery + "')]", doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE); + addNodesToResults.call(this, result, result.snapshotLength, result.snapshotItem); + } + + function matchXPathQuery(doc) + { + const result = doc.__proto__.evaluate.call(doc, whitespaceTrimmedQuery, doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE); + addNodesToResults.call(this, result, result.snapshotLength, result.snapshotItem); + } + + function finishedSearching() + { + // Remove the searchResultsProperty now that the search is finished. + for (var i = 0; i < this._searchResults.length; ++i) + delete this._searchResults[i][searchResultsProperty]; + } + + const mainFrameDocument = InspectorController.inspectedWindow().document; + const searchDocuments = [mainFrameDocument]; + + if (tagNameQuery && startTagFound && endTagFound) + const searchFunctions = [matchExactTagNames, matchPlainText]; + else if (tagNameQuery && startTagFound) + const searchFunctions = [matchStartOfTagNames, matchPlainText]; + else if (tagNameQuery && endTagFound) { + // FIXME: we should have a matchEndOfTagNames search function if endTagFound is true but not startTagFound. + // This requires ends-with() support in XPath, WebKit only supports starts-with() and contains(). + const searchFunctions = [matchPartialTagNames, matchPlainText]; + } else if (whitespaceTrimmedQuery === "//*" || whitespaceTrimmedQuery === "*") { + // These queries will match every node. Matching everything isn't useful and can be slow for large pages, + // so limit the search functions list to plain text and attribute matching. + const searchFunctions = [matchPartialAttributeValues, matchPlainText]; + } else + const searchFunctions = [matchExactItems, matchStyleSelector, matchPartialTagNamesAndAttributeValues, matchPlainText, matchXPathQuery]; + + // Find all frames, iframes and object elements to search their documents. + const querySelectorAllFunction = InspectorController.inspectedWindow().Document.prototype.querySelectorAll; + const subdocumentResult = querySelectorAllFunction.call(mainFrameDocument, "iframe, frame, object"); + + for (var i = 0; i < subdocumentResult.length; ++i) { + var element = subdocumentResult.item(i); + if (element.contentDocument) + searchDocuments.push(element.contentDocument); + } + + const panel = this; + var documentIndex = 0; + var searchFunctionIndex = 0; + var chunkIntervalIdentifier = null; + + // Split up the work into chunks so we don't block the UI thread while processing. + + function processChunk() + { + var searchDocument = searchDocuments[documentIndex]; + var searchFunction = searchFunctions[searchFunctionIndex]; + + if (++searchFunctionIndex > searchFunctions.length) { + searchFunction = searchFunctions[0]; + searchFunctionIndex = 0; + + if (++documentIndex > searchDocuments.length) { + if (panel._currentSearchChunkIntervalIdentifier === chunkIntervalIdentifier) + delete panel._currentSearchChunkIntervalIdentifier; + clearInterval(chunkIntervalIdentifier); + finishedSearching.call(panel); + return; + } + + searchDocument = searchDocuments[documentIndex]; + } + + if (!searchDocument || !searchFunction) + return; + + try { + searchFunction.call(panel, searchDocument); + } catch(err) { + // ignore any exceptions. the query might be malformed, but we allow that. + } + } + + processChunk(); + + chunkIntervalIdentifier = setInterval(processChunk, 25); + this._currentSearchChunkIntervalIdentifier = chunkIntervalIdentifier; }, jumpToNextSearchResult: function() @@ -297,6 +515,12 @@ WebInspector.ElementsPanel.prototype = { this.focusedDOMNode = this._searchResults[this._currentSearchResultIndex]; }, + inspectedWindowCleared: function(window) + { + if (InspectorController.isWindowVisible()) + this.updateMutationEventListeners(window); + }, + renameSelector: function(oldIdentifier, newIdentifier, oldSelector, newSelector) { // TODO: Implement Shifting the oldSelector, and its contents to a newSelector @@ -308,16 +532,16 @@ WebInspector.ElementsPanel.prototype = { return; var selector = style.parentRule.selectorText; - if (!this._changedStyles[identifier]) - this._changedStyles[identifier] = {}; + if (!this.styles[identifier]) + this.styles[identifier] = {}; - if (!this._changedStyles[identifier][selector]) - this._changedStyles[identifier][selector] = {}; + if (!this.styles[identifier][selector]) + this.styles[identifier][selector] = {}; - if (!this._changedStyles[identifier][selector][property]) + if (!this.styles[identifier][selector][property]) WebInspector.styleChanges += 1; - this._changedStyles[identifier][selector][property] = style.getPropertyValue(property); + this.styles[identifier][selector][property] = style.getPropertyValue(property); }, removeStyleChange: function(identifier, style, property) @@ -326,11 +550,11 @@ WebInspector.ElementsPanel.prototype = { return; var selector = style.parentRule.selectorText; - if (!this._changedStyles[identifier] || !this._changedStyles[identifier][selector]) + if (!this.styles[identifier] || !this.styles[identifier][selector]) return; - if (this._changedStyles[identifier][selector][property]) { - delete this._changedStyles[identifier][selector][property]; + if (this.styles[identifier][selector][property]) { + delete this.styles[identifier][selector][property]; WebInspector.styleChanges -= 1; } }, @@ -342,20 +566,20 @@ WebInspector.ElementsPanel.prototype = { // Merge Down to Just Selectors var mergedSelectors = {}; - for (var identifier in this._changedStyles) { - for (var selector in this._changedStyles[identifier]) { + for (var identifier in this.styles) { + for (var selector in this.styles[identifier]) { if (!mergedSelectors[selector]) - mergedSelectors[selector] = this._changedStyles[identifier][selector]; + mergedSelectors[selector] = this.styles[identifier][selector]; else { // merge on selector var merge = {}; for (var property in mergedSelectors[selector]) merge[property] = mergedSelectors[selector][property]; - for (var property in this._changedStyles[identifier][selector]) { + for (var property in this.styles[identifier][selector]) { if (!merge[property]) - merge[property] = this._changedStyles[identifier][selector][property]; + merge[property] = this.styles[identifier][selector][property]; else { // merge on property within a selector, include comment to notify user var value1 = merge[property]; - var value2 = this._changedStyles[identifier][selector][property]; + var value2 = this.styles[identifier][selector][property]; if (value1 === value2) merge[property] = [value1]; @@ -412,6 +636,53 @@ WebInspector.ElementsPanel.prototype = { InspectorController.inspectedWindow().console.log(result); }, + _addMutationEventListeners: function(monitoredWindow) + { + monitoredWindow.document.addEventListener("DOMNodeInserted", this._nodeInsertedEventListener, true); + monitoredWindow.document.addEventListener("DOMNodeRemoved", this._nodeRemovedEventListener, true); + if (monitoredWindow.frameElement) + monitoredWindow.addEventListener("DOMContentLoaded", this._contentLoadedEventListener, true); + }, + + _removeMutationEventListeners: function(monitoredWindow) + { + if (monitoredWindow.frameElement) + monitoredWindow.removeEventListener("DOMContentLoaded", this._contentLoadedEventListener, true); + if (!monitoredWindow.document) + return; + monitoredWindow.document.removeEventListener("DOMNodeInserted", this._nodeInsertedEventListener, true); + monitoredWindow.document.removeEventListener("DOMNodeRemoved", this._nodeRemovedEventListener, true); + }, + + updateMutationEventListeners: function(monitoredWindow) + { + this._addMutationEventListeners(monitoredWindow); + }, + + registerMutationEventListeners: function(monitoredWindow) + { + if (!monitoredWindow || this._mutationMonitoredWindows.indexOf(monitoredWindow) !== -1) + return; + this._mutationMonitoredWindows.push(monitoredWindow); + if (InspectorController.isWindowVisible()) + this._addMutationEventListeners(monitoredWindow); + }, + + unregisterMutationEventListeners: function(monitoredWindow) + { + if (!monitoredWindow || this._mutationMonitoredWindows.indexOf(monitoredWindow) === -1) + return; + this._mutationMonitoredWindows.remove(monitoredWindow); + this._removeMutationEventListeners(monitoredWindow); + }, + + unregisterAllMutationEventListeners: function() + { + for (var i = 0; i < this._mutationMonitoredWindows.length; ++i) + this._removeMutationEventListeners(this._mutationMonitoredWindows[i]); + this._mutationMonitoredWindows = []; + }, + get rootDOMNode() { return this.treeOutline.rootDOMNode; @@ -432,6 +703,13 @@ WebInspector.ElementsPanel.prototype = { this.treeOutline.focusedDOMNode = x; }, + _contentLoaded: function(event) + { + this.recentlyModifiedNodes.push({node: event.target, parent: event.target.defaultView.frameElement, replaced: true}); + if (this.visible) + this._updateModifiedNodesSoon(); + }, + _nodeInserted: function(event) { this.recentlyModifiedNodes.push({node: event.target, parent: event.relatedNode, inserted: true}); @@ -469,14 +747,14 @@ WebInspector.ElementsPanel.prototype = { if (!parent) continue; - var parentNodeItem = this.treeOutline.findTreeElement(parent); + var parentNodeItem = this.treeOutline.findTreeElement(parent, null, null, objectsAreSame); if (parentNodeItem && !parentNodeItem.alreadyUpdatedChildren) { parentNodeItem.updateChildren(replaced); parentNodeItem.alreadyUpdatedChildren = true; updatedParentTreeElements.push(parentNodeItem); } - if (!updateBreadcrumbs && (this.focusedDOMNode === parent || isAncestorIncludingParentFrames(this.focusedDOMNode, parent))) + if (!updateBreadcrumbs && (objectsAreSame(this.focusedDOMNode, parent) || isAncestorIncludingParentFrames(this.focusedDOMNode, parent))) updateBreadcrumbs = true; } @@ -536,7 +814,7 @@ WebInspector.ElementsPanel.prototype = { var foundRoot = false; var crumb = crumbs.firstChild; while (crumb) { - if (crumb.representedObject === this.rootDOMNode) + if (objectsAreSame(crumb.representedObject, this.rootDOMNode)) foundRoot = true; if (foundRoot) @@ -544,7 +822,7 @@ WebInspector.ElementsPanel.prototype = { else crumb.removeStyleClass("dimmed"); - if (crumb.representedObject === this.focusedDOMNode) { + if (objectsAreSame(crumb.representedObject, this.focusedDOMNode)) { crumb.addStyleClass("selected"); handled = true; } else { @@ -601,7 +879,7 @@ WebInspector.ElementsPanel.prototype = { if (current.nodeType === Node.DOCUMENT_NODE) continue; - if (current === this.rootDOMNode) + if (objectsAreSame(current, this.rootDOMNode)) foundRoot = true; var crumb = document.createElement("span"); @@ -684,7 +962,7 @@ WebInspector.ElementsPanel.prototype = { if (foundRoot) crumb.addStyleClass("dimmed"); - if (current === this.focusedDOMNode) + if (objectsAreSame(current, this.focusedDOMNode)) crumb.addStyleClass("selected"); if (!crumbs.childNodes.length) crumb.addStyleClass("end"); @@ -991,8 +1269,7 @@ WebInspector.ElementsPanel.prototype = { switch (this.focusedDOMNode.nodeType) { case Node.ELEMENT_NODE: - // TODO: Introduce InspectorController.copyEvent that pushes appropriate markup into the clipboard. - var data = null; + var data = this.focusedDOMNode.outerHTML; break; case Node.COMMENT_NODE: @@ -1039,7 +1316,10 @@ WebInspector.ElementsPanel.prototype = { { InspectorController.toggleNodeSearch(); - this.nodeSearchButton.toggled = InspectorController.searchingForNode(); + if (InspectorController.searchingForNode()) + this.nodeSearchButton.addStyleClass("toggled-on"); + else + this.nodeSearchButton.removeStyleClass("toggled-on"); } } |