@getodk/xforms-engine 0.16.0 → 0.17.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 (124) hide show
  1. package/dist/client/AttributeNode.d.ts +4 -3
  2. package/dist/client/InputNode.d.ts +8 -4
  3. package/dist/client/MarkdownNode.d.ts +3 -0
  4. package/dist/client/NoteNode.d.ts +6 -2
  5. package/dist/client/form/FormInstanceConfig.d.ts +4 -0
  6. package/dist/client/form/LoadFormResult.d.ts +5 -14
  7. package/dist/client/form/ResetFormInstance.d.ts +13 -0
  8. package/dist/entrypoints/FormResult/BaseFormResult.d.ts +1 -0
  9. package/dist/entrypoints/FormResult/BaseInstantiableFormResult.d.ts +2 -0
  10. package/dist/entrypoints/FormResult/FormFailureResult.d.ts +2 -0
  11. package/dist/entrypoints/createPotentiallyClientOwnedReactiveScope.d.ts +19 -0
  12. package/dist/index.js +22150 -25908
  13. package/dist/index.js.map +1 -1
  14. package/dist/instance/Attribute.d.ts +11 -23
  15. package/dist/instance/Group.d.ts +3 -0
  16. package/dist/instance/InputControl.d.ts +3 -0
  17. package/dist/instance/ModelValue.d.ts +4 -0
  18. package/dist/instance/Note.d.ts +4 -0
  19. package/dist/instance/PrimaryInstance.d.ts +7 -1
  20. package/dist/instance/RangeControl.d.ts +4 -0
  21. package/dist/instance/RankControl.d.ts +5 -1
  22. package/dist/instance/Root.d.ts +3 -0
  23. package/dist/instance/SelectControl.d.ts +5 -1
  24. package/dist/instance/TriggerControl.d.ts +4 -0
  25. package/dist/instance/UploadControl.d.ts +3 -0
  26. package/dist/instance/abstract/DescendantNode.d.ts +5 -4
  27. package/dist/instance/abstract/InstanceNode.d.ts +4 -3
  28. package/dist/instance/hierarchy.d.ts +2 -1
  29. package/dist/instance/internal-api/AttributeContext.d.ts +1 -0
  30. package/dist/instance/internal-api/InstanceConfig.d.ts +2 -1
  31. package/dist/instance/internal-api/InstanceValueContext.d.ts +1 -0
  32. package/dist/instance/markdown/MarkdownNode.d.ts +14 -9
  33. package/dist/instance/repeat/RepeatInstance.d.ts +2 -0
  34. package/dist/integration/xpath/adapter/XFormsXPathNode.d.ts +1 -1
  35. package/dist/integration/xpath/adapter/kind.d.ts +5 -3
  36. package/dist/integration/xpath/adapter/traversal.d.ts +3 -3
  37. package/dist/integration/xpath/static-dom/StaticAttribute.d.ts +1 -0
  38. package/dist/integration/xpath/static-dom/StaticDocument.d.ts +2 -0
  39. package/dist/lib/codecs/{Geopoint/Geopoint.d.ts → geolocation/Geolocation.d.ts} +11 -15
  40. package/dist/lib/codecs/geolocation/Geopoint.d.ts +7 -0
  41. package/dist/lib/codecs/geolocation/Geoshape.d.ts +7 -0
  42. package/dist/lib/codecs/geolocation/Geotrace.d.ts +7 -0
  43. package/dist/lib/codecs/geolocation/createGeolocationValueCodec.d.ts +3 -0
  44. package/dist/lib/codecs/getSharedValueCodec.d.ts +7 -5
  45. package/dist/lib/reactivity/text/createTextRange.d.ts +0 -2
  46. package/dist/parse/XFormDOM.d.ts +7 -1
  47. package/dist/parse/body/appearance/inputAppearanceParser.d.ts +1 -1
  48. package/dist/parse/model/ActionDefinition.d.ts +1 -1
  49. package/dist/parse/model/AttributeDefinition.d.ts +2 -0
  50. package/dist/parse/model/BindPreloadDefinition.d.ts +2 -1
  51. package/dist/parse/model/ModelActionMap.d.ts +3 -2
  52. package/dist/parse/model/ModelDefinition.d.ts +3 -5
  53. package/dist/parse/model/SecondaryInstance/sources/CSVExternalSecondaryInstance.d.ts +0 -17
  54. package/dist/parse/model/SecondaryInstance/sources/external-instance-csv-parser.d.ts +8 -0
  55. package/dist/parse/model/TranslationDefinitionMap.d.ts +4 -0
  56. package/dist/solid.js +21608 -25366
  57. package/dist/solid.js.map +1 -1
  58. package/package.json +2 -2
  59. package/src/client/AttributeNode.ts +4 -3
  60. package/src/client/InputNode.ts +11 -3
  61. package/src/client/MarkdownNode.ts +3 -0
  62. package/src/client/NoteNode.ts +9 -1
  63. package/src/client/form/FormInstanceConfig.ts +6 -0
  64. package/src/client/form/LoadFormResult.ts +5 -17
  65. package/src/client/form/ResetFormInstance.ts +17 -0
  66. package/src/entrypoints/FormInstance.ts +2 -0
  67. package/src/entrypoints/FormResult/BaseFormResult.ts +1 -0
  68. package/src/entrypoints/FormResult/BaseInstantiableFormResult.ts +10 -1
  69. package/src/entrypoints/FormResult/FormFailureResult.ts +3 -0
  70. package/src/entrypoints/createPotentiallyClientOwnedReactiveScope.ts +30 -0
  71. package/src/entrypoints/loadForm.ts +1 -31
  72. package/src/instance/Attribute.ts +38 -54
  73. package/src/instance/Group.ts +12 -4
  74. package/src/instance/InputControl.ts +15 -9
  75. package/src/instance/ModelValue.ts +13 -4
  76. package/src/instance/Note.ts +13 -4
  77. package/src/instance/PrimaryInstance.ts +29 -6
  78. package/src/instance/RangeControl.ts +13 -4
  79. package/src/instance/RankControl.ts +14 -5
  80. package/src/instance/Root.ts +12 -4
  81. package/src/instance/SelectControl.ts +14 -5
  82. package/src/instance/TriggerControl.ts +13 -4
  83. package/src/instance/UploadControl.ts +13 -3
  84. package/src/instance/abstract/DescendantNode.ts +4 -3
  85. package/src/instance/abstract/InstanceNode.ts +5 -3
  86. package/src/instance/attachments/buildAttributes.ts +26 -2
  87. package/src/instance/children/childrenInitOptions.ts +2 -1
  88. package/src/instance/hierarchy.ts +2 -0
  89. package/src/instance/internal-api/AttributeContext.ts +1 -0
  90. package/src/instance/internal-api/InstanceConfig.ts +3 -0
  91. package/src/instance/internal-api/InstanceValueContext.ts +1 -0
  92. package/src/instance/markdown/MarkdownNode.ts +19 -7
  93. package/src/instance/repeat/RepeatInstance.ts +11 -3
  94. package/src/instance/text/markdownFormat.ts +4 -3
  95. package/src/integration/xpath/adapter/XFormsXPathNode.ts +1 -0
  96. package/src/integration/xpath/adapter/engineDOMAdapter.ts +2 -2
  97. package/src/integration/xpath/adapter/kind.ts +6 -1
  98. package/src/integration/xpath/adapter/names.ts +1 -0
  99. package/src/integration/xpath/adapter/traversal.ts +5 -6
  100. package/src/integration/xpath/static-dom/StaticAttribute.ts +1 -0
  101. package/src/integration/xpath/static-dom/StaticDocument.ts +2 -0
  102. package/src/lib/codecs/{Geopoint/Geopoint.ts → geolocation/Geolocation.ts} +43 -24
  103. package/src/lib/codecs/geolocation/Geopoint.ts +15 -0
  104. package/src/lib/codecs/geolocation/Geoshape.ts +36 -0
  105. package/src/lib/codecs/geolocation/Geotrace.ts +36 -0
  106. package/src/lib/codecs/geolocation/createGeolocationValueCodec.ts +18 -0
  107. package/src/lib/codecs/getSharedValueCodec.ts +37 -11
  108. package/src/lib/reactivity/createInstanceValueState.ts +90 -34
  109. package/src/lib/reactivity/text/createTextRange.ts +71 -45
  110. package/src/parse/XFormDOM.ts +22 -2
  111. package/src/parse/model/ActionDefinition.ts +6 -6
  112. package/src/parse/model/AttributeDefinition.ts +7 -0
  113. package/src/parse/model/BindDefinition.ts +1 -1
  114. package/src/parse/model/BindPreloadDefinition.ts +21 -14
  115. package/src/parse/model/ModelActionMap.ts +30 -13
  116. package/src/parse/model/ModelDefinition.ts +5 -10
  117. package/src/parse/model/RootDefinition.ts +2 -1
  118. package/src/parse/model/SecondaryInstance/sources/CSVExternalSecondaryInstance.ts +2 -184
  119. package/src/parse/model/SecondaryInstance/sources/external-instance-csv-parser.ts +185 -0
  120. package/src/parse/model/TranslationDefinitionMap.ts +23 -0
  121. package/dist/lib/codecs/Geopoint/GeopointValueCodec.d.ts +0 -5
  122. package/dist/parse/model/generateItextChunks.d.ts +0 -5
  123. package/src/lib/codecs/Geopoint/GeopointValueCodec.ts +0 -20
  124. package/src/parse/model/generateItextChunks.ts +0 -61
@@ -0,0 +1,15 @@
1
+ import { Geolocation, type LocationPoint } from './Geolocation.ts';
2
+
3
+ export type GeopointRuntimeValue = LocationPoint | null;
4
+
5
+ export type GeopointInputValue = GeopointRuntimeValue | string;
6
+
7
+ export class Geopoint extends Geolocation {
8
+ static parseStringToGeopoint(value: string): GeopointRuntimeValue {
9
+ return Geolocation.parseString(value);
10
+ }
11
+
12
+ static parseGeopointToString(value: GeopointInputValue): string {
13
+ return Geolocation.toCoordinatesString(value);
14
+ }
15
+ }
@@ -0,0 +1,36 @@
1
+ import { Geolocation, type LocationPoint, SEGMENT_SEPARATOR } from './Geolocation.ts';
2
+
3
+ export type GeoshapeRuntimeValue = LocationPoint[] | null;
4
+
5
+ export type GeoshapeInputValue = GeoshapeRuntimeValue | string;
6
+
7
+ export class Geoshape extends Geolocation {
8
+ static parseStringToGeoshape(value: string): GeoshapeRuntimeValue {
9
+ const parts = Geolocation.getSegments(value);
10
+ if (parts === null) {
11
+ return null;
12
+ }
13
+
14
+ const points = parts.map((point) => Geolocation.parseString(point)) as LocationPoint[];
15
+ if (points.some((p) => p === null) || points.length < 3 || !Geolocation.isClosedShape(points)) {
16
+ return null;
17
+ }
18
+
19
+ return points;
20
+ }
21
+
22
+ static parseGeoshapeString(points: GeoshapeInputValue): string {
23
+ const decodedPoints =
24
+ typeof points === 'string' ? Geoshape.parseStringToGeoshape(points) : points;
25
+ if (!decodedPoints) {
26
+ return '';
27
+ }
28
+
29
+ const segments = decodedPoints.map((point) => Geolocation.toCoordinatesString(point));
30
+ if (segments.some((s) => !s.length)) {
31
+ return '';
32
+ }
33
+
34
+ return segments.join(SEGMENT_SEPARATOR);
35
+ }
36
+ }
@@ -0,0 +1,36 @@
1
+ import { Geolocation, type LocationPoint, SEGMENT_SEPARATOR } from './Geolocation.ts';
2
+
3
+ export type GeotraceRuntimeValue = LocationPoint[] | null;
4
+
5
+ export type GeotraceInputValue = GeotraceRuntimeValue | string;
6
+
7
+ export class Geotrace extends Geolocation {
8
+ static parseStringToGeotrace(value: string): GeotraceRuntimeValue {
9
+ const parts = Geolocation.getSegments(value);
10
+ if (parts === null) {
11
+ return null;
12
+ }
13
+
14
+ const points = parts.map((point) => Geolocation.parseString(point)) as LocationPoint[];
15
+ if (points.some((p) => p === null) || points.length < 2 || Geolocation.isClosedShape(points)) {
16
+ return null;
17
+ }
18
+
19
+ return points;
20
+ }
21
+
22
+ static parseGeotraceString(points: GeotraceInputValue): string {
23
+ const decodedPoints =
24
+ typeof points === 'string' ? Geotrace.parseStringToGeotrace(points) : points;
25
+ if (!decodedPoints) {
26
+ return '';
27
+ }
28
+
29
+ const segments = decodedPoints.map((point) => Geolocation.toCoordinatesString(point));
30
+ if (segments.some((s) => !s.length)) {
31
+ return '';
32
+ }
33
+
34
+ return segments.join(SEGMENT_SEPARATOR);
35
+ }
36
+ }
@@ -0,0 +1,18 @@
1
+ import type { ValueType } from '../../../client';
2
+ import { type CodecDecoder, type CodecEncoder, ValueCodec } from '../ValueCodec.ts';
3
+
4
+ export function createGeolocationValueCodec<
5
+ V extends ValueType,
6
+ RuntimeValue extends RuntimeInputValue,
7
+ RuntimeInputValue = RuntimeValue,
8
+ >(
9
+ valueType: V,
10
+ encodeValue: CodecEncoder<RuntimeInputValue>,
11
+ decodeValue: CodecDecoder<RuntimeValue>
12
+ ): ValueCodec<V, RuntimeValue, RuntimeInputValue> {
13
+ return new (class extends ValueCodec<V, RuntimeValue, RuntimeInputValue> {})(
14
+ valueType,
15
+ encodeValue,
16
+ decodeValue
17
+ );
18
+ }
@@ -2,13 +2,27 @@ import type { ValueType } from '../../client/ValueType.ts';
2
2
  import type { DatetimeInputValue, DatetimeRuntimeValue } from './DateValueCodec.ts';
3
3
  import { DateValueCodec } from './DateValueCodec.ts';
4
4
  import {
5
- DecimalValueCodec,
6
5
  type DecimalInputValue,
7
6
  type DecimalRuntimeValue,
7
+ DecimalValueCodec,
8
8
  } from './DecimalValueCodec.ts';
9
- import type { GeopointInputValue, GeopointRuntimeValue } from './Geopoint/Geopoint.ts';
10
- import { GeopointValueCodec } from './Geopoint/GeopointValueCodec.ts';
11
- import { IntValueCodec, type IntInputValue, type IntRuntimeValue } from './IntValueCodec.ts';
9
+ import { createGeolocationValueCodec } from './geolocation/createGeolocationValueCodec.ts';
10
+ import {
11
+ Geopoint,
12
+ type GeopointInputValue,
13
+ type GeopointRuntimeValue,
14
+ } from './geolocation/Geopoint.ts';
15
+ import {
16
+ Geoshape,
17
+ type GeoshapeInputValue,
18
+ type GeoshapeRuntimeValue,
19
+ } from './geolocation/Geoshape.ts';
20
+ import {
21
+ Geotrace,
22
+ type GeotraceInputValue,
23
+ type GeotraceRuntimeValue,
24
+ } from './geolocation/Geotrace.ts';
25
+ import { type IntInputValue, type IntRuntimeValue, IntValueCodec } from './IntValueCodec.ts';
12
26
  import { StringValueCodec } from './StringValueCodec.ts';
13
27
  import type { ValueCodec } from './ValueCodec.ts';
14
28
  import { ValueTypePlaceholderCodec } from './ValueTypePlaceholderCodec.ts';
@@ -22,8 +36,8 @@ interface RuntimeValuesByType {
22
36
  readonly time: string;
23
37
  readonly dateTime: string;
24
38
  readonly geopoint: GeopointRuntimeValue;
25
- readonly geotrace: string;
26
- readonly geoshape: string;
39
+ readonly geotrace: GeotraceRuntimeValue;
40
+ readonly geoshape: GeoshapeRuntimeValue;
27
41
  readonly binary: string;
28
42
  readonly barcode: string;
29
43
  readonly intent: string;
@@ -40,8 +54,8 @@ interface RuntimeInputValuesByType {
40
54
  readonly time: string;
41
55
  readonly dateTime: string;
42
56
  readonly geopoint: GeopointInputValue;
43
- readonly geotrace: string;
44
- readonly geoshape: string;
57
+ readonly geotrace: GeotraceInputValue;
58
+ readonly geoshape: GeoshapeInputValue;
45
59
  readonly binary: string;
46
60
  readonly barcode: string;
47
61
  readonly intent: string;
@@ -68,12 +82,24 @@ export const sharedValueCodecs: SharedValueCodecs = {
68
82
  date: new DateValueCodec(),
69
83
  time: new ValueTypePlaceholderCodec('time'),
70
84
  dateTime: new ValueTypePlaceholderCodec('dateTime'),
71
- geopoint: new GeopointValueCodec(),
72
- geotrace: new ValueTypePlaceholderCodec('geotrace'),
73
- geoshape: new ValueTypePlaceholderCodec('geoshape'),
74
85
  binary: new ValueTypePlaceholderCodec('binary'),
75
86
  barcode: new ValueTypePlaceholderCodec('barcode'),
76
87
  intent: new ValueTypePlaceholderCodec('intent'),
88
+ geopoint: createGeolocationValueCodec<'geopoint', GeopointRuntimeValue, GeopointInputValue>(
89
+ 'geopoint',
90
+ (value) => Geopoint.parseGeopointToString(value),
91
+ (value) => Geopoint.parseStringToGeopoint(value)
92
+ ),
93
+ geotrace: createGeolocationValueCodec<'geotrace', GeotraceRuntimeValue, GeotraceInputValue>(
94
+ 'geotrace',
95
+ (value) => Geotrace.parseGeotraceString(value),
96
+ (value) => Geotrace.parseStringToGeotrace(value)
97
+ ),
98
+ geoshape: createGeolocationValueCodec<'geoshape', GeoshapeRuntimeValue, GeoshapeInputValue>(
99
+ 'geoshape',
100
+ (value) => Geoshape.parseGeoshapeString(value),
101
+ (value) => Geoshape.parseStringToGeoshape(value)
102
+ ),
77
103
  };
78
104
 
79
105
  export const getSharedValueCodec = <V extends ValueType>(valueType: V): SharedValueCodec<V> => {
@@ -7,6 +7,8 @@ import type { BindComputationExpression } from '../../parse/expression/BindCompu
7
7
  import { ActionDefinition } from '../../parse/model/ActionDefinition.ts';
8
8
  import type { AnyBindPreloadDefinition } from '../../parse/model/BindPreloadDefinition.ts';
9
9
  import { XFORM_EVENT } from '../../parse/model/Event.ts';
10
+ import { SET_GEOPOINT_LOCAL_NAME } from '../../parse/XFormDOM.ts';
11
+ import { sharedValueCodecs } from '../codecs/getSharedValueCodec.ts';
10
12
  import { createComputedExpression } from './createComputedExpression.ts';
11
13
  import type { SimpleAtomicState, SimpleAtomicStateSetter } from './types.ts';
12
14
 
@@ -15,7 +17,7 @@ const REPEAT_INDEX_REGEX = /([^[]*)(\[[0-9]+\])/g;
15
17
  type ValueContext = AttributeContext | InstanceValueContext;
16
18
 
17
19
  const isInstanceFirstLoad = (context: ValueContext) => {
18
- return context.rootDocument.initializationMode === 'create';
20
+ return context.rootDocument.initializationMode === 'create' && !isAddingRepeatChild(context);
19
21
  };
20
22
 
21
23
  const isAddingRepeatChild = (context: ValueContext) => {
@@ -26,7 +28,7 @@ const isAddingRepeatChild = (context: ValueContext) => {
26
28
  * Special case, does not correspond to any event.
27
29
  */
28
30
  const isEditInitialLoad = (context: ValueContext) => {
29
- return context.rootDocument.initializationMode === 'edit';
31
+ return context.rootDocument.initializationMode === 'edit' && !isAddingRepeatChild(context);
30
32
  };
31
33
 
32
34
  const getInitialValue = (context: ValueContext): string => {
@@ -94,16 +96,6 @@ const guardDownstreamReadonlyWrites = (
94
96
  return [getValue, setValue];
95
97
  };
96
98
 
97
- /**
98
- * @todo It feels increasingly awkward to keep piling up preload stuff here, but it won't stay that way for long. In the meantime, this seems like the best way to express the cases where `preload="uid"` should be effective, i.e.:
99
- *
100
- * - When an instance is first loaded ({@link isInstanceFirstLoad})
101
- * - When an instance is initially loaded for editing ({@link isEditInitialLoad})
102
- */
103
- const isLoading = (context: ValueContext) => {
104
- return isInstanceFirstLoad(context) || isEditInitialLoad(context);
105
- };
106
-
107
99
  const setValueIfPreloadDefined = (
108
100
  context: ValueContext,
109
101
  setValue: SimpleAtomicStateSetter<string>,
@@ -134,11 +126,14 @@ const preloadValue = (context: ValueContext, setValue: SimpleAtomicStateSetter<s
134
126
 
135
127
  if (preload.event === XFORM_EVENT.xformsRevalidate) {
136
128
  postloadValue(context, setValue, preload);
137
- return;
138
- }
139
-
140
- if (isLoading(context)) {
141
- setValueIfPreloadDefined(context, setValue, preload);
129
+ } else if (preload.event === XFORM_EVENT.odkInstanceFirstLoad) {
130
+ if (isInstanceFirstLoad(context)) {
131
+ setValueIfPreloadDefined(context, setValue, preload);
132
+ }
133
+ } else if (preload.event === XFORM_EVENT.odkInstanceLoad) {
134
+ if (isInstanceFirstLoad(context) || isEditInitialLoad(context)) {
135
+ setValueIfPreloadDefined(context, setValue, preload);
136
+ }
142
137
  }
143
138
  };
144
139
 
@@ -199,6 +194,42 @@ const createCalculation = (
199
194
  });
200
195
  };
201
196
 
197
+ /**
198
+ * Runs the computation without maintaining a reactive listener, so
199
+ * actions that should run only at a specific time are not triggered
200
+ * when referenced elements are updated.
201
+ */
202
+ const createActionCalculation = (
203
+ context: ValueContext,
204
+ setRelevantValue: SimpleAtomicStateSetter<string>,
205
+ computation: ActionComputationExpression<'string'>
206
+ ): void => {
207
+ createComputed(() => {
208
+ if (context.isAttached()) {
209
+ // use untrack so the expression evaluation isn't reactive
210
+ const relevant = untrack(() => context.isRelevant());
211
+ if (!relevant) {
212
+ return;
213
+ }
214
+ const calculated = untrack(() => {
215
+ return context.evaluator.evaluateString(computation.expression, context);
216
+ });
217
+ const value = context.decodeInstanceValue(calculated);
218
+ setRelevantValue(value);
219
+ }
220
+ });
221
+ };
222
+
223
+ const resolveAndSetValueChanged = (
224
+ context: ValueContext,
225
+ setRelevantValue: SimpleAtomicStateSetter<string>,
226
+ expression: string
227
+ ): void => {
228
+ const calc = context.evaluator.evaluateString(expression, context);
229
+ const value = context.decodeInstanceValue(calc);
230
+ setRelevantValue(value);
231
+ };
232
+
202
233
  const createValueChangedCalculation = (
203
234
  context: ValueContext,
204
235
  setRelevantValue: SimpleAtomicStateSetter<string>,
@@ -206,21 +237,27 @@ const createValueChangedCalculation = (
206
237
  ): void => {
207
238
  const { source, ref } = bindToRepeatInstance(context, action);
208
239
  if (!source) {
209
- // no element to listen to
240
+ // No element to listen to
210
241
  return;
211
242
  }
212
- let previous = '';
243
+ let previous: string;
213
244
  const sourceElementExpression = new ActionComputationExpression('string', source);
214
- const calculateValueSource = createComputedExpression(context, sourceElementExpression); // registers listener
245
+ const calculateValueSource = createComputedExpression(context, sourceElementExpression); // Registers listener
215
246
  createComputed(() => {
216
247
  if (context.isAttached() && context.isRelevant()) {
217
248
  const valueSource = calculateValueSource();
218
- if (previous !== valueSource) {
219
- // only update if value has changed
220
- if (referencesCurrentNode(context, ref)) {
221
- const calc = context.evaluator.evaluateString(action.computation.expression, context);
222
- const value = context.decodeInstanceValue(calc);
223
- setRelevantValue(value);
249
+ if (
250
+ previous !== undefined &&
251
+ previous !== valueSource &&
252
+ referencesCurrentNode(context, ref)
253
+ ) {
254
+ // Only update if value has changed
255
+ if (action.element.nodeName === SET_GEOPOINT_LOCAL_NAME) {
256
+ getGeopointValue(context, (point) => {
257
+ setRelevantValue(point);
258
+ });
259
+ } else {
260
+ resolveAndSetValueChanged(context, setRelevantValue, action.computation.expression);
224
261
  }
225
262
  }
226
263
  previous = valueSource;
@@ -228,24 +265,45 @@ const createValueChangedCalculation = (
228
265
  });
229
266
  };
230
267
 
231
- const registerAction = (
268
+ const getGeopointValue = (context: ValueContext, callback: (value: string) => void) => {
269
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we don't want to block
270
+ context.rootDocument.getBackgroundGeopoint()?.then((point) => {
271
+ // Allow the codec to manage all geolocation validation.
272
+ // It decodes and encodes the value, and setValue expects a string.
273
+ callback(sharedValueCodecs.geopoint.encodeValue(point));
274
+ });
275
+ };
276
+
277
+ const performActionComputation = (
278
+ context: ValueContext,
279
+ setValue: SimpleAtomicStateSetter<string>,
280
+ action: ActionDefinition
281
+ ) => {
282
+ if (action.element.nodeName === SET_GEOPOINT_LOCAL_NAME) {
283
+ getGeopointValue(context, (point) => setValue(point));
284
+ return;
285
+ }
286
+ createActionCalculation(context, setValue, action.computation);
287
+ };
288
+
289
+ const dispatchAction = (
232
290
  context: ValueContext,
233
291
  setValue: SimpleAtomicStateSetter<string>,
234
292
  action: ActionDefinition
235
293
  ) => {
236
294
  if (action.events.includes(XFORM_EVENT.odkInstanceFirstLoad)) {
237
295
  if (isInstanceFirstLoad(context)) {
238
- createCalculation(context, setValue, action.computation);
296
+ performActionComputation(context, setValue, action);
239
297
  }
240
298
  }
241
299
  if (action.events.includes(XFORM_EVENT.odkInstanceLoad)) {
242
300
  if (!isAddingRepeatChild(context)) {
243
- createCalculation(context, setValue, action.computation);
301
+ performActionComputation(context, setValue, action);
244
302
  }
245
303
  }
246
304
  if (action.events.includes(XFORM_EVENT.odkNewRepeat)) {
247
305
  if (isAddingRepeatChild(context)) {
248
- createCalculation(context, setValue, action.computation);
306
+ performActionComputation(context, setValue, action);
249
307
  }
250
308
  }
251
309
  if (action.events.includes(XFORM_EVENT.xformsValueChanged)) {
@@ -281,10 +339,8 @@ export const createInstanceValueState = (context: ValueContext): InstanceValueSt
281
339
  createCalculation(context, setValue, calculate);
282
340
  }
283
341
 
284
- const action = context.definition.model.actions.get(context.contextReference());
285
- if (action) {
286
- registerAction(context, setValue, action);
287
- }
342
+ const actions = context.definition.model.actions.get(context.contextReference());
343
+ actions?.forEach((action) => dispatchAction(context, setValue, action));
288
344
 
289
345
  return guardDownstreamReadonlyWrites(context, relevantValueState);
290
346
  });
@@ -1,14 +1,17 @@
1
1
  import {
2
+ isResourceType,
2
3
  JRResourceURL,
3
4
  type JRResourceURLString,
5
+ type ResourceType,
4
6
  } from '@getodk/common/jr-resources/JRResourceURL.ts';
7
+ import { isElementNode, isTextNode } from '@getodk/common/lib/dom/predicates.ts';
5
8
  import type { Accessor } from 'solid-js';
6
9
  import { createMemo } from 'solid-js';
7
10
  import type { TextRole } from '../../../client/TextRange.ts';
8
11
  import type { EvaluationContext } from '../../../instance/internal-api/EvaluationContext.ts';
9
12
  import { TextChunk } from '../../../instance/text/TextChunk.ts';
10
13
  import { TextRange, type MediaSources } from '../../../instance/text/TextRange.ts';
11
- import { type TextChunkExpression } from '../../../parse/expression/TextChunkExpression.ts';
14
+ import { TextChunkExpression } from '../../../parse/expression/TextChunkExpression.ts';
12
15
  import type { TextRangeDefinition } from '../../../parse/text/abstract/TextRangeDefinition.ts';
13
16
  import { createComputedExpression } from '../createComputedExpression.ts';
14
17
 
@@ -17,57 +20,84 @@ interface ChunksAndMedia {
17
20
  mediaSources: MediaSources;
18
21
  }
19
22
 
23
+ const generateChunk = (node: Node): TextChunkExpression<'string'> | null => {
24
+ if (isElementNode(node)) {
25
+ return TextChunkExpression.fromOutput(node);
26
+ }
27
+ if (isTextNode(node)) {
28
+ const formAttribute = node.parentElement!.getAttribute('form') as ResourceType;
29
+ if (isResourceType(formAttribute)) {
30
+ return TextChunkExpression.fromResource(node.data as JRResourceURLString, formAttribute);
31
+ }
32
+ return TextChunkExpression.fromLiteral(node.data);
33
+ }
34
+ return null;
35
+ };
36
+
37
+ const generateChunksForTranslation = (
38
+ textElement: Element
39
+ ): Array<TextChunkExpression<'string'>> => {
40
+ const chunks = [];
41
+ for (const child of textElement.childNodes) {
42
+ for (const grandchild of child.childNodes) {
43
+ const chunk = generateChunk(grandchild);
44
+ if (chunk) {
45
+ chunks.push(chunk);
46
+ }
47
+ }
48
+ }
49
+ return chunks;
50
+ };
51
+
52
+ const getChunkExpressions = <Role extends TextRole>(
53
+ context: EvaluationContext,
54
+ definition: TextRangeDefinition<Role>
55
+ ): ReadonlyArray<TextChunkExpression<'string'>> => {
56
+ if (definition.chunks[0]?.source !== 'translation') {
57
+ // only translations have 'nodes' chunks
58
+ return definition.chunks as Array<TextChunkExpression<'string'>>;
59
+ }
60
+ const itextId = context.evaluator.evaluateString(definition.chunks[0].toString()!, {
61
+ contextNode: context.contextNode,
62
+ });
63
+ const lang = context.getActiveLanguage();
64
+ const elem = definition.form.model.getItextElement(lang, itextId);
65
+ return elem ? generateChunksForTranslation(elem) : [];
66
+ };
67
+
20
68
  /**
21
- * Creates a reactive accessor for text chunks and an optional image from text source expressions.
69
+ * Creates a reactive accessor for text chunks and an optional image, audio and video from text source expressions.
22
70
  * - Combines chunks from literal and computed sources into a single array.
23
- * - Captures the first image found with a 'from="image"' attribute.
71
+ * - Captures the first image found with a 'from=<"image"|"audio"|"video">' attribute.
24
72
  *
25
- * @param context - The evaluation context for reactive XPath computations.
26
- * @param chunkExpressions - Array of text source expressions to process.
27
- * @returns An accessor for an object with all chunks and the first image (if any).
73
+ * @param context The evaluation context for reactive XPath computations.
74
+ * @param definition The definition for the text range which contains chunks to transform
75
+ * @returns An accessor for an object with all chunks and the first image, audio and video (if any).
28
76
  */
29
77
  const createTextChunks = <Role extends TextRole>(
30
78
  context: EvaluationContext,
31
79
  definition: TextRangeDefinition<Role>
32
- ): Accessor<ChunksAndMedia> => {
33
- return createMemo(() => {
34
- const chunks: TextChunk[] = [];
35
- const mediaSources: MediaSources = {};
36
-
37
- let chunkExpressions: ReadonlyArray<TextChunkExpression<'string'>>;
38
-
39
- if (definition.chunks[0]?.source === 'translation') {
40
- const itextId = context.evaluator.evaluateString(definition.chunks[0].toString()!, {
41
- contextNode: context.contextNode,
42
- });
43
- chunkExpressions = definition.form.model.getTranslationChunks(
44
- itextId,
45
- context.getActiveLanguage()
80
+ ): ChunksAndMedia => {
81
+ const chunks: TextChunk[] = [];
82
+ const mediaSources: MediaSources = {};
83
+ const chunkExpressions = getChunkExpressions(context, definition);
84
+ chunkExpressions.forEach((chunkExpression) => {
85
+ if (chunkExpression.resourceType) {
86
+ mediaSources[chunkExpression.resourceType] = JRResourceURL.from(
87
+ chunkExpression.stringValue as JRResourceURLString
46
88
  );
47
- } else {
48
- // only translations have 'nodes' chunks
49
- chunkExpressions = definition.chunks as Array<TextChunkExpression<'string'>>;
89
+ return;
50
90
  }
51
91
 
52
- chunkExpressions.forEach((chunkExpression) => {
53
- if (chunkExpression.resourceType) {
54
- mediaSources[chunkExpression.resourceType] = JRResourceURL.from(
55
- chunkExpression.stringValue as JRResourceURLString
56
- );
57
- return;
58
- }
59
-
60
- if (chunkExpression.source === 'literal') {
61
- chunks.push(new TextChunk(context, chunkExpression.source, chunkExpression.stringValue));
62
- return;
63
- }
64
-
65
- const computed = createComputedExpression(context, chunkExpression)();
66
- chunks.push(new TextChunk(context, chunkExpression.source, computed));
67
- });
92
+ if (chunkExpression.source === 'literal') {
93
+ chunks.push(new TextChunk(context, chunkExpression.source, chunkExpression.stringValue));
94
+ return;
95
+ }
68
96
 
69
- return { chunks, mediaSources };
97
+ const computed = createComputedExpression(context, chunkExpression)();
98
+ chunks.push(new TextChunk(context, chunkExpression.source, computed));
70
99
  });
100
+ return { chunks, mediaSources };
71
101
  };
72
102
 
73
103
  type ComputedFormTextRange<Role extends TextRole> = Accessor<TextRange<Role, 'form'>>;
@@ -77,8 +107,6 @@ type ComputedFormTextRange<Role extends TextRole> = Accessor<TextRange<Role, 'fo
77
107
  *
78
108
  * - The form's current language (e.g. `<label ref="jr:itext('text-id')" />`)
79
109
  * - Direct `<output>` references within the label's children
80
- *
81
- * @todo This does not yet handle itext translations **with** outputs!
82
110
  */
83
111
  export const createTextRange = <Role extends TextRole>(
84
112
  context: EvaluationContext,
@@ -86,10 +114,8 @@ export const createTextRange = <Role extends TextRole>(
86
114
  definition: TextRangeDefinition<Role>
87
115
  ): ComputedFormTextRange<Role> => {
88
116
  return context.scope.runTask(() => {
89
- const textChunks = createTextChunks(context, definition);
90
-
91
117
  return createMemo(() => {
92
- const chunks = textChunks();
118
+ const chunks = createTextChunks(context, definition);
93
119
  return new TextRange('form', role, chunks.chunks, chunks.mediaSources);
94
120
  });
95
121
  });
@@ -10,8 +10,13 @@ import type {
10
10
  } from '@getodk/common/types/dom.ts';
11
11
  import { DefaultEvaluator } from '@getodk/xpath';
12
12
 
13
+ export const SET_VALUE_LOCAL_NAME = 'setvalue';
14
+ export const SET_GEOPOINT_LOCAL_NAME = 'odk:setgeopoint';
15
+
13
16
  interface DOMBindElement extends KnownAttributeLocalNamedElement<'bind', 'nodeset'> {}
14
- interface DOMSetValueElement extends KnownAttributeLocalNamedElement<'setvalue', 'event'> {}
17
+ export interface DOMSetValueElement extends KnownAttributeLocalNamedElement<'setvalue', 'event'> {}
18
+ export interface DOMSetGeopointElement
19
+ extends KnownAttributeLocalNamedElement<'odk:setgeopoint', 'event'> {}
15
20
 
16
21
  const getMetaElement = (primaryInstanceRoot: Element): Element | null => {
17
22
  for (const child of primaryInstanceRoot.children) {
@@ -325,6 +330,15 @@ export class XFormDOM {
325
330
  return new this(sourceXML, { isNormalized: false });
326
331
  }
327
332
 
333
+ isInstanceID = (nodeset: string) => {
334
+ const meta = getMetaElement(this.primaryInstanceRoot);
335
+ const instanceId = meta && getMetaChildElement(meta, 'instanceID');
336
+ return (
337
+ instanceId &&
338
+ nodeset === `/${this.primaryInstanceRoot.nodeName}/${meta.nodeName}/${instanceId.nodeName}`
339
+ );
340
+ };
341
+
328
342
  protected readonly normalizedXML: string;
329
343
 
330
344
  // Commonly accessed landmark nodes
@@ -338,6 +352,7 @@ export class XFormDOM {
338
352
  readonly model: Element;
339
353
  readonly binds: readonly DOMBindElement[];
340
354
  readonly setValues: readonly DOMSetValueElement[];
355
+ readonly setGeopoints: readonly DOMSetGeopointElement[];
341
356
  readonly primaryInstance: Element;
342
357
  readonly primaryInstanceRoot: Element;
343
358
 
@@ -371,11 +386,15 @@ export class XFormDOM {
371
386
  }
372
387
  );
373
388
  const setValues: readonly DOMSetValueElement[] = evaluator.evaluateNodes<DOMSetValueElement>(
374
- './xf:setvalue[@event]',
389
+ `./xf:${SET_VALUE_LOCAL_NAME}[@event]`,
375
390
  {
376
391
  contextNode: model,
377
392
  }
378
393
  );
394
+ const setGeopoints: readonly DOMSetGeopointElement[] =
395
+ evaluator.evaluateNodes<DOMSetGeopointElement>(`./${SET_GEOPOINT_LOCAL_NAME}[@event]`, {
396
+ contextNode: model,
397
+ });
379
398
 
380
399
  const instances = evaluator.evaluateNodes<DOMInstanceElement>('./xf:instance', {
381
400
  contextNode: model,
@@ -426,6 +445,7 @@ export class XFormDOM {
426
445
  this.model = model;
427
446
  this.binds = binds;
428
447
  this.setValues = setValues;
448
+ this.setGeopoints = setGeopoints;
429
449
  this.primaryInstance = primaryInstance;
430
450
  this.primaryInstanceRoot = primaryInstanceRoot;
431
451
  this.itextTranslationElements = itextTranslationElements;
@@ -4,12 +4,12 @@ import { type XFormEvent, XFORM_EVENT } from './Event.ts';
4
4
  import type { ModelDefinition } from './ModelDefinition.ts';
5
5
 
6
6
  export class ActionDefinition {
7
- static getRef(model: ModelDefinition, setValueElement: Element): string | null {
8
- if (setValueElement.hasAttribute('ref')) {
9
- return setValueElement.getAttribute('ref') ?? null;
7
+ static getRef(model: ModelDefinition, element: Element): string | null {
8
+ if (element.hasAttribute('ref')) {
9
+ return element.getAttribute('ref') ?? null;
10
10
  }
11
- if (setValueElement.hasAttribute('bind')) {
12
- const bindId = setValueElement.getAttribute('bind');
11
+ if (element.hasAttribute('bind')) {
12
+ const bindId = element.getAttribute('bind');
13
13
  const bindDefinition = Array.from(model.binds.values()).find((definition) => {
14
14
  return definition.bindElement.getAttribute('id') === bindId;
15
15
  });
@@ -58,7 +58,7 @@ export class ActionDefinition {
58
58
  const ref = ActionDefinition.getRef(model, element);
59
59
  if (!ref) {
60
60
  throw new Error(
61
- 'Invalid setvalue element - you must define either "ref" or "bind" attribute'
61
+ `Invalid ${element.localName} element - you must define either "ref" or "bind" attribute`
62
62
  );
63
63
  }
64
64
  this.ref = ref;
@@ -19,6 +19,7 @@ export class AttributeDefinition
19
19
 
20
20
  readonly value: string;
21
21
  readonly type = 'attribute';
22
+ readonly valueType = 'string';
22
23
  readonly namespaceDeclarations: NamespaceDeclarationMap;
23
24
  readonly bodyElement = null;
24
25
  readonly root: RootDefinition;
@@ -56,4 +57,10 @@ export class AttributeDefinition
56
57
  serializeAttributeXML(): string {
57
58
  return this.serializedXML;
58
59
  }
60
+
61
+ toJSON() {
62
+ const { bind, bodyElement, parent, root, ...rest } = this;
63
+
64
+ return rest;
65
+ }
59
66
  }