@case-framework/survey-core 0.8.0 → 0.9.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/README.md +126 -244
- package/build/answer-types-DH0gv0Ea.d.mts +20 -0
- package/build/answer-types-DH0gv0Ea.d.mts.map +1 -0
- package/build/answer-types-Dtm6MqZQ.mjs +19 -0
- package/build/answer-types-Dtm6MqZQ.mjs.map +1 -0
- package/build/editor/colors.d.mts +11 -2
- package/build/editor/colors.d.mts.map +1 -0
- package/build/editor.d.mts +72913 -266
- package/build/editor.d.mts.map +1 -1
- package/build/editor.mjs +1268 -1001
- package/build/editor.mjs.map +1 -1
- package/build/index.d.mts +2 -3
- package/build/index.mjs +1620 -824
- package/build/index.mjs.map +1 -1
- package/build/keyed-object-D1yh_W5v.mjs +45 -0
- package/build/keyed-object-D1yh_W5v.mjs.map +1 -0
- package/build/localization-1lzNpyFX.mjs +1373 -0
- package/build/localization-1lzNpyFX.mjs.map +1 -0
- package/build/package.json +9 -3
- package/build/projection-DrLPFroE.mjs +2344 -0
- package/build/projection-DrLPFroE.mjs.map +1 -0
- package/build/registry-BOXcyWfX.d.mts +54292 -0
- package/build/registry-BOXcyWfX.d.mts.map +1 -0
- package/build/schema.d.mts +3 -0
- package/build/schema.mjs +4 -0
- package/package.json +9 -3
- package/build/index-SDbZ5WQM.d.mts +0 -2208
- package/build/index-SDbZ5WQM.d.mts.map +0 -1
- package/build/item-colors-DgoJeSCQ.d.mts +0 -12
- package/build/item-colors-DgoJeSCQ.d.mts.map +0 -1
- package/build/projection-Bl6qCinx.mjs +0 -3930
- package/build/projection-Bl6qCinx.mjs.map +0 -1
package/build/index.mjs
CHANGED
|
@@ -1,776 +1,8 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import {
|
|
1
|
+
import { $ as inspectAnnotated, A as slotPrefillSchema, B as ValueType, C as createCompositionSchema, Ct as calendarDaySchema, D as followUpBody, Dt as isCalendarDate, E as elementShape, Et as formatCalendarDate, F as richTextImageDimensionSchema, G as stringRecord, H as isResponseValue, I as richTextImageSourceSchema, J as boundField, K as annotated, L as surveyContentSchema, M as templateValueSchema, N as validationRuleSchema, O as itemEditorMetadataSchema, Ot as isCalendarDateUnit, P as jsonValueSchema, Q as extendObject, R as DurationUnits, S as conditionsShape, St as calendarDateToLocalDate, T as createSurveySchema, Tt as differenceCalendarDates, U as ownValue, V as assertResponseValue, W as setOwnValue, X as contentOwnerIds, Y as contentOwner, Z as expressionField, _ as hasRenderableRichTextBlock, _t as CALENDAR_DATE_UNITS, a as getLocalizationItems, at as expressionEditorConfigSchema, b as CURRENT_SURVEY_SCHEMA, bt as calendarDateFromLocalDate, c as listLocalizationEntries, ct as responseValueSchema, d as validateLocalizationRef, dt as remapExpression, et as ownedId, f as ContentType, ft as expressionArguments, g as getPlainTextFromRichTextContent, gt as visitExpression, h as getContentPlainText, ht as unreachable, i as getLocalizationEntryKey, it as transformAnnotated, j as slotTransformSchema, k as layoutPolicySchema, l as readLocalizationContent, lt as valueTypeSchema, m as getAssetUsagesFromContent, mt as transformExpression, n as getLocalizationCellKey, nt as remapAnnotated, o as hasLocalizationText, ot as expressionSchema, p as createRichTextContent, pt as operand, q as annotatedReferences, r as getLocalizationCoverage, rt as schemaAnnotation, s as isEmptySurveyLocalizationEntry, st as functionNameSchema, t as SURVEY_LOCALIZATION_SURFACES, tt as referenceId, u as resolveParticipantContent, ut as collectExpressionDependencies, v as hasRenderableRichTextContent, vt as addCalendarDate, w as createFlowItemSchema, wt as compareCalendarDates, x as CURRENT_SURVEY_SCHEMA_VERSION, xt as calendarDatePrecision, y as isContentEmpty, yt as calendarDateBounds, z as NumberPrecision } from "./localization-1lzNpyFX.mjs";
|
|
2
|
+
import { $ as shuffleArray, A as isAdmissibleInDraft, B as expressionIssues, C as surveyEventSchema, D as DefinitionError, E as hasPermittedChange, F as allocateResponseVariableName, G as responseValueElements, H as functionTypeIssues, I as isValidResponseVariableName, J as FUNCTION_SIGNATURES, K as responseValuesEqual, L as suggestResponseVariableName, M as getCompositionLabel, N as ResponseSlotRegistry, O as Survey, P as RESPONSE_VARIABLE_NAME_MAX_LENGTH, Q as resolveUniqueItemKey, R as ExpressionEvaluationError, S as itemResponseMetadataSchema, T as canWriteSelection, U as inferExpressionReturnType, V as expressionTypeIssues, W as invalidResponseDomainValues, X as generateCodingKey, Y as createSeededRandom, Z as generateId, _ as SlotTransformMode, a as optionIds, b as surveyResponseSchema, c as resolvedSlotEncoding, d as exportSingleResponseSlot, et as shuffleIndices, f as decodeExportColumnId, g as META_COLUMN_ORDER, h as ExportColumnSource, i as localizedExportSlots, j as surveyLocales, k as checkDocumentBudget, l as BooleanExportValues, m as EXPORTER_PROFILE_SCHEMA_VERSION, n as getDefaultProjectionDiagnostics, o as optionMapping, p as encodeExportColumnId, q as validateResponseSlotValue, r as getDefaultProjectionIssues, s as projectSlot, t as formatProjectedValue, tt as structuredCloneMethod, u as DEFAULT_TRANSFORM, v as SURVEY_RESPONSE_SCHEMA_VERSION, w as SurveyStateGraph, x as SurveyEventTypes, y as SurveyResponse, z as evaluateExpression } from "./projection-DrLPFroE.mjs";
|
|
3
|
+
import { t as strictKeyedObject } from "./keyed-object-D1yh_W5v.mjs";
|
|
3
4
|
import { format } from "date-fns/format";
|
|
4
|
-
import {
|
|
5
|
-
//#region src/to_mirgrate/data_types/legacy-types.ts
|
|
6
|
-
const isLegacyItemGroupComponent = (item) => {
|
|
7
|
-
const items = item.items;
|
|
8
|
-
return items !== void 0 && items.length > 0;
|
|
9
|
-
};
|
|
10
|
-
const isLegacySurveyGroupItem = (item) => {
|
|
11
|
-
const items = item.items;
|
|
12
|
-
return items !== void 0 && items.length > 0;
|
|
13
|
-
};
|
|
14
|
-
//#endregion
|
|
15
|
-
//#region src/survey/responses/response-meta.ts
|
|
16
|
-
const SurveyEventTypes = {
|
|
17
|
-
openSurvey: "openSurvey",
|
|
18
|
-
pageChanged: "pageChanged",
|
|
19
|
-
responseChanged: "responseChanged",
|
|
20
|
-
languageChanged: "languageChanged"
|
|
21
|
-
};
|
|
22
|
-
//#endregion
|
|
23
|
-
//#region src/expressions/item-expression-registry.ts
|
|
24
|
-
function getItemExpressionDefinition(definitions, expressionId) {
|
|
25
|
-
return definitions?.find((definition) => definition.id === expressionId);
|
|
26
|
-
}
|
|
27
|
-
function buildItemExpression(item, definitions, expressionId, params) {
|
|
28
|
-
const definition = getItemExpressionDefinition(definitions, expressionId);
|
|
29
|
-
if (!definition) throw new Error(`Unknown item expression: ${expressionId}`);
|
|
30
|
-
return definition.buildExpression(item, params);
|
|
31
|
-
}
|
|
32
|
-
//#endregion
|
|
33
|
-
//#region src/engine/active-responses.ts
|
|
34
|
-
const itemNode = (id) => JSON.stringify(["item", id]);
|
|
35
|
-
const componentNode = (id, component) => JSON.stringify([
|
|
36
|
-
"component",
|
|
37
|
-
id,
|
|
38
|
-
component
|
|
39
|
-
]);
|
|
40
|
-
const slotNode = (id) => JSON.stringify(["answer", id]);
|
|
41
|
-
function assertDeclarativeVisibility(expression) {
|
|
42
|
-
if (!expression) return;
|
|
43
|
-
if (expression.type === ExpressionType.ContextVariable) {
|
|
44
|
-
if (expression.contextType === ContextVariableType.CustomExpression) throw new Error("Visibility conditions cannot use custom callback expressions; provide a serializable custom context value instead.");
|
|
45
|
-
assertDeclarativeVisibility(expression.key);
|
|
46
|
-
}
|
|
47
|
-
if ("arguments" in expression) for (const argument of expression.arguments ?? []) assertDeclarativeVisibility(argument);
|
|
48
|
-
}
|
|
49
|
-
/** Compile the dependencies once; each evaluation reads an immutable logical session snapshot. */
|
|
50
|
-
var ActiveResponsePlan = class {
|
|
51
|
-
survey;
|
|
52
|
-
parents = /* @__PURE__ */ new Map();
|
|
53
|
-
components = /* @__PURE__ */ new Map();
|
|
54
|
-
slots;
|
|
55
|
-
constructor(survey) {
|
|
56
|
-
this.survey = survey;
|
|
57
|
-
this.slots = survey.getResponseSlotRegistry();
|
|
58
|
-
for (const item of survey.surveyItems.values()) {
|
|
59
|
-
if (item.type === "group") for (const child of item.items ?? []) this.parents.set(child, item.id);
|
|
60
|
-
for (const { slotId, componentId } of item.getConditionalResponseSlots()) this.components.set(slotId, [...this.components.get(slotId) ?? [], componentId]);
|
|
61
|
-
}
|
|
62
|
-
const graph = /* @__PURE__ */ new Map();
|
|
63
|
-
const labels = /* @__PURE__ */ new Map();
|
|
64
|
-
const refs = (expression) => expression?.responseVariableRefs.map((ref) => slotNode(ref.slotId)) ?? [];
|
|
65
|
-
for (const item of survey.surveyItems.values()) {
|
|
66
|
-
for (const expression of [item.displayConditions?.root, ...Object.values(item.displayConditions?.components ?? {})]) assertDeclarativeVisibility(expression?.serialize());
|
|
67
|
-
const label = item.metadata?.itemLabel?.trim() || item.id;
|
|
68
|
-
const parent = this.parents.get(item.id);
|
|
69
|
-
graph.set(itemNode(item.id), [...parent ? [itemNode(parent)] : [], ...refs(item.displayConditions?.root)]);
|
|
70
|
-
labels.set(itemNode(item.id), `${label} visibility`);
|
|
71
|
-
for (const [component, expression] of Object.entries(item.displayConditions?.components ?? {})) {
|
|
72
|
-
graph.set(componentNode(item.id, component), [itemNode(item.id), ...refs(expression)]);
|
|
73
|
-
labels.set(componentNode(item.id, component), `${label} (${component}) visibility`);
|
|
74
|
-
}
|
|
75
|
-
for (const slot of item.getResponseSlotDefinitions()) {
|
|
76
|
-
const activation = item.getActiveResponseDependencies(slot.slotId);
|
|
77
|
-
graph.set(slotNode(slot.slotId), [
|
|
78
|
-
itemNode(item.id),
|
|
79
|
-
...(this.components.get(slot.slotId) ?? []).map((id) => componentNode(item.id, id)),
|
|
80
|
-
...item.getComputedResponseDependencies(slot.slotId).map(slotNode),
|
|
81
|
-
...activation.slotIds.map(slotNode),
|
|
82
|
-
...activation.componentIds.map((id) => componentNode(item.id, id))
|
|
83
|
-
]);
|
|
84
|
-
labels.set(slotNode(slot.slotId), `${label} (${slot.variableName || slot.slotId})`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
const done = /* @__PURE__ */ new Set(), visiting = /* @__PURE__ */ new Set(), path = [];
|
|
88
|
-
const visit = (node) => {
|
|
89
|
-
if (visiting.has(node)) throw new Error(`Cyclic response/visibility dependency: ${[...path.slice(path.indexOf(node)), node].map((id) => labels.get(id) ?? id).join(" → ")}`);
|
|
90
|
-
if (done.has(node)) return;
|
|
91
|
-
visiting.add(node);
|
|
92
|
-
path.push(node);
|
|
93
|
-
for (const dependency of graph.get(node) ?? []) visit(dependency);
|
|
94
|
-
path.pop();
|
|
95
|
-
visiting.delete(node);
|
|
96
|
-
done.add(node);
|
|
97
|
-
};
|
|
98
|
-
for (const node of graph.keys()) visit(node);
|
|
99
|
-
}
|
|
100
|
-
evaluate(responses, context) {
|
|
101
|
-
const diagnostics = [];
|
|
102
|
-
const cache = /* @__PURE__ */ new Map();
|
|
103
|
-
const visiting = /* @__PURE__ */ new Set();
|
|
104
|
-
const resolve = (key, compute) => {
|
|
105
|
-
if (cache.has(key)) return cache.get(key);
|
|
106
|
-
if (visiting.has(key)) throw new Error(`Cyclic response/visibility dependency: ${key}`);
|
|
107
|
-
visiting.add(key);
|
|
108
|
-
try {
|
|
109
|
-
const value = compute();
|
|
110
|
-
cache.set(key, value);
|
|
111
|
-
return value;
|
|
112
|
-
} finally {
|
|
113
|
-
visiting.delete(key);
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
const condition = (itemId, expression, componentId) => {
|
|
117
|
-
if (!expression) return true;
|
|
118
|
-
const value = evaluator.eval(expression);
|
|
119
|
-
if (value !== void 0 && value.type !== ValueType.boolean) diagnostics.push({
|
|
120
|
-
code: "non-boolean-visibility-result",
|
|
121
|
-
itemId,
|
|
122
|
-
...componentId === void 0 ? {} : { componentId },
|
|
123
|
-
actualType: value.type
|
|
124
|
-
});
|
|
125
|
-
return value?.type === ValueType.boolean && value.value;
|
|
126
|
-
};
|
|
127
|
-
const isItemVisible = (id) => resolve(itemNode(id), () => {
|
|
128
|
-
const item = this.survey.surveyItems.get(id);
|
|
129
|
-
const parent = this.parents.get(id);
|
|
130
|
-
return !!item && (id === this.survey.rootItem?.id || !!parent && isItemVisible(parent)) && condition(id, item.displayConditions?.root);
|
|
131
|
-
});
|
|
132
|
-
const isComponentVisible = (id, component) => resolve(componentNode(id, component), () => isItemVisible(id) && condition(id, this.survey.surveyItems.get(id)?.displayConditions?.components?.[component], component));
|
|
133
|
-
const getAnswer = (slotId) => resolve(slotNode(slotId), () => {
|
|
134
|
-
const owner = this.slots.get(slotId);
|
|
135
|
-
if (!owner || !isItemVisible(owner.itemId) || !(this.components.get(slotId) ?? []).every((id) => isComponentVisible(owner.itemId, id))) return void 0;
|
|
136
|
-
const item = this.survey.surveyItems.get(owner.itemId);
|
|
137
|
-
if (!owner.computed) return item.getActiveResponseValue(slotId, responses[owner.itemId]?.response?.get(slotId), (id) => isComponentVisible(owner.itemId, id), getAnswer);
|
|
138
|
-
const sources = new ResponseItem();
|
|
139
|
-
for (const id of item.getComputedResponseDependencies(slotId)) sources.setSlotValue(id, getAnswer(id));
|
|
140
|
-
return item.getComputedResponseValue(slotId, sources, (id) => isComponentVisible(owner.itemId, id));
|
|
141
|
-
});
|
|
142
|
-
const evaluator = new ExpressionEvaluator({
|
|
143
|
-
responses,
|
|
144
|
-
surveyContext: context,
|
|
145
|
-
resolveResponseValue: getAnswer
|
|
146
|
-
});
|
|
147
|
-
const getItemResponse = (id) => {
|
|
148
|
-
const response = new ResponseItem();
|
|
149
|
-
for (const slot of this.survey.surveyItems.get(id)?.getResponseSlotDefinitions() ?? []) if (!slot.computed) response.setSlotValue(slot.slotId, getAnswer(slot.slotId));
|
|
150
|
-
return response.isEmpty() ? void 0 : response;
|
|
151
|
-
};
|
|
152
|
-
return {
|
|
153
|
-
evaluator,
|
|
154
|
-
getAnswer,
|
|
155
|
-
getItemResponse,
|
|
156
|
-
isItemVisible,
|
|
157
|
-
isComponentVisible,
|
|
158
|
-
getDiagnostics: () => diagnostics.map((diagnostic) => ({ ...diagnostic }))
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
//#endregion
|
|
163
|
-
//#region src/engine/engine.ts
|
|
164
|
-
var SurveyEngineCore = class SurveyEngineCore {
|
|
165
|
-
activeResponsePlan;
|
|
166
|
-
activeState;
|
|
167
|
-
responseDiagnostics = /* @__PURE__ */ new Map();
|
|
168
|
-
surveyDef;
|
|
169
|
-
renderedSurveyTree;
|
|
170
|
-
renderedItemsCache;
|
|
171
|
-
context;
|
|
172
|
-
locale;
|
|
173
|
-
sessionSeedValue;
|
|
174
|
-
responses;
|
|
175
|
-
prefills;
|
|
176
|
-
_events;
|
|
177
|
-
_openedAt;
|
|
178
|
-
dateLocales;
|
|
179
|
-
prefillEnvironment;
|
|
180
|
-
cache;
|
|
181
|
-
constructor(survey, context, prefills, dateLocales, options) {
|
|
182
|
-
this._openedAt = this._getTimestamp();
|
|
183
|
-
this.sessionSeedValue = generateId();
|
|
184
|
-
this.surveyDef = survey;
|
|
185
|
-
const { issues: configurationIssues, plan } = SurveyEngineCore.inspectConfiguration(survey);
|
|
186
|
-
if (configurationIssues.length) throw new Error(configurationIssues.join("\n"));
|
|
187
|
-
this.activeResponsePlan = plan;
|
|
188
|
-
this.context = context ? context : { locale: "en" };
|
|
189
|
-
this.locale = this.context.locale;
|
|
190
|
-
if (prefills) {
|
|
191
|
-
const registry = survey.getResponseSlotRegistry();
|
|
192
|
-
this.prefills = Object.create(null);
|
|
193
|
-
for (const [slotId, value] of Object.entries(prefills)) {
|
|
194
|
-
const owner = registry.get(slotId);
|
|
195
|
-
if (!owner || owner.computed) throw new Error(`Cannot restore unknown or computed response slot "${slotId}"`);
|
|
196
|
-
const item = survey.surveyItems.get(owner.itemId);
|
|
197
|
-
(this.prefills[owner.itemId] ??= new SurveyItemResponse({
|
|
198
|
-
itemId: item.id,
|
|
199
|
-
itemType: item.type
|
|
200
|
-
}, new ResponseItem())).response.setSlotValue(slotId, value);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
this.prefillEnvironment = options?.prefillEnvironment;
|
|
204
|
-
this.dateLocales = dateLocales || [{
|
|
205
|
-
code: "en",
|
|
206
|
-
locale: enUS
|
|
207
|
-
}];
|
|
208
|
-
this.responses = this.initResponseObject(this.surveyDef.surveyItems);
|
|
209
|
-
this.initCache();
|
|
210
|
-
if (options?.applyConfiguredPrefills !== false) this.applyConfiguredPrefills();
|
|
211
|
-
this.evalExpressions();
|
|
212
|
-
if (!survey.rootItem) throw new Error("constructor: root item not found");
|
|
213
|
-
this.renderedSurveyTree = this.renderGroup(survey.rootItem);
|
|
214
|
-
this._events = [{
|
|
215
|
-
type: SurveyEventTypes.openSurvey,
|
|
216
|
-
timestamp: this._getTimestamp(),
|
|
217
|
-
value: this.context.locale
|
|
218
|
-
}];
|
|
219
|
-
}
|
|
220
|
-
setContext(context) {
|
|
221
|
-
this.updateContext(context);
|
|
222
|
-
}
|
|
223
|
-
getDateLocales() {
|
|
224
|
-
return this.dateLocales.slice();
|
|
225
|
-
}
|
|
226
|
-
getCurrentDateLocale() {
|
|
227
|
-
const found = this.dateLocales.find((dl) => dl.code === this.locale);
|
|
228
|
-
if (!found) {
|
|
229
|
-
console.warn(`Locale '${this.locale}' is not available. Using default locale.`);
|
|
230
|
-
if (this.dateLocales.length > 0) return this.dateLocales[0].locale;
|
|
231
|
-
return enUS;
|
|
232
|
-
}
|
|
233
|
-
return found?.locale;
|
|
234
|
-
}
|
|
235
|
-
updateContext(context) {
|
|
236
|
-
const localeChanged = this.locale !== context.locale;
|
|
237
|
-
this.context = context;
|
|
238
|
-
this.locale = context.locale;
|
|
239
|
-
if (localeChanged) this._registerSurveyEvent({
|
|
240
|
-
type: SurveyEventTypes.languageChanged,
|
|
241
|
-
timestamp: this._getTimestamp(),
|
|
242
|
-
value: context.locale
|
|
243
|
-
});
|
|
244
|
-
this.evalExpressions();
|
|
245
|
-
this.reRenderSurveyTree();
|
|
246
|
-
}
|
|
247
|
-
/** Change the presentation language without losing participant/custom context. */
|
|
248
|
-
updateLocale(locale) {
|
|
249
|
-
if (this.context.locale === locale) return false;
|
|
250
|
-
this.updateContext({
|
|
251
|
-
...this.context,
|
|
252
|
-
locale
|
|
253
|
-
});
|
|
254
|
-
return true;
|
|
255
|
-
}
|
|
256
|
-
setResponse(targetKey, response) {
|
|
257
|
-
const target = this.getResponseItem(targetKey);
|
|
258
|
-
if (!target) throw new Error("setResponse: target not found for key: " + targetKey);
|
|
259
|
-
const item = this.surveyDef.surveyItems.get(targetKey);
|
|
260
|
-
const previousSlots = structuredClone(target.response?.serialize()?.slots ?? {});
|
|
261
|
-
const normalized = item.normalizeResponse(response);
|
|
262
|
-
target.response = normalized.response;
|
|
263
|
-
this.responseDiagnostics.set(targetKey, normalized.diagnostics ?? []);
|
|
264
|
-
const nextSlots = target.response?.serialize()?.slots ?? {};
|
|
265
|
-
const timestamp = this._getTimestamp();
|
|
266
|
-
const comparable = (value) => value === void 0 ? void 0 : JSON.stringify(value, Object.keys(value).sort());
|
|
267
|
-
for (const slotId of /* @__PURE__ */ new Set([...Object.keys(previousSlots), ...Object.keys(nextSlots)])) {
|
|
268
|
-
if (comparable(previousSlots[slotId]) === comparable(nextSlots[slotId])) continue;
|
|
269
|
-
this._registerSurveyEvent({
|
|
270
|
-
type: SurveyEventTypes.responseChanged,
|
|
271
|
-
timestamp,
|
|
272
|
-
slotId,
|
|
273
|
-
operation: nextSlots[slotId] === void 0 ? "clear" : "set"
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
this.evalExpressions();
|
|
277
|
-
this.reRenderSurveyTree();
|
|
278
|
-
}
|
|
279
|
-
get openedAt() {
|
|
280
|
-
return this._openedAt;
|
|
281
|
-
}
|
|
282
|
-
/** Invalid visibility results in the current evaluation; unanswered conditions are omitted. */
|
|
283
|
-
getEvaluationDiagnostics() {
|
|
284
|
-
return this.activeEvaluation().getDiagnostics();
|
|
285
|
-
}
|
|
286
|
-
/** Diagnostics from the latest initialization or response update for an item. */
|
|
287
|
-
getResponseDiagnostics(itemId) {
|
|
288
|
-
return (this.responseDiagnostics.get(itemId) ?? []).map((diagnostic) => ({ ...diagnostic }));
|
|
289
|
-
}
|
|
290
|
-
get sessionSeed() {
|
|
291
|
-
return this.sessionSeedValue;
|
|
292
|
-
}
|
|
293
|
-
get survey() {
|
|
294
|
-
return this.surveyDef;
|
|
295
|
-
}
|
|
296
|
-
getSurveyPages(size) {
|
|
297
|
-
const renderedSurvey = flattenTree(this.renderedSurveyTree);
|
|
298
|
-
const pages = new Array();
|
|
299
|
-
if (!size) size = "large";
|
|
300
|
-
let currentPage = [];
|
|
301
|
-
renderedSurvey.forEach((item) => {
|
|
302
|
-
if (item.type === "page-break") {
|
|
303
|
-
if (currentPage.length > 0) {
|
|
304
|
-
pages.push([...currentPage]);
|
|
305
|
-
currentPage = [];
|
|
306
|
-
}
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
currentPage.push(item);
|
|
310
|
-
if (!this.surveyDef.maxItemsPerPage) return;
|
|
311
|
-
let max = 0;
|
|
312
|
-
switch (size) {
|
|
313
|
-
case "large":
|
|
314
|
-
max = this.surveyDef.maxItemsPerPage.large;
|
|
315
|
-
break;
|
|
316
|
-
case "small": max = this.surveyDef.maxItemsPerPage.small;
|
|
317
|
-
}
|
|
318
|
-
if (currentPage.length >= max) {
|
|
319
|
-
pages.push([...currentPage]);
|
|
320
|
-
currentPage = [];
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
if (currentPage.length > 0) pages.push([...currentPage]);
|
|
324
|
-
return pages;
|
|
325
|
-
}
|
|
326
|
-
/** Read-only projection, with stable identity until the engine renders a new tree. */
|
|
327
|
-
getRenderedSurveyItems() {
|
|
328
|
-
if (this.renderedItemsCache?.tree !== this.renderedSurveyTree) this.renderedItemsCache = {
|
|
329
|
-
tree: this.renderedSurveyTree,
|
|
330
|
-
items: Object.freeze(flattenTree(this.renderedSurveyTree).map((item) => Object.freeze(item)))
|
|
331
|
-
};
|
|
332
|
-
return this.renderedItemsCache.items;
|
|
333
|
-
}
|
|
334
|
-
getRenderedSurveyItem(itemId) {
|
|
335
|
-
const item = this.getRenderedSurveyItems().find((item) => item.id === itemId);
|
|
336
|
-
return item ? { ...item } : void 0;
|
|
337
|
-
}
|
|
338
|
-
onPageChanged(pageIndex) {
|
|
339
|
-
this._registerSurveyEvent({
|
|
340
|
-
type: SurveyEventTypes.pageChanged,
|
|
341
|
-
timestamp: this._getTimestamp(),
|
|
342
|
-
value: pageIndex
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
getEvents() {
|
|
346
|
-
return this._events.slice();
|
|
347
|
-
}
|
|
348
|
-
/** Current session answers, including retained answers to hidden items. */
|
|
349
|
-
getResponses() {
|
|
350
|
-
return this.collectResponses(true);
|
|
351
|
-
}
|
|
352
|
-
/** Answers to currently visible items and response slots on every page; does not erase hidden session answers. */
|
|
353
|
-
getSubmissionResponses() {
|
|
354
|
-
return this.collectResponses(false);
|
|
355
|
-
}
|
|
356
|
-
/** Flat submission with events and item metadata kept separate from answer identity. */
|
|
357
|
-
getSurveyResponse(key, includeHidden = false) {
|
|
358
|
-
const response = new SurveyResponse(key);
|
|
359
|
-
for (const item of this.collectResponses(includeHidden)) response.addItemResponse(item);
|
|
360
|
-
response.evaluationContext = structuredClone({
|
|
361
|
-
locale: this.context.locale,
|
|
362
|
-
participantFlags: this.context.participantFlags
|
|
363
|
-
});
|
|
364
|
-
response.events = structuredClone(this._events);
|
|
365
|
-
response.context = this.context.customValues && structuredClone(this.context.customValues);
|
|
366
|
-
return response;
|
|
367
|
-
}
|
|
368
|
-
getAnswer(slotId) {
|
|
369
|
-
const owner = this.surveyDef.getResponseSlotRegistry().get(slotId);
|
|
370
|
-
if (!owner) throw new Error(`Unknown response slot "${slotId}"`);
|
|
371
|
-
return owner.computed ? this.getComputedResponseValue(slotId) : this.responses[owner.itemId]?.response?.get(slotId);
|
|
372
|
-
}
|
|
373
|
-
setAnswer(slotId, value) {
|
|
374
|
-
const owner = this.surveyDef.getResponseSlotRegistry().get(slotId);
|
|
375
|
-
if (!owner || owner.computed) throw new Error(`Cannot write unknown or computed response slot "${slotId}"`);
|
|
376
|
-
const response = this.responses[owner.itemId]?.response?.clone() ?? new ResponseItem();
|
|
377
|
-
response.setSlotValue(slotId, value);
|
|
378
|
-
this.setResponse(owner.itemId, response);
|
|
379
|
-
}
|
|
380
|
-
collectResponses(includeHidden) {
|
|
381
|
-
const renderedPositions = /* @__PURE__ */ new Map();
|
|
382
|
-
flattenTree(this.renderedSurveyTree).filter((item) => item.type !== "page-break").forEach((item, index) => {
|
|
383
|
-
if (this.responses[item.id]) renderedPositions.set(item.id, index);
|
|
384
|
-
});
|
|
385
|
-
const active = includeHidden ? void 0 : this.activeEvaluation();
|
|
386
|
-
const responses = [];
|
|
387
|
-
this.surveyDef.surveyItems.forEach((item, itemId) => {
|
|
388
|
-
if (!item.isInteractive()) return;
|
|
389
|
-
const response = this.getResponseItem(itemId);
|
|
390
|
-
if (!response) return;
|
|
391
|
-
const position = renderedPositions.get(itemId);
|
|
392
|
-
response.meta = position === void 0 ? void 0 : { position };
|
|
393
|
-
if (!includeHidden && position === void 0) return;
|
|
394
|
-
if (includeHidden) {
|
|
395
|
-
responses.push(response);
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
const submission = new SurveyItemResponse(response, active.getItemResponse(itemId));
|
|
399
|
-
submission.meta = response.meta && { ...response.meta };
|
|
400
|
-
responses.push(submission);
|
|
401
|
-
});
|
|
402
|
-
return responses;
|
|
403
|
-
}
|
|
404
|
-
/** Author and intrinsic validation failures, restricted to currently rendered items. */
|
|
405
|
-
getInvalidResponseItems(itemIds) {
|
|
406
|
-
const rendered = new Set(this.getRenderedSurveyItems().map((item) => item.id));
|
|
407
|
-
return [...new Set(itemIds ?? rendered)].filter((id) => rendered.has(id) && Object.values(this.getValidationValues(id) ?? {}).some((valid) => !valid));
|
|
408
|
-
}
|
|
409
|
-
getIntrinsicValidationValues(itemKey) {
|
|
410
|
-
const active = this.activeEvaluation();
|
|
411
|
-
if (!active.isItemVisible(itemKey)) return {};
|
|
412
|
-
return { ...this.surveyDef.surveyItems.get(itemKey)?.getResponseValidationValues(active.getItemResponse(itemKey), (id) => active.isComponentVisible(itemKey, id), (expression) => active.evaluator.eval(Expression.deserialize(expression))) };
|
|
413
|
-
}
|
|
414
|
-
getAuthorValidationValues(itemKey) {
|
|
415
|
-
if (!this.activeEvaluation().isItemVisible(itemKey)) return {};
|
|
416
|
-
return Object.fromEntries(Object.entries(this.cache.validations[itemKey] ?? {}).map(([key, validation]) => [key, validation.result]));
|
|
417
|
-
}
|
|
418
|
-
getDisplayConditionValue(itemKey, componentKey) {
|
|
419
|
-
if (componentKey) return this.cache.displayConditions[itemKey]?.components?.[componentKey]?.result;
|
|
420
|
-
return this.cache.displayConditions[itemKey]?.root?.result;
|
|
421
|
-
}
|
|
422
|
-
getDisabledConditionValue(itemKey, componentKey) {
|
|
423
|
-
return this.cache.disabledConditions[itemKey]?.components?.[componentKey]?.result;
|
|
424
|
-
}
|
|
425
|
-
getTemplateValue(templateValueKey) {
|
|
426
|
-
return this.cache.templateValues[templateValueKey]?.value;
|
|
427
|
-
}
|
|
428
|
-
getValidationValues(itemKey) {
|
|
429
|
-
const result = this.getIntrinsicValidationValues(itemKey);
|
|
430
|
-
for (const [key, valid] of Object.entries(this.getAuthorValidationValues(itemKey))) result[key] = valid && (result[key] ?? true);
|
|
431
|
-
return Object.keys(result).length ? result : void 0;
|
|
432
|
-
}
|
|
433
|
-
initCache() {
|
|
434
|
-
this.cache = {
|
|
435
|
-
validations: {},
|
|
436
|
-
displayConditions: {},
|
|
437
|
-
templateValues: {},
|
|
438
|
-
disabledConditions: {}
|
|
439
|
-
};
|
|
440
|
-
this.surveyDef.getTemplateValueKeys().forEach((templateKey) => {
|
|
441
|
-
this.cache.templateValues[templateKey] = {
|
|
442
|
-
value: void 0,
|
|
443
|
-
templateDef: this.surveyDef.getTemplateValue(templateKey)
|
|
444
|
-
};
|
|
445
|
-
});
|
|
446
|
-
this.surveyDef.surveyItems.forEach((item, itemId) => {
|
|
447
|
-
if (item.validations && Object.keys(item.validations).length > 0) {
|
|
448
|
-
this.cache.validations[itemId] = {};
|
|
449
|
-
Object.keys(item.validations).forEach((validationKey) => {
|
|
450
|
-
const valExp = item.validations[validationKey];
|
|
451
|
-
if (!valExp) {
|
|
452
|
-
console.warn("initCache: validation expression not found: " + itemId + " " + validationKey);
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
this.cache.validations[itemId][validationKey] = {
|
|
456
|
-
expression: valExp,
|
|
457
|
-
result: false
|
|
458
|
-
};
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
if (item.displayConditions !== void 0 && (item.displayConditions.root || item.displayConditions.components)) {
|
|
462
|
-
this.cache.displayConditions[itemId] = {};
|
|
463
|
-
if (item.displayConditions.root) this.cache.displayConditions[itemId].root = {
|
|
464
|
-
expression: item.displayConditions.root,
|
|
465
|
-
result: false
|
|
466
|
-
};
|
|
467
|
-
if (item.displayConditions.components) {
|
|
468
|
-
this.cache.displayConditions[itemId].components = {};
|
|
469
|
-
Object.keys(item.displayConditions.components).forEach((componentKey) => {
|
|
470
|
-
const compExp = item.displayConditions?.components?.[componentKey];
|
|
471
|
-
if (!compExp) {
|
|
472
|
-
console.warn("initCache: display condition component expression not found: " + itemId + " " + componentKey);
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
this.cache.displayConditions[itemId].components[componentKey] = {
|
|
476
|
-
expression: compExp,
|
|
477
|
-
result: false
|
|
478
|
-
};
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
if (item.disabledConditions !== void 0 && item.disabledConditions.components !== void 0) {
|
|
483
|
-
this.cache.disabledConditions[itemId] = { components: {} };
|
|
484
|
-
Object.keys(item.disabledConditions.components).forEach((componentKey) => {
|
|
485
|
-
const compExp = item.disabledConditions?.components?.[componentKey];
|
|
486
|
-
if (!compExp) {
|
|
487
|
-
console.warn("initCache: disabled condition component expression not found: " + itemId + " " + componentKey);
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
this.cache.disabledConditions[itemId].components[componentKey] = {
|
|
491
|
-
expression: compExp,
|
|
492
|
-
result: false
|
|
493
|
-
};
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
initResponseObject(items) {
|
|
499
|
-
const respGroup = {};
|
|
500
|
-
items.forEach((item, itemId) => {
|
|
501
|
-
if (!item.isInteractive()) return;
|
|
502
|
-
else {
|
|
503
|
-
const prefill = this.prefills?.[itemId];
|
|
504
|
-
const applyPrefill = prefill && prefill.itemType === item.type;
|
|
505
|
-
const normalized = item.normalizeResponse(applyPrefill ? prefill.response : void 0);
|
|
506
|
-
this.responseDiagnostics.set(itemId, normalized.diagnostics ?? []);
|
|
507
|
-
respGroup[itemId] = new SurveyItemResponse({
|
|
508
|
-
itemId,
|
|
509
|
-
itemType: item.type
|
|
510
|
-
}, normalized.response);
|
|
511
|
-
}
|
|
512
|
-
});
|
|
513
|
-
return respGroup;
|
|
514
|
-
}
|
|
515
|
-
applyConfiguredPrefills() {
|
|
516
|
-
const maxPasses = Math.max(this.surveyDef.surveyItems.size, 1);
|
|
517
|
-
for (let pass = 0; pass < maxPasses; pass++) {
|
|
518
|
-
this.evalTemplateValues(this.createEvaluator());
|
|
519
|
-
let changedInPass = false;
|
|
520
|
-
this.surveyDef.surveyItems.forEach((item) => {
|
|
521
|
-
if (!item.isInteractive()) return;
|
|
522
|
-
changedInPass = this.applyConfiguredPrefillsForItem(item) || changedInPass;
|
|
523
|
-
});
|
|
524
|
-
if (!changedInPass) break;
|
|
525
|
-
}
|
|
526
|
-
this.evalTemplateValues(this.createEvaluator());
|
|
527
|
-
}
|
|
528
|
-
applyConfiguredPrefillsForItem(item) {
|
|
529
|
-
const responseItem = this.responses[item.id];
|
|
530
|
-
if (!responseItem) return false;
|
|
531
|
-
let changed = false;
|
|
532
|
-
for (const prefill of item.prefills ?? []) {
|
|
533
|
-
const evalEngine = this.createEvaluator();
|
|
534
|
-
const targetSlot = item.resolvePrefillTarget(prefill.target);
|
|
535
|
-
if (!targetSlot) continue;
|
|
536
|
-
if ((prefill.apply ?? SurveyItemPrefillApplyMode.ifEmpty) === SurveyItemPrefillApplyMode.ifEmpty && responseItem.response?.get(targetSlot.slotId) !== void 0) continue;
|
|
537
|
-
if (!this.shouldApplyConfiguredPrefill(prefill, evalEngine)) continue;
|
|
538
|
-
const resolvedValue = this.resolveConfiguredPrefillValue(prefill, targetSlot, evalEngine);
|
|
539
|
-
if (!resolvedValue) continue;
|
|
540
|
-
const normalizedValue = item.normalizePrefillValue(prefill, targetSlot, resolvedValue);
|
|
541
|
-
if (!normalizedValue) continue;
|
|
542
|
-
const nextResponse = responseItem.response?.clone() ?? new ResponseItem();
|
|
543
|
-
nextResponse.setSlotValue(targetSlot.slotId, normalizedValue);
|
|
544
|
-
const normalized = item.normalizeResponse(nextResponse);
|
|
545
|
-
this.responseDiagnostics.set(item.id, normalized.diagnostics ?? []);
|
|
546
|
-
const next = normalized.response?.isEmpty() ? void 0 : normalized.response;
|
|
547
|
-
if (JSON.stringify(responseItem.response?.serialize()) !== JSON.stringify(next?.serialize())) changed = true;
|
|
548
|
-
responseItem.response = next;
|
|
549
|
-
}
|
|
550
|
-
return changed;
|
|
551
|
-
}
|
|
552
|
-
shouldApplyConfiguredPrefill(prefill, evalEngine) {
|
|
553
|
-
if (!prefill.when) return true;
|
|
554
|
-
const resolved = evalEngine.eval(prefill.when);
|
|
555
|
-
return resolved?.type === ValueType.boolean && resolved.value === true;
|
|
556
|
-
}
|
|
557
|
-
resolveConfiguredPrefillValue(prefill, _target, evalEngine) {
|
|
558
|
-
switch (prefill.source.type) {
|
|
559
|
-
case "static": return prefill.source.value;
|
|
560
|
-
case "expression": return evalEngine.eval(prefill.source.expression);
|
|
561
|
-
case "templateValue": return this.getTemplateValue(prefill.source.key);
|
|
562
|
-
case "previousResponse": {
|
|
563
|
-
if (!this.prefillEnvironment?.getPreviousResponse) return;
|
|
564
|
-
const resolved = this.prefillEnvironment.getPreviousResponse({
|
|
565
|
-
surveyKey: prefill.source.ref.surveyKey,
|
|
566
|
-
slotId: prefill.source.ref.slotId
|
|
567
|
-
});
|
|
568
|
-
if (!resolved) return;
|
|
569
|
-
return resolved;
|
|
570
|
-
}
|
|
571
|
-
default: return;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
shouldRender(itemId, componentId) {
|
|
575
|
-
const displayConditionResult = this.getDisplayConditionValue(itemId, componentId);
|
|
576
|
-
if (displayConditionResult !== void 0) return displayConditionResult;
|
|
577
|
-
return true;
|
|
578
|
-
}
|
|
579
|
-
sequentialRender(groupDef) {
|
|
580
|
-
const newItems = [];
|
|
581
|
-
for (const itemId of groupDef.items || []) {
|
|
582
|
-
if (!this.shouldRender(itemId)) continue;
|
|
583
|
-
const itemDef = this.surveyDef.surveyItems.get(itemId);
|
|
584
|
-
if (!itemDef) {
|
|
585
|
-
console.warn("sequentialRender: item not found: " + itemId);
|
|
586
|
-
continue;
|
|
587
|
-
}
|
|
588
|
-
if (itemDef.type === "group") {
|
|
589
|
-
newItems.push(this.renderGroup(itemDef));
|
|
590
|
-
continue;
|
|
591
|
-
}
|
|
592
|
-
newItems.push({
|
|
593
|
-
id: itemId,
|
|
594
|
-
type: itemDef.type
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
return {
|
|
598
|
-
id: groupDef.id,
|
|
599
|
-
type: "group",
|
|
600
|
-
items: newItems
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
randomizedItemRender(groupDef, parent) {
|
|
604
|
-
const newItems = parent.items?.filter((rItem) => this.shouldRender(rItem.id)) || [];
|
|
605
|
-
const itemIds = groupDef.items || [];
|
|
606
|
-
const shuffledIndices = shuffleIndices(itemIds.length);
|
|
607
|
-
for (const index of shuffledIndices) {
|
|
608
|
-
const itemId = itemIds[index];
|
|
609
|
-
if (parent.items?.find((rItem) => rItem.id === itemId)) continue;
|
|
610
|
-
if (!this.shouldRender(itemId)) continue;
|
|
611
|
-
const itemDef = this.surveyDef.surveyItems.get(itemId);
|
|
612
|
-
if (!itemDef) {
|
|
613
|
-
console.warn("randomizedItemRender: item not found: " + itemId);
|
|
614
|
-
continue;
|
|
615
|
-
}
|
|
616
|
-
if (itemDef.type === "group") {
|
|
617
|
-
newItems.push(this.renderGroup(itemDef, parent));
|
|
618
|
-
continue;
|
|
619
|
-
}
|
|
620
|
-
newItems.push({
|
|
621
|
-
id: itemId,
|
|
622
|
-
type: itemDef.type
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
return {
|
|
626
|
-
id: groupDef.id,
|
|
627
|
-
type: "group",
|
|
628
|
-
items: newItems
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
renderGroup(groupDef, parent) {
|
|
632
|
-
if (!this.activeEvaluation().isItemVisible(groupDef.id)) return {
|
|
633
|
-
id: groupDef.id,
|
|
634
|
-
type: groupDef.type,
|
|
635
|
-
items: []
|
|
636
|
-
};
|
|
637
|
-
if (!parent) parent = {
|
|
638
|
-
id: groupDef.id,
|
|
639
|
-
type: "group",
|
|
640
|
-
items: []
|
|
641
|
-
};
|
|
642
|
-
if (groupDef.config.shuffleItems) return this.randomizedItemRender(groupDef, parent);
|
|
643
|
-
return this.sequentialRender(groupDef);
|
|
644
|
-
}
|
|
645
|
-
reRenderSurveyTree() {
|
|
646
|
-
if (!this.surveyDef.rootItem) throw new Error("reRenderSurveyTree: root item not found");
|
|
647
|
-
this.renderedSurveyTree = this.renderGroup(this.surveyDef.rootItem);
|
|
648
|
-
}
|
|
649
|
-
_registerSurveyEvent(event) {
|
|
650
|
-
this._events.push(event);
|
|
651
|
-
}
|
|
652
|
-
_getTimestamp() {
|
|
653
|
-
return Math.floor(Date.now() / 1e3);
|
|
654
|
-
}
|
|
655
|
-
getResponseItem(itemId) {
|
|
656
|
-
return this.responses[itemId];
|
|
657
|
-
}
|
|
658
|
-
/** Resolve computed references against current answers and conditions, with cycle detection. */
|
|
659
|
-
getComputedResponseValue(slotId) {
|
|
660
|
-
return this.activeEvaluation().getAnswer(slotId);
|
|
661
|
-
}
|
|
662
|
-
/** Evaluate against the same current responses/context used by conditions and prefills. */
|
|
663
|
-
evaluateExpression(expression) {
|
|
664
|
-
return this.activeEvaluation().evaluator.eval(Expression.deserialize(expression));
|
|
665
|
-
}
|
|
666
|
-
activeEvaluation() {
|
|
667
|
-
return this.activeState ??= this.activeResponsePlan.evaluate(this.responses, this.context);
|
|
668
|
-
}
|
|
669
|
-
createEvaluator() {
|
|
670
|
-
return this.activeResponsePlan.evaluate(this.responses, this.context).evaluator;
|
|
671
|
-
}
|
|
672
|
-
/** Active answer used by conditions and scoring; hidden session values remain in getAnswer(). */
|
|
673
|
-
getActiveAnswer(slotId) {
|
|
674
|
-
return this.activeEvaluation().getAnswer(slotId);
|
|
675
|
-
}
|
|
676
|
-
/** Validate static runtime contracts without constructing a response session. */
|
|
677
|
-
static getConfigurationIssues(survey) {
|
|
678
|
-
return SurveyEngineCore.inspectConfiguration(survey).issues;
|
|
679
|
-
}
|
|
680
|
-
static inspectConfiguration(survey) {
|
|
681
|
-
const issues = [...survey.surveyItems.values()].flatMap((item) => item.getExecutionConfigurationIssues().map((issue) => `${(item.metadata?.itemLabel ?? "")?.trim() || item.id}: ${issue}`));
|
|
682
|
-
let plan;
|
|
683
|
-
try {
|
|
684
|
-
plan = new ActiveResponsePlan(survey);
|
|
685
|
-
} catch (error) {
|
|
686
|
-
issues.push(error instanceof Error ? error.message : String(error));
|
|
687
|
-
}
|
|
688
|
-
issues.push(...survey.getResponseSlotRegistry().diagnostics.map((diagnostic) => diagnostic.message));
|
|
689
|
-
issues.push(...survey.getBlockingExpressionIssues());
|
|
690
|
-
return {
|
|
691
|
-
issues,
|
|
692
|
-
plan
|
|
693
|
-
};
|
|
694
|
-
}
|
|
695
|
-
evalExpressions() {
|
|
696
|
-
this.activeState = this.activeResponsePlan.evaluate(this.responses, this.context);
|
|
697
|
-
const evalEngine = this.activeState.evaluator;
|
|
698
|
-
this.evalTemplateValues(evalEngine);
|
|
699
|
-
this.evalDisplayConditions();
|
|
700
|
-
this.evalDisableConditions(evalEngine);
|
|
701
|
-
this.evalValidations(evalEngine);
|
|
702
|
-
}
|
|
703
|
-
evalTemplateValues(evalEngine) {
|
|
704
|
-
Object.keys(this.cache.templateValues).forEach((templateValueKey) => {
|
|
705
|
-
const templateValue = this.cache.templateValues[templateValueKey];
|
|
706
|
-
this.cache.templateValues[templateValueKey].value = void 0;
|
|
707
|
-
if (!templateValue.templateDef?.expression) {
|
|
708
|
-
console.warn("evalTemplateValues: template value expression not found: " + templateValueKey);
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
let evaluatedValue = evalEngine.eval(templateValue.templateDef.expression);
|
|
712
|
-
if (evaluatedValue === void 0) {
|
|
713
|
-
console.warn("evalTemplateValues: template value expression returned undefined: " + templateValueKey);
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
switch (templateValue.templateDef.type) {
|
|
717
|
-
case "date2string":
|
|
718
|
-
if (evaluatedValue.type !== ValueType.date) {
|
|
719
|
-
console.warn("evalTemplateValues: template value expression returned non-date value: " + templateValueKey);
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
evaluatedValue = {
|
|
723
|
-
type: ValueType.string,
|
|
724
|
-
value: formatCalendarDate(evaluatedValue.value, templateValue.templateDef.dateFormat)
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
this.cache.templateValues[templateValueKey].value = evaluatedValue;
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
evalDisplayConditions() {
|
|
731
|
-
const active = this.activeEvaluation();
|
|
732
|
-
for (const [itemId, conditions] of Object.entries(this.cache.displayConditions)) {
|
|
733
|
-
if (conditions.root) conditions.root.result = active.isItemVisible(itemId);
|
|
734
|
-
for (const [id, condition] of Object.entries(conditions.components ?? {})) condition.result = active.isComponentVisible(itemId, id);
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
evalDisableConditions(evalEngine) {
|
|
738
|
-
Object.keys(this.cache.disabledConditions).forEach((itemKey) => {
|
|
739
|
-
const disableCondition = this.cache.disabledConditions[itemKey];
|
|
740
|
-
if (disableCondition.components) Object.keys(disableCondition.components).forEach((componentKey) => {
|
|
741
|
-
const resolvedValue = evalEngine.eval(disableCondition.components[componentKey].expression);
|
|
742
|
-
if (resolvedValue === void 0 || resolvedValue.type !== ValueType.boolean) {
|
|
743
|
-
console.warn("evalDisableConditions: disable condition component expression returned undefined: " + itemKey + "." + componentKey);
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
this.cache.disabledConditions[itemKey].components[componentKey].result = resolvedValue.value;
|
|
747
|
-
});
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
evalValidations(evalEngine) {
|
|
751
|
-
Object.keys(this.cache.validations).forEach((itemId) => {
|
|
752
|
-
const validation = this.cache.validations[itemId];
|
|
753
|
-
Object.keys(validation).forEach((validationKey) => {
|
|
754
|
-
const resolvedValue = evalEngine.eval(validation[validationKey].expression);
|
|
755
|
-
if (resolvedValue === void 0 || resolvedValue.type !== ValueType.boolean) {
|
|
756
|
-
console.warn("evalValidations: validation expression returned undefined: " + itemId + "." + validationKey);
|
|
757
|
-
this.cache.validations[itemId][validationKey].result = false;
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
this.cache.validations[itemId][validationKey].result = resolvedValue.value;
|
|
761
|
-
});
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
};
|
|
765
|
-
const flattenTree = (itemTree) => {
|
|
766
|
-
const flatTree = new Array();
|
|
767
|
-
itemTree.items?.forEach((item) => {
|
|
768
|
-
if (item.type === "group") flatTree.push(...flattenTree(item));
|
|
769
|
-
else flatTree.push({ ...item });
|
|
770
|
-
});
|
|
771
|
-
return flatTree;
|
|
772
|
-
};
|
|
773
|
-
//#endregion
|
|
5
|
+
import { z } from "zod";
|
|
774
6
|
//#region src/json-patch.ts
|
|
775
7
|
var JsonPatchError = class extends Error {
|
|
776
8
|
code;
|
|
@@ -956,15 +188,1551 @@ const applyJsonPatch = (document, operations) => {
|
|
|
956
188
|
return patchedDocument;
|
|
957
189
|
};
|
|
958
190
|
//#endregion
|
|
959
|
-
//#region src/
|
|
960
|
-
/**
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
191
|
+
//#region src/expressions/shortcuts/selection.ts
|
|
192
|
+
/**
|
|
193
|
+
* Build a selected-option test joined with `not(ownerState.disabled)` for that option.
|
|
194
|
+
* `multiple` chooses membership instead of equality. The result is an ordinary expression tree;
|
|
195
|
+
* no shortcut node is persisted or handled specially by the evaluator.
|
|
196
|
+
*/
|
|
197
|
+
function selectedAndEnabled(selection, optionId, multiple) {
|
|
198
|
+
return {
|
|
199
|
+
type: "function",
|
|
200
|
+
functionName: "and",
|
|
201
|
+
arguments: [{
|
|
202
|
+
type: "function",
|
|
203
|
+
functionName: multiple ? "list_contains" : "eq",
|
|
204
|
+
arguments: [selection, {
|
|
205
|
+
type: "const",
|
|
206
|
+
value: {
|
|
207
|
+
type: "reference",
|
|
208
|
+
value: optionId
|
|
209
|
+
}
|
|
210
|
+
}]
|
|
211
|
+
}, {
|
|
212
|
+
type: "function",
|
|
213
|
+
functionName: "not",
|
|
214
|
+
arguments: [{
|
|
215
|
+
type: "ownerState",
|
|
216
|
+
state: "disabled",
|
|
217
|
+
targetRef: { ownerId: optionId }
|
|
218
|
+
}]
|
|
219
|
+
}]
|
|
220
|
+
};
|
|
966
221
|
}
|
|
967
222
|
//#endregion
|
|
223
|
+
//#region src/survey/utils/group-utils.ts
|
|
224
|
+
/**
|
|
225
|
+
* Shared utilities for array operations.
|
|
226
|
+
*/
|
|
227
|
+
/**
|
|
228
|
+
* Swap two elements in an array by index.
|
|
229
|
+
* @returns A new array with the elements swapped
|
|
230
|
+
*/
|
|
231
|
+
function swapArrayElements(arr, fromIndex, toIndex) {
|
|
232
|
+
const result = [...arr];
|
|
233
|
+
[result[fromIndex], result[toIndex]] = [result[toIndex], result[fromIndex]];
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Move an element from one index to another.
|
|
238
|
+
* @returns A new array with the element moved
|
|
239
|
+
*/
|
|
240
|
+
function moveArrayElement(arr, fromIndex, toIndex) {
|
|
241
|
+
const result = [...arr];
|
|
242
|
+
const [moved] = result.splice(fromIndex, 1);
|
|
243
|
+
result.splice(toIndex, 0, moved);
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
//#endregion
|
|
247
|
+
//#region src/expressions/editors/expression-editor.ts
|
|
248
|
+
const constantNode = (value, editorConfig) => {
|
|
249
|
+
const node = expressionSchema.parse({
|
|
250
|
+
type: "const",
|
|
251
|
+
value,
|
|
252
|
+
editorConfig
|
|
253
|
+
});
|
|
254
|
+
if (node.type !== "const") throw new Error("Expected constant node");
|
|
255
|
+
return node;
|
|
256
|
+
};
|
|
257
|
+
/** Editor-state builders produce the canonical JSON tree, never runtime wrappers. */
|
|
258
|
+
const createFunctionExpression = (functionName, args, editorConfig) => ({
|
|
259
|
+
type: "function",
|
|
260
|
+
functionName,
|
|
261
|
+
arguments: expressionArguments(args),
|
|
262
|
+
editorConfig
|
|
263
|
+
});
|
|
264
|
+
const contextNode = (contextType, key, args, asType, editorConfig) => ({
|
|
265
|
+
type: "contextVariable",
|
|
266
|
+
contextType,
|
|
267
|
+
...key ? { key } : {},
|
|
268
|
+
...args ? { arguments: expressionArguments(args) } : {},
|
|
269
|
+
asType,
|
|
270
|
+
editorConfig
|
|
271
|
+
});
|
|
272
|
+
var ExpressionEditor = class {
|
|
273
|
+
returnType;
|
|
274
|
+
_editorConfig;
|
|
275
|
+
get editorConfig() {
|
|
276
|
+
return this._editorConfig;
|
|
277
|
+
}
|
|
278
|
+
withEditorConfig(editorConfig) {
|
|
279
|
+
this._editorConfig = editorConfig;
|
|
280
|
+
return this;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
var ConstStringArrayEditor = class extends ExpressionEditor {
|
|
284
|
+
returnType = ValueType.stringArray;
|
|
285
|
+
_values;
|
|
286
|
+
constructor(values, editorConfig) {
|
|
287
|
+
super();
|
|
288
|
+
this._values = values;
|
|
289
|
+
this._editorConfig = editorConfig;
|
|
290
|
+
}
|
|
291
|
+
get values() {
|
|
292
|
+
return this._values;
|
|
293
|
+
}
|
|
294
|
+
set values(values) {
|
|
295
|
+
this._values = values;
|
|
296
|
+
}
|
|
297
|
+
getExpression() {
|
|
298
|
+
return constantNode({
|
|
299
|
+
type: ValueType.stringArray,
|
|
300
|
+
value: this._values
|
|
301
|
+
}, this._editorConfig);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
var ConstStringEditor = class extends ExpressionEditor {
|
|
305
|
+
returnType = ValueType.string;
|
|
306
|
+
_value;
|
|
307
|
+
constructor(value, editorConfig) {
|
|
308
|
+
super();
|
|
309
|
+
this._value = value;
|
|
310
|
+
this._editorConfig = editorConfig;
|
|
311
|
+
}
|
|
312
|
+
get value() {
|
|
313
|
+
return this._value;
|
|
314
|
+
}
|
|
315
|
+
set value(value) {
|
|
316
|
+
this._value = value;
|
|
317
|
+
}
|
|
318
|
+
getExpression() {
|
|
319
|
+
return constantNode({
|
|
320
|
+
type: ValueType.string,
|
|
321
|
+
value: this._value
|
|
322
|
+
}, this._editorConfig);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
var ConstNumberEditor = class extends ExpressionEditor {
|
|
326
|
+
returnType = ValueType.number;
|
|
327
|
+
_value;
|
|
328
|
+
constructor(value, editorConfig) {
|
|
329
|
+
super();
|
|
330
|
+
this._value = value;
|
|
331
|
+
this._editorConfig = editorConfig;
|
|
332
|
+
}
|
|
333
|
+
get value() {
|
|
334
|
+
return this._value;
|
|
335
|
+
}
|
|
336
|
+
set value(value) {
|
|
337
|
+
this._value = value;
|
|
338
|
+
}
|
|
339
|
+
getExpression() {
|
|
340
|
+
return constantNode({
|
|
341
|
+
type: ValueType.number,
|
|
342
|
+
value: this._value
|
|
343
|
+
}, this._editorConfig);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
var ConstBooleanEditor = class extends ExpressionEditor {
|
|
347
|
+
returnType = ValueType.boolean;
|
|
348
|
+
_value;
|
|
349
|
+
constructor(value, editorConfig) {
|
|
350
|
+
super();
|
|
351
|
+
this._value = value;
|
|
352
|
+
this._editorConfig = editorConfig;
|
|
353
|
+
}
|
|
354
|
+
get value() {
|
|
355
|
+
return this._value;
|
|
356
|
+
}
|
|
357
|
+
set value(value) {
|
|
358
|
+
this._value = value;
|
|
359
|
+
}
|
|
360
|
+
getExpression() {
|
|
361
|
+
return constantNode({
|
|
362
|
+
type: ValueType.boolean,
|
|
363
|
+
value: this._value
|
|
364
|
+
}, this._editorConfig);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
var ConstDateEditor = class extends ExpressionEditor {
|
|
368
|
+
returnType = ValueType.date;
|
|
369
|
+
_value;
|
|
370
|
+
constructor(value, editorConfig) {
|
|
371
|
+
super();
|
|
372
|
+
this._value = value;
|
|
373
|
+
this._editorConfig = editorConfig;
|
|
374
|
+
}
|
|
375
|
+
get value() {
|
|
376
|
+
return this._value;
|
|
377
|
+
}
|
|
378
|
+
set value(value) {
|
|
379
|
+
this._value = value;
|
|
380
|
+
}
|
|
381
|
+
getExpression() {
|
|
382
|
+
const value = this._value;
|
|
383
|
+
return constantNode({
|
|
384
|
+
type: ValueType.date,
|
|
385
|
+
value
|
|
386
|
+
}, this._editorConfig);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
var ConstNumberArrayEditor = class extends ExpressionEditor {
|
|
390
|
+
returnType = ValueType.numberArray;
|
|
391
|
+
_values;
|
|
392
|
+
constructor(values, editorConfig) {
|
|
393
|
+
super();
|
|
394
|
+
this._values = values;
|
|
395
|
+
this._editorConfig = editorConfig;
|
|
396
|
+
}
|
|
397
|
+
get values() {
|
|
398
|
+
return this._values;
|
|
399
|
+
}
|
|
400
|
+
set values(values) {
|
|
401
|
+
this._values = values;
|
|
402
|
+
}
|
|
403
|
+
getExpression() {
|
|
404
|
+
return constantNode({
|
|
405
|
+
type: ValueType.numberArray,
|
|
406
|
+
value: this._values
|
|
407
|
+
}, this._editorConfig);
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
var ConstDateArrayEditor = class extends ExpressionEditor {
|
|
411
|
+
returnType = ValueType.dateArray;
|
|
412
|
+
_values;
|
|
413
|
+
constructor(values, editorConfig) {
|
|
414
|
+
super();
|
|
415
|
+
this._values = values;
|
|
416
|
+
this._editorConfig = editorConfig;
|
|
417
|
+
}
|
|
418
|
+
get values() {
|
|
419
|
+
return this._values;
|
|
420
|
+
}
|
|
421
|
+
set values(values) {
|
|
422
|
+
this._values = values;
|
|
423
|
+
}
|
|
424
|
+
getExpression() {
|
|
425
|
+
const value = this._values;
|
|
426
|
+
return constantNode({
|
|
427
|
+
type: ValueType.dateArray,
|
|
428
|
+
value
|
|
429
|
+
}, this._editorConfig);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
var ResponseVariableEditor = class extends ExpressionEditor {
|
|
433
|
+
variableRef;
|
|
434
|
+
constructor(variableRef, variableType, editorConfig) {
|
|
435
|
+
super();
|
|
436
|
+
this.variableRef = variableRef;
|
|
437
|
+
this._editorConfig = editorConfig;
|
|
438
|
+
this.returnType = variableRef.method === "isDefined" ? ValueType.boolean : variableType;
|
|
439
|
+
}
|
|
440
|
+
getExpression() {
|
|
441
|
+
return {
|
|
442
|
+
type: "responseVariable",
|
|
443
|
+
variableRef: { ...this.variableRef },
|
|
444
|
+
editorConfig: this._editorConfig
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
var DisabledStateEditor = class extends ExpressionEditor {
|
|
449
|
+
ownerId;
|
|
450
|
+
returnType = ValueType.boolean;
|
|
451
|
+
constructor(ownerId, editorConfig) {
|
|
452
|
+
super();
|
|
453
|
+
this.ownerId = ownerId;
|
|
454
|
+
this._editorConfig = editorConfig;
|
|
455
|
+
}
|
|
456
|
+
getExpression() {
|
|
457
|
+
return {
|
|
458
|
+
type: "ownerState",
|
|
459
|
+
state: "disabled",
|
|
460
|
+
targetRef: { ownerId: this.ownerId },
|
|
461
|
+
editorConfig: this._editorConfig
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
var ContextVariableEditor = class extends ExpressionEditor {
|
|
466
|
+
_contextType;
|
|
467
|
+
_key;
|
|
468
|
+
_args;
|
|
469
|
+
_asType;
|
|
470
|
+
constructor(contextType, key, args, asType, editorConfig) {
|
|
471
|
+
super();
|
|
472
|
+
this._contextType = contextType;
|
|
473
|
+
this._key = key;
|
|
474
|
+
this._args = args;
|
|
475
|
+
this._asType = asType;
|
|
476
|
+
this._editorConfig = editorConfig;
|
|
477
|
+
}
|
|
478
|
+
getExpression() {
|
|
479
|
+
return contextNode(this._contextType, this._key?.getExpression(), this._args?.map((arg) => arg.getExpression()), this._asType, this._editorConfig);
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
var CtxLocaleEditor = class extends ContextVariableEditor {
|
|
483
|
+
returnType = ValueType.string;
|
|
484
|
+
constructor(editorConfig) {
|
|
485
|
+
super("locale", void 0, void 0, ValueType.string, editorConfig);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
var CtxCurrentDateEditor = class extends ContextVariableEditor {
|
|
489
|
+
returnType = ValueType.date;
|
|
490
|
+
constructor(editorConfig) {
|
|
491
|
+
super("currentDate", void 0, void 0, ValueType.date, editorConfig);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
var CtxPFlagIsDefinedEditor = class extends ContextVariableEditor {
|
|
495
|
+
returnType = ValueType.boolean;
|
|
496
|
+
constructor(key, editorConfig) {
|
|
497
|
+
super("participantFlag", key, void 0, ValueType.boolean, editorConfig);
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
var CtxPFlagStringEditor = class extends ContextVariableEditor {
|
|
501
|
+
returnType = ValueType.string;
|
|
502
|
+
constructor(key, editorConfig) {
|
|
503
|
+
super("participantFlag", key, void 0, ValueType.string, editorConfig);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
var CtxPFlagNumEditor = class extends ContextVariableEditor {
|
|
507
|
+
returnType = ValueType.number;
|
|
508
|
+
constructor(key, editorConfig) {
|
|
509
|
+
super("participantFlag", key, void 0, ValueType.number, editorConfig);
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
var CtxPFlagDateEditor = class extends ContextVariableEditor {
|
|
513
|
+
returnType = ValueType.date;
|
|
514
|
+
constructor(key, editorConfig) {
|
|
515
|
+
super("participantFlag", key, void 0, ValueType.date, editorConfig);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
var CtxCustomValueEditor = class extends ContextVariableEditor {
|
|
519
|
+
constructor(key, asType, editorConfig) {
|
|
520
|
+
super("customValue", key, void 0, asType, editorConfig);
|
|
521
|
+
this.returnType = asType;
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
var GroupExpressionEditor = class extends ExpressionEditor {
|
|
525
|
+
_args;
|
|
526
|
+
constructor(args, editorConfig) {
|
|
527
|
+
super();
|
|
528
|
+
this._args = args;
|
|
529
|
+
this._editorConfig = editorConfig;
|
|
530
|
+
}
|
|
531
|
+
get args() {
|
|
532
|
+
return this._args;
|
|
533
|
+
}
|
|
534
|
+
addArg(arg, position) {
|
|
535
|
+
if (position === void 0) this._args.push(arg);
|
|
536
|
+
else this._args.splice(position, 0, arg);
|
|
537
|
+
}
|
|
538
|
+
removeArg(position) {
|
|
539
|
+
this._args.splice(position, 1);
|
|
540
|
+
}
|
|
541
|
+
replaceArg(position, arg) {
|
|
542
|
+
this._args[position] = arg;
|
|
543
|
+
}
|
|
544
|
+
swapArgs(activeIndex, overIndex) {
|
|
545
|
+
this._args = moveArrayElement(this._args, activeIndex, overIndex);
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
var AndExpressionEditor = class extends GroupExpressionEditor {
|
|
549
|
+
returnType = ValueType.boolean;
|
|
550
|
+
constructor(args, editorConfig) {
|
|
551
|
+
if (args.some((arg) => arg.returnType !== ValueType.boolean)) throw new Error("And expression editor must have boolean arguments");
|
|
552
|
+
super(args, editorConfig);
|
|
553
|
+
}
|
|
554
|
+
getExpression() {
|
|
555
|
+
return createFunctionExpression("and", this.args.map((arg) => arg.getExpression()), this._editorConfig);
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
var OrExpressionEditor = class extends GroupExpressionEditor {
|
|
559
|
+
returnType = ValueType.boolean;
|
|
560
|
+
constructor(args, editorConfig) {
|
|
561
|
+
if (args.some((arg) => arg.returnType !== ValueType.boolean)) throw new Error("Or expression editor must have boolean arguments");
|
|
562
|
+
super(args, editorConfig);
|
|
563
|
+
}
|
|
564
|
+
getExpression() {
|
|
565
|
+
return createFunctionExpression("or", this.args.map((arg) => arg.getExpression()), this._editorConfig);
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
var StrListContainsExpressionEditor = class extends ExpressionEditor {
|
|
569
|
+
returnType = ValueType.boolean;
|
|
570
|
+
_list;
|
|
571
|
+
_item;
|
|
572
|
+
constructor(list, item, editorConfig) {
|
|
573
|
+
super();
|
|
574
|
+
if (list.returnType !== ValueType.stringArray) throw new Error("List contains expression editor must have a string array list");
|
|
575
|
+
if (item.returnType !== ValueType.string) throw new Error("List contains expression editor must have a string item");
|
|
576
|
+
this._list = list;
|
|
577
|
+
this._item = item;
|
|
578
|
+
this._editorConfig = editorConfig;
|
|
579
|
+
}
|
|
580
|
+
get list() {
|
|
581
|
+
return this._list;
|
|
582
|
+
}
|
|
583
|
+
get item() {
|
|
584
|
+
return this._item;
|
|
585
|
+
}
|
|
586
|
+
set list(list) {
|
|
587
|
+
this._list = list;
|
|
588
|
+
}
|
|
589
|
+
set item(item) {
|
|
590
|
+
this._item = item;
|
|
591
|
+
}
|
|
592
|
+
getExpression() {
|
|
593
|
+
return createFunctionExpression("list_contains", [this._list?.getExpression(), this._item?.getExpression()], this._editorConfig);
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
var StrEqExpressionEditor = class extends ExpressionEditor {
|
|
597
|
+
returnType = ValueType.boolean;
|
|
598
|
+
_a;
|
|
599
|
+
_b;
|
|
600
|
+
constructor(a, b, editorConfig) {
|
|
601
|
+
super();
|
|
602
|
+
this._a = a;
|
|
603
|
+
this._b = b;
|
|
604
|
+
this._editorConfig = editorConfig;
|
|
605
|
+
}
|
|
606
|
+
get a() {
|
|
607
|
+
return this._a;
|
|
608
|
+
}
|
|
609
|
+
get b() {
|
|
610
|
+
return this._b;
|
|
611
|
+
}
|
|
612
|
+
set a(a) {
|
|
613
|
+
this._a = a;
|
|
614
|
+
}
|
|
615
|
+
set b(b) {
|
|
616
|
+
this._b = b;
|
|
617
|
+
}
|
|
618
|
+
getExpression() {
|
|
619
|
+
return createFunctionExpression("str_eq", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
var EqExpressionEditor = class extends ExpressionEditor {
|
|
623
|
+
returnType = ValueType.boolean;
|
|
624
|
+
_a;
|
|
625
|
+
_b;
|
|
626
|
+
constructor(a, b, editorConfig) {
|
|
627
|
+
super();
|
|
628
|
+
this._a = a;
|
|
629
|
+
this._b = b;
|
|
630
|
+
this._editorConfig = editorConfig;
|
|
631
|
+
}
|
|
632
|
+
get a() {
|
|
633
|
+
return this._a;
|
|
634
|
+
}
|
|
635
|
+
get b() {
|
|
636
|
+
return this._b;
|
|
637
|
+
}
|
|
638
|
+
set a(a) {
|
|
639
|
+
this._a = a;
|
|
640
|
+
}
|
|
641
|
+
set b(b) {
|
|
642
|
+
this._b = b;
|
|
643
|
+
}
|
|
644
|
+
getExpression() {
|
|
645
|
+
return createFunctionExpression("eq", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
var GtExpressionEditor = class extends ExpressionEditor {
|
|
649
|
+
returnType = ValueType.boolean;
|
|
650
|
+
_a;
|
|
651
|
+
_b;
|
|
652
|
+
constructor(a, b, editorConfig) {
|
|
653
|
+
super();
|
|
654
|
+
this._a = a;
|
|
655
|
+
this._b = b;
|
|
656
|
+
this._editorConfig = editorConfig;
|
|
657
|
+
}
|
|
658
|
+
get a() {
|
|
659
|
+
return this._a;
|
|
660
|
+
}
|
|
661
|
+
get b() {
|
|
662
|
+
return this._b;
|
|
663
|
+
}
|
|
664
|
+
set a(a) {
|
|
665
|
+
this._a = a;
|
|
666
|
+
}
|
|
667
|
+
set b(b) {
|
|
668
|
+
this._b = b;
|
|
669
|
+
}
|
|
670
|
+
getExpression() {
|
|
671
|
+
return createFunctionExpression("gt", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
var GteExpressionEditor = class extends ExpressionEditor {
|
|
675
|
+
returnType = ValueType.boolean;
|
|
676
|
+
_a;
|
|
677
|
+
_b;
|
|
678
|
+
constructor(a, b, editorConfig) {
|
|
679
|
+
super();
|
|
680
|
+
this._a = a;
|
|
681
|
+
this._b = b;
|
|
682
|
+
this._editorConfig = editorConfig;
|
|
683
|
+
}
|
|
684
|
+
get a() {
|
|
685
|
+
return this._a;
|
|
686
|
+
}
|
|
687
|
+
get b() {
|
|
688
|
+
return this._b;
|
|
689
|
+
}
|
|
690
|
+
set a(a) {
|
|
691
|
+
this._a = a;
|
|
692
|
+
}
|
|
693
|
+
set b(b) {
|
|
694
|
+
this._b = b;
|
|
695
|
+
}
|
|
696
|
+
getExpression() {
|
|
697
|
+
return createFunctionExpression("gte", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
var LtExpressionEditor = class extends ExpressionEditor {
|
|
701
|
+
returnType = ValueType.boolean;
|
|
702
|
+
_a;
|
|
703
|
+
_b;
|
|
704
|
+
constructor(a, b, editorConfig) {
|
|
705
|
+
super();
|
|
706
|
+
this._a = a;
|
|
707
|
+
this._b = b;
|
|
708
|
+
this._editorConfig = editorConfig;
|
|
709
|
+
}
|
|
710
|
+
get a() {
|
|
711
|
+
return this._a;
|
|
712
|
+
}
|
|
713
|
+
get b() {
|
|
714
|
+
return this._b;
|
|
715
|
+
}
|
|
716
|
+
set a(a) {
|
|
717
|
+
this._a = a;
|
|
718
|
+
}
|
|
719
|
+
set b(b) {
|
|
720
|
+
this._b = b;
|
|
721
|
+
}
|
|
722
|
+
getExpression() {
|
|
723
|
+
return createFunctionExpression("lt", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
var LteExpressionEditor = class extends ExpressionEditor {
|
|
727
|
+
returnType = ValueType.boolean;
|
|
728
|
+
_a;
|
|
729
|
+
_b;
|
|
730
|
+
constructor(a, b, editorConfig) {
|
|
731
|
+
super();
|
|
732
|
+
this._a = a;
|
|
733
|
+
this._b = b;
|
|
734
|
+
this._editorConfig = editorConfig;
|
|
735
|
+
}
|
|
736
|
+
get a() {
|
|
737
|
+
return this._a;
|
|
738
|
+
}
|
|
739
|
+
get b() {
|
|
740
|
+
return this._b;
|
|
741
|
+
}
|
|
742
|
+
set b(b) {
|
|
743
|
+
this._b = b;
|
|
744
|
+
}
|
|
745
|
+
set a(a) {
|
|
746
|
+
this._a = a;
|
|
747
|
+
}
|
|
748
|
+
getExpression() {
|
|
749
|
+
return createFunctionExpression("lte", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
var InRangeExpressionEditor = class extends ExpressionEditor {
|
|
753
|
+
returnType = ValueType.boolean;
|
|
754
|
+
value;
|
|
755
|
+
min;
|
|
756
|
+
max;
|
|
757
|
+
inclusive;
|
|
758
|
+
constructor(value, min, max, inclusive, editorConfig) {
|
|
759
|
+
super();
|
|
760
|
+
this.value = value;
|
|
761
|
+
this.min = min;
|
|
762
|
+
this.max = max;
|
|
763
|
+
this.inclusive = inclusive;
|
|
764
|
+
this._editorConfig = editorConfig;
|
|
765
|
+
}
|
|
766
|
+
getExpression() {
|
|
767
|
+
return createFunctionExpression("in_range", [
|
|
768
|
+
this.value?.getExpression(),
|
|
769
|
+
this.min?.getExpression(),
|
|
770
|
+
this.max?.getExpression(),
|
|
771
|
+
this.inclusive?.getExpression()
|
|
772
|
+
], this._editorConfig);
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
var SumExpressionEditor = class extends GroupExpressionEditor {
|
|
776
|
+
returnType = ValueType.number;
|
|
777
|
+
constructor(args, editorConfig) {
|
|
778
|
+
super(args, editorConfig);
|
|
779
|
+
}
|
|
780
|
+
getExpression() {
|
|
781
|
+
return createFunctionExpression("sum", this.args.map((arg) => arg.getExpression()), this._editorConfig);
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
var MinExpressionEditor = class extends GroupExpressionEditor {
|
|
785
|
+
returnType = ValueType.number;
|
|
786
|
+
constructor(args, editorConfig) {
|
|
787
|
+
super(args, editorConfig);
|
|
788
|
+
}
|
|
789
|
+
getExpression() {
|
|
790
|
+
return createFunctionExpression("min", this.args.map((arg) => arg.getExpression()), this._editorConfig);
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
var MaxExpressionEditor = class extends GroupExpressionEditor {
|
|
794
|
+
returnType = ValueType.number;
|
|
795
|
+
constructor(args, editorConfig) {
|
|
796
|
+
super(args, editorConfig);
|
|
797
|
+
}
|
|
798
|
+
getExpression() {
|
|
799
|
+
return createFunctionExpression("max", this.args.map((arg) => arg.getExpression()), this._editorConfig);
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
//#endregion
|
|
803
|
+
//#region src/expressions/editors/expression-editor-generators.ts
|
|
804
|
+
const const_string_array = (...values) => {
|
|
805
|
+
return new ConstStringArrayEditor(values);
|
|
806
|
+
};
|
|
807
|
+
const const_string = (value) => {
|
|
808
|
+
return new ConstStringEditor(value);
|
|
809
|
+
};
|
|
810
|
+
const const_number_array = (...values) => {
|
|
811
|
+
return new ConstNumberArrayEditor(values);
|
|
812
|
+
};
|
|
813
|
+
const const_number = (value) => {
|
|
814
|
+
return new ConstNumberEditor(value);
|
|
815
|
+
};
|
|
816
|
+
const const_boolean = (value) => {
|
|
817
|
+
return new ConstBooleanEditor(value);
|
|
818
|
+
};
|
|
819
|
+
const const_date = (value) => {
|
|
820
|
+
return new ConstDateEditor(value);
|
|
821
|
+
};
|
|
822
|
+
const const_date_array = (...values) => {
|
|
823
|
+
return new ConstDateArrayEditor(values);
|
|
824
|
+
};
|
|
825
|
+
const response_string = (valueRef) => {
|
|
826
|
+
return new ResponseVariableEditor(valueRef, ValueType.string);
|
|
827
|
+
};
|
|
828
|
+
const response_string_array = (valueRef) => {
|
|
829
|
+
return new ResponseVariableEditor(valueRef, ValueType.stringArray);
|
|
830
|
+
};
|
|
831
|
+
const response_number = (valueRef) => {
|
|
832
|
+
return new ResponseVariableEditor(valueRef, ValueType.number);
|
|
833
|
+
};
|
|
834
|
+
const response_boolean = (valueRef) => {
|
|
835
|
+
return new ResponseVariableEditor(valueRef, ValueType.boolean);
|
|
836
|
+
};
|
|
837
|
+
const response_date = (valueRef) => {
|
|
838
|
+
return new ResponseVariableEditor(valueRef, ValueType.date);
|
|
839
|
+
};
|
|
840
|
+
const response_number_array = (valueRef) => {
|
|
841
|
+
return new ResponseVariableEditor(valueRef, ValueType.numberArray);
|
|
842
|
+
};
|
|
843
|
+
const response_date_array = (valueRef) => {
|
|
844
|
+
return new ResponseVariableEditor(valueRef, ValueType.dateArray);
|
|
845
|
+
};
|
|
846
|
+
const ctx_locale = () => {
|
|
847
|
+
return new CtxLocaleEditor();
|
|
848
|
+
};
|
|
849
|
+
const ctx_current_date = () => {
|
|
850
|
+
return new CtxCurrentDateEditor();
|
|
851
|
+
};
|
|
852
|
+
const ctx_pflag_is_defined = (key) => {
|
|
853
|
+
return new CtxPFlagIsDefinedEditor(key);
|
|
854
|
+
};
|
|
855
|
+
const ctx_pflag_string = (key) => {
|
|
856
|
+
return new CtxPFlagStringEditor(key);
|
|
857
|
+
};
|
|
858
|
+
const ctx_pflag_num = (key) => {
|
|
859
|
+
return new CtxPFlagNumEditor(key);
|
|
860
|
+
};
|
|
861
|
+
const ctx_pflag_date = (key) => {
|
|
862
|
+
return new CtxPFlagDateEditor(key);
|
|
863
|
+
};
|
|
864
|
+
const ctx_custom_value = (key, dType) => {
|
|
865
|
+
return new CtxCustomValueEditor(key, dType);
|
|
866
|
+
};
|
|
867
|
+
const and = (...args) => {
|
|
868
|
+
return new AndExpressionEditor(args);
|
|
869
|
+
};
|
|
870
|
+
const or = (...args) => {
|
|
871
|
+
return new OrExpressionEditor(args);
|
|
872
|
+
};
|
|
873
|
+
const str_list_contains = (list, item) => {
|
|
874
|
+
return new StrListContainsExpressionEditor(list, item);
|
|
875
|
+
};
|
|
876
|
+
/** Number of list entries, or of entries that also appear in `filter` (e.g. listed options). */
|
|
877
|
+
const list_count = (list, filter) => new FunctionCallEditor("list_count", filter ? [list, filter] : [list], ValueType.number);
|
|
878
|
+
/** `then` when the condition is true, otherwise `otherwise` (missing when omitted). */
|
|
879
|
+
const if_else = (condition, then, otherwise) => new FunctionCallEditor("if", otherwise ? [
|
|
880
|
+
condition,
|
|
881
|
+
then,
|
|
882
|
+
otherwise
|
|
883
|
+
] : [condition, then], then.returnType ?? otherwise?.returnType);
|
|
884
|
+
const str_eq = (a, b) => {
|
|
885
|
+
return new StrEqExpressionEditor(a, b);
|
|
886
|
+
};
|
|
887
|
+
const eq = (a, b) => {
|
|
888
|
+
return new EqExpressionEditor(a, b);
|
|
889
|
+
};
|
|
890
|
+
const gt = (a, b) => {
|
|
891
|
+
return new GtExpressionEditor(a, b);
|
|
892
|
+
};
|
|
893
|
+
const gte = (a, b) => {
|
|
894
|
+
return new GteExpressionEditor(a, b);
|
|
895
|
+
};
|
|
896
|
+
const lt = (a, b) => {
|
|
897
|
+
return new LtExpressionEditor(a, b);
|
|
898
|
+
};
|
|
899
|
+
const lte = (a, b) => {
|
|
900
|
+
return new LteExpressionEditor(a, b);
|
|
901
|
+
};
|
|
902
|
+
const in_range = (value, min, max, inclusive) => {
|
|
903
|
+
return new InRangeExpressionEditor(value, min, max, inclusive);
|
|
904
|
+
};
|
|
905
|
+
const sum = (...args) => {
|
|
906
|
+
return new SumExpressionEditor(args);
|
|
907
|
+
};
|
|
908
|
+
const min = (...args) => {
|
|
909
|
+
return new MinExpressionEditor(args);
|
|
910
|
+
};
|
|
911
|
+
const max = (...args) => {
|
|
912
|
+
return new MaxExpressionEditor(args);
|
|
913
|
+
};
|
|
914
|
+
var FunctionCallEditor = class extends ExpressionEditor {
|
|
915
|
+
name;
|
|
916
|
+
args;
|
|
917
|
+
returnType;
|
|
918
|
+
constructor(name, args, returnType) {
|
|
919
|
+
super();
|
|
920
|
+
this.name = name;
|
|
921
|
+
this.args = args;
|
|
922
|
+
this.returnType = returnType;
|
|
923
|
+
}
|
|
924
|
+
getExpression() {
|
|
925
|
+
return createFunctionExpression(this.name, this.args.map((arg) => arg.getExpression()), this.editorConfig);
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
const date_eq = (a, b) => new FunctionCallEditor("date_eq", [a, b], ValueType.boolean);
|
|
929
|
+
const date_gt = (a, b) => new FunctionCallEditor("date_gt", [a, b], ValueType.boolean);
|
|
930
|
+
const date_gte = (a, b) => new FunctionCallEditor("date_gte", [a, b], ValueType.boolean);
|
|
931
|
+
const date_lt = (a, b) => new FunctionCallEditor("date_lt", [a, b], ValueType.boolean);
|
|
932
|
+
const date_lte = (a, b) => new FunctionCallEditor("date_lte", [a, b], ValueType.boolean);
|
|
933
|
+
const date_add_days = (a, b) => new FunctionCallEditor("date_add_days", [a, b], ValueType.date);
|
|
934
|
+
const date_add_months = (a, b) => new FunctionCallEditor("date_add_months", [a, b], ValueType.date);
|
|
935
|
+
const date_add_years = (a, b) => new FunctionCallEditor("date_add_years", [a, b], ValueType.date);
|
|
936
|
+
/** Signed completed units in a - b, e.g. age as `date_diff(ctx_current_date(), birth, "years")`. */
|
|
937
|
+
const date_diff = (a, b, unit) => new FunctionCallEditor("date_diff", [
|
|
938
|
+
a,
|
|
939
|
+
b,
|
|
940
|
+
const_string(unit)
|
|
941
|
+
], ValueType.number);
|
|
942
|
+
const date_min = (...args) => new FunctionCallEditor("date_min", args, ValueType.date);
|
|
943
|
+
const date_max = (...args) => new FunctionCallEditor("date_max", args, ValueType.date);
|
|
944
|
+
const is_disabled = (ownerId) => new DisabledStateEditor(ownerId);
|
|
945
|
+
//#endregion
|
|
946
|
+
//#region src/survey/composition/registry.ts
|
|
947
|
+
function defineElement(definition) {
|
|
948
|
+
const schema = contentOwner(z.strictObject({
|
|
949
|
+
...elementShape,
|
|
950
|
+
elementType: z.literal(definition.elementType),
|
|
951
|
+
config: definition.config.nonoptional()
|
|
952
|
+
}), definition.roles ?? []);
|
|
953
|
+
return {
|
|
954
|
+
elementType: definition.elementType,
|
|
955
|
+
labelByLocale: definition.labelByLocale,
|
|
956
|
+
schema,
|
|
957
|
+
inspect(input) {
|
|
958
|
+
const node = schema.parse(input);
|
|
959
|
+
return {
|
|
960
|
+
slots: [],
|
|
961
|
+
bindings: [],
|
|
962
|
+
followUps: [],
|
|
963
|
+
fullWidth: false,
|
|
964
|
+
minWidthRem: 12,
|
|
965
|
+
componentParents: {},
|
|
966
|
+
...definition.facts?.(node.config, node.id)
|
|
967
|
+
};
|
|
968
|
+
},
|
|
969
|
+
compute(input, slotId, read) {
|
|
970
|
+
return definition.compute?.(schema.parse(input).config, slotId, read);
|
|
971
|
+
},
|
|
972
|
+
validate(input, read, evaluate) {
|
|
973
|
+
const node = schema.parse(input);
|
|
974
|
+
return definition.validate?.(node.config, node.id, read, evaluate) ?? [];
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
/** A registry is finalized once. Its one recursive schema is shared by built-in and custom bodies. */
|
|
979
|
+
function createElementRegistry(create) {
|
|
980
|
+
const definitions = create(z.lazy(() => composition));
|
|
981
|
+
const byType = /* @__PURE__ */ new Map();
|
|
982
|
+
for (const definition of definitions) {
|
|
983
|
+
if (byType.has(definition.elementType)) throw new Error(`Duplicate element registration: ${definition.elementType}`);
|
|
984
|
+
byType.set(definition.elementType, definition);
|
|
985
|
+
}
|
|
986
|
+
const variants = definitions.map((definition) => definition.schema);
|
|
987
|
+
const element = variants.length ? z.union(variants) : z.never();
|
|
988
|
+
const composition = createCompositionSchema(element);
|
|
989
|
+
return {
|
|
990
|
+
definitions,
|
|
991
|
+
labels: new Map(definitions.map((definition) => [definition.elementType, definition.labelByLocale ?? {}])),
|
|
992
|
+
elementSchema: element,
|
|
993
|
+
bodySchema: composition,
|
|
994
|
+
surveySchema: createSurveySchema(composition),
|
|
995
|
+
get(elementType) {
|
|
996
|
+
const definition = byType.get(elementType);
|
|
997
|
+
if (!definition) throw new Error(`Unknown element type: ${elementType}`);
|
|
998
|
+
return definition;
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
//#endregion
|
|
1003
|
+
//#region src/survey/composition/semantics.ts
|
|
1004
|
+
/** Structural context is independent of an element's explicit accessible label. */
|
|
1005
|
+
function getElementSemanticContext(survey, elementId) {
|
|
1006
|
+
const owner = survey.owners.get(elementId);
|
|
1007
|
+
if (!owner) throw new Error(`Unknown element '${elementId}'`);
|
|
1008
|
+
let ancestor = owner.parentId ? survey.owners.get(owner.parentId) : void 0;
|
|
1009
|
+
while (ancestor && ancestor.followUpTriggerId === owner.followUpTriggerId) {
|
|
1010
|
+
if (ancestor.kind === "section" || ancestor.kind === "item") return ancestor;
|
|
1011
|
+
ancestor = ancestor.parentId ? survey.owners.get(ancestor.parentId) : void 0;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
function resolveElementName(survey, elementId, locales) {
|
|
1015
|
+
const owner = survey.owners.get(elementId);
|
|
1016
|
+
if (!owner) throw new Error(`Unknown element '${elementId}'`);
|
|
1017
|
+
const name = (ownerId, role) => {
|
|
1018
|
+
if (!survey.owners.get(ownerId)?.roles.some((entry) => entry.role === role)) return void 0;
|
|
1019
|
+
const resolved = resolveParticipantContent(survey, ownerId, role, locales);
|
|
1020
|
+
const text = getContentPlainText(resolved?.content).trim();
|
|
1021
|
+
return text && resolved ? {
|
|
1022
|
+
ownerId,
|
|
1023
|
+
role,
|
|
1024
|
+
text,
|
|
1025
|
+
content: resolved.content,
|
|
1026
|
+
locale: resolved.locale
|
|
1027
|
+
} : void 0;
|
|
1028
|
+
};
|
|
1029
|
+
const own = name(elementId, "label");
|
|
1030
|
+
if (own) return own;
|
|
1031
|
+
let ancestor = owner.parentId ? survey.owners.get(owner.parentId) : void 0;
|
|
1032
|
+
while (ancestor) {
|
|
1033
|
+
const current = ancestor;
|
|
1034
|
+
if ([...survey.elements.values()].filter((element) => {
|
|
1035
|
+
const candidate = survey.owners.get(element.node.id);
|
|
1036
|
+
return element.facts.bindings.length > 0 && candidate.followUpTriggerId === owner.followUpTriggerId && current.path.every((part, index) => candidate.path[index] === part);
|
|
1037
|
+
}).length === 1) {
|
|
1038
|
+
const inherited = name(ancestor.id, "title") ?? name(ancestor.id, "label");
|
|
1039
|
+
if (inherited) return inherited;
|
|
1040
|
+
}
|
|
1041
|
+
if (ancestor.kind === "section" || ancestor.id === owner.followUpTriggerId) return void 0;
|
|
1042
|
+
ancestor = ancestor.parentId ? survey.owners.get(ancestor.parentId) : void 0;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
//#endregion
|
|
1046
|
+
//#region src/engine/validation.ts
|
|
1047
|
+
function validateSurveyAnswers(plan, answers, context, options) {
|
|
1048
|
+
const snapshot = plan.evaluate({
|
|
1049
|
+
answers,
|
|
1050
|
+
context
|
|
1051
|
+
});
|
|
1052
|
+
const evaluationDiagnostics = [];
|
|
1053
|
+
const eligible = (slotId) => {
|
|
1054
|
+
const binding = plan.bindings.get(slotId);
|
|
1055
|
+
return !!binding && hasPermittedChange(binding, snapshot);
|
|
1056
|
+
};
|
|
1057
|
+
const evaluate = (expression, path) => {
|
|
1058
|
+
try {
|
|
1059
|
+
return evaluateExpression(expression, {
|
|
1060
|
+
...context,
|
|
1061
|
+
answer: snapshot.answer,
|
|
1062
|
+
disabled: snapshot.disabled
|
|
1063
|
+
});
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
if (!(error instanceof ExpressionEvaluationError)) throw error;
|
|
1066
|
+
evaluationDiagnostics.push({
|
|
1067
|
+
code: "validation-evaluation",
|
|
1068
|
+
severity: "warning",
|
|
1069
|
+
path,
|
|
1070
|
+
message: error.message
|
|
1071
|
+
});
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
const issues = [];
|
|
1076
|
+
const authorWarnings = [];
|
|
1077
|
+
const active = (slotId) => {
|
|
1078
|
+
const binding = plan.bindings.get(slotId);
|
|
1079
|
+
return !!binding && [binding.controlId, ...binding.visibilityIds ?? []].every(snapshot.visible);
|
|
1080
|
+
};
|
|
1081
|
+
const included = (itemId) => !options.itemIds || options.itemIds.includes(itemId);
|
|
1082
|
+
for (const element of plan.document.elements.values()) {
|
|
1083
|
+
if (!included(element.itemId)) continue;
|
|
1084
|
+
for (const binding of element.facts.bindings) {
|
|
1085
|
+
if (!binding.selection || !active(binding.slotId) || [binding.controlId, ...binding.visibilityIds ?? []].some(snapshot.disabled)) continue;
|
|
1086
|
+
if (!hasPermittedChange(binding, snapshot)) authorWarnings.push({
|
|
1087
|
+
ownerId: binding.controlId,
|
|
1088
|
+
code: "non-interactive-choice",
|
|
1089
|
+
message: "Option locks, visibility, or the selection limit leave this choice with no permitted participant change. Its constraints will not block progress; its active answer is still submitted."
|
|
1090
|
+
});
|
|
1091
|
+
if (binding.selection.multiple) {
|
|
1092
|
+
const answer = snapshot.answer(binding.slotId);
|
|
1093
|
+
if (answer?.type === "reference[]") for (const optionId of answer.value.filter(snapshot.disabled)) authorWarnings.push({
|
|
1094
|
+
ownerId: optionId,
|
|
1095
|
+
code: "locked-checkbox-selection",
|
|
1096
|
+
message: "This selected checkbox is now locked and cannot be unticked. It remains an active, submitted answer. Use visibility if the answer should stop applying."
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
for (const issue of plan.document.registry.get(element.node.elementType).validate(element.node, snapshot.answer, (expression) => evaluate(expression, element.path))) if (eligible(issue.slotId)) issues.push(issue);
|
|
1101
|
+
else if (active(issue.slotId)) authorWarnings.push({
|
|
1102
|
+
ownerId: issue.messageOwnerId ?? plan.bindings.get(issue.slotId).controlId,
|
|
1103
|
+
code: "locked-input-constraint",
|
|
1104
|
+
message: `The ${issue.code} constraint is not satisfied, but this input currently permits no participant change. It will not block progress; its active answer is still submitted.`
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
for (const item of plan.document.data.surveyItems) {
|
|
1108
|
+
if (item.itemType !== "content" || !included(item.id)) continue;
|
|
1109
|
+
for (const rule of item.config.validationRules ?? []) {
|
|
1110
|
+
if (rule.phase === "submit" && options.phase !== "submit") continue;
|
|
1111
|
+
const targets = rule.targets.filter(eligible);
|
|
1112
|
+
if (!rule.targets.filter(active).length) continue;
|
|
1113
|
+
if (rule.when) {
|
|
1114
|
+
const result = evaluate(rule.when, [...plan.document.owners.get(rule.id).path, "when"]);
|
|
1115
|
+
if (result?.type !== "boolean" || !result.value) continue;
|
|
1116
|
+
}
|
|
1117
|
+
const result = evaluate(rule.expression, [...plan.document.owners.get(rule.id).path, "expression"]);
|
|
1118
|
+
if (result?.type !== "boolean" || !result.value) {
|
|
1119
|
+
issues.push(...targets.map((slotId) => ({
|
|
1120
|
+
slotId,
|
|
1121
|
+
code: "rule",
|
|
1122
|
+
messageOwnerId: rule.id
|
|
1123
|
+
})));
|
|
1124
|
+
if (!targets.length) authorWarnings.push({
|
|
1125
|
+
ownerId: rule.id,
|
|
1126
|
+
code: "locked-rule-targets",
|
|
1127
|
+
message: "This applicable rule fails, but none of its active targets permits a participant change. It will not block progress. Check the rule and disabling conditions."
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
return {
|
|
1133
|
+
issues,
|
|
1134
|
+
diagnostics: [...snapshot.diagnostics, ...evaluationDiagnostics],
|
|
1135
|
+
authorWarnings
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
const authoringDependencyPlans = /* @__PURE__ */ new WeakMap();
|
|
1139
|
+
const staticWarnings = /* @__PURE__ */ new WeakMap();
|
|
1140
|
+
/** Conservative proofs only: dynamic conditions and prefills are checked in a real preview. */
|
|
1141
|
+
function staticConstraintWarnings(survey, plan) {
|
|
1142
|
+
const literal = (expression, value) => expression?.type === "const" && expression.value?.type === "boolean" && expression.value.value === value;
|
|
1143
|
+
const parents = (id) => {
|
|
1144
|
+
const owner = survey.owners.get(id);
|
|
1145
|
+
return [...owner.parentId ? [owner.parentId] : [], ...owner.elementId ? ownValue(survey.elements.get(owner.elementId)?.facts.componentParents, id) ?? [] : []];
|
|
1146
|
+
};
|
|
1147
|
+
const visible = (id) => {
|
|
1148
|
+
const owner = survey.owners.get(id);
|
|
1149
|
+
return !owner.followUpTriggerId && (!owner.visibility || literal(owner.visibility, true)) && parents(id).every(visible);
|
|
1150
|
+
};
|
|
1151
|
+
const hidden = (id) => literal(survey.owners.get(id).visibility, false) || parents(id).some(hidden);
|
|
1152
|
+
const disabled = (id) => literal(survey.owners.get(id).disabled, true) || parents(id).some(disabled);
|
|
1153
|
+
const locked = (slotId) => {
|
|
1154
|
+
const binding = plan.bindings.get(slotId);
|
|
1155
|
+
if (!binding) return false;
|
|
1156
|
+
const controls = [binding.controlId, ...binding.visibilityIds ?? []];
|
|
1157
|
+
return controls.every(visible) && (controls.some(disabled) || !!binding.selection?.options.every((id) => hidden(id) || disabled(id)));
|
|
1158
|
+
};
|
|
1159
|
+
const warnings = [];
|
|
1160
|
+
for (const element of survey.elements.values()) {
|
|
1161
|
+
if (!element.facts.bindings.some((binding) => locked(binding.slotId))) continue;
|
|
1162
|
+
for (const issue of survey.registry.get(element.node.elementType).validate(element.node, () => void 0, () => void 0)) if ((issue.code === "required" || issue.code === "mustBeTrue") && locked(issue.slotId) && !ownValue(element.node.prefills, issue.slotId)?.length) warnings.push({
|
|
1163
|
+
ownerId: issue.messageOwnerId ?? plan.bindings.get(issue.slotId).controlId,
|
|
1164
|
+
code: "locked-input-constraint",
|
|
1165
|
+
message: `The ${issue.code} constraint starts unsatisfied on an input with no permitted participant change and no prefill. It will not block progress. Check the requirement and disabling conditions.`
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
for (const item of survey.data.surveyItems) {
|
|
1169
|
+
if (item.itemType !== "content") continue;
|
|
1170
|
+
for (const rule of item.config.validationRules ?? []) {
|
|
1171
|
+
if (!literal(rule.expression, false) || rule.when && !literal(rule.when, true)) continue;
|
|
1172
|
+
if (rule.targets.length && rule.targets.every(locked)) warnings.push({
|
|
1173
|
+
ownerId: rule.id,
|
|
1174
|
+
code: "locked-rule-targets",
|
|
1175
|
+
message: "This rule always fails, but all its targets are locked. It will not block progress. Check the rule and disabling conditions."
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return warnings;
|
|
1180
|
+
}
|
|
1181
|
+
function getAuthoringWarnings(survey, locale) {
|
|
1182
|
+
const warnings = [];
|
|
1183
|
+
const itemsWithElements = new Set([...survey.elements.values()].map((element) => element.itemId));
|
|
1184
|
+
for (const item of survey.data.surveyItems) if (item.itemType === "content" && !itemsWithElements.has(item.id)) warnings.push({
|
|
1185
|
+
ownerId: item.id,
|
|
1186
|
+
code: "empty-content-item",
|
|
1187
|
+
message: "Add an element to this empty item, or remove the item."
|
|
1188
|
+
});
|
|
1189
|
+
for (const owner of survey.owners.values()) for (const role of owner.roles.filter((role) => role.recommended)) if (!hasLocalizationText(readLocalizationContent(survey, {
|
|
1190
|
+
scope: "content",
|
|
1191
|
+
ownerId: owner.id,
|
|
1192
|
+
role: role.role
|
|
1193
|
+
}, locale))) warnings.push({
|
|
1194
|
+
ownerId: owner.id,
|
|
1195
|
+
code: "missing-content",
|
|
1196
|
+
message: `Add ${role.role} to make this structure understandable`
|
|
1197
|
+
});
|
|
1198
|
+
let plan = authoringDependencyPlans.get(survey.elements);
|
|
1199
|
+
if (!plan) {
|
|
1200
|
+
plan = new SurveyStateGraph(survey, { admission: "draft" });
|
|
1201
|
+
authoringDependencyPlans.set(survey.elements, plan);
|
|
1202
|
+
}
|
|
1203
|
+
let conflicts = staticWarnings.get(survey.elements);
|
|
1204
|
+
if (!conflicts) {
|
|
1205
|
+
conflicts = staticConstraintWarnings(survey, plan);
|
|
1206
|
+
staticWarnings.set(survey.elements, conflicts);
|
|
1207
|
+
}
|
|
1208
|
+
warnings.push(...conflicts);
|
|
1209
|
+
for (const item of survey.data.surveyItems) {
|
|
1210
|
+
if (item.itemType !== "content") continue;
|
|
1211
|
+
for (const rule of item.config.validationRules ?? []) {
|
|
1212
|
+
if (rule.phase === "submit") continue;
|
|
1213
|
+
if ([rule.expression, ...rule.when ? [rule.when] : []].flatMap((expression) => [...plan.storedDependencies(expression)]).some((id) => {
|
|
1214
|
+
const dependencyItem = survey.slots.get(id)?.itemId;
|
|
1215
|
+
return dependencyItem && dependencyItem !== item.id && !isStaticallyBefore(survey, dependencyItem, item.id);
|
|
1216
|
+
})) warnings.push({
|
|
1217
|
+
ownerId: rule.id,
|
|
1218
|
+
code: "cross-item-page-rule",
|
|
1219
|
+
message: "This rule runs before leaving the page and reads answers from another item. If those answers are on a later page, the participant may be unable to continue. Consider submit-only validation or an explicit condition for unanswered dependencies."
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
for (const element of survey.elements.values()) for (const binding of element.facts.bindings) if (binding.labelRefs) {
|
|
1224
|
+
for (const ref of binding.labelRefs) if (!hasLocalizationText(readLocalizationContent(survey, {
|
|
1225
|
+
scope: "content",
|
|
1226
|
+
...ref
|
|
1227
|
+
}, locale)) && !warnings.some((warning) => warning.ownerId === ref.ownerId && warning.code === "missing-content")) warnings.push({
|
|
1228
|
+
ownerId: ref.ownerId,
|
|
1229
|
+
code: "missing-accessible-name",
|
|
1230
|
+
message: `Add ${ref.role} to name this input in the selected language`
|
|
1231
|
+
});
|
|
1232
|
+
} else if (!resolveElementName(survey, element.node.id, [locale]) && !warnings.some((warning) => warning.ownerId === element.node.id && warning.code === "missing-accessible-name")) warnings.push({
|
|
1233
|
+
ownerId: element.node.id,
|
|
1234
|
+
code: "missing-accessible-name",
|
|
1235
|
+
message: "Add a label, or an unambiguous enclosing title, in the selected language"
|
|
1236
|
+
});
|
|
1237
|
+
for (const element of survey.elements.values()) for (const binding of element.facts.bindings) {
|
|
1238
|
+
if (!binding.selection?.multiple) continue;
|
|
1239
|
+
for (const id of binding.selection.options) {
|
|
1240
|
+
const condition = survey.owners.get(id)?.disabled;
|
|
1241
|
+
if (condition && [...plan.storedDependencies(condition)].some((slotId) => slotId !== binding.slotId)) warnings.push({
|
|
1242
|
+
ownerId: id,
|
|
1243
|
+
code: "external-checkbox-lock",
|
|
1244
|
+
message: "This condition depends on another answer. An already selected checkbox can become locked and will still be submitted. Use visibility if the selection should stop applying."
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
return warnings;
|
|
1249
|
+
}
|
|
1250
|
+
/** A shared shuffled ancestor only changes order between its distinct children. */
|
|
1251
|
+
function isStaticallyBefore(survey, earlierId, laterId) {
|
|
1252
|
+
const earlier = [...survey.getItemPath(earlierId), earlierId];
|
|
1253
|
+
const later = [...survey.getItemPath(laterId), laterId];
|
|
1254
|
+
const branch = earlier.findIndex((id, index) => id !== later[index]);
|
|
1255
|
+
if (branch < 1) return false;
|
|
1256
|
+
const parent = survey.getItem(earlier[branch - 1]);
|
|
1257
|
+
return parent?.itemType === "group" && !parent.config.shuffleItems && parent.config.items.indexOf(earlier[branch]) < parent.config.items.indexOf(later[branch]);
|
|
1258
|
+
}
|
|
1259
|
+
//#endregion
|
|
1260
|
+
//#region src/survey/composition/diagnostics.ts
|
|
1261
|
+
/** Observed author-only conflicts use the same validation result as the player and host. */
|
|
1262
|
+
function getSessionDiagnostics(session) {
|
|
1263
|
+
const result = session.validate("submit");
|
|
1264
|
+
return [...result.authorWarnings.map((warning) => {
|
|
1265
|
+
const owner = session.document.owners.get(warning.ownerId);
|
|
1266
|
+
return {
|
|
1267
|
+
...warning,
|
|
1268
|
+
severity: "warning",
|
|
1269
|
+
path: owner?.path ?? [],
|
|
1270
|
+
ownerItemId: owner?.itemId
|
|
1271
|
+
};
|
|
1272
|
+
}), ...result.diagnostics.map((diagnostic) => {
|
|
1273
|
+
const owner = ownerAtPath(session.document, diagnostic.path);
|
|
1274
|
+
return {
|
|
1275
|
+
...diagnostic,
|
|
1276
|
+
ownerId: owner?.id,
|
|
1277
|
+
ownerItemId: owner?.itemId
|
|
1278
|
+
};
|
|
1279
|
+
})];
|
|
1280
|
+
}
|
|
1281
|
+
function ownerAtPath(survey, path) {
|
|
1282
|
+
return [...survey.owners.values()].filter((owner) => owner.path.length <= path.length && owner.path.every((part, index) => part === path[index])).sort((a, b) => b.path.length - a.path.length)[0];
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Every dangling reference an editor draft admits: missing answers, state targets, options,
|
|
1286
|
+
* owners and non-flow items from the document index, plus comparisons with removed option
|
|
1287
|
+
* identities reported by the compiled graph. Always empty for a runnable survey.
|
|
1288
|
+
*/
|
|
1289
|
+
function getBrokenReferences(survey) {
|
|
1290
|
+
return getDefinitionDiagnostics(survey).filter((diagnostic) => diagnostic.severity === "error" && diagnostic.kind === "reference");
|
|
1291
|
+
}
|
|
1292
|
+
const compiledDiagnostics = /* @__PURE__ */ new WeakMap();
|
|
1293
|
+
/** Use the authoritative definition/compiler diagnostics, including state reads and custom definitions. */
|
|
1294
|
+
function getDefinitionDiagnostics(survey, locale) {
|
|
1295
|
+
let compiled = compiledDiagnostics.get(survey.elements);
|
|
1296
|
+
if (!compiled) {
|
|
1297
|
+
try {
|
|
1298
|
+
compiled = new SurveyStateGraph(survey, { admission: "draft" }).admittedDiagnostics;
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
if (error instanceof DefinitionError) compiled = error.diagnostics;
|
|
1301
|
+
else throw error;
|
|
1302
|
+
}
|
|
1303
|
+
compiledDiagnostics.set(survey.elements, compiled);
|
|
1304
|
+
}
|
|
1305
|
+
const located = [...survey.diagnostics, ...compiled].map((diagnostic) => {
|
|
1306
|
+
const owner = ownerAtPath(survey, diagnostic.path) ?? (diagnostic.path[0] === "translations" && diagnostic.path[2] === "content" ? survey.owners.get(String(diagnostic.path[3])) : void 0);
|
|
1307
|
+
return {
|
|
1308
|
+
...diagnostic,
|
|
1309
|
+
ownerItemId: owner?.itemId,
|
|
1310
|
+
ownerId: owner?.id
|
|
1311
|
+
};
|
|
1312
|
+
});
|
|
1313
|
+
if (locale) try {
|
|
1314
|
+
for (const warning of getAuthoringWarnings(survey, locale)) {
|
|
1315
|
+
const owner = survey.owners.get(warning.ownerId);
|
|
1316
|
+
located.push({
|
|
1317
|
+
...warning,
|
|
1318
|
+
severity: "warning",
|
|
1319
|
+
path: owner?.path ?? [],
|
|
1320
|
+
ownerItemId: owner?.itemId
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
if (!(error instanceof DefinitionError)) throw error;
|
|
1325
|
+
}
|
|
1326
|
+
return [...new Map(located.map((issue) => [JSON.stringify([
|
|
1327
|
+
issue.code,
|
|
1328
|
+
issue.path,
|
|
1329
|
+
issue.message
|
|
1330
|
+
]), issue])).values()];
|
|
1331
|
+
}
|
|
1332
|
+
//#endregion
|
|
1333
|
+
//#region src/engine/session.ts
|
|
1334
|
+
const sessionSnapshotSchema = z.strictObject({
|
|
1335
|
+
schemaVersion: z.literal(1),
|
|
1336
|
+
binding: z.strictObject({
|
|
1337
|
+
sessionId: z.string().min(1),
|
|
1338
|
+
definitionId: z.string().min(1)
|
|
1339
|
+
}),
|
|
1340
|
+
answers: stringRecord(z.string().min(1), responseValueSchema),
|
|
1341
|
+
initialization: z.literal("complete"),
|
|
1342
|
+
navigation: z.strictObject({
|
|
1343
|
+
pageIndex: z.number().int().nonnegative(),
|
|
1344
|
+
viewport: z.enum(["small", "large"])
|
|
1345
|
+
}),
|
|
1346
|
+
randomization: z.strictObject({ seed: z.string().min(1) }),
|
|
1347
|
+
context: z.strictObject({
|
|
1348
|
+
locale: z.string().min(1),
|
|
1349
|
+
/** Defaults to the participant-local day when a new session starts; kept on resume. */
|
|
1350
|
+
currentDate: calendarDaySchema.optional(),
|
|
1351
|
+
participantFlags: stringRecord(z.string(), z.string()).optional(),
|
|
1352
|
+
customValues: stringRecord(z.string(), responseValueSchema).optional()
|
|
1353
|
+
}),
|
|
1354
|
+
events: z.array(surveyEventSchema)
|
|
1355
|
+
});
|
|
1356
|
+
const eventTimestamp = () => Math.floor(Date.now() / 1e3);
|
|
1357
|
+
const comparableAnswer = (value) => JSON.stringify(value, Object.keys(value).sort());
|
|
1358
|
+
function freezeAnswer(value) {
|
|
1359
|
+
if (Array.isArray(value.value)) Object.freeze(value.value);
|
|
1360
|
+
return Object.freeze(value);
|
|
1361
|
+
}
|
|
1362
|
+
function sameRecord(left, right, equal) {
|
|
1363
|
+
const entries = Object.entries(left ?? {});
|
|
1364
|
+
return entries.length === Object.keys(right ?? {}).length && entries.every(([key, value]) => {
|
|
1365
|
+
const other = ownValue(right, key);
|
|
1366
|
+
return other !== void 0 && equal(value, other);
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
/** Owns retained answers; participant adapters receive no privileged initialization write path. */
|
|
1370
|
+
var SurveySession = class SurveySession {
|
|
1371
|
+
currentDocument;
|
|
1372
|
+
state;
|
|
1373
|
+
stateGraph;
|
|
1374
|
+
revision = 0;
|
|
1375
|
+
presentationRevision = 0;
|
|
1376
|
+
itemPresentationRevisions = /* @__PURE__ */ new Map();
|
|
1377
|
+
initializationDiagnostics = [];
|
|
1378
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1379
|
+
subscribe = (listener) => {
|
|
1380
|
+
this.listeners.add(listener);
|
|
1381
|
+
return () => {
|
|
1382
|
+
this.listeners.delete(listener);
|
|
1383
|
+
};
|
|
1384
|
+
};
|
|
1385
|
+
getRevision = () => this.revision;
|
|
1386
|
+
/** State/context changes notify every item; wording changes only notify their owning item. */
|
|
1387
|
+
getItemRevision = (itemId) => this.revision - this.presentationRevision + (this.itemPresentationRevisions.get(itemId) ?? 0);
|
|
1388
|
+
/** Prefer the current page, then the next surviving item, then the previous one. */
|
|
1389
|
+
pageAnchors(writtenItems = /* @__PURE__ */ new Set()) {
|
|
1390
|
+
const pages = this.pages();
|
|
1391
|
+
return [
|
|
1392
|
+
...(pages[this.pageIndex] ?? []).filter((id) => writtenItems.has(id)),
|
|
1393
|
+
...pages.slice(this.pageIndex).flat(),
|
|
1394
|
+
...pages.slice(0, this.pageIndex).flat().reverse()
|
|
1395
|
+
];
|
|
1396
|
+
}
|
|
1397
|
+
reconcilePageAndNotify(anchors) {
|
|
1398
|
+
const pages = this.pages();
|
|
1399
|
+
const pageByItem = new Map(pages.flatMap((items, index) => items.map((id) => [id, index])));
|
|
1400
|
+
const anchoredPage = anchors?.map((id) => pageByItem.get(id)).find((index) => index !== void 0);
|
|
1401
|
+
this.state.navigation.pageIndex = anchoredPage ?? Math.min(this.pageIndex, Math.max(0, pages.length - 1));
|
|
1402
|
+
this.revision++;
|
|
1403
|
+
this.listeners.forEach((listener) => listener());
|
|
1404
|
+
}
|
|
1405
|
+
constructor(currentDocument, state, admission = "strict") {
|
|
1406
|
+
this.currentDocument = currentDocument;
|
|
1407
|
+
this.state = state;
|
|
1408
|
+
this.stateGraph = new SurveyStateGraph(currentDocument, { admission });
|
|
1409
|
+
this.validateAnswers(state.answers);
|
|
1410
|
+
for (const value of Object.values(state.answers)) freezeAnswer(value);
|
|
1411
|
+
for (const value of Object.values(state.context.customValues ?? {})) freezeAnswer(value);
|
|
1412
|
+
}
|
|
1413
|
+
get document() {
|
|
1414
|
+
return this.currentDocument;
|
|
1415
|
+
}
|
|
1416
|
+
/** Wording-only authoring revisions share the compiler indexes and cannot change execution.
|
|
1417
|
+
* Keep the compiled graph, answers, navigation and initialization when updating a live preview.
|
|
1418
|
+
* A structural/configuration revision must create a new session instead.
|
|
1419
|
+
*/
|
|
1420
|
+
updatePresentation(document) {
|
|
1421
|
+
if (document === this.currentDocument) return;
|
|
1422
|
+
if (document.elements !== this.currentDocument.elements) throw new Error("Only wording revisions can update a session presentation");
|
|
1423
|
+
const affected = /* @__PURE__ */ new Set();
|
|
1424
|
+
let sharedContentChanged = false;
|
|
1425
|
+
for (const locale of /* @__PURE__ */ new Set([...document.locales, ...this.currentDocument.locales])) {
|
|
1426
|
+
const before = this.currentDocument.data.translations?.[locale];
|
|
1427
|
+
const after = document.data.translations?.[locale];
|
|
1428
|
+
if (before === after) continue;
|
|
1429
|
+
sharedContentChanged ||= before?.navigationContent !== after?.navigationContent || before?.validationMessages !== after?.validationMessages;
|
|
1430
|
+
for (const owner of document.owners.values()) if (before?.content[owner.id] !== after?.content[owner.id]) affected.add(owner.itemId);
|
|
1431
|
+
}
|
|
1432
|
+
if (!sharedContentChanged) {
|
|
1433
|
+
this.presentationRevision++;
|
|
1434
|
+
for (const id of affected) this.itemPresentationRevisions.set(id, (this.itemPresentationRevisions.get(id) ?? 0) + 1);
|
|
1435
|
+
}
|
|
1436
|
+
this.currentDocument = document;
|
|
1437
|
+
this.reconcilePageAndNotify();
|
|
1438
|
+
}
|
|
1439
|
+
static async create(document, input, sources = {}) {
|
|
1440
|
+
if (input.mode === "resumed") {
|
|
1441
|
+
const snapshot = sessionSnapshotSchema.parse(input.snapshot);
|
|
1442
|
+
if (snapshot.binding.sessionId !== input.binding.sessionId || snapshot.binding.definitionId !== input.binding.definitionId) throw new Error("Resume snapshot does not match the session and exact survey definition");
|
|
1443
|
+
const session = new SurveySession(document, snapshot);
|
|
1444
|
+
if (session.pageIndex >= Math.max(1, session.pages().length)) throw new Error("Resume page index is outside the survey");
|
|
1445
|
+
return session;
|
|
1446
|
+
}
|
|
1447
|
+
const session = new SurveySession(document, sessionSnapshotSchema.parse({
|
|
1448
|
+
schemaVersion: 1,
|
|
1449
|
+
binding: input.binding,
|
|
1450
|
+
answers: input.answers ?? {},
|
|
1451
|
+
initialization: "complete",
|
|
1452
|
+
navigation: {
|
|
1453
|
+
pageIndex: 0,
|
|
1454
|
+
viewport: "large"
|
|
1455
|
+
},
|
|
1456
|
+
randomization: { seed: input.seed ?? crypto.randomUUID() },
|
|
1457
|
+
context: {
|
|
1458
|
+
...input.context,
|
|
1459
|
+
currentDate: input.context.currentDate ?? calendarDateFromLocalDate(/* @__PURE__ */ new Date())
|
|
1460
|
+
},
|
|
1461
|
+
events: [{
|
|
1462
|
+
type: "openSurvey",
|
|
1463
|
+
timestamp: eventTimestamp(),
|
|
1464
|
+
value: input.context.locale
|
|
1465
|
+
}]
|
|
1466
|
+
}), input.admission);
|
|
1467
|
+
await session.initialize(sources);
|
|
1468
|
+
return session;
|
|
1469
|
+
}
|
|
1470
|
+
snapshot() {
|
|
1471
|
+
return structuredClone(this.state);
|
|
1472
|
+
}
|
|
1473
|
+
itemMetadata() {
|
|
1474
|
+
return Object.fromEntries(this.pages().flat().map((itemId, position) => [itemId, { position }]));
|
|
1475
|
+
}
|
|
1476
|
+
active() {
|
|
1477
|
+
return this.stateGraph.evaluate({
|
|
1478
|
+
answers: this.state.answers,
|
|
1479
|
+
context: this.state.context
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
get locale() {
|
|
1483
|
+
return this.state.context.locale;
|
|
1484
|
+
}
|
|
1485
|
+
get pageIndex() {
|
|
1486
|
+
return this.state.navigation.pageIndex;
|
|
1487
|
+
}
|
|
1488
|
+
setLocale(locale) {
|
|
1489
|
+
this.setContext({
|
|
1490
|
+
...this.state.context,
|
|
1491
|
+
locale
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
/** Replace host context without restarting initialization or losing retained answers. */
|
|
1495
|
+
setContext(context) {
|
|
1496
|
+
const parsed = sessionSnapshotSchema.shape.context.parse(context);
|
|
1497
|
+
const previous = this.state.context;
|
|
1498
|
+
const next = {
|
|
1499
|
+
...parsed,
|
|
1500
|
+
currentDate: parsed.currentDate ?? previous.currentDate
|
|
1501
|
+
};
|
|
1502
|
+
if (!next.locale.trim()) throw new Error("A locale is required");
|
|
1503
|
+
const localeChanged = next.locale !== previous.locale;
|
|
1504
|
+
if (!localeChanged && next.currentDate === previous.currentDate && sameRecord(previous.participantFlags, next.participantFlags, (a, b) => a === b) && sameRecord(previous.customValues, next.customValues, (a, b) => JSON.stringify(a) === JSON.stringify(b))) return;
|
|
1505
|
+
for (const value of Object.values(next.customValues ?? {})) freezeAnswer(value);
|
|
1506
|
+
const anchors = this.pageAnchors();
|
|
1507
|
+
this.state = {
|
|
1508
|
+
...this.state,
|
|
1509
|
+
context: next,
|
|
1510
|
+
events: localeChanged ? [...this.state.events, {
|
|
1511
|
+
type: "languageChanged",
|
|
1512
|
+
timestamp: eventTimestamp(),
|
|
1513
|
+
value: next.locale
|
|
1514
|
+
}] : this.state.events
|
|
1515
|
+
};
|
|
1516
|
+
this.reconcilePageAndNotify(anchors);
|
|
1517
|
+
}
|
|
1518
|
+
orderedIds(ids, ownerId) {
|
|
1519
|
+
return shuffleArray(ids, createSeededRandom(`${this.state.randomization.seed}:${ownerId}`));
|
|
1520
|
+
}
|
|
1521
|
+
setViewport(viewport) {
|
|
1522
|
+
if (viewport === this.state.navigation.viewport) return;
|
|
1523
|
+
const anchors = this.pageAnchors();
|
|
1524
|
+
this.state.navigation.viewport = viewport;
|
|
1525
|
+
this.reconcilePageAndNotify(anchors);
|
|
1526
|
+
}
|
|
1527
|
+
pages(viewport = this.state.navigation.viewport) {
|
|
1528
|
+
const active = this.active(), byId = new Map(this.document.data.surveyItems.map((item) => [item.id, item]));
|
|
1529
|
+
const pages = [[]];
|
|
1530
|
+
const limit = this.document.data.maxItemsPerPage?.[viewport] ?? Infinity;
|
|
1531
|
+
const next = () => {
|
|
1532
|
+
if (pages.at(-1).length) pages.push([]);
|
|
1533
|
+
};
|
|
1534
|
+
const visit = (id) => {
|
|
1535
|
+
const item = byId.get(id);
|
|
1536
|
+
if (!item || !active.visible(id)) return;
|
|
1537
|
+
if (item.itemType === "group") (item.config.shuffleItems ? this.orderedIds(item.config.items, item.id) : item.config.items).forEach(visit);
|
|
1538
|
+
else if (item.itemType === "page-break") next();
|
|
1539
|
+
else {
|
|
1540
|
+
if (pages.at(-1).length >= limit) next();
|
|
1541
|
+
pages.at(-1).push(id);
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
visit(this.document.rootId);
|
|
1545
|
+
return pages.filter((page) => page.length);
|
|
1546
|
+
}
|
|
1547
|
+
validate(phase, itemIds) {
|
|
1548
|
+
const result = validateSurveyAnswers(this.stateGraph, this.state.answers, this.state.context, {
|
|
1549
|
+
phase,
|
|
1550
|
+
itemIds
|
|
1551
|
+
});
|
|
1552
|
+
return {
|
|
1553
|
+
...result,
|
|
1554
|
+
diagnostics: [...this.initializationDiagnostics, ...result.diagnostics]
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
goToPage(index, viewport = this.state.navigation.viewport) {
|
|
1558
|
+
const pages = this.pages(viewport);
|
|
1559
|
+
if (!Number.isInteger(index) || index < 0 || index >= pages.length) throw new Error("Invalid page index");
|
|
1560
|
+
if (index > this.pageIndex) {
|
|
1561
|
+
if (this.validate("page", pages[this.pageIndex]).issues.length) throw new Error("Current page has blocking validation errors");
|
|
1562
|
+
}
|
|
1563
|
+
this.state = {
|
|
1564
|
+
...this.state,
|
|
1565
|
+
navigation: {
|
|
1566
|
+
pageIndex: index,
|
|
1567
|
+
viewport
|
|
1568
|
+
},
|
|
1569
|
+
events: [...this.state.events, {
|
|
1570
|
+
type: "pageChanged",
|
|
1571
|
+
timestamp: eventTimestamp(),
|
|
1572
|
+
value: index
|
|
1573
|
+
}]
|
|
1574
|
+
};
|
|
1575
|
+
this.reconcilePageAndNotify();
|
|
1576
|
+
}
|
|
1577
|
+
submission(responseId) {
|
|
1578
|
+
if (this.validate("submit").issues.length) throw new Error("Survey has blocking validation errors");
|
|
1579
|
+
return structuredClone({
|
|
1580
|
+
schemaVersion: 2,
|
|
1581
|
+
responseId,
|
|
1582
|
+
definitionId: this.state.binding.definitionId,
|
|
1583
|
+
answers: this.active().activeAnswers(),
|
|
1584
|
+
events: this.state.events,
|
|
1585
|
+
itemMetadata: this.itemMetadata(),
|
|
1586
|
+
customValues: this.state.context.customValues,
|
|
1587
|
+
evaluationContext: {
|
|
1588
|
+
locale: this.state.context.locale,
|
|
1589
|
+
currentDate: this.state.context.currentDate,
|
|
1590
|
+
participantFlags: this.state.context.participantFlags
|
|
1591
|
+
},
|
|
1592
|
+
randomization: this.state.randomization
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
validateAnswers(answers) {
|
|
1596
|
+
for (const [slotId, value] of Object.entries(answers)) {
|
|
1597
|
+
const slot = this.document.slots.get(slotId);
|
|
1598
|
+
if (!slot || slot.computed) throw new Error(`Unknown or computed answer target '${slotId}'`);
|
|
1599
|
+
const errors = validateResponseSlotValue(slot, value);
|
|
1600
|
+
if (errors.length) throw new Error(errors.join("; "));
|
|
1601
|
+
if (value.type === "reference[]" && new Set(value.value).size !== value.value.length) throw new Error(`Duplicate selected options in '${slotId}'`);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
write(writes) {
|
|
1605
|
+
if (new Set(writes.map((write) => write.slotId)).size !== writes.length) throw new Error("One action cannot write a slot twice");
|
|
1606
|
+
const before = this.active();
|
|
1607
|
+
const changed = writes.filter(({ slotId, value }) => {
|
|
1608
|
+
const previous = ownValue(this.state.answers, slotId);
|
|
1609
|
+
return previous === void 0 || value === void 0 ? previous !== value : comparableAnswer(previous) !== comparableAnswer(value);
|
|
1610
|
+
});
|
|
1611
|
+
for (const { slotId, value } of writes) {
|
|
1612
|
+
const slot = this.document.slots.get(slotId);
|
|
1613
|
+
const binding = this.stateGraph.bindings.get(slotId);
|
|
1614
|
+
if (!slot || slot.computed || !binding) throw new Error(`Slot '${slotId}' is not a writable input`);
|
|
1615
|
+
if (value !== void 0) this.validateAnswers({ [slotId]: value });
|
|
1616
|
+
}
|
|
1617
|
+
for (const { slotId, value } of changed) {
|
|
1618
|
+
const binding = this.stateGraph.bindings.get(slotId);
|
|
1619
|
+
if (!canWriteSelection(binding, ownValue(this.state.answers, slotId), value, before)) throw new Error(`Participant write to '${slotId}' is not permitted in the current state`);
|
|
1620
|
+
}
|
|
1621
|
+
if (!changed.length) return;
|
|
1622
|
+
const anchors = this.pageAnchors(new Set(changed.map(({ slotId }) => this.document.slots.get(slotId).itemId)));
|
|
1623
|
+
const answers = { ...this.state.answers };
|
|
1624
|
+
const timestamp = eventTimestamp();
|
|
1625
|
+
for (const { slotId, value } of changed) if (value === void 0) delete answers[slotId];
|
|
1626
|
+
else setOwnValue(answers, slotId, freezeAnswer(structuredClone(value)));
|
|
1627
|
+
const events = [...this.state.events];
|
|
1628
|
+
for (const { slotId, value } of changed) {
|
|
1629
|
+
const event = {
|
|
1630
|
+
type: "responseChanged",
|
|
1631
|
+
slotId,
|
|
1632
|
+
operation: value === void 0 ? "clear" : "set",
|
|
1633
|
+
timestamp
|
|
1634
|
+
};
|
|
1635
|
+
const previous = events.at(-1);
|
|
1636
|
+
if (previous?.type === event.type && previous.slotId === slotId && previous.operation === event.operation) events[events.length - 1] = event;
|
|
1637
|
+
else events.push(event);
|
|
1638
|
+
}
|
|
1639
|
+
this.state = {
|
|
1640
|
+
...this.state,
|
|
1641
|
+
answers,
|
|
1642
|
+
events
|
|
1643
|
+
};
|
|
1644
|
+
this.reconcilePageAndNotify(anchors);
|
|
1645
|
+
}
|
|
1646
|
+
evaluate(expression) {
|
|
1647
|
+
const active = this.active();
|
|
1648
|
+
return evaluateExpression(expression, {
|
|
1649
|
+
...this.state.context,
|
|
1650
|
+
answer: active.answer,
|
|
1651
|
+
disabled: active.disabled
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
async initialize(sources) {
|
|
1655
|
+
const prefills = new Map([...this.document.elements.values()].flatMap((element) => Object.entries(element.node.prefills ?? {})));
|
|
1656
|
+
for (const slotId of this.stateGraph.initializationOrder) {
|
|
1657
|
+
if (Object.hasOwn(this.state.answers, slotId)) continue;
|
|
1658
|
+
for (const [index, candidate] of (prefills.get(slotId) ?? []).entries()) {
|
|
1659
|
+
const path = [
|
|
1660
|
+
...this.document.elements.get(this.document.slots.get(slotId).elementId).path,
|
|
1661
|
+
"prefills",
|
|
1662
|
+
slotId,
|
|
1663
|
+
index
|
|
1664
|
+
];
|
|
1665
|
+
const warn = (message) => this.initializationDiagnostics.push({
|
|
1666
|
+
code: "prefill-value",
|
|
1667
|
+
severity: "warning",
|
|
1668
|
+
path,
|
|
1669
|
+
message
|
|
1670
|
+
});
|
|
1671
|
+
const evaluate = (expression) => {
|
|
1672
|
+
try {
|
|
1673
|
+
return this.evaluate(expression);
|
|
1674
|
+
} catch (error) {
|
|
1675
|
+
if (!(error instanceof ExpressionEvaluationError)) throw error;
|
|
1676
|
+
warn(`Prefill '${candidate.id}' was skipped: ${error.message}`);
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
if (candidate.when) {
|
|
1681
|
+
const result = evaluate(candidate.when);
|
|
1682
|
+
if (result?.type !== "boolean" || !result.value) continue;
|
|
1683
|
+
}
|
|
1684
|
+
let value;
|
|
1685
|
+
switch (candidate.source.type) {
|
|
1686
|
+
case "static":
|
|
1687
|
+
value = candidate.source.value;
|
|
1688
|
+
break;
|
|
1689
|
+
case "expression":
|
|
1690
|
+
value = evaluate(candidate.source.expression);
|
|
1691
|
+
break;
|
|
1692
|
+
case "templateValue": {
|
|
1693
|
+
const template = ownValue(this.document.data.templateValues, candidate.source.key);
|
|
1694
|
+
if (template?.expression) {
|
|
1695
|
+
value = evaluate(template.expression);
|
|
1696
|
+
if (template.type === "formatDate" && value?.type === "date") value = {
|
|
1697
|
+
type: "string",
|
|
1698
|
+
value: formatCalendarDate(value.value, template.dateFormat)
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
break;
|
|
1702
|
+
}
|
|
1703
|
+
case "previousResponse":
|
|
1704
|
+
if (!sources.previousResponse && candidate.source.ifUnsupported !== "skip") throw new Error("This host does not supply previous responses for prefills");
|
|
1705
|
+
value = await sources.previousResponse?.(candidate.source.ref);
|
|
1706
|
+
}
|
|
1707
|
+
if (value === void 0) continue;
|
|
1708
|
+
if (candidate.source.type === "previousResponse") {
|
|
1709
|
+
const parsed = responseValueSchema.safeParse(value);
|
|
1710
|
+
const slot = this.document.slots.get(slotId);
|
|
1711
|
+
if (parsed.success && parsed.data.type === "reference[]" && slot.valueType === "reference[]" && slot.allowedValues) {
|
|
1712
|
+
const allowed = new Set(slot.allowedValues.flatMap(responseValueElements).flatMap((option) => option.type === "reference" ? [option.value] : []));
|
|
1713
|
+
const retained = parsed.data.value.filter((id) => allowed.has(id));
|
|
1714
|
+
if (retained.length !== parsed.data.value.length) {
|
|
1715
|
+
warn(`Prefill '${candidate.id}' discarded options outside the current response domain`);
|
|
1716
|
+
if (!retained.length) continue;
|
|
1717
|
+
value = {
|
|
1718
|
+
type: "reference[]",
|
|
1719
|
+
value: retained
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
const issues = this.document.slots.validateAnswers({ [slotId]: value });
|
|
1725
|
+
if (issues.length) {
|
|
1726
|
+
warn(`Prefill '${candidate.id}' was skipped: ${issues.map((issue) => issue.message).join("; ")}`);
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
setOwnValue(this.state.answers, slotId, freezeAnswer(structuredClone(value)));
|
|
1730
|
+
break;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
//#endregion
|
|
968
1736
|
//#region src/response-exporter/csv-serializer.ts
|
|
969
1737
|
const DEFAULT_OPTIONS = {
|
|
970
1738
|
separator: ",",
|
|
@@ -999,6 +1767,13 @@ function serializeToCsv(columns, rows, options) {
|
|
|
999
1767
|
return lines.join(opts.lineEnding);
|
|
1000
1768
|
}
|
|
1001
1769
|
//#endregion
|
|
1770
|
+
//#region src/response-exporter/validate-configuration.ts
|
|
1771
|
+
/** Export requires an executable definition, including complete expression operands. */
|
|
1772
|
+
function validateExportConfiguration(survey, registry = survey.slots) {
|
|
1773
|
+
registry.assertValid();
|
|
1774
|
+
new SurveyStateGraph(survey);
|
|
1775
|
+
}
|
|
1776
|
+
//#endregion
|
|
1002
1777
|
//#region src/response-exporter/exporter.ts
|
|
1003
1778
|
const canonical = (value) => JSON.stringify(value, (_key, entry) => entry && typeof entry === "object" && !Array.isArray(entry) ? Object.fromEntries(Object.entries(entry).sort(([a], [b]) => a.localeCompare(b))) : entry);
|
|
1004
1779
|
/** Export only against schemas selected by the host, never against response version tags. */
|
|
@@ -1038,18 +1813,18 @@ var SurveyResponseExporter = class SurveyResponseExporter {
|
|
|
1038
1813
|
}
|
|
1039
1814
|
compileContexts() {
|
|
1040
1815
|
return new Map(this.batches.map((batch) => {
|
|
1041
|
-
const registry = batch.schema.
|
|
1816
|
+
const registry = batch.schema.slots;
|
|
1042
1817
|
validateExportConfiguration(batch.schema, registry);
|
|
1043
|
-
const slots = new Map(
|
|
1818
|
+
const slots = new Map(localizedExportSlots(batch.schema).map((slot) => [slot.slotId, slot]));
|
|
1044
1819
|
const profile = this.profile(batch);
|
|
1045
1820
|
const transforms = new Map([...slots.values()].map((slot) => [slot.slotId, {
|
|
1046
1821
|
...DEFAULT_TRANSFORM,
|
|
1047
1822
|
...slot.exportSettings,
|
|
1048
|
-
...profile?.slotOverrides
|
|
1823
|
+
...ownValue(profile?.slotOverrides, slot.slotId)
|
|
1049
1824
|
}]));
|
|
1050
1825
|
const columns = [...slots.values()].flatMap((slot) => projectSlot(slot, transforms.get(slot.slotId)).map((column) => ({
|
|
1051
1826
|
...column,
|
|
1052
|
-
header: profile?.columnHeaders
|
|
1827
|
+
header: ownValue(profile?.columnHeaders, column.id)?.trim() ?? column.header
|
|
1053
1828
|
})));
|
|
1054
1829
|
const encodings = /* @__PURE__ */ new Map();
|
|
1055
1830
|
for (const slotId of new Set(columns.map((column) => column.slotId))) {
|
|
@@ -1064,7 +1839,7 @@ var SurveyResponseExporter = class SurveyResponseExporter {
|
|
|
1064
1839
|
encodings,
|
|
1065
1840
|
columns,
|
|
1066
1841
|
included: new Set(columns.map((column) => column.id)),
|
|
1067
|
-
|
|
1842
|
+
stateGraph: new SurveyStateGraph(batch.schema)
|
|
1068
1843
|
}];
|
|
1069
1844
|
}));
|
|
1070
1845
|
}
|
|
@@ -1072,7 +1847,7 @@ var SurveyResponseExporter = class SurveyResponseExporter {
|
|
|
1072
1847
|
if (!profile) return;
|
|
1073
1848
|
if (profile.schemaVersion !== 1) throw new Error(`Unsupported exporter profile schema version: ${String(profile.schemaVersion)}`);
|
|
1074
1849
|
const slots = new Map(batches.flatMap((batch) => [...contexts.get(batch).slots]));
|
|
1075
|
-
const contextKeys = new Set([...batches.flatMap((batch) => batch.responses), ...extraResponses].flatMap((response) => Object.keys(response.
|
|
1850
|
+
const contextKeys = new Set([...batches.flatMap((batch) => batch.responses), ...extraResponses].flatMap((response) => Object.keys(response.customValues ?? {})));
|
|
1076
1851
|
const columns = /* @__PURE__ */ new Set([
|
|
1077
1852
|
...[...slots.values()].flatMap((slot) => projectSlot(slot, { mode: "default" }, true).map((column) => column.id)),
|
|
1078
1853
|
...META_COLUMN_ORDER.map((field) => encodeExportColumnId(["meta", field])),
|
|
@@ -1108,7 +1883,7 @@ var SurveyResponseExporter = class SurveyResponseExporter {
|
|
|
1108
1883
|
const signature = canonical({
|
|
1109
1884
|
type: slot.valueType,
|
|
1110
1885
|
allowedValues: slot.allowedValues?.map((value) => canonical(value)).sort(),
|
|
1111
|
-
codes: this.options.codingPolicy === "harmonized" ? context.profile?.slotOverrides
|
|
1886
|
+
codes: this.options.codingPolicy === "harmonized" ? ownValue(context.profile?.slotOverrides, slot.slotId)?.optionCodes ?? slot.referenceValueKeys : slot.referenceValueKeys,
|
|
1112
1887
|
computed: slot.computed,
|
|
1113
1888
|
scores: slot.referenceValueScores
|
|
1114
1889
|
});
|
|
@@ -1127,7 +1902,7 @@ var SurveyResponseExporter = class SurveyResponseExporter {
|
|
|
1127
1902
|
});
|
|
1128
1903
|
const profile = this.options.profile ?? (this.batches.length === 1 ? this.batches[0].profile : void 0);
|
|
1129
1904
|
const allResponses = [...this.batches.flatMap((batch) => batch.responses), ...extraResponses];
|
|
1130
|
-
const contextKeys = [...new Set(allResponses.flatMap((response) => Object.keys(response.
|
|
1905
|
+
const contextKeys = [...new Set(allResponses.flatMap((response) => Object.keys(response.customValues ?? {})))];
|
|
1131
1906
|
const ordered = (values, order, id, name) => values.sort((a, b) => {
|
|
1132
1907
|
const left = order.indexOf(id(a)), right = order.indexOf(id(b));
|
|
1133
1908
|
if (left >= 0 || right >= 0) return (left < 0 ? Infinity : left) - (right < 0 ? Infinity : right);
|
|
@@ -1160,23 +1935,18 @@ var SurveyResponseExporter = class SurveyResponseExporter {
|
|
|
1160
1935
|
const diagnostics = registry.validateAnswers(response.answers);
|
|
1161
1936
|
if (diagnostics.length) throw new Error(diagnostics.map((issue) => issue.message).join("\n"));
|
|
1162
1937
|
const row = Object.create(null);
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
const active = context.activePlan?.evaluate(recorded, {
|
|
1173
|
-
locale: response.evaluationContext?.locale ?? batch.schema.locales[0] ?? "en",
|
|
1174
|
-
participantFlags: response.evaluationContext?.participantFlags,
|
|
1175
|
-
customValues: response.context
|
|
1938
|
+
const active = context.stateGraph.evaluate({
|
|
1939
|
+
answers: response.answers,
|
|
1940
|
+
context: {
|
|
1941
|
+
locale: response.evaluationContext?.locale ?? batch.schema.locales[0] ?? "en",
|
|
1942
|
+
currentDate: response.evaluationContext?.currentDate,
|
|
1943
|
+
participantFlags: response.evaluationContext?.participantFlags,
|
|
1944
|
+
customValues: response.customValues
|
|
1945
|
+
}
|
|
1176
1946
|
});
|
|
1177
1947
|
for (const column of columns) if (column.source === ExportColumnSource.meta) switch (column.metaField) {
|
|
1178
1948
|
case "responseId":
|
|
1179
|
-
row[column.header] = response.
|
|
1949
|
+
row[column.header] = response.responseId;
|
|
1180
1950
|
break;
|
|
1181
1951
|
case "participantId":
|
|
1182
1952
|
row[column.header] = response.participantId ?? "";
|
|
@@ -1187,18 +1957,18 @@ var SurveyResponseExporter = class SurveyResponseExporter {
|
|
|
1187
1957
|
case "submittedAt": row[column.header] = response.submittedAt ? format(/* @__PURE__ */ new Date(response.submittedAt * 1e3), "yyyy-MM-dd'T'HH:mm:ss.SSSxxx") : "";
|
|
1188
1958
|
}
|
|
1189
1959
|
else if (column.source === ExportColumnSource.context) {
|
|
1190
|
-
const value = response.
|
|
1960
|
+
const value = ownValue(response.customValues, column.contextKey);
|
|
1191
1961
|
row[column.header] = exportSingleResponseSlot({
|
|
1192
1962
|
value,
|
|
1193
1963
|
valueType: value?.type ?? ValueType.string,
|
|
1194
|
-
transformConfig: profile?.contextOverrides
|
|
1964
|
+
transformConfig: ownValue(profile?.contextOverrides, column.contextKey) ?? {
|
|
1195
1965
|
...DEFAULT_TRANSFORM,
|
|
1196
1966
|
categorical: "ids"
|
|
1197
1967
|
}
|
|
1198
1968
|
});
|
|
1199
1969
|
} else {
|
|
1200
1970
|
const slot = slots.get(column.slotId);
|
|
1201
|
-
row[column.header] = slot && included.has(column.id) ? formatProjectedValue(slot, column,
|
|
1971
|
+
row[column.header] = slot && included.has(column.id) ? formatProjectedValue(slot, column, active.answer(slot.slotId), transforms.get(slot.slotId)) : "";
|
|
1202
1972
|
}
|
|
1203
1973
|
return row;
|
|
1204
1974
|
}
|
|
@@ -1228,16 +1998,16 @@ var SurveyResponseExporter = class SurveyResponseExporter {
|
|
|
1228
1998
|
* Generate a structured codebook from a survey definition.
|
|
1229
1999
|
*/
|
|
1230
2000
|
function generateCodebook(survey, options) {
|
|
1231
|
-
const registry = survey.
|
|
2001
|
+
const registry = survey.slots;
|
|
1232
2002
|
validateExportConfiguration(survey, registry);
|
|
1233
|
-
const slots =
|
|
2003
|
+
const slots = localizedExportSlots(survey);
|
|
1234
2004
|
const locales = options?.locales ?? survey.locales;
|
|
1235
2005
|
const slotDefsByItemId = /* @__PURE__ */ new Map();
|
|
1236
2006
|
for (const slot of slots) {
|
|
1237
2007
|
const entries = slotDefsByItemId.get(slot.itemId) ?? [];
|
|
1238
2008
|
entries.push({
|
|
1239
2009
|
slotId: slot.slotId,
|
|
1240
|
-
|
|
2010
|
+
elementId: slot.elementId,
|
|
1241
2011
|
...slot.rowId ? { rowId: slot.rowId } : {},
|
|
1242
2012
|
...slot.columnId ? { columnId: slot.columnId } : {},
|
|
1243
2013
|
variableName: slot.variableName,
|
|
@@ -1245,45 +2015,47 @@ function generateCodebook(survey, options) {
|
|
|
1245
2015
|
encoding: resolvedSlotEncoding(slot, {
|
|
1246
2016
|
mode: "default",
|
|
1247
2017
|
...slot.exportSettings,
|
|
1248
|
-
...options?.profile?.slotOverrides
|
|
2018
|
+
...ownValue(options?.profile?.slotOverrides, slot.slotId)
|
|
1249
2019
|
}),
|
|
1250
2020
|
optionLabels: slot.referenceValueLabels,
|
|
1251
|
-
allowedValues: slot.allowedValues,
|
|
1252
|
-
...slot.computed ? { computed:
|
|
2021
|
+
...slot.allowedValues ? { allowedValues: slot.allowedValues.map((value) => structuredClone(value)) } : {},
|
|
2022
|
+
...slot.computed ? { computed: {
|
|
2023
|
+
...slot.computed,
|
|
2024
|
+
sourceSlotIds: [...slot.computed.sourceSlotIds]
|
|
2025
|
+
} } : {},
|
|
1253
2026
|
...slot.referenceValueKeys ? { referenceValueKeys: { ...slot.referenceValueKeys } } : {},
|
|
1254
2027
|
...slot.referenceValueScores ? { referenceValueScores: { ...slot.referenceValueScores } } : {}
|
|
1255
2028
|
});
|
|
1256
2029
|
slotDefsByItemId.set(slot.itemId, entries);
|
|
1257
2030
|
}
|
|
1258
2031
|
const items = [];
|
|
1259
|
-
for (const item of survey.surveyItems
|
|
2032
|
+
for (const item of survey.data.surveyItems) {
|
|
1260
2033
|
const slotList = slotDefsByItemId.get(item.id) ?? [];
|
|
1261
2034
|
slotList.sort((a, b) => a.slotId.localeCompare(b.slotId));
|
|
1262
2035
|
let contentByLocale;
|
|
1263
2036
|
if (locales.length > 0) {
|
|
1264
2037
|
contentByLocale = {};
|
|
1265
|
-
const
|
|
1266
|
-
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
for (const [contentKey, content] of Object.entries(all)) flat[contentKey] = getContentPlainText(content);
|
|
1271
|
-
contentByLocale[locale] = flat;
|
|
2038
|
+
for (const locale of locales) {
|
|
2039
|
+
const values = {};
|
|
2040
|
+
for (const owner of survey.owners.values()) if (owner.itemId === item.id) {
|
|
2041
|
+
const roles = ownValue(ownValue(survey.data.translations, locale)?.content, owner.id);
|
|
2042
|
+
if (roles) setOwnValue(values, owner.id, Object.fromEntries(Object.entries(roles).map(([role, content]) => [role, getContentPlainText(content)])));
|
|
1272
2043
|
}
|
|
2044
|
+
setOwnValue(contentByLocale, locale, values);
|
|
1273
2045
|
}
|
|
1274
2046
|
}
|
|
1275
2047
|
items.push({
|
|
1276
2048
|
id: item.id,
|
|
1277
2049
|
editorName: survey.resolveItemEditorName(item.id, locales[0]),
|
|
1278
2050
|
breadcrumb: survey.getItemDisplayPath(item.id, locales[0]).breadcrumb,
|
|
1279
|
-
type: item.
|
|
1280
|
-
interactive:
|
|
2051
|
+
type: item.itemType,
|
|
2052
|
+
interactive: slotList.some((slot) => !slot.computed),
|
|
1281
2053
|
slots: slotList,
|
|
1282
2054
|
contentByLocale
|
|
1283
2055
|
});
|
|
1284
2056
|
}
|
|
1285
2057
|
return {
|
|
1286
|
-
surveyKey: survey.surveyKey,
|
|
2058
|
+
surveyKey: survey.data.metadata?.surveyKey,
|
|
1287
2059
|
items,
|
|
1288
2060
|
projection: new SurveyResponseExporter([{
|
|
1289
2061
|
schema: survey,
|
|
@@ -1307,18 +2079,42 @@ function generateExportCodebook(batches, options = {}) {
|
|
|
1307
2079
|
sourceId: batch.sourceId,
|
|
1308
2080
|
surveySchemaVersion: batch.schema.serialize().schemaVersion,
|
|
1309
2081
|
profile: batch.profile && structuredClone(batch.profile),
|
|
1310
|
-
variables: batch.schema
|
|
2082
|
+
variables: localizedExportSlots(batch.schema).map((slot) => ({
|
|
1311
2083
|
...slot,
|
|
1312
2084
|
encoding: resolvedSlotEncoding(slot, {
|
|
1313
2085
|
mode: "default",
|
|
1314
2086
|
...slot.exportSettings,
|
|
1315
|
-
...options.profile?.slotOverrides
|
|
2087
|
+
...ownValue(options.profile?.slotOverrides, slot.slotId) ?? ownValue(batch.profile?.slotOverrides, slot.slotId)
|
|
1316
2088
|
})
|
|
1317
2089
|
}))
|
|
1318
2090
|
}))
|
|
1319
2091
|
};
|
|
1320
2092
|
}
|
|
1321
2093
|
//#endregion
|
|
1322
|
-
|
|
2094
|
+
//#region src/engine/submission.ts
|
|
2095
|
+
/** The host chooses the exact definition and trusted context before calling this boundary. */
|
|
2096
|
+
function validateSurveySubmission(survey, input, context, binding) {
|
|
2097
|
+
const response = SurveyResponse.deserialize(input);
|
|
2098
|
+
if (response.definitionId !== binding.definitionId) throw new Error(`Response was given against definition '${response.definitionId}', not '${binding.definitionId}'`);
|
|
2099
|
+
const issues = survey.slots.validateAnswers(response.answers);
|
|
2100
|
+
if (issues.length) throw new Error(issues.map((issue) => issue.message).join("\n"));
|
|
2101
|
+
const plan = new SurveyStateGraph(survey);
|
|
2102
|
+
const applicable = plan.evaluate({
|
|
2103
|
+
answers: response.answers,
|
|
2104
|
+
context
|
|
2105
|
+
}).activeAnswers();
|
|
2106
|
+
const validation = validateSurveyAnswers(plan, applicable, context, { phase: "submit" });
|
|
2107
|
+
if (validation.issues.length) throw new Error(validation.issues.map((issue) => `${issue.slotId}: ${issue.code}`).join("\n"));
|
|
2108
|
+
response.answers = structuredClone(applicable);
|
|
2109
|
+
response.evaluationContext = {
|
|
2110
|
+
locale: context.locale,
|
|
2111
|
+
currentDate: context.currentDate,
|
|
2112
|
+
participantFlags: context.participantFlags && { ...context.participantFlags }
|
|
2113
|
+
};
|
|
2114
|
+
response.customValues = context.customValues && structuredClone(context.customValues);
|
|
2115
|
+
return response;
|
|
2116
|
+
}
|
|
2117
|
+
//#endregion
|
|
2118
|
+
export { AndExpressionEditor, BooleanExportValues, CALENDAR_DATE_UNITS, CURRENT_SURVEY_SCHEMA, CURRENT_SURVEY_SCHEMA_VERSION, ConstBooleanEditor, ConstDateArrayEditor, ConstDateEditor, ConstNumberArrayEditor, ConstNumberEditor, ConstStringArrayEditor, ConstStringEditor, ContentType, CtxCurrentDateEditor, CtxCustomValueEditor, CtxLocaleEditor, CtxPFlagDateEditor, CtxPFlagIsDefinedEditor, CtxPFlagNumEditor, CtxPFlagStringEditor, DEFAULT_TRANSFORM, DefinitionError, DisabledStateEditor, DurationUnits, EXPORTER_PROFILE_SCHEMA_VERSION, EqExpressionEditor, ExportColumnSource, ExpressionEditor, ExpressionEvaluationError, FUNCTION_SIGNATURES, GtExpressionEditor, GteExpressionEditor, InRangeExpressionEditor, JsonPatchError, LtExpressionEditor, LteExpressionEditor, META_COLUMN_ORDER, MaxExpressionEditor, MinExpressionEditor, NumberPrecision, OrExpressionEditor, RESPONSE_VARIABLE_NAME_MAX_LENGTH, ResponseSlotRegistry, ResponseVariableEditor, SURVEY_LOCALIZATION_SURFACES, SURVEY_RESPONSE_SCHEMA_VERSION, SlotTransformMode, StrEqExpressionEditor, StrListContainsExpressionEditor, SumExpressionEditor, Survey, SurveyEventTypes, SurveyResponse, SurveyResponseExporter, SurveySession, SurveyStateGraph, ValueType, addCalendarDate, allocateResponseVariableName, and, annotated, annotatedReferences, applyJsonPatch, assertResponseValue, boundField, calendarDateBounds, calendarDateFromLocalDate, calendarDatePrecision, calendarDateToLocalDate, calendarDaySchema, canWriteSelection, checkDocumentBudget, collectExpressionDependencies, compareCalendarDates, conditionsShape, const_boolean, const_date, const_date_array, const_number, const_number_array, const_string, const_string_array, contentOwner, contentOwnerIds, createCompositionSchema, createElementRegistry, createFlowItemSchema, createFunctionExpression, createRichTextContent, createSeededRandom, createSurveySchema, ctx_current_date, ctx_custom_value, ctx_locale, ctx_pflag_date, ctx_pflag_is_defined, ctx_pflag_num, ctx_pflag_string, date_add_days, date_add_months, date_add_years, date_diff, date_eq, date_gt, date_gte, date_lt, date_lte, date_max, date_min, decodeExportColumnId, decodeJsonPointerSegment, defineElement, differenceCalendarDates, elementShape, encodeExportColumnId, encodeJsonPointerSegment, eq, escapeCsvCell, evaluateExpression, exportSingleResponseSlot, expressionArguments, expressionEditorConfigSchema, expressionField, expressionIssues, expressionSchema, expressionTypeIssues, extendObject, followUpBody, formatCalendarDate, formatProjectedValue, functionNameSchema, functionTypeIssues, generateCodebook, generateCodingKey, generateExportCodebook, generateId, getAssetUsagesFromContent, getAuthoringWarnings, getBrokenReferences, getCompositionLabel, getContentPlainText, getDefaultProjectionDiagnostics, getDefaultProjectionIssues, getDefinitionDiagnostics, getElementSemanticContext, getLocalizationCellKey, getLocalizationCoverage, getLocalizationEntryKey, getLocalizationItems, getPlainTextFromRichTextContent, getSessionDiagnostics, getValueAtJsonPointer, gt, gte, hasLocalizationText, hasPermittedChange, hasRenderableRichTextBlock, hasRenderableRichTextContent, if_else, in_range, inferExpressionReturnType, inspectAnnotated, invalidResponseDomainValues, isAdmissibleInDraft, isCalendarDate, isCalendarDateUnit, isContentEmpty, isEmptySurveyLocalizationEntry, isResponseValue, isValidResponseVariableName, is_disabled, itemEditorMetadataSchema, itemResponseMetadataSchema, jsonValueSchema, layoutPolicySchema, listLocalizationEntries, list_count, localizedExportSlots, lt, lte, max, min, moveArrayElement, operand, optionIds, optionMapping, or, ownValue, ownedId, ownerAtPath, parseJsonPointer, projectSlot, readLocalizationContent, referenceId, remapAnnotated, remapExpression, resolveElementName, resolveParticipantContent, resolveUniqueItemKey, resolvedSlotEncoding, responseValueElements, responseValueSchema, responseValuesEqual, response_boolean, response_date, response_date_array, response_number, response_number_array, response_string, response_string_array, richTextImageDimensionSchema, richTextImageSourceSchema, schemaAnnotation, selectedAndEnabled, serializeToCsv, sessionSnapshotSchema, setOwnValue, shuffleArray, shuffleIndices, slotPrefillSchema, slotTransformSchema, str_eq, str_list_contains, strictKeyedObject, stringRecord, structuredCloneMethod, suggestResponseVariableName, sum, surveyContentSchema, surveyEventSchema, surveyLocales, surveyResponseSchema, swapArrayElements, templateValueSchema, transformAnnotated, transformExpression, unreachable, validateLocalizationRef, validateResponseSlotValue, validateSurveyAnswers, validateSurveySubmission, validationRuleSchema, valueTypeSchema, visitExpression };
|
|
1323
2119
|
|
|
1324
2120
|
//# sourceMappingURL=index.mjs.map
|