@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,325 @@
1
+ import {
2
+ ref,
3
+ computed,
4
+ toRaw,
5
+ toValue,
6
+ watch,
7
+ provide,
8
+ inject
9
+ } from "vue";
10
+ import {
11
+ getNestedValue,
12
+ setNestedValueMutating,
13
+ objectDiff,
14
+ debounce
15
+ } from "@mapomodule/utils";
16
+ import { resolveFieldAccessor } from "../types/index.js";
17
+ import { useCurrentLang } from "./useCurrentLang.js";
18
+ import { useSnackStore } from "@mapomodule/store/runtime/stores/snack";
19
+ import { useMapoT } from "@mapomodule/i18n/runtime/composables/useMapoT";
20
+ import { useAuthStore } from "@mapomodule/store/runtime/stores/auth";
21
+ function safeClone(v) {
22
+ const raw = toRaw(v);
23
+ if (typeof structuredClone === "function") {
24
+ try {
25
+ return structuredClone(raw);
26
+ } catch {
27
+ }
28
+ }
29
+ return JSON.parse(JSON.stringify(raw));
30
+ }
31
+ export const FORM_KEY = Symbol("mapoForm");
32
+ function provideMapoForm(ctx) {
33
+ provide(FORM_KEY, ctx);
34
+ }
35
+ export function injectMapoForm() {
36
+ return inject(FORM_KEY, void 0);
37
+ }
38
+ export function useMapoForm(options) {
39
+ const {
40
+ model,
41
+ fields,
42
+ errors = ref({}),
43
+ languages = [],
44
+ currentLang: currentLangProp,
45
+ immediate = false,
46
+ debounce: debounceMs = 300,
47
+ notifyErrors = true,
48
+ registry
49
+ } = options;
50
+ const currentLang = useCurrentLang(currentLangProp);
51
+ const snack = useSnackStore();
52
+ const t = useMapoT();
53
+ const authStore = useAuthStore();
54
+ const backup = ref(safeClone(model.value));
55
+ const clientErrors = ref({});
56
+ const isLoading = ref(false);
57
+ const readonly = ref(false);
58
+ const touched = ref(/* @__PURE__ */ new Set());
59
+ const submitted = ref(false);
60
+ if (notifyErrors) {
61
+ watch(
62
+ errors,
63
+ (val) => {
64
+ const entries = Object.entries(val);
65
+ if (!entries.length) return;
66
+ const lines = entries.map(([field, msgs]) => `${field}: ${msgs.join(", ")}`).join("\n");
67
+ snack.show(lines, "error");
68
+ },
69
+ { deep: true }
70
+ );
71
+ }
72
+ const dirtyKeys = ref(/* @__PURE__ */ new Set());
73
+ const isDirty = computed(() => dirtyKeys.value.size > 0);
74
+ const DRAFT_TTL = 864e5;
75
+ const draftBanner = ref(null);
76
+ function _draftStorageKey() {
77
+ const key = toValue(options.draftKey);
78
+ if (!key) return null;
79
+ const userId = authStore.info?.id ?? "anonymous";
80
+ return `mapo:draft:${userId}:${key}`;
81
+ }
82
+ function clearDraft() {
83
+ const key = _draftStorageKey();
84
+ if (key && typeof localStorage !== "undefined")
85
+ localStorage.removeItem(key);
86
+ }
87
+ function checkDraft() {
88
+ const key = _draftStorageKey();
89
+ if (!key || typeof localStorage === "undefined") return;
90
+ try {
91
+ const raw = localStorage.getItem(key);
92
+ if (!raw) return;
93
+ const entry = JSON.parse(raw);
94
+ if (Date.now() - entry.savedAt > DRAFT_TTL) {
95
+ localStorage.removeItem(key);
96
+ return;
97
+ }
98
+ const savedAt = new Date(entry.savedAt);
99
+ draftBanner.value = {
100
+ savedAt,
101
+ restore: () => {
102
+ Object.assign(model.value, entry.model);
103
+ draftBanner.value = null;
104
+ },
105
+ discard: () => {
106
+ localStorage.removeItem(key);
107
+ draftBanner.value = null;
108
+ }
109
+ };
110
+ } catch {
111
+ }
112
+ }
113
+ if (options.draftKey) {
114
+ const saveDraft = debounce(() => {
115
+ const key = _draftStorageKey();
116
+ if (!key || !isDirty.value || typeof localStorage === "undefined") return;
117
+ try {
118
+ const noDraftKeys = new Set(
119
+ toValue(fields).filter((f) => f.noDraft).map((f) => f.key)
120
+ );
121
+ const snapshot = toRaw(model.value);
122
+ const filtered = noDraftKeys.size > 0 ? Object.fromEntries(
123
+ Object.entries(snapshot).filter(([k]) => !noDraftKeys.has(k))
124
+ ) : snapshot;
125
+ localStorage.setItem(
126
+ key,
127
+ JSON.stringify({ model: filtered, savedAt: Date.now() })
128
+ );
129
+ } catch {
130
+ }
131
+ }, 2e3);
132
+ watch(model, saveDraft, { deep: true });
133
+ }
134
+ function markTouched(key) {
135
+ if (touched.value.has(key)) return;
136
+ touched.value.add(key);
137
+ touched.value = new Set(touched.value);
138
+ }
139
+ function isTouched(key) {
140
+ return submitted.value || touched.value.has(key);
141
+ }
142
+ const allErrors = computed(() => {
143
+ const merged = { ...errors.value };
144
+ for (const [k, v] of Object.entries(clientErrors.value)) {
145
+ merged[k] = [v, ...merged[k] ?? []];
146
+ }
147
+ return merged;
148
+ });
149
+ function resolvePath(descriptor) {
150
+ const lang = currentLang.value;
151
+ if (descriptor.translatable && lang) {
152
+ return `translations.${lang}.${descriptor.key}`;
153
+ }
154
+ return descriptor.key;
155
+ }
156
+ function initLang(lang) {
157
+ const m = model.value;
158
+ if (!m.translations) m.translations = {};
159
+ const translations = m.translations;
160
+ if (!translations[lang]) translations[lang] = {};
161
+ }
162
+ function setFieldValue(descriptor, val) {
163
+ const accessor = resolveFieldAccessor(descriptor, registry);
164
+ const setVal = accessor.set ? accessor.set({ model: model.value, val, lang: currentLang.value }) : val;
165
+ if (descriptor.synci18n && languages.length) {
166
+ for (const lang of languages) {
167
+ initLang(lang);
168
+ setNestedValueMutating(
169
+ model.value,
170
+ `translations.${lang}.${descriptor.key}`,
171
+ setVal
172
+ );
173
+ }
174
+ } else {
175
+ if (descriptor.translatable && currentLang.value)
176
+ initLang(currentLang.value);
177
+ setNestedValueMutating(
178
+ model.value,
179
+ resolvePath(descriptor),
180
+ setVal
181
+ );
182
+ }
183
+ descriptor.onChange?.({ model: model.value, val: setVal });
184
+ const key = descriptor.key;
185
+ if (!dirtyKeys.value.has(key)) {
186
+ dirtyKeys.value.add(key);
187
+ dirtyKeys.value = new Set(dirtyKeys.value);
188
+ }
189
+ if (clientErrors.value[key]) {
190
+ const next = { ...clientErrors.value };
191
+ delete next[key];
192
+ clientErrors.value = next;
193
+ }
194
+ }
195
+ function getFieldValue(descriptor) {
196
+ const raw = getNestedValue(model.value, resolvePath(descriptor));
197
+ const accessor = resolveFieldAccessor(descriptor, registry);
198
+ return accessor.get ? accessor.get({ model: model.value, val: raw, lang: currentLang.value }) : raw;
199
+ }
200
+ function checkRequired(descriptor, val) {
201
+ if (!descriptor.required) return null;
202
+ if (val === null || val === void 0 || val === "")
203
+ return t("mapo.form.required");
204
+ return null;
205
+ }
206
+ function getClientError(descriptor) {
207
+ if (!descriptor.required && !descriptor.validate) return null;
208
+ if (!immediate && !isTouched(descriptor.key)) return null;
209
+ const val = getFieldValue(descriptor);
210
+ return checkRequired(descriptor, val) ?? descriptor.validate?.(val, {
211
+ model: model.value
212
+ }) ?? null;
213
+ }
214
+ function validateClient() {
215
+ submitted.value = true;
216
+ const errs = {};
217
+ const fs = toValue(fields);
218
+ for (const descriptor of fs) {
219
+ if (!descriptor.required && !descriptor.validate) continue;
220
+ const val = getFieldValue(descriptor);
221
+ const err = checkRequired(descriptor, val) ?? descriptor.validate?.(val, {
222
+ model: model.value
223
+ }) ?? null;
224
+ if (err) errs[descriptor.key] = err;
225
+ }
226
+ clientErrors.value = errs;
227
+ return { valid: Object.keys(errs).length === 0, errors: errs };
228
+ }
229
+ function resetDirty() {
230
+ backup.value = safeClone(model.value);
231
+ dirtyKeys.value = /* @__PURE__ */ new Set();
232
+ }
233
+ function getPatch() {
234
+ return objectDiff(backup.value, model.value);
235
+ }
236
+ async function submit(handler, isNew = false) {
237
+ if (isLoading.value) return;
238
+ submitted.value = true;
239
+ const { valid } = validateClient();
240
+ if (!valid) {
241
+ snack.show(t("mapo.fixErrors"), "error");
242
+ return;
243
+ }
244
+ isLoading.value = true;
245
+ try {
246
+ await handler(isNew ? model.value : getPatch(), isNew);
247
+ resetDirty();
248
+ clearDraft();
249
+ submitted.value = false;
250
+ touched.value = /* @__PURE__ */ new Set();
251
+ } finally {
252
+ isLoading.value = false;
253
+ }
254
+ }
255
+ const fieldIndex = computed(() => {
256
+ const map = /* @__PURE__ */ new Map();
257
+ for (const f of toValue(fields)) map.set(f.key, f);
258
+ return map;
259
+ });
260
+ const findDescriptor = (key) => fieldIndex.value.get(key);
261
+ const ctx = {
262
+ model,
263
+ fields,
264
+ errors: allErrors,
265
+ languages,
266
+ currentLang,
267
+ readonly,
268
+ immediate,
269
+ debounce: debounceMs,
270
+ registry,
271
+ setFieldValue: (key, val) => {
272
+ const descriptor = findDescriptor(key);
273
+ if (descriptor) setFieldValue(descriptor, val);
274
+ },
275
+ getClientError: (key) => {
276
+ const descriptor = findDescriptor(key);
277
+ return descriptor ? getClientError(descriptor) : null;
278
+ },
279
+ markTouched,
280
+ isTouched,
281
+ submitted
282
+ };
283
+ function setFieldValuePublic(keyOrDescriptor, val) {
284
+ if (typeof keyOrDescriptor === "string") {
285
+ const descriptor = findDescriptor(keyOrDescriptor);
286
+ if (descriptor) setFieldValue(descriptor, val);
287
+ return;
288
+ }
289
+ setFieldValue(keyOrDescriptor, val);
290
+ }
291
+ function getClientErrorPublic(keyOrDescriptor) {
292
+ if (typeof keyOrDescriptor === "string") {
293
+ const descriptor = findDescriptor(keyOrDescriptor);
294
+ return descriptor ? getClientError(descriptor) : null;
295
+ }
296
+ return getClientError(keyOrDescriptor);
297
+ }
298
+ provideMapoForm(ctx);
299
+ return {
300
+ state: computed(() => ({
301
+ model: model.value,
302
+ errors: allErrors.value,
303
+ isDirty: isDirty.value,
304
+ currentLang: currentLang.value,
305
+ isLoading: isLoading.value
306
+ })),
307
+ isDirty,
308
+ isLoading,
309
+ readonly,
310
+ currentLang,
311
+ getFieldValue,
312
+ setFieldValue: setFieldValuePublic,
313
+ getClientError: getClientErrorPublic,
314
+ markTouched,
315
+ isTouched,
316
+ validateClient,
317
+ resetDirty,
318
+ getPatch,
319
+ submit,
320
+ ctx,
321
+ draftBanner,
322
+ checkDraft,
323
+ clearDraft
324
+ };
325
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Resolves the Media Manager picker dialog at runtime.
3
+ *
4
+ * The media field types ship with `@mapomodule/form`, but the Media Manager
5
+ * itself lives in `@mapomodule/uikit`. Resolving the dialog by name instead of
6
+ * importing it keeps `form` free of a static dependency on `uikit` (which
7
+ * depends on `form`, so a static import would be a cycle) and lets `form` stay
8
+ * usable standalone — the fields then render an explicit notice instead of a
9
+ * broken picker.
10
+ *
11
+ * Must be called from `setup()`.
12
+ */
13
+ export declare function useMediaManager(): {
14
+ MediaDialog: string | import("vue").ConcreteComponent<{}, any, any, import("vue").ComputedOptions, import("vue").MethodOptions, {}, any>;
15
+ available: boolean;
16
+ };
17
+ //# sourceMappingURL=useMediaManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMediaManager.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useMediaManager.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe;;;EAM9B"}
@@ -0,0 +1,6 @@
1
+ import { resolveComponent } from "vue";
2
+ export function useMediaManager() {
3
+ const MediaDialog = resolveComponent("MapoMediaManagerDialog");
4
+ const available = typeof MediaDialog !== "string";
5
+ return { MediaDialog, available };
6
+ }
@@ -0,0 +1,44 @@
1
+ type Matcher<T = unknown> = (val: T) => boolean;
2
+ type Condition<TModel extends object> = (ctx: {
3
+ model: TModel;
4
+ }) => boolean;
5
+ /**
6
+ * Combines conditions with AND: all conditions must match.
7
+ *
8
+ * @example
9
+ * visible: when(matchesField('seo_enabled', true), matchesField('status', isOneOf(['published'])))
10
+ */
11
+ export declare function when<TModel extends object>(...conditions: Condition<TModel>[]): Condition<TModel>;
12
+ /**
13
+ * Combines conditions with OR: at least one condition must match.
14
+ */
15
+ export declare function whenAny<TModel extends object>(...conditions: Condition<TModel>[]): Condition<TModel>;
16
+ /**
17
+ * Negates a condition.
18
+ */
19
+ export declare function whenNot<TModel extends object>(condition: Condition<TModel>): Condition<TModel>;
20
+ /**
21
+ * Checks that a field value satisfies a matcher, or equals a literal value.
22
+ * Supports dotted paths such as `meta.status`.
23
+ *
24
+ * @example
25
+ * matchesField('type', 'article')
26
+ * matchesField('count', greaterThan(0))
27
+ */
28
+ export declare function matchesField<TModel extends object>(key: string, matcher: unknown | Matcher): Condition<TModel>;
29
+ /** The value is included in the options array. */
30
+ export declare const isOneOf: <T>(options: T[]) => (val: unknown) => boolean;
31
+ /** The value is NOT included in the options array. */
32
+ export declare const isNoneOf: <T>(options: T[]) => (val: unknown) => boolean;
33
+ /** The value is non-empty (not null, not undefined, not '', not false, array with at least one item). */
34
+ export declare const isNotEmpty: () => (val: unknown) => boolean;
35
+ /** The value is empty. */
36
+ export declare const isEmpty: () => (val: unknown) => boolean;
37
+ /** Returns true when the numeric value is greater than `n`. */
38
+ export declare const greaterThan: (n: number) => (val: unknown) => boolean;
39
+ /** Returns true when the numeric value is less than `n`. */
40
+ export declare const lessThan: (n: number) => (val: unknown) => boolean;
41
+ /** Returns true when the stringified value matches the given regular expression. */
42
+ export declare const matches: (pattern: RegExp) => (val: unknown) => boolean;
43
+ export {};
44
+ //# sourceMappingURL=useProgressiveDisclosure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useProgressiveDisclosure.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useProgressiveDisclosure.ts"],"names":[],"mappings":"AAEA,KAAK,OAAO,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,CAAC;AAChD,KAAK,SAAS,CAAC,MAAM,SAAS,MAAM,IAAI,CAAC,GAAG,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,KAAK,OAAO,CAAC;AAE5E;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,MAAM,SAAS,MAAM,EACxC,GAAG,UAAU,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE,GACjC,SAAS,CAAC,MAAM,CAAC,CAEnB;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,MAAM,SAAS,MAAM,EAC3C,GAAG,UAAU,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE,GACjC,SAAS,CAAC,MAAM,CAAC,CAEnB;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,MAAM,SAAS,MAAM,EAC3C,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC,GAC3B,SAAS,CAAC,MAAM,CAAC,CAEnB;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,MAAM,SAAS,MAAM,EAChD,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,OAAO,GAAG,OAAO,GACzB,SAAS,CAAC,MAAM,CAAC,CAOnB;AAID,kDAAkD;AAClD,eAAO,MAAM,OAAO,GACjB,CAAC,EAAE,SAAS,CAAC,EAAE,MACf,KAAK,OAAO,KAAG,OACY,CAAC;AAE/B,sDAAsD;AACtD,eAAO,MAAM,QAAQ,GAClB,CAAC,EAAE,SAAS,CAAC,EAAE,MACf,KAAK,OAAO,KAAG,OACa,CAAC;AAEhC,yGAAyG;AACzG,eAAO,MAAM,UAAU,SAEpB,KAAK,OAAO,KAAG,OAIyB,CAAC;AAE5C,0BAA0B;AAC1B,eAAO,MAAM,OAAO,SAEjB,KAAK,OAAO,KAAG,OACI,CAAC;AAEvB,+DAA+D;AAC/D,eAAO,MAAM,WAAW,GACrB,GAAG,MAAM,MACT,KAAK,OAAO,KAAG,OACC,CAAC;AAEpB,4DAA4D;AAC5D,eAAO,MAAM,QAAQ,GAClB,GAAG,MAAM,MACT,KAAK,OAAO,KAAG,OACC,CAAC;AAEpB,oFAAoF;AACpF,eAAO,MAAM,OAAO,GACjB,SAAS,MAAM,MACf,KAAK,OAAO,KAAG,OACiB,CAAC"}
@@ -0,0 +1,23 @@
1
+ import { getNestedValue } from "@mapomodule/utils";
2
+ export function when(...conditions) {
3
+ return (ctx) => conditions.every((c) => c(ctx));
4
+ }
5
+ export function whenAny(...conditions) {
6
+ return (ctx) => conditions.some((c) => c(ctx));
7
+ }
8
+ export function whenNot(condition) {
9
+ return (ctx) => !condition(ctx);
10
+ }
11
+ export function matchesField(key, matcher) {
12
+ return (ctx) => {
13
+ const val = getNestedValue(ctx.model, key);
14
+ return typeof matcher === "function" ? matcher(val) : val === matcher;
15
+ };
16
+ }
17
+ export const isOneOf = (options) => (val) => options.includes(val);
18
+ export const isNoneOf = (options) => (val) => !options.includes(val);
19
+ export const isNotEmpty = () => (val) => val != null && val !== "" && val !== false && (!Array.isArray(val) || val.length > 0);
20
+ export const isEmpty = () => (val) => !isNotEmpty()(val);
21
+ export const greaterThan = (n) => (val) => Number(val) > n;
22
+ export const lessThan = (n) => (val) => Number(val) < n;
23
+ export const matches = (pattern) => (val) => pattern.test(String(val ?? ""));
@@ -0,0 +1,24 @@
1
+ import { type Ref } from "vue";
2
+ /** Optional configuration for the unsaved-changes navigation guard. */
3
+ export interface UnsavedChangesGuardOptions {
4
+ message?: string;
5
+ enabled?: Ref<boolean> | (() => boolean);
6
+ }
7
+ /**
8
+ * Registers two guards that warn the user before abandoning a dirty form:
9
+ * - A Vue Router `onBeforeRouteLeave` guard (in-app navigation).
10
+ * - A native `beforeunload` event listener (tab close / hard refresh).
11
+ *
12
+ * Both guards are no-ops when `isDirty` is `false` or when `options.enabled`
13
+ * returns `false`. The browser guard is skipped in SSR environments.
14
+ *
15
+ * @param isDirty - Reactive flag indicating whether the form has unsaved changes.
16
+ * @param options - Optional message override and an `enabled` predicate.
17
+ *
18
+ * @example
19
+ * useUnsavedChangesGuard(form.isDirty, {
20
+ * message: 'Leave without saving your article?',
21
+ * })
22
+ */
23
+ export declare function useUnsavedChangesGuard(isDirty: Ref<boolean>, options?: UnsavedChangesGuardOptions): void;
24
+ //# sourceMappingURL=useUnsavedChangesGuard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useUnsavedChangesGuard.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useUnsavedChangesGuard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,GAAG,EAAE,MAAM,KAAK,CAAC;AAIhD,uEAAuE;AACvE,MAAM,WAAW,0BAA0B;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,OAAO,CAAC,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,EACrB,OAAO,GAAE,0BAA+B,QA6BzC"}
@@ -0,0 +1,28 @@
1
+ import { onBeforeUnmount } from "vue";
2
+ import { onBeforeRouteLeave } from "#app";
3
+ import { useMapoT } from "@mapomodule/i18n/runtime/composables/useMapoT";
4
+ export function useUnsavedChangesGuard(isDirty, options = {}) {
5
+ const message = options.message ?? useMapoT()("mapo.confirmLeaveChanges");
6
+ const canUseWindow = typeof window !== "undefined";
7
+ const isEnabled = () => {
8
+ if (typeof options.enabled === "function") return options.enabled();
9
+ if (options.enabled) return options.enabled.value;
10
+ return true;
11
+ };
12
+ onBeforeRouteLeave(() => {
13
+ if (!isEnabled() || !isDirty.value) return true;
14
+ if (!canUseWindow) return true;
15
+ return window.confirm(message);
16
+ });
17
+ const beforeUnload = (e) => {
18
+ if (!isEnabled() || !isDirty.value) return;
19
+ e.preventDefault();
20
+ e.returnValue = "";
21
+ };
22
+ if (canUseWindow) {
23
+ window.addEventListener("beforeunload", beforeUnload);
24
+ onBeforeUnmount(
25
+ () => window.removeEventListener("beforeunload", beforeUnload)
26
+ );
27
+ }
28
+ }