@case-framework/survey-core 0.1.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/build/editor.d.mts +245 -0
- package/build/editor.d.mts.map +1 -0
- package/build/editor.mjs +603 -0
- package/build/editor.mjs.map +1 -0
- package/build/index.d.mts +597 -0
- package/build/index.d.mts.map +1 -0
- package/build/index.mjs +1586 -0
- package/build/index.mjs.map +1 -0
- package/build/package.json/package.json +49 -0
- package/build/survey-C3ZHI-5z.mjs +931 -0
- package/build/survey-C3ZHI-5z.mjs.map +1 -0
- package/build/survey-TUPUXiXl.d.mts +614 -0
- package/build/survey-TUPUXiXl.d.mts.map +1 -0
- package/package.json +49 -0
package/build/editor.mjs
ADDED
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
import { D as structuredCloneMethod, T as generateId, n as GroupItemCore, r as SurveyItemTranslations, s as ReservedSurveyItemTypes, t as Survey } from "./survey-C3ZHI-5z.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/editor/item-copy-paste.ts
|
|
4
|
+
var ItemCopyPaste = class ItemCopyPaste {
|
|
5
|
+
survey;
|
|
6
|
+
constructor(survey) {
|
|
7
|
+
this.survey = survey;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Copy a survey item and all its data to clipboard format
|
|
11
|
+
* When copying a group, automatically includes all items in the subtree
|
|
12
|
+
* @param itemId - The id of the item to copy
|
|
13
|
+
* @returns Clipboard data that can be serialized to JSON for clipboard
|
|
14
|
+
*/
|
|
15
|
+
copyItem(itemId) {
|
|
16
|
+
if (!this.survey.surveyItems.get(itemId)) throw new Error(`Item with id '${itemId}' not found`);
|
|
17
|
+
const itemsToCopy = this.collectItemsForCopy(itemId);
|
|
18
|
+
const items = itemsToCopy.map((id) => {
|
|
19
|
+
const item = this.survey.surveyItems.get(id);
|
|
20
|
+
if (!item) throw new Error(`Item with id '${id}' not found during copy`);
|
|
21
|
+
return {
|
|
22
|
+
itemId: id,
|
|
23
|
+
itemData: item.rawItem
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
const translations = {};
|
|
27
|
+
itemsToCopy.forEach((id) => {
|
|
28
|
+
try {
|
|
29
|
+
const itemTranslations = this.survey.getItemTranslations(id);
|
|
30
|
+
if (itemTranslations?.locales?.length) {
|
|
31
|
+
const serializedTranslations = {};
|
|
32
|
+
itemTranslations.locales.forEach((locale) => {
|
|
33
|
+
const localeContent = itemTranslations.getAllForLocale(locale);
|
|
34
|
+
if (localeContent) serializedTranslations[locale] = localeContent;
|
|
35
|
+
});
|
|
36
|
+
translations[id] = serializedTranslations;
|
|
37
|
+
} else translations[id] = {};
|
|
38
|
+
} catch {
|
|
39
|
+
translations[id] = {};
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
type: "survey-item",
|
|
44
|
+
version: "1.0.0",
|
|
45
|
+
items,
|
|
46
|
+
translations,
|
|
47
|
+
rootItemId: itemId,
|
|
48
|
+
timestamp: Date.now()
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Collect all items that should be copied, including subtree for groups
|
|
53
|
+
* @param itemId - The root item id
|
|
54
|
+
* @returns Array of item keys to copy
|
|
55
|
+
*/
|
|
56
|
+
collectItemsForCopy(itemId) {
|
|
57
|
+
const itemsToCopy = [itemId];
|
|
58
|
+
const item = this.survey.surveyItems.get(itemId);
|
|
59
|
+
if (!item) return [];
|
|
60
|
+
if (item instanceof GroupItemCore) {
|
|
61
|
+
const childIds = item.items;
|
|
62
|
+
if (childIds.length > 0) childIds.forEach((childId) => {
|
|
63
|
+
const childItems = this.collectItemsForCopy(childId);
|
|
64
|
+
itemsToCopy.push(...childItems);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return itemsToCopy;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Add an item to its parent group.
|
|
71
|
+
* Ensures key uniqueness among siblings.
|
|
72
|
+
* @param target - Target location information
|
|
73
|
+
* @param item - The item to add (may have key adjusted for uniqueness)
|
|
74
|
+
*/
|
|
75
|
+
addItemToParentGroup(target, item) {
|
|
76
|
+
const parentGroup = this.survey.surveyItems.get(target.parentId);
|
|
77
|
+
if (!parentGroup) throw new Error(`Parent item with id '${target.parentId}' not found`);
|
|
78
|
+
if (!(parentGroup instanceof GroupItemCore)) throw new Error(`Parent item '${target.parentId}' is not a group item`);
|
|
79
|
+
const siblings = parentGroup.items.map((id) => this.survey.surveyItems.get(id)).filter((s) => s !== void 0);
|
|
80
|
+
const siblingKeys = new Set(siblings.map((s) => s.key));
|
|
81
|
+
let uniqueKey = item.key;
|
|
82
|
+
if (siblingKeys.has(uniqueKey)) {
|
|
83
|
+
let counter = 1;
|
|
84
|
+
const originalKey = item.key;
|
|
85
|
+
while (siblingKeys.has(uniqueKey)) {
|
|
86
|
+
uniqueKey = originalKey + `_${counter}`;
|
|
87
|
+
counter++;
|
|
88
|
+
}
|
|
89
|
+
if (uniqueKey !== item.key) {
|
|
90
|
+
const updatedRawItem = {
|
|
91
|
+
...item.rawItem,
|
|
92
|
+
key: uniqueKey
|
|
93
|
+
};
|
|
94
|
+
const newItem = this.survey.createItemFromRaw(updatedRawItem);
|
|
95
|
+
this.survey.surveyItems.delete(item.id);
|
|
96
|
+
this.survey.surveyItems.set(newItem.id, newItem);
|
|
97
|
+
item = newItem;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const insertIndex = target.index !== void 0 ? Math.min(target.index, parentGroup.items.length) : parentGroup.items.length;
|
|
101
|
+
parentGroup.addChild(item.id, insertIndex);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Paste a survey item from clipboard data to a target location
|
|
105
|
+
* Handles multiple items and subtrees from the clipboard data
|
|
106
|
+
* @param clipboardData - The clipboard data containing the item(s) to paste
|
|
107
|
+
* @param target - Target location where to paste the item
|
|
108
|
+
* @returns The id of the pasted root item
|
|
109
|
+
*/
|
|
110
|
+
pasteItem(clipboardData, target) {
|
|
111
|
+
if (!ItemCopyPaste.isValidClipboardData(clipboardData)) throw new Error("Invalid clipboard data format");
|
|
112
|
+
const idMapping = {};
|
|
113
|
+
clipboardData.items.forEach(({ itemId }) => {
|
|
114
|
+
idMapping[itemId] = generateId();
|
|
115
|
+
});
|
|
116
|
+
clipboardData.items.forEach(({ itemId, itemData }) => {
|
|
117
|
+
const newId = idMapping[itemId];
|
|
118
|
+
if (newId) {
|
|
119
|
+
const updatedItemData = this.updateItemIdsInData(itemData, idMapping);
|
|
120
|
+
updatedItemData.id = newId;
|
|
121
|
+
const newItem = this.survey.createItemFromRaw(updatedItemData);
|
|
122
|
+
this.survey.surveyItems.set(newItem.id, newItem);
|
|
123
|
+
if (itemId === clipboardData.rootItemId) this.addItemToParentGroup(target, newItem);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
const updatedTranslations = this.updateTranslations(clipboardData.translations, idMapping);
|
|
127
|
+
Object.keys(updatedTranslations).forEach((itemId) => {
|
|
128
|
+
const itemTranslations = new SurveyItemTranslations();
|
|
129
|
+
const serializedData = updatedTranslations[itemId];
|
|
130
|
+
Object.keys(serializedData).forEach((locale) => {
|
|
131
|
+
itemTranslations.setAllForLocale(locale, serializedData[locale]);
|
|
132
|
+
});
|
|
133
|
+
this.survey.translations.setItemTranslations(itemId, itemTranslations);
|
|
134
|
+
});
|
|
135
|
+
return idMapping[clipboardData.rootItemId];
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Update all item ids in the raw item data recursively
|
|
139
|
+
*/
|
|
140
|
+
updateItemIdsInData(itemData, idMapping) {
|
|
141
|
+
const updatedData = JSON.parse(JSON.stringify(itemData));
|
|
142
|
+
if (updatedData.itemType === ReservedSurveyItemTypes.Group && updatedData.config) {
|
|
143
|
+
const config = updatedData.config;
|
|
144
|
+
if (config.items) config.items = config.items.map((childId) => idMapping[childId] ?? childId);
|
|
145
|
+
}
|
|
146
|
+
this.updateExpressionsInItemData(updatedData, idMapping);
|
|
147
|
+
return updatedData;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Update expressions in item data that reference the old ids
|
|
151
|
+
*/
|
|
152
|
+
updateExpressionsInItemData(itemData, idMapping) {
|
|
153
|
+
if (itemData.displayConditions?.root) this.updateExpressionReferences(itemData.displayConditions.root, idMapping);
|
|
154
|
+
if (itemData.displayConditions?.components) Object.values(itemData.displayConditions.components).forEach((expr) => {
|
|
155
|
+
if (expr) this.updateExpressionReferences(expr, idMapping);
|
|
156
|
+
});
|
|
157
|
+
if (itemData.disabledConditions?.components) Object.values(itemData.disabledConditions.components).forEach((expr) => {
|
|
158
|
+
if (expr) this.updateExpressionReferences(expr, idMapping);
|
|
159
|
+
});
|
|
160
|
+
if (itemData.validations) Object.values(itemData.validations).forEach((expr) => {
|
|
161
|
+
if (expr) this.updateExpressionReferences(expr, idMapping);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Update references in a single expression (recursive)
|
|
166
|
+
*/
|
|
167
|
+
updateExpressionReferences(expression, idMapping) {
|
|
168
|
+
if (!expression || typeof expression !== "object") return;
|
|
169
|
+
const expr = expression;
|
|
170
|
+
if (Array.isArray(expr.data)) expr.data.forEach((arg) => {
|
|
171
|
+
if (arg && typeof arg === "object" && arg !== null && "str" in arg) {
|
|
172
|
+
const argObj = arg;
|
|
173
|
+
if (typeof argObj.str === "string" && Object.prototype.hasOwnProperty.call(idMapping, argObj.str)) argObj.str = idMapping[argObj.str];
|
|
174
|
+
this.updateExpressionReferences(arg, idMapping);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
Object.keys(expr).forEach((key) => {
|
|
178
|
+
const val = expr[key];
|
|
179
|
+
if (val && typeof val === "object") this.updateExpressionReferences(val, idMapping);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Update translation keys for the pasted item
|
|
184
|
+
*/
|
|
185
|
+
updateTranslations(translations, idMapping) {
|
|
186
|
+
const newTranslations = {};
|
|
187
|
+
Object.keys(translations).forEach((itemId) => {
|
|
188
|
+
const itemTranslations = translations[itemId];
|
|
189
|
+
const newItemId = idMapping[itemId];
|
|
190
|
+
newTranslations[newItemId] = itemTranslations;
|
|
191
|
+
});
|
|
192
|
+
return newTranslations;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Validate clipboard data format
|
|
196
|
+
*/
|
|
197
|
+
static isValidClipboardData(data) {
|
|
198
|
+
if (typeof data !== "object" || data === null || data === void 0) return false;
|
|
199
|
+
const clipboardData = data;
|
|
200
|
+
return clipboardData.type === "survey-item" && clipboardData.version === "1.0.0" && clipboardData.rootItemId !== void 0;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/editor/undo-redo.ts
|
|
206
|
+
const CommitSource = {
|
|
207
|
+
USER: "user",
|
|
208
|
+
SYSTEM: "system"
|
|
209
|
+
};
|
|
210
|
+
var MemoryCalculator = class {
|
|
211
|
+
static encoder = new TextEncoder();
|
|
212
|
+
static calculateSize(obj) {
|
|
213
|
+
const jsonString = JSON.stringify(obj);
|
|
214
|
+
return this.encoder.encode(jsonString).length;
|
|
215
|
+
}
|
|
216
|
+
static formatBytes(bytes) {
|
|
217
|
+
const units = [
|
|
218
|
+
"B",
|
|
219
|
+
"KB",
|
|
220
|
+
"MB",
|
|
221
|
+
"GB"
|
|
222
|
+
];
|
|
223
|
+
let size = bytes;
|
|
224
|
+
let unitIndex = 0;
|
|
225
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
226
|
+
size /= 1024;
|
|
227
|
+
unitIndex++;
|
|
228
|
+
}
|
|
229
|
+
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
|
|
233
|
+
history = [];
|
|
234
|
+
currentIndex = -1;
|
|
235
|
+
_config;
|
|
236
|
+
constructor(initialSurvey, config = {}, meta = {
|
|
237
|
+
label: "Initial state",
|
|
238
|
+
source: CommitSource.SYSTEM
|
|
239
|
+
}) {
|
|
240
|
+
this._config = {
|
|
241
|
+
maxTotalMemoryMB: 50,
|
|
242
|
+
minHistorySize: 10,
|
|
243
|
+
maxHistorySize: 200,
|
|
244
|
+
...config
|
|
245
|
+
};
|
|
246
|
+
this.saveSnapshot(initialSurvey, meta);
|
|
247
|
+
}
|
|
248
|
+
saveSnapshot(survey, meta) {
|
|
249
|
+
const memorySize = MemoryCalculator.calculateSize(survey);
|
|
250
|
+
this.history = this.history.slice(0, this.currentIndex + 1);
|
|
251
|
+
this.history.push({
|
|
252
|
+
survey: structuredCloneMethod(survey),
|
|
253
|
+
timestamp: Date.now(),
|
|
254
|
+
meta,
|
|
255
|
+
memorySize
|
|
256
|
+
});
|
|
257
|
+
this.currentIndex++;
|
|
258
|
+
this.cleanupHistory();
|
|
259
|
+
}
|
|
260
|
+
cleanupHistory() {
|
|
261
|
+
let totalMemory = this.getTotalMemoryUsage();
|
|
262
|
+
const maxMemoryBytes = this._config.maxTotalMemoryMB * 1024 * 1024;
|
|
263
|
+
while (this.history.length > this._config.minHistorySize && (totalMemory > maxMemoryBytes || this.history.length > this._config.maxHistorySize)) {
|
|
264
|
+
const removedSnapshot = this.history.shift();
|
|
265
|
+
this.currentIndex--;
|
|
266
|
+
if (removedSnapshot) totalMemory -= removedSnapshot.memorySize;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
getTotalMemoryUsage() {
|
|
270
|
+
return this.history.reduce((total, entry) => total + entry.memorySize, 0);
|
|
271
|
+
}
|
|
272
|
+
commit(survey, meta) {
|
|
273
|
+
this.saveSnapshot(survey, meta);
|
|
274
|
+
}
|
|
275
|
+
getCurrentState() {
|
|
276
|
+
if (this.currentIndex < 0 || this.currentIndex >= this.history.length) throw new Error("Invalid history state");
|
|
277
|
+
return structuredCloneMethod(this.history[this.currentIndex].survey);
|
|
278
|
+
}
|
|
279
|
+
undo() {
|
|
280
|
+
if (!this.canUndo()) return null;
|
|
281
|
+
this.currentIndex--;
|
|
282
|
+
return this.getCurrentState();
|
|
283
|
+
}
|
|
284
|
+
redo() {
|
|
285
|
+
if (!this.canRedo()) return null;
|
|
286
|
+
this.currentIndex++;
|
|
287
|
+
return this.getCurrentState();
|
|
288
|
+
}
|
|
289
|
+
canUndo() {
|
|
290
|
+
return this.currentIndex > 0;
|
|
291
|
+
}
|
|
292
|
+
canRedo() {
|
|
293
|
+
return this.currentIndex < this.history.length - 1;
|
|
294
|
+
}
|
|
295
|
+
getUndoMeta() {
|
|
296
|
+
if (!this.canUndo()) return null;
|
|
297
|
+
return this.history[this.currentIndex].meta;
|
|
298
|
+
}
|
|
299
|
+
getRedoMeta() {
|
|
300
|
+
if (!this.canRedo()) return null;
|
|
301
|
+
return this.history[this.currentIndex + 1].meta;
|
|
302
|
+
}
|
|
303
|
+
getMemoryUsage() {
|
|
304
|
+
return {
|
|
305
|
+
totalMB: this.getTotalMemoryUsage() / (1024 * 1024),
|
|
306
|
+
entries: this.history.length
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
getConfig() {
|
|
310
|
+
return { ...this._config };
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Get the full history list with metadata
|
|
314
|
+
*/
|
|
315
|
+
getHistory() {
|
|
316
|
+
return this.history.map((entry, index) => ({
|
|
317
|
+
index,
|
|
318
|
+
meta: entry.meta,
|
|
319
|
+
timestamp: entry.timestamp,
|
|
320
|
+
memorySize: entry.memorySize,
|
|
321
|
+
isCurrent: index === this.currentIndex
|
|
322
|
+
}));
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Get the current index in the history
|
|
326
|
+
*/
|
|
327
|
+
getCurrentIndex() {
|
|
328
|
+
return this.currentIndex;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Get the total number of history entries
|
|
332
|
+
*/
|
|
333
|
+
getHistoryLength() {
|
|
334
|
+
return this.history.length;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Jump to a specific index in the history (can go forward or backward)
|
|
338
|
+
* @param targetIndex The index to jump to
|
|
339
|
+
* @returns The survey state at the target index, or null if invalid
|
|
340
|
+
*/
|
|
341
|
+
jumpToIndex(targetIndex) {
|
|
342
|
+
if (targetIndex < 0 || targetIndex >= this.history.length || targetIndex === this.currentIndex) return null;
|
|
343
|
+
this.currentIndex = targetIndex;
|
|
344
|
+
return this.getCurrentState();
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Check if we can jump to a specific index
|
|
348
|
+
*/
|
|
349
|
+
canJumpToIndex(targetIndex) {
|
|
350
|
+
return targetIndex >= 0 && targetIndex < this.history.length && targetIndex !== this.currentIndex;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Serialize the undo/redo state to JSON
|
|
354
|
+
* @returns A JSON-serializable object containing the complete state
|
|
355
|
+
*/
|
|
356
|
+
serialize() {
|
|
357
|
+
return {
|
|
358
|
+
history: this.history.map((entry) => ({
|
|
359
|
+
survey: entry.survey,
|
|
360
|
+
timestamp: entry.timestamp,
|
|
361
|
+
meta: entry.meta,
|
|
362
|
+
memorySize: entry.memorySize
|
|
363
|
+
})),
|
|
364
|
+
currentIndex: this.currentIndex,
|
|
365
|
+
config: { ...this._config }
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Create a new SurveyEditorUndoRedo instance from JSON data
|
|
370
|
+
* @param jsonData The serialized undo/redo state
|
|
371
|
+
* @returns A new SurveyEditorUndoRedo instance with the restored state
|
|
372
|
+
*/
|
|
373
|
+
static deserialize(jsonData) {
|
|
374
|
+
if (!jsonData.history || !Array.isArray(jsonData.history) || jsonData.history.length === 0) throw new Error("Invalid object: history must be an array and must not be empty");
|
|
375
|
+
if (typeof jsonData.currentIndex !== "number" || jsonData.currentIndex < 0 || jsonData.currentIndex >= jsonData.history.length) throw new Error("Invalid object: currentIndex must be a valid index within the history array");
|
|
376
|
+
if (!jsonData.config) throw new Error("Invalid object: config is required");
|
|
377
|
+
const instance = new SurveyEditorUndoRedo(jsonData.history[0].survey, jsonData.config);
|
|
378
|
+
instance.history = jsonData.history.map((entry) => ({
|
|
379
|
+
survey: structuredCloneMethod(entry.survey),
|
|
380
|
+
timestamp: entry.timestamp,
|
|
381
|
+
meta: entry.meta,
|
|
382
|
+
memorySize: entry.memorySize
|
|
383
|
+
}));
|
|
384
|
+
instance.currentIndex = jsonData.currentIndex;
|
|
385
|
+
return instance;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
//#endregion
|
|
390
|
+
//#region src/editor/survey-editor.ts
|
|
391
|
+
var SurveyEditor = class SurveyEditor {
|
|
392
|
+
_survey;
|
|
393
|
+
_undoRedo;
|
|
394
|
+
_hasUncommittedChanges = false;
|
|
395
|
+
_pluginRegistry;
|
|
396
|
+
constructor(survey, config = {}, meta) {
|
|
397
|
+
this._survey = survey;
|
|
398
|
+
const { pluginRegistry, ...undoRedoConfig } = config;
|
|
399
|
+
this._pluginRegistry = pluginRegistry;
|
|
400
|
+
this._undoRedo = new SurveyEditorUndoRedo(survey.serialize(), undoRedoConfig, meta);
|
|
401
|
+
}
|
|
402
|
+
get survey() {
|
|
403
|
+
return this._survey;
|
|
404
|
+
}
|
|
405
|
+
get hasUncommittedChanges() {
|
|
406
|
+
return this._hasUncommittedChanges;
|
|
407
|
+
}
|
|
408
|
+
get undoRedo() {
|
|
409
|
+
return this._undoRedo;
|
|
410
|
+
}
|
|
411
|
+
commit(meta) {
|
|
412
|
+
this._undoRedo.commit(this._survey.serialize(), meta);
|
|
413
|
+
this._hasUncommittedChanges = false;
|
|
414
|
+
}
|
|
415
|
+
commitIfNeeded() {
|
|
416
|
+
if (this._hasUncommittedChanges) this.commit({
|
|
417
|
+
label: "Latest content changes",
|
|
418
|
+
source: CommitSource.SYSTEM
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
undo() {
|
|
422
|
+
if (this._hasUncommittedChanges) {
|
|
423
|
+
this._survey = Survey.fromJson(this._undoRedo.getCurrentState(), this._pluginRegistry);
|
|
424
|
+
this._hasUncommittedChanges = false;
|
|
425
|
+
return true;
|
|
426
|
+
} else {
|
|
427
|
+
const previousState = this._undoRedo.undo();
|
|
428
|
+
if (previousState) {
|
|
429
|
+
this._survey = Survey.fromJson(previousState, this._pluginRegistry);
|
|
430
|
+
this._hasUncommittedChanges = false;
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
redo() {
|
|
437
|
+
if (this._hasUncommittedChanges) return false;
|
|
438
|
+
const nextState = this._undoRedo.redo();
|
|
439
|
+
if (nextState) {
|
|
440
|
+
this._survey = Survey.fromJson(nextState, this._pluginRegistry);
|
|
441
|
+
this._hasUncommittedChanges = false;
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Jump to a specific index in the history (can go forward or backward)
|
|
448
|
+
*/
|
|
449
|
+
jumpToIndex(targetIndex) {
|
|
450
|
+
if (this._hasUncommittedChanges) return false;
|
|
451
|
+
const targetState = this._undoRedo.jumpToIndex(targetIndex);
|
|
452
|
+
if (targetState) {
|
|
453
|
+
this._survey = Survey.fromJson(targetState, this._pluginRegistry);
|
|
454
|
+
this._hasUncommittedChanges = false;
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
canUndo() {
|
|
460
|
+
return this._hasUncommittedChanges || this._undoRedo.canUndo();
|
|
461
|
+
}
|
|
462
|
+
canRedo() {
|
|
463
|
+
return !this._hasUncommittedChanges && this._undoRedo.canRedo();
|
|
464
|
+
}
|
|
465
|
+
getUndoMeta() {
|
|
466
|
+
if (this._hasUncommittedChanges) return {
|
|
467
|
+
label: "Latest content changes",
|
|
468
|
+
source: CommitSource.SYSTEM
|
|
469
|
+
};
|
|
470
|
+
return this._undoRedo.getUndoMeta();
|
|
471
|
+
}
|
|
472
|
+
getRedoMeta() {
|
|
473
|
+
if (this._hasUncommittedChanges) return null;
|
|
474
|
+
return this._undoRedo.getRedoMeta();
|
|
475
|
+
}
|
|
476
|
+
getMemoryUsage() {
|
|
477
|
+
return this._undoRedo.getMemoryUsage();
|
|
478
|
+
}
|
|
479
|
+
getUndoRedoConfig() {
|
|
480
|
+
return this._undoRedo.getConfig();
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Serialize the SurveyEditor state to JSON
|
|
484
|
+
* @returns A JSON-serializable object containing the complete editor state
|
|
485
|
+
*/
|
|
486
|
+
toJson() {
|
|
487
|
+
return {
|
|
488
|
+
version: "1.0.0",
|
|
489
|
+
survey: this._survey.serialize(),
|
|
490
|
+
undoRedo: this._undoRedo.serialize(),
|
|
491
|
+
hasUncommittedChanges: this._hasUncommittedChanges
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Create a new SurveyEditor instance from JSON data
|
|
496
|
+
* @param jsonData The serialized editor state
|
|
497
|
+
* @param pluginRegistry Optional plugin registry for deserializing survey state (required when survey contains custom item types)
|
|
498
|
+
* @returns A new SurveyEditor instance with the restored state
|
|
499
|
+
*/
|
|
500
|
+
static fromJson(jsonData, pluginRegistry) {
|
|
501
|
+
if (!jsonData.survey) throw new Error("Invalid object: survey is required");
|
|
502
|
+
if (!jsonData.undoRedo) throw new Error("Invalid object: undoRedo is required");
|
|
503
|
+
if (typeof jsonData.hasUncommittedChanges !== "boolean") throw new Error("Invalid object: hasUncommittedChanges must be a boolean");
|
|
504
|
+
if (jsonData.version && !jsonData.version.startsWith("1.")) console.warn(`Warning: Loading SurveyEditor with version ${jsonData.version}, current version is 1.0.0`);
|
|
505
|
+
const editor = new SurveyEditor(Survey.fromJson(jsonData.survey, pluginRegistry), { pluginRegistry });
|
|
506
|
+
editor._undoRedo = SurveyEditorUndoRedo.deserialize(jsonData.undoRedo);
|
|
507
|
+
editor._hasUncommittedChanges = jsonData.hasUncommittedChanges;
|
|
508
|
+
return editor;
|
|
509
|
+
}
|
|
510
|
+
markAsModified() {
|
|
511
|
+
this._hasUncommittedChanges = true;
|
|
512
|
+
}
|
|
513
|
+
addItem(target, item, content) {
|
|
514
|
+
this.markAsModified();
|
|
515
|
+
let parentGroup;
|
|
516
|
+
if (!target) {
|
|
517
|
+
const rootItem = this._survey.rootItem;
|
|
518
|
+
if (!rootItem) throw new Error("No root group found in survey");
|
|
519
|
+
if (!(rootItem instanceof GroupItemCore)) throw new Error("Root item is not a group item");
|
|
520
|
+
parentGroup = rootItem;
|
|
521
|
+
} else {
|
|
522
|
+
const targetItem = this._survey.surveyItems.get(target.parentId);
|
|
523
|
+
if (!targetItem) throw new Error(`Parent item with id '${target.parentId}' not found`);
|
|
524
|
+
if (!(targetItem instanceof GroupItemCore)) throw new Error(`Parent item '${target.parentId}' is not a group item (${targetItem.type})`);
|
|
525
|
+
parentGroup = targetItem;
|
|
526
|
+
}
|
|
527
|
+
if (parentGroup.hasChild(item.id)) throw new Error(`Item ${item.id} already in this group`);
|
|
528
|
+
const siblings = Array.from(this._survey.surveyItems.values()).filter((sItem) => parentGroup.items?.includes(sItem.id));
|
|
529
|
+
let counter = 1;
|
|
530
|
+
while (siblings.some((sibling) => sibling.key === item.key)) {
|
|
531
|
+
item.key += `_${counter}`;
|
|
532
|
+
counter++;
|
|
533
|
+
}
|
|
534
|
+
let insertIndex;
|
|
535
|
+
if (target?.index !== void 0) insertIndex = Math.min(target.index, parentGroup.items.length);
|
|
536
|
+
else insertIndex = parentGroup.items.length;
|
|
537
|
+
this._survey.surveyItems.set(item.id, item);
|
|
538
|
+
parentGroup.items.splice(insertIndex, 0, item.id);
|
|
539
|
+
if (content) this._survey.translations.setItemTranslations(item.id, content);
|
|
540
|
+
}
|
|
541
|
+
removeItem(itemId, nested = false) {
|
|
542
|
+
this.markAsModified();
|
|
543
|
+
const item = this._survey.surveyItems.get(itemId);
|
|
544
|
+
if (!item) return false;
|
|
545
|
+
const parentItem = this._survey.getParentItem(itemId);
|
|
546
|
+
if (!parentItem) throw new Error(`Item with id '${itemId}' is the root item`);
|
|
547
|
+
if (item instanceof GroupItemCore) for (const childId of item.getChildrenIds()) this.removeItem(childId, true);
|
|
548
|
+
this._survey.surveyItems.delete(itemId);
|
|
549
|
+
this._survey.translations?.onItemDeleted(itemId);
|
|
550
|
+
if (!nested) parentItem.removeChild(itemId);
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
moveItem(itemId, newTarget) {
|
|
554
|
+
this.markAsModified();
|
|
555
|
+
const item = this._survey.surveyItems.get(itemId);
|
|
556
|
+
if (!item) throw new Error(`Item with id '${itemId}' not found`);
|
|
557
|
+
const targetItem = this._survey.surveyItems.get(newTarget.parentId);
|
|
558
|
+
if (!targetItem) throw new Error(`Target parent with id '${newTarget.parentId}' not found`);
|
|
559
|
+
if (!(targetItem instanceof GroupItemCore)) throw new Error(`Target parent '${newTarget.parentId}' is not a group item (${targetItem.type})`);
|
|
560
|
+
if (this._survey.isDescendantOf(newTarget.parentId, itemId)) throw new Error(`Cannot move item '${itemId}' to its descendant '${newTarget.parentId}'`);
|
|
561
|
+
const currentParentItem = this._survey.getParentItem(itemId);
|
|
562
|
+
if (currentParentItem?.id === newTarget.parentId) throw new Error(`Item '${itemId}' is already in the target parent '${newTarget.parentId}'`);
|
|
563
|
+
if (currentParentItem) currentParentItem.removeChild(itemId);
|
|
564
|
+
const targetGroup = targetItem;
|
|
565
|
+
const siblings = Array.from(this._survey.surveyItems.values()).filter((sItem) => targetGroup.hasChild(sItem.id));
|
|
566
|
+
let counter = 1;
|
|
567
|
+
while (siblings.some((sibling) => sibling.key === item.key)) {
|
|
568
|
+
item.key += `_${counter}`;
|
|
569
|
+
counter++;
|
|
570
|
+
}
|
|
571
|
+
const insertIndex = newTarget.index !== void 0 ? Math.min(newTarget.index, targetGroup.items.length) : targetGroup.items.length;
|
|
572
|
+
targetGroup.items.splice(insertIndex, 0, itemId);
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
updateItemTranslations(itemId, updatedContent) {
|
|
576
|
+
this.markAsModified();
|
|
577
|
+
if (!this._survey.surveyItems.get(itemId)) throw new Error(`Item with id '${itemId}' not found`);
|
|
578
|
+
this._survey.translations.setItemTranslations(itemId, updatedContent);
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Copy a survey item and all its data to clipboard format
|
|
583
|
+
* @param itemKey - The full key of the item to copy
|
|
584
|
+
* @returns Clipboard data that can be serialized to JSON for clipboard
|
|
585
|
+
*/
|
|
586
|
+
copyItem(itemKey) {
|
|
587
|
+
return new ItemCopyPaste(this._survey).copyItem(itemKey);
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Paste a survey item from clipboard data to a target location
|
|
591
|
+
* @param clipboardData - The clipboard data containing the item to paste
|
|
592
|
+
* @param target - Target location where to paste the item
|
|
593
|
+
* @returns The full key of the pasted item
|
|
594
|
+
*/
|
|
595
|
+
pasteItem(clipboardData, target) {
|
|
596
|
+
this.markAsModified();
|
|
597
|
+
return new ItemCopyPaste(this._survey).pasteItem(clipboardData, target);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
//#endregion
|
|
602
|
+
export { CommitSource, ItemCopyPaste, SurveyEditor, SurveyEditorUndoRedo };
|
|
603
|
+
//# sourceMappingURL=editor.mjs.map
|