37a9f582beaeaf22484e2445092e6592f859a5880aef7059acbcddc89249830a59b3170d2bf7170bd90600ef9ce1cbe22f44d9a197ef62770db8635e680700 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. 'use strict';
  2. var csstree = require('css-tree'),
  3. csstools = require('../css-tools');
  4. var CSSStyleDeclaration = function(node) {
  5. this.parentNode = node;
  6. this.properties = new Map();
  7. this.hasSynced = false;
  8. this.styleAttr = null;
  9. this.styleValue = null;
  10. this.parseError = false;
  11. };
  12. /**
  13. * Performs a deep clone of this object.
  14. *
  15. * @param parentNode the parentNode to assign to the cloned result
  16. */
  17. CSSStyleDeclaration.prototype.clone = function(parentNode) {
  18. var node = this;
  19. var nodeData = {};
  20. Object.keys(node).forEach(function(key) {
  21. if (key !== 'parentNode') {
  22. nodeData[key] = node[key];
  23. }
  24. });
  25. // Deep-clone node data.
  26. nodeData = JSON.parse(JSON.stringify(nodeData));
  27. var clone = new CSSStyleDeclaration(parentNode);
  28. Object.assign(clone, nodeData);
  29. return clone;
  30. };
  31. CSSStyleDeclaration.prototype.hasStyle = function() {
  32. this.addStyleHandler();
  33. };
  34. // attr.style
  35. CSSStyleDeclaration.prototype.addStyleHandler = function() {
  36. this.styleAttr = { // empty style attr
  37. 'name': 'style',
  38. 'value': null
  39. };
  40. Object.defineProperty(this.parentNode.attrs, 'style', {
  41. get: this.getStyleAttr.bind(this),
  42. set: this.setStyleAttr.bind(this),
  43. enumerable: true,
  44. configurable: true
  45. });
  46. this.addStyleValueHandler();
  47. };
  48. // attr.style.value
  49. CSSStyleDeclaration.prototype.addStyleValueHandler = function() {
  50. Object.defineProperty(this.styleAttr, 'value', {
  51. get: this.getStyleValue.bind(this),
  52. set: this.setStyleValue.bind(this),
  53. enumerable: true,
  54. configurable: true
  55. });
  56. };
  57. CSSStyleDeclaration.prototype.getStyleAttr = function() {
  58. return this.styleAttr;
  59. };
  60. CSSStyleDeclaration.prototype.setStyleAttr = function(newStyleAttr) {
  61. this.setStyleValue(newStyleAttr.value); // must before applying value handler!
  62. this.styleAttr = newStyleAttr;
  63. this.addStyleValueHandler();
  64. this.hasSynced = false; // raw css changed
  65. };
  66. CSSStyleDeclaration.prototype.getStyleValue = function() {
  67. return this.getCssText();
  68. };
  69. CSSStyleDeclaration.prototype.setStyleValue = function(newValue) {
  70. this.properties.clear(); // reset all existing properties
  71. this.styleValue = newValue;
  72. this.hasSynced = false; // raw css changed
  73. };
  74. CSSStyleDeclaration.prototype._loadCssText = function() {
  75. if (this.hasSynced) {
  76. return;
  77. }
  78. this.hasSynced = true; // must be set here to prevent loop in setProperty(...)
  79. if (!this.styleValue || this.styleValue.length === 0) {
  80. return;
  81. }
  82. var inlineCssStr = this.styleValue;
  83. var declarations = {};
  84. try {
  85. declarations = csstree.parse(inlineCssStr, {
  86. context: 'declarationList',
  87. parseValue: false
  88. });
  89. } catch (parseError) {
  90. this.parseError = parseError;
  91. return;
  92. }
  93. this.parseError = false;
  94. var self = this;
  95. declarations.children.each(function(declaration) {
  96. try {
  97. var styleDeclaration = csstools.csstreeToStyleDeclaration(declaration);
  98. self.setProperty(styleDeclaration.name, styleDeclaration.value, styleDeclaration.priority);
  99. } catch(styleError) {
  100. if(styleError.message !== 'Unknown node type: undefined') {
  101. self.parseError = styleError;
  102. }
  103. }
  104. });
  105. };
  106. // only reads from properties
  107. /**
  108. * Get the textual representation of the declaration block (equivalent to .cssText attribute).
  109. *
  110. * @return {String} Textual representation of the declaration block (empty string for no properties)
  111. */
  112. CSSStyleDeclaration.prototype.getCssText = function() {
  113. var properties = this.getProperties();
  114. if (this.parseError) {
  115. // in case of a parse error, pass through original styles
  116. return this.styleValue;
  117. }
  118. var cssText = [];
  119. properties.forEach(function(property, propertyName) {
  120. var strImportant = property.priority === 'important' ? '!important' : '';
  121. cssText.push(propertyName.trim() + ':' + property.value.trim() + strImportant);
  122. });
  123. return cssText.join(';');
  124. };
  125. CSSStyleDeclaration.prototype._handleParseError = function() {
  126. if (this.parseError) {
  127. console.warn('Warning: Parse error when parsing inline styles, style properties of this element cannot be used. The raw styles can still be get/set using .attr(\'style\').value. Error details: ' + this.parseError);
  128. }
  129. };
  130. CSSStyleDeclaration.prototype._getProperty = function(propertyName) {
  131. if(typeof propertyName === 'undefined') {
  132. throw Error('1 argument required, but only 0 present.');
  133. }
  134. var properties = this.getProperties();
  135. this._handleParseError();
  136. var property = properties.get(propertyName.trim());
  137. return property;
  138. };
  139. /**
  140. * Return the optional priority, "important".
  141. *
  142. * @param {String} propertyName representing the property name to be checked.
  143. * @return {String} priority that represents the priority (e.g. "important") if one exists. If none exists, returns the empty string.
  144. */
  145. CSSStyleDeclaration.prototype.getPropertyPriority = function(propertyName) {
  146. var property = this._getProperty(propertyName);
  147. return property ? property.priority : '';
  148. };
  149. /**
  150. * Return the property value given a property name.
  151. *
  152. * @param {String} propertyName representing the property name to be checked.
  153. * @return {String} value containing the value of the property. If not set, returns the empty string.
  154. */
  155. CSSStyleDeclaration.prototype.getPropertyValue = function(propertyName) {
  156. var property = this._getProperty(propertyName);
  157. return property ? property.value : null;
  158. };
  159. /**
  160. * Return a property name.
  161. *
  162. * @param {Number} index of the node to be fetched. The index is zero-based.
  163. * @return {String} propertyName that is the name of the CSS property at the specified index.
  164. */
  165. CSSStyleDeclaration.prototype.item = function(index) {
  166. if(typeof index === 'undefined') {
  167. throw Error('1 argument required, but only 0 present.');
  168. }
  169. var properties = this.getProperties();
  170. this._handleParseError();
  171. return Array.from(properties.keys())[index];
  172. };
  173. /**
  174. * Return all properties of the node.
  175. *
  176. * @return {Map} properties that is a Map with propertyName as key and property (propertyValue + propertyPriority) as value.
  177. */
  178. CSSStyleDeclaration.prototype.getProperties = function() {
  179. this._loadCssText();
  180. return this.properties;
  181. };
  182. // writes to properties
  183. /**
  184. * Remove a property from the CSS declaration block.
  185. *
  186. * @param {String} propertyName representing the property name to be removed.
  187. * @return {String} oldValue equal to the value of the CSS property before it was removed.
  188. */
  189. CSSStyleDeclaration.prototype.removeProperty = function(propertyName) {
  190. if(typeof propertyName === 'undefined') {
  191. throw Error('1 argument required, but only 0 present.');
  192. }
  193. this.hasStyle();
  194. var properties = this.getProperties();
  195. this._handleParseError();
  196. var oldValue = this.getPropertyValue(propertyName);
  197. properties.delete(propertyName.trim());
  198. return oldValue;
  199. };
  200. /**
  201. * Modify an existing CSS property or creates a new CSS property in the declaration block.
  202. *
  203. * @param {String} propertyName representing the CSS property name to be modified.
  204. * @param {String} [value] containing the new property value. If not specified, treated as the empty string. value must not contain "!important" -- that should be set using the priority parameter.
  205. * @param {String} [priority] allowing the "important" CSS priority to be set. If not specified, treated as the empty string.
  206. * @return {undefined}
  207. */
  208. CSSStyleDeclaration.prototype.setProperty = function(propertyName, value, priority) {
  209. if(typeof propertyName === 'undefined') {
  210. throw Error('propertyName argument required, but only not present.');
  211. }
  212. this.hasStyle();
  213. var properties = this.getProperties();
  214. this._handleParseError();
  215. var property = {
  216. value: value.trim(),
  217. priority: priority.trim()
  218. };
  219. properties.set(propertyName.trim(), property);
  220. return property;
  221. };
  222. module.exports = CSSStyleDeclaration;