@mapomodule/form 2.0.0-beta.0 → 2.0.0-beta.10

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.
Files changed (194) hide show
  1. package/README.md +2 -1
  2. package/dist/__tests__/__mocks__/nuxt-imports.d.ts +4 -0
  3. package/dist/__tests__/__mocks__/nuxt-imports.d.ts.map +1 -0
  4. package/dist/__tests__/camomillaRoundTrip.test.d.ts +2 -0
  5. package/dist/__tests__/camomillaRoundTrip.test.d.ts.map +1 -0
  6. package/dist/__tests__/registry.test.d.ts +2 -0
  7. package/dist/__tests__/registry.test.d.ts.map +1 -0
  8. package/dist/__tests__/useFormFromSchema.test.d.ts +2 -0
  9. package/dist/__tests__/useFormFromSchema.test.d.ts.map +1 -0
  10. package/dist/__tests__/useMapoForm.test.d.ts +2 -0
  11. package/dist/__tests__/useMapoForm.test.d.ts.map +1 -0
  12. package/dist/index.d.mts +9 -1
  13. package/dist/index.d.ts +15 -1
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.mjs +7 -34
  16. package/dist/module.d.mts +20 -2
  17. package/dist/module.d.ts +12 -2
  18. package/dist/module.d.ts.map +1 -0
  19. package/dist/module.json +1 -1
  20. package/dist/module.mjs +187 -13
  21. package/dist/runtime/components/MapoFocusPortal.vue +115 -0
  22. package/dist/runtime/components/MapoFocusPortal.vue.d.ts +4 -0
  23. package/dist/runtime/components/MapoFocusPortal.vue.d.ts.map +1 -0
  24. package/dist/runtime/components/MapoForm.vue +168 -0
  25. package/dist/runtime/components/MapoForm.vue.d.ts +47 -0
  26. package/dist/runtime/components/MapoForm.vue.d.ts.map +1 -0
  27. package/dist/runtime/components/MapoFormField.vue +269 -0
  28. package/dist/runtime/components/MapoFormField.vue.d.ts +46 -0
  29. package/dist/runtime/components/MapoFormField.vue.d.ts.map +1 -0
  30. package/dist/runtime/components/MapoFormFlatSection.vue +149 -0
  31. package/dist/runtime/components/MapoFormFlatSection.vue.d.ts +15 -0
  32. package/dist/runtime/components/MapoFormFlatSection.vue.d.ts.map +1 -0
  33. package/dist/runtime/components/MapoFormGroup.vue +247 -0
  34. package/dist/runtime/components/MapoFormGroup.vue.d.ts +44 -0
  35. package/dist/runtime/components/MapoFormGroup.vue.d.ts.map +1 -0
  36. package/dist/runtime/components/MapoFormTabs.vue +110 -0
  37. package/dist/runtime/components/MapoFormTabs.vue.d.ts +15 -0
  38. package/dist/runtime/components/MapoFormTabs.vue.d.ts.map +1 -0
  39. package/dist/runtime/components/MapoUnknownField.vue +62 -0
  40. package/dist/runtime/components/MapoUnknownField.vue.d.ts +16 -0
  41. package/dist/runtime/components/MapoUnknownField.vue.d.ts.map +1 -0
  42. package/dist/runtime/components/fields/MapoDateField.vue +88 -0
  43. package/dist/runtime/components/fields/MapoDateField.vue.d.ts +17 -0
  44. package/dist/runtime/components/fields/MapoDateField.vue.d.ts.map +1 -0
  45. package/dist/runtime/components/fields/MapoDateTimeField.vue +159 -0
  46. package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts +17 -0
  47. package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts.map +1 -0
  48. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue +130 -0
  49. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue.d.ts +16 -0
  50. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue.d.ts.map +1 -0
  51. package/dist/runtime/components/fields/MapoFileField.vue +99 -0
  52. package/dist/runtime/components/fields/MapoFileField.vue.d.ts +16 -0
  53. package/dist/runtime/components/fields/MapoFileField.vue.d.ts.map +1 -0
  54. package/dist/runtime/components/fields/MapoFksField.vue +122 -0
  55. package/dist/runtime/components/fields/MapoFksField.vue.d.ts +16 -0
  56. package/dist/runtime/components/fields/MapoFksField.vue.d.ts.map +1 -0
  57. package/dist/runtime/components/fields/MapoMapField.vue +99 -0
  58. package/dist/runtime/components/fields/MapoMapField.vue.d.ts +22 -0
  59. package/dist/runtime/components/fields/MapoMapField.vue.d.ts.map +1 -0
  60. package/dist/runtime/components/fields/MapoMapFieldClient.vue +67 -0
  61. package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts +20 -0
  62. package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts.map +1 -0
  63. package/dist/runtime/components/fields/MapoMediaField.vue +91 -0
  64. package/dist/runtime/components/fields/MapoMediaField.vue.d.ts +16 -0
  65. package/dist/runtime/components/fields/MapoMediaField.vue.d.ts.map +1 -0
  66. package/dist/runtime/components/fields/MapoMediaM2mField.vue +110 -0
  67. package/dist/runtime/components/fields/MapoMediaM2mField.vue.d.ts +16 -0
  68. package/dist/runtime/components/fields/MapoMediaM2mField.vue.d.ts.map +1 -0
  69. package/dist/runtime/components/fields/MapoMediaUnavailable.vue +15 -0
  70. package/dist/runtime/components/fields/MapoMediaUnavailable.vue.d.ts +4 -0
  71. package/dist/runtime/components/fields/MapoMediaUnavailable.vue.d.ts.map +1 -0
  72. package/dist/runtime/components/fields/MapoRepeater.vue +459 -0
  73. package/dist/runtime/components/fields/MapoRepeater.vue.d.ts +16 -0
  74. package/dist/runtime/components/fields/MapoRepeater.vue.d.ts.map +1 -0
  75. package/dist/runtime/components/fields/MapoRepeaterItem.vue +346 -0
  76. package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts +55 -0
  77. package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts.map +1 -0
  78. package/dist/runtime/components/fields/MapoSeoPreview.vue +122 -0
  79. package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts +22 -0
  80. package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts.map +1 -0
  81. package/dist/runtime/components/fields/MapoTimeField.vue +44 -0
  82. package/dist/runtime/components/fields/MapoTimeField.vue.d.ts +17 -0
  83. package/dist/runtime/components/fields/MapoTimeField.vue.d.ts.map +1 -0
  84. package/dist/runtime/components/fields/MapoWygEditor.vue +252 -0
  85. package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts +16 -0
  86. package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts.map +1 -0
  87. package/dist/runtime/components/fields/nui/NuiCheckbox.vue +9 -0
  88. package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts +4 -0
  89. package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts.map +1 -0
  90. package/dist/runtime/components/fields/nui/NuiInput.vue +9 -0
  91. package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts +4 -0
  92. package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts.map +1 -0
  93. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue +18 -0
  94. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts +4 -0
  95. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts.map +1 -0
  96. package/dist/runtime/components/fields/nui/NuiSlider.vue +9 -0
  97. package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts +4 -0
  98. package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts.map +1 -0
  99. package/dist/runtime/components/fields/nui/NuiSwitch.vue +9 -0
  100. package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts +4 -0
  101. package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts.map +1 -0
  102. package/dist/runtime/components/fields/nui/NuiTextarea.vue +9 -0
  103. package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts +4 -0
  104. package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts.map +1 -0
  105. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts +13 -0
  106. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts.map +1 -0
  107. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.js +19 -0
  108. package/dist/runtime/composables/defineFormField.d.ts +25 -0
  109. package/dist/runtime/composables/defineFormField.d.ts.map +1 -0
  110. package/dist/runtime/composables/defineFormField.js +27 -0
  111. package/dist/runtime/composables/useCurrentLang.d.ts +6 -0
  112. package/dist/runtime/composables/useCurrentLang.d.ts.map +1 -0
  113. package/dist/runtime/composables/useCurrentLang.js +9 -0
  114. package/dist/runtime/composables/useFocusMode.d.ts +40 -0
  115. package/dist/runtime/composables/useFocusMode.d.ts.map +1 -0
  116. package/dist/runtime/composables/useFocusMode.js +31 -0
  117. package/dist/runtime/composables/useFormDraft.d.ts +60 -0
  118. package/dist/runtime/composables/useFormDraft.d.ts.map +1 -0
  119. package/dist/runtime/composables/useFormDraft.js +52 -0
  120. package/dist/runtime/composables/useFormFromSchema.d.ts +21 -0
  121. package/dist/runtime/composables/useFormFromSchema.d.ts.map +1 -0
  122. package/dist/runtime/composables/useFormFromSchema.js +145 -0
  123. package/dist/runtime/composables/useMapoForm.d.ts +96 -0
  124. package/dist/runtime/composables/useMapoForm.d.ts.map +1 -0
  125. package/dist/runtime/composables/useMapoForm.js +325 -0
  126. package/dist/runtime/composables/useMediaManager.d.ts +17 -0
  127. package/dist/runtime/composables/useMediaManager.d.ts.map +1 -0
  128. package/dist/runtime/composables/useMediaManager.js +6 -0
  129. package/dist/runtime/composables/useProgressiveDisclosure.d.ts +44 -0
  130. package/dist/runtime/composables/useProgressiveDisclosure.d.ts.map +1 -0
  131. package/dist/runtime/composables/useProgressiveDisclosure.js +23 -0
  132. package/dist/runtime/composables/useUnsavedChangesGuard.d.ts +24 -0
  133. package/dist/runtime/composables/useUnsavedChangesGuard.d.ts.map +1 -0
  134. package/dist/runtime/composables/useUnsavedChangesGuard.js +28 -0
  135. package/dist/runtime/devtools/DevtoolsFormsPage.vue +235 -0
  136. package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts +4 -0
  137. package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts.map +1 -0
  138. package/dist/runtime/plugins/form-registry.d.ts +18 -0
  139. package/dist/runtime/plugins/form-registry.d.ts.map +1 -0
  140. package/dist/runtime/plugins/form-registry.js +19 -0
  141. package/dist/runtime/registry/defaults.d.ts +3 -0
  142. package/dist/runtime/registry/defaults.d.ts.map +1 -0
  143. package/dist/runtime/registry/defaults.js +93 -0
  144. package/dist/runtime/types/descriptor.d.ts +373 -0
  145. package/dist/runtime/types/descriptor.d.ts.map +1 -0
  146. package/dist/runtime/types/descriptor.js +26 -0
  147. package/dist/runtime/types/index.d.ts +12 -0
  148. package/dist/runtime/types/index.d.ts.map +1 -0
  149. package/dist/runtime/types/index.js +6 -0
  150. package/dist/runtime/types/layout.d.ts +21 -0
  151. package/dist/runtime/types/layout.d.ts.map +1 -0
  152. package/dist/runtime/types/layout.js +0 -0
  153. package/dist/runtime/types/media.d.ts +43 -0
  154. package/dist/runtime/types/media.d.ts.map +1 -0
  155. package/dist/runtime/types/media.js +0 -0
  156. package/dist/runtime/types/registry.d.ts +62 -0
  157. package/dist/runtime/types/registry.d.ts.map +1 -0
  158. package/dist/runtime/types/registry.js +19 -0
  159. package/dist/runtime/utils/flattenFieldGroups.d.ts +50 -0
  160. package/dist/runtime/utils/flattenFieldGroups.d.ts.map +1 -0
  161. package/dist/runtime/utils/flattenFieldGroups.js +24 -0
  162. package/dist/types.d.mts +6 -10
  163. package/package.json +10 -8
  164. package/src/__tests__/article.schema.json +193 -0
  165. package/src/__tests__/camomillaRoundTrip.test.ts +73 -0
  166. package/src/__tests__/useFormFromSchema.test.ts +24 -0
  167. package/src/__tests__/useMapoForm.test.ts +5 -0
  168. package/src/index.ts +5 -0
  169. package/src/module.ts +34 -0
  170. package/src/runtime/components/MapoFocusPortal.vue +3 -1
  171. package/src/runtime/components/MapoFormTabs.vue +4 -1
  172. package/src/runtime/components/fields/MapoDateField.vue +3 -1
  173. package/src/runtime/components/fields/MapoDateTimeField.vue +3 -1
  174. package/src/runtime/components/fields/MapoEnhancedMediaField.vue +153 -0
  175. package/src/runtime/components/fields/MapoFileField.vue +3 -1
  176. package/src/runtime/components/fields/MapoFksField.vue +7 -2
  177. package/src/runtime/components/fields/MapoMapField.vue +5 -2
  178. package/src/runtime/components/fields/MapoMediaField.vue +104 -0
  179. package/src/runtime/components/fields/MapoMediaM2mField.vue +130 -0
  180. package/src/runtime/components/fields/MapoMediaUnavailable.vue +19 -0
  181. package/src/runtime/components/fields/MapoRepeater.vue +33 -26
  182. package/src/runtime/components/fields/MapoRepeaterItem.vue +14 -8
  183. package/src/runtime/components/fields/MapoSeoPreview.vue +17 -13
  184. package/src/runtime/components/fields/MapoWygEditor.vue +47 -5
  185. package/src/runtime/composables/useFormFromSchema.ts +4 -1
  186. package/src/runtime/composables/useMapoForm.ts +4 -2
  187. package/src/runtime/composables/useMediaManager.ts +21 -0
  188. package/src/runtime/composables/useUnsavedChangesGuard.ts +3 -3
  189. package/src/runtime/registry/defaults.ts +9 -1
  190. package/src/runtime/types/descriptor.ts +22 -7
  191. package/src/runtime/types/index.ts +4 -0
  192. package/src/runtime/types/media.ts +44 -0
  193. package/dist/index.d.cts +0 -1
  194. package/dist/module.d.cts +0 -2
@@ -0,0 +1,459 @@
1
+ <script setup>
2
+ import { ref, computed, watchEffect } from "vue";
3
+ import { useI18n } from "vue-i18n";
4
+ import { VueDraggable } from "vue-draggable-plus";
5
+ import { useConfirmStore } from "@mapomodule/store/runtime/stores/confirm";
6
+ import { injectMapoForm } from "../../composables/useMapoForm.js";
7
+ import MapoRepeaterItem from "./MapoRepeaterItem.vue";
8
+ const props = defineProps({
9
+ modelValue: { type: null, required: true },
10
+ descriptor: { type: Object, required: true },
11
+ errors: { type: Array, required: false },
12
+ readonly: { type: Boolean, required: false },
13
+ disabled: { type: Boolean, required: false }
14
+ });
15
+ const emit = defineEmits(["update:modelValue"]);
16
+ const form = injectMapoForm();
17
+ const { t } = useI18n();
18
+ const attrs = computed(() => props.descriptor.attrs ?? {});
19
+ const items = computed({
20
+ get: () => Array.isArray(props.modelValue) ? props.modelValue : [],
21
+ set: (val) => emit("update:modelValue", val)
22
+ });
23
+ function emitItems(newItems) {
24
+ emit("update:modelValue", [...newItems]);
25
+ }
26
+ const itemUids = /* @__PURE__ */ new WeakMap();
27
+ let _uidSeq = 0;
28
+ function uidFor(item) {
29
+ let id = itemUids.get(item);
30
+ if (!id) {
31
+ id = `it_${++_uidSeq}`;
32
+ itemUids.set(item, id);
33
+ }
34
+ return id;
35
+ }
36
+ const undoStack = ref([]);
37
+ function pushUndo() {
38
+ undoStack.value.push(items.value.map((i) => ({ ...i })));
39
+ if (undoStack.value.length > 20) undoStack.value.shift();
40
+ }
41
+ function undo() {
42
+ const prev = undoStack.value.pop();
43
+ if (prev) emit("update:modelValue", prev);
44
+ }
45
+ const templates = computed(() => attrs.value.templates);
46
+ const templateKeys = computed(
47
+ () => templates.value ? Object.keys(templates.value) : []
48
+ );
49
+ const hasTemplates = computed(() => templateKeys.value.length > 0);
50
+ const selectedTemplate = ref("");
51
+ watchEffect(() => {
52
+ if (!selectedTemplate.value && templateKeys.value[0]) {
53
+ selectedTemplate.value = templateKeys.value[0];
54
+ }
55
+ });
56
+ function fieldsForItem(item) {
57
+ if (!templates.value) return props.descriptor.fields ?? [];
58
+ const key = item._template ?? selectedTemplate.value;
59
+ return templates.value[key] ?? props.descriptor.fields ?? [];
60
+ }
61
+ const canAdd = computed(
62
+ () => !props.readonly && (!attrs.value.maxItems || items.value.length < attrs.value.maxItems)
63
+ );
64
+ const canDelete = computed(
65
+ () => !props.readonly && (!attrs.value.minItems || items.value.length > attrs.value.minItems)
66
+ );
67
+ function remainingSlots() {
68
+ if (!attrs.value.maxItems) return Number.POSITIVE_INFINITY;
69
+ return Math.max(0, attrs.value.maxItems - items.value.length);
70
+ }
71
+ function minBound() {
72
+ return attrs.value.minItems ?? 0;
73
+ }
74
+ async function askConfirm(message, title) {
75
+ title ??= t("mapo.confirm");
76
+ try {
77
+ return await useConfirmStore().ask({ title, message });
78
+ } catch {
79
+ return typeof globalThis.confirm === "function" ? globalThis.confirm(message) : false;
80
+ }
81
+ }
82
+ function addItem() {
83
+ if (!canAdd.value) return;
84
+ pushUndo();
85
+ const newItem = hasTemplates.value ? { _template: selectedTemplate.value } : {};
86
+ emitItems([...items.value, newItem]);
87
+ }
88
+ async function deleteItem(index) {
89
+ if (!canDelete.value) return;
90
+ if (attrs.value.confirmDelete !== false) {
91
+ const confirmed = await askConfirm(
92
+ t("mapo.confirmDelete"),
93
+ t("mapo.repeater.deleteItem")
94
+ );
95
+ if (!confirmed) return;
96
+ }
97
+ pushUndo();
98
+ const next = [...items.value];
99
+ next.splice(index, 1);
100
+ emitItems(next);
101
+ }
102
+ function duplicateItem(index) {
103
+ if (remainingSlots() < 1) return;
104
+ const source = items.value[index];
105
+ if (!source) return;
106
+ pushUndo();
107
+ const copy = { ...source };
108
+ const next = [...items.value];
109
+ next.splice(index + 1, 0, copy);
110
+ emitItems(next);
111
+ }
112
+ function updateItem(index, val) {
113
+ const oldItem = items.value[index];
114
+ if (oldItem && oldItem !== val) {
115
+ const existingUid = itemUids.get(oldItem);
116
+ if (existingUid) itemUids.set(val, existingUid);
117
+ }
118
+ const next = [...items.value];
119
+ next[index] = val;
120
+ emitItems(next);
121
+ }
122
+ function moveUp(index) {
123
+ moveTo(index, index - 1);
124
+ }
125
+ function moveDown(index) {
126
+ moveTo(index, index + 1);
127
+ }
128
+ function moveTo(from, to) {
129
+ if (to < 0 || to >= items.value.length || from === to) return;
130
+ pushUndo();
131
+ const next = [...items.value];
132
+ const [item] = next.splice(from, 1);
133
+ if (item === void 0) return;
134
+ next.splice(to, 0, item);
135
+ emitItems(next);
136
+ }
137
+ const collapseVersion = ref(0);
138
+ const globalExpanded = ref(attrs.value.defaultExpanded ?? false);
139
+ function collapseAll() {
140
+ globalExpanded.value = false;
141
+ collapseVersion.value++;
142
+ }
143
+ function expandAll() {
144
+ globalExpanded.value = true;
145
+ collapseVersion.value++;
146
+ }
147
+ function onDragStart() {
148
+ pushUndo();
149
+ }
150
+ const selectionMode = ref(false);
151
+ const selectedUids = ref(/* @__PURE__ */ new Set());
152
+ const anchorUid = ref(null);
153
+ function toggleSelectionMode() {
154
+ selectionMode.value = !selectionMode.value;
155
+ if (!selectionMode.value) {
156
+ selectedUids.value = /* @__PURE__ */ new Set();
157
+ anchorUid.value = null;
158
+ collapseVersion.value++;
159
+ } else {
160
+ globalExpanded.value = false;
161
+ collapseVersion.value++;
162
+ }
163
+ }
164
+ function isSelected(uid) {
165
+ return selectedUids.value.has(uid);
166
+ }
167
+ function toggleSelect(uid, withShift = false) {
168
+ if (withShift && anchorUid.value && anchorUid.value !== uid) {
169
+ const uids = items.value.map(uidFor);
170
+ const a = uids.indexOf(anchorUid.value);
171
+ const b = uids.indexOf(uid);
172
+ if (a !== -1 && b !== -1) {
173
+ const [lo, hi] = a < b ? [a, b] : [b, a];
174
+ const next2 = new Set(selectedUids.value);
175
+ for (let i = lo; i <= hi; i++) {
176
+ const uid2 = uids[i];
177
+ if (uid2) next2.add(uid2);
178
+ }
179
+ selectedUids.value = next2;
180
+ return;
181
+ }
182
+ }
183
+ const next = new Set(selectedUids.value);
184
+ if (next.has(uid)) next.delete(uid);
185
+ else next.add(uid);
186
+ selectedUids.value = next;
187
+ anchorUid.value = uid;
188
+ }
189
+ function selectAll() {
190
+ selectedUids.value = new Set(items.value.map(uidFor));
191
+ }
192
+ function clearSelection() {
193
+ selectedUids.value = /* @__PURE__ */ new Set();
194
+ anchorUid.value = null;
195
+ }
196
+ function selectedItems() {
197
+ return items.value.filter((i) => selectedUids.value.has(uidFor(i)));
198
+ }
199
+ async function bulkDelete() {
200
+ const count = selectedUids.value.size;
201
+ if (count === 0) return;
202
+ if (items.value.length - count < minBound()) {
203
+ await askConfirm(
204
+ t("mapo.repeater.minItemsBlock", { min: minBound() }),
205
+ t("mapo.repeater.actionBlocked")
206
+ );
207
+ return;
208
+ }
209
+ const confirmed = await askConfirm(
210
+ t("mapo.repeater.confirmBulkDelete", { count }),
211
+ t("mapo.repeater.deleteItems")
212
+ );
213
+ if (!confirmed) return;
214
+ pushUndo();
215
+ emitItems(items.value.filter((i) => !selectedUids.value.has(uidFor(i))));
216
+ clearSelection();
217
+ }
218
+ function bulkDuplicate() {
219
+ const count = selectedUids.value.size;
220
+ if (count === 0) return;
221
+ const slots = remainingSlots();
222
+ if (slots <= 0) return;
223
+ const toDup = selectedItems().slice(0, slots);
224
+ pushUndo();
225
+ const next = [...items.value];
226
+ const dupSet = new Set(toDup.map(uidFor));
227
+ for (let i = next.length - 1; i >= 0; i--) {
228
+ const item = next[i];
229
+ if (item && dupSet.has(uidFor(item))) {
230
+ next.splice(i + 1, 0, { ...item });
231
+ }
232
+ }
233
+ emitItems(next);
234
+ clearSelection();
235
+ }
236
+ function bulkMoveToTop() {
237
+ if (selectedUids.value.size === 0) return;
238
+ pushUndo();
239
+ const selected = items.value.filter((i) => selectedUids.value.has(uidFor(i)));
240
+ const rest = items.value.filter((i) => !selectedUids.value.has(uidFor(i)));
241
+ emitItems([...selected, ...rest]);
242
+ }
243
+ const hasSelection = computed(() => selectedUids.value.size > 0);
244
+ const selectionCount = computed(() => selectedUids.value.size);
245
+ const parentErrors = computed(() => form.errors.value);
246
+ const errorPrefix = (index) => `${props.descriptor.key}.${index}`;
247
+ </script>
248
+
249
+ <template>
250
+ <div class="mapo-repeater space-y-2">
251
+ <!-- ── Main toolbar ────────────────────────────────────────────────────── -->
252
+ <div
253
+ class="flex flex-wrap items-center gap-2 rounded border border-gray-200 bg-gray-50 px-3 py-2"
254
+ >
255
+ <span class="text-sm font-medium text-gray-700">
256
+ {{ t("mapo.repeater.nItems", { n: items.length }, items.length) }}
257
+ <span v-if="attrs.maxItems" class="text-gray-400"
258
+ >/ {{ attrs.maxItems }}</span
259
+ >
260
+ </span>
261
+
262
+ <div class="flex flex-1 flex-wrap items-center justify-end gap-2">
263
+ <UButton size="xs" variant="ghost" color="neutral" @click="collapseAll">
264
+ {{ t("mapo.repeater.collapseAll") }}
265
+ </UButton>
266
+ <UButton size="xs" variant="ghost" color="neutral" @click="expandAll">
267
+ {{ t("mapo.repeater.expandAll") }}
268
+ </UButton>
269
+ <UButton
270
+ v-if="undoStack.length"
271
+ size="xs"
272
+ variant="ghost"
273
+ color="neutral"
274
+ icon="i-lucide-undo-2"
275
+ :title="t('mapo.repeater.undoLastAction')"
276
+ @click="undo"
277
+ />
278
+
279
+ <!-- Bulk selection mode -->
280
+ <UButton
281
+ size="xs"
282
+ :variant="selectionMode ? 'solid' : 'ghost'"
283
+ color="neutral"
284
+ icon="i-lucide-check-square"
285
+ :title="t('mapo.repeater.bulkSelection')"
286
+ :label="
287
+ selectionMode ? t('mapo.repeater.exitSelection') : t('mapo.select')
288
+ "
289
+ @click="toggleSelectionMode"
290
+ />
291
+
292
+ <!-- Template selector -->
293
+ <USelectMenu
294
+ v-if="hasTemplates"
295
+ v-model="selectedTemplate"
296
+ :items="templateKeys"
297
+ size="xs"
298
+ class="w-32"
299
+ />
300
+
301
+ <UButton
302
+ size="sm"
303
+ color="primary"
304
+ icon="i-lucide-plus"
305
+ :disabled="!canAdd"
306
+ @click="addItem"
307
+ >
308
+ {{ t("mapo.add") }}
309
+ </UButton>
310
+ </div>
311
+ </div>
312
+
313
+ <!-- ── Bulk actions bar (visible when there is a selection) ───────────── -->
314
+ <Transition name="slide-down">
315
+ <div
316
+ v-if="selectionMode && hasSelection"
317
+ class="flex items-center gap-2 rounded border border-primary-200 bg-primary-50 px-3 py-2"
318
+ >
319
+ <span class="text-sm font-medium text-primary-700">
320
+ {{ t("mapo.repeater.nSelected", { n: selectionCount }) }}
321
+ </span>
322
+ <div class="flex flex-1 flex-wrap items-center gap-2">
323
+ <UButton size="xs" variant="ghost" color="neutral" @click="selectAll">
324
+ {{ t("mapo.repeater.all") }}
325
+ </UButton>
326
+ <UButton
327
+ size="xs"
328
+ variant="ghost"
329
+ color="neutral"
330
+ @click="clearSelection"
331
+ >
332
+ {{ t("mapo.clear") }}
333
+ </UButton>
334
+ <UButton
335
+ size="xs"
336
+ variant="outline"
337
+ color="neutral"
338
+ icon="i-lucide-copy"
339
+ :disabled="remainingSlots() <= 0"
340
+ :title="
341
+ remainingSlots() < selectionCount ? t('mapo.repeater.duplicateLimited', {
342
+ slots: remainingSlots(),
343
+ count: selectionCount
344
+ }) : t('mapo.repeater.duplicateSelected')
345
+ "
346
+ @click="bulkDuplicate"
347
+ >
348
+ {{ t("mapo.duplicate") }} ({{
349
+ Math.min(selectionCount, remainingSlots())
350
+ }})
351
+ </UButton>
352
+ <UButton
353
+ size="xs"
354
+ variant="outline"
355
+ color="neutral"
356
+ icon="i-lucide-arrow-up"
357
+ @click="bulkMoveToTop"
358
+ >
359
+ {{ t("mapo.repeater.moveToTop") }}
360
+ </UButton>
361
+ <UButton
362
+ size="xs"
363
+ variant="outline"
364
+ color="error"
365
+ icon="i-lucide-trash-2"
366
+ :disabled="!canDelete"
367
+ @click="bulkDelete"
368
+ >
369
+ {{ t("mapo.delete") }} ({{ selectionCount }})
370
+ </UButton>
371
+ </div>
372
+ </div>
373
+ </Transition>
374
+
375
+ <!-- ── Item list with drag&drop (ClientOnly for SSR) ───────────────────── -->
376
+ <ClientOnly>
377
+ <VueDraggable
378
+ v-model="items"
379
+ handle=".drag-handle"
380
+ :animation="150"
381
+ :disabled="readonly || selectionMode"
382
+ @start="onDragStart"
383
+ >
384
+ <MapoRepeaterItem
385
+ v-for="(item, index) in items"
386
+ :key="`${uidFor(item)}-${collapseVersion}`"
387
+ :uid="uidFor(item)"
388
+ :item="item"
389
+ :fields="fieldsForItem(item)"
390
+ :index="index"
391
+ :error-prefix="errorPrefix(index)"
392
+ :parent-errors="parentErrors"
393
+ :languages="form.languages"
394
+ :current-lang="form.currentLang.value"
395
+ :readonly="readonly ?? false"
396
+ :registry="form.registry"
397
+ :preview-label="attrs.previewLabel"
398
+ :default-expanded="globalExpanded"
399
+ :allow-duplicate="attrs.allowDuplicate"
400
+ :repeater-descriptor="descriptor"
401
+ :total-items="items.length"
402
+ :selected="isSelected(uidFor(item))"
403
+ :selection-mode="selectionMode"
404
+ @update:item="updateItem(index, $event)"
405
+ @delete="deleteItem(index)"
406
+ @duplicate="duplicateItem(index)"
407
+ @move-up="moveUp(index)"
408
+ @move-down="moveDown(index)"
409
+ @move-to="moveTo(index, $event)"
410
+ @toggle-select="toggleSelect(uidFor(item), $event)"
411
+ />
412
+ </VueDraggable>
413
+
414
+ <!-- SSR fallback: list without drag -->
415
+ <template #fallback>
416
+ <MapoRepeaterItem
417
+ v-for="(item, index) in items"
418
+ :key="`${uidFor(item)}-${collapseVersion}`"
419
+ :uid="uidFor(item)"
420
+ :item="item"
421
+ :fields="fieldsForItem(item)"
422
+ :index="index"
423
+ :error-prefix="errorPrefix(index)"
424
+ :parent-errors="parentErrors"
425
+ :languages="form.languages"
426
+ :current-lang="form.currentLang.value"
427
+ :readonly="readonly ?? false"
428
+ :registry="form.registry"
429
+ :preview-label="attrs.previewLabel"
430
+ :default-expanded="globalExpanded"
431
+ :allow-duplicate="attrs.allowDuplicate"
432
+ :repeater-descriptor="descriptor"
433
+ :total-items="items.length"
434
+ :selected="isSelected(uidFor(item))"
435
+ :selection-mode="selectionMode"
436
+ @update:item="updateItem(index, $event)"
437
+ @delete="deleteItem(index)"
438
+ @duplicate="duplicateItem(index)"
439
+ @move-up="moveUp(index)"
440
+ @move-down="moveDown(index)"
441
+ @move-to="moveTo(index, $event)"
442
+ @toggle-select="toggleSelect(uidFor(item), $event)"
443
+ />
444
+ </template>
445
+ </ClientOnly>
446
+
447
+ <!-- ── Empty state ─────────────────────────────────────────────────────── -->
448
+ <div
449
+ v-if="items.length === 0"
450
+ class="rounded border border-dashed border-gray-300 py-8 text-center text-sm text-gray-400"
451
+ >
452
+ {{ t("mapo.repeater.empty") }}
453
+ </div>
454
+ </div>
455
+ </template>
456
+
457
+ <style scoped>
458
+ .slide-down-enter-active,.slide-down-leave-active{display:grid;transition:grid-template-rows .25s ease,opacity .2s ease}.slide-down-enter-from,.slide-down-leave-to{grid-template-rows:0fr;opacity:0}.slide-down-enter-to,.slide-down-leave-from{grid-template-rows:1fr;opacity:1}
459
+ </style>
@@ -0,0 +1,16 @@
1
+ import type { RepeaterDescriptor } from "../../types/index.js";
2
+ type __VLS_Props = {
3
+ modelValue: unknown;
4
+ descriptor: RepeaterDescriptor;
5
+ errors?: string[];
6
+ readonly?: boolean;
7
+ disabled?: boolean;
8
+ };
9
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ "update:modelValue": (value: unknown[]) => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ "onUpdate:modelValue"?: ((value: unknown[]) => any) | undefined;
13
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ declare const _default: typeof __VLS_export;
15
+ export default _default;
16
+ //# sourceMappingURL=MapoRepeater.vue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MapoRepeater.vue.d.ts","sourceRoot":"","sources":["../../../../src/runtime/components/fields/MapoRepeater.vue"],"names":[],"mappings":"AAujBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAI/D,KAAK,WAAW,GAAG;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,kBAAkB,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAgjCF,QAAA,MAAM,YAAY;;;;kFAGhB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}