@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.
- package/README.md +3 -1
- package/dist/module.d.mts +4 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +319 -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.vue +9 -4
- 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/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 +37 -0
- package/dist/runtime/app/components/cms/VisualEditor.vue +533 -0
- package/dist/runtime/app/components/cms/VisualEditor.vue.d.ts +37 -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-runtime.d.ts +1 -0
- package/dist/runtime/app/composables/cms-settings.js +8 -2
- package/dist/runtime/app/layouts/cms-admin.vue +5 -0
- 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 +33 -4
- package/dist/runtime/app/pages/admin-content.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-content.vue +284 -0
- package/dist/runtime/app/pages/admin-content.vue.d.ts +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 +3 -2
- 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/utils/graphql.js +23 -8
- package/dist/runtime/server/utils/registry.d.ts +1 -0
- package/dist/runtime/server/utils/registry.js +6 -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/validation.d.ts +2 -0
- package/dist/runtime/shared/validation.js +15 -2
- 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;
|