@mapomodule/form 2.0.0-beta.1 → 2.0.0-beta.11
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 +37 -118
- package/dist/__tests__/__mocks__/nuxt-imports.d.ts +4 -0
- package/dist/__tests__/__mocks__/nuxt-imports.d.ts.map +1 -0
- package/dist/__tests__/camomillaRoundTrip.test.d.ts +2 -0
- package/dist/__tests__/camomillaRoundTrip.test.d.ts.map +1 -0
- package/dist/__tests__/registry.test.d.ts +2 -0
- package/dist/__tests__/registry.test.d.ts.map +1 -0
- package/dist/__tests__/tabVisibility.test.d.ts +2 -0
- package/dist/__tests__/tabVisibility.test.d.ts.map +1 -0
- package/dist/__tests__/useFormFromSchema.test.d.ts +2 -0
- package/dist/__tests__/useFormFromSchema.test.d.ts.map +1 -0
- package/dist/__tests__/useMapoForm.test.d.ts +2 -0
- package/dist/__tests__/useMapoForm.test.d.ts.map +1 -0
- package/dist/index.d.mts +9 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +7 -34
- package/dist/module.d.mts +20 -2
- package/dist/module.d.ts +12 -2
- package/dist/module.d.ts.map +1 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +187 -13
- package/dist/runtime/components/MapoFocusPortal.vue +115 -0
- package/dist/runtime/components/MapoFocusPortal.vue.d.ts +4 -0
- package/dist/runtime/components/MapoFocusPortal.vue.d.ts.map +1 -0
- package/dist/runtime/components/MapoForm.vue +177 -0
- package/dist/runtime/components/MapoForm.vue.d.ts +47 -0
- package/dist/runtime/components/MapoForm.vue.d.ts.map +1 -0
- package/dist/runtime/components/MapoFormField.vue +269 -0
- package/dist/runtime/components/MapoFormField.vue.d.ts +46 -0
- package/dist/runtime/components/MapoFormField.vue.d.ts.map +1 -0
- package/dist/runtime/components/MapoFormFlatSection.vue +149 -0
- package/dist/runtime/components/MapoFormFlatSection.vue.d.ts +15 -0
- package/dist/runtime/components/MapoFormFlatSection.vue.d.ts.map +1 -0
- package/dist/runtime/components/MapoFormGroup.vue +247 -0
- package/dist/runtime/components/MapoFormGroup.vue.d.ts +44 -0
- package/dist/runtime/components/MapoFormGroup.vue.d.ts.map +1 -0
- package/dist/runtime/components/MapoFormTabs.vue +122 -0
- package/dist/runtime/components/MapoFormTabs.vue.d.ts +15 -0
- package/dist/runtime/components/MapoFormTabs.vue.d.ts.map +1 -0
- package/dist/runtime/components/MapoUnknownField.vue +62 -0
- package/dist/runtime/components/MapoUnknownField.vue.d.ts +16 -0
- package/dist/runtime/components/MapoUnknownField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoDateField.vue +88 -0
- package/dist/runtime/components/fields/MapoDateField.vue.d.ts +17 -0
- package/dist/runtime/components/fields/MapoDateField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoDateTimeField.vue +159 -0
- package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts +17 -0
- package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts.map +1 -0
- 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 +99 -0
- package/dist/runtime/components/fields/MapoFileField.vue.d.ts +16 -0
- package/dist/runtime/components/fields/MapoFileField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoFksField.vue +122 -0
- package/dist/runtime/components/fields/MapoFksField.vue.d.ts +16 -0
- package/dist/runtime/components/fields/MapoFksField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoMapField.vue +99 -0
- package/dist/runtime/components/fields/MapoMapField.vue.d.ts +22 -0
- package/dist/runtime/components/fields/MapoMapField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoMapFieldClient.vue +67 -0
- package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts +20 -0
- package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoMediaField.vue +93 -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 +111 -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 +459 -0
- package/dist/runtime/components/fields/MapoRepeater.vue.d.ts +16 -0
- package/dist/runtime/components/fields/MapoRepeater.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoRepeaterItem.vue +346 -0
- package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts +55 -0
- package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoSeoPreview.vue +122 -0
- package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts +22 -0
- package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoTimeField.vue +44 -0
- package/dist/runtime/components/fields/MapoTimeField.vue.d.ts +17 -0
- package/dist/runtime/components/fields/MapoTimeField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoWygEditor.vue +258 -0
- package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts +16 -0
- package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/NuiCheckbox.vue +9 -0
- package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts +4 -0
- package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/NuiInput.vue +9 -0
- package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts +4 -0
- package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/NuiSelectMenu.vue +18 -0
- package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts +4 -0
- package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/NuiSlider.vue +9 -0
- package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts +4 -0
- package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/NuiSwitch.vue +9 -0
- package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts +4 -0
- package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/NuiTextarea.vue +9 -0
- package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts +4 -0
- package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts +13 -0
- package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/useNuiFieldAttrs.js +19 -0
- package/dist/runtime/composables/defineFormField.d.ts +25 -0
- package/dist/runtime/composables/defineFormField.d.ts.map +1 -0
- package/dist/runtime/composables/defineFormField.js +27 -0
- package/dist/runtime/composables/useCurrentLang.d.ts +6 -0
- package/dist/runtime/composables/useCurrentLang.d.ts.map +1 -0
- package/dist/runtime/composables/useCurrentLang.js +9 -0
- package/dist/runtime/composables/useFocusMode.d.ts +40 -0
- package/dist/runtime/composables/useFocusMode.d.ts.map +1 -0
- package/dist/runtime/composables/useFocusMode.js +31 -0
- package/dist/runtime/composables/useFormDraft.d.ts +60 -0
- package/dist/runtime/composables/useFormDraft.d.ts.map +1 -0
- package/dist/runtime/composables/useFormDraft.js +52 -0
- package/dist/runtime/composables/useFormFromSchema.d.ts +21 -0
- package/dist/runtime/composables/useFormFromSchema.d.ts.map +1 -0
- package/dist/runtime/composables/useFormFromSchema.js +145 -0
- package/dist/runtime/composables/useMapoForm.d.ts +96 -0
- package/dist/runtime/composables/useMapoForm.d.ts.map +1 -0
- package/dist/runtime/composables/useMapoForm.js +325 -0
- 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/useProgressiveDisclosure.d.ts +44 -0
- package/dist/runtime/composables/useProgressiveDisclosure.d.ts.map +1 -0
- package/dist/runtime/composables/useProgressiveDisclosure.js +23 -0
- package/dist/runtime/composables/useUnsavedChangesGuard.d.ts +24 -0
- package/dist/runtime/composables/useUnsavedChangesGuard.d.ts.map +1 -0
- package/dist/runtime/composables/useUnsavedChangesGuard.js +28 -0
- package/dist/runtime/devtools/DevtoolsFormsPage.vue +235 -0
- package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts +4 -0
- package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts.map +1 -0
- package/dist/runtime/plugins/form-registry.d.ts +18 -0
- package/dist/runtime/plugins/form-registry.d.ts.map +1 -0
- package/dist/runtime/plugins/form-registry.js +19 -0
- package/dist/runtime/registry/defaults.d.ts +3 -0
- package/dist/runtime/registry/defaults.d.ts.map +1 -0
- package/dist/runtime/registry/defaults.js +93 -0
- package/dist/runtime/types/descriptor.d.ts +373 -0
- package/dist/runtime/types/descriptor.d.ts.map +1 -0
- package/dist/runtime/types/descriptor.js +26 -0
- package/dist/runtime/types/index.d.ts +12 -0
- package/dist/runtime/types/index.d.ts.map +1 -0
- package/dist/runtime/types/index.js +6 -0
- package/dist/runtime/types/layout.d.ts +21 -0
- package/dist/runtime/types/layout.d.ts.map +1 -0
- package/dist/runtime/types/layout.js +0 -0
- 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/dist/runtime/types/registry.d.ts +62 -0
- package/dist/runtime/types/registry.d.ts.map +1 -0
- package/dist/runtime/types/registry.js +19 -0
- package/dist/runtime/utils/flattenFieldGroups.d.ts +50 -0
- package/dist/runtime/utils/flattenFieldGroups.d.ts.map +1 -0
- package/dist/runtime/utils/flattenFieldGroups.js +24 -0
- package/dist/runtime/utils/tabVisibility.d.ts +18 -0
- package/dist/runtime/utils/tabVisibility.d.ts.map +1 -0
- package/dist/runtime/utils/tabVisibility.js +18 -0
- package/dist/types.d.mts +6 -10
- package/package.json +10 -8
- package/src/__tests__/article.schema.json +193 -0
- package/src/__tests__/camomillaRoundTrip.test.ts +73 -0
- package/src/__tests__/tabVisibility.test.ts +102 -0
- package/src/__tests__/useFormFromSchema.test.ts +24 -0
- package/src/__tests__/useMapoForm.test.ts +5 -0
- package/src/index.ts +5 -0
- package/src/module.ts +34 -0
- package/src/runtime/components/MapoFocusPortal.vue +3 -1
- package/src/runtime/components/MapoForm.vue +17 -4
- package/src/runtime/components/MapoFormTabs.vue +29 -6
- 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 +106 -0
- package/src/runtime/components/fields/MapoMediaM2mField.vue +131 -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 +55 -7
- package/src/runtime/composables/useFormFromSchema.ts +4 -1
- 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
- package/src/runtime/utils/tabVisibility.ts +44 -0
- package/dist/index.d.cts +0 -1
- package/dist/module.d.cts +0 -2
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { computed, useSlots, watchEffect, inject, onUnmounted } from "vue";
|
|
3
3
|
import type { Ref } from "vue";
|
|
4
4
|
import { useNuxtApp, useRuntimeConfig } from "#app";
|
|
5
|
+
import { useI18n } from "vue-i18n";
|
|
5
6
|
import type {
|
|
6
7
|
AnyFieldDescriptor,
|
|
7
8
|
FieldRegistry,
|
|
@@ -135,6 +136,12 @@ provideCurrentLang(currentLangRef);
|
|
|
135
136
|
|
|
136
137
|
// ─── Group fields by tab/group ───────────────────────────────────────────────
|
|
137
138
|
|
|
139
|
+
const { t } = useI18n();
|
|
140
|
+
|
|
141
|
+
// Fields without a `tab` go here. It becomes a tab of its own only when other
|
|
142
|
+
// fields have one, and then it shows as "General" (`mapo.form.defaultTab`).
|
|
143
|
+
const DEFAULT_TAB = "__default__";
|
|
144
|
+
|
|
138
145
|
function getOrCreateTab(map: Map<string, TabEntry>, name: string): TabEntry {
|
|
139
146
|
if (!map.has(name)) {
|
|
140
147
|
map.set(name, { name, groups: new Map(), children: new Map() });
|
|
@@ -151,7 +158,7 @@ const grouped = computed<Map<string, TabEntry>>(() => {
|
|
|
151
158
|
// Normalise: 'a/b', ['a','b'], or 'a' all become a string[]
|
|
152
159
|
const rawTab = field.tab;
|
|
153
160
|
const tabPath: string[] = !rawTab
|
|
154
|
-
? [
|
|
161
|
+
? [DEFAULT_TAB]
|
|
155
162
|
: Array.isArray(rawTab)
|
|
156
163
|
? rawTab
|
|
157
164
|
: rawTab.split("/");
|
|
@@ -179,17 +186,23 @@ const grouped = computed<Map<string, TabEntry>>(() => {
|
|
|
179
186
|
}
|
|
180
187
|
}
|
|
181
188
|
|
|
189
|
+
const defaultTab = tabs.get(DEFAULT_TAB);
|
|
190
|
+
if (defaultTab) defaultTab.label = t("mapo.form.defaultTab");
|
|
191
|
+
|
|
182
192
|
return tabs;
|
|
183
193
|
});
|
|
184
194
|
|
|
185
195
|
const hasTabs = computed(() => {
|
|
186
196
|
const keys = [...grouped.value.keys()];
|
|
187
|
-
|
|
197
|
+
const hasFields = keys.length > 0;
|
|
198
|
+
if (!hasFields) return false;
|
|
199
|
+
const hasSeveralTabs = keys.length > 1;
|
|
200
|
+
const hasNamedTab = keys[0] !== DEFAULT_TAB;
|
|
201
|
+
return hasSeveralTabs || hasNamedTab;
|
|
188
202
|
});
|
|
189
203
|
|
|
190
204
|
const defaultGroups = computed(
|
|
191
|
-
() =>
|
|
192
|
-
grouped.value.get("__default__")?.groups ?? new Map<string, GroupEntry>(),
|
|
205
|
+
() => grouped.value.get(DEFAULT_TAB)?.groups ?? new Map<string, GroupEntry>(),
|
|
193
206
|
);
|
|
194
207
|
|
|
195
208
|
const tabList = computed<TabEntry[]>(() => [...grouped.value.values()]);
|
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
3
4
|
import type { TabEntry } from "../types/layout.js";
|
|
4
5
|
import { injectMapoForm } from "../composables/useMapoForm.js";
|
|
6
|
+
import { pickActiveTab, tabHasShownFields } from "../utils/tabVisibility.js";
|
|
5
7
|
import MapoFormGroup from "./MapoFormGroup.vue";
|
|
6
8
|
import MapoFormFlatSection from "./MapoFormFlatSection.vue";
|
|
7
9
|
|
|
10
|
+
const { t } = useI18n();
|
|
11
|
+
|
|
8
12
|
const props = defineProps<{ tabs: TabEntry[] }>();
|
|
9
13
|
|
|
10
|
-
defineSlots<Record<string, unknown>>();
|
|
14
|
+
const slots = defineSlots<Record<string, unknown>>();
|
|
15
|
+
|
|
16
|
+
const form = injectMapoForm<Record<string, unknown>>()!;
|
|
17
|
+
|
|
18
|
+
// A tab whose fields are all hidden by `visible` or `show` would open on an
|
|
19
|
+
// empty panel, so it leaves the tab bar until one of them shows. Content passed
|
|
20
|
+
// through the tab's own slots counts as something to show.
|
|
21
|
+
function tabHasContent(tab: TabEntry): boolean {
|
|
22
|
+
const hasPanelSlot = Boolean(slots[`tab.${tab.name}`]);
|
|
23
|
+
const hasBeforeSlot = Boolean(slots[`tab.${tab.name}.before`]);
|
|
24
|
+
const hasAfterSlot = Boolean(slots[`tab.${tab.name}.after`]);
|
|
25
|
+
if (hasPanelSlot || hasBeforeSlot || hasAfterSlot) return true;
|
|
26
|
+
return tabHasShownFields(tab, form.model.value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const shownTabs = computed(() => props.tabs.filter(tabHasContent));
|
|
11
30
|
|
|
12
|
-
|
|
31
|
+
// The tab the user clicked; until then, or while it is hidden, the first shown
|
|
32
|
+
// tab is open.
|
|
33
|
+
const pickedTab = ref<string | null>(null);
|
|
34
|
+
const activeTab = computed(() =>
|
|
35
|
+
pickActiveTab(shownTabs.value, pickedTab.value),
|
|
36
|
+
);
|
|
13
37
|
|
|
14
38
|
// Recursively check if a tab (or any of its descendants) contains errors.
|
|
15
39
|
function tabHasErrors(tab: TabEntry, errorKeys: Set<string>): boolean {
|
|
@@ -23,7 +47,6 @@ function tabHasErrors(tab: TabEntry, errorKeys: Set<string>): boolean {
|
|
|
23
47
|
}
|
|
24
48
|
|
|
25
49
|
// Highlight tabs containing errors.
|
|
26
|
-
const form = injectMapoForm()!;
|
|
27
50
|
const tabsWithErrors = computed(() => {
|
|
28
51
|
const errorKeys = new Set(Object.keys(form.errors.value));
|
|
29
52
|
return new Set(
|
|
@@ -37,7 +60,7 @@ const tabsWithErrors = computed(() => {
|
|
|
37
60
|
<!-- Tab bar -->
|
|
38
61
|
<div class="flex gap-1 border-b border-gray-200 pb-px">
|
|
39
62
|
<button
|
|
40
|
-
v-for="tab in
|
|
63
|
+
v-for="tab in shownTabs"
|
|
41
64
|
:key="tab.name"
|
|
42
65
|
type="button"
|
|
43
66
|
class="relative flex items-center gap-1.5 rounded-t px-4 py-2 text-sm font-medium transition-colors"
|
|
@@ -46,7 +69,7 @@ const tabsWithErrors = computed(() => {
|
|
|
46
69
|
? 'border-b-2 border-primary-500 text-primary-600'
|
|
47
70
|
: 'text-gray-500 hover:bg-gray-50 hover:text-gray-700',
|
|
48
71
|
]"
|
|
49
|
-
@click="
|
|
72
|
+
@click="pickedTab = tab.name"
|
|
50
73
|
>
|
|
51
74
|
<slot :name="`tab.${tab.name}.label`" :tab="tab">
|
|
52
75
|
{{ tab.label ?? tab.name }}
|
|
@@ -54,7 +77,7 @@ const tabsWithErrors = computed(() => {
|
|
|
54
77
|
<span
|
|
55
78
|
v-if="tabsWithErrors.has(tab.name)"
|
|
56
79
|
class="inline-block h-1.5 w-1.5 rounded-full bg-red-500"
|
|
57
|
-
aria-label="
|
|
80
|
+
:aria-label="t('mapo.form.tabErrors')"
|
|
58
81
|
/>
|
|
59
82
|
</button>
|
|
60
83
|
<!-- Extra content appended to the tab bar (actions, badges, …). -->
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, ref } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
3
4
|
import { parseDate, CalendarDate } from "@internationalized/date";
|
|
4
5
|
import type { DateDescriptor } from "../../types/index.js";
|
|
5
6
|
|
|
@@ -14,6 +15,7 @@ const props = defineProps<{
|
|
|
14
15
|
|
|
15
16
|
const emit = defineEmits<{ "update:modelValue": [value: string | null] }>();
|
|
16
17
|
|
|
18
|
+
const { t } = useI18n();
|
|
17
19
|
const open = ref(false);
|
|
18
20
|
|
|
19
21
|
/** Parsed calendar value derived from the current ISO-like model string. */
|
|
@@ -30,7 +32,7 @@ const calendarValue = computed<CalendarDate | undefined>(() => {
|
|
|
30
32
|
|
|
31
33
|
const displayLabel = computed(() => {
|
|
32
34
|
const v = calendarValue.value;
|
|
33
|
-
if (!v) return "
|
|
35
|
+
if (!v) return t("mapo.dateField.selectDate");
|
|
34
36
|
return `${String(v.day).padStart(2, "0")}/${String(v.month).padStart(2, "0")}/${v.year}`;
|
|
35
37
|
});
|
|
36
38
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
today,
|
|
7
7
|
getLocalTimeZone,
|
|
8
8
|
} from "@internationalized/date";
|
|
9
|
+
import { useI18n } from "vue-i18n";
|
|
9
10
|
import type { DateDescriptor } from "../../types/index.js";
|
|
10
11
|
|
|
11
12
|
/** Datetime field with configurable naive/UTC serialisation semantics. */
|
|
@@ -19,6 +20,7 @@ const props = defineProps<{
|
|
|
19
20
|
|
|
20
21
|
const emit = defineEmits<{ "update:modelValue": [value: string | null] }>();
|
|
21
22
|
|
|
23
|
+
const { t } = useI18n();
|
|
22
24
|
const open = ref(false);
|
|
23
25
|
|
|
24
26
|
// Timezone strategy configurable through `descriptor.attrs.tz`:
|
|
@@ -72,7 +74,7 @@ function emitFromCalendar(v: CalendarDateTime) {
|
|
|
72
74
|
// Displayed label in the trigger button
|
|
73
75
|
const displayLabel = computed(() => {
|
|
74
76
|
const v = calendarValue.value;
|
|
75
|
-
if (!v) return "
|
|
77
|
+
if (!v) return t("mapo.dateField.selectDateTime");
|
|
76
78
|
const date = `${String(v.day).padStart(2, "0")}/${String(v.month).padStart(2, "0")}/${v.year}`;
|
|
77
79
|
const time = `${String(v.hour).padStart(2, "0")}:${String(v.minute).padStart(2, "0")}`;
|
|
78
80
|
return `${date} ${time}`;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
4
|
+
import type {
|
|
5
|
+
EnhancedMediaDescriptor,
|
|
6
|
+
EnhancedMediaValue,
|
|
7
|
+
MediaItem,
|
|
8
|
+
} from "../../types/index.js";
|
|
9
|
+
import { useMediaManager } from "../../composables/useMediaManager.js";
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
modelValue: EnhancedMediaValue | null | undefined;
|
|
13
|
+
descriptor: EnhancedMediaDescriptor;
|
|
14
|
+
errors?: string[];
|
|
15
|
+
readonly?: boolean;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{
|
|
20
|
+
"update:modelValue": [value: EnhancedMediaValue | null];
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
const { t } = useI18n();
|
|
24
|
+
|
|
25
|
+
const dialogOpen = ref(false);
|
|
26
|
+
|
|
27
|
+
// Picker dialog comes from @mapomodule/uikit, resolved at runtime.
|
|
28
|
+
const { MediaDialog, available } = useMediaManager();
|
|
29
|
+
|
|
30
|
+
const current = computed<MediaItem | null>(
|
|
31
|
+
() => props.modelValue?.media ?? null,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const altValue = computed({
|
|
35
|
+
get: () => props.modelValue?.alt ?? "",
|
|
36
|
+
set: (v: string) =>
|
|
37
|
+
emit("update:modelValue", {
|
|
38
|
+
...(props.modelValue ?? { media: null }),
|
|
39
|
+
alt: v,
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const captionValue = computed({
|
|
44
|
+
get: () => props.modelValue?.caption ?? "",
|
|
45
|
+
set: (v: string) =>
|
|
46
|
+
emit("update:modelValue", {
|
|
47
|
+
...(props.modelValue ?? { media: null }),
|
|
48
|
+
caption: v,
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const mime = computed(() => props.descriptor.attrs?.mime as string | undefined);
|
|
53
|
+
|
|
54
|
+
function onConfirm(selection: MediaItem | MediaItem[]) {
|
|
55
|
+
const media = Array.isArray(selection) ? (selection[0] ?? null) : selection;
|
|
56
|
+
emit("update:modelValue", {
|
|
57
|
+
...(props.modelValue ?? {}),
|
|
58
|
+
media,
|
|
59
|
+
alt: props.modelValue?.alt ?? media?.alt_text ?? "",
|
|
60
|
+
caption: props.modelValue?.caption ?? "",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function clear() {
|
|
65
|
+
emit("update:modelValue", null);
|
|
66
|
+
}
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<template>
|
|
70
|
+
<div class="mapo-enhanced-media-field space-y-3">
|
|
71
|
+
<MapoMediaUnavailable v-if="!available" />
|
|
72
|
+
|
|
73
|
+
<template v-else>
|
|
74
|
+
<div class="flex gap-3">
|
|
75
|
+
<!-- Preview -->
|
|
76
|
+
<div class="shrink-0">
|
|
77
|
+
<div
|
|
78
|
+
class="flex size-24 cursor-pointer items-center justify-center overflow-hidden rounded-lg border-2 border-dashed transition-colors"
|
|
79
|
+
:class="
|
|
80
|
+
current
|
|
81
|
+
? 'border-default'
|
|
82
|
+
: 'border-accented hover:border-primary'
|
|
83
|
+
"
|
|
84
|
+
@click="!readonly && !disabled && (dialogOpen = true)"
|
|
85
|
+
>
|
|
86
|
+
<MapoMediaPreview v-if="current" :media="current" size="md" />
|
|
87
|
+
<div v-else class="flex flex-col items-center gap-1 text-dimmed">
|
|
88
|
+
<UIcon name="i-lucide-image-plus" class="size-7" />
|
|
89
|
+
<span class="text-[10px]">{{ t("mapo.select") }}</span>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<!-- Metadata fields -->
|
|
95
|
+
<div class="flex-1 space-y-2">
|
|
96
|
+
<div v-if="current" class="flex items-start justify-between gap-2">
|
|
97
|
+
<p class="truncate text-xs text-muted">
|
|
98
|
+
{{ current.title || current.file?.split("/").pop() }}
|
|
99
|
+
</p>
|
|
100
|
+
<div class="flex gap-1">
|
|
101
|
+
<UButton
|
|
102
|
+
v-if="!readonly && !disabled"
|
|
103
|
+
icon="i-lucide-refresh-cw"
|
|
104
|
+
size="xs"
|
|
105
|
+
variant="ghost"
|
|
106
|
+
color="neutral"
|
|
107
|
+
:title="t('mapo.change')"
|
|
108
|
+
@click="dialogOpen = true"
|
|
109
|
+
/>
|
|
110
|
+
<UButton
|
|
111
|
+
v-if="!readonly && !disabled"
|
|
112
|
+
icon="i-lucide-x"
|
|
113
|
+
size="xs"
|
|
114
|
+
variant="ghost"
|
|
115
|
+
color="neutral"
|
|
116
|
+
:title="t('mapo.remove')"
|
|
117
|
+
@click="clear"
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<UInput
|
|
123
|
+
v-model="altValue"
|
|
124
|
+
size="sm"
|
|
125
|
+
:placeholder="t('mapo.enhancedMediaField.altText')"
|
|
126
|
+
:disabled="readonly || disabled || !current"
|
|
127
|
+
/>
|
|
128
|
+
<UInput
|
|
129
|
+
v-model="captionValue"
|
|
130
|
+
size="sm"
|
|
131
|
+
:placeholder="t('mapo.enhancedMediaField.caption')"
|
|
132
|
+
:disabled="readonly || disabled || !current"
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<!-- Dialog -->
|
|
138
|
+
<component
|
|
139
|
+
:is="MediaDialog"
|
|
140
|
+
v-model="dialogOpen"
|
|
141
|
+
selection-mode="single"
|
|
142
|
+
:mime="mime"
|
|
143
|
+
:selected="current"
|
|
144
|
+
@confirm="onConfirm"
|
|
145
|
+
/>
|
|
146
|
+
</template>
|
|
147
|
+
|
|
148
|
+
<!-- Errors -->
|
|
149
|
+
<p v-for="err in errors" :key="err" class="text-xs text-error" role="alert">
|
|
150
|
+
{{ err }}
|
|
151
|
+
</p>
|
|
152
|
+
</div>
|
|
153
|
+
</template>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
3
4
|
import type { FileDescriptor } from "../../types/index.js";
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
@@ -12,6 +13,7 @@ const props = defineProps<{
|
|
|
12
13
|
|
|
13
14
|
const emit = defineEmits<{ "update:modelValue": [value: File | null] }>();
|
|
14
15
|
|
|
16
|
+
const { t } = useI18n();
|
|
15
17
|
const inputRef = ref<HTMLInputElement | null>(null);
|
|
16
18
|
|
|
17
19
|
const currentFileName = computed<string | null>(() => {
|
|
@@ -78,7 +80,7 @@ const accept = computed(
|
|
|
78
80
|
variant="ghost"
|
|
79
81
|
color="neutral"
|
|
80
82
|
icon="i-lucide-x"
|
|
81
|
-
aria-label="
|
|
83
|
+
:aria-label="t('mapo.fileField.removeFile')"
|
|
82
84
|
@click="clear"
|
|
83
85
|
/>
|
|
84
86
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed, watch, onMounted } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
3
4
|
import { useNuxtApp } from "#app";
|
|
4
5
|
import { debounce } from "@mapomodule/utils";
|
|
5
6
|
import type { FksDescriptor } from "../../types/index.js";
|
|
@@ -14,6 +15,8 @@ const props = defineProps<{
|
|
|
14
15
|
|
|
15
16
|
const emit = defineEmits<{ "update:modelValue": [value: unknown] }>();
|
|
16
17
|
|
|
18
|
+
const { t } = useI18n();
|
|
19
|
+
|
|
17
20
|
const attrs = computed(() => props.descriptor.attrs);
|
|
18
21
|
const isMultiple = computed(
|
|
19
22
|
() => props.descriptor.type === "m2m" || !!attrs.value.multiple,
|
|
@@ -70,7 +73,7 @@ const internalValue = computed({
|
|
|
70
73
|
|
|
71
74
|
// Placeholder
|
|
72
75
|
const placeholder = computed(() =>
|
|
73
|
-
isMultiple.value ? "
|
|
76
|
+
isMultiple.value ? t("mapo.fksField.searchSelect") : t("mapo.search"),
|
|
74
77
|
);
|
|
75
78
|
|
|
76
79
|
// Chip text shown in the trigger.
|
|
@@ -144,7 +147,9 @@ function getItemKey(item: unknown): unknown {
|
|
|
144
147
|
<!-- Empty state -->
|
|
145
148
|
<template #empty>
|
|
146
149
|
<span class="text-sm text-gray-500">
|
|
147
|
-
{{
|
|
150
|
+
{{
|
|
151
|
+
searchTerm ? t("mapo.fksField.noResults") : t("mapo.fksField.noItems")
|
|
152
|
+
}}
|
|
148
153
|
</span>
|
|
149
154
|
</template>
|
|
150
155
|
</USelectMenu>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed, defineAsyncComponent } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
3
4
|
import type { MapDescriptor } from "../../types/index.js";
|
|
4
5
|
|
|
5
6
|
// Leaflet is loaded only on the client because it is not SSR-safe.
|
|
@@ -7,6 +8,8 @@ const LeafletMap = defineAsyncComponent(
|
|
|
7
8
|
() => import("./MapoMapFieldClient.vue"),
|
|
8
9
|
);
|
|
9
10
|
|
|
11
|
+
const { t } = useI18n();
|
|
12
|
+
|
|
10
13
|
/** Geographic coordinates stored by the map field. */
|
|
11
14
|
interface LatLng {
|
|
12
15
|
lat: number;
|
|
@@ -63,7 +66,7 @@ function clear() {
|
|
|
63
66
|
<div class="flex gap-2">
|
|
64
67
|
<UInput
|
|
65
68
|
v-model="latInput"
|
|
66
|
-
placeholder="
|
|
69
|
+
:placeholder="t('mapo.latitude')"
|
|
67
70
|
class="flex-1"
|
|
68
71
|
:readonly="readonly"
|
|
69
72
|
:disabled="disabled"
|
|
@@ -71,7 +74,7 @@ function clear() {
|
|
|
71
74
|
/>
|
|
72
75
|
<UInput
|
|
73
76
|
v-model="lngInput"
|
|
74
|
-
placeholder="
|
|
77
|
+
:placeholder="t('mapo.longitude')"
|
|
75
78
|
class="flex-1"
|
|
76
79
|
:readonly="readonly"
|
|
77
80
|
:disabled="disabled"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
4
|
+
import type { MediaDescriptor, MediaItem } from "../../types/index.js";
|
|
5
|
+
import { useMediaManager } from "../../composables/useMediaManager.js";
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
modelValue: MediaItem | number | null | undefined;
|
|
9
|
+
descriptor: MediaDescriptor;
|
|
10
|
+
errors?: string[];
|
|
11
|
+
readonly?: boolean;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}>();
|
|
14
|
+
|
|
15
|
+
const emit = defineEmits<{
|
|
16
|
+
"update:modelValue": [value: MediaItem | null];
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
const { t } = useI18n();
|
|
20
|
+
|
|
21
|
+
const dialogOpen = ref(false);
|
|
22
|
+
|
|
23
|
+
// Picker dialog comes from @mapomodule/uikit, resolved at runtime.
|
|
24
|
+
const { MediaDialog, available } = useMediaManager();
|
|
25
|
+
|
|
26
|
+
const current = computed<MediaItem | null>(() => {
|
|
27
|
+
if (!props.modelValue) return null;
|
|
28
|
+
if (typeof props.modelValue === "object") return props.modelValue;
|
|
29
|
+
return null;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const mime = computed(() => props.descriptor.attrs?.mime as string | undefined);
|
|
33
|
+
|
|
34
|
+
function onConfirm(selection: MediaItem | MediaItem[]) {
|
|
35
|
+
emit(
|
|
36
|
+
"update:modelValue",
|
|
37
|
+
Array.isArray(selection) ? (selection[0] ?? null) : selection,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function clear() {
|
|
42
|
+
emit("update:modelValue", null);
|
|
43
|
+
}
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<div class="mapo-media-field space-y-2">
|
|
48
|
+
<MapoMediaUnavailable v-if="!available" />
|
|
49
|
+
|
|
50
|
+
<template v-else>
|
|
51
|
+
<!-- Selected media preview. It and the trigger below paint their own
|
|
52
|
+
surface, like an input: `soft` buttons and transparent cards take
|
|
53
|
+
the page background and vanish on a tinted page. -->
|
|
54
|
+
<div
|
|
55
|
+
v-if="current"
|
|
56
|
+
class="flex items-center gap-3 rounded-lg border border-default bg-default p-2"
|
|
57
|
+
>
|
|
58
|
+
<MapoMediaPreview :media="current" size="sm" />
|
|
59
|
+
<div class="flex-1 min-w-0">
|
|
60
|
+
<p class="truncate text-xs font-medium">
|
|
61
|
+
{{ current.title || current.file?.split("/").pop() }}
|
|
62
|
+
</p>
|
|
63
|
+
<p class="text-xs text-dimmed">
|
|
64
|
+
{{ current.mime_type }}
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
<UButton
|
|
68
|
+
v-if="!readonly && !disabled"
|
|
69
|
+
icon="i-lucide-x"
|
|
70
|
+
size="xs"
|
|
71
|
+
variant="ghost"
|
|
72
|
+
color="neutral"
|
|
73
|
+
@click="clear"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<!-- Trigger button -->
|
|
78
|
+
<UButton
|
|
79
|
+
v-if="!readonly && !disabled"
|
|
80
|
+
:icon="current ? 'i-lucide-refresh-cw' : 'i-lucide-image-plus'"
|
|
81
|
+
size="sm"
|
|
82
|
+
variant="outline"
|
|
83
|
+
color="neutral"
|
|
84
|
+
:block="!current"
|
|
85
|
+
@click="dialogOpen = true"
|
|
86
|
+
>
|
|
87
|
+
{{ current ? t("mapo.change") : t("mapo.mediaField.selectMedia") }}
|
|
88
|
+
</UButton>
|
|
89
|
+
|
|
90
|
+
<!-- Dialog -->
|
|
91
|
+
<component
|
|
92
|
+
:is="MediaDialog"
|
|
93
|
+
v-model="dialogOpen"
|
|
94
|
+
selection-mode="single"
|
|
95
|
+
:mime="mime"
|
|
96
|
+
:selected="current"
|
|
97
|
+
@confirm="onConfirm"
|
|
98
|
+
/>
|
|
99
|
+
</template>
|
|
100
|
+
|
|
101
|
+
<!-- Errors -->
|
|
102
|
+
<p v-for="err in errors" :key="err" class="text-xs text-error" role="alert">
|
|
103
|
+
{{ err }}
|
|
104
|
+
</p>
|
|
105
|
+
</div>
|
|
106
|
+
</template>
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
4
|
+
import type { MediaM2mDescriptor, MediaItem } from "../../types/index.js";
|
|
5
|
+
import { useMediaManager } from "../../composables/useMediaManager.js";
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
modelValue: MediaItem[] | number[] | null | undefined;
|
|
9
|
+
descriptor: MediaM2mDescriptor;
|
|
10
|
+
errors?: string[];
|
|
11
|
+
readonly?: boolean;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}>();
|
|
14
|
+
|
|
15
|
+
const emit = defineEmits<{
|
|
16
|
+
"update:modelValue": [value: MediaItem[]];
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
const { t } = useI18n();
|
|
20
|
+
|
|
21
|
+
const dialogOpen = ref(false);
|
|
22
|
+
|
|
23
|
+
// Picker dialog comes from @mapomodule/uikit, resolved at runtime.
|
|
24
|
+
const { MediaDialog, available } = useMediaManager();
|
|
25
|
+
|
|
26
|
+
const current = computed<MediaItem[]>(() => {
|
|
27
|
+
if (!props.modelValue) return [];
|
|
28
|
+
return props.modelValue.filter((v): v is MediaItem => typeof v === "object");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const mime = computed(() => props.descriptor.attrs?.mime as string | undefined);
|
|
32
|
+
|
|
33
|
+
function onConfirm(selection: MediaItem | MediaItem[]) {
|
|
34
|
+
emit("update:modelValue", Array.isArray(selection) ? selection : [selection]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function removeItem(id: number) {
|
|
38
|
+
emit(
|
|
39
|
+
"update:modelValue",
|
|
40
|
+
current.value.filter((m) => m.id !== id),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// HTML5 drag reorder — order matters for m2m relations (legacy parity).
|
|
45
|
+
const dragIndex = ref<number | null>(null);
|
|
46
|
+
|
|
47
|
+
function onDragStart(index: number) {
|
|
48
|
+
dragIndex.value = index;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function onDragOverItem(index: number) {
|
|
52
|
+
if (dragIndex.value === null || dragIndex.value === index) return;
|
|
53
|
+
const items = [...current.value];
|
|
54
|
+
const [moved] = items.splice(dragIndex.value, 1);
|
|
55
|
+
if (!moved) return;
|
|
56
|
+
items.splice(index, 0, moved);
|
|
57
|
+
dragIndex.value = index;
|
|
58
|
+
emit("update:modelValue", items);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function onDragEnd() {
|
|
62
|
+
dragIndex.value = null;
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<template>
|
|
67
|
+
<div class="mapo-media-m2m-field space-y-2">
|
|
68
|
+
<MapoMediaUnavailable v-if="!available" />
|
|
69
|
+
|
|
70
|
+
<template v-else>
|
|
71
|
+
<!-- Selected items grid -->
|
|
72
|
+
<div v-if="current.length > 0" class="flex flex-wrap gap-2">
|
|
73
|
+
<div
|
|
74
|
+
v-for="(media, i) in current"
|
|
75
|
+
:key="media.id"
|
|
76
|
+
class="group relative"
|
|
77
|
+
:class="{
|
|
78
|
+
'opacity-50': dragIndex === i,
|
|
79
|
+
'cursor-grab': !readonly && !disabled,
|
|
80
|
+
}"
|
|
81
|
+
:draggable="!readonly && !disabled"
|
|
82
|
+
@dragstart="onDragStart(i)"
|
|
83
|
+
@dragover.prevent="onDragOverItem(i)"
|
|
84
|
+
@dragend="onDragEnd"
|
|
85
|
+
>
|
|
86
|
+
<MapoMediaPreview :media="media" size="sm" />
|
|
87
|
+
<button
|
|
88
|
+
v-if="!readonly && !disabled"
|
|
89
|
+
class="absolute -right-1.5 -top-1.5 hidden size-4 items-center justify-center rounded-full bg-error text-inverted group-hover:flex"
|
|
90
|
+
type="button"
|
|
91
|
+
@click="removeItem(media.id)"
|
|
92
|
+
>
|
|
93
|
+
<UIcon name="i-lucide-x" class="size-2.5" />
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<!-- Add button: outlined like MapoMediaField's trigger, so it keeps its
|
|
99
|
+
own surface on a tinted page. -->
|
|
100
|
+
<UButton
|
|
101
|
+
v-if="!readonly && !disabled"
|
|
102
|
+
icon="i-lucide-plus"
|
|
103
|
+
size="sm"
|
|
104
|
+
variant="outline"
|
|
105
|
+
color="neutral"
|
|
106
|
+
@click="dialogOpen = true"
|
|
107
|
+
>
|
|
108
|
+
{{
|
|
109
|
+
current.length > 0
|
|
110
|
+
? t("mapo.mediaM2MField.addMedia")
|
|
111
|
+
: t("mapo.mediaField.selectMedia")
|
|
112
|
+
}}
|
|
113
|
+
</UButton>
|
|
114
|
+
|
|
115
|
+
<!-- Dialog -->
|
|
116
|
+
<component
|
|
117
|
+
:is="MediaDialog"
|
|
118
|
+
v-model="dialogOpen"
|
|
119
|
+
selection-mode="multi"
|
|
120
|
+
:mime="mime"
|
|
121
|
+
:selected="current"
|
|
122
|
+
@confirm="onConfirm"
|
|
123
|
+
/>
|
|
124
|
+
</template>
|
|
125
|
+
|
|
126
|
+
<!-- Errors -->
|
|
127
|
+
<p v-for="err in errors" :key="err" class="text-xs text-error" role="alert">
|
|
128
|
+
{{ err }}
|
|
129
|
+
</p>
|
|
130
|
+
</div>
|
|
131
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Placeholder rendered by the media fields when the Media Manager is not
|
|
4
|
+
* registered — i.e. `@mapomodule/form` is installed without `@mapomodule/uikit`.
|
|
5
|
+
* Better than a silently broken picker: it names the missing package.
|
|
6
|
+
*/
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<div
|
|
11
|
+
class="flex items-center gap-2 rounded-lg border border-dashed border-default px-3 py-2 text-xs text-muted"
|
|
12
|
+
>
|
|
13
|
+
<UIcon
|
|
14
|
+
name="i-lucide-triangle-alert"
|
|
15
|
+
class="size-4 shrink-0 text-warning"
|
|
16
|
+
/>
|
|
17
|
+
<span>Media fields require <code>@mapomodule/uikit</code>.</span>
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|