@base44/vite-plugin 0.2.26 → 0.2.28

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 +200 -0
  4. package/dist/capabilities/inline-edit/controller.js.map +1 -0
  5. package/dist/capabilities/inline-edit/dom-utils.d.ts +7 -0
  6. package/dist/capabilities/inline-edit/dom-utils.d.ts.map +1 -0
  7. package/dist/capabilities/inline-edit/dom-utils.js +59 -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 +86 -16
  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 +251 -0
  24. package/src/capabilities/inline-edit/dom-utils.ts +58 -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 +105 -16
@@ -0,0 +1,251 @@
1
+ import type { InlineEditHost, InlineEditController } from "./types.js";
2
+ import {
3
+ injectFocusOutlineCSS,
4
+ removeFocusOutlineCSS,
5
+ selectText,
6
+ shouldEnterInlineEditingMode,
7
+ isStaticArrayTextElement,
8
+ } from "./dom-utils.js";
9
+
10
+ const DEBOUNCE_MS = 500;
11
+
12
+ export function createInlineEditController(
13
+ host: InlineEditHost
14
+ ): InlineEditController {
15
+ let currentEditingElement: HTMLElement | null = null;
16
+ let debouncedSendTimeout: ReturnType<typeof setTimeout> | null = null;
17
+ let enabled = false;
18
+ const listenerAbortControllers = new WeakMap<HTMLElement, AbortController>();
19
+
20
+ // --- Private helpers ---
21
+
22
+ const repositionOverlays = () => {
23
+ const selectedId = host.getSelectedElementId();
24
+ if (!selectedId) return;
25
+ const elements = host.findElementsById(selectedId);
26
+ const overlays = host.getSelectedOverlays();
27
+ overlays.forEach((overlay, i) => {
28
+ if (i < elements.length && elements[i]) {
29
+ host.positionOverlay(overlay, elements[i]);
30
+ }
31
+ });
32
+ };
33
+
34
+ const reportEdit = (element: HTMLElement) => {
35
+ const originalContent = element.dataset.originalTextContent;
36
+ const newContent = element.textContent;
37
+
38
+ const svgElement = element as unknown as SVGElement;
39
+ const rect = element.getBoundingClientRect();
40
+
41
+ const message: Record<string, unknown> = {
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
+ if (isStaticArrayTextElement(element)) {
71
+ message.arrIndex = element.dataset.arrIndex;
72
+ message.arrVariableName = element.dataset.arrVariableName;
73
+ message.arrField = element.dataset.arrField;
74
+ }
75
+
76
+ window.parent.postMessage(message, "*");
77
+
78
+ element.dataset.originalTextContent = newContent || "";
79
+ };
80
+
81
+ const debouncedReport = (element: HTMLElement) => {
82
+ if (debouncedSendTimeout) clearTimeout(debouncedSendTimeout);
83
+ debouncedSendTimeout = setTimeout(() => reportEdit(element), DEBOUNCE_MS);
84
+ };
85
+
86
+ const onTextInput = (element: HTMLElement) => {
87
+ repositionOverlays();
88
+ debouncedReport(element);
89
+ };
90
+
91
+ const handleInputEvent = function (this: HTMLElement) {
92
+ onTextInput(this);
93
+ };
94
+
95
+ const makeEditable = (element: HTMLElement) => {
96
+ injectFocusOutlineCSS();
97
+
98
+ element.dataset.originalTextContent = element.textContent || "";
99
+ element.dataset.originalCursor = element.style.cursor;
100
+ element.contentEditable = "true";
101
+
102
+ const abortController = new AbortController();
103
+ listenerAbortControllers.set(element, abortController);
104
+ element.addEventListener("input", handleInputEvent, {
105
+ signal: abortController.signal,
106
+ });
107
+
108
+ element.style.cursor = "text";
109
+ selectText(element);
110
+ setTimeout(() => {
111
+ if (element.isConnected) {
112
+ element.focus();
113
+ }
114
+ }, 0);
115
+ };
116
+
117
+ const makeNonEditable = (element: HTMLElement) => {
118
+ const abortController = listenerAbortControllers.get(element);
119
+ if (abortController) {
120
+ abortController.abort();
121
+ listenerAbortControllers.delete(element);
122
+ }
123
+
124
+ if (!element.isConnected) return;
125
+
126
+ removeFocusOutlineCSS();
127
+ element.contentEditable = "false";
128
+ delete element.dataset.originalTextContent;
129
+
130
+ if (element.dataset.originalCursor !== undefined) {
131
+ element.style.cursor = element.dataset.originalCursor;
132
+ delete element.dataset.originalCursor;
133
+ }
134
+ };
135
+
136
+ // --- Public API ---
137
+
138
+ return {
139
+ get enabled() {
140
+ return enabled;
141
+ },
142
+ set enabled(value: boolean) {
143
+ enabled = value;
144
+ },
145
+
146
+ isEditing() {
147
+ return currentEditingElement !== null;
148
+ },
149
+
150
+ getCurrentElement() {
151
+ return currentEditingElement;
152
+ },
153
+
154
+ canEdit(element: Element) {
155
+ return shouldEnterInlineEditingMode(element);
156
+ },
157
+
158
+ startEditing(element: HTMLElement) {
159
+ currentEditingElement = element;
160
+
161
+ host.getSelectedOverlays().forEach((o) => {
162
+ o.style.display = "none";
163
+ });
164
+
165
+ makeEditable(element);
166
+
167
+ window.parent.postMessage(
168
+ {
169
+ type: "content-editing-started",
170
+ visualSelectorId: host.getSelectedElementId(),
171
+ },
172
+ "*"
173
+ );
174
+ },
175
+
176
+ stopEditing() {
177
+ if (!currentEditingElement) return;
178
+
179
+ if (debouncedSendTimeout) {
180
+ clearTimeout(debouncedSendTimeout);
181
+ debouncedSendTimeout = null;
182
+ }
183
+
184
+ const element = currentEditingElement;
185
+ makeNonEditable(element);
186
+
187
+ host.getSelectedOverlays().forEach((o) => {
188
+ o.style.display = "";
189
+ });
190
+
191
+ repositionOverlays();
192
+
193
+ window.parent.postMessage(
194
+ {
195
+ type: "content-editing-ended",
196
+ visualSelectorId: host.getSelectedElementId(),
197
+ },
198
+ "*"
199
+ );
200
+
201
+ currentEditingElement = null;
202
+ },
203
+
204
+ markElementsSelected(elements: Element[]) {
205
+ elements.forEach((el) => {
206
+ if (el instanceof HTMLElement) {
207
+ el.dataset.selected = "true";
208
+ }
209
+ });
210
+ },
211
+
212
+ clearSelectedMarks(elementId: string | null) {
213
+ if (!elementId) return;
214
+ host.findElementsById(elementId).forEach((el) => {
215
+ if (el instanceof HTMLElement) {
216
+ delete el.dataset.selected;
217
+ }
218
+ });
219
+ },
220
+
221
+ handleToggleMessage(data: { dataSourceLocation: string; inlineEditingMode: boolean }) {
222
+ if (!enabled) return;
223
+
224
+ const elements = host.findElementsById(data.dataSourceLocation);
225
+ if (elements.length === 0 || !(elements[0] instanceof HTMLElement)) return;
226
+
227
+ const element = elements[0];
228
+
229
+ if (data.inlineEditingMode) {
230
+ if (!shouldEnterInlineEditingMode(element)) return;
231
+
232
+ // Select the element first if not already selected
233
+ if (host.getSelectedElementId() !== data.dataSourceLocation) {
234
+ this.stopEditing();
235
+ host.clearSelection();
236
+ this.markElementsSelected(elements);
237
+ host.createSelectionOverlays(elements, data.dataSourceLocation);
238
+ }
239
+ this.startEditing(element);
240
+ } else {
241
+ if (currentEditingElement === element) {
242
+ this.stopEditing();
243
+ }
244
+ }
245
+ },
246
+
247
+ cleanup() {
248
+ this.stopEditing();
249
+ },
250
+ };
251
+ }
@@ -0,0 +1,58 @@
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 isStaticArrayTextElement = (element: HTMLElement): boolean => {
9
+ return !!element.dataset.arrField;
10
+ };
11
+
12
+ const passesStructuralChecks = (element: HTMLElement): boolean => {
13
+ if (!EDITABLE_TAGS.includes(element.tagName.toLowerCase())) return false;
14
+ if (!element.textContent?.trim()) return false;
15
+ if (element.querySelector("img, video, canvas, svg")) return false;
16
+ if (element.children?.length > 0) return false;
17
+ return true;
18
+ };
19
+
20
+ export const injectFocusOutlineCSS = () => {
21
+ if (document.getElementById(FOCUS_STYLE_ID)) return;
22
+
23
+ const style = document.createElement("style");
24
+ style.id = FOCUS_STYLE_ID;
25
+ style.textContent = `
26
+ [data-selected="true"][contenteditable="true"]:focus {
27
+ outline: none !important;
28
+ }
29
+ `;
30
+ document.head.appendChild(style);
31
+ };
32
+
33
+ export const removeFocusOutlineCSS = () => {
34
+ document.getElementById(FOCUS_STYLE_ID)?.remove();
35
+ };
36
+
37
+ export const selectText = (element: HTMLElement) => {
38
+ const range = document.createRange();
39
+ range.selectNodeContents(element);
40
+ const selection = window.getSelection();
41
+ selection?.removeAllRanges();
42
+ selection?.addRange(range);
43
+ };
44
+
45
+ export const isEditableTextElement = (element: Element): boolean => {
46
+ if (!(element instanceof HTMLElement)) return false;
47
+ if (!passesStructuralChecks(element)) return false;
48
+ if (isStaticArrayTextElement(element)) return true;
49
+ if (element.dataset.dynamicContent === "true") return false;
50
+ return true;
51
+ };
52
+
53
+ export const shouldEnterInlineEditingMode = (element: Element): boolean => {
54
+ if (!(element instanceof HTMLElement) || element.dataset.selected !== "true") {
55
+ return false;
56
+ }
57
+ return isEditableTextElement(element);
58
+ };
@@ -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) => {
@@ -96,6 +127,13 @@ export function setupVisualEditAgent() {
96
127
  const rect = element.getBoundingClientRect();
97
128
  const svgElement = element as SVGElement;
98
129
  const isTextElement = TEXT_TAGS.includes(element.tagName?.toLowerCase());
130
+
131
+ const arrEl = htmlElement.closest("[data-arr-variable-name]") as HTMLElement | null;
132
+ const staticArrayName = arrEl?.dataset?.arrVariableName || null;
133
+ const rawIdx = arrEl?.dataset?.arrIndex;
134
+ const staticArrayIndex = rawIdx != null ? parseInt(rawIdx, 10) : null;
135
+ const staticArrayField = htmlElement.dataset?.arrField || null;
136
+
99
137
  window.parent.postMessage({
100
138
  type: "element-selected",
101
139
  tagName: element.tagName,
@@ -121,6 +159,9 @@ export function setupVisualEditAgent() {
121
159
  },
122
160
  attributes: collectAllowedAttributes(element, ALLOWED_ATTRIBUTES),
123
161
  isTextElement,
162
+ staticArrayName,
163
+ staticArrayIndex,
164
+ staticArrayField,
124
165
  }, "*");
125
166
  };
126
167
 
@@ -152,7 +193,8 @@ export function setupVisualEditAgent() {
152
193
 
153
194
  // Handle mouse over event
154
195
  const handleMouseOver = (e: MouseEvent) => {
155
- if (!isVisualEditMode || isPopoverDragging) return;
196
+ if (!isVisualEditMode || isPopoverDragging || inlineEdit.isEditing()) return;
197
+
156
198
 
157
199
  const target = e.target as Element;
158
200
 
@@ -221,6 +263,21 @@ export function setupVisualEditAgent() {
221
263
  // Let layer dropdown clicks pass through without interference
222
264
  if (target.closest(`[${LAYER_DROPDOWN_ATTR}]`)) return;
223
265
 
266
+ // Let clicks inside the editable element pass through to the browser
267
+ // so the user can reposition the cursor and select text naturally.
268
+ if (inlineEdit.enabled && target instanceof HTMLElement && target.contentEditable === "true") {
269
+ return;
270
+ }
271
+
272
+ // Clicking outside the editable element exits inline editing mode.
273
+ if (inlineEdit.isEditing()) {
274
+ e.preventDefault();
275
+ e.stopPropagation();
276
+ e.stopImmediatePropagation();
277
+ inlineEdit.stopEditing();
278
+ return;
279
+ }
280
+
224
281
  // Close dropdowns when clicking anywhere in iframe if a dropdown is open
225
282
  if (isDropdownOpen) {
226
283
  e.preventDefault();
@@ -249,14 +306,31 @@ export function setupVisualEditAgent() {
249
306
  return;
250
307
  }
251
308
 
309
+ const htmlElement = element as HTMLElement;
310
+ const visualSelectorId = getElementSelectorId(element);
311
+
312
+ const isAlreadySelected =
313
+ selectedElementId === visualSelectorId &&
314
+ htmlElement.dataset.selected === "true";
315
+
316
+ if (isAlreadySelected && inlineEdit.enabled && inlineEdit.canEdit(htmlElement)) {
317
+ inlineEdit.startEditing(htmlElement);
318
+ return;
319
+ }
320
+
321
+ inlineEdit.stopEditing();
322
+
323
+ if (inlineEdit.enabled) {
324
+ inlineEdit.markElementsSelected(findElementsById(visualSelectorId));
325
+ }
326
+
252
327
  const selectedOverlay = selectElement(element);
253
328
  layerController.attachToOverlay(selectedOverlay, element);
254
329
  };
255
330
 
256
- // Clear the current selection
257
- const clearSelection = () => {
258
- clearSelectedOverlays();
259
- selectedElementId = null;
331
+ const unselectElement = () => {
332
+ inlineEdit.stopEditing();
333
+ clearSelection();
260
334
  };
261
335
 
262
336
  const updateElementClassesAndReposition = (visualSelectorId: string, classes: string) => {
@@ -288,7 +362,7 @@ export function setupVisualEditAgent() {
288
362
  });
289
363
  }
290
364
  }
291
- }, 50);
365
+ }, REPOSITION_DELAY_MS);
292
366
  };
293
367
 
294
368
  // Update element attribute by visual selector ID
@@ -311,17 +385,22 @@ export function setupVisualEditAgent() {
311
385
  }
312
386
  });
313
387
  }
314
- }, 50);
388
+ }, REPOSITION_DELAY_MS);
315
389
  };
316
390
 
317
- // Update element content by visual selector ID
318
- const updateElementContent = (visualSelectorId: string, content: string) => {
319
- const elements = findElementsById(visualSelectorId);
391
+ const updateElementContent = (visualSelectorId: string, content: string, arrIndex?: number) => {
392
+ let elements = findElementsById(visualSelectorId);
320
393
 
321
394
  if (elements.length === 0) {
322
395
  return;
323
396
  }
324
397
 
398
+ if (arrIndex != null) {
399
+ elements = elements.filter(
400
+ (el) => (el as HTMLElement).dataset.arrIndex === String(arrIndex)
401
+ );
402
+ }
403
+
325
404
  elements.forEach((element) => {
326
405
  (element as HTMLElement).innerText = content;
327
406
  });
@@ -334,7 +413,7 @@ export function setupVisualEditAgent() {
334
413
  }
335
414
  });
336
415
  }
337
- }, 50);
416
+ }, REPOSITION_DELAY_MS);
338
417
  };
339
418
 
340
419
  // --- Layer dropdown controller ---
@@ -356,12 +435,12 @@ export function setupVisualEditAgent() {
356
435
  isVisualEditMode = isEnabled;
357
436
 
358
437
  if (!isEnabled) {
438
+ inlineEdit.stopEditing();
439
+ clearSelection();
359
440
  layerController.cleanup();
360
441
  clearHoverOverlays();
361
- clearSelectedOverlays();
362
442
 
363
443
  currentHighlightedElements = [];
364
- selectedElementId = null;
365
444
  document.body.style.cursor = "default";
366
445
 
367
446
  document.removeEventListener("mouseover", handleMouseOver);
@@ -422,6 +501,9 @@ export function setupVisualEditAgent() {
422
501
  switch (message.type) {
423
502
  case "toggle-visual-edit-mode":
424
503
  toggleVisualEditMode(message.data.enabled);
504
+ if (message.data.specs?.newInlineEditEnabled !== undefined) {
505
+ inlineEdit.enabled = message.data.specs.newInlineEditEnabled;
506
+ }
425
507
  break;
426
508
 
427
509
  case "update-classes":
@@ -459,7 +541,7 @@ export function setupVisualEditAgent() {
459
541
  break;
460
542
 
461
543
  case "unselect-element":
462
- clearSelection();
544
+ unselectElement();
463
545
  break;
464
546
 
465
547
  case "refresh-page":
@@ -470,7 +552,8 @@ export function setupVisualEditAgent() {
470
552
  if (message.data && message.data.content !== undefined) {
471
553
  updateElementContent(
472
554
  message.data.visualSelectorId,
473
- message.data.content
555
+ message.data.content,
556
+ message.data.arrIndex
474
557
  );
475
558
  } else {
476
559
  console.warn(
@@ -537,6 +620,12 @@ export function setupVisualEditAgent() {
537
620
  }
538
621
  break;
539
622
 
623
+ case "toggle-inline-edit-mode":
624
+ if (message.data) {
625
+ inlineEdit.handleToggleMessage(message.data);
626
+ }
627
+ break;
628
+
540
629
  default:
541
630
  break;
542
631
  }
@@ -601,7 +690,7 @@ export function setupVisualEditAgent() {
601
690
  });
602
691
 
603
692
  if (needsUpdate) {
604
- setTimeout(handleResize, 50);
693
+ setTimeout(handleResize, REPOSITION_DELAY_MS);
605
694
  }
606
695
  });
607
696