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

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.
@@ -11,6 +11,15 @@ export function setupVisualEditAgent() {
11
11
  let selectedOverlays: HTMLDivElement[] = [];
12
12
  let currentHighlightedElements: Element[] = [];
13
13
  let selectedElementId: string | null = null;
14
+ let currentEditingElement: HTMLElement | null = null;
15
+ let debouncedSendTimeout: ReturnType<typeof setTimeout> | null = null;
16
+ let isInlineEditExperimentEnabled = false;
17
+
18
+ const INLINE_EDIT_DEBOUNCE_MS = 500;
19
+ const REPOSITION_DELAY_MS = 50;
20
+
21
+ // WeakMap to track AbortControllers for each element's input listener
22
+ const listenerAbortControllers = new WeakMap<HTMLElement, AbortController>();
14
23
 
15
24
  // Create overlay element
16
25
  const createOverlay = (isSelected = false): HTMLDivElement => {
@@ -69,6 +78,258 @@ export function setupVisualEditAgent() {
69
78
  }
70
79
  };
71
80
 
81
+ // --- Inline editing utilities ---
82
+
83
+ const injectFocusOutlineCSS = () => {
84
+ const existingStyle = document.getElementById("visual-edit-focus-styles");
85
+ if (existingStyle) return;
86
+
87
+ const style = document.createElement("style");
88
+ style.id = "visual-edit-focus-styles";
89
+ style.textContent = `
90
+ [data-selected="true"][contenteditable="true"]:focus {
91
+ outline: none !important;
92
+ }
93
+ `;
94
+ document.head.appendChild(style);
95
+ };
96
+
97
+ const removeFocusOutlineCSS = () => {
98
+ const style = document.getElementById("visual-edit-focus-styles");
99
+ if (style) {
100
+ style.remove();
101
+ }
102
+ };
103
+
104
+ const selectText = (element: HTMLElement) => {
105
+ const range = document.createRange();
106
+ range.selectNodeContents(element);
107
+ const selection = window.getSelection();
108
+ selection?.removeAllRanges();
109
+ selection?.addRange(range);
110
+ };
111
+
112
+ const isEditableTextElement = (element: Element): boolean => {
113
+ if (!(element instanceof HTMLElement)) {
114
+ return false;
115
+ }
116
+
117
+ const allowedTags = [
118
+ "div", "p", "h1", "h2", "h3", "h4", "h5", "h6",
119
+ "span", "li", "td", "a", "button", "label",
120
+ ];
121
+ if (!allowedTags.includes(element.tagName.toLowerCase())) {
122
+ return false;
123
+ }
124
+
125
+ const textContent = element.textContent?.trim() || "";
126
+ if (textContent.length === 0) {
127
+ return false;
128
+ }
129
+
130
+ if (element.querySelector("img, video, canvas, svg") !== null) {
131
+ return false;
132
+ }
133
+
134
+ if (element.children.length > 0) {
135
+ return false;
136
+ }
137
+
138
+ if (element.dataset.dynamicContent === "true") {
139
+ return false;
140
+ }
141
+
142
+ return true;
143
+ };
144
+
145
+ const shouldEnterInlineEditingMode = (element: Element): boolean => {
146
+ if (!(element instanceof HTMLElement) || element.dataset.selected !== "true") {
147
+ return false;
148
+ }
149
+ return isEditableTextElement(element);
150
+ };
151
+
152
+ const handleInputEvent = function (this: HTMLElement) {
153
+ onTextInputChange(this);
154
+ };
155
+
156
+ const enterInlineEditingMode = (element: HTMLElement) => {
157
+ injectFocusOutlineCSS();
158
+
159
+ element.dataset.originalTextContent = element.textContent || "";
160
+ element.dataset.originalCursor = element.style.cursor;
161
+
162
+ element.contentEditable = "true";
163
+
164
+ const abortController = new AbortController();
165
+ listenerAbortControllers.set(element, abortController);
166
+ element.addEventListener("input", handleInputEvent, { signal: abortController.signal });
167
+
168
+ element.style.cursor = "text";
169
+ selectText(element);
170
+
171
+ setTimeout(() => {
172
+ element.focus();
173
+ }, 0);
174
+ };
175
+
176
+ const clearInlineEditingMode = (element: HTMLElement) => {
177
+ const abortController = listenerAbortControllers.get(element);
178
+ if (abortController) {
179
+ abortController.abort();
180
+ listenerAbortControllers.delete(element);
181
+ }
182
+
183
+ if (!element.isConnected) {
184
+ return;
185
+ }
186
+
187
+ removeFocusOutlineCSS();
188
+ element.contentEditable = "false";
189
+ delete element.dataset.originalTextContent;
190
+
191
+ if (element.dataset.originalCursor !== undefined) {
192
+ element.style.cursor = element.dataset.originalCursor;
193
+ delete element.dataset.originalCursor;
194
+ }
195
+ };
196
+
197
+ const repositionSelectedOverlays = () => {
198
+ if (selectedElementId) {
199
+ const elements = findElementsById(selectedElementId);
200
+ selectedOverlays.forEach((overlay, index) => {
201
+ if (index < elements.length) {
202
+ positionOverlay(overlay, elements[index]!);
203
+ }
204
+ });
205
+ }
206
+ };
207
+
208
+ const handleEnterInlineEditingMode = (element: HTMLElement) => {
209
+ currentEditingElement = element;
210
+
211
+ selectedOverlays.forEach((overlay) => {
212
+ overlay.style.display = "none";
213
+ });
214
+
215
+ enterInlineEditingMode(element);
216
+
217
+ window.parent.postMessage(
218
+ {
219
+ type: "content-editing-started",
220
+ visualSelectorId: selectedElementId,
221
+ },
222
+ "*"
223
+ );
224
+ };
225
+
226
+ const handleClearInlineEditingMode = () => {
227
+ if (!currentEditingElement) return;
228
+
229
+ if (debouncedSendTimeout) {
230
+ clearTimeout(debouncedSendTimeout);
231
+ debouncedSendTimeout = null;
232
+ }
233
+
234
+ const element = currentEditingElement;
235
+ clearInlineEditingMode(element);
236
+
237
+ selectedOverlays.forEach((overlay) => {
238
+ overlay.style.display = "";
239
+ });
240
+
241
+ repositionSelectedOverlays();
242
+
243
+ window.parent.postMessage(
244
+ {
245
+ type: "content-editing-ended",
246
+ visualSelectorId: selectedElementId,
247
+ },
248
+ "*"
249
+ );
250
+
251
+ currentEditingElement = null;
252
+ };
253
+
254
+ const reportInlineEdit = (element: HTMLElement) => {
255
+ const originalContent = element.dataset.originalTextContent;
256
+ const newContent = element.textContent;
257
+
258
+ const svgElement = element as unknown as SVGElement;
259
+ const elementInfo = {
260
+ tagName: element.tagName,
261
+ classes:
262
+ (svgElement.className as unknown as SVGAnimatedString)?.baseVal ||
263
+ element.className ||
264
+ "",
265
+ visualSelectorId: selectedElementId,
266
+ content: newContent,
267
+ dataSourceLocation: element.dataset.sourceLocation,
268
+ isDynamicContent: element.dataset.dynamicContent === "true",
269
+ linenumber: element.dataset.linenumber,
270
+ filename: element.dataset.filename,
271
+ position: (() => {
272
+ const rect = element.getBoundingClientRect();
273
+ return {
274
+ top: rect.top,
275
+ left: rect.left,
276
+ right: rect.right,
277
+ bottom: rect.bottom,
278
+ width: rect.width,
279
+ height: rect.height,
280
+ centerX: rect.left + rect.width / 2,
281
+ centerY: rect.top + rect.height / 2,
282
+ };
283
+ })(),
284
+ };
285
+
286
+ window.parent.postMessage(
287
+ {
288
+ type: "inline-edit",
289
+ elementInfo,
290
+ originalContent,
291
+ newContent,
292
+ },
293
+ "*"
294
+ );
295
+
296
+ element.dataset.originalTextContent = newContent || "";
297
+ };
298
+
299
+ const debouncedSendInlineEditMessage = (element: HTMLElement) => {
300
+ if (debouncedSendTimeout) {
301
+ clearTimeout(debouncedSendTimeout);
302
+ }
303
+
304
+ debouncedSendTimeout = setTimeout(() => {
305
+ reportInlineEdit(element);
306
+ }, INLINE_EDIT_DEBOUNCE_MS);
307
+ };
308
+
309
+ const onTextInputChange = (element: HTMLElement) => {
310
+ repositionSelectedOverlays();
311
+ debouncedSendInlineEditMessage(element);
312
+ };
313
+
314
+ const clearSelection = () => {
315
+ if (selectedElementId) {
316
+ const elements = findElementsById(selectedElementId);
317
+ elements.forEach((el) => {
318
+ if (el instanceof HTMLElement) {
319
+ delete el.dataset.selected;
320
+ }
321
+ });
322
+ }
323
+
324
+ selectedOverlays.forEach((overlay) => {
325
+ if (overlay && overlay.parentNode) {
326
+ overlay.remove();
327
+ }
328
+ });
329
+ selectedOverlays = [];
330
+ selectedElementId = null;
331
+ };
332
+
72
333
  // Clear hover overlays
73
334
  const clearHoverOverlays = () => {
74
335
  hoverOverlays.forEach((overlay) => {
@@ -154,6 +415,9 @@ export function setupVisualEditAgent() {
154
415
  const handleMouseOver = (e: MouseEvent) => {
155
416
  if (!isVisualEditMode || isPopoverDragging) return;
156
417
 
418
+ // Skip hover effects when inline editing
419
+ if (currentEditingElement) return;
420
+
157
421
  const target = e.target as Element;
158
422
 
159
423
  // Prevent hover effects when a dropdown is open
@@ -221,6 +485,20 @@ export function setupVisualEditAgent() {
221
485
  // Let layer dropdown clicks pass through without interference
222
486
  if (target.closest(`[${LAYER_DROPDOWN_ATTR}]`)) return;
223
487
 
488
+ // Allow normal editing when clicking inside a contentEditable element
489
+ if (isInlineEditExperimentEnabled && target instanceof HTMLElement && target.contentEditable === "true") {
490
+ return;
491
+ }
492
+
493
+ // If currently editing, clicking outside should exit editing mode
494
+ if (currentEditingElement) {
495
+ e.preventDefault();
496
+ e.stopPropagation();
497
+ e.stopImmediatePropagation();
498
+ handleClearInlineEditingMode();
499
+ return;
500
+ }
501
+
224
502
  // Close dropdowns when clicking anywhere in iframe if a dropdown is open
225
503
  if (isDropdownOpen) {
226
504
  e.preventDefault();
@@ -249,14 +527,45 @@ export function setupVisualEditAgent() {
249
527
  return;
250
528
  }
251
529
 
530
+ const htmlElement = element as HTMLElement;
531
+ const visualSelectorId = getElementSelectorId(element);
532
+
533
+ // Check if this element is already selected (second click scenario)
534
+ const isAlreadySelected =
535
+ selectedElementId === visualSelectorId &&
536
+ htmlElement.dataset.selected === "true";
537
+
538
+ if (isAlreadySelected) {
539
+ if (isInlineEditExperimentEnabled && shouldEnterInlineEditingMode(htmlElement)) {
540
+ handleEnterInlineEditingMode(htmlElement);
541
+ return;
542
+ }
543
+ }
544
+
545
+ // Select the element, mark for inline editing, and attach layer dropdown
546
+ if (currentEditingElement) {
547
+ handleClearInlineEditingMode();
548
+ }
549
+
550
+ if (isInlineEditExperimentEnabled) {
551
+ const elements = findElementsById(visualSelectorId);
552
+ elements.forEach((el) => {
553
+ if (el instanceof HTMLElement) {
554
+ el.dataset.selected = "true";
555
+ }
556
+ });
557
+ }
558
+
252
559
  const selectedOverlay = selectElement(element);
253
560
  layerController.attachToOverlay(selectedOverlay, element);
254
561
  };
255
562
 
256
- // Clear the current selection
257
- const clearSelection = () => {
258
- clearSelectedOverlays();
259
- selectedElementId = null;
563
+ // Unselect the current element
564
+ const unselectElement = () => {
565
+ if (currentEditingElement) {
566
+ handleClearInlineEditingMode();
567
+ }
568
+ clearSelection();
260
569
  };
261
570
 
262
571
  const updateElementClassesAndReposition = (visualSelectorId: string, classes: string) => {
@@ -288,7 +597,7 @@ export function setupVisualEditAgent() {
288
597
  });
289
598
  }
290
599
  }
291
- }, 50);
600
+ }, REPOSITION_DELAY_MS);
292
601
  };
293
602
 
294
603
  // Update element attribute by visual selector ID
@@ -311,7 +620,7 @@ export function setupVisualEditAgent() {
311
620
  }
312
621
  });
313
622
  }
314
- }, 50);
623
+ }, REPOSITION_DELAY_MS);
315
624
  };
316
625
 
317
626
  // Update element content by visual selector ID
@@ -334,7 +643,7 @@ export function setupVisualEditAgent() {
334
643
  }
335
644
  });
336
645
  }
337
- }, 50);
646
+ }, REPOSITION_DELAY_MS);
338
647
  };
339
648
 
340
649
  // --- Layer dropdown controller ---
@@ -356,12 +665,15 @@ export function setupVisualEditAgent() {
356
665
  isVisualEditMode = isEnabled;
357
666
 
358
667
  if (!isEnabled) {
668
+ if (currentEditingElement) {
669
+ handleClearInlineEditingMode();
670
+ }
671
+ clearSelection();
359
672
  layerController.cleanup();
360
673
  clearHoverOverlays();
361
674
  clearSelectedOverlays();
362
675
 
363
676
  currentHighlightedElements = [];
364
- selectedElementId = null;
365
677
  document.body.style.cursor = "default";
366
678
 
367
679
  document.removeEventListener("mouseover", handleMouseOver);
@@ -422,6 +734,9 @@ export function setupVisualEditAgent() {
422
734
  switch (message.type) {
423
735
  case "toggle-visual-edit-mode":
424
736
  toggleVisualEditMode(message.data.enabled);
737
+ if (message.data.specs?.newInlineEditEnabled !== undefined) {
738
+ isInlineEditExperimentEnabled = message.data.specs.newInlineEditEnabled;
739
+ }
425
740
  break;
426
741
 
427
742
  case "update-classes":
@@ -459,7 +774,7 @@ export function setupVisualEditAgent() {
459
774
  break;
460
775
 
461
776
  case "unselect-element":
462
- clearSelection();
777
+ unselectElement();
463
778
  break;
464
779
 
465
780
  case "refresh-page":
@@ -537,6 +852,50 @@ export function setupVisualEditAgent() {
537
852
  }
538
853
  break;
539
854
 
855
+ case "toggle-inline-edit-mode":
856
+ if (!isInlineEditExperimentEnabled) break;
857
+ if (!message.data || !message.data.dataSourceLocation) break;
858
+
859
+ {
860
+ const elements = findElementsById(message.data.dataSourceLocation);
861
+ if (elements.length === 0 || !(elements[0] instanceof HTMLElement)) break;
862
+
863
+ const element = elements[0];
864
+
865
+ if (message.data.inlineEditingMode) {
866
+ if (shouldEnterInlineEditingMode(element)) {
867
+ // Select the element first if not already selected
868
+ if (selectedElementId !== message.data.dataSourceLocation) {
869
+ if (currentEditingElement) {
870
+ handleClearInlineEditingMode();
871
+ }
872
+ clearSelection();
873
+
874
+ elements.forEach((el) => {
875
+ if (el instanceof HTMLElement) {
876
+ el.dataset.selected = "true";
877
+ }
878
+ });
879
+
880
+ elements.forEach((el) => {
881
+ const overlay = createOverlay(true);
882
+ document.body.appendChild(overlay);
883
+ selectedOverlays.push(overlay);
884
+ positionOverlay(overlay, el, true);
885
+ });
886
+
887
+ selectedElementId = message.data.dataSourceLocation;
888
+ }
889
+ handleEnterInlineEditingMode(element);
890
+ }
891
+ } else {
892
+ if (currentEditingElement === element) {
893
+ handleClearInlineEditingMode();
894
+ }
895
+ }
896
+ }
897
+ break;
898
+
540
899
  default:
541
900
  break;
542
901
  }
@@ -601,7 +960,7 @@ export function setupVisualEditAgent() {
601
960
  });
602
961
 
603
962
  if (needsUpdate) {
604
- setTimeout(handleResize, 50);
963
+ setTimeout(handleResize, REPOSITION_DELAY_MS);
605
964
  }
606
965
  });
607
966