@gjsify/dom-elements 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/lib/esm/dom-matrix.js +124 -0
  2. package/lib/esm/font-face.js +90 -0
  3. package/lib/esm/html-element.js +65 -1
  4. package/lib/esm/html-image-element.js +38 -1
  5. package/lib/esm/index.js +12 -1
  6. package/lib/esm/location-stub.js +25 -0
  7. package/lib/esm/match-media.js +22 -0
  8. package/lib/esm/register/canvas.js +16 -0
  9. package/lib/esm/register/document.js +36 -0
  10. package/lib/esm/register/font-face.js +14 -0
  11. package/lib/esm/register/helpers.js +16 -0
  12. package/lib/esm/register/image.js +5 -0
  13. package/lib/esm/register/location.js +3 -0
  14. package/lib/esm/register/match-media.js +3 -0
  15. package/lib/esm/register/navigator.js +3 -0
  16. package/lib/esm/register/observers.js +7 -0
  17. package/lib/esm/register.js +8 -47
  18. package/lib/types/dom-matrix.d.ts +64 -0
  19. package/lib/types/font-face.d.ts +45 -0
  20. package/lib/types/html-element.d.ts +10 -1
  21. package/lib/types/html-image-element.spec.d.ts +2 -0
  22. package/lib/types/index.d.ts +4 -0
  23. package/lib/types/location-stub.d.ts +21 -0
  24. package/lib/types/match-media.d.ts +12 -0
  25. package/lib/types/register/canvas.d.ts +1 -0
  26. package/lib/types/register/document.d.ts +1 -0
  27. package/lib/types/register/font-face.d.ts +1 -0
  28. package/lib/types/register/helpers.d.ts +4 -0
  29. package/lib/types/register/image.d.ts +1 -0
  30. package/lib/types/register/location.d.ts +1 -0
  31. package/lib/types/register/match-media.d.ts +1 -0
  32. package/lib/types/register/navigator.d.ts +1 -0
  33. package/lib/types/register/observers.d.ts +1 -0
  34. package/lib/types/register.d.ts +8 -1
  35. package/lib/types/register.spec.d.ts +3 -0
  36. package/lib/types/stubs.spec.d.ts +2 -0
  37. package/package.json +37 -12
  38. package/src/dom-matrix.ts +109 -0
  39. package/src/font-face.ts +97 -0
  40. package/src/html-element.ts +64 -1
  41. package/src/html-image-element.spec.ts +285 -0
  42. package/src/html-image-element.ts +43 -2
  43. package/src/index.ts +4 -0
  44. package/src/location-stub.ts +20 -0
  45. package/src/match-media.ts +32 -0
  46. package/src/register/canvas.ts +23 -0
  47. package/src/register/document.ts +56 -0
  48. package/src/register/font-face.ts +18 -0
  49. package/src/register/helpers.ts +15 -0
  50. package/src/register/image.ts +8 -0
  51. package/src/register/location.ts +6 -0
  52. package/src/register/match-media.ts +6 -0
  53. package/src/register/navigator.ts +6 -0
  54. package/src/register/observers.ts +10 -0
  55. package/src/register.spec.ts +115 -0
  56. package/src/register.ts +13 -72
  57. package/src/stubs.spec.ts +284 -0
  58. package/src/test.mts +4 -1
  59. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Minimal DOMMatrix implementation — supports 2D and 3D construction, plus
3
+ * 2D multiply/inverse/translate/scale operations used by Canvas 2D libraries
4
+ * like Excalibur. Full 3D math (4x4 multiply, inverse) is NOT implemented.
5
+ */
6
+ export declare class DOMMatrix {
7
+ a: number;
8
+ b: number;
9
+ c: number;
10
+ d: number;
11
+ e: number;
12
+ f: number;
13
+ m11: number;
14
+ m12: number;
15
+ m13: number;
16
+ m14: number;
17
+ m21: number;
18
+ m22: number;
19
+ m23: number;
20
+ m24: number;
21
+ m31: number;
22
+ m32: number;
23
+ m33: number;
24
+ m34: number;
25
+ m41: number;
26
+ m42: number;
27
+ m43: number;
28
+ m44: number;
29
+ is2D: boolean;
30
+ isIdentity: boolean;
31
+ constructor(init?: number[] | string);
32
+ /**
33
+ * Multiply this 2D matrix by another 2D matrix and return a new matrix.
34
+ * [a c e] [a' c' e'] [a*a'+c*b' a*c'+c*d' a*e'+c*f'+e]
35
+ * [b d f] [b' d' f'] = [b*a'+d*b' b*c'+d*d' b*e'+d*f'+f]
36
+ * [0 0 1] [0 0 1 ] [0 0 1 ]
37
+ */
38
+ multiply(other: {
39
+ a: number;
40
+ b: number;
41
+ c: number;
42
+ d: number;
43
+ e: number;
44
+ f: number;
45
+ }): DOMMatrix;
46
+ /** In-place multiply; returns this. */
47
+ multiplySelf(other: {
48
+ a: number;
49
+ b: number;
50
+ c: number;
51
+ d: number;
52
+ e: number;
53
+ f: number;
54
+ }): DOMMatrix;
55
+ /** 2D inverse. Throws if non-invertible (det === 0). */
56
+ inverse(): DOMMatrix;
57
+ translate(tx?: number, ty?: number): DOMMatrix;
58
+ scale(sx?: number, sy?: number): DOMMatrix;
59
+ }
60
+ /**
61
+ * DOMMatrixReadOnly alias — MDN specifies this as the immutable base class.
62
+ * We expose the same impl since consumers (Excalibur, three.js) rarely care.
63
+ */
64
+ export declare const DOMMatrixReadOnly: typeof DOMMatrix;
@@ -0,0 +1,45 @@
1
+ export declare class FontFace {
2
+ readonly family: string;
3
+ readonly source: string;
4
+ status: 'unloaded' | 'loading' | 'loaded' | 'error';
5
+ loaded: Promise<FontFace>;
6
+ display: string;
7
+ style: string;
8
+ weight: string;
9
+ stretch: string;
10
+ unicodeRange: string;
11
+ variant: string;
12
+ featureSettings: string;
13
+ constructor(family: string, source: string | ArrayBuffer | ArrayBufferView, _descriptors?: Record<string, string>);
14
+ private _extractFilePath;
15
+ load(): Promise<FontFace>;
16
+ }
17
+ /**
18
+ * FontFaceSet — tracks loaded FontFace objects and exposes them to consumers.
19
+ *
20
+ * Intentionally does NOT extend EventTarget. The dom-elements /register module
21
+ * runs before dom-events/register in the inject order, so EventTarget may not
22
+ * yet exist when this class is defined at module load time. All event methods
23
+ * are provided as no-ops; consumers that call addEventListener('loadingdone')
24
+ * etc. will silently receive nothing.
25
+ */
26
+ export declare class FontFaceSet {
27
+ status: 'loading' | 'loaded';
28
+ ready: Promise<FontFaceSet>;
29
+ private _faces;
30
+ addEventListener(_type: string, _listener: unknown): void;
31
+ removeEventListener(_type: string, _listener: unknown): void;
32
+ dispatchEvent(_event: unknown): boolean;
33
+ add(face: FontFace): FontFaceSet;
34
+ delete(face: FontFace): boolean;
35
+ clear(): void;
36
+ has(face: FontFace): boolean;
37
+ check(_font: string, _text?: string): boolean;
38
+ load(_font: string, _text?: string): Promise<FontFace[]>;
39
+ forEach(callback: (value: FontFace, key: FontFace, parent: FontFaceSet) => void): void;
40
+ values(): IterableIterator<FontFace>;
41
+ keys(): IterableIterator<FontFace>;
42
+ entries(): IterableIterator<[FontFace, FontFace]>;
43
+ [Symbol.iterator](): Iterator<FontFace>;
44
+ get size(): number;
45
+ }
@@ -7,7 +7,15 @@ import { Element } from './element.js';
7
7
  */
8
8
  export declare class CSSStyleDeclaration {
9
9
  [key: string]: unknown;
10
- cssText: string;
10
+ private _cssText;
11
+ /** Setting cssText parses individual declarations and stores them as camelCase properties,
12
+ * matching browser behavior for feature-detection checks like Excalibur's rgbaSupport. */
13
+ get cssText(): string;
14
+ set cssText(value: string);
15
+ setProperty(property: string, value: string, _priority?: string): void;
16
+ getPropertyValue(property: string): string;
17
+ removeProperty(property: string): string;
18
+ getPropertyPriority(_property: string): string;
11
19
  }
12
20
  /**
13
21
  * HTML Element base class.
@@ -16,6 +24,7 @@ export declare class CSSStyleDeclaration {
16
24
  */
17
25
  export declare class HTMLElement extends Element {
18
26
  readonly style: CSSStyleDeclaration;
27
+ get dataset(): Record<string, string>;
19
28
  get title(): string;
20
29
  set title(value: string);
21
30
  get lang(): string;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -19,3 +19,7 @@ export { IntersectionObserver } from './intersection-observer.js';
19
19
  export { NodeType } from './node-type.js';
20
20
  export { NamespaceURI } from './namespace-uri.js';
21
21
  export * as PropertySymbol from './property-symbol.js';
22
+ export { FontFace, FontFaceSet } from './font-face.js';
23
+ export { MediaQueryList, matchMedia } from './match-media.js';
24
+ export { location } from './location-stub.js';
25
+ export { DOMMatrix, DOMMatrixReadOnly } from './dom-matrix.js';
@@ -0,0 +1,21 @@
1
+ export declare const location: {
2
+ href: string;
3
+ origin: string;
4
+ protocol: string;
5
+ host: string;
6
+ hostname: string;
7
+ port: string;
8
+ pathname: string;
9
+ search: string;
10
+ hash: string;
11
+ assign(_url: string): void;
12
+ replace(_url: string): void;
13
+ reload(): void;
14
+ toString(): string;
15
+ ancestorOrigins: {
16
+ length: number;
17
+ item: () => null;
18
+ contains: () => boolean;
19
+ [Symbol.iterator]: () => Generator<string>;
20
+ };
21
+ };
@@ -0,0 +1,12 @@
1
+ import { EventTarget } from '@gjsify/dom-events';
2
+ export declare class MediaQueryList extends EventTarget {
3
+ readonly media: string;
4
+ readonly matches: boolean;
5
+ onchange: ((this: MediaQueryList, ev: unknown) => unknown) | null;
6
+ constructor(query: string);
7
+ /** @deprecated Use addEventListener('change', ...) */
8
+ addListener(_listener: unknown): void;
9
+ /** @deprecated Use removeEventListener('change', ...) */
10
+ removeListener(_listener: unknown): void;
11
+ }
12
+ export declare function matchMedia(query: string): MediaQueryList;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ /** Unconditionally expose a DOM class on `globalThis` (writable + configurable). */
2
+ export declare function defineGlobal(name: string, value: unknown): void;
3
+ /** Only set the global if it hasn't already been defined. */
4
+ export declare function defineGlobalIfMissing(name: string, value: unknown): void;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -1 +1,8 @@
1
- export {};
1
+ import './register/document.js';
2
+ import './register/canvas.js';
3
+ import './register/image.js';
4
+ import './register/observers.js';
5
+ import './register/font-face.js';
6
+ import './register/match-media.js';
7
+ import './register/location.js';
8
+ import './register/navigator.js';
@@ -0,0 +1,3 @@
1
+ import '@gjsify/dom-elements/register';
2
+ declare const _default: () => Promise<void>;
3
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/dom-elements",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "DOM element hierarchy (Node, Element, HTMLElement, HTMLImageElement) for GJS",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -13,10 +13,35 @@
13
13
  "./register": {
14
14
  "types": "./lib/types/register.d.ts",
15
15
  "default": "./lib/esm/register.js"
16
+ },
17
+ "./register/document": {
18
+ "default": "./lib/esm/register/document.js"
19
+ },
20
+ "./register/canvas": {
21
+ "default": "./lib/esm/register/canvas.js"
22
+ },
23
+ "./register/image": {
24
+ "default": "./lib/esm/register/image.js"
25
+ },
26
+ "./register/observers": {
27
+ "default": "./lib/esm/register/observers.js"
28
+ },
29
+ "./register/font-face": {
30
+ "default": "./lib/esm/register/font-face.js"
31
+ },
32
+ "./register/match-media": {
33
+ "default": "./lib/esm/register/match-media.js"
34
+ },
35
+ "./register/location": {
36
+ "default": "./lib/esm/register/location.js"
37
+ },
38
+ "./register/navigator": {
39
+ "default": "./lib/esm/register/navigator.js"
16
40
  }
17
41
  },
18
42
  "sideEffects": [
19
- "./lib/esm/register.js"
43
+ "./lib/esm/register.js",
44
+ "./lib/esm/register/*.js"
20
45
  ],
21
46
  "scripts": {
22
47
  "clear": "rm -rf lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs test.node.mjs || exit 0",
@@ -36,18 +61,18 @@
36
61
  "node"
37
62
  ],
38
63
  "dependencies": {
39
- "@girs/gdkpixbuf-2.0": "^2.0.0-4.0.0-rc.1",
40
- "@girs/gjs": "^4.0.0-rc.1",
41
- "@girs/glib-2.0": "^2.88.0-4.0.0-rc.1",
42
- "@gjsify/abort-controller": "^0.1.8",
43
- "@gjsify/canvas2d-core": "^0.1.8",
44
- "@gjsify/dom-events": "^0.1.8",
45
- "@gjsify/fetch": "^0.1.8"
64
+ "@girs/gdkpixbuf-2.0": "^2.0.0-4.0.0-rc.2",
65
+ "@girs/gjs": "^4.0.0-rc.2",
66
+ "@girs/glib-2.0": "^2.88.0-4.0.0-rc.2",
67
+ "@gjsify/abort-controller": "^0.1.10",
68
+ "@gjsify/canvas2d-core": "^0.1.10",
69
+ "@gjsify/dom-events": "^0.1.10",
70
+ "@gjsify/fetch": "^0.1.10"
46
71
  },
47
72
  "devDependencies": {
48
- "@gjsify/cli": "^0.1.8",
49
- "@gjsify/unit": "^0.1.8",
50
- "@types/node": "^25.5.2",
73
+ "@gjsify/cli": "^0.1.10",
74
+ "@gjsify/unit": "^0.1.10",
75
+ "@types/node": "^25.6.0",
51
76
  "typescript": "^6.0.2"
52
77
  }
53
78
  }
@@ -0,0 +1,109 @@
1
+ // DOMMatrix stub for GJS — minimal 2D/3D transformation matrix.
2
+ // Libraries like Excalibur construct `new DOMMatrix([a,b,c,d,e,f])` and pass
3
+ // the result to `ctx.setTransform(matrix)` — we store the 6 2D components
4
+ // plus 16 3D components. Our Canvas 2D context's setTransform() accepts
5
+ // either a DOMMatrix or six numbers, so this works transparently.
6
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/API/DOMMatrix
7
+
8
+ /**
9
+ * Minimal DOMMatrix implementation — supports 2D and 3D construction, plus
10
+ * 2D multiply/inverse/translate/scale operations used by Canvas 2D libraries
11
+ * like Excalibur. Full 3D math (4x4 multiply, inverse) is NOT implemented.
12
+ */
13
+ export class DOMMatrix {
14
+ // 2D components
15
+ a = 1; b = 0; c = 0; d = 1; e = 0; f = 0;
16
+ // 3D components (column-major)
17
+ m11 = 1; m12 = 0; m13 = 0; m14 = 0;
18
+ m21 = 0; m22 = 1; m23 = 0; m24 = 0;
19
+ m31 = 0; m32 = 0; m33 = 1; m34 = 0;
20
+ m41 = 0; m42 = 0; m43 = 0; m44 = 1;
21
+ is2D = true;
22
+ isIdentity = true;
23
+
24
+ constructor(init?: number[] | string) {
25
+ if (Array.isArray(init)) {
26
+ if (init.length === 6) {
27
+ // 2D affine: [a, b, c, d, e, f]
28
+ this.a = this.m11 = init[0];
29
+ this.b = this.m12 = init[1];
30
+ this.c = this.m21 = init[2];
31
+ this.d = this.m22 = init[3];
32
+ this.e = this.m41 = init[4];
33
+ this.f = this.m42 = init[5];
34
+ this.is2D = true;
35
+ } else if (init.length === 16) {
36
+ // 3D: column-major 4x4
37
+ this.m11 = init[0]; this.m12 = init[1]; this.m13 = init[2]; this.m14 = init[3];
38
+ this.m21 = init[4]; this.m22 = init[5]; this.m23 = init[6]; this.m24 = init[7];
39
+ this.m31 = init[8]; this.m32 = init[9]; this.m33 = init[10]; this.m34 = init[11];
40
+ this.m41 = init[12]; this.m42 = init[13]; this.m43 = init[14]; this.m44 = init[15];
41
+ this.a = this.m11; this.b = this.m12;
42
+ this.c = this.m21; this.d = this.m22;
43
+ this.e = this.m41; this.f = this.m42;
44
+ this.is2D = false;
45
+ }
46
+ this.isIdentity =
47
+ this.a === 1 && this.b === 0 && this.c === 0 &&
48
+ this.d === 1 && this.e === 0 && this.f === 0;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Multiply this 2D matrix by another 2D matrix and return a new matrix.
54
+ * [a c e] [a' c' e'] [a*a'+c*b' a*c'+c*d' a*e'+c*f'+e]
55
+ * [b d f] [b' d' f'] = [b*a'+d*b' b*c'+d*d' b*e'+d*f'+f]
56
+ * [0 0 1] [0 0 1 ] [0 0 1 ]
57
+ */
58
+ multiply(other: { a: number; b: number; c: number; d: number; e: number; f: number }): DOMMatrix {
59
+ const a = this.a * other.a + this.c * other.b;
60
+ const b = this.b * other.a + this.d * other.b;
61
+ const c = this.a * other.c + this.c * other.d;
62
+ const d = this.b * other.c + this.d * other.d;
63
+ const e = this.a * other.e + this.c * other.f + this.e;
64
+ const f = this.b * other.e + this.d * other.f + this.f;
65
+ return new DOMMatrix([a, b, c, d, e, f]);
66
+ }
67
+
68
+ /** In-place multiply; returns this. */
69
+ multiplySelf(other: { a: number; b: number; c: number; d: number; e: number; f: number }): DOMMatrix {
70
+ const result = this.multiply(other);
71
+ this.a = result.a; this.b = result.b;
72
+ this.c = result.c; this.d = result.d;
73
+ this.e = result.e; this.f = result.f;
74
+ this.m11 = this.a; this.m12 = this.b;
75
+ this.m21 = this.c; this.m22 = this.d;
76
+ this.m41 = this.e; this.m42 = this.f;
77
+ this.isIdentity = false;
78
+ return this;
79
+ }
80
+
81
+ /** 2D inverse. Throws if non-invertible (det === 0). */
82
+ inverse(): DOMMatrix {
83
+ const det = this.a * this.d - this.b * this.c;
84
+ if (det === 0) return new DOMMatrix([1, 0, 0, 1, 0, 0]);
85
+ const invDet = 1 / det;
86
+ return new DOMMatrix([
87
+ this.d * invDet,
88
+ -this.b * invDet,
89
+ -this.c * invDet,
90
+ this.a * invDet,
91
+ (this.c * this.f - this.d * this.e) * invDet,
92
+ (this.b * this.e - this.a * this.f) * invDet,
93
+ ]);
94
+ }
95
+
96
+ translate(tx = 0, ty = 0): DOMMatrix {
97
+ return this.multiply({ a: 1, b: 0, c: 0, d: 1, e: tx, f: ty });
98
+ }
99
+
100
+ scale(sx = 1, sy: number = sx): DOMMatrix {
101
+ return this.multiply({ a: sx, b: 0, c: 0, d: sy, e: 0, f: 0 });
102
+ }
103
+ }
104
+
105
+ /**
106
+ * DOMMatrixReadOnly alias — MDN specifies this as the immutable base class.
107
+ * We expose the same impl since consumers (Excalibur, three.js) rarely care.
108
+ */
109
+ export const DOMMatrixReadOnly = DOMMatrix;
@@ -0,0 +1,97 @@
1
+ // FontFace implementation for GJS — registers custom TTF fonts in PangoCairo
2
+ // so that Canvas2D fillText uses the correct font family.
3
+ //
4
+ // Flow (Excalibur): XHR(blob) → /tmp/gjsify-blob-N.ttf → createObjectURL →
5
+ // new FontFace(family, 'url(file:///tmp/gjsify-blob-N.ttf)') → face.load()
6
+ // The file is already on disk when load() is called; we just tell PangoCairo
7
+ // about it via font_map_get_default().add_font_file(path).
8
+ //
9
+ // Node.js: dynamic gi:// import fails gracefully → status='loaded' still set.
10
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/API/FontFace
11
+
12
+ export class FontFace {
13
+ readonly family: string;
14
+ readonly source: string;
15
+ status: 'unloaded' | 'loading' | 'loaded' | 'error' = 'unloaded';
16
+ loaded: Promise<FontFace>;
17
+ display = 'auto';
18
+ style = 'normal';
19
+ weight = 'normal';
20
+ stretch = 'normal';
21
+ unicodeRange = 'U+0-10FFFF';
22
+ variant = 'normal';
23
+ featureSettings = 'normal';
24
+
25
+ constructor(family: string, source: string | ArrayBuffer | ArrayBufferView, _descriptors?: Record<string, string>) {
26
+ this.family = family;
27
+ this.source = typeof source === 'string' ? source : '[binary]';
28
+ this.loaded = Promise.resolve(this);
29
+ }
30
+
31
+ // Parses: url(file:///path), url("file:///path"), url('file:///path')
32
+ private _extractFilePath(): string | null {
33
+ const m = this.source.match(/url\s*\(\s*["']?(file:\/\/\/[^"')]+)["']?\s*\)/i);
34
+ if (!m) return null;
35
+ // Strip file:// prefix → /path/to/file.ttf
36
+ return m[1].replace(/^file:\/\//, '');
37
+ }
38
+
39
+ async load(): Promise<FontFace> {
40
+ this.status = 'loading';
41
+ const filePath = this._extractFilePath();
42
+ if (filePath) {
43
+ try {
44
+ // gi:// only available in GJS — fails gracefully on Node.js
45
+ // @ts-ignore
46
+ const { default: PangoCairo } = await import('gi://PangoCairo?version=1.0');
47
+ PangoCairo.font_map_get_default().add_font_file(filePath);
48
+ } catch {
49
+ // Not in GJS, or font file not found — fall through to loaded
50
+ // Canvas fillText will use system font fallback (acceptable)
51
+ }
52
+ }
53
+ this.status = 'loaded';
54
+ return this;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * FontFaceSet — tracks loaded FontFace objects and exposes them to consumers.
60
+ *
61
+ * Intentionally does NOT extend EventTarget. The dom-elements /register module
62
+ * runs before dom-events/register in the inject order, so EventTarget may not
63
+ * yet exist when this class is defined at module load time. All event methods
64
+ * are provided as no-ops; consumers that call addEventListener('loadingdone')
65
+ * etc. will silently receive nothing.
66
+ */
67
+ export class FontFaceSet {
68
+ status: 'loading' | 'loaded' = 'loaded';
69
+ ready: Promise<FontFaceSet> = Promise.resolve(this);
70
+
71
+ private _faces = new Set<FontFace>();
72
+
73
+ addEventListener(_type: string, _listener: unknown): void {}
74
+ removeEventListener(_type: string, _listener: unknown): void {}
75
+ dispatchEvent(_event: unknown): boolean { return true; }
76
+
77
+ add(face: FontFace): FontFaceSet {
78
+ this._faces.add(face);
79
+ return this;
80
+ }
81
+ delete(face: FontFace): boolean { return this._faces.delete(face); }
82
+ clear(): void { this._faces.clear(); }
83
+ has(face: FontFace): boolean { return this._faces.has(face); }
84
+ check(_font: string, _text?: string): boolean { return false; }
85
+ load(_font: string, _text?: string): Promise<FontFace[]> { return Promise.resolve([]); }
86
+ forEach(callback: (value: FontFace, key: FontFace, parent: FontFaceSet) => void): void {
87
+ this._faces.forEach(f => callback(f, f, this));
88
+ }
89
+ values(): IterableIterator<FontFace> { return this._faces.values(); }
90
+ keys(): IterableIterator<FontFace> { return this._faces.values(); }
91
+ entries(): IterableIterator<[FontFace, FontFace]> {
92
+ const faces = Array.from(this._faces);
93
+ return faces.map(f => [f, f] as [FontFace, FontFace])[Symbol.iterator]() as IterableIterator<[FontFace, FontFace]>;
94
+ }
95
+ [Symbol.iterator](): Iterator<FontFace> { return this._faces[Symbol.iterator](); }
96
+ get size(): number { return this._faces.size; }
97
+ }
@@ -15,7 +15,37 @@ import * as PS from './property-symbol.js';
15
15
  */
16
16
  export class CSSStyleDeclaration {
17
17
  [key: string]: unknown;
18
- cssText = '';
18
+ private _cssText = '';
19
+
20
+ /** Setting cssText parses individual declarations and stores them as camelCase properties,
21
+ * matching browser behavior for feature-detection checks like Excalibur's rgbaSupport. */
22
+ get cssText(): string { return this._cssText; }
23
+ set cssText(value: string) {
24
+ this._cssText = value;
25
+ for (const decl of value.split(';')) {
26
+ const colon = decl.indexOf(':');
27
+ if (colon === -1) continue;
28
+ const prop = decl.slice(0, colon).trim();
29
+ const val = decl.slice(colon + 1).trim();
30
+ if (!prop) continue;
31
+ const camel = prop.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
32
+ (this as Record<string, unknown>)[camel] = val;
33
+ }
34
+ }
35
+
36
+ setProperty(property: string, value: string, _priority?: string): void {
37
+ (this as Record<string, unknown>)[property] = value;
38
+ }
39
+ getPropertyValue(property: string): string {
40
+ const v = (this as Record<string, unknown>)[property];
41
+ return typeof v === 'string' ? v : '';
42
+ }
43
+ removeProperty(property: string): string {
44
+ const v = (this as Record<string, unknown>)[property];
45
+ delete (this as Record<string, unknown>)[property];
46
+ return typeof v === 'string' ? v : '';
47
+ }
48
+ getPropertyPriority(_property: string): string { return ''; }
19
49
  }
20
50
 
21
51
  /**
@@ -27,6 +57,39 @@ export class HTMLElement extends Element {
27
57
  // -- Style stub (no layout engine — assignments are no-ops) --
28
58
  readonly style: CSSStyleDeclaration = new CSSStyleDeclaration();
29
59
 
60
+ // -- dataset: Proxy-backed DOMStringMap over data-* attributes --
61
+ // Converts `data-original-src` ↔ `originalSrc` (camelCase). Used by
62
+ // Excalibur's ImageSource which sets data-original-src on images for
63
+ // debugging and TextureLoader.checkImageSizeSupportedAndLog.
64
+ get dataset(): Record<string, string> {
65
+ const el = this;
66
+ return new Proxy({} as Record<string, string>, {
67
+ get(_target, prop: string): string | undefined {
68
+ if (typeof prop !== 'string') return undefined;
69
+ const attr = 'data-' + prop.replace(/[A-Z]/g, c => '-' + c.toLowerCase());
70
+ const value = el.getAttribute(attr);
71
+ return value ?? undefined;
72
+ },
73
+ set(_target, prop: string, value: string): boolean {
74
+ if (typeof prop !== 'string') return false;
75
+ const attr = 'data-' + prop.replace(/[A-Z]/g, c => '-' + c.toLowerCase());
76
+ el.setAttribute(attr, String(value));
77
+ return true;
78
+ },
79
+ deleteProperty(_target, prop: string): boolean {
80
+ if (typeof prop !== 'string') return false;
81
+ const attr = 'data-' + prop.replace(/[A-Z]/g, c => '-' + c.toLowerCase());
82
+ el.removeAttribute(attr);
83
+ return true;
84
+ },
85
+ has(_target, prop: string): boolean {
86
+ if (typeof prop !== 'string') return false;
87
+ const attr = 'data-' + prop.replace(/[A-Z]/g, c => '-' + c.toLowerCase());
88
+ return el.hasAttribute(attr);
89
+ },
90
+ });
91
+ }
92
+
30
93
  // -- Attribute-backed string properties --
31
94
 
32
95
  get title(): string {