@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/build/editor.mjs CHANGED
@@ -1,6 +1,186 @@
1
- import { $ as generateId, nt as structuredCloneMethod, t as Survey, u as SurveyItemTranslations, v as GroupItemCore } from "./survey-BKblV5rN.mjs";
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 idMapping = {};
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
- clipboardData.items.forEach(({ itemId, itemData }) => {
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
- const newItem = this.survey.createItemFromRaw(updatedItemData);
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
- const updatedTranslations = this.updateTranslations(clipboardData.translations, idMapping);
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: structuredCloneMethod(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 = cloneAssets(nextStateAssets);
334
- this.history = [newFirstEntry, ...this.history.slice(nextStateIndex + 1)];
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 = structuredCloneMethod(assets);
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() / (1024 * 1024),
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 instance = new SurveyEditorUndoRedo(firstEntry.kind === "survey-snapshot" ? firstEntry.survey : (() => {
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
- })(), jsonData.config, firstEntry.meta, initialAssetsOverride ?? jsonData.initialAssets);
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 = structuredCloneMethod(survey);
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._hasUncommittedChanges) return;
563
- this._undoRedo.commit(stripAssetsFromSurveySnapshot(this._survey.serialize()), meta);
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._hasUncommittedChanges) this.commit({
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._hasUncommittedChanges) {
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._hasUncommittedChanges) return false;
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._hasUncommittedChanges) return false;
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._hasUncommittedChanges || this._undoRedo.canUndo();
940
+ return this.hasUncommittedChanges || this._undoRedo.canUndo();
615
941
  }
616
942
  canRedo() {
617
- return !this._hasUncommittedChanges && this._undoRedo.canRedo();
943
+ return !this.hasUncommittedChanges && this._undoRedo.canRedo();
618
944
  }
619
945
  getUndoMeta() {
620
- if (this._hasUncommittedChanges) return {
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._hasUncommittedChanges) return null;
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._hasUncommittedChanges
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 editor = new SurveyEditor(Survey.fromJson(jsonData.survey, pluginRegistry), { pluginRegistry });
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
- item.updateRawItem({
716
- ...item.rawItem,
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
- const item = this._survey.surveyItems.get(itemId);
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
- const newKey = merged.key ?? existingItem.key;
869
- const parentGroup = this._survey.getParentItem(itemId);
870
- if (parentGroup) {
871
- const siblingWithSameKey = (parentGroup.items ?? []).filter((id) => id !== itemId).map((id) => this._survey.surveyItems.get(id)).find((s) => s !== void 0 && s.key === newKey);
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 newItemId = new ItemCopyPaste(this._survey).pasteItem(clipboardData, target);
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