@case-framework/survey-core 0.2.0 → 0.4.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.
@@ -1,16 +1,37 @@
1
1
  //#region src/utils.ts
2
+ function hashString(seed) {
3
+ let hash = 2166136261;
4
+ for (let i = 0; i < seed.length; i++) {
5
+ hash ^= seed.charCodeAt(i);
6
+ hash = Math.imul(hash, 16777619);
7
+ }
8
+ return hash >>> 0;
9
+ }
10
+ function createSeededRandom(seed) {
11
+ let state = hashString(seed);
12
+ return () => {
13
+ state = state + 1831565813 >>> 0;
14
+ let next = state;
15
+ next = Math.imul(next ^ next >>> 15, next | 1);
16
+ next ^= next + Math.imul(next ^ next >>> 7, next | 61);
17
+ return ((next ^ next >>> 14) >>> 0) / 4294967296;
18
+ };
19
+ }
20
+ function shuffleArray(values, random = Math.random) {
21
+ const shuffledValues = [...values];
22
+ for (let i = shuffledValues.length - 1; i > 0; i--) {
23
+ const j = Math.floor(random() * (i + 1));
24
+ [shuffledValues[i], shuffledValues[j]] = [shuffledValues[j], shuffledValues[i]];
25
+ }
26
+ return shuffledValues;
27
+ }
2
28
  /**
3
29
  * Shuffles an array of indices using the Fisher-Yates shuffle algorithm
4
30
  * @param length - The length of the array to create indices for
5
31
  * @returns A shuffled array of indices from 0 to length-1
6
32
  */
7
- function shuffleIndices(length) {
8
- const shuffledIndices = Array.from({ length }, (_, i) => i);
9
- for (let i = shuffledIndices.length - 1; i > 0; i--) {
10
- const j = Math.floor(Math.random() * (i + 1));
11
- [shuffledIndices[i], shuffledIndices[j]] = [shuffledIndices[j], shuffledIndices[i]];
12
- }
13
- return shuffledIndices;
33
+ function shuffleIndices(length, random = Math.random) {
34
+ return shuffleArray(Array.from({ length }, (_, i) => i), random);
14
35
  }
15
36
  function structuredCloneMethod(obj) {
16
37
  if (typeof structuredClone !== "undefined") return structuredClone(obj);
@@ -44,7 +65,6 @@ function generateCodingKey(length = 4) {
44
65
  for (let i = 0; i < length; i++) key += CODING_KEY_ALPHABET.charAt(Math.floor(Math.random() * base));
45
66
  return key;
46
67
  }
47
-
48
68
  //#endregion
49
69
  //#region src/survey/utils/value-reference.ts
50
70
  const SEPARATOR = "...";
@@ -88,9 +108,9 @@ let ReferenceUsageType = /* @__PURE__ */ function(ReferenceUsageType) {
88
108
  ReferenceUsageType["templateValues"] = "templateValues";
89
109
  ReferenceUsageType["validations"] = "validations";
90
110
  ReferenceUsageType["disabledConditions"] = "disabledConditions";
111
+ ReferenceUsageType["prefills"] = "prefills";
91
112
  return ReferenceUsageType;
92
113
  }({});
93
-
94
114
  //#endregion
95
115
  //#region src/expressions/expression.ts
96
116
  const ExpressionType = {
@@ -260,7 +280,6 @@ var FunctionExpression = class FunctionExpression extends Expression {
260
280
  };
261
281
  }
262
282
  };
263
-
264
283
  //#endregion
265
284
  //#region src/expressions/template-value.ts
266
285
  let TemplateDefTypes = /* @__PURE__ */ function(TemplateDefTypes) {
@@ -274,7 +293,7 @@ const serializeTemplateValue = (templateValue) => {
274
293
  returnType: templateValue.returnType,
275
294
  expression: templateValue.expression?.serialize()
276
295
  };
277
- if (templateValue.type === TemplateDefTypes.Date2String) json.dateFormat = templateValue.dateFormat;
296
+ if (templateValue.type === "date2string") json.dateFormat = templateValue.dateFormat;
278
297
  return json;
279
298
  };
280
299
  const serializeTemplateValues = (templateValues) => {
@@ -293,7 +312,72 @@ const deserializeTemplateValue = (json) => {
293
312
  const deserializeTemplateValues = (json) => {
294
313
  return new Map(Object.entries(json).map(([key, value]) => [key, deserializeTemplateValue(value)]));
295
314
  };
296
-
315
+ //#endregion
316
+ //#region src/survey/responses/value-types.ts
317
+ const ValueType = {
318
+ string: "string",
319
+ duration: "duration",
320
+ reference: "reference",
321
+ number: "number",
322
+ boolean: "boolean",
323
+ date: "date",
324
+ stringArray: "string[]",
325
+ durationArray: "duration[]",
326
+ referenceArray: "reference[]",
327
+ numberArray: "number[]",
328
+ dateArray: "date[]"
329
+ };
330
+ const DurationUnits = {
331
+ seconds: "seconds",
332
+ minutes: "minutes",
333
+ hours: "hours",
334
+ days: "days",
335
+ weeks: "weeks",
336
+ months: "months",
337
+ years: "years"
338
+ };
339
+ const NumberPrecision = {
340
+ int: "int",
341
+ float: "float"
342
+ };
343
+ function isPlainObject(value) {
344
+ return value !== null && typeof value === "object" && !Array.isArray(value);
345
+ }
346
+ function isFiniteNumber(value) {
347
+ return typeof value === "number" && Number.isFinite(value);
348
+ }
349
+ function isStringArray(value) {
350
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
351
+ }
352
+ function isNumberArray(value) {
353
+ return Array.isArray(value) && value.every((item) => isFiniteNumber(item));
354
+ }
355
+ function isOptionalNumberPrecision(value) {
356
+ return value === void 0 || value === NumberPrecision.int || value === NumberPrecision.float;
357
+ }
358
+ function isDurationUnit(value) {
359
+ return typeof value === "string" && Object.values(DurationUnits).includes(value);
360
+ }
361
+ function isResponseValue(value) {
362
+ if (!isPlainObject(value) || typeof value.type !== "string") return false;
363
+ switch (value.type) {
364
+ case ValueType.string:
365
+ case ValueType.reference: return typeof value.value === "string";
366
+ case ValueType.number: return isFiniteNumber(value.value) && isOptionalNumberPrecision(value.precision);
367
+ case ValueType.boolean: return typeof value.value === "boolean";
368
+ case ValueType.date: return isFiniteNumber(value.value);
369
+ case ValueType.duration: return isFiniteNumber(value.value) && isDurationUnit(value.unit) && isOptionalNumberPrecision(value.precision);
370
+ case ValueType.stringArray:
371
+ case ValueType.referenceArray: return isStringArray(value.value);
372
+ case ValueType.numberArray:
373
+ case ValueType.dateArray: return isNumberArray(value.value) && (value.type !== ValueType.numberArray || isOptionalNumberPrecision(value.precision));
374
+ case ValueType.durationArray: return isNumberArray(value.value) && isDurationUnit(value.unit) && isOptionalNumberPrecision(value.precision);
375
+ default: return false;
376
+ }
377
+ }
378
+ function assertResponseValue(value, path) {
379
+ if (!isResponseValue(value)) throw new Error(`Invalid response value at '${path}'`);
380
+ }
297
381
  //#endregion
298
382
  //#region src/survey/items/utils.ts
299
383
  const displayConditionsFromJson = (json) => {
@@ -305,7 +389,119 @@ const displayConditionsFromJson = (json) => {
305
389
  const disabledConditionsFromJson = (json) => {
306
390
  return { components: json.components ? Object.fromEntries(Object.entries(json.components).map(([key, value]) => [key, Expression.deserialize(value)])) : void 0 };
307
391
  };
308
-
392
+ //#endregion
393
+ //#region src/survey/items/prefill.ts
394
+ const SurveyItemPrefillApplyMode = { ifEmpty: "ifEmpty" };
395
+ const SurveyItemPrefillTargetType = {
396
+ itemResponse: "itemResponse",
397
+ field: "field",
398
+ embeddedField: "embeddedField"
399
+ };
400
+ function deserializeSurveyItemPrefill(json) {
401
+ switch (json.source.type) {
402
+ case "static": return {
403
+ id: json.id,
404
+ target: json.target,
405
+ when: Expression.deserialize(json.when),
406
+ apply: json.apply,
407
+ source: {
408
+ type: "static",
409
+ value: json.source.value
410
+ }
411
+ };
412
+ case "expression": return {
413
+ id: json.id,
414
+ target: json.target,
415
+ when: Expression.deserialize(json.when),
416
+ apply: json.apply,
417
+ source: {
418
+ type: "expression",
419
+ expression: Expression.deserialize(json.source.expression) ?? (() => {
420
+ throw new Error(`Prefill '${json.id}' is missing a valid expression source`);
421
+ })()
422
+ }
423
+ };
424
+ case "templateValue": return {
425
+ id: json.id,
426
+ target: json.target,
427
+ when: Expression.deserialize(json.when),
428
+ apply: json.apply,
429
+ source: {
430
+ type: "templateValue",
431
+ key: json.source.key
432
+ }
433
+ };
434
+ case "previousResponse": return {
435
+ id: json.id,
436
+ target: json.target,
437
+ when: Expression.deserialize(json.when),
438
+ apply: json.apply,
439
+ source: {
440
+ type: "previousResponse",
441
+ ref: json.source.ref,
442
+ ifUnsupported: json.source.ifUnsupported
443
+ }
444
+ };
445
+ default: throw new Error(`Unsupported prefill source type: ${json.source.type}`);
446
+ }
447
+ }
448
+ function serializeSurveyItemPrefill(prefill) {
449
+ switch (prefill.source.type) {
450
+ case "static": return {
451
+ id: prefill.id,
452
+ target: prefill.target,
453
+ when: prefill.when?.serialize(),
454
+ apply: prefill.apply,
455
+ source: {
456
+ type: "static",
457
+ value: prefill.source.value
458
+ }
459
+ };
460
+ case "expression": return {
461
+ id: prefill.id,
462
+ target: prefill.target,
463
+ when: prefill.when?.serialize(),
464
+ apply: prefill.apply,
465
+ source: {
466
+ type: "expression",
467
+ expression: prefill.source.expression.serialize() ?? (() => {
468
+ throw new Error(`Prefill '${prefill.id}' cannot serialize an empty expression source`);
469
+ })()
470
+ }
471
+ };
472
+ case "templateValue": return {
473
+ id: prefill.id,
474
+ target: prefill.target,
475
+ when: prefill.when?.serialize(),
476
+ apply: prefill.apply,
477
+ source: {
478
+ type: "templateValue",
479
+ key: prefill.source.key
480
+ }
481
+ };
482
+ case "previousResponse": return {
483
+ id: prefill.id,
484
+ target: prefill.target,
485
+ when: prefill.when?.serialize(),
486
+ apply: prefill.apply,
487
+ source: {
488
+ type: "previousResponse",
489
+ ref: prefill.source.ref,
490
+ ifUnsupported: prefill.source.ifUnsupported
491
+ }
492
+ };
493
+ default: throw new Error(`Unsupported prefill source type: ${prefill.source.type}`);
494
+ }
495
+ }
496
+ function prefillTargetsEqual(left, right) {
497
+ if (left.type !== right.type) return false;
498
+ switch (left.type) {
499
+ case SurveyItemPrefillTargetType.itemResponse: return true;
500
+ case SurveyItemPrefillTargetType.field: return left.fieldId === right.fieldId;
501
+ case SurveyItemPrefillTargetType.embeddedField: return left.optionId === right.optionId && left.fieldId === right.fieldId;
502
+ default: return false;
503
+ }
504
+ }
309
505
  //#endregion
310
506
  //#region src/survey/items/survey-item.ts
311
507
  /**
@@ -321,7 +517,7 @@ var SurveyItemCore = class {
321
517
  displayConditions;
322
518
  disabledConditions;
323
519
  validations;
324
- prefillRules;
520
+ prefills;
325
521
  constructor(rawItem) {
326
522
  this._rawItem = rawItem;
327
523
  this.updateExpressions();
@@ -329,6 +525,19 @@ var SurveyItemCore = class {
329
525
  this.key = this._rawItem.key;
330
526
  this.config = this.parseConfig(this._rawItem.config);
331
527
  }
528
+ getAdditionalResponseValueSlots() {
529
+ return {};
530
+ }
531
+ getAvailableResponseValueSlots(byType) {
532
+ const valueRefs = {};
533
+ for (const slot of this.getResponseSlotDefinitions()) {
534
+ valueRefs[`${this.id}...get...${slot.slotId}`] = slot.valueType;
535
+ valueRefs[`${this.id}...isDefined...${slot.slotId}`] = ValueType.boolean;
536
+ }
537
+ Object.assign(valueRefs, this.getAdditionalResponseValueSlots());
538
+ if (!byType) return valueRefs;
539
+ return Object.fromEntries(Object.entries(valueRefs).filter(([, valueType]) => valueType === byType));
540
+ }
332
541
  get rawItem() {
333
542
  return this._rawItem;
334
543
  }
@@ -352,39 +561,61 @@ var SurveyItemCore = class {
352
561
  this.displayConditions = this._rawItem.displayConditions ? displayConditionsFromJson(this._rawItem.displayConditions) : void 0;
353
562
  this.disabledConditions = this._rawItem.disabledConditions ? disabledConditionsFromJson(this._rawItem.disabledConditions) : void 0;
354
563
  this.validations = this._rawItem.validations ? Object.fromEntries(Object.entries(this._rawItem.validations).map(([key, value]) => [key, Expression.deserialize(value)])) : void 0;
355
- this.prefillRules = this._rawItem.prefillRules ? this._rawItem.prefillRules.map((rule) => rule ? Expression.deserialize(rule) : void 0) : void 0;
564
+ this.prefills = this._rawItem.prefills?.map((prefill) => deserializeSurveyItemPrefill(prefill));
356
565
  }
357
566
  getReferenceUsages() {
358
567
  const usages = [];
359
568
  if (this.displayConditions) {
360
569
  for (const ref of this.displayConditions.root?.responseVariableRefs || []) usages.push({
361
570
  itemId: this.id,
362
- usageType: ReferenceUsageType.displayConditions,
571
+ usageType: "displayConditions",
363
572
  valueReference: ref
364
573
  });
365
574
  for (const [componentKey, expression] of Object.entries(this.displayConditions.components || {})) for (const ref of expression?.responseVariableRefs || []) usages.push({
366
575
  itemId: this.id,
367
576
  fullComponentKey: componentKey,
368
- usageType: ReferenceUsageType.displayConditions,
577
+ usageType: "displayConditions",
369
578
  valueReference: ref
370
579
  });
371
580
  }
372
581
  if (this.disabledConditions) for (const [componentKey, expression] of Object.entries(this.disabledConditions.components || {})) for (const ref of expression?.responseVariableRefs || []) usages.push({
373
582
  itemId: this.id,
374
583
  fullComponentKey: componentKey,
375
- usageType: ReferenceUsageType.disabledConditions,
584
+ usageType: "disabledConditions",
376
585
  valueReference: ref
377
586
  });
378
587
  if (this.validations) for (const [validationKey, expression] of Object.entries(this.validations)) for (const ref of expression?.responseVariableRefs || []) usages.push({
379
588
  itemId: this.id,
380
589
  fullComponentKey: validationKey,
381
- usageType: ReferenceUsageType.validations,
590
+ usageType: "validations",
382
591
  valueReference: ref
383
592
  });
593
+ if (this.prefills) for (const prefill of this.prefills) {
594
+ for (const ref of prefill.when?.responseVariableRefs || []) usages.push({
595
+ itemId: this.id,
596
+ fullComponentKey: prefill.id,
597
+ usageType: "prefills",
598
+ valueReference: ref
599
+ });
600
+ if (prefill.source.type === "expression") for (const ref of prefill.source.expression.responseVariableRefs || []) usages.push({
601
+ itemId: this.id,
602
+ fullComponentKey: prefill.id,
603
+ usageType: "prefills",
604
+ valueReference: ref
605
+ });
606
+ }
384
607
  return usages;
385
608
  }
609
+ resolvePrefillTarget(target) {
610
+ const slotDefinitions = this.getResponseSlotDefinitions();
611
+ const explicitMatch = slotDefinitions.find((slot) => slot.prefillTarget !== void 0 && prefillTargetsEqual(slot.prefillTarget, target));
612
+ if (explicitMatch) return explicitMatch;
613
+ if (target.type === SurveyItemPrefillTargetType.itemResponse && slotDefinitions.length === 1) return slotDefinitions[0];
614
+ }
615
+ normalizePrefillValue(_prefill, targetSlot, value) {
616
+ return value.type === targetSlot.valueType ? value : void 0;
617
+ }
386
618
  };
387
-
388
619
  //#endregion
389
620
  //#region src/survey/items/types.ts
390
621
  let ReservedSurveyItemTypes = /* @__PURE__ */ function(ReservedSurveyItemTypes) {
@@ -392,7 +623,6 @@ let ReservedSurveyItemTypes = /* @__PURE__ */ function(ReservedSurveyItemTypes)
392
623
  ReservedSurveyItemTypes["PageBreak"] = "page-break";
393
624
  return ReservedSurveyItemTypes;
394
625
  }({});
395
-
396
626
  //#endregion
397
627
  //#region src/survey/item-key.ts
398
628
  /**
@@ -429,7 +659,6 @@ var SurveyItemKey = class {
429
659
  this._keySeparator = keySeparator;
430
660
  }
431
661
  };
432
-
433
662
  //#endregion
434
663
  //#region src/survey/utils/group-utils.ts
435
664
  /**
@@ -454,11 +683,10 @@ function moveArrayElement(arr, fromIndex, toIndex) {
454
683
  result.splice(toIndex, 0, moved);
455
684
  return result;
456
685
  }
457
-
458
686
  //#endregion
459
687
  //#region src/survey/registry/built-in-items.ts
460
688
  var GroupItemCore = class extends SurveyItemCore {
461
- type = ReservedSurveyItemTypes.Group;
689
+ type = "group";
462
690
  parseConfig(rawConfig) {
463
691
  const cfg = rawConfig ?? {};
464
692
  return {
@@ -473,8 +701,8 @@ var GroupItemCore = class extends SurveyItemCore {
473
701
  isInteractive() {
474
702
  return false;
475
703
  }
476
- getAvailableResponseValueSlots() {
477
- return {};
704
+ getResponseSlotDefinitions() {
705
+ return [];
478
706
  }
479
707
  isRoot() {
480
708
  return this.config.isRoot ?? false;
@@ -578,7 +806,7 @@ var GroupItemCore = class extends SurveyItemCore {
578
806
  }
579
807
  };
580
808
  var PageBreakItemCore = class extends SurveyItemCore {
581
- type = ReservedSurveyItemTypes.PageBreak;
809
+ type = "page-break";
582
810
  parseConfig(_rawConfig) {
583
811
  return {};
584
812
  }
@@ -586,16 +814,15 @@ var PageBreakItemCore = class extends SurveyItemCore {
586
814
  isInteractive() {
587
815
  return false;
588
816
  }
589
- getAvailableResponseValueSlots() {
590
- return {};
817
+ getResponseSlotDefinitions() {
818
+ return [];
591
819
  }
592
820
  };
593
821
  const builtInItemCoreRegistry = {
594
- [ReservedSurveyItemTypes.Group]: GroupItemCore,
595
- [ReservedSurveyItemTypes.PageBreak]: PageBreakItemCore
822
+ ["group"]: GroupItemCore,
823
+ ["page-break"]: PageBreakItemCore
596
824
  };
597
825
  const isBuiltInItemType = (itemType) => itemType in builtInItemCoreRegistry;
598
-
599
826
  //#endregion
600
827
  //#region src/survey/registry/item-registry.ts
601
828
  /** Convert constructor registry to explicit definitions for higher-layer composition. */
@@ -633,17 +860,15 @@ function createFullRegistry(pluginRegistry) {
633
860
  function createItemTypeDefinitionRegistry(pluginRegistry) {
634
861
  return toItemTypeDefinitionRegistry(createFullRegistry(pluginRegistry));
635
862
  }
636
-
637
863
  //#endregion
638
864
  //#region src/survey/survey-file-schema.ts
639
865
  const CURRENT_SURVEY_SCHEMA = "https://github.com/case-framework/case-survey-toolkit/packages/survey-core/schemas/survey-schema.json";
640
-
641
866
  //#endregion
642
867
  //#region src/survey/utils/translations.ts
643
868
  const validateLocale = (locale) => {
644
869
  if (locale.trim() === "") throw new Error("Locale cannot be empty");
645
870
  };
646
- var SurveyItemTranslations = class {
871
+ var SurveyItemTranslations = class SurveyItemTranslations {
647
872
  _translations;
648
873
  constructor() {
649
874
  this._translations = {};
@@ -676,6 +901,48 @@ var SurveyItemTranslations = class {
676
901
  if (content) return content;
677
902
  if (fallbackLocale) return this._translations?.[fallbackLocale]?.[contentKey];
678
903
  }
904
+ /**
905
+ * Create a deep clone that can be safely mutated before persisting with
906
+ * `setItemTranslations` or `updateItemTranslations`.
907
+ */
908
+ clone() {
909
+ const cloned = new SurveyItemTranslations();
910
+ for (const locale of this.locales) {
911
+ const localeContent = this.getAllForLocale(locale);
912
+ if (!localeContent) continue;
913
+ cloned.setAllForLocale(locale, structuredCloneMethod(localeContent));
914
+ }
915
+ return cloned;
916
+ }
917
+ /**
918
+ * Remove a single content key from every locale in this translation set.
919
+ */
920
+ removeContentKey(contentKey) {
921
+ for (const locale of this.locales) delete this._translations?.[locale]?.[contentKey];
922
+ }
923
+ /**
924
+ * Remove every content key that is exactly the prefix or nested beneath it.
925
+ */
926
+ removeContentKeysWithPrefix(prefix) {
927
+ for (const locale of this.locales) {
928
+ const localeContent = this._translations?.[locale];
929
+ if (!localeContent) continue;
930
+ for (const contentKey of Object.keys(localeContent)) if (contentKey === prefix || contentKey.startsWith(prefix + ".")) delete localeContent[contentKey];
931
+ }
932
+ }
933
+ /**
934
+ * Remove empty locales in place.
935
+ * Returns `undefined` when no translations remain so callers can pass the
936
+ * result directly into `setItemTranslations` / `updateItemTranslations`.
937
+ */
938
+ compact() {
939
+ for (const locale of this.locales) {
940
+ const localeContent = this._translations?.[locale];
941
+ if (!localeContent) continue;
942
+ if (Object.keys(localeContent).length === 0) delete this._translations?.[locale];
943
+ }
944
+ return this.locales.length > 0 ? this : void 0;
945
+ }
679
946
  };
680
947
  var SurveyTranslations = class {
681
948
  _translations;
@@ -760,20 +1027,26 @@ var SurveyTranslations = class {
760
1027
  const contentForLocale = this._translations?.[locale].itemTranslations?.[itemId];
761
1028
  itemTranslations.setAllForLocale(locale, contentForLocale);
762
1029
  }
763
- return itemTranslations;
1030
+ return itemTranslations.locales.length > 0 ? itemTranslations : void 0;
764
1031
  }
1032
+ /**
1033
+ * Persist item translations after normalizing them:
1034
+ * empty keys/locales are removed automatically, and an empty translation set
1035
+ * clears the item's stored translations entirely.
1036
+ */
765
1037
  setItemTranslations(itemId, itemContent) {
766
- itemContent?.locales.forEach((locale) => validateLocale(locale));
767
- if (!itemContent) {
1038
+ const normalizedItemContent = itemContent?.clone().compact();
1039
+ normalizedItemContent?.locales.forEach((locale) => validateLocale(locale));
1040
+ if (!normalizedItemContent) {
768
1041
  for (const locale of this.locales) if (this._translations[locale].itemTranslations?.[itemId]) delete this._translations[locale].itemTranslations?.[itemId];
769
1042
  } else {
770
- const localesInUpdate = itemContent.locales;
1043
+ const localesInUpdate = normalizedItemContent.locales;
771
1044
  for (const locale of localesInUpdate) if (!this.locales.includes(locale)) this._translations[locale] = {};
772
1045
  for (const locale of this.locales) if (localesInUpdate.includes(locale)) {
773
1046
  if (!this._translations[locale]) this._translations[locale] = {};
774
1047
  if (!this._translations[locale].itemTranslations) this._translations[locale].itemTranslations = {};
775
- this._translations[locale].itemTranslations[itemId] = itemContent.getAllForLocale(locale) ?? {};
776
- } else delete this._translations[locale].itemTranslations[itemId];
1048
+ this._translations[locale].itemTranslations[itemId] = normalizedItemContent.getAllForLocale(locale);
1049
+ } else delete this._translations[locale].itemTranslations?.[itemId];
777
1050
  }
778
1051
  }
779
1052
  /**
@@ -814,7 +1087,110 @@ var SurveyTranslations = class {
814
1087
  for (const locale of this.locales) delete this._translations[locale].itemTranslations?.[id];
815
1088
  }
816
1089
  };
817
-
1090
+ //#endregion
1091
+ //#region src/survey/utils/content.ts
1092
+ let ContentType = /* @__PURE__ */ function(ContentType) {
1093
+ ContentType["richText"] = "richText";
1094
+ ContentType["plain"] = "plain";
1095
+ ContentType["md"] = "md";
1096
+ return ContentType;
1097
+ }({});
1098
+ function hasRenderableRichTextBlock(block) {
1099
+ switch (block.type) {
1100
+ case "paragraph": return block.children.length > 0;
1101
+ case "heading": return block.children.length > 0;
1102
+ case "bulletList": return block.items.some((item) => item.children.some((paragraph) => paragraph.children.length > 0));
1103
+ case "image": return true;
1104
+ case "infoBox": return true;
1105
+ case "separator": return true;
1106
+ case "blockExtension": return true;
1107
+ }
1108
+ }
1109
+ function hasRenderableRichTextContent(content) {
1110
+ return content.doc.blocks.some((block) => hasRenderableRichTextBlock(block));
1111
+ }
1112
+ function createRichTextContent(text = "") {
1113
+ return {
1114
+ type: "richText",
1115
+ version: 1,
1116
+ doc: {
1117
+ type: "doc",
1118
+ blocks: [{
1119
+ type: "paragraph",
1120
+ children: text.length > 0 ? text.split("\n").flatMap((line, index) => {
1121
+ const parts = [];
1122
+ if (index > 0) parts.push({ type: "lineBreak" });
1123
+ if (line.length > 0) parts.push({
1124
+ type: "text",
1125
+ text: line
1126
+ });
1127
+ return parts;
1128
+ }) : []
1129
+ }]
1130
+ }
1131
+ };
1132
+ }
1133
+ function getPlainTextFromLinkChildren(children) {
1134
+ return children.map((inline) => {
1135
+ switch (inline.type) {
1136
+ case "text": return inline.text;
1137
+ case "template": return `{${inline.key}}`;
1138
+ case "lineBreak": return "\n";
1139
+ }
1140
+ }).join("");
1141
+ }
1142
+ function getPlainTextFromInlineNodes(inlines) {
1143
+ return inlines.map((inline) => {
1144
+ switch (inline.type) {
1145
+ case "text": return inline.text;
1146
+ case "template": return `{${inline.key}}`;
1147
+ case "link": return getPlainTextFromLinkChildren(inline.children);
1148
+ case "lineBreak": return "\n";
1149
+ case "inlineExtension": return "";
1150
+ }
1151
+ }).join("");
1152
+ }
1153
+ function getPlainTextFromParagraphs(paragraphs) {
1154
+ return paragraphs.map((paragraph) => getPlainTextFromInlineNodes(paragraph.children)).join("\n");
1155
+ }
1156
+ function getPlainTextFromBlock(block) {
1157
+ switch (block.type) {
1158
+ case "paragraph": return getPlainTextFromInlineNodes(block.children);
1159
+ case "heading": return getPlainTextFromInlineNodes(block.children);
1160
+ case "bulletList": return block.items.map((item) => getPlainTextFromParagraphs(item.children)).join("\n");
1161
+ case "image": {
1162
+ const captionText = block.caption ? getPlainTextFromParagraphs(block.caption) : "";
1163
+ return [block.alt ?? "", captionText].filter(Boolean).join("\n");
1164
+ }
1165
+ case "infoBox": return [block.title ? getPlainTextFromInlineNodes(block.title) : "", getPlainTextFromParagraphs(block.children)].filter(Boolean).join("\n");
1166
+ case "separator": return "";
1167
+ case "blockExtension": return "";
1168
+ }
1169
+ }
1170
+ function getPlainTextFromRichTextContent(content) {
1171
+ return content.doc.blocks.map((block) => getPlainTextFromBlock(block)).filter((text) => text.length > 0).join("\n").trim();
1172
+ }
1173
+ function getContentPlainText(content) {
1174
+ if (!content) return "";
1175
+ if (content.type !== "richText") return content.content ?? "";
1176
+ return getPlainTextFromRichTextContent(content);
1177
+ }
1178
+ function isContentEmpty(content) {
1179
+ if (!content) return true;
1180
+ if (content.type !== "richText") return content.content.trim().length === 0;
1181
+ return !hasRenderableRichTextContent(content);
1182
+ }
1183
+ function getAssetUsagesFromContent(content) {
1184
+ if (!content || content.type !== "richText") return [];
1185
+ const usages = [];
1186
+ content.doc.blocks.forEach((block, blockIndex) => {
1187
+ if (block.type === "image" && block.source.type === "asset") usages.push({
1188
+ assetId: block.source.assetId,
1189
+ blockIndex
1190
+ });
1191
+ });
1192
+ return usages;
1193
+ }
818
1194
  //#endregion
819
1195
  //#region src/survey/survey.ts
820
1196
  var Survey = class Survey {
@@ -822,6 +1198,7 @@ var Survey = class Survey {
822
1198
  metadata;
823
1199
  surveyItems = /* @__PURE__ */ new Map();
824
1200
  _templateValues = /* @__PURE__ */ new Map();
1201
+ _assets = /* @__PURE__ */ new Map();
825
1202
  _translations;
826
1203
  constructor(pluginRegistry) {
827
1204
  this.pluginRegistry = pluginRegistry;
@@ -848,7 +1225,7 @@ var Survey = class Survey {
848
1225
  surveyItems: [{
849
1226
  id: generateId(),
850
1227
  key: newKey,
851
- itemType: ReservedSurveyItemTypes.Group,
1228
+ itemType: "group",
852
1229
  config: {
853
1230
  isRoot: true,
854
1231
  items: [],
@@ -861,7 +1238,7 @@ var Survey = class Survey {
861
1238
  static fromJson(json, pluginRegistry) {
862
1239
  const survey = new Survey(pluginRegistry);
863
1240
  const rawSurvey = json;
864
- if (rawSurvey.$schema !== CURRENT_SURVEY_SCHEMA) throw new Error(`Unsupported survey schema: ${rawSurvey.$schema}`);
1241
+ if (rawSurvey.$schema !== "https://github.com/case-framework/case-survey-toolkit/packages/survey-core/schemas/survey-schema.json") throw new Error(`Unsupported survey schema: ${rawSurvey.$schema}`);
865
1242
  survey.surveyItems = /* @__PURE__ */ new Map();
866
1243
  if (!rawSurvey.surveyItems || rawSurvey.surveyItems.length === 0) throw new Error("surveyItems is required");
867
1244
  rawSurvey.surveyItems.forEach((item) => {
@@ -869,6 +1246,7 @@ var Survey = class Survey {
869
1246
  survey.surveyItems.set(surveyItem.id, surveyItem);
870
1247
  });
871
1248
  if (rawSurvey.templateValues) survey._templateValues = deserializeTemplateValues(rawSurvey.templateValues);
1249
+ if (rawSurvey.assets) survey._assets = new Map(Object.entries(rawSurvey.assets));
872
1250
  survey._translations = new SurveyTranslations(rawSurvey.translations);
873
1251
  if (rawSurvey.maxItemsPerPage) survey.maxItemsPerPage = rawSurvey.maxItemsPerPage;
874
1252
  if (rawSurvey.metadata) survey.metadata = rawSurvey.metadata;
@@ -881,6 +1259,7 @@ var Survey = class Survey {
881
1259
  };
882
1260
  json.translations = this._translations?.serialize();
883
1261
  if (this._templateValues) json.templateValues = serializeTemplateValues(this._templateValues);
1262
+ if (this._assets && this._assets.size > 0) json.assets = Object.fromEntries(this._assets.entries());
884
1263
  if (this.maxItemsPerPage) json.maxItemsPerPage = this.maxItemsPerPage;
885
1264
  if (this.metadata) json.metadata = this.metadata;
886
1265
  return json;
@@ -960,6 +1339,52 @@ var Survey = class Survey {
960
1339
  getTemplateValueKeys() {
961
1340
  return Array.from(this._templateValues?.keys() || []);
962
1341
  }
1342
+ getAsset(assetId) {
1343
+ const asset = this._assets?.get(assetId);
1344
+ return asset ? structuredCloneMethod(asset) : void 0;
1345
+ }
1346
+ setAsset(assetId, asset) {
1347
+ if (!this._assets) this._assets = /* @__PURE__ */ new Map();
1348
+ this._assets.set(assetId, structuredCloneMethod(asset));
1349
+ }
1350
+ deleteAsset(assetId) {
1351
+ this._assets?.delete(assetId);
1352
+ }
1353
+ hasAsset(assetId) {
1354
+ return this._assets?.has(assetId) ?? false;
1355
+ }
1356
+ getAssetIds() {
1357
+ return Array.from(this._assets?.keys() || []);
1358
+ }
1359
+ getAssets() {
1360
+ return new Map(Array.from(this._assets?.entries() || []).map(([assetId, asset]) => [assetId, structuredCloneMethod(asset)]));
1361
+ }
1362
+ getAssetUsages(filterAssetId) {
1363
+ const usages = [];
1364
+ const pushContentUsages = (content, usageScope, contentKey, locale, itemId) => {
1365
+ for (const usage of getAssetUsagesFromContent(content)) {
1366
+ if (filterAssetId && usage.assetId !== filterAssetId) continue;
1367
+ usages.push({
1368
+ assetId: usage.assetId,
1369
+ itemId,
1370
+ locale,
1371
+ contentKey,
1372
+ usageScope,
1373
+ blockIndex: usage.blockIndex
1374
+ });
1375
+ }
1376
+ };
1377
+ const serializedTranslations = this._translations?.serialize();
1378
+ const rootItemId = this.rootItem?.id;
1379
+ if (!serializedTranslations) return usages;
1380
+ for (const [locale, localeTranslations] of Object.entries(serializedTranslations)) {
1381
+ for (const [contentKey, content] of Object.entries(localeTranslations.surveyCardContent || {})) pushContentUsages(content, "surveyCard", contentKey, locale, rootItemId);
1382
+ for (const [contentKey, content] of Object.entries(localeTranslations.navigationContent || {})) pushContentUsages(content, "navigation", contentKey, locale, rootItemId);
1383
+ for (const [contentKey, content] of Object.entries(localeTranslations.validationMessages || {})) pushContentUsages(content, "validationMessages", contentKey, locale, rootItemId);
1384
+ for (const [itemId, itemTranslations] of Object.entries(localeTranslations.itemTranslations || {})) for (const [contentKey, content] of Object.entries(itemTranslations || {})) pushContentUsages(content, "itemTranslation", contentKey, locale, itemId);
1385
+ }
1386
+ return usages;
1387
+ }
963
1388
  getAvailableResponseValueSlots(byType) {
964
1389
  let valueRefs = {};
965
1390
  for (const item of this.surveyItems.values()) valueRefs = {
@@ -968,6 +1393,12 @@ var Survey = class Survey {
968
1393
  };
969
1394
  return valueRefs;
970
1395
  }
1396
+ getResponseSlotDefinitions() {
1397
+ return Array.from(this.surveyItems.values()).map((item) => ({
1398
+ itemId: item.id,
1399
+ slots: item.getResponseSlotDefinitions()
1400
+ }));
1401
+ }
971
1402
  /**
972
1403
  * Get all reference usages for the survey
973
1404
  * @param forItemId - optional item id to filter usages for a specific item and its children (if not provided, all usages are returned)
@@ -981,7 +1412,7 @@ var Survey = class Survey {
981
1412
  }
982
1413
  if (this._templateValues) for (const [templateValueKey, templateValue] of this._templateValues.entries()) for (const ref of templateValue.expression?.responseVariableRefs || []) usages.push({
983
1414
  itemId: templateValueKey,
984
- usageType: ReferenceUsageType.templateValues,
1415
+ usageType: "templateValues",
985
1416
  valueReference: ref
986
1417
  });
987
1418
  return usages;
@@ -991,7 +1422,7 @@ var Survey = class Survey {
991
1422
  return this.getItemPath(targetId).includes(ancestorId);
992
1423
  }
993
1424
  };
994
-
995
1425
  //#endregion
996
- export { ValueReference as A, ContextVariableType as C, FunctionExpressionNames as D, FunctionExpression as E, structuredCloneMethod as F, generateCodingKey as M, generateId as N, ResponseVariableExpression as O, shuffleIndices as P, ContextVariableExpression as S, ExpressionType as T, deserializeTemplateValue as _, createFullRegistry as a, serializeTemplateValues as b, toItemTypeDefinitionRegistry as c, builtInItemCoreRegistry as d, isBuiltInItemType as f, TemplateDefTypes as g, SurveyItemCore as h, validateLocale as i, ValueReferenceMethod as j, ReferenceUsageType as k, GroupItemCore as l, ReservedSurveyItemTypes as m, SurveyItemTranslations as n, createItemCore as o, SurveyItemKey as p, SurveyTranslations as r, createItemTypeDefinitionRegistry as s, Survey as t, PageBreakItemCore as u, deserializeTemplateValues as v, Expression as w, ConstExpression as x, serializeTemplateValue as y };
997
- //# sourceMappingURL=survey-DQmpzihl.mjs.map
1426
+ export { generateId as $, DurationUnits as A, ConstExpression as B, ReservedSurveyItemTypes as C, deserializeSurveyItemPrefill as D, SurveyItemPrefillTargetType as E, TemplateDefTypes as F, FunctionExpression as G, ContextVariableType as H, deserializeTemplateValue as I, ReferenceUsageType as J, FunctionExpressionNames as K, deserializeTemplateValues as L, ValueType as M, assertResponseValue as N, prefillTargetsEqual as O, isResponseValue as P, generateCodingKey as Q, serializeTemplateValue as R, SurveyItemKey as S, SurveyItemPrefillApplyMode as T, Expression as U, ContextVariableExpression as V, ExpressionType as W, ValueReferenceMethod as X, ValueReference as Y, createSeededRandom as Z, toItemTypeDefinitionRegistry as _, getContentPlainText as a, builtInItemCoreRegistry as b, hasRenderableRichTextContent as c, SurveyTranslations as d, shuffleArray as et, validateLocale as f, createItemTypeDefinitionRegistry as g, createItemCore as h, getAssetUsagesFromContent as i, NumberPrecision as j, serializeSurveyItemPrefill as k, isContentEmpty as l, createFullRegistry as m, ContentType as n, structuredCloneMethod as nt, getPlainTextFromRichTextContent as o, CURRENT_SURVEY_SCHEMA as p, ResponseVariableExpression as q, createRichTextContent as r, hasRenderableRichTextBlock as s, Survey as t, shuffleIndices as tt, SurveyItemTranslations as u, GroupItemCore as v, SurveyItemCore as w, isBuiltInItemType as x, PageBreakItemCore as y, serializeTemplateValues as z };
1427
+
1428
+ //# sourceMappingURL=survey-yXdl8xkf.mjs.map