css-typed-om-polyfill
Version:
A JavaScript polyfill for the CSS Typed Object Model (Level 1)
240 lines (186 loc) • 10.7 kB
Markdown
# CSS Typed OM Polyfill
A JavaScript polyfill for the [CSS Typed Object Model (OM) Level 1](https://www.w3.org/TR/css-typed-om/) specification. This polyfill aims to provide a functional subset of the CSS Typed OM API for browsers that do not natively support it, allowing developers to interact with CSS values as typed JavaScript objects.
## Table of Contents
- [Introduction](#introduction)
- [Features](#features)
- [Supported CSS Typed OM Interfaces](#supported-css-typed-om-interfaces)
- [Installation](#installation)
- [Basic Usage](#basic-usage)
- [Advanced Usage & Notes](#advanced-usage--notes)
- [Parsing CSS Values](#parsing-css-values)
- [`calc()` and Math Functions](#calc-and-math-functions)
- [Arithmetic Operations](#arithmetic-operations)
- [`var()` (CSS Variables)](#var-css-variables)
- [Limitations & To-Do](#limitations--to-do)
- [Development](#development)
- [License](#license)
## Introduction
The CSS Typed Object Model provides a way to manipulate CSS values in JavaScript with type awareness, rather than just dealing with strings. This can lead to more robust and performant code. This polyfill brings some of that power to environments lacking native support.
It focuses on parsing CSS strings into typed objects, performing arithmetic on numeric values, and providing the `element.attributeStyleMap` interface for getting and setting styles in a typed manner.
## Features
* **`HTMLElement.prototype.attributeStyleMap`:** Access and modify inline styles using a `StylePropertyMap`.
* **CSS Value Parsing:**
* `CSSStyleValue.parse(property, cssText)`: Parses a single CSS value string.
* `CSSStyleValue.parseAll(property, cssText)`: Parses a comma-separated list of CSS values.
* **Numeric Values:**
* `CSSNumericValue` (base class for numbers and units).
* `CSSUnitValue` (e.g., `10px`, `50%`, `3.14rad`).
* `CSS.*` factory functions (e.g., `CSS.px(10)`, `CSS.percent(50)`, `CSS.number(5)`).
* Arithmetic operations: `.add()`, `.sub()`, `.mul()`, `.div()`, `.negate()`, `.invert()`.
* Unit conversion: `.to('targetUnit')` for compatible types (e.g., `s` to `ms`).
* Type checking for math operations (e.g., `length + angle` is invalid).
* **Mathematical Expressions:**
* `CSSMathSum`, `CSSMathProduct`, `CSSMathNegate`, `CSSMathInvert`.
* `CSSMathMin`, `CSSMathMax`.
* Recursive parsing of `calc()`, `min()`, `max()` expressions, including nested functions and `var()`.
* **Other CSS Values:**
* `CSSKeywordValue` (e.g., `auto`, `inherit`, `initial`).
* `CSSVariableReferenceValue` (for `var(--custom-property)`).
* `CSSUnparsedValue` (for values not fully parsed or unsupported).
* **Stubbed/Partial Implementations:**
* `CSSImageValue` (basic stub, returns `url(...)` as string).
* `CSSPositionValue` (basic stub).
* `CSSTransformValue` and its components (`CSSTranslate`, `CSSRotate`, `CSSScale`) (basic stubs with constructors and `toString()`).
## Supported CSS Typed OM Interfaces
**Core Classes:**
* `CSSStyleValue` (Abstract Base)
* `CSSNumericValue` (Abstract Base for numeric types)
* `CSSUnitValue`
* `CSSKeywordValue`
* `CSSUnparsedValue`
* `CSSVariableReferenceValue`
**Math Classes (subclass `CSSNumericValue`):**
* `CSSMathValue` (Abstract Base)
* `CSSMathSum`
* `CSSMathProduct`
* `CSSMathNegate`
* `CSSMathInvert`
* `CSSMathMin`
* `CSSMathMax`
**Other (Stubbed/Partial):**
* `CSSImageValue`
* `CSSPositionValue`
* `CSSTransformValue`
* `CSSTransformComponent` (Abstract Base)
* `CSSTranslate`
* `CSSRotate`
* `CSSScale`
**Interfaces:**
* `StylePropertyMap` (via `element.attributeStyleMap`)
## Installation
Simply include the polyfill script in your HTML file before any scripts that use the CSS Typed OM API:
```html
<script src="css-typed-om-polyfill.js"></script>
<script>
// Your code using Typed OM
const el = document.getElementById('myElement');
if (el && el.attributeStyleMap) {
el.attributeStyleMap.set('width', CSS.px(100));
console.log(el.attributeStyleMap.get('width').toString()); // "100px"
}
</script>
```
The polyfill will automatically initialize and attach necessary properties to the global scope (`window`) and `HTMLElement.prototype`.
## Basic Usage
### Accessing `attributeStyleMap`
```javascript
const element = document.createElement('div');
document.body.appendChild(element);
// Set a style
element.attributeStyleMap.set('width', CSS.px(200));
element.attributeStyleMap.set('opacity', CSS.number(0.5));
element.attributeStyleMap.set('margin-top', CSS.em(1.5));
// Get a style
const width = element.attributeStyleMap.get('width'); // CSSUnitValue { value: 200, unit: "px" }
console.log(width.value); // 200
console.log(width.unit); // "px"
console.log(width.toString()); // "200px"
const opacity = element.attributeStyleMap.get('opacity'); // CSSNumericValue { _value: 0.5, _unitType: "number" }
console.log(opacity.toString()); // "0.5"
```
### Using `CSS.*` Factory Functions
```javascript
const length = CSS.px(100);
const percentage = CSS.percent(50);
const duration = CSS.s(2.5);
const angle = CSS.deg(90);
const number = CSS.number(10);
console.log(length.add(CSS.vw(10)).toString()); // "calc(100px + 10vw)"
```
## Advanced Usage & Notes
### Parsing CSS Values
```javascript
// Parse a single value
const parsedWidth = CSSStyleValue.parse('width', 'calc(100% - 20px)');
console.log(parsedWidth.toString()); // "calc(100% - 20px)"
if (parsedWidth instanceof CSSMathSum) {
console.log(parsedWidth.operator); // "sum"
console.log(parsedWidth.values[0].toString()); // "100%"
console.log(parsedWidth.values[1].toString()); // "calc(-1 * 20px)" (negated second term)
}
const keyword = CSSStyleValue.parse('display', 'block');
console.log(keyword.value); // "block"
// Parse a list of values (e.g., for font-family)
const fontFamilies = CSSStyleValue.parseAll('font-family', '"Arial", sans-serif');
fontFamilies.forEach(font => console.log(font.toString()));
// Output:
// "Arial" (CSSUnparsedValue for the string)
// "sans-serif" (CSSKeywordValue)
```
### `calc()` and Math Functions
The polyfill includes a robust parser for `calc()`, `min()`, and `max()` expressions, respecting operator precedence and parentheses.
```javascript
const complexCalc = CSSStyleValue.parse('width', 'calc( (var(--A) + 2 * ( 5vw - var(--B) )) / 3 )');
console.log(complexCalc.toString()); // "calc((var(--A) + 2 * (5vw - var(--B))) / 3)"
// The internal structure will be a tree of CSSMathProduct, CSSMathSum, etc.
```
### Arithmetic Operations
`CSSNumericValue` and its subclasses support arithmetic. Operations involving different compatible units or `var()` will result in `CSSMathSum` or `CSSMathProduct` objects.
```javascript
const val1 = CSS.px(10);
const val2 = CSS.em(2);
const val3 = CSS.percent(50);
const num = CSS.number(5);
// Addition
console.log(val1.add(val2).toString()); // "calc(10px + 2em)"
console.log(val1.add(CSS.px(5)).toString()); // "15px" (same unit)
// Subtraction
console.log(val3.sub(CSS.px(10)).toString()); // "calc(50% - 10px)"
// Multiplication
console.log(val1.mul(num).toString()); // "50px" (length * number = length)
console.log(val1.mul(CSS.percent(200)).toString()); // "calc(10px * 200%)" (length * percent)
// Division
console.log(val1.div(num).toString()); // "2px"
console.log(val1.div(CSS.px(2)).toString()); // "5" (length / length = number)
```
### `var()` (CSS Variables)
`var()` expressions are parsed into `CSSVariableReferenceValue` objects. They can be used within math functions and arithmetic operations, remaining unresolved.
```javascript
const myVar = CSSStyleValue.parse('width', 'var(--my-width, 100px)');
console.log(myVar.variable); // "--my-width"
console.log(myVar.fallback.toString()); // "100px"
const calcWithVar = CSS.px(10).add(myVar);
console.log(calcWithVar.toString()); // "calc(10px + var(--my-width, 100px))"
```
## Limitations & To-Do
This polyfill is a work in progress and does not cover the entire CSS Typed OM specification. Key limitations include:
* **`clamp()`:** Not currently supported in the math expression parser (will parse as `CSSUnparsedValue` if top-level, or throw error if inside `calc`).
* **Color Values:** Complex color functions (e.g., `rgb()`, `hsl()`, `color()`) are generally parsed as `CSSUnparsedValue`. Basic named colors might parse as `CSSKeywordValue`.
* **Detailed Transform Parsing:** While `CSSTransformValue` and component classes (`CSSTranslate`, `CSSRotate`, `CSSScale`) exist with constructors, the main `CSSStyleValue.parse` will return `CSSUnparsedValue` for transform function strings. You'd need to construct them manually.
* **`CSSNumericValue.equals()`:** Provides a simplified structural check, primarily via `toString()` comparison, which may not be robust for all mathematically equivalent but structurally different expressions.
* **Type System Nuances:** The `type()` method on math values attempts to determine the resulting type (e.g., `length`, `angle`, `percent`), but complex interactions, especially with `var()`, can lead to indeterminate types.
* **No Actual Value Computation:** The polyfill parses and represents CSS values and expressions. It does **not** compute the final used value of expressions (e.g., it won't resolve `calc(10px + 5%)` to a single pixel value, as that requires layout context).
* **Shorthand Properties:** `StylePropertyMap.get()` might return `null` or an unparsed value for shorthands if the browser doesn't expand them into longhands in `element.style`. The polyfill relies on what `element.style.getPropertyValue()` returns.
* **Limited Property-Specific Parsing:** `CSSStyleValue.parse(property, cssText)` does not currently use the `property` argument to guide parsing specific to that property's grammar (beyond basic distinctions).
**Future Enhancements (To-Do):**
* Implement `clamp()`.
* Improve parsing for color values.
* Full parsing of transform functions into `CSSTransformValue`.
* More robust `CSSNumericValue.equals()`.
* Support for more `CSSStyleValue` subclasses (e.g., `CSSSkew`, `CSSPerspective`, `CSSMatrixComponent`).
* Consider `CSSMathClamp`.
## Development
The code is contained within a single IIFE (Immediately Invoked Function Expression). The core parsing logic for math expressions is in `parseCssMathExpression`. `CSSStyleValue.parse` is the main entry point for parsing CSS text.
The script includes example usage code wrapped in a `setTimeout` at the end, which can be used for testing in a browser environment.
## License
[MIT](LICENSE)