@gjsify/dom-elements 0.1.7 → 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.
- 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 -96
- 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 -0
- 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 -2
- package/lib/types/index.spec.d.ts +2 -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 -0
- package/lib/types/register.spec.d.ts +3 -0
- package/lib/types/stubs.spec.d.ts +2 -0
- package/package.json +43 -11
- 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.spec.ts +3 -4
- package/src/index.ts +13 -112
- 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 -0
- package/src/stubs.spec.ts +284 -0
- package/src/test.mts +4 -1
- package/tsconfig.json +1 -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.spec.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { describe, it, expect } from '@gjsify/unit';
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
-
Node, Element, HTMLElement, NodeType,
|
|
7
|
+
Node, Element, HTMLElement, NodeType, Attr,
|
|
8
8
|
CharacterData, Text, Comment, DocumentFragment, DOMTokenList,
|
|
9
9
|
} from '@gjsify/dom-elements';
|
|
10
10
|
import { Event } from '@gjsify/dom-events';
|
|
@@ -325,7 +325,7 @@ export default async () => {
|
|
|
325
325
|
await it('should getElementsByTagName', async () => {
|
|
326
326
|
const root = new Element();
|
|
327
327
|
const div = new Element();
|
|
328
|
-
div[Symbol.for ? Symbol.for('tagName') : 'tagName'] = 'DIV';
|
|
328
|
+
(div as any)[Symbol.for ? Symbol.for('tagName') : 'tagName'] = 'DIV';
|
|
329
329
|
// Use internal symbol access via setting attribute approach
|
|
330
330
|
// Instead, test with the proper API
|
|
331
331
|
root.appendChild(div);
|
|
@@ -336,14 +336,13 @@ export default async () => {
|
|
|
336
336
|
|
|
337
337
|
await it('should dispatch events and call on* handlers', async () => {
|
|
338
338
|
const el = new Element();
|
|
339
|
-
let handlerCalled = false;
|
|
340
339
|
let listenerCalled = false;
|
|
341
340
|
|
|
342
341
|
el.addEventListener('click', () => {
|
|
343
342
|
listenerCalled = true;
|
|
344
343
|
});
|
|
345
344
|
|
|
346
|
-
el[Symbol.for ? Symbol.for('propertyEventListeners') : 'propertyEventListeners'] = new Map();
|
|
345
|
+
(el as any)[Symbol.for ? Symbol.for('propertyEventListeners') : 'propertyEventListeners'] = new Map();
|
|
347
346
|
// Use the propertyEventListeners through proper API in HTMLElement
|
|
348
347
|
// For Element, test addEventListener only
|
|
349
348
|
el.dispatchEvent(new Event('click'));
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
// DOM element hierarchy for GJS — original implementation adapted from happy-dom
|
|
2
2
|
// Copyright (c) David Ortner (capricorn86). MIT license.
|
|
3
|
+
//
|
|
4
|
+
// Note: This module has no side effects. Importing it gives you the DOM
|
|
5
|
+
// classes as named exports but does NOT register `document`, `Image`,
|
|
6
|
+
// `HTMLCanvasElement` etc. on globalThis, and does NOT wire up the 2d
|
|
7
|
+
// canvas context factory.
|
|
8
|
+
//
|
|
9
|
+
// If you need the globals (or `canvas.getContext('2d')` to work), import
|
|
10
|
+
// `@gjsify/dom-elements/register` explicitly — or let the gjsify esbuild
|
|
11
|
+
// plugin auto-inject it based on references in your code.
|
|
3
12
|
|
|
4
13
|
export { Attr } from './attr.js';
|
|
5
14
|
export { NamedNodeMap } from './named-node-map.js';
|
|
@@ -22,115 +31,7 @@ export { IntersectionObserver } from './intersection-observer.js';
|
|
|
22
31
|
export { NodeType } from './node-type.js';
|
|
23
32
|
export { NamespaceURI } from './namespace-uri.js';
|
|
24
33
|
export * as PropertySymbol from './property-symbol.js';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
import '@gjsify/abort-controller'; // registers globalThis.AbortController + AbortSignal
|
|
30
|
-
import '@gjsify/fetch'; // registers globalThis.fetch, Request, Response, Headers
|
|
31
|
-
import { Text } from './text.js';
|
|
32
|
-
import { Comment } from './comment.js';
|
|
33
|
-
import { DocumentFragment } from './document-fragment.js';
|
|
34
|
-
import { DOMTokenList } from './dom-token-list.js';
|
|
35
|
-
import { HTMLCanvasElement } from './html-canvas-element.js';
|
|
36
|
-
import { HTMLImageElement } from './html-image-element.js';
|
|
37
|
-
import { Image } from './image.js';
|
|
38
|
-
import { document } from './document.js';
|
|
39
|
-
import { MutationObserver } from './mutation-observer.js';
|
|
40
|
-
import { ResizeObserver } from './resize-observer.js';
|
|
41
|
-
import { IntersectionObserver } from './intersection-observer.js';
|
|
42
|
-
|
|
43
|
-
Object.defineProperty(globalThis, 'Text', {
|
|
44
|
-
value: Text,
|
|
45
|
-
writable: true,
|
|
46
|
-
configurable: true,
|
|
47
|
-
});
|
|
48
|
-
Object.defineProperty(globalThis, 'Comment', {
|
|
49
|
-
value: Comment,
|
|
50
|
-
writable: true,
|
|
51
|
-
configurable: true,
|
|
52
|
-
});
|
|
53
|
-
Object.defineProperty(globalThis, 'DocumentFragment', {
|
|
54
|
-
value: DocumentFragment,
|
|
55
|
-
writable: true,
|
|
56
|
-
configurable: true,
|
|
57
|
-
});
|
|
58
|
-
Object.defineProperty(globalThis, 'DOMTokenList', {
|
|
59
|
-
value: DOMTokenList,
|
|
60
|
-
writable: true,
|
|
61
|
-
configurable: true,
|
|
62
|
-
});
|
|
63
|
-
Object.defineProperty(globalThis, 'HTMLCanvasElement', {
|
|
64
|
-
value: HTMLCanvasElement,
|
|
65
|
-
writable: true,
|
|
66
|
-
configurable: true,
|
|
67
|
-
});
|
|
68
|
-
Object.defineProperty(globalThis, 'HTMLImageElement', {
|
|
69
|
-
value: HTMLImageElement,
|
|
70
|
-
writable: true,
|
|
71
|
-
configurable: true,
|
|
72
|
-
});
|
|
73
|
-
Object.defineProperty(globalThis, 'Image', {
|
|
74
|
-
value: Image,
|
|
75
|
-
writable: true,
|
|
76
|
-
configurable: true,
|
|
77
|
-
});
|
|
78
|
-
Object.defineProperty(globalThis, 'document', {
|
|
79
|
-
value: document,
|
|
80
|
-
writable: true,
|
|
81
|
-
configurable: true,
|
|
82
|
-
});
|
|
83
|
-
Object.defineProperty(globalThis, 'MutationObserver', {
|
|
84
|
-
value: MutationObserver,
|
|
85
|
-
writable: true,
|
|
86
|
-
configurable: true,
|
|
87
|
-
});
|
|
88
|
-
Object.defineProperty(globalThis, 'ResizeObserver', {
|
|
89
|
-
value: ResizeObserver,
|
|
90
|
-
writable: true,
|
|
91
|
-
configurable: true,
|
|
92
|
-
});
|
|
93
|
-
Object.defineProperty(globalThis, 'IntersectionObserver', {
|
|
94
|
-
value: IntersectionObserver,
|
|
95
|
-
writable: true,
|
|
96
|
-
configurable: true,
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// Auto-register the '2d' context factory on HTMLCanvasElement.
|
|
100
|
-
// Mirrors browser behavior: canvas.getContext('2d') works without any explicit import.
|
|
101
|
-
// The factory is idempotent — re-registering from @gjsify/canvas2d has no effect.
|
|
102
|
-
import { CanvasRenderingContext2D } from '@gjsify/canvas2d-core';
|
|
103
|
-
|
|
104
|
-
const CANVAS2D_KEY = Symbol.for('gjsify_canvas2d_context');
|
|
105
|
-
HTMLCanvasElement.registerContextFactory('2d', (canvas, options) => {
|
|
106
|
-
const existing = (canvas as any)[CANVAS2D_KEY];
|
|
107
|
-
if (existing) return existing;
|
|
108
|
-
const ctx = new CanvasRenderingContext2D(canvas as any, options);
|
|
109
|
-
(canvas as any)[CANVAS2D_KEY] = ctx;
|
|
110
|
-
return ctx;
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
Object.defineProperty(globalThis, 'CanvasRenderingContext2D', {
|
|
114
|
-
value: CanvasRenderingContext2D,
|
|
115
|
-
writable: true,
|
|
116
|
-
configurable: true,
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// self — three.js checks `typeof self !== 'undefined'` for animation context
|
|
120
|
-
if (typeof (globalThis as any).self === 'undefined') {
|
|
121
|
-
Object.defineProperty(globalThis, 'self', { value: globalThis, writable: true, configurable: true });
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// devicePixelRatio — defaults to 1 (no HiDPI scaling in GTK GL context)
|
|
125
|
-
if (typeof (globalThis as any).devicePixelRatio === 'undefined') {
|
|
126
|
-
Object.defineProperty(globalThis, 'devicePixelRatio', { value: 1, writable: true, configurable: true });
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// alert — stub redirecting to console.error (GTK dialog version can override via writable)
|
|
130
|
-
if (typeof (globalThis as any).alert === 'undefined') {
|
|
131
|
-
Object.defineProperty(globalThis, 'alert', {
|
|
132
|
-
value: (...args: any[]) => console.error('alert:', ...args),
|
|
133
|
-
writable: true,
|
|
134
|
-
configurable: true,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
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
|
+
}
|