@xleddyl/nuxt-cms 0.1.9 → 0.1.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.
- package/dist/module.json +1 -1
- package/dist/module.mjs +5 -1
- package/dist/runtime/app/components/cms/BlocksField.vue +59 -16
- package/dist/runtime/app/components/cms/ConfirmModal.d.vue.ts +20 -0
- package/dist/runtime/app/components/cms/ConfirmModal.vue +43 -0
- package/dist/runtime/app/components/cms/ConfirmModal.vue.d.ts +20 -0
- package/dist/runtime/app/components/cms/MediaGallery.vue +9 -2
- package/dist/runtime/app/components/cms/RelationField.vue +56 -10
- package/dist/runtime/app/components/cms/RichTextField.vue +63 -2
- package/dist/runtime/app/composables/cms-confirm.d.ts +4 -0
- package/dist/runtime/app/composables/cms-confirm.js +7 -0
- package/dist/runtime/app/i18n/en.json +32 -2
- package/dist/runtime/app/i18n/it.json +32 -2
- package/dist/runtime/app/pages/admin-collection.vue +89 -14
- package/dist/runtime/app/pages/admin-entry.vue +12 -5
- package/dist/runtime/server/api/collection.get.d.ts +1 -3
- package/dist/runtime/server/api/collection.get.js +34 -8
- package/dist/runtime/server/api/collection.post.js +11 -7
- package/dist/runtime/server/api/collection.put.js +11 -7
- package/dist/runtime/server/api/item.get.js +3 -2
- package/dist/runtime/server/api/item.put.js +12 -8
- package/dist/runtime/server/api/media-presign.post.js +2 -1
- package/dist/runtime/server/api/media.post.js +2 -1
- package/dist/runtime/server/plugins/migrate-postgres.js +9 -1
- package/dist/runtime/server/plugins/migrate-sqlite.js +9 -1
- package/dist/runtime/server/routes/auth/login.post.js +1 -1
- package/dist/runtime/server/utils/db-postgres.d.ts +4 -0
- package/dist/runtime/server/utils/db-postgres.js +3 -0
- package/dist/runtime/server/utils/db-sqlite.d.ts +2 -0
- package/dist/runtime/server/utils/db-sqlite.js +19 -0
- package/dist/runtime/server/utils/graphql.js +10 -4
- package/dist/runtime/server/utils/media.d.ts +1 -0
- package/dist/runtime/server/utils/media.js +27 -0
- package/dist/runtime/server/utils/relations.d.ts +3 -2
- package/dist/runtime/server/utils/relations.js +2 -4
- package/dist/runtime/server/utils/require-admin.js +15 -1
- package/dist/runtime/shared/index.d.ts +69 -1
- package/package.json +4 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -552,7 +552,11 @@ const module$1 = defineNuxtModule({
|
|
|
552
552
|
]
|
|
553
553
|
});
|
|
554
554
|
});
|
|
555
|
-
const {
|
|
555
|
+
const {
|
|
556
|
+
driver = "sqlite",
|
|
557
|
+
path: dbPath = "data/cms.db",
|
|
558
|
+
url: databaseUrl = ""
|
|
559
|
+
} = options.database;
|
|
556
560
|
nuxt.options.alias["#cms-db"] = resolver.resolve(
|
|
557
561
|
driver === "postgres" ? "./runtime/server/utils/db-postgres" : "./runtime/server/utils/db-sqlite"
|
|
558
562
|
);
|
|
@@ -6,14 +6,26 @@
|
|
|
6
6
|
class="cms-card flex flex-col gap-4 p-4"
|
|
7
7
|
>
|
|
8
8
|
<div class="flex items-center gap-1">
|
|
9
|
-
<
|
|
9
|
+
<button
|
|
10
|
+
type="button"
|
|
11
|
+
class="cms-kicker flex grow items-center gap-1.5 text-left"
|
|
12
|
+
:aria-label="
|
|
13
|
+
isCollapsed(item) ? t('cms.form.expandBlock') : t('cms.form.collapseBlock')
|
|
14
|
+
"
|
|
15
|
+
@click="toggleCollapsed(item)"
|
|
16
|
+
>
|
|
17
|
+
<UIcon
|
|
18
|
+
:name="isCollapsed(item) ? 'i-lucide-chevron-right' : 'i-lucide-chevron-down'"
|
|
19
|
+
class="size-3.5"
|
|
20
|
+
/>
|
|
10
21
|
{{ blocks[item.type]?.label ?? item.type }}
|
|
11
|
-
</
|
|
22
|
+
</button>
|
|
12
23
|
<UButton
|
|
13
24
|
icon="i-lucide-chevron-up"
|
|
14
25
|
size="xs"
|
|
15
26
|
variant="ghost"
|
|
16
27
|
color="neutral"
|
|
28
|
+
:aria-label="t('cms.form.moveUp')"
|
|
17
29
|
:disabled="index === 0"
|
|
18
30
|
@click="move(index, -1)"
|
|
19
31
|
/>
|
|
@@ -22,30 +34,42 @@
|
|
|
22
34
|
size="xs"
|
|
23
35
|
variant="ghost"
|
|
24
36
|
color="neutral"
|
|
37
|
+
:aria-label="t('cms.form.moveDown')"
|
|
25
38
|
:disabled="index === items.length - 1"
|
|
26
39
|
@click="move(index, 1)"
|
|
27
40
|
/>
|
|
41
|
+
<UButton
|
|
42
|
+
icon="i-lucide-copy"
|
|
43
|
+
size="xs"
|
|
44
|
+
variant="ghost"
|
|
45
|
+
color="neutral"
|
|
46
|
+
:aria-label="t('cms.form.duplicateBlock')"
|
|
47
|
+
@click="duplicate(index)"
|
|
48
|
+
/>
|
|
28
49
|
<UButton
|
|
29
50
|
icon="i-lucide-trash-2"
|
|
30
51
|
size="xs"
|
|
31
52
|
variant="ghost"
|
|
32
53
|
color="error"
|
|
54
|
+
:aria-label="t('cms.form.removeBlock')"
|
|
33
55
|
@click="remove(index)"
|
|
34
56
|
/>
|
|
35
57
|
</div>
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
<template v-if="!isCollapsed(item)">
|
|
59
|
+
<UFormField
|
|
60
|
+
v-for="(blockField, blockKey) in blocks[item.type]?.fields"
|
|
61
|
+
:key="blockKey"
|
|
62
|
+
:label="blockField.label"
|
|
63
|
+
:required="blockField.required"
|
|
64
|
+
:ui="CMS_FIELD_UI"
|
|
65
|
+
>
|
|
66
|
+
<CmsFieldInput
|
|
67
|
+
:model-value="item[blockKey]"
|
|
68
|
+
:field="blockField"
|
|
69
|
+
@update:model-value="(value) => updateField(index, blockKey, value)"
|
|
70
|
+
/>
|
|
71
|
+
</UFormField>
|
|
72
|
+
</template>
|
|
49
73
|
</div>
|
|
50
74
|
<UDropdownMenu :items="addItems">
|
|
51
75
|
<UButton
|
|
@@ -59,7 +83,7 @@
|
|
|
59
83
|
</template>
|
|
60
84
|
|
|
61
85
|
<script setup>
|
|
62
|
-
import { computed, useI18n } from "#imports";
|
|
86
|
+
import { computed, ref, useI18n } from "#imports";
|
|
63
87
|
import { CMS_FIELD_UI } from "../../utils/ui";
|
|
64
88
|
const props = defineProps({
|
|
65
89
|
field: { type: Object, required: true }
|
|
@@ -95,6 +119,25 @@ function remove(index) {
|
|
|
95
119
|
const next = items.value.filter((_, i) => i !== index);
|
|
96
120
|
model.value = next.length ? next : null;
|
|
97
121
|
}
|
|
122
|
+
function duplicate(index) {
|
|
123
|
+
const current = items.value[index];
|
|
124
|
+
if (!current) return;
|
|
125
|
+
const copy = JSON.parse(JSON.stringify(current));
|
|
126
|
+
const next = [...items.value];
|
|
127
|
+
next.splice(index + 1, 0, copy);
|
|
128
|
+
model.value = next;
|
|
129
|
+
}
|
|
130
|
+
const collapsedUids = ref(/* @__PURE__ */ new Set());
|
|
131
|
+
function isCollapsed(item) {
|
|
132
|
+
return collapsedUids.value.has(uidFor(item));
|
|
133
|
+
}
|
|
134
|
+
function toggleCollapsed(item) {
|
|
135
|
+
const uid = uidFor(item);
|
|
136
|
+
const next = new Set(collapsedUids.value);
|
|
137
|
+
if (next.has(uid)) next.delete(uid);
|
|
138
|
+
else next.add(uid);
|
|
139
|
+
collapsedUids.value = next;
|
|
140
|
+
}
|
|
98
141
|
function move(index, delta) {
|
|
99
142
|
const next = [...items.value];
|
|
100
143
|
const [item] = next.splice(index, 1);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
message: string;
|
|
3
|
+
title?: string;
|
|
4
|
+
confirmLabel?: string;
|
|
5
|
+
};
|
|
6
|
+
type __VLS_ModelProps = {
|
|
7
|
+
'open'?: boolean;
|
|
8
|
+
};
|
|
9
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
10
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
11
|
+
close: (value: boolean) => any;
|
|
12
|
+
"after:leave": () => any;
|
|
13
|
+
"update:open": (value: boolean) => any;
|
|
14
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
15
|
+
onClose?: ((value: boolean) => any) | undefined;
|
|
16
|
+
"onAfter:leave"?: (() => any) | undefined;
|
|
17
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
18
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const _default: typeof __VLS_export;
|
|
20
|
+
export default _default;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<UModal
|
|
3
|
+
v-model:open="open"
|
|
4
|
+
:title="title ?? t('cms.confirm.title')"
|
|
5
|
+
:ui="ui"
|
|
6
|
+
@after:leave="emit('after:leave')"
|
|
7
|
+
>
|
|
8
|
+
<template #body>
|
|
9
|
+
<div class="flex flex-col gap-5">
|
|
10
|
+
<p class="text-(--ui-text) text-sm">{{ message }}</p>
|
|
11
|
+
<div class="flex justify-end gap-2">
|
|
12
|
+
<UButton
|
|
13
|
+
:label="t('cms.confirm.cancel')"
|
|
14
|
+
variant="subtle"
|
|
15
|
+
color="neutral"
|
|
16
|
+
class="rounded-full px-4"
|
|
17
|
+
@click="emit('close', false)"
|
|
18
|
+
/>
|
|
19
|
+
<UButton
|
|
20
|
+
:label="confirmLabel ?? t('cms.confirm.confirm')"
|
|
21
|
+
color="error"
|
|
22
|
+
class="rounded-full px-4"
|
|
23
|
+
@click="emit('close', true)"
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
28
|
+
</UModal>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<script setup>
|
|
32
|
+
import { useI18n } from "#imports";
|
|
33
|
+
import { CMS_MODAL_UI } from "../../utils/ui";
|
|
34
|
+
defineProps({
|
|
35
|
+
message: { type: String, required: true },
|
|
36
|
+
title: { type: String, required: false },
|
|
37
|
+
confirmLabel: { type: String, required: false }
|
|
38
|
+
});
|
|
39
|
+
const open = defineModel("open", { type: Boolean, ...{ default: true } });
|
|
40
|
+
const emit = defineEmits(["close", "after:leave"]);
|
|
41
|
+
const { t } = useI18n();
|
|
42
|
+
const ui = { ...CMS_MODAL_UI, content: "rounded-2xl shadow-xl sm:max-w-md" };
|
|
43
|
+
</script>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
message: string;
|
|
3
|
+
title?: string;
|
|
4
|
+
confirmLabel?: string;
|
|
5
|
+
};
|
|
6
|
+
type __VLS_ModelProps = {
|
|
7
|
+
'open'?: boolean;
|
|
8
|
+
};
|
|
9
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
10
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
11
|
+
close: (value: boolean) => any;
|
|
12
|
+
"after:leave": () => any;
|
|
13
|
+
"update:open": (value: boolean) => any;
|
|
14
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
15
|
+
onClose?: ((value: boolean) => any) | undefined;
|
|
16
|
+
"onAfter:leave"?: (() => any) | undefined;
|
|
17
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
18
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const _default: typeof __VLS_export;
|
|
20
|
+
export default _default;
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
/>
|
|
87
87
|
<div
|
|
88
88
|
v-if="!selectable"
|
|
89
|
-
class="absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100"
|
|
89
|
+
class="absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100"
|
|
90
90
|
>
|
|
91
91
|
<UButton
|
|
92
92
|
icon="i-lucide-pencil"
|
|
@@ -187,6 +187,7 @@
|
|
|
187
187
|
<script setup>
|
|
188
188
|
import { computed, onMounted, ref, useI18n, useToast } from "#imports";
|
|
189
189
|
import { MEDIA_TYPES, mediaFilename, mediaIconFor } from "#nuxt-cms";
|
|
190
|
+
import { useCmsConfirm } from "../../composables/cms-confirm";
|
|
190
191
|
import { CMS_MODAL_UI, errorMessage } from "../../utils/ui";
|
|
191
192
|
const props = defineProps({
|
|
192
193
|
selectable: { type: Boolean, required: false },
|
|
@@ -293,8 +294,12 @@ async function copy(item) {
|
|
|
293
294
|
toast.add({ title: t("cms.media.copyError"), color: "error" });
|
|
294
295
|
}
|
|
295
296
|
}
|
|
297
|
+
const confirmAction = useCmsConfirm();
|
|
298
|
+
const removing = ref(false);
|
|
296
299
|
async function remove(item) {
|
|
297
|
-
if (
|
|
300
|
+
if (removing.value) return;
|
|
301
|
+
if (!await confirmAction(t("cms.media.deleteConfirm"))) return;
|
|
302
|
+
removing.value = true;
|
|
298
303
|
try {
|
|
299
304
|
await $fetch(endpoint, { method: "DELETE", query: { key: item.key } });
|
|
300
305
|
await reload();
|
|
@@ -304,6 +309,8 @@ async function remove(item) {
|
|
|
304
309
|
description: errorMessage(err),
|
|
305
310
|
color: "error"
|
|
306
311
|
});
|
|
312
|
+
} finally {
|
|
313
|
+
removing.value = false;
|
|
307
314
|
}
|
|
308
315
|
}
|
|
309
316
|
</script>
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
<USelectMenu
|
|
3
3
|
v-if="multiple"
|
|
4
4
|
v-model="many"
|
|
5
|
+
v-model:search-term="searchTerm"
|
|
5
6
|
multiple
|
|
7
|
+
ignore-filter
|
|
6
8
|
:items="options"
|
|
7
9
|
value-key="value"
|
|
8
10
|
:loading="loading"
|
|
@@ -12,6 +14,8 @@
|
|
|
12
14
|
<USelectMenu
|
|
13
15
|
v-else
|
|
14
16
|
v-model="single"
|
|
17
|
+
v-model:search-term="searchTerm"
|
|
18
|
+
ignore-filter
|
|
15
19
|
:items="singleItems"
|
|
16
20
|
value-key="value"
|
|
17
21
|
:loading="loading"
|
|
@@ -22,7 +26,7 @@
|
|
|
22
26
|
|
|
23
27
|
<script setup>
|
|
24
28
|
import { isTranslatableField } from "#nuxt-cms";
|
|
25
|
-
import { computed, onMounted, ref, useI18n, useToast } from "#imports";
|
|
29
|
+
import { computed, onMounted, ref, useI18n, useToast, watch } from "#imports";
|
|
26
30
|
import cmsConfig from "#cms-config";
|
|
27
31
|
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
28
32
|
import { errorMessage } from "../../utils/ui";
|
|
@@ -35,11 +39,19 @@ const model = defineModel({ type: [String, Array, null], ...{ required: true } }
|
|
|
35
39
|
const { t } = useI18n();
|
|
36
40
|
const toast = useToast();
|
|
37
41
|
const target = cmsConfig[props.to];
|
|
38
|
-
const
|
|
42
|
+
const endpoint = `/api/cms/admin/${props.to}`;
|
|
43
|
+
const fetched = ref([]);
|
|
44
|
+
const selectedRows = ref(/* @__PURE__ */ new Map());
|
|
39
45
|
const loading = ref(true);
|
|
40
|
-
|
|
46
|
+
const searchTerm = ref("");
|
|
47
|
+
async function fetchOptions() {
|
|
48
|
+
loading.value = true;
|
|
41
49
|
try {
|
|
42
|
-
|
|
50
|
+
const term = searchTerm.value.trim();
|
|
51
|
+
const page = await $fetch(endpoint, {
|
|
52
|
+
query: { light: "true", limit: 25, ...term ? { search: term } : {} }
|
|
53
|
+
});
|
|
54
|
+
fetched.value = page.items;
|
|
43
55
|
} catch (err) {
|
|
44
56
|
toast.add({
|
|
45
57
|
title: t("cms.toast.loadFailed"),
|
|
@@ -49,6 +61,31 @@ onMounted(async () => {
|
|
|
49
61
|
} finally {
|
|
50
62
|
loading.value = false;
|
|
51
63
|
}
|
|
64
|
+
}
|
|
65
|
+
let searchTimer;
|
|
66
|
+
watch(searchTerm, () => {
|
|
67
|
+
clearTimeout(searchTimer);
|
|
68
|
+
searchTimer = setTimeout(() => void fetchOptions(), 300);
|
|
69
|
+
});
|
|
70
|
+
const selectedIds = computed(() => {
|
|
71
|
+
if (props.multiple) return model.value ?? [];
|
|
72
|
+
return model.value ? [model.value] : [];
|
|
73
|
+
});
|
|
74
|
+
async function resolveSelected() {
|
|
75
|
+
const missing = selectedIds.value.filter((id) => !selectedRows.value.has(id));
|
|
76
|
+
await Promise.all(
|
|
77
|
+
missing.map(async (id) => {
|
|
78
|
+
try {
|
|
79
|
+
const row = await $fetch(`${endpoint}/${id}`);
|
|
80
|
+
selectedRows.value.set(id, row);
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
onMounted(() => {
|
|
87
|
+
void fetchOptions();
|
|
88
|
+
void resolveSelected();
|
|
52
89
|
});
|
|
53
90
|
const { i18n: contentI18n } = useCmsRuntime();
|
|
54
91
|
const titleField = target?.titleField ?? "id";
|
|
@@ -62,12 +99,21 @@ function optionLabel(row) {
|
|
|
62
99
|
}
|
|
63
100
|
return String(value ?? `#${row.id}`);
|
|
64
101
|
}
|
|
65
|
-
const options = computed(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
);
|
|
102
|
+
const options = computed(() => {
|
|
103
|
+
const seen = /* @__PURE__ */ new Set();
|
|
104
|
+
const list = [];
|
|
105
|
+
for (const row of fetched.value) {
|
|
106
|
+
const id = row.id;
|
|
107
|
+
list.push({ label: optionLabel(row), value: id });
|
|
108
|
+
seen.add(id);
|
|
109
|
+
}
|
|
110
|
+
for (const id of selectedIds.value) {
|
|
111
|
+
if (seen.has(id)) continue;
|
|
112
|
+
const row = selectedRows.value.get(id);
|
|
113
|
+
list.push({ label: row ? optionLabel(row) : `#${id}`, value: id });
|
|
114
|
+
}
|
|
115
|
+
return list;
|
|
116
|
+
});
|
|
71
117
|
const singleItems = computed(() => [
|
|
72
118
|
...props.required ? [] : [{ label: "\u2014", value: null }],
|
|
73
119
|
...options.value
|
|
@@ -8,14 +8,48 @@
|
|
|
8
8
|
size="xs"
|
|
9
9
|
:variant="editor?.isActive(action.mark, action.attrs) ? 'soft' : 'ghost'"
|
|
10
10
|
color="neutral"
|
|
11
|
+
:aria-label="t(action.label)"
|
|
12
|
+
:title="t(action.label)"
|
|
11
13
|
@click="action.run"
|
|
12
14
|
/>
|
|
15
|
+
<UPopover v-model:open="linkOpen">
|
|
16
|
+
<UButton
|
|
17
|
+
icon="i-lucide-link"
|
|
18
|
+
size="xs"
|
|
19
|
+
:variant="editor?.isActive('link') ? 'soft' : 'ghost'"
|
|
20
|
+
color="neutral"
|
|
21
|
+
:aria-label="t('cms.richtext.link')"
|
|
22
|
+
:title="t('cms.richtext.link')"
|
|
23
|
+
/>
|
|
24
|
+
<template #content>
|
|
25
|
+
<div class="flex items-center gap-2 p-2">
|
|
26
|
+
<UInput
|
|
27
|
+
v-model="linkUrl"
|
|
28
|
+
size="sm"
|
|
29
|
+
placeholder="https://"
|
|
30
|
+
class="w-64"
|
|
31
|
+
@keydown.enter.prevent="applyLink"
|
|
32
|
+
/>
|
|
33
|
+
<UButton size="xs" :label="t('cms.richtext.setLink')" @click="applyLink" />
|
|
34
|
+
<UButton
|
|
35
|
+
v-if="editor?.isActive('link')"
|
|
36
|
+
size="xs"
|
|
37
|
+
variant="subtle"
|
|
38
|
+
color="error"
|
|
39
|
+
:label="t('cms.richtext.unsetLink')"
|
|
40
|
+
@click="removeLink"
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
</UPopover>
|
|
13
45
|
<span class="grow" />
|
|
14
46
|
<UButton
|
|
15
47
|
icon="i-lucide-undo-2"
|
|
16
48
|
size="xs"
|
|
17
49
|
variant="ghost"
|
|
18
50
|
color="neutral"
|
|
51
|
+
:aria-label="t('cms.richtext.undo')"
|
|
52
|
+
:title="t('cms.richtext.undo')"
|
|
19
53
|
:disabled="!editor?.can().undo()"
|
|
20
54
|
@click="undo"
|
|
21
55
|
/>
|
|
@@ -24,6 +58,8 @@
|
|
|
24
58
|
size="xs"
|
|
25
59
|
variant="ghost"
|
|
26
60
|
color="neutral"
|
|
61
|
+
:aria-label="t('cms.richtext.redo')"
|
|
62
|
+
:title="t('cms.richtext.redo')"
|
|
27
63
|
:disabled="!editor?.can().redo()"
|
|
28
64
|
@click="redo"
|
|
29
65
|
/>
|
|
@@ -35,11 +71,12 @@
|
|
|
35
71
|
<script setup>
|
|
36
72
|
import StarterKit from "@tiptap/starter-kit";
|
|
37
73
|
import { EditorContent, useEditor } from "@tiptap/vue-3";
|
|
38
|
-
import { watch } from "#imports";
|
|
74
|
+
import { ref, useI18n, watch } from "#imports";
|
|
39
75
|
const model = defineModel({ type: [String, null], ...{ required: true } });
|
|
76
|
+
const { t } = useI18n();
|
|
40
77
|
const editor = useEditor({
|
|
41
78
|
content: model.value ?? "",
|
|
42
|
-
extensions: [StarterKit],
|
|
79
|
+
extensions: [StarterKit.configure({ link: { openOnClick: false } })],
|
|
43
80
|
editorProps: {
|
|
44
81
|
attributes: { class: "cms-richtext-body" }
|
|
45
82
|
},
|
|
@@ -58,6 +95,7 @@ const actions = [
|
|
|
58
95
|
{
|
|
59
96
|
icon: "i-lucide-bold",
|
|
60
97
|
mark: "bold",
|
|
98
|
+
label: "cms.richtext.bold",
|
|
61
99
|
run: () => {
|
|
62
100
|
editor.value?.chain().focus().toggleBold().run();
|
|
63
101
|
}
|
|
@@ -65,6 +103,7 @@ const actions = [
|
|
|
65
103
|
{
|
|
66
104
|
icon: "i-lucide-italic",
|
|
67
105
|
mark: "italic",
|
|
106
|
+
label: "cms.richtext.italic",
|
|
68
107
|
run: () => {
|
|
69
108
|
editor.value?.chain().focus().toggleItalic().run();
|
|
70
109
|
}
|
|
@@ -72,6 +111,7 @@ const actions = [
|
|
|
72
111
|
{
|
|
73
112
|
icon: "i-lucide-strikethrough",
|
|
74
113
|
mark: "strike",
|
|
114
|
+
label: "cms.richtext.strike",
|
|
75
115
|
run: () => {
|
|
76
116
|
editor.value?.chain().focus().toggleStrike().run();
|
|
77
117
|
}
|
|
@@ -80,6 +120,7 @@ const actions = [
|
|
|
80
120
|
icon: "i-lucide-heading-2",
|
|
81
121
|
mark: "heading",
|
|
82
122
|
attrs: { level: 2 },
|
|
123
|
+
label: "cms.richtext.h2",
|
|
83
124
|
run: () => {
|
|
84
125
|
editor.value?.chain().focus().toggleHeading({ level: 2 }).run();
|
|
85
126
|
}
|
|
@@ -88,6 +129,7 @@ const actions = [
|
|
|
88
129
|
icon: "i-lucide-heading-3",
|
|
89
130
|
mark: "heading",
|
|
90
131
|
attrs: { level: 3 },
|
|
132
|
+
label: "cms.richtext.h3",
|
|
91
133
|
run: () => {
|
|
92
134
|
editor.value?.chain().focus().toggleHeading({ level: 3 }).run();
|
|
93
135
|
}
|
|
@@ -95,6 +137,7 @@ const actions = [
|
|
|
95
137
|
{
|
|
96
138
|
icon: "i-lucide-list",
|
|
97
139
|
mark: "bulletList",
|
|
140
|
+
label: "cms.richtext.bulletList",
|
|
98
141
|
run: () => {
|
|
99
142
|
editor.value?.chain().focus().toggleBulletList().run();
|
|
100
143
|
}
|
|
@@ -102,6 +145,7 @@ const actions = [
|
|
|
102
145
|
{
|
|
103
146
|
icon: "i-lucide-list-ordered",
|
|
104
147
|
mark: "orderedList",
|
|
148
|
+
label: "cms.richtext.orderedList",
|
|
105
149
|
run: () => {
|
|
106
150
|
editor.value?.chain().focus().toggleOrderedList().run();
|
|
107
151
|
}
|
|
@@ -109,11 +153,28 @@ const actions = [
|
|
|
109
153
|
{
|
|
110
154
|
icon: "i-lucide-text-quote",
|
|
111
155
|
mark: "blockquote",
|
|
156
|
+
label: "cms.richtext.blockquote",
|
|
112
157
|
run: () => {
|
|
113
158
|
editor.value?.chain().focus().toggleBlockquote().run();
|
|
114
159
|
}
|
|
115
160
|
}
|
|
116
161
|
];
|
|
162
|
+
const linkOpen = ref(false);
|
|
163
|
+
const linkUrl = ref("");
|
|
164
|
+
watch(linkOpen, (open) => {
|
|
165
|
+
if (open) linkUrl.value = editor.value?.getAttributes("link").href ?? "";
|
|
166
|
+
});
|
|
167
|
+
function applyLink() {
|
|
168
|
+
const url = linkUrl.value.trim();
|
|
169
|
+
const chain = editor.value?.chain().focus().extendMarkRange("link");
|
|
170
|
+
if (url) chain?.setLink({ href: url }).run();
|
|
171
|
+
else chain?.unsetLink().run();
|
|
172
|
+
linkOpen.value = false;
|
|
173
|
+
}
|
|
174
|
+
function removeLink() {
|
|
175
|
+
editor.value?.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
176
|
+
linkOpen.value = false;
|
|
177
|
+
}
|
|
117
178
|
function undo() {
|
|
118
179
|
editor.value?.chain().focus().undo().run();
|
|
119
180
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { useOverlay } from "#imports";
|
|
2
|
+
import ConfirmModal from "../components/cms/ConfirmModal.vue";
|
|
3
|
+
export function useCmsConfirm() {
|
|
4
|
+
const overlay = useOverlay();
|
|
5
|
+
const modal = overlay.create(ConfirmModal);
|
|
6
|
+
return async (message, options) => await modal.open({ message, ...options }) === true;
|
|
7
|
+
}
|
|
@@ -27,7 +27,16 @@
|
|
|
27
27
|
"emptyBody": "Entries you create will show up here.",
|
|
28
28
|
"deleteConfirm": "Delete this row?",
|
|
29
29
|
"status": "Status",
|
|
30
|
-
"columns": "Columns"
|
|
30
|
+
"columns": "Columns",
|
|
31
|
+
"loadError": "Could not load entries",
|
|
32
|
+
"retry": "Retry",
|
|
33
|
+
"searchPlaceholder": "Search…",
|
|
34
|
+
"noResults": "No matching entries"
|
|
35
|
+
},
|
|
36
|
+
"confirm": {
|
|
37
|
+
"title": "Are you sure?",
|
|
38
|
+
"confirm": "Confirm",
|
|
39
|
+
"cancel": "Cancel"
|
|
31
40
|
},
|
|
32
41
|
"entry": {
|
|
33
42
|
"unsavedChanges": "You have unsaved changes. Leave anyway?"
|
|
@@ -79,7 +88,28 @@
|
|
|
79
88
|
"mediaChange": "Change",
|
|
80
89
|
"mediaRemove": "Remove",
|
|
81
90
|
"invalidJson": "Invalid JSON",
|
|
82
|
-
"addBlock": "Add block"
|
|
91
|
+
"addBlock": "Add block",
|
|
92
|
+
"moveUp": "Move up",
|
|
93
|
+
"moveDown": "Move down",
|
|
94
|
+
"duplicateBlock": "Duplicate block",
|
|
95
|
+
"removeBlock": "Remove block",
|
|
96
|
+
"collapseBlock": "Collapse block",
|
|
97
|
+
"expandBlock": "Expand block"
|
|
98
|
+
},
|
|
99
|
+
"richtext": {
|
|
100
|
+
"bold": "Bold",
|
|
101
|
+
"italic": "Italic",
|
|
102
|
+
"strike": "Strikethrough",
|
|
103
|
+
"h2": "Heading 2",
|
|
104
|
+
"h3": "Heading 3",
|
|
105
|
+
"bulletList": "Bullet list",
|
|
106
|
+
"orderedList": "Numbered list",
|
|
107
|
+
"blockquote": "Quote",
|
|
108
|
+
"link": "Link",
|
|
109
|
+
"setLink": "Apply",
|
|
110
|
+
"unsetLink": "Remove link",
|
|
111
|
+
"undo": "Undo",
|
|
112
|
+
"redo": "Redo"
|
|
83
113
|
},
|
|
84
114
|
"validation": {
|
|
85
115
|
"required": "Required field",
|
|
@@ -27,7 +27,16 @@
|
|
|
27
27
|
"emptyBody": "Le voci che crei appariranno qui.",
|
|
28
28
|
"deleteConfirm": "Eliminare questa voce?",
|
|
29
29
|
"status": "Stato",
|
|
30
|
-
"columns": "Colonne"
|
|
30
|
+
"columns": "Colonne",
|
|
31
|
+
"loadError": "Impossibile caricare le voci",
|
|
32
|
+
"retry": "Riprova",
|
|
33
|
+
"searchPlaceholder": "Cerca…",
|
|
34
|
+
"noResults": "Nessuna voce corrispondente"
|
|
35
|
+
},
|
|
36
|
+
"confirm": {
|
|
37
|
+
"title": "Sei sicuro?",
|
|
38
|
+
"confirm": "Conferma",
|
|
39
|
+
"cancel": "Annulla"
|
|
31
40
|
},
|
|
32
41
|
"entry": {
|
|
33
42
|
"unsavedChanges": "Ci sono modifiche non salvate. Uscire comunque?"
|
|
@@ -79,7 +88,28 @@
|
|
|
79
88
|
"mediaChange": "Cambia",
|
|
80
89
|
"mediaRemove": "Rimuovi",
|
|
81
90
|
"invalidJson": "JSON non valido",
|
|
82
|
-
"addBlock": "Aggiungi blocco"
|
|
91
|
+
"addBlock": "Aggiungi blocco",
|
|
92
|
+
"moveUp": "Sposta su",
|
|
93
|
+
"moveDown": "Sposta giù",
|
|
94
|
+
"duplicateBlock": "Duplica blocco",
|
|
95
|
+
"removeBlock": "Rimuovi blocco",
|
|
96
|
+
"collapseBlock": "Comprimi blocco",
|
|
97
|
+
"expandBlock": "Espandi blocco"
|
|
98
|
+
},
|
|
99
|
+
"richtext": {
|
|
100
|
+
"bold": "Grassetto",
|
|
101
|
+
"italic": "Corsivo",
|
|
102
|
+
"strike": "Barrato",
|
|
103
|
+
"h2": "Titolo 2",
|
|
104
|
+
"h3": "Titolo 3",
|
|
105
|
+
"bulletList": "Elenco puntato",
|
|
106
|
+
"orderedList": "Elenco numerato",
|
|
107
|
+
"blockquote": "Citazione",
|
|
108
|
+
"link": "Link",
|
|
109
|
+
"setLink": "Applica",
|
|
110
|
+
"unsetLink": "Rimuovi link",
|
|
111
|
+
"undo": "Annulla",
|
|
112
|
+
"redo": "Ripristina"
|
|
83
113
|
},
|
|
84
114
|
"validation": {
|
|
85
115
|
"required": "Campo obbligatorio",
|