blob: 27a48d5f3efb94beced2395bcd36f3c4ee048e1f [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 = new Map;
this._stylesMap = new Multimap;
this._matchedRules = [];
this._inheritedRules = [];
this._pseudoElements = new Map;
this._inlineStyle = null;
this._attributesStyle = null;
this._computedStyle = null;
this._orderedStyles = [];
this._computedPrimaryFont = null;
this._propertyNameToEffectivePropertyMap = {};
this._usedCSSVariables = new Set;
this._allCSSVariables = new Set;
this._variableStylesByType = null;
this._pendingRefreshTask = null;
this.refresh();
this._trackedStyleSheets = new WeakSet;
WI.CSSStyleSheet.addEventListener(WI.CSSStyleSheet.Event.ContentDidChange, this._handleCSSStyleSheetContentDidChange, this);
}
// Static
static parseSelectorListPayload(selectorList)
{
let selectors = selectorList.selectors;
if (!selectors.length)
return [];
return selectors.map(function(selectorPayload) {
return new WI.CSSSelector(selectorPayload.text, selectorPayload.specificity, selectorPayload.dynamic);
});
}
static createSourceCodeLocation(sourceURL, {line, column, documentNode} = {})
{
if (!sourceURL)
return null;
let sourceCode = null;
// Try to use the node to find the frame which has the correct resource first.
if (documentNode) {
let mainResource = WI.networkManager.resourcesForURL(documentNode.documentURL).firstValue;
if (mainResource) {
let parentFrame = mainResource.parentFrame;
sourceCode = parentFrame.resourcesForURL(sourceURL).firstValue;
}
}
// If that didn't find the resource, then search all frames.
if (!sourceCode)
sourceCode = WI.networkManager.resourcesForURL(sourceURL).firstValue;
if (!sourceCode)
return null;
return sourceCode.createSourceCodeLocation(line || 0, column || 0);
}
static uniqueOrderedStyles(orderedStyles)
{
let uniqueOrderedStyles = [];
for (let style of orderedStyles) {
let rule = style.ownerRule;
if (!rule) {
uniqueOrderedStyles.push(style);
continue;
}
let found = false;
for (let existingStyle of uniqueOrderedStyles) {
if (rule.isEqualTo(existingStyle.ownerRule)) {
found = true;
break;
}
}
if (!found)
uniqueOrderedStyles.push(style);
}
return uniqueOrderedStyles;
}
// Public
get node() { return this._node; }
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; }
get computedPrimaryFont() { return this._computedPrimaryFont; }
get usedCSSVariables() { return this._usedCSSVariables; }
get allCSSVariables() { return this._allCSSVariables; }
get needsRefresh()
{
return this._pendingRefreshTask || this._needsRefresh;
}
get uniqueOrderedStyles()
{
return WI.DOMNodeStyles.uniqueOrderedStyles(this._orderedStyles);
}
get variableStylesByType()
{
if (this._variableStylesByType)
return this._variableStylesByType;
let properties = this._computedStyle?.properties;
if (!properties)
return new Map;
// Will iterate in order through type checkers for each CSS variable to identify its type.
// The catch-all "other" must always be last.
const typeCheckFunctions = [
{
type: WI.DOMNodeStyles.VariablesGroupType.Colors,
checker: (property) => WI.Color.fromString(property.value),
},
{
type: WI.DOMNodeStyles.VariablesGroupType.Dimensions,
// FIXME: <https://webkit.org/b/233576> build RegExp from `WI.CSSCompletions.lengthUnits`.
checker: (property) => /^-?\d+(\.\d+)?\D+$/.test(property.value),
},
{
type: WI.DOMNodeStyles.VariablesGroupType.Numbers,
checker: (property) => /^-?\d+(\.\d+)?$/.test(property.value),
},
{
type: WI.DOMNodeStyles.VariablesGroupType.Other,
checker: (property) => true,
},
];
let variablesForType = {};
for (let property of properties) {
if (!property.isVariable)
continue;
for (let {type, checker} of typeCheckFunctions) {
if (checker(property)) {
variablesForType[type] ||= [];
variablesForType[type].push(property);
break;
}
}
}
this._variableStylesByType = new Map;
for (let {type} of typeCheckFunctions) {
if (!variablesForType[type]?.length)
continue;
const ownerStyleSheet = null;
const id = null;
const inherited = false;
const text = null;
let style = new WI.CSSStyleDeclaration(this, ownerStyleSheet, id, WI.CSSStyleDeclaration.Type.Computed, this._node, inherited, text, variablesForType[type]);
this._variableStylesByType.set(type, style);
}
return this._variableStylesByType;
}
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;
let fetchedFontDataPromise = 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();
}
};
}
let parseRuleMatchArrayPayload = (matchArray, node, inherited, pseudoId) => {
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, pseudoId, ruleOccurrences);
if (!rule)
continue;
result.push(rule);
}
return result;
};
function fetchedMatchedStyles(error, matchedRulesPayload, pseudoElementRulesPayload, inheritedRulesPayload)
{
matchedRulesPayload = matchedRulesPayload || [];
pseudoElementRulesPayload = pseudoElementRulesPayload || [];
inheritedRulesPayload = inheritedRulesPayload || [];
this._previousStylesMap = this._stylesMap;
this._stylesMap = new Multimap;
this._matchedRules = parseRuleMatchArrayPayload(matchedRulesPayload, this._node);
this._pseudoElements.clear();
for (let {pseudoId, matches} of pseudoElementRulesPayload) {
let pseudoElementRules = parseRuleMatchArrayPayload(matches, this._node, false, pseudoId);
this._pseudoElements.set(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, null, WI.CSSStyleDeclaration.Type.Inline) : null;
inheritedRuleInfo.matchedRules = inheritedRulePayload.matchedCSSRules ? parseRuleMatchArrayPayload(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, null, WI.CSSStyleDeclaration.Type.Inline) : null;
this._attributesStyle = attributesStylePayload ? this._parseStyleDeclarationPayload(attributesStylePayload, this._node, false, null, 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.cssManager.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, styles] of this._stylesMap.sets()) {
// Check if the same key exists in the previous map and has the same style objects.
let previousStyles = this._previousStylesMap.get(key);
if (previousStyles) {
// 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 styles) {
if (previousStyles.has(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 = styles.firstValue;
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 (let [key, previousStyles] of this._previousStylesMap.sets()) {
// Check if the same key exists in current map. If it does exist it was already checked for equality above.
if (this._stylesMap.has(key))
continue;
if (!this._includeUserAgentRulesOnNextRefresh) {
// See above for why we skip user agent style rules.
let firstStyle = previousStyles.firstValue;
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;
}
}
if (significantChange)
this._variableStylesByType = null;
this._previousStylesMap = null;
this._includeUserAgentRulesOnNextRefresh = false;
fetchedComputedStylesPromise.resolve({significantChange});
}
function fetchedFontData(error, fontDataPayload)
{
if (fontDataPayload)
this._computedPrimaryFont = WI.Font.fromPayload(fontDataPayload);
else
this._computedPrimaryFont = null;
fetchedFontDataPromise.resolve();
}
let target = WI.assumingMainTarget();
target.CSSAgent.getMatchedStylesForNode.invoke({nodeId: this._node.id, includePseudo: true, includeInherited: true}, wrap.call(this, fetchedMatchedStyles, fetchedMatchedStylesPromise));
target.CSSAgent.getInlineStylesForNode.invoke({nodeId: this._node.id}, wrap.call(this, fetchedInlineStyles, fetchedInlineStylesPromise));
target.CSSAgent.getComputedStyleForNode.invoke({nodeId: this._node.id}, wrap.call(this, fetchedComputedStyle, fetchedComputedStylesPromise));
// COMPATIBILITY (iOS 14.0): `CSS.getFontDataForNode` did not exist yet.
if (InspectorBackend.hasCommand("CSS.getFontDataForNode"))
target.CSSAgent.getFontDataForNode.invoke({nodeId: this._node.id}, wrap.call(this, fetchedFontData, fetchedFontDataPromise));
else
fetchedFontDataPromise.resolve();
this._pendingRefreshTask = Promise.all([fetchedComputedStylesPromise.promise, fetchedMatchedStylesPromise.promise, fetchedInlineStylesPromise.promise, fetchedFontDataPromise.promise])
.then(([fetchComputedStylesResult]) => {
this._pendingRefreshTask = null;
this.dispatchEventToListeners(WI.DOMNodeStyles.Event.Refreshed, {
significantChange: fetchComputedStylesResult.significantChange,
});
return this;
});
return this._pendingRefreshTask;
}
addRule(selector, text, styleSheetId)
{
selector = selector || this._node.appropriateSelectorFor(true);
let target = WI.assumingMainTarget();
function completed()
{
target.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;
}
target.CSSAgent.setStyleText(rulePayload.style.styleId, text, styleChanged.bind(this));
}
function inspectorStyleSheetAvailable(styleSheet)
{
if (!styleSheet)
return;
target.CSSAgent.addRule(styleSheet.id, selector, addedRule.bind(this));
}
if (styleSheetId)
inspectorStyleSheetAvailable.call(this, WI.cssManager.styleSheetForIdentifier(styleSheetId));
else
WI.cssManager.preferredInspectorStyleSheetForFrame(this._node.frame, inspectorStyleSheetAvailable.bind(this));
}
effectivePropertyForName(name)
{
let property = this._propertyNameToEffectivePropertyMap[name];
if (property)
return property;
let canonicalName = WI.cssManager.canonicalNameForPropertyName(name);
return this._propertyNameToEffectivePropertyMap[canonicalName] || null;
}
// Protected
mediaQueryResultDidChange()
{
this._markAsNeedsRefresh();
}
pseudoClassesDidChange(node)
{
this._includeUserAgentRulesOnNextRefresh = true;
this._markAsNeedsRefresh();
}
attributeDidChange(node, attributeName)
{
this._markAsNeedsRefresh();
}
changeRuleSelector(rule, selector)
{
selector = selector || "";
let result = new WI.WrappedPromise;
let target = WI.assumingMainTarget();
function ruleSelectorChanged(error, rulePayload)
{
if (error) {
result.reject(error);
return;
}
target.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;
target.CSSAgent.setRuleSelector(rule.id, selector, ruleSelectorChanged.bind(this));
return result.promise;
}
changeStyleText(style, text, callback)
{
if (!style.ownerStyleSheet || !style.styleSheetTextRange) {
callback();
return;
}
text = text || "";
let didSetStyleText = (error, stylePayload) => {
if (error) {
callback(error);
return;
}
callback();
// Update validity of each property for rules that don't match the selected DOM node.
// These rules don't get updated by CSSAgent.getMatchedStylesForNode.
if (style.ownerRule && !style.ownerRule.matchedSelectorIndices.length)
this._parseStyleDeclarationPayload(stylePayload, this._node, false, null, style.type, style.ownerRule, false);
this.refresh();
};
let target = WI.assumingMainTarget();
target.CSSAgent.setStyleText(style.id, text, didSetStyleText);
}
// Private
_parseSourceRangePayload(payload)
{
if (!payload)
return null;
return new WI.TextRange(payload.startLine, payload.startColumn, payload.endLine, payload.endColumn);
}
_parseStylePropertyPayload(payload, index, styleDeclaration)
{
var text = payload.text || "";
var name = payload.name;
var value = payload.value || "";
var priority = payload.priority || "";
let range = payload.range || null;
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;
}
if (range) {
// Last property of inline style has mismatching range.
// The actual text has one line, but the range spans two lines.
let rangeLineCount = 1 + range.endLine - range.startLine;
if (rangeLineCount > 1) {
let textLineCount = text.lineCount;
if (textLineCount === rangeLineCount - 1) {
range.endLine = range.startLine + (textLineCount - 1);
range.endColumn = range.startColumn + text.lastLine.length;
}
}
}
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) : styleDeclaration.enabledProperties[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, pseudoId, type, rule, matchesNode = true)
{
if (!payload)
return null;
rule = rule || null;
inherited = inherited || false;
var id = payload.styleId;
var mapKey = id ? id.styleSheetId + ":" + id.ordinal : null;
if (pseudoId)
mapKey += ":" + pseudoId;
if (type === WI.CSSStyleDeclaration.Type.Attribute)
mapKey += ":" + node.id + ":attribute";
let style = rule ? rule.style : null;
console.assert(matchesNode || style);
if (matchesNode) {
console.assert(this._previousStylesMap);
let existingStyles = this._previousStylesMap.get(mapKey);
if (existingStyles && !style) {
for (let existingStyle of existingStyles) {
if (existingStyle.node === node && existingStyle.inherited === inherited) {
style = existingStyle;
break;
}
}
}
if (style)
this._stylesMap.add(mapKey, style);
}
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;
let property = this._parseStylePropertyPayload(propertyPayload, i, style);
properties.push(property);
}
let text = payload.cssText;
var styleSheetTextRange = this._parseSourceRangePayload(payload.range);
if (style) {
style.update(text, properties, styleSheetTextRange);
return style;
}
if (!matchesNode)
return null;
var styleSheet = id ? WI.cssManager.styleSheetForIdentifier(id.styleSheetId) : null;
if (styleSheet) {
if (type === WI.CSSStyleDeclaration.Type.Inline)
styleSheet.markAsInlineStyleAttributeStyleSheet();
this._trackedStyleSheets.add(styleSheet);
}
if (inherited && !inheritedPropertyCount)
return null;
style = new WI.CSSStyleDeclaration(this, styleSheet, id, type, node, inherited, text, properties, styleSheetTextRange);
if (mapKey)
this._stylesMap.add(mapKey, style);
return style;
}
_parseRulePayload(payload, matchedSelectorIndices, node, inherited, pseudoId, 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") + ":" + (pseudoId ? pseudoId + ":" : "") + 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;
}
let rule = this._rulesMap.get(mapKey);
var style = this._parseStyleDeclarationPayload(payload.style, node, inherited, pseudoId, WI.CSSStyleDeclaration.Type.Rule, rule);
if (!style)
return null;
var styleSheet = id ? WI.cssManager.styleSheetForIdentifier(id.styleSheetId) : null;
var selectorText = payload.selectorList.text;
let selectors = DOMNodeStyles.parseSelectorListPayload(payload.selectorList);
var type = WI.CSSManager.protocolStyleSheetOriginToEnum(payload.origin);
var sourceCodeLocation = null;
var sourceRange = payload.selectorList.range;
if (sourceRange) {
sourceCodeLocation = DOMNodeStyles.createSourceCodeLocation(payload.sourceURL, {
line: sourceRange.startLine,
column: sourceRange.startColumn,
documentNode: this._node.ownerDocument,
});
} else {
// FIXME: Is it possible for a CSSRule to have a sourceLine without its selectorList having a sourceRange? Fall back just in case.
sourceCodeLocation = DOMNodeStyles.createSourceCodeLocation(payload.sourceURL, {
line: payload.sourceLine,
documentNode: this._node.ownerDocument,
});
}
if (styleSheet) {
if (!sourceCodeLocation && sourceRange)
sourceCodeLocation = styleSheet.createSourceCodeLocation(sourceRange.startLine, sourceRange.startColumn);
sourceCodeLocation = styleSheet.offsetSourceCodeLocation(sourceCodeLocation);
}
// COMPATIBILITY (iOS 13): CSS.CSSRule.groupings did not exist yet.
let groupings = (payload.groupings || payload.media || []).map((grouping) => {
let groupingType = WI.CSSManager.protocolGroupingTypeToEnum(grouping.type || grouping.source);
let groupingSourceCodeLocation = DOMNodeStyles.createSourceCodeLocation(grouping.sourceURL);
if (styleSheet)
groupingSourceCodeLocation = styleSheet.offsetSourceCodeLocation(groupingSourceCodeLocation);
return new WI.CSSGrouping(groupingType, grouping.text, groupingSourceCodeLocation);
});
if (rule) {
rule.update(sourceCodeLocation, selectorText, selectors, matchedSelectorIndices, style, groupings);
return rule;
}
if (styleSheet)
this._trackedStyleSheets.add(styleSheet);
rule = new WI.CSSRule(this, styleSheet, id, type, sourceCodeLocation, selectorText, selectors, matchedSelectorIndices, style, groupings);
if (mapKey)
this._rulesMap.set(mapKey, rule);
return rule;
}
_markAsNeedsRefresh()
{
this._needsRefresh = true;
this.dispatchEventToListeners(WI.DOMNodeStyles.Event.NeedsRefresh);
}
_handleCSSStyleSheetContentDidChange(event)
{
let styleSheet = event.target;
if (!this._trackedStyleSheets.has(styleSheet))
return;
// Ignore the stylesheet we know we just changed and handled above.
if (styleSheet === this._ignoreNextContentDidChangeForStyleSheet) {
this._ignoreNextContentDidChangeForStyleSheet = null;
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.pushAll(inheritedCascadeOrder);
}
this._orderedStyles = cascadeOrderedStyleDeclarations;
this._propertyNameToEffectivePropertyMap = {};
this._associateRelatedProperties(cascadeOrderedStyleDeclarations, this._propertyNameToEffectivePropertyMap);
this._markOverriddenProperties(cascadeOrderedStyleDeclarations, this._propertyNameToEffectivePropertyMap);
this._collectCSSVariables(cascadeOrderedStyleDeclarations);
for (let pseudoElementInfo of this._pseudoElements.values()) {
pseudoElementInfo.orderedStyles = this._collectStylesInCascadeOrder(pseudoElementInfo.matchedRules, null, null);
this._associateRelatedProperties(pseudoElementInfo.orderedStyles);
this._markOverriddenProperties(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.pushAll(userAndUserAgentStyles);
return result;
}
_markOverriddenProperties(styles, propertyNameToEffectiveProperty)
{
propertyNameToEffectiveProperty = propertyNameToEffectiveProperty || {};
function isOverriddenByRelatedShorthand(property) {
let shorthand = property.relatedShorthandProperty;
if (!shorthand)
return false;
if (property.important && !shorthand.important)
return false;
if (!property.important && shorthand.important)
return true;
if (property.ownerStyle === shorthand.ownerStyle)
return shorthand.index > property.index;
let propertyStyleIndex = styles.indexOf(property.ownerStyle);
let shorthandStyleIndex = styles.indexOf(shorthand.ownerStyle);
return shorthandStyleIndex > propertyStyleIndex;
}
for (var i = 0; i < styles.length; ++i) {
var style = styles[i];
var properties = style.enabledProperties;
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;
property.overridingProperty = effectiveProperty;
continue;
}
} else if (effectiveProperty.important || !property.important || effectiveProperty.ownerStyle.node !== property.ownerStyle.node) {
property.overridden = true;
property.overridingProperty = effectiveProperty;
continue;
}
if (!property.anonymous) {
effectiveProperty.overridden = true;
effectiveProperty.overridingProperty = property;
}
}
if (isOverriddenByRelatedShorthand(property)) {
property.overridden = true;
property.overridingProperty = property.relatedShorthandProperty;
} else
property.overridden = false;
propertyNameToEffectiveProperty[canonicalName] = property;
}
}
}
_associateRelatedProperties(styles, propertyNameToEffectiveProperty)
{
for (var i = 0; i < styles.length; ++i) {
var properties = styles[i].enabledProperties;
var knownShorthands = {};
for (var j = 0; j < properties.length; ++j) {
var property = properties[j];
if (!property.valid)
continue;
if (!WI.CSSKeywordCompletions.LonghandNamesForShorthandProperty.has(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.CSSKeywordCompletions.ShorthandNamesForLongHandProperty.get(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;
}
}
}
_collectCSSVariables(styles)
{
this._allCSSVariables = new Set;
this._usedCSSVariables = new Set;
for (let style of styles) {
for (let property of style.enabledProperties) {
if (property.isVariable)
this._allCSSVariables.add(property.name);
let variables = WI.CSSProperty.findVariableNames(property.value);
if (!style.inherited) {
// FIXME: <https://webkit.org/b/226648> Support the case of variables declared on matching styles but not used anywhere.
this._usedCSSVariables.addAll(variables);
continue;
}
// Always collect variables used in values of inheritable properties.
if (WI.CSSKeywordCompletions.InheritedProperties.has(property.name)) {
this._usedCSSVariables.addAll(variables);
continue;
}
// For variables from inherited styles, leverage the fact that styles are already sorted in cascade order to support inherited variables referencing other variables.
// If the variable was found to be used before, collect any variables used in its declaration value
// (if any variables are found, this isn't the end of the variable reference chain in the inheritance stack).
if (property.isVariable && this._usedCSSVariables.has(property.name))
this._usedCSSVariables.addAll(variables);
}
}
}
_isPropertyFoundInMatchingRules(propertyName)
{
return this._orderedStyles.some((style) => {
return style.enabledProperties.some((property) => property.name === propertyName);
});
}
};
WI.DOMNodeStyles.Event = {
NeedsRefresh: "dom-node-styles-needs-refresh",
Refreshed: "dom-node-styles-refreshed"
};
WI.DOMNodeStyles.VariablesGroupType = {
Ungrouped: "ungrouped",
Colors: "colors",
Dimensions: "dimensions",
Numbers: "numbers",
Other: "other",
};