/* * Copyright (C) 2013 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. */ WebInspector.DOMNodeStyles = function(node) { WebInspector.Object.call(this); console.assert(node); this._node = node || null; this._rulesMap = {}; this._styleDeclarationsMap = {}; this._matchedRules = []; this._inheritedRules = []; this._pseudoElements = {}; this._inlineStyle = null; this._attributesStyle = null; this._computedStyle = null; this._orderedStyles = []; this._stylesNeedingTextCommited = []; this._propertyNameToEffectivePropertyMap = {}; this.refresh(); }; WebInspector.Object.addConstructorFunctions(WebInspector.DOMNodeStyles); WebInspector.DOMNodeStyles.Event = { NeedsRefresh: "dom-node-styles-needs-refresh", Refreshed: "dom-node-styles-refreshed" }; WebInspector.DOMNodeStyles.prototype = { constructor: WebInspector.DOMNodeStyles, // Public get node() { return this._node; }, get needsRefresh() { return this._refreshPending || this._needsRefresh; }, refreshIfNeeded: function() { if (!this._needsRefresh) return; this.refresh(); }, refresh: function() { if (this._refreshPending) return; this._needsRefresh = false; this._refreshPending = true; function parseRuleMatchArrayPayload(matchArray, node, inherited) { var result = []; var ruleOccurrences = {}; // Iterate in reverse order to match the cascade order. for (var i = matchArray.length - 1; i >= 0; --i) { // COMPATIBILITY (iOS 6): This was just an array of rules, now it is an array of matches that have // a 'rule' property. Support both here. And 'matchingSelectors' does not exist on iOS 6. var matchedSelectorIndices = matchArray[i].matchingSelectors || []; var rule = this._parseRulePayload(matchArray[i].rule || matchArray[i], matchedSelectorIndices, node, inherited, ruleOccurrences); if (!rule) continue; result.push(rule); } return result; } function fetchedMatchedStyles(error, matchedRulesPayload, pseudoElementRulesPayload, inheritedRulesPayload) { matchedRulesPayload = matchedRulesPayload || []; pseudoElementRulesPayload = pseudoElementRulesPayload || []; inheritedRulesPayload = inheritedRulesPayload || []; // Move the current maps to previous. this._previousRulesMap = this._rulesMap; this._previousStyleDeclarationsMap = this._styleDeclarationsMap; // Clear the current maps. this._rulesMap = {}; this._styleDeclarationsMap = {}; this._matchedRules = parseRuleMatchArrayPayload.call(this, matchedRulesPayload, this._node); this._pseudoElements = {}; for (var i = 0; i < pseudoElementRulesPayload.length; ++i) { var pseudoElementRulePayload = pseudoElementRulesPayload[i]; // COMPATIBILITY (iOS 6): The entry payload had a 'rules' property, now it has a 'matches' property. Support both here. var pseudoElementRules = parseRuleMatchArrayPayload.call(this, pseudoElementRulePayload.matches || pseudoElementRulePayload.rules, this._node); this._pseudoElements[pseudoElementRulePayload.pseudoId] = {matchedRules: pseudoElementRules}; } this._inheritedRules = []; var i = 0; var currentNode = this._node.parentNode; while (currentNode && i < inheritedRulesPayload.length) { var inheritedRulePayload = inheritedRulesPayload[i]; var inheritedRuleInfo = {node: currentNode}; inheritedRuleInfo.inlineStyle = inheritedRulePayload.inlineStyle ? this._parseStyleDeclarationPayload(inheritedRulePayload.inlineStyle, currentNode, true, WebInspector.CSSStyleDeclaration.Type.Inline) : null; inheritedRuleInfo.matchedRules = inheritedRulePayload.matchedCSSRules ? parseRuleMatchArrayPayload.call(this, inheritedRulePayload.matchedCSSRules, currentNode, true) : []; if (inheritedRuleInfo.inlineStyle || inheritedRuleInfo.matchedRules.length) this._inheritedRules.push(inheritedRuleInfo); currentNode = currentNode.parentNode; ++i; } } function fetchedInlineStyles(error, inlineStylePayload, attributesStylePayload) { this._inlineStyle = inlineStylePayload ? this._parseStyleDeclarationPayload(inlineStylePayload, this._node, false, WebInspector.CSSStyleDeclaration.Type.Inline) : null; this._attributesStyle = attributesStylePayload ? this._parseStyleDeclarationPayload(attributesStylePayload, this._node, false, WebInspector.CSSStyleDeclaration.Type.Attribute) : null; this._updateStyleCascade(); } function fetchedComputedStyle(error, computedPropertiesPayload) { var properties = []; for (var i = 0; computedPropertiesPayload && i < computedPropertiesPayload.length; ++i) { var propertyPayload = computedPropertiesPayload[i]; var canonicalName = WebInspector.cssStyleManager.canonicalNameForPropertyName(propertyPayload.name); propertyPayload.implicit = !this._propertyNameToEffectivePropertyMap[canonicalName]; var property = this._parseStylePropertyPayload(propertyPayload, NaN, this._computedStyle); properties.push(property); } if (this._computedStyle) this._computedStyle.update(null, properties); else this._computedStyle = new WebInspector.CSSStyleDeclaration(this, null, null, WebInspector.CSSStyleDeclaration.Type.Computed, this._node, false, null, properties); this._refreshPending = false; var significantChange = this._previousSignificantChange || false; if (!significantChange) { for (var key in this._styleDeclarationsMap) { // Check if the same key exists in the previous map and has the same style objects. if (key in this._previousStyleDeclarationsMap && Object.shallowEqual(this._styleDeclarationsMap[key], this._previousStyleDeclarationsMap[key])) continue; if (!this._includeUserAgentRulesOnNextRefresh) { // We can assume all the styles with the same key are from the same stylesheet and rule, so we only check the first. var firstStyle = this._styleDeclarationsMap[key][0]; if (firstStyle && firstStyle.ownerRule && firstStyle.ownerRule.type === WebInspector.CSSRule.Type.UserAgent) { // User Agent styles get different identifiers after some edits. This would cause us to fire a significant refreshed // event more than it is helpful. And since the user agent stylesheet is static it shouldn't match differently // between refreshes for the same node. This issue is tracked by: https://webkit.org/b/110055 continue; } } // This key is new or has different style objects than before. This is a significant change. significantChange = true; break; } } if (!significantChange) { for (var key in this._previousStyleDeclarationsMap) { // Check if the same key exists in current map. If it does exist it was already checked for equality above. if (key in this._styleDeclarationsMap) continue; if (!this._includeUserAgentRulesOnNextRefresh) { // See above for why we skip user agent style rules. var firstStyle = this._previousStyleDeclarationsMap[key][0]; if (firstStyle && firstStyle.ownerRule && firstStyle.ownerRule.type === WebInspector.CSSRule.Type.UserAgent) continue; } // This key no longer exists. This is a significant change. significantChange = true; break; } } delete this._includeUserAgentRulesOnNextRefresh; // Delete the previous maps now that any reused rules and style have been moved over. delete this._previousRulesMap; delete this._previousStyleDeclarationsMap; var styleToCommit = this._stylesNeedingTextCommited.shift(); if (styleToCommit) { // Remember the significant change flag so we can pass it along when the pending style // changes trigger a refresh. If we wait to scan later we might not find a significant change // and fail to tell listeners about it. this._previousSignificantChange = significantChange; this.changeStyleText(styleToCommit, styleToCommit.__pendingText); return; } // Delete the previous saved significant change flag so we rescan for a significant change next time. delete this._previousSignificantChange; this.dispatchEventToListeners(WebInspector.DOMNodeStyles.Event.Refreshed, {significantChange: significantChange}); } CSSAgent.getMatchedStylesForNode.invoke({nodeId: this._node.id, includePseudo: true, includeInherited: true}, fetchedMatchedStyles.bind(this)); CSSAgent.getInlineStylesForNode.invoke({nodeId: this._node.id}, fetchedInlineStyles.bind(this)); CSSAgent.getComputedStyleForNode.invoke({nodeId: this._node.id}, fetchedComputedStyle.bind(this)); }, addRule: function(selector) { function addedRule(error, rulePayload) { if (error) return; DOMAgent.markUndoableState(); this.refresh(); } selector = selector || this._node.appropriateSelectorFor(true); CSSAgent.addRule.invoke({contextNodeId: this._node.id, selector: selector}, addedRule.bind(this)); }, get matchedRules() { return this._matchedRules; }, get inheritedRules() { return this._inheritedRules; }, get inlineStyle() { return this._inlineStyle; }, get attributesStyle() { return this._attributesStyle; }, get pseudoElements() { return this._pseudoElements; }, get computedStyle() { return this._computedStyle; }, get orderedStyles() { return this._orderedStyles; }, effectivePropertyForName: function(name) { var canonicalName = WebInspector.cssStyleManager.canonicalNameForPropertyName(name); return this._propertyNameToEffectivePropertyMap[canonicalName] || null; }, // Protected mediaQueryResultDidChange: function() { this._markAsNeedsRefresh(); }, pseudoClassesDidChange: function(node) { this._includeUserAgentRulesOnNextRefresh = true; this._markAsNeedsRefresh(); }, attributeDidChange: function(node, attributeName) { // Ignore the attribute we know we just changed and handled above. if (this._ignoreNextStyleAttributeDidChangeEvent && node === this._node && attributeName === "style") { delete this._ignoreNextStyleAttributeDidChangeEvent; return; } this._markAsNeedsRefresh(); }, changeRuleSelector: function(rule, selector) { selector = selector || ""; function ruleSelectorChanged(error, rulePayload) { DOMAgent.markUndoableState(); // Do a full refresh incase the rule no longer matches the node or the // matched selector indices changed. this.refresh(); } this._needsRefresh = true; this._ignoreNextContentDidChangeForStyleSheet = rule.ownerStyleSheet; CSSAgent.setRuleSelector(rule.id, selector, ruleSelectorChanged.bind(this)); }, changeStyleText: function(style, text) { if (!style.ownerStyleSheet || !style.styleSheetTextRange) return; text = text || ""; if (CSSAgent.setStyleText) { function styleChanged(error, stylePayload) { if (error) return; this.refresh(); } CSSAgent.setStyleText(style.id, text, styleChanged.bind(this)); return; } // COMPATIBILITY (iOS 6): CSSAgent.setStyleText was not available in iOS 6. // Setting the text on CSSStyleSheet for inline styles causes a crash. https://webkit.org/b/110359 // So we just set the style attribute to get the same affect. This also avoids SourceCodeRevisions. if (style.type === WebInspector.CSSStyleDeclaration.Type.Inline) { text = text.trim(); function attributeChanged(error) { if (error) return; this.refresh(); } this._ignoreNextStyleAttributeDidChangeEvent = true; if (text) style.node.setAttributeValue("style", text, attributeChanged.bind(this)); else style.node.removeAttribute("style", attributeChanged.bind(this)); return; } if (this._needsRefresh || this._refreshPending) { // If we need refreshed then it is not safe to use the styleSheetTextRange since the range likely has // changed and we need updated ranges. Store the text and remember the style so we can commit it after // the next refresh. style.__pendingText = text; if (!this._stylesNeedingTextCommited.contains(style)) this._stylesNeedingTextCommited.push(style); return; } function fetchedStyleSheetContent(styleSheet, content) { console.assert(style.styleSheetTextRange); if (!style.styleSheetTextRange) return; var startOffset = style.styleSheetTextRange.startOffset; var endOffset = style.styleSheetTextRange.endOffset; if (isNaN(startOffset) || isNaN(endOffset)) { style.styleSheetTextRange.resolveOffsets(content); startOffset = style.styleSheetTextRange.startOffset; endOffset = style.styleSheetTextRange.endOffset; } console.assert(!isNaN(startOffset)); console.assert(!isNaN(endOffset)); if (isNaN(startOffset) || isNaN(endOffset)) return; function contentDidChange() { style.ownerStyleSheet.removeEventListener(WebInspector.CSSStyleSheet.Event.ContentDidChange, contentDidChange, this); this.refresh(); } style.ownerStyleSheet.addEventListener(WebInspector.CSSStyleSheet.Event.ContentDidChange, contentDidChange, this); var newContent = content.substring(0, startOffset) + text + content.substring(endOffset); WebInspector.branchManager.currentBranch.revisionForRepresentedObject(style.ownerStyleSheet).content = newContent; } this._stylesNeedingTextCommited.remove(style); delete style.__pendingText; this._needsRefresh = true; this._ignoreNextContentDidChangeForStyleSheet = style.ownerStyleSheet; style.ownerStyleSheet.requestContent(fetchedStyleSheetContent.bind(this)); }, changeProperty: function(property, name, value, priority) { var text = name ? name + ": " + value + (priority ? " !" + priority : "") + ";" : ""; this.changePropertyText(property, text); }, changePropertyText: function(property, text) { text = text || ""; var index = property.index; var newProperty = isNaN(index); var overwrite = true; // If this is a new property, then give it an index at the end of the current properties. // Also don't overwrite, which will cause the property to be added at that index. if (newProperty) { index = property.ownerStyle.properties.length; overwrite = false; } if (text && text.charAt(text.length - 1) !== ";") text += ";"; this._needsRefresh = true; this._ignoreNextContentDidChangeForStyleSheet = property.ownerStyle.ownerStyleSheet; CSSAgent.setPropertyText(property.ownerStyle.id, index, text, overwrite, this._handlePropertyChange.bind(this, property)); }, changePropertyEnabledState: function(property, enabled) { enabled = !!enabled; // Can't change a pending property with a NaN index. if (isNaN(property.index)) return; this._ignoreNextContentDidChangeForStyleSheet = property.ownerStyle.ownerStyleSheet; CSSAgent.toggleProperty(property.ownerStyle.id, property.index, !enabled, this._handlePropertyChange.bind(this, property)); }, addProperty: function(property) { // Can't add a property unless it has a NaN index. if (!isNaN(property.index)) return; // Adding is done by setting the text. this.changePropertyText(property, property.text); }, removeProperty: function(property) { // Can't remove a pending property with a NaN index. if (isNaN(property.index)) return; // Removing is done by setting text to an empty string. this.changePropertyText(property, ""); }, // Private _handlePropertyChange: function(property, error, stylePayload) { if (error) return; DOMAgent.markUndoableState(); // Do a refresh instead of handling stylePayload so computed style is updated and we get valid // styleSheetTextRange values for all the rules after this change. this.refresh(); }, _createSourceCodeLocation: function(sourceURL, sourceLine, sourceColumn) { if (!sourceURL) return null; var sourceCode; // Try to use the node to find the frame which has the correct resource first. if (this._node.ownerDocument) { var mainResource = WebInspector.frameResourceManager.resourceForURL(this._node.ownerDocument.documentURL); if (mainResource) { var parentFrame = mainResource.parentFrame; sourceCode = parentFrame.resourceForURL(sourceURL); } } // If that didn't find the resource, then search all frames. if (!sourceCode) sourceCode = WebInspector.frameResourceManager.resourceForURL(sourceURL); if (!sourceCode) return null; return sourceCode.createSourceCodeLocation(sourceLine || 0, sourceColumn || 0); }, _parseSourceRangePayload: function(payload, text) { if (!payload) return null; // COMPATIBILITY (iOS 6): The range use to only contain start and end offsets. Now it // has line and column for the start and end position. Support both here. if ("start" in payload && "end" in payload) { var textRange = new WebInspector.TextRange(payload.start, payload.end); if (typeof text === "string") textRange.resolveLinesAndColumns(text); return textRange; } return new WebInspector.TextRange(payload.startLine, payload.startColumn, payload.endLine, payload.endColumn); }, _parseStylePropertyPayload: function(payload, index, styleDeclaration, styleText) { var text = payload.text || ""; var name = payload.name; var value = (payload.value || "").replace(/\s*!important\s*$/, ""); var priority = payload.priority || ""; var enabled = true; var overridden = false; var implicit = payload.implicit || false; var anonymous = false; var valid = "parsedOk" in payload ? payload.parsedOk : true; switch (payload.status || "style") { case "active": enabled = true; break; case "inactive": overridden = true; enabled = true; break; case "disabled": enabled = false; break; case "style": anonymous = true; break; } var styleSheetTextRange = null; var styleDeclarationTextRange = null; // COMPATIBILITY (iOS 6): The range is in the style text, not the whole stylesheet. // Later the range was changed to be in the whole stylesheet. if (payload.range && "start" in payload.range && "end" in payload.range) styleDeclarationTextRange = this._parseSourceRangePayload(payload.range, styleText); else styleSheetTextRange = this._parseSourceRangePayload(payload.range); if (styleDeclaration) { // Use propertyForName when the index is NaN since propertyForName is fast in that case. var property = isNaN(index) ? styleDeclaration.propertyForName(name, true) : styleDeclaration.properties[index]; // Reuse a property if the index and name matches. Otherwise it is a different property // and should be created from scratch. This works in the simple cases where only existing // properties change in place and no properties are inserted or deleted at the beginning. // FIXME: This could be smarter by ignoring index and just go by name. However, that gets // tricky for rules that have more than one property with the same name. if (property && property.name === name && (property.index === index || (isNaN(property.index) && isNaN(index)))) { property.update(text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange, styleDeclarationTextRange); return property; } // Reuse a pending property with the same name. These properties are pending being committed, // so if we find a match that likely means it got committed and we should use it. var pendingProperties = styleDeclaration.pendingProperties; for (var i = 0; i < pendingProperties.length; ++i) { var pendingProperty = pendingProperties[i]; if (pendingProperty.name === name && isNaN(pendingProperty.index)) { pendingProperty.index = index; pendingProperty.update(text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange, styleDeclarationTextRange); return pendingProperty; } } } return new WebInspector.CSSProperty(index, text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange, styleDeclarationTextRange); }, _parseStyleDeclarationPayload: function(payload, node, inherited, type, rule, updateAllStyles) { if (!payload) return null; rule = rule || null; inherited = inherited || false; var id = payload.styleId; var mapKey = id ? id.styleSheetId + ":" + id.ordinal : null; var styleDeclaration = rule ? rule.style : null; var styleDeclarations = []; // Look for existing styles in the previous map if there is one, otherwise use the current map. var previousStyleDeclarationsMap = this._previousStyleDeclarationsMap || this._styleDeclarationsMap; if (mapKey && mapKey in previousStyleDeclarationsMap) { styleDeclarations = previousStyleDeclarationsMap[mapKey]; // If we need to update all styles, then stop here and call _parseStyleDeclarationPayload for each style. // We need to parse multiple times so we reuse the right properties from each style. if (updateAllStyles && styleDeclarations.length) { for (var i = 0; i < styleDeclarations.length; ++i) { var styleDeclaration = styleDeclarations[i]; this._parseStyleDeclarationPayload(payload, styleDeclaration.node, styleDeclaration.inherited, styleDeclaration.type, styleDeclaration.ownerRule); } return; } if (!styleDeclaration) { var filteredStyleDeclarations = styleDeclarations.filter(function(styleDeclaration) { // This case only applies for styles that are not part of a rule. if (styleDeclaration.ownerRule) { console.assert(!rule); return false; } if (styleDeclaration.node !== node) return false; if (styleDeclaration.inherited !== inherited) return false; return true; }); console.assert(filteredStyleDeclarations.length <= 1); styleDeclaration = filteredStyleDeclarations[0] || null; } } if (previousStyleDeclarationsMap !== this._styleDeclarationsMap) { // If the previous and current maps differ then make sure the found styleDeclaration is added to the current map. styleDeclarations = mapKey && mapKey in this._styleDeclarationsMap ? this._styleDeclarationsMap[mapKey] : [] ; if (styleDeclaration && !styleDeclarations.contains(styleDeclaration)) { styleDeclarations.push(styleDeclaration); this._styleDeclarationsMap[mapKey] = styleDeclarations; } } var shorthands = {}; for (var i = 0; payload.shorthandEntries && i < payload.shorthandEntries.length; ++i) { var shorthand = payload.shorthandEntries[i]; shorthands[shorthand.name] = shorthand.value; } var text = payload.cssText; var inheritedPropertyCount = 0; var properties = []; for (var i = 0; payload.cssProperties && i < payload.cssProperties.length; ++i) { var propertyPayload = payload.cssProperties[i]; if (inherited && propertyPayload.name in WebInspector.CSSKeywordCompletions.InheritedProperties) ++inheritedPropertyCount; var property = this._parseStylePropertyPayload(propertyPayload, i, styleDeclaration, text); properties.push(property); } if (inherited && !inheritedPropertyCount) return null; var styleSheetTextRange = this._parseSourceRangePayload(payload.range); if (styleDeclaration) { styleDeclaration.update(text, properties, styleSheetTextRange); return styleDeclaration; } var styleSheet = id ? WebInspector.cssStyleManager.styleSheetForIdentifier(id.styleSheetId) : null; if (styleSheet) styleSheet.addEventListener(WebInspector.CSSStyleSheet.Event.ContentDidChange, this._styleSheetContentDidChange, this); styleDeclaration = new WebInspector.CSSStyleDeclaration(this, styleSheet, id, type, node, inherited, text, properties, styleSheetTextRange); if (mapKey) { styleDeclarations.push(styleDeclaration); this._styleDeclarationsMap[mapKey] = styleDeclarations; } return styleDeclaration; }, _parseRulePayload: function(payload, matchedSelectorIndices, node, inherited, ruleOccurrences) { if (!payload) return null; // User and User Agent rules don't have 'ruleId' in the payload. However, their style's have 'styleId' and // 'styleId' is the same identifier the backend uses for Author rule identifiers, so do the same here. // They are excluded by the backend because they are not editable, however our front-end does not determine // editability solely based on the existence of the id like the open source front-end does. var id = payload.ruleId || payload.style.styleId; var mapKey = id ? id.styleSheetId + ":" + id.ordinal + ":" + (inherited ? "I" : "N") + ":" + node.id : null; // Rules can match multiple times if they have multiple selectors or because of inheritance. We keep a count // of occurrences so we have unique rules per occurrence, that way properties will be correctly marked as overridden. var occurrence = 0; if (mapKey) { if (mapKey in ruleOccurrences) occurrence = ++ruleOccurrences[mapKey]; else ruleOccurrences[mapKey] = occurrence; } // Append the occurrence number to the map key for lookup in the rules map. mapKey += ":" + occurrence; var rule = null; // Look for existing rules in the previous map if there is one, otherwise use the current map. var previousRulesMap = this._previousRulesMap || this._rulesMap; if (mapKey && mapKey in previousRulesMap) { rule = previousRulesMap[mapKey]; if (previousRulesMap !== this._rulesMap) { // If the previous and current maps differ then make sure the found rule is added to the current map. this._rulesMap[mapKey] = rule; } } var style = this._parseStyleDeclarationPayload(payload.style, node, inherited, WebInspector.CSSStyleDeclaration.Type.Rule, rule); if (!style) return null; // COMPATIBILITY (iOS 6): The payload had 'selectorText' as a property, // now it has 'selectorList' with a 'text' property. Support both here. var selectorText = payload.selectorList ? payload.selectorList.text : payload.selectorText; var selectors = payload.selectorList ? payload.selectorList.selectors : []; // COMPATIBILITY (iOS 6): The payload did not have 'selectorList'. // Fallback to using 'sourceLine' without column information. if (payload.selectorList && payload.selectorList.range) { var sourceRange = payload.selectorList.range; var sourceCodeLocation = this._createSourceCodeLocation(payload.sourceURL, sourceRange.startLine, sourceRange.startColumn); } else var sourceCodeLocation = this._createSourceCodeLocation(payload.sourceURL, payload.sourceLine); var type; switch (payload.origin) { case "regular": type = WebInspector.CSSRule.Type.Author; break; case "user": type = WebInspector.CSSRule.Type.User; break; case "user-agent": type = WebInspector.CSSRule.Type.UserAgent; break; case "inspector": type = WebInspector.CSSRule.Type.Inspector; break; } var mediaList = []; for (var i = 0; payload.media && i < payload.media.length; ++i) { var mediaItem = payload.media[i]; var mediaType; switch (mediaItem.source) { case "mediaRule": mediaType = WebInspector.CSSMedia.Type.MediaRule; break; case "importRule": mediaType = WebInspector.CSSMedia.Type.ImportRule; break; case "linkedSheet": mediaType = WebInspector.CSSMedia.Type.LinkedStyleSheet; break; case "inlineSheet": mediaType = WebInspector.CSSMedia.Type.InlineStyleSheet; break; } var mediaText = mediaItem.text; var mediaSourceCodeLocation = this._createSourceCodeLocation(mediaItem.sourceURL, mediaItem.sourceLine); mediaList.push(new WebInspector.CSSMedia(mediaType, mediaText, mediaSourceCodeLocation)); } if (rule) { rule.update(sourceCodeLocation, selectorText, selectors, matchedSelectorIndices, style, mediaList); return rule; } var styleSheet = id ? WebInspector.cssStyleManager.styleSheetForIdentifier(id.styleSheetId) : null; if (styleSheet) styleSheet.addEventListener(WebInspector.CSSStyleSheet.Event.ContentDidChange, this._styleSheetContentDidChange, this); rule = new WebInspector.CSSRule(this, styleSheet, id, type, sourceCodeLocation, selectorText, selectors, matchedSelectorIndices, style, mediaList); if (mapKey) this._rulesMap[mapKey] = rule; return rule; }, _markAsNeedsRefresh: function() { this._needsRefresh = true; this.dispatchEventToListeners(WebInspector.DOMNodeStyles.Event.NeedsRefresh); }, _styleSheetContentDidChange: function(event) { var styleSheet = event.target; console.assert(styleSheet); if (!styleSheet) return; // Ignore the stylesheet we know we just changed and handled above. if (styleSheet === this._ignoreNextContentDidChangeForStyleSheet) { delete this._ignoreNextContentDidChangeForStyleSheet; return; } this._markAsNeedsRefresh(); }, _updateStyleCascade: function() { var cascadeOrderedStyleDeclarations = this._collectStylesInCascadeOrder(this._matchedRules, this._inlineStyle, this._attributesStyle); for (var i = 0; i < this._inheritedRules.length; ++i) { var inheritedStyleInfo = this._inheritedRules[i]; var inheritedCascadeOrder = this._collectStylesInCascadeOrder(inheritedStyleInfo.matchedRules, inheritedStyleInfo.inlineStyle, null); cascadeOrderedStyleDeclarations = cascadeOrderedStyleDeclarations.concat(inheritedCascadeOrder); } this._orderedStyles = cascadeOrderedStyleDeclarations; this._propertyNameToEffectivePropertyMap = {}; this._markOverriddenProperties(cascadeOrderedStyleDeclarations, this._propertyNameToEffectivePropertyMap); this._associateRelatedProperties(cascadeOrderedStyleDeclarations, this._propertyNameToEffectivePropertyMap); for (var pseudoIdentifier in this._pseudoElements) { var pseudoElementInfo = this._pseudoElements[pseudoIdentifier]; pseudoElementInfo.orderedStyles = this._collectStylesInCascadeOrder(pseudoElementInfo.matchedRules, null, null); this._markOverriddenProperties(pseudoElementInfo.orderedStyles); this._associateRelatedProperties(pseudoElementInfo.orderedStyles); } }, _collectStylesInCascadeOrder: function(matchedRules, inlineStyle, attributesStyle) { var result = []; // Inline style has the greatest specificity. So it goes first in the cascade order. if (inlineStyle) result.push(inlineStyle); var userAndUserAgentStyles = []; for (var i = 0; i < matchedRules.length; ++i) { var rule = matchedRules[i]; // Only append to the result array here for author and inspector rules since attribute // styles come between author rules and user/user agent rules. switch (rule.type) { case WebInspector.CSSRule.Type.Inspector: case WebInspector.CSSRule.Type.Author: result.push(rule.style); break; case WebInspector.CSSRule.Type.User: case WebInspector.CSSRule.Type.UserAgent: userAndUserAgentStyles.push(rule.style); break; } } // Style properties from HTML attributes are next. if (attributesStyle) result.push(attributesStyle); // Finally add the user and user stylesheet's matched style rules we collected earlier. result = result.concat(userAndUserAgentStyles); return result; }, _markOverriddenProperties: function(styles, propertyNameToEffectiveProperty) { propertyNameToEffectiveProperty = propertyNameToEffectiveProperty || {}; for (var i = 0; i < styles.length; ++i) { var style = styles[i]; var properties = style.properties; for (var j = 0; j < properties.length; ++j) { var property = properties[j]; if (!property.enabled || property.anonymous || !property.valid) { property.overridden = false; continue; } if (style.inherited && !property.inherited) { property.overridden = false; continue; } var canonicalName = property.canonicalName; if (canonicalName in propertyNameToEffectiveProperty) { var effectiveProperty = propertyNameToEffectiveProperty[canonicalName]; if (effectiveProperty.ownerStyle === property.ownerStyle) { if (effectiveProperty.important && !property.important) { property.overridden = true; continue; } } else if (effectiveProperty.important || !property.important || effectiveProperty.ownerStyle.node !== property.ownerStyle.node) { property.overridden = true; continue; } effectiveProperty.overridden = true; } property.overridden = false; propertyNameToEffectiveProperty[canonicalName] = property; } } }, _associateRelatedProperties: function(styles, propertyNameToEffectiveProperty) { for (var i = 0; i < styles.length; ++i) { var properties = styles[i].properties; var knownShorthands = {}; for (var j = 0; j < properties.length; ++j) { var property = properties[j]; if (!property.valid) continue; if (!WebInspector.CSSCompletions.cssNameCompletions.isShorthandPropertyName(property.name)) continue; if (knownShorthands[property.canonicalName] && !knownShorthands[property.canonicalName].overridden) { console.assert(property.overridden); continue; } knownShorthands[property.canonicalName] = property; } for (var j = 0; j < properties.length; ++j) { var property = properties[j]; if (!property.valid) continue; var shorthandProperty = null; if (!isEmptyObject(knownShorthands)) { var possibleShorthands = WebInspector.CSSCompletions.cssNameCompletions.shorthandsForLonghand(property.canonicalName); for (var k = 0; k < possibleShorthands.length; ++k) { if (possibleShorthands[k] in knownShorthands) { shorthandProperty = knownShorthands[possibleShorthands[k]]; break; } } } if (!shorthandProperty || shorthandProperty.overridden !== property.overridden) { property.relatedShorthandProperty = null; property.clearRelatedLonghandProperties(); continue; } shorthandProperty.addRelatedLonghandProperty(property); property.relatedShorthandProperty = shorthandProperty; if (propertyNameToEffectiveProperty && propertyNameToEffectiveProperty[shorthandProperty.canonicalName] === shorthandProperty) propertyNameToEffectiveProperty[property.canonicalName] = property; } } } }; WebInspector.DOMNodeStyles.prototype.__proto__ = WebInspector.Object.prototype;