1037 lines
41 KiB
JavaScript
1037 lines
41 KiB
JavaScript
/*
|
|
* 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._groupingsMap = new Map;
|
|
|
|
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._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; }
|
|
|
|
set ignoreNextContentDidChangeForStyleSheet(ignoreNextContentDidChangeForStyleSheet) { this._ignoreNextContentDidChangeForStyleSheet = ignoreNextContentDidChangeForStyleSheet; }
|
|
|
|
get needsRefresh()
|
|
{
|
|
return this._pendingRefreshTask || this._needsRefresh;
|
|
}
|
|
|
|
get uniqueOrderedStyles()
|
|
{
|
|
return WI.DOMNodeStyles.uniqueOrderedStyles(this._orderedStyles);
|
|
}
|
|
|
|
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._groupingsMap = new Map;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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.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, 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) => {
|
|
// COMPATIBILITY (macOS 13, iOS 16) CSS.CSSRule.ruleId did not exist yet.
|
|
let ruleId = grouping.ruleId;
|
|
|
|
let ruleIdForMap = null;
|
|
if (ruleId) {
|
|
ruleIdForMap = `${ruleId.styleSheetId}-${ruleId.ordinal}`;
|
|
|
|
let existingGroupingForRuleId = this._groupingsMap.get(ruleIdForMap);
|
|
if (existingGroupingForRuleId) {
|
|
console.assert(existingGroupingForRuleId.text === grouping.text);
|
|
console.assert(existingGroupingForRuleId.type === grouping.type);
|
|
return existingGroupingForRuleId;
|
|
}
|
|
}
|
|
|
|
let groupingType = WI.CSSManager.protocolGroupingTypeToEnum(grouping.type || grouping.source);
|
|
|
|
let location = {};
|
|
if (payload.range) {
|
|
location.line = payload.range.startLine;
|
|
location.column = payload.range.startColumn;
|
|
location.documentNode = this._node.ownerDocument;
|
|
}
|
|
|
|
// The style sheet may be different from the style rule's style sheet, since groupings are computed beyond
|
|
// `@import` boundaries, and an `@import` statement from another style sheet may have been wrapped in
|
|
// another `@` rule.
|
|
let groupingStyleSheet = ruleId ? WI.cssManager.styleSheetForIdentifier(ruleId.styleSheetId) : null;
|
|
|
|
let groupingSourceCodeLocation = WI.DOMNodeStyles.createSourceCodeLocation(grouping.sourceURL, location);
|
|
let offsetGroupingSourceCodeLocation = styleSheet?.offsetSourceCodeLocation(groupingSourceCodeLocation) ?? groupingSourceCodeLocation;
|
|
|
|
let cssGrouping = new WI.CSSGrouping(this, groupingType, {
|
|
ownerStyleSheet: groupingStyleSheet,
|
|
id: grouping.ruleId,
|
|
text: grouping.text,
|
|
sourceCodeLocation: offsetGroupingSourceCodeLocation,
|
|
});
|
|
|
|
if (ruleIdForMap)
|
|
this._groupingsMap.set(ruleIdForMap, cssGrouping);
|
|
|
|
return cssGrouping;
|
|
});
|
|
|
|
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"
|
|
};
|