vite-plugin-visual-selector 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/dist/runtime.js CHANGED
@@ -1,150 +1,890 @@
1
1
  // src/shared/constants.ts
2
2
  var DEFAULT_ATTRIBUTE_NAME = "data-source-location";
3
- var OVERLAY_ATTRIBUTE_NAME = "data-visual-selector-overlay";
3
+ var CONTROL_MESSAGE_TYPE = "visual-selector:set-active";
4
+ var AGENT_ATTR = "data-vite-plugin-element";
4
5
 
5
- // src/runtime.ts
6
- var activeCleanup = null;
7
- function getElementSelectorId(element, attributeName) {
8
- return element.getAttribute(attributeName);
6
+ // src/runtime/utils.ts
7
+ function debounce(fn, ms) {
8
+ let timer = null;
9
+ return ((...args) => {
10
+ if (timer) clearTimeout(timer);
11
+ timer = setTimeout(() => fn(...args), ms);
12
+ });
9
13
  }
10
- function isOverlayElement(element) {
11
- return element.hasAttribute(OVERLAY_ATTRIBUTE_NAME);
14
+ function getSourceId(element) {
15
+ return element.dataset?.sourceLocation ?? element.dataset?.visualSelectorId ?? null;
12
16
  }
13
- function getSelectableElement(target, attributeName) {
14
- if (!(target instanceof Element)) {
15
- return null;
17
+ function findAllElementsById(id) {
18
+ let elements = Array.from(document.querySelectorAll(`[data-source-location="${id}"]`));
19
+ if (elements.length === 0) {
20
+ elements = Array.from(document.querySelectorAll(`[data-visual-selector-id="${id}"]`));
16
21
  }
17
- const closestOverlay = target.closest(`[${OVERLAY_ATTRIBUTE_NAME}]`);
18
- if (closestOverlay) {
19
- return null;
22
+ return elements;
23
+ }
24
+ function hasSourceLocation(el) {
25
+ return el.hasAttribute("data-source-location") || el.hasAttribute("data-visual-selector-id");
26
+ }
27
+ function getElementPosition(element) {
28
+ const rect = element.getBoundingClientRect();
29
+ return {
30
+ top: rect.top,
31
+ left: rect.left,
32
+ right: rect.right,
33
+ bottom: rect.bottom,
34
+ width: rect.width,
35
+ height: rect.height,
36
+ centerX: rect.left + rect.width / 2,
37
+ centerY: rect.top + rect.height / 2
38
+ };
39
+ }
40
+ var TEXT_TAGS = ["P", "H1", "H2", "H3", "H4", "H5", "H6", "SPAN", "A", "LABEL"];
41
+ var INLINE_EDIT_TAGS = [
42
+ "div",
43
+ "p",
44
+ "h1",
45
+ "h2",
46
+ "h3",
47
+ "h4",
48
+ "h5",
49
+ "h6",
50
+ "span",
51
+ "li",
52
+ "td",
53
+ "a",
54
+ "button",
55
+ "label"
56
+ ];
57
+
58
+ // src/runtime/inline-edit.ts
59
+ function canInlineEdit(element) {
60
+ return INLINE_EDIT_TAGS.includes(element.tagName.toLowerCase()) && !!element.textContent?.trim() && !element.querySelector("img, video, canvas, svg") && element.children.length === 0;
61
+ }
62
+ function startInlineEditing(state, element) {
63
+ if (!canInlineEdit(element)) return;
64
+ state.editingElement = element;
65
+ element.dataset.originalTextContent = element.textContent || "";
66
+ element.contentEditable = "true";
67
+ element.setAttribute(AGENT_ATTR, "");
68
+ element.style.cursor = "text";
69
+ const range = document.createRange();
70
+ range.selectNodeContents(element);
71
+ const sel = window.getSelection();
72
+ sel?.removeAllRanges();
73
+ sel?.addRange(range);
74
+ element.focus();
75
+ state.selectionOverlays.forEach((o) => o.style.display = "none");
76
+ const debouncedReport = debounce(() => {
77
+ try {
78
+ window.parent.postMessage(
79
+ {
80
+ type: "inline-edit",
81
+ elementInfo: {
82
+ tagName: element.tagName,
83
+ visualSelectorId: state.selectedId,
84
+ dataSourceLocation: element.dataset?.sourceLocation
85
+ },
86
+ originalContent: element.dataset.originalTextContent || "",
87
+ newContent: element.textContent || ""
88
+ },
89
+ state.targetOrigin
90
+ );
91
+ } catch {
92
+ }
93
+ }, 500);
94
+ element.addEventListener("input", debouncedReport);
95
+ element.__inlineEditHandler = debouncedReport;
96
+ try {
97
+ window.parent.postMessage(
98
+ {
99
+ type: "content-editing-started",
100
+ visualSelectorId: state.selectedId
101
+ },
102
+ state.targetOrigin
103
+ );
104
+ } catch {
105
+ }
106
+ }
107
+ function stopInlineEditing(state, updatePositions) {
108
+ if (!state.editingElement) return;
109
+ state.editingElement.contentEditable = "false";
110
+ state.editingElement.removeAttribute(AGENT_ATTR);
111
+ state.editingElement.style.cursor = "";
112
+ const handler = state.editingElement.__inlineEditHandler;
113
+ if (handler) {
114
+ state.editingElement.removeEventListener("input", handler);
115
+ delete state.editingElement.__inlineEditHandler;
116
+ }
117
+ state.selectionOverlays.forEach((o) => o.style.display = "");
118
+ updatePositions();
119
+ try {
120
+ window.parent.postMessage(
121
+ {
122
+ type: "content-editing-ended",
123
+ visualSelectorId: state.selectedId
124
+ },
125
+ state.targetOrigin
126
+ );
127
+ } catch {
128
+ }
129
+ state.editingElement = null;
130
+ }
131
+ function handleInlineEdit(state, data, updatePositions) {
132
+ if (data.inlineEditingMode) {
133
+ const el = document.querySelector(
134
+ `[data-source-location="${data.dataSourceLocation}"]`
135
+ );
136
+ if (el) {
137
+ startInlineEditing(state, el);
138
+ }
139
+ } else {
140
+ stopInlineEditing(state, updatePositions);
141
+ }
142
+ }
143
+
144
+ // src/runtime/layer-navigation.ts
145
+ function buildLayerTree(element) {
146
+ const tree = [];
147
+ const ancestors = [];
148
+ let parent = element.parentElement;
149
+ while (parent && parent !== document.body) {
150
+ if (hasSourceLocation(parent)) {
151
+ ancestors.push(parent);
152
+ }
153
+ parent = parent.parentElement;
154
+ }
155
+ ancestors.reverse();
156
+ ancestors.forEach((el) => {
157
+ tree.push({ element: el, tagName: el.tagName.toLowerCase(), depth: 0 });
158
+ });
159
+ tree.push({ element, tagName: element.tagName.toLowerCase(), depth: 0 });
160
+ for (let i = 0; i < element.children.length; i++) {
161
+ const child = element.children[i];
162
+ if (hasSourceLocation(child)) {
163
+ tree.push({ element: child, tagName: child.tagName.toLowerCase(), depth: 0 });
164
+ }
165
+ }
166
+ return tree;
167
+ }
168
+ function removeLayerDropdown(state) {
169
+ if (state.layerDropdown) {
170
+ const parentOverlay = state.layerDropdown.parentElement;
171
+ if (parentOverlay) {
172
+ const arrowEl = parentOverlay.querySelector("[data-tag-arrow]");
173
+ if (arrowEl) arrowEl.textContent = "\u2304";
174
+ }
175
+ state.layerDropdown.remove();
176
+ state.layerDropdown = null;
177
+ document.removeEventListener("keydown", handleLayerKeyboard);
178
+ }
179
+ }
180
+ function toggleLayerDropdown(state, element, anchor, onSelectElement) {
181
+ if (state.layerDropdown) {
182
+ removeLayerDropdown(state);
183
+ return;
184
+ }
185
+ const layers = buildLayerTree(element);
186
+ renderLayerDropdown(state, anchor, layers, element, onSelectElement);
187
+ }
188
+ function renderLayerDropdown(state, anchor, layers, currentElement, onSelectElement) {
189
+ const dropdown = document.createElement("div");
190
+ dropdown.setAttribute("data-layer-dropdown", "true");
191
+ dropdown.setAttribute(AGENT_ATTR, "");
192
+ Object.assign(dropdown.style, {
193
+ position: "absolute",
194
+ backgroundColor: "#ffffff",
195
+ border: "1px solid #e2e8f0",
196
+ borderRadius: "6px",
197
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
198
+ fontSize: "12px",
199
+ zIndex: "99999",
200
+ pointerEvents: "auto",
201
+ padding: "4px 0",
202
+ minWidth: "120px",
203
+ maxHeight: "320px",
204
+ overflowY: "auto"
205
+ });
206
+ let focusedIndex = -1;
207
+ const items = [];
208
+ layers.forEach((layer, idx) => {
209
+ const item = document.createElement("div");
210
+ item.setAttribute(AGENT_ATTR, "");
211
+ item.textContent = layer.tagName;
212
+ item.style.padding = "6px 16px";
213
+ item.style.cursor = "pointer";
214
+ item.style.whiteSpace = "nowrap";
215
+ if (layer.element === currentElement) {
216
+ item.style.color = "#000000";
217
+ item.style.backgroundColor = "rgba(255, 204, 0, 0.2)";
218
+ item.style.fontWeight = "600";
219
+ focusedIndex = idx;
220
+ } else {
221
+ item.style.color = "#64748b";
222
+ }
223
+ item.addEventListener("click", (e) => {
224
+ e.stopPropagation();
225
+ removeLayerDropdown(state);
226
+ onSelectElement(layer.element);
227
+ });
228
+ item.addEventListener("mouseenter", () => {
229
+ if (layer.element !== currentElement) {
230
+ item.style.backgroundColor = "#f1f5f9";
231
+ item.style.color = "#334155";
232
+ }
233
+ });
234
+ item.addEventListener("mouseleave", () => {
235
+ if (layer.element !== currentElement) {
236
+ item.style.backgroundColor = "transparent";
237
+ item.style.color = "#64748b";
238
+ }
239
+ });
240
+ items.push(item);
241
+ dropdown.appendChild(item);
242
+ });
243
+ document.body.appendChild(dropdown);
244
+ const anchorRect = anchor.getBoundingClientRect();
245
+ dropdown.style.top = `${anchorRect.bottom + window.scrollY + 2}px`;
246
+ dropdown.style.left = `${anchorRect.left + window.scrollX}px`;
247
+ requestAnimationFrame(() => {
248
+ const ddRect = dropdown.getBoundingClientRect();
249
+ if (ddRect.right > window.innerWidth - 4) {
250
+ dropdown.style.left = `${window.innerWidth - ddRect.width - 4 + window.scrollX}px`;
251
+ }
252
+ if (ddRect.left < 4) {
253
+ dropdown.style.left = `${4 + window.scrollX}px`;
254
+ }
255
+ if (ddRect.bottom > window.innerHeight) {
256
+ dropdown.style.top = `${anchorRect.top + window.scrollY - ddRect.height - 2}px`;
257
+ }
258
+ });
259
+ state.layerDropdown = dropdown;
260
+ const handleKeydown = (e) => {
261
+ if (!state.layerDropdown) return;
262
+ switch (e.key) {
263
+ case "ArrowDown":
264
+ e.preventDefault();
265
+ focusedIndex = Math.min(focusedIndex + 1, items.length - 1);
266
+ highlightItem(items, focusedIndex, layers, currentElement);
267
+ break;
268
+ case "ArrowUp":
269
+ e.preventDefault();
270
+ focusedIndex = Math.max(focusedIndex - 1, 0);
271
+ highlightItem(items, focusedIndex, layers, currentElement);
272
+ break;
273
+ case "Enter":
274
+ e.preventDefault();
275
+ if (focusedIndex >= 0 && focusedIndex < layers.length) {
276
+ removeLayerDropdown(state);
277
+ onSelectElement(layers[focusedIndex].element);
278
+ }
279
+ break;
280
+ case "Escape":
281
+ e.preventDefault();
282
+ removeLayerDropdown(state);
283
+ break;
284
+ }
285
+ };
286
+ handleLayerKeyboard.current = handleKeydown;
287
+ document.addEventListener("keydown", handleLayerKeyboard);
288
+ }
289
+ function handleLayerKeyboard(e) {
290
+ const current = handleLayerKeyboard.current;
291
+ if (current) current(e);
292
+ }
293
+ function highlightItem(items, index, layers, currentElement) {
294
+ items.forEach((item, i) => {
295
+ if (layers[i].element === currentElement) {
296
+ item.style.color = "#000000";
297
+ item.style.backgroundColor = i === index ? "rgba(255, 204, 0, 0.35)" : "rgba(255, 204, 0, 0.2)";
298
+ item.style.fontWeight = "600";
299
+ } else {
300
+ item.style.backgroundColor = i === index ? "#f1f5f9" : "transparent";
301
+ item.style.color = i === index ? "#334155" : "#64748b";
302
+ item.style.fontWeight = "normal";
303
+ }
304
+ });
305
+ }
306
+
307
+ // src/runtime/messages.ts
308
+ function reportElementSelected(state, element) {
309
+ const el = element;
310
+ const tagName = element.tagName;
311
+ const isTextElement = TEXT_TAGS.includes(tagName);
312
+ const computed = window.getComputedStyle(el);
313
+ const computedStyles = {
314
+ fontFamily: computed.fontFamily,
315
+ fontSize: computed.fontSize,
316
+ fontWeight: computed.fontWeight,
317
+ textAlign: computed.textAlign,
318
+ textDecoration: computed.textDecoration,
319
+ textDecorationLine: computed.textDecorationLine,
320
+ color: computed.color,
321
+ backgroundColor: computed.backgroundColor,
322
+ lineHeight: computed.lineHeight,
323
+ letterSpacing: computed.letterSpacing,
324
+ opacity: computed.opacity
325
+ };
326
+ try {
327
+ window.parent.postMessage(
328
+ {
329
+ type: "element-selected",
330
+ tagName,
331
+ classes: typeof el.className === "string" ? el.className : "",
332
+ visualSelectorId: getSourceId(element),
333
+ content: isTextElement ? el.innerText : void 0,
334
+ dataSourceLocation: el.dataset?.sourceLocation,
335
+ isDynamicContent: el.dataset?.dynamicContent === "true",
336
+ position: getElementPosition(element),
337
+ attributes: {},
338
+ isTextElement,
339
+ staticArrayName: null,
340
+ collectionId: null,
341
+ computedStyles
342
+ },
343
+ state.targetOrigin
344
+ );
345
+ } catch {
346
+ }
347
+ }
348
+ function reportPositionUpdate(state) {
349
+ if (!state.selectedId || !state.selectedElement?.isConnected) return;
350
+ const rect = state.selectedElement.getBoundingClientRect();
351
+ const isInViewport = rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
352
+ try {
353
+ window.parent.postMessage(
354
+ {
355
+ type: "element-position-update",
356
+ position: getElementPosition(state.selectedElement),
357
+ isInViewport,
358
+ visualSelectorId: state.selectedId
359
+ },
360
+ state.targetOrigin
361
+ );
362
+ } catch {
363
+ }
364
+ }
365
+ function handleMessage(state, e, callbacks) {
366
+ if (state.targetOrigin !== "*" && e.origin !== state.targetOrigin) return;
367
+ const msg = e.data;
368
+ if (!msg || typeof msg !== "object" || !msg.type) return;
369
+ switch (msg.type) {
370
+ case "toggle-visual-edit-mode":
371
+ callbacks.enableEditMode(msg.data?.enabled ?? false, msg.data?.specs);
372
+ break;
373
+ case "update-classes":
374
+ if (msg.data?.visualSelectorId && typeof msg.data?.classes === "string") {
375
+ findAllElementsById(msg.data.visualSelectorId).forEach(
376
+ (el) => el.setAttribute("class", msg.data.classes)
377
+ );
378
+ setTimeout(callbacks.updateAllOverlayPositions, 50);
379
+ }
380
+ break;
381
+ case "update-attribute":
382
+ if (msg.data?.visualSelectorId && msg.data?.attribute && msg.data?.value !== void 0) {
383
+ findAllElementsById(msg.data.visualSelectorId).forEach(
384
+ (el) => el.setAttribute(msg.data.attribute, msg.data.value)
385
+ );
386
+ }
387
+ break;
388
+ case "update-content":
389
+ if (msg.data?.visualSelectorId && msg.data?.content !== void 0) {
390
+ findAllElementsById(msg.data.visualSelectorId).forEach((el) => {
391
+ el.innerText = msg.data.content;
392
+ });
393
+ setTimeout(callbacks.updateAllOverlayPositions, 50);
394
+ }
395
+ break;
396
+ case "unselect-element":
397
+ if (state.editingElement) callbacks.stopInlineEditing();
398
+ state.selectionOverlays.forEach((o) => o.remove());
399
+ state.selectionOverlays = [];
400
+ state.selectedId = null;
401
+ state.selectedElement = null;
402
+ break;
403
+ case "update-theme-variables":
404
+ if (msg.data?.variables) {
405
+ const root = msg.data.mode === "dark" ? document.querySelector(".dark") : document.documentElement;
406
+ if (root) {
407
+ for (const [name, value] of Object.entries(msg.data.variables)) {
408
+ root.style.setProperty(name, value);
409
+ }
410
+ }
411
+ }
412
+ break;
413
+ case "toggle-inline-edit-mode":
414
+ if (msg.data) {
415
+ callbacks.handleInlineEdit(msg.data);
416
+ }
417
+ break;
418
+ case "inject-font-import":
419
+ if (msg.data?.fontUrl && /^https?:\/\//.test(msg.data.fontUrl)) {
420
+ const link = document.createElement("link");
421
+ link.rel = "stylesheet";
422
+ link.href = msg.data.fontUrl;
423
+ link.setAttribute(AGENT_ATTR, "");
424
+ document.head.appendChild(link);
425
+ }
426
+ break;
427
+ case "popover-drag-state":
428
+ case "dropdown-state":
429
+ break;
430
+ case "refresh-page":
431
+ window.location.reload();
432
+ break;
433
+ case "request-element-position":
434
+ reportPositionUpdate(state);
435
+ break;
436
+ case CONTROL_MESSAGE_TYPE: {
437
+ const controlMsg = msg;
438
+ if (typeof controlMsg.active === "boolean") {
439
+ callbacks.enableEditMode(controlMsg.active);
440
+ }
441
+ break;
442
+ }
443
+ case "update-element-style":
444
+ case "update-element-text":
445
+ case "update-element-class":
446
+ handleLegacyUpdateMessage(state, msg);
447
+ break;
20
448
  }
21
- return target.closest(`[${attributeName}]`);
22
449
  }
23
- function findMatchingElements(selectorId, attributeName, root) {
24
- return Array.from(
25
- root.querySelectorAll(`[${attributeName}="${selectorId}"]`)
450
+ function handleLegacyUpdateMessage(state, msg) {
451
+ if (!msg.dataSourceLocation) return;
452
+ const el = document.querySelector(
453
+ `[${state.attributeName}="${msg.dataSourceLocation}"]`
26
454
  );
455
+ if (!el) return;
456
+ if (msg.type === "update-element-style" && msg.styles) {
457
+ for (const [prop, value] of Object.entries(msg.styles)) {
458
+ el.style.setProperty(
459
+ prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`),
460
+ value
461
+ );
462
+ }
463
+ } else if (msg.type === "update-element-text" && typeof msg.text === "string") {
464
+ el.textContent = msg.text;
465
+ } else if (msg.type === "update-element-class" && typeof msg.className === "string") {
466
+ el.className = msg.className;
467
+ }
27
468
  }
28
- function createOverlay(mode, doc) {
29
- const overlay = doc.createElement("div");
30
- overlay.setAttribute(OVERLAY_ATTRIBUTE_NAME, mode);
469
+
470
+ // src/runtime/overlay.ts
471
+ function createOverlay(opts) {
472
+ const overlay = document.createElement("div");
473
+ overlay.setAttribute(AGENT_ATTR, "");
31
474
  overlay.style.position = "absolute";
32
475
  overlay.style.pointerEvents = "none";
33
476
  overlay.style.zIndex = "9999";
34
- overlay.style.transition = "all 0.1s ease-in-out";
35
- if (mode === "selected") {
36
- overlay.style.border = "2px solid #2563EB";
477
+ overlay.style.boxSizing = "border-box";
478
+ if (opts.selected) {
479
+ overlay.style.boxShadow = "inset 0 0 0 2px #FC0";
37
480
  } else {
38
- overlay.style.border = "2px solid #95a5fc";
39
- overlay.style.backgroundColor = "rgba(99, 102, 241, 0.05)";
481
+ overlay.style.boxShadow = "inset 0 0 0 2px rgba(255, 204, 0, 0.5)";
482
+ overlay.style.backgroundColor = "rgba(255, 204, 0, 0.05)";
40
483
  }
41
484
  return overlay;
42
485
  }
43
- function positionOverlay(overlay, target, view) {
486
+ function positionOverlay(overlay, target, tagMode = "none", callbacks) {
44
487
  const rect = target.getBoundingClientRect();
45
- overlay.style.top = `${rect.top + view.scrollY}px`;
46
- overlay.style.left = `${rect.left + view.scrollX}px`;
488
+ overlay.style.top = `${rect.top + window.scrollY}px`;
489
+ overlay.style.left = `${rect.left + window.scrollX}px`;
47
490
  overlay.style.width = `${rect.width}px`;
48
491
  overlay.style.height = `${rect.height}px`;
492
+ if (tagMode !== "none") {
493
+ let tag = overlay.querySelector("[data-tag-label]");
494
+ if (!tag) {
495
+ tag = document.createElement("div");
496
+ tag.setAttribute(AGENT_ATTR, "");
497
+ tag.setAttribute("data-tag-label", "");
498
+ Object.assign(tag.style, {
499
+ position: "absolute",
500
+ padding: "2px 8px",
501
+ fontSize: "11px",
502
+ fontWeight: "500",
503
+ borderRadius: "3px",
504
+ minWidth: "24px",
505
+ textAlign: "center",
506
+ display: "flex",
507
+ alignItems: "center",
508
+ gap: "3px",
509
+ lineHeight: "1.4"
510
+ });
511
+ if (tagMode === "selected") {
512
+ tag.style.backgroundColor = "#FC0";
513
+ tag.style.color = "#000000";
514
+ tag.style.cursor = "pointer";
515
+ tag.style.pointerEvents = "auto";
516
+ const textSpan = document.createElement("span");
517
+ textSpan.textContent = target.tagName.toLowerCase();
518
+ tag.appendChild(textSpan);
519
+ const arrow = document.createElement("span");
520
+ arrow.setAttribute("data-tag-arrow", "");
521
+ arrow.textContent = "\u2304";
522
+ arrow.style.fontSize = "10px";
523
+ arrow.style.lineHeight = "1";
524
+ tag.appendChild(arrow);
525
+ if (callbacks?.onTagClick) {
526
+ const onTagClick = callbacks.onTagClick;
527
+ tag.addEventListener("click", (e) => {
528
+ e.stopPropagation();
529
+ onTagClick(target, tag);
530
+ });
531
+ }
532
+ } else {
533
+ tag.style.backgroundColor = "rgba(255, 204, 0, 0.7)";
534
+ tag.style.color = "#000000";
535
+ tag.style.pointerEvents = "none";
536
+ tag.textContent = target.tagName.toLowerCase();
537
+ }
538
+ overlay.appendChild(tag);
539
+ }
540
+ positionTag(tag, rect);
541
+ }
49
542
  }
50
- function createMessage(type, element, attributeName) {
51
- const rect = element.getBoundingClientRect();
52
- return {
53
- type,
54
- tagName: element.tagName,
55
- className: element.className,
56
- selectorId: getElementSelectorId(element, attributeName),
57
- dataSourceLocation: getElementSelectorId(element, attributeName),
58
- rect: {
59
- top: rect.top,
60
- left: rect.left,
61
- right: rect.right,
62
- bottom: rect.bottom,
63
- width: rect.width,
64
- height: rect.height
543
+ function positionTag(tag, rect) {
544
+ const isNearTop = rect.top < 27;
545
+ const isTallEnough = rect.height >= 54;
546
+ const isFullWidth = rect.width >= window.innerWidth - 4;
547
+ const leftOffset = isFullWidth ? 8 : 4;
548
+ if (isNearTop && isTallEnough) {
549
+ tag.style.top = "2px";
550
+ } else if (isNearTop) {
551
+ tag.style.top = `${rect.height + 2}px`;
552
+ } else {
553
+ tag.style.top = "-27px";
554
+ }
555
+ tag.style.left = `${leftOffset}px`;
556
+ requestAnimationFrame(() => {
557
+ const tagRect = tag.getBoundingClientRect();
558
+ if (tagRect.right > window.innerWidth - 4) {
559
+ const shift = tagRect.right - window.innerWidth + 8;
560
+ tag.style.left = `${leftOffset - shift}px`;
65
561
  }
66
- };
562
+ if (tagRect.left < 4) {
563
+ const shift = 4 - tagRect.left;
564
+ tag.style.left = `${leftOffset + shift}px`;
565
+ }
566
+ });
67
567
  }
68
- function setupSelectionBridge(options = {}) {
69
- activeCleanup?.();
70
- const targetWindow = options.targetWindow ?? window;
71
- const targetDocument = targetWindow.document;
72
- const parentWindow = options.parentWindow ?? window.parent;
73
- const attributeName = options.attributeName ?? DEFAULT_ATTRIBUTE_NAME;
74
- const targetOrigin = options.targetOrigin ?? "*";
75
- const enableHover = options.enableHover ?? true;
76
- const enableClickSelection = options.enableClickSelection ?? true;
77
- let selectedOverlays = [];
78
- let hoverOverlays = [];
79
- function clearOverlays(overlays) {
80
- overlays.forEach((overlay) => overlay.remove());
81
- }
82
- function renderOverlays(mode, sourceElement) {
83
- const selectorId = getElementSelectorId(sourceElement, attributeName);
84
- if (!selectorId) {
85
- return [];
86
- }
87
- return findMatchingElements(selectorId, attributeName, targetDocument).map((element) => {
88
- const overlay = createOverlay(mode, targetDocument);
89
- positionOverlay(overlay, element, targetWindow);
90
- targetDocument.body.appendChild(overlay);
91
- return overlay;
568
+ function clearHoverOverlays(state) {
569
+ state.hoverOverlays.forEach((o) => o.remove());
570
+ state.hoverOverlays = [];
571
+ state.currentHoverId = null;
572
+ }
573
+ function clearSelectionOverlays(state, removeDropdown) {
574
+ state.selectionOverlays.forEach((o) => o.remove());
575
+ state.selectionOverlays = [];
576
+ removeDropdown();
577
+ }
578
+ function clearAllOverlays(state, removeDropdown) {
579
+ clearHoverOverlays(state);
580
+ clearSelectionOverlays(state, removeDropdown);
581
+ state.selectedId = null;
582
+ state.selectedElement = null;
583
+ }
584
+ function freezeAnimations() {
585
+ document.documentElement.setAttribute("data-visual-edit-active", "");
586
+ if (!document.getElementById("freeze-animations")) {
587
+ const style = document.createElement("style");
588
+ style.id = "freeze-animations";
589
+ style.setAttribute(AGENT_ATTR, "");
590
+ style.textContent = `
591
+ [data-visual-edit-active] *:not([${AGENT_ATTR}]):not([${AGENT_ATTR}] *),
592
+ [data-visual-edit-active] *:not([${AGENT_ATTR}]):not([${AGENT_ATTR}] *)::before,
593
+ [data-visual-edit-active] *:not([${AGENT_ATTR}]):not([${AGENT_ATTR}] *)::after {
594
+ animation-play-state: paused !important;
595
+ transition: none !important;
596
+ }
597
+ `;
598
+ document.head.appendChild(style);
599
+ }
600
+ if (!document.getElementById("freeze-overflow")) {
601
+ const overflowStyle = document.createElement("style");
602
+ overflowStyle.id = "freeze-overflow";
603
+ overflowStyle.setAttribute(AGENT_ATTR, "");
604
+ overflowStyle.textContent = `
605
+ html[data-visual-edit-active] {
606
+ overflow-y: scroll !important;
607
+ overflow-x: hidden !important;
608
+ }
609
+ `;
610
+ document.head.appendChild(overflowStyle);
611
+ }
612
+ if (!document.getElementById("freeze-pointer-events")) {
613
+ const pointerStyle = document.createElement("style");
614
+ pointerStyle.id = "freeze-pointer-events";
615
+ pointerStyle.setAttribute(AGENT_ATTR, "");
616
+ pointerStyle.textContent = `
617
+ [data-visual-edit-active] * { pointer-events: none !important; }
618
+ [${AGENT_ATTR}], [${AGENT_ATTR}] * { pointer-events: auto !important; }
619
+ `;
620
+ document.head.appendChild(pointerStyle);
621
+ }
622
+ try {
623
+ document.getAnimations().forEach((anim) => {
624
+ const effect = anim.effect;
625
+ const target = effect?.target;
626
+ if (target instanceof Element && target.closest(`[${AGENT_ATTR}]`)) return;
627
+ try {
628
+ anim.finish();
629
+ } catch {
630
+ anim.pause();
631
+ }
92
632
  });
633
+ } catch {
93
634
  }
94
- function onClick(event) {
95
- if (!enableClickSelection) {
96
- return;
635
+ }
636
+ function unfreezeAnimations() {
637
+ document.documentElement.removeAttribute("data-visual-edit-active");
638
+ const freezeStyle = document.getElementById("freeze-animations");
639
+ if (freezeStyle) freezeStyle.remove();
640
+ const pointerStyle = document.getElementById("freeze-pointer-events");
641
+ if (pointerStyle) pointerStyle.remove();
642
+ const overflowStyle = document.getElementById("freeze-overflow");
643
+ if (overflowStyle) overflowStyle.remove();
644
+ }
645
+
646
+ // src/runtime/state.ts
647
+ function createAgentState(options = {}) {
648
+ return {
649
+ editModeEnabled: false,
650
+ selectedId: null,
651
+ selectedElement: null,
652
+ hoverOverlays: [],
653
+ selectionOverlays: [],
654
+ currentHoverId: null,
655
+ editingElement: null,
656
+ layerDropdown: null,
657
+ mutationObserver: null,
658
+ attributeName: options.attributeName ?? DEFAULT_ATTRIBUTE_NAME,
659
+ targetOrigin: options.targetOrigin ?? "*"
660
+ };
661
+ }
662
+
663
+ // src/runtime/index.ts
664
+ function setupVisualEditAgent(options = {}) {
665
+ const state = createAgentState(options);
666
+ function updateAllOverlayPositions() {
667
+ if (state.selectedId && state.selectedElement?.isConnected) {
668
+ const siblings = findAllElementsById(state.selectedId);
669
+ state.selectionOverlays.forEach((overlay, i) => {
670
+ if (siblings[i]) {
671
+ positionOverlay(overlay, siblings[i], i === 0 ? "selected" : "none", {
672
+ onTagClick
673
+ });
674
+ }
675
+ });
97
676
  }
98
- const selectableElement = getSelectableElement(event.target, attributeName);
99
- if (!selectableElement || isOverlayElement(selectableElement)) {
100
- return;
677
+ reportPositionUpdate(state);
678
+ }
679
+ function onTagClick(target, tag) {
680
+ const arrowEl = tag.querySelector("[data-tag-arrow]");
681
+ if (arrowEl) {
682
+ arrowEl.textContent = state.layerDropdown ? "\u2304" : "\u2303";
101
683
  }
102
- clearOverlays(selectedOverlays);
103
- selectedOverlays = renderOverlays("selected", selectableElement);
104
- parentWindow.postMessage(
105
- createMessage("element-selected", selectableElement, attributeName),
106
- targetOrigin
107
- );
684
+ toggleLayerDropdown(state, target, tag, selectElement);
108
685
  }
109
- function onMouseMove(event) {
110
- if (!enableHover) {
686
+ function findElementAtPoint(x, y) {
687
+ const freezeStyle = document.getElementById("freeze-pointer-events");
688
+ if (freezeStyle) freezeStyle.disabled = true;
689
+ const element = document.elementFromPoint(x, y);
690
+ if (freezeStyle) freezeStyle.disabled = false;
691
+ if (!element) return null;
692
+ if (element.closest(`[${AGENT_ATTR}]`)) return null;
693
+ return element.closest(`[${state.attributeName}], [data-visual-selector-id]`) ?? null;
694
+ }
695
+ function findHoverTarget(x, y, excludeId) {
696
+ const element = findElementAtPoint(x, y);
697
+ if (!element) return null;
698
+ const id = getSourceId(element);
699
+ if (!id || id === excludeId) return null;
700
+ return id;
701
+ }
702
+ function onMouseMove(e) {
703
+ if (!state.editModeEnabled) return;
704
+ requestAnimationFrame(() => {
705
+ const id = findHoverTarget(e.clientX, e.clientY, state.selectedId);
706
+ if (!id) {
707
+ clearHoverOverlays(state);
708
+ return;
709
+ }
710
+ if (id === state.currentHoverId) return;
711
+ clearHoverOverlays(state);
712
+ const elements = findAllElementsById(id);
713
+ elements.forEach((el, i) => {
714
+ const overlay = createOverlay({ selected: false });
715
+ overlay.setAttribute(AGENT_ATTR, "");
716
+ document.body.appendChild(overlay);
717
+ positionOverlay(overlay, el, i === 0 ? "hover" : "none");
718
+ state.hoverOverlays.push(overlay);
719
+ });
720
+ state.currentHoverId = id;
721
+ });
722
+ }
723
+ function onMouseLeave() {
724
+ clearHoverOverlays(state);
725
+ }
726
+ function onClick(e) {
727
+ if (!state.editModeEnabled) return;
728
+ const target = e.target;
729
+ if (target?.closest?.(`[data-layer-dropdown]`)) return;
730
+ if (target?.closest?.(`[data-tag-label]`) && state.selectionOverlays.some((o) => o.contains(target)))
111
731
  return;
732
+ e.preventDefault();
733
+ e.stopPropagation();
734
+ e.stopImmediatePropagation();
735
+ clearHoverOverlays(state);
736
+ const element = findElementAtPoint(e.clientX, e.clientY);
737
+ if (!element) return;
738
+ selectElement(element);
739
+ }
740
+ function selectElement(element) {
741
+ const id = getSourceId(element);
742
+ if (!id) return;
743
+ if (state.editingElement) {
744
+ stopInlineEditing(state, updateAllOverlayPositions);
112
745
  }
113
- const selectableElement = getSelectableElement(event.target, attributeName);
114
- clearOverlays(hoverOverlays);
115
- hoverOverlays = [];
116
- if (!selectableElement || isOverlayElement(selectableElement)) {
117
- return;
746
+ clearSelectionOverlays(state, () => removeLayerDropdown(state));
747
+ const siblings = findAllElementsById(id);
748
+ siblings.forEach((el, i) => {
749
+ const overlay = createOverlay({ selected: true });
750
+ overlay.setAttribute(AGENT_ATTR, "");
751
+ document.body.appendChild(overlay);
752
+ positionOverlay(overlay, el, i === 0 ? "selected" : "none", {
753
+ onTagClick
754
+ });
755
+ state.selectionOverlays.push(overlay);
756
+ });
757
+ state.selectedId = id;
758
+ state.selectedElement = element;
759
+ clearHoverOverlays(state);
760
+ reportElementSelected(state, element);
761
+ }
762
+ let rafPending = false;
763
+ const throttledPositionUpdate = () => {
764
+ if (rafPending) return;
765
+ rafPending = true;
766
+ requestAnimationFrame(() => {
767
+ rafPending = false;
768
+ reportPositionUpdate(state);
769
+ });
770
+ };
771
+ function enableEditMode(enabled, _specs) {
772
+ state.editModeEnabled = enabled;
773
+ if (enabled) {
774
+ document.body.style.cursor = "crosshair";
775
+ freezeAnimations();
776
+ document.addEventListener("mousemove", onMouseMove);
777
+ document.addEventListener("mouseleave", onMouseLeave);
778
+ document.addEventListener("click", onClick, true);
779
+ window.addEventListener("scroll", throttledPositionUpdate, true);
780
+ document.addEventListener("scroll", throttledPositionUpdate, true);
781
+ window.addEventListener("resize", updateAllOverlayPositions);
782
+ startMutationObserver();
783
+ } else {
784
+ document.body.style.cursor = "default";
785
+ unfreezeAnimations();
786
+ clearAllOverlays(state, () => removeLayerDropdown(state));
787
+ if (state.editingElement) {
788
+ stopInlineEditing(state, updateAllOverlayPositions);
789
+ }
790
+ document.removeEventListener("mousemove", onMouseMove);
791
+ document.removeEventListener("mouseleave", onMouseLeave);
792
+ document.removeEventListener("click", onClick, true);
793
+ window.removeEventListener("scroll", throttledPositionUpdate, true);
794
+ document.removeEventListener("scroll", throttledPositionUpdate, true);
795
+ window.removeEventListener("resize", updateAllOverlayPositions);
796
+ stopMutationObserver();
118
797
  }
119
- hoverOverlays = renderOverlays("hover", selectableElement);
120
798
  }
121
- targetDocument.addEventListener("click", onClick, true);
122
- targetDocument.addEventListener("mousemove", onMouseMove, true);
123
- const cleanup = () => {
124
- targetDocument.removeEventListener("click", onClick, true);
125
- targetDocument.removeEventListener("mousemove", onMouseMove, true);
126
- clearOverlays(selectedOverlays);
127
- clearOverlays(hoverOverlays);
128
- selectedOverlays = [];
129
- hoverOverlays = [];
130
- if (activeCleanup === cleanup) {
131
- activeCleanup = null;
799
+ function startMutationObserver() {
800
+ if (state.mutationObserver) return;
801
+ state.mutationObserver = new MutationObserver((mutations) => {
802
+ const hasRelevantChange = mutations.some((m) => {
803
+ if (m.type === "attributes" && ["style", "class", "width", "height"].includes(m.attributeName ?? "") && containsTrackedElement(m.target))
804
+ return true;
805
+ if (m.type === "childList" && containsTrackedElement(m.target)) return true;
806
+ return false;
807
+ });
808
+ if (hasRelevantChange) {
809
+ setTimeout(updateAllOverlayPositions, 50);
810
+ }
811
+ });
812
+ state.mutationObserver.observe(document.body, {
813
+ attributes: true,
814
+ childList: true,
815
+ subtree: true,
816
+ attributeFilter: ["style", "class", "width", "height"]
817
+ });
818
+ }
819
+ function stopMutationObserver() {
820
+ if (state.mutationObserver) {
821
+ state.mutationObserver.disconnect();
822
+ state.mutationObserver = null;
132
823
  }
824
+ }
825
+ function containsTrackedElement(node) {
826
+ if (!(node instanceof Element)) return false;
827
+ if (node.hasAttribute(AGENT_ATTR)) return false;
828
+ return hasSourceLocation(node) || !!node.querySelector(`[${state.attributeName}]`);
829
+ }
830
+ function setupSandboxMountObserver() {
831
+ if (window.self === window.top) return;
832
+ const mountObserver = new MutationObserver((mutations) => {
833
+ const hasChanges = mutations.some(
834
+ (m) => m.addedNodes.length > 0 || m.removedNodes.length > 0
835
+ );
836
+ if (!hasChanges) return;
837
+ const hasTrackedElements = document.body.querySelectorAll("[data-source-location], [data-dynamic-content]").length > 0;
838
+ try {
839
+ window.parent.postMessage(
840
+ {
841
+ type: hasTrackedElements ? "sandbox:onMounted" : "sandbox:onUnmounted"
842
+ },
843
+ state.targetOrigin
844
+ );
845
+ } catch {
846
+ }
847
+ });
848
+ if (document.body) {
849
+ mountObserver.observe(document.body, { childList: true, subtree: true });
850
+ } else {
851
+ document.addEventListener("DOMContentLoaded", () => {
852
+ mountObserver.observe(document.body, { childList: true, subtree: true });
853
+ });
854
+ }
855
+ }
856
+ const onMessage = (e) => {
857
+ handleMessage(state, e, {
858
+ enableEditMode,
859
+ updateAllOverlayPositions,
860
+ stopInlineEditing: () => stopInlineEditing(state, updateAllOverlayPositions),
861
+ handleInlineEdit: (data) => handleInlineEdit(state, data, updateAllOverlayPositions)
862
+ });
133
863
  };
134
- activeCleanup = cleanup;
864
+ window.addEventListener("message", onMessage);
865
+ setupSandboxMountObserver();
866
+ try {
867
+ window.parent.postMessage({ type: "visual-edit-agent-ready" }, state.targetOrigin);
868
+ } catch {
869
+ }
135
870
  return {
136
871
  destroy() {
137
- cleanup();
872
+ window.removeEventListener("message", onMessage);
873
+ if (state.editModeEnabled) {
874
+ enableEditMode(false);
875
+ }
138
876
  },
877
+ enableEditMode,
878
+ selectElement,
139
879
  clearSelection() {
140
- clearOverlays(selectedOverlays);
141
- clearOverlays(hoverOverlays);
142
- selectedOverlays = [];
143
- hoverOverlays = [];
880
+ clearAllOverlays(state, () => removeLayerDropdown(state));
881
+ },
882
+ getSelectedId() {
883
+ return state.selectedId;
144
884
  }
145
885
  };
146
886
  }
147
887
  export {
148
- setupSelectionBridge
888
+ setupVisualEditAgent
149
889
  };
150
890
  //# sourceMappingURL=runtime.js.map