@toaster-builder/site-runtime 0.1.0 → 0.1.1
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 +34 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/BlockEditorFrame.vue +154 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,3 +36,37 @@ export default defineNuxtConfig({
|
|
|
36
36
|
modules: ['@toaster-builder/site-runtime'],
|
|
37
37
|
})
|
|
38
38
|
```
|
|
39
|
+
|
|
40
|
+
## Граница блока в Builder
|
|
41
|
+
|
|
42
|
+
В dev-режиме Runtime автоматически добавляет корневому DOM-элементу обычного
|
|
43
|
+
Block атрибуты `data-toaster-editor-root="auto"` и
|
|
44
|
+
`data-toaster-placement-id`. Рамка
|
|
45
|
+
Builder строится по фактическому `getBoundingClientRect()` этого элемента, поэтому
|
|
46
|
+
работает для элементов с `position: fixed`, `sticky` и `absolute`.
|
|
47
|
+
|
|
48
|
+
Block с несколькими корнями или `Teleport` должен явно отметить визуальный
|
|
49
|
+
элемент, который Builder будет считать его границей. Явная граница имеет
|
|
50
|
+
приоритет над автоматически найденным корнем:
|
|
51
|
+
|
|
52
|
+
```vue
|
|
53
|
+
<script setup lang="ts">
|
|
54
|
+
import { inject, ref } from 'vue'
|
|
55
|
+
import { blockPlacementIdKey } from '#toaster-site-runtime/utils/block-placement'
|
|
56
|
+
|
|
57
|
+
const placementId = inject(blockPlacementIdKey, ref(''))
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<template>
|
|
61
|
+
<Teleport to="body">
|
|
62
|
+
<div
|
|
63
|
+
data-toaster-editor-root
|
|
64
|
+
:data-toaster-placement-id="placementId"
|
|
65
|
+
>
|
|
66
|
+
<!-- Содержимое модального окна -->
|
|
67
|
+
</div>
|
|
68
|
+
</Teleport>
|
|
69
|
+
</template>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Для модального Block атрибут ставится на само окно, а не на backdrop.
|
package/dist/module.json
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
nextTick,
|
|
5
|
+
onBeforeUnmount,
|
|
6
|
+
onMounted,
|
|
7
|
+
ref,
|
|
8
|
+
shallowRef
|
|
9
|
+
} from "vue";
|
|
3
10
|
import addBlockIcon from "../assets/editor/add-block.svg";
|
|
4
11
|
import aiAssistantIcon from "../assets/editor/ai-assistant.svg";
|
|
5
12
|
import blockSettingsCenterIcon from "../assets/editor/block-settings-center.svg";
|
|
@@ -24,6 +31,19 @@ const props = defineProps({
|
|
|
24
31
|
region: { type: null, required: true }
|
|
25
32
|
});
|
|
26
33
|
const builderEvents = useBuilderEventService();
|
|
34
|
+
const contentElement = ref(null);
|
|
35
|
+
const editorTarget = shallowRef(null);
|
|
36
|
+
const overlayElement = ref(null);
|
|
37
|
+
const overlayHovered = ref(false);
|
|
38
|
+
const overlayFocused = ref(false);
|
|
39
|
+
const overlayStyle = ref({});
|
|
40
|
+
const overlayVisible = ref(false);
|
|
41
|
+
const targetHovered = ref(false);
|
|
42
|
+
const targetFocused = ref(false);
|
|
43
|
+
const overlayActive = computed(() => overlayHovered.value || overlayFocused.value || targetHovered.value || targetFocused.value);
|
|
44
|
+
let boundsFrame = null;
|
|
45
|
+
let mutationObserver = null;
|
|
46
|
+
let resizeObserver = null;
|
|
27
47
|
const blockVersionLabel = computed(() => {
|
|
28
48
|
const [name, variant = ""] = props.blockId.split("/");
|
|
29
49
|
const formattedVariant = /^v\d+$/i.test(variant) ? variant.toUpperCase() : variant;
|
|
@@ -36,6 +56,108 @@ function postBuilderEvent(event, payload = {}) {
|
|
|
36
56
|
region: props.region
|
|
37
57
|
}, payload);
|
|
38
58
|
}
|
|
59
|
+
function updateOverlayBounds() {
|
|
60
|
+
boundsFrame = null;
|
|
61
|
+
const target = editorTarget.value;
|
|
62
|
+
if (!target) {
|
|
63
|
+
overlayVisible.value = false;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const bounds = target.getBoundingClientRect();
|
|
67
|
+
overlayVisible.value = bounds.width > 0 && bounds.height > 0;
|
|
68
|
+
overlayStyle.value = {
|
|
69
|
+
top: `${bounds.top}px`,
|
|
70
|
+
left: `${bounds.left}px`,
|
|
71
|
+
width: `${bounds.width}px`,
|
|
72
|
+
height: `${bounds.height}px`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function scheduleOverlayBoundsUpdate() {
|
|
76
|
+
if (boundsFrame !== null) return;
|
|
77
|
+
boundsFrame = window.requestAnimationFrame(updateOverlayBounds);
|
|
78
|
+
}
|
|
79
|
+
function handleTargetPointerEnter() {
|
|
80
|
+
targetHovered.value = true;
|
|
81
|
+
}
|
|
82
|
+
function handleTargetPointerLeave() {
|
|
83
|
+
targetHovered.value = false;
|
|
84
|
+
}
|
|
85
|
+
function handleTargetFocusIn() {
|
|
86
|
+
targetFocused.value = true;
|
|
87
|
+
}
|
|
88
|
+
function handleTargetFocusOut() {
|
|
89
|
+
window.requestAnimationFrame(() => {
|
|
90
|
+
targetFocused.value = editorTarget.value?.contains(document.activeElement) ?? false;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function unbindEditorTarget() {
|
|
94
|
+
const target = editorTarget.value;
|
|
95
|
+
if (target) {
|
|
96
|
+
target.removeEventListener("pointerenter", handleTargetPointerEnter);
|
|
97
|
+
target.removeEventListener("pointerleave", handleTargetPointerLeave);
|
|
98
|
+
target.removeEventListener("focusin", handleTargetFocusIn);
|
|
99
|
+
target.removeEventListener("focusout", handleTargetFocusOut);
|
|
100
|
+
}
|
|
101
|
+
resizeObserver?.disconnect();
|
|
102
|
+
resizeObserver = null;
|
|
103
|
+
targetHovered.value = false;
|
|
104
|
+
targetFocused.value = false;
|
|
105
|
+
}
|
|
106
|
+
function bindEditorTarget(target) {
|
|
107
|
+
if (editorTarget.value === target) {
|
|
108
|
+
scheduleOverlayBoundsUpdate();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
unbindEditorTarget();
|
|
112
|
+
editorTarget.value = target;
|
|
113
|
+
if (!target) {
|
|
114
|
+
overlayVisible.value = false;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
target.addEventListener("pointerenter", handleTargetPointerEnter);
|
|
118
|
+
target.addEventListener("pointerleave", handleTargetPointerLeave);
|
|
119
|
+
target.addEventListener("focusin", handleTargetFocusIn);
|
|
120
|
+
target.addEventListener("focusout", handleTargetFocusOut);
|
|
121
|
+
resizeObserver = new ResizeObserver(scheduleOverlayBoundsUpdate);
|
|
122
|
+
resizeObserver.observe(target);
|
|
123
|
+
scheduleOverlayBoundsUpdate();
|
|
124
|
+
}
|
|
125
|
+
function findPlacementTargets(root) {
|
|
126
|
+
return Array.from(root.querySelectorAll("[data-toaster-editor-root]")).filter((element) => element.dataset.toasterPlacementId === props.placementId);
|
|
127
|
+
}
|
|
128
|
+
function resolveEditorTarget() {
|
|
129
|
+
const documentTargets = findPlacementTargets(document);
|
|
130
|
+
const explicitTarget = documentTargets.find(
|
|
131
|
+
(element) => element.dataset.toasterEditorRoot !== "auto"
|
|
132
|
+
);
|
|
133
|
+
const automaticTarget = contentElement.value ? findPlacementTargets(contentElement.value).find(
|
|
134
|
+
(element) => element.dataset.toasterEditorRoot === "auto"
|
|
135
|
+
) : documentTargets.find((element) => element.dataset.toasterEditorRoot === "auto");
|
|
136
|
+
bindEditorTarget(explicitTarget ?? automaticTarget ?? null);
|
|
137
|
+
}
|
|
138
|
+
function handleOverlayFocusOut() {
|
|
139
|
+
window.requestAnimationFrame(() => {
|
|
140
|
+
overlayFocused.value = overlayElement.value?.contains(document.activeElement) ?? false;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
onMounted(() => {
|
|
144
|
+
document.addEventListener("scroll", scheduleOverlayBoundsUpdate, true);
|
|
145
|
+
window.addEventListener("resize", scheduleOverlayBoundsUpdate);
|
|
146
|
+
mutationObserver = new MutationObserver(resolveEditorTarget);
|
|
147
|
+
mutationObserver.observe(document.body, { childList: true, subtree: true });
|
|
148
|
+
void nextTick(resolveEditorTarget);
|
|
149
|
+
});
|
|
150
|
+
onBeforeUnmount(() => {
|
|
151
|
+
document.removeEventListener("scroll", scheduleOverlayBoundsUpdate, true);
|
|
152
|
+
window.removeEventListener("resize", scheduleOverlayBoundsUpdate);
|
|
153
|
+
mutationObserver?.disconnect();
|
|
154
|
+
mutationObserver = null;
|
|
155
|
+
unbindEditorTarget();
|
|
156
|
+
if (boundsFrame !== null) {
|
|
157
|
+
window.cancelAnimationFrame(boundsFrame);
|
|
158
|
+
boundsFrame = null;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
39
161
|
</script>
|
|
40
162
|
|
|
41
163
|
<template>
|
|
@@ -49,16 +171,34 @@ function postBuilderEvent(event, payload = {}) {
|
|
|
49
171
|
:data-toaster-placement-id="placementId"
|
|
50
172
|
:data-toaster-region="region"
|
|
51
173
|
>
|
|
52
|
-
<div
|
|
174
|
+
<div
|
|
175
|
+
ref="contentElement"
|
|
176
|
+
class="block-editor-frame__content"
|
|
177
|
+
>
|
|
53
178
|
<BlockPlacementProvider :placement-id="placementId">
|
|
54
179
|
<component
|
|
55
180
|
:is="blockComponent"
|
|
181
|
+
data-toaster-editor-root="auto"
|
|
182
|
+
:data-toaster-placement-id="placementId"
|
|
56
183
|
:options="options"
|
|
57
184
|
/>
|
|
58
185
|
</BlockPlacementProvider>
|
|
59
186
|
</div>
|
|
60
187
|
|
|
61
|
-
<
|
|
188
|
+
<div
|
|
189
|
+
v-if="builderReady"
|
|
190
|
+
v-show="overlayVisible"
|
|
191
|
+
ref="overlayElement"
|
|
192
|
+
class="block-editor-frame__overlay"
|
|
193
|
+
:class="{ 'block-editor-frame__overlay--active': overlayActive }"
|
|
194
|
+
data-toaster-editor-overlay
|
|
195
|
+
:data-toaster-placement-id="placementId"
|
|
196
|
+
:style="overlayStyle"
|
|
197
|
+
@pointerenter="overlayHovered = true"
|
|
198
|
+
@pointerleave="overlayHovered = false"
|
|
199
|
+
@focusin="overlayFocused = true"
|
|
200
|
+
@focusout="handleOverlayFocusOut"
|
|
201
|
+
>
|
|
62
202
|
<div class="block-editor-frame__toolbar">
|
|
63
203
|
<button
|
|
64
204
|
class="block-editor-frame__control block-editor-frame__version"
|
|
@@ -208,7 +348,7 @@ function postBuilderEvent(event, payload = {}) {
|
|
|
208
348
|
aria-hidden="true"
|
|
209
349
|
>
|
|
210
350
|
</button>
|
|
211
|
-
</
|
|
351
|
+
</div>
|
|
212
352
|
</div>
|
|
213
353
|
</template>
|
|
214
354
|
|
|
@@ -216,16 +356,16 @@ function postBuilderEvent(event, payload = {}) {
|
|
|
216
356
|
.block-editor-frame {
|
|
217
357
|
position: relative;
|
|
218
358
|
}
|
|
219
|
-
|
|
220
|
-
|
|
359
|
+
|
|
360
|
+
.block-editor-frame__overlay {
|
|
361
|
+
position: fixed;
|
|
221
362
|
z-index: 2147483645;
|
|
363
|
+
box-sizing: border-box;
|
|
222
364
|
border: 1px dashed transparent;
|
|
223
|
-
content: "";
|
|
224
|
-
inset: 0;
|
|
225
365
|
pointer-events: none;
|
|
226
366
|
transition: border-color 120ms ease;
|
|
227
367
|
}
|
|
228
|
-
.block-editor-
|
|
368
|
+
.block-editor-frame__overlay--active {
|
|
229
369
|
border-color: #ff0099;
|
|
230
370
|
}
|
|
231
371
|
|
|
@@ -389,24 +529,19 @@ function postBuilderEvent(event, payload = {}) {
|
|
|
389
529
|
transform: translateX(-50%);
|
|
390
530
|
}
|
|
391
531
|
|
|
392
|
-
.block-editor-
|
|
393
|
-
.block-editor-
|
|
394
|
-
.block-editor-
|
|
395
|
-
.block-editor-frame--builder-ready:focus-within .block-editor-frame__add,
|
|
396
|
-
.block-editor-frame--builder-ready:hover .block-editor-frame__add-ai,
|
|
397
|
-
.block-editor-frame--builder-ready:focus-within .block-editor-frame__add-ai {
|
|
532
|
+
.block-editor-frame__overlay--active .block-editor-frame__toolbar,
|
|
533
|
+
.block-editor-frame__overlay--active .block-editor-frame__add,
|
|
534
|
+
.block-editor-frame__overlay--active .block-editor-frame__add-ai {
|
|
398
535
|
opacity: 1;
|
|
399
536
|
pointer-events: auto;
|
|
400
537
|
}
|
|
401
538
|
|
|
402
|
-
.block-editor-
|
|
403
|
-
.block-editor-frame--builder-ready:focus-within .block-editor-frame__toolbar {
|
|
539
|
+
.block-editor-frame__overlay--active .block-editor-frame__toolbar {
|
|
404
540
|
transform: translateY(0);
|
|
405
541
|
}
|
|
406
542
|
|
|
407
543
|
@media (max-width: 499px) {
|
|
408
|
-
.block-editor-
|
|
409
|
-
.block-editor-frame--builder-ready:focus-within::after {
|
|
544
|
+
.block-editor-frame__overlay--active {
|
|
410
545
|
border-color: transparent;
|
|
411
546
|
}
|
|
412
547
|
.block-editor-frame__toolbar,
|