| /* |
| * 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.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(); |
| |
| return false; |
| } |
| |
| get selectorEditable() |
| { |
| return this._ownerRule && this._ownerRule.editable; |
| } |
| |
| get locked() { return this._locked; } |
| set locked(value) { this._locked = value; } |
| |
| 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.isDebugUIEnabled() && 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._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.enabledProperties.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, dontCreateIfMissing) |
| { |
| 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. |
| |
| function findMatch(properties) |
| { |
| for (var i = 0; i < properties.length; ++i) { |
| var property = properties[i]; |
| if (property.canonicalName !== name && property.name !== name) |
| continue; |
| if (bestMatchProperty && !bestMatchProperty.overridden && property.overridden) |
| continue; |
| bestMatchProperty = property; |
| } |
| } |
| |
| var bestMatchProperty = null; |
| |
| findMatch(this.enabledProperties); |
| |
| if (bestMatchProperty) |
| return bestMatchProperty; |
| |
| if (dontCreateIfMissing || !this.editable) |
| return null; |
| |
| findMatch(this._pendingProperties, true); |
| |
| if (bestMatchProperty) |
| return bestMatchProperty; |
| |
| var newProperty = new WI.CSSProperty(NaN, null, name); |
| newProperty.ownerStyle = this; |
| |
| this._pendingProperties.push(newProperty); |
| |
| return newProperty; |
| } |
| |
| 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) => token.value.startsWith("--") && /\bvariable-2\b/.test(token.type)); |
| if (variableNameIndex === -1) |
| continue; |
| |
| let variableProperty = this.propertyForName(variableTokens[variableNameIndex].value, true); |
| 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); |
| |
| this._properties.insertAtIndex(property, propertyIndex); |
| for (let index = propertyIndex + 1; index < this._properties.length; index++) |
| this._properties[index].index = index; |
| |
| 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); |
| } |
| |
| shiftPropertiesAfter(cssProperty, lineDelta, columnDelta, propertyWasRemoved) |
| { |
| // cssProperty.index could be set to NaN by WI.CSSStyleDeclaration.prototype.update. |
| let realIndex = this._properties.indexOf(cssProperty); |
| if (realIndex === -1) |
| return; |
| |
| let endLine = cssProperty.styleSheetTextRange.endLine; |
| |
| for (let i = realIndex + 1; i < this._properties.length; i++) { |
| let property = this._properties[i]; |
| |
| if (property._styleSheetTextRange) { |
| if (property.styleSheetTextRange.startLine === endLine) { |
| // Only update column data if it's on the same line. |
| property._styleSheetTextRange = property._styleSheetTextRange.cloneAndModify(lineDelta, columnDelta, lineDelta, columnDelta); |
| } else |
| property._styleSheetTextRange = property._styleSheetTextRange.cloneAndModify(lineDelta, 0, lineDelta, 0); |
| } |
| |
| if (propertyWasRemoved && !isNaN(property._index)) |
| property._index--; |
| } |
| |
| if (propertyWasRemoved) |
| 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); |
| } |
| |
| generateCSSRuleString() |
| { |
| let indentString = WI.indentString(); |
| let styleText = ""; |
| let groupings = this.groupings.filter((grouping) => grouping.text !== "all"); |
| let groupingsCount = groupings.length; |
| for (let i = groupingsCount - 1; i >= 0; --i) |
| styleText += indentString.repeat(groupingsCount - i - 1) + groupings[i].prefix + " " + groupings[i].text + " {\n"; |
| |
| styleText += indentString.repeat(groupingsCount) + this.selectorText + " {\n"; |
| |
| for (let property of (this._styleSheetTextRange ? this.visibleProperties : this._properties)) |
| styleText += indentString.repeat(groupingsCount + 1) + property.formattedText + "\n"; |
| |
| for (let i = groupingsCount; i > 0; --i) |
| styleText += indentString.repeat(i) + "}\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" |
| }; |