@getodk/xforms-engine 0.16.1 → 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 (80) hide show
  1. package/dist/client/InputNode.d.ts +8 -4
  2. package/dist/client/MarkdownNode.d.ts +3 -0
  3. package/dist/client/NoteNode.d.ts +6 -2
  4. package/dist/client/form/FormInstanceConfig.d.ts +4 -0
  5. package/dist/client/form/LoadFormResult.d.ts +5 -14
  6. package/dist/client/form/ResetFormInstance.d.ts +13 -0
  7. package/dist/entrypoints/FormResult/BaseFormResult.d.ts +1 -0
  8. package/dist/entrypoints/FormResult/BaseInstantiableFormResult.d.ts +2 -0
  9. package/dist/entrypoints/FormResult/FormFailureResult.d.ts +2 -0
  10. package/dist/entrypoints/createPotentiallyClientOwnedReactiveScope.d.ts +19 -0
  11. package/dist/index.js +21681 -25500
  12. package/dist/index.js.map +1 -1
  13. package/dist/instance/PrimaryInstance.d.ts +4 -1
  14. package/dist/instance/internal-api/AttributeContext.d.ts +1 -0
  15. package/dist/instance/internal-api/InstanceConfig.d.ts +2 -1
  16. package/dist/instance/internal-api/InstanceValueContext.d.ts +1 -0
  17. package/dist/instance/markdown/MarkdownNode.d.ts +14 -9
  18. package/dist/integration/xpath/static-dom/StaticDocument.d.ts +2 -0
  19. package/dist/lib/codecs/{Geopoint/Geopoint.d.ts → geolocation/Geolocation.d.ts} +11 -15
  20. package/dist/lib/codecs/geolocation/Geopoint.d.ts +7 -0
  21. package/dist/lib/codecs/geolocation/Geoshape.d.ts +7 -0
  22. package/dist/lib/codecs/geolocation/Geotrace.d.ts +7 -0
  23. package/dist/lib/codecs/geolocation/createGeolocationValueCodec.d.ts +3 -0
  24. package/dist/lib/codecs/getSharedValueCodec.d.ts +7 -5
  25. package/dist/lib/reactivity/text/createTextRange.d.ts +0 -2
  26. package/dist/parse/XFormDOM.d.ts +7 -1
  27. package/dist/parse/body/appearance/inputAppearanceParser.d.ts +1 -1
  28. package/dist/parse/model/ActionDefinition.d.ts +1 -1
  29. package/dist/parse/model/BindPreloadDefinition.d.ts +2 -1
  30. package/dist/parse/model/ModelActionMap.d.ts +3 -2
  31. package/dist/parse/model/ModelDefinition.d.ts +3 -5
  32. package/dist/parse/model/SecondaryInstance/sources/CSVExternalSecondaryInstance.d.ts +0 -17
  33. package/dist/parse/model/SecondaryInstance/sources/external-instance-csv-parser.d.ts +8 -0
  34. package/dist/parse/model/TranslationDefinitionMap.d.ts +4 -0
  35. package/dist/solid.js +21407 -25226
  36. package/dist/solid.js.map +1 -1
  37. package/package.json +2 -2
  38. package/src/client/InputNode.ts +11 -3
  39. package/src/client/MarkdownNode.ts +3 -0
  40. package/src/client/NoteNode.ts +9 -1
  41. package/src/client/form/FormInstanceConfig.ts +6 -0
  42. package/src/client/form/LoadFormResult.ts +5 -17
  43. package/src/client/form/ResetFormInstance.ts +17 -0
  44. package/src/entrypoints/FormInstance.ts +2 -0
  45. package/src/entrypoints/FormResult/BaseFormResult.ts +1 -0
  46. package/src/entrypoints/FormResult/BaseInstantiableFormResult.ts +10 -1
  47. package/src/entrypoints/FormResult/FormFailureResult.ts +3 -0
  48. package/src/entrypoints/createPotentiallyClientOwnedReactiveScope.ts +30 -0
  49. package/src/entrypoints/loadForm.ts +1 -31
  50. package/src/instance/InputControl.ts +3 -5
  51. package/src/instance/PrimaryInstance.ts +17 -2
  52. package/src/instance/attachments/buildAttributes.ts +21 -1
  53. package/src/instance/internal-api/AttributeContext.ts +1 -0
  54. package/src/instance/internal-api/InstanceConfig.ts +3 -0
  55. package/src/instance/internal-api/InstanceValueContext.ts +1 -0
  56. package/src/instance/markdown/MarkdownNode.ts +19 -7
  57. package/src/instance/text/markdownFormat.ts +4 -3
  58. package/src/integration/xpath/static-dom/StaticDocument.ts +2 -0
  59. package/src/lib/codecs/{Geopoint/Geopoint.ts → geolocation/Geolocation.ts} +43 -24
  60. package/src/lib/codecs/geolocation/Geopoint.ts +15 -0
  61. package/src/lib/codecs/geolocation/Geoshape.ts +36 -0
  62. package/src/lib/codecs/geolocation/Geotrace.ts +36 -0
  63. package/src/lib/codecs/geolocation/createGeolocationValueCodec.ts +18 -0
  64. package/src/lib/codecs/getSharedValueCodec.ts +37 -11
  65. package/src/lib/reactivity/createInstanceValueState.ts +64 -34
  66. package/src/lib/reactivity/text/createTextRange.ts +71 -45
  67. package/src/parse/XFormDOM.ts +22 -2
  68. package/src/parse/model/ActionDefinition.ts +6 -6
  69. package/src/parse/model/BindDefinition.ts +1 -1
  70. package/src/parse/model/BindPreloadDefinition.ts +21 -14
  71. package/src/parse/model/ModelActionMap.ts +30 -13
  72. package/src/parse/model/ModelDefinition.ts +5 -10
  73. package/src/parse/model/RootDefinition.ts +2 -1
  74. package/src/parse/model/SecondaryInstance/sources/CSVExternalSecondaryInstance.ts +2 -184
  75. package/src/parse/model/SecondaryInstance/sources/external-instance-csv-parser.ts +185 -0
  76. package/src/parse/model/TranslationDefinitionMap.ts +23 -0
  77. package/dist/lib/codecs/Geopoint/GeopointValueCodec.d.ts +0 -5
  78. package/dist/parse/model/generateItextChunks.d.ts +0 -5
  79. package/src/lib/codecs/Geopoint/GeopointValueCodec.ts +0 -20
  80. package/src/parse/model/generateItextChunks.ts +0 -61
@@ -1,8 +1,3 @@
1
- /**
2
- * This abstract class defines the minimal behavior for a default geopoint.
3
- * It can be expanded later to support units (e.g., degrees or meters),
4
- * which would also serve as documentation to clarify what each value represents.
5
- */
6
1
  abstract class SemanticValue<Semantic extends string, Value extends number | null> {
7
2
  abstract readonly semantic: Semantic;
8
3
 
@@ -25,26 +20,21 @@ class Accuracy<Value extends number | null = number> extends SemanticValue<'accu
25
20
  readonly semantic = 'accuracy';
26
21
  }
27
22
 
28
- export interface GeopointValue {
23
+ export interface LocationPoint {
29
24
  readonly latitude: number;
30
25
  readonly longitude: number;
31
26
  readonly altitude: number | null;
32
27
  readonly accuracy: number | null;
33
28
  }
34
29
 
35
- export type GeopointRuntimeValue = GeopointValue | null;
36
-
37
- // TODO: Add support for GeoJSONValue
38
- export type GeopointInputValue = GeopointRuntimeValue | string;
39
-
40
- interface GeopointInternalValue {
30
+ interface GeolocationInternalValue {
41
31
  readonly latitude: Latitude;
42
32
  readonly longitude: Longitude;
43
33
  readonly altitude: Altitude<null> | Altitude<number>;
44
34
  readonly accuracy: Accuracy<null> | Accuracy<number>;
45
35
  }
46
36
 
47
- type GeopointTuple =
37
+ type LocationPointTuple =
48
38
  | readonly [
49
39
  latitude: Latitude,
50
40
  longitude: Longitude,
@@ -54,6 +44,8 @@ type GeopointTuple =
54
44
  | readonly [latitude: Latitude, longitude: Longitude, altitude: Altitude]
55
45
  | readonly [latitude: Latitude, longitude: Longitude];
56
46
 
47
+ export const SEGMENT_SEPARATOR = ';';
48
+
57
49
  const DEGREES_MAX = {
58
50
  latitude: 90,
59
51
  longitude: 180,
@@ -61,10 +53,10 @@ const DEGREES_MAX = {
61
53
 
62
54
  type CoordinateType = keyof typeof DEGREES_MAX;
63
55
 
64
- export class Geopoint {
65
- private readonly internalValue: GeopointInternalValue;
56
+ export class Geolocation {
57
+ private readonly internalValue: GeolocationInternalValue;
66
58
 
67
- constructor(coordinates: GeopointValue) {
59
+ constructor(coordinates: LocationPoint) {
68
60
  const { latitude, longitude, altitude, accuracy } = coordinates;
69
61
 
70
62
  this.internalValue = {
@@ -75,7 +67,7 @@ export class Geopoint {
75
67
  };
76
68
  }
77
69
 
78
- getTuple(): GeopointTuple {
70
+ getTuple(): LocationPointTuple {
79
71
  const { latitude, longitude, altitude, accuracy } = this.internalValue;
80
72
 
81
73
  if (accuracy.value != null) {
@@ -89,12 +81,16 @@ export class Geopoint {
89
81
  return [latitude, longitude];
90
82
  }
91
83
 
92
- getRuntimeValue(): GeopointRuntimeValue {
84
+ getRuntimeValue(): LocationPoint | null {
93
85
  const { latitude, longitude, altitude, accuracy } = this.internalValue;
94
86
  const isLatitude = this.isValidDegrees('latitude', latitude.value);
95
87
  const isLongitude = this.isValidDegrees('longitude', longitude.value);
96
88
 
97
- if (!isLatitude || !isLongitude || Geopoint.isNullLocation(latitude.value, longitude.value)) {
89
+ if (
90
+ !isLatitude ||
91
+ !isLongitude ||
92
+ Geolocation.isNullLocation(latitude.value, longitude.value)
93
+ ) {
98
94
  return null;
99
95
  }
100
96
 
@@ -118,8 +114,9 @@ export class Geopoint {
118
114
  return latitude === 0 && longitude === 0;
119
115
  }
120
116
 
121
- static parseString(value: string): GeopointRuntimeValue {
122
- if (value.trim() === '') {
117
+ static parseString(value: string): LocationPoint | null {
118
+ value = value.trim();
119
+ if (value === '') {
123
120
  return null;
124
121
  }
125
122
 
@@ -132,12 +129,12 @@ export class Geopoint {
132
129
  return new this({ latitude, longitude, altitude, accuracy }).getRuntimeValue();
133
130
  }
134
131
 
135
- static toCoordinatesString(value: GeopointInputValue): string {
136
- const decodedValue = typeof value === 'string' ? Geopoint.parseString(value) : value;
132
+ static toCoordinatesString(value: LocationPoint | string | null): string {
133
+ const decodedValue = typeof value === 'string' ? Geolocation.parseString(value) : value;
137
134
 
138
135
  if (
139
136
  decodedValue == null ||
140
- Geopoint.isNullLocation(decodedValue.latitude, decodedValue.longitude)
137
+ Geolocation.isNullLocation(decodedValue.latitude, decodedValue.longitude)
141
138
  ) {
142
139
  return '';
143
140
  }
@@ -147,4 +144,26 @@ export class Geopoint {
147
144
  .map((item) => item.value ?? 0)
148
145
  .join(' ');
149
146
  }
147
+
148
+ static isClosedShape(points: LocationPoint[]) {
149
+ const firstPoint = points[0];
150
+ const lastPoint = points[points.length - 1];
151
+ return (
152
+ firstPoint?.latitude === lastPoint?.latitude && firstPoint?.longitude === lastPoint?.longitude
153
+ );
154
+ }
155
+
156
+ static getSegments(value: string): string[] | null {
157
+ if (value.trim() === '') {
158
+ return null;
159
+ }
160
+
161
+ const parts = value.split(SEGMENT_SEPARATOR);
162
+ // Handles trailing semicolon, which is valid and common in ODK.
163
+ if (parts[parts.length - 1]?.trim() === '') {
164
+ parts.pop();
165
+ }
166
+
167
+ return parts;
168
+ }
150
169
  }
@@ -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
 
@@ -225,6 +220,16 @@ const createActionCalculation = (
225
220
  });
226
221
  };
227
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
+
228
233
  const createValueChangedCalculation = (
229
234
  context: ValueContext,
230
235
  setRelevantValue: SimpleAtomicStateSetter<string>,
@@ -232,21 +237,27 @@ const createValueChangedCalculation = (
232
237
  ): void => {
233
238
  const { source, ref } = bindToRepeatInstance(context, action);
234
239
  if (!source) {
235
- // no element to listen to
240
+ // No element to listen to
236
241
  return;
237
242
  }
238
- let previous = '';
243
+ let previous: string;
239
244
  const sourceElementExpression = new ActionComputationExpression('string', source);
240
- const calculateValueSource = createComputedExpression(context, sourceElementExpression); // registers listener
245
+ const calculateValueSource = createComputedExpression(context, sourceElementExpression); // Registers listener
241
246
  createComputed(() => {
242
247
  if (context.isAttached() && context.isRelevant()) {
243
248
  const valueSource = calculateValueSource();
244
- if (previous !== valueSource) {
245
- // only update if value has changed
246
- if (referencesCurrentNode(context, ref)) {
247
- const calc = context.evaluator.evaluateString(action.computation.expression, context);
248
- const value = context.decodeInstanceValue(calc);
249
- 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);
250
261
  }
251
262
  }
252
263
  previous = valueSource;
@@ -254,24 +265,45 @@ const createValueChangedCalculation = (
254
265
  });
255
266
  };
256
267
 
257
- 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 = (
258
290
  context: ValueContext,
259
291
  setValue: SimpleAtomicStateSetter<string>,
260
292
  action: ActionDefinition
261
293
  ) => {
262
294
  if (action.events.includes(XFORM_EVENT.odkInstanceFirstLoad)) {
263
295
  if (isInstanceFirstLoad(context)) {
264
- createActionCalculation(context, setValue, action.computation);
296
+ performActionComputation(context, setValue, action);
265
297
  }
266
298
  }
267
299
  if (action.events.includes(XFORM_EVENT.odkInstanceLoad)) {
268
300
  if (!isAddingRepeatChild(context)) {
269
- createActionCalculation(context, setValue, action.computation);
301
+ performActionComputation(context, setValue, action);
270
302
  }
271
303
  }
272
304
  if (action.events.includes(XFORM_EVENT.odkNewRepeat)) {
273
305
  if (isAddingRepeatChild(context)) {
274
- createActionCalculation(context, setValue, action.computation);
306
+ performActionComputation(context, setValue, action);
275
307
  }
276
308
  }
277
309
  if (action.events.includes(XFORM_EVENT.xformsValueChanged)) {
@@ -307,10 +339,8 @@ export const createInstanceValueState = (context: ValueContext): InstanceValueSt
307
339
  createCalculation(context, setValue, calculate);
308
340
  }
309
341
 
310
- const action = context.definition.model.actions.get(context.contextReference());
311
- if (action) {
312
- registerAction(context, setValue, action);
313
- }
342
+ const actions = context.definition.model.actions.get(context.contextReference());
343
+ actions?.forEach((action) => dispatchAction(context, setValue, action));
314
344
 
315
345
  return guardDownstreamReadonlyWrites(context, relevantValueState);
316
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
  });