@base44-preview/vite-plugin 0.2.25-pr.36.792e85b → 0.2.26-pr.42.3359429

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.
Files changed (27) hide show
  1. package/dist/capabilities/inline-edit/controller.d.ts +3 -0
  2. package/dist/capabilities/inline-edit/controller.d.ts.map +1 -0
  3. package/dist/capabilities/inline-edit/controller.js +165 -0
  4. package/dist/capabilities/inline-edit/controller.js.map +1 -0
  5. package/dist/capabilities/inline-edit/dom-utils.d.ts +6 -0
  6. package/dist/capabilities/inline-edit/dom-utils.d.ts.map +1 -0
  7. package/dist/capabilities/inline-edit/dom-utils.js +49 -0
  8. package/dist/capabilities/inline-edit/dom-utils.js.map +1 -0
  9. package/dist/capabilities/inline-edit/index.d.ts +3 -0
  10. package/dist/capabilities/inline-edit/index.d.ts.map +1 -0
  11. package/dist/capabilities/inline-edit/index.js +2 -0
  12. package/dist/capabilities/inline-edit/index.js.map +1 -0
  13. package/dist/capabilities/inline-edit/types.d.ts +18 -0
  14. package/dist/capabilities/inline-edit/types.d.ts.map +1 -0
  15. package/dist/capabilities/inline-edit/types.js +2 -0
  16. package/dist/capabilities/inline-edit/types.js.map +1 -0
  17. package/dist/injections/visual-edit-agent.d.ts.map +1 -1
  18. package/dist/injections/visual-edit-agent.js +85 -11
  19. package/dist/injections/visual-edit-agent.js.map +1 -1
  20. package/dist/statics/index.mjs +5 -1
  21. package/dist/statics/index.mjs.map +1 -1
  22. package/package.json +1 -1
  23. package/src/capabilities/inline-edit/controller.ts +215 -0
  24. package/src/capabilities/inline-edit/dom-utils.ts +48 -0
  25. package/src/capabilities/inline-edit/index.ts +2 -0
  26. package/src/capabilities/inline-edit/types.ts +22 -0
  27. package/src/injections/visual-edit-agent.ts +98 -11
@@ -0,0 +1,215 @@
1
+ import type { InlineEditHost, InlineEditController } from "./types.js";
2
+ import {
3
+ injectFocusOutlineCSS,
4
+ removeFocusOutlineCSS,
5
+ selectText,
6
+ shouldEnterInlineEditingMode,
7
+ } from "./dom-utils.js";
8
+
9
+ const DEBOUNCE_MS = 500;
10
+
11
+ export function createInlineEditController(
12
+ host: InlineEditHost
13
+ ): InlineEditController {
14
+ let currentEditingElement: HTMLElement | null = null;
15
+ let debouncedSendTimeout: ReturnType<typeof setTimeout> | null = null;
16
+ let enabled = false;
17
+ const listenerAbortControllers = new WeakMap<HTMLElement, AbortController>();
18
+
19
+ // --- Private helpers ---
20
+
21
+ const repositionOverlays = () => {
22
+ const selectedId = host.getSelectedElementId();
23
+ if (!selectedId) return;
24
+ const elements = host.findElementsById(selectedId);
25
+ const overlays = host.getSelectedOverlays();
26
+ overlays.forEach((overlay, i) => {
27
+ if (i < elements.length) {
28
+ host.positionOverlay(overlay, elements[i]!);
29
+ }
30
+ });
31
+ };
32
+
33
+ const reportEdit = (element: HTMLElement) => {
34
+ const originalContent = element.dataset.originalTextContent;
35
+ const newContent = element.textContent;
36
+
37
+ const svgElement = element as unknown as SVGElement;
38
+ const rect = element.getBoundingClientRect();
39
+
40
+ window.parent.postMessage(
41
+ {
42
+ type: "inline-edit",
43
+ elementInfo: {
44
+ tagName: element.tagName,
45
+ classes:
46
+ (svgElement.className as unknown as SVGAnimatedString)?.baseVal ||
47
+ element.className ||
48
+ "",
49
+ visualSelectorId: host.getSelectedElementId(),
50
+ content: newContent,
51
+ dataSourceLocation: element.dataset.sourceLocation,
52
+ isDynamicContent: element.dataset.dynamicContent === "true",
53
+ linenumber: element.dataset.linenumber,
54
+ filename: element.dataset.filename,
55
+ position: {
56
+ top: rect.top,
57
+ left: rect.left,
58
+ right: rect.right,
59
+ bottom: rect.bottom,
60
+ width: rect.width,
61
+ height: rect.height,
62
+ centerX: rect.left + rect.width / 2,
63
+ centerY: rect.top + rect.height / 2,
64
+ },
65
+ },
66
+ originalContent,
67
+ newContent,
68
+ },
69
+ "*"
70
+ );
71
+
72
+ element.dataset.originalTextContent = newContent || "";
73
+ };
74
+
75
+ const debouncedReport = (element: HTMLElement) => {
76
+ if (debouncedSendTimeout) clearTimeout(debouncedSendTimeout);
77
+ debouncedSendTimeout = setTimeout(() => reportEdit(element), DEBOUNCE_MS);
78
+ };
79
+
80
+ const onTextInput = (element: HTMLElement) => {
81
+ repositionOverlays();
82
+ debouncedReport(element);
83
+ };
84
+
85
+ const handleInputEvent = function (this: HTMLElement) {
86
+ onTextInput(this);
87
+ };
88
+
89
+ const makeEditable = (element: HTMLElement) => {
90
+ injectFocusOutlineCSS();
91
+
92
+ element.dataset.originalTextContent = element.textContent || "";
93
+ element.dataset.originalCursor = element.style.cursor;
94
+ element.contentEditable = "true";
95
+
96
+ const abortController = new AbortController();
97
+ listenerAbortControllers.set(element, abortController);
98
+ element.addEventListener("input", handleInputEvent, {
99
+ signal: abortController.signal,
100
+ });
101
+
102
+ element.style.cursor = "text";
103
+ selectText(element);
104
+ setTimeout(() => element.focus(), 0);
105
+ };
106
+
107
+ const makeNonEditable = (element: HTMLElement) => {
108
+ const abortController = listenerAbortControllers.get(element);
109
+ if (abortController) {
110
+ abortController.abort();
111
+ listenerAbortControllers.delete(element);
112
+ }
113
+
114
+ if (!element.isConnected) return;
115
+
116
+ removeFocusOutlineCSS();
117
+ element.contentEditable = "false";
118
+ delete element.dataset.originalTextContent;
119
+
120
+ if (element.dataset.originalCursor !== undefined) {
121
+ element.style.cursor = element.dataset.originalCursor;
122
+ delete element.dataset.originalCursor;
123
+ }
124
+ };
125
+
126
+ // --- Public API ---
127
+
128
+ return {
129
+ get enabled() {
130
+ return enabled;
131
+ },
132
+ set enabled(value: boolean) {
133
+ enabled = value;
134
+ },
135
+
136
+ isEditing() {
137
+ return currentEditingElement !== null;
138
+ },
139
+
140
+ getCurrentElement() {
141
+ return currentEditingElement;
142
+ },
143
+
144
+ canEdit(element: Element) {
145
+ return shouldEnterInlineEditingMode(element);
146
+ },
147
+
148
+ startEditing(element: HTMLElement) {
149
+ currentEditingElement = element;
150
+
151
+ host.getSelectedOverlays().forEach((o) => {
152
+ o.style.display = "none";
153
+ });
154
+
155
+ makeEditable(element);
156
+
157
+ window.parent.postMessage(
158
+ {
159
+ type: "content-editing-started",
160
+ visualSelectorId: host.getSelectedElementId(),
161
+ },
162
+ "*"
163
+ );
164
+ },
165
+
166
+ stopEditing() {
167
+ if (!currentEditingElement) return;
168
+
169
+ if (debouncedSendTimeout) {
170
+ clearTimeout(debouncedSendTimeout);
171
+ debouncedSendTimeout = null;
172
+ }
173
+
174
+ const element = currentEditingElement;
175
+ makeNonEditable(element);
176
+
177
+ host.getSelectedOverlays().forEach((o) => {
178
+ o.style.display = "";
179
+ });
180
+
181
+ repositionOverlays();
182
+
183
+ window.parent.postMessage(
184
+ {
185
+ type: "content-editing-ended",
186
+ visualSelectorId: host.getSelectedElementId(),
187
+ },
188
+ "*"
189
+ );
190
+
191
+ currentEditingElement = null;
192
+ },
193
+
194
+ markElementsSelected(elements: Element[]) {
195
+ elements.forEach((el) => {
196
+ if (el instanceof HTMLElement) {
197
+ el.dataset.selected = "true";
198
+ }
199
+ });
200
+ },
201
+
202
+ clearSelectedMarks(elementId: string | null) {
203
+ if (!elementId) return;
204
+ host.findElementsById(elementId).forEach((el) => {
205
+ if (el instanceof HTMLElement) {
206
+ delete el.dataset.selected;
207
+ }
208
+ });
209
+ },
210
+
211
+ cleanup() {
212
+ this.stopEditing();
213
+ },
214
+ };
215
+ }
@@ -0,0 +1,48 @@
1
+ const FOCUS_STYLE_ID = "visual-edit-focus-styles";
2
+
3
+ const EDITABLE_TAGS = [
4
+ "div", "p", "h1", "h2", "h3", "h4", "h5", "h6",
5
+ "span", "li", "td", "a", "button", "label",
6
+ ];
7
+
8
+ export const injectFocusOutlineCSS = () => {
9
+ if (document.getElementById(FOCUS_STYLE_ID)) return;
10
+
11
+ const style = document.createElement("style");
12
+ style.id = FOCUS_STYLE_ID;
13
+ style.textContent = `
14
+ [data-selected="true"][contenteditable="true"]:focus {
15
+ outline: none !important;
16
+ }
17
+ `;
18
+ document.head.appendChild(style);
19
+ };
20
+
21
+ export const removeFocusOutlineCSS = () => {
22
+ document.getElementById(FOCUS_STYLE_ID)?.remove();
23
+ };
24
+
25
+ export const selectText = (element: HTMLElement) => {
26
+ const range = document.createRange();
27
+ range.selectNodeContents(element);
28
+ const selection = window.getSelection();
29
+ selection?.removeAllRanges();
30
+ selection?.addRange(range);
31
+ };
32
+
33
+ export const isEditableTextElement = (element: Element): boolean => {
34
+ if (!(element instanceof HTMLElement)) return false;
35
+ if (!EDITABLE_TAGS.includes(element.tagName.toLowerCase())) return false;
36
+ if (!element.textContent?.trim()) return false;
37
+ if (element.querySelector("img, video, canvas, svg")) return false;
38
+ if (element.children.length > 0) return false;
39
+ if (element.dataset.dynamicContent === "true") return false;
40
+ return true;
41
+ };
42
+
43
+ export const shouldEnterInlineEditingMode = (element: Element): boolean => {
44
+ if (!(element instanceof HTMLElement) || element.dataset.selected !== "true") {
45
+ return false;
46
+ }
47
+ return isEditableTextElement(element);
48
+ };
@@ -0,0 +1,2 @@
1
+ export { createInlineEditController } from "./controller.js";
2
+ export type { InlineEditController, InlineEditHost } from "./types.js";
@@ -0,0 +1,22 @@
1
+ export interface InlineEditHost {
2
+ findElementsById(id: string | null): Element[];
3
+ getSelectedElementId(): string | null;
4
+ getSelectedOverlays(): HTMLDivElement[];
5
+ positionOverlay(
6
+ overlay: HTMLDivElement,
7
+ element: Element,
8
+ isSelected?: boolean
9
+ ): void;
10
+ }
11
+
12
+ export interface InlineEditController {
13
+ enabled: boolean;
14
+ isEditing(): boolean;
15
+ getCurrentElement(): HTMLElement | null;
16
+ canEdit(element: Element): boolean;
17
+ startEditing(element: HTMLElement): void;
18
+ stopEditing(): void;
19
+ markElementsSelected(elements: Element[]): void;
20
+ clearSelectedMarks(elementId: string | null): void;
21
+ cleanup(): void;
22
+ }
@@ -1,6 +1,7 @@
1
1
  import { findElementsById, updateElementClasses, updateElementAttribute, collectAllowedAttributes, ALLOWED_ATTRIBUTES, getElementSelectorId } from "./utils.js";
2
2
  import { createLayerController } from "./layer-dropdown/controller.js";
3
3
  import { LAYER_DROPDOWN_ATTR } from "./layer-dropdown/consts.js";
4
+ import { createInlineEditController } from "../capabilities/inline-edit/index.js";
4
5
 
5
6
  export function setupVisualEditAgent() {
6
7
  // State variables (replacing React useState/useRef)
@@ -12,6 +13,8 @@ export function setupVisualEditAgent() {
12
13
  let currentHighlightedElements: Element[] = [];
13
14
  let selectedElementId: string | null = null;
14
15
 
16
+ const REPOSITION_DELAY_MS = 50;
17
+
15
18
  // Create overlay element
16
19
  const createOverlay = (isSelected = false): HTMLDivElement => {
17
20
  const overlay = document.createElement("div");
@@ -69,6 +72,20 @@ export function setupVisualEditAgent() {
69
72
  }
70
73
  };
71
74
 
75
+ // --- Inline edit controller ---
76
+ const inlineEdit = createInlineEditController({
77
+ findElementsById,
78
+ getSelectedElementId: () => selectedElementId,
79
+ getSelectedOverlays: () => selectedOverlays,
80
+ positionOverlay,
81
+ });
82
+
83
+ const clearSelection = () => {
84
+ inlineEdit.clearSelectedMarks(selectedElementId);
85
+ clearSelectedOverlays();
86
+ selectedElementId = null;
87
+ };
88
+
72
89
  // Clear hover overlays
73
90
  const clearHoverOverlays = () => {
74
91
  hoverOverlays.forEach((overlay) => {
@@ -154,6 +171,8 @@ export function setupVisualEditAgent() {
154
171
  const handleMouseOver = (e: MouseEvent) => {
155
172
  if (!isVisualEditMode || isPopoverDragging) return;
156
173
 
174
+ if (inlineEdit.isEditing()) return;
175
+
157
176
  const target = e.target as Element;
158
177
 
159
178
  // Prevent hover effects when a dropdown is open
@@ -221,6 +240,18 @@ export function setupVisualEditAgent() {
221
240
  // Let layer dropdown clicks pass through without interference
222
241
  if (target.closest(`[${LAYER_DROPDOWN_ATTR}]`)) return;
223
242
 
243
+ if (inlineEdit.enabled && target instanceof HTMLElement && target.contentEditable === "true") {
244
+ return;
245
+ }
246
+
247
+ if (inlineEdit.isEditing()) {
248
+ e.preventDefault();
249
+ e.stopPropagation();
250
+ e.stopImmediatePropagation();
251
+ inlineEdit.stopEditing();
252
+ return;
253
+ }
254
+
224
255
  // Close dropdowns when clicking anywhere in iframe if a dropdown is open
225
256
  if (isDropdownOpen) {
226
257
  e.preventDefault();
@@ -249,14 +280,31 @@ export function setupVisualEditAgent() {
249
280
  return;
250
281
  }
251
282
 
283
+ const htmlElement = element as HTMLElement;
284
+ const visualSelectorId = getElementSelectorId(element);
285
+
286
+ const isAlreadySelected =
287
+ selectedElementId === visualSelectorId &&
288
+ htmlElement.dataset.selected === "true";
289
+
290
+ if (isAlreadySelected && inlineEdit.enabled && inlineEdit.canEdit(htmlElement)) {
291
+ inlineEdit.startEditing(htmlElement);
292
+ return;
293
+ }
294
+
295
+ inlineEdit.stopEditing();
296
+
297
+ if (inlineEdit.enabled) {
298
+ inlineEdit.markElementsSelected(findElementsById(visualSelectorId));
299
+ }
300
+
252
301
  const selectedOverlay = selectElement(element);
253
302
  layerController.attachToOverlay(selectedOverlay, element);
254
303
  };
255
304
 
256
- // Clear the current selection
257
- const clearSelection = () => {
258
- clearSelectedOverlays();
259
- selectedElementId = null;
305
+ const unselectElement = () => {
306
+ inlineEdit.stopEditing();
307
+ clearSelection();
260
308
  };
261
309
 
262
310
  const updateElementClassesAndReposition = (visualSelectorId: string, classes: string) => {
@@ -288,7 +336,7 @@ export function setupVisualEditAgent() {
288
336
  });
289
337
  }
290
338
  }
291
- }, 50);
339
+ }, REPOSITION_DELAY_MS);
292
340
  };
293
341
 
294
342
  // Update element attribute by visual selector ID
@@ -311,7 +359,7 @@ export function setupVisualEditAgent() {
311
359
  }
312
360
  });
313
361
  }
314
- }, 50);
362
+ }, REPOSITION_DELAY_MS);
315
363
  };
316
364
 
317
365
  // Update element content by visual selector ID
@@ -334,7 +382,7 @@ export function setupVisualEditAgent() {
334
382
  }
335
383
  });
336
384
  }
337
- }, 50);
385
+ }, REPOSITION_DELAY_MS);
338
386
  };
339
387
 
340
388
  // --- Layer dropdown controller ---
@@ -356,12 +404,12 @@ export function setupVisualEditAgent() {
356
404
  isVisualEditMode = isEnabled;
357
405
 
358
406
  if (!isEnabled) {
407
+ inlineEdit.stopEditing();
408
+ clearSelection();
359
409
  layerController.cleanup();
360
410
  clearHoverOverlays();
361
- clearSelectedOverlays();
362
411
 
363
412
  currentHighlightedElements = [];
364
- selectedElementId = null;
365
413
  document.body.style.cursor = "default";
366
414
 
367
415
  document.removeEventListener("mouseover", handleMouseOver);
@@ -422,6 +470,9 @@ export function setupVisualEditAgent() {
422
470
  switch (message.type) {
423
471
  case "toggle-visual-edit-mode":
424
472
  toggleVisualEditMode(message.data.enabled);
473
+ if (message.data.specs?.newInlineEditEnabled !== undefined) {
474
+ inlineEdit.enabled = message.data.specs.newInlineEditEnabled;
475
+ }
425
476
  break;
426
477
 
427
478
  case "update-classes":
@@ -459,7 +510,7 @@ export function setupVisualEditAgent() {
459
510
  break;
460
511
 
461
512
  case "unselect-element":
462
- clearSelection();
513
+ unselectElement();
463
514
  break;
464
515
 
465
516
  case "refresh-page":
@@ -537,6 +588,42 @@ export function setupVisualEditAgent() {
537
588
  }
538
589
  break;
539
590
 
591
+ case "toggle-inline-edit-mode":
592
+ if (!inlineEdit.enabled) break;
593
+ if (!message.data || !message.data.dataSourceLocation) break;
594
+
595
+ {
596
+ const elements = findElementsById(message.data.dataSourceLocation);
597
+ if (elements.length === 0 || !(elements[0] instanceof HTMLElement)) break;
598
+
599
+ const element = elements[0];
600
+
601
+ if (message.data.inlineEditingMode) {
602
+ if (inlineEdit.canEdit(element)) {
603
+ if (selectedElementId !== message.data.dataSourceLocation) {
604
+ inlineEdit.stopEditing();
605
+ clearSelection();
606
+ inlineEdit.markElementsSelected(elements);
607
+
608
+ elements.forEach((el) => {
609
+ const overlay = createOverlay(true);
610
+ document.body.appendChild(overlay);
611
+ selectedOverlays.push(overlay);
612
+ positionOverlay(overlay, el, true);
613
+ });
614
+
615
+ selectedElementId = message.data.dataSourceLocation;
616
+ }
617
+ inlineEdit.startEditing(element);
618
+ }
619
+ } else {
620
+ if (inlineEdit.getCurrentElement() === element) {
621
+ inlineEdit.stopEditing();
622
+ }
623
+ }
624
+ }
625
+ break;
626
+
540
627
  default:
541
628
  break;
542
629
  }
@@ -601,7 +688,7 @@ export function setupVisualEditAgent() {
601
688
  });
602
689
 
603
690
  if (needsUpdate) {
604
- setTimeout(handleResize, 50);
691
+ setTimeout(handleResize, REPOSITION_DELAY_MS);
605
692
  }
606
693
  });
607
694