@gjsify/dom-elements 0.1.8 → 0.1.9

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,285 @@
1
+ // HTMLImageElement lifecycle tests — verifies the src → onload → pixbuf
2
+ // chain Excalibur relies on, plus the native GJS extensions used by
3
+ // @gjsify/canvas2d-core drawImage.
4
+ //
5
+ // Ported from refs/happy-dom/packages/happy-dom/test/nodes/html-image-element/
6
+ // HTMLImageElement.test.ts (MIT, David Ortner) plus custom cases that
7
+ // lock in compatibility with Excalibur's ImageSource.load pattern:
8
+ // const image = new Image();
9
+ // image.onload = () => futureResolve();
10
+ // image.src = url;
11
+ // await future;
12
+ // (refs/excalibur/src/engine/graphics/image-source.ts:226-273)
13
+
14
+ import { describe, it, expect } from '@gjsify/unit';
15
+ import GLib from 'gi://GLib?version=2.0';
16
+ import GdkPixbuf from 'gi://GdkPixbuf?version=2.0';
17
+
18
+ import { HTMLImageElement, Image } from '@gjsify/dom-elements';
19
+
20
+ // ── Test fixture setup ────────────────────────────────────────────────────
21
+ //
22
+ // Generates a 2×2 RGBA test PNG in /tmp at module load. Each pixel has a
23
+ // distinct color (red/green/blue/white) so get-pixel tests can verify
24
+ // byte order. Cleaned up by /tmp rotation; no explicit teardown needed.
25
+
26
+ const FIXTURE_DIR = GLib.get_tmp_dir();
27
+ const FIXTURE_PATH = GLib.build_filenamev([FIXTURE_DIR, 'gjsify-test-2x2.png']);
28
+ const FIXTURE_URI = 'file://' + FIXTURE_PATH;
29
+
30
+ function writeFixturePng(): void {
31
+ // 2×2 RGBA raw pixel buffer:
32
+ // (0,0) red (1,0) green
33
+ // (0,1) blue (1,1) white
34
+ const pixels = new Uint8Array([
35
+ 255, 0, 0, 255, 0, 255, 0, 255,
36
+ 0, 0, 255, 255, 255, 255, 255, 255,
37
+ ]);
38
+ // GdkPixbuf.Pixbuf.new_from_bytes expects row-major, w*4 stride for RGBA.
39
+ const bytes = GLib.Bytes.new(pixels);
40
+ const pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(
41
+ bytes,
42
+ GdkPixbuf.Colorspace.RGB,
43
+ true, // has_alpha
44
+ 8, // bits_per_sample
45
+ 2, // width
46
+ 2, // height
47
+ 8, // rowstride (2 px * 4 bytes)
48
+ );
49
+ pixbuf.savev(FIXTURE_PATH, 'png', [], []);
50
+ }
51
+
52
+ writeFixturePng();
53
+
54
+ export default async () => {
55
+ await describe('HTMLImageElement', async () => {
56
+
57
+ await describe('constructor + static Image alias', async () => {
58
+ await it('new HTMLImageElement() creates an empty image', async () => {
59
+ const img = new HTMLImageElement();
60
+ expect(img.complete).toBe(false);
61
+ expect(img.naturalWidth).toBe(0);
62
+ expect(img.naturalHeight).toBe(0);
63
+ expect(img.src).toBe('');
64
+ });
65
+
66
+ await it('new Image() yields an HTMLImageElement instance', async () => {
67
+ const img = new Image();
68
+ expect(img instanceof HTMLImageElement).toBe(true);
69
+ });
70
+ });
71
+
72
+ await describe('src setter — file:// loading', async () => {
73
+ await it('loads a file:// URL synchronously and sets natural dimensions', async () => {
74
+ const img = new HTMLImageElement();
75
+ img.src = FIXTURE_URI;
76
+ expect(img.complete).toBe(true);
77
+ expect(img.naturalWidth).toBe(2);
78
+ expect(img.naturalHeight).toBe(2);
79
+ expect(img.isPixbuf()).toBe(true);
80
+ });
81
+
82
+ await it('fires the load event (sync dispatch)', async () => {
83
+ const img = new HTMLImageElement();
84
+ let loaded = false;
85
+ img.addEventListener('load', () => { loaded = true; });
86
+ img.src = FIXTURE_URI;
87
+ expect(loaded).toBe(true);
88
+ });
89
+
90
+ await it('fires the error event for non-existent files', async () => {
91
+ const img = new HTMLImageElement();
92
+ let errored = false;
93
+ img.addEventListener('error', () => { errored = true; });
94
+ img.src = 'file:///nonexistent/definitely/not/here.png';
95
+ expect(errored).toBe(true);
96
+ expect(img.complete).toBe(true);
97
+ expect(img.isPixbuf()).toBe(false);
98
+ });
99
+
100
+ await it('fires error immediately for http:// URLs (not supported in GJS)', async () => {
101
+ const img = new HTMLImageElement();
102
+ let errored = false;
103
+ img.addEventListener('error', () => { errored = true; });
104
+ img.src = 'http://example.com/img.png';
105
+ expect(errored).toBe(true);
106
+ expect(img.complete).toBe(true);
107
+ });
108
+ });
109
+
110
+ await describe('Excalibur.ImageSource.load pattern — handler before src', async () => {
111
+ // This is the canonical pattern Excalibur uses in
112
+ // refs/excalibur/src/engine/graphics/image-source.ts:247-252.
113
+ // The handler must be set BEFORE src so the sync dispatch fires
114
+ // it and the awaited promise resolves correctly.
115
+ await it('onload-set-then-src-set resolves the future', async () => {
116
+ const img = new HTMLImageElement();
117
+ const future = new Promise<void>((resolve) => {
118
+ img.onload = () => resolve();
119
+ });
120
+ img.src = FIXTURE_URI;
121
+ // Sync dispatch fires onload before returning from src=.
122
+ await future;
123
+ expect(img.complete).toBe(true);
124
+ expect(img.naturalWidth).toBe(2);
125
+ });
126
+
127
+ await it('onload registered AFTER src is set does NOT fire', async () => {
128
+ // Matches browser semantics: the load event is a one-shot
129
+ // dispatch — registering a listener after it fires does not
130
+ // retroactively invoke it.
131
+ const img = new HTMLImageElement();
132
+ img.src = FIXTURE_URI;
133
+ expect(img.complete).toBe(true);
134
+ let fired = false;
135
+ img.onload = () => { fired = true; };
136
+ // Give microtasks a chance to fire (they won't).
137
+ await new Promise<void>((r) => r());
138
+ expect(fired).toBe(false);
139
+ });
140
+ });
141
+
142
+ await describe('getImageData — RGBA byte order', async () => {
143
+ await it('extracts correct pixel bytes from the loaded pixbuf', async () => {
144
+ const img = new HTMLImageElement();
145
+ img.src = FIXTURE_URI;
146
+ const data = img.getImageData();
147
+ expect(data).not.toBeNull();
148
+ expect(data!.width).toBe(2);
149
+ expect(data!.height).toBe(2);
150
+ // Pixel (0,0) — red
151
+ expect(data!.data[0]).toBe(255);
152
+ expect(data!.data[1]).toBe(0);
153
+ expect(data!.data[2]).toBe(0);
154
+ expect(data!.data[3]).toBe(255);
155
+ // Pixel (1,0) — green
156
+ expect(data!.data[4]).toBe(0);
157
+ expect(data!.data[5]).toBe(255);
158
+ expect(data!.data[6]).toBe(0);
159
+ expect(data!.data[7]).toBe(255);
160
+ // Pixel (0,1) — blue
161
+ expect(data!.data[8]).toBe(0);
162
+ expect(data!.data[9]).toBe(0);
163
+ expect(data!.data[10]).toBe(255);
164
+ expect(data!.data[11]).toBe(255);
165
+ // Pixel (1,1) — white
166
+ expect(data!.data[12]).toBe(255);
167
+ expect(data!.data[13]).toBe(255);
168
+ expect(data!.data[14]).toBe(255);
169
+ expect(data!.data[15]).toBe(255);
170
+ });
171
+ });
172
+
173
+ await describe('src setter — data: URI loading', async () => {
174
+ // Excalibur's Loader.onBeforeLoad() sets img.src to a data:image/png;base64,...
175
+ // URI for the loader logo. If this fires error instead of load, the
176
+ // _imageLoaded promise never resolves and the loader hangs forever.
177
+ // Regression test for the GJS fix using GLib.base64_decode + Gio.MemoryInputStream.
178
+
179
+ function makeDataUri(): string {
180
+ // Encode the 2×2 fixture PNG as a data URI using GLib.
181
+ const [ok, bytes] = GLib.file_get_contents(FIXTURE_PATH);
182
+ if (!ok) throw new Error('fixture PNG not found');
183
+ const b64 = GLib.base64_encode(bytes as unknown as Uint8Array);
184
+ return `data:image/png;base64,${b64}`;
185
+ }
186
+
187
+ await it('loads a base64 PNG data URI and sets natural dimensions', async () => {
188
+ const img = new HTMLImageElement();
189
+ const dataUri = makeDataUri();
190
+ img.src = dataUri;
191
+ expect(img.complete).toBe(true);
192
+ expect(img.naturalWidth).toBe(2);
193
+ expect(img.naturalHeight).toBe(2);
194
+ expect(img.isPixbuf()).toBe(true);
195
+ });
196
+
197
+ await it('fires load event for a base64 PNG data URI', async () => {
198
+ const img = new HTMLImageElement();
199
+ let loaded = false;
200
+ img.addEventListener('load', () => { loaded = true; });
201
+ img.src = makeDataUri();
202
+ expect(loaded).toBe(true);
203
+ });
204
+
205
+ await it('Excalibur pattern: onload-then-src resolves for data URIs', async () => {
206
+ const img = new HTMLImageElement();
207
+ const future = new Promise<void>((resolve) => {
208
+ img.onload = () => resolve();
209
+ });
210
+ img.src = makeDataUri();
211
+ await future;
212
+ expect(img.complete).toBe(true);
213
+ expect(img.naturalWidth).toBe(2);
214
+ });
215
+
216
+ await it('fires error for a malformed data URI (no comma)', async () => {
217
+ const img = new HTMLImageElement();
218
+ let errored = false;
219
+ img.addEventListener('error', () => { errored = true; });
220
+ img.src = 'data:image/png;base64';
221
+ expect(errored).toBe(true);
222
+ expect(img.complete).toBe(true);
223
+ });
224
+
225
+ await it('getImageData returns correct RGBA pixels from data URI', async () => {
226
+ const img = new HTMLImageElement();
227
+ img.src = makeDataUri();
228
+ const data = img.getImageData();
229
+ expect(data).not.toBeNull();
230
+ expect(data!.width).toBe(2);
231
+ expect(data!.height).toBe(2);
232
+ // Pixel (0,0) — red
233
+ expect(data!.data[0]).toBe(255);
234
+ expect(data!.data[1]).toBe(0);
235
+ expect(data!.data[2]).toBe(0);
236
+ expect(data!.data[3]).toBe(255);
237
+ });
238
+ });
239
+
240
+ await describe('dataset proxy — Excalibur data-original-src pattern', async () => {
241
+ // Excalibur's ImageSource.load does:
242
+ // image.setAttribute('data-original-src', this.path);
243
+ // and TextureLoader.checkImageSizeSupportedAndLog reads
244
+ // image.dataset.originalSrc
245
+ // Our dataset Proxy must perform the kebab→camel case conversion.
246
+ await it('reads a data-* attribute via camelCase dataset access', async () => {
247
+ const img = new HTMLImageElement();
248
+ img.setAttribute('data-original-src', 'sprites/hero.png');
249
+ expect(img.dataset.originalSrc).toBe('sprites/hero.png');
250
+ });
251
+
252
+ await it('sets a data-* attribute via dataset assignment', async () => {
253
+ const img = new HTMLImageElement();
254
+ img.dataset.tileIndex = '42';
255
+ expect(img.getAttribute('data-tile-index')).toBe('42');
256
+ });
257
+
258
+ await it('delete operator removes the data-* attribute', async () => {
259
+ const img = new HTMLImageElement();
260
+ img.setAttribute('data-foo', 'bar');
261
+ delete img.dataset.foo;
262
+ expect(img.hasAttribute('data-foo')).toBe(false);
263
+ });
264
+
265
+ await it('returns undefined for unset dataset keys', async () => {
266
+ const img = new HTMLImageElement();
267
+ expect(img.dataset.nothing).toBe(undefined);
268
+ });
269
+ });
270
+
271
+ await describe('attribute-backed properties', async () => {
272
+ await it('alt, title, crossOrigin round-trip through attributes', async () => {
273
+ const img = new HTMLImageElement();
274
+ img.alt = 'hero sprite';
275
+ img.title = 'A jumping jelly';
276
+ img.crossOrigin = 'anonymous';
277
+ expect(img.getAttribute('alt')).toBe('hero sprite');
278
+ expect(img.getAttribute('title')).toBe('A jumping jelly');
279
+ expect(img.getAttribute('crossorigin')).toBe('anonymous');
280
+ expect(img.alt).toBe('hero sprite');
281
+ expect(img.crossOrigin).toBe('anonymous');
282
+ });
283
+ });
284
+ });
285
+ };
@@ -2,6 +2,7 @@
2
2
  // Reference: refs/happy-dom/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts
3
3
 
4
4
  import GLib from '@girs/glib-2.0';
5
+ import Gio from '@girs/gio-2.0';
5
6
  import GdkPixbuf from '@girs/gdkpixbuf-2.0';
6
7
  import { Event } from '@gjsify/dom-events';
7
8
  import { HTMLElement } from './html-element.js';
@@ -118,12 +119,49 @@ export class HTMLImageElement extends HTMLElement {
118
119
  set src(src: string) {
119
120
  this.setAttribute('src', src);
120
121
 
122
+ const DEBUG = (globalThis as any).__GJSIFY_DEBUG_IMG === true;
123
+
124
+ // Handle data: URIs (e.g. base64 PNG logos from Excalibur's loader)
125
+ if (src.startsWith('data:')) {
126
+ const commaIdx = src.indexOf(',');
127
+ if (commaIdx === -1) {
128
+ this._complete = true;
129
+ this.dispatchEvent(new Event('error'));
130
+ return;
131
+ }
132
+ const meta = src.slice(5, commaIdx); // between 'data:' and ','
133
+ const data = src.slice(commaIdx + 1);
134
+ const isBase64 = meta.includes(';base64');
135
+ try {
136
+ let bytes: Uint8Array;
137
+ if (isBase64) {
138
+ // Use GLib.base64_decode — available in all GJS versions, no global needed
139
+ bytes = GLib.base64_decode(data) as unknown as Uint8Array;
140
+ } else {
141
+ bytes = new TextEncoder().encode(decodeURIComponent(data));
142
+ }
143
+ const gbytes = GLib.Bytes.new(bytes);
144
+ const stream = Gio.MemoryInputStream.new_from_bytes(gbytes);
145
+ this._pixbuf = GdkPixbuf.Pixbuf.new_from_stream(stream, null);
146
+ this._naturalWidth = this._pixbuf!.get_width();
147
+ this._naturalHeight = this._pixbuf!.get_height();
148
+ this._complete = true;
149
+ if (DEBUG) console.log(`[img] ok data: (${this._naturalWidth}x${this._naturalHeight})`);
150
+ this.dispatchEvent(new Event('load'));
151
+ } catch (_error) {
152
+ if (DEBUG) console.warn(`[img] error data:: ${(_error as any)?.message ?? _error}`);
153
+ this._complete = true;
154
+ this.dispatchEvent(new Event('error'));
155
+ }
156
+ return;
157
+ }
158
+
121
159
  let filename: string;
122
160
  if (src.startsWith('file://')) {
123
161
  // GLib.filename_from_uri returns [localPath, hostname]
124
162
  filename = GLib.filename_from_uri(src)[0];
125
- } else if (src.startsWith('http://') || src.startsWith('https://') || src.startsWith('data:')) {
126
- // Remote URLs and data URIs are not supported in GJS — fire error
163
+ } else if (src.startsWith('http://') || src.startsWith('https://')) {
164
+ // Remote URLs are not supported in GJS — fire error
127
165
  this._complete = true;
128
166
  this.dispatchEvent(new Event('error'));
129
167
  return;
@@ -134,13 +172,16 @@ export class HTMLImageElement extends HTMLElement {
134
172
  }
135
173
 
136
174
  try {
175
+ if (DEBUG) console.log(`[img] load ${filename}`);
137
176
  this._pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename);
138
177
  this._naturalWidth = this._pixbuf.get_width();
139
178
  this._naturalHeight = this._pixbuf.get_height();
140
179
  this._complete = true;
180
+ if (DEBUG) console.log(`[img] ok ${filename} (${this._naturalWidth}x${this._naturalHeight})`);
141
181
 
142
182
  this.dispatchEvent(new Event('load'));
143
183
  } catch (_error) {
184
+ if (DEBUG) console.warn(`[img] error ${filename}: ${(_error as any)?.message ?? _error}`);
144
185
  this._complete = true;
145
186
  this.dispatchEvent(new Event('error'));
146
187
  }
package/src/index.ts CHANGED
@@ -31,3 +31,7 @@ export { IntersectionObserver } from './intersection-observer.js';
31
31
  export { NodeType } from './node-type.js';
32
32
  export { NamespaceURI } from './namespace-uri.js';
33
33
  export * as PropertySymbol from './property-symbol.js';
34
+ export { FontFace, FontFaceSet } from './font-face.js';
35
+ export { MediaQueryList, matchMedia } from './match-media.js';
36
+ export { location } from './location-stub.js';
37
+ export { DOMMatrix, DOMMatrixReadOnly } from './dom-matrix.js';
@@ -0,0 +1,20 @@
1
+ // window.location stub for GJS — provides minimal Location-compatible object.
2
+ // In GJS apps, there is no browser URL — we use file:// as a reasonable default.
3
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/API/Location
4
+
5
+ export const location = {
6
+ href: 'file://',
7
+ origin: 'file://',
8
+ protocol: 'file:',
9
+ host: '',
10
+ hostname: '',
11
+ port: '',
12
+ pathname: '/',
13
+ search: '',
14
+ hash: '',
15
+ assign(_url: string): void {},
16
+ replace(_url: string): void {},
17
+ reload(): void {},
18
+ toString(): string { return this.href; },
19
+ ancestorOrigins: { length: 0, item: () => null, contains: () => false, [Symbol.iterator]: function*(): Generator<string> {} },
20
+ };
@@ -0,0 +1,32 @@
1
+ // matchMedia stub for GJS — used by Excalibur and other libraries to monitor
2
+ // devicePixelRatio changes. Returns a minimal MediaQueryList-compatible object.
3
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia
4
+ //
5
+ // NOTE: imports EventTarget directly from @gjsify/dom-events rather than
6
+ // using the global, because dom-elements/register runs BEFORE
7
+ // dom-events/register in the inject order — so `globalThis.EventTarget` may
8
+ // not yet exist when this class is defined at module load time.
9
+
10
+ import { EventTarget } from '@gjsify/dom-events';
11
+
12
+ export class MediaQueryList extends EventTarget {
13
+ readonly media: string;
14
+ readonly matches: boolean;
15
+ onchange: ((this: MediaQueryList, ev: unknown) => unknown) | null = null;
16
+
17
+ constructor(query: string) {
18
+ super();
19
+ this.media = query;
20
+ this.matches = false;
21
+ }
22
+
23
+ /** @deprecated Use addEventListener('change', ...) */
24
+ addListener(_listener: unknown): void {}
25
+
26
+ /** @deprecated Use removeEventListener('change', ...) */
27
+ removeListener(_listener: unknown): void {}
28
+ }
29
+
30
+ export function matchMedia(query: string): MediaQueryList {
31
+ return new MediaQueryList(query);
32
+ }
@@ -0,0 +1,23 @@
1
+ // Registers: HTMLCanvasElement, CanvasRenderingContext2D, DOMMatrix,
2
+ // DOMMatrixReadOnly + the '2d' context factory.
3
+
4
+ import { CanvasRenderingContext2D } from '@gjsify/canvas2d-core';
5
+
6
+ import { HTMLCanvasElement } from '../html-canvas-element.js';
7
+ import { DOMMatrix, DOMMatrixReadOnly } from '../dom-matrix.js';
8
+ import { defineGlobal } from './helpers.js';
9
+
10
+ defineGlobal('HTMLCanvasElement', HTMLCanvasElement);
11
+ defineGlobal('CanvasRenderingContext2D', CanvasRenderingContext2D);
12
+ defineGlobal('DOMMatrix', DOMMatrix);
13
+ defineGlobal('DOMMatrixReadOnly', DOMMatrixReadOnly);
14
+
15
+ // Register the '2d' context factory on HTMLCanvasElement.
16
+ const CANVAS2D_KEY = Symbol.for('gjsify_canvas2d_context');
17
+ HTMLCanvasElement.registerContextFactory('2d', (canvas, options) => {
18
+ const existing = (canvas as any)[CANVAS2D_KEY];
19
+ if (existing) return existing;
20
+ const ctx = new CanvasRenderingContext2D(canvas as any, options);
21
+ (canvas as any)[CANVAS2D_KEY] = ctx;
22
+ return ctx;
23
+ });
@@ -0,0 +1,56 @@
1
+ // Registers: document, Text, Comment, DocumentFragment, DOMTokenList
2
+ // + browser environment globals: self, window, Window, focus, blur, top,
3
+ // alert, devicePixelRatio, addEventListener/removeEventListener/dispatchEvent
4
+
5
+ import { EventTarget as OurEventTarget } from '@gjsify/dom-events';
6
+
7
+ import { Comment } from '../comment.js';
8
+ import { document } from '../document.js';
9
+ import { DocumentFragment } from '../document-fragment.js';
10
+ import { DOMTokenList } from '../dom-token-list.js';
11
+ import { Text } from '../text.js';
12
+ import { defineGlobal, defineGlobalIfMissing } from './helpers.js';
13
+
14
+ defineGlobal('Text', Text);
15
+ defineGlobal('Comment', Comment);
16
+ defineGlobal('DocumentFragment', DocumentFragment);
17
+ defineGlobal('DOMTokenList', DOMTokenList);
18
+ defineGlobal('document', document);
19
+
20
+ // self — three.js checks `typeof self !== 'undefined'` for animation context
21
+ defineGlobalIfMissing('self', globalThis);
22
+
23
+ // window + Window — Excalibur's _applyDisplayMode uses `this.parent instanceof Window`
24
+ class Window {}
25
+ defineGlobalIfMissing('Window', Window);
26
+ defineGlobalIfMissing('window', globalThis);
27
+
28
+ // window.focus() / window.blur() stubs
29
+ defineGlobalIfMissing('focus', () => {});
30
+ defineGlobalIfMissing('blur', () => {});
31
+
32
+ // globalThis.addEventListener / removeEventListener / dispatchEvent
33
+ if (typeof (globalThis as any).addEventListener !== 'function') {
34
+ const _globalEventTarget = new OurEventTarget();
35
+ (globalThis as any).__gjsify_globalEventTarget = _globalEventTarget;
36
+ (globalThis as any).addEventListener = (type: string, listener: any, options?: any) =>
37
+ _globalEventTarget.addEventListener(type, listener, options);
38
+ (globalThis as any).removeEventListener = (type: string, listener: any, options?: any) =>
39
+ _globalEventTarget.removeEventListener(type, listener, options);
40
+ (globalThis as any).dispatchEvent = (event: Event) =>
41
+ _globalEventTarget.dispatchEvent(event as any);
42
+ }
43
+
44
+ // devicePixelRatio — defaults to 1 (no HiDPI scaling in GTK GL context)
45
+ defineGlobalIfMissing('devicePixelRatio', 1);
46
+
47
+ // alert — stub redirecting to console.error
48
+ defineGlobalIfMissing('alert', (...args: unknown[]) => console.error('alert:', ...args));
49
+
50
+ // window.top — prevents Excalibur's iframe detection from crashing
51
+ if (typeof (globalThis as any).top === 'undefined') {
52
+ Object.defineProperty(globalThis, 'top', {
53
+ get: () => globalThis,
54
+ configurable: true,
55
+ });
56
+ }
@@ -0,0 +1,18 @@
1
+ // Registers: FontFace + patches document.fonts
2
+
3
+ import { FontFace, FontFaceSet } from '../font-face.js';
4
+ import { defineGlobalIfMissing } from './helpers.js';
5
+
6
+ defineGlobalIfMissing('FontFace', FontFace);
7
+ if (typeof (globalThis as any).FontFace === 'undefined') {
8
+ (globalThis as any).FontFace = FontFace;
9
+ }
10
+ // Patch document.fonts stub onto the existing document object
11
+ const _doc = (globalThis as any).document;
12
+ if (_doc && typeof _doc.fonts === 'undefined') {
13
+ Object.defineProperty(_doc, 'fonts', {
14
+ value: new FontFaceSet(),
15
+ configurable: true,
16
+ writable: true,
17
+ });
18
+ }
@@ -0,0 +1,15 @@
1
+ /** Unconditionally expose a DOM class on `globalThis` (writable + configurable). */
2
+ export function defineGlobal(name: string, value: unknown): void {
3
+ Object.defineProperty(globalThis, name, {
4
+ value,
5
+ writable: true,
6
+ configurable: true,
7
+ });
8
+ }
9
+
10
+ /** Only set the global if it hasn't already been defined. */
11
+ export function defineGlobalIfMissing(name: string, value: unknown): void {
12
+ if (typeof (globalThis as any)[name] === 'undefined') {
13
+ defineGlobal(name, value);
14
+ }
15
+ }
@@ -0,0 +1,8 @@
1
+ // Registers: Image, HTMLImageElement
2
+
3
+ import { HTMLImageElement } from '../html-image-element.js';
4
+ import { Image } from '../image.js';
5
+ import { defineGlobal } from './helpers.js';
6
+
7
+ defineGlobal('HTMLImageElement', HTMLImageElement);
8
+ defineGlobal('Image', Image);
@@ -0,0 +1,6 @@
1
+ // Registers: location (file:// origin stub for GJS apps)
2
+
3
+ import { location } from '../location-stub.js';
4
+ import { defineGlobalIfMissing } from './helpers.js';
5
+
6
+ defineGlobalIfMissing('location', location);
@@ -0,0 +1,6 @@
1
+ // Registers: matchMedia
2
+
3
+ import { matchMedia } from '../match-media.js';
4
+ import { defineGlobalIfMissing } from './helpers.js';
5
+
6
+ defineGlobalIfMissing('matchMedia', matchMedia);
@@ -0,0 +1,6 @@
1
+ // Registers: navigator stub (base navigator object for GJS)
2
+ // Gamepad support (navigator.getGamepads) is provided by @gjsify/gamepad/register
3
+
4
+ if (typeof (globalThis as any).navigator === 'undefined') {
5
+ (globalThis as any).navigator = {};
6
+ }
@@ -0,0 +1,10 @@
1
+ // Registers: MutationObserver, ResizeObserver, IntersectionObserver
2
+
3
+ import { IntersectionObserver } from '../intersection-observer.js';
4
+ import { MutationObserver } from '../mutation-observer.js';
5
+ import { ResizeObserver } from '../resize-observer.js';
6
+ import { defineGlobal } from './helpers.js';
7
+
8
+ defineGlobal('MutationObserver', MutationObserver);
9
+ defineGlobal('ResizeObserver', ResizeObserver);
10
+ defineGlobal('IntersectionObserver', IntersectionObserver);