@case-framework/survey-core 0.6.2 → 0.8.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 +148 -23
- package/build/editor/colors.d.mts +2 -12
- package/build/editor/colors.mjs.map +1 -1
- package/build/editor.d.mts +74 -17
- package/build/editor.d.mts.map +1 -1
- package/build/editor.mjs +445 -106
- package/build/editor.mjs.map +1 -1
- package/build/index-SDbZ5WQM.d.mts +2208 -0
- package/build/index-SDbZ5WQM.d.mts.map +1 -0
- package/build/index.d.mts +3 -887
- package/build/index.mjs +615 -1665
- package/build/index.mjs.map +1 -1
- package/build/item-colors-DgoJeSCQ.d.mts +12 -0
- package/build/item-colors-DgoJeSCQ.d.mts.map +1 -0
- package/build/package.json +7 -8
- package/build/projection-Bl6qCinx.mjs +3930 -0
- package/build/projection-Bl6qCinx.mjs.map +1 -0
- package/package.json +7 -8
- package/build/editor/colors.d.mts.map +0 -1
- package/build/index.d.mts.map +0 -1
- package/build/survey-BKblV5rN.mjs +0 -1460
- package/build/survey-BKblV5rN.mjs.map +0 -1
- package/build/survey-DedDemSc.d.mts +0 -997
- package/build/survey-DedDemSc.d.mts.map +0 -1
package/build/editor.mjs
CHANGED
|
@@ -1,6 +1,186 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as SurveyItemTranslations, G as generateId, Jn as allocateResponseVariableName, M as GroupItemCore, Y as structuredCloneMethod, Yn as isValidResponseVariableName, cr as ValueType, i as projectSlot, m as Survey, n as getDefaultProjectionIssues, zn as ExpressionType } from "./projection-Bl6qCinx.mjs";
|
|
2
|
+
//#region src/editor/response-naming.ts
|
|
3
|
+
function readPath(config, path) {
|
|
4
|
+
let current = config;
|
|
5
|
+
for (const key of path) current = current?.[key];
|
|
6
|
+
return current;
|
|
7
|
+
}
|
|
8
|
+
function writePath(config, path, value) {
|
|
9
|
+
if (!path.length || path.some((key) => key === "__proto__" || key === "constructor" || key === "prototype")) throw new Error("Invalid response naming path");
|
|
10
|
+
const parent = readPath(config, path.slice(0, -1));
|
|
11
|
+
if (!parent || typeof parent !== "object") throw new Error("Response naming declaration does not match its config");
|
|
12
|
+
parent[path.at(-1)] = value;
|
|
13
|
+
}
|
|
14
|
+
const assertName = (name) => {
|
|
15
|
+
if (!isValidResponseVariableName(name)) throw new Error(`Invalid variable name "${name}". Use lowercase snake_case, starting with a letter, at most 32 characters.`);
|
|
16
|
+
};
|
|
17
|
+
/** Detached, validated preview; applying it uses the editor's ordinary mutation/history path. */
|
|
18
|
+
function previewNaming(survey, edits) {
|
|
19
|
+
const registry = survey.getResponseSlotRegistry();
|
|
20
|
+
const raw = structuredClone(survey.serialize());
|
|
21
|
+
const config = (slot) => raw.surveyItems.find((item) => item.id === slot.itemId).config;
|
|
22
|
+
for (const edit of edits) if ("slotId" in edit) {
|
|
23
|
+
const slot = registry.get(edit.slotId);
|
|
24
|
+
if (!slot) throw new Error(`Unknown response variable "${edit.slotId}"`);
|
|
25
|
+
if (slot.naming?.kind === "matrix") throw new Error("Matrix cell names are derived; edit the matrix prefix or row/column coding names");
|
|
26
|
+
if (!slot.naming) throw new Error(`The input owning "${edit.slotId}" does not expose a naming mutation`);
|
|
27
|
+
assertName(edit.variableName);
|
|
28
|
+
writePath(config(slot), slot.naming.path, edit.variableName);
|
|
29
|
+
} else {
|
|
30
|
+
const slots = registry.definitions.filter((slot) => slot.primitiveId === edit.primitiveId && slot.naming?.kind === "matrix");
|
|
31
|
+
if (!slots.length) throw new Error(`Unknown matrix primitive "${edit.primitiveId}"`);
|
|
32
|
+
for (const [ids, kind] of [[edit.rows, "row"], [edit.columns, "column"]]) for (const [id, name] of Object.entries(ids ?? {})) {
|
|
33
|
+
assertName(name);
|
|
34
|
+
const slot = slots.find((slot) => kind === "row" ? slot.rowId === id : slot.columnId === id);
|
|
35
|
+
const naming = slot?.naming;
|
|
36
|
+
const path = naming?.kind === "matrix" ? kind === "row" ? naming.rowPath : naming.columnPath : void 0;
|
|
37
|
+
if (!slot || !path) throw new Error(`Unknown matrix ${kind} "${id}"`);
|
|
38
|
+
writePath(config(slot), path, name);
|
|
39
|
+
}
|
|
40
|
+
if (edit.prefix !== void 0) {
|
|
41
|
+
assertName(edit.prefix);
|
|
42
|
+
const slot = slots[0], naming = slot.naming;
|
|
43
|
+
if (naming?.kind === "matrix") writePath(config(slot), naming.prefixPath, edit.prefix);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const next = Survey.fromJson(raw, survey.getPluginRegistry());
|
|
47
|
+
const nextRegistry = next.getResponseSlotRegistry();
|
|
48
|
+
const beforeIssues = new Set(registry.diagnostics.map((issue) => JSON.stringify(issue)));
|
|
49
|
+
const issues = nextRegistry.diagnostics.filter((issue) => !beforeIssues.has(JSON.stringify(issue)));
|
|
50
|
+
if (issues.length) throw new Error(issues.map((issue) => issue.message).join("\n"));
|
|
51
|
+
const changes = nextRegistry.definitions.flatMap((slot) => {
|
|
52
|
+
const before = registry.get(slot.slotId).variableName;
|
|
53
|
+
return before === slot.variableName ? [] : [{
|
|
54
|
+
slotId: slot.slotId,
|
|
55
|
+
itemId: slot.itemId,
|
|
56
|
+
before,
|
|
57
|
+
after: slot.variableName
|
|
58
|
+
}];
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
survey: next.serialize(),
|
|
62
|
+
changes
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function assertProjectionChanges(before, after) {
|
|
66
|
+
const existing = new Set(getDefaultProjectionIssues(before));
|
|
67
|
+
const introduced = getDefaultProjectionIssues(after).filter((issue) => !existing.has(issue));
|
|
68
|
+
if (introduced.length) throw new Error(introduced.join("\n"));
|
|
69
|
+
}
|
|
70
|
+
function previewResponseNaming(survey, edits) {
|
|
71
|
+
const preview = previewNaming(survey, edits);
|
|
72
|
+
assertProjectionChanges(survey, Survey.fromJson(preview.survey, survey.getPluginRegistry()));
|
|
73
|
+
return preview;
|
|
74
|
+
}
|
|
75
|
+
/** Allocate whole matrix prefixes and individual names in deterministic declaration order. */
|
|
76
|
+
function allocateCopiedResponseNames(survey, items) {
|
|
77
|
+
const existingIds = new Set(survey.getResponseSlotDefinitions().flatMap(({ slots }) => slots.map((slot) => slot.slotId)));
|
|
78
|
+
for (const item of items) for (const slot of item.getResponseSlotDefinitions()) {
|
|
79
|
+
if (existingIds.has(slot.slotId)) throw new Error(`Inserted input must allocate a fresh slot identity: ${slot.slotId}`);
|
|
80
|
+
existingIds.add(slot.slotId);
|
|
81
|
+
}
|
|
82
|
+
const used = new Set(survey.getResponseSlotRegistry().definitions.map((slot) => slot.variableName));
|
|
83
|
+
const usedPrefixes = new Set(survey.getResponseSlotRegistry().definitions.flatMap((slot) => slot.naming?.kind === "matrix" ? [String(readPath(survey.surveyItems.get(slot.itemId).config, slot.naming.prefixPath))] : []));
|
|
84
|
+
const changes = [];
|
|
85
|
+
for (const item of items) {
|
|
86
|
+
const initial = item.getResponseSlotDefinitions();
|
|
87
|
+
const groups = /* @__PURE__ */ new Map();
|
|
88
|
+
for (const slot of initial) {
|
|
89
|
+
const key = slot.naming?.kind === "matrix" ? JSON.stringify(slot.naming.prefixPath) : slot.slotId;
|
|
90
|
+
groups.set(key, [...groups.get(key) ?? [], slot]);
|
|
91
|
+
}
|
|
92
|
+
for (const group of groups.values()) {
|
|
93
|
+
const first = group[0], naming = first.naming;
|
|
94
|
+
const raw = structuredClone(item.rawItem);
|
|
95
|
+
if (naming?.kind === "matrix") {
|
|
96
|
+
const prefix = String(readPath(raw.config, naming.prefixPath));
|
|
97
|
+
assertName(prefix);
|
|
98
|
+
const tails = group.map((slot) => slot.variableName.slice(prefix.length));
|
|
99
|
+
if (new Set(tails).size !== tails.length) throw new Error("Matrix coding components produce duplicate variable names");
|
|
100
|
+
let candidate = prefix;
|
|
101
|
+
const blocked = /* @__PURE__ */ new Set();
|
|
102
|
+
while (usedPrefixes.has(candidate) || tails.some((tail) => used.has(`${candidate}${tail}`))) {
|
|
103
|
+
blocked.add(candidate);
|
|
104
|
+
const longestTail = Math.max(...tails.map((tail) => tail.length));
|
|
105
|
+
try {
|
|
106
|
+
candidate = allocateResponseVariableName(prefix, blocked, 32 - longestTail);
|
|
107
|
+
} catch {
|
|
108
|
+
const longest = group[tails.findIndex((tail) => tail.length === longestTail)];
|
|
109
|
+
throw new Error(`Cannot allocate a unique matrix prefix for "${longest.variableName}". Shorten its row or column coding name to leave room for a unique prefix within 32 characters.`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
usedPrefixes.add(candidate);
|
|
113
|
+
if (candidate !== prefix) {
|
|
114
|
+
writePath(raw.config, naming.prefixPath, candidate);
|
|
115
|
+
item.updateRawItem(raw);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
assertName(first.variableName);
|
|
119
|
+
const name = allocateResponseVariableName(first.variableName, used);
|
|
120
|
+
if (name !== first.variableName) {
|
|
121
|
+
if (!naming) throw new Error(`Cannot allocate a unique name for plugin variable "${first.variableName}"`);
|
|
122
|
+
writePath(raw.config, naming.path, name);
|
|
123
|
+
item.updateRawItem(raw);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const slot of item.getResponseSlotDefinitions().filter((slot) => group.some((original) => original.slotId === slot.slotId))) {
|
|
127
|
+
assertName(slot.variableName);
|
|
128
|
+
used.add(slot.variableName);
|
|
129
|
+
const before = initial.find((original) => original.slotId === slot.slotId).variableName;
|
|
130
|
+
if (before !== slot.variableName) changes.push({
|
|
131
|
+
slotId: slot.slotId,
|
|
132
|
+
itemId: item.id,
|
|
133
|
+
before,
|
|
134
|
+
after: slot.variableName
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return changes;
|
|
140
|
+
}
|
|
141
|
+
/** Naming, categorical codes, and formats share one detached, atomic preview. */
|
|
142
|
+
function previewResponseSettings(survey, naming, settings) {
|
|
143
|
+
const preview = previewNaming(survey, naming);
|
|
144
|
+
const raw = preview.survey;
|
|
145
|
+
const registry = survey.getResponseSlotRegistry();
|
|
146
|
+
const codeWrites = /* @__PURE__ */ new Map();
|
|
147
|
+
for (const edit of settings) {
|
|
148
|
+
const slot = registry.get(edit.slotId);
|
|
149
|
+
if (!slot) throw new Error(`Unknown response variable "${edit.slotId}"`);
|
|
150
|
+
const item = raw.surveyItems.find((item) => item.id === slot.itemId);
|
|
151
|
+
for (const [id, code] of Object.entries(edit.optionCodes ?? {})) {
|
|
152
|
+
const path = slot.optionCodePaths?.[id];
|
|
153
|
+
if (!path) throw new Error(`Option "${id}" does not expose an export-code mutation`);
|
|
154
|
+
if (!code.trim()) throw new Error("Option export codes cannot be empty");
|
|
155
|
+
const target = JSON.stringify([slot.itemId, path]);
|
|
156
|
+
const previous = codeWrites.get(target);
|
|
157
|
+
if (previous && previous.code !== code) throw new Error(`Conflicting export codes for shared option "${id}" on variables "${registry.get(previous.slotId).variableName}" and "${slot.variableName}". Supply one code for this shared option.`);
|
|
158
|
+
codeWrites.set(target, {
|
|
159
|
+
code,
|
|
160
|
+
slotId: slot.slotId
|
|
161
|
+
});
|
|
162
|
+
writePath(item.config, path, code);
|
|
163
|
+
}
|
|
164
|
+
if (edit.exportSettings) item.responseSettings = {
|
|
165
|
+
...item.responseSettings,
|
|
166
|
+
[slot.slotId]: structuredClone(edit.exportSettings)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const next = Survey.fromJson(raw, survey.getPluginRegistry());
|
|
170
|
+
for (const slot of next.getResponseSlotRegistry().definitions.filter((slot) => settings.some((edit) => edit.slotId === slot.slotId))) projectSlot(slot, {
|
|
171
|
+
mode: "default",
|
|
172
|
+
...slot.exportSettings
|
|
173
|
+
});
|
|
174
|
+
assertProjectionChanges(survey, next);
|
|
175
|
+
return {
|
|
176
|
+
...preview,
|
|
177
|
+
survey: next.serialize()
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
2
181
|
//#region src/editor/item-copy-paste.ts
|
|
3
182
|
var ItemCopyPaste = class ItemCopyPaste {
|
|
183
|
+
lastResponseNameChanges = [];
|
|
4
184
|
survey;
|
|
5
185
|
constructor(survey) {
|
|
6
186
|
this.survey = survey;
|
|
@@ -22,12 +202,12 @@ var ItemCopyPaste = class ItemCopyPaste {
|
|
|
22
202
|
itemData: item.rawItem
|
|
23
203
|
};
|
|
24
204
|
});
|
|
25
|
-
const translations =
|
|
205
|
+
const translations = Object.create(null);
|
|
26
206
|
itemsToCopy.forEach((id) => {
|
|
27
207
|
try {
|
|
28
208
|
const itemTranslations = this.survey.getItemTranslations(id);
|
|
29
209
|
if (itemTranslations?.locales?.length) {
|
|
30
|
-
const serializedTranslations =
|
|
210
|
+
const serializedTranslations = Object.create(null);
|
|
31
211
|
itemTranslations.locales.forEach((locale) => {
|
|
32
212
|
const localeContent = itemTranslations.getAllForLocale(locale);
|
|
33
213
|
if (localeContent) serializedTranslations[locale] = localeContent;
|
|
@@ -75,27 +255,6 @@ var ItemCopyPaste = class ItemCopyPaste {
|
|
|
75
255
|
const parentGroup = this.survey.surveyItems.get(target.parentId);
|
|
76
256
|
if (!parentGroup) throw new Error(`Parent item with id '${target.parentId}' not found`);
|
|
77
257
|
if (!(parentGroup instanceof GroupItemCore)) throw new Error(`Parent item '${target.parentId}' is not a group item`);
|
|
78
|
-
const siblings = parentGroup.items.map((id) => this.survey.surveyItems.get(id)).filter((s) => s !== void 0);
|
|
79
|
-
const siblingKeys = new Set(siblings.map((s) => s.key));
|
|
80
|
-
let uniqueKey = item.key;
|
|
81
|
-
if (siblingKeys.has(uniqueKey)) {
|
|
82
|
-
let counter = 1;
|
|
83
|
-
const originalKey = item.key;
|
|
84
|
-
while (siblingKeys.has(uniqueKey)) {
|
|
85
|
-
uniqueKey = originalKey + `_${counter}`;
|
|
86
|
-
counter++;
|
|
87
|
-
}
|
|
88
|
-
if (uniqueKey !== item.key) {
|
|
89
|
-
const updatedRawItem = {
|
|
90
|
-
...item.rawItem,
|
|
91
|
-
key: uniqueKey
|
|
92
|
-
};
|
|
93
|
-
const newItem = this.survey.createItemFromRaw(updatedRawItem);
|
|
94
|
-
this.survey.surveyItems.delete(item.id);
|
|
95
|
-
this.survey.surveyItems.set(newItem.id, newItem);
|
|
96
|
-
item = newItem;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
258
|
const insertIndex = target.index !== void 0 ? Math.min(target.index, parentGroup.items.length) : parentGroup.items.length;
|
|
100
259
|
parentGroup.addChild(item.id, insertIndex);
|
|
101
260
|
}
|
|
@@ -108,21 +267,49 @@ var ItemCopyPaste = class ItemCopyPaste {
|
|
|
108
267
|
*/
|
|
109
268
|
pasteItem(clipboardData, target) {
|
|
110
269
|
if (!ItemCopyPaste.isValidClipboardData(clipboardData)) throw new Error("Invalid clipboard data format");
|
|
111
|
-
const
|
|
270
|
+
const itemIds = /* @__PURE__ */ new Set();
|
|
271
|
+
for (const { itemId, itemData } of clipboardData.items) {
|
|
272
|
+
if (itemIds.has(itemId) || itemId !== itemData.id) throw new Error(`Ambiguous clipboard item identity: ${itemId}`);
|
|
273
|
+
itemIds.add(itemId);
|
|
274
|
+
}
|
|
275
|
+
if (!itemIds.has(clipboardData.rootItemId)) throw new Error("Clipboard root item is missing");
|
|
276
|
+
if (!(this.survey.surveyItems.get(target.parentId) instanceof GroupItemCore)) throw new Error(`Paste target '${target.parentId}' must be a group item`);
|
|
277
|
+
const idMapping = Object.create(null);
|
|
112
278
|
clipboardData.items.forEach(({ itemId }) => {
|
|
113
279
|
idMapping[itemId] = generateId();
|
|
114
280
|
});
|
|
115
|
-
|
|
281
|
+
const componentMapping = Object.create(null);
|
|
282
|
+
const copiedSlotIds = /* @__PURE__ */ new Set();
|
|
283
|
+
clipboardData.items.forEach(({ itemData }) => {
|
|
284
|
+
const item = this.survey.createItemFromRaw(itemData);
|
|
285
|
+
for (const slot of item.getResponseSlotDefinitions()) {
|
|
286
|
+
if (copiedSlotIds.has(slot.slotId)) throw new Error(`Duplicate copied slot ID: ${slot.slotId}`);
|
|
287
|
+
copiedSlotIds.add(slot.slotId);
|
|
288
|
+
}
|
|
289
|
+
const declaredIds = item.getResponseSlotDefinitions().flatMap((slot) => [
|
|
290
|
+
slot.slotId,
|
|
291
|
+
slot.primitiveId,
|
|
292
|
+
slot.rowId,
|
|
293
|
+
slot.columnId
|
|
294
|
+
].filter((id) => !!id));
|
|
295
|
+
for (const id of new Set([...item.getOwnedComponentIds(), ...declaredIds].filter((id) => id !== item.id))) {
|
|
296
|
+
if (componentMapping[id]) throw new Error(`Duplicate owned component ID: ${id}`);
|
|
297
|
+
componentMapping[id] = generateId();
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
const stagedItems = clipboardData.items.map(({ itemId, itemData }) => {
|
|
116
301
|
const newId = idMapping[itemId];
|
|
117
302
|
if (newId) {
|
|
118
|
-
const updatedItemData = this.updateItemIdsInData(itemData, idMapping);
|
|
303
|
+
const updatedItemData = this.updateItemIdsInData(itemData, idMapping, componentMapping);
|
|
119
304
|
updatedItemData.id = newId;
|
|
120
|
-
|
|
121
|
-
this.survey.surveyItems.set(newItem.id, newItem);
|
|
122
|
-
if (itemId === clipboardData.rootItemId) this.addItemToParentGroup(target, newItem);
|
|
305
|
+
return this.survey.createItemFromRaw(updatedItemData);
|
|
123
306
|
}
|
|
307
|
+
throw new Error(`Missing copied identity for '${itemId}'`);
|
|
124
308
|
});
|
|
125
|
-
|
|
309
|
+
this.lastResponseNameChanges = allocateCopiedResponseNames(this.survey, stagedItems);
|
|
310
|
+
for (const item of stagedItems) this.survey.surveyItems.set(item.id, item);
|
|
311
|
+
this.addItemToParentGroup(target, stagedItems.find((item) => item.id === idMapping[clipboardData.rootItemId]));
|
|
312
|
+
const updatedTranslations = this.updateTranslations(clipboardData.translations, idMapping, componentMapping);
|
|
126
313
|
Object.keys(updatedTranslations).forEach((itemId) => {
|
|
127
314
|
const itemTranslations = new SurveyItemTranslations();
|
|
128
315
|
const serializedData = updatedTranslations[itemId];
|
|
@@ -136,12 +323,47 @@ var ItemCopyPaste = class ItemCopyPaste {
|
|
|
136
323
|
/**
|
|
137
324
|
* Update all item ids in the raw item data recursively
|
|
138
325
|
*/
|
|
139
|
-
updateItemIdsInData(itemData, idMapping) {
|
|
326
|
+
updateItemIdsInData(itemData, idMapping, componentMapping) {
|
|
140
327
|
const updatedData = JSON.parse(JSON.stringify(itemData));
|
|
141
328
|
if (updatedData.itemType === "group" && updatedData.config) {
|
|
142
329
|
const config = updatedData.config;
|
|
143
330
|
if (config.items) config.items = config.items.map((childId) => idMapping[childId] ?? childId);
|
|
144
331
|
}
|
|
332
|
+
const remap = (value, key) => {
|
|
333
|
+
if (typeof value === "string") {
|
|
334
|
+
if ([
|
|
335
|
+
"key",
|
|
336
|
+
"prefix",
|
|
337
|
+
"variableName",
|
|
338
|
+
"rowName",
|
|
339
|
+
"columnName",
|
|
340
|
+
"code",
|
|
341
|
+
"label",
|
|
342
|
+
"description",
|
|
343
|
+
"errorMessage",
|
|
344
|
+
"placeholder",
|
|
345
|
+
"unit"
|
|
346
|
+
].includes(key ?? "")) return value;
|
|
347
|
+
if ((key === "id" || key === "slotId") && idMapping[value]) return idMapping[value];
|
|
348
|
+
return componentMapping[value] ?? value;
|
|
349
|
+
}
|
|
350
|
+
if (Array.isArray(value)) return value.map((entry) => remap(entry));
|
|
351
|
+
if (value && typeof value === "object" && value.type === "previousResponse") return structuredClone(value);
|
|
352
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([name, entry]) => [componentMapping[name] ?? idMapping[name] ?? name, name === "value" && (typeof entry === "string" || Array.isArray(entry)) && value.type !== ValueType.reference && value.type !== ValueType.referenceArray ? entry : remap(entry, name)]));
|
|
353
|
+
return value;
|
|
354
|
+
};
|
|
355
|
+
updatedData.config = remap(updatedData.config);
|
|
356
|
+
if (updatedData.responseSettings) updatedData.responseSettings = Object.fromEntries(Object.entries(updatedData.responseSettings).map(([slotId, settings]) => {
|
|
357
|
+
const value = structuredClone(settings);
|
|
358
|
+
for (const name of ["optionCodes", "optionLabels"]) if (value[name]) value[name] = Object.fromEntries(Object.entries(value[name]).map(([id, code]) => [componentMapping[id] ?? id, code]));
|
|
359
|
+
return [componentMapping[slotId] ?? idMapping[slotId] ?? slotId, value];
|
|
360
|
+
}));
|
|
361
|
+
if (Object.keys(componentMapping).length) {
|
|
362
|
+
updatedData.displayConditions = remap(updatedData.displayConditions);
|
|
363
|
+
updatedData.disabledConditions = remap(updatedData.disabledConditions);
|
|
364
|
+
updatedData.validations = remap(updatedData.validations);
|
|
365
|
+
updatedData.prefills = remap(updatedData.prefills);
|
|
366
|
+
}
|
|
145
367
|
this.updateExpressionsInItemData(updatedData, idMapping);
|
|
146
368
|
return updatedData;
|
|
147
369
|
}
|
|
@@ -149,6 +371,12 @@ var ItemCopyPaste = class ItemCopyPaste {
|
|
|
149
371
|
* Update expressions in item data that reference the old ids
|
|
150
372
|
*/
|
|
151
373
|
updateExpressionsInItemData(itemData, idMapping) {
|
|
374
|
+
const visitBounds = (value) => {
|
|
375
|
+
if (!value || typeof value !== "object") return;
|
|
376
|
+
for (const [key, child] of Object.entries(value)) if (key === "valueExpression" && child && typeof child === "object") this.updateExpressionReferences(child, idMapping);
|
|
377
|
+
else visitBounds(child);
|
|
378
|
+
};
|
|
379
|
+
visitBounds(itemData.config);
|
|
152
380
|
if (itemData.displayConditions?.root) this.updateExpressionReferences(itemData.displayConditions.root, idMapping);
|
|
153
381
|
if (itemData.displayConditions?.components) Object.values(itemData.displayConditions.components).forEach((expr) => {
|
|
154
382
|
if (expr) this.updateExpressionReferences(expr, idMapping);
|
|
@@ -162,7 +390,6 @@ var ItemCopyPaste = class ItemCopyPaste {
|
|
|
162
390
|
if (itemData.prefills) itemData.prefills.forEach((prefill) => {
|
|
163
391
|
if (prefill.when) this.updateExpressionReferences(prefill.when, idMapping);
|
|
164
392
|
if (prefill.source.type === "expression") this.updateExpressionReferences(prefill.source.expression, idMapping);
|
|
165
|
-
if (prefill.source.type === "previousResponse") prefill.source.ref.itemId = idMapping[prefill.source.ref.itemId] ?? prefill.source.ref.itemId;
|
|
166
393
|
});
|
|
167
394
|
}
|
|
168
395
|
/**
|
|
@@ -171,6 +398,10 @@ var ItemCopyPaste = class ItemCopyPaste {
|
|
|
171
398
|
updateExpressionReferences(expression, idMapping) {
|
|
172
399
|
if (!expression || typeof expression !== "object") return;
|
|
173
400
|
const expr = expression;
|
|
401
|
+
if (expr.type === ExpressionType.ResponseVariable && expr.variableRef && typeof expr.variableRef === "object") {
|
|
402
|
+
const reference = expr.variableRef;
|
|
403
|
+
if (reference.slotId && Object.prototype.hasOwnProperty.call(idMapping, reference.slotId)) reference.slotId = idMapping[reference.slotId];
|
|
404
|
+
}
|
|
174
405
|
if (Array.isArray(expr.data)) expr.data.forEach((arg) => {
|
|
175
406
|
if (arg && typeof arg === "object" && arg !== null && "str" in arg) {
|
|
176
407
|
const argObj = arg;
|
|
@@ -186,12 +417,12 @@ var ItemCopyPaste = class ItemCopyPaste {
|
|
|
186
417
|
/**
|
|
187
418
|
* Update translation keys for the pasted item
|
|
188
419
|
*/
|
|
189
|
-
updateTranslations(translations, idMapping) {
|
|
190
|
-
const newTranslations =
|
|
420
|
+
updateTranslations(translations, idMapping, componentMapping) {
|
|
421
|
+
const newTranslations = Object.create(null);
|
|
191
422
|
Object.keys(translations).forEach((itemId) => {
|
|
192
423
|
const itemTranslations = translations[itemId];
|
|
193
424
|
const newItemId = idMapping[itemId];
|
|
194
|
-
newTranslations[newItemId] = itemTranslations;
|
|
425
|
+
newTranslations[newItemId] = Object.fromEntries(Object.entries(itemTranslations).map(([locale, content]) => [locale, Object.fromEntries(Object.entries(content).map(([key, value]) => [key.split(".").map((part) => componentMapping[part] ?? part).join("."), value]))]));
|
|
195
426
|
});
|
|
196
427
|
return newTranslations;
|
|
197
428
|
}
|
|
@@ -243,6 +474,7 @@ function normalizeHistoryEntry(entry) {
|
|
|
243
474
|
if (entry.kind === "survey-snapshot") return {
|
|
244
475
|
kind: "survey-snapshot",
|
|
245
476
|
survey: structuredCloneMethod(entry.survey),
|
|
477
|
+
id: entry.id ?? generateId(),
|
|
246
478
|
timestamp: entry.timestamp,
|
|
247
479
|
meta: entry.meta,
|
|
248
480
|
memorySize: entry.memorySize
|
|
@@ -250,6 +482,7 @@ function normalizeHistoryEntry(entry) {
|
|
|
250
482
|
return {
|
|
251
483
|
kind: "asset-change",
|
|
252
484
|
changes: structuredCloneMethod(entry.changes),
|
|
485
|
+
id: entry.id ?? generateId(),
|
|
253
486
|
timestamp: entry.timestamp,
|
|
254
487
|
meta: entry.meta,
|
|
255
488
|
memorySize: entry.memorySize
|
|
@@ -258,6 +491,7 @@ function normalizeHistoryEntry(entry) {
|
|
|
258
491
|
return {
|
|
259
492
|
kind: "survey-snapshot",
|
|
260
493
|
survey: structuredCloneMethod(entry.survey),
|
|
494
|
+
id: entry.id ?? generateId(),
|
|
261
495
|
timestamp: entry.timestamp,
|
|
262
496
|
meta: entry.meta,
|
|
263
497
|
memorySize: entry.memorySize
|
|
@@ -267,6 +501,7 @@ var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
|
|
|
267
501
|
history = [];
|
|
268
502
|
currentIndex = -1;
|
|
269
503
|
_config;
|
|
504
|
+
baseAssetSize;
|
|
270
505
|
_initialAssets;
|
|
271
506
|
constructor(initialSurvey, config = {}, meta = {
|
|
272
507
|
label: "Initial state",
|
|
@@ -293,6 +528,7 @@ var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
|
|
|
293
528
|
this.history = this.history.slice(0, this.currentIndex + 1);
|
|
294
529
|
this.history.push({
|
|
295
530
|
...structuredCloneMethod(entry),
|
|
531
|
+
id: generateId(),
|
|
296
532
|
timestamp: Date.now(),
|
|
297
533
|
memorySize
|
|
298
534
|
});
|
|
@@ -302,12 +538,12 @@ var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
|
|
|
302
538
|
saveSnapshot(survey, meta) {
|
|
303
539
|
this.saveEntry({
|
|
304
540
|
kind: "survey-snapshot",
|
|
305
|
-
survey
|
|
541
|
+
survey,
|
|
306
542
|
meta
|
|
307
543
|
});
|
|
308
544
|
}
|
|
309
545
|
getBaseAssetsSize() {
|
|
310
|
-
return this._initialAssets ? MemoryCalculator.calculateSize(this._initialAssets) : 0;
|
|
546
|
+
return this.baseAssetSize ??= this._initialAssets ? MemoryCalculator.calculateSize(this._initialAssets) : 0;
|
|
311
547
|
}
|
|
312
548
|
cleanupHistory() {
|
|
313
549
|
const maxMemoryBytes = this._config.maxTotalMemoryMB * 1024 * 1024;
|
|
@@ -321,6 +557,7 @@ var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
|
|
|
321
557
|
const nextEntry = this.history[nextStateIndex];
|
|
322
558
|
const newFirstEntry = {
|
|
323
559
|
kind: "survey-snapshot",
|
|
560
|
+
id: nextEntry.id,
|
|
324
561
|
survey: nextStateSurvey,
|
|
325
562
|
meta: nextEntry.meta,
|
|
326
563
|
timestamp: nextEntry.timestamp,
|
|
@@ -330,8 +567,9 @@ var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
|
|
|
330
567
|
})
|
|
331
568
|
};
|
|
332
569
|
const removedBytes = this.history[0].memorySize;
|
|
333
|
-
this._initialAssets =
|
|
334
|
-
this.
|
|
570
|
+
this._initialAssets = nextStateAssets;
|
|
571
|
+
this.baseAssetSize = void 0;
|
|
572
|
+
this.history = [newFirstEntry, ...this.history.slice(2)];
|
|
335
573
|
this.currentIndex--;
|
|
336
574
|
return removedBytes;
|
|
337
575
|
}
|
|
@@ -347,7 +585,7 @@ var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
|
|
|
347
585
|
throw new Error("Invalid history state");
|
|
348
586
|
}
|
|
349
587
|
applyAssetPatch(assets, patch) {
|
|
350
|
-
const nextAssets =
|
|
588
|
+
const nextAssets = assets;
|
|
351
589
|
if (patch.next) nextAssets[patch.assetId] = structuredCloneMethod(patch.next);
|
|
352
590
|
else delete nextAssets[patch.assetId];
|
|
353
591
|
return nextAssets;
|
|
@@ -362,6 +600,12 @@ var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
|
|
|
362
600
|
}
|
|
363
601
|
return Object.keys(assets).length > 0 ? assets : void 0;
|
|
364
602
|
}
|
|
603
|
+
getEntryId(index) {
|
|
604
|
+
return this.history[index]?.id;
|
|
605
|
+
}
|
|
606
|
+
findEntryIndex(id) {
|
|
607
|
+
return this.history.findIndex((entry) => entry.id === id);
|
|
608
|
+
}
|
|
365
609
|
commit(survey, meta) {
|
|
366
610
|
this.saveSnapshot(survey, meta);
|
|
367
611
|
}
|
|
@@ -407,7 +651,7 @@ var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
|
|
|
407
651
|
}
|
|
408
652
|
getMemoryUsage() {
|
|
409
653
|
return {
|
|
410
|
-
totalMB: this.getTotalMemoryUsage() /
|
|
654
|
+
totalMB: this.getTotalMemoryUsage() / 1048576,
|
|
411
655
|
entries: this.history.length
|
|
412
656
|
};
|
|
413
657
|
}
|
|
@@ -422,6 +666,7 @@ var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
|
|
|
422
666
|
index,
|
|
423
667
|
kind: entry.kind,
|
|
424
668
|
meta: entry.meta,
|
|
669
|
+
id: entry.id ?? generateId(),
|
|
425
670
|
timestamp: entry.timestamp,
|
|
426
671
|
memorySize: entry.memorySize,
|
|
427
672
|
isCurrent: index === this.currentIndex
|
|
@@ -440,6 +685,7 @@ var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
|
|
|
440
685
|
index,
|
|
441
686
|
kind: entry.kind,
|
|
442
687
|
meta: entry.meta,
|
|
688
|
+
id: entry.id ?? generateId(),
|
|
443
689
|
timestamp: entry.timestamp,
|
|
444
690
|
memorySize: entry.memorySize,
|
|
445
691
|
isCurrent: index === this.currentIndex
|
|
@@ -484,6 +730,7 @@ var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
|
|
|
484
730
|
if (entry.kind === "survey-snapshot") return {
|
|
485
731
|
kind: "survey-snapshot",
|
|
486
732
|
survey: entry.survey,
|
|
733
|
+
id: entry.id ?? generateId(),
|
|
487
734
|
timestamp: entry.timestamp,
|
|
488
735
|
meta: entry.meta,
|
|
489
736
|
memorySize: entry.memorySize
|
|
@@ -491,6 +738,7 @@ var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
|
|
|
491
738
|
return {
|
|
492
739
|
kind: "asset-change",
|
|
493
740
|
changes: entry.changes,
|
|
741
|
+
id: entry.id ?? generateId(),
|
|
494
742
|
timestamp: entry.timestamp,
|
|
495
743
|
meta: entry.meta,
|
|
496
744
|
memorySize: entry.memorySize
|
|
@@ -512,9 +760,10 @@ var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
|
|
|
512
760
|
if (!jsonData.config) throw new Error("Invalid object: config is required");
|
|
513
761
|
const normalizedHistory = jsonData.history.map(normalizeHistoryEntry);
|
|
514
762
|
const firstEntry = normalizedHistory[0];
|
|
515
|
-
const
|
|
763
|
+
const firstSurvey = firstEntry.kind === "survey-snapshot" ? firstEntry.survey : (() => {
|
|
516
764
|
throw new Error("Invalid object: first history entry must resolve to a survey snapshot");
|
|
517
|
-
})()
|
|
765
|
+
})();
|
|
766
|
+
const instance = new SurveyEditorUndoRedo(firstSurvey, jsonData.config, firstEntry.meta, initialAssetsOverride ?? jsonData.initialAssets);
|
|
518
767
|
instance.history = normalizedHistory;
|
|
519
768
|
instance.currentIndex = jsonData.currentIndex;
|
|
520
769
|
instance._initialAssets = cloneAssets(initialAssetsOverride ?? jsonData.initialAssets);
|
|
@@ -524,8 +773,7 @@ var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
|
|
|
524
773
|
//#endregion
|
|
525
774
|
//#region src/editor/survey-editor.ts
|
|
526
775
|
function stripAssetsFromSurveySnapshot(survey) {
|
|
527
|
-
const snapshot =
|
|
528
|
-
delete snapshot.assets;
|
|
776
|
+
const { assets: _assets, ...snapshot } = survey;
|
|
529
777
|
return snapshot;
|
|
530
778
|
}
|
|
531
779
|
function mergeSurveySnapshotWithAssets(snapshot, assets) {
|
|
@@ -535,45 +783,123 @@ function mergeSurveySnapshotWithAssets(snapshot, assets) {
|
|
|
535
783
|
return merged;
|
|
536
784
|
}
|
|
537
785
|
var SurveyEditor = class SurveyEditor {
|
|
786
|
+
lastResponseNameChanges = [];
|
|
787
|
+
renameResponseVariables(edits) {
|
|
788
|
+
const preview = previewResponseNaming(this._survey, edits);
|
|
789
|
+
if (!preview.changes.length) return [];
|
|
790
|
+
const next = Survey.fromJson(preview.survey, this._survey.getPluginRegistry());
|
|
791
|
+
this._survey = next;
|
|
792
|
+
for (const id of new Set(preview.changes.map((change) => change.itemId))) this.touchItemModifiedMetadata(id, this.createEditorTimestamp());
|
|
793
|
+
this.lastResponseNameChanges = preview.changes;
|
|
794
|
+
this.markAsModified();
|
|
795
|
+
return preview.changes;
|
|
796
|
+
}
|
|
797
|
+
updateResponseSettings(naming, settings) {
|
|
798
|
+
const preview = previewResponseSettings(this._survey, naming, settings);
|
|
799
|
+
this._survey = Survey.fromJson(preview.survey, this._survey.getPluginRegistry());
|
|
800
|
+
const changedItems = /* @__PURE__ */ new Set([...preview.changes.map((change) => change.itemId), ...settings.map((edit) => this._survey.getResponseSlotRegistry().get(edit.slotId).itemId)]);
|
|
801
|
+
for (const itemId of changedItems) this.touchItemModifiedMetadata(itemId, this.createEditorTimestamp());
|
|
802
|
+
this.lastResponseNameChanges = preview.changes;
|
|
803
|
+
this.markAsModified();
|
|
804
|
+
return preview.changes;
|
|
805
|
+
}
|
|
538
806
|
_survey;
|
|
539
807
|
_undoRedo;
|
|
540
808
|
_hasUncommittedChanges = false;
|
|
809
|
+
mutationEpoch = 0;
|
|
810
|
+
pendingAssets = /* @__PURE__ */ new Map();
|
|
541
811
|
_pluginRegistry;
|
|
542
812
|
constructor(survey, config = {}, meta) {
|
|
543
813
|
this._survey = survey;
|
|
544
814
|
const { pluginRegistry, ...undoRedoConfig } = config;
|
|
545
|
-
this._pluginRegistry = pluginRegistry;
|
|
815
|
+
this._pluginRegistry = pluginRegistry ?? survey.getPluginRegistry();
|
|
546
816
|
const serialized = survey.serialize();
|
|
547
817
|
this._undoRedo = new SurveyEditorUndoRedo(stripAssetsFromSurveySnapshot(serialized), undoRedoConfig, meta, serialized.assets);
|
|
548
818
|
}
|
|
819
|
+
/**
|
|
820
|
+
* An isolated field edit owns only its field value (held by the caller) and
|
|
821
|
+
* affected item metadata. Any intervening core action invalidates this handle.
|
|
822
|
+
* The caller must finish pre-existing work before opening a handle.
|
|
823
|
+
*/
|
|
824
|
+
beginFieldEdit(itemId) {
|
|
825
|
+
if (this.hasUncommittedChanges) throw new Error("Finish pending changes before editing a field");
|
|
826
|
+
let expectedEpoch = this.mutationEpoch;
|
|
827
|
+
const history = this._undoRedo;
|
|
828
|
+
const index = history.getCurrentIndex();
|
|
829
|
+
const locales = this._survey.translations.locales;
|
|
830
|
+
const metadata = itemId ? structuredCloneMethod(this._survey.surveyItems.get(itemId)?.rawItem.editorMetadata) : void 0;
|
|
831
|
+
const owns = () => this.mutationEpoch === expectedEpoch && this._undoRedo === history && history.getCurrentIndex() === index;
|
|
832
|
+
return {
|
|
833
|
+
run: (write) => {
|
|
834
|
+
if (!owns()) throw new Error("Field edit was superseded by another action");
|
|
835
|
+
const result = write();
|
|
836
|
+
expectedEpoch = this.mutationEpoch;
|
|
837
|
+
return result;
|
|
838
|
+
},
|
|
839
|
+
reconcile: (restoreValue) => {
|
|
840
|
+
if (!owns()) return false;
|
|
841
|
+
restoreValue?.();
|
|
842
|
+
if (itemId) {
|
|
843
|
+
const item = this._survey.surveyItems.get(itemId);
|
|
844
|
+
if (!item) return false;
|
|
845
|
+
this._survey.surveyItems.set(itemId, this._survey.createItemFromRaw({
|
|
846
|
+
...item.rawItem,
|
|
847
|
+
editorMetadata: structuredCloneMethod(metadata)
|
|
848
|
+
}));
|
|
849
|
+
}
|
|
850
|
+
for (const locale of this._survey.translations.locales) if (!locales.includes(locale)) {
|
|
851
|
+
const data = this._survey.translations.serialize()?.[locale];
|
|
852
|
+
if (data && !data.surveyCardContent && !data.navigationContent && !data.validationMessages && Object.keys(data.itemTranslations ?? {}).length === 0) this._survey.translations.removeLocale(locale);
|
|
853
|
+
}
|
|
854
|
+
this._hasUncommittedChanges = false;
|
|
855
|
+
this.mutationEpoch++;
|
|
856
|
+
expectedEpoch = this.mutationEpoch;
|
|
857
|
+
return true;
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
get hasCommittedChanges() {
|
|
862
|
+
return this._undoRedo.getCurrentIndex() > 0;
|
|
863
|
+
}
|
|
549
864
|
/** Returns an immutable copy of the current survey state. */
|
|
550
865
|
get survey() {
|
|
551
|
-
const serialized = this._survey.serialize();
|
|
866
|
+
const serialized = structuredCloneMethod(this._survey.serialize());
|
|
552
867
|
const registry = this._pluginRegistry ?? this._survey.getPluginRegistry();
|
|
553
868
|
return Survey.fromJson(serialized, registry);
|
|
554
869
|
}
|
|
555
870
|
get hasUncommittedChanges() {
|
|
556
|
-
return this._hasUncommittedChanges;
|
|
871
|
+
return this._hasUncommittedChanges || this.pendingAssets.size > 0;
|
|
557
872
|
}
|
|
558
873
|
get undoRedo() {
|
|
559
874
|
return this._undoRedo;
|
|
560
875
|
}
|
|
561
876
|
commit(meta) {
|
|
562
|
-
if (!this.
|
|
563
|
-
this.
|
|
877
|
+
if (!this.hasUncommittedChanges) return;
|
|
878
|
+
this.mutationEpoch++;
|
|
879
|
+
if (this._hasUncommittedChanges) this._undoRedo.commit(stripAssetsFromSurveySnapshot(this._survey.serialize()), meta);
|
|
880
|
+
if (this.pendingAssets.size) {
|
|
881
|
+
this._undoRedo.commitAssetChange([...this.pendingAssets].map(([assetId, patch]) => ({
|
|
882
|
+
assetId,
|
|
883
|
+
...patch
|
|
884
|
+
})), meta);
|
|
885
|
+
this.pendingAssets.clear();
|
|
886
|
+
}
|
|
564
887
|
this._hasUncommittedChanges = false;
|
|
565
888
|
}
|
|
566
889
|
restoreHistoryState(snapshot, assets) {
|
|
890
|
+
this.mutationEpoch++;
|
|
891
|
+
this.lastResponseNameChanges = [];
|
|
892
|
+
this.pendingAssets.clear();
|
|
567
893
|
this._survey = Survey.fromJson(mergeSurveySnapshotWithAssets(snapshot, assets), this._pluginRegistry);
|
|
568
894
|
}
|
|
569
895
|
commitIfNeeded() {
|
|
570
|
-
if (this.
|
|
896
|
+
if (this.hasUncommittedChanges) this.commit({
|
|
571
897
|
label: "Latest content changes",
|
|
572
898
|
source: CommitSource.SYSTEM
|
|
573
899
|
});
|
|
574
900
|
}
|
|
575
901
|
undo() {
|
|
576
|
-
if (this.
|
|
902
|
+
if (this.hasUncommittedChanges) {
|
|
577
903
|
this.restoreHistoryState(this._undoRedo.getCurrentState(), this._undoRedo.getCurrentAssets());
|
|
578
904
|
this._hasUncommittedChanges = false;
|
|
579
905
|
return true;
|
|
@@ -588,7 +914,7 @@ var SurveyEditor = class SurveyEditor {
|
|
|
588
914
|
}
|
|
589
915
|
}
|
|
590
916
|
redo() {
|
|
591
|
-
if (this.
|
|
917
|
+
if (this.hasUncommittedChanges) return false;
|
|
592
918
|
const nextState = this._undoRedo.redo();
|
|
593
919
|
if (nextState) {
|
|
594
920
|
this.restoreHistoryState(nextState, this._undoRedo.getCurrentAssets());
|
|
@@ -601,7 +927,7 @@ var SurveyEditor = class SurveyEditor {
|
|
|
601
927
|
* Jump to a specific index in the history (can go forward or backward)
|
|
602
928
|
*/
|
|
603
929
|
jumpToIndex(targetIndex) {
|
|
604
|
-
if (this.
|
|
930
|
+
if (this.hasUncommittedChanges) return false;
|
|
605
931
|
const targetState = this._undoRedo.jumpToIndex(targetIndex);
|
|
606
932
|
if (targetState) {
|
|
607
933
|
this.restoreHistoryState(targetState, this._undoRedo.getCurrentAssets());
|
|
@@ -611,20 +937,20 @@ var SurveyEditor = class SurveyEditor {
|
|
|
611
937
|
return false;
|
|
612
938
|
}
|
|
613
939
|
canUndo() {
|
|
614
|
-
return this.
|
|
940
|
+
return this.hasUncommittedChanges || this._undoRedo.canUndo();
|
|
615
941
|
}
|
|
616
942
|
canRedo() {
|
|
617
|
-
return !this.
|
|
943
|
+
return !this.hasUncommittedChanges && this._undoRedo.canRedo();
|
|
618
944
|
}
|
|
619
945
|
getUndoMeta() {
|
|
620
|
-
if (this.
|
|
946
|
+
if (this.hasUncommittedChanges) return {
|
|
621
947
|
label: "Latest content changes",
|
|
622
948
|
source: CommitSource.SYSTEM
|
|
623
949
|
};
|
|
624
950
|
return this._undoRedo.getUndoMeta();
|
|
625
951
|
}
|
|
626
952
|
getRedoMeta() {
|
|
627
|
-
if (this.
|
|
953
|
+
if (this.hasUncommittedChanges) return null;
|
|
628
954
|
return this._undoRedo.getRedoMeta();
|
|
629
955
|
}
|
|
630
956
|
getMemoryUsage() {
|
|
@@ -646,9 +972,9 @@ var SurveyEditor = class SurveyEditor {
|
|
|
646
972
|
toJson() {
|
|
647
973
|
return {
|
|
648
974
|
version: "1.0.0",
|
|
649
|
-
survey: this._survey.serialize(),
|
|
975
|
+
survey: structuredCloneMethod(this._survey.serialize()),
|
|
650
976
|
undoRedo: this._undoRedo.serialize(),
|
|
651
|
-
hasUncommittedChanges: this.
|
|
977
|
+
hasUncommittedChanges: this.hasUncommittedChanges
|
|
652
978
|
};
|
|
653
979
|
}
|
|
654
980
|
/**
|
|
@@ -662,7 +988,8 @@ var SurveyEditor = class SurveyEditor {
|
|
|
662
988
|
if (!jsonData.undoRedo) throw new Error("Invalid object: undoRedo is required");
|
|
663
989
|
if (typeof jsonData.hasUncommittedChanges !== "boolean") throw new Error("Invalid object: hasUncommittedChanges must be a boolean");
|
|
664
990
|
if (jsonData.version && !jsonData.version.startsWith("1.")) console.warn(`Warning: Loading SurveyEditor with version ${jsonData.version}, current version is 1.0.0`);
|
|
665
|
-
const
|
|
991
|
+
const survey = Survey.fromJson(jsonData.survey, pluginRegistry);
|
|
992
|
+
const editor = new SurveyEditor(survey, { pluginRegistry });
|
|
666
993
|
editor._undoRedo = SurveyEditorUndoRedo.deserialize({
|
|
667
994
|
...jsonData.undoRedo,
|
|
668
995
|
history: jsonData.undoRedo.history.map((entry) => "survey" in entry ? {
|
|
@@ -671,9 +998,16 @@ var SurveyEditor = class SurveyEditor {
|
|
|
671
998
|
} : entry)
|
|
672
999
|
}, jsonData.survey.assets);
|
|
673
1000
|
editor._hasUncommittedChanges = JSON.stringify(stripAssetsFromSurveySnapshot(jsonData.survey)) !== JSON.stringify(editor._undoRedo.getCurrentState());
|
|
1001
|
+
const baselineAssets = editor._undoRedo.getCurrentAssets() ?? {};
|
|
1002
|
+
const liveAssets = jsonData.survey.assets ?? {};
|
|
1003
|
+
for (const assetId of /* @__PURE__ */ new Set([...Object.keys(baselineAssets), ...Object.keys(liveAssets)])) if (JSON.stringify(baselineAssets[assetId]) !== JSON.stringify(liveAssets[assetId])) editor.pendingAssets.set(assetId, {
|
|
1004
|
+
prev: structuredCloneMethod(baselineAssets[assetId]),
|
|
1005
|
+
next: structuredCloneMethod(liveAssets[assetId])
|
|
1006
|
+
});
|
|
674
1007
|
return editor;
|
|
675
1008
|
}
|
|
676
1009
|
markAsModified() {
|
|
1010
|
+
this.mutationEpoch++;
|
|
677
1011
|
this._hasUncommittedChanges = true;
|
|
678
1012
|
}
|
|
679
1013
|
createEditorTimestamp() {
|
|
@@ -686,7 +1020,6 @@ var SurveyEditor = class SurveyEditor {
|
|
|
686
1020
|
}
|
|
687
1021
|
addItem(target, item, content) {
|
|
688
1022
|
if (this._survey.surveyItems.has(item.id)) throw new Error(`Item with id '${item.id}' already exists in the survey`);
|
|
689
|
-
this.markAsModified();
|
|
690
1023
|
const timestamp = this.createEditorTimestamp();
|
|
691
1024
|
let parentGroup;
|
|
692
1025
|
if (!target) {
|
|
@@ -701,21 +1034,11 @@ var SurveyEditor = class SurveyEditor {
|
|
|
701
1034
|
parentGroup = targetItem;
|
|
702
1035
|
}
|
|
703
1036
|
if (parentGroup.hasChild(item.id)) throw new Error(`Item ${item.id} already in this group`);
|
|
704
|
-
const siblings = Array.from(this._survey.surveyItems.values()).filter((sItem) => parentGroup.items?.includes(sItem.id));
|
|
705
|
-
let counter = 1;
|
|
706
|
-
const originalKey = item.key;
|
|
707
|
-
let uniqueKey = originalKey;
|
|
708
|
-
while (siblings.some((sibling) => sibling.key === uniqueKey)) {
|
|
709
|
-
uniqueKey = `${originalKey}_${counter}`;
|
|
710
|
-
counter++;
|
|
711
|
-
}
|
|
712
1037
|
let insertIndex;
|
|
713
1038
|
if (target?.index !== void 0) insertIndex = Math.min(target.index, parentGroup.items.length);
|
|
714
1039
|
else insertIndex = parentGroup.items.length;
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
key: uniqueKey
|
|
718
|
-
});
|
|
1040
|
+
this.lastResponseNameChanges = allocateCopiedResponseNames(this._survey, [item]);
|
|
1041
|
+
this.markAsModified();
|
|
719
1042
|
item.setCreatedAt(timestamp);
|
|
720
1043
|
item.setModifiedAt(timestamp);
|
|
721
1044
|
this._survey.surveyItems.set(item.id, item);
|
|
@@ -742,8 +1065,7 @@ var SurveyEditor = class SurveyEditor {
|
|
|
742
1065
|
moveItem(itemId, newTarget) {
|
|
743
1066
|
this.markAsModified();
|
|
744
1067
|
const timestamp = this.createEditorTimestamp();
|
|
745
|
-
|
|
746
|
-
if (!item) throw new Error(`Item with id '${itemId}' not found`);
|
|
1068
|
+
if (!this._survey.surveyItems.get(itemId)) throw new Error(`Item with id '${itemId}' not found`);
|
|
747
1069
|
const targetItem = this._survey.surveyItems.get(newTarget.parentId);
|
|
748
1070
|
if (!targetItem) throw new Error(`Target parent with id '${newTarget.parentId}' not found`);
|
|
749
1071
|
if (!(targetItem instanceof GroupItemCore)) throw new Error(`Target parent '${newTarget.parentId}' is not a group item (${targetItem.type})`);
|
|
@@ -756,17 +1078,6 @@ var SurveyEditor = class SurveyEditor {
|
|
|
756
1078
|
this.touchItemModifiedMetadata(currentParentGroup.id, timestamp);
|
|
757
1079
|
}
|
|
758
1080
|
const targetGroup = targetItem;
|
|
759
|
-
const siblings = Array.from(this._survey.surveyItems.values()).filter((sItem) => targetGroup.hasChild(sItem.id));
|
|
760
|
-
let counter = 1;
|
|
761
|
-
let uniqueKey = item.key;
|
|
762
|
-
while (siblings.some((sibling) => sibling.key === uniqueKey)) {
|
|
763
|
-
uniqueKey = `${item.key}_${counter}`;
|
|
764
|
-
counter++;
|
|
765
|
-
}
|
|
766
|
-
if (uniqueKey !== item.key) item.updateRawItem({
|
|
767
|
-
...item.rawItem,
|
|
768
|
-
key: uniqueKey
|
|
769
|
-
});
|
|
770
1081
|
const insertIndex = newTarget.index !== void 0 ? Math.min(newTarget.index, targetGroup.items.length) : targetGroup.items.length;
|
|
771
1082
|
targetGroup.addChild(itemId, insertIndex);
|
|
772
1083
|
this.touchItemModifiedMetadata(targetGroup.id, timestamp);
|
|
@@ -813,17 +1124,7 @@ var SurveyEditor = class SurveyEditor {
|
|
|
813
1124
|
parent.removeChild(item.id);
|
|
814
1125
|
this.touchItemModifiedMetadata(parent.id, timestamp);
|
|
815
1126
|
}
|
|
816
|
-
const siblingKeys = new Set(targetGroup.items.map((id) => this._survey.surveyItems.get(id)?.key).filter((key) => Boolean(key)));
|
|
817
1127
|
entries.forEach(({ item }, offset) => {
|
|
818
|
-
const originalKey = item.key;
|
|
819
|
-
let uniqueKey = originalKey;
|
|
820
|
-
let counter = 1;
|
|
821
|
-
while (siblingKeys.has(uniqueKey)) uniqueKey = `${originalKey}_${counter++}`;
|
|
822
|
-
if (uniqueKey !== item.key) item.updateRawItem({
|
|
823
|
-
...item.rawItem,
|
|
824
|
-
key: uniqueKey
|
|
825
|
-
});
|
|
826
|
-
siblingKeys.add(uniqueKey);
|
|
827
1128
|
targetGroup.addChild(item.id, insertionIndex + offset);
|
|
828
1129
|
this.touchItemModifiedMetadata(item.id, timestamp);
|
|
829
1130
|
});
|
|
@@ -865,12 +1166,20 @@ var SurveyEditor = class SurveyEditor {
|
|
|
865
1166
|
...rawItemData,
|
|
866
1167
|
id: itemId
|
|
867
1168
|
};
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
const
|
|
872
|
-
if (siblingWithSameKey) throw new Error(`Key '${newKey}' is already in use by sibling item '${siblingWithSameKey.id}'`);
|
|
1169
|
+
if (merged.responseSettings) {
|
|
1170
|
+
merged.responseSettings = { ...merged.responseSettings };
|
|
1171
|
+
const nextSlotIds = new Set(this._survey.createItemFromRaw(merged).getResponseSlotDefinitions().map((slot) => slot.slotId));
|
|
1172
|
+
for (const slot of existingItem.getResponseSlotDefinitions()) if (!nextSlotIds.has(slot.slotId)) delete merged.responseSettings[slot.slotId];
|
|
873
1173
|
}
|
|
1174
|
+
const stagedRaw = this._survey.serialize();
|
|
1175
|
+
stagedRaw.surveyItems = stagedRaw.surveyItems.map((item) => item.id === itemId ? merged : item);
|
|
1176
|
+
const beforeIssues = new Set(this._survey.getResponseSlotRegistry().diagnostics.map((issue) => JSON.stringify(issue)));
|
|
1177
|
+
const stagedSurvey = Survey.fromJson(stagedRaw, this._survey.getPluginRegistry());
|
|
1178
|
+
const introduced = stagedSurvey.getResponseSlotRegistry().diagnostics.filter((issue) => !beforeIssues.has(JSON.stringify(issue)));
|
|
1179
|
+
if (introduced.length) throw new Error(introduced.map((issue) => issue.message).join("\n"));
|
|
1180
|
+
const previousProjectionIssues = new Set(getDefaultProjectionIssues(this._survey));
|
|
1181
|
+
const projectionIssues = getDefaultProjectionIssues(stagedSurvey).filter((issue) => !previousProjectionIssues.has(issue));
|
|
1182
|
+
if (projectionIssues.length) throw new Error(projectionIssues.join("\n"));
|
|
874
1183
|
const newItem = this._survey.createItemFromRaw({
|
|
875
1184
|
...merged,
|
|
876
1185
|
editorMetadata: existingItem.rawItem.editorMetadata
|
|
@@ -879,6 +1188,19 @@ var SurveyEditor = class SurveyEditor {
|
|
|
879
1188
|
this._survey.surveyItems.set(itemId, newItem);
|
|
880
1189
|
this.markAsModified();
|
|
881
1190
|
}
|
|
1191
|
+
/** Detached field read; does not construct a Survey or visit other locales. */
|
|
1192
|
+
getItemTranslationContent(itemId, locale, contentKey, fallbackLocale) {
|
|
1193
|
+
return this._survey.translations.getItemTranslationContent(itemId, locale, contentKey, fallbackLocale);
|
|
1194
|
+
}
|
|
1195
|
+
/** Publish accepted content without committing history. Equivalent writes are no-ops. */
|
|
1196
|
+
setItemTranslationContent(itemId, locale, contentKey, content) {
|
|
1197
|
+
const item = this._survey.surveyItems.get(itemId);
|
|
1198
|
+
if (!item) return false;
|
|
1199
|
+
if (!this._survey.translations.setItemTranslationContent(itemId, locale, contentKey, content)) return false;
|
|
1200
|
+
item.setModifiedAt(this.createEditorTimestamp());
|
|
1201
|
+
this.markAsModified();
|
|
1202
|
+
return true;
|
|
1203
|
+
}
|
|
882
1204
|
updateItemTranslations(itemId, updatedContent) {
|
|
883
1205
|
const item = this._survey.surveyItems.get(itemId);
|
|
884
1206
|
if (!item) return false;
|
|
@@ -987,7 +1309,21 @@ var SurveyEditor = class SurveyEditor {
|
|
|
987
1309
|
/**
|
|
988
1310
|
* Add or replace a survey asset.
|
|
989
1311
|
*/
|
|
1312
|
+
/** Stage one live asset delta. Default setAsset/removeAsset remain immediate. */
|
|
1313
|
+
stageAsset(assetId, asset) {
|
|
1314
|
+
const previous = this.pendingAssets.get(assetId)?.prev ?? this._survey.getAsset(assetId);
|
|
1315
|
+
const baseline = this.pendingAssets.has(assetId) ? this.pendingAssets.get(assetId).prev : previous;
|
|
1316
|
+
if (asset) this._survey.setAsset(assetId, structuredCloneMethod(asset));
|
|
1317
|
+
else this._survey.deleteAsset(assetId);
|
|
1318
|
+
if (JSON.stringify(baseline) === JSON.stringify(asset)) this.pendingAssets.delete(assetId);
|
|
1319
|
+
else this.pendingAssets.set(assetId, {
|
|
1320
|
+
prev: baseline,
|
|
1321
|
+
next: structuredCloneMethod(asset)
|
|
1322
|
+
});
|
|
1323
|
+
this.mutationEpoch++;
|
|
1324
|
+
}
|
|
990
1325
|
setAsset(assetId, asset) {
|
|
1326
|
+
this.mutationEpoch++;
|
|
991
1327
|
const previousAsset = this._survey.getAsset(assetId);
|
|
992
1328
|
this._survey.setAsset(assetId, structuredCloneMethod(asset));
|
|
993
1329
|
this._undoRedo.commitAssetChange([{
|
|
@@ -1003,6 +1339,7 @@ var SurveyEditor = class SurveyEditor {
|
|
|
1003
1339
|
* Remove a survey asset.
|
|
1004
1340
|
*/
|
|
1005
1341
|
removeAsset(assetId) {
|
|
1342
|
+
this.mutationEpoch++;
|
|
1006
1343
|
const previousAsset = this._survey.getAsset(assetId);
|
|
1007
1344
|
if (!previousAsset) return;
|
|
1008
1345
|
this._survey.deleteAsset(assetId);
|
|
@@ -1029,10 +1366,12 @@ var SurveyEditor = class SurveyEditor {
|
|
|
1029
1366
|
* @returns The ID of the pasted item
|
|
1030
1367
|
*/
|
|
1031
1368
|
pasteItem(clipboardData, target) {
|
|
1032
|
-
this.markAsModified();
|
|
1033
1369
|
const timestamp = this.createEditorTimestamp();
|
|
1034
1370
|
const itemIdsBeforePaste = new Set(this._survey.surveyItems.keys());
|
|
1035
|
-
const
|
|
1371
|
+
const copyPaste = new ItemCopyPaste(this._survey);
|
|
1372
|
+
const newItemId = copyPaste.pasteItem(clipboardData, target);
|
|
1373
|
+
this.lastResponseNameChanges = copyPaste.lastResponseNameChanges;
|
|
1374
|
+
this.markAsModified();
|
|
1036
1375
|
const pastedItemIds = Array.from(this._survey.surveyItems.keys()).filter((itemId) => !itemIdsBeforePaste.has(itemId));
|
|
1037
1376
|
for (const itemId of pastedItemIds) {
|
|
1038
1377
|
const item = this._survey.surveyItems.get(itemId);
|
|
@@ -1046,6 +1385,6 @@ var SurveyEditor = class SurveyEditor {
|
|
|
1046
1385
|
}
|
|
1047
1386
|
};
|
|
1048
1387
|
//#endregion
|
|
1049
|
-
export { CommitSource, ItemCopyPaste, SurveyEditor, SurveyEditorUndoRedo };
|
|
1388
|
+
export { CommitSource, ItemCopyPaste, SurveyEditor, SurveyEditorUndoRedo, allocateCopiedResponseNames, previewResponseNaming, previewResponseSettings };
|
|
1050
1389
|
|
|
1051
1390
|
//# sourceMappingURL=editor.mjs.map
|