@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 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
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.0.0"
6
6
  },
7
- "version": "0.1.0",
7
+ "version": "0.1.1",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
@@ -1,5 +1,12 @@
1
1
  <script setup>
2
- import { computed } from "vue";
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 class="block-editor-frame__content">
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
- <template v-if="builderReady">
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
- </template>
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
- .block-editor-frame::after {
220
- position: absolute;
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-frame--builder-ready:hover::after, .block-editor-frame--builder-ready:focus-within::after {
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-frame--builder-ready:hover .block-editor-frame__toolbar,
393
- .block-editor-frame--builder-ready:focus-within .block-editor-frame__toolbar,
394
- .block-editor-frame--builder-ready:hover .block-editor-frame__add,
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-frame--builder-ready:hover .block-editor-frame__toolbar,
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-frame--builder-ready:hover::after,
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toaster-builder/site-runtime",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Shared Nuxt runtime for Toaster sites and Builder integration",
5
5
  "license": "MIT",
6
6
  "type": "module",