@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.
Files changed (92) hide show
  1. package/README.md +4 -2
  2. package/dist/module.d.mts +4 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +329 -32
  5. package/dist/runtime/app/blocks/CmsBlocks.d.vue.ts +45 -0
  6. package/dist/runtime/app/blocks/CmsBlocks.vue +36 -0
  7. package/dist/runtime/app/blocks/CmsBlocks.vue.d.ts +45 -0
  8. package/dist/runtime/app/blocks/resolve.d.ts +21 -0
  9. package/dist/runtime/app/blocks/resolve.js +37 -0
  10. package/dist/runtime/app/components/cms/BlockOutline.d.vue.ts +17 -0
  11. package/dist/runtime/app/components/cms/BlockOutline.vue +99 -0
  12. package/dist/runtime/app/components/cms/BlockOutline.vue.d.ts +17 -0
  13. package/dist/runtime/app/components/cms/BlockPalette.d.vue.ts +12 -0
  14. package/dist/runtime/app/components/cms/BlockPalette.vue +29 -0
  15. package/dist/runtime/app/components/cms/BlockPalette.vue.d.ts +12 -0
  16. package/dist/runtime/app/components/cms/BlocksField.d.vue.ts +2 -0
  17. package/dist/runtime/app/components/cms/BlocksField.vue +83 -3
  18. package/dist/runtime/app/components/cms/BlocksField.vue.d.ts +2 -0
  19. package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +2 -0
  20. package/dist/runtime/app/components/cms/EntryForm.vue +43 -7
  21. package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +2 -0
  22. package/dist/runtime/app/components/cms/FieldInput.d.vue.ts +2 -0
  23. package/dist/runtime/app/components/cms/FieldInput.vue +18 -1
  24. package/dist/runtime/app/components/cms/FieldInput.vue.d.ts +2 -0
  25. package/dist/runtime/app/components/cms/MediaDetail.vue +25 -13
  26. package/dist/runtime/app/components/cms/Search.d.vue.ts +7 -0
  27. package/dist/runtime/app/components/cms/Search.vue +347 -0
  28. package/dist/runtime/app/components/cms/Search.vue.d.ts +7 -0
  29. package/dist/runtime/app/components/cms/SettingsEntry.vue +3 -2
  30. package/dist/runtime/app/components/cms/SettingsModal.vue +2 -1
  31. package/dist/runtime/app/components/cms/StatusBadge.d.vue.ts +2 -0
  32. package/dist/runtime/app/components/cms/StatusBadge.vue +9 -3
  33. package/dist/runtime/app/components/cms/StatusBadge.vue.d.ts +2 -0
  34. package/dist/runtime/app/components/cms/VisualEditor.d.vue.ts +39 -0
  35. package/dist/runtime/app/components/cms/VisualEditor.vue +553 -0
  36. package/dist/runtime/app/components/cms/VisualEditor.vue.d.ts +39 -0
  37. package/dist/runtime/app/composables/cms-entry-disabled.d.ts +7 -1
  38. package/dist/runtime/app/composables/cms-entry-disabled.js +15 -0
  39. package/dist/runtime/app/composables/cms-entry.d.ts +7 -1
  40. package/dist/runtime/app/composables/cms-entry.js +51 -1
  41. package/dist/runtime/app/composables/cms-field-focus.d.ts +4 -0
  42. package/dist/runtime/app/composables/cms-field-focus.js +20 -0
  43. package/dist/runtime/app/composables/cms-runtime.d.ts +1 -0
  44. package/dist/runtime/app/composables/cms-settings.js +8 -2
  45. package/dist/runtime/app/layouts/cms-admin.d.vue.ts +2 -2
  46. package/dist/runtime/app/layouts/cms-admin.vue +7 -0
  47. package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
  48. package/dist/runtime/app/layouts/cms-editor.d.vue.ts +13 -0
  49. package/dist/runtime/app/layouts/cms-editor.vue +14 -0
  50. package/dist/runtime/app/layouts/cms-editor.vue.d.ts +13 -0
  51. package/dist/runtime/app/pages/admin-collection.vue +36 -4
  52. package/dist/runtime/app/pages/admin-content.d.vue.ts +3 -0
  53. package/dist/runtime/app/pages/admin-content.vue +287 -0
  54. package/dist/runtime/app/pages/admin-content.vue.d.ts +3 -0
  55. package/dist/runtime/app/pages/admin-entry.vue +3 -0
  56. package/dist/runtime/app/pages/admin-preview.d.vue.ts +3 -0
  57. package/dist/runtime/app/pages/admin-preview.vue +132 -0
  58. package/dist/runtime/app/pages/admin-preview.vue.d.ts +3 -0
  59. package/dist/runtime/app/preview/PreviewOverlay.d.vue.ts +15 -0
  60. package/dist/runtime/app/preview/PreviewOverlay.vue +362 -0
  61. package/dist/runtime/app/preview/PreviewOverlay.vue.d.ts +15 -0
  62. package/dist/runtime/app/utils/ui.js +1 -0
  63. package/dist/runtime/assets/main.css +1 -1
  64. package/dist/runtime/server/api/collection.get.js +4 -5
  65. package/dist/runtime/server/api/collection.post.js +4 -1
  66. package/dist/runtime/server/api/item.delete.js +2 -1
  67. package/dist/runtime/server/api/item.put.js +2 -1
  68. package/dist/runtime/server/api/media-download.get.d.ts +2 -0
  69. package/dist/runtime/server/api/media-download.get.js +90 -0
  70. package/dist/runtime/server/api/search.get.d.ts +3 -0
  71. package/dist/runtime/server/api/search.get.js +24 -0
  72. package/dist/runtime/server/utils/graphql.js +23 -8
  73. package/dist/runtime/server/utils/media-download.d.ts +1 -0
  74. package/dist/runtime/server/utils/media-download.js +8 -0
  75. package/dist/runtime/server/utils/media.d.ts +1 -0
  76. package/dist/runtime/server/utils/media.js +18 -3
  77. package/dist/runtime/server/utils/registry.d.ts +1 -0
  78. package/dist/runtime/server/utils/registry.js +6 -0
  79. package/dist/runtime/server/utils/search.d.ts +20 -0
  80. package/dist/runtime/server/utils/search.js +233 -0
  81. package/dist/runtime/server/utils/visibility.d.ts +4 -0
  82. package/dist/runtime/server/utils/visibility.js +12 -0
  83. package/dist/runtime/shared/graphql-sdl.js +4 -1
  84. package/dist/runtime/shared/index.d.ts +59 -5
  85. package/dist/runtime/shared/index.js +133 -0
  86. package/dist/runtime/shared/preview.d.ts +85 -0
  87. package/dist/runtime/shared/preview.js +219 -0
  88. package/dist/runtime/shared/search.d.ts +52 -0
  89. package/dist/runtime/shared/search.js +207 -0
  90. package/dist/runtime/shared/validation.d.ts +2 -0
  91. package/dist/runtime/shared/validation.js +15 -2
  92. 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>;