@cratis/scene.engine 4.14.0 → 4.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/QueryBindingState.ts +43 -0
  2. package/QueryBindingTicket.ts +8 -0
  3. package/dist/cjs/QueryBindingState.js +38 -0
  4. package/dist/cjs/QueryBindingState.js.map +1 -0
  5. package/dist/cjs/forms/CommandFormLayoutKey.js +14 -0
  6. package/dist/cjs/forms/CommandFormLayoutKey.js.map +1 -0
  7. package/dist/cjs/forms/applyCommandFormLayoutKey.js +72 -0
  8. package/dist/cjs/forms/applyCommandFormLayoutKey.js.map +1 -0
  9. package/dist/cjs/forms/stackCommandFormLayout.js +37 -0
  10. package/dist/cjs/forms/stackCommandFormLayout.js.map +1 -0
  11. package/dist/cjs/index.js +12 -0
  12. package/dist/cjs/index.js.map +1 -1
  13. package/dist/cjs/nestBindingScope.js +26 -0
  14. package/dist/cjs/nestBindingScope.js.map +1 -0
  15. package/dist/cjs/removeComponentOutputs.js +19 -0
  16. package/dist/cjs/removeComponentOutputs.js.map +1 -0
  17. package/dist/cjs/resolveBindingExpression.js +12 -8
  18. package/dist/cjs/resolveBindingExpression.js.map +1 -1
  19. package/dist/esm/QueryBindingState.d.ts +10 -0
  20. package/dist/esm/QueryBindingState.d.ts.map +1 -0
  21. package/dist/esm/QueryBindingState.js +36 -0
  22. package/dist/esm/QueryBindingState.js.map +1 -0
  23. package/dist/esm/QueryBindingTicket.d.ts +5 -0
  24. package/dist/esm/QueryBindingTicket.d.ts.map +1 -0
  25. package/dist/esm/QueryBindingTicket.js +2 -0
  26. package/dist/esm/QueryBindingTicket.js.map +1 -0
  27. package/dist/esm/forms/CommandFormLayoutKey.d.ts +7 -0
  28. package/dist/esm/forms/CommandFormLayoutKey.d.ts.map +1 -0
  29. package/dist/esm/forms/CommandFormLayoutKey.js +12 -0
  30. package/dist/esm/forms/CommandFormLayoutKey.js.map +1 -0
  31. package/dist/esm/forms/applyCommandFormLayoutKey.d.ts +5 -0
  32. package/dist/esm/forms/applyCommandFormLayoutKey.d.ts.map +1 -0
  33. package/dist/esm/forms/applyCommandFormLayoutKey.js +70 -0
  34. package/dist/esm/forms/applyCommandFormLayoutKey.js.map +1 -0
  35. package/dist/esm/forms/index.d.ts +3 -0
  36. package/dist/esm/forms/index.d.ts.map +1 -1
  37. package/dist/esm/forms/index.js +3 -0
  38. package/dist/esm/forms/index.js.map +1 -1
  39. package/dist/esm/forms/stackCommandFormLayout.d.ts +3 -0
  40. package/dist/esm/forms/stackCommandFormLayout.d.ts.map +1 -0
  41. package/dist/esm/forms/stackCommandFormLayout.js +35 -0
  42. package/dist/esm/forms/stackCommandFormLayout.js.map +1 -0
  43. package/dist/esm/index.d.ts +4 -0
  44. package/dist/esm/index.d.ts.map +1 -1
  45. package/dist/esm/index.js +6 -0
  46. package/dist/esm/index.js.map +1 -1
  47. package/dist/esm/nestBindingScope.d.ts +3 -0
  48. package/dist/esm/nestBindingScope.d.ts.map +1 -0
  49. package/dist/esm/nestBindingScope.js +24 -0
  50. package/dist/esm/nestBindingScope.js.map +1 -0
  51. package/dist/esm/removeComponentOutputs.d.ts +3 -0
  52. package/dist/esm/removeComponentOutputs.d.ts.map +1 -0
  53. package/dist/esm/removeComponentOutputs.js +17 -0
  54. package/dist/esm/removeComponentOutputs.js.map +1 -0
  55. package/dist/esm/resolveBindingExpression.d.ts.map +1 -1
  56. package/dist/esm/resolveBindingExpression.js +12 -8
  57. package/dist/esm/resolveBindingExpression.js.map +1 -1
  58. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  59. package/forms/CommandFormLayoutKey.ts +10 -0
  60. package/forms/applyCommandFormLayoutKey.ts +47 -0
  61. package/forms/index.ts +3 -0
  62. package/forms/stackCommandFormLayout.ts +22 -0
  63. package/index.ts +4 -0
  64. package/nestBindingScope.ts +19 -0
  65. package/package.json +3 -3
  66. package/removeComponentOutputs.ts +15 -0
  67. package/resolveBindingExpression.ts +14 -7
@@ -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,10 @@ 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 './removeComponentOutputs';
11
+ export * from './QueryBindingState';
12
+ export * from './QueryBindingTicket';
9
13
  export * from './validateBindingExpression';
10
14
  export * from './renderElement';
11
15
  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.14.0",
3
+ "version": "4.15.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.14.0"
51
+ "@cratis/scene.model": "4.15.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@cratis/scene.model": "4.14.0"
54
+ "@cratis/scene.model": "4.15.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
+ }
@@ -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
- const source = sourceValue(binding, scope, kind);
15
- const value = readPath(source, sourcePath(binding, kind));
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 (typeof current !== 'object') return undefined;
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
  }