@upstart.gg/vite-plugins 0.1.40 → 0.1.42

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 (37) hide show
  1. package/dist/upstart-editor-api.d.ts +59 -1
  2. package/dist/upstart-editor-api.d.ts.map +1 -1
  3. package/dist/upstart-editor-api.js +273 -4
  4. package/dist/upstart-editor-api.js.map +1 -1
  5. package/dist/vite-plugin-upstart-attrs.d.ts +2 -1
  6. package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -1
  7. package/dist/vite-plugin-upstart-attrs.js +91 -4
  8. package/dist/vite-plugin-upstart-attrs.js.map +1 -1
  9. package/dist/vite-plugin-upstart-editor/runtime/array-controls.js +342 -0
  10. package/dist/vite-plugin-upstart-editor/runtime/array-controls.js.map +1 -0
  11. package/dist/vite-plugin-upstart-editor/runtime/click-handler.d.ts.map +1 -1
  12. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +29 -2
  13. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
  14. package/dist/vite-plugin-upstart-editor/runtime/form-guard.js +80 -0
  15. package/dist/vite-plugin-upstart-editor/runtime/form-guard.js.map +1 -0
  16. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.d.ts.map +1 -1
  17. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js +2 -1
  18. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js.map +1 -1
  19. package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -1
  20. package/dist/vite-plugin-upstart-editor/runtime/index.js +26 -4
  21. package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
  22. package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts.map +1 -1
  23. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js +40 -36
  24. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js.map +1 -1
  25. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +20 -4
  26. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
  27. package/package.json +3 -3
  28. package/src/tests/vite-plugin-upstart-attrs.test.ts +412 -0
  29. package/src/upstart-editor-api.ts +314 -5
  30. package/src/vite-plugin-upstart-attrs.ts +154 -4
  31. package/src/vite-plugin-upstart-editor/runtime/array-controls.ts +478 -0
  32. package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +43 -0
  33. package/src/vite-plugin-upstart-editor/runtime/form-guard.ts +121 -0
  34. package/src/vite-plugin-upstart-editor/runtime/hover-overlay.ts +6 -1
  35. package/src/vite-plugin-upstart-editor/runtime/index.ts +20 -4
  36. package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +49 -58
  37. package/src/vite-plugin-upstart-editor/runtime/types.ts +31 -4
@@ -2,6 +2,8 @@ import { initClickHandler } from "./click-handler.js";
2
2
  import { initHoverOverlay, hideOverlays } from "./hover-overlay.js";
3
3
  import { initErrorHandler } from "./error-handler.js";
4
4
  import { initTextEditor, activateAllEditors, destroyAllActiveEditors } from "./text-editor.js";
5
+ import { initArrayControls, refreshArrayControls, hideArrayControls } from "./array-controls.js";
6
+ import { initFormGuard } from "./form-guard.js";
5
7
  import { sendToParent } from "./utils.js";
6
8
  import { getCurrentMode, setCurrentMode } from "./state.js";
7
9
  import type { EditorMode, UpstartParentMessage } from "./types.js";
@@ -71,19 +73,24 @@ export function initUpstartEditor(): void {
71
73
 
72
74
  window.addEventListener("message", handleParentMessage);
73
75
 
76
+ // Current path (pathname + search), reported to the parent so it can reload
77
+ // the iframe on the page being viewed instead of always the home page.
78
+ const currentPath = () => location.pathname + location.search;
79
+
74
80
  // Notify parent on SPA navigation so it can resend the current editMode
81
+ // and track the current page for the next reload.
75
82
  window.addEventListener("popstate", () => {
76
- sendToParent({ type: "editor-navigated" });
83
+ sendToParent({ type: "editor-navigated", path: currentPath() });
77
84
  });
78
85
  const originalPushState = history.pushState.bind(history);
79
86
  history.pushState = (...args) => {
80
87
  originalPushState(...args);
81
- sendToParent({ type: "editor-navigated" });
88
+ sendToParent({ type: "editor-navigated", path: currentPath() });
82
89
  };
83
90
  const originalReplaceState = history.replaceState.bind(history);
84
91
  history.replaceState = (...args) => {
85
92
  originalReplaceState(...args);
86
- sendToParent({ type: "editor-navigated" });
93
+ sendToParent({ type: "editor-navigated", path: currentPath() });
87
94
  };
88
95
 
89
96
  // i18next integration: if the app exposes it on window.__i18next (set before hydration),
@@ -97,9 +104,11 @@ export function initUpstartEditor(): void {
97
104
  initTextEditor(getRawI18nTemplate ? { getRawI18nTemplate } : {});
98
105
  initClickHandler();
99
106
  initHoverOverlay();
107
+ initArrayControls();
108
+ initFormGuard();
100
109
  initErrorHandler();
101
110
 
102
- sendToParent({ type: "editor-ready" });
111
+ sendToParent({ type: "editor-ready", path: currentPath() });
103
112
  } catch (error) {
104
113
  console.error("[Upstart Editor] Initialization failed:", error);
105
114
  sendToParent({
@@ -152,6 +161,11 @@ function handleParentMessage(event: MessageEvent): void {
152
161
  } else if (styleEl) {
153
162
  styleEl.textContent = "";
154
163
  }
164
+ } else if (message.type === "preview-image") {
165
+ const el = document.querySelector<HTMLImageElement>(`[data-upstart-image-id="${message.imageId}"]`);
166
+ if (el) {
167
+ el.src = message.src;
168
+ }
155
169
  } else if (message.type === "request-scroll-position") {
156
170
  sendToParent({ type: "scroll-position", x: window.scrollX, y: window.scrollY });
157
171
  } else if (message.type === "restore-scroll-position") {
@@ -163,6 +177,7 @@ function enableEditMode(): void {
163
177
  console.log("[Upstart Editor] Edit mode enabled");
164
178
  document.documentElement.setAttribute("data-upstart-edit-mode", "");
165
179
  activateAllEditors();
180
+ refreshArrayControls();
166
181
  }
167
182
 
168
183
  function disableEditMode(): void {
@@ -170,6 +185,7 @@ function disableEditMode(): void {
170
185
  document.documentElement.removeAttribute("data-upstart-edit-mode");
171
186
  destroyAllActiveEditors();
172
187
  hideOverlays();
188
+ hideArrayControls();
173
189
  }
174
190
 
175
191
  export { initTextEditor } from "./text-editor.js";
@@ -128,7 +128,11 @@ function buildI18nContent(rawTemplate: string, renderedText: string): object {
128
128
  return { type: "doc", content: [{ type: "paragraph", content: inlineNodes }] };
129
129
  }
130
130
 
131
- const activeEditors = new Map<string, EditorInstance>();
131
+ // Keyed by the host DOM element, NOT by data-upstart-hash: a component rendered
132
+ // multiple times (e.g. a TimelineItem reused 4×) emits the same source-derived
133
+ // hash on every instance, so keying by hash would let only the first instance
134
+ // become editable. The element is the only per-instance unique identity.
135
+ const activeEditors = new Map<HTMLElement, EditorInstance>();
132
136
  let i18nSyncInProgress = false;
133
137
  const styleCache = new WeakMap<
134
138
  HTMLElement,
@@ -185,6 +189,14 @@ function injectEditorStyles(): void {
185
189
  "[data-upstart-editor-active] .ProseMirror:focus {",
186
190
  " outline: none !important;",
187
191
  "}",
192
+ // Single-line editors (plain/direct, e.g. array-item pills) render their text
193
+ // inside a ProseMirror <p>, which would otherwise pick up the browser's default
194
+ // paragraph margin and change the element's height when edit mode activates.
195
+ "[data-upstart-editor-active][data-upstart-editable-text-mode='plain'] .ProseMirror p,",
196
+ "[data-upstart-editor-active][data-upstart-editable-text-mode='direct'] .ProseMirror p {",
197
+ " margin: 0 !important;",
198
+ " padding: 0 !important;",
199
+ "}",
188
200
  ].join("\n");
189
201
  document.head.appendChild(style);
190
202
  }
@@ -210,14 +222,14 @@ export function activateAllEditors(): void {
210
222
  */
211
223
  export function destroyAllActiveEditors(): void {
212
224
  stopDomObserver();
213
- for (const hash of activeEditors.keys()) {
214
- destroyEditor(hash);
225
+ for (const element of activeEditors.keys()) {
226
+ destroyEditor(element);
215
227
  }
216
228
  }
217
229
 
218
230
  function setupEditableElement(element: HTMLElement, options: Required<UpstartEditorOptions>): void {
219
231
  const hash = getEditableHash(element);
220
- if (!hash || activeEditors.has(hash)) {
232
+ if (!hash || activeEditors.has(element)) {
221
233
  return;
222
234
  }
223
235
 
@@ -232,19 +244,19 @@ function activateEditor(element: HTMLElement, hash: string, options: Required<Up
232
244
 
233
245
  switch (mode) {
234
246
  case "direct":
235
- editor = createDirectEditor(element, hash, options);
247
+ editor = createDirectEditor(element, options);
236
248
  break;
237
249
  case "rich-panel":
238
- editor = createRichPanelEditor(element, hash, options);
250
+ editor = createRichPanelEditor(element, options);
239
251
  break;
240
252
  case "block-rich":
241
- editor = createRichTextEditor(element, hash, options);
253
+ editor = createRichTextEditor(element, options);
242
254
  break;
243
255
  case "inline-rich":
244
- editor = createInlineRichTextEditor(element, hash, options);
256
+ editor = createInlineRichTextEditor(element, options);
245
257
  break;
246
258
  case "plain":
247
- editor = createPlainTextEditor(element, hash, options);
259
+ editor = createPlainTextEditor(element, options);
248
260
  break;
249
261
  }
250
262
 
@@ -252,14 +264,10 @@ function activateEditor(element: HTMLElement, hash: string, options: Required<Up
252
264
  element.dataset.upstartEditorActive = "true";
253
265
 
254
266
  applyActiveStyles(element);
255
- activeEditors.set(hash, { editor, element, hash, mode });
267
+ activeEditors.set(element, { editor, element, hash, mode });
256
268
  }
257
269
 
258
- function createPlainTextEditor(
259
- element: HTMLElement,
260
- hash: string,
261
- options: Required<UpstartEditorOptions>,
262
- ): Editor {
270
+ function createPlainTextEditor(element: HTMLElement, options: Required<UpstartEditorOptions>): Editor {
263
271
  const renderedText = element.textContent ?? "";
264
272
 
265
273
  let content: string | object = renderedText;
@@ -320,23 +328,19 @@ function createPlainTextEditor(
320
328
  onUpdate: () => {
321
329
  if (i18nSyncInProgress) return;
322
330
  hasChanged = true;
323
- syncI18nSiblings(hash);
331
+ syncI18nSiblings(element);
324
332
  },
325
333
  onBlur: ({ editor: e }) => {
326
334
  if (!hasChanged) return;
327
335
  hasChanged = false;
328
- saveText(hash, e.getText());
336
+ saveText(element, e.getText());
329
337
  },
330
338
  });
331
339
 
332
340
  return editor;
333
341
  }
334
342
 
335
- function createRichTextEditor(
336
- element: HTMLElement,
337
- hash: string,
338
- options: Required<UpstartEditorOptions>,
339
- ): Editor {
343
+ function createRichTextEditor(element: HTMLElement, options: Required<UpstartEditorOptions>): Editor {
340
344
  const content = element.innerHTML;
341
345
  element.innerHTML = ""; // Clear the element first!
342
346
  let hasChanged = false;
@@ -369,12 +373,12 @@ function createRichTextEditor(
369
373
  onUpdate: () => {
370
374
  if (i18nSyncInProgress) return;
371
375
  hasChanged = true;
372
- syncI18nSiblings(hash);
376
+ syncI18nSiblings(element);
373
377
  },
374
378
  onBlur: ({ editor: e }) => {
375
379
  if (!hasChanged) return;
376
380
  hasChanged = false;
377
- saveText(hash, e.getHTML());
381
+ saveText(element, e.getHTML());
378
382
  },
379
383
  });
380
384
 
@@ -385,11 +389,7 @@ function createRichTextEditor(
385
389
  return editor;
386
390
  }
387
391
 
388
- function createInlineRichTextEditor(
389
- element: HTMLElement,
390
- hash: string,
391
- options: Required<UpstartEditorOptions>,
392
- ): Editor {
392
+ function createInlineRichTextEditor(element: HTMLElement, options: Required<UpstartEditorOptions>): Editor {
393
393
  const content = element.innerHTML;
394
394
  element.innerHTML = ""; // Clear the element first!
395
395
  let hasChanged = false;
@@ -433,12 +433,12 @@ function createInlineRichTextEditor(
433
433
  onUpdate: () => {
434
434
  if (i18nSyncInProgress) return;
435
435
  hasChanged = true;
436
- syncI18nSiblings(hash);
436
+ syncI18nSiblings(element);
437
437
  },
438
438
  onBlur: ({ editor: e }) => {
439
439
  if (!hasChanged) return;
440
440
  hasChanged = false;
441
- saveText(hash, e.getHTML());
441
+ saveText(element, e.getHTML());
442
442
  },
443
443
  });
444
444
 
@@ -449,11 +449,7 @@ function createInlineRichTextEditor(
449
449
  return editor;
450
450
  }
451
451
 
452
- function createDirectEditor(
453
- element: HTMLElement,
454
- hash: string,
455
- options: Required<UpstartEditorOptions>,
456
- ): Editor {
452
+ function createDirectEditor(element: HTMLElement, options: Required<UpstartEditorOptions>): Editor {
457
453
  const content = element.innerHTML;
458
454
  element.innerHTML = "";
459
455
  let hasChanged = false;
@@ -489,7 +485,7 @@ function createDirectEditor(
489
485
  if (!hasChanged) return;
490
486
  hasChanged = false;
491
487
  // Strip the outer <p> that TipTap adds when using default document schema
492
- saveText(hash, e.getHTML().replace(/^<p>([\s\S]*)<\/p>$/, "$1"));
488
+ saveText(element, e.getHTML().replace(/^<p>([\s\S]*)<\/p>$/, "$1"));
493
489
  },
494
490
  });
495
491
 
@@ -500,11 +496,7 @@ function createDirectEditor(
500
496
  return editor;
501
497
  }
502
498
 
503
- function createRichPanelEditor(
504
- element: HTMLElement,
505
- hash: string,
506
- options: Required<UpstartEditorOptions>,
507
- ): Editor {
499
+ function createRichPanelEditor(element: HTMLElement, options: Required<UpstartEditorOptions>): Editor {
508
500
  const content = element.innerHTML;
509
501
  element.innerHTML = "";
510
502
  let hasChanged = false;
@@ -527,7 +519,7 @@ function createRichPanelEditor(
527
519
  onBlur: ({ editor: e }) => {
528
520
  if (!hasChanged) return;
529
521
  hasChanged = false;
530
- saveText(hash, e.getHTML());
522
+ saveText(element, e.getHTML());
531
523
  },
532
524
  });
533
525
 
@@ -567,11 +559,11 @@ function getEditorMode(element: HTMLElement, options: Required<UpstartEditorOpti
567
559
  return "block-rich";
568
560
  }
569
561
 
570
- function syncI18nSiblings(sourceHash: string): void {
571
- console.log("[Upstart Editor] Syncing i18n siblings for", sourceHash);
572
- const instance = activeEditors.get(sourceHash);
562
+ function syncI18nSiblings(sourceElement: HTMLElement): void {
563
+ const instance = activeEditors.get(sourceElement);
564
+ console.log("[Upstart Editor] Syncing i18n siblings for", instance?.hash);
573
565
  if (!instance) {
574
- console.warn("[Upstart Editor] No editor instance found for hash:", sourceHash);
566
+ console.warn("[Upstart Editor] No editor instance found for element:", sourceElement);
575
567
  return;
576
568
  }
577
569
 
@@ -593,12 +585,11 @@ function syncI18nSiblings(sourceHash: string): void {
593
585
  for (const sibling of siblings) {
594
586
  if (sibling === instance.element) continue;
595
587
 
596
- const siblingHash = sibling.dataset.upstartHash;
597
- const siblingInstance = siblingHash ? activeEditors.get(siblingHash) : null;
588
+ const siblingInstance = activeEditors.get(sibling);
598
589
 
599
590
  if (siblingInstance) {
600
591
  console.log(
601
- `[Upstart Editor] Updating sibling editor (hash: ${siblingHash}) with new content`,
592
+ `[Upstart Editor] Updating sibling editor (hash: ${siblingInstance.hash}) with new content`,
602
593
  siblingInstance,
603
594
  );
604
595
  sibling.innerText = plainContent;
@@ -612,9 +603,9 @@ function syncI18nSiblings(sourceHash: string): void {
612
603
  }
613
604
  }
614
605
 
615
- function saveText(hash: string, newText: string): void {
606
+ function saveText(element: HTMLElement, newText: string): void {
616
607
  try {
617
- const instance = activeEditors.get(hash);
608
+ const instance = activeEditors.get(element);
618
609
  const dataset = instance?.element.dataset ?? {};
619
610
 
620
611
  // direct/rich-panel always go via editTextDirect; plain goes via editTextDirect when a
@@ -638,7 +629,7 @@ function saveText(hash: string, newText: string): void {
638
629
  });
639
630
  }
640
631
 
641
- console.log("[Upstart Editor] Text save message sent:", hash);
632
+ console.log("[Upstart Editor] Text save message sent:", instance?.hash);
642
633
  } catch (error) {
643
634
  console.error("[Upstart Editor] Failed to send save message:", error);
644
635
  sendToParent({
@@ -648,8 +639,8 @@ function saveText(hash: string, newText: string): void {
648
639
  }
649
640
  }
650
641
 
651
- function destroyEditor(hash: string): void {
652
- const instance = activeEditors.get(hash);
642
+ function destroyEditor(element: HTMLElement): void {
643
+ const instance = activeEditors.get(element);
653
644
  if (!instance) {
654
645
  return;
655
646
  }
@@ -674,7 +665,7 @@ function destroyEditor(hash: string): void {
674
665
  }
675
666
 
676
667
  restoreStyles(instance.element);
677
- activeEditors.delete(hash);
668
+ activeEditors.delete(element);
678
669
  }
679
670
 
680
671
  // ---------------------------------------------------------------------------
@@ -749,10 +740,10 @@ function scheduleReactivation(): void {
749
740
  * (e.g. React replaced the subtree during a re-render).
750
741
  */
751
742
  function cleanupOrphanedEditors(): void {
752
- for (const [hash, instance] of activeEditors) {
743
+ for (const [element, instance] of activeEditors) {
753
744
  if (!document.contains(instance.element)) {
754
745
  instance.editor.destroy();
755
- activeEditors.delete(hash);
746
+ activeEditors.delete(element);
756
747
  }
757
748
  }
758
749
  }
@@ -1,5 +1,11 @@
1
1
  import type { Editor } from "@tiptap/core";
2
- import type { PayloadEditText, PayloadEditTextDirect } from "~/upstart-editor-api";
2
+ import type {
3
+ PayloadEditText,
4
+ PayloadEditTextDirect,
5
+ PayloadArrayItemAdd,
6
+ PayloadArrayItemDelete,
7
+ PayloadArraySet,
8
+ } from "~/upstart-editor-api";
3
9
 
4
10
  /**
5
11
  * Editor mode
@@ -38,13 +44,31 @@ export interface EditorInstance {
38
44
  mode: TextEditorMode;
39
45
  }
40
46
 
47
+ /**
48
+ * A swappable image surfaced to the inspector. `id` maps to a registry entry of
49
+ * type "image"; `src` is the currently rendered URL (for the thumbnail).
50
+ */
51
+ export interface EditableImageRef {
52
+ id: string;
53
+ src: string;
54
+ alt?: string;
55
+ }
56
+
41
57
  /**
42
58
  * Messages sent from iframe to parent editor.
43
59
  */
44
60
  export type EditorMessage =
45
- | { type: "text-edit"; payload: PayloadEditText | PayloadEditTextDirect }
46
- | { type: "editor-ready" }
47
- | { type: "editor-navigated" }
61
+ | {
62
+ type: "text-edit";
63
+ payload:
64
+ | PayloadEditText
65
+ | PayloadEditTextDirect
66
+ | PayloadArrayItemAdd
67
+ | PayloadArrayItemDelete
68
+ | PayloadArraySet;
69
+ }
70
+ | { type: "editor-ready"; path?: string }
71
+ | { type: "editor-navigated"; path?: string }
48
72
  | { type: "scroll-position"; x: number; y: number }
49
73
  | { type: "editor-error"; error: string }
50
74
  | {
@@ -59,6 +83,8 @@ export type EditorMessage =
59
83
  themeColors?: Record<string, string>;
60
84
  bounds: ElementBounds;
61
85
  viewportWidth?: number;
86
+ /** Editable images found in the clicked element's nearest subtree. */
87
+ images?: EditableImageRef[];
62
88
  };
63
89
 
64
90
  /**
@@ -67,6 +93,7 @@ export type EditorMessage =
67
93
  export type ParentMessage =
68
94
  | { type: "set-mode"; mode: EditorMode }
69
95
  | { type: "preview-classname"; classNameId: string; className: string; previewCSS?: string }
96
+ | { type: "preview-image"; imageId: string; src: string }
70
97
  | { type: "request-scroll-position" }
71
98
  | { type: "restore-scroll-position"; x: number; y: number };
72
99