/* * 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.CSSStyleDeclaration = class CSSStyleDeclaration extends WI.Object { constructor(nodeStyles, ownerStyleSheet, id, type, node, inherited, text, properties, styleSheetTextRange) { super(); console.assert(nodeStyles); this._nodeStyles = nodeStyles; this._ownerRule = null; this._ownerStyleSheet = ownerStyleSheet || null; this._id = id || null; this._type = type || null; this._node = node || null; this._inherited = inherited || false; this._initialState = null; this._updatesInProgressCount = 0; this._pendingPropertiesChanged = false; this._locked = false; this._pendingProperties = []; this._propertyNameMap = {}; this._properties = []; this._enabledProperties = null; this._visibleProperties = null; this._variablesForType = new Map; this.update(text, properties, styleSheetTextRange, {dontFireEvents: true}); } // Public get initialState() { return this._initialState; } get id() { return this._id; } get stringId() { if (this._id) return this._id.styleSheetId + "/" + this._id.ordinal; else return ""; } get ownerStyleSheet() { return this._ownerStyleSheet; } get type() { return this._type; } get inherited() { return this._inherited; } get node() { return this._node; } get editable() { if (!this._id) return false; if (this._type === WI.CSSStyleDeclaration.Type.Rule) return this._ownerRule && this._ownerRule.editable; if (this._type === WI.CSSStyleDeclaration.Type.Inline) return !this._node.isInUserAgentShadowTree() || WI.DOMManager.supportsEditingUserAgentShadowTrees(); return false; } get selectorEditable() { return this._ownerRule && this._ownerRule.editable && InspectorBackend.hasCommand("CSS.setRuleSelector"); } get locked() { return this._locked; } set locked(value) { this._locked = value; } variablesForType(type) { console.assert(Object.values(WI.CSSStyleDeclaration.VariablesGroupType).includes(type), type); let variables = this._variablesForType.get(type); if (variables) return variables; // 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.CSSStyleDeclaration.VariablesGroupType.Colors, checker: (property) => WI.Color.fromString(property.value), }, { type: WI.CSSStyleDeclaration.VariablesGroupType.Dimensions, checker: (property) => /^-?\d+(\.\d+)?\D+$/.test(property.value), }, { type: WI.CSSStyleDeclaration.VariablesGroupType.Numbers, checker: (property) => /^-?\d+(\.\d+)?$/.test(property.value), }, { type: WI.CSSStyleDeclaration.VariablesGroupType.Other, checker: (property) => true, }, ]; // Ensure all types have a list. Empty lists are a signal to views to skip rendering. for (let {type} of typeCheckFunctions) this._variablesForType.set(type, []); for (let property of this._properties) { if (!property.isVariable) continue; for (let {type, checker} of typeCheckFunctions) { if (checker(property)) { this._variablesForType.get(type).push(property); break; } } } return this._variablesForType.get(type); } update(text, properties, styleSheetTextRange, options = {}) { let dontFireEvents = options.dontFireEvents || false; // When two consequent setText calls happen (A and B), only update when the last call (B) is finished. // Front-end: A B // Back-end: A B // _updatesInProgressCount: 0 1 2 1 0 // ^ // update only happens here if (this._updatesInProgressCount > 0 && !options.forceUpdate) { if (WI.settings.debugEnableStyleEditingDebugMode.value && text !== this._text) console.warn("Style modified while editing:", text); return; } // Allow updates from the backend when text matches because `properties` may contain warnings that need to be shown. if (this._locked && !options.forceUpdate && text !== this._text) return; text = text || ""; properties = properties || []; let oldProperties = this._properties || []; let oldText = this._text; this._text = text; this._properties = properties; this._styleSheetTextRange = styleSheetTextRange; this._propertyNameMap = {}; this._variablesForType.clear(); this._enabledProperties = null; this._visibleProperties = null; let editable = this.editable; for (let property of this._properties) { property.ownerStyle = this; // Store the property in a map if we aren't editable. This // allows for quick lookup for computed style. Editable // styles don't use the map since they need to account for // overridden properties. if (!editable) this._propertyNameMap[property.name] = property; else { // Remove from pendingProperties (if it was pending). this._pendingProperties.remove(property); } } for (let oldProperty of oldProperties) { if (this._properties.includes(oldProperty)) continue; // Clear the index, since it is no longer valid. oldProperty.index = NaN; // Keep around old properties in pending in case they // are needed again during editing. if (editable) this._pendingProperties.push(oldProperty); } if (dontFireEvents) return; // Don't fire the event if text hasn't changed. However, it should still fire for Computed style declarations // because it never has text. if (oldText === this._text && !this._pendingPropertiesChanged && this._type !== WI.CSSStyleDeclaration.Type.Computed) return; this._pendingPropertiesChanged = false; function delayed() { this.dispatchEventToListeners(WI.CSSStyleDeclaration.Event.PropertiesChanged); } // Delay firing the PropertiesChanged event so DOMNodeStyles has a chance to mark overridden and associated properties. setTimeout(delayed.bind(this), 0); } get ownerRule() { return this._ownerRule; } set ownerRule(rule) { this._ownerRule = rule || null; } get text() { return this._text; } set text(text) { if (this._text === text) return; let trimmedText = text.trim(); if (this._text === trimmedText) return; if (!trimmedText.length || this._type === WI.CSSStyleDeclaration.Type.Inline) text = trimmedText; this._text = text; ++this._updatesInProgressCount; let timeoutId = setTimeout(() => { console.error("Timed out when setting style text:", text); styleTextDidChange(); }, 2000); let styleTextDidChange = () => { if (!timeoutId) return; clearTimeout(timeoutId); timeoutId = null; this._updatesInProgressCount = Math.max(0, this._updatesInProgressCount - 1); this._pendingPropertiesChanged = true; }; this._nodeStyles.changeStyleText(this, text, styleTextDidChange); } get enabledProperties() { if (!this._enabledProperties) this._enabledProperties = this._properties.filter((property) => property.enabled); return this._enabledProperties; } get properties() { return this._properties; } set properties(properties) { if (properties === this._properties) return; this._properties = properties; this._enabledProperties = null; this._visibleProperties = null; } get visibleProperties() { if (!this._visibleProperties) this._visibleProperties = this._properties.filter((property) => !!property.styleDeclarationTextRange); return this._visibleProperties; } get pendingProperties() { return this._pendingProperties; } get styleSheetTextRange() { return this._styleSheetTextRange; } get groupings() { if (this._ownerRule) return this._ownerRule.groupings; return []; } get selectorText() { if (this._ownerRule) return this._ownerRule.selectorText; return this._node.appropriateSelectorFor(true); } propertyForName(name) { console.assert(name); if (!name) return null; if (!this.editable) return this._propertyNameMap[name] || null; // Editable styles don't use the map since they need to // account for overridden properties. let bestMatchProperty = null; for (let property of this.enabledProperties) { if (property.canonicalName !== name && property.name !== name) continue; if (bestMatchProperty && !bestMatchProperty.overridden && property.overridden) continue; bestMatchProperty = property; } return bestMatchProperty; } resolveVariableValue(text) { const invalid = Symbol("invalid"); let checkTokens = (tokens) => { let startIndex = NaN; let openParenthesis = 0; for (let i = 0; i < tokens.length; i++) { let token = tokens[i]; if (token.value === "var" && token.type && token.type.includes("atom")) { if (isNaN(startIndex)) { startIndex = i; openParenthesis = 0; } continue; } if (isNaN(startIndex)) continue; if (token.value === "(") { ++openParenthesis; continue; } if (token.value === ")") { --openParenthesis; if (openParenthesis > 0) continue; let variableTokens = tokens.slice(startIndex, i + 1); startIndex = NaN; let variableNameIndex = variableTokens.findIndex((token) => WI.CSSProperty.isVariable(token.value) && /\bvariable-2\b/.test(token.type)); if (variableNameIndex === -1) continue; let variableProperty = this.propertyForName(variableTokens[variableNameIndex].value); if (variableProperty) return variableProperty.value.trim(); let fallbackStartIndex = variableTokens.findIndex((value, j) => j > variableNameIndex + 1 && /\bm-css\b/.test(value.type)); if (fallbackStartIndex === -1) return invalid; let fallbackTokens = variableTokens.slice(fallbackStartIndex, i); return checkTokens(fallbackTokens) || fallbackTokens.reduce((accumulator, token) => accumulator + token.value, "").trim(); } } return null; }; let resolved = checkTokens(WI.tokenizeCSSValue(text)); return resolved === invalid ? null : resolved; } newBlankProperty(propertyIndex) { let text, name, value, priority, overridden, implicit, anonymous; let enabled = true; let valid = false; let styleSheetTextRange = this._rangeAfterPropertyAtIndex(propertyIndex - 1); this.markModified(); let property = new WI.CSSProperty(propertyIndex, text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange); property.isNewProperty = true; this.insertProperty(property, propertyIndex); this.update(this._text, this._properties, this._styleSheetTextRange, {dontFireEvents: true, forceUpdate: true}); return property; } markModified() { if (!this._initialState) { let visibleProperties = this.visibleProperties.map((property) => { return property.clone(); }); this._initialState = new WI.CSSStyleDeclaration( this._nodeStyles, this._ownerStyleSheet, this._id, this._type, this._node, this._inherited, this._text, visibleProperties, this._styleSheetTextRange); } WI.cssManager.addModifiedStyle(this); } insertProperty(cssProperty, propertyIndex) { this._properties.insertAtIndex(cssProperty, propertyIndex); for (let index = propertyIndex + 1; index < this._properties.length; index++) this._properties[index].index = index; // Invalidate cached properties. this._enabledProperties = null; this._visibleProperties = null; } removeProperty(cssProperty) { // cssProperty.index could be set to NaN by WI.CSSStyleDeclaration.prototype.update. let realIndex = this._properties.indexOf(cssProperty); if (realIndex === -1) return; this._properties.splice(realIndex, 1); // Invalidate cached properties. this._enabledProperties = null; this._visibleProperties = null; } updatePropertiesModifiedState() { if (!this._initialState) return; if (this._type === WI.CSSStyleDeclaration.Type.Computed) return; let initialCSSProperties = this._initialState.visibleProperties; let cssProperties = this.visibleProperties; let hasModified = false; function onEach(cssProperty, action) { if (action !== 0) hasModified = true; cssProperty.modified = action === 1; } function comparator(a, b) { return a.equals(b); } Array.diffArrays(initialCSSProperties, cssProperties, onEach, comparator); if (!hasModified) WI.cssManager.removeModifiedStyle(this); } generateFormattedText(options = {}) { let indentString = WI.indentString(); let styleText = ""; let groupings = this.groupings.filter((grouping) => !grouping.isMedia || grouping.text !== "all"); let groupingsCount = groupings.length; if (options.includeGroupingsAndSelectors) { for (let i = groupingsCount - 1; i >= 0; --i) { if (options.multiline) styleText += indentString.repeat(groupingsCount - i - 1); let prefix = groupings[i].prefix; if (prefix) styleText += prefix; if (groupings[i].text) styleText += " " + groupings[i].text; styleText += " {"; if (options.multiline) styleText += "\n"; } if (options.multiline) styleText += indentString.repeat(groupingsCount); styleText += this.selectorText + " {"; } let properties = this._styleSheetTextRange ? this.visibleProperties : this._properties; if (properties.length) { if (options.multiline) { let propertyIndent = indentString.repeat(groupingsCount + 1); for (let property of properties) styleText += "\n" + propertyIndent + property.formattedText; styleText += "\n"; if (!options.includeGroupingsAndSelectors) { // Indent the closing "}" for nested rules. styleText += indentString.repeat(groupingsCount); } } else styleText += properties.map((property) => property.formattedText).join(" "); } if (options.includeGroupingsAndSelectors) { for (let i = groupingsCount; i > 0; --i) { if (options.multiline) styleText += indentString.repeat(i); styleText += "}"; if (options.multiline) styleText += "\n"; } styleText += "}"; } return styleText; } // Protected get nodeStyles() { return this._nodeStyles; } // Private _rangeAfterPropertyAtIndex(index) { if (index < 0) return this._styleSheetTextRange.collapseToStart(); if (index >= this.visibleProperties.length) return this._styleSheetTextRange.collapseToEnd(); let property = this.visibleProperties[index]; return property.styleSheetTextRange.collapseToEnd(); } }; WI.CSSStyleDeclaration.Event = { PropertiesChanged: "css-style-declaration-properties-changed", }; WI.CSSStyleDeclaration.Type = { Rule: "css-style-declaration-type-rule", Inline: "css-style-declaration-type-inline", Attribute: "css-style-declaration-type-attribute", Computed: "css-style-declaration-type-computed" }; WI.CSSStyleDeclaration.VariablesGroupType = { Ungrouped: "ungrouped", Colors: "colors", Dimensions: "dimensions", Numbers: "numbers", Other: "other", };