@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,36 @@
1
+ <template>
2
+ <component
3
+ :is="block.component"
4
+ v-for="block in resolved"
5
+ :key="block.key"
6
+ v-bind="annotate ? { ...block.props, ...annotation(block) } : block.props"
7
+ />
8
+ </template>
9
+
10
+ <script setup>
11
+ import { computed } from "#imports";
12
+ import { cmsBlockTypeName, resolveCmsBlocks } from "./resolve";
13
+ const props = defineProps({
14
+ blocks: { type: Array, default: null },
15
+ entry: { type: String, default: void 0 },
16
+ field: { type: String, default: void 0 },
17
+ annotate: { type: Boolean, default: false }
18
+ });
19
+ function warnMissing(item) {
20
+ if (!import.meta.dev) return;
21
+ const name = cmsBlockTypeName(item, props.entry, props.field) ?? item.type;
22
+ console.warn(
23
+ `[nuxt-cms] <CmsBlocks> has no component for block '${name}': set component on the block in cms.config, or pass entry and field when the data has no __typename`
24
+ );
25
+ }
26
+ const resolved = computed(
27
+ () => resolveCmsBlocks(props.blocks, {
28
+ entry: props.entry,
29
+ field: props.field,
30
+ onMissing: warnMissing
31
+ })
32
+ );
33
+ function annotation(block) {
34
+ return { "data-cms-block": block.index, "data-cms-block-type": block.type };
35
+ }
36
+ </script>
@@ -0,0 +1,45 @@
1
+ import type { PropType } from 'vue';
2
+ import type { CmsBlockEntryName } from '#cms-types';
3
+ import type { CmsBlockItem } from './resolve.js';
4
+ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
5
+ blocks: {
6
+ type: PropType<readonly CmsBlockItem[] | null>;
7
+ default: null;
8
+ };
9
+ entry: {
10
+ type: PropType<CmsBlockEntryName>;
11
+ default: undefined;
12
+ };
13
+ field: {
14
+ type: StringConstructor;
15
+ default: undefined;
16
+ };
17
+ annotate: {
18
+ type: BooleanConstructor;
19
+ default: boolean;
20
+ };
21
+ }>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
22
+ blocks: {
23
+ type: PropType<readonly CmsBlockItem[] | null>;
24
+ default: null;
25
+ };
26
+ entry: {
27
+ type: PropType<CmsBlockEntryName>;
28
+ default: undefined;
29
+ };
30
+ field: {
31
+ type: StringConstructor;
32
+ default: undefined;
33
+ };
34
+ annotate: {
35
+ type: BooleanConstructor;
36
+ default: boolean;
37
+ };
38
+ }>> & Readonly<{}>, {
39
+ field: string;
40
+ blocks: readonly CmsBlockItem[] | null;
41
+ entry: string;
42
+ annotate: boolean;
43
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
44
+ declare const _default: typeof __VLS_export;
45
+ export default _default;
@@ -0,0 +1,21 @@
1
+ import type { Component } from 'vue';
2
+ export interface CmsBlockItem {
3
+ type: string;
4
+ __typename?: string;
5
+ }
6
+ export interface ResolvedCmsBlock {
7
+ key: string;
8
+ index: number;
9
+ type: string;
10
+ component: Component;
11
+ props: Record<string, unknown>;
12
+ }
13
+ export declare const isHiddenCmsBlock: (item: CmsBlockItem) => boolean;
14
+ export declare function cmsBlockTypeName(item: CmsBlockItem, entry?: string, field?: string): string | undefined;
15
+ export declare function cmsBlockComponent(item: CmsBlockItem, entry?: string, field?: string): Component | undefined;
16
+ export declare function cmsBlockProps(item: CmsBlockItem): Record<string, unknown>;
17
+ export declare function resolveCmsBlocks(blocks: readonly CmsBlockItem[] | null | undefined, options?: {
18
+ entry?: string;
19
+ field?: string;
20
+ onMissing?: (item: CmsBlockItem) => void;
21
+ }): ResolvedCmsBlock[];
@@ -0,0 +1,37 @@
1
+ import { cmsBlockComponents } from "#cms-blocks";
2
+ import { CONTENT_BODY_FIELD, blockTypeName, isHiddenBlock } from "../../shared/index.js";
3
+ const OMITTED_KEYS = /* @__PURE__ */ new Set(["type", "__typename"]);
4
+ export const isHiddenCmsBlock = isHiddenBlock;
5
+ export function cmsBlockTypeName(item, entry, field = CONTENT_BODY_FIELD) {
6
+ return entry ? blockTypeName(entry, field, item.type) : item.__typename;
7
+ }
8
+ export function cmsBlockComponent(item, entry, field) {
9
+ const name = cmsBlockTypeName(item, entry, field);
10
+ return name && Object.hasOwn(cmsBlockComponents, name) ? cmsBlockComponents[name] : void 0;
11
+ }
12
+ export function cmsBlockProps(item) {
13
+ return Object.fromEntries(
14
+ Object.entries(item).filter(
15
+ ([key]) => !OMITTED_KEYS.has(key)
16
+ )
17
+ );
18
+ }
19
+ export function resolveCmsBlocks(blocks, options = {}) {
20
+ const resolved = [];
21
+ (blocks ?? []).forEach((item, index) => {
22
+ if (!item || typeof item !== "object" || isHiddenCmsBlock(item)) return;
23
+ const component = cmsBlockComponent(item, options.entry, options.field);
24
+ if (!component) {
25
+ options.onMissing?.(item);
26
+ return;
27
+ }
28
+ resolved.push({
29
+ key: `${index}:${item.type}`,
30
+ index,
31
+ type: item.type,
32
+ component,
33
+ props: cmsBlockProps(item)
34
+ });
35
+ });
36
+ return resolved;
37
+ }
@@ -0,0 +1,17 @@
1
+ import type { FieldConfig } from '#nuxt-cms';
2
+ import type { CmsBlockValue } from '../../../shared/preview.js';
3
+ type __VLS_Props = {
4
+ items: CmsBlockValue[];
5
+ field: FieldConfig;
6
+ locale: string;
7
+ selected: number | null;
8
+ };
9
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ select: (index: number) => any;
11
+ move: (from: number, to: number) => any;
12
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
13
+ onSelect?: ((index: number) => any) | undefined;
14
+ onMove?: ((from: number, to: number) => any) | undefined;
15
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ declare const _default: typeof __VLS_export;
17
+ export default _default;
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <ol class="cms-outline" aria-label="Blocks">
3
+ <li
4
+ v-for="(item, index) in items"
5
+ :key="uidFor(item)"
6
+ class="cms-outline-row"
7
+ :class="{
8
+ 'is-selected': index === selected,
9
+ 'is-hidden': isHiddenBlock(item),
10
+ 'is-dragging': dragIndex === index,
11
+ 'is-drop-target': dropIndex === index && dragIndex !== index
12
+ }"
13
+ draggable="true"
14
+ @dragstart="onDragStart(index, $event)"
15
+ @dragover.prevent="dropIndex = index"
16
+ @dragleave="dropIndex === index && (dropIndex = null)"
17
+ @drop.prevent="onDrop(index)"
18
+ @dragend="onDragEnd"
19
+ >
20
+ <span class="cms-outline-grip" aria-hidden="true">
21
+ <CmsIcon name="bars-2" class="size-3.5" />
22
+ </span>
23
+ <button
24
+ type="button"
25
+ class="cms-outline-main"
26
+ :aria-current="index === selected ? 'true' : void 0"
27
+ :aria-label="`${labelOf(item)}, ${index + 1} of ${items.length}${isHiddenBlock(item) ? ', hidden' : ''}. Alt and the arrow keys move it`"
28
+ aria-keyshortcuts="Alt+ArrowUp Alt+ArrowDown"
29
+ @click="emit('select', index)"
30
+ @keydown.alt.up.prevent="index > 0 && emit('move', index, index - 1)"
31
+ @keydown.alt.down.prevent="index < items.length - 1 && emit('move', index, index + 1)"
32
+ >
33
+ <CmsIcon :name="blockOf(item)?.icon ?? 'rectangle-stack'" class="size-4 shrink-0" />
34
+ <span class="cms-outline-text">
35
+ <span class="cms-outline-label">{{ labelOf(item) }}</span>
36
+ <span v-if="summaryOf(item)" class="cms-outline-summary">
37
+ {{ summaryOf(item) }}
38
+ </span>
39
+ </span>
40
+ <CmsIcon
41
+ v-if="isHiddenBlock(item)"
42
+ name="eye-slash"
43
+ class="cms-outline-hidden size-4 shrink-0"
44
+ />
45
+ </button>
46
+ </li>
47
+ </ol>
48
+ </template>
49
+
50
+ <script setup>
51
+ import { isHiddenBlock } from "#nuxt-cms";
52
+ import { ref } from "#imports";
53
+ import { blockSummary } from "../../../shared/preview";
54
+ import { useCmsRuntime } from "../../composables/cms-runtime";
55
+ const props = defineProps({
56
+ items: { type: Array, required: true },
57
+ field: { type: Object, required: true },
58
+ locale: { type: String, required: true },
59
+ selected: { type: [Number, null], required: true }
60
+ });
61
+ const emit = defineEmits(["select", "move"]);
62
+ const { i18n } = useCmsRuntime();
63
+ let uidCounter = 0;
64
+ const uids = /* @__PURE__ */ new WeakMap();
65
+ function uidFor(item) {
66
+ let uid = uids.get(item);
67
+ if (uid === void 0) {
68
+ uid = ++uidCounter;
69
+ uids.set(item, uid);
70
+ }
71
+ return uid;
72
+ }
73
+ function blockOf(item) {
74
+ return props.field.blocks?.[String(item.type)];
75
+ }
76
+ function labelOf(item) {
77
+ return blockOf(item)?.label ?? String(item.type);
78
+ }
79
+ function summaryOf(item) {
80
+ return blockSummary(blockOf(item), item, props.locale, i18n.defaultLocale, 60);
81
+ }
82
+ const dragIndex = ref(null);
83
+ const dropIndex = ref(null);
84
+ function onDragStart(index, event) {
85
+ dragIndex.value = index;
86
+ if (event.dataTransfer) {
87
+ event.dataTransfer.effectAllowed = "move";
88
+ event.dataTransfer.setData("text/plain", String(index));
89
+ }
90
+ }
91
+ function onDrop(index) {
92
+ if (dragIndex.value !== null && dragIndex.value !== index) emit("move", dragIndex.value, index);
93
+ onDragEnd();
94
+ }
95
+ function onDragEnd() {
96
+ dragIndex.value = null;
97
+ dropIndex.value = null;
98
+ }
99
+ </script>
@@ -0,0 +1,17 @@
1
+ import type { FieldConfig } from '#nuxt-cms';
2
+ import type { CmsBlockValue } from '../../../shared/preview.js';
3
+ type __VLS_Props = {
4
+ items: CmsBlockValue[];
5
+ field: FieldConfig;
6
+ locale: string;
7
+ selected: number | null;
8
+ };
9
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ select: (index: number) => any;
11
+ move: (from: number, to: number) => any;
12
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
13
+ onSelect?: ((index: number) => any) | undefined;
14
+ onMove?: ((from: number, to: number) => any) | undefined;
15
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ declare const _default: typeof __VLS_export;
17
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import type { BlockConfig } from '#nuxt-cms';
2
+ type __VLS_Props = {
3
+ blocks: Record<string, BlockConfig>;
4
+ label?: string;
5
+ };
6
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
7
+ pick: (type: string) => any;
8
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
9
+ onPick?: ((type: string) => any) | undefined;
10
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
11
+ declare const _default: typeof __VLS_export;
12
+ export default _default;
@@ -0,0 +1,29 @@
1
+ <template>
2
+ <div class="cms-palette" role="group" :aria-label="label">
3
+ <button
4
+ v-for="(block, type) in blocks"
5
+ :key="type"
6
+ type="button"
7
+ class="cms-palette-item"
8
+ @click="emit('pick', String(type))"
9
+ >
10
+ <span class="cms-palette-icon">
11
+ <CmsIcon :name="block.icon ?? 'rectangle-stack'" class="size-4" />
12
+ </span>
13
+ <span class="cms-palette-text">
14
+ <span class="cms-palette-label">{{ block.label }}</span>
15
+ <span v-if="block.description" class="cms-palette-description">
16
+ {{ block.description }}
17
+ </span>
18
+ </span>
19
+ </button>
20
+ </div>
21
+ </template>
22
+
23
+ <script setup>
24
+ defineProps({
25
+ blocks: { type: Object, required: true },
26
+ label: { type: String, required: false, default: "Available blocks" }
27
+ });
28
+ const emit = defineEmits(["pick"]);
29
+ </script>
@@ -0,0 +1,12 @@
1
+ import type { BlockConfig } from '#nuxt-cms';
2
+ type __VLS_Props = {
3
+ blocks: Record<string, BlockConfig>;
4
+ label?: string;
5
+ };
6
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
7
+ pick: (type: string) => any;
8
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
9
+ onPick?: ((type: string) => any) | undefined;
10
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
11
+ declare const _default: typeof __VLS_export;
12
+ export default _default;
@@ -2,6 +2,8 @@ import type { FieldConfig } from '#nuxt-cms';
2
2
  type __VLS_Props = {
3
3
  field: FieldConfig;
4
4
  locale?: string;
5
+ entryName?: string;
6
+ fieldKey?: string;
5
7
  };
6
8
  type __VLS_ModelProps = {
7
9
  modelValue: Record<string, unknown>[] | null;
@@ -112,6 +112,45 @@
112
112
  </button>
113
113
  </div>
114
114
 
115
+ <CmsButton
116
+ v-if="visual"
117
+ class="self-start"
118
+ label="Open visual editor"
119
+ icon="window"
120
+ size="sm"
121
+ variant="soft"
122
+ color="neutral"
123
+ @click="visualOpen = true"
124
+ />
125
+
126
+ <Teleport to="body">
127
+ <div v-if="visual && visualOpen" class="cms-scope cms-editor-overlay" :data-theme="theme">
128
+ <CmsVisualEditor
129
+ :state="{ [fieldKey]: model }"
130
+ :entry-name="entryName"
131
+ :field-key="fieldKey"
132
+ :fields="{ [fieldKey]: field }"
133
+ :title="field.label"
134
+ @update:state="onVisualUpdate"
135
+ >
136
+ <template #start>
137
+ <CmsButton
138
+ icon="arrow-left"
139
+ size="sm"
140
+ variant="ghost"
141
+ color="neutral"
142
+ aria-label="Back to the form"
143
+ title="Back to the form"
144
+ @click="visualOpen = false"
145
+ />
146
+ </template>
147
+ <template #actions>
148
+ <CmsButton label="Done" size="sm" @click="visualOpen = false" />
149
+ </template>
150
+ </CmsVisualEditor>
151
+ </div>
152
+ </Teleport>
153
+
115
154
  <CmsModal v-if="mediaOnly" v-model:open="pickerOpen" title="Media" size="lg">
116
155
  <template #body>
117
156
  <CmsMediaGallery
@@ -167,6 +206,14 @@
167
206
  color="neutral"
168
207
  @click="duplicateCurrent"
169
208
  />
209
+ <CmsButton
210
+ v-if="editorItem && !declaresHiddenField(blockOf(editorItem))"
211
+ :label="isHidden(editorItem) ? 'Show' : 'Hide'"
212
+ :icon="isHidden(editorItem) ? 'eye' : 'eye-slash'"
213
+ variant="soft"
214
+ color="neutral"
215
+ @click="toggleHiddenCurrent"
216
+ />
170
217
  </div>
171
218
  <div class="cms-actions">
172
219
  <CmsButton
@@ -187,6 +234,8 @@
187
234
 
188
235
  <script setup>
189
236
  import {
237
+ declaresHiddenField,
238
+ isHiddenBlock,
190
239
  isTranslatableMediaField,
191
240
  mediaFilename,
192
241
  mediaIconFor,
@@ -194,13 +243,17 @@ import {
194
243
  mediaTypeForKey,
195
244
  pickTranslatedMedia
196
245
  } from "#nuxt-cms";
197
- import { computed, ref, useState } from "#imports";
246
+ import { computed, ref, useState, watch } from "#imports";
198
247
  import { useCmsMediaKeys } from "../../composables/cms-media-keys";
248
+ import { useCmsOverlay } from "../../composables/cms-overlay";
199
249
  import { useCmsRuntime } from "../../composables/cms-runtime";
250
+ import { useCmsTheme } from "../../composables/cms-theme";
200
251
  import { fieldIcon } from "../../utils/ui";
201
252
  const props = defineProps({
202
253
  field: { type: Object, required: true },
203
- locale: { type: String, required: false }
254
+ locale: { type: String, required: false },
255
+ entryName: { type: String, required: false },
256
+ fieldKey: { type: String, required: false }
204
257
  });
205
258
  const model = defineModel({ type: [Array, null], ...{ required: true } });
206
259
  const { mediaBaseUrl, i18n } = useCmsRuntime();
@@ -217,6 +270,23 @@ const mediaOnly = computed(() => {
217
270
  if (field.type !== "media" || isTranslatableMediaField(field)) return null;
218
271
  return { type, key, field };
219
272
  });
273
+ const visual = computed(() => {
274
+ const list = Object.values(blocks.value);
275
+ return !mediaOnly.value && !!props.entryName && !!props.fieldKey && !!list.length && list.every((block) => typeof block.component === "string" && !!block.component.trim());
276
+ });
277
+ const visualOpen = ref(false);
278
+ const theme = useCmsTheme();
279
+ const visualOverlay = useCmsOverlay(() => {
280
+ visualOpen.value = false;
281
+ });
282
+ watch(visualOpen, (open) => {
283
+ if (open) visualOverlay.activate();
284
+ else visualOverlay.deactivate();
285
+ });
286
+ function onVisualUpdate(state) {
287
+ const value = props.fieldKey ? state[props.fieldKey] : null;
288
+ model.value = Array.isArray(value) && value.length ? value : null;
289
+ }
220
290
  const addMediaLabel = computed(() => {
221
291
  const types = [mediaOnly.value?.field.mediaType ?? []].flat();
222
292
  return types.length === 1 && types[0] === "image" ? "Add photo" : "Add media";
@@ -293,7 +363,7 @@ function summaryOf(item) {
293
363
  return "";
294
364
  }
295
365
  function isHidden(item) {
296
- return blockOf(item)?.fields.hidden?.type === "boolean" && item.hidden === true;
366
+ return isHiddenBlock(item);
297
367
  }
298
368
  function updateField(index, key, value) {
299
369
  const current = items.value[index];
@@ -406,6 +476,16 @@ function moveCurrent(delta) {
406
476
  move(editorIndex.value, delta);
407
477
  editorIndex.value = next;
408
478
  }
479
+ function toggleHiddenCurrent() {
480
+ if (editorIndex.value === null || !editorItem.value) return;
481
+ const index = editorIndex.value;
482
+ const current = items.value[index];
483
+ if (!current) return;
484
+ const { hidden: _hidden, ...rest } = current;
485
+ const updated = isHidden(current) ? rest : { ...rest, hidden: true };
486
+ uids.set(updated, uidFor(current));
487
+ model.value = items.value.map((item, i) => i === index ? updated : item);
488
+ }
409
489
  function duplicateCurrent() {
410
490
  if (editorIndex.value === null) return;
411
491
  duplicate(editorIndex.value);
@@ -2,6 +2,8 @@ import type { FieldConfig } from '#nuxt-cms';
2
2
  type __VLS_Props = {
3
3
  field: FieldConfig;
4
4
  locale?: string;
5
+ entryName?: string;
6
+ fieldKey?: string;
5
7
  };
6
8
  type __VLS_ModelProps = {
7
9
  modelValue: Record<string, unknown>[] | null;
@@ -1,4 +1,5 @@
1
1
  import type { CmsEntry, CmsTab } from '#nuxt-cms';
2
+ import type { CmsSearchFocus } from '../../../shared/search.js';
2
3
  type __VLS_Props = {
3
4
  fields: CmsEntry['fields'];
4
5
  tabs?: CmsTab[];
@@ -7,6 +8,7 @@ type __VLS_Props = {
7
8
  loading?: boolean;
8
9
  formId?: string;
9
10
  footer?: boolean;
11
+ focus?: CmsSearchFocus | null;
10
12
  };
11
13
  type __VLS_ModelProps = {
12
14
  modelValue: Record<string, unknown>;
@@ -1,5 +1,12 @@
1
1
  <template>
2
- <CmsForm :id="formId" :state="state" :schema="schema" @submit="emit('submit')" @error="onError">
2
+ <CmsForm
3
+ :id="formId"
4
+ ref="form"
5
+ :state="state"
6
+ :schema="schema"
7
+ @submit="emit('submit')"
8
+ @error="onError"
9
+ >
3
10
  <div v-if="tabList.length > 1" class="cms-tabs" role="tablist">
4
11
  <button
5
12
  v-for="tab in tabList"
@@ -53,6 +60,8 @@
53
60
  :icon="fieldIcon(fields[key])"
54
61
  :name="key"
55
62
  :required="fields[key].required"
63
+ :data-cms-field="key"
64
+ :class="{ 'is-search-focus': highlighted === key }"
56
65
  >
57
66
  <template
58
67
  v-if="hasLocaleSwitch(fields[key]) && i18n.locales.length > 1"
@@ -70,9 +79,9 @@
70
79
  v-model="state[key]"
71
80
  :field="fields[key]"
72
81
  :locale="localeFor(key)"
73
- :slug-source="
74
- fields[key].from ? state[fields[key].from] : void 0
75
- "
82
+ :slug-source="slugSource(fields[key])"
83
+ :entry-name="entryName"
84
+ :field-key="key"
76
85
  />
77
86
  </CmsFormField>
78
87
  </div>
@@ -99,10 +108,12 @@ import {
99
108
  hasTranslatableBlockFields,
100
109
  isFieldVisible,
101
110
  isTranslatableField,
102
- resolveFormLayout
111
+ resolveFormLayout,
112
+ slugSourceValue
103
113
  } from "#nuxt-cms";
104
- import { computed, nextTick, ref, watch } from "#imports";
114
+ import { computed, nextTick, onMounted, ref, watch } from "#imports";
105
115
  import { buildEntrySchema } from "../../../shared/validation";
116
+ import { useCmsFieldFocus } from "../../composables/cms-field-focus";
106
117
  import { useCmsRuntime } from "../../composables/cms-runtime";
107
118
  import { entryFormLayout, useCmsSettingsState } from "../../composables/cms-settings";
108
119
  import { fieldIcon } from "../../utils/ui";
@@ -113,7 +124,8 @@ const props = defineProps({
113
124
  drafts: { type: Boolean, required: false },
114
125
  loading: { type: Boolean, required: false },
115
126
  formId: { type: String, required: false },
116
- footer: { type: Boolean, required: false, default: true }
127
+ footer: { type: Boolean, required: false, default: true },
128
+ focus: { type: [Object, null], required: false }
117
129
  });
118
130
  const state = defineModel({ type: Object, ...{ required: true } });
119
131
  const emit = defineEmits(["submit", "error"]);
@@ -168,6 +180,10 @@ function onError() {
168
180
  if (key) void nextTick(() => revealField(key));
169
181
  emit("error");
170
182
  }
183
+ function slugSource(field) {
184
+ if (!field.from) return void 0;
185
+ return slugSourceValue(props.fields[field.from], state.value[field.from], i18n.defaultLocale);
186
+ }
171
187
  function hasLocaleSwitch(field) {
172
188
  return isTranslatableField(field) || hasTranslatableBlockFields(field);
173
189
  }
@@ -189,4 +205,24 @@ const published = computed({
189
205
  function togglePublished() {
190
206
  published.value = !published.value;
191
207
  }
208
+ const form = ref(null);
209
+ const { highlighted, reveal } = useCmsFieldFocus();
210
+ function applyFocus(focus) {
211
+ if (focus.tab && tabList.value.some((tab2) => tab2.id === focus.tab)) activeTab.value = focus.tab;
212
+ const key = focus.field;
213
+ const field = key ? props.fields[key] : void 0;
214
+ if (!key || !field) return;
215
+ const tab = tabList.value.length ? fieldTab(field, tabList.value) : void 0;
216
+ if (tab) activeTab.value = tab;
217
+ if (focus.locale && hasLocaleSwitch(field) && i18n.locales.includes(focus.locale)) {
218
+ setLocale(key, focus.locale);
219
+ }
220
+ void nextTick(() => {
221
+ revealField(key);
222
+ reveal(() => form.value?.$el, key);
223
+ });
224
+ }
225
+ onMounted(() => {
226
+ if (props.focus) applyFocus(props.focus);
227
+ });
192
228
  </script>
@@ -1,4 +1,5 @@
1
1
  import type { CmsEntry, CmsTab } from '#nuxt-cms';
2
+ import type { CmsSearchFocus } from '../../../shared/search.js';
2
3
  type __VLS_Props = {
3
4
  fields: CmsEntry['fields'];
4
5
  tabs?: CmsTab[];
@@ -7,6 +8,7 @@ type __VLS_Props = {
7
8
  loading?: boolean;
8
9
  formId?: string;
9
10
  footer?: boolean;
11
+ focus?: CmsSearchFocus | null;
10
12
  };
11
13
  type __VLS_ModelProps = {
12
14
  modelValue: Record<string, unknown>;
@@ -3,6 +3,8 @@ type __VLS_Props = {
3
3
  field: FieldConfig;
4
4
  slugSource?: string | null;
5
5
  locale?: string;
6
+ entryName?: string;
7
+ fieldKey?: string;
6
8
  };
7
9
  type __VLS_ModelProps = {
8
10
  modelValue: unknown;
@@ -10,6 +10,8 @@
10
10
  v-model="blocksValue"
11
11
  :field="field"
12
12
  :locale="locale"
13
+ :entry-name="entryName"
14
+ :field-key="fieldKey"
13
15
  />
14
16
  <CmsRichTextField v-else-if="field.type === 'richtext'" v-model="strOrNull" />
15
17
  <CmsTextarea
@@ -50,6 +52,7 @@
50
52
  :placeholder="field.placeholder"
51
53
  />
52
54
  <CmsInput v-else-if="field.type === 'date'" v-model="str" type="date" />
55
+ <CmsInput v-else-if="field.type === 'datetime'" v-model="localDateTime" type="datetime-local" />
53
56
  <CmsRelationField
54
57
  v-else-if="field.type === 'relation'"
55
58
  v-model="relationValue"
@@ -66,7 +69,9 @@ import { computed } from "#imports";
66
69
  const props = defineProps({
67
70
  field: { type: Object, required: true },
68
71
  slugSource: { type: [String, null], required: false },
69
- locale: { type: String, required: false }
72
+ locale: { type: String, required: false },
73
+ entryName: { type: String, required: false },
74
+ fieldKey: { type: String, required: false }
70
75
  });
71
76
  const model = defineModel({ type: null, ...{ required: true } });
72
77
  const selectItems = computed(() => [
@@ -97,6 +102,18 @@ const translatable = proxy((v) => v);
97
102
  const blocksValue = proxy(
98
103
  (v) => v
99
104
  );
105
+ function toLocalDateTime(value) {
106
+ if (typeof value !== "string" || !value) return "";
107
+ const date = new Date(value);
108
+ if (Number.isNaN(date.getTime())) return "";
109
+ return new Date(date.getTime() - date.getTimezoneOffset() * 6e4).toISOString().slice(0, 16);
110
+ }
111
+ function fromLocalDateTime(value) {
112
+ if (!value) return null;
113
+ const date = new Date(value);
114
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
115
+ }
116
+ const localDateTime = proxy(toLocalDateTime, fromLocalDateTime);
100
117
  const relationValue = proxy((v) => v);
101
118
  const jsonValue = proxy((v) => v);
102
119
  </script>