@base44/vite-plugin 0.2.26 → 0.2.27

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 +194 -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 +25 -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 +72 -12
  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 +245 -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 +30 -0
  27. package/src/injections/visual-edit-agent.ts +85 -12
@@ -0,0 +1,245 @@
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 && elements[i]) {
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(() => {
105
+ if (element.isConnected) {
106
+ element.focus();
107
+ }
108
+ }, 0);
109
+ };
110
+
111
+ const makeNonEditable = (element: HTMLElement) => {
112
+ const abortController = listenerAbortControllers.get(element);
113
+ if (abortController) {
114
+ abortController.abort();
115
+ listenerAbortControllers.delete(element);
116
+ }
117
+
118
+ if (!element.isConnected) return;
119
+
120
+ removeFocusOutlineCSS();
121
+ element.contentEditable = "false";
122
+ delete element.dataset.originalTextContent;
123
+
124
+ if (element.dataset.originalCursor !== undefined) {
125
+ element.style.cursor = element.dataset.originalCursor;
126
+ delete element.dataset.originalCursor;
127
+ }
128
+ };
129
+
130
+ // --- Public API ---
131
+
132
+ return {
133
+ get enabled() {
134
+ return enabled;
135
+ },
136
+ set enabled(value: boolean) {
137
+ enabled = value;
138
+ },
139
+
140
+ isEditing() {
141
+ return currentEditingElement !== null;
142
+ },
143
+
144
+ getCurrentElement() {
145
+ return currentEditingElement;
146
+ },
147
+
148
+ canEdit(element: Element) {
149
+ return shouldEnterInlineEditingMode(element);
150
+ },
151
+
152
+ startEditing(element: HTMLElement) {
153
+ currentEditingElement = element;
154
+
155
+ host.getSelectedOverlays().forEach((o) => {
156
+ o.style.display = "none";
157
+ });
158
+
159
+ makeEditable(element);
160
+
161
+ window.parent.postMessage(
162
+ {
163
+ type: "content-editing-started",
164
+ visualSelectorId: host.getSelectedElementId(),
165
+ },
166
+ "*"
167
+ );
168
+ },
169
+
170
+ stopEditing() {
171
+ if (!currentEditingElement) return;
172
+
173
+ if (debouncedSendTimeout) {
174
+ clearTimeout(debouncedSendTimeout);
175
+ debouncedSendTimeout = null;
176
+ }
177
+
178
+ const element = currentEditingElement;
179
+ makeNonEditable(element);
180
+
181
+ host.getSelectedOverlays().forEach((o) => {
182
+ o.style.display = "";
183
+ });
184
+
185
+ repositionOverlays();
186
+
187
+ window.parent.postMessage(
188
+ {
189
+ type: "content-editing-ended",
190
+ visualSelectorId: host.getSelectedElementId(),
191
+ },
192
+ "*"
193
+ );
194
+
195
+ currentEditingElement = null;
196
+ },
197
+
198
+ markElementsSelected(elements: Element[]) {
199
+ elements.forEach((el) => {
200
+ if (el instanceof HTMLElement) {
201
+ el.dataset.selected = "true";
202
+ }
203
+ });
204
+ },
205
+
206
+ clearSelectedMarks(elementId: string | null) {
207
+ if (!elementId) return;
208
+ host.findElementsById(elementId).forEach((el) => {
209
+ if (el instanceof HTMLElement) {
210
+ delete el.dataset.selected;
211
+ }
212
+ });
213
+ },
214
+
215
+ handleToggleMessage(data: { dataSourceLocation: string; inlineEditingMode: boolean }) {
216
+ if (!enabled) return;
217
+
218
+ const elements = host.findElementsById(data.dataSourceLocation);
219
+ if (elements.length === 0 || !(elements[0] instanceof HTMLElement)) return;
220
+
221
+ const element = elements[0];
222
+
223
+ if (data.inlineEditingMode) {
224
+ if (!shouldEnterInlineEditingMode(element)) return;
225
+
226
+ // Select the element first if not already selected
227
+ if (host.getSelectedElementId() !== data.dataSourceLocation) {
228
+ this.stopEditing();
229
+ host.clearSelection();
230
+ this.markElementsSelected(elements);
231
+ host.createSelectionOverlays(elements, data.dataSourceLocation);
232
+ }
233
+ this.startEditing(element);
234
+ } else {
235
+ if (currentEditingElement === element) {
236
+ this.stopEditing();
237
+ }
238
+ }
239
+ },
240
+
241
+ cleanup() {
242
+ this.stopEditing();
243
+ },
244
+ };
245
+ }
@@ -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,30 @@
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
+ clearSelection(): void;
11
+ createSelectionOverlays(elements: Element[], elementId: string): void;
12
+ }
13
+
14
+ export interface ToggleInlineEditData {
15
+ dataSourceLocation: string;
16
+ inlineEditingMode: boolean;
17
+ }
18
+
19
+ export interface InlineEditController {
20
+ enabled: boolean;
21
+ isEditing(): boolean;
22
+ getCurrentElement(): HTMLElement | null;
23
+ canEdit(element: Element): boolean;
24
+ startEditing(element: HTMLElement): void;
25
+ stopEditing(): void;
26
+ markElementsSelected(elements: Element[]): void;
27
+ clearSelectedMarks(elementId: string | null): void;
28
+ handleToggleMessage(data: ToggleInlineEditData): void;
29
+ cleanup(): void;
30
+ }
@@ -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,34 @@ 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
+ clearSelection: () => {
82
+ inlineEdit.clearSelectedMarks(selectedElementId);
83
+ clearSelectedOverlays();
84
+ selectedElementId = null;
85
+ },
86
+ createSelectionOverlays: (elements, elementId) => {
87
+ elements.forEach((el) => {
88
+ const overlay = createOverlay(true);
89
+ document.body.appendChild(overlay);
90
+ selectedOverlays.push(overlay);
91
+ positionOverlay(overlay, el, true);
92
+ });
93
+ selectedElementId = elementId;
94
+ },
95
+ });
96
+
97
+ const clearSelection = () => {
98
+ inlineEdit.clearSelectedMarks(selectedElementId);
99
+ clearSelectedOverlays();
100
+ selectedElementId = null;
101
+ };
102
+
72
103
  // Clear hover overlays
73
104
  const clearHoverOverlays = () => {
74
105
  hoverOverlays.forEach((overlay) => {
@@ -152,7 +183,8 @@ export function setupVisualEditAgent() {
152
183
 
153
184
  // Handle mouse over event
154
185
  const handleMouseOver = (e: MouseEvent) => {
155
- if (!isVisualEditMode || isPopoverDragging) return;
186
+ if (!isVisualEditMode || isPopoverDragging || inlineEdit.isEditing()) return;
187
+
156
188
 
157
189
  const target = e.target as Element;
158
190
 
@@ -221,6 +253,21 @@ export function setupVisualEditAgent() {
221
253
  // Let layer dropdown clicks pass through without interference
222
254
  if (target.closest(`[${LAYER_DROPDOWN_ATTR}]`)) return;
223
255
 
256
+ // Let clicks inside the editable element pass through to the browser
257
+ // so the user can reposition the cursor and select text naturally.
258
+ if (inlineEdit.enabled && target instanceof HTMLElement && target.contentEditable === "true") {
259
+ return;
260
+ }
261
+
262
+ // Clicking outside the editable element exits inline editing mode.
263
+ if (inlineEdit.isEditing()) {
264
+ e.preventDefault();
265
+ e.stopPropagation();
266
+ e.stopImmediatePropagation();
267
+ inlineEdit.stopEditing();
268
+ return;
269
+ }
270
+
224
271
  // Close dropdowns when clicking anywhere in iframe if a dropdown is open
225
272
  if (isDropdownOpen) {
226
273
  e.preventDefault();
@@ -249,14 +296,31 @@ export function setupVisualEditAgent() {
249
296
  return;
250
297
  }
251
298
 
299
+ const htmlElement = element as HTMLElement;
300
+ const visualSelectorId = getElementSelectorId(element);
301
+
302
+ const isAlreadySelected =
303
+ selectedElementId === visualSelectorId &&
304
+ htmlElement.dataset.selected === "true";
305
+
306
+ if (isAlreadySelected && inlineEdit.enabled && inlineEdit.canEdit(htmlElement)) {
307
+ inlineEdit.startEditing(htmlElement);
308
+ return;
309
+ }
310
+
311
+ inlineEdit.stopEditing();
312
+
313
+ if (inlineEdit.enabled) {
314
+ inlineEdit.markElementsSelected(findElementsById(visualSelectorId));
315
+ }
316
+
252
317
  const selectedOverlay = selectElement(element);
253
318
  layerController.attachToOverlay(selectedOverlay, element);
254
319
  };
255
320
 
256
- // Clear the current selection
257
- const clearSelection = () => {
258
- clearSelectedOverlays();
259
- selectedElementId = null;
321
+ const unselectElement = () => {
322
+ inlineEdit.stopEditing();
323
+ clearSelection();
260
324
  };
261
325
 
262
326
  const updateElementClassesAndReposition = (visualSelectorId: string, classes: string) => {
@@ -288,7 +352,7 @@ export function setupVisualEditAgent() {
288
352
  });
289
353
  }
290
354
  }
291
- }, 50);
355
+ }, REPOSITION_DELAY_MS);
292
356
  };
293
357
 
294
358
  // Update element attribute by visual selector ID
@@ -311,7 +375,7 @@ export function setupVisualEditAgent() {
311
375
  }
312
376
  });
313
377
  }
314
- }, 50);
378
+ }, REPOSITION_DELAY_MS);
315
379
  };
316
380
 
317
381
  // Update element content by visual selector ID
@@ -334,7 +398,7 @@ export function setupVisualEditAgent() {
334
398
  }
335
399
  });
336
400
  }
337
- }, 50);
401
+ }, REPOSITION_DELAY_MS);
338
402
  };
339
403
 
340
404
  // --- Layer dropdown controller ---
@@ -356,12 +420,12 @@ export function setupVisualEditAgent() {
356
420
  isVisualEditMode = isEnabled;
357
421
 
358
422
  if (!isEnabled) {
423
+ inlineEdit.stopEditing();
424
+ clearSelection();
359
425
  layerController.cleanup();
360
426
  clearHoverOverlays();
361
- clearSelectedOverlays();
362
427
 
363
428
  currentHighlightedElements = [];
364
- selectedElementId = null;
365
429
  document.body.style.cursor = "default";
366
430
 
367
431
  document.removeEventListener("mouseover", handleMouseOver);
@@ -422,6 +486,9 @@ export function setupVisualEditAgent() {
422
486
  switch (message.type) {
423
487
  case "toggle-visual-edit-mode":
424
488
  toggleVisualEditMode(message.data.enabled);
489
+ if (message.data.specs?.newInlineEditEnabled !== undefined) {
490
+ inlineEdit.enabled = message.data.specs.newInlineEditEnabled;
491
+ }
425
492
  break;
426
493
 
427
494
  case "update-classes":
@@ -459,7 +526,7 @@ export function setupVisualEditAgent() {
459
526
  break;
460
527
 
461
528
  case "unselect-element":
462
- clearSelection();
529
+ unselectElement();
463
530
  break;
464
531
 
465
532
  case "refresh-page":
@@ -537,6 +604,12 @@ export function setupVisualEditAgent() {
537
604
  }
538
605
  break;
539
606
 
607
+ case "toggle-inline-edit-mode":
608
+ if (message.data) {
609
+ inlineEdit.handleToggleMessage(message.data);
610
+ }
611
+ break;
612
+
540
613
  default:
541
614
  break;
542
615
  }
@@ -601,7 +674,7 @@ export function setupVisualEditAgent() {
601
674
  });
602
675
 
603
676
  if (needsUpdate) {
604
- setTimeout(handleResize, 50);
677
+ setTimeout(handleResize, REPOSITION_DELAY_MS);
605
678
  }
606
679
  });
607
680