@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.
- package/dist/client/AttributeNode.d.ts +4 -3
- package/dist/client/InputNode.d.ts +8 -4
- package/dist/client/MarkdownNode.d.ts +3 -0
- package/dist/client/NoteNode.d.ts +6 -2
- package/dist/client/form/FormInstanceConfig.d.ts +4 -0
- package/dist/client/form/LoadFormResult.d.ts +5 -14
- package/dist/client/form/ResetFormInstance.d.ts +13 -0
- package/dist/entrypoints/FormResult/BaseFormResult.d.ts +1 -0
- package/dist/entrypoints/FormResult/BaseInstantiableFormResult.d.ts +2 -0
- package/dist/entrypoints/FormResult/FormFailureResult.d.ts +2 -0
- package/dist/entrypoints/createPotentiallyClientOwnedReactiveScope.d.ts +19 -0
- package/dist/index.js +22150 -25908
- package/dist/index.js.map +1 -1
- package/dist/instance/Attribute.d.ts +11 -23
- package/dist/instance/Group.d.ts +3 -0
- package/dist/instance/InputControl.d.ts +3 -0
- package/dist/instance/ModelValue.d.ts +4 -0
- package/dist/instance/Note.d.ts +4 -0
- package/dist/instance/PrimaryInstance.d.ts +7 -1
- package/dist/instance/RangeControl.d.ts +4 -0
- package/dist/instance/RankControl.d.ts +5 -1
- package/dist/instance/Root.d.ts +3 -0
- package/dist/instance/SelectControl.d.ts +5 -1
- package/dist/instance/TriggerControl.d.ts +4 -0
- package/dist/instance/UploadControl.d.ts +3 -0
- package/dist/instance/abstract/DescendantNode.d.ts +5 -4
- package/dist/instance/abstract/InstanceNode.d.ts +4 -3
- package/dist/instance/hierarchy.d.ts +2 -1
- package/dist/instance/internal-api/AttributeContext.d.ts +1 -0
- package/dist/instance/internal-api/InstanceConfig.d.ts +2 -1
- package/dist/instance/internal-api/InstanceValueContext.d.ts +1 -0
- package/dist/instance/markdown/MarkdownNode.d.ts +14 -9
- package/dist/instance/repeat/RepeatInstance.d.ts +2 -0
- package/dist/integration/xpath/adapter/XFormsXPathNode.d.ts +1 -1
- package/dist/integration/xpath/adapter/kind.d.ts +5 -3
- package/dist/integration/xpath/adapter/traversal.d.ts +3 -3
- package/dist/integration/xpath/static-dom/StaticAttribute.d.ts +1 -0
- package/dist/integration/xpath/static-dom/StaticDocument.d.ts +2 -0
- package/dist/lib/codecs/{Geopoint/Geopoint.d.ts → geolocation/Geolocation.d.ts} +11 -15
- package/dist/lib/codecs/geolocation/Geopoint.d.ts +7 -0
- package/dist/lib/codecs/geolocation/Geoshape.d.ts +7 -0
- package/dist/lib/codecs/geolocation/Geotrace.d.ts +7 -0
- package/dist/lib/codecs/geolocation/createGeolocationValueCodec.d.ts +3 -0
- package/dist/lib/codecs/getSharedValueCodec.d.ts +7 -5
- package/dist/lib/reactivity/text/createTextRange.d.ts +0 -2
- package/dist/parse/XFormDOM.d.ts +7 -1
- package/dist/parse/body/appearance/inputAppearanceParser.d.ts +1 -1
- package/dist/parse/model/ActionDefinition.d.ts +1 -1
- package/dist/parse/model/AttributeDefinition.d.ts +2 -0
- package/dist/parse/model/BindPreloadDefinition.d.ts +2 -1
- package/dist/parse/model/ModelActionMap.d.ts +3 -2
- package/dist/parse/model/ModelDefinition.d.ts +3 -5
- package/dist/parse/model/SecondaryInstance/sources/CSVExternalSecondaryInstance.d.ts +0 -17
- package/dist/parse/model/SecondaryInstance/sources/external-instance-csv-parser.d.ts +8 -0
- package/dist/parse/model/TranslationDefinitionMap.d.ts +4 -0
- package/dist/solid.js +21608 -25366
- package/dist/solid.js.map +1 -1
- package/package.json +2 -2
- package/src/client/AttributeNode.ts +4 -3
- package/src/client/InputNode.ts +11 -3
- package/src/client/MarkdownNode.ts +3 -0
- package/src/client/NoteNode.ts +9 -1
- package/src/client/form/FormInstanceConfig.ts +6 -0
- package/src/client/form/LoadFormResult.ts +5 -17
- package/src/client/form/ResetFormInstance.ts +17 -0
- package/src/entrypoints/FormInstance.ts +2 -0
- package/src/entrypoints/FormResult/BaseFormResult.ts +1 -0
- package/src/entrypoints/FormResult/BaseInstantiableFormResult.ts +10 -1
- package/src/entrypoints/FormResult/FormFailureResult.ts +3 -0
- package/src/entrypoints/createPotentiallyClientOwnedReactiveScope.ts +30 -0
- package/src/entrypoints/loadForm.ts +1 -31
- package/src/instance/Attribute.ts +38 -54
- package/src/instance/Group.ts +12 -4
- package/src/instance/InputControl.ts +15 -9
- package/src/instance/ModelValue.ts +13 -4
- package/src/instance/Note.ts +13 -4
- package/src/instance/PrimaryInstance.ts +29 -6
- package/src/instance/RangeControl.ts +13 -4
- package/src/instance/RankControl.ts +14 -5
- package/src/instance/Root.ts +12 -4
- package/src/instance/SelectControl.ts +14 -5
- package/src/instance/TriggerControl.ts +13 -4
- package/src/instance/UploadControl.ts +13 -3
- package/src/instance/abstract/DescendantNode.ts +4 -3
- package/src/instance/abstract/InstanceNode.ts +5 -3
- package/src/instance/attachments/buildAttributes.ts +26 -2
- package/src/instance/children/childrenInitOptions.ts +2 -1
- package/src/instance/hierarchy.ts +2 -0
- package/src/instance/internal-api/AttributeContext.ts +1 -0
- package/src/instance/internal-api/InstanceConfig.ts +3 -0
- package/src/instance/internal-api/InstanceValueContext.ts +1 -0
- package/src/instance/markdown/MarkdownNode.ts +19 -7
- package/src/instance/repeat/RepeatInstance.ts +11 -3
- package/src/instance/text/markdownFormat.ts +4 -3
- package/src/integration/xpath/adapter/XFormsXPathNode.ts +1 -0
- package/src/integration/xpath/adapter/engineDOMAdapter.ts +2 -2
- package/src/integration/xpath/adapter/kind.ts +6 -1
- package/src/integration/xpath/adapter/names.ts +1 -0
- package/src/integration/xpath/adapter/traversal.ts +5 -6
- package/src/integration/xpath/static-dom/StaticAttribute.ts +1 -0
- package/src/integration/xpath/static-dom/StaticDocument.ts +2 -0
- package/src/lib/codecs/{Geopoint/Geopoint.ts → geolocation/Geolocation.ts} +43 -24
- package/src/lib/codecs/geolocation/Geopoint.ts +15 -0
- package/src/lib/codecs/geolocation/Geoshape.ts +36 -0
- package/src/lib/codecs/geolocation/Geotrace.ts +36 -0
- package/src/lib/codecs/geolocation/createGeolocationValueCodec.ts +18 -0
- package/src/lib/codecs/getSharedValueCodec.ts +37 -11
- package/src/lib/reactivity/createInstanceValueState.ts +90 -34
- package/src/lib/reactivity/text/createTextRange.ts +71 -45
- package/src/parse/XFormDOM.ts +22 -2
- package/src/parse/model/ActionDefinition.ts +6 -6
- package/src/parse/model/AttributeDefinition.ts +7 -0
- package/src/parse/model/BindDefinition.ts +1 -1
- package/src/parse/model/BindPreloadDefinition.ts +21 -14
- package/src/parse/model/ModelActionMap.ts +30 -13
- package/src/parse/model/ModelDefinition.ts +5 -10
- package/src/parse/model/RootDefinition.ts +2 -1
- package/src/parse/model/SecondaryInstance/sources/CSVExternalSecondaryInstance.ts +2 -184
- package/src/parse/model/SecondaryInstance/sources/external-instance-csv-parser.ts +185 -0
- package/src/parse/model/TranslationDefinitionMap.ts +23 -0
- package/dist/lib/codecs/Geopoint/GeopointValueCodec.d.ts +0 -5
- package/dist/parse/model/generateItextChunks.d.ts +0 -5
- package/src/lib/codecs/Geopoint/GeopointValueCodec.ts +0 -20
- 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
|
|
10
|
-
import {
|
|
11
|
-
|
|
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:
|
|
26
|
-
readonly geoshape:
|
|
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:
|
|
44
|
-
readonly geoshape:
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
//
|
|
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); //
|
|
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 (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
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
|
-
|
|
296
|
+
performActionComputation(context, setValue, action);
|
|
239
297
|
}
|
|
240
298
|
}
|
|
241
299
|
if (action.events.includes(XFORM_EVENT.odkInstanceLoad)) {
|
|
242
300
|
if (!isAddingRepeatChild(context)) {
|
|
243
|
-
|
|
301
|
+
performActionComputation(context, setValue, action);
|
|
244
302
|
}
|
|
245
303
|
}
|
|
246
304
|
if (action.events.includes(XFORM_EVENT.odkNewRepeat)) {
|
|
247
305
|
if (isAddingRepeatChild(context)) {
|
|
248
|
-
|
|
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
|
|
285
|
-
|
|
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 {
|
|
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
|
|
71
|
+
* - Captures the first image found with a 'from=<"image"|"audio"|"video">' attribute.
|
|
24
72
|
*
|
|
25
|
-
* @param context
|
|
26
|
-
* @param
|
|
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
|
-
):
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
48
|
-
// only translations have 'nodes' chunks
|
|
49
|
-
chunkExpressions = definition.chunks as Array<TextChunkExpression<'string'>>;
|
|
89
|
+
return;
|
|
50
90
|
}
|
|
51
91
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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 =
|
|
118
|
+
const chunks = createTextChunks(context, definition);
|
|
93
119
|
return new TextRange('form', role, chunks.chunks, chunks.mediaSources);
|
|
94
120
|
});
|
|
95
121
|
});
|
package/src/parse/XFormDOM.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
8
|
-
if (
|
|
9
|
-
return
|
|
7
|
+
static getRef(model: ModelDefinition, element: Element): string | null {
|
|
8
|
+
if (element.hasAttribute('ref')) {
|
|
9
|
+
return element.getAttribute('ref') ?? null;
|
|
10
10
|
}
|
|
11
|
-
if (
|
|
12
|
-
const bindId =
|
|
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
|
-
|
|
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
|
}
|