@xleddyl/nuxt-cms 0.2.2 → 0.3.0
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 +4 -2
- package/dist/module.d.mts +4 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +329 -32
- package/dist/runtime/app/blocks/CmsBlocks.d.vue.ts +45 -0
- package/dist/runtime/app/blocks/CmsBlocks.vue +36 -0
- package/dist/runtime/app/blocks/CmsBlocks.vue.d.ts +45 -0
- package/dist/runtime/app/blocks/resolve.d.ts +21 -0
- package/dist/runtime/app/blocks/resolve.js +37 -0
- package/dist/runtime/app/components/cms/BlockOutline.d.vue.ts +17 -0
- package/dist/runtime/app/components/cms/BlockOutline.vue +99 -0
- package/dist/runtime/app/components/cms/BlockOutline.vue.d.ts +17 -0
- package/dist/runtime/app/components/cms/BlockPalette.d.vue.ts +12 -0
- package/dist/runtime/app/components/cms/BlockPalette.vue +29 -0
- package/dist/runtime/app/components/cms/BlockPalette.vue.d.ts +12 -0
- package/dist/runtime/app/components/cms/BlocksField.d.vue.ts +2 -0
- package/dist/runtime/app/components/cms/BlocksField.vue +83 -3
- package/dist/runtime/app/components/cms/BlocksField.vue.d.ts +2 -0
- package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +2 -0
- package/dist/runtime/app/components/cms/EntryForm.vue +43 -7
- package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +2 -0
- package/dist/runtime/app/components/cms/FieldInput.d.vue.ts +2 -0
- package/dist/runtime/app/components/cms/FieldInput.vue +18 -1
- package/dist/runtime/app/components/cms/FieldInput.vue.d.ts +2 -0
- package/dist/runtime/app/components/cms/MediaDetail.vue +25 -13
- package/dist/runtime/app/components/cms/Search.d.vue.ts +7 -0
- package/dist/runtime/app/components/cms/Search.vue +347 -0
- package/dist/runtime/app/components/cms/Search.vue.d.ts +7 -0
- package/dist/runtime/app/components/cms/SettingsEntry.vue +3 -2
- package/dist/runtime/app/components/cms/SettingsModal.vue +2 -1
- package/dist/runtime/app/components/cms/StatusBadge.d.vue.ts +2 -0
- package/dist/runtime/app/components/cms/StatusBadge.vue +9 -3
- package/dist/runtime/app/components/cms/StatusBadge.vue.d.ts +2 -0
- package/dist/runtime/app/components/cms/VisualEditor.d.vue.ts +39 -0
- package/dist/runtime/app/components/cms/VisualEditor.vue +553 -0
- package/dist/runtime/app/components/cms/VisualEditor.vue.d.ts +39 -0
- package/dist/runtime/app/composables/cms-entry-disabled.d.ts +7 -1
- package/dist/runtime/app/composables/cms-entry-disabled.js +15 -0
- package/dist/runtime/app/composables/cms-entry.d.ts +7 -1
- package/dist/runtime/app/composables/cms-entry.js +51 -1
- package/dist/runtime/app/composables/cms-field-focus.d.ts +4 -0
- package/dist/runtime/app/composables/cms-field-focus.js +20 -0
- package/dist/runtime/app/composables/cms-runtime.d.ts +1 -0
- package/dist/runtime/app/composables/cms-settings.js +8 -2
- package/dist/runtime/app/layouts/cms-admin.d.vue.ts +2 -2
- package/dist/runtime/app/layouts/cms-admin.vue +7 -0
- package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
- package/dist/runtime/app/layouts/cms-editor.d.vue.ts +13 -0
- package/dist/runtime/app/layouts/cms-editor.vue +14 -0
- package/dist/runtime/app/layouts/cms-editor.vue.d.ts +13 -0
- package/dist/runtime/app/pages/admin-collection.vue +36 -4
- package/dist/runtime/app/pages/admin-content.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-content.vue +287 -0
- package/dist/runtime/app/pages/admin-content.vue.d.ts +3 -0
- package/dist/runtime/app/pages/admin-entry.vue +3 -0
- package/dist/runtime/app/pages/admin-preview.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-preview.vue +132 -0
- package/dist/runtime/app/pages/admin-preview.vue.d.ts +3 -0
- package/dist/runtime/app/preview/PreviewOverlay.d.vue.ts +15 -0
- package/dist/runtime/app/preview/PreviewOverlay.vue +362 -0
- package/dist/runtime/app/preview/PreviewOverlay.vue.d.ts +15 -0
- package/dist/runtime/app/utils/ui.js +1 -0
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/server/api/collection.get.js +4 -5
- package/dist/runtime/server/api/collection.post.js +4 -1
- package/dist/runtime/server/api/item.delete.js +2 -1
- package/dist/runtime/server/api/item.put.js +2 -1
- package/dist/runtime/server/api/media-download.get.d.ts +2 -0
- package/dist/runtime/server/api/media-download.get.js +90 -0
- package/dist/runtime/server/api/search.get.d.ts +3 -0
- package/dist/runtime/server/api/search.get.js +24 -0
- package/dist/runtime/server/utils/graphql.js +23 -8
- package/dist/runtime/server/utils/media-download.d.ts +1 -0
- package/dist/runtime/server/utils/media-download.js +8 -0
- package/dist/runtime/server/utils/media.d.ts +1 -0
- package/dist/runtime/server/utils/media.js +18 -3
- package/dist/runtime/server/utils/registry.d.ts +1 -0
- package/dist/runtime/server/utils/registry.js +6 -0
- package/dist/runtime/server/utils/search.d.ts +20 -0
- package/dist/runtime/server/utils/search.js +233 -0
- package/dist/runtime/server/utils/visibility.d.ts +4 -0
- package/dist/runtime/server/utils/visibility.js +12 -0
- package/dist/runtime/shared/graphql-sdl.js +4 -1
- package/dist/runtime/shared/index.d.ts +59 -5
- package/dist/runtime/shared/index.js +133 -0
- package/dist/runtime/shared/preview.d.ts +85 -0
- package/dist/runtime/shared/preview.js +219 -0
- package/dist/runtime/shared/search.d.ts +52 -0
- package/dist/runtime/shared/search.js +207 -0
- package/dist/runtime/shared/validation.d.ts +2 -0
- package/dist/runtime/shared/validation.js +15 -2
- package/package.json +2 -2
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cms-editor">
|
|
3
|
+
<header class="cms-editor-bar">
|
|
4
|
+
<div class="cms-editor-bar-start">
|
|
5
|
+
<slot name="start" />
|
|
6
|
+
<div class="cms-editor-heading">
|
|
7
|
+
<span class="cms-editor-title" :title="title">{{ title }}</span>
|
|
8
|
+
<slot name="status" />
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="cms-editor-bar-center">
|
|
12
|
+
<CmsLocaleSwitch
|
|
13
|
+
v-if="i18n.locales.length > 1"
|
|
14
|
+
v-model="locale"
|
|
15
|
+
:value="localeFilled"
|
|
16
|
+
/>
|
|
17
|
+
<div class="cms-editor-devices" role="group" aria-label="Preview width">
|
|
18
|
+
<button
|
|
19
|
+
v-for="option in DEVICES"
|
|
20
|
+
:key="option.id"
|
|
21
|
+
type="button"
|
|
22
|
+
class="cms-editor-device"
|
|
23
|
+
:class="{ 'is-active': device === option.id }"
|
|
24
|
+
:aria-pressed="device === option.id"
|
|
25
|
+
:aria-label="option.label"
|
|
26
|
+
:title="option.label"
|
|
27
|
+
@click="device = option.id"
|
|
28
|
+
>
|
|
29
|
+
<CmsIcon :name="option.icon" class="size-4" />
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="cms-editor-bar-end">
|
|
34
|
+
<slot name="actions" />
|
|
35
|
+
</div>
|
|
36
|
+
</header>
|
|
37
|
+
|
|
38
|
+
<div class="cms-editor-body">
|
|
39
|
+
<div class="cms-editor-canvas">
|
|
40
|
+
<div class="cms-editor-frame" :class="`is-${device}`">
|
|
41
|
+
<iframe
|
|
42
|
+
ref="frame"
|
|
43
|
+
:src="previewPath"
|
|
44
|
+
title="Live preview"
|
|
45
|
+
class="cms-editor-iframe"
|
|
46
|
+
@load="sendState"
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<aside class="cms-editor-panel" aria-label="Inspector">
|
|
52
|
+
<div v-if="tabList.length > 1" class="cms-editor-tabs">
|
|
53
|
+
<div class="cms-tabs" role="tablist">
|
|
54
|
+
<button
|
|
55
|
+
v-for="tab in tabList"
|
|
56
|
+
:id="`cms-editor-tab-${tab.id}`"
|
|
57
|
+
:key="tab.id"
|
|
58
|
+
type="button"
|
|
59
|
+
role="tab"
|
|
60
|
+
class="cms-tab"
|
|
61
|
+
:class="{ 'is-active': tab.id === activeTab }"
|
|
62
|
+
:aria-selected="tab.id === activeTab"
|
|
63
|
+
:aria-controls="`cms-editor-tabpanel-${tab.id}`"
|
|
64
|
+
@click="activeTab = tab.id"
|
|
65
|
+
>
|
|
66
|
+
{{ tab.label }}
|
|
67
|
+
<span
|
|
68
|
+
v-if="tabHasError(tab.id)"
|
|
69
|
+
class="cms-editor-tab-dot"
|
|
70
|
+
aria-label="has errors"
|
|
71
|
+
/>
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div
|
|
77
|
+
:id="`cms-editor-tabpanel-${activeTab}`"
|
|
78
|
+
ref="panel"
|
|
79
|
+
class="cms-editor-panel-body"
|
|
80
|
+
role="tabpanel"
|
|
81
|
+
:aria-labelledby="tabList.length > 1 ? `cms-editor-tab-${activeTab}` : void 0"
|
|
82
|
+
>
|
|
83
|
+
<template v-if="activeTab === 'block'">
|
|
84
|
+
<template v-if="selectedItem && selected !== null">
|
|
85
|
+
<div class="cms-editor-block-head">
|
|
86
|
+
<CmsButton
|
|
87
|
+
icon="arrow-left"
|
|
88
|
+
size="sm"
|
|
89
|
+
variant="ghost"
|
|
90
|
+
color="neutral"
|
|
91
|
+
label="All blocks"
|
|
92
|
+
@click="select(null)"
|
|
93
|
+
/>
|
|
94
|
+
<span class="cms-editor-block-position">
|
|
95
|
+
{{ selected + 1 }} / {{ blocks.length }}
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="cms-editor-block-title">
|
|
99
|
+
<CmsIcon :name="selectedBlock?.icon ?? 'rectangle-stack'" class="size-4" />
|
|
100
|
+
<span>{{ selectedBlock?.label ?? selectedItem.type }}</span>
|
|
101
|
+
<span v-if="isHiddenBlock(selectedItem)" class="cms-badge is-muted">
|
|
102
|
+
Hidden
|
|
103
|
+
</span>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="cms-editor-block-actions">
|
|
106
|
+
<CmsButton
|
|
107
|
+
icon="arrow-up"
|
|
108
|
+
size="sm"
|
|
109
|
+
variant="soft"
|
|
110
|
+
color="neutral"
|
|
111
|
+
aria-label="Move up"
|
|
112
|
+
title="Move up"
|
|
113
|
+
:disabled="selected === 0"
|
|
114
|
+
@click="moveSelected(-1)"
|
|
115
|
+
/>
|
|
116
|
+
<CmsButton
|
|
117
|
+
icon="arrow-down"
|
|
118
|
+
size="sm"
|
|
119
|
+
variant="soft"
|
|
120
|
+
color="neutral"
|
|
121
|
+
aria-label="Move down"
|
|
122
|
+
title="Move down"
|
|
123
|
+
:disabled="selected === blocks.length - 1"
|
|
124
|
+
@click="moveSelected(1)"
|
|
125
|
+
/>
|
|
126
|
+
<CmsButton
|
|
127
|
+
icon="document-duplicate"
|
|
128
|
+
size="sm"
|
|
129
|
+
variant="soft"
|
|
130
|
+
color="neutral"
|
|
131
|
+
label="Duplicate"
|
|
132
|
+
@click="duplicate(selected)"
|
|
133
|
+
/>
|
|
134
|
+
<CmsButton
|
|
135
|
+
v-if="canHideBlock(selectedBlock)"
|
|
136
|
+
:icon="isHiddenBlock(selectedItem) ? 'eye' : 'eye-slash'"
|
|
137
|
+
size="sm"
|
|
138
|
+
variant="soft"
|
|
139
|
+
color="neutral"
|
|
140
|
+
:label="isHiddenBlock(selectedItem) ? 'Show' : 'Hide'"
|
|
141
|
+
@click="toggleHidden(selected)"
|
|
142
|
+
/>
|
|
143
|
+
<CmsButton
|
|
144
|
+
icon="trash"
|
|
145
|
+
size="sm"
|
|
146
|
+
variant="soft"
|
|
147
|
+
color="error"
|
|
148
|
+
aria-label="Delete block"
|
|
149
|
+
title="Delete"
|
|
150
|
+
@click="remove(selected)"
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="cms-editor-fields">
|
|
154
|
+
<CmsFormField
|
|
155
|
+
v-for="(blockField, blockKey) in selectedBlock?.fields ?? {}"
|
|
156
|
+
:key="`${selected}-${blockKey}`"
|
|
157
|
+
:label="blockField.label"
|
|
158
|
+
:description="blockField.description"
|
|
159
|
+
:icon="fieldIcon(blockField)"
|
|
160
|
+
:required="blockField.required"
|
|
161
|
+
:name="`${fieldKey}.${selected}.${blockKey}`"
|
|
162
|
+
>
|
|
163
|
+
<CmsFieldInput
|
|
164
|
+
:model-value="selectedItem[blockKey]"
|
|
165
|
+
:field="blockField"
|
|
166
|
+
:locale="locale"
|
|
167
|
+
@update:model-value="
|
|
168
|
+
(value) => updateBlockField(String(blockKey), value)
|
|
169
|
+
"
|
|
170
|
+
/>
|
|
171
|
+
</CmsFormField>
|
|
172
|
+
</div>
|
|
173
|
+
</template>
|
|
174
|
+
|
|
175
|
+
<template v-else>
|
|
176
|
+
<p v-if="errors[fieldKey]" class="cms-form-error">
|
|
177
|
+
<CmsIcon name="exclamation-circle" class="size-3.5 shrink-0" />
|
|
178
|
+
{{ errors[fieldKey] }}
|
|
179
|
+
</p>
|
|
180
|
+
<div v-if="blocks.length" class="cms-editor-section">
|
|
181
|
+
<span class="cms-label">{{ blocksField.label }}</span>
|
|
182
|
+
<CmsBlockOutline
|
|
183
|
+
:items="blocks"
|
|
184
|
+
:field="blocksField"
|
|
185
|
+
:locale="locale"
|
|
186
|
+
:selected="selected"
|
|
187
|
+
@select="select"
|
|
188
|
+
@move="move"
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
<div class="cms-editor-section">
|
|
192
|
+
<span class="cms-label">Add a block</span>
|
|
193
|
+
<CmsBlockPalette
|
|
194
|
+
:blocks="blocksField.blocks ?? {}"
|
|
195
|
+
@pick="(type) => insert(blocks.length, type)"
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
</template>
|
|
199
|
+
</template>
|
|
200
|
+
|
|
201
|
+
<div v-else class="cms-editor-fields">
|
|
202
|
+
<template v-for="section in sections" :key="section.id">
|
|
203
|
+
<div v-if="section.title" class="cms-editor-section-title">
|
|
204
|
+
<span class="cms-label">{{ section.title }}</span>
|
|
205
|
+
<span v-if="section.description" class="cms-form-description">
|
|
206
|
+
{{ section.description }}
|
|
207
|
+
</span>
|
|
208
|
+
</div>
|
|
209
|
+
<CmsFormField
|
|
210
|
+
v-for="key in section.keys"
|
|
211
|
+
:key="key"
|
|
212
|
+
:label="fields[key].label"
|
|
213
|
+
:description="fields[key].description"
|
|
214
|
+
:icon="fieldIcon(fields[key])"
|
|
215
|
+
:name="key"
|
|
216
|
+
:required="fields[key].required"
|
|
217
|
+
:data-cms-field="key"
|
|
218
|
+
:class="{ 'is-search-focus': highlighted === key }"
|
|
219
|
+
>
|
|
220
|
+
<CmsFieldInput
|
|
221
|
+
:model-value="state[key]"
|
|
222
|
+
:field="fields[key]"
|
|
223
|
+
:locale="locale"
|
|
224
|
+
:slug-source="slugSource(fields[key])"
|
|
225
|
+
@update:model-value="(value) => setValue(key, value)"
|
|
226
|
+
/>
|
|
227
|
+
</CmsFormField>
|
|
228
|
+
</template>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</aside>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<CmsModal v-model:open="paletteOpen" title="Insert a block">
|
|
235
|
+
<template #body>
|
|
236
|
+
<CmsBlockPalette :blocks="blocksField.blocks ?? {}" @pick="insertFromPalette" />
|
|
237
|
+
</template>
|
|
238
|
+
</CmsModal>
|
|
239
|
+
</div>
|
|
240
|
+
</template>
|
|
241
|
+
|
|
242
|
+
<script setup>
|
|
243
|
+
import { isFieldVisible, isHiddenBlock, isTranslatableField, slugSourceValue } from "#nuxt-cms";
|
|
244
|
+
import {
|
|
245
|
+
computed,
|
|
246
|
+
onBeforeUnmount,
|
|
247
|
+
onMounted,
|
|
248
|
+
provide,
|
|
249
|
+
reactive,
|
|
250
|
+
ref,
|
|
251
|
+
shallowRef,
|
|
252
|
+
watch
|
|
253
|
+
} from "#imports";
|
|
254
|
+
import { buildEntrySchema } from "../../../shared/validation";
|
|
255
|
+
import {
|
|
256
|
+
DEFAULT_CMS_PREVIEW_PATH,
|
|
257
|
+
canHideBlock,
|
|
258
|
+
collectMediaKeys,
|
|
259
|
+
duplicateBlock,
|
|
260
|
+
emptyBlock,
|
|
261
|
+
insertBlock,
|
|
262
|
+
isTrustedPreviewEvent,
|
|
263
|
+
moveBlock,
|
|
264
|
+
parseFrameMessage,
|
|
265
|
+
previewEnvelope,
|
|
266
|
+
previewMedia,
|
|
267
|
+
removeBlock,
|
|
268
|
+
selectionAfterMove,
|
|
269
|
+
selectionAfterRemove,
|
|
270
|
+
toggleBlockHidden
|
|
271
|
+
} from "../../../shared/preview";
|
|
272
|
+
import { useCmsConfirm } from "../../composables/cms-confirm";
|
|
273
|
+
import { useCmsFieldFocus } from "../../composables/cms-field-focus";
|
|
274
|
+
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
275
|
+
import { entryFormLayout, useCmsSettingsState } from "../../composables/cms-settings";
|
|
276
|
+
import { CMS_FORM_ERRORS, fieldIcon } from "../../utils/ui";
|
|
277
|
+
const SEO_KEYS = ["seoTitle", "seoDescription", "seoImage"];
|
|
278
|
+
const DEVICES = [
|
|
279
|
+
{ id: "desktop", label: "Desktop width", icon: "computer-desktop" },
|
|
280
|
+
{ id: "mobile", label: "Mobile width", icon: "device-phone-mobile" }
|
|
281
|
+
];
|
|
282
|
+
const props = defineProps({
|
|
283
|
+
entryName: { type: String, required: true },
|
|
284
|
+
fieldKey: { type: String, required: true },
|
|
285
|
+
fields: { type: Object, required: true },
|
|
286
|
+
title: { type: String, required: true },
|
|
287
|
+
content: { type: Boolean, required: false },
|
|
288
|
+
focus: { type: [Object, null], required: false }
|
|
289
|
+
});
|
|
290
|
+
const state = defineModel("state", { type: Object, ...{ required: true } });
|
|
291
|
+
const runtime = useCmsRuntime();
|
|
292
|
+
const { i18n } = runtime;
|
|
293
|
+
const previewPath = runtime.previewPath ?? DEFAULT_CMS_PREVIEW_PATH;
|
|
294
|
+
const confirmAction = useCmsConfirm();
|
|
295
|
+
const settings = useCmsSettingsState();
|
|
296
|
+
const locale = ref(i18n.defaultLocale);
|
|
297
|
+
const device = ref("desktop");
|
|
298
|
+
const selected = ref(null);
|
|
299
|
+
const paletteOpen = ref(false);
|
|
300
|
+
const paletteIndex = ref(0);
|
|
301
|
+
const frame = ref(null);
|
|
302
|
+
const library = shallowRef(/* @__PURE__ */ new Map());
|
|
303
|
+
const blocksField = computed(() => props.fields[props.fieldKey]);
|
|
304
|
+
const blocks = computed(() => {
|
|
305
|
+
const value = state.value[props.fieldKey];
|
|
306
|
+
return Array.isArray(value) ? value : [];
|
|
307
|
+
});
|
|
308
|
+
const selectedItem = computed(
|
|
309
|
+
() => selected.value === null ? void 0 : blocks.value[selected.value]
|
|
310
|
+
);
|
|
311
|
+
const selectedBlock = computed(
|
|
312
|
+
() => selectedItem.value ? blocksField.value.blocks?.[String(selectedItem.value.type)] : void 0
|
|
313
|
+
);
|
|
314
|
+
const tabList = computed(() => {
|
|
315
|
+
if (!props.content) return [{ id: "block", label: "Block" }];
|
|
316
|
+
const tabs = [
|
|
317
|
+
{ id: "content", label: "Content" },
|
|
318
|
+
{ id: "block", label: "Block" }
|
|
319
|
+
];
|
|
320
|
+
if (seoKeys.value.length) tabs.push({ id: "seo", label: "SEO" });
|
|
321
|
+
return tabs;
|
|
322
|
+
});
|
|
323
|
+
const activeTab = ref(props.content ? "content" : "block");
|
|
324
|
+
const seoKeys = computed(() => SEO_KEYS.filter((key) => Object.hasOwn(props.fields, key)));
|
|
325
|
+
const contentFields = computed(
|
|
326
|
+
() => Object.fromEntries(
|
|
327
|
+
Object.entries(props.fields).filter(
|
|
328
|
+
([key]) => key !== props.fieldKey && !SEO_KEYS.includes(key)
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
);
|
|
332
|
+
const sections = computed(() => {
|
|
333
|
+
if (activeTab.value === "seo")
|
|
334
|
+
return [{ id: "seo", title: void 0, description: void 0, keys: seoKeys.value }];
|
|
335
|
+
const layout = entryFormLayout(props.entryName, settings.value, contentFields.value);
|
|
336
|
+
return layout.main.map((section, index) => ({
|
|
337
|
+
id: `${index}-${section.title ?? ""}`,
|
|
338
|
+
title: section.title,
|
|
339
|
+
description: section.description,
|
|
340
|
+
keys: section.rows.flat().filter((key) => {
|
|
341
|
+
const field = props.fields[key];
|
|
342
|
+
return !!field && isFieldVisible(field, state.value);
|
|
343
|
+
})
|
|
344
|
+
})).filter((section) => section.keys.length);
|
|
345
|
+
});
|
|
346
|
+
const localeFilled = computed(() => {
|
|
347
|
+
const title = props.content ? state.value.title : null;
|
|
348
|
+
return title && typeof title === "object" ? title : null;
|
|
349
|
+
});
|
|
350
|
+
function setValue(key, value) {
|
|
351
|
+
state.value = { ...state.value, [key]: value };
|
|
352
|
+
}
|
|
353
|
+
function setBlocks(list) {
|
|
354
|
+
setValue(props.fieldKey, list.length ? list : null);
|
|
355
|
+
}
|
|
356
|
+
function slugSource(field) {
|
|
357
|
+
if (!field.from) return void 0;
|
|
358
|
+
return slugSourceValue(props.fields[field.from], state.value[field.from], i18n.defaultLocale);
|
|
359
|
+
}
|
|
360
|
+
function select(index) {
|
|
361
|
+
selected.value = index;
|
|
362
|
+
if (selected.value !== null) activeTab.value = "block";
|
|
363
|
+
}
|
|
364
|
+
function blockConfigAt(index) {
|
|
365
|
+
return blocksField.value.blocks?.[String(blocks.value[index]?.type)];
|
|
366
|
+
}
|
|
367
|
+
function insert(index, type) {
|
|
368
|
+
const block = blocksField.value.blocks?.[type];
|
|
369
|
+
if (!block) return;
|
|
370
|
+
const at = Math.min(Math.max(index, 0), blocks.value.length);
|
|
371
|
+
setBlocks(insertBlock(blocks.value, at, emptyBlock(type, block)));
|
|
372
|
+
select(at);
|
|
373
|
+
}
|
|
374
|
+
function insertFromPalette(type) {
|
|
375
|
+
paletteOpen.value = false;
|
|
376
|
+
insert(paletteIndex.value, type);
|
|
377
|
+
}
|
|
378
|
+
function openPalette(index) {
|
|
379
|
+
paletteIndex.value = index;
|
|
380
|
+
paletteOpen.value = true;
|
|
381
|
+
}
|
|
382
|
+
function move(from, to) {
|
|
383
|
+
if (to < 0 || to >= blocks.value.length || from === to) return;
|
|
384
|
+
setBlocks(moveBlock(blocks.value, from, to));
|
|
385
|
+
selected.value = selectionAfterMove(selected.value, from, to);
|
|
386
|
+
}
|
|
387
|
+
function moveSelected(delta) {
|
|
388
|
+
if (selected.value === null) return;
|
|
389
|
+
move(selected.value, selected.value + delta);
|
|
390
|
+
}
|
|
391
|
+
function duplicate(index) {
|
|
392
|
+
setBlocks(duplicateBlock(blocks.value, index));
|
|
393
|
+
select(index + 1);
|
|
394
|
+
}
|
|
395
|
+
function toggleHidden(index) {
|
|
396
|
+
setBlocks(toggleBlockHidden(blocks.value, index, blockConfigAt(index)));
|
|
397
|
+
}
|
|
398
|
+
async function remove(index) {
|
|
399
|
+
const label = blockConfigAt(index)?.label ?? "block";
|
|
400
|
+
if (!await confirmAction(`Delete this ${label.toLowerCase()} block?`)) return;
|
|
401
|
+
setBlocks(removeBlock(blocks.value, index));
|
|
402
|
+
selected.value = selectionAfterRemove(selected.value, index);
|
|
403
|
+
}
|
|
404
|
+
function updateBlockField(key, value) {
|
|
405
|
+
const index = selected.value;
|
|
406
|
+
const current = index === null ? void 0 : blocks.value[index];
|
|
407
|
+
if (index === null || !current) return;
|
|
408
|
+
setBlocks(blocks.value.map((item, i) => i === index ? { ...current, [key]: value } : item));
|
|
409
|
+
}
|
|
410
|
+
const media = computed(
|
|
411
|
+
() => Object.fromEntries(
|
|
412
|
+
collectMediaKeys(props.fields, state.value).map((key) => [
|
|
413
|
+
key,
|
|
414
|
+
previewMedia(key, library.value.get(key), runtime.mediaBaseUrl)
|
|
415
|
+
])
|
|
416
|
+
)
|
|
417
|
+
);
|
|
418
|
+
const previewState = computed(() => ({
|
|
419
|
+
entry: props.entryName,
|
|
420
|
+
field: props.fieldKey,
|
|
421
|
+
locale: locale.value,
|
|
422
|
+
blocks: blocks.value,
|
|
423
|
+
item: props.content ? state.value : null,
|
|
424
|
+
media: media.value,
|
|
425
|
+
selected: selected.value
|
|
426
|
+
}));
|
|
427
|
+
function sendState() {
|
|
428
|
+
const target = frame.value?.contentWindow;
|
|
429
|
+
if (!target) return;
|
|
430
|
+
const snapshot = JSON.parse(JSON.stringify(previewState.value));
|
|
431
|
+
target.postMessage(previewEnvelope({ type: "render", state: snapshot }), window.location.origin);
|
|
432
|
+
}
|
|
433
|
+
watch(previewState, sendState, { deep: true });
|
|
434
|
+
function onFrameMessage(message) {
|
|
435
|
+
switch (message.type) {
|
|
436
|
+
case "ready":
|
|
437
|
+
sendState();
|
|
438
|
+
break;
|
|
439
|
+
case "select":
|
|
440
|
+
select(message.index);
|
|
441
|
+
break;
|
|
442
|
+
case "insert":
|
|
443
|
+
openPalette(message.index);
|
|
444
|
+
break;
|
|
445
|
+
case "move":
|
|
446
|
+
move(message.from, message.to);
|
|
447
|
+
select(message.to);
|
|
448
|
+
break;
|
|
449
|
+
case "duplicate":
|
|
450
|
+
duplicate(message.index);
|
|
451
|
+
break;
|
|
452
|
+
case "toggle-hidden":
|
|
453
|
+
toggleHidden(message.index);
|
|
454
|
+
break;
|
|
455
|
+
case "remove":
|
|
456
|
+
void remove(message.index);
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function onMessage(event) {
|
|
461
|
+
if (!isTrustedPreviewEvent(event, window.location.origin, frame.value?.contentWindow)) return;
|
|
462
|
+
const message = parseFrameMessage(event.data);
|
|
463
|
+
if (message) onFrameMessage(message);
|
|
464
|
+
}
|
|
465
|
+
async function loadLibrary() {
|
|
466
|
+
try {
|
|
467
|
+
const result = await $fetch("/api/cms/admin/media");
|
|
468
|
+
library.value = new Map(result.items.map((item) => [item.key, item]));
|
|
469
|
+
} catch {
|
|
470
|
+
library.value = /* @__PURE__ */ new Map();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const panel = ref(null);
|
|
474
|
+
const { highlighted, reveal } = useCmsFieldFocus();
|
|
475
|
+
function applyFocus(focus) {
|
|
476
|
+
if (focus.locale && i18n.locales.includes(focus.locale)) locale.value = focus.locale;
|
|
477
|
+
const key = focus.field;
|
|
478
|
+
if (!key || !Object.hasOwn(props.fields, key)) return;
|
|
479
|
+
if (key === props.fieldKey) {
|
|
480
|
+
activeTab.value = "block";
|
|
481
|
+
if (focus.block != null && focus.block < blocks.value.length) select(focus.block);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
activeTab.value = tabOfKey(key);
|
|
485
|
+
reveal(() => panel.value, key);
|
|
486
|
+
}
|
|
487
|
+
onMounted(() => {
|
|
488
|
+
window.addEventListener("message", onMessage);
|
|
489
|
+
void loadLibrary();
|
|
490
|
+
if (props.focus) applyFocus(props.focus);
|
|
491
|
+
});
|
|
492
|
+
onBeforeUnmount(() => {
|
|
493
|
+
window.removeEventListener("message", onMessage);
|
|
494
|
+
});
|
|
495
|
+
const errors = reactive({});
|
|
496
|
+
provide(CMS_FORM_ERRORS, errors);
|
|
497
|
+
const schema = computed(
|
|
498
|
+
() => buildEntrySchema({ fields: props.fields, drafts: props.content }, i18n)
|
|
499
|
+
);
|
|
500
|
+
let submitted = false;
|
|
501
|
+
function collectErrors() {
|
|
502
|
+
for (const key of Object.keys(errors)) delete errors[key];
|
|
503
|
+
const result = schema.value.safeParse(state.value);
|
|
504
|
+
if (result.success) return [];
|
|
505
|
+
const issues = result.error.issues;
|
|
506
|
+
for (const issue of issues) {
|
|
507
|
+
const [key, index, blockKey] = issue.path;
|
|
508
|
+
if (key === void 0) continue;
|
|
509
|
+
errors[String(key)] ??= issue.message;
|
|
510
|
+
if (String(key) === props.fieldKey && typeof index === "number" && blockKey !== void 0)
|
|
511
|
+
errors[`${String(key)}.${index}.${String(blockKey)}`] ??= issue.message;
|
|
512
|
+
}
|
|
513
|
+
return issues;
|
|
514
|
+
}
|
|
515
|
+
function tabOfKey(key) {
|
|
516
|
+
if (key === props.fieldKey) return "block";
|
|
517
|
+
return SEO_KEYS.includes(key) ? "seo" : "content";
|
|
518
|
+
}
|
|
519
|
+
function tabHasError(tab) {
|
|
520
|
+
return Object.keys(errors).some((key) => tabOfKey(key.split(".")[0]) === tab);
|
|
521
|
+
}
|
|
522
|
+
function validate() {
|
|
523
|
+
submitted = true;
|
|
524
|
+
const issues = collectErrors();
|
|
525
|
+
const first = issues[0];
|
|
526
|
+
if (!first) return true;
|
|
527
|
+
const [key, index] = first.path;
|
|
528
|
+
const name = String(key ?? "");
|
|
529
|
+
const field = props.fields[name];
|
|
530
|
+
if (name === props.fieldKey) {
|
|
531
|
+
select(typeof index === "number" ? index : null);
|
|
532
|
+
activeTab.value = "block";
|
|
533
|
+
} else if (field) {
|
|
534
|
+
activeTab.value = tabOfKey(name);
|
|
535
|
+
if (isTranslatableField(field)) locale.value = i18n.defaultLocale;
|
|
536
|
+
}
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
watch(
|
|
540
|
+
state,
|
|
541
|
+
() => {
|
|
542
|
+
if (submitted) collectErrors();
|
|
543
|
+
},
|
|
544
|
+
{ deep: true }
|
|
545
|
+
);
|
|
546
|
+
watch(
|
|
547
|
+
() => blocks.value.length,
|
|
548
|
+
(length) => {
|
|
549
|
+
if (selected.value !== null && selected.value >= length) selected.value = null;
|
|
550
|
+
}
|
|
551
|
+
);
|
|
552
|
+
defineExpose({ validate, locale });
|
|
553
|
+
</script>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { FieldConfig } from '#nuxt-cms';
|
|
2
|
+
import type { CmsSearchFocus } from '../../../shared/search.js';
|
|
3
|
+
type __VLS_Props = {
|
|
4
|
+
entryName: string;
|
|
5
|
+
fieldKey: string;
|
|
6
|
+
fields: Record<string, FieldConfig>;
|
|
7
|
+
title: string;
|
|
8
|
+
content?: boolean;
|
|
9
|
+
focus?: CmsSearchFocus | null;
|
|
10
|
+
};
|
|
11
|
+
declare function validate(): boolean;
|
|
12
|
+
type __VLS_ModelProps = {
|
|
13
|
+
'state': Record<string, unknown>;
|
|
14
|
+
};
|
|
15
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
16
|
+
declare var __VLS_1: {}, __VLS_3: {}, __VLS_15: {};
|
|
17
|
+
type __VLS_Slots = {} & {
|
|
18
|
+
start?: (props: typeof __VLS_1) => any;
|
|
19
|
+
} & {
|
|
20
|
+
status?: (props: typeof __VLS_3) => any;
|
|
21
|
+
} & {
|
|
22
|
+
actions?: (props: typeof __VLS_15) => any;
|
|
23
|
+
};
|
|
24
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
25
|
+
validate: typeof validate;
|
|
26
|
+
locale: import("vue").Ref<string, string>;
|
|
27
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
28
|
+
"update:state": (value: Record<string, unknown>) => any;
|
|
29
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
30
|
+
"onUpdate:state"?: ((value: Record<string, unknown>) => any) | undefined;
|
|
31
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
32
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
33
|
+
declare const _default: typeof __VLS_export;
|
|
34
|
+
export default _default;
|
|
35
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
36
|
+
new (): {
|
|
37
|
+
$slots: S;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AsyncData } from 'nuxt/app';
|
|
2
|
-
import type { CmsCollectionName, CmsCollectionTypes, CmsPagePath, CmsPageTypes, CmsSingleName, CmsSingleTypes } from '#cms-types';
|
|
2
|
+
import type { CmsCollectionName, CmsCollectionTypes, CmsContentName, CmsContentTypes, CmsPagePath, CmsPageTypes, CmsSingleName, CmsSingleTypes } from '#cms-types';
|
|
3
3
|
import type { CmsAsyncDataOptions } from './cms-query-disabled.js';
|
|
4
4
|
export interface CmsSortInput<Entry> {
|
|
5
5
|
field: Extract<keyof Entry, string>;
|
|
@@ -18,6 +18,12 @@ export interface CmsCollectionOptions<ResT, DefaultT, Entry> extends CmsAsyncDat
|
|
|
18
18
|
limit?: number;
|
|
19
19
|
offset?: number;
|
|
20
20
|
}
|
|
21
|
+
export interface CmsContentOptions<ResT, DefaultT> extends CmsAsyncDataOptions<ResT, DefaultT> {
|
|
22
|
+
locale?: string;
|
|
23
|
+
}
|
|
24
|
+
export type CmsContentsOptions<ResT, DefaultT, Entry> = CmsCollectionOptions<ResT, DefaultT, Entry>;
|
|
21
25
|
export declare function useCmsSingle<K extends CmsSingleName, DefaultT = null>(name: K, options?: CmsSingleOptions<CmsSingleTypes[K] | null, DefaultT>): AsyncData<CmsSingleTypes[K] | DefaultT | null, Error | undefined>;
|
|
22
26
|
export declare function useCmsCollection<K extends CmsCollectionName, DefaultT = CmsCollectionTypes[K][]>(name: K, options?: CmsCollectionOptions<CmsCollectionTypes[K][], DefaultT, CmsCollectionTypes[K]>): AsyncData<CmsCollectionTypes[K][] | DefaultT, Error | undefined>;
|
|
27
|
+
export declare function useCmsContents<K extends CmsContentName, DefaultT = CmsContentTypes[K][]>(name: K, options?: CmsContentsOptions<CmsContentTypes[K][], DefaultT, CmsContentTypes[K]>): AsyncData<CmsContentTypes[K][] | DefaultT, Error | undefined>;
|
|
28
|
+
export declare function useCmsContent<K extends CmsContentName, DefaultT = null>(name: K, slug: string, options?: CmsContentOptions<CmsContentTypes[K] | null, DefaultT>): AsyncData<CmsContentTypes[K] | DefaultT | null, Error | undefined>;
|
|
23
29
|
export declare function useCmsPage<P extends CmsPagePath, DefaultT = null>(path: P, options?: CmsPageOptions<CmsPageTypes[P] | null, DefaultT>): AsyncData<CmsPageTypes[P] | DefaultT | null, Error | undefined>;
|
|
@@ -14,6 +14,21 @@ export function useCmsCollection(name, options = {}) {
|
|
|
14
14
|
async () => fallback ? fallback() : []
|
|
15
15
|
);
|
|
16
16
|
}
|
|
17
|
+
export function useCmsContents(name, options = {}) {
|
|
18
|
+
const { locale, filters, sort, limit, offset, key, default: fallback } = options;
|
|
19
|
+
const variables = { locale, filters, sort, limit, offset };
|
|
20
|
+
return useAsyncData(
|
|
21
|
+
key ?? `cms-contents:${String(name)}:${JSON.stringify(variables)}`,
|
|
22
|
+
async () => fallback ? fallback() : []
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
export function useCmsContent(name, slug, options = {}) {
|
|
26
|
+
const { locale, key, default: fallback } = options;
|
|
27
|
+
return useAsyncData(
|
|
28
|
+
key ?? `cms-content:${String(name)}:${slug}:${locale ?? ""}`,
|
|
29
|
+
async () => fallback ? fallback() : null
|
|
30
|
+
);
|
|
31
|
+
}
|
|
17
32
|
export function useCmsPage(path, options = {}) {
|
|
18
33
|
const { locale, key, default: fallback } = options;
|
|
19
34
|
return useAsyncData(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AsyncData } from 'nuxt/app';
|
|
2
|
-
import type { CmsCollectionName, CmsCollectionTypes, CmsPagePath, CmsPageTypes, CmsSingleName, CmsSingleTypes } from '#cms-types';
|
|
2
|
+
import type { CmsCollectionName, CmsCollectionTypes, CmsContentName, CmsContentTypes, CmsPagePath, CmsPageTypes, CmsSingleName, CmsSingleTypes } from '#cms-types';
|
|
3
3
|
import type { CmsAsyncDataOptions } from './cms-query.js';
|
|
4
4
|
export interface CmsSortInput<Entry> {
|
|
5
5
|
field: Extract<keyof Entry, string>;
|
|
@@ -18,6 +18,12 @@ export interface CmsCollectionOptions<ResT, DefaultT, Entry> extends CmsAsyncDat
|
|
|
18
18
|
limit?: number;
|
|
19
19
|
offset?: number;
|
|
20
20
|
}
|
|
21
|
+
export interface CmsContentOptions<ResT, DefaultT> extends CmsAsyncDataOptions<ResT, DefaultT> {
|
|
22
|
+
locale?: string;
|
|
23
|
+
}
|
|
24
|
+
export type CmsContentsOptions<ResT, DefaultT, Entry> = CmsCollectionOptions<ResT, DefaultT, Entry>;
|
|
21
25
|
export declare function useCmsSingle<K extends CmsSingleName, DefaultT = null>(name: K, options?: CmsSingleOptions<CmsSingleTypes[K] | null, DefaultT>): AsyncData<CmsSingleTypes[K] | DefaultT | null, Error | undefined>;
|
|
22
26
|
export declare function useCmsCollection<K extends CmsCollectionName, DefaultT = CmsCollectionTypes[K][]>(name: K, options?: CmsCollectionOptions<CmsCollectionTypes[K][], DefaultT, CmsCollectionTypes[K]>): AsyncData<CmsCollectionTypes[K][] | DefaultT, Error | undefined>;
|
|
27
|
+
export declare function useCmsContents<K extends CmsContentName, DefaultT = CmsContentTypes[K][]>(name: K, options?: CmsContentsOptions<CmsContentTypes[K][], DefaultT, CmsContentTypes[K]>): AsyncData<CmsContentTypes[K][] | DefaultT, Error | undefined>;
|
|
28
|
+
export declare function useCmsContent<K extends CmsContentName, DefaultT = null>(name: K, slug: string, options?: CmsContentOptions<CmsContentTypes[K] | null, DefaultT>): AsyncData<CmsContentTypes[K] | DefaultT | null, Error | undefined>;
|
|
23
29
|
export declare function useCmsPage<P extends CmsPagePath, DefaultT = null>(path: P, options?: CmsPageOptions<CmsPageTypes[P] | null, DefaultT>): AsyncData<CmsPageTypes[P] | DefaultT | null, Error | undefined>;
|