@playcanvas/web-components 0.19.0 → 0.20.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.
- package/dist/app.d.cts +14 -0
- package/dist/app.d.ts +14 -0
- package/dist/components/button-component.d.cts +9 -5
- package/dist/components/button-component.d.ts +9 -5
- package/dist/components/joint-component.d.cts +24 -10
- package/dist/components/joint-component.d.ts +24 -10
- package/dist/components/script-component.d.cts +4 -2
- package/dist/components/script-component.d.ts +4 -2
- package/dist/components/script-instance.d.cts +14 -6
- package/dist/components/script-instance.d.ts +14 -6
- package/dist/components/scroll-view-component.d.cts +24 -12
- package/dist/components/scroll-view-component.d.ts +24 -12
- package/dist/components/scrollbar-component.d.cts +6 -3
- package/dist/components/scrollbar-component.d.ts +6 -3
- package/dist/custom-elements.json +21 -21
- package/dist/parse.d.cts +7 -2
- package/dist/parse.d.ts +7 -2
- package/dist/pwc.cjs +406 -128
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +406 -128
- package/dist/pwc.js.map +1 -1
- package/dist/pwc.min.js +1 -1
- package/dist/pwc.min.js.map +1 -1
- package/dist/pwc.min.mjs +1 -1
- package/dist/pwc.min.mjs.map +1 -1
- package/dist/pwc.mjs +406 -128
- package/dist/pwc.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +10 -10
- package/dist/web-types.json +20 -20
- package/package.json +3 -3
- package/src/app.ts +76 -41
- package/src/components/button-component.ts +18 -10
- package/src/components/joint-component.ts +29 -15
- package/src/components/script-component.ts +25 -12
- package/src/components/script-instance.ts +14 -6
- package/src/components/scroll-view-component.ts +49 -29
- package/src/components/scrollbar-component.ts +13 -8
- package/src/parse.ts +213 -16
|
@@ -3,7 +3,9 @@ import { Color, Quat, Vec2, Vec3, Vec4 } from 'playcanvas';
|
|
|
3
3
|
|
|
4
4
|
import { useAsset } from '../asset';
|
|
5
5
|
import {
|
|
6
|
+
findEntityElement,
|
|
6
7
|
getEntity,
|
|
8
|
+
idHint,
|
|
7
9
|
parseBool,
|
|
8
10
|
parseColor,
|
|
9
11
|
parseComponents,
|
|
@@ -11,7 +13,8 @@ import {
|
|
|
11
13
|
parseQuat,
|
|
12
14
|
parseVec2,
|
|
13
15
|
parseVec3,
|
|
14
|
-
parseVec4
|
|
16
|
+
parseVec4,
|
|
17
|
+
unresolvedCause
|
|
15
18
|
} from '../parse';
|
|
16
19
|
|
|
17
20
|
import { ComponentElement } from './component';
|
|
@@ -107,10 +110,11 @@ const camelToKebab = (name: string): string => {
|
|
|
107
110
|
|
|
108
111
|
/**
|
|
109
112
|
* A conversion applied to a script attribute value carrying an explicit type prefix. Receives the
|
|
110
|
-
* text after the prefix
|
|
111
|
-
*
|
|
113
|
+
* text after the prefix, the raw value, and the element the value is declared under — which
|
|
114
|
+
* scopes entity references — and returns the raw value (having warned) when it cannot resolve or
|
|
115
|
+
* parse it — callers rely on that identity to tell failure from success.
|
|
112
116
|
*/
|
|
113
|
-
type Conversion = (rest: string, raw: string) => any;
|
|
117
|
+
type Conversion = (rest: string, raw: string, from: Element) => any;
|
|
114
118
|
|
|
115
119
|
/**
|
|
116
120
|
* Resolves an `asset:` prefix to the Asset created by the `pc-asset` element with that id.
|
|
@@ -128,18 +132,25 @@ const assetConversion: Conversion = (rest, raw) => {
|
|
|
128
132
|
};
|
|
129
133
|
|
|
130
134
|
/**
|
|
131
|
-
* Resolves an `entity:` prefix to the Entity backing a `pc-entity`
|
|
132
|
-
*
|
|
135
|
+
* Resolves an `entity:` prefix to the Entity backing a `pc-entity`, `pc-model` or `pc-node`
|
|
136
|
+
* element. The reference is a name — resolved against the nearest enclosing entity first, then
|
|
137
|
+
* outward, then the document — or a document-wide `#` selector. The failure warning names which
|
|
138
|
+
* of the three causes ({@link unresolvedCause}) it hit.
|
|
133
139
|
* @param rest - The entity reference.
|
|
134
140
|
* @param raw - The raw value, returned unchanged when the reference does not resolve.
|
|
141
|
+
* @param from - The element the value is declared under, which scopes the reference.
|
|
135
142
|
* @returns The entity, or `raw`.
|
|
136
143
|
*/
|
|
137
|
-
const entityConversion: Conversion = (rest, raw) => {
|
|
138
|
-
const entity = getEntity(rest);
|
|
144
|
+
const entityConversion: Conversion = (rest, raw, from) => {
|
|
145
|
+
const entity = getEntity(rest, from);
|
|
139
146
|
if (entity) {
|
|
140
147
|
return entity;
|
|
141
148
|
}
|
|
142
|
-
|
|
149
|
+
const element = findEntityElement(rest, from);
|
|
150
|
+
const hint = element ? '' : idHint(rest, 'entity:');
|
|
151
|
+
console.warn(
|
|
152
|
+
`Unable to resolve '${raw}' in script attributes - ${unresolvedCause(element)}.${hint ? ` ${hint}` : ''}`
|
|
153
|
+
);
|
|
143
154
|
return raw;
|
|
144
155
|
};
|
|
145
156
|
|
|
@@ -298,8 +309,10 @@ class ScriptComponentElement extends ComponentElement {
|
|
|
298
309
|
/**
|
|
299
310
|
* Recursively converts raw attribute data into proper PlayCanvas types. Supported conversions:
|
|
300
311
|
* - "asset:id" → the Asset created by the `pc-asset` element with that id
|
|
301
|
-
* - "entity:ref" → the Entity backing a `pc-entity` element. The
|
|
302
|
-
*
|
|
312
|
+
* - "entity:ref" → the Entity backing a `pc-entity`, `pc-model` or `pc-node` element. The
|
|
313
|
+
* reference is a name, resolved against this element's nearest enclosing entity first,
|
|
314
|
+
* then outward, then the document — or a document-wide `#` selector (`entity:#id`). A bare
|
|
315
|
+
* value is always a name, never an id.
|
|
303
316
|
* - "vec2:1 2" → new Vec2(1, 2)
|
|
304
317
|
* - "vec3:1 2 3" → new Vec3(1, 2, 3)
|
|
305
318
|
* - "vec4:1 2 3 4" → new Vec4(1, 2, 3, 4)
|
|
@@ -313,7 +326,7 @@ class ScriptComponentElement extends ComponentElement {
|
|
|
313
326
|
private convertAttributes(item: any): any {
|
|
314
327
|
if (typeof item === 'string') {
|
|
315
328
|
const match = matchConversion(item);
|
|
316
|
-
return match ? match.convert(match.rest, item) : item;
|
|
329
|
+
return match ? match.convert(match.rest, item, this) : item;
|
|
317
330
|
}
|
|
318
331
|
|
|
319
332
|
if (Array.isArray(item)) {
|
|
@@ -15,7 +15,9 @@ import { parseBool } from '../parse';
|
|
|
15
15
|
* Values are parsed according to the type of the attribute's current value — initially the
|
|
16
16
|
* script's declared default (numbers, booleans, strings, Vec2/3/4, Color, Quat as Euler
|
|
17
17
|
* angles) — and the `asset:`/`entity:`/`vec2:`/`vec3:`/`vec4:`/`color:` prefixes may be used
|
|
18
|
-
* to be explicit.
|
|
18
|
+
* to be explicit. An `entity:` reference is an entity name — resolved against the nearest
|
|
19
|
+
* enclosing entity first, then outward, then the document — or a document-wide `#` selector
|
|
20
|
+
* (`entity:#id`); a bare value is always a name, never an element id.
|
|
19
21
|
* - **The `attributes` JSON attribute**: an object supporting nested structures and attribute
|
|
20
22
|
* names that collide with reserved HTML attribute names (e.g. `title`).
|
|
21
23
|
*
|
|
@@ -31,7 +33,8 @@ import { parseBool } from '../parse';
|
|
|
31
33
|
*
|
|
32
34
|
* @elementSummary The `<pc-script-instance>` element attaches one script class, named by `name`, to
|
|
33
35
|
* the entity of its parent `<pc-script>`. Its other attributes set script attributes of the same
|
|
34
|
-
* name, and `attributes` takes a JSON object instead.
|
|
36
|
+
* name, and `attributes` takes a JSON object instead. An `entity:` value is an entity name —
|
|
37
|
+
* write `entity:#id` for an element id. Must be a direct child of `<pc-script>`.
|
|
35
38
|
*
|
|
36
39
|
* @fires {CustomEvent} scriptattributeschange - Fired when the script's attributes change. The
|
|
37
40
|
* `detail` carries the new `attributes` object. Bubbles.
|
|
@@ -54,9 +57,11 @@ class ScriptInstanceElement extends AsyncElement {
|
|
|
54
57
|
/**
|
|
55
58
|
* Sets the attributes of the script as an object. Values are converted with the same rules
|
|
56
59
|
* as the `attributes` attribute: `asset:`/`entity:` references and `vec2:`/`vec3:`/`vec4:`/
|
|
57
|
-
* `color:` prefixed strings are resolved
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
+
* `color:` prefixed strings are resolved (an entity name against the nearest enclosing
|
|
61
|
+
* entity first, then outward, then the document — or a document-wide `#` selector; a bare
|
|
62
|
+
* value is always a name, never an element id), and a plain numeric array is converted to
|
|
63
|
+
* the type of the attribute it targets when that attribute currently holds a Vec2, Vec3,
|
|
64
|
+
* Vec4 or Color.
|
|
60
65
|
* @param value - The attributes of the script.
|
|
61
66
|
*/
|
|
62
67
|
set scriptAttributes(value: Record<string, any>) {
|
|
@@ -70,7 +75,10 @@ class ScriptInstanceElement extends AsyncElement {
|
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
/**
|
|
73
|
-
* Gets the attributes of the script
|
|
78
|
+
* Gets the attributes of the script as an object whose `asset:`, `entity:`, `vec2:`, `vec3:`,
|
|
79
|
+
* `vec4:` and `color:` prefixed values are resolved when applied — an `entity:` value being
|
|
80
|
+
* an entity name (nearest enclosing entity first, then outward, then the document) or a
|
|
81
|
+
* document-wide `#` selector (`entity:#id`), never a bare element id.
|
|
74
82
|
* @returns The attributes of the script.
|
|
75
83
|
*/
|
|
76
84
|
get scriptAttributes(): Record<string, any> {
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
Vec2
|
|
9
9
|
} from 'playcanvas';
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { parseBool, parseEnum, parseNumber, parseVec2, resolveEntity } from '../parse';
|
|
12
12
|
|
|
13
13
|
import { ComponentElement } from './component';
|
|
14
14
|
|
|
@@ -82,22 +82,22 @@ class ScrollViewComponentElement extends ComponentElement {
|
|
|
82
82
|
verticalScrollbarVisibility: visibilities.get(this._verticalScrollbarVisibility)
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
-
const viewport =
|
|
85
|
+
const viewport = resolveEntity(this._viewport, this, 'viewport', 'reference ignored');
|
|
86
86
|
if (viewport) {
|
|
87
87
|
data.viewportEntity = viewport;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
const content =
|
|
90
|
+
const content = resolveEntity(this._content, this, 'content', 'reference ignored');
|
|
91
91
|
if (content) {
|
|
92
92
|
data.contentEntity = content;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
const horizontalScrollbar =
|
|
95
|
+
const horizontalScrollbar = resolveEntity(this._horizontalScrollbar, this, 'horizontal-scrollbar', 'reference ignored');
|
|
96
96
|
if (horizontalScrollbar) {
|
|
97
97
|
data.horizontalScrollbarEntity = horizontalScrollbar;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
const verticalScrollbar =
|
|
100
|
+
const verticalScrollbar = resolveEntity(this._verticalScrollbar, this, 'vertical-scrollbar', 'reference ignored');
|
|
101
101
|
if (verticalScrollbar) {
|
|
102
102
|
data.verticalScrollbarEntity = verticalScrollbar;
|
|
103
103
|
}
|
|
@@ -294,20 +294,25 @@ class ScrollViewComponentElement extends ComponentElement {
|
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
/**
|
|
297
|
-
* Sets the reference (
|
|
298
|
-
* viewport, which clips the content to the scroll view's
|
|
297
|
+
* Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
|
|
298
|
+
* selector) to the entity used as the viewport, which clips the content to the scroll view's
|
|
299
|
+
* bounds. An exact name resolves against the nearest enclosing entity first, then outward,
|
|
300
|
+
* then the document. A non-empty reference that does not resolve warns and is ignored.
|
|
299
301
|
* @param value - The viewport entity reference.
|
|
300
302
|
*/
|
|
301
303
|
set viewport(value: string) {
|
|
302
304
|
this._viewport = value;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
305
|
+
if (this.component) {
|
|
306
|
+
const entity = resolveEntity(value, this, 'viewport', 'reference ignored');
|
|
307
|
+
if (entity) {
|
|
308
|
+
this.component.viewportEntity = entity;
|
|
309
|
+
}
|
|
306
310
|
}
|
|
307
311
|
}
|
|
308
312
|
|
|
309
313
|
/**
|
|
310
|
-
* Gets the reference
|
|
314
|
+
* Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
|
|
315
|
+
* selector) to the entity used as the viewport.
|
|
311
316
|
* @returns The viewport entity reference.
|
|
312
317
|
*/
|
|
313
318
|
get viewport() {
|
|
@@ -315,20 +320,25 @@ class ScrollViewComponentElement extends ComponentElement {
|
|
|
315
320
|
}
|
|
316
321
|
|
|
317
322
|
/**
|
|
318
|
-
* Sets the reference (
|
|
319
|
-
* content, which is moved as the scroll view is
|
|
323
|
+
* Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
|
|
324
|
+
* selector) to the entity used as the content, which is moved as the scroll view is
|
|
325
|
+
* scrolled. An exact name resolves against the nearest enclosing entity first, then outward,
|
|
326
|
+
* then the document. A non-empty reference that does not resolve warns and is ignored.
|
|
320
327
|
* @param value - The content entity reference.
|
|
321
328
|
*/
|
|
322
329
|
set content(value: string) {
|
|
323
330
|
this._content = value;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
331
|
+
if (this.component) {
|
|
332
|
+
const entity = resolveEntity(value, this, 'content', 'reference ignored');
|
|
333
|
+
if (entity) {
|
|
334
|
+
this.component.contentEntity = entity;
|
|
335
|
+
}
|
|
327
336
|
}
|
|
328
337
|
}
|
|
329
338
|
|
|
330
339
|
/**
|
|
331
|
-
* Gets the reference
|
|
340
|
+
* Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
|
|
341
|
+
* selector) to the entity used as the content.
|
|
332
342
|
* @returns The content entity reference.
|
|
333
343
|
*/
|
|
334
344
|
get content() {
|
|
@@ -336,20 +346,25 @@ class ScrollViewComponentElement extends ComponentElement {
|
|
|
336
346
|
}
|
|
337
347
|
|
|
338
348
|
/**
|
|
339
|
-
* Sets the reference (
|
|
340
|
-
* the horizontal `<pc-scrollbar>`.
|
|
349
|
+
* Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
|
|
350
|
+
* selector) to the entity containing the horizontal `<pc-scrollbar>`. An exact name resolves
|
|
351
|
+
* against the nearest enclosing entity first, then outward, then the document. A non-empty
|
|
352
|
+
* reference that does not resolve warns and is ignored.
|
|
341
353
|
* @param value - The horizontal scrollbar entity reference.
|
|
342
354
|
*/
|
|
343
355
|
set horizontalScrollbar(value: string) {
|
|
344
356
|
this._horizontalScrollbar = value;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
357
|
+
if (this.component) {
|
|
358
|
+
const entity = resolveEntity(value, this, 'horizontal-scrollbar', 'reference ignored');
|
|
359
|
+
if (entity) {
|
|
360
|
+
this.component.horizontalScrollbarEntity = entity;
|
|
361
|
+
}
|
|
348
362
|
}
|
|
349
363
|
}
|
|
350
364
|
|
|
351
365
|
/**
|
|
352
|
-
* Gets the reference
|
|
366
|
+
* Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
|
|
367
|
+
* selector) to the entity containing the horizontal scrollbar.
|
|
353
368
|
* @returns The horizontal scrollbar entity reference.
|
|
354
369
|
*/
|
|
355
370
|
get horizontalScrollbar() {
|
|
@@ -357,20 +372,25 @@ class ScrollViewComponentElement extends ComponentElement {
|
|
|
357
372
|
}
|
|
358
373
|
|
|
359
374
|
/**
|
|
360
|
-
* Sets the reference (
|
|
361
|
-
* the vertical `<pc-scrollbar>`.
|
|
375
|
+
* Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
|
|
376
|
+
* selector) to the entity containing the vertical `<pc-scrollbar>`. An exact name resolves
|
|
377
|
+
* against the nearest enclosing entity first, then outward, then the document. A non-empty
|
|
378
|
+
* reference that does not resolve warns and is ignored.
|
|
362
379
|
* @param value - The vertical scrollbar entity reference.
|
|
363
380
|
*/
|
|
364
381
|
set verticalScrollbar(value: string) {
|
|
365
382
|
this._verticalScrollbar = value;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
383
|
+
if (this.component) {
|
|
384
|
+
const entity = resolveEntity(value, this, 'vertical-scrollbar', 'reference ignored');
|
|
385
|
+
if (entity) {
|
|
386
|
+
this.component.verticalScrollbarEntity = entity;
|
|
387
|
+
}
|
|
369
388
|
}
|
|
370
389
|
}
|
|
371
390
|
|
|
372
391
|
/**
|
|
373
|
-
* Gets the reference
|
|
392
|
+
* Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
|
|
393
|
+
* selector) to the entity containing the vertical scrollbar.
|
|
374
394
|
* @returns The vertical scrollbar entity reference.
|
|
375
395
|
*/
|
|
376
396
|
get verticalScrollbar() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ScrollbarComponent } from 'playcanvas';
|
|
2
2
|
import { ORIENTATION_HORIZONTAL, ORIENTATION_VERTICAL } from 'playcanvas';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { parseEnum, parseNumber, resolveEntity } from '../parse';
|
|
5
5
|
|
|
6
6
|
import { ComponentElement } from './component';
|
|
7
7
|
|
|
@@ -45,7 +45,7 @@ class ScrollbarComponentElement extends ComponentElement {
|
|
|
45
45
|
handleSize: this._handleSize
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
-
const handle =
|
|
48
|
+
const handle = resolveEntity(this._handle, this, 'handle', 'reference ignored');
|
|
49
49
|
if (handle) {
|
|
50
50
|
data.handleEntity = handle;
|
|
51
51
|
}
|
|
@@ -120,20 +120,25 @@ class ScrollbarComponentElement extends ComponentElement {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
|
-
* Sets the reference (
|
|
124
|
-
* scrollbar handle.
|
|
123
|
+
* Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
|
|
124
|
+
* selector) to the entity used as the scrollbar handle. An exact name resolves against the
|
|
125
|
+
* nearest enclosing entity first, then outward, then the document. A non-empty reference that
|
|
126
|
+
* does not resolve warns and is ignored.
|
|
125
127
|
* @param value - The handle entity reference.
|
|
126
128
|
*/
|
|
127
129
|
set handle(value: string) {
|
|
128
130
|
this._handle = value;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
if (this.component) {
|
|
132
|
+
const entity = resolveEntity(value, this, 'handle', 'reference ignored');
|
|
133
|
+
if (entity) {
|
|
134
|
+
this.component.handleEntity = entity;
|
|
135
|
+
}
|
|
132
136
|
}
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
/**
|
|
136
|
-
* Gets the reference
|
|
140
|
+
* Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
|
|
141
|
+
* selector) to the entity used as the scrollbar handle.
|
|
137
142
|
* @returns The handle entity reference.
|
|
138
143
|
*/
|
|
139
144
|
get handle() {
|
package/src/parse.ts
CHANGED
|
@@ -12,8 +12,13 @@
|
|
|
12
12
|
* - `parseBool` and `parseTags` take no attribute name, because every value is valid for them and
|
|
13
13
|
* so they never warn.
|
|
14
14
|
*
|
|
15
|
-
* `getEntity`
|
|
16
|
-
* literal, and
|
|
15
|
+
* `findEntityElement` and `getEntity` are the exceptions: they resolve a reference rather than
|
|
16
|
+
* parsing a literal, and return `null` instead of falling back to a default. A reference
|
|
17
|
+
* beginning with `#` is a document-wide selector (an element id, or any selector rooted in one);
|
|
18
|
+
* anything else is an entity name, resolved lexically through the entity hierarchy first and
|
|
19
|
+
* against the document after — never as a selector or an id. They also do not warn - what an
|
|
20
|
+
* unresolved reference means depends on the element holding it - so elements report through
|
|
21
|
+
* `resolveEntity`, which takes that meaning as parameters.
|
|
17
22
|
*/
|
|
18
23
|
|
|
19
24
|
import type { Entity } from 'playcanvas';
|
|
@@ -324,32 +329,224 @@ export const parseVec4 = <T extends Vec4 | null>(
|
|
|
324
329
|
};
|
|
325
330
|
|
|
326
331
|
/**
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
332
|
+
* Runs querySelector, absorbing the SyntaxError an unparseable selector throws - references are
|
|
333
|
+
* arbitrary author text, so a lookup must fail to `null`, never throw.
|
|
334
|
+
*
|
|
335
|
+
* @param selector - The selector to query.
|
|
336
|
+
* @returns The matched element, or `null`.
|
|
337
|
+
*/
|
|
338
|
+
const query = (selector: string): Element | null => {
|
|
339
|
+
try {
|
|
340
|
+
return document.querySelector(selector);
|
|
341
|
+
} catch {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Runs a lookup against one scope, checking the scope element itself before its subtree — a
|
|
348
|
+
* reference deep in a cloned prefab must be able to name the prefab's root. Absorbs the
|
|
349
|
+
* SyntaxError of an invalid selector like {@link query}: escaping quotes and backslashes does not
|
|
350
|
+
* make arbitrary text a valid CSS string (a reference containing a newline still throws), so a
|
|
351
|
+
* lookup must fail to `null`, never throw.
|
|
352
|
+
*
|
|
353
|
+
* @param scope - The element whose inclusive subtree to search.
|
|
354
|
+
* @param selector - The selector to query.
|
|
355
|
+
* @returns The matched element, or `null`.
|
|
356
|
+
*/
|
|
357
|
+
const queryScope = (scope: Element, selector: string): Element | null => {
|
|
358
|
+
try {
|
|
359
|
+
return scope.matches(selector) ? scope : scope.querySelector(selector);
|
|
360
|
+
} catch {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Reads the entity a resolved element is backing, through the `entity` accessor every
|
|
367
|
+
* entity-fronting element exposes. `null` for no element, and for an element backing nothing.
|
|
368
|
+
*
|
|
369
|
+
* @param element - The element to read, or `null`.
|
|
370
|
+
* @returns The backing entity, or `null`.
|
|
371
|
+
*/
|
|
372
|
+
const entityOf = (element: Element | null): Entity | null => {
|
|
373
|
+
return (element as { entity?: Entity } | null)?.entity ?? null;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* The elements that front an entity: what a bare name can resolve to, and the scopes of the
|
|
378
|
+
* lexical name lookup.
|
|
379
|
+
*/
|
|
380
|
+
const ENTITY_KINDS = ['pc-entity', 'pc-model', 'pc-node'] as const;
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* The entity-fronting elements as one selector, for the scope walk.
|
|
384
|
+
*/
|
|
385
|
+
const ENTITY_SCOPES = ENTITY_KINDS.join(', ');
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Resolves a reference string to the element it names. The grammar is closed — every reference
|
|
389
|
+
* has exactly one interpretation:
|
|
390
|
+
*
|
|
391
|
+
* - A reference beginning with `#` is a document-wide CSS selector — an element id (`#body`), or
|
|
392
|
+
* any selector rooted in one (`#hud pc-entity`). It is authoritative: the name lookup never
|
|
393
|
+
* runs for it, so an unusually named entity cannot shadow it.
|
|
394
|
+
* - Any other reference is the name of an entity-fronting element (`<pc-entity>`, `<pc-model>` or
|
|
395
|
+
* `<pc-node>` — for a node, the glTF node name it binds), and nothing else. A bare reference is
|
|
396
|
+
* never interpreted as a selector or an element id, so adding or renaming elements can never
|
|
397
|
+
* change which form it takes.
|
|
398
|
+
*
|
|
399
|
+
* When `from` is supplied, a name resolves lexically first: the closest entity-fronting
|
|
400
|
+
* ancestor's inclusive subtree, then each outer entity-fronting ancestor, then the containing
|
|
401
|
+
* `<pc-app>`, then the document. This is what lets a `<template>` prefab reference its own
|
|
402
|
+
* entities by name — every clone resolves within itself before a document-wide lookup could reach
|
|
403
|
+
* an earlier clone — provided the prefab has a single entity-fronting root to be the enclosing
|
|
404
|
+
* scope.
|
|
405
|
+
*
|
|
406
|
+
* Separate from {@link getEntity} so a caller reporting a failure can tell the causes apart
|
|
407
|
+
* ({@link unresolvedCause} words them): nothing in the document matches the reference, or
|
|
408
|
+
* something matches but is not backing an entity (yet, or ever).
|
|
330
409
|
*
|
|
331
410
|
* @param ref - The reference string to resolve.
|
|
332
|
-
* @
|
|
411
|
+
* @param from - The element resolving the reference, whose entity-fronting ancestors scope the
|
|
412
|
+
* name lookup. Omitted, the name lookup is document-wide only.
|
|
413
|
+
* @returns The matched element, or `null`.
|
|
333
414
|
* @internal
|
|
334
415
|
*/
|
|
335
|
-
export const
|
|
416
|
+
export const findEntityElement = (ref: string, from?: Element): Element | null => {
|
|
336
417
|
if (!ref) {
|
|
337
418
|
return null;
|
|
338
419
|
}
|
|
339
420
|
|
|
340
|
-
|
|
421
|
+
// A '#' reference is document-wide and bypasses the name lookup entirely - an entity named
|
|
422
|
+
// '#body' must never shadow the element whose id is 'body'.
|
|
423
|
+
if (ref.startsWith('#')) {
|
|
424
|
+
return query(ref);
|
|
425
|
+
}
|
|
341
426
|
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
427
|
+
// The name lands inside a quoted CSS string, so its quotes and backslashes are escaped -
|
|
428
|
+
// a name like `say "hi"` must resolve, not turn the lookup into a SyntaxError.
|
|
429
|
+
const escaped = ref.replace(/["\\]/g, '\\$&');
|
|
430
|
+
const nameSelector = ENTITY_KINDS.map(kind => `${kind}[name="${escaped}"]`).join(', ');
|
|
431
|
+
|
|
432
|
+
if (from) {
|
|
433
|
+
let scope = from.parentElement?.closest(ENTITY_SCOPES);
|
|
434
|
+
while (scope) {
|
|
435
|
+
const element = queryScope(scope, nameSelector);
|
|
436
|
+
if (element) {
|
|
437
|
+
return element;
|
|
438
|
+
}
|
|
439
|
+
scope = scope.parentElement?.closest(ENTITY_SCOPES);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const app = from.parentElement?.closest('pc-app');
|
|
443
|
+
if (app) {
|
|
444
|
+
const element = queryScope(app, nameSelector);
|
|
445
|
+
if (element) {
|
|
446
|
+
return element;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
348
449
|
}
|
|
349
450
|
|
|
451
|
+
return query(nameSelector);
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Resolves a reference string to the {@link Entity} backing an entity-fronting element
|
|
456
|
+
* (`<pc-entity>`, `<pc-model>` or `<pc-node>`). The reference is a name — resolved lexically
|
|
457
|
+
* through the entity hierarchy first when `from` is supplied — or a document-wide `#` selector
|
|
458
|
+
* ({@link findEntityElement} details the grammar and order). Returns `null` if no matching
|
|
459
|
+
* element (or backing entity) is found.
|
|
460
|
+
*
|
|
461
|
+
* @param ref - The reference string to resolve.
|
|
462
|
+
* @param from - The element resolving the reference, whose entity-fronting ancestors scope the
|
|
463
|
+
* name lookup. Omitted, the name lookup is document-wide only.
|
|
464
|
+
* @returns The resolved entity, or `null`.
|
|
465
|
+
* @internal
|
|
466
|
+
*/
|
|
467
|
+
export const getEntity = (ref: string, from?: Element): Entity | null => {
|
|
468
|
+
return entityOf(findEntityElement(ref, from));
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Describes why a non-empty reference did not resolve, for a warning. Three causes, because they
|
|
473
|
+
* have three different fixes: nothing matches (usually a typo), the matched element is not backing
|
|
474
|
+
* an entity yet (usually timing - a `pc-node` whose asset has not loaded - so resolving again
|
|
475
|
+
* later can work), or the matched element can never back one (the reference points at the wrong
|
|
476
|
+
* element, so only correcting it can). Capability is the `entity` accessor every entity-backing
|
|
477
|
+
* element inherits from EntityBaseElement.
|
|
478
|
+
*
|
|
479
|
+
* @param element - The element the reference matched, or `null` when nothing did.
|
|
480
|
+
* @returns The cause, phrased to follow `could not resolve ... -`.
|
|
481
|
+
* @internal
|
|
482
|
+
*/
|
|
483
|
+
export const unresolvedCause = (element: Element | null): string => {
|
|
350
484
|
if (!element) {
|
|
351
|
-
|
|
485
|
+
return 'nothing in the document matches it';
|
|
352
486
|
}
|
|
487
|
+
const tag = `<${element.tagName.toLowerCase()}>`;
|
|
488
|
+
return 'entity' in element
|
|
489
|
+
? `${tag} matches it but is not backing an entity yet`
|
|
490
|
+
: `${tag} matches it but cannot back an entity`;
|
|
491
|
+
};
|
|
353
492
|
|
|
354
|
-
|
|
493
|
+
/**
|
|
494
|
+
* Builds the migration pointer for a bare reference that names nothing but matches the id of an
|
|
495
|
+
* entity-fronting element - it was almost certainly meant as an id, so point at the form that
|
|
496
|
+
* expresses it, escaped so the suggestion actually parses as a selector (an id like `a:b` must
|
|
497
|
+
* be written `#a\:b`). Empty when the reference is already a `#` form, matches no id, or the id
|
|
498
|
+
* belongs to an element that could never back an entity - suggesting it would only trade this
|
|
499
|
+
* warning for the wrong-target one.
|
|
500
|
+
*
|
|
501
|
+
* @param ref - The unresolved reference.
|
|
502
|
+
* @param prefix - Text the suggested form must carry in the caller's syntax (e.g. `entity:`).
|
|
503
|
+
* @returns The advice sentence, or an empty string.
|
|
504
|
+
* @internal
|
|
505
|
+
*/
|
|
506
|
+
export const idHint = (ref: string, prefix = ''): string => {
|
|
507
|
+
const match = !ref.startsWith('#') && document.getElementById(ref);
|
|
508
|
+
return match && 'entity' in match
|
|
509
|
+
? `A bare reference is a name - write '${prefix}#${CSS.escape(ref)}' to reference the element with that id.`
|
|
510
|
+
: '';
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Resolves a reference string to the {@link Entity} backing an entity-fronting element, scoped to
|
|
515
|
+
* the resolving element ({@link findEntityElement} details the order) and warning when a
|
|
516
|
+
* non-empty reference does not resolve - otherwise the reference fails silently, invisible
|
|
517
|
+
* except through the behavior it should have driven. The message names which of the three causes
|
|
518
|
+
* ({@link unresolvedCause}) it hit, and advises reassigning later only when that can work.
|
|
519
|
+
*
|
|
520
|
+
* An empty reference stays silent: it is the unset state of an optional attribute, and on some
|
|
521
|
+
* elements (`pc-joint` `entity-b`, `pc-button` `image`) a documented value of its own.
|
|
522
|
+
*
|
|
523
|
+
* @param ref - The reference string to resolve.
|
|
524
|
+
* @param from - The element resolving the reference; scopes the lookup and names the message.
|
|
525
|
+
* @param attribute - The attribute being resolved, for the message.
|
|
526
|
+
* @param consequence - What the unresolved reference means for the element, for the message.
|
|
527
|
+
* @returns The resolved entity, or `null`.
|
|
528
|
+
* @internal
|
|
529
|
+
*/
|
|
530
|
+
export const resolveEntity = (ref: string, from: Element, attribute: string, consequence: string): Entity | null => {
|
|
531
|
+
if (!ref) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const element = findEntityElement(ref, from);
|
|
536
|
+
const entity = entityOf(element);
|
|
537
|
+
if (!entity) {
|
|
538
|
+
let advice = `Assign ${attribute} again once the entity exists.`;
|
|
539
|
+
if (element && !('entity' in element)) {
|
|
540
|
+
advice = `Point ${attribute} at a pc-entity, pc-model or pc-node instead.`;
|
|
541
|
+
} else if (!element) {
|
|
542
|
+
const hint = idHint(ref);
|
|
543
|
+
if (hint) {
|
|
544
|
+
advice = hint;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
console.warn(
|
|
548
|
+
`${from.tagName.toLowerCase()} could not resolve ${attribute} '${ref}' - ${unresolvedCause(element)} - ${consequence}. ${advice}`
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
return entity;
|
|
355
552
|
};
|