@toaster-builder/site-runtime 0.1.0 → 0.1.2

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,57 @@ 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.
73
+
74
+ ## Действия блока в Builder
75
+
76
+ По умолчанию Builder показывает полный набор действий. Block может ограничить
77
+ его атрибутом `data-toaster-editor-actions` на том же элементе, который отмечен
78
+ как `data-toaster-editor-root`:
79
+
80
+ ```vue
81
+ <div
82
+ data-toaster-editor-root
83
+ data-toaster-editor-actions="version settings assistant add add-ai"
84
+ :data-toaster-placement-id="placementId"
85
+ >
86
+ <!-- Содержимое блока -->
87
+ </div>
88
+ ```
89
+
90
+ Доступные значения: `version`, `move`, `copy`, `settings`, `assistant`,
91
+ `visibility`, `delete`, `add`, `add-ai`. Если атрибут отсутствует, доступны все
92
+ действия. Пустой атрибут скрывает все элементы управления.
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.2",
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";
@@ -12,6 +19,10 @@ import moveBlockUpIcon from "../assets/editor/move-block-up.svg";
12
19
  import showBlockIcon from "../assets/editor/show-block.svg";
13
20
  import BlockPlacementProvider from "#toaster-site-runtime/components/BlockPlacementProvider.vue";
14
21
  import { useBuilderEventService } from "#toaster-site-runtime/composables/useBuilderEventService";
22
+ import {
23
+ blockEditorActions,
24
+ parseBlockEditorActions
25
+ } from "#toaster-site-runtime/utils/block-editor-actions";
15
26
  const props = defineProps({
16
27
  blockComponent: { type: null, required: true },
17
28
  blockId: { type: String, required: true },
@@ -24,6 +35,22 @@ const props = defineProps({
24
35
  region: { type: null, required: true }
25
36
  });
26
37
  const builderEvents = useBuilderEventService();
38
+ const contentElement = ref(null);
39
+ const editorTarget = shallowRef(null);
40
+ const overlayElement = ref(null);
41
+ const overlayHovered = ref(false);
42
+ const overlayFocused = ref(false);
43
+ const overlayStyle = ref({});
44
+ const overlayVisible = ref(false);
45
+ const enabledEditorActions = shallowRef(
46
+ new Set(blockEditorActions)
47
+ );
48
+ const targetHovered = ref(false);
49
+ const targetFocused = ref(false);
50
+ const overlayActive = computed(() => overlayHovered.value || overlayFocused.value || targetHovered.value || targetFocused.value);
51
+ let boundsFrame = null;
52
+ let mutationObserver = null;
53
+ let resizeObserver = null;
27
54
  const blockVersionLabel = computed(() => {
28
55
  const [name, variant = ""] = props.blockId.split("/");
29
56
  const formattedVariant = /^v\d+$/i.test(variant) ? variant.toUpperCase() : variant;
@@ -36,6 +63,123 @@ function postBuilderEvent(event, payload = {}) {
36
63
  region: props.region
37
64
  }, payload);
38
65
  }
66
+ function updateOverlayBounds() {
67
+ boundsFrame = null;
68
+ const target = editorTarget.value;
69
+ if (!target) {
70
+ overlayVisible.value = false;
71
+ return;
72
+ }
73
+ const bounds = target.getBoundingClientRect();
74
+ overlayVisible.value = bounds.width > 0 && bounds.height > 0;
75
+ overlayStyle.value = {
76
+ top: `${bounds.top}px`,
77
+ left: `${bounds.left}px`,
78
+ width: `${bounds.width}px`,
79
+ height: `${bounds.height}px`
80
+ };
81
+ }
82
+ function scheduleOverlayBoundsUpdate() {
83
+ if (boundsFrame !== null) return;
84
+ boundsFrame = window.requestAnimationFrame(updateOverlayBounds);
85
+ }
86
+ function handleTargetPointerEnter() {
87
+ targetHovered.value = true;
88
+ }
89
+ function handleTargetPointerLeave() {
90
+ targetHovered.value = false;
91
+ }
92
+ function handleTargetFocusIn() {
93
+ targetFocused.value = true;
94
+ }
95
+ function handleTargetFocusOut() {
96
+ window.requestAnimationFrame(() => {
97
+ targetFocused.value = editorTarget.value?.contains(document.activeElement) ?? false;
98
+ });
99
+ }
100
+ function unbindEditorTarget() {
101
+ const target = editorTarget.value;
102
+ if (target) {
103
+ target.removeEventListener("pointerenter", handleTargetPointerEnter);
104
+ target.removeEventListener("pointerleave", handleTargetPointerLeave);
105
+ target.removeEventListener("focusin", handleTargetFocusIn);
106
+ target.removeEventListener("focusout", handleTargetFocusOut);
107
+ }
108
+ resizeObserver?.disconnect();
109
+ resizeObserver = null;
110
+ targetHovered.value = false;
111
+ targetFocused.value = false;
112
+ }
113
+ function bindEditorTarget(target) {
114
+ enabledEditorActions.value = parseBlockEditorActions(
115
+ target?.dataset.toasterEditorActions
116
+ );
117
+ if (editorTarget.value === target) {
118
+ scheduleOverlayBoundsUpdate();
119
+ return;
120
+ }
121
+ unbindEditorTarget();
122
+ editorTarget.value = target;
123
+ if (!target) {
124
+ overlayVisible.value = false;
125
+ return;
126
+ }
127
+ target.addEventListener("pointerenter", handleTargetPointerEnter);
128
+ target.addEventListener("pointerleave", handleTargetPointerLeave);
129
+ target.addEventListener("focusin", handleTargetFocusIn);
130
+ target.addEventListener("focusout", handleTargetFocusOut);
131
+ resizeObserver = new ResizeObserver(scheduleOverlayBoundsUpdate);
132
+ resizeObserver.observe(target);
133
+ scheduleOverlayBoundsUpdate();
134
+ }
135
+ function hasEditorAction(action) {
136
+ return enabledEditorActions.value.has(action);
137
+ }
138
+ function findPlacementTargets(root) {
139
+ return Array.from(root.querySelectorAll("[data-toaster-editor-root]")).filter((element) => element.dataset.toasterPlacementId === props.placementId);
140
+ }
141
+ function resolveEditorTarget() {
142
+ const documentTargets = findPlacementTargets(document);
143
+ const explicitTarget = documentTargets.find(
144
+ (element) => element.dataset.toasterEditorRoot !== "auto"
145
+ );
146
+ const automaticTarget = contentElement.value ? findPlacementTargets(contentElement.value).find(
147
+ (element) => element.dataset.toasterEditorRoot === "auto"
148
+ ) : documentTargets.find((element) => element.dataset.toasterEditorRoot === "auto");
149
+ bindEditorTarget(explicitTarget ?? automaticTarget ?? null);
150
+ }
151
+ function handleOverlayFocusOut() {
152
+ window.requestAnimationFrame(() => {
153
+ overlayFocused.value = overlayElement.value?.contains(document.activeElement) ?? false;
154
+ });
155
+ }
156
+ onMounted(() => {
157
+ document.addEventListener("scroll", scheduleOverlayBoundsUpdate, true);
158
+ window.addEventListener("resize", scheduleOverlayBoundsUpdate);
159
+ mutationObserver = new MutationObserver(resolveEditorTarget);
160
+ mutationObserver.observe(document.body, {
161
+ attributes: true,
162
+ attributeFilter: [
163
+ "data-toaster-editor-actions",
164
+ "data-toaster-editor-root",
165
+ "data-toaster-placement-id"
166
+ ],
167
+ childList: true,
168
+ subtree: true
169
+ });
170
+ void nextTick(resolveEditorTarget);
171
+ });
172
+ onBeforeUnmount(() => {
173
+ document.removeEventListener("scroll", scheduleOverlayBoundsUpdate, true);
174
+ window.removeEventListener("resize", scheduleOverlayBoundsUpdate);
175
+ mutationObserver?.disconnect();
176
+ mutationObserver = null;
177
+ unbindEditorTarget();
178
+ if (boundsFrame !== null) {
179
+ window.cancelAnimationFrame(boundsFrame);
180
+ boundsFrame = null;
181
+ }
182
+ });
39
183
  </script>
40
184
 
41
185
  <template>
@@ -49,18 +193,38 @@ function postBuilderEvent(event, payload = {}) {
49
193
  :data-toaster-placement-id="placementId"
50
194
  :data-toaster-region="region"
51
195
  >
52
- <div class="block-editor-frame__content">
196
+ <div
197
+ ref="contentElement"
198
+ class="block-editor-frame__content"
199
+ >
53
200
  <BlockPlacementProvider :placement-id="placementId">
54
201
  <component
55
202
  :is="blockComponent"
203
+ data-toaster-editor-root="auto"
204
+ :data-toaster-placement-id="placementId"
56
205
  :options="options"
57
206
  />
58
207
  </BlockPlacementProvider>
59
208
  </div>
60
209
 
61
- <template v-if="builderReady">
210
+ <div
211
+ v-if="builderReady"
212
+ v-show="overlayVisible"
213
+ ref="overlayElement"
214
+ class="block-editor-frame__overlay"
215
+ :class="{ 'block-editor-frame__overlay--active': overlayActive }"
216
+ data-toaster-editor-overlay
217
+ :data-toaster-editor-actions="[...enabledEditorActions].join(' ')"
218
+ :data-toaster-placement-id="placementId"
219
+ :style="overlayStyle"
220
+ @pointerenter="overlayHovered = true"
221
+ @pointerleave="overlayHovered = false"
222
+ @focusin="overlayFocused = true"
223
+ @focusout="handleOverlayFocusOut"
224
+ >
62
225
  <div class="block-editor-frame__toolbar">
63
226
  <button
227
+ v-if="hasEditorAction('version')"
64
228
  class="block-editor-frame__control block-editor-frame__version"
65
229
  type="button"
66
230
  :aria-label="`\u0421\u043C\u0435\u043D\u0438\u0442\u044C \u0432\u0435\u0440\u0441\u0438\u044E \u0431\u043B\u043E\u043A\u0430 ${blockVersionLabel}`"
@@ -71,6 +235,7 @@ function postBuilderEvent(event, payload = {}) {
71
235
  </button>
72
236
 
73
237
  <button
238
+ v-if="hasEditorAction('move')"
74
239
  class="block-editor-frame__control block-editor-frame__icon-button"
75
240
  type="button"
76
241
  :aria-label="`\u041F\u0435\u0440\u0435\u043D\u0435\u0441\u0442\u0438 \u0432\u0432\u0435\u0440\u0445 \u0431\u043B\u043E\u043A ${blockVersionLabel}`"
@@ -86,6 +251,7 @@ function postBuilderEvent(event, payload = {}) {
86
251
  </button>
87
252
 
88
253
  <button
254
+ v-if="hasEditorAction('move')"
89
255
  class="block-editor-frame__control block-editor-frame__icon-button"
90
256
  type="button"
91
257
  :aria-label="`\u041F\u0435\u0440\u0435\u043D\u0435\u0441\u0442\u0438 \u0432\u043D\u0438\u0437 \u0431\u043B\u043E\u043A ${blockVersionLabel}`"
@@ -101,6 +267,7 @@ function postBuilderEvent(event, payload = {}) {
101
267
  </button>
102
268
 
103
269
  <button
270
+ v-if="hasEditorAction('copy')"
104
271
  class="block-editor-frame__control block-editor-frame__icon-button"
105
272
  type="button"
106
273
  :aria-label="`\u041A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0431\u043B\u043E\u043A ${blockVersionLabel}`"
@@ -115,6 +282,7 @@ function postBuilderEvent(event, payload = {}) {
115
282
  </button>
116
283
 
117
284
  <button
285
+ v-if="hasEditorAction('settings')"
118
286
  class="block-editor-frame__control block-editor-frame__icon-button"
119
287
  type="button"
120
288
  :aria-label="`\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0431\u043B\u043E\u043A\u0430 ${blockVersionLabel}`"
@@ -139,6 +307,7 @@ function postBuilderEvent(event, payload = {}) {
139
307
  </button>
140
308
 
141
309
  <button
310
+ v-if="hasEditorAction('assistant')"
142
311
  class="block-editor-frame__control block-editor-frame__icon-button"
143
312
  type="button"
144
313
  :aria-label="`\u041E\u0442\u043A\u0440\u044B\u0442\u044C AI-\u0430\u0441\u0441\u0438\u0441\u0442\u0435\u043D\u0442\u0430 \u0434\u043B\u044F \u0431\u043B\u043E\u043A\u0430 ${blockVersionLabel}`"
@@ -153,6 +322,7 @@ function postBuilderEvent(event, payload = {}) {
153
322
  </button>
154
323
 
155
324
  <button
325
+ v-if="hasEditorAction('visibility')"
156
326
  class="block-editor-frame__control block-editor-frame__icon-button"
157
327
  type="button"
158
328
  :aria-label="enabled ? `\u0412\u0440\u0435\u043C\u0435\u043D\u043D\u043E \u0441\u043A\u0440\u044B\u0442\u044C \u0431\u043B\u043E\u043A ${blockVersionLabel}` : `\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0431\u043B\u043E\u043A ${blockVersionLabel}`"
@@ -167,6 +337,7 @@ function postBuilderEvent(event, payload = {}) {
167
337
  </button>
168
338
 
169
339
  <button
340
+ v-if="hasEditorAction('delete')"
170
341
  class="block-editor-frame__control block-editor-frame__icon-button"
171
342
  type="button"
172
343
  :aria-label="`\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0431\u043B\u043E\u043A ${blockVersionLabel}`"
@@ -182,6 +353,7 @@ function postBuilderEvent(event, payload = {}) {
182
353
  </div>
183
354
 
184
355
  <button
356
+ v-if="hasEditorAction('add')"
185
357
  class="block-editor-frame__control block-editor-frame__add block-editor-frame__add--after"
186
358
  type="button"
187
359
  aria-label="Добавить блок снизу"
@@ -196,6 +368,7 @@ function postBuilderEvent(event, payload = {}) {
196
368
  </button>
197
369
 
198
370
  <button
371
+ v-if="hasEditorAction('add-ai')"
199
372
  class="block-editor-frame__control block-editor-frame__add-ai"
200
373
  type="button"
201
374
  :aria-label="`\u0421\u043E\u0437\u0434\u0430\u0442\u044C \u0431\u043B\u043E\u043A \u0447\u0435\u0440\u0435\u0437 AI \u043F\u043E\u0441\u043B\u0435 \u0431\u043B\u043E\u043A\u0430 ${blockVersionLabel}`"
@@ -208,7 +381,7 @@ function postBuilderEvent(event, payload = {}) {
208
381
  aria-hidden="true"
209
382
  >
210
383
  </button>
211
- </template>
384
+ </div>
212
385
  </div>
213
386
  </template>
214
387
 
@@ -216,16 +389,16 @@ function postBuilderEvent(event, payload = {}) {
216
389
  .block-editor-frame {
217
390
  position: relative;
218
391
  }
219
- .block-editor-frame::after {
220
- position: absolute;
392
+
393
+ .block-editor-frame__overlay {
394
+ position: fixed;
221
395
  z-index: 2147483645;
396
+ box-sizing: border-box;
222
397
  border: 1px dashed transparent;
223
- content: "";
224
- inset: 0;
225
398
  pointer-events: none;
226
399
  transition: border-color 120ms ease;
227
400
  }
228
- .block-editor-frame--builder-ready:hover::after, .block-editor-frame--builder-ready:focus-within::after {
401
+ .block-editor-frame__overlay--active {
229
402
  border-color: #ff0099;
230
403
  }
231
404
 
@@ -389,24 +562,19 @@ function postBuilderEvent(event, payload = {}) {
389
562
  transform: translateX(-50%);
390
563
  }
391
564
 
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 {
565
+ .block-editor-frame__overlay--active .block-editor-frame__toolbar,
566
+ .block-editor-frame__overlay--active .block-editor-frame__add,
567
+ .block-editor-frame__overlay--active .block-editor-frame__add-ai {
398
568
  opacity: 1;
399
569
  pointer-events: auto;
400
570
  }
401
571
 
402
- .block-editor-frame--builder-ready:hover .block-editor-frame__toolbar,
403
- .block-editor-frame--builder-ready:focus-within .block-editor-frame__toolbar {
572
+ .block-editor-frame__overlay--active .block-editor-frame__toolbar {
404
573
  transform: translateY(0);
405
574
  }
406
575
 
407
576
  @media (max-width: 499px) {
408
- .block-editor-frame--builder-ready:hover::after,
409
- .block-editor-frame--builder-ready:focus-within::after {
577
+ .block-editor-frame__overlay--active {
410
578
  border-color: transparent;
411
579
  }
412
580
  .block-editor-frame__toolbar,
@@ -0,0 +1,3 @@
1
+ export declare const blockEditorActions: readonly ["version", "move", "copy", "settings", "assistant", "visibility", "delete", "add", "add-ai"];
2
+ export type BlockEditorAction = (typeof blockEditorActions)[number];
3
+ export declare function parseBlockEditorActions(value: string | undefined): ReadonlySet<BlockEditorAction>;
@@ -0,0 +1,18 @@
1
+ export const blockEditorActions = [
2
+ "version",
3
+ "move",
4
+ "copy",
5
+ "settings",
6
+ "assistant",
7
+ "visibility",
8
+ "delete",
9
+ "add",
10
+ "add-ai"
11
+ ];
12
+ const knownActions = new Set(blockEditorActions);
13
+ export function parseBlockEditorActions(value) {
14
+ if (value === void 0) return new Set(blockEditorActions);
15
+ return new Set(
16
+ value.split(/\s+/).filter((action) => knownActions.has(action))
17
+ );
18
+ }
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.2",
4
4
  "description": "Shared Nuxt runtime for Toaster sites and Builder integration",
5
5
  "license": "MIT",
6
6
  "type": "module",