@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.
- 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 +21681 -25500
- package/dist/index.js.map +1 -1
- package/dist/instance/PrimaryInstance.d.ts +4 -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/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/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 +21407 -25226
- package/dist/solid.js.map +1 -1
- package/package.json +2 -2
- 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/InputControl.ts +3 -5
- package/src/instance/PrimaryInstance.ts +17 -2
- package/src/instance/attachments/buildAttributes.ts +21 -1
- 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/text/markdownFormat.ts +4 -3
- 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 +64 -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/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
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
65
|
-
private readonly internalValue:
|
|
56
|
+
export class Geolocation {
|
|
57
|
+
private readonly internalValue: GeolocationInternalValue;
|
|
66
58
|
|
|
67
|
-
constructor(coordinates:
|
|
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():
|
|
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():
|
|
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 (
|
|
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):
|
|
122
|
-
|
|
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:
|
|
136
|
-
const decodedValue = typeof value === 'string' ?
|
|
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
|
-
|
|
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
|
|
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
|
|
|
@@ -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
|
-
//
|
|
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); //
|
|
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 (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
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
|
-
|
|
296
|
+
performActionComputation(context, setValue, action);
|
|
265
297
|
}
|
|
266
298
|
}
|
|
267
299
|
if (action.events.includes(XFORM_EVENT.odkInstanceLoad)) {
|
|
268
300
|
if (!isAddingRepeatChild(context)) {
|
|
269
|
-
|
|
301
|
+
performActionComputation(context, setValue, action);
|
|
270
302
|
}
|
|
271
303
|
}
|
|
272
304
|
if (action.events.includes(XFORM_EVENT.odkNewRepeat)) {
|
|
273
305
|
if (isAddingRepeatChild(context)) {
|
|
274
|
-
|
|
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
|
|
311
|
-
|
|
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 {
|
|
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
|
});
|