blob: 11e6f97cb393bee5b61a9e6dc35852d6abb9a7b9 [file] [log] [blame]
/*
* 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.
*/
WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
{
constructor(node)
{
super();
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._propertyNameToEffectivePropertyMap = {};
this._pendingRefreshTask = null;
this.refresh();
}
// Public
get node()
{
return this._node;
}
get needsRefresh()
{
return this._pendingRefreshTask || this._needsRefresh;
}
refreshIfNeeded()
{
if (this._pendingRefreshTask)
return this._pendingRefreshTask;
if (!this._needsRefresh)
return Promise.resolve(this);
return this.refresh();
}
refresh()
{
if (this._pendingRefreshTask)
return this._pendingRefreshTask;
this._needsRefresh = false;
let fetchedMatchedStylesPromise = new WI.WrappedPromise;
let fetchedInlineStylesPromise = new WI.WrappedPromise;
let fetchedComputedStylesPromise = new WI.WrappedPromise;
// Ensure we resolve these promises even in the case of an error.
function wrap(func, promise) {
return (...args) => {
try {
func.apply(this, args);
} catch (e) {
console.error(e);
promise.resolve();
}
};
}
function parseRuleMatchArrayPayload(matchArray, node, inherited)
{
var result = [];
// Iterate in reverse order to match the cascade order.
var ruleOccurrences = {};
for (var i = matchArray.length - 1; i >= 0; --i) {
var rule = this._parseRulePayload(matchArray[i].rule, matchArray[i].matchingSelectors, 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 pseudoElementRulePayload of pseudoElementRulesPayload) {
var pseudoElementRules = parseRuleMatchArrayPayload.call(this, pseudoElementRulePayload.matches, 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, WI.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;
}
fetchedMatchedStylesPromise.resolve();
}
function fetchedInlineStyles(error, inlineStylePayload, attributesStylePayload)
{
this._inlineStyle = inlineStylePayload ? this._parseStyleDeclarationPayload(inlineStylePayload, this._node, false, WI.CSSStyleDeclaration.Type.Inline) : null;
this._attributesStyle = attributesStylePayload ? this._parseStyleDeclarationPayload(attributesStylePayload, this._node, false, WI.CSSStyleDeclaration.Type.Attribute) : null;
this._updateStyleCascade();
fetchedInlineStylesPromise.resolve();
}
function fetchedComputedStyle(error, computedPropertiesPayload)
{
var properties = [];
for (var i = 0; computedPropertiesPayload && i < computedPropertiesPayload.length; ++i) {
var propertyPayload = computedPropertiesPayload[i];
var canonicalName = WI.cssStyleManager.canonicalNameForPropertyName(propertyPayload.name);
propertyPayload.implicit = !this._propertyNameToEffectivePropertyMap[canonicalName];
var property = this._parseStylePropertyPayload(propertyPayload, NaN, this._computedStyle);
if (!property.implicit)
property.implicit = !this._isPropertyFoundInMatchingRules(property.name);
properties.push(property);
}
if (this._computedStyle)
this._computedStyle.update(null, properties);
else
this._computedStyle = new WI.CSSStyleDeclaration(this, null, null, WI.CSSStyleDeclaration.Type.Computed, this._node, false, null, properties);
let significantChange = false;
for (let 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) {
if (Array.shallowEqual(this._styleDeclarationsMap[key], this._previousStyleDeclarationsMap[key]))
continue;
// Some styles have selectors such that they will match with the DOM node twice (for example "::before, ::after").
// In this case a second style for a second matching may be generated and added which will cause the shallowEqual
// to not return true, so in this case we just want to ensure that all the current styles existed previously.
let styleFound = false;
for (let style of this._styleDeclarationsMap[key]) {
if (this._previousStyleDeclarationsMap[key].includes(style)) {
styleFound = true;
break;
}
}
if (styleFound)
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.
let firstStyle = this._styleDeclarationsMap[key][0];
if (firstStyle && firstStyle.ownerRule && firstStyle.ownerRule.type === WI.CSSStyleSheet.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 === WI.CSSStyleSheet.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;
this.dispatchEventToListeners(WI.DOMNodeStyles.Event.Refreshed, {significantChange});
fetchedComputedStylesPromise.resolve();
}
// FIXME: Convert to pushing StyleSheet information to the frontend. <rdar://problem/13213680>
WI.cssStyleManager.fetchStyleSheetsIfNeeded();
CSSAgent.getMatchedStylesForNode.invoke({nodeId: this._node.id, includePseudo: true, includeInherited: true}, wrap.call(this, fetchedMatchedStyles, fetchedMatchedStylesPromise));
CSSAgent.getInlineStylesForNode.invoke({nodeId: this._node.id}, wrap.call(this, fetchedInlineStyles, fetchedInlineStylesPromise));
CSSAgent.getComputedStyleForNode.invoke({nodeId: this._node.id}, wrap.call(this, fetchedComputedStyle, fetchedComputedStylesPromise));
this._pendingRefreshTask = Promise.all([fetchedMatchedStylesPromise.promise, fetchedInlineStylesPromise.promise, fetchedComputedStylesPromise.promise])
.then(() => {
this._pendingRefreshTask = null;
return this;
});
return this._pendingRefreshTask;
}
addRule(selector, text, styleSheetId)
{
selector = selector || this._node.appropriateSelectorFor(true);
function completed()
{
DOMAgent.markUndoableState();
this.refresh();
}
function styleChanged(error, stylePayload)
{
if (error)
return;
completed.call(this);
}
function addedRule(error, rulePayload)
{
if (error)
return;
if (!text || !text.length) {
completed.call(this);
return;
}
CSSAgent.setStyleText(rulePayload.style.styleId, text, styleChanged.bind(this));
}
// COMPATIBILITY (iOS 9): Before CSS.createStyleSheet, CSS.addRule could be called with a contextNode.
if (!CSSAgent.createStyleSheet) {
CSSAgent.addRule.invoke({contextNodeId: this._node.id, selector}, addedRule.bind(this));
return;
}
function inspectorStyleSheetAvailable(styleSheet)
{
if (!styleSheet)
return;
CSSAgent.addRule(styleSheet.id, selector, addedRule.bind(this));
}
if (styleSheetId)
inspectorStyleSheetAvailable.call(this, WI.cssStyleManager.styleSheetForIdentifier(styleSheetId));
else
WI.cssStyleManager.preferredInspectorStyleSheetForFrame(this._node.frame, inspectorStyleSheetAvailable.bind(this));
}
rulesForSelector(selector)
{
selector = selector || this._node.appropriateSelectorFor(true);
function ruleHasSelector(rule) {
return !rule.mediaList.length && rule.selectorText === selector;
}
let rules = this._matchedRules.filter(ruleHasSelector);
for (let id in this._pseudoElements)
rules = rules.concat(this._pseudoElements[id].matchedRules.filter(ruleHasSelector));
return rules;
}
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(name)
{
let property = this._propertyNameToEffectivePropertyMap[name];
if (property)
return property;
let canonicalName = WI.cssStyleManager.canonicalNameForPropertyName(name);
return this._propertyNameToEffectivePropertyMap[canonicalName] || null;
}
// Protected
mediaQueryResultDidChange()
{
this._markAsNeedsRefresh();
}
pseudoClassesDidChange(node)
{
this._includeUserAgentRulesOnNextRefresh = true;
this._markAsNeedsRefresh();
}
attributeDidChange(node, attributeName)
{
this._markAsNeedsRefresh();
}
changeRule(rule, selector, text)
{
if (!rule)
return;
selector = selector || "";
function changeCompleted()
{
DOMAgent.markUndoableState();
this.refresh();
}
function styleChanged(error, stylePayload)
{
if (error)
return;
changeCompleted.call(this);
}
function changeText(styleId)
{
if (!text || !text.length) {
changeCompleted.call(this);
return;
}
CSSAgent.setStyleText(styleId, text, styleChanged.bind(this));
}
function ruleSelectorChanged(error, rulePayload)
{
if (error)
return;
changeText.call(this, rulePayload.style.styleId);
}
this._needsRefresh = true;
this._ignoreNextContentDidChangeForStyleSheet = rule.ownerStyleSheet;
CSSAgent.setRuleSelector(rule.id, selector, ruleSelectorChanged.bind(this));
}
changeRuleSelector(rule, selector)
{
selector = selector || "";
let result = new WI.WrappedPromise;
function ruleSelectorChanged(error, rulePayload)
{
if (error) {
result.reject(error);
return;
}
DOMAgent.markUndoableState();
// Do a full refresh incase the rule no longer matches the node or the
// matched selector indices changed.
this.refresh().then(() => {
result.resolve(rulePayload);
});
}
this._needsRefresh = true;
this._ignoreNextContentDidChangeForStyleSheet = rule.ownerStyleSheet;
CSSAgent.setRuleSelector(rule.id, selector, ruleSelectorChanged.bind(this));
return result.promise;
}
changeStyleText(style, text)
{
if (!style.ownerStyleSheet || !style.styleSheetTextRange)
return;
text = text || "";
function styleChanged(error, stylePayload)
{
if (error)
return;
this.refresh();
}
CSSAgent.setStyleText(style.id, text, styleChanged.bind(this));
}
// Private
_createSourceCodeLocation(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 = WI.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 = WI.frameResourceManager.resourceForURL(sourceURL);
if (!sourceCode)
return null;
return sourceCode.createSourceCodeLocation(sourceLine || 0, sourceColumn || 0);
}
_parseSourceRangePayload(payload)
{
if (!payload)
return null;
return new WI.TextRange(payload.startLine, payload.startColumn, payload.endLine, payload.endColumn);
}
_parseStylePropertyPayload(payload, index, styleDeclaration, styleText)
{
var text = payload.text || "";
var name = payload.name;
var value = payload.value || "";
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":
// FIXME: Is this still needed? This includes UserAgent styles and HTML attribute styles.
anonymous = true;
break;
}
var 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);
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);
return pendingProperty;
}
}
}
return new WI.CSSProperty(index, text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange);
}
_parseStyleDeclarationPayload(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;
if (type === WI.CSSStyleDeclaration.Type.Attribute)
mapKey = node.id + ":attribute";
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 null;
}
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.includes(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 && WI.CSSProperty.isInheritedPropertyName(propertyPayload.name))
++inheritedPropertyCount;
var property = this._parseStylePropertyPayload(propertyPayload, i, styleDeclaration, text);
properties.push(property);
}
var styleSheetTextRange = this._parseSourceRangePayload(payload.range);
if (styleDeclaration) {
styleDeclaration.update(text, properties, styleSheetTextRange);
return styleDeclaration;
}
var styleSheet = id ? WI.cssStyleManager.styleSheetForIdentifier(id.styleSheetId) : null;
if (styleSheet) {
if (type === WI.CSSStyleDeclaration.Type.Inline)
styleSheet.markAsInlineStyleAttributeStyleSheet();
styleSheet.addEventListener(WI.CSSStyleSheet.Event.ContentDidChange, this._styleSheetContentDidChange, this);
}
if (inherited && !inheritedPropertyCount)
return null;
styleDeclaration = new WI.CSSStyleDeclaration(this, styleSheet, id, type, node, inherited, text, properties, styleSheetTextRange);
if (mapKey) {
styleDeclarations.push(styleDeclaration);
this._styleDeclarationsMap[mapKey] = styleDeclarations;
}
return styleDeclaration;
}
_parseSelectorListPayload(selectorList)
{
var selectors = selectorList.selectors;
if (!selectors.length)
return [];
// COMPATIBILITY (iOS 8): The selectorList payload was an array of selector text strings.
// Now they are CSSSelector objects with multiple properties.
if (typeof selectors[0] === "string") {
return selectors.map(function(selectorText) {
return new WI.CSSSelector(selectorText);
});
}
return selectors.map(function(selectorPayload) {
return new WI.CSSSelector(selectorPayload.text, selectorPayload.specificity, selectorPayload.dynamic);
});
}
_parseRulePayload(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, WI.CSSStyleDeclaration.Type.Rule, rule);
if (!style)
return null;
var styleSheet = id ? WI.cssStyleManager.styleSheetForIdentifier(id.styleSheetId) : null;
var selectorText = payload.selectorList.text;
var selectors = this._parseSelectorListPayload(payload.selectorList);
var type = WI.CSSStyleManager.protocolStyleSheetOriginToEnum(payload.origin);
var sourceCodeLocation = null;
var sourceRange = payload.selectorList.range;
if (sourceRange)
sourceCodeLocation = this._createSourceCodeLocation(payload.sourceURL, sourceRange.startLine, sourceRange.startColumn);
else {
// FIXME: Is it possible for a CSSRule to have a sourceLine without its selectorList having a sourceRange? Fall back just in case.
sourceCodeLocation = this._createSourceCodeLocation(payload.sourceURL, payload.sourceLine);
}
if (styleSheet) {
if (!sourceCodeLocation && styleSheet.isInspectorStyleSheet())
sourceCodeLocation = styleSheet.createSourceCodeLocation(sourceRange.startLine, sourceRange.startColumn);
sourceCodeLocation = styleSheet.offsetSourceCodeLocation(sourceCodeLocation);
}
var mediaList = [];
for (var i = 0; payload.media && i < payload.media.length; ++i) {
var mediaItem = payload.media[i];
var mediaType = WI.CSSStyleManager.protocolMediaSourceToEnum(mediaItem.source);
var mediaText = mediaItem.text;
var mediaSourceCodeLocation = this._createSourceCodeLocation(mediaItem.sourceURL, mediaItem.sourceLine);
if (styleSheet)
mediaSourceCodeLocation = styleSheet.offsetSourceCodeLocation(mediaSourceCodeLocation);
mediaList.push(new WI.CSSMedia(mediaType, mediaText, mediaSourceCodeLocation));
}
if (rule) {
rule.update(sourceCodeLocation, selectorText, selectors, matchedSelectorIndices, style, mediaList);
return rule;
}
if (styleSheet)
styleSheet.addEventListener(WI.CSSStyleSheet.Event.ContentDidChange, this._styleSheetContentDidChange, this);
rule = new WI.CSSRule(this, styleSheet, id, type, sourceCodeLocation, selectorText, selectors, matchedSelectorIndices, style, mediaList);
if (mapKey)
this._rulesMap[mapKey] = rule;
return rule;
}
_markAsNeedsRefresh()
{
this._needsRefresh = true;
this.dispatchEventToListeners(WI.DOMNodeStyles.Event.NeedsRefresh);
}
_styleSheetContentDidChange(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()
{
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(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 WI.CSSStyleSheet.Type.Inspector:
case WI.CSSStyleSheet.Type.Author:
result.push(rule.style);
break;
case WI.CSSStyleSheet.Type.User:
case WI.CSSStyleSheet.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(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.attached || !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;
}
if (!property.anonymous)
effectiveProperty.overridden = true;
}
property.overridden = false;
propertyNameToEffectiveProperty[canonicalName] = property;
}
}
}
_associateRelatedProperties(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 (!WI.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 = WI.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;
}
}
}
_isPropertyFoundInMatchingRules(propertyName)
{
return this._orderedStyles.some((style) => {
return style.properties.some((property) => property.name === propertyName);
});
}
};
WI.DOMNodeStyles.Event = {
NeedsRefresh: "dom-node-styles-needs-refresh",
Refreshed: "dom-node-styles-refreshed"
};