"use strict"; const { parse } = require('./style_parser'); module.exports = function (elt) { const style = new CSSStyleDeclaration(elt) const handler = { get: function(target, property) { return property in target ? target[property] : target.getPropertyValue(dasherizeProperty(property)); }, has: function(target, key) { return true; }, set: function(target, property, value) { if (property in target) { target[property] = value; } else { target.setProperty(dasherizeProperty(property), value ?? undefined); } return true; } }; return new Proxy(style, handler); }; function dasherizeProperty(property) { return property.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); } function CSSStyleDeclaration(elt) { this._element = elt; } const IMPORTANT_BANG = '!important'; // Utility function for parsing style declarations // Pass in a string like "margin-left: 5px; border-style: solid" // and this function returns an object like // {"margin-left":"5px", "border-style":"solid"} function parseStyles(value) { const result = { property: {}, priority: {}, } if (!value) { return result; } const styleValues = parse(value); if (styleValues.length < 2) { return result; } for (let i = 0; i < styleValues.length; i += 2) { const name = styleValues[i]; let value = styleValues[i+1]; if (value.endsWith(IMPORTANT_BANG)) { result.priority[name] = 'important'; value = value.slice(0, -IMPORTANT_BANG.length).trim(); } result.property[name] = value; } return result; } var NO_CHANGE = {}; // Private marker object CSSStyleDeclaration.prototype = Object.create(Object.prototype, { // Return the parsed form of the element's style attribute. // If the element's style attribute has never been parsed // or if it has changed since the last parse, then reparse it // Note that the styles don't get parsed until they're actually needed _parsed: { get: function() { if (!this._parsedStyles || this.cssText !== this._lastParsedText) { var text = this.cssText; this._parsedStyles = parseStyles(text); this._lastParsedText = text; delete this._names; } return this._parsedStyles; }}, // Call this method any time the parsed representation of the // style changes. It converts the style properties to a string and // sets cssText and the element's style attribute _serialize: { value: function() { var styles = this._parsed; var s = ""; for(var name in styles.property) { if (s) s += " "; s += name + ": " + styles.property[name]; if (styles.priority[name]) { s += " !" + styles.priority[name]; } s += ";"; } this.cssText = s; // also sets the style attribute this._lastParsedText = s; // so we don't reparse delete this._names; }}, cssText: { get: function() { // XXX: this is a CSSStyleDeclaration for an element. // A different impl might be necessary for a set of styles // associated returned by getComputedStyle(), e.g. return this._element.getAttribute("style"); }, set: function(value) { // XXX: I should parse and serialize the value to // normalize it and remove errors. FF and chrome do that. this._element.setAttribute("style", value); } }, length: { get: function() { if (!this._names) this._names = Object.getOwnPropertyNames(this._parsed.property); return this._names.length; }}, item: { value: function(n) { if (!this._names) this._names = Object.getOwnPropertyNames(this._parsed.property); return this._names[n]; }}, getPropertyValue: { value: function(property) { property = property.toLowerCase(); return this._parsed.property[property] || ""; }}, getPropertyPriority: { value: function(property) { property = property.toLowerCase(); return this._parsed.priority[property] || ""; }}, setProperty: { value: function(property, value, priority) { property = property.toLowerCase(); if (value === null || value === undefined) { value = ""; } if (priority === null || priority === undefined) { priority = ""; } // String coercion if (value !== NO_CHANGE) { value = "" + value; } value = value.trim(); if (value === "") { this.removeProperty(property); return; } if (priority !== "" && priority !== NO_CHANGE && !/^important$/i.test(priority)) { return; } var styles = this._parsed; if (value === NO_CHANGE) { if (!styles.property[property]) { return; // Not a valid property name. } if (priority !== "") { styles.priority[property] = "important"; } else { delete styles.priority[property]; } } else { // We don't just accept the property value. Instead // we parse it to ensure that it is something valid. // If it contains a semicolon it is invalid if (value.indexOf(";") !== -1) return; var newprops = parseStyles(property + ":" + value); if (Object.getOwnPropertyNames(newprops.property).length === 0) { return; // no valid property found } if (Object.getOwnPropertyNames(newprops.priority).length !== 0) { return; // if the value included '!important' it wasn't valid. } // XXX handle shorthand properties for (var p in newprops.property) { styles.property[p] = newprops.property[p]; if (priority === NO_CHANGE) { continue; } else if (priority !== "") { styles.priority[p] = "important"; } else if (styles.priority[p]) { delete styles.priority[p]; } } } // Serialize and update cssText and element.style! this._serialize(); }}, setPropertyValue: { value: function(property, value) { return this.setProperty(property, value, NO_CHANGE); }}, setPropertyPriority: { value: function(property, priority) { return this.setProperty(property, NO_CHANGE, priority); }}, removeProperty: { value: function(property) { property = property.toLowerCase(); var styles = this._parsed; if (property in styles.property) { delete styles.property[property]; delete styles.priority[property]; // Serialize and update cssText and element.style! this._serialize(); } }}, });