@mapomodule/form 2.0.0-beta.4 → 2.0.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/module.d.ts.map +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +16 -1
- package/dist/runtime/components/MapoFocusPortal.vue +3 -1
- package/dist/runtime/components/MapoFocusPortal.vue.d.ts.map +1 -1
- package/dist/runtime/components/MapoFormTabs.vue +4 -2
- package/dist/runtime/components/MapoFormTabs.vue.d.ts.map +1 -1
- package/dist/runtime/components/fields/MapoDateField.vue +3 -1
- package/dist/runtime/components/fields/MapoDateField.vue.d.ts.map +1 -1
- package/dist/runtime/components/fields/MapoDateTimeField.vue +5 -3
- package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts.map +1 -1
- package/dist/runtime/components/fields/MapoEnhancedMediaField.vue +130 -0
- package/dist/runtime/components/fields/MapoEnhancedMediaField.vue.d.ts +16 -0
- package/dist/runtime/components/fields/MapoEnhancedMediaField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoFileField.vue +3 -1
- package/dist/runtime/components/fields/MapoFileField.vue.d.ts.map +1 -1
- package/dist/runtime/components/fields/MapoFksField.vue +6 -2
- package/dist/runtime/components/fields/MapoFksField.vue.d.ts.map +1 -1
- package/dist/runtime/components/fields/MapoMapField.vue +4 -2
- package/dist/runtime/components/fields/MapoMapField.vue.d.ts.map +1 -1
- package/dist/runtime/components/fields/MapoMediaField.vue +91 -0
- package/dist/runtime/components/fields/MapoMediaField.vue.d.ts +16 -0
- package/dist/runtime/components/fields/MapoMediaField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoMediaM2mField.vue +110 -0
- package/dist/runtime/components/fields/MapoMediaM2mField.vue.d.ts +16 -0
- package/dist/runtime/components/fields/MapoMediaM2mField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoMediaUnavailable.vue +15 -0
- package/dist/runtime/components/fields/MapoMediaUnavailable.vue.d.ts +4 -0
- package/dist/runtime/components/fields/MapoMediaUnavailable.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoRepeater.vue +32 -22
- package/dist/runtime/components/fields/MapoRepeater.vue.d.ts.map +1 -1
- package/dist/runtime/components/fields/MapoRepeaterItem.vue +13 -8
- package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts.map +1 -1
- package/dist/runtime/components/fields/MapoSeoPreview.vue +16 -12
- package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts.map +1 -1
- package/dist/runtime/components/fields/MapoWygEditor.vue +30 -5
- package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts.map +1 -1
- package/dist/runtime/composables/useMapoForm.d.ts.map +1 -1
- package/dist/runtime/composables/useMapoForm.js +4 -2
- package/dist/runtime/composables/useMediaManager.d.ts +17 -0
- package/dist/runtime/composables/useMediaManager.d.ts.map +1 -0
- package/dist/runtime/composables/useMediaManager.js +6 -0
- package/dist/runtime/composables/useUnsavedChangesGuard.d.ts.map +1 -1
- package/dist/runtime/composables/useUnsavedChangesGuard.js +3 -2
- package/dist/runtime/registry/defaults.d.ts.map +1 -1
- package/dist/runtime/registry/defaults.js +8 -2
- package/dist/runtime/types/descriptor.d.ts +18 -4
- package/dist/runtime/types/descriptor.d.ts.map +1 -1
- package/dist/runtime/types/index.d.ts +2 -1
- package/dist/runtime/types/index.d.ts.map +1 -1
- package/dist/runtime/types/media.d.ts +43 -0
- package/dist/runtime/types/media.d.ts.map +1 -0
- package/dist/runtime/types/media.js +0 -0
- package/package.json +9 -6
- package/src/__tests__/useMapoForm.test.ts +5 -0
- package/src/index.ts +5 -0
- package/src/module.ts +24 -0
- package/src/runtime/components/MapoFocusPortal.vue +3 -1
- package/src/runtime/components/MapoFormTabs.vue +4 -1
- package/src/runtime/components/fields/MapoDateField.vue +3 -1
- package/src/runtime/components/fields/MapoDateTimeField.vue +3 -1
- package/src/runtime/components/fields/MapoEnhancedMediaField.vue +153 -0
- package/src/runtime/components/fields/MapoFileField.vue +3 -1
- package/src/runtime/components/fields/MapoFksField.vue +7 -2
- package/src/runtime/components/fields/MapoMapField.vue +5 -2
- package/src/runtime/components/fields/MapoMediaField.vue +104 -0
- package/src/runtime/components/fields/MapoMediaM2mField.vue +130 -0
- package/src/runtime/components/fields/MapoMediaUnavailable.vue +19 -0
- package/src/runtime/components/fields/MapoRepeater.vue +33 -26
- package/src/runtime/components/fields/MapoRepeaterItem.vue +14 -8
- package/src/runtime/components/fields/MapoSeoPreview.vue +17 -13
- package/src/runtime/components/fields/MapoWygEditor.vue +47 -5
- package/src/runtime/composables/useMapoForm.ts +4 -2
- package/src/runtime/composables/useMediaManager.ts +21 -0
- package/src/runtime/composables/useUnsavedChangesGuard.ts +3 -3
- package/src/runtime/registry/defaults.ts +9 -1
- package/src/runtime/types/descriptor.ts +22 -7
- package/src/runtime/types/index.ts +4 -0
- package/src/runtime/types/media.ts +44 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { watch, onBeforeUnmount } from "vue";
|
|
2
|
+
import { ref, watch, onBeforeUnmount, resolveComponent } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
3
4
|
import { useEditor, EditorContent } from "@tiptap/vue-3";
|
|
4
5
|
import type { AnyExtension } from "@tiptap/vue-3";
|
|
5
6
|
import StarterKit from "@tiptap/starter-kit";
|
|
6
7
|
import Link from "@tiptap/extension-link";
|
|
7
8
|
import Underline from "@tiptap/extension-underline";
|
|
9
|
+
import Image from "@tiptap/extension-image";
|
|
8
10
|
import type { EditorDescriptor } from "../../types/index.js";
|
|
9
11
|
|
|
10
12
|
const props = defineProps<{
|
|
@@ -17,6 +19,8 @@ const props = defineProps<{
|
|
|
17
19
|
|
|
18
20
|
const emit = defineEmits<{ "update:modelValue": [value: string] }>();
|
|
19
21
|
|
|
22
|
+
const { t } = useI18n();
|
|
23
|
+
|
|
20
24
|
const extraExtensions =
|
|
21
25
|
(props.descriptor.attrs?.extensions as AnyExtension[] | undefined) ?? [];
|
|
22
26
|
|
|
@@ -85,6 +89,7 @@ const editor = useEditor({
|
|
|
85
89
|
validate: (href: string) => isSafeUrl(href),
|
|
86
90
|
}),
|
|
87
91
|
Underline,
|
|
92
|
+
Image.configure({ inline: false, allowBase64: false }),
|
|
88
93
|
...extraExtensions,
|
|
89
94
|
] as AnyExtension[],
|
|
90
95
|
onUpdate: ({ editor: e }) => {
|
|
@@ -112,6 +117,28 @@ watch(
|
|
|
112
117
|
);
|
|
113
118
|
|
|
114
119
|
onBeforeUnmount(() => editor.value?.destroy());
|
|
120
|
+
|
|
121
|
+
// ─── Image insertion ──────────────────────────────────────────────────────
|
|
122
|
+
// MapoMediaManagerDialog is globally registered by @mapomodule/uikit.
|
|
123
|
+
// resolveComponent() works at runtime because it is registered before this
|
|
124
|
+
// component renders. We avoid a direct import to break the form → uikit cycle.
|
|
125
|
+
const imagePickerOpen = ref(false);
|
|
126
|
+
const MediaManagerDialog = resolveComponent("MapoMediaManagerDialog");
|
|
127
|
+
const hasMediaManager = typeof MediaManagerDialog !== "string";
|
|
128
|
+
|
|
129
|
+
function onImageConfirm(
|
|
130
|
+
selection:
|
|
131
|
+
| { file: string; alt_text?: string }
|
|
132
|
+
| Array<{ file: string; alt_text?: string }>,
|
|
133
|
+
) {
|
|
134
|
+
const media = Array.isArray(selection) ? selection[0] : selection;
|
|
135
|
+
if (!media || !editor.value) return;
|
|
136
|
+
editor.value
|
|
137
|
+
.chain()
|
|
138
|
+
.focus()
|
|
139
|
+
.setImage({ src: media.file, alt: media.alt_text ?? "" })
|
|
140
|
+
.run();
|
|
141
|
+
}
|
|
115
142
|
</script>
|
|
116
143
|
|
|
117
144
|
<template>
|
|
@@ -223,9 +250,13 @@ onBeforeUnmount(() => editor.value?.destroy());
|
|
|
223
250
|
|
|
224
251
|
<USeparator orientation="vertical" class="mx-1 h-5" />
|
|
225
252
|
|
|
226
|
-
<!-- Insert Image
|
|
253
|
+
<!-- Insert Image via Media Manager (available when @mapomodule/uikit is installed) -->
|
|
227
254
|
<UTooltip
|
|
228
|
-
text="
|
|
255
|
+
:text="
|
|
256
|
+
hasMediaManager
|
|
257
|
+
? t('mapo.wygEditor.insertImage')
|
|
258
|
+
: t('mapo.wygEditor.requiresUikit')
|
|
259
|
+
"
|
|
229
260
|
:delay-open="300"
|
|
230
261
|
>
|
|
231
262
|
<UButton
|
|
@@ -233,12 +264,13 @@ onBeforeUnmount(() => editor.value?.destroy());
|
|
|
233
264
|
variant="ghost"
|
|
234
265
|
color="neutral"
|
|
235
266
|
icon="i-lucide-image"
|
|
236
|
-
disabled
|
|
267
|
+
:disabled="readonly || disabled || !hasMediaManager"
|
|
268
|
+
@click="imagePickerOpen = true"
|
|
237
269
|
/>
|
|
238
270
|
</UTooltip>
|
|
239
271
|
</div>
|
|
240
272
|
|
|
241
|
-
<!--
|
|
273
|
+
<!-- Editor content -->
|
|
242
274
|
<EditorContent
|
|
243
275
|
:editor="editor"
|
|
244
276
|
class="prose prose-sm max-w-none px-3 py-2 focus-within:outline-none"
|
|
@@ -253,4 +285,14 @@ onBeforeUnmount(() => editor.value?.destroy());
|
|
|
253
285
|
{{ err }}
|
|
254
286
|
</p>
|
|
255
287
|
</div>
|
|
288
|
+
|
|
289
|
+
<!-- Image picker dialog — only rendered when MapoMediaManagerDialog is globally available -->
|
|
290
|
+
<component
|
|
291
|
+
:is="MediaManagerDialog"
|
|
292
|
+
v-if="hasMediaManager"
|
|
293
|
+
v-model="imagePickerOpen"
|
|
294
|
+
selection-mode="single"
|
|
295
|
+
mime="image/*"
|
|
296
|
+
@confirm="onImageConfirm"
|
|
297
|
+
/>
|
|
256
298
|
</template>
|
|
@@ -19,6 +19,7 @@ import type { AnyFieldDescriptor, FieldRegistry } from "../types/index.js";
|
|
|
19
19
|
import { resolveFieldAccessor } from "../types/index.js";
|
|
20
20
|
import { useCurrentLang } from "./useCurrentLang.js";
|
|
21
21
|
import { useSnackStore } from "@mapomodule/store/runtime/stores/snack";
|
|
22
|
+
import { useMapoT } from "@mapomodule/i18n/runtime/composables/useMapoT";
|
|
22
23
|
import { useAuthStore } from "@mapomodule/store/runtime/stores/auth";
|
|
23
24
|
|
|
24
25
|
/** Options accepted by `useMapoForm()`. */
|
|
@@ -121,6 +122,7 @@ export function useMapoForm<T extends object>(options: UseMapoFormOptions<T>) {
|
|
|
121
122
|
|
|
122
123
|
const currentLang = useCurrentLang(currentLangProp);
|
|
123
124
|
const snack = useSnackStore();
|
|
125
|
+
const t = useMapoT();
|
|
124
126
|
const authStore = useAuthStore();
|
|
125
127
|
const backup = ref(safeClone(model.value)) as Ref<T>;
|
|
126
128
|
const clientErrors = ref<Record<string, string>>({});
|
|
@@ -318,7 +320,7 @@ export function useMapoForm<T extends object>(options: UseMapoFormOptions<T>) {
|
|
|
318
320
|
): string | null {
|
|
319
321
|
if (!descriptor.required) return null;
|
|
320
322
|
if (val === null || val === undefined || val === "")
|
|
321
|
-
return "
|
|
323
|
+
return t("mapo.form.required");
|
|
322
324
|
return null;
|
|
323
325
|
}
|
|
324
326
|
|
|
@@ -379,7 +381,7 @@ export function useMapoForm<T extends object>(options: UseMapoFormOptions<T>) {
|
|
|
379
381
|
submitted.value = true;
|
|
380
382
|
const { valid } = validateClient();
|
|
381
383
|
if (!valid) {
|
|
382
|
-
snack.show("
|
|
384
|
+
snack.show(t("mapo.fixErrors"), "error");
|
|
383
385
|
return;
|
|
384
386
|
}
|
|
385
387
|
isLoading.value = true;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { resolveComponent } from "vue";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the Media Manager picker dialog at runtime.
|
|
5
|
+
*
|
|
6
|
+
* The media field types ship with `@mapomodule/form`, but the Media Manager
|
|
7
|
+
* itself lives in `@mapomodule/uikit`. Resolving the dialog by name instead of
|
|
8
|
+
* importing it keeps `form` free of a static dependency on `uikit` (which
|
|
9
|
+
* depends on `form`, so a static import would be a cycle) and lets `form` stay
|
|
10
|
+
* usable standalone — the fields then render an explicit notice instead of a
|
|
11
|
+
* broken picker.
|
|
12
|
+
*
|
|
13
|
+
* Must be called from `setup()`.
|
|
14
|
+
*/
|
|
15
|
+
export function useMediaManager() {
|
|
16
|
+
const MediaDialog = resolveComponent("MapoMediaManagerDialog");
|
|
17
|
+
// resolveComponent() returns the name unchanged when nothing is registered.
|
|
18
|
+
const available = typeof MediaDialog !== "string";
|
|
19
|
+
|
|
20
|
+
return { MediaDialog, available };
|
|
21
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { onBeforeUnmount, type Ref } from "vue";
|
|
2
|
-
import { onBeforeRouteLeave } from "
|
|
2
|
+
import { onBeforeRouteLeave } from "#app";
|
|
3
|
+
import { useMapoT } from "@mapomodule/i18n/runtime/composables/useMapoT";
|
|
3
4
|
|
|
4
5
|
/** Optional configuration for the unsaved-changes navigation guard. */
|
|
5
6
|
export interface UnsavedChangesGuardOptions {
|
|
@@ -27,8 +28,7 @@ export function useUnsavedChangesGuard(
|
|
|
27
28
|
isDirty: Ref<boolean>,
|
|
28
29
|
options: UnsavedChangesGuardOptions = {},
|
|
29
30
|
) {
|
|
30
|
-
const message =
|
|
31
|
-
options.message ?? "You have unsaved changes. Do you really want to leave?";
|
|
31
|
+
const message = options.message ?? useMapoT()("mapo.confirmLeaveChanges");
|
|
32
32
|
const canUseWindow = typeof window !== "undefined";
|
|
33
33
|
|
|
34
34
|
const isEnabled = (): boolean => {
|
|
@@ -31,7 +31,15 @@ export const defaultRegistry: FieldRegistry = {
|
|
|
31
31
|
editor: () => import("../components/fields/MapoWygEditor.vue"),
|
|
32
32
|
map: () => import("../components/fields/MapoMapField.vue"),
|
|
33
33
|
repeater: () => import("../components/fields/MapoRepeater.vue"),
|
|
34
|
-
|
|
34
|
+
|
|
35
|
+
// ─── Media pickers ────────────────────────────────────────────────────────
|
|
36
|
+
// The fields live here with every other field type; the Media Manager they
|
|
37
|
+
// drive ships with @mapomodule/uikit and is resolved at runtime
|
|
38
|
+
// (see useMediaManager). Without uikit they render an explicit notice.
|
|
39
|
+
media: () => import("../components/fields/MapoMediaField.vue"),
|
|
40
|
+
"media-m2m": () => import("../components/fields/MapoMediaM2mField.vue"),
|
|
41
|
+
"enhanced-media": () =>
|
|
42
|
+
import("../components/fields/MapoEnhancedMediaField.vue"),
|
|
35
43
|
},
|
|
36
44
|
|
|
37
45
|
attrs: {
|
|
@@ -344,15 +344,28 @@ export interface RepeaterDescriptor<
|
|
|
344
344
|
};
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
/** Descriptor for media
|
|
347
|
+
/** Descriptor for a single-media picker field (`type: 'media'`). */
|
|
348
348
|
export interface MediaDescriptor<
|
|
349
349
|
T = Record<string, unknown>,
|
|
350
350
|
> extends FieldBase<T> {
|
|
351
|
-
type:
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
351
|
+
type: `${KnownFieldType.Media}`;
|
|
352
|
+
attrs?: { mime?: string };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Descriptor for a multi-media picker field (`type: 'media-m2m'`). */
|
|
356
|
+
export interface MediaM2mDescriptor<
|
|
357
|
+
T = Record<string, unknown>,
|
|
358
|
+
> extends FieldBase<T> {
|
|
359
|
+
type: `${KnownFieldType.MediaM2m}`;
|
|
360
|
+
attrs?: { mime?: string; maxItems?: number };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Descriptor for a media picker with inline alt/caption metadata (`type: 'enhanced-media'`). */
|
|
364
|
+
export interface EnhancedMediaDescriptor<
|
|
365
|
+
T = Record<string, unknown>,
|
|
366
|
+
> extends FieldBase<T> {
|
|
367
|
+
type: `${KnownFieldType.EnhancedMedia}`;
|
|
368
|
+
attrs?: { mime?: string };
|
|
356
369
|
}
|
|
357
370
|
|
|
358
371
|
/**
|
|
@@ -395,7 +408,9 @@ export type FieldDescriptor<T = Record<string, unknown>> =
|
|
|
395
408
|
| SeoDescriptor<T>
|
|
396
409
|
| MapDescriptor<T>
|
|
397
410
|
| RepeaterDescriptor<T>
|
|
398
|
-
| MediaDescriptor<T
|
|
411
|
+
| MediaDescriptor<T>
|
|
412
|
+
| MediaM2mDescriptor<T>
|
|
413
|
+
| EnhancedMediaDescriptor<T>;
|
|
399
414
|
|
|
400
415
|
/**
|
|
401
416
|
* {@link FieldDescriptor} plus the {@link CustomDescriptor} escape hatch.
|
|
@@ -22,9 +22,13 @@ export type {
|
|
|
22
22
|
MapDescriptor,
|
|
23
23
|
RepeaterDescriptor,
|
|
24
24
|
MediaDescriptor,
|
|
25
|
+
MediaM2mDescriptor,
|
|
26
|
+
EnhancedMediaDescriptor,
|
|
25
27
|
CustomDescriptor,
|
|
26
28
|
} from "./descriptor.js";
|
|
27
29
|
|
|
30
|
+
export type { MediaLink, MediaItem, EnhancedMediaValue } from "./media.js";
|
|
31
|
+
|
|
28
32
|
export type {
|
|
29
33
|
FieldComponentEntry,
|
|
30
34
|
FieldRegistry,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media **value** types — the shape that lands in the form model for the
|
|
3
|
+
* `media`, `media-m2m`, and `enhanced-media` field types.
|
|
4
|
+
*
|
|
5
|
+
* These live in `@mapomodule/form` (next to their descriptors) because they are
|
|
6
|
+
* a form contract, not a Media Manager implementation detail. `@mapomodule/uikit`
|
|
7
|
+
* re-exports them from `runtime/types/media.ts` alongside the store/adapter types
|
|
8
|
+
* so the Media Manager keeps a single import surface.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** A model instance referencing a media (Camomilla returns these as `links`). */
|
|
12
|
+
export interface MediaLink {
|
|
13
|
+
model: string;
|
|
14
|
+
id: number;
|
|
15
|
+
name: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MediaItem {
|
|
19
|
+
id: number;
|
|
20
|
+
file: string;
|
|
21
|
+
mime_type: string;
|
|
22
|
+
title: string;
|
|
23
|
+
alt_text: string;
|
|
24
|
+
description: string;
|
|
25
|
+
size: number;
|
|
26
|
+
created: string;
|
|
27
|
+
modified: string;
|
|
28
|
+
folder?: number | null;
|
|
29
|
+
name?: string;
|
|
30
|
+
/** Model instances that reference this media — shown in the editor info panel. */
|
|
31
|
+
links?: MediaLink[];
|
|
32
|
+
translations?: Record<
|
|
33
|
+
string,
|
|
34
|
+
{ title?: string; alt_text?: string; description?: string }
|
|
35
|
+
>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Value of an `enhanced-media` field: a media plus inline caption metadata. */
|
|
39
|
+
export interface EnhancedMediaValue {
|
|
40
|
+
media: MediaItem | null;
|
|
41
|
+
alt?: string;
|
|
42
|
+
caption?: string;
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
}
|