@cratis/scene.engine 4.14.0 → 4.16.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/QueryBindingState.ts +43 -0
- package/QueryBindingTicket.ts +8 -0
- package/dist/cjs/QueryBindingState.js +38 -0
- package/dist/cjs/QueryBindingState.js.map +1 -0
- package/dist/cjs/editing/resolveEffectiveConfiguration.js +11 -0
- package/dist/cjs/editing/resolveEffectiveConfiguration.js.map +1 -1
- package/dist/cjs/editing/validateValue.js +17 -1
- package/dist/cjs/editing/validateValue.js.map +1 -1
- package/dist/cjs/forms/CommandFormLayoutKey.js +14 -0
- package/dist/cjs/forms/CommandFormLayoutKey.js.map +1 -0
- package/dist/cjs/forms/applyCommandFormLayoutKey.js +72 -0
- package/dist/cjs/forms/applyCommandFormLayoutKey.js.map +1 -0
- package/dist/cjs/forms/stackCommandFormLayout.js +37 -0
- package/dist/cjs/forms/stackCommandFormLayout.js.map +1 -0
- package/dist/cjs/index.js +15 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/nestBindingScope.js +26 -0
- package/dist/cjs/nestBindingScope.js.map +1 -0
- package/dist/cjs/removeComponentOutputs.js +19 -0
- package/dist/cjs/removeComponentOutputs.js.map +1 -0
- package/dist/cjs/resolveArgumentSource.js +36 -0
- package/dist/cjs/resolveArgumentSource.js.map +1 -0
- package/dist/cjs/resolveBindingExpression.js +12 -8
- package/dist/cjs/resolveBindingExpression.js.map +1 -1
- package/dist/esm/QueryBindingState.d.ts +10 -0
- package/dist/esm/QueryBindingState.d.ts.map +1 -0
- package/dist/esm/QueryBindingState.js +36 -0
- package/dist/esm/QueryBindingState.js.map +1 -0
- package/dist/esm/QueryBindingTicket.d.ts +5 -0
- package/dist/esm/QueryBindingTicket.d.ts.map +1 -0
- package/dist/esm/QueryBindingTicket.js +2 -0
- package/dist/esm/QueryBindingTicket.js.map +1 -0
- package/dist/esm/editing/resolveEffectiveConfiguration.d.ts.map +1 -1
- package/dist/esm/editing/resolveEffectiveConfiguration.js +11 -0
- package/dist/esm/editing/resolveEffectiveConfiguration.js.map +1 -1
- package/dist/esm/editing/validateValue.d.ts.map +1 -1
- package/dist/esm/editing/validateValue.js +18 -2
- package/dist/esm/editing/validateValue.js.map +1 -1
- package/dist/esm/forms/CommandFormLayoutKey.d.ts +7 -0
- package/dist/esm/forms/CommandFormLayoutKey.d.ts.map +1 -0
- package/dist/esm/forms/CommandFormLayoutKey.js +12 -0
- package/dist/esm/forms/CommandFormLayoutKey.js.map +1 -0
- package/dist/esm/forms/applyCommandFormLayoutKey.d.ts +5 -0
- package/dist/esm/forms/applyCommandFormLayoutKey.d.ts.map +1 -0
- package/dist/esm/forms/applyCommandFormLayoutKey.js +70 -0
- package/dist/esm/forms/applyCommandFormLayoutKey.js.map +1 -0
- package/dist/esm/forms/index.d.ts +3 -0
- package/dist/esm/forms/index.d.ts.map +1 -1
- package/dist/esm/forms/index.js +3 -0
- package/dist/esm/forms/index.js.map +1 -1
- package/dist/esm/forms/stackCommandFormLayout.d.ts +3 -0
- package/dist/esm/forms/stackCommandFormLayout.d.ts.map +1 -0
- package/dist/esm/forms/stackCommandFormLayout.js +35 -0
- package/dist/esm/forms/stackCommandFormLayout.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/nestBindingScope.d.ts +3 -0
- package/dist/esm/nestBindingScope.d.ts.map +1 -0
- package/dist/esm/nestBindingScope.js +24 -0
- package/dist/esm/nestBindingScope.js.map +1 -0
- package/dist/esm/removeComponentOutputs.d.ts +3 -0
- package/dist/esm/removeComponentOutputs.d.ts.map +1 -0
- package/dist/esm/removeComponentOutputs.js +17 -0
- package/dist/esm/removeComponentOutputs.js.map +1 -0
- package/dist/esm/resolveArgumentSource.d.ts +4 -0
- package/dist/esm/resolveArgumentSource.d.ts.map +1 -0
- package/dist/esm/resolveArgumentSource.js +33 -0
- package/dist/esm/resolveArgumentSource.js.map +1 -0
- package/dist/esm/resolveBindingExpression.d.ts.map +1 -1
- package/dist/esm/resolveBindingExpression.js +12 -8
- package/dist/esm/resolveBindingExpression.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/editing/resolveEffectiveConfiguration.ts +11 -0
- package/editing/validateValue.ts +21 -1
- package/forms/CommandFormLayoutKey.ts +10 -0
- package/forms/applyCommandFormLayoutKey.ts +47 -0
- package/forms/index.ts +3 -0
- package/forms/stackCommandFormLayout.ts +22 -0
- package/index.ts +5 -0
- package/nestBindingScope.ts +19 -0
- package/package.json +3 -3
- package/removeComponentOutputs.ts +15 -0
- package/resolveArgumentSource.ts +33 -0
- package/resolveBindingExpression.ts +14 -7
|
@@ -162,6 +162,17 @@ export function resolveEffectiveConfiguration(
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
const maximumItems = grant.descriptor.constraints?.maximumItems;
|
|
166
|
+
if (isCollection && maximumItems !== undefined && items.length > maximumItems) {
|
|
167
|
+
// Saved items that no longer fit - the owner lowered the limit after they were added - are left out of
|
|
168
|
+
// the result in order and kept in the saved data, like any other saved value that no longer fits.
|
|
169
|
+
const dropped = items.splice(maximumItems);
|
|
170
|
+
diagnostics.push(errorDiagnostic(
|
|
171
|
+
DiagnosticCode.ContributionTypeMismatch,
|
|
172
|
+
`'${grant.path}' on '${grant.component}' allows at most ${maximumItems} items; ${dropped.map(item => `'${item.id}'`).join(', ')} ${dropped.length === 1 ? 'is' : 'are'} ignored and kept.`,
|
|
173
|
+
{ instance: current.contributedBy ?? owner.instance, component: grant.component, path: grant.path }));
|
|
174
|
+
}
|
|
175
|
+
|
|
165
176
|
if (isCollection) {
|
|
166
177
|
current.items = items;
|
|
167
178
|
current.inheritedValue = baseItems.map(toFlatItem);
|
package/editing/validateValue.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Cratis. All rights reserved.
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
|
-
import { CollectionItemDescriptor, PropertyDescriptor, PropertyValueType } from '@cratis/scene.model';
|
|
4
|
+
import { BindingSourceKind, CollectionItemDescriptor, PropertyDescriptor, PropertyValueType } from '@cratis/scene.model';
|
|
5
5
|
|
|
6
6
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
7
7
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -24,6 +24,13 @@ export function validateValue(descriptor: PropertyDescriptor, value: unknown): s
|
|
|
24
24
|
return constraints.required ? 'a value is required' : undefined;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
const binding = bindingKindOf(value);
|
|
28
|
+
if (binding !== undefined && descriptor.acceptedBindingKinds !== undefined) {
|
|
29
|
+
return descriptor.acceptedBindingKinds.includes(binding.kind)
|
|
30
|
+
? undefined
|
|
31
|
+
: `a ${binding.kind} binding is not accepted here; use ${descriptor.acceptedBindingKinds.join(' or ')}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
switch (descriptor.valueType) {
|
|
28
35
|
case PropertyValueType.String: {
|
|
29
36
|
if (typeof value !== 'string') return 'expected a string';
|
|
@@ -77,6 +84,19 @@ export function validateValue(descriptor: PropertyDescriptor, value: unknown): s
|
|
|
77
84
|
* The deepest nesting a JSON value may have. Authored configuration (chart datasets, option lists) is shallow;
|
|
78
85
|
* the limit turns a runaway or hostile structure into a validation message instead of a stack overflow.
|
|
79
86
|
*/
|
|
87
|
+
const bindingKinds = new Set<string>(Object.values(BindingSourceKind));
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The source kind of a value that is a typed binding expression - an object with a known `kind` and a
|
|
91
|
+
* string `path` - rather than a value of the property's own type. Only consulted for properties that declare
|
|
92
|
+
* `acceptedBindingKinds`, so a collection property such as a command form's `inputs` can be bound to a
|
|
93
|
+
* table's `selectedItem` instead of listing its items.
|
|
94
|
+
*/
|
|
95
|
+
function bindingKindOf(value: unknown): { kind: BindingSourceKind } | undefined {
|
|
96
|
+
if (!isRecord(value) || typeof value.kind !== 'string' || !bindingKinds.has(value.kind) || typeof value.path !== 'string') return undefined;
|
|
97
|
+
return { kind: value.kind as BindingSourceKind };
|
|
98
|
+
}
|
|
99
|
+
|
|
80
100
|
const maximumJsonDepth = 64;
|
|
81
101
|
|
|
82
102
|
/**
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
/** The keys a designer maps to layout moves and resizes. */
|
|
5
|
+
export enum CommandFormLayoutKey {
|
|
6
|
+
ArrowLeft = 'ArrowLeft',
|
|
7
|
+
ArrowRight = 'ArrowRight',
|
|
8
|
+
ArrowUp = 'ArrowUp',
|
|
9
|
+
ArrowDown = 'ArrowDown',
|
|
10
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { CommandFormLayout, FormFieldPlacement } from '@cratis/scene.model';
|
|
5
|
+
import { CommandFormLayoutDraft } from './CommandFormLayoutDraft';
|
|
6
|
+
import { CommandFormLayoutKey } from './CommandFormLayoutKey';
|
|
7
|
+
import { updateCommandFormFieldPlacement } from './updateCommandFormFieldPlacement';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Moves or resizes one field from the keyboard, so the layout can be arranged without a pointer.
|
|
11
|
+
*
|
|
12
|
+
* An arrow key moves the field one cell; with `resize`, Left and Right narrow and widen its column span and
|
|
13
|
+
* Up and Down shorten and lengthen its row span. Positions are one-based and never go below one, and spans never below one.
|
|
14
|
+
* The result is a draft validated like any other placement change, so a move or resize past the last
|
|
15
|
+
* column is reported in its diagnostics and the committed layout is kept. Returns undefined for a field that is not placed.
|
|
16
|
+
*/
|
|
17
|
+
export function applyCommandFormLayoutKey(
|
|
18
|
+
committed: CommandFormLayout,
|
|
19
|
+
field: string,
|
|
20
|
+
key: CommandFormLayoutKey,
|
|
21
|
+
resize = false,
|
|
22
|
+
fields: readonly string[] = [],
|
|
23
|
+
): CommandFormLayoutDraft | undefined {
|
|
24
|
+
const placement = committed.placements.find(candidate => candidate.field === field);
|
|
25
|
+
if (!placement) return undefined;
|
|
26
|
+
return updateCommandFormFieldPlacement(committed, resize ? resized(placement, key) : moved(placement, key), fields);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function moved(placement: FormFieldPlacement, key: CommandFormLayoutKey): FormFieldPlacement {
|
|
30
|
+
switch (key) {
|
|
31
|
+
case CommandFormLayoutKey.ArrowLeft: return { ...placement, column: Math.max(1, placement.column - 1) };
|
|
32
|
+
case CommandFormLayoutKey.ArrowRight: return { ...placement, column: placement.column + 1 };
|
|
33
|
+
case CommandFormLayoutKey.ArrowUp: return { ...placement, row: Math.max(1, placement.row - 1) };
|
|
34
|
+
case CommandFormLayoutKey.ArrowDown: return { ...placement, row: placement.row + 1 };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resized(placement: FormFieldPlacement, key: CommandFormLayoutKey): FormFieldPlacement {
|
|
39
|
+
const columnSpan = placement.columnSpan ?? 1;
|
|
40
|
+
const rowSpan = placement.rowSpan ?? 1;
|
|
41
|
+
switch (key) {
|
|
42
|
+
case CommandFormLayoutKey.ArrowLeft: return { ...placement, columnSpan: Math.max(1, columnSpan - 1) };
|
|
43
|
+
case CommandFormLayoutKey.ArrowRight: return { ...placement, columnSpan: columnSpan + 1 };
|
|
44
|
+
case CommandFormLayoutKey.ArrowUp: return { ...placement, rowSpan: Math.max(1, rowSpan - 1) };
|
|
45
|
+
case CommandFormLayoutKey.ArrowDown: return { ...placement, rowSpan: rowSpan + 1 };
|
|
46
|
+
}
|
|
47
|
+
}
|
package/forms/index.ts
CHANGED
|
@@ -13,3 +13,6 @@ export * from './preserveDirtyCommandValues';
|
|
|
13
13
|
export * from './resizeCommandFormColumn';
|
|
14
14
|
export * from './updateCommandFormFieldPlacement';
|
|
15
15
|
export * from './validateCommandFormLayout';
|
|
16
|
+
export * from './CommandFormLayoutKey';
|
|
17
|
+
export * from './applyCommandFormLayoutKey';
|
|
18
|
+
export * from './stackCommandFormLayout';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { CommandFormLayout, FormWidthUnit } from '@cratis/scene.model';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The layout a command form uses at a compact width: one full-width column, every field on its own row,
|
|
8
|
+
* in reading order - row first, then column, then authored order. Spans and per-field widths are dropped
|
|
9
|
+
* because there is nothing to span. The authored layout is not changed, so the form returns to its columns
|
|
10
|
+
* at a regular width.
|
|
11
|
+
*/
|
|
12
|
+
export function stackCommandFormLayout(layout: CommandFormLayout): CommandFormLayout {
|
|
13
|
+
const ordered = layout.placements
|
|
14
|
+
.map((placement, order) => ({ placement, order }))
|
|
15
|
+
.sort((left, right) => left.placement.row - right.placement.row || left.placement.column - right.placement.column || left.order - right.order);
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
...layout,
|
|
19
|
+
columns: [{ index: 1, width: { unit: FormWidthUnit.Fraction, value: 1 } }],
|
|
20
|
+
placements: ordered.map(({ placement }, index) => ({ field: placement.field, row: index + 1, column: 1 })),
|
|
21
|
+
};
|
|
22
|
+
}
|
package/index.ts
CHANGED
|
@@ -6,6 +6,11 @@ export * from './BindingResolver';
|
|
|
6
6
|
export * from './BindingScope';
|
|
7
7
|
export * from './BindingDiagnostic';
|
|
8
8
|
export * from './resolveBindingExpression';
|
|
9
|
+
export * from './nestBindingScope';
|
|
10
|
+
export * from './resolveArgumentSource';
|
|
11
|
+
export * from './removeComponentOutputs';
|
|
12
|
+
export * from './QueryBindingState';
|
|
13
|
+
export * from './QueryBindingTicket';
|
|
9
14
|
export * from './validateBindingExpression';
|
|
10
15
|
export * from './renderElement';
|
|
11
16
|
export * from './forms';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { BindingScope } from './BindingScope';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The scope a nested template or region sees. A data context it declares replaces the inherited one - a
|
|
8
|
+
* detail region bound to the selected row reads that row, not the screen's - while query results and
|
|
9
|
+
* component outputs are inherited, with its own entries shadowing the parent's of the same name. A null
|
|
10
|
+
* data context inherits, as in the C# engine, which cannot tell null from absent. The parent scope is not
|
|
11
|
+
* changed.
|
|
12
|
+
*/
|
|
13
|
+
export function nestBindingScope(parent: BindingScope, nested: BindingScope): BindingScope {
|
|
14
|
+
return {
|
|
15
|
+
dataContext: nested.dataContext ?? parent.dataContext,
|
|
16
|
+
queryResults: { ...(parent.queryResults ?? {}), ...(nested.queryResults ?? {}) },
|
|
17
|
+
componentOutputs: { ...(parent.componentOutputs ?? {}), ...(nested.componentOutputs ?? {}) },
|
|
18
|
+
};
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cratis/scene.engine",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.16.0",
|
|
4
4
|
"description": "The Scene runtime: walks a Scene element tree and drives whichever renderer is plugged in. Does not know React, or any other rendering platform, exists.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cratis",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
"up": "yarn g:up"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@cratis/scene.model": "4.
|
|
51
|
+
"@cratis/scene.model": "4.16.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@cratis/scene.model": "4.
|
|
54
|
+
"@cratis/scene.model": "4.16.0"
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { BindingScope } from './BindingScope';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The scope after elements leave the tree - unmounted, deleted or renamed to a new id. Their output
|
|
8
|
+
* properties are removed, so a binding to them resolves as absent and validates as `unknownComponent`
|
|
9
|
+
* instead of reading a value from an element that no longer exists.
|
|
10
|
+
*/
|
|
11
|
+
export function removeComponentOutputs(scope: BindingScope, elementIds: string[]): BindingScope {
|
|
12
|
+
const removed = new Set(elementIds);
|
|
13
|
+
const componentOutputs = Object.fromEntries(Object.entries(scope.componentOutputs ?? {}).filter(([id]) => !removed.has(id)));
|
|
14
|
+
return { ...scope, componentOutputs };
|
|
15
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { BindingSourceKind } from '@cratis/scene.model';
|
|
5
|
+
import { BindingScope } from './BindingScope';
|
|
6
|
+
import { resolveBindingExpression } from './resolveBindingExpression';
|
|
7
|
+
|
|
8
|
+
/** The prefix of an argument source that reads another element's output. */
|
|
9
|
+
export const componentArgumentSourcePrefix = 'component.';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolves where a command argument comes from, as an action's argument mapping writes it.
|
|
13
|
+
*
|
|
14
|
+
* `component.<id>.<output>` reads an output another element publishes - an input's `value`, a table's
|
|
15
|
+
* `selectedItem.id` - from the same output scope a `componentProperty` binding reads. Element ids may contain
|
|
16
|
+
* dots, so the longest id with outputs in scope wins: `component.order.lines.value` reads `value` of
|
|
17
|
+
* `order.lines` when that element exists. Any other source is a path in the data context. An empty source,
|
|
18
|
+
* or an element with no outputs in scope, resolves as absent.
|
|
19
|
+
*
|
|
20
|
+
* The C# engine (`ArgumentSources.Resolve`) behaves identically; both run `argument-source-fixtures.json`.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveArgumentSource(source: string, scope: BindingScope): unknown {
|
|
23
|
+
if (!source) return undefined;
|
|
24
|
+
if (!source.startsWith(componentArgumentSourcePrefix)) return resolveBindingExpression({ path: source }, scope);
|
|
25
|
+
|
|
26
|
+
const reference = source.substring(componentArgumentSourcePrefix.length);
|
|
27
|
+
const componentId = Object.keys(scope.componentOutputs ?? {})
|
|
28
|
+
.filter(id => reference === id || reference.startsWith(`${id}.`))
|
|
29
|
+
.sort((left, right) => right.length - left.length)[0];
|
|
30
|
+
if (componentId === undefined) return undefined;
|
|
31
|
+
|
|
32
|
+
return resolveBindingExpression({ kind: BindingSourceKind.ComponentProperty, componentId, path: reference.substring(componentId.length + 1) }, scope);
|
|
33
|
+
}
|
|
@@ -9,10 +9,12 @@ import { BindingScope } from './BindingScope';
|
|
|
9
9
|
*/
|
|
10
10
|
export function resolveBindingExpression(binding: BindingExpression, scope: BindingScope): unknown {
|
|
11
11
|
const kind = binding.kind ?? BindingSourceKind.DataContext;
|
|
12
|
-
if (kind === BindingSourceKind.Literal) return binding.value;
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
// A literal without a value is absent, whether the author wrote `null` or nothing: the C# model cannot
|
|
14
|
+
// tell the two apart, and both engines must resolve the same document to the same value.
|
|
15
|
+
const value = kind === BindingSourceKind.Literal
|
|
16
|
+
? binding.value ?? undefined
|
|
17
|
+
: readPath(sourceValue(binding, scope, kind), sourcePath(binding, kind));
|
|
16
18
|
if (value !== null && value !== undefined) return value;
|
|
17
19
|
|
|
18
20
|
if (binding.nullBehavior === BindingNullBehavior.Preserve) return undefined;
|
|
@@ -30,7 +32,7 @@ export function createBindingResolver(scope: BindingScope): (binding: BindingExp
|
|
|
30
32
|
function sourceValue(binding: BindingExpression, scope: BindingScope, kind: BindingSourceKind): unknown {
|
|
31
33
|
switch (kind) {
|
|
32
34
|
case BindingSourceKind.DataContext:
|
|
33
|
-
return scope.dataContext;
|
|
35
|
+
return scope.dataContext ?? undefined;
|
|
34
36
|
case BindingSourceKind.QueryResult:
|
|
35
37
|
return binding.query ? scope.queryResults?.[binding.query] : undefined;
|
|
36
38
|
case BindingSourceKind.ComponentProperty:
|
|
@@ -45,11 +47,16 @@ function sourcePath(binding: BindingExpression, kind: BindingSourceKind): string
|
|
|
45
47
|
return binding.path;
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Reads a dotted path. Only own properties and array indexes are read - never inherited members such as
|
|
52
|
+
* `constructor` - and anything that is not an object or array ends the walk, so the C# engine can follow
|
|
53
|
+
* exactly the same rule over JSON.
|
|
54
|
+
*/
|
|
48
55
|
function readPath(source: unknown, path: string): unknown {
|
|
49
56
|
if (!path) return source;
|
|
50
57
|
return path.split('.').reduce<unknown>((current, segment) => {
|
|
51
|
-
if (current === null || current === undefined) return undefined;
|
|
52
|
-
if (
|
|
53
|
-
return (current as Record<string, unknown>)[segment];
|
|
58
|
+
if (current === null || current === undefined || typeof current !== 'object') return undefined;
|
|
59
|
+
if (Array.isArray(current)) return /^(0|[1-9][0-9]*)$/.test(segment) ? current[Number(segment)] : undefined;
|
|
60
|
+
return Object.hasOwn(current, segment) ? (current as Record<string, unknown>)[segment] : undefined;
|
|
54
61
|
}, source);
|
|
55
62
|
}
|