Skip to content

Commit

Permalink
Migrate CSSStyleDeclaration to WebIDL2JS
Browse files Browse the repository at this point in the history
Co-authored-by: Timothy Gu <[email protected]>
  • Loading branch information
ExE-Boss and TimothyGu committed Mar 27, 2021
1 parent b527ed7 commit 66283a6
Show file tree
Hide file tree
Showing 22 changed files with 431 additions and 140 deletions.
4 changes: 4 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
node_modules
lib/CSSStyleDeclaration.js
lib/Function.js
lib/VoidFunction.js
lib/implementedProperties.js
lib/properties.js
lib/utils.js
jest.config.js
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
node_modules
npm-debug.log
lib/CSSStyleDeclaration.js
lib/Function.js
lib/VoidFunction.js
lib/implementedProperties.js
lib/properties.js
lib/utils.js
coverage
src/CSSStyleDeclaration-properties.webidl
2 changes: 2 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/*
!lib/
lib/Function.js
lib/VoidFunction.js
!LICENSE
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ install:
- npm install
- npm install -g codecov
node_js:
- "8"
- "10"
- "12"

Expand Down
45 changes: 40 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,47 @@ A Node JS implementation of the CSS Object Model [CSSStyleDeclaration interface]

[![NpmVersion](https://img.shields.io/npm/v/cssstyle.svg)](https://www.npmjs.com/package/cssstyle) [![Build Status](https://travis-ci.org/jsdom/cssstyle.svg?branch=master)](https://travis-ci.org/jsdom/cssstyle) [![codecov](https://codecov.io/gh/jsdom/cssstyle/branch/master/graph/badge.svg)](https://codecov.io/gh/jsdom/cssstyle)

---
## Background

#### Background
This package is an extension of the CSSStyleDeclaration class in Nikita Vasilyev's [CSSOM](https://github.com/NV/CSSOM) with added support for CSS 2 & 3 properties. The primary use case is for testing browser code in a Node environment.

This package is an extension of the CSSStyleDeclaration class in Nikita Vasilyev's [CSSOM](https://github.com/NV/CSSOM) with added support for CSS 2 & 3 properties. The primary use case is for testing browser code in a Node environment.

It was originally created by Chad Walker, it is now maintained by the jsdom community.
It was originally created by Chad Walker, it is now maintained by Jon Sakas and other open source contributors.

Bug reports and pull requests are welcome.

## APIs

This package exposes two flavors of the `CSSStyleDeclaration` interface depending on the imported module.

### `cssstyle` module

This module default-exports the `CSSStyleDeclaration` interface constructor, with the change that it can be constructed with an optional `onChangeCallback` parameter. Whenever any CSS property is modified through an instance of this class, the callback (if provided) will be called with a string that represents all CSS properties of this element, serialized. This allows the embedding environment to properly reflect the style changes to an element's `style` attribute.

Here is a crude example of using the `onChangeCallback` to implement the `style` property of `HTMLElement`:
```js
const CSSStyleDeclaration = require('cssstyle');

class HTMLElement extends Element {
constructor() {
this._style = new CSSStyleDeclaration(newCSSText => {
this.setAttributeNS(null, "style", newCSSText);
});
}

get style() {
return this._style;
}

set style(text) {
this._style.cssText = text;
}
}
```

### `cssstyle/webidl2js-wrapper` module

This module exports the `CSSStyleDeclaration` [interface wrapper API](https://github.com/jsdom/webidl2js#for-interfaces) generated by [webidl2js](https://github.com/jsdom/webidl2js). Unlike the default export, `CSSStyleDeclaration` constructors installed by the webidl2js wrapper do _not_ support construction, just like how they actually are in browsers. Creating new `CSSStyleDeclaration` objects can be done with the [`create`](https://github.com/jsdom/webidl2js#createglobalobject-constructorargs-privatedata) method of the wrapper.

#### `privateData`

The `privateData` parameter of `create` and `createImpl` provides a way to specify the `onChangeCallback` that is a constructor parameter in the default export. Only the `onChangeCallback` property is supported on `privateData` currently, with the same semantics as the constructor parameter documented above.
34 changes: 34 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict';
const webidlWrapper = require('./webidl2js-wrapper.js');

const sharedGlobalObject = {};
webidlWrapper.install(sharedGlobalObject, ['Window']);

const origCSSStyleDeclaration = sharedGlobalObject.CSSStyleDeclaration;

/**
* @constructor
* @param {((cssText: string) => void) | null} [onChangeCallback]
* The callback that is invoked whenever a property changes.
*/
function CSSStyleDeclaration(onChangeCallback = null) {
if (new.target === undefined) {
throw new TypeError("Class constructor CSSStyleDeclaration cannot be invoked without 'new'");
}

if (onChangeCallback !== null && typeof onChangeCallback !== 'function') {
throw new TypeError('Failed to construct CSSStyleDeclaration: parameter 1 is not a function');
}

return webidlWrapper.create(sharedGlobalObject, undefined, { onChangeCallback });
}

sharedGlobalObject.CSSStyleDeclaration = CSSStyleDeclaration;
Object.defineProperty(CSSStyleDeclaration, 'prototype', {
value: origCSSStyleDeclaration.prototype,
writable: false,
});
CSSStyleDeclaration.prototype.constructor = CSSStyleDeclaration;
Object.setPrototypeOf(CSSStyleDeclaration, Object.getPrototypeOf(origCSSStyleDeclaration));

module.exports = CSSStyleDeclaration;
2 changes: 2 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ module.exports = {
"collectCoverage": true,
"collectCoverageFrom": [
"lib/**/*.js",
"!lib/CSSStyleDeclaration.js",
"!lib/implementedProperties.js",
"!lib/properties.js",
"!lib/utils.js",
],
"coverageDirectory": "coverage",
};
155 changes: 78 additions & 77 deletions lib/CSSStyleDeclaration.js → lib/CSSStyleDeclaration-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,28 @@ var CSSOM = require('cssom');
var allProperties = require('./allProperties');
var allExtraProperties = require('./allExtraProperties');
var implementedProperties = require('./implementedProperties');
var { dashedToCamelCase } = require('./parsers');
var { cssPropertyToIDLAttribute } = require('./parsers');
var getBasicPropertyDescriptor = require('./utils/getBasicPropertyDescriptor');
const idlUtils = require('./utils.js');

/**
* @constructor
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration
*/
var CSSStyleDeclaration = function CSSStyleDeclaration(onChangeCallback) {
this._values = {};
this._importants = {};
this._length = 0;
this._onChange =
onChangeCallback ||
function() {
return;
};
};
CSSStyleDeclaration.prototype = {
constructor: CSSStyleDeclaration,
class CSSStyleDeclarationImpl {
/**
* @constructor
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration
*
* @param {object} globalObject
* @param {*[]} args
* @param {object} privateData
* @param {((cssText: string) => void) | null} [privateData.onChangeCallback]
*/
constructor(globalObject, args, { onChangeCallback }) {
this._globalObject = globalObject;
this._values = Object.create(null);
this._importants = Object.create(null);
this._length = 0;
this._onChange = onChangeCallback || (() => {});
this.parentRule = null;
}

/**
*
Expand All @@ -34,25 +37,19 @@ CSSStyleDeclaration.prototype = {
* @return {string} the value of the property if it has been explicitly set for this declaration block.
* Returns the empty string if the property has not been set.
*/
getPropertyValue: function(name) {
if (!this._values.hasOwnProperty(name)) {
return '';
}
return this._values[name].toString();
},
getPropertyValue(name) {
return this._values[name] || '';
}

/**
*
* @param {string} name
* @param {string} value
* @param {string} [priority=null] "important" or null
* @param {string} [priority=""] "important" or ""
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-setProperty
*/
setProperty: function(name, value, priority) {
if (value === undefined) {
return;
}
if (value === null || value === '') {
setProperty(name, value, priority = '') {
if (value === '') {
this.removeProperty(name);
return;
}
Expand All @@ -68,8 +65,16 @@ CSSStyleDeclaration.prototype = {

this[lowercaseName] = value;
this._importants[lowercaseName] = priority;
},
_setProperty: function(name, value, priority) {
}

/**
* @param {string} name
* @param {string | null} value
* @param {string} [priority=""]
*/
_setProperty(name, value, priority = '') {
// FIXME: A good chunk of the implemented properties call this method
// with `value = undefined`, expecting it to do nothing:
if (value === undefined) {
return;
}
Expand All @@ -92,7 +97,7 @@ CSSStyleDeclaration.prototype = {
this._values[name] = value;
this._importants[name] = priority;
this._onChange(this.cssText);
},
}

/**
*
Expand All @@ -101,8 +106,8 @@ CSSStyleDeclaration.prototype = {
* @return {string} the value of the property if it has been explicitly set for this declaration block.
* Returns the empty string if the property has not been set or the property name does not correspond to a known CSS property.
*/
removeProperty: function(name) {
if (!this._values.hasOwnProperty(name)) {
removeProperty(name) {
if (!idlUtils.hasOwn(this._values, name)) {
return '';
}

Expand All @@ -123,49 +128,36 @@ CSSStyleDeclaration.prototype = {

this._onChange(this.cssText);
return prevValue;
},
}

/**
*
* @param {String} name
*/
getPropertyPriority: function(name) {
getPropertyPriority(name) {
return this._importants[name] || '';
},

getPropertyCSSValue: function() {
//FIXME
return;
},

/**
* element.style.overflow = "auto"
* element.style.getPropertyShorthand("overflow-x")
* -> "overflow"
*/
getPropertyShorthand: function() {
//FIXME
return;
},

isPropertyImplicit: function() {
//FIXME
return;
},
}

/**
* http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-item
*/
item: function(index) {
index = parseInt(index, 10);
item(index) {
if (index < 0 || index >= this._length) {
return '';
}
return this[index];
},
};
}

[idlUtils.supportsPropertyIndex](index) {
return index >= 0 && index < this._length;
}

Object.defineProperties(CSSStyleDeclaration.prototype, {
[idlUtils.supportedPropertyIndices]() {
return Array.prototype.keys.call(this);
}
}

Object.defineProperties(CSSStyleDeclarationImpl.prototype, {
cssText: {
get: function() {
var properties = [];
Expand All @@ -178,9 +170,9 @@ Object.defineProperties(CSSStyleDeclaration.prototype, {
value = this.getPropertyValue(name);
priority = this.getPropertyPriority(name);
if (priority !== '') {
priority = ' !' + priority;
priority = ` !${priority}`;
}
properties.push([name, ': ', value, priority, ';'].join(''));
properties.push(`${name}: ${value}${priority};`);
}
return properties.join(' ');
},
Expand Down Expand Up @@ -211,13 +203,6 @@ Object.defineProperties(CSSStyleDeclaration.prototype, {
enumerable: true,
configurable: true,
},
parentRule: {
get: function() {
return null;
},
enumerable: true,
configurable: true,
},
length: {
get: function() {
return this._length;
Expand All @@ -239,22 +224,38 @@ Object.defineProperties(CSSStyleDeclaration.prototype, {
},
});

require('./properties')(CSSStyleDeclaration.prototype);
require('./properties')(CSSStyleDeclarationImpl.prototype);

// TODO: Consider using `[Reflect]` for basic properties
allProperties.forEach(function(property) {
if (!implementedProperties.has(property)) {
var declaration = getBasicPropertyDescriptor(property);
Object.defineProperty(CSSStyleDeclaration.prototype, property, declaration);
Object.defineProperty(CSSStyleDeclaration.prototype, dashedToCamelCase(property), declaration);
Object.defineProperty(CSSStyleDeclarationImpl.prototype, property, declaration);
Object.defineProperty(
CSSStyleDeclarationImpl.prototype,
cssPropertyToIDLAttribute(property),
declaration
);
}
});

allExtraProperties.forEach(function(property) {
if (!implementedProperties.has(property)) {
var declaration = getBasicPropertyDescriptor(property);
Object.defineProperty(CSSStyleDeclaration.prototype, property, declaration);
Object.defineProperty(CSSStyleDeclaration.prototype, dashedToCamelCase(property), declaration);
Object.defineProperty(CSSStyleDeclarationImpl.prototype, property, declaration);
Object.defineProperty(
CSSStyleDeclarationImpl.prototype,
cssPropertyToIDLAttribute(property),
declaration
);
if (property.startsWith('-webkit-')) {
Object.defineProperty(
CSSStyleDeclarationImpl.prototype,
cssPropertyToIDLAttribute(property, /* lowercaseFirst = */ true),
declaration
);
}
}
});

exports.CSSStyleDeclaration = CSSStyleDeclaration;
exports.implementation = CSSStyleDeclarationImpl;
Loading

0 comments on commit 66283a6

Please sign in to comment.