@getodk/xforms-engine 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/AttributeNode.d.ts +53 -0
- package/dist/client/BaseItem.d.ts +6 -0
- package/dist/client/GroupNode.d.ts +4 -4
- package/dist/client/MarkdownNode.d.ts +33 -0
- package/dist/client/RankNode.d.ts +2 -4
- package/dist/client/SelectNode.d.ts +2 -5
- package/dist/client/TextRange.d.ts +2 -2
- package/dist/client/hierarchy.d.ts +1 -2
- package/dist/client/index.d.ts +2 -1
- package/dist/client/node-types.d.ts +2 -2
- package/dist/client/validation.d.ts +7 -1
- package/dist/index.js +10758 -402
- package/dist/index.js.map +1 -1
- package/dist/instance/Attribute.d.ts +58 -0
- package/dist/instance/Group.d.ts +4 -0
- package/dist/instance/PrimaryInstance.d.ts +4 -0
- package/dist/instance/Root.d.ts +4 -0
- package/dist/instance/UploadControl.d.ts +4 -0
- package/dist/instance/abstract/InstanceNode.d.ts +7 -4
- package/dist/instance/abstract/ValueNode.d.ts +1 -0
- package/dist/instance/attachments/buildAttributes.d.ts +3 -0
- package/dist/instance/hierarchy.d.ts +6 -6
- package/dist/instance/internal-api/AttributeContext.d.ts +29 -0
- package/dist/instance/internal-api/serialization/ClientReactiveSerializableAttributeNode.d.ts +16 -0
- package/dist/instance/internal-api/serialization/ClientReactiveSerializableParentNode.d.ts +2 -0
- package/dist/instance/internal-api/serialization/ClientReactiveSerializableTemplatedNode.d.ts +2 -2
- package/dist/instance/markdown/MarkdownNode.d.ts +75 -0
- package/dist/instance/repeat/BaseRepeatRange.d.ts +5 -0
- package/dist/instance/repeat/RepeatInstance.d.ts +4 -2
- package/dist/instance/text/TextChunk.d.ts +0 -1
- package/dist/instance/text/TextRange.d.ts +2 -1
- package/dist/instance/text/markdownFormat.d.ts +3 -0
- package/dist/integration/xpath/adapter/XFormsXPathNode.d.ts +2 -2
- package/dist/lib/client-reactivity/instance-state/createAttributeNodeInstanceState.d.ts +3 -0
- package/dist/lib/client-reactivity/instance-state/createTemplatedNodeInstanceState.d.ts +0 -3
- package/dist/lib/names/NamespaceDeclarationMap.d.ts +1 -1
- package/dist/lib/reactivity/createAttributeState.d.ts +16 -0
- package/dist/lib/reactivity/createAttributeValueState.d.ts +15 -0
- package/dist/lib/reactivity/createItemCollection.d.ts +5 -7
- package/dist/lib/xml-serialization.d.ts +5 -9
- package/dist/parse/XFormDOM.d.ts +1 -1
- package/dist/parse/body/BodyDefinition.d.ts +2 -8
- package/dist/parse/body/GroupElementDefinition.d.ts +22 -0
- package/dist/parse/body/control/ItemsetDefinition.d.ts +3 -0
- package/dist/parse/expression/ItemPropertyExpression.d.ts +6 -0
- package/dist/parse/model/AttributeDefinition.d.ts +22 -0
- package/dist/parse/model/{RootAttributeMap.d.ts → AttributeDefinitionMap.d.ts} +4 -10
- package/dist/parse/model/{SubtreeDefinition.d.ts → GroupDefinition.d.ts} +8 -4
- package/dist/parse/model/LeafNodeDefinition.d.ts +1 -0
- package/dist/parse/model/NodeDefinition.d.ts +13 -9
- package/dist/parse/model/RepeatDefinition.d.ts +5 -2
- package/dist/parse/model/RootDefinition.d.ts +3 -3
- package/dist/parse/text/LabelDefinition.d.ts +4 -5
- package/dist/solid.js +10758 -402
- package/dist/solid.js.map +1 -1
- package/package.json +6 -5
- package/src/client/AttributeNode.ts +62 -0
- package/src/client/BaseItem.ts +7 -0
- package/src/client/GroupNode.ts +4 -10
- package/src/client/MarkdownNode.ts +53 -0
- package/src/client/RankNode.ts +2 -5
- package/src/client/SelectNode.ts +2 -6
- package/src/client/TextRange.ts +2 -2
- package/src/client/hierarchy.ts +0 -2
- package/src/client/index.ts +2 -1
- package/src/client/node-types.ts +1 -1
- package/src/client/validation.ts +9 -1
- package/src/instance/Attribute.ts +164 -0
- package/src/instance/Group.ts +17 -1
- package/src/instance/InputControl.ts +1 -0
- package/src/instance/ModelValue.ts +1 -0
- package/src/instance/Note.ts +1 -0
- package/src/instance/PrimaryInstance.ts +17 -0
- package/src/instance/RangeControl.ts +1 -0
- package/src/instance/RankControl.ts +1 -0
- package/src/instance/Root.ts +16 -0
- package/src/instance/SelectControl.ts +1 -0
- package/src/instance/TriggerControl.ts +1 -0
- package/src/instance/UploadControl.ts +14 -0
- package/src/instance/abstract/DescendantNode.ts +5 -1
- package/src/instance/abstract/InstanceNode.ts +6 -3
- package/src/instance/abstract/ValueNode.ts +1 -0
- package/src/instance/attachments/buildAttributes.ts +8 -0
- package/src/instance/children/buildChildren.ts +1 -17
- package/src/instance/children/normalizeChildInitOptions.ts +44 -59
- package/src/instance/hierarchy.ts +3 -7
- package/src/instance/internal-api/AttributeContext.ts +34 -0
- package/src/instance/internal-api/serialization/ClientReactiveSerializableAttributeNode.ts +19 -0
- package/src/instance/internal-api/serialization/ClientReactiveSerializableParentNode.ts +2 -0
- package/src/instance/internal-api/serialization/ClientReactiveSerializableTemplatedNode.ts +2 -3
- package/src/instance/markdown/MarkdownNode.ts +115 -0
- package/src/instance/repeat/BaseRepeatRange.ts +14 -0
- package/src/instance/repeat/RepeatInstance.ts +14 -5
- package/src/instance/text/TextChunk.ts +0 -5
- package/src/instance/text/TextRange.ts +5 -3
- package/src/instance/text/markdownFormat.ts +214 -0
- package/src/integration/xpath/adapter/XFormsXPathNode.ts +3 -1
- package/src/integration/xpath/adapter/names.ts +0 -1
- package/src/lib/client-reactivity/instance-state/createAttributeNodeInstanceState.ts +16 -0
- package/src/lib/client-reactivity/instance-state/createParentNodeInstanceState.ts +5 -5
- package/src/lib/client-reactivity/instance-state/createRootInstanceState.ts +6 -9
- package/src/lib/client-reactivity/instance-state/createTemplatedNodeInstanceState.ts +5 -15
- package/src/lib/names/NamespaceDeclarationMap.ts +1 -1
- package/src/lib/reactivity/createAttributeState.ts +51 -0
- package/src/lib/reactivity/createAttributeValueState.ts +189 -0
- package/src/lib/reactivity/createItemCollection.ts +25 -9
- package/src/lib/xml-serialization.ts +30 -34
- package/src/parse/body/BodyDefinition.ts +7 -34
- package/src/parse/body/GroupElementDefinition.ts +47 -0
- package/src/parse/body/control/ItemsetDefinition.ts +7 -0
- package/src/parse/expression/ItemPropertyExpression.ts +12 -0
- package/src/parse/model/AttributeDefinition.ts +58 -0
- package/src/parse/model/{RootAttributeMap.ts → AttributeDefinitionMap.ts} +7 -13
- package/src/parse/model/{SubtreeDefinition.ts → GroupDefinition.ts} +12 -8
- package/src/parse/model/LeafNodeDefinition.ts +1 -0
- package/src/parse/model/NodeDefinition.ts +19 -12
- package/src/parse/model/RepeatDefinition.ts +10 -3
- package/src/parse/model/RootDefinition.ts +6 -6
- package/src/parse/model/nodeDefinitionMap.ts +1 -1
- package/src/parse/text/LabelDefinition.ts +4 -9
- package/dist/client/SubtreeNode.d.ts +0 -56
- package/dist/error/TemplatedNodeAttributeSerializationError.d.ts +0 -22
- package/dist/instance/Subtree.d.ts +0 -38
- package/dist/instance/text/FormattedTextStub.d.ts +0 -1
- package/dist/parse/body/group/BaseGroupDefinition.d.ts +0 -40
- package/dist/parse/body/group/LogicalGroupDefinition.d.ts +0 -6
- package/dist/parse/body/group/PresentationGroupDefinition.d.ts +0 -11
- package/dist/parse/body/group/StructuralGroupDefinition.d.ts +0 -6
- package/dist/parse/model/RootAttributeDefinition.d.ts +0 -21
- package/src/client/SubtreeNode.ts +0 -61
- package/src/error/TemplatedNodeAttributeSerializationError.ts +0 -24
- package/src/instance/Subtree.ts +0 -102
- package/src/instance/text/FormattedTextStub.ts +0 -8
- package/src/parse/body/group/BaseGroupDefinition.ts +0 -89
- package/src/parse/body/group/LogicalGroupDefinition.ts +0 -11
- package/src/parse/body/group/PresentationGroupDefinition.ts +0 -28
- package/src/parse/body/group/StructuralGroupDefinition.ts +0 -11
- package/src/parse/model/RootAttributeDefinition.ts +0 -44
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Accessor, Setter, Signal } from 'solid-js';
|
|
2
|
+
import { createSignal } from 'solid-js';
|
|
3
|
+
import type { Attribute } from '../../instance/Attribute.ts';
|
|
4
|
+
import type { ReactiveScope } from './scope.ts';
|
|
5
|
+
|
|
6
|
+
export interface AttributeState {
|
|
7
|
+
readonly attributes: Signal<readonly Attribute[]>;
|
|
8
|
+
readonly getAttributes: Accessor<readonly Attribute[]>;
|
|
9
|
+
readonly setAttributes: Setter<readonly Attribute[]>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates attributes state suitable for all node types
|
|
14
|
+
*
|
|
15
|
+
* The produced {@link AttributeState.attributes} (and its get/set convenience
|
|
16
|
+
* methods) signal is intended to be used to store the engine's attribute state,
|
|
17
|
+
* and update that state when appropriate.
|
|
18
|
+
*/
|
|
19
|
+
export const createAttributeState = (scope: ReactiveScope): AttributeState => {
|
|
20
|
+
return scope.runTask(() => {
|
|
21
|
+
const baseState = createSignal<readonly Attribute[]>([]);
|
|
22
|
+
const [getAttributes, baseSetAttributes] = baseState;
|
|
23
|
+
|
|
24
|
+
type AttributeSetterCallback = (prev: readonly Attribute[]) => readonly Attribute[];
|
|
25
|
+
type AttributeOrSetterCallback = AttributeSetterCallback | readonly Attribute[];
|
|
26
|
+
|
|
27
|
+
const setAttributes: Setter<readonly Attribute[]> = (
|
|
28
|
+
valueOrSetterCallback: AttributeOrSetterCallback
|
|
29
|
+
) => {
|
|
30
|
+
let setterCallback: AttributeSetterCallback;
|
|
31
|
+
|
|
32
|
+
if (typeof valueOrSetterCallback === 'function') {
|
|
33
|
+
setterCallback = valueOrSetterCallback;
|
|
34
|
+
} else {
|
|
35
|
+
setterCallback = (_) => valueOrSetterCallback;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return baseSetAttributes((prev) => {
|
|
39
|
+
return setterCallback(prev);
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const attributes: Signal<readonly Attribute[]> = [getAttributes, setAttributes];
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
attributes,
|
|
47
|
+
getAttributes,
|
|
48
|
+
setAttributes,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { Signal } from 'solid-js';
|
|
2
|
+
import { createComputed, createMemo, createSignal, untrack } from 'solid-js';
|
|
3
|
+
import type { AttributeContext } from '../../instance/internal-api/AttributeContext.ts';
|
|
4
|
+
import type { BindComputationExpression } from '../../parse/expression/BindComputationExpression.ts';
|
|
5
|
+
import { createComputedExpression } from './createComputedExpression.ts';
|
|
6
|
+
import type { SimpleAtomicState, SimpleAtomicStateSetter } from './types.ts';
|
|
7
|
+
|
|
8
|
+
const isInstanceFirstLoad = (context: AttributeContext) => {
|
|
9
|
+
return context.rootDocument.initializationMode === 'create';
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Special case, does not correspond to any event.
|
|
14
|
+
*
|
|
15
|
+
* @see {@link shouldPreloadUID}
|
|
16
|
+
*/
|
|
17
|
+
const isEditInitialLoad = (context: AttributeContext) => {
|
|
18
|
+
return context.rootDocument.initializationMode === 'edit';
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const getInitialValue = (context: AttributeContext): string => {
|
|
22
|
+
const sourceNode = context.instanceNode ?? context.definition.template;
|
|
23
|
+
|
|
24
|
+
return context.decodeInstanceValue(sourceNode.value);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type BaseValueState = Signal<string>;
|
|
28
|
+
|
|
29
|
+
type RelevantValueState = SimpleAtomicState<string>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Wraps {@link baseValueState} in a signal-like interface which:
|
|
33
|
+
*
|
|
34
|
+
* - produces a blank value for nodes ({@link context}) in a non-relevant state
|
|
35
|
+
* - persists, and restores, the most recent non-blank value state when a
|
|
36
|
+
* node/context's relevance is restored
|
|
37
|
+
*/
|
|
38
|
+
const createRelevantValueState = (
|
|
39
|
+
context: AttributeContext,
|
|
40
|
+
baseValueState: BaseValueState
|
|
41
|
+
): RelevantValueState => {
|
|
42
|
+
return context.scope.runTask(() => {
|
|
43
|
+
const [getRelevantValue, setValue] = baseValueState;
|
|
44
|
+
|
|
45
|
+
const getValue = createMemo(() => {
|
|
46
|
+
if (context.isRelevant()) {
|
|
47
|
+
return getRelevantValue();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return '';
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return [getValue, setValue];
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* For fields with a `readonly` bind expression, prevent downstream
|
|
59
|
+
* (client/user) writes when the field is in a `readonly` state.
|
|
60
|
+
*/
|
|
61
|
+
const guardDownstreamReadonlyWrites = (
|
|
62
|
+
context: AttributeContext,
|
|
63
|
+
baseState: SimpleAtomicState<string>
|
|
64
|
+
): SimpleAtomicState<string> => {
|
|
65
|
+
const { readonly } = context.definition.bind;
|
|
66
|
+
|
|
67
|
+
if (readonly.isDefaultExpression) {
|
|
68
|
+
return baseState;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const [getValue, baseSetValue] = baseState;
|
|
72
|
+
|
|
73
|
+
const setValue: SimpleAtomicStateSetter<string> = (value) => {
|
|
74
|
+
if (context.isReadonly()) {
|
|
75
|
+
const reference = untrack(() => context.contextReference());
|
|
76
|
+
|
|
77
|
+
throw new Error(`Cannot write to readonly field: ${reference}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return baseSetValue(value);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return [getValue, setValue];
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Per {@link https://getodk.github.io/xforms-spec/#preload-attributes:~:text=concatenation%20of%20%E2%80%98uuid%3A%E2%80%99%20and%20uuid()}
|
|
88
|
+
*/
|
|
89
|
+
const PRELOAD_UID_EXPRESSION = 'concat("uuid:", uuid())';
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @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.:
|
|
93
|
+
*
|
|
94
|
+
* - When an instance is first loaded ({@link isInstanceFirstLoad})
|
|
95
|
+
* - When an instance is initially loaded for editing ({@link isEditInitialLoad})
|
|
96
|
+
*/
|
|
97
|
+
const shouldPreloadUID = (context: AttributeContext) => {
|
|
98
|
+
return isInstanceFirstLoad(context) || isEditInitialLoad(context);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @todo This is a temporary one-off, until we support the full range of
|
|
103
|
+
* {@link https://getodk.github.io/xforms-spec/#preload-attributes | preloads}.
|
|
104
|
+
*
|
|
105
|
+
* @todo ALSO, IMPORTANTLY(!): the **call site** for this function is
|
|
106
|
+
* semantically where we would expect to trigger a
|
|
107
|
+
* {@link https://getodk.github.io/xforms-spec/#event:odk-instance-first-load | odk-instance-first-load event},
|
|
108
|
+
* _and compute_ preloads semantically associated with that event.
|
|
109
|
+
*/
|
|
110
|
+
const setPreloadUIDValue = (context: AttributeContext, valueState: RelevantValueState): void => {
|
|
111
|
+
const { preload } = context.definition.bind;
|
|
112
|
+
|
|
113
|
+
if (preload?.type !== 'uid' || !shouldPreloadUID(context)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const preloadUIDValue = context.evaluator.evaluateString(PRELOAD_UID_EXPRESSION, {
|
|
118
|
+
contextNode: context.contextNode,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const [, setValue] = valueState;
|
|
122
|
+
|
|
123
|
+
setValue(preloadUIDValue);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Defines a reactive effect which writes the result of `calculate` bind
|
|
128
|
+
* computations to the provided value setter, on initialization and any
|
|
129
|
+
* subsequent reactive update.
|
|
130
|
+
*
|
|
131
|
+
* @see {@link setPreloadUIDValue} for important details about spec ordering of
|
|
132
|
+
* events and computations.
|
|
133
|
+
*/
|
|
134
|
+
const createCalculation = (
|
|
135
|
+
context: AttributeContext,
|
|
136
|
+
setRelevantValue: SimpleAtomicStateSetter<string>,
|
|
137
|
+
calculateDefinition: BindComputationExpression<'calculate'>
|
|
138
|
+
): void => {
|
|
139
|
+
context.scope.runTask(() => {
|
|
140
|
+
const calculate = createComputedExpression(context, calculateDefinition, {
|
|
141
|
+
defaultValue: '',
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
createComputed(() => {
|
|
145
|
+
if (context.isAttached() && context.isRelevant()) {
|
|
146
|
+
const calculated = calculate();
|
|
147
|
+
const value = context.decodeInstanceValue(calculated);
|
|
148
|
+
|
|
149
|
+
setRelevantValue(value);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export type InstanceValueState = SimpleAtomicState<string>;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Provides a consistent interface for value nodes of any type which:
|
|
159
|
+
*
|
|
160
|
+
* - derives initial state from either an existing instance (e.g. for edits) or
|
|
161
|
+
* the node's definition (e.g. initializing a new instance)
|
|
162
|
+
* - decodes current primary instance state into the value node's runtime type
|
|
163
|
+
* - encodes updated runtime values to store updated instance state
|
|
164
|
+
* - initializes reactive computation of `calculate` bind expressions for those
|
|
165
|
+
* nodes defined with one
|
|
166
|
+
* - prevents downstream writes to nodes in a readonly state
|
|
167
|
+
*/
|
|
168
|
+
export const createAttributeValueState = (context: AttributeContext): InstanceValueState => {
|
|
169
|
+
return context.scope.runTask(() => {
|
|
170
|
+
const initialValue = getInitialValue(context);
|
|
171
|
+
const baseValueState = createSignal(initialValue);
|
|
172
|
+
const relevantValueState = createRelevantValueState(context, baseValueState);
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @see {@link setPreloadUIDValue} for important details about spec ordering of events and computations.
|
|
176
|
+
*/
|
|
177
|
+
setPreloadUIDValue(context, relevantValueState);
|
|
178
|
+
|
|
179
|
+
const { calculate } = context.definition.bind;
|
|
180
|
+
|
|
181
|
+
if (calculate != null) {
|
|
182
|
+
const [, setValue] = relevantValueState;
|
|
183
|
+
|
|
184
|
+
createCalculation(context, setValue, calculate);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return guardDownstreamReadonlyWrites(context, relevantValueState);
|
|
188
|
+
});
|
|
189
|
+
};
|
|
@@ -2,13 +2,12 @@ import { UpsertableMap } from '@getodk/common/lib/collections/UpsertableMap.ts';
|
|
|
2
2
|
import type { Accessor } from 'solid-js';
|
|
3
3
|
import { createMemo } from 'solid-js';
|
|
4
4
|
import type { ActiveLanguage } from '../../client/FormLanguage.ts';
|
|
5
|
-
import type {
|
|
6
|
-
import type { RankItem } from '../../client/RankNode.ts';
|
|
5
|
+
import type { BaseItem } from '../../client/BaseItem.ts';
|
|
7
6
|
import type { TextRange as ClientTextRange } from '../../client/TextRange.ts';
|
|
8
7
|
import type { EvaluationContext } from '../../instance/internal-api/EvaluationContext.ts';
|
|
9
8
|
import type { TranslationContext } from '../../instance/internal-api/TranslationContext.ts';
|
|
10
|
-
import type { SelectControl } from '../../instance/SelectControl.ts';
|
|
11
9
|
import type { RankControl } from '../../instance/RankControl.ts';
|
|
10
|
+
import type { SelectControl } from '../../instance/SelectControl.ts';
|
|
12
11
|
import { TextChunk } from '../../instance/text/TextChunk.ts';
|
|
13
12
|
import { TextRange } from '../../instance/text/TextRange.ts';
|
|
14
13
|
import type { EngineXPathNode } from '../../integration/xpath/adapter/kind.ts';
|
|
@@ -19,8 +18,7 @@ import { createComputedExpression } from './createComputedExpression.ts';
|
|
|
19
18
|
import type { ReactiveScope } from './scope.ts';
|
|
20
19
|
import { createTextRange } from './text/createTextRange.ts';
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
type Item = RankItem | SelectItem;
|
|
21
|
+
type ItemCollectionControl = RankControl | SelectControl;
|
|
24
22
|
type DerivedItemLabel = ClientTextRange<'item-label', 'form-derived'>;
|
|
25
23
|
|
|
26
24
|
const derivedItemLabel = (context: TranslationContext, value: string): DerivedItemLabel => {
|
|
@@ -45,7 +43,7 @@ const createItemLabel = (
|
|
|
45
43
|
const createTranslatedStaticItems = (
|
|
46
44
|
control: ItemCollectionControl,
|
|
47
45
|
items: readonly ItemDefinition[]
|
|
48
|
-
): Accessor<readonly
|
|
46
|
+
): Accessor<readonly BaseItem[]> => {
|
|
49
47
|
return control.scope.runTask(() => {
|
|
50
48
|
const labeledItems = items.map((item) => {
|
|
51
49
|
const { value } = item;
|
|
@@ -54,6 +52,7 @@ const createTranslatedStaticItems = (
|
|
|
54
52
|
return () => ({
|
|
55
53
|
value,
|
|
56
54
|
label: label(),
|
|
55
|
+
properties: [],
|
|
57
56
|
});
|
|
58
57
|
});
|
|
59
58
|
|
|
@@ -101,6 +100,7 @@ const createItemsetItemLabel = (
|
|
|
101
100
|
interface ItemsetItem {
|
|
102
101
|
label(): ClientTextRange<'item-label'>;
|
|
103
102
|
value(): string;
|
|
103
|
+
properties: Array<[string, () => string]>;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
const createItemsetItems = (
|
|
@@ -122,9 +122,20 @@ const createItemsetItems = (
|
|
|
122
122
|
});
|
|
123
123
|
const label = createItemsetItemLabel(context, itemset, value);
|
|
124
124
|
|
|
125
|
+
const nodeElements = itemNode
|
|
126
|
+
.getXPathChildNodes()
|
|
127
|
+
.filter((node) => node.nodeType === 'static-element');
|
|
128
|
+
const properties = itemset.getPropertiesExpressions(nodeElements).map((expression) => {
|
|
129
|
+
return [expression.toString(), createComputedExpression(context, expression)] as [
|
|
130
|
+
string,
|
|
131
|
+
() => string,
|
|
132
|
+
];
|
|
133
|
+
});
|
|
134
|
+
|
|
125
135
|
return {
|
|
126
136
|
label,
|
|
127
137
|
value,
|
|
138
|
+
properties,
|
|
128
139
|
};
|
|
129
140
|
});
|
|
130
141
|
});
|
|
@@ -135,7 +146,7 @@ const createItemsetItems = (
|
|
|
135
146
|
const createItemset = (
|
|
136
147
|
control: ItemCollectionControl,
|
|
137
148
|
itemset: ItemsetDefinition
|
|
138
|
-
): Accessor<readonly
|
|
149
|
+
): Accessor<readonly BaseItem[]> => {
|
|
139
150
|
return control.scope.runTask(() => {
|
|
140
151
|
const itemsetItems = createItemsetItems(control, itemset);
|
|
141
152
|
|
|
@@ -144,6 +155,9 @@ const createItemset = (
|
|
|
144
155
|
return {
|
|
145
156
|
label: item.label(),
|
|
146
157
|
value: item.value(),
|
|
158
|
+
properties: item.properties.map(
|
|
159
|
+
([propLabel, propValue]) => [propLabel, propValue()] as [string, string]
|
|
160
|
+
),
|
|
147
161
|
};
|
|
148
162
|
});
|
|
149
163
|
});
|
|
@@ -152,7 +166,7 @@ const createItemset = (
|
|
|
152
166
|
|
|
153
167
|
/**
|
|
154
168
|
* Creates a reactive computation of a {@link ItemCollectionControl}'s
|
|
155
|
-
* {@link
|
|
169
|
+
* {@link BaseItem}s, in support of the field's `valueOptions`.
|
|
156
170
|
*
|
|
157
171
|
* - The control defined with static `<item>`s will compute to an corresponding
|
|
158
172
|
* static list of items.
|
|
@@ -162,7 +176,9 @@ const createItemset = (
|
|
|
162
176
|
* their appropriate dependencies (whether relative to the itemset item node,
|
|
163
177
|
* referencing a form's `itext` translations, etc).
|
|
164
178
|
*/
|
|
165
|
-
export const createItemCollection = (
|
|
179
|
+
export const createItemCollection = (
|
|
180
|
+
control: ItemCollectionControl
|
|
181
|
+
): Accessor<readonly BaseItem[]> => {
|
|
166
182
|
const { items, itemset } = control.definition.bodyElement;
|
|
167
183
|
|
|
168
184
|
if (itemset != null) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { GeneralChildNode } from '../client/hierarchy.ts';
|
|
2
|
+
import type { Attribute } from '../instance/Attribute.ts';
|
|
1
3
|
import type { NamespaceDeclarationMap } from './names/NamespaceDeclarationMap.ts';
|
|
2
4
|
import type { QualifiedName } from './names/QualifiedName.ts';
|
|
3
5
|
|
|
@@ -78,15 +80,6 @@ export const escapeXMLText = <Text extends string>(
|
|
|
78
80
|
: (out as EscapedXMLText);
|
|
79
81
|
};
|
|
80
82
|
|
|
81
|
-
interface SerializableElementAttribute {
|
|
82
|
-
serializeAttributeXML(): string;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
interface ElementXMLSerializationOptions {
|
|
86
|
-
readonly namespaceDeclarations?: NamespaceDeclarationMap;
|
|
87
|
-
readonly attributes?: readonly SerializableElementAttribute[];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
83
|
const serializeElementNamespaceDeclarationXML = (
|
|
91
84
|
namespaceDeclarations?: NamespaceDeclarationMap
|
|
92
85
|
): string => {
|
|
@@ -103,24 +96,11 @@ const serializeElementNamespaceDeclarationXML = (
|
|
|
103
96
|
.join('');
|
|
104
97
|
};
|
|
105
98
|
|
|
106
|
-
const serializeElementAttributeXML = (
|
|
107
|
-
attributes?: readonly SerializableElementAttribute[]
|
|
108
|
-
): string => {
|
|
109
|
-
if (attributes == null) {
|
|
110
|
-
return '';
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return attributes
|
|
114
|
-
.map((attribute) => {
|
|
115
|
-
return attribute.serializeAttributeXML();
|
|
116
|
-
})
|
|
117
|
-
.join('');
|
|
118
|
-
};
|
|
119
|
-
|
|
120
99
|
const serializeElementXML = (
|
|
121
100
|
qualifiedName: QualifiedName,
|
|
122
101
|
children: string,
|
|
123
|
-
|
|
102
|
+
attributes: string,
|
|
103
|
+
namespaceDeclarations?: NamespaceDeclarationMap
|
|
124
104
|
): string => {
|
|
125
105
|
// See JSDoc for the `getPrefixedName` method. If we find we do actually need
|
|
126
106
|
// custom element (subtree) prefix resolution, we'd uncomment the argument
|
|
@@ -133,11 +113,9 @@ const serializeElementXML = (
|
|
|
133
113
|
const nodeName = qualifiedName.getPrefixedName(
|
|
134
114
|
// options.namespaceDeclarations
|
|
135
115
|
);
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const attributes = serializeElementAttributeXML(options.attributes);
|
|
140
|
-
const prefix = `<${nodeName}${namespaceDeclarations}${attributes}`;
|
|
116
|
+
const serializedNamespaceDeclarations =
|
|
117
|
+
serializeElementNamespaceDeclarationXML(namespaceDeclarations);
|
|
118
|
+
const prefix = `<${nodeName}${serializedNamespaceDeclarations}${attributes}`;
|
|
141
119
|
|
|
142
120
|
if (children === '') {
|
|
143
121
|
return `${prefix}/>`;
|
|
@@ -146,18 +124,36 @@ const serializeElementXML = (
|
|
|
146
124
|
return `${prefix}>${children}</${nodeName}>`;
|
|
147
125
|
};
|
|
148
126
|
|
|
127
|
+
export const serializeAttributeXML = (
|
|
128
|
+
qualifiedName: QualifiedName,
|
|
129
|
+
xmlValue: EscapedXMLText
|
|
130
|
+
): string => {
|
|
131
|
+
const nodeName = qualifiedName.getPrefixedName();
|
|
132
|
+
return ` ${nodeName}="${xmlValue.normalize()}"`;
|
|
133
|
+
};
|
|
134
|
+
|
|
149
135
|
export const serializeParentElementXML = (
|
|
150
136
|
qualifiedName: QualifiedName,
|
|
151
|
-
|
|
152
|
-
|
|
137
|
+
children: readonly GeneralChildNode[],
|
|
138
|
+
attributes: readonly Attribute[],
|
|
139
|
+
namespaceDeclarations?: NamespaceDeclarationMap
|
|
153
140
|
): string => {
|
|
154
|
-
|
|
141
|
+
const serializedChildren = children.map((child) => child.instanceState.instanceXML).join('');
|
|
142
|
+
const serializedAttributes = attributes
|
|
143
|
+
.map((attribute) => attribute.instanceState.instanceXML)
|
|
144
|
+
.join('');
|
|
145
|
+
return serializeElementXML(
|
|
146
|
+
qualifiedName,
|
|
147
|
+
serializedChildren,
|
|
148
|
+
serializedAttributes,
|
|
149
|
+
namespaceDeclarations
|
|
150
|
+
);
|
|
155
151
|
};
|
|
156
152
|
|
|
157
153
|
export const serializeLeafElementXML = (
|
|
158
154
|
qualifiedName: QualifiedName,
|
|
159
155
|
xmlValue: EscapedXMLText,
|
|
160
|
-
|
|
156
|
+
namespaceDeclarations?: NamespaceDeclarationMap
|
|
161
157
|
): string => {
|
|
162
|
-
return serializeElementXML(qualifiedName, xmlValue.normalize(),
|
|
158
|
+
return serializeElementXML(qualifiedName, xmlValue.normalize(), '', namespaceDeclarations);
|
|
163
159
|
};
|
|
@@ -10,9 +10,7 @@ import type { AnySelectControlDefinition } from './control/SelectControlDefiniti
|
|
|
10
10
|
import { SelectControlDefinition } from './control/SelectControlDefinition.ts';
|
|
11
11
|
import { TriggerControlDefinition } from './control/TriggerControlDefinition.ts';
|
|
12
12
|
import { UploadControlDefinition } from './control/UploadControlDefinition.ts';
|
|
13
|
-
import {
|
|
14
|
-
import { PresentationGroupDefinition } from './group/PresentationGroupDefinition.ts';
|
|
15
|
-
import { StructuralGroupDefinition } from './group/StructuralGroupDefinition.ts';
|
|
13
|
+
import { GroupElementDefinition } from './GroupElementDefinition.ts';
|
|
16
14
|
import { RepeatElementDefinition } from './RepeatElementDefinition.ts';
|
|
17
15
|
import { UnsupportedBodyElementDefinition } from './UnsupportedBodyElementDefinition.ts';
|
|
18
16
|
|
|
@@ -31,22 +29,18 @@ export type ControlElementDefinition =
|
|
|
31
29
|
| TriggerControlDefinition
|
|
32
30
|
| UploadControlDefinition;
|
|
33
31
|
|
|
32
|
+
// prettier-ignore
|
|
34
33
|
type SupportedBodyElementDefinition =
|
|
35
|
-
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
| PresentationGroupDefinition
|
|
39
|
-
| StructuralGroupDefinition
|
|
40
|
-
| ControlElementDefinition;
|
|
34
|
+
| ControlElementDefinition
|
|
35
|
+
| GroupElementDefinition
|
|
36
|
+
| RepeatElementDefinition;
|
|
41
37
|
|
|
42
38
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
39
|
type BodyElementDefinitionConstructor = new (...args: any[]) => SupportedBodyElementDefinition;
|
|
44
40
|
|
|
45
41
|
const BodyElementDefinitionConstructors = [
|
|
46
42
|
RepeatElementDefinition,
|
|
47
|
-
|
|
48
|
-
PresentationGroupDefinition,
|
|
49
|
-
StructuralGroupDefinition,
|
|
43
|
+
GroupElementDefinition,
|
|
50
44
|
InputControlDefinition,
|
|
51
45
|
SelectControlDefinition,
|
|
52
46
|
RangeControlDefinition,
|
|
@@ -63,23 +57,6 @@ export type BodyElementDefinitionArray = readonly AnyBodyElementDefinition[];
|
|
|
63
57
|
|
|
64
58
|
export type AnyBodyElementType = AnyBodyElementDefinition['type'];
|
|
65
59
|
|
|
66
|
-
export type AnyGroupElementDefinition = Extract<
|
|
67
|
-
AnyBodyElementDefinition,
|
|
68
|
-
{ readonly type: `${string}-group` }
|
|
69
|
-
>;
|
|
70
|
-
|
|
71
|
-
const isGroupElementDefinition = (
|
|
72
|
-
element: AnyBodyElementDefinition
|
|
73
|
-
): element is AnyGroupElementDefinition => {
|
|
74
|
-
return element.type.endsWith('-group');
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export const groupElementDefinition = (
|
|
78
|
-
element: AnyBodyElementDefinition
|
|
79
|
-
): AnyGroupElementDefinition | null => {
|
|
80
|
-
return isGroupElementDefinition(element) ? element : null;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
60
|
export type AnyControlElementDefinition = Extract<
|
|
84
61
|
AnyBodyElementDefinition,
|
|
85
62
|
{ readonly category: 'control' }
|
|
@@ -119,11 +96,7 @@ class BodyElementMap extends Map<BodyElementReference, AnyBodyElementDefinition>
|
|
|
119
96
|
this.mapElementsByReference(element.children);
|
|
120
97
|
}
|
|
121
98
|
|
|
122
|
-
if (
|
|
123
|
-
element instanceof LogicalGroupDefinition ||
|
|
124
|
-
element instanceof PresentationGroupDefinition ||
|
|
125
|
-
element instanceof StructuralGroupDefinition
|
|
126
|
-
) {
|
|
99
|
+
if (element instanceof GroupElementDefinition) {
|
|
127
100
|
if (reference != null) {
|
|
128
101
|
this.set(reference, element);
|
|
129
102
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { LabelDefinition } from '../text/LabelDefinition.ts';
|
|
2
|
+
import type { XFormDefinition } from '../XFormDefinition.ts';
|
|
3
|
+
import { parseNodesetReference } from '../xpath/reference-parsing.ts';
|
|
4
|
+
import type { StructureElementAppearanceDefinition } from './appearance/structureElementAppearanceParser.ts';
|
|
5
|
+
import { structureElementAppearanceParser } from './appearance/structureElementAppearanceParser.ts';
|
|
6
|
+
import {
|
|
7
|
+
type BodyElementDefinitionArray,
|
|
8
|
+
type BodyElementParentContext,
|
|
9
|
+
} from './BodyDefinition.ts';
|
|
10
|
+
import { BodyElementDefinition } from './BodyElementDefinition.ts';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* As per the spec: https://getodk.github.io/xforms-spec/#groups
|
|
14
|
+
*
|
|
15
|
+
* A group combines elements together.
|
|
16
|
+
* The group can have a label, and if so is referred to as a "presentation group".
|
|
17
|
+
* The group can have a ref, and if so is referred to as a "logical group".
|
|
18
|
+
*/
|
|
19
|
+
export class GroupElementDefinition extends BodyElementDefinition<'group'> {
|
|
20
|
+
override readonly category = 'structure';
|
|
21
|
+
override readonly type = 'group';
|
|
22
|
+
|
|
23
|
+
readonly children: BodyElementDefinitionArray;
|
|
24
|
+
|
|
25
|
+
override readonly reference: string | null;
|
|
26
|
+
readonly appearances: StructureElementAppearanceDefinition;
|
|
27
|
+
override readonly label: LabelDefinition | null;
|
|
28
|
+
|
|
29
|
+
static override isCompatible(localName: string): boolean {
|
|
30
|
+
return localName === 'group';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) {
|
|
34
|
+
super(form, parent, element);
|
|
35
|
+
|
|
36
|
+
const childElements = Array.from(element.children).filter((child) => {
|
|
37
|
+
const childName = child.localName;
|
|
38
|
+
|
|
39
|
+
return childName !== 'label';
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
this.children = this.body.getChildElementDefinitions(form, this, element, childElements);
|
|
43
|
+
this.reference = parseNodesetReference(parent, element, 'ref');
|
|
44
|
+
this.appearances = structureElementAppearanceParser.parseFrom(element, 'appearance');
|
|
45
|
+
this.label = LabelDefinition.forGroup(form, this);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import type { StaticElement } from '../../../integration/xpath/static-dom/StaticElement.ts';
|
|
1
2
|
import type { ItemsetElement } from '../../../lib/dom/query.ts';
|
|
2
3
|
import { getValueElement } from '../../../lib/dom/query.ts';
|
|
4
|
+
import { DependentExpression } from '../../expression/abstract/DependentExpression.ts';
|
|
5
|
+
import { ItemPropertyExpression } from '../../expression/ItemPropertyExpression.ts';
|
|
3
6
|
import { ItemsetNodesetExpression } from '../../expression/ItemsetNodesetExpression.ts';
|
|
4
7
|
import { ItemsetValueExpression } from '../../expression/ItemsetValueExpression.ts';
|
|
5
8
|
import { ItemsetLabelDefinition } from '../../text/ItemsetLabelDefinition.ts';
|
|
@@ -52,4 +55,8 @@ export class ItemsetDefinition extends BodyElementDefinition<'itemset'> {
|
|
|
52
55
|
this.value = new ItemsetValueExpression(this, valueExpression);
|
|
53
56
|
this.label = ItemsetLabelDefinition.from(form, this);
|
|
54
57
|
}
|
|
58
|
+
|
|
59
|
+
getPropertiesExpressions(propertiesNodes: StaticElement[]): Array<DependentExpression<'string'>> {
|
|
60
|
+
return ItemPropertyExpression.from(propertiesNodes);
|
|
61
|
+
}
|
|
55
62
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StaticElement } from '../../integration/xpath/static-dom/StaticElement.ts';
|
|
2
|
+
import { DependentExpression } from './abstract/DependentExpression.ts';
|
|
3
|
+
|
|
4
|
+
export class ItemPropertyExpression extends DependentExpression<'string'> {
|
|
5
|
+
static from(propertiesNodes: StaticElement[]) {
|
|
6
|
+
return propertiesNodes.map((node: StaticElement) => new this(node.qualifiedName.localName));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
constructor(propertyName: string) {
|
|
10
|
+
super('string', propertyName);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { XMLNS_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts';
|
|
2
|
+
import type { StaticAttribute } from '../../integration/xpath/static-dom/StaticAttribute.ts';
|
|
3
|
+
import {
|
|
4
|
+
NamespaceDeclarationMap,
|
|
5
|
+
type NamedNodeDefinition,
|
|
6
|
+
} from '../../lib/names/NamespaceDeclarationMap.ts';
|
|
7
|
+
import { QualifiedName } from '../../lib/names/QualifiedName.ts';
|
|
8
|
+
import { escapeXMLText, serializeAttributeXML } from '../../lib/xml-serialization.ts';
|
|
9
|
+
import type { BindDefinition } from './BindDefinition.ts';
|
|
10
|
+
import { NodeDefinition } from './NodeDefinition.ts';
|
|
11
|
+
import type { RootDefinition } from './RootDefinition.ts';
|
|
12
|
+
|
|
13
|
+
export class AttributeDefinition
|
|
14
|
+
extends NodeDefinition<'attribute'>
|
|
15
|
+
implements NamedNodeDefinition
|
|
16
|
+
{
|
|
17
|
+
private readonly serializedXML: string;
|
|
18
|
+
|
|
19
|
+
readonly value: string;
|
|
20
|
+
readonly type = 'attribute';
|
|
21
|
+
readonly namespaceDeclarations: NamespaceDeclarationMap;
|
|
22
|
+
readonly bodyElement = null;
|
|
23
|
+
readonly root: RootDefinition;
|
|
24
|
+
readonly isTranslated: boolean = false;
|
|
25
|
+
readonly parent = null;
|
|
26
|
+
readonly children = null;
|
|
27
|
+
readonly attributes = null;
|
|
28
|
+
|
|
29
|
+
readonly qualifiedName: QualifiedName;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
root: RootDefinition,
|
|
33
|
+
bind: BindDefinition,
|
|
34
|
+
readonly template: StaticAttribute
|
|
35
|
+
) {
|
|
36
|
+
super(bind);
|
|
37
|
+
|
|
38
|
+
const { value } = template;
|
|
39
|
+
|
|
40
|
+
this.root = root;
|
|
41
|
+
|
|
42
|
+
this.value = value;
|
|
43
|
+
this.qualifiedName = template.qualifiedName;
|
|
44
|
+
this.namespaceDeclarations = new NamespaceDeclarationMap(this);
|
|
45
|
+
|
|
46
|
+
// We serialize namespace declarations separately
|
|
47
|
+
if (this.qualifiedName.namespaceURI?.href === XMLNS_NAMESPACE_URI) {
|
|
48
|
+
this.serializedXML = '';
|
|
49
|
+
} else {
|
|
50
|
+
const xmlValue = escapeXMLText(this.value, true);
|
|
51
|
+
this.serializedXML = serializeAttributeXML(this.qualifiedName, xmlValue);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
serializeAttributeXML(): string {
|
|
56
|
+
return this.serializedXML;
|
|
57
|
+
}
|
|
58
|
+
}
|