@xleddyl/nuxt-cms 0.2.2 → 0.2.3

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 (69) hide show
  1. package/README.md +3 -1
  2. package/dist/module.d.mts +4 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +319 -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.vue +9 -4
  20. package/dist/runtime/app/components/cms/FieldInput.d.vue.ts +2 -0
  21. package/dist/runtime/app/components/cms/FieldInput.vue +18 -1
  22. package/dist/runtime/app/components/cms/FieldInput.vue.d.ts +2 -0
  23. package/dist/runtime/app/components/cms/SettingsEntry.vue +3 -2
  24. package/dist/runtime/app/components/cms/SettingsModal.vue +2 -1
  25. package/dist/runtime/app/components/cms/StatusBadge.d.vue.ts +2 -0
  26. package/dist/runtime/app/components/cms/StatusBadge.vue +9 -3
  27. package/dist/runtime/app/components/cms/StatusBadge.vue.d.ts +2 -0
  28. package/dist/runtime/app/components/cms/VisualEditor.d.vue.ts +37 -0
  29. package/dist/runtime/app/components/cms/VisualEditor.vue +533 -0
  30. package/dist/runtime/app/components/cms/VisualEditor.vue.d.ts +37 -0
  31. package/dist/runtime/app/composables/cms-entry-disabled.d.ts +7 -1
  32. package/dist/runtime/app/composables/cms-entry-disabled.js +15 -0
  33. package/dist/runtime/app/composables/cms-entry.d.ts +7 -1
  34. package/dist/runtime/app/composables/cms-entry.js +51 -1
  35. package/dist/runtime/app/composables/cms-runtime.d.ts +1 -0
  36. package/dist/runtime/app/composables/cms-settings.js +8 -2
  37. package/dist/runtime/app/layouts/cms-admin.vue +5 -0
  38. package/dist/runtime/app/layouts/cms-editor.d.vue.ts +13 -0
  39. package/dist/runtime/app/layouts/cms-editor.vue +14 -0
  40. package/dist/runtime/app/layouts/cms-editor.vue.d.ts +13 -0
  41. package/dist/runtime/app/pages/admin-collection.vue +33 -4
  42. package/dist/runtime/app/pages/admin-content.d.vue.ts +3 -0
  43. package/dist/runtime/app/pages/admin-content.vue +284 -0
  44. package/dist/runtime/app/pages/admin-content.vue.d.ts +3 -0
  45. package/dist/runtime/app/pages/admin-preview.d.vue.ts +3 -0
  46. package/dist/runtime/app/pages/admin-preview.vue +132 -0
  47. package/dist/runtime/app/pages/admin-preview.vue.d.ts +3 -0
  48. package/dist/runtime/app/preview/PreviewOverlay.d.vue.ts +15 -0
  49. package/dist/runtime/app/preview/PreviewOverlay.vue +362 -0
  50. package/dist/runtime/app/preview/PreviewOverlay.vue.d.ts +15 -0
  51. package/dist/runtime/app/utils/ui.js +1 -0
  52. package/dist/runtime/assets/main.css +1 -1
  53. package/dist/runtime/server/api/collection.get.js +3 -2
  54. package/dist/runtime/server/api/collection.post.js +4 -1
  55. package/dist/runtime/server/api/item.delete.js +2 -1
  56. package/dist/runtime/server/api/item.put.js +2 -1
  57. package/dist/runtime/server/utils/graphql.js +23 -8
  58. package/dist/runtime/server/utils/registry.d.ts +1 -0
  59. package/dist/runtime/server/utils/registry.js +6 -0
  60. package/dist/runtime/server/utils/visibility.d.ts +4 -0
  61. package/dist/runtime/server/utils/visibility.js +12 -0
  62. package/dist/runtime/shared/graphql-sdl.js +4 -1
  63. package/dist/runtime/shared/index.d.ts +59 -5
  64. package/dist/runtime/shared/index.js +133 -0
  65. package/dist/runtime/shared/preview.d.ts +85 -0
  66. package/dist/runtime/shared/preview.js +219 -0
  67. package/dist/runtime/shared/validation.d.ts +2 -0
  68. package/dist/runtime/shared/validation.js +15 -2
  69. package/package.json +1 -1
@@ -0,0 +1,132 @@
1
+ <template>
2
+ <div class="cms-preview" :data-cms-preview="state ? 'ready' : 'waiting'">
3
+ <template v-if="state && field">
4
+ <component :is="wrapper" v-if="wrapper && item" :item="item" :locale="state.locale">
5
+ <CmsBlocks :blocks="blocks" :entry="blockEntry" :field="state.field" annotate />
6
+ <div v-if="!visibleCount" class="cms-preview-empty">
7
+ <button
8
+ type="button"
9
+ class="cms-preview-empty-button"
10
+ data-cms-preview-keep
11
+ @click="post({ type: 'insert', index: state.blocks.length })"
12
+ >
13
+ <CmsIcon name="plus" />
14
+ <span>Add a block</span>
15
+ </button>
16
+ </div>
17
+ </component>
18
+ <template v-else>
19
+ <CmsBlocks :blocks="blocks" :entry="blockEntry" :field="state.field" annotate />
20
+ <div v-if="!visibleCount" class="cms-preview-empty">
21
+ <button
22
+ type="button"
23
+ class="cms-preview-empty-button"
24
+ data-cms-preview-keep
25
+ @click="post({ type: 'insert', index: state.blocks.length })"
26
+ >
27
+ <CmsIcon name="plus" />
28
+ <span>Add a block</span>
29
+ </button>
30
+ </div>
31
+ </template>
32
+ <PreviewOverlay
33
+ :field="field"
34
+ :total="state.blocks.length"
35
+ :selected="state.selected"
36
+ :version="version"
37
+ @message="post"
38
+ />
39
+ </template>
40
+ <p v-else-if="standalone" class="cms-preview-notice">
41
+ This page shows the live preview of the visual editor. Open an item in the admin to use it.
42
+ </p>
43
+ </div>
44
+ </template>
45
+
46
+ <script setup>
47
+ import { entryFieldsFor, isHiddenBlock } from "#nuxt-cms";
48
+ import {
49
+ computed,
50
+ definePageMeta,
51
+ onBeforeUnmount,
52
+ onMounted,
53
+ ref,
54
+ shallowRef,
55
+ useHead
56
+ } from "#imports";
57
+ import cmsConfig from "#cms-config";
58
+ import { cmsPreviewComponents } from "#cms-blocks";
59
+ import {
60
+ isTrustedPreviewEvent,
61
+ parseHostMessage,
62
+ previewBlocks,
63
+ previewEnvelope,
64
+ previewItem
65
+ } from "../../shared/preview";
66
+ import { useCmsRuntime } from "../composables/cms-runtime";
67
+ import PreviewOverlay from "../preview/PreviewOverlay.vue";
68
+ definePageMeta({ middleware: "cms-auth" });
69
+ const { i18n } = useCmsRuntime();
70
+ const state = shallowRef(null);
71
+ const version = ref(0);
72
+ const standalone = ref(false);
73
+ useHead({
74
+ title: "Preview",
75
+ meta: [{ name: "robots", content: "noindex, nofollow" }],
76
+ htmlAttrs: { lang: computed(() => state.value?.locale) }
77
+ });
78
+ const entry = computed(
79
+ () => state.value ? cmsConfig[state.value.entry] : void 0
80
+ );
81
+ const blockEntry = computed(() => state.value?.entry);
82
+ const fields = computed(() => entry.value ? entryFieldsFor(entry.value) : {});
83
+ const field = computed(() => {
84
+ const candidate = state.value ? fields.value[state.value.field] : void 0;
85
+ return candidate?.type === "blocks" ? candidate : void 0;
86
+ });
87
+ const blocks = computed(
88
+ () => state.value && field.value ? previewBlocks(
89
+ field.value,
90
+ state.value.blocks,
91
+ state.value.locale,
92
+ i18n.defaultLocale,
93
+ state.value.media
94
+ ) : []
95
+ );
96
+ const visibleCount = computed(() => blocks.value.filter((block) => !isHiddenBlock(block)).length);
97
+ const item = computed(
98
+ () => state.value?.item && entry.value?.kind === "content" ? previewItem(
99
+ fields.value,
100
+ state.value.item,
101
+ state.value.locale,
102
+ i18n.defaultLocale,
103
+ state.value.media
104
+ ) : null
105
+ );
106
+ const wrapper = computed(
107
+ () => state.value && entry.value?.kind === "content" ? cmsPreviewComponents[state.value.entry] : void 0
108
+ );
109
+ function post(message) {
110
+ if (window.parent === window) return;
111
+ window.parent.postMessage(previewEnvelope(message), window.location.origin);
112
+ }
113
+ function onMessage(event) {
114
+ if (!isTrustedPreviewEvent(event, window.location.origin, window.parent)) return;
115
+ const message = parseHostMessage(event.data);
116
+ if (!message) return;
117
+ state.value = message.state;
118
+ version.value++;
119
+ }
120
+ onMounted(() => {
121
+ standalone.value = window.parent === window;
122
+ window.addEventListener("message", onMessage);
123
+ post({ type: "ready" });
124
+ });
125
+ onBeforeUnmount(() => {
126
+ window.removeEventListener("message", onMessage);
127
+ });
128
+ </script>
129
+
130
+ <style>
131
+ .cms-preview-overlay{--cms-preview-accent:#5b5bd6;--cms-preview-ink:#111114;--cms-preview-surface:#fff;--cms-preview-line:rgba(17,17,20,.12);--cms-preview-danger:#d9383a;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif;font-size:12px;inset:0;line-height:1;pointer-events:none;position:fixed;z-index:2147483000}.cms-preview-outline{border:1px dashed color-mix(in oklab,var(--cms-preview-accent) 70%,transparent);border-radius:2px;box-sizing:border-box;position:absolute}.cms-preview-outline.is-selected{border:2px solid var(--cms-preview-accent);box-shadow:0 0 0 4px color-mix(in oklab,var(--cms-preview-accent) 14%,transparent)}.cms-preview-label{background:var(--cms-preview-accent);border-radius:2px 0 6px 0;color:#fff;font-weight:600;left:-1px;letter-spacing:.01em;padding:4px 7px;position:absolute;top:-1px;white-space:nowrap}.cms-preview-toolbar{align-items:center;background:var(--cms-preview-surface);border-radius:8px;box-shadow:0 0 0 1px var(--cms-preview-line),0 8px 24px -8px rgba(17,17,20,.28);display:flex;gap:2px;padding:3px;pointer-events:auto;position:absolute;transform:translateX(-100%)}.cms-preview-toolbar:not(.is-selected){opacity:.92}.cms-preview-empty-button,.cms-preview-insert,.cms-preview-tool{all:unset;align-items:center;box-sizing:border-box;cursor:pointer;display:inline-flex;justify-content:center}.cms-preview-tool{border-radius:6px;color:var(--cms-preview-ink);height:28px;width:28px}.cms-preview-tool:hover:not(:disabled){background:rgba(17,17,20,.06)}.cms-preview-tool:disabled{cursor:default;opacity:.35}.cms-preview-tool.is-danger{color:var(--cms-preview-danger)}.cms-preview-handle{cursor:grab;touch-action:none}.cms-preview .cms-preview-empty .cms-preview-empty-button:focus-visible,.cms-preview-overlay .cms-preview-insert:focus-visible,.cms-preview-overlay .cms-preview-tool:focus-visible{outline:2px solid var(--cms-preview-accent);outline-offset:2px}.cms-preview-separator{background:var(--cms-preview-line);height:18px;margin:0 2px;width:1px}.cms-preview-overlay svg{flex-shrink:0;height:16px;width:16px}.cms-preview-insert{background:var(--cms-preview-accent);border-radius:999px;box-shadow:0 2px 8px -2px rgba(17,17,20,.4);color:#fff;height:26px;margin:-13px 0 0 -13px;opacity:0;pointer-events:auto;position:absolute;transform:scale(.85);transition:opacity .14s ease,transform .14s ease;width:26px}.cms-preview-insert.is-near,.cms-preview-insert:focus-visible,.cms-preview-insert:hover{opacity:1;transform:scale(1)}.cms-preview-overlay.is-dragging .cms-preview-insert,.cms-preview-overlay.is-dragging .cms-preview-toolbar:not(.is-selected){visibility:hidden}.cms-preview-dropline{background:var(--cms-preview-accent);border-radius:3px;box-shadow:0 0 0 3px color-mix(in oklab,var(--cms-preview-accent) 20%,transparent);height:3px;margin-top:-1.5px;position:absolute}.cms-preview-grabbing,.cms-preview-grabbing *{cursor:grabbing!important;-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.cms-preview-empty{display:flex;justify-content:center;padding:48px 16px}.cms-preview-empty-button{border:1px dashed #5b5bd6;border-radius:10px;color:#5b5bd6;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif;font-size:14px;font-weight:600;gap:8px;padding:12px 18px}.cms-preview-empty-button svg{height:18px;width:18px}.cms-preview-notice{color:#6b6b75;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif;margin:64px auto;max-width:32rem;padding:0 16px;text-align:center}
132
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,15 @@
1
+ import type { FieldConfig } from '../../shared/index.js';
2
+ import type { CmsPreviewFrameMessage } from '../../shared/preview.js';
3
+ type __VLS_Props = {
4
+ field: FieldConfig;
5
+ total: number;
6
+ selected: number | null;
7
+ version: number;
8
+ };
9
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ message: (message: CmsPreviewFrameMessage) => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ onMessage?: ((message: CmsPreviewFrameMessage) => any) | undefined;
13
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ declare const _default: typeof __VLS_export;
15
+ export default _default;
@@ -0,0 +1,362 @@
1
+ <template>
2
+ <div ref="root" class="cms-preview-overlay" :class="{ 'is-dragging': drag?.moved }">
3
+ <template v-for="box in boxes" :key="box.index">
4
+ <div
5
+ v-if="box.index === hovered || box.index === selected"
6
+ class="cms-preview-outline"
7
+ :class="{ 'is-selected': box.index === selected }"
8
+ :style="outlineStyle(box)"
9
+ >
10
+ <span class="cms-preview-label">{{ labelOf(box) }}</span>
11
+ </div>
12
+ <div
13
+ v-if="box.index === hovered || box.index === selected"
14
+ class="cms-preview-toolbar"
15
+ :class="{ 'is-selected': box.index === selected }"
16
+ :style="toolbarStyle(box)"
17
+ role="toolbar"
18
+ :aria-label="`${labelOf(box)} block`"
19
+ >
20
+ <button
21
+ type="button"
22
+ class="cms-preview-tool cms-preview-handle"
23
+ :aria-label="`Move ${labelOf(box)}: drag, or use the up and down arrow keys`"
24
+ title="Drag to reorder"
25
+ @pointerdown="startDrag($event, box.index)"
26
+ @keydown="onHandleKey($event, box.index)"
27
+ @click="onHandleClick(box.index)"
28
+ >
29
+ <CmsIcon name="bars-2" />
30
+ </button>
31
+ <button
32
+ type="button"
33
+ class="cms-preview-tool"
34
+ aria-label="Move up"
35
+ title="Move up"
36
+ :disabled="box.index === 0"
37
+ @click="emitMove(box.index, box.index - 1)"
38
+ >
39
+ <CmsIcon name="arrow-up" />
40
+ </button>
41
+ <button
42
+ type="button"
43
+ class="cms-preview-tool"
44
+ aria-label="Move down"
45
+ title="Move down"
46
+ :disabled="box.index >= total - 1"
47
+ @click="emitMove(box.index, box.index + 1)"
48
+ >
49
+ <CmsIcon name="arrow-down" />
50
+ </button>
51
+ <span class="cms-preview-separator" />
52
+ <button
53
+ type="button"
54
+ class="cms-preview-tool"
55
+ aria-label="Duplicate"
56
+ title="Duplicate"
57
+ @click="emit('message', { type: 'duplicate', index: box.index })"
58
+ >
59
+ <CmsIcon name="document-duplicate" />
60
+ </button>
61
+ <button
62
+ v-if="hideable(box)"
63
+ type="button"
64
+ class="cms-preview-tool"
65
+ aria-label="Hide"
66
+ title="Hide"
67
+ @click="emit('message', { type: 'toggle-hidden', index: box.index })"
68
+ >
69
+ <CmsIcon name="eye-slash" />
70
+ </button>
71
+ <button
72
+ type="button"
73
+ class="cms-preview-tool is-danger"
74
+ aria-label="Delete"
75
+ title="Delete"
76
+ @click="emit('message', { type: 'remove', index: box.index })"
77
+ >
78
+ <CmsIcon name="trash" />
79
+ </button>
80
+ </div>
81
+ </template>
82
+
83
+ <button
84
+ v-for="slot in insertSlots"
85
+ :key="`insert-${slot.index}-${slot.edge}`"
86
+ type="button"
87
+ class="cms-preview-insert"
88
+ :class="{ 'is-near': slot.near }"
89
+ :style="{ top: `${slot.y}px`, left: `${slot.x}px` }"
90
+ :aria-label="`Insert a block at position ${slot.index + 1}`"
91
+ title="Insert a block"
92
+ @click="emit('message', { type: 'insert', index: slot.index })"
93
+ >
94
+ <CmsIcon name="plus" />
95
+ </button>
96
+
97
+ <div
98
+ v-if="drag?.moved && dropY !== null"
99
+ class="cms-preview-dropline"
100
+ :style="{ top: `${dropY}px`, left: `${dropLeft}px`, width: `${dropWidth}px` }"
101
+ />
102
+ </div>
103
+ </template>
104
+
105
+ <script setup>
106
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "#imports";
107
+ import { canHideBlock, insertionIndex, moveTargetForInsertion } from "../../shared/preview";
108
+ const props = defineProps({
109
+ field: { type: Object, required: true },
110
+ total: { type: Number, required: true },
111
+ selected: { type: [Number, null], required: true },
112
+ version: { type: Number, required: true }
113
+ });
114
+ const emit = defineEmits(["message"]);
115
+ const root = ref(null);
116
+ const boxes = ref([]);
117
+ const hovered = ref(null);
118
+ const viewportWidth = ref(0);
119
+ const viewportHeight = ref(0);
120
+ const drag = ref(null);
121
+ let refocus = false;
122
+ let suppressClick = false;
123
+ let frame = 0;
124
+ function labelOf(box) {
125
+ return props.field.blocks?.[box.type]?.label ?? box.type;
126
+ }
127
+ function hideable(box) {
128
+ return canHideBlock(props.field.blocks?.[box.type]);
129
+ }
130
+ function measure() {
131
+ frame = 0;
132
+ viewportWidth.value = window.innerWidth;
133
+ viewportHeight.value = window.innerHeight;
134
+ const found = [];
135
+ for (const element of document.querySelectorAll("[data-cms-block]")) {
136
+ const index = Number(element.dataset.cmsBlock);
137
+ if (!Number.isInteger(index)) continue;
138
+ const rect = element.getBoundingClientRect();
139
+ found.push({
140
+ index,
141
+ type: element.dataset.cmsBlockType ?? "",
142
+ top: rect.top,
143
+ left: rect.left,
144
+ width: rect.width,
145
+ height: rect.height,
146
+ bottom: rect.bottom
147
+ });
148
+ }
149
+ boxes.value = found.sort((a, b) => a.top - b.top);
150
+ }
151
+ function scheduleMeasure() {
152
+ if (!frame) frame = requestAnimationFrame(measure);
153
+ }
154
+ function outlineStyle(box) {
155
+ return {
156
+ top: `${box.top}px`,
157
+ left: `${box.left}px`,
158
+ width: `${box.width}px`,
159
+ height: `${box.height}px`
160
+ };
161
+ }
162
+ function toolbarStyle(box) {
163
+ const top = Math.min(Math.max(box.top + 8, 8), Math.max(box.bottom - 44, 8));
164
+ const right = Math.min(box.left + box.width, viewportWidth.value) - 8;
165
+ return { top: `${top}px`, left: `${right}px` };
166
+ }
167
+ const INSERT_MARGIN = 14;
168
+ const insertSlots = computed(() => {
169
+ const list = boxes.value;
170
+ const near = /* @__PURE__ */ new Set([hovered.value, props.selected]);
171
+ const maxY = viewportHeight.value - INSERT_MARGIN;
172
+ const clampY = (y) => Math.min(Math.max(y, INSERT_MARGIN), Math.max(maxY, INSERT_MARGIN));
173
+ return list.flatMap((box, position) => {
174
+ const next = list[position + 1];
175
+ const x = box.left + box.width / 2;
176
+ const after = {
177
+ index: box.index + 1,
178
+ edge: "after",
179
+ y: clampY(next ? (box.bottom + next.top) / 2 : box.bottom),
180
+ x,
181
+ near: near.has(box.index) || !!next && near.has(next.index)
182
+ };
183
+ if (position > 0) return [after];
184
+ const before = {
185
+ index: box.index,
186
+ edge: "before",
187
+ y: clampY(box.top),
188
+ x,
189
+ near: near.has(box.index)
190
+ };
191
+ return [before, after];
192
+ });
193
+ });
194
+ const dropIndex = computed(
195
+ () => drag.value ? insertionIndex(boxes.value, drag.value.y, props.total) : null
196
+ );
197
+ const dropY = computed(() => {
198
+ if (dropIndex.value === null) return null;
199
+ const list = boxes.value;
200
+ const position = list.findIndex((box) => box.index >= dropIndex.value);
201
+ if (position === -1) return list.at(-1)?.bottom ?? null;
202
+ const previous = list[position - 1];
203
+ return previous ? (previous.bottom + list[position].top) / 2 : list[position].top;
204
+ });
205
+ const dropLeft = computed(() => boxes.value[0]?.left ?? 0);
206
+ const dropWidth = computed(() => boxes.value[0]?.width ?? viewportWidth.value);
207
+ function emitMove(from, to, focus = false) {
208
+ if (to < 0 || to >= props.total || from === to) return;
209
+ emit("message", focus ? { type: "move", from, to, focus: true } : { type: "move", from, to });
210
+ }
211
+ function onHandleKey(event, index) {
212
+ if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
213
+ event.preventDefault();
214
+ refocus = true;
215
+ emitMove(index, event.key === "ArrowUp" ? index - 1 : index + 1, true);
216
+ }
217
+ function onHandleClick(index) {
218
+ if (drag.value?.moved) return;
219
+ emit("message", { type: "select", index });
220
+ }
221
+ function startDrag(event, index) {
222
+ if (event.button !== 0) return;
223
+ drag.value = { from: index, startY: event.clientY, y: event.clientY, moved: false };
224
+ window.addEventListener("pointermove", onDragMove);
225
+ window.addEventListener("pointerup", onDragEnd);
226
+ window.addEventListener("pointercancel", cancelDrag);
227
+ }
228
+ function onDragMove(event) {
229
+ const current = drag.value;
230
+ if (!current) return;
231
+ current.y = event.clientY;
232
+ if (!current.moved && Math.abs(event.clientY - current.startY) > 4) {
233
+ current.moved = true;
234
+ document.documentElement.classList.add("cms-preview-grabbing");
235
+ }
236
+ if (current.moved) {
237
+ event.preventDefault();
238
+ const edge = 48;
239
+ if (event.clientY < edge) window.scrollBy(0, -12);
240
+ else if (event.clientY > window.innerHeight - edge) window.scrollBy(0, 12);
241
+ }
242
+ }
243
+ function stopDrag() {
244
+ window.removeEventListener("pointermove", onDragMove);
245
+ window.removeEventListener("pointerup", onDragEnd);
246
+ window.removeEventListener("pointercancel", cancelDrag);
247
+ document.documentElement.classList.remove("cms-preview-grabbing");
248
+ }
249
+ function onDragEnd() {
250
+ const current = drag.value;
251
+ stopDrag();
252
+ if (current?.moved && dropIndex.value !== null) {
253
+ suppressClick = true;
254
+ emitMove(current.from, moveTargetForInsertion(current.from, dropIndex.value));
255
+ }
256
+ setTimeout(() => {
257
+ drag.value = null;
258
+ suppressClick = false;
259
+ });
260
+ }
261
+ function cancelDrag() {
262
+ stopDrag();
263
+ drag.value = null;
264
+ }
265
+ function blockIndexOf(target) {
266
+ const element = target instanceof Element ? target.closest("[data-cms-block]") : null;
267
+ const index = element ? Number(element.dataset.cmsBlock) : NaN;
268
+ return Number.isInteger(index) ? index : null;
269
+ }
270
+ function insideOverlay(target) {
271
+ return target instanceof Node && !!root.value?.contains(target);
272
+ }
273
+ function onPointerMove(event) {
274
+ if (drag.value || insideOverlay(event.target)) return;
275
+ hovered.value = blockIndexOf(event.target);
276
+ }
277
+ function onPointerLeave() {
278
+ if (!drag.value) hovered.value = null;
279
+ }
280
+ function onClick(event) {
281
+ if (suppressClick) {
282
+ suppressClick = false;
283
+ event.preventDefault();
284
+ event.stopPropagation();
285
+ return;
286
+ }
287
+ if (insideOverlay(event.target)) return;
288
+ const target = event.target instanceof Element ? event.target : null;
289
+ if (target?.closest("a, button, input, select, textarea, label, summary")) event.preventDefault();
290
+ if (target?.closest("[data-cms-preview-keep]")) return;
291
+ event.stopPropagation();
292
+ emit("message", { type: "select", index: blockIndexOf(event.target) });
293
+ }
294
+ function onSubmit(event) {
295
+ event.preventDefault();
296
+ }
297
+ function onKeydown(event) {
298
+ if (event.key === "Escape" && props.selected !== null) {
299
+ emit("message", { type: "select", index: null });
300
+ }
301
+ }
302
+ function revealSelected() {
303
+ if (props.selected === null || drag.value) return;
304
+ const element = document.querySelector(`[data-cms-block="${props.selected}"]`);
305
+ if (!element) return;
306
+ const rect = element.getBoundingClientRect();
307
+ if (rect.bottom < 0 || rect.top > window.innerHeight) {
308
+ element.scrollIntoView({ block: "center", behavior: "smooth" });
309
+ }
310
+ }
311
+ watch(
312
+ () => props.version,
313
+ async () => {
314
+ await nextTick();
315
+ measure();
316
+ if (refocus) {
317
+ refocus = false;
318
+ root.value?.querySelector(".cms-preview-toolbar.is-selected .cms-preview-handle")?.focus();
319
+ }
320
+ }
321
+ );
322
+ watch(
323
+ () => props.selected,
324
+ async () => {
325
+ await nextTick();
326
+ revealSelected();
327
+ }
328
+ );
329
+ let resizeObserver = null;
330
+ let mutationObserver = null;
331
+ onMounted(() => {
332
+ measure();
333
+ document.addEventListener("pointermove", onPointerMove, { passive: true });
334
+ document.documentElement.addEventListener("pointerleave", onPointerLeave);
335
+ document.addEventListener("click", onClick, true);
336
+ document.addEventListener("submit", onSubmit, true);
337
+ document.addEventListener("keydown", onKeydown);
338
+ document.addEventListener("load", scheduleMeasure, true);
339
+ window.addEventListener("scroll", scheduleMeasure, { passive: true, capture: true });
340
+ window.addEventListener("resize", scheduleMeasure);
341
+ resizeObserver = new ResizeObserver(scheduleMeasure);
342
+ resizeObserver.observe(document.body);
343
+ mutationObserver = new MutationObserver((records) => {
344
+ if (records.some((record) => !root.value?.contains(record.target))) scheduleMeasure();
345
+ });
346
+ mutationObserver.observe(document.body, { childList: true, subtree: true, attributes: true });
347
+ });
348
+ onBeforeUnmount(() => {
349
+ stopDrag();
350
+ if (frame) cancelAnimationFrame(frame);
351
+ document.removeEventListener("pointermove", onPointerMove);
352
+ document.documentElement.removeEventListener("pointerleave", onPointerLeave);
353
+ document.removeEventListener("click", onClick, true);
354
+ document.removeEventListener("submit", onSubmit, true);
355
+ document.removeEventListener("keydown", onKeydown);
356
+ document.removeEventListener("load", scheduleMeasure, true);
357
+ window.removeEventListener("scroll", scheduleMeasure, { capture: true });
358
+ window.removeEventListener("resize", scheduleMeasure);
359
+ resizeObserver?.disconnect();
360
+ mutationObserver?.disconnect();
361
+ });
362
+ </script>
@@ -0,0 +1,15 @@
1
+ import type { FieldConfig } from '../../shared/index.js';
2
+ import type { CmsPreviewFrameMessage } from '../../shared/preview.js';
3
+ type __VLS_Props = {
4
+ field: FieldConfig;
5
+ total: number;
6
+ selected: number | null;
7
+ version: number;
8
+ };
9
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ message: (message: CmsPreviewFrameMessage) => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ onMessage?: ((message: CmsPreviewFrameMessage) => any) | undefined;
13
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ declare const _default: typeof __VLS_export;
15
+ export default _default;
@@ -9,6 +9,7 @@ const FIELD_ICONS = {
9
9
  number: "hashtag",
10
10
  boolean: "check-circle",
11
11
  date: "calendar",
12
+ datetime: "clock",
12
13
  email: "at-symbol",
13
14
  slug: "link",
14
15
  select: "chevron-up-down",