@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.
- package/lib/esm/dom-matrix.js +124 -0
- package/lib/esm/font-face.js +90 -0
- package/lib/esm/html-element.js +65 -1
- package/lib/esm/html-image-element.js +38 -1
- package/lib/esm/index.js +12 -1
- package/lib/esm/location-stub.js +25 -0
- package/lib/esm/match-media.js +22 -0
- package/lib/esm/register/canvas.js +16 -0
- package/lib/esm/register/document.js +36 -0
- package/lib/esm/register/font-face.js +14 -0
- package/lib/esm/register/helpers.js +16 -0
- package/lib/esm/register/image.js +5 -0
- package/lib/esm/register/location.js +3 -0
- package/lib/esm/register/match-media.js +3 -0
- package/lib/esm/register/navigator.js +3 -0
- package/lib/esm/register/observers.js +7 -0
- package/lib/esm/register.js +8 -47
- package/lib/types/dom-matrix.d.ts +64 -0
- package/lib/types/font-face.d.ts +45 -0
- package/lib/types/html-element.d.ts +10 -1
- package/lib/types/html-image-element.spec.d.ts +2 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/location-stub.d.ts +21 -0
- package/lib/types/match-media.d.ts +12 -0
- package/lib/types/register/canvas.d.ts +1 -0
- package/lib/types/register/document.d.ts +1 -0
- package/lib/types/register/font-face.d.ts +1 -0
- package/lib/types/register/helpers.d.ts +4 -0
- package/lib/types/register/image.d.ts +1 -0
- package/lib/types/register/location.d.ts +1 -0
- package/lib/types/register/match-media.d.ts +1 -0
- package/lib/types/register/navigator.d.ts +1 -0
- package/lib/types/register/observers.d.ts +1 -0
- package/lib/types/register.d.ts +8 -1
- package/lib/types/register.spec.d.ts +3 -0
- package/lib/types/stubs.spec.d.ts +2 -0
- package/package.json +37 -12
- package/src/dom-matrix.ts +109 -0
- package/src/font-face.ts +97 -0
- package/src/html-element.ts +64 -1
- package/src/html-image-element.spec.ts +285 -0
- package/src/html-image-element.ts +43 -2
- package/src/index.ts +4 -0
- package/src/location-stub.ts +20 -0
- package/src/match-media.ts +32 -0
- package/src/register/canvas.ts +23 -0
- package/src/register/document.ts +56 -0
- package/src/register/font-face.ts +18 -0
- package/src/register/helpers.ts +15 -0
- package/src/register/image.ts +8 -0
- package/src/register/location.ts +6 -0
- package/src/register/match-media.ts +6 -0
- package/src/register/navigator.ts +6 -0
- package/src/register/observers.ts +10 -0
- package/src/register.spec.ts +115 -0
- package/src/register.ts +13 -72
- package/src/stubs.spec.ts +284 -0
- package/src/test.mts +4 -1
- 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://')
|
|
126
|
-
// Remote URLs
|
|
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,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);
|