@gjsify/dom-elements 0.1.0

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 (84) hide show
  1. package/README.md +31 -0
  2. package/lib/esm/attr.js +31 -0
  3. package/lib/esm/character-data.js +56 -0
  4. package/lib/esm/comment.js +21 -0
  5. package/lib/esm/document-fragment.js +112 -0
  6. package/lib/esm/document.js +83 -0
  7. package/lib/esm/dom-token-list.js +109 -0
  8. package/lib/esm/element.js +237 -0
  9. package/lib/esm/html-canvas-element.js +65 -0
  10. package/lib/esm/html-element.js +346 -0
  11. package/lib/esm/html-image-element.js +184 -0
  12. package/lib/esm/image.js +23 -0
  13. package/lib/esm/index.js +112 -0
  14. package/lib/esm/intersection-observer.js +19 -0
  15. package/lib/esm/mutation-observer.js +14 -0
  16. package/lib/esm/named-node-map.js +124 -0
  17. package/lib/esm/namespace-uri.js +10 -0
  18. package/lib/esm/node-list.js +34 -0
  19. package/lib/esm/node-type.js +14 -0
  20. package/lib/esm/node.js +227 -0
  21. package/lib/esm/property-symbol.js +30 -0
  22. package/lib/esm/resize-observer.js +13 -0
  23. package/lib/esm/text.js +51 -0
  24. package/lib/esm/types/i-html-image-element.js +0 -0
  25. package/lib/esm/types/image-data.js +0 -0
  26. package/lib/esm/types/index.js +3 -0
  27. package/lib/esm/types/predefined-color-space.js +0 -0
  28. package/lib/types/attr.d.ts +22 -0
  29. package/lib/types/character-data.d.ts +24 -0
  30. package/lib/types/comment.d.ts +12 -0
  31. package/lib/types/document-fragment.d.ts +37 -0
  32. package/lib/types/document.d.ts +39 -0
  33. package/lib/types/dom-token-list.d.ts +30 -0
  34. package/lib/types/element.d.ts +58 -0
  35. package/lib/types/html-canvas-element.d.ts +40 -0
  36. package/lib/types/html-element.d.ts +119 -0
  37. package/lib/types/html-image-element.d.ts +65 -0
  38. package/lib/types/image.d.ts +17 -0
  39. package/lib/types/index.d.ts +21 -0
  40. package/lib/types/intersection-observer.d.ts +21 -0
  41. package/lib/types/mutation-observer.d.ts +24 -0
  42. package/lib/types/named-node-map.d.ts +31 -0
  43. package/lib/types/namespace-uri.d.ts +7 -0
  44. package/lib/types/node-list.d.ts +18 -0
  45. package/lib/types/node-type.d.ts +11 -0
  46. package/lib/types/node.d.ts +63 -0
  47. package/lib/types/property-symbol.d.ts +14 -0
  48. package/lib/types/resize-observer.d.ts +13 -0
  49. package/lib/types/text.d.ts +21 -0
  50. package/lib/types/types/i-html-image-element.d.ts +41 -0
  51. package/lib/types/types/image-data.d.ts +11 -0
  52. package/lib/types/types/index.d.ts +3 -0
  53. package/lib/types/types/predefined-color-space.d.ts +1 -0
  54. package/package.json +43 -0
  55. package/src/attr.ts +61 -0
  56. package/src/character-data.ts +79 -0
  57. package/src/comment.ts +31 -0
  58. package/src/document-fragment.ts +137 -0
  59. package/src/document.ts +93 -0
  60. package/src/dom-token-list.ts +140 -0
  61. package/src/element.ts +299 -0
  62. package/src/html-canvas-element.ts +81 -0
  63. package/src/html-element.ts +422 -0
  64. package/src/html-image-element.ts +242 -0
  65. package/src/image.ts +31 -0
  66. package/src/index.spec.ts +897 -0
  67. package/src/index.ts +95 -0
  68. package/src/intersection-observer.ts +42 -0
  69. package/src/mutation-observer.ts +39 -0
  70. package/src/named-node-map.ts +159 -0
  71. package/src/namespace-uri.ts +11 -0
  72. package/src/node-list.ts +52 -0
  73. package/src/node-type.ts +14 -0
  74. package/src/node.ts +250 -0
  75. package/src/property-symbol.ts +23 -0
  76. package/src/resize-observer.ts +28 -0
  77. package/src/test.mts +6 -0
  78. package/src/text.ts +67 -0
  79. package/src/types/i-html-image-element.ts +44 -0
  80. package/src/types/image-data.ts +12 -0
  81. package/src/types/index.ts +3 -0
  82. package/src/types/predefined-color-space.ts +1 -0
  83. package/tsconfig.json +37 -0
  84. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,13 @@
1
+ import type { Element } from './element.js';
2
+ /**
3
+ * ResizeObserver stub.
4
+ * Many libraries check for ResizeObserver existence; this prevents crashes.
5
+ *
6
+ * Reference: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver
7
+ */
8
+ export declare class ResizeObserver {
9
+ constructor(_callback: (...args: unknown[]) => void);
10
+ observe(_target: Element): void;
11
+ unobserve(_target: Element): void;
12
+ disconnect(): void;
13
+ }
@@ -0,0 +1,21 @@
1
+ import { CharacterData } from './character-data.js';
2
+ /**
3
+ * Text node.
4
+ *
5
+ * Reference: https://developer.mozilla.org/en-US/docs/Web/API/Text
6
+ */
7
+ export declare class Text extends CharacterData {
8
+ constructor(data?: string);
9
+ get nodeName(): string;
10
+ /**
11
+ * Returns the combined text of this node and all adjacent Text siblings.
12
+ */
13
+ get wholeText(): string;
14
+ /**
15
+ * Splits the text node at the given offset, returning the new Text node
16
+ * containing the text after the offset.
17
+ */
18
+ splitText(offset: number): Text;
19
+ cloneNode(_deep?: boolean): Text;
20
+ get [Symbol.toStringTag](): string;
21
+ }
@@ -0,0 +1,41 @@
1
+ import type { HTMLElement } from '@gjsify/dom-elements';
2
+ /**
3
+ * HTML Image Element.
4
+ *
5
+ * Reference:
6
+ * https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement.
7
+ */
8
+ export interface IHTMLImageElement extends HTMLElement {
9
+ alt: string;
10
+ readonly complete: boolean;
11
+ crossOrigin: string | null;
12
+ readonly currentSrc: string;
13
+ decoding: string;
14
+ height: number;
15
+ isMap: boolean;
16
+ loading: string;
17
+ readonly naturalHeight: number;
18
+ readonly naturalWidth: number;
19
+ referrerPolicy: string;
20
+ sizes: string;
21
+ src: string;
22
+ srcset: string;
23
+ useMap: string;
24
+ width: number;
25
+ readonly x: number;
26
+ readonly y: number;
27
+ /**
28
+ * The decode() method of the HTMLImageElement interface returns a Promise that resolves when the image is decoded and it is safe to append the image to the DOM.
29
+ *
30
+ * @returns Promise.
31
+ */
32
+ decode(): Promise<void>;
33
+ /**
34
+ * Clones a node.
35
+ *
36
+ * @override
37
+ * @param [deep=false] "true" to clone deep.
38
+ * @returns Cloned node.
39
+ */
40
+ cloneNode(deep?: boolean): IHTMLImageElement;
41
+ }
@@ -0,0 +1,11 @@
1
+ import type { PredefinedColorSpace } from './predefined-color-space.js';
2
+ /** The underlying pixel data of an area of a <canvas> element. It is created using the ImageData() constructor or creator methods on the CanvasRenderingContext2D object associated with a canvas: createImageData() and getImageData(). It can also be used to set a part of the canvas by using putImageData(). */
3
+ export interface ImageData {
4
+ readonly colorSpace: PredefinedColorSpace;
5
+ /** Returns the one-dimensional array containing the data in RGBA order, as integers in the range 0 to 255. */
6
+ readonly data: Uint8ClampedArray<ArrayBuffer>;
7
+ /** Returns the actual dimensions of the data in the ImageData object, in pixels. */
8
+ readonly height: number;
9
+ /** Returns the actual dimensions of the data in the ImageData object, in pixels. */
10
+ readonly width: number;
11
+ }
@@ -0,0 +1,3 @@
1
+ export * from './i-html-image-element.js';
2
+ export * from './image-data.js';
3
+ export * from './predefined-color-space.js';
@@ -0,0 +1 @@
1
+ export type PredefinedColorSpace = "display-p3" | "srgb";
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@gjsify/dom-elements",
3
+ "version": "0.1.0",
4
+ "description": "DOM element hierarchy (Node, Element, HTMLElement, HTMLImageElement) for GJS",
5
+ "type": "module",
6
+ "module": "lib/esm/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "default": "./lib/esm/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "clear": "rm -rf lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs test.node.mjs || exit 0",
16
+ "check": "tsc --noEmit",
17
+ "build": "yarn build:gjsify && yarn build:types",
18
+ "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
19
+ "build:types": "tsc",
20
+ "build:test": "yarn build:test:gjs",
21
+ "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
22
+ "test": "yarn build:gjsify && yarn build:test && yarn test:gjs",
23
+ "test:gjs": "gjs -m test.gjs.mjs"
24
+ },
25
+ "keywords": [
26
+ "gjs",
27
+ "dom",
28
+ "element",
29
+ "node"
30
+ ],
31
+ "dependencies": {
32
+ "@girs/gdkpixbuf-2.0": "^2.0.0-4.0.0-beta.42",
33
+ "@girs/gjs": "^4.0.0-beta.42",
34
+ "@girs/glib-2.0": "^2.88.0-4.0.0-beta.42",
35
+ "@gjsify/dom-events": "^0.1.0"
36
+ },
37
+ "devDependencies": {
38
+ "@gjsify/cli": "^0.1.0",
39
+ "@gjsify/unit": "^0.1.0",
40
+ "@types/node": "^25.5.0",
41
+ "typescript": "^6.0.2"
42
+ }
43
+ }
package/src/attr.ts ADDED
@@ -0,0 +1,61 @@
1
+ // Adapted from happy-dom (refs/happy-dom/packages/happy-dom/src/nodes/attr/Attr.ts)
2
+ // Copyright (c) David Ortner (capricorn86). MIT license.
3
+ // Modifications: Simplified for gjsify — lightweight data holder, does not extend Node
4
+
5
+ import * as PS from './property-symbol.js';
6
+
7
+ import type { Element } from './element.js';
8
+
9
+ /**
10
+ * Represents a DOM attribute.
11
+ *
12
+ * Reference: https://developer.mozilla.org/en-US/docs/Web/API/Attr
13
+ */
14
+ export class Attr {
15
+ public [PS.name]: string;
16
+ public [PS.value]: string;
17
+ public [PS.ownerElement]: Element | null;
18
+
19
+ public readonly localName: string;
20
+ public readonly namespaceURI: string | null;
21
+ public readonly prefix: string | null;
22
+ public readonly specified = true;
23
+
24
+ constructor(
25
+ name: string,
26
+ value: string,
27
+ namespaceURI: string | null = null,
28
+ prefix: string | null = null,
29
+ ownerElement: Element | null = null,
30
+ ) {
31
+ this[PS.name] = name;
32
+ this[PS.value] = value;
33
+ this[PS.ownerElement] = ownerElement;
34
+ this.namespaceURI = namespaceURI;
35
+ this.prefix = prefix;
36
+
37
+ // localName is the part after the prefix colon, or the full name if no prefix
38
+ const colonIndex = name.indexOf(':');
39
+ this.localName = colonIndex !== -1 ? name.slice(colonIndex + 1) : name;
40
+ }
41
+
42
+ get name(): string {
43
+ return this[PS.name];
44
+ }
45
+
46
+ get value(): string {
47
+ return this[PS.value];
48
+ }
49
+
50
+ set value(value: string) {
51
+ this[PS.value] = value;
52
+ }
53
+
54
+ get ownerElement(): Element | null {
55
+ return this[PS.ownerElement];
56
+ }
57
+
58
+ get [Symbol.toStringTag](): string {
59
+ return 'Attr';
60
+ }
61
+ }
@@ -0,0 +1,79 @@
1
+ // Adapted from happy-dom (refs/happy-dom/packages/happy-dom/src/nodes/character-data/CharacterData.ts)
2
+ // Copyright (c) David Ortner (capricorn86). MIT license.
3
+ // Modifications: Simplified for gjsify — no ChildNode utilities, no mutation observer hooks
4
+
5
+ import { Node } from './node.js';
6
+ import { NodeType } from './node-type.js';
7
+ import * as PS from './property-symbol.js';
8
+
9
+ /**
10
+ * CharacterData base class for Text and Comment nodes.
11
+ *
12
+ * Reference: https://developer.mozilla.org/en-US/docs/Web/API/CharacterData
13
+ */
14
+ export class CharacterData extends Node {
15
+ private _data: string;
16
+
17
+ constructor(data = '') {
18
+ super();
19
+ this[PS.nodeType] = NodeType.TEXT_NODE;
20
+ this._data = data;
21
+ }
22
+
23
+ get data(): string {
24
+ return this._data;
25
+ }
26
+
27
+ set data(value: string) {
28
+ this._data = value;
29
+ }
30
+
31
+ get textContent(): string {
32
+ return this._data;
33
+ }
34
+
35
+ set textContent(value: string) {
36
+ this._data = value;
37
+ }
38
+
39
+ get nodeValue(): string {
40
+ return this._data;
41
+ }
42
+
43
+ set nodeValue(value: string) {
44
+ this._data = value;
45
+ }
46
+
47
+ get length(): number {
48
+ return this._data.length;
49
+ }
50
+
51
+ appendData(data: string): void {
52
+ this._data += data;
53
+ }
54
+
55
+ deleteData(offset: number, count: number): void {
56
+ this._data = this._data.substring(0, offset) + this._data.substring(offset + count);
57
+ }
58
+
59
+ insertData(offset: number, data: string): void {
60
+ this._data = this._data.substring(0, offset) + data + this._data.substring(offset);
61
+ }
62
+
63
+ replaceData(offset: number, count: number, data: string): void {
64
+ this._data = this._data.substring(0, offset) + data + this._data.substring(offset + count);
65
+ }
66
+
67
+ substringData(offset: number, count: number): string {
68
+ return this._data.substring(offset, offset + count);
69
+ }
70
+
71
+ cloneNode(_deep = false): CharacterData {
72
+ const clone = new (this.constructor as typeof CharacterData)(this._data);
73
+ return clone;
74
+ }
75
+
76
+ get [Symbol.toStringTag](): string {
77
+ return 'CharacterData';
78
+ }
79
+ }
package/src/comment.ts ADDED
@@ -0,0 +1,31 @@
1
+ // Adapted from happy-dom (refs/happy-dom/packages/happy-dom/src/nodes/comment/Comment.ts)
2
+ // Copyright (c) David Ortner (capricorn86). MIT license.
3
+ // Modifications: Simplified for gjsify
4
+
5
+ import { CharacterData } from './character-data.js';
6
+ import { NodeType } from './node-type.js';
7
+ import * as PS from './property-symbol.js';
8
+
9
+ /**
10
+ * Comment node.
11
+ *
12
+ * Reference: https://developer.mozilla.org/en-US/docs/Web/API/Comment
13
+ */
14
+ export class Comment extends CharacterData {
15
+ constructor(data = '') {
16
+ super(data);
17
+ this[PS.nodeType] = NodeType.COMMENT_NODE;
18
+ }
19
+
20
+ get nodeName(): string {
21
+ return '#comment';
22
+ }
23
+
24
+ cloneNode(_deep = false): Comment {
25
+ return new Comment(this.data);
26
+ }
27
+
28
+ get [Symbol.toStringTag](): string {
29
+ return 'Comment';
30
+ }
31
+ }
@@ -0,0 +1,137 @@
1
+ // Adapted from happy-dom (refs/happy-dom/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts)
2
+ // Copyright (c) David Ortner (capricorn86). MIT license.
3
+ // Modifications: Simplified for gjsify — no querySelector/querySelectorAll, no HTML parsing
4
+
5
+ import { Node } from './node.js';
6
+ import { Element } from './element.js';
7
+ import { Text } from './text.js';
8
+ import { NodeType } from './node-type.js';
9
+ import * as PS from './property-symbol.js';
10
+
11
+ /**
12
+ * DocumentFragment.
13
+ *
14
+ * Reference: https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment
15
+ */
16
+ export class DocumentFragment extends Node {
17
+ constructor() {
18
+ super();
19
+ this[PS.nodeType] = NodeType.DOCUMENT_FRAGMENT_NODE;
20
+ }
21
+
22
+ get nodeName(): string {
23
+ return '#document-fragment';
24
+ }
25
+
26
+ /** Element children only (excludes text/comment nodes) */
27
+ get children(): Element[] {
28
+ return this[PS.elementChildren] as Element[];
29
+ }
30
+
31
+ get childElementCount(): number {
32
+ return this[PS.elementChildren].length;
33
+ }
34
+
35
+ get firstElementChild(): Element | null {
36
+ return (this[PS.elementChildren][0] as Element) ?? null;
37
+ }
38
+
39
+ get lastElementChild(): Element | null {
40
+ const children = this[PS.elementChildren];
41
+ return (children[children.length - 1] as Element) ?? null;
42
+ }
43
+
44
+ get textContent(): string {
45
+ let text = '';
46
+ for (const child of this.childNodes) {
47
+ if (child.textContent !== null) {
48
+ text += child.textContent;
49
+ }
50
+ }
51
+ return text;
52
+ }
53
+
54
+ set textContent(value: string) {
55
+ // Remove all children
56
+ while (this.firstChild) {
57
+ this.removeChild(this.firstChild);
58
+ }
59
+ // Add text node if value is non-empty
60
+ if (value) {
61
+ // Import Text lazily to avoid circular dependency
62
+ this.appendChild(new Text(value));
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Append nodes or strings to this fragment.
68
+ */
69
+ append(...nodes: (Node | string)[]): void {
70
+ for (const node of nodes) {
71
+ if (typeof node === 'string') {
72
+ this.appendChild(new Text(node));
73
+ } else {
74
+ this.appendChild(node);
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Prepend nodes or strings to this fragment.
81
+ */
82
+ prepend(...nodes: (Node | string)[]): void {
83
+ const firstChild = this.firstChild;
84
+ for (const node of nodes) {
85
+ if (typeof node === 'string') {
86
+ this.insertBefore(new Text(node), firstChild);
87
+ } else {
88
+ this.insertBefore(node, firstChild);
89
+ }
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Replace all children with the given nodes.
95
+ */
96
+ replaceChildren(...nodes: (Node | string)[]): void {
97
+ while (this.firstChild) {
98
+ this.removeChild(this.firstChild);
99
+ }
100
+ this.append(...nodes);
101
+ }
102
+
103
+ /**
104
+ * Find an element by ID in this fragment's children.
105
+ */
106
+ getElementById(id: string): Element | null {
107
+ for (const child of this.children) {
108
+ if (child.id === id) return child;
109
+ const found = this._findById(child, id);
110
+ if (found) return found;
111
+ }
112
+ return null;
113
+ }
114
+
115
+ private _findById(element: Element, id: string): Element | null {
116
+ for (const child of element.children) {
117
+ if (child.id === id) return child;
118
+ const found = this._findById(child, id);
119
+ if (found) return found;
120
+ }
121
+ return null;
122
+ }
123
+
124
+ cloneNode(deep = false): DocumentFragment {
125
+ const clone = new DocumentFragment();
126
+ if (deep) {
127
+ for (const child of this.childNodes) {
128
+ clone.appendChild(child.cloneNode(true));
129
+ }
130
+ }
131
+ return clone;
132
+ }
133
+
134
+ get [Symbol.toStringTag](): string {
135
+ return 'DocumentFragment';
136
+ }
137
+ }
@@ -0,0 +1,93 @@
1
+ // Document stub for GJS — original implementation
2
+ // Reference: refs/happy-dom/packages/happy-dom/src/nodes/document/Document.ts
3
+
4
+ import { Node } from './node.js';
5
+ import { Element } from './element.js';
6
+ import { HTMLElement } from './html-element.js';
7
+ import { HTMLImageElement } from './html-image-element.js';
8
+ import { HTMLCanvasElement } from './html-canvas-element.js';
9
+ import { Text } from './text.js';
10
+ import { Comment } from './comment.js';
11
+ import { DocumentFragment } from './document-fragment.js';
12
+ import { Event } from '@gjsify/dom-events';
13
+
14
+ type ElementFactory = () => HTMLElement;
15
+
16
+ export class Document extends Node {
17
+ /** External packages register element factories here (e.g. @gjsify/iframe registers 'iframe') */
18
+ private static _elementFactories = new Map<string, ElementFactory>();
19
+
20
+ /** Stub body element */
21
+ readonly body: HTMLElement = new HTMLElement();
22
+
23
+ /** Stub head element */
24
+ readonly head: HTMLElement = new HTMLElement();
25
+
26
+ /** Stub documentElement */
27
+ readonly documentElement: HTMLElement = new HTMLElement();
28
+
29
+ /**
30
+ * Register a factory for a custom element tag name.
31
+ * Called as a side-effect by DOM packages to avoid circular dependencies.
32
+ *
33
+ * Example: `Document.registerElementFactory('iframe', () => new HTMLIFrameElement())`
34
+ */
35
+ static registerElementFactory(tagName: string, factory: ElementFactory): void {
36
+ Document._elementFactories.set(tagName.toLowerCase(), factory);
37
+ }
38
+
39
+ createElementNS(_namespace: string | null, tagName: string): HTMLElement {
40
+ const tag = tagName.toLowerCase();
41
+ switch (tag) {
42
+ case 'img': return new HTMLImageElement();
43
+ case 'canvas': return new HTMLCanvasElement();
44
+ default: {
45
+ const factory = Document._elementFactories.get(tag);
46
+ if (factory) return factory();
47
+ return new HTMLElement();
48
+ }
49
+ }
50
+ }
51
+
52
+ createElement(tagName: string): HTMLElement {
53
+ return this.createElementNS('http://www.w3.org/1999/xhtml', tagName);
54
+ }
55
+
56
+ createTextNode(data: string): Text {
57
+ return new Text(data);
58
+ }
59
+
60
+ createComment(data: string): Comment {
61
+ return new Comment(data);
62
+ }
63
+
64
+ createDocumentFragment(): DocumentFragment {
65
+ return new DocumentFragment();
66
+ }
67
+
68
+ createEvent(type: string): Event {
69
+ return new Event(type);
70
+ }
71
+
72
+ /**
73
+ * Find an element by ID. Searches body's descendants.
74
+ */
75
+ getElementById(id: string): Element | null {
76
+ return this._findById(this.body, id);
77
+ }
78
+
79
+ private _findById(element: Element, id: string): Element | null {
80
+ if (element.id === id) return element;
81
+ for (const child of element.children) {
82
+ const found = this._findById(child, id);
83
+ if (found) return found;
84
+ }
85
+ return null;
86
+ }
87
+
88
+ get [Symbol.toStringTag](): string {
89
+ return 'Document';
90
+ }
91
+ }
92
+
93
+ export const document = new Document();
@@ -0,0 +1,140 @@
1
+ // Adapted from happy-dom (refs/happy-dom/packages/happy-dom/src/dom/DOMTokenList.ts)
2
+ // Copyright (c) David Ortner (capricorn86). MIT license.
3
+ // Modifications: Simplified for gjsify — no Proxy, plain array-based implementation
4
+
5
+ import type { Element } from './element.js';
6
+
7
+ /**
8
+ * DOMTokenList — manages a set of space-separated tokens on an attribute.
9
+ *
10
+ * Reference: https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList
11
+ */
12
+ export class DOMTokenList {
13
+ private _ownerElement: Element;
14
+ private _attributeName: string;
15
+
16
+ constructor(ownerElement: Element, attributeName: string) {
17
+ this._ownerElement = ownerElement;
18
+ this._attributeName = attributeName;
19
+ }
20
+
21
+ private _getTokens(): string[] {
22
+ const value = this._ownerElement.getAttribute(this._attributeName);
23
+ if (!value) return [];
24
+ return value.split(/\s+/).filter(t => t.length > 0);
25
+ }
26
+
27
+ private _setTokens(tokens: string[]): void {
28
+ const value = tokens.join(' ');
29
+ if (value) {
30
+ this._ownerElement.setAttribute(this._attributeName, value);
31
+ } else {
32
+ this._ownerElement.removeAttribute(this._attributeName);
33
+ }
34
+ }
35
+
36
+ get length(): number {
37
+ return this._getTokens().length;
38
+ }
39
+
40
+ get value(): string {
41
+ return this._ownerElement.getAttribute(this._attributeName) ?? '';
42
+ }
43
+
44
+ set value(val: string) {
45
+ if (val) {
46
+ this._ownerElement.setAttribute(this._attributeName, val);
47
+ } else {
48
+ this._ownerElement.removeAttribute(this._attributeName);
49
+ }
50
+ }
51
+
52
+ item(index: number): string | null {
53
+ const tokens = this._getTokens();
54
+ return index >= 0 && index < tokens.length ? tokens[index] : null;
55
+ }
56
+
57
+ contains(token: string): boolean {
58
+ return this._getTokens().includes(token);
59
+ }
60
+
61
+ add(...tokens: string[]): void {
62
+ const current = this._getTokens();
63
+ for (const token of tokens) {
64
+ if (token && !current.includes(token)) {
65
+ current.push(token);
66
+ }
67
+ }
68
+ this._setTokens(current);
69
+ }
70
+
71
+ remove(...tokens: string[]): void {
72
+ const current = this._getTokens().filter(t => !tokens.includes(t));
73
+ this._setTokens(current);
74
+ }
75
+
76
+ toggle(token: string, force?: boolean): boolean {
77
+ const has = this.contains(token);
78
+ if (force !== undefined) {
79
+ if (force) {
80
+ this.add(token);
81
+ return true;
82
+ } else {
83
+ this.remove(token);
84
+ return false;
85
+ }
86
+ }
87
+ if (has) {
88
+ this.remove(token);
89
+ return false;
90
+ } else {
91
+ this.add(token);
92
+ return true;
93
+ }
94
+ }
95
+
96
+ replace(token: string, newToken: string): boolean {
97
+ const tokens = this._getTokens();
98
+ const idx = tokens.indexOf(token);
99
+ if (idx === -1) return false;
100
+ tokens[idx] = newToken;
101
+ this._setTokens(tokens);
102
+ return true;
103
+ }
104
+
105
+ supports(_token: string): boolean {
106
+ // No validation — all tokens are supported
107
+ return true;
108
+ }
109
+
110
+ forEach(callback: (value: string, index: number, list: DOMTokenList) => void): void {
111
+ const tokens = this._getTokens();
112
+ for (let i = 0; i < tokens.length; i++) {
113
+ callback(tokens[i], i, this);
114
+ }
115
+ }
116
+
117
+ keys(): IterableIterator<number> {
118
+ return this._getTokens().keys();
119
+ }
120
+
121
+ values(): IterableIterator<string> {
122
+ return this._getTokens().values();
123
+ }
124
+
125
+ entries(): IterableIterator<[number, string]> {
126
+ return this._getTokens().entries();
127
+ }
128
+
129
+ [Symbol.iterator](): IterableIterator<string> {
130
+ return this._getTokens().values();
131
+ }
132
+
133
+ toString(): string {
134
+ return this.value;
135
+ }
136
+
137
+ get [Symbol.toStringTag](): string {
138
+ return 'DOMTokenList';
139
+ }
140
+ }