@microsoft/fast-html 1.0.0-alpha.4 → 1.0.0-alpha.41
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/README.md +266 -19
- package/dist/dts/components/element.d.ts +10 -0
- package/dist/dts/components/index.d.ts +3 -1
- package/dist/dts/components/observer-map.d.ts +27 -0
- package/dist/dts/components/schema.d.ts +144 -0
- package/dist/dts/components/template.d.ts +83 -7
- package/dist/dts/components/utilities.d.ts +128 -37
- package/dist/dts/index.d.ts +1 -1
- package/dist/dts/tsdoc-metadata.json +1 -1
- package/dist/esm/components/element.js +73 -0
- package/dist/esm/components/index.js +3 -1
- package/dist/esm/components/observer-map.js +68 -0
- package/dist/esm/components/observer-map.spec.js +39 -0
- package/dist/esm/components/schema.js +250 -0
- package/dist/esm/components/schema.spec.js +484 -0
- package/dist/esm/components/template.js +242 -213
- package/dist/esm/components/utilities.js +1008 -64
- package/dist/esm/components/utilities.spec.js +522 -93
- package/dist/esm/index.js +1 -1
- package/dist/fast-html.api.json +350 -1
- package/dist/fast-html.d.ts +283 -6
- package/dist/fast-html.untrimmed.d.ts +283 -6
- package/package.json +27 -38
- package/rules/attribute-directives.yml +38 -0
- package/rules/call-expression-with-event-argument.yml +41 -0
- package/rules/member-expression.yml +33 -0
- package/rules/tag-function-to-template-literal.yml +16 -0
- package/dist/dts/fixtures/binding/binding.spec.d.ts +0 -1
- package/dist/dts/fixtures/binding/main.d.ts +0 -1
- package/dist/dts/fixtures/children/children.spec.d.ts +0 -1
- package/dist/dts/fixtures/children/main.d.ts +0 -1
- package/dist/dts/fixtures/dot-syntax/dot-syntax.spec.d.ts +0 -1
- package/dist/dts/fixtures/dot-syntax/main.d.ts +0 -1
- package/dist/dts/fixtures/event/event.spec.d.ts +0 -1
- package/dist/dts/fixtures/event/main.d.ts +0 -1
- package/dist/dts/fixtures/partial/main.d.ts +0 -1
- package/dist/dts/fixtures/partial/partial.spec.d.ts +0 -1
- package/dist/dts/fixtures/ref/main.d.ts +0 -1
- package/dist/dts/fixtures/ref/ref.spec.d.ts +0 -1
- package/dist/dts/fixtures/repeat/main.d.ts +0 -1
- package/dist/dts/fixtures/repeat/repeat.spec.d.ts +0 -1
- package/dist/dts/fixtures/slotted/main.d.ts +0 -1
- package/dist/dts/fixtures/slotted/slotted.spec.d.ts +0 -1
- package/dist/dts/fixtures/when/main.d.ts +0 -1
- package/dist/dts/fixtures/when/when.spec.d.ts +0 -1
- package/dist/esm/fixtures/attribute/attribute.spec.js +0 -23
- package/dist/esm/fixtures/attribute/main.js +0 -19
- package/dist/esm/fixtures/binding/binding.spec.js +0 -17
- package/dist/esm/fixtures/binding/main.js +0 -19
- package/dist/esm/fixtures/children/children.spec.js +0 -33
- package/dist/esm/fixtures/children/main.js +0 -24
- package/dist/esm/fixtures/dot-syntax/dot-syntax.spec.js +0 -9
- package/dist/esm/fixtures/dot-syntax/main.js +0 -16
- package/dist/esm/fixtures/event/event.spec.js +0 -12
- package/dist/esm/fixtures/event/main.js +0 -16
- package/dist/esm/fixtures/partial/main.js +0 -31
- package/dist/esm/fixtures/partial/partial.spec.js +0 -14
- package/dist/esm/fixtures/ref/main.js +0 -14
- package/dist/esm/fixtures/ref/ref.spec.js +0 -13
- package/dist/esm/fixtures/repeat/main.js +0 -19
- package/dist/esm/fixtures/repeat/repeat.spec.js +0 -26
- package/dist/esm/fixtures/slotted/main.js +0 -22
- package/dist/esm/fixtures/slotted/slotted.spec.js +0 -25
- package/dist/esm/fixtures/when/main.js +0 -146
- package/dist/esm/fixtures/when/when.spec.js +0 -82
- package/dist/esm/tsconfig.tsbuildinfo +0 -1
- /package/dist/dts/{fixtures/attribute/attribute.spec.d.ts → components/observer-map.spec.d.ts} +0 -0
- /package/dist/dts/{fixtures/attribute/main.d.ts → components/schema.spec.d.ts} +0 -0
|
@@ -1,9 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { Observable } from "@microsoft/fast-element/observable.js";
|
|
2
|
+
import { defsPropertyName, fastContextMetaData, refPropertyName, Schema, } from "./schema.js";
|
|
3
|
+
const openClientSideBinding = "{";
|
|
4
|
+
const closeClientSideBinding = "}";
|
|
5
|
+
const openContentBinding = "{{";
|
|
6
|
+
const closeContentBinding = "}}";
|
|
7
|
+
const openUnescapedBinding = "{{{";
|
|
8
|
+
const closeUnescapedBinding = "}}}";
|
|
3
9
|
const openTagStart = "<f-";
|
|
4
10
|
const tagEnd = ">";
|
|
5
11
|
const closeTagStart = "</f-";
|
|
6
12
|
const attributeDirectivePrefix = "f-";
|
|
13
|
+
export const contextPrefix = "$c";
|
|
14
|
+
export const contextPrefixDot = `${contextPrefix}.`;
|
|
15
|
+
const startInnerHTMLDiv = `<div :innerHTML="{{`;
|
|
16
|
+
const startInnerHTMLDivLength = startInnerHTMLDiv.length;
|
|
17
|
+
const endInnerHTMLDiv = `}}"></div>`;
|
|
18
|
+
const endInnerHTMLDivLength = endInnerHTMLDiv.length;
|
|
19
|
+
const LogicalOperator = {
|
|
20
|
+
AND: "&&",
|
|
21
|
+
OR: "||",
|
|
22
|
+
};
|
|
23
|
+
const ComparisonOperator = {
|
|
24
|
+
ACCESS: "access",
|
|
25
|
+
EQUALS: "==",
|
|
26
|
+
GREATER_THAN: ">",
|
|
27
|
+
GREATER_THAN_OR_EQUALS: ">=",
|
|
28
|
+
LESS_THAN: "<",
|
|
29
|
+
LESS_THAN_OR_EQUALS: "<=",
|
|
30
|
+
NOT: "!",
|
|
31
|
+
NOT_EQUALS: "!=",
|
|
32
|
+
};
|
|
33
|
+
const Operator = {
|
|
34
|
+
...LogicalOperator,
|
|
35
|
+
...ComparisonOperator,
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* A map of proxied objects
|
|
39
|
+
*/
|
|
40
|
+
const objectTargetsMap = new WeakMap();
|
|
41
|
+
/**
|
|
42
|
+
* A map of arrays being observered
|
|
43
|
+
*/
|
|
44
|
+
const observedArraysMap = new WeakMap();
|
|
7
45
|
/**
|
|
8
46
|
* Get the index of the next matching tag
|
|
9
47
|
* @param openingTagStartSlice - The slice starting from the opening tag
|
|
@@ -108,7 +146,11 @@ function getAttributeDataBindingConfig(innerHTML, config) {
|
|
|
108
146
|
firstCharOfAttribute === ":"
|
|
109
147
|
? firstCharOfAttribute
|
|
110
148
|
: null;
|
|
111
|
-
return
|
|
149
|
+
return {
|
|
150
|
+
...config,
|
|
151
|
+
subtype: "attribute",
|
|
152
|
+
aspect,
|
|
153
|
+
};
|
|
112
154
|
}
|
|
113
155
|
/**
|
|
114
156
|
* Get the attribute directive binding config
|
|
@@ -121,7 +163,11 @@ function getAttributeDirectiveDataBindingConfig(innerHTML, config) {
|
|
|
121
163
|
const lastItem = splitInnerHTML[splitInnerHTML.length - 1];
|
|
122
164
|
const equals = lastItem.indexOf("=");
|
|
123
165
|
const name = lastItem.slice(2, equals);
|
|
124
|
-
return
|
|
166
|
+
return {
|
|
167
|
+
...config,
|
|
168
|
+
subtype: "attributeDirective",
|
|
169
|
+
name: name,
|
|
170
|
+
};
|
|
125
171
|
}
|
|
126
172
|
/**
|
|
127
173
|
* Get the content data binding config
|
|
@@ -129,7 +175,51 @@ function getAttributeDirectiveDataBindingConfig(innerHTML, config) {
|
|
|
129
175
|
* @returns ContentDataBindingBehaviorConfig
|
|
130
176
|
*/
|
|
131
177
|
function getContentDataBindingConfig(config) {
|
|
132
|
-
return
|
|
178
|
+
return {
|
|
179
|
+
...config,
|
|
180
|
+
subtype: "content",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Finds the next data binding in innerHTML and determines its type and indices
|
|
185
|
+
* @param innerHTML - The innerHTML string to search for data bindings
|
|
186
|
+
* @returns NextDataBindingBehaviorConfig containing the binding type and start indices
|
|
187
|
+
*/
|
|
188
|
+
function getIndexAndBindingTypeOfNextDataBindingBehavior(innerHTML) {
|
|
189
|
+
// {{{}}} binding
|
|
190
|
+
const openingUnescapedStartIndex = innerHTML.indexOf(openUnescapedBinding);
|
|
191
|
+
const closingUnescapedStartIndex = innerHTML.indexOf(closeUnescapedBinding);
|
|
192
|
+
// {{}} binding
|
|
193
|
+
const openingContentStartIndex = innerHTML.indexOf(openContentBinding);
|
|
194
|
+
const closingContentStartIndex = innerHTML.indexOf(closeContentBinding);
|
|
195
|
+
// {} binding
|
|
196
|
+
const openingClientStartIndex = innerHTML.indexOf(openClientSideBinding);
|
|
197
|
+
const closingClientStartIndex = innerHTML.indexOf(closeClientSideBinding);
|
|
198
|
+
if (openingUnescapedStartIndex !== -1 &&
|
|
199
|
+
openingUnescapedStartIndex <= openingContentStartIndex &&
|
|
200
|
+
openingUnescapedStartIndex <= openingClientStartIndex) {
|
|
201
|
+
// is unescaped {{{}}}
|
|
202
|
+
return {
|
|
203
|
+
openingStartIndex: openingUnescapedStartIndex,
|
|
204
|
+
closingStartIndex: closingUnescapedStartIndex,
|
|
205
|
+
bindingType: "unescaped",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
else if (openingContentStartIndex !== -1 &&
|
|
209
|
+
openingContentStartIndex <= openingClientStartIndex) {
|
|
210
|
+
// is default {{}}
|
|
211
|
+
return {
|
|
212
|
+
openingStartIndex: openingContentStartIndex,
|
|
213
|
+
closingStartIndex: closingContentStartIndex,
|
|
214
|
+
bindingType: "default",
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
// is client {}
|
|
218
|
+
return {
|
|
219
|
+
openingStartIndex: openingClientStartIndex,
|
|
220
|
+
closingStartIndex: closingClientStartIndex,
|
|
221
|
+
bindingType: "client",
|
|
222
|
+
};
|
|
133
223
|
}
|
|
134
224
|
/**
|
|
135
225
|
* Get the next data binding
|
|
@@ -137,14 +227,15 @@ function getContentDataBindingConfig(config) {
|
|
|
137
227
|
* @returns DataBindingBehaviorConfig - A configuration object
|
|
138
228
|
*/
|
|
139
229
|
function getNextDataBindingBehavior(innerHTML) {
|
|
140
|
-
const openingStartIndex = innerHTML
|
|
141
|
-
const
|
|
230
|
+
const { openingStartIndex, closingStartIndex, bindingType } = getIndexAndBindingTypeOfNextDataBindingBehavior(innerHTML);
|
|
231
|
+
const bindingLength = bindingType === "client" ? 1 : bindingType === "default" ? 2 : 3;
|
|
142
232
|
const partialConfig = {
|
|
143
233
|
type: "dataBinding",
|
|
234
|
+
bindingType,
|
|
144
235
|
openingStartIndex,
|
|
145
|
-
openingEndIndex: openingStartIndex +
|
|
236
|
+
openingEndIndex: openingStartIndex + bindingLength,
|
|
146
237
|
closingStartIndex,
|
|
147
|
-
closingEndIndex: closingStartIndex +
|
|
238
|
+
closingEndIndex: closingStartIndex + bindingLength,
|
|
148
239
|
};
|
|
149
240
|
return isAttributeDirective(innerHTML, openingStartIndex)
|
|
150
241
|
? getAttributeDirectiveDataBindingConfig(innerHTML, partialConfig)
|
|
@@ -157,46 +248,67 @@ function getNextDataBindingBehavior(innerHTML) {
|
|
|
157
248
|
* @param innerHTML - The innerHTML string to evaluate
|
|
158
249
|
* @returns DataBindingBehaviorConfig | DirectiveBehaviorConfig | null - A configuration object or null
|
|
159
250
|
*/
|
|
160
|
-
export function getNextBehavior(innerHTML) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
251
|
+
export function getNextBehavior(innerHTML, offset = 0) {
|
|
252
|
+
// eslint-disable-next-line no-constant-condition
|
|
253
|
+
while (true) {
|
|
254
|
+
const currentSlice = innerHTML.slice(offset);
|
|
255
|
+
// client side binding will capture all bindings starting with "{"
|
|
256
|
+
const dataBindingOpen = currentSlice.indexOf(openClientSideBinding);
|
|
257
|
+
const directiveBindingOpen = currentSlice.indexOf(openTagStart);
|
|
258
|
+
const nextDataBindingBehavior = getNextDataBindingBehavior(currentSlice);
|
|
259
|
+
if (dataBindingOpen === -1 && directiveBindingOpen === -1) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
if (dataBindingOpen !== -1 &&
|
|
263
|
+
nextDataBindingBehavior.bindingType === "client" &&
|
|
264
|
+
!isLegitimateClientSideBinding(nextDataBindingBehavior)) {
|
|
265
|
+
offset = nextDataBindingBehavior.closingEndIndex + offset;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (directiveBindingOpen !== -1 &&
|
|
269
|
+
(dataBindingOpen === -1 || dataBindingOpen > directiveBindingOpen)) {
|
|
270
|
+
return offsetDirective(getNextDirectiveBehavior(currentSlice), offset);
|
|
271
|
+
}
|
|
272
|
+
return offsetDataBinding(nextDataBindingBehavior, offset);
|
|
168
273
|
}
|
|
169
|
-
return getNextDataBindingBehavior(innerHTML);
|
|
170
274
|
}
|
|
171
275
|
/**
|
|
172
|
-
*
|
|
173
|
-
* @param
|
|
174
|
-
* @param offset
|
|
175
|
-
* @
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
276
|
+
* Apply an offset to a data binding
|
|
277
|
+
* @param config DataBindingBehaviorConfig
|
|
278
|
+
* @param offset number
|
|
279
|
+
* @returns DataBindingBehaviorConfig
|
|
280
|
+
*/
|
|
281
|
+
function offsetDataBinding(config, offset) {
|
|
282
|
+
config.openingStartIndex = config.openingStartIndex + offset;
|
|
283
|
+
config.openingEndIndex = config.openingEndIndex + offset;
|
|
284
|
+
config.closingStartIndex = config.closingStartIndex + offset;
|
|
285
|
+
config.closingEndIndex = config.closingEndIndex + offset;
|
|
286
|
+
return config;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Apply an offset to a directive
|
|
290
|
+
* @param config TemplateDirectiveBehaviorConfig
|
|
291
|
+
* @param offset number
|
|
292
|
+
* @returns TemplateDirectiveBehaviorConfig
|
|
293
|
+
*/
|
|
294
|
+
function offsetDirective(config, offset) {
|
|
295
|
+
config.openingTagStartIndex = config.openingTagStartIndex + offset;
|
|
296
|
+
config.openingTagEndIndex = config.openingTagEndIndex + offset;
|
|
297
|
+
config.closingTagStartIndex = config.closingTagStartIndex + offset;
|
|
298
|
+
config.closingTagEndIndex = config.closingTagEndIndex + offset;
|
|
299
|
+
return config;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Determine if this client side binding is legitimate.
|
|
303
|
+
* Single-brace (client) bindings are only valid for events, properties, and attribute directives.
|
|
304
|
+
* Checking for this prevents CSS/JS curly braces from being misinterpreted as bindings.
|
|
305
|
+
* @param result
|
|
306
|
+
* @returns
|
|
307
|
+
*/
|
|
308
|
+
function isLegitimateClientSideBinding(result) {
|
|
309
|
+
return ((result.subtype === "attribute" &&
|
|
310
|
+
(result.aspect === "@" || result.aspect === ":")) ||
|
|
311
|
+
result.subtype === "attributeDirective");
|
|
200
312
|
}
|
|
201
313
|
/**
|
|
202
314
|
* Create a function to resolve a value from an object using a path with dot syntax.
|
|
@@ -205,9 +317,45 @@ export function getAllPartials(innerHTML, offset = 0, partials = {}) {
|
|
|
205
317
|
* @param self - Where the first item in the path path refers to the item itself (used by repeat).
|
|
206
318
|
* @returns A function to access the value from a given path.
|
|
207
319
|
*/
|
|
208
|
-
export function pathResolver(path,
|
|
320
|
+
export function pathResolver(path, contextPath, level, rootSchema) {
|
|
321
|
+
var _a, _b;
|
|
209
322
|
let splitPath = path.split(".");
|
|
210
|
-
|
|
323
|
+
// Explicit context access via contextPrefix — resolve directly from ExecutionContext
|
|
324
|
+
if (splitPath[0] === contextPrefix) {
|
|
325
|
+
const contextAccessPath = splitPath.slice(1);
|
|
326
|
+
return (_accessibleObject, context) => {
|
|
327
|
+
return contextAccessPath.reduce((prev, item) => prev === null || prev === void 0 ? void 0 : prev[item], context);
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
let levelCount = level;
|
|
331
|
+
let self = splitPath[0] === contextPath;
|
|
332
|
+
const parentContexts = [];
|
|
333
|
+
if (level > 0 &&
|
|
334
|
+
((_b = (_a = rootSchema === null || rootSchema === void 0 ? void 0 : rootSchema[defsPropertyName]) === null || _a === void 0 ? void 0 : _a[contextPath]) === null || _b === void 0 ? void 0 : _b[fastContextMetaData]) ===
|
|
335
|
+
splitPath.at(-1)) {
|
|
336
|
+
self = true;
|
|
337
|
+
}
|
|
338
|
+
while (levelCount > 0 && !self) {
|
|
339
|
+
if (levelCount !== 1) {
|
|
340
|
+
parentContexts.push("parentContext");
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
parentContexts.push("parent");
|
|
344
|
+
}
|
|
345
|
+
levelCount--;
|
|
346
|
+
}
|
|
347
|
+
splitPath = [...parentContexts, ...splitPath];
|
|
348
|
+
return pathWithContextResolver(splitPath, self);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Creates a resolver function that can access properties from an object using a split path array
|
|
352
|
+
* @param splitPath - The dot syntax path split into an array of property names
|
|
353
|
+
* @param self - Whether the first item in the path refers to the item itself
|
|
354
|
+
* @returns A function that resolves the value from the given path on an accessible object
|
|
355
|
+
*/
|
|
356
|
+
function pathWithContextResolver(splitPath, self) {
|
|
357
|
+
const isInPreviousContext = splitPath[0] === "parent" || splitPath[0] === "parentContext";
|
|
358
|
+
if (self && !isInPreviousContext) {
|
|
211
359
|
if (splitPath.length > 1) {
|
|
212
360
|
splitPath = splitPath.slice(1);
|
|
213
361
|
}
|
|
@@ -217,9 +365,11 @@ export function pathResolver(path, self = false) {
|
|
|
217
365
|
};
|
|
218
366
|
}
|
|
219
367
|
}
|
|
220
|
-
if (
|
|
221
|
-
return (accessibleObject) => {
|
|
222
|
-
return
|
|
368
|
+
if (isInPreviousContext) {
|
|
369
|
+
return (accessibleObject, context) => {
|
|
370
|
+
return splitPath.reduce((previousAccessors, pathItem) => {
|
|
371
|
+
return previousAccessors === null || previousAccessors === void 0 ? void 0 : previousAccessors[pathItem];
|
|
372
|
+
}, context);
|
|
223
373
|
};
|
|
224
374
|
}
|
|
225
375
|
return (accessibleObject) => {
|
|
@@ -228,12 +378,83 @@ export function pathResolver(path, self = false) {
|
|
|
228
378
|
}, accessibleObject);
|
|
229
379
|
};
|
|
230
380
|
}
|
|
381
|
+
export function bindingResolver(previousString, rootPropertyName, path, parentContext, type, schema, currentContext, level) {
|
|
382
|
+
// Explicit context access — resolve from ExecutionContext, skip schema tracking
|
|
383
|
+
if (path.startsWith(contextPrefixDot)) {
|
|
384
|
+
const segments = path.split(".").slice(1);
|
|
385
|
+
return (_x, context) => {
|
|
386
|
+
return segments.reduce((prev, item) => prev === null || prev === void 0 ? void 0 : prev[item], context);
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
rootPropertyName = getRootPropertyName(rootPropertyName, path, currentContext, type);
|
|
390
|
+
if (type !== "event" && rootPropertyName !== null) {
|
|
391
|
+
const childrenMap = getChildrenMap(previousString);
|
|
392
|
+
schema.addPath({
|
|
393
|
+
pathConfig: {
|
|
394
|
+
type,
|
|
395
|
+
currentContext,
|
|
396
|
+
parentContext,
|
|
397
|
+
path,
|
|
398
|
+
},
|
|
399
|
+
rootPropertyName,
|
|
400
|
+
childrenMap,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
return pathResolver(path, currentContext, level, schema.getSchema(rootPropertyName));
|
|
404
|
+
}
|
|
405
|
+
export function expressionResolver(rootPropertyName, expression, parentContext, level, schema) {
|
|
406
|
+
// Extract all paths from the expression and add them to the schema
|
|
407
|
+
if (rootPropertyName !== null) {
|
|
408
|
+
const paths = extractPathsFromChainedExpression(expression);
|
|
409
|
+
paths.forEach(path => {
|
|
410
|
+
if (path.startsWith(contextPrefixDot))
|
|
411
|
+
return;
|
|
412
|
+
schema.addPath({
|
|
413
|
+
pathConfig: {
|
|
414
|
+
type: "access",
|
|
415
|
+
currentContext: parentContext,
|
|
416
|
+
parentContext: null,
|
|
417
|
+
path,
|
|
418
|
+
},
|
|
419
|
+
rootPropertyName,
|
|
420
|
+
childrenMap: null,
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return (x, c) => resolveChainedExpression(x, c, level, parentContext || null, expression, schema.getSchema(rootPropertyName));
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Extracts all paths from a ChainedExpression, including nested expressions
|
|
428
|
+
* @param chainedExpression - The chained expression to extract paths from
|
|
429
|
+
* @returns A Set containing all unique paths found in the expression chain
|
|
430
|
+
*/
|
|
431
|
+
export function extractPathsFromChainedExpression(chainedExpression) {
|
|
432
|
+
const paths = new Set();
|
|
433
|
+
function processExpression(expr) {
|
|
434
|
+
// Check left operand (only add if it's not a literal value)
|
|
435
|
+
if (typeof expr.left === "string" && !expr.leftIsValue) {
|
|
436
|
+
paths.add(expr.left);
|
|
437
|
+
}
|
|
438
|
+
// Check right operand (only add if it's not a literal value)
|
|
439
|
+
if (typeof expr.right === "string" && !expr.rightIsValue) {
|
|
440
|
+
paths.add(expr.right);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
let current = chainedExpression;
|
|
444
|
+
while (current !== undefined) {
|
|
445
|
+
processExpression(current.expression);
|
|
446
|
+
current = current.next;
|
|
447
|
+
}
|
|
448
|
+
return paths;
|
|
449
|
+
}
|
|
231
450
|
/**
|
|
232
451
|
* Determine if the operand is a value (boolean, number, string) or an accessor.
|
|
233
|
-
* @param operand
|
|
452
|
+
* @param operand - The string to evaluate as either a literal value or property accessor
|
|
453
|
+
* @returns An object containing the parsed value and whether it represents a literal value
|
|
234
454
|
*/
|
|
235
455
|
function isOperandValue(operand) {
|
|
236
456
|
try {
|
|
457
|
+
operand = operand.replaceAll("'", '"');
|
|
237
458
|
const value = JSON.parse(operand);
|
|
238
459
|
return {
|
|
239
460
|
value,
|
|
@@ -248,34 +469,757 @@ function isOperandValue(operand) {
|
|
|
248
469
|
}
|
|
249
470
|
}
|
|
250
471
|
/**
|
|
251
|
-
*
|
|
252
|
-
* @param
|
|
253
|
-
* @
|
|
472
|
+
* Evaluates parts of an expression chain and chains them with the specified operator
|
|
473
|
+
* @param parts - Each part of an expression chain to be evaluated
|
|
474
|
+
* @param operator - The logical operator used to chain the expression parts
|
|
475
|
+
* @returns A ChainedExpression object representing the linked expressions, or void if no valid expressions found
|
|
254
476
|
*/
|
|
255
|
-
|
|
256
|
-
|
|
477
|
+
function evaluatePartsInExpressionChain(parts, operator) {
|
|
478
|
+
// Process each part recursively and chain them with ||
|
|
479
|
+
const firstPart = getExpressionChain(parts[0]);
|
|
480
|
+
if (firstPart) {
|
|
481
|
+
let current = firstPart;
|
|
482
|
+
for (let i = 1; i < parts.length; i++) {
|
|
483
|
+
const nextPart = getExpressionChain(parts[i]);
|
|
484
|
+
if (nextPart) {
|
|
485
|
+
// Find the end of the current chain
|
|
486
|
+
while (current.next) {
|
|
487
|
+
current = current.next;
|
|
488
|
+
}
|
|
489
|
+
current.next = {
|
|
490
|
+
operator,
|
|
491
|
+
...nextPart,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return firstPart;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Gets the expression chain as a configuration object
|
|
500
|
+
* @param value - The binding string value
|
|
501
|
+
* @returns - A configuration object containing information about the expression
|
|
502
|
+
*/
|
|
503
|
+
export function getExpressionChain(value) {
|
|
504
|
+
// Decode HTML entities in the expression value first
|
|
505
|
+
const decodedValue = decodeExpressionOperators(value);
|
|
506
|
+
// Handle operator precedence: || has lower precedence than &&
|
|
507
|
+
// First, split by || (lowest precedence)
|
|
508
|
+
const orParts = decodedValue.split(/\s*\|\|\s*/);
|
|
509
|
+
if (orParts.length > 1) {
|
|
510
|
+
const firstPart = evaluatePartsInExpressionChain(orParts, Operator.OR);
|
|
511
|
+
if (firstPart) {
|
|
512
|
+
return firstPart;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// If no ||, check for && (higher precedence)
|
|
516
|
+
const andParts = decodedValue.split(/\s*&&\s*/);
|
|
517
|
+
if (andParts.length > 1) {
|
|
518
|
+
// Process each part recursively and chain them with &&
|
|
519
|
+
const firstPart = evaluatePartsInExpressionChain(andParts, "&&");
|
|
520
|
+
if (firstPart) {
|
|
521
|
+
return firstPart;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// No chaining operators found, create a single expression
|
|
525
|
+
if (decodedValue.trim()) {
|
|
257
526
|
return {
|
|
258
|
-
|
|
259
|
-
|
|
527
|
+
expression: getExpression(decodedValue.trim()),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
return void 0;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Parses a binding value string into an Expression object
|
|
534
|
+
* @param value - The binding string value to parse (e.g., "!condition", "foo == bar", "property")
|
|
535
|
+
* @returns An Expression object containing the operator, operands, and whether operands are literal values
|
|
536
|
+
*/
|
|
537
|
+
function getExpression(value) {
|
|
538
|
+
if (value[0] === Operator.NOT) {
|
|
539
|
+
const left = value.slice(1);
|
|
540
|
+
const operandValue = isOperandValue(left);
|
|
541
|
+
return {
|
|
542
|
+
operator: Operator.NOT,
|
|
543
|
+
left,
|
|
544
|
+
leftIsValue: operandValue.isValue,
|
|
260
545
|
right: null,
|
|
261
546
|
rightIsValue: null,
|
|
262
547
|
};
|
|
263
548
|
}
|
|
264
|
-
const split = value.split(
|
|
549
|
+
const split = value.split(/\s*([=!]=|[><]=?)\s*/);
|
|
265
550
|
if (split.length === 3) {
|
|
266
551
|
const operator = split[1];
|
|
267
|
-
const
|
|
552
|
+
const right = split[2];
|
|
553
|
+
const rightOperandValue = isOperandValue(right);
|
|
554
|
+
const left = split[0];
|
|
555
|
+
const leftOperandValue = isOperandValue(left);
|
|
268
556
|
return {
|
|
269
557
|
operator,
|
|
270
558
|
left: split[0],
|
|
271
|
-
|
|
272
|
-
|
|
559
|
+
leftIsValue: leftOperandValue.isValue,
|
|
560
|
+
right: rightOperandValue.isValue ? rightOperandValue.value : right,
|
|
561
|
+
rightIsValue: rightOperandValue.isValue,
|
|
273
562
|
};
|
|
274
563
|
}
|
|
275
564
|
return {
|
|
276
|
-
operator:
|
|
565
|
+
operator: Operator.ACCESS,
|
|
277
566
|
left: value,
|
|
567
|
+
leftIsValue: false,
|
|
278
568
|
right: null,
|
|
279
569
|
rightIsValue: null,
|
|
280
570
|
};
|
|
281
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* Decodes HTML entities within expression strings only (for operators like &&, <, >)
|
|
574
|
+
* This is safer than decoding the entire template as it preserves HTML-encoded content
|
|
575
|
+
* and only decodes operators needed for expression evaluation
|
|
576
|
+
* @param expression - The expression string to decode
|
|
577
|
+
* @returns The expression with operators decoded
|
|
578
|
+
*/
|
|
579
|
+
function decodeExpressionOperators(expression) {
|
|
580
|
+
return expression
|
|
581
|
+
.replace(/&&/g, Operator.AND)
|
|
582
|
+
.replace(/</g, Operator.LESS_THAN)
|
|
583
|
+
.replace(/>/g, Operator.GREATER_THAN);
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Resolve a single expression by evaluating its operator and operands
|
|
587
|
+
* @param x - The current data context
|
|
588
|
+
* @param c - The parent context for accessing parent scope data
|
|
589
|
+
* @param level - The nesting level for context resolution
|
|
590
|
+
* @param contextPath - The current context path for property resolution
|
|
591
|
+
* @param expression - The expression object to evaluate
|
|
592
|
+
* @param rootSchema - The root JSON schema for data validation and navigation
|
|
593
|
+
* @returns The resolved value of the expression
|
|
594
|
+
*/
|
|
595
|
+
function resolveExpression(x, c, level, contextPath, expression, rootSchema) {
|
|
596
|
+
const { operator, left, right, rightIsValue } = expression;
|
|
597
|
+
const resolvedLeft = pathResolver(left, contextPath, level, rootSchema)(x, c);
|
|
598
|
+
let resolvedRight = right;
|
|
599
|
+
if (!rightIsValue && typeof right === "string") {
|
|
600
|
+
resolvedRight = pathResolver(right, contextPath, level, rootSchema)(x, c);
|
|
601
|
+
}
|
|
602
|
+
switch (operator) {
|
|
603
|
+
case Operator.NOT: {
|
|
604
|
+
return !resolvedLeft;
|
|
605
|
+
}
|
|
606
|
+
case Operator.EQUALS: {
|
|
607
|
+
return resolvedLeft == resolvedRight;
|
|
608
|
+
}
|
|
609
|
+
case Operator.NOT_EQUALS: {
|
|
610
|
+
return resolvedLeft != resolvedRight;
|
|
611
|
+
}
|
|
612
|
+
case Operator.GREATER_THAN_OR_EQUALS: {
|
|
613
|
+
return resolvedLeft >= resolvedRight;
|
|
614
|
+
}
|
|
615
|
+
case Operator.GREATER_THAN: {
|
|
616
|
+
return resolvedLeft > resolvedRight;
|
|
617
|
+
}
|
|
618
|
+
case Operator.LESS_THAN_OR_EQUALS: {
|
|
619
|
+
return resolvedLeft <= resolvedRight;
|
|
620
|
+
}
|
|
621
|
+
case Operator.LESS_THAN: {
|
|
622
|
+
return resolvedLeft < resolvedRight;
|
|
623
|
+
}
|
|
624
|
+
default: {
|
|
625
|
+
if (typeof resolvedLeft === "boolean") {
|
|
626
|
+
return resolvedLeft;
|
|
627
|
+
}
|
|
628
|
+
if (typeof resolvedLeft === "number") {
|
|
629
|
+
return resolvedLeft !== 0;
|
|
630
|
+
}
|
|
631
|
+
if (typeof resolvedLeft === "string") {
|
|
632
|
+
return resolvedLeft.length > 0;
|
|
633
|
+
}
|
|
634
|
+
return !!resolvedLeft;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Resolve a chained expression by evaluating expressions linked with logical operators
|
|
640
|
+
* @param x - The current data context
|
|
641
|
+
* @param c - The parent context for accessing parent scope data
|
|
642
|
+
* @param level - The nesting level for context resolution
|
|
643
|
+
* @param contextPath - The current context path for property resolution
|
|
644
|
+
* @param expression - The chained expression object containing linked expressions
|
|
645
|
+
* @param rootSchema - The root JSON schema for data validation and navigation
|
|
646
|
+
* @returns The resolved boolean result of the chained expression
|
|
647
|
+
*/
|
|
648
|
+
function resolveChainedExpression(x, c, level, contextPath, expression, rootSchema) {
|
|
649
|
+
const { expression: expr, next } = expression;
|
|
650
|
+
const resolvedLeft = resolveExpression(x, c, level, contextPath, expr, rootSchema);
|
|
651
|
+
if (next) {
|
|
652
|
+
const resolvedRight = resolveChainedExpression(x, c, level, contextPath, next, rootSchema);
|
|
653
|
+
switch (next.operator) {
|
|
654
|
+
case Operator.AND: {
|
|
655
|
+
return resolvedLeft && resolvedRight;
|
|
656
|
+
}
|
|
657
|
+
case Operator.OR: {
|
|
658
|
+
return resolvedLeft || resolvedRight;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return resolvedLeft;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* This is the transform utility for rationalizing declarative HTML syntax
|
|
666
|
+
* with bindings in the ViewTemplate
|
|
667
|
+
* @param innerHTML The innerHTML to transform
|
|
668
|
+
* @param index The index to start the current slice of HTML to evaluate
|
|
669
|
+
*/
|
|
670
|
+
export function transformInnerHTML(innerHTML, index = 0) {
|
|
671
|
+
const sliceToEvaluate = innerHTML.slice(index);
|
|
672
|
+
const nextBinding = getNextBehavior(sliceToEvaluate);
|
|
673
|
+
let transformedInnerHTML = innerHTML;
|
|
674
|
+
if (nextBinding && nextBinding.type === "dataBinding") {
|
|
675
|
+
if (nextBinding.bindingType === "unescaped") {
|
|
676
|
+
transformedInnerHTML = `${innerHTML.slice(0, index)}${sliceToEvaluate.slice(0, nextBinding.openingStartIndex)}${startInnerHTMLDiv}${sliceToEvaluate.slice(nextBinding.openingStartIndex + 3, nextBinding.closingStartIndex)}${endInnerHTMLDiv}${sliceToEvaluate.slice(nextBinding.closingStartIndex + 3)}`;
|
|
677
|
+
return transformInnerHTML(transformedInnerHTML, index +
|
|
678
|
+
startInnerHTMLDivLength +
|
|
679
|
+
endInnerHTMLDivLength +
|
|
680
|
+
nextBinding.closingStartIndex -
|
|
681
|
+
3);
|
|
682
|
+
}
|
|
683
|
+
else if (nextBinding.bindingType === "client") {
|
|
684
|
+
return transformInnerHTML(transformedInnerHTML, index + nextBinding.closingEndIndex);
|
|
685
|
+
}
|
|
686
|
+
return transformInnerHTML(transformedInnerHTML, index + nextBinding.closingEndIndex);
|
|
687
|
+
}
|
|
688
|
+
else if (nextBinding) {
|
|
689
|
+
return transformInnerHTML(transformedInnerHTML, index + nextBinding.closingTagEndIndex);
|
|
690
|
+
}
|
|
691
|
+
return transformedInnerHTML;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Resolves f-when
|
|
695
|
+
* @param self - Where the first item in the path path refers to the item itself (used by repeat).
|
|
696
|
+
* @param chainedExpression - The chained expression which includes the expression and the next expression
|
|
697
|
+
* if there is another in the chain
|
|
698
|
+
* @returns - A binding that resolves the chained expression logic
|
|
699
|
+
*/
|
|
700
|
+
export function resolveWhen(rootPropertyName, expression, parentContext, level, schema) {
|
|
701
|
+
const binding = expressionResolver(rootPropertyName, expression, parentContext, level, schema);
|
|
702
|
+
return (x, c) => binding(x, c);
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Determines the data type of the provided data
|
|
706
|
+
* @param data - The data to analyze
|
|
707
|
+
* @returns "array" for arrays, "object" for non-null objects, "primitive" for other types
|
|
708
|
+
*/
|
|
709
|
+
function getDataType(data) {
|
|
710
|
+
if (Array.isArray(data))
|
|
711
|
+
return "array";
|
|
712
|
+
if (typeof data === "object" && data !== null)
|
|
713
|
+
return "object";
|
|
714
|
+
return "primitive";
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Get properties from an anyOf array
|
|
718
|
+
* @param anyOf - The anyOf array in a JSON schema
|
|
719
|
+
* @returns The array item matching a ref if it exists
|
|
720
|
+
*/
|
|
721
|
+
function getSchemaPropertiesFromAnyOf(anyOf) {
|
|
722
|
+
let propertiesFromAnyOf = null;
|
|
723
|
+
for (const anyOfItem of anyOf) {
|
|
724
|
+
if (anyOfItem[refPropertyName]) {
|
|
725
|
+
const splitRef = anyOfItem[refPropertyName].split("/");
|
|
726
|
+
const customElement = splitRef.slice(-2)[0];
|
|
727
|
+
const attributeName = splitRef.slice(-1)[0].slice(0, -5);
|
|
728
|
+
if (Schema.jsonSchemaMap.has(customElement)) {
|
|
729
|
+
const customElementSchemaMap = Schema.jsonSchemaMap.get(customElement);
|
|
730
|
+
propertiesFromAnyOf = customElementSchemaMap.get(attributeName);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return propertiesFromAnyOf;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Gets a properties definition if one exists
|
|
738
|
+
* @param schema - The JSON schema to get properties from
|
|
739
|
+
* @returns A JSON schema with properties or null
|
|
740
|
+
*/
|
|
741
|
+
function getSchemaProperties(schema) {
|
|
742
|
+
if (schema === null || schema === void 0 ? void 0 : schema.properties) {
|
|
743
|
+
return schema.properties;
|
|
744
|
+
}
|
|
745
|
+
else if (schema === null || schema === void 0 ? void 0 : schema.anyOf) {
|
|
746
|
+
return getSchemaPropertiesFromAnyOf(schema.anyOf);
|
|
747
|
+
}
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Assigns Observable properties to items in an array and sets up change notifications
|
|
752
|
+
* @param proxiedData - The array data to make observable
|
|
753
|
+
* @param schema - The schema defining the structure of array items
|
|
754
|
+
* @param rootSchema - The root schema for the entire data structure
|
|
755
|
+
* @returns The array with observable properties and change notifications
|
|
756
|
+
*/
|
|
757
|
+
function assignObservablesToArray(proxiedData, schema, rootSchema, target, rootProperty) {
|
|
758
|
+
const data = proxiedData.map((item) => {
|
|
759
|
+
const originalItem = Object.assign({}, item);
|
|
760
|
+
assignProxyToItemsInArray(item, originalItem, schema, rootSchema);
|
|
761
|
+
return Object.assign(item, originalItem);
|
|
762
|
+
});
|
|
763
|
+
Observable.getNotifier(data).subscribe({
|
|
764
|
+
handleChange(subject, args) {
|
|
765
|
+
args.forEach((arg) => {
|
|
766
|
+
if (arg.addedCount > 0) {
|
|
767
|
+
for (let i = arg.addedCount - 1; i >= 0; i--) {
|
|
768
|
+
const item = subject[arg.index + i];
|
|
769
|
+
const originalItem = Object.assign({}, item);
|
|
770
|
+
assignProxyToItemsInArray(item, originalItem, schema, rootSchema);
|
|
771
|
+
Object.assign(item, originalItem);
|
|
772
|
+
}
|
|
773
|
+
// Notify observers of the target object's root property
|
|
774
|
+
Observable.notify(target, rootProperty);
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
},
|
|
778
|
+
});
|
|
779
|
+
return data;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Extracts the definition name from a JSON Schema $ref property
|
|
783
|
+
* @param defName - The $ref string (e.g., "#/$defs/MyType")
|
|
784
|
+
* @returns The definition name (e.g., "MyType")
|
|
785
|
+
*/
|
|
786
|
+
function getDefFromRef(defName) {
|
|
787
|
+
const splitName = defName.split("/");
|
|
788
|
+
return splitName.at(-1);
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Find a definition
|
|
792
|
+
* This may exist as a $ref at the root or as a $ref in any anyOf or not at all
|
|
793
|
+
* if the Observer Map has not been enabled on a child component
|
|
794
|
+
* @param schema - The JSON schema to find the ref in
|
|
795
|
+
* @returns The definition or null
|
|
796
|
+
*/
|
|
797
|
+
export function findDef(schema) {
|
|
798
|
+
const defStartingString = "#/$defs";
|
|
799
|
+
if (schema[refPropertyName] &&
|
|
800
|
+
schema[refPropertyName].startsWith(defStartingString)) {
|
|
801
|
+
return getDefFromRef(schema[refPropertyName]);
|
|
802
|
+
}
|
|
803
|
+
if (schema.anyOf) {
|
|
804
|
+
const index = schema.anyOf.findIndex((anyOfItem) => {
|
|
805
|
+
return (!!anyOfItem[refPropertyName] &&
|
|
806
|
+
anyOfItem[refPropertyName].startsWith(defStartingString));
|
|
807
|
+
});
|
|
808
|
+
if (index > -1) {
|
|
809
|
+
const ref = schema.anyOf[index][refPropertyName];
|
|
810
|
+
if (ref.startsWith(defStartingString)) {
|
|
811
|
+
return getDefFromRef(ref);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Subscribe to a notifier on data that is an observed array
|
|
819
|
+
* @param data - The array being observed
|
|
820
|
+
* @param updateArrayObservables - The function to call to update the array item
|
|
821
|
+
*/
|
|
822
|
+
function assignSubscribeToObservableArray(data, updateArrayObservables) {
|
|
823
|
+
Observable.getNotifier(data).subscribe({
|
|
824
|
+
handleChange(subject, args) {
|
|
825
|
+
args.forEach((arg) => {
|
|
826
|
+
updateArrayObservables();
|
|
827
|
+
});
|
|
828
|
+
},
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Assign observables to data
|
|
833
|
+
* @param schema - The schema
|
|
834
|
+
* @param rootSchema - The root schema mapping to the root property
|
|
835
|
+
* @param data - The data
|
|
836
|
+
* @param target - The target custom element
|
|
837
|
+
* @param rootProperty - The root property
|
|
838
|
+
* @returns
|
|
839
|
+
*/
|
|
840
|
+
export function assignObservables(schema, rootSchema, data, target, rootProperty) {
|
|
841
|
+
var _a;
|
|
842
|
+
const dataType = getDataType(data);
|
|
843
|
+
let proxiedData = data;
|
|
844
|
+
switch (dataType) {
|
|
845
|
+
case "array": {
|
|
846
|
+
const context = findDef(schema);
|
|
847
|
+
if (context) {
|
|
848
|
+
proxiedData = assignObservablesToArray(proxiedData, (_a = rootSchema[defsPropertyName]) === null || _a === void 0 ? void 0 : _a[context], rootSchema, target, rootProperty);
|
|
849
|
+
if (!observedArraysMap.has(proxiedData)) {
|
|
850
|
+
observedArraysMap.set(proxiedData, assignSubscribeToObservableArray(proxiedData, () => {
|
|
851
|
+
var _a;
|
|
852
|
+
return assignObservablesToArray(proxiedData, (_a = rootSchema[defsPropertyName]) === null || _a === void 0 ? void 0 : _a[context], rootSchema, target, rootProperty);
|
|
853
|
+
}));
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
case "object": {
|
|
859
|
+
proxiedData = assignProxyToItemsInObject(target, rootProperty, proxiedData, schema, rootSchema);
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return proxiedData;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Assign a proxy to items in an array
|
|
867
|
+
* @param proxiableItem - The array item to proxy
|
|
868
|
+
* @param originalItem - The original array item
|
|
869
|
+
* @param schema - The schema mapping to the items in the array
|
|
870
|
+
* @param rootSchema - The root schema assigned to the root property
|
|
871
|
+
*/
|
|
872
|
+
function assignProxyToItemsInArray(proxiableItem, originalItem, schema, rootSchema) {
|
|
873
|
+
const schemaProperties = getSchemaProperties(schema);
|
|
874
|
+
getObjectProperties(proxiableItem, schemaProperties).forEach(key => {
|
|
875
|
+
// Initialize the property as undefined if it doesn't exist
|
|
876
|
+
if (!(key in originalItem)) {
|
|
877
|
+
originalItem[key] = undefined;
|
|
878
|
+
}
|
|
879
|
+
// Assign the proxy first
|
|
880
|
+
originalItem[key] = assignProxyToItemsInObject(proxiableItem, key, originalItem[key], schemaProperties[key], rootSchema);
|
|
881
|
+
// Then make the property observable
|
|
882
|
+
Observable.defineProperty(proxiableItem, key);
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Get an objects properties as agreed upon between the schema and data
|
|
887
|
+
* @param data - The data
|
|
888
|
+
* @param schemaProperties - The schema properties
|
|
889
|
+
* @returns A list of strings the schema properties enumerate (includes properties not present in data)
|
|
890
|
+
*/
|
|
891
|
+
function getObjectProperties(data, schemaProperties) {
|
|
892
|
+
const dataKeys = Object.keys(data);
|
|
893
|
+
const schemaPropertyKeys = Object.keys(schemaProperties !== null && schemaProperties !== void 0 ? schemaProperties : {});
|
|
894
|
+
// Return all schema properties that are either in the data or in the schema
|
|
895
|
+
// This ensures properties defined in schema but missing from data get initialized
|
|
896
|
+
const allKeys = new Set([...dataKeys, ...schemaPropertyKeys]);
|
|
897
|
+
return Array.from(allKeys).filter(function (key) {
|
|
898
|
+
return schemaPropertyKeys.indexOf(key) !== -1;
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Assign a proxy to items in an object
|
|
903
|
+
* @param target - The target custom element
|
|
904
|
+
* @param rootProperty - The root property
|
|
905
|
+
* @param data - The data to proxy
|
|
906
|
+
* @param schema - The schema for the data
|
|
907
|
+
* @param rootSchema - The root schema for the root property
|
|
908
|
+
* @returns a Proxy
|
|
909
|
+
*/
|
|
910
|
+
function assignProxyToItemsInObject(target, rootProperty, data, schema, rootSchema) {
|
|
911
|
+
var _a;
|
|
912
|
+
const type = getDataType(data);
|
|
913
|
+
const schemaProperties = getSchemaProperties(schema);
|
|
914
|
+
let proxiedData = data;
|
|
915
|
+
if (type === "object" && schemaProperties) {
|
|
916
|
+
// navigate through all items in the object
|
|
917
|
+
getObjectProperties(proxiedData, schemaProperties).forEach(property => {
|
|
918
|
+
proxiedData[property] = assignProxyToItemsInObject(target, rootProperty, proxiedData[property], schemaProperties[property], rootSchema);
|
|
919
|
+
});
|
|
920
|
+
// assign a Proxy to the object
|
|
921
|
+
proxiedData = assignProxy(schema, rootSchema, target, rootProperty, proxiedData);
|
|
922
|
+
// Add this target to the object's target list
|
|
923
|
+
addTargetToObject(proxiedData, target, rootProperty);
|
|
924
|
+
}
|
|
925
|
+
else if (type === "array") {
|
|
926
|
+
const context = findDef(schema.items);
|
|
927
|
+
if (context) {
|
|
928
|
+
const definition = (_a = rootSchema[defsPropertyName]) === null || _a === void 0 ? void 0 : _a[context];
|
|
929
|
+
if ((definition === null || definition === void 0 ? void 0 : definition.type) === "object") {
|
|
930
|
+
proxiedData = assignObservablesToArray(proxiedData, definition, rootSchema, target, rootProperty);
|
|
931
|
+
if (!observedArraysMap.has(proxiedData)) {
|
|
932
|
+
observedArraysMap.set(proxiedData, assignObservablesToArray(proxiedData, definition, rootSchema, target, rootProperty));
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
return proxiedData;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Add a target to an object's target list
|
|
941
|
+
* @param object - The object to associate with the target
|
|
942
|
+
* @param target - The target custom element
|
|
943
|
+
* @param rootProperty - The root property name
|
|
944
|
+
*/
|
|
945
|
+
function addTargetToObject(object, target, rootProperty) {
|
|
946
|
+
if (!objectTargetsMap.has(object)) {
|
|
947
|
+
objectTargetsMap.set(object, []);
|
|
948
|
+
}
|
|
949
|
+
const targets = objectTargetsMap.get(object);
|
|
950
|
+
targets.push({ target, rootProperty });
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Get all targets for an object
|
|
954
|
+
* @param object - The object to get targets for
|
|
955
|
+
* @returns Array of target info objects
|
|
956
|
+
*/
|
|
957
|
+
function getTargetsForObject(object) {
|
|
958
|
+
return objectTargetsMap.get(object) || [];
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Notify any observables mapped to the object
|
|
962
|
+
* @param targetObject The object that is mapped to a target and rootProperty
|
|
963
|
+
*/
|
|
964
|
+
function notifyObservables(targetObject) {
|
|
965
|
+
getTargetsForObject(targetObject).forEach((targetItem) => {
|
|
966
|
+
// Trigger notification for property changes
|
|
967
|
+
Observable.notify(targetItem.target, targetItem.rootProperty);
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Assign a proxy to an object
|
|
972
|
+
* @param schema - The current schema
|
|
973
|
+
* @param rootSchema - The root schema for the root property
|
|
974
|
+
* @param target - The target custom element
|
|
975
|
+
* @param rootProperty - The root property
|
|
976
|
+
* @param object - The object to assign the proxy to
|
|
977
|
+
* @returns Proxy object
|
|
978
|
+
*/
|
|
979
|
+
export function assignProxy(schema, rootSchema, target, rootProperty, object) {
|
|
980
|
+
if (!object.$isProxy) {
|
|
981
|
+
// Create a proxy for the object that triggers Observable.notify on mutations
|
|
982
|
+
const proxy = new Proxy(object, {
|
|
983
|
+
set: (obj, prop, value) => {
|
|
984
|
+
const currentValue = obj[prop];
|
|
985
|
+
if (deepEqual(currentValue, value)) {
|
|
986
|
+
return true;
|
|
987
|
+
}
|
|
988
|
+
obj[prop] = assignObservables(schema, rootSchema, value, target, rootProperty);
|
|
989
|
+
notifyObservables(proxy);
|
|
990
|
+
return true;
|
|
991
|
+
},
|
|
992
|
+
get: (target, key) => {
|
|
993
|
+
if (key !== "$isProxy") {
|
|
994
|
+
return target[key];
|
|
995
|
+
}
|
|
996
|
+
return true;
|
|
997
|
+
},
|
|
998
|
+
deleteProperty: (obj, prop) => {
|
|
999
|
+
if (prop in obj) {
|
|
1000
|
+
delete obj[prop];
|
|
1001
|
+
notifyObservables(proxy);
|
|
1002
|
+
return true;
|
|
1003
|
+
}
|
|
1004
|
+
return false;
|
|
1005
|
+
},
|
|
1006
|
+
});
|
|
1007
|
+
return proxy;
|
|
1008
|
+
}
|
|
1009
|
+
return object;
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Get the root property name
|
|
1013
|
+
* @param rootPropertyName - The root property
|
|
1014
|
+
* @param path - The dot syntax path
|
|
1015
|
+
* @param context - The context created by a repeat
|
|
1016
|
+
* @param type - The type of path binding
|
|
1017
|
+
* @returns
|
|
1018
|
+
*/
|
|
1019
|
+
export function getRootPropertyName(rootPropertyName, path, context, type) {
|
|
1020
|
+
return (rootPropertyName === null || context === null) && type !== "event"
|
|
1021
|
+
? path.split(".")[0]
|
|
1022
|
+
: rootPropertyName;
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Get details of bindings to the attributes of child custom elements
|
|
1026
|
+
* @param previousString - The previous string before the binding
|
|
1027
|
+
* @returns null, or a custom element name and attribute name
|
|
1028
|
+
*/
|
|
1029
|
+
export function getChildrenMap(previousString) {
|
|
1030
|
+
if (typeof previousString === "string" &&
|
|
1031
|
+
isAttribute(previousString, previousString.length)) {
|
|
1032
|
+
const customElementName = getAttributesCustomElementName(previousString);
|
|
1033
|
+
if (customElementName) {
|
|
1034
|
+
return {
|
|
1035
|
+
customElementName,
|
|
1036
|
+
attributeName: getAttributeName(previousString),
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Get the HTML element that is passing the attribute binding
|
|
1044
|
+
* @param previousString - The previous string before the binding
|
|
1045
|
+
* @returns null if this is not a custom element, or the custom element that is passing the binding as an attribute
|
|
1046
|
+
*/
|
|
1047
|
+
function getAttributesCustomElementName(previousString) {
|
|
1048
|
+
const indexOfElementTagStart = previousString.lastIndexOf("<") + 1;
|
|
1049
|
+
const indexOfElementTagEnd = previousString.slice(indexOfElementTagStart).indexOf(" ") +
|
|
1050
|
+
indexOfElementTagStart;
|
|
1051
|
+
const elementName = previousString.slice(indexOfElementTagStart, indexOfElementTagEnd);
|
|
1052
|
+
if (elementName.includes("-")) {
|
|
1053
|
+
return elementName;
|
|
1054
|
+
}
|
|
1055
|
+
return null;
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Gets a non-aspected attribute name
|
|
1059
|
+
* @param previousString - The previous string before the binding
|
|
1060
|
+
* @returns The attribute name with any aspects (:, ?, @) removed
|
|
1061
|
+
*/
|
|
1062
|
+
function getAttributeName(previousString) {
|
|
1063
|
+
const indexOfAttributeStart = previousString.lastIndexOf(" ") + 1;
|
|
1064
|
+
const indexOfAttributeEnd = previousString.slice(indexOfAttributeStart).indexOf("=") + indexOfAttributeStart;
|
|
1065
|
+
const attributeName = previousString.slice(indexOfAttributeStart, indexOfAttributeEnd);
|
|
1066
|
+
const potentialAspect = attributeName.charAt(0);
|
|
1067
|
+
if (potentialAspect === ":" || potentialAspect === "@" || potentialAspect === "?") {
|
|
1068
|
+
return attributeName.slice(1);
|
|
1069
|
+
}
|
|
1070
|
+
return attributeName;
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Determine if an object has an observable accessor for a backing field
|
|
1074
|
+
* @param object - The object to check
|
|
1075
|
+
* @param backingField - The backing field name
|
|
1076
|
+
* @returns True if the object has an observable accessor for the backing field, false otherwise
|
|
1077
|
+
*/
|
|
1078
|
+
function hasObservableAccessor(object, backingField) {
|
|
1079
|
+
const accessors = Observable.getAccessors(object);
|
|
1080
|
+
if (!accessors) {
|
|
1081
|
+
return false;
|
|
1082
|
+
}
|
|
1083
|
+
return accessors.some((accessor) => accessor.name === backingField);
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Determine if a key should be skipped during deep comparison
|
|
1087
|
+
*
|
|
1088
|
+
* @param object - The object to check
|
|
1089
|
+
* @param key - The key to evaluate
|
|
1090
|
+
* @returns True if the key should be skipped during comparison, false otherwise
|
|
1091
|
+
*/
|
|
1092
|
+
function shouldSkipKey(object, key) {
|
|
1093
|
+
if (key[0] !== "_" || key === "_") {
|
|
1094
|
+
return false;
|
|
1095
|
+
}
|
|
1096
|
+
return hasObservableAccessor(object, key.slice(1));
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Get comparable keys from an object, excluding those that should be skipped
|
|
1100
|
+
*
|
|
1101
|
+
* @param object - The object to extract keys from
|
|
1102
|
+
* @returns An array of keys that should be compared
|
|
1103
|
+
*/
|
|
1104
|
+
function getComparableKeys(object) {
|
|
1105
|
+
const hasOwn = Object.prototype.hasOwnProperty;
|
|
1106
|
+
const keys = [];
|
|
1107
|
+
for (const key in object) {
|
|
1108
|
+
if (!hasOwn.call(object, key) || shouldSkipKey(object, key)) {
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
keys.push(key);
|
|
1112
|
+
}
|
|
1113
|
+
return keys;
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Deeply compares two objects for equality.
|
|
1117
|
+
*
|
|
1118
|
+
* @param obj1 - First object to compare
|
|
1119
|
+
* @param obj2 - Second object to compare
|
|
1120
|
+
* @returns True if the objects are deeply equal, false otherwise
|
|
1121
|
+
*/
|
|
1122
|
+
export function deepEqual(obj1, obj2) {
|
|
1123
|
+
if (Object.is(obj1, obj2)) {
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
if (obj1 == null || obj2 == null) {
|
|
1127
|
+
return false;
|
|
1128
|
+
}
|
|
1129
|
+
const type1 = typeof obj1;
|
|
1130
|
+
const type2 = typeof obj2;
|
|
1131
|
+
if (type1 !== type2 || type1 !== "object") {
|
|
1132
|
+
return false;
|
|
1133
|
+
}
|
|
1134
|
+
const isArray1 = Array.isArray(obj1);
|
|
1135
|
+
const isArray2 = Array.isArray(obj2);
|
|
1136
|
+
if (isArray1 !== isArray2) {
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
if (isArray1) {
|
|
1140
|
+
const len = obj1.length;
|
|
1141
|
+
if (len !== obj2.length) {
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
for (let i = 0; i < len; i++) {
|
|
1145
|
+
if (!deepEqual(obj1[i], obj2[i])) {
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return true;
|
|
1150
|
+
}
|
|
1151
|
+
const hasOwn = Object.prototype.hasOwnProperty;
|
|
1152
|
+
const obj1Keys = getComparableKeys(obj1);
|
|
1153
|
+
const obj2Keys = getComparableKeys(obj2);
|
|
1154
|
+
if (obj1Keys.length !== obj2Keys.length) {
|
|
1155
|
+
return false;
|
|
1156
|
+
}
|
|
1157
|
+
for (const key of obj1Keys) {
|
|
1158
|
+
if (!hasOwn.call(obj2, key) || !deepEqual(obj1[key], obj2[key])) {
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return true;
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Checks if a value is a plain object (not an array, null, or other type).
|
|
1166
|
+
*
|
|
1167
|
+
* @param value - The value to check
|
|
1168
|
+
* @returns True if the value is a plain object, false otherwise
|
|
1169
|
+
*/
|
|
1170
|
+
export function isPlainObject(value) {
|
|
1171
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Deeply merges the source object into the target object.
|
|
1175
|
+
*
|
|
1176
|
+
* @param target - The target object to merge into
|
|
1177
|
+
* @param source - The source object to merge from
|
|
1178
|
+
* @returns boolean indicating whether changes were made
|
|
1179
|
+
*/
|
|
1180
|
+
export function deepMerge(target, source) {
|
|
1181
|
+
const hasOwn = Object.prototype.hasOwnProperty;
|
|
1182
|
+
let hasChanges = false;
|
|
1183
|
+
for (const key in source) {
|
|
1184
|
+
if (!hasOwn.call(source, key)) {
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
const sourceValue = source[key];
|
|
1188
|
+
if (sourceValue === void 0) {
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
const targetValue = target[key];
|
|
1192
|
+
if (deepEqual(targetValue, sourceValue)) {
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
hasChanges = true;
|
|
1196
|
+
if (Array.isArray(sourceValue)) {
|
|
1197
|
+
const isTargetArray = Array.isArray(targetValue);
|
|
1198
|
+
const clonedItems = sourceValue.map((item) => isPlainObject(item) ? { ...item } : item);
|
|
1199
|
+
if (isTargetArray) {
|
|
1200
|
+
// Use splice to maintain observable array tracking
|
|
1201
|
+
targetValue.splice(0, targetValue.length, ...clonedItems);
|
|
1202
|
+
}
|
|
1203
|
+
else {
|
|
1204
|
+
// Target isn't an array, replace it
|
|
1205
|
+
target[key] = clonedItems;
|
|
1206
|
+
}
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
if (isPlainObject(sourceValue)) {
|
|
1210
|
+
const targetIsObject = isPlainObject(targetValue);
|
|
1211
|
+
const nextTarget = targetIsObject ? { ...targetValue } : {};
|
|
1212
|
+
const nestedChanged = deepMerge(nextTarget, sourceValue);
|
|
1213
|
+
if (!targetIsObject) {
|
|
1214
|
+
target[key] = nextTarget;
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
if (nestedChanged) {
|
|
1218
|
+
target[key] = nextTarget;
|
|
1219
|
+
}
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
target[key] = sourceValue;
|
|
1223
|
+
}
|
|
1224
|
+
return hasChanges;
|
|
1225
|
+
}
|