@playcanvas/web-components 0.8.2 → 0.10.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 (79) hide show
  1. package/README.md +18 -0
  2. package/dist/app.d.ts +13 -4
  3. package/dist/asset.d.ts +33 -3
  4. package/dist/async-element.d.ts +54 -4
  5. package/dist/components/button-component.d.ts +11 -5
  6. package/dist/components/camera-component.d.ts +10 -5
  7. package/dist/components/collision-component.d.ts +10 -5
  8. package/dist/components/component.d.ts +8 -2
  9. package/dist/components/element-component.d.ts +18 -13
  10. package/dist/components/gsplat-component.d.ts +7 -2
  11. package/dist/components/layoutchild-component.d.ts +7 -2
  12. package/dist/components/layoutgroup-component.d.ts +22 -16
  13. package/dist/components/light-component.d.ts +13 -7
  14. package/dist/components/listener-component.d.ts +6 -1
  15. package/dist/components/particlesystem-component.d.ts +7 -2
  16. package/dist/components/render-component.d.ts +14 -5
  17. package/dist/components/rigidbody-component.d.ts +10 -5
  18. package/dist/components/screen-component.d.ts +7 -2
  19. package/dist/components/script-component.d.ts +116 -16
  20. package/dist/components/script.d.ts +79 -8
  21. package/dist/components/scrollbar-component.d.ts +11 -5
  22. package/dist/components/scrollview-component.d.ts +18 -11
  23. package/dist/components/sound-component.d.ts +7 -2
  24. package/dist/components/sound-slot.d.ts +17 -4
  25. package/dist/custom-elements.json +16231 -0
  26. package/dist/entity.d.ts +45 -3
  27. package/dist/index.d.ts +3 -2
  28. package/dist/material.d.ts +976 -4
  29. package/dist/model.d.ts +6 -1
  30. package/dist/module.d.ts +15 -0
  31. package/dist/parse.d.ts +144 -0
  32. package/dist/pwc.cjs +5735 -2923
  33. package/dist/pwc.cjs.map +1 -1
  34. package/dist/pwc.js +5735 -2923
  35. package/dist/pwc.js.map +1 -1
  36. package/dist/pwc.min.js +1 -1
  37. package/dist/pwc.min.js.map +1 -1
  38. package/dist/pwc.min.mjs +2 -0
  39. package/dist/pwc.min.mjs.map +1 -0
  40. package/dist/pwc.mjs +5736 -2925
  41. package/dist/pwc.mjs.map +1 -1
  42. package/dist/scene.d.ts +15 -6
  43. package/dist/sky.d.ts +7 -1
  44. package/dist/vscode.html-custom-data.json +1795 -0
  45. package/dist/web-types.json +3592 -0
  46. package/package.json +32 -14
  47. package/src/app.ts +42 -15
  48. package/src/asset.ts +78 -8
  49. package/src/async-element.ts +89 -5
  50. package/src/components/button-component.ts +31 -24
  51. package/src/components/camera-component.ts +30 -24
  52. package/src/components/collision-component.ts +20 -14
  53. package/src/components/component.ts +33 -14
  54. package/src/components/element-component.ts +62 -56
  55. package/src/components/gsplat-component.ts +16 -9
  56. package/src/components/layoutchild-component.ts +17 -10
  57. package/src/components/layoutgroup-component.ts +39 -32
  58. package/src/components/light-component.ts +34 -32
  59. package/src/components/listener-component.ts +8 -2
  60. package/src/components/particlesystem-component.ts +10 -4
  61. package/src/components/render-component.ts +28 -12
  62. package/src/components/rigidbody-component.ts +22 -16
  63. package/src/components/screen-component.ts +16 -10
  64. package/src/components/script-component.ts +512 -125
  65. package/src/components/script.ts +123 -16
  66. package/src/components/scrollbar-component.ts +21 -14
  67. package/src/components/scrollview-component.ts +42 -34
  68. package/src/components/sound-component.ts +17 -10
  69. package/src/components/sound-slot.ts +50 -19
  70. package/src/entity.ts +84 -76
  71. package/src/index.ts +5 -2
  72. package/src/material.ts +2432 -62
  73. package/src/model.ts +8 -2
  74. package/src/module.ts +16 -0
  75. package/src/parse.ts +298 -0
  76. package/src/scene.ts +26 -13
  77. package/src/sky.ts +36 -18
  78. package/dist/utils.d.ts +0 -56
  79. package/src/utils.ts +0 -119
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcanvas/web-components",
3
- "version": "0.8.2",
3
+ "version": "0.10.0",
4
4
  "author": "PlayCanvas <support@playcanvas.com>",
5
5
  "homepage": "https://playcanvas.com",
6
6
  "description": "Web Components for the PlayCanvas Engine",
@@ -25,8 +25,11 @@
25
25
  },
26
26
  "main": "dist/pwc.cjs",
27
27
  "module": "dist/pwc.mjs",
28
- "browser": "dist/pwc.js",
28
+ "unpkg": "dist/pwc.min.js",
29
+ "jsdelivr": "dist/pwc.min.js",
29
30
  "types": "dist/index.d.ts",
31
+ "customElements": "dist/custom-elements.json",
32
+ "web-types": "dist/web-types.json",
30
33
  "exports": {
31
34
  ".": {
32
35
  "types": "./dist/index.d.ts",
@@ -44,21 +47,31 @@
44
47
  "scripts": {
45
48
  "prebuild": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
46
49
  "build": "rollup -c",
50
+ "postbuild": "npm run cem",
51
+ "cem": "cem analyze && node utils/cem/validate.mjs",
47
52
  "dev": "concurrently \"npm run watch\" \"npm run serve\"",
48
53
  "docs": "typedoc",
49
- "lint": "eslint examples/js examples/assets/scripts src",
54
+ "lint": "eslint examples/js examples/assets/scripts src test vitest.config.ts",
50
55
  "serve": "serve",
51
- "test": "echo \"Error: no test specified\" && exit 1",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest",
58
+ "test:coverage": "vitest run --coverage",
59
+ "test:unit": "vitest run --project unit",
60
+ "test:elements": "vitest run --project elements",
61
+ "test:integration": "vitest run --project integration",
52
62
  "publint": "publint",
53
- "type-check": "tsc --noEmit",
54
- "type-check:watch": "npm run type-check -- --watch",
63
+ "type-check": "npm run type-check:src && npm run type-check:test",
64
+ "type-check:src": "tsc --noEmit -p tsconfig.json",
65
+ "type-check:test": "tsc --noEmit -p tsconfig.test.json",
66
+ "type-check:watch": "npm run type-check:src -- --watch",
55
67
  "watch": "rollup -c -w"
56
68
  },
57
69
  "peerDependencies": {
58
70
  "playcanvas": "^2.20.1"
59
71
  },
60
72
  "devDependencies": {
61
- "@mediapipe/tasks-vision": "0.10.35",
73
+ "@custom-elements-manifest/analyzer": "0.11.0",
74
+ "@mediapipe/tasks-vision": "1.0.1",
62
75
  "@playcanvas/eslint-config": "2.1.0",
63
76
  "@rollup/plugin-commonjs": "29.0.3",
64
77
  "@rollup/plugin-node-resolve": "16.0.3",
@@ -67,20 +80,25 @@
67
80
  "@tweenjs/tween.js": "25.0.0",
68
81
  "@typescript-eslint/eslint-plugin": "8.65.0",
69
82
  "@typescript-eslint/parser": "8.65.0",
70
- "concurrently": "10.0.3",
83
+ "@vitest/coverage-v8": "4.1.10",
84
+ "concurrently": "10.0.4",
85
+ "custom-element-jet-brains-integration": "1.7.0",
86
+ "custom-element-vs-code-integration": "1.5.0",
71
87
  "earcut": "3.2.3",
72
88
  "eslint": "9.39.5",
73
89
  "eslint-import-resolver-typescript": "4.4.5",
74
- "globals": "17.7.0",
75
- "mediabunny": "1.51.0",
90
+ "globals": "17.9.0",
91
+ "jsdom": "30.0.1",
92
+ "mediabunny": "1.52.2",
76
93
  "opentype.js": "2.0.0",
77
- "playcanvas": "2.21.0",
78
- "publint": "0.3.21",
79
- "rollup": "4.62.2",
94
+ "playcanvas": "2.21.3",
95
+ "publint": "0.3.22",
96
+ "rollup": "4.62.4",
80
97
  "serve": "14.2.6",
81
98
  "tslib": "2.8.1",
82
99
  "typedoc": "0.28.20",
83
100
  "typedoc-plugin-mdn-links": "5.1.1",
84
- "typescript": "6.0.3"
101
+ "typescript": "6.0.3",
102
+ "vitest": "4.1.10"
85
103
  }
86
104
  }
package/src/app.ts CHANGED
@@ -71,6 +71,7 @@ import { AsyncElement } from './async-element';
71
71
  import { EntityElement } from './entity';
72
72
  import { MaterialElement } from './material';
73
73
  import { ModuleElement } from './module';
74
+ import { parseBool, parseEnum } from './parse';
74
75
 
75
76
  /**
76
77
  * The AppElement interface provides properties and methods for manipulating
@@ -86,7 +87,7 @@ class AppElement extends AsyncElement {
86
87
 
87
88
  private _alpha = true;
88
89
 
89
- private _backend: 'webgpu' | 'webgl2' | 'null' = 'webgl2';
90
+ private _backend: 'webgpu' | 'webgl2' | 'null' = 'webgpu';
90
91
 
91
92
  private _antialias = true;
92
93
 
@@ -116,10 +117,16 @@ class AppElement extends AsyncElement {
116
117
  pointerup: null
117
118
  };
118
119
 
120
+ private _app: AppBase | null = null;
121
+
119
122
  /**
120
- * The PlayCanvas application instance.
123
+ * The PlayCanvas application instance. Available once the element is ready — await
124
+ * {@link whenReady} or the element's `ready()` promise before accessing it.
125
+ * @returns The application instance.
121
126
  */
122
- app: AppBase | null = null;
127
+ get app(): AppBase {
128
+ return this._app!;
129
+ }
123
130
 
124
131
  /**
125
132
  * Creates a new AppElement instance.
@@ -227,7 +234,7 @@ class AppElement extends AsyncElement {
227
234
  createOptions.batchManager = BatchManager;
228
235
  createOptions.xr = XrManager;
229
236
 
230
- this.app = new AppBase(this._canvas);
237
+ this._app = new AppBase(this._canvas);
231
238
  this.app.init(createOptions);
232
239
 
233
240
  this.app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
@@ -282,7 +289,7 @@ class AppElement extends AsyncElement {
282
289
  // Clean up the application
283
290
  if (this.app) {
284
291
  this.app.destroy();
285
- this.app = null;
292
+ this._app = null;
286
293
  }
287
294
 
288
295
  // Remove event listeners
@@ -314,6 +321,14 @@ class AppElement extends AsyncElement {
314
321
  ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'].forEach((type) => {
315
322
  this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
316
323
  this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
324
+
325
+ // Attach canvas handlers for listeners registered before this point (e.g. handlers
326
+ // created from onpointer* attributes when their elements were first upgraded)
327
+ const anyListeners = Array.from(this.querySelectorAll<EntityElement>('pc-entity'))
328
+ .some(entity => entity.hasListeners(type));
329
+ if (anyListeners) {
330
+ this._onPointerListenerAdded(type);
331
+ }
317
332
  });
318
333
  }
319
334
 
@@ -332,6 +347,13 @@ class AppElement extends AsyncElement {
332
347
  pointerdown: null,
333
348
  pointerup: null
334
349
  };
350
+ this._hasPointerListeners = {
351
+ pointerenter: false,
352
+ pointerleave: false,
353
+ pointerdown: false,
354
+ pointerup: false,
355
+ pointermove: false
356
+ };
335
357
  }
336
358
 
337
359
  // New helper to convert CSS coordinates to canvas (picker) coordinates
@@ -506,7 +528,8 @@ class AppElement extends AsyncElement {
506
528
  }
507
529
 
508
530
  /**
509
- * Sets the graphics backend.
531
+ * Sets the graphics backend. Defaults to 'webgpu', which falls back to 'webgl2' if WebGPU
532
+ * is not supported by the browser.
510
533
  * @param value - The graphics backend ('webgpu', 'webgl2', or 'null').
511
534
  */
512
535
  set backend(value: 'webgpu' | 'webgl2' | 'null') {
@@ -586,27 +609,25 @@ class AppElement extends AsyncElement {
586
609
  return ['alpha', 'antialias', 'backend', 'depth', 'stencil', 'high-resolution'];
587
610
  }
588
611
 
589
- attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
612
+ attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
590
613
  switch (name) {
591
614
  case 'alpha':
592
- this.alpha = newValue !== 'false';
615
+ this.alpha = parseBool(newValue, true);
593
616
  break;
594
617
  case 'antialias':
595
- this.antialias = newValue !== 'false';
618
+ this.antialias = parseBool(newValue, true);
596
619
  break;
597
620
  case 'backend':
598
- if (newValue === 'webgpu' || newValue === 'webgl2' || newValue === 'null') {
599
- this.backend = newValue;
600
- }
621
+ this.backend = parseEnum(newValue, ['webgpu', 'webgl2', 'null'], 'webgpu', name);
601
622
  break;
602
623
  case 'depth':
603
- this.depth = newValue !== 'false';
624
+ this.depth = parseBool(newValue, true);
604
625
  break;
605
626
  case 'high-resolution':
606
- this.highResolution = newValue !== 'false';
627
+ this.highResolution = parseBool(newValue, true);
607
628
  break;
608
629
  case 'stencil':
609
- this.stencil = newValue !== 'false';
630
+ this.stencil = parseBool(newValue, true);
610
631
  break;
611
632
  }
612
633
  }
@@ -614,4 +635,10 @@ class AppElement extends AsyncElement {
614
635
 
615
636
  customElements.define('pc-app', AppElement);
616
637
 
638
+ declare global {
639
+ interface HTMLElementTagNameMap {
640
+ 'pc-app': AppElement;
641
+ }
642
+ }
643
+
617
644
  export { AppElement };
package/src/asset.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { Asset, SPRITE_RENDERMODE_SIMPLE, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED } from 'playcanvas';
2
2
 
3
- import { parseEnum } from './utils';
3
+ import { AsyncElement } from './async-element';
4
+ import { parseBool, parseEnum, parseNumber } from './parse';
4
5
  import { MeshoptDecoder } from '../lib/meshopt_decoder.module.js';
5
6
 
6
- const renderModes = new Map<string, number>([
7
+ const renderModes = new Map<'simple' | 'sliced' | 'tiled', number>([
7
8
  ['simple', SPRITE_RENDERMODE_SIMPLE],
8
9
  ['sliced', SPRITE_RENDERMODE_SLICED],
9
10
  ['tiled', SPRITE_RENDERMODE_TILED]
@@ -15,11 +16,13 @@ const extToType = new Map([
15
16
  ['frag', 'shader'],
16
17
  ['glb', 'container'],
17
18
  ['glsl', 'shader'],
19
+ ['gltf', 'container'],
18
20
  ['hdr', 'texture'],
19
21
  ['html', 'html'],
20
22
  ['jpg', 'texture'],
21
23
  ['js', 'script'],
22
24
  ['json', 'json'],
25
+ ['ktx2', 'texture'],
23
26
  ['mp3', 'audio'],
24
27
  ['mjs', 'script'],
25
28
  ['ply', 'gsplat'],
@@ -74,15 +77,74 @@ const processBufferView = (
74
77
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-asset/ | `<pc-asset>`} elements.
75
78
  * The AssetElement interface also inherits the properties and methods of the
76
79
  * {@link HTMLElement} interface.
80
+ *
81
+ * The element becomes ready once the containing application has started and the asset is in the
82
+ * state declared by the markup: loaded for preloaded assets (even if loading failed — check the
83
+ * asset's `resource`), or registered and awaiting a load for `lazy` assets. Elements inserted
84
+ * while the application is running are created and registered on insertion, and begin loading
85
+ * immediately unless `lazy`. A `pc-asset` must be a direct child of `pc-app` — elements placed
86
+ * elsewhere, or with an unsupported asset type, never become ready.
87
+ *
88
+ * Apart from `lazy`, these attributes are read once when the asset is created, so changing them
89
+ * later has no effect.
90
+ *
91
+ * @attribute {string} id - The identifier used to reference the asset from other elements.
92
+ * @attribute {string} src - The URL of the asset to load.
93
+ * @attribute {string} type - The asset type. Inferred from the `src` file extension when omitted.
94
+ * @attribute {string} data - Additional asset data, as a JSON object.
95
+ * @attribute {string} atlas - For a `sprite` asset, the `id` of the texture atlas asset it uses.
96
+ * The atlas must be declared before the sprite.
97
+ * @attribute {string} frame-keys - For a `sprite` asset, the atlas frame keys it uses, separated
98
+ * by spaces or commas.
99
+ * @attribute {number} pixels-per-unit - For a `sprite` asset, the number of pixels per world unit.
100
+ * @attribute {'simple' | 'sliced' | 'tiled'} render-mode - For a `sprite` asset, how the sprite is
101
+ * rendered when resized.
77
102
  */
78
- class AssetElement extends HTMLElement {
103
+ class AssetElement extends AsyncElement {
79
104
  private _lazy: boolean = false;
80
105
 
81
106
  /**
82
- * The asset that is loaded.
107
+ * The asset that is loaded. Available once the element is ready — await
108
+ * {@link whenReady} or the element's `ready()` promise before accessing it.
83
109
  */
84
110
  asset: Asset | null = null;
85
111
 
112
+ async connectedCallback() {
113
+ const appElement = this.closestApp;
114
+ if (!appElement) return;
115
+
116
+ // Assets must be direct children of pc-app (matches the boot query ':scope > pc-asset')
117
+ if (this.parentElement !== appElement) {
118
+ console.warn(`pc-asset '${this.getAttribute('id') ?? this.getAttribute('src')}' must be a direct child of pc-app - asset not created`);
119
+ return;
120
+ }
121
+
122
+ await appElement.ready();
123
+
124
+ // The element may have been removed or re-parented while waiting for the app
125
+ if (!this.isConnected || this.parentElement !== appElement) return;
126
+
127
+ // Assets present at startup are created by AppElement's boot; this branch handles
128
+ // elements inserted (or re-inserted) after the app is already running
129
+ if (!this.asset) {
130
+ const app = appElement.app;
131
+ if (!app) return; // pc-app is re-connecting; its own boot will create this asset
132
+
133
+ this.createAsset();
134
+ if (this.asset) {
135
+ app.assets.add(this.asset); // add() auto-loads when preload is true
136
+ if (!this.lazy) {
137
+ app.assets.load(this.asset);
138
+ }
139
+ }
140
+ }
141
+
142
+ // Never ready if createAsset failed (unsupported asset type)
143
+ if (this.asset) {
144
+ this._onReady();
145
+ }
146
+ }
147
+
86
148
  disconnectedCallback() {
87
149
  this.destroyAsset();
88
150
  }
@@ -167,12 +229,12 @@ class AssetElement extends HTMLElement {
167
229
 
168
230
  const pixelsPerUnit = this.getAttribute('pixels-per-unit');
169
231
  if (pixelsPerUnit !== null) {
170
- data.pixelsPerUnit = Number(pixelsPerUnit);
232
+ data.pixelsPerUnit = parseNumber(pixelsPerUnit, 1, 'pixels-per-unit');
171
233
  }
172
234
 
173
235
  const renderMode = this.getAttribute('render-mode');
174
236
  if (renderMode !== null) {
175
- data.renderMode = parseEnum(renderMode, renderModes, SPRITE_RENDERMODE_SIMPLE);
237
+ data.renderMode = renderModes.get(parseEnum(renderMode, renderModes, 'simple', 'render-mode'));
176
238
  }
177
239
 
178
240
  // Apply engine defaults for any values not supplied.
@@ -187,6 +249,8 @@ class AssetElement extends HTMLElement {
187
249
 
188
250
  destroyAsset() {
189
251
  if (this.asset) {
252
+ // Deregister first so unload() can still notify the registry
253
+ this.asset.registry?.remove(this.asset);
190
254
  this.asset.unload();
191
255
  this.asset = null;
192
256
  }
@@ -220,13 +284,19 @@ class AssetElement extends HTMLElement {
220
284
  return ['lazy'];
221
285
  }
222
286
 
223
- attributeChangedCallback(name: string, _oldValue: string, _newValue: string) {
287
+ attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
224
288
  if (name === 'lazy') {
225
- this.lazy = this.hasAttribute('lazy');
289
+ this.lazy = parseBool(newValue, false);
226
290
  }
227
291
  }
228
292
  }
229
293
 
230
294
  customElements.define('pc-asset', AssetElement);
231
295
 
296
+ declare global {
297
+ interface HTMLElementTagNameMap {
298
+ 'pc-asset': AssetElement;
299
+ }
300
+ }
301
+
232
302
  export { AssetElement };
@@ -3,6 +3,9 @@ import { EntityElement } from './entity';
3
3
 
4
4
  /**
5
5
  * Base class for all PlayCanvas Web Components that initialize asynchronously.
6
+ *
7
+ * @fires {CustomEvent} ready - Fired once the element is fully initialized. Bubbles and is
8
+ * composed.
6
9
  */
7
10
  class AsyncElement extends HTMLElement {
8
11
  private _readyPromise: Promise<void>;
@@ -26,16 +29,18 @@ class AsyncElement extends HTMLElement {
26
29
  }
27
30
 
28
31
  /**
29
- * Called when the element is fully initialized and ready.
30
- * Subclasses should call this when they're ready.
32
+ * Called when the element is fully initialized and ready. Subclasses should call this when
33
+ * they're ready. Resolves the ready promise and dispatches a bubbling, composed `ready`
34
+ * event.
31
35
  */
32
36
  protected _onReady() {
33
37
  this._readyResolve();
34
- this.dispatchEvent(new CustomEvent('ready'));
38
+ this.dispatchEvent(new CustomEvent('ready', { bubbles: true, composed: true }));
35
39
  }
36
40
 
37
41
  /**
38
- * Returns a promise that resolves with this element when it's ready.
42
+ * Returns a promise that resolves with this element when it's ready. This is the low-level
43
+ * primitive underlying {@link whenReady}, which is the recommended way to wait for elements.
39
44
  * @returns A promise that resolves with this element when it's ready.
40
45
  */
41
46
  ready(): Promise<this> {
@@ -43,4 +48,83 @@ class AsyncElement extends HTMLElement {
43
48
  }
44
49
  }
45
50
 
46
- export { AsyncElement };
51
+ /**
52
+ * A union of the tag names of all elements that initialize asynchronously (i.e. elements whose
53
+ * classes extend {@link AsyncElement}).
54
+ */
55
+ type AsyncElementTagName = {
56
+ [K in keyof HTMLElementTagNameMap]: HTMLElementTagNameMap[K] extends AsyncElement ? K : never
57
+ }[keyof HTMLElementTagNameMap];
58
+
59
+ /**
60
+ * Waits for the first element matching the given tag name to be fully initialized. Note that the
61
+ * promise never settles if the element cannot finish initializing (for example, a `<pc-script>`
62
+ * that is not a direct child of `<pc-scripts>`). A component element outside a `<pc-entity>` is
63
+ * the exception: it still becomes ready, but its `component` is `null`. Either way, a misplaced
64
+ * element logs a warning naming the parent it requires.
65
+ * @param target - The tag name of the element to wait for (e.g. `'pc-app'`).
66
+ * @returns A promise that resolves with the element once it's ready.
67
+ * @example
68
+ * const { app } = await whenReady('pc-app');
69
+ */
70
+ function whenReady<K extends AsyncElementTagName>(target: K): Promise<HTMLElementTagNameMap[K]>;
71
+ /**
72
+ * Waits for the given element to be fully initialized. Note that the promise never settles if
73
+ * the element cannot finish initializing (for example, an element that is never added to the
74
+ * document).
75
+ * @param target - The element to wait for.
76
+ * @returns A promise that resolves with the element once it's ready.
77
+ * @example
78
+ * const appElement = document.createElement('pc-app');
79
+ * document.body.appendChild(appElement);
80
+ * const { app } = await whenReady(appElement);
81
+ */
82
+ function whenReady<T extends AsyncElement>(target: T): Promise<T>;
83
+ /**
84
+ * Waits for the first element matching the given CSS selector to be fully initialized. Note that
85
+ * the promise never settles if the element cannot finish initializing (for example, a `<pc-script>`
86
+ * that is not a direct child of `<pc-scripts>`). A component element outside a `<pc-entity>` is
87
+ * the exception: it still becomes ready, but its `component` is `null`. Either way, a misplaced
88
+ * element logs a warning naming the parent it requires.
89
+ * @param target - A CSS selector matching the element to wait for (e.g. `'#my-app'`).
90
+ * @returns A promise that resolves with the element once it's ready.
91
+ * @example
92
+ * // In TypeScript, supply the element type when using an arbitrary selector
93
+ * const { entity } = await whenReady<EntityElement>('pc-entity[name="camera"]');
94
+ */
95
+ function whenReady<T extends AsyncElement = AsyncElement, S extends string = string>(
96
+ target: S extends Exclude<keyof HTMLElementTagNameMap, AsyncElementTagName> ? never : S
97
+ ): Promise<T>;
98
+ async function whenReady(target: string | AsyncElement): Promise<AsyncElement> {
99
+ let element: Element | null;
100
+ if (typeof target === 'string') {
101
+ if (document.readyState === 'loading') {
102
+ await new Promise((resolve) => {
103
+ document.addEventListener('DOMContentLoaded', resolve, { once: true });
104
+ });
105
+ }
106
+
107
+ try {
108
+ element = document.querySelector(target);
109
+ } catch {
110
+ throw new Error(`whenReady: '${target}' is not a valid CSS selector`);
111
+ }
112
+ if (!element) {
113
+ throw new Error(`whenReady: no element found matching '${target}'`);
114
+ }
115
+ } else {
116
+ element = target;
117
+ }
118
+
119
+ if (!(element instanceof AsyncElement)) {
120
+ const description = element instanceof Element ? `<${element.tagName.toLowerCase()}>` : String(target);
121
+ throw new Error(`whenReady: ${description} does not initialize asynchronously`);
122
+ }
123
+
124
+ await element.ready();
125
+
126
+ return element;
127
+ }
128
+
129
+ export { AsyncElement, whenReady };
130
+ export type { AsyncElementTagName };
@@ -2,9 +2,9 @@ import { BUTTON_TRANSITION_MODE_SPRITE_CHANGE, BUTTON_TRANSITION_MODE_TINT, Butt
2
2
 
3
3
  import { AssetElement } from '../asset';
4
4
  import { ComponentElement } from './component';
5
- import { getEntity, parseColor, parseEnum, parseVec4 } from '../utils';
5
+ import { getEntity, parseBool, parseColor, parseEnum, parseNumber, parseVec4 } from '../parse';
6
6
 
7
- const transitionModes = new Map<string, number>([
7
+ const transitionModes = new Map<'tint' | 'sprite', number>([
8
8
  ['tint', BUTTON_TRANSITION_MODE_TINT],
9
9
  ['sprite', BUTTON_TRANSITION_MODE_SPRITE_CHANGE]
10
10
  ]);
@@ -24,7 +24,7 @@ class ButtonComponentElement extends ComponentElement {
24
24
 
25
25
  private _hitPadding = new Vec4(0, 0, 0, 0);
26
26
 
27
- private _transitionMode: number = BUTTON_TRANSITION_MODE_TINT;
27
+ private _transitionMode: 'tint' | 'sprite' = 'tint';
28
28
 
29
29
  private _hoverTint = new Color(1, 1, 1, 1);
30
30
 
@@ -55,7 +55,7 @@ class ButtonComponentElement extends ComponentElement {
55
55
  const data: Record<string, any> = {
56
56
  active: this._active,
57
57
  hitPadding: this._hitPadding,
58
- transitionMode: this._transitionMode,
58
+ transitionMode: transitionModes.get(this._transitionMode),
59
59
  hoverTint: this._hoverTint,
60
60
  pressedTint: this._pressedTint,
61
61
  inactiveTint: this._inactiveTint,
@@ -94,8 +94,8 @@ class ButtonComponentElement extends ComponentElement {
94
94
  * Gets the underlying PlayCanvas button component.
95
95
  * @returns The button component.
96
96
  */
97
- get component(): ButtonComponent | null {
98
- return super.component as ButtonComponent | null;
97
+ get component(): ButtonComponent {
98
+ return super.component as ButtonComponent;
99
99
  }
100
100
 
101
101
  /**
@@ -158,13 +158,14 @@ class ButtonComponentElement extends ComponentElement {
158
158
  }
159
159
 
160
160
  /**
161
- * Sets how the button reacts to being hovered/pressed. Can be `tint` (0) or `sprite` (1).
161
+ * Sets how the button reacts to being hovered/pressed. Can be `tint` or `sprite`. Defaults to
162
+ * `tint`.
162
163
  * @param value - The transition mode.
163
164
  */
164
- set transitionMode(value: number) {
165
+ set transitionMode(value: 'tint' | 'sprite') {
165
166
  this._transitionMode = value;
166
167
  if (this.component) {
167
- this.component.transitionMode = value;
168
+ this.component.transitionMode = transitionModes.get(value) ?? BUTTON_TRANSITION_MODE_TINT;
168
169
  }
169
170
  }
170
171
 
@@ -395,51 +396,51 @@ class ButtonComponentElement extends ComponentElement {
395
396
  ];
396
397
  }
397
398
 
398
- attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
399
+ attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
399
400
  super.attributeChangedCallback(name, _oldValue, newValue);
400
401
 
401
402
  switch (name) {
402
403
  case 'active':
403
- this.active = newValue !== 'false';
404
+ this.active = parseBool(newValue, true);
404
405
  break;
405
406
  case 'image':
406
- this.image = newValue;
407
+ this.image = newValue ?? '';
407
408
  break;
408
409
  case 'hit-padding':
409
- this.hitPadding = parseVec4(newValue);
410
+ this.hitPadding = parseVec4(newValue, Vec4.ZERO, name);
410
411
  break;
411
412
  case 'transition-mode':
412
- this.transitionMode = parseEnum(newValue, transitionModes, BUTTON_TRANSITION_MODE_TINT);
413
+ this.transitionMode = parseEnum(newValue, transitionModes, 'tint', name);
413
414
  break;
414
415
  case 'hover-tint':
415
- this.hoverTint = parseColor(newValue);
416
+ this.hoverTint = parseColor(newValue, Color.WHITE, name);
416
417
  break;
417
418
  case 'pressed-tint':
418
- this.pressedTint = parseColor(newValue);
419
+ this.pressedTint = parseColor(newValue, Color.WHITE, name);
419
420
  break;
420
421
  case 'inactive-tint':
421
- this.inactiveTint = parseColor(newValue);
422
+ this.inactiveTint = parseColor(newValue, Color.WHITE, name);
422
423
  break;
423
424
  case 'fade-duration':
424
- this.fadeDuration = Number(newValue);
425
+ this.fadeDuration = parseNumber(newValue, 0, name);
425
426
  break;
426
427
  case 'hover-sprite-asset':
427
- this.hoverSpriteAsset = newValue;
428
+ this.hoverSpriteAsset = newValue ?? '';
428
429
  break;
429
430
  case 'hover-sprite-frame':
430
- this.hoverSpriteFrame = Number(newValue);
431
+ this.hoverSpriteFrame = parseNumber(newValue, 0, name);
431
432
  break;
432
433
  case 'pressed-sprite-asset':
433
- this.pressedSpriteAsset = newValue;
434
+ this.pressedSpriteAsset = newValue ?? '';
434
435
  break;
435
436
  case 'pressed-sprite-frame':
436
- this.pressedSpriteFrame = Number(newValue);
437
+ this.pressedSpriteFrame = parseNumber(newValue, 0, name);
437
438
  break;
438
439
  case 'inactive-sprite-asset':
439
- this.inactiveSpriteAsset = newValue;
440
+ this.inactiveSpriteAsset = newValue ?? '';
440
441
  break;
441
442
  case 'inactive-sprite-frame':
442
- this.inactiveSpriteFrame = Number(newValue);
443
+ this.inactiveSpriteFrame = parseNumber(newValue, 0, name);
443
444
  break;
444
445
  }
445
446
  }
@@ -447,4 +448,10 @@ class ButtonComponentElement extends ComponentElement {
447
448
 
448
449
  customElements.define('pc-button', ButtonComponentElement);
449
450
 
451
+ declare global {
452
+ interface HTMLElementTagNameMap {
453
+ 'pc-button': ButtonComponentElement;
454
+ }
455
+ }
456
+
450
457
  export { ButtonComponentElement };