Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate CSSStyleDeclaration to WebIDL2JS #116

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
4 changes: 4 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/*
!index.js
!webidl2js-wrapper.js
!lib/
lib/Function.js
lib/VoidFunction.js
!LICENSE
41 changes: 38 additions & 3 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=main)](https://travis-ci.org/jsdom/cssstyle) [![codecov](https://codecov.io/gh/jsdom/cssstyle/branch/main/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.

It was originally created by Chad Walker, it is now maintained by the jsdom community.

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 = { Object, String, Number, Array, TypeError };
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) {
ExE-Boss marked this conversation as resolved.
Show resolved Hide resolved
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;
4 changes: 4 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ module.exports = {
"collectCoverage": true,
"collectCoverageFrom": [
"lib/**/*.js",
"!lib/CSSStyleDeclaration.js",
"!lib/Function.js",
"!lib/VoidFunction.js",
"!lib/implementedProperties.js",
"!lib/properties.js",
"!lib/utils.js",
],
"coverageDirectory": "coverage",
};
236 changes: 236 additions & 0 deletions lib/CSSStyleDeclaration-impl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
/*********************************************************************
* This is a fork from the CSS Style Declaration part of
* https://github.com/NV/CSSOM
********************************************************************/
'use strict';
const CSSOM = require('rrweb-cssom');
const allProperties = require('./allProperties');
const allExtraProperties = require('./allExtraProperties');
const implementedProperties = require('./implementedProperties');
const idlUtils = require('./utils.js');

class CSSStyleDeclarationImpl {
/**
* @constructor
* @see https://drafts.csswg.org/cssom/#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._list = [];
this._onChange = onChangeCallback;
this._setInProgress = false;
this.parentRule = null;
}

/**
* @see https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-csstext
*/
get cssText() {
const { _list } = this;
const properties = [];
for (let i = 0; i < _list.length; i++) {
const name = _list[i];
const value = this.getPropertyValue(name);
let priority = this.getPropertyPriority(name);
if (priority !== '') {
priority = ` !${priority}`;
}
properties.push(`${name}: ${value}${priority};`);
}
return properties.join(' ');
}

set cssText(value) {
this._values = Object.create(null);
this._importants = Object.create(null);
this._list = [];
let dummyRule;
try {
dummyRule = CSSOM.parse('#bogus{' + value + '}').cssRules[0].style;
} catch (err) {
// malformed css, just return
return;
}
this._setInProgress = true;
const rule_length = dummyRule.length;
for (let i = 0; i < rule_length; ++i) {
const name = dummyRule[i];
this.setProperty(
dummyRule[i],
dummyRule.getPropertyValue(name),
dummyRule.getPropertyPriority(name)
);
}
this._setInProgress = false;
if (this._onChange) {
this._onChange(this.cssText);
}
}

/**
* @see https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-length
*/
get length() {
return this._list.length;
}

/**
* This deletes indices if the new length is less then the current
* length. If the new length is more, it does nothing, the new indices
* will be undefined until set.
**/
set length(value) {
this._list.length = value;
}

/**
*
* @param {string} name
* @see https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-getpropertyvalue
* @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(name) {
return this._values[name] || '';
}

/**
*
* @param {string} name
* @param {string} value
* @param {string} [priority=""] "important" or ""
* @see https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty
*/
setProperty(name, value, priority = '') {
if (value === '') {
this.removeProperty(name);
return;
}

if (name.startsWith('--')) {
this._setProperty(name, value, priority);
return;
}

const lowercaseName = name.toLowerCase();
if (!allProperties.has(lowercaseName) && !allExtraProperties.has(lowercaseName)) {
return;
}

if (implementedProperties.has(lowercaseName)) {
this[lowercaseName] = value;
} else {
this._setProperty(lowercaseName, value, priority);
}
this._importants[lowercaseName] = 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;
}
if (value === null || value === '') {
this.removeProperty(name);
return;
}

let originalText;
if (this._onChange) {
originalText = this.cssText;
}

if (this._values[name]) {
// Property already exist. Overwrite it.
if (!this._list.includes(name)) {
this._list.push(name);
}
} else {
// New property.
this._list.push(name);
}
this._values[name] = value;
this._importants[name] = priority;
if (this._onChange && this.cssText !== originalText && !this._setInProgress) {
this._onChange(this.cssText);
}
}

/**
*
* @param {string} name
* @see https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-removeproperty
* @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(name) {
if (!idlUtils.hasOwn(this._values, name)) {
return '';
}

const prevValue = this._values[name];
delete this._values[name];
delete this._importants[name];

const index = this._list.indexOf(name);
if (index < 0) {
return prevValue;
}

// That's what WebKit and Opera do
this._list.splice(index, 1);

// That's what Firefox does
//this._list[index] = ''

if (this._onChange) {
this._onChange(this.cssText);
}
return prevValue;
}

/**
*
* @param {String} name
* @see https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-getpropertypriority
*/
getPropertyPriority(name) {
return this._importants[name] || '';
}

/**
* @see https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-item
*/
item(index) {
const { _list } = this;
if (index < 0 || index >= _list.length) {
return '';
}
return _list[index];
}

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

[idlUtils.supportedPropertyIndices]() {
return this._list.keys();
}
}

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

exports.implementation = CSSStyleDeclarationImpl;
Loading