@einblick/sdk 0.5.2 → 0.5.5

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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -10
  3. package/dist/assets.d.ts.map +1 -1
  4. package/dist/assets.js.map +1 -1
  5. package/dist/cli.js +2 -2
  6. package/dist/cli.js.map +1 -1
  7. package/dist/client.d.ts +18 -0
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +18 -31
  10. package/dist/client.js.map +1 -1
  11. package/dist/client.test.js +56 -68
  12. package/dist/client.test.js.map +1 -1
  13. package/dist/codegen.d.ts.map +1 -1
  14. package/dist/codegen.js +51 -12
  15. package/dist/codegen.js.map +1 -1
  16. package/dist/codegen.test.js +51 -7
  17. package/dist/codegen.test.js.map +1 -1
  18. package/dist/next/edit-boot.d.ts.map +1 -1
  19. package/dist/next/edit-boot.js +2 -6
  20. package/dist/next/edit-boot.js.map +1 -1
  21. package/dist/next/index.d.ts +2 -2
  22. package/dist/next/index.d.ts.map +1 -1
  23. package/dist/next/index.js +1 -1
  24. package/dist/next/index.js.map +1 -1
  25. package/dist/next/revalidate.d.ts.map +1 -1
  26. package/dist/next/revalidate.js +2 -7
  27. package/dist/next/revalidate.js.map +1 -1
  28. package/dist/react/i18n.d.ts +5 -0
  29. package/dist/react/i18n.d.ts.map +1 -1
  30. package/dist/react/i18n.js +14 -14
  31. package/dist/react/i18n.js.map +1 -1
  32. package/dist/react/image.d.ts.map +1 -1
  33. package/dist/react/image.js +1 -1
  34. package/dist/react/image.js.map +1 -1
  35. package/dist/react/index.d.ts +2 -2
  36. package/dist/react/index.d.ts.map +1 -1
  37. package/dist/react/index.js +2 -2
  38. package/dist/react/index.js.map +1 -1
  39. package/dist/react/markers.d.ts.map +1 -1
  40. package/dist/react/markers.js +6 -12
  41. package/dist/react/markers.js.map +1 -1
  42. package/dist/react/provider.d.ts.map +1 -1
  43. package/dist/react/provider.js +5 -3
  44. package/dist/react/provider.js.map +1 -1
  45. package/dist/react/runtime.d.ts.map +1 -1
  46. package/dist/react/runtime.js +727 -379
  47. package/dist/react/runtime.js.map +1 -1
  48. package/dist/react/shared.d.ts +2 -0
  49. package/dist/react/shared.d.ts.map +1 -1
  50. package/dist/react/shared.js +18 -17
  51. package/dist/react/shared.js.map +1 -1
  52. package/package.json +11 -4
@@ -1,98 +1,10 @@
1
- import { EINBLICK_AUTH_SOURCE, EINBLICK_BRIDGE_SOURCE, EINBLICK_EDITOR_SOURCE, EINBLICK_PARENT_SOURCE, formatValueForText, isEditSessionExpired, readBindingFromElement, resolveAppOrigin, serializeBindingValue, shouldUseInlineEditor, } from './shared.js';
1
+ import { EINBLICK_AUTH_SOURCE, EINBLICK_BRIDGE_SOURCE, EINBLICK_EDITOR_SOURCE, EINBLICK_PARENT_SOURCE, formatValueForText, isMediaEditableBinding, isEditSessionExpired, readBindingFromElement, resolveAppOrigin, serializeBindingValue, shouldUseInlineEditor, } from './shared.js';
2
2
  import { getEinblickSdkMessages, resolveEinblickSdkLocale, } from './i18n.js';
3
3
  const RUNTIME_STYLE_ID = 'einblick-edit-runtime-styles';
4
- const RELATIVE_POSITION_SENTINEL = '__einblick_runtime_original_static__';
5
4
  const EINBLICK_ACCENT_RGB = '250, 105, 62';
6
5
  const EINBLICK_ACCENT_COLOR = `rgb(${EINBLICK_ACCENT_RGB})`;
7
6
  const EINBLICK_ACCENT_BORDER = `rgba(${EINBLICK_ACCENT_RGB}, 0.92)`;
8
7
  const CONTROL_BAR_HEIGHT = 44;
9
- const INLINE_EDITOR_SHADOW_STYLES = `
10
- :host {
11
- display: block;
12
- width: 100%;
13
- color: rgb(15, 23, 42);
14
- font-family: system-ui, sans-serif;
15
- }
16
-
17
- *, *::before, *::after {
18
- box-sizing: border-box;
19
- }
20
-
21
- [data-einblick-inline-shell] {
22
- display: flex;
23
- flex-direction: column;
24
- gap: 0.75rem;
25
- width: 100%;
26
- color: rgb(15, 23, 42);
27
- font-family: system-ui, sans-serif;
28
- }
29
-
30
- [data-einblick-inline-control],
31
- [data-einblick-inline-textarea],
32
- [data-einblick-inline-select] {
33
- appearance: none;
34
- width: 100%;
35
- min-width: 0;
36
- margin: 0;
37
- border-radius: 0.9rem;
38
- border: 1px solid rgba(148, 163, 184, 0.6);
39
- background: rgb(255, 255, 255);
40
- padding: 0.75rem 0.9rem;
41
- font: 400 0.95rem/1.5 system-ui, sans-serif;
42
- color: rgb(15, 23, 42);
43
- box-shadow: none;
44
- letter-spacing: normal;
45
- text-transform: none;
46
- text-indent: 0;
47
- }
48
-
49
- [data-einblick-inline-control]:focus,
50
- [data-einblick-inline-textarea]:focus,
51
- [data-einblick-inline-select]:focus {
52
- outline: 2px solid rgba(${EINBLICK_ACCENT_RGB}, 0.3);
53
- outline-offset: 2px;
54
- border-color: ${EINBLICK_ACCENT_COLOR};
55
- }
56
-
57
- [data-einblick-inline-textarea] {
58
- min-height: 8rem;
59
- resize: vertical;
60
- }
61
-
62
- [data-einblick-inline-actions] {
63
- display: flex;
64
- flex-wrap: wrap;
65
- justify-content: flex-end;
66
- gap: 0.5rem;
67
- }
68
-
69
- [data-einblick-edit-action] {
70
- appearance: none;
71
- border: 1px solid rgba(148, 163, 184, 0.6);
72
- background: rgb(255, 255, 255);
73
- color: rgb(15, 23, 42);
74
- border-radius: 999px;
75
- padding: 0.65rem 1rem;
76
- font: 600 0.9rem/1 system-ui, sans-serif;
77
- cursor: pointer;
78
- }
79
-
80
- [data-einblick-edit-action]:disabled {
81
- cursor: wait;
82
- opacity: 0.72;
83
- }
84
-
85
- [data-einblick-edit-action="primary"] {
86
- border-color: ${EINBLICK_ACCENT_COLOR};
87
- background: ${EINBLICK_ACCENT_COLOR};
88
- color: rgb(255, 255, 255);
89
- }
90
-
91
- [data-einblick-inline-error] {
92
- font: 400 0.85rem/1.5 system-ui, sans-serif;
93
- color: rgb(185, 28, 28);
94
- }
95
- `;
96
8
  function getBrandBadgeSvg(size) {
97
9
  return `
98
10
  <svg viewBox="0 0 40 40" width="${size}" height="${size}" aria-hidden="true" focusable="false">
@@ -135,6 +47,32 @@ function createRandomId() {
135
47
  }
136
48
  return Math.random().toString(36).slice(2);
137
49
  }
50
+ function stopEvent(event) {
51
+ event.preventDefault();
52
+ event.stopPropagation();
53
+ }
54
+ function clamp(value, min, max) {
55
+ return Math.min(Math.max(value, min), max);
56
+ }
57
+ function isTextInputEvent(event) {
58
+ return (event.key.length === 1 ||
59
+ event.key === 'Enter' ||
60
+ event.key === 'Backspace' ||
61
+ event.key === 'Delete');
62
+ }
63
+ function fieldTypeAllowsMultiline(binding) {
64
+ return binding.fieldType === 'text' || binding.fieldType === 'markdown';
65
+ }
66
+ function getMediaAcceptValue(binding) {
67
+ return binding.fieldType === 'image' || binding.fieldType === 'images'
68
+ ? 'image/*'
69
+ : '';
70
+ }
71
+ function getMediaActionLabel(binding, messages) {
72
+ return binding.fieldType === 'image' || binding.fieldType === 'images'
73
+ ? messages.actions.replaceImage
74
+ : messages.actions.replaceFile;
75
+ }
138
76
  function injectRuntimeStyles() {
139
77
  if (typeof document === 'undefined') {
140
78
  return;
@@ -149,15 +87,27 @@ function injectRuntimeStyles() {
149
87
  scroll-margin-top: 96px;
150
88
  }
151
89
 
152
- [data-einblick-editable-active="true"] {
153
- outline: 2px solid ${EINBLICK_ACCENT_BORDER};
154
- outline-offset: 4px;
90
+ [data-einblick-edit-outline],
91
+ [data-einblick-edit-field-outline] {
92
+ position: fixed;
93
+ z-index: 2147483643;
94
+ box-sizing: border-box;
95
+ pointer-events: none;
96
+ border-radius: var(--einblick-overlay-radius, 0.35rem);
97
+ }
98
+
99
+ [data-einblick-edit-outline] {
100
+ border: 1px solid ${EINBLICK_ACCENT_BORDER};
101
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.7);
102
+ }
103
+
104
+ [data-einblick-edit-field-outline] {
105
+ border: 1px dashed rgba(${EINBLICK_ACCENT_RGB}, 0.78);
106
+ background: rgba(${EINBLICK_ACCENT_RGB}, 0.04);
155
107
  }
156
108
 
157
109
  [data-einblick-edit-controls] {
158
- position: absolute;
159
- top: 0.5rem;
160
- right: 0.5rem;
110
+ position: fixed;
161
111
  z-index: 2147483644;
162
112
  display: inline-flex;
163
113
  align-items: center;
@@ -165,7 +115,8 @@ function injectRuntimeStyles() {
165
115
  }
166
116
 
167
117
  [data-einblick-edit-button],
168
- [data-einblick-create-button] {
118
+ [data-einblick-create-button],
119
+ [data-einblick-field-button] {
169
120
  border: 1px solid ${EINBLICK_ACCENT_COLOR};
170
121
  border-radius: 999px;
171
122
  padding: 0.3rem 0.7rem;
@@ -175,22 +126,46 @@ function injectRuntimeStyles() {
175
126
  box-shadow: 0 8px 20px rgba(15, 23, 42, 0.16);
176
127
  }
177
128
 
178
- [data-einblick-edit-button] {
129
+ [data-einblick-edit-button],
130
+ [data-einblick-field-button="media"] {
179
131
  background: ${EINBLICK_ACCENT_COLOR};
180
132
  color: rgb(255, 255, 255);
181
133
  }
182
134
 
183
- [data-einblick-create-button] {
135
+ [data-einblick-edit-button] {
136
+ padding: 0.42rem 0.85rem;
137
+ font-size: 13px;
138
+ }
139
+
140
+ [data-einblick-create-button],
141
+ [data-einblick-field-button="text"] {
184
142
  background: rgb(255, 255, 255);
185
143
  color: ${EINBLICK_ACCENT_COLOR};
186
144
  }
187
145
 
188
146
  [data-einblick-edit-button]:hover,
189
- [data-einblick-create-button]:hover {
147
+ [data-einblick-create-button]:hover,
148
+ [data-einblick-field-button]:hover {
190
149
  background: rgb(255, 255, 255);
191
150
  color: ${EINBLICK_ACCENT_COLOR};
192
151
  }
193
152
 
153
+ [data-einblick-field-overlays] {
154
+ position: fixed;
155
+ inset: 0;
156
+ z-index: 2147483644;
157
+ pointer-events: none;
158
+ }
159
+
160
+ [data-einblick-field-button] {
161
+ position: fixed;
162
+ z-index: 2147483644;
163
+ pointer-events: auto;
164
+ padding: 0.24rem 0.58rem;
165
+ font-size: 11px;
166
+ box-shadow: 0 8px 18px rgba(15, 23, 42, 0.12);
167
+ }
168
+
194
169
  [data-einblick-brand-badge] {
195
170
  position: fixed;
196
171
  right: 1rem;
@@ -340,64 +315,11 @@ function injectRuntimeStyles() {
340
315
  }
341
316
 
342
317
  [data-einblick-inline-editing="true"] {
343
- outline: 2px solid ${EINBLICK_ACCENT_BORDER};
344
- outline-offset: 4px;
345
- border-radius: 1rem;
346
- background: rgba(${EINBLICK_ACCENT_RGB}, 0.08);
347
- padding: 0.8rem;
348
- }
349
-
350
- [data-einblick-inline-shell] {
351
- display: flex;
352
- flex-direction: column;
353
- gap: 0.75rem;
354
- width: 100%;
355
- }
356
-
357
- [data-einblick-inline-control],
358
- [data-einblick-inline-textarea],
359
- [data-einblick-inline-select] {
360
- width: 100%;
361
- min-width: 0;
362
- border-radius: 0.9rem;
363
- border: 1px solid rgba(148, 163, 184, 0.6);
364
- background: rgb(255, 255, 255);
365
- padding: 0.75rem 0.9rem;
366
- font: 400 0.95rem/1.5 system-ui, sans-serif;
367
- color: inherit;
368
- box-sizing: border-box;
318
+ cursor: text;
369
319
  }
370
320
 
371
- [data-einblick-inline-textarea] {
372
- min-height: 8rem;
373
- resize: vertical;
374
- }
375
-
376
- [data-einblick-inline-actions] {
377
- display: flex;
378
- flex-wrap: wrap;
379
- justify-content: flex-end;
380
- gap: 0.5rem;
381
- }
382
-
383
- [data-einblick-edit-action] {
384
- border-radius: 999px;
385
- border: 1px solid rgba(148, 163, 184, 0.6);
386
- background: rgb(255, 255, 255);
387
- padding: 0.65rem 1rem;
388
- font: 600 0.9rem/1 system-ui, sans-serif;
389
- cursor: pointer;
390
- }
391
-
392
- [data-einblick-edit-action="primary"] {
393
- border-color: ${EINBLICK_ACCENT_COLOR};
394
- background: ${EINBLICK_ACCENT_COLOR};
395
- color: rgb(255, 255, 255);
396
- }
397
-
398
- [data-einblick-inline-error] {
399
- font: 400 0.85rem/1.5 system-ui, sans-serif;
400
- color: rgb(185, 28, 28);
321
+ [data-einblick-inline-editing="true"]:focus {
322
+ outline: none;
401
323
  }
402
324
 
403
325
  [data-einblick-editor-backdrop] {
@@ -485,9 +407,12 @@ class EinblickEditRuntime {
485
407
  bridgeFrame = null;
486
408
  bridgePingInterval = null;
487
409
  activeElement = null;
410
+ activeOutline = null;
488
411
  hoverButtonContainer = null;
489
412
  hoverButton = null;
490
413
  hoverCreateButton = null;
414
+ quickFieldOverlayContainer = null;
415
+ quickFieldOverlays = [];
491
416
  brandBadge = null;
492
417
  brandPopover = null;
493
418
  brandPopoverSiteName = null;
@@ -510,6 +435,7 @@ class EinblickEditRuntime {
510
435
  currentInlineTarget = null;
511
436
  currentInlineBinding = null;
512
437
  currentInlineRestoreState = null;
438
+ inlineSaveInFlight = null;
513
439
  editorBackdrop = null;
514
440
  editorNonce = null;
515
441
  editorTarget = null;
@@ -519,7 +445,9 @@ class EinblickEditRuntime {
519
445
  navigatorNonce = null;
520
446
  navigatorOpen = false;
521
447
  pendingInlineSaves = new Map();
448
+ pendingInlineMediaSaves = new Map();
522
449
  pendingLogin = null;
450
+ pendingLogout = null;
523
451
  constructor(options) {
524
452
  this.siteKey = options.siteKey;
525
453
  this.appOrigin = resolveAppOrigin(options.appOrigin, options.appUrl);
@@ -603,15 +531,23 @@ class EinblickEditRuntime {
603
531
  this.mutationObserver?.disconnect();
604
532
  this.mutationObserver = null;
605
533
  this.clearBridgePing();
534
+ if (this.pendingLogout) {
535
+ window.clearTimeout(this.pendingLogout.timeoutId);
536
+ this.pendingLogout.resolve(false);
537
+ this.pendingLogout = null;
538
+ }
606
539
  this.bridgeFrame?.remove();
607
540
  this.bridgeFrame = null;
608
541
  this.setActiveElement(null);
542
+ this.activeOutline?.remove();
543
+ this.activeOutline = null;
609
544
  this.hoverButtonContainer?.remove();
610
545
  this.hoverButtonContainer = null;
611
546
  this.hoverButton?.remove();
612
547
  this.hoverButton = null;
613
548
  this.hoverCreateButton?.remove();
614
549
  this.hoverCreateButton = null;
550
+ this.clearQuickFieldOverlays();
615
551
  this.brandBadge?.remove();
616
552
  this.brandBadge = null;
617
553
  this.brandPopover?.remove();
@@ -645,6 +581,11 @@ class EinblickEditRuntime {
645
581
  pending.reject(new Error(this.messages.errors.editingInterrupted));
646
582
  }
647
583
  this.pendingInlineSaves.clear();
584
+ for (const pending of this.pendingInlineMediaSaves.values()) {
585
+ window.clearTimeout(pending.timeoutId);
586
+ pending.reject(new Error(this.messages.errors.editingInterrupted));
587
+ }
588
+ this.pendingInlineMediaSaves.clear();
648
589
  this.setState({
649
590
  enabled: true,
650
591
  active: false,
@@ -656,7 +597,7 @@ class EinblickEditRuntime {
656
597
  }
657
598
  refreshLocalizedUi() {
658
599
  if (this.hoverButton) {
659
- this.hoverButton.textContent = this.messages.actions.edit;
600
+ this.updateHoverButtonLabel();
660
601
  }
661
602
  if (this.hoverCreateButton) {
662
603
  this.hoverCreateButton.textContent = this.messages.actions.addRecord;
@@ -673,11 +614,11 @@ class EinblickEditRuntime {
673
614
  this.messages.actions.goToEinblick;
674
615
  }
675
616
  if (this.brandPopoverLogoutButton) {
676
- this.brandPopoverLogoutButton.textContent =
677
- this.messages.actions.logout;
617
+ this.brandPopoverLogoutButton.textContent = this.messages.actions.logout;
678
618
  }
679
619
  this.updateBrandPopover();
680
620
  this.updateBottomBar();
621
+ this.updateHoverChrome();
681
622
  }
682
623
  async login(options) {
683
624
  if (typeof window === 'undefined') {
@@ -716,12 +657,7 @@ class EinblickEditRuntime {
716
657
  }
717
658
  }
718
659
  }, 400);
719
- this.pendingLogin = {
720
- nonce: loginNonce,
721
- popup,
722
- resolve,
723
- closePollId,
724
- };
660
+ this.pendingLogin = { nonce: loginNonce, popup, resolve, closePollId };
725
661
  });
726
662
  }
727
663
  openEditor(binding, element) {
@@ -744,13 +680,24 @@ class EinblickEditRuntime {
744
680
  this.setActiveElement(null);
745
681
  return;
746
682
  }
683
+ if (this.isSdkChromeTarget(event.target)) {
684
+ this.updateHoverChrome();
685
+ return;
686
+ }
747
687
  const target = this.findEditableTarget(event.target);
748
688
  this.setActiveElement(target);
749
689
  };
750
690
  handleViewportChange = () => {
691
+ if (this.currentInlineTarget?.isConnected) {
692
+ this.ensureActiveOutline();
693
+ this.positionOutline(this.activeOutline, this.currentInlineTarget);
694
+ return;
695
+ }
751
696
  if (this.activeElement && !this.activeElement.isConnected) {
752
697
  this.setActiveElement(null);
698
+ return;
753
699
  }
700
+ this.updateHoverChrome();
754
701
  };
755
702
  handleWindowBlur = () => {
756
703
  this.setBrandPopoverOpen(false);
@@ -791,8 +738,7 @@ class EinblickEditRuntime {
791
738
  this.setActiveElement(null);
792
739
  };
793
740
  handleHoverButtonClick = (event) => {
794
- event.preventDefault();
795
- event.stopPropagation();
741
+ stopEvent(event);
796
742
  if (!this.activeElement) {
797
743
  return;
798
744
  }
@@ -800,11 +746,14 @@ class EinblickEditRuntime {
800
746
  if (!binding) {
801
747
  return;
802
748
  }
749
+ if (binding.fieldKey && isMediaEditableBinding(binding)) {
750
+ this.openMediaPicker(this.activeElement, binding, this.hoverButton);
751
+ return;
752
+ }
803
753
  this.openEditor(binding, this.activeElement);
804
754
  };
805
755
  handleCreateButtonClick = (event) => {
806
- event.preventDefault();
807
- event.stopPropagation();
756
+ stopEvent(event);
808
757
  if (!this.activeElement) {
809
758
  return;
810
759
  }
@@ -821,6 +770,16 @@ class EinblickEditRuntime {
821
770
  this.setBottomBarMenuOpen(false);
822
771
  return;
823
772
  }
773
+ if (this.currentInlineTarget) {
774
+ if (this.currentInlineTarget.contains(target) ||
775
+ this.isSdkChromeTarget(target)) {
776
+ return;
777
+ }
778
+ event.preventDefault();
779
+ event.stopImmediatePropagation();
780
+ void this.commitInlineEditor();
781
+ return;
782
+ }
824
783
  if (this.isBrandPopoverOpen() &&
825
784
  !this.brandBadge?.contains(target) &&
826
785
  !this.brandPopover?.contains(target)) {
@@ -895,6 +854,35 @@ class EinblickEditRuntime {
895
854
  }
896
855
  return;
897
856
  }
857
+ if (message.type === 'logout-result') {
858
+ const pending = this.pendingLogout;
859
+ if (!pending || pending.requestId !== message.requestId) {
860
+ return;
861
+ }
862
+ window.clearTimeout(pending.timeoutId);
863
+ this.pendingLogout = null;
864
+ if (!message.ok && message.message) {
865
+ this.callbacks.onError?.(message.message);
866
+ }
867
+ pending.resolve(message.ok);
868
+ return;
869
+ }
870
+ if (message.type === 'inline-media-result') {
871
+ const pending = this.pendingInlineMediaSaves.get(message.requestId);
872
+ if (!pending) {
873
+ return;
874
+ }
875
+ this.pendingInlineMediaSaves.delete(message.requestId);
876
+ window.clearTimeout(pending.timeoutId);
877
+ if (message.ok) {
878
+ pending.resolve({ fileId: message.fileId, value: message.value });
879
+ return;
880
+ }
881
+ const error = new Error(message.error || this.messages.errors.saveFieldFailed);
882
+ pending.reject(error);
883
+ this.callbacks.onError?.(error.message);
884
+ return;
885
+ }
898
886
  const pending = this.pendingInlineSaves.get(message.requestId);
899
887
  if (!pending) {
900
888
  return;
@@ -921,11 +909,7 @@ class EinblickEditRuntime {
921
909
  if (message.ok) {
922
910
  if (!message.sessionToken) {
923
911
  const errorMessage = this.messages.errors.signInDidNotReturnSession;
924
- this.setState({
925
- ...this.state,
926
- errorMessage,
927
- status: 'error',
928
- });
912
+ this.setState({ ...this.state, errorMessage, status: 'error' });
929
913
  this.callbacks.onError?.(errorMessage);
930
914
  pending.resolve(false);
931
915
  return;
@@ -943,11 +927,7 @@ class EinblickEditRuntime {
943
927
  return;
944
928
  }
945
929
  const errorMessage = message.message || this.messages.errors.signInFailed;
946
- this.setState({
947
- ...this.state,
948
- errorMessage,
949
- status: 'error',
950
- });
930
+ this.setState({ ...this.state, errorMessage, status: 'error' });
951
931
  this.callbacks.onError?.(errorMessage);
952
932
  pending.resolve(false);
953
933
  }
@@ -1074,16 +1054,23 @@ class EinblickEditRuntime {
1074
1054
  container.hidden = true;
1075
1055
  const editButton = document.createElement('button');
1076
1056
  editButton.type = 'button';
1077
- editButton.textContent = this.messages.actions.edit;
1057
+ editButton.textContent = this.messages.actions.editElement;
1078
1058
  editButton.setAttribute('data-einblick-edit-button', 'true');
1059
+ editButton.addEventListener('pointerdown', (event) => {
1060
+ event.stopPropagation();
1061
+ });
1079
1062
  editButton.addEventListener('click', this.handleHoverButtonClick);
1080
1063
  const createButton = document.createElement('button');
1081
1064
  createButton.type = 'button';
1082
1065
  createButton.textContent = this.messages.actions.addRecord;
1083
1066
  createButton.hidden = true;
1084
1067
  createButton.setAttribute('data-einblick-create-button', 'true');
1068
+ createButton.addEventListener('pointerdown', (event) => {
1069
+ event.stopPropagation();
1070
+ });
1085
1071
  createButton.addEventListener('click', this.handleCreateButtonClick);
1086
1072
  container.append(editButton, createButton);
1073
+ document.body.append(container);
1087
1074
  this.hoverButtonContainer = container;
1088
1075
  this.hoverButton = editButton;
1089
1076
  this.hoverCreateButton = createButton;
@@ -1635,6 +1622,48 @@ class EinblickEditRuntime {
1635
1622
  siteName: this.state.siteName,
1636
1623
  errorMessage: undefined,
1637
1624
  });
1625
+ const loggedOutViaBridge = await this.requestBridgeLogout();
1626
+ if (!loggedOutViaBridge) {
1627
+ await this.logoutViaPopup(sessionToken);
1628
+ }
1629
+ this.reloadBridge();
1630
+ }
1631
+ requestBridgeLogout() {
1632
+ if (typeof window === 'undefined' || this.pendingLogout) {
1633
+ return Promise.resolve(false);
1634
+ }
1635
+ const target = this.bridgeFrame?.contentWindow;
1636
+ if (!target) {
1637
+ return Promise.resolve(false);
1638
+ }
1639
+ const requestId = createRandomId();
1640
+ return new Promise((resolve) => {
1641
+ const timeoutId = window.setTimeout(() => {
1642
+ if (this.pendingLogout?.requestId !== requestId) {
1643
+ return;
1644
+ }
1645
+ this.pendingLogout = null;
1646
+ resolve(false);
1647
+ }, 2500);
1648
+ this.pendingLogout = { requestId, resolve, timeoutId };
1649
+ try {
1650
+ target.postMessage({
1651
+ source: EINBLICK_PARENT_SOURCE,
1652
+ type: 'logout',
1653
+ nonce: this.bridgeNonce,
1654
+ requestId,
1655
+ }, this.appOrigin);
1656
+ }
1657
+ catch {
1658
+ window.clearTimeout(timeoutId);
1659
+ if (this.pendingLogout?.requestId === requestId) {
1660
+ this.pendingLogout = null;
1661
+ }
1662
+ resolve(false);
1663
+ }
1664
+ });
1665
+ }
1666
+ async logoutViaPopup(sessionToken) {
1638
1667
  const logoutUrl = this.buildBridgeUrl({
1639
1668
  mode: 'logout',
1640
1669
  nonce: createRandomId(),
@@ -1661,28 +1690,11 @@ class EinblickEditRuntime {
1661
1690
  }
1662
1691
  }, 400);
1663
1692
  });
1664
- this.reloadBridge();
1665
1693
  }
1666
1694
  setActiveElement(element) {
1667
1695
  if (this.activeElement === element) {
1668
- if (this.hoverButtonContainer &&
1669
- element &&
1670
- !element.contains(this.hoverButtonContainer)) {
1671
- element.append(this.hoverButtonContainer);
1672
- }
1673
- this.updateHoverButtonVisibility(element);
1674
- return;
1675
- }
1676
- if (this.activeElement) {
1677
- this.activeElement.removeAttribute('data-einblick-editable-active');
1678
- const originalPosition = this.activeElement.dataset.einblickRuntimeOriginalPosition;
1679
- if (originalPosition !== undefined) {
1680
- this.activeElement.style.position =
1681
- originalPosition === RELATIVE_POSITION_SENTINEL
1682
- ? ''
1683
- : originalPosition;
1684
- delete this.activeElement.dataset.einblickRuntimeOriginalPosition;
1685
- }
1696
+ this.updateHoverChrome();
1697
+ return;
1686
1698
  }
1687
1699
  this.activeElement = element;
1688
1700
  if (!element ||
@@ -1691,22 +1703,176 @@ class EinblickEditRuntime {
1691
1703
  !this.state.isAuthenticated) {
1692
1704
  if (this.hoverButtonContainer) {
1693
1705
  this.hoverButtonContainer.hidden = true;
1694
- this.hoverButtonContainer.remove();
1695
1706
  }
1707
+ this.activeOutline?.remove();
1708
+ this.activeOutline = null;
1709
+ this.clearQuickFieldOverlays();
1696
1710
  if (this.hoverCreateButton) {
1697
1711
  this.hoverCreateButton.hidden = true;
1698
1712
  }
1699
1713
  return;
1700
1714
  }
1701
- element.setAttribute('data-einblick-editable-active', 'true');
1702
- if (window.getComputedStyle(element).position === 'static') {
1703
- element.dataset.einblickRuntimeOriginalPosition =
1704
- element.style.position || RELATIVE_POSITION_SENTINEL;
1705
- element.style.position = 'relative';
1706
- }
1715
+ this.ensureActiveOutline();
1716
+ this.hoverButtonContainer.hidden = false;
1717
+ this.updateHoverButtonLabel();
1707
1718
  this.updateHoverButtonVisibility(element);
1719
+ this.renderQuickFieldOverlays(element);
1720
+ this.updateHoverChrome();
1721
+ }
1722
+ ensureActiveOutline() {
1723
+ if (this.activeOutline || typeof document === 'undefined') {
1724
+ return;
1725
+ }
1726
+ const outline = document.createElement('div');
1727
+ outline.setAttribute('data-einblick-edit-outline', 'true');
1728
+ document.body.append(outline);
1729
+ this.activeOutline = outline;
1730
+ }
1731
+ updateHoverChrome() {
1732
+ if (!this.activeElement || !this.state.isAuthenticated) {
1733
+ this.activeOutline?.remove();
1734
+ this.activeOutline = null;
1735
+ if (this.hoverButtonContainer) {
1736
+ this.hoverButtonContainer.hidden = true;
1737
+ }
1738
+ this.clearQuickFieldOverlays();
1739
+ return;
1740
+ }
1741
+ if (!this.activeElement.isConnected) {
1742
+ this.setActiveElement(null);
1743
+ return;
1744
+ }
1745
+ this.ensureActiveOutline();
1746
+ const rect = this.activeElement.getBoundingClientRect();
1747
+ const isVisible = this.positionOutline(this.activeOutline, this.activeElement);
1748
+ if (!isVisible || rect.width <= 0 || rect.height <= 0) {
1749
+ if (this.hoverButtonContainer) {
1750
+ this.hoverButtonContainer.hidden = true;
1751
+ }
1752
+ this.clearQuickFieldOverlays();
1753
+ return;
1754
+ }
1755
+ this.updateHoverButtonLabel();
1756
+ this.updateHoverButtonVisibility(this.activeElement);
1757
+ this.positionHoverControls(rect);
1758
+ this.updateQuickFieldOverlayPositions();
1759
+ }
1760
+ positionOutline(outline, target) {
1761
+ if (!outline) {
1762
+ return false;
1763
+ }
1764
+ const rect = target.getBoundingClientRect();
1765
+ if (rect.width <= 0 || rect.height <= 0) {
1766
+ outline.hidden = true;
1767
+ return false;
1768
+ }
1769
+ const style = window.getComputedStyle(target);
1770
+ outline.hidden = false;
1771
+ outline.style.setProperty('--einblick-overlay-radius', style.borderRadius || '0.35rem');
1772
+ outline.style.left = `${Math.round(rect.left)}px`;
1773
+ outline.style.top = `${Math.round(rect.top)}px`;
1774
+ outline.style.width = `${Math.round(rect.width)}px`;
1775
+ outline.style.height = `${Math.round(rect.height)}px`;
1776
+ return true;
1777
+ }
1778
+ positionHoverControls(rect) {
1779
+ if (!this.hoverButtonContainer) {
1780
+ return;
1781
+ }
1708
1782
  this.hoverButtonContainer.hidden = false;
1709
- element.append(this.hoverButtonContainer);
1783
+ const margin = 8;
1784
+ const width = this.hoverButtonContainer.offsetWidth || 180;
1785
+ const height = this.hoverButtonContainer.offsetHeight || 32;
1786
+ const left = clamp(rect.right - width - margin, margin, Math.max(margin, window.innerWidth - width - margin));
1787
+ const top = clamp(rect.top + margin, margin, Math.max(margin, window.innerHeight - height - margin));
1788
+ this.hoverButtonContainer.style.left = `${Math.round(left)}px`;
1789
+ this.hoverButtonContainer.style.top = `${Math.round(top)}px`;
1790
+ }
1791
+ clearQuickFieldOverlays() {
1792
+ this.quickFieldOverlayContainer?.remove();
1793
+ this.quickFieldOverlayContainer = null;
1794
+ this.quickFieldOverlays = [];
1795
+ }
1796
+ renderQuickFieldOverlays(element) {
1797
+ this.clearQuickFieldOverlays();
1798
+ const targets = this.getQuickEditableTargets(element);
1799
+ if (targets.length === 0 || typeof document === 'undefined') {
1800
+ return;
1801
+ }
1802
+ const container = document.createElement('div');
1803
+ container.setAttribute('data-einblick-field-overlays', 'true');
1804
+ this.quickFieldOverlays = targets.map((target) => {
1805
+ const binding = readBindingFromElement(target);
1806
+ const outline = document.createElement('div');
1807
+ outline.setAttribute('data-einblick-edit-field-outline', 'true');
1808
+ const button = document.createElement('button');
1809
+ button.type = 'button';
1810
+ button.setAttribute('data-einblick-field-button', binding && isMediaEditableBinding(binding) ? 'media' : 'text');
1811
+ button.textContent =
1812
+ binding && isMediaEditableBinding(binding)
1813
+ ? getMediaActionLabel(binding, this.messages)
1814
+ : this.messages.actions.editText;
1815
+ button.addEventListener('pointerdown', (event) => {
1816
+ event.stopPropagation();
1817
+ });
1818
+ button.addEventListener('click', (event) => {
1819
+ stopEvent(event);
1820
+ const nextBinding = readBindingFromElement(target);
1821
+ if (!nextBinding) {
1822
+ return;
1823
+ }
1824
+ if (isMediaEditableBinding(nextBinding)) {
1825
+ this.openMediaPicker(target, nextBinding, button);
1826
+ return;
1827
+ }
1828
+ this.openInlineEditor(target, nextBinding);
1829
+ });
1830
+ container.append(outline, button);
1831
+ return { target, outline, button };
1832
+ });
1833
+ document.body.append(container);
1834
+ this.quickFieldOverlayContainer = container;
1835
+ this.updateQuickFieldOverlayPositions();
1836
+ }
1837
+ updateQuickFieldOverlayPositions() {
1838
+ if (!this.quickFieldOverlayContainer) {
1839
+ return;
1840
+ }
1841
+ for (const overlay of this.quickFieldOverlays) {
1842
+ if (!overlay.target.isConnected) {
1843
+ overlay.outline.hidden = true;
1844
+ overlay.button.hidden = true;
1845
+ continue;
1846
+ }
1847
+ const rect = overlay.target.getBoundingClientRect();
1848
+ const isVisible = this.positionOutline(overlay.outline, overlay.target);
1849
+ overlay.button.hidden = !isVisible;
1850
+ if (!isVisible) {
1851
+ continue;
1852
+ }
1853
+ const margin = 4;
1854
+ const width = overlay.button.offsetWidth || 96;
1855
+ const height = overlay.button.offsetHeight || 26;
1856
+ const left = clamp(rect.left + margin, margin, Math.max(margin, window.innerWidth - width - margin));
1857
+ const top = clamp(rect.top + margin, margin, Math.max(margin, window.innerHeight - height - margin));
1858
+ overlay.button.style.left = `${Math.round(left)}px`;
1859
+ overlay.button.style.top = `${Math.round(top)}px`;
1860
+ }
1861
+ }
1862
+ updateHoverButtonLabel() {
1863
+ if (!this.hoverButton || !this.activeElement) {
1864
+ return;
1865
+ }
1866
+ const binding = readBindingFromElement(this.activeElement);
1867
+ if (!binding?.fieldKey) {
1868
+ this.hoverButton.textContent = this.messages.actions.editElement;
1869
+ return;
1870
+ }
1871
+ this.hoverButton.textContent = isMediaEditableBinding(binding)
1872
+ ? getMediaActionLabel(binding, this.messages)
1873
+ : shouldUseInlineEditor(binding)
1874
+ ? this.messages.actions.editText
1875
+ : this.messages.actions.edit;
1710
1876
  }
1711
1877
  reloadBridge() {
1712
1878
  this.clearBridgePing();
@@ -1859,7 +2025,67 @@ class EinblickEditRuntime {
1859
2025
  return null;
1860
2026
  }
1861
2027
  const editableTarget = target.closest('[data-einblick-editable="true"]');
1862
- return editableTarget ?? null;
2028
+ if (!editableTarget) {
2029
+ return null;
2030
+ }
2031
+ const binding = readBindingFromElement(editableTarget);
2032
+ if (!binding?.fieldKey) {
2033
+ return editableTarget;
2034
+ }
2035
+ const entryTarget = this.findEntryTargetForField(editableTarget, binding);
2036
+ return entryTarget ?? editableTarget;
2037
+ }
2038
+ findEntryTargetForField(fieldElement, binding) {
2039
+ let parent = fieldElement.parentElement;
2040
+ while (parent) {
2041
+ const parentBinding = readBindingFromElement(parent);
2042
+ if (parentBinding &&
2043
+ !parentBinding.fieldKey &&
2044
+ parentBinding.resourceSlug === binding.resourceSlug &&
2045
+ parentBinding.recordId === binding.recordId) {
2046
+ return parent;
2047
+ }
2048
+ parent = parent.parentElement;
2049
+ }
2050
+ return null;
2051
+ }
2052
+ getQuickEditableTargets(element) {
2053
+ const binding = readBindingFromElement(element);
2054
+ if (!binding) {
2055
+ return [];
2056
+ }
2057
+ if (binding.fieldKey) {
2058
+ return shouldUseInlineEditor(binding) || isMediaEditableBinding(binding)
2059
+ ? [element]
2060
+ : [];
2061
+ }
2062
+ const selector = [
2063
+ '[data-einblick-editable="true"]',
2064
+ `[data-einblick-resource="${CSS.escape(binding.resourceSlug)}"]`,
2065
+ `[data-einblick-record-id="${CSS.escape(binding.recordId)}"]`,
2066
+ '[data-einblick-field-key]',
2067
+ ].join('');
2068
+ const fields = Array.from(element.querySelectorAll(selector));
2069
+ return fields
2070
+ .filter((candidate) => {
2071
+ const candidateBinding = readBindingFromElement(candidate);
2072
+ return (!!candidateBinding &&
2073
+ (shouldUseInlineEditor(candidateBinding) ||
2074
+ isMediaEditableBinding(candidateBinding)));
2075
+ })
2076
+ .slice(0, 24);
2077
+ }
2078
+ isSdkChromeTarget(target) {
2079
+ if (!(target instanceof Node)) {
2080
+ return false;
2081
+ }
2082
+ return (!!this.hoverButtonContainer?.contains(target) ||
2083
+ !!this.quickFieldOverlayContainer?.contains(target) ||
2084
+ !!this.activeOutline?.contains(target) ||
2085
+ !!this.brandBadge?.contains(target) ||
2086
+ !!this.brandPopover?.contains(target) ||
2087
+ !!this.bottomBarHost?.contains(target) ||
2088
+ !!this.bottomBarMoreMenu?.contains(target));
1863
2089
  }
1864
2090
  findElementForBinding(binding) {
1865
2091
  const selector = [
@@ -1888,217 +2114,286 @@ class EinblickEditRuntime {
1888
2114
  this.closeDrawer();
1889
2115
  this.closeInlineEditor();
1890
2116
  this.setActiveElement(null);
1891
- const computedDisplay = window.getComputedStyle(target).display;
1892
- const isInlineLayout = computedDisplay === 'inline' ||
1893
- computedDisplay === 'inline-block' ||
1894
- computedDisplay === 'inline-flex' ||
1895
- computedDisplay === 'contents';
1896
- const targetRect = target.getBoundingClientRect();
1897
- this.currentInlineRestoreState = {
1898
- childNodes: Array.from(target.childNodes),
1899
- display: target.style.display,
1900
- flexDirection: target.style.flexDirection,
1901
- alignItems: target.style.alignItems,
1902
- gap: target.style.gap,
1903
- minWidth: target.style.minWidth,
2117
+ const restoreState = {
2118
+ childNodes: Array.from(target.childNodes).map((node) => node.cloneNode(true)),
2119
+ contentEditable: target.getAttribute('contenteditable'),
2120
+ spellcheck: target.getAttribute('spellcheck'),
2121
+ tabIndex: target.tabIndex,
2122
+ hadTabIndex: target.hasAttribute('tabindex'),
2123
+ role: target.getAttribute('role'),
2124
+ ariaMultiline: target.getAttribute('aria-multiline'),
2125
+ title: target.getAttribute('title'),
2126
+ value: binding.value,
2127
+ cleanup: [],
1904
2128
  };
2129
+ this.currentInlineRestoreState = restoreState;
1905
2130
  target.dataset.einblickInlineEditing = 'true';
1906
- target.style.display = isInlineLayout ? 'inline-flex' : 'flex';
1907
- target.style.flexDirection = 'column';
1908
- target.style.alignItems = 'stretch';
1909
- target.style.gap = '0.75rem';
1910
- target.style.minWidth = isInlineLayout
1911
- ? `${Math.min(Math.max(Math.round(targetRect.width), 220), Math.max(window.innerWidth - 32, 220))}px`
1912
- : target.style.minWidth;
1913
- const shellHost = document.createElement('div');
1914
- const shadowRoot = shellHost.attachShadow({ mode: 'open' });
1915
- const shadowStyle = document.createElement('style');
1916
- shadowStyle.textContent = INLINE_EDITOR_SHADOW_STYLES;
1917
- const shell = document.createElement('div');
1918
- shell.setAttribute('data-einblick-inline-shell', 'true');
1919
- const control = this.createInlineControl(binding, target);
1920
- this.setControlValue(control, binding, binding.value);
1921
- const errorMessage = document.createElement('div');
1922
- errorMessage.setAttribute('data-einblick-inline-error', 'true');
1923
- errorMessage.hidden = true;
1924
- const actions = document.createElement('div');
1925
- actions.setAttribute('data-einblick-inline-actions', 'true');
1926
- const cancelButton = document.createElement('button');
1927
- cancelButton.type = 'button';
1928
- cancelButton.textContent = this.messages.actions.cancel;
1929
- cancelButton.setAttribute('data-einblick-edit-action', 'secondary');
1930
- cancelButton.addEventListener('click', () => this.closeInlineEditor());
1931
- const saveButton = document.createElement('button');
1932
- saveButton.type = 'button';
1933
- saveButton.textContent = this.messages.actions.save;
1934
- saveButton.setAttribute('data-einblick-edit-action', 'primary');
1935
- const runSave = async () => {
1936
- errorMessage.hidden = true;
1937
- errorMessage.textContent = '';
1938
- saveButton.disabled = true;
1939
- saveButton.textContent = this.messages.actions.saving;
1940
- cancelButton.disabled = true;
1941
- try {
1942
- const nextValue = this.getControlValue(control, binding);
1943
- const savedValue = await this.saveInlineField(binding, nextValue);
1944
- this.closeInlineEditor();
1945
- this.applyValueToElement(target, binding, savedValue);
1946
- await this.callbacks.onSave?.({
1947
- binding,
1948
- value: savedValue,
1949
- source: 'inline',
1950
- });
1951
- }
1952
- catch (error) {
1953
- errorMessage.hidden = false;
1954
- errorMessage.textContent =
1955
- error instanceof Error
1956
- ? error.message
1957
- : this.messages.errors.saveFieldFailed;
1958
- saveButton.disabled = false;
1959
- saveButton.textContent = this.messages.actions.save;
1960
- cancelButton.disabled = false;
1961
- }
2131
+ target.setAttribute('contenteditable', 'plaintext-only');
2132
+ target.setAttribute('spellcheck', 'true');
2133
+ target.setAttribute('role', 'textbox');
2134
+ target.setAttribute('aria-multiline', fieldTypeAllowsMultiline(binding) ? 'true' : 'false');
2135
+ if (!target.hasAttribute('tabindex')) {
2136
+ target.tabIndex = 0;
2137
+ }
2138
+ if ((target.textContent ?? '').trim().length === 0 &&
2139
+ binding.value !== undefined &&
2140
+ binding.value !== null) {
2141
+ target.textContent = formatValueForText(binding.value);
2142
+ }
2143
+ const handleClick = (event) => {
2144
+ event.preventDefault();
2145
+ event.stopPropagation();
1962
2146
  };
1963
- saveButton.addEventListener('click', () => {
1964
- void runSave();
1965
- });
1966
- control.addEventListener('keydown', (event) => {
1967
- const keyboardEvent = event;
2147
+ const handleKeyDown = (keyboardEvent) => {
1968
2148
  if (keyboardEvent.key === 'Escape') {
1969
2149
  keyboardEvent.preventDefault();
2150
+ keyboardEvent.stopPropagation();
1970
2151
  this.closeInlineEditor();
1971
2152
  return;
1972
2153
  }
1973
2154
  if (keyboardEvent.key === 'Enter' &&
1974
- (!(control instanceof HTMLTextAreaElement) ||
2155
+ (!fieldTypeAllowsMultiline(binding) ||
1975
2156
  keyboardEvent.metaKey ||
1976
2157
  keyboardEvent.ctrlKey)) {
1977
2158
  keyboardEvent.preventDefault();
1978
- void runSave();
2159
+ keyboardEvent.stopPropagation();
2160
+ void this.commitInlineEditor();
2161
+ return;
1979
2162
  }
1980
- });
1981
- actions.append(cancelButton, saveButton);
1982
- shell.append(control, errorMessage, actions);
1983
- shadowRoot.append(shadowStyle, shell);
1984
- target.replaceChildren(shellHost);
2163
+ if (isTextInputEvent(keyboardEvent)) {
2164
+ keyboardEvent.stopPropagation();
2165
+ }
2166
+ };
2167
+ const handlePaste = (event) => {
2168
+ const text = event.clipboardData?.getData('text/plain');
2169
+ if (text === undefined) {
2170
+ return;
2171
+ }
2172
+ event.preventDefault();
2173
+ event.stopPropagation();
2174
+ this.insertPlainTextAtSelection(text);
2175
+ };
2176
+ const handleBlur = () => {
2177
+ window.setTimeout(() => {
2178
+ if (this.currentInlineTarget === target) {
2179
+ void this.commitInlineEditor();
2180
+ }
2181
+ }, 0);
2182
+ };
2183
+ target.addEventListener('click', handleClick, true);
2184
+ target.addEventListener('keydown', handleKeyDown, true);
2185
+ target.addEventListener('paste', handlePaste, true);
2186
+ target.addEventListener('blur', handleBlur, true);
2187
+ restoreState.cleanup.push(() => target.removeEventListener('click', handleClick, true), () => target.removeEventListener('keydown', handleKeyDown, true), () => target.removeEventListener('paste', handlePaste, true), () => target.removeEventListener('blur', handleBlur, true));
1985
2188
  this.currentInlineTarget = target;
1986
2189
  this.currentInlineBinding = binding;
2190
+ this.ensureActiveOutline();
2191
+ this.activeOutline?.setAttribute('data-einblick-inline-active', 'true');
2192
+ this.positionOutline(this.activeOutline, target);
1987
2193
  window.setTimeout(() => {
1988
- if ('focus' in control && typeof control.focus === 'function') {
1989
- control.focus();
1990
- }
2194
+ target.focus({ preventScroll: true });
2195
+ this.placeCaretAtEnd(target);
1991
2196
  }, 0);
1992
2197
  }
1993
- closeInlineEditor() {
2198
+ closeInlineEditor(options = {}) {
2199
+ const restore = options.restore ?? true;
1994
2200
  if (this.currentInlineTarget && this.currentInlineRestoreState) {
1995
- this.currentInlineTarget.replaceChildren(...this.currentInlineRestoreState.childNodes);
1996
- this.currentInlineTarget.style.display =
1997
- this.currentInlineRestoreState.display;
1998
- this.currentInlineTarget.style.flexDirection =
1999
- this.currentInlineRestoreState.flexDirection;
2000
- this.currentInlineTarget.style.alignItems =
2001
- this.currentInlineRestoreState.alignItems;
2002
- this.currentInlineTarget.style.gap = this.currentInlineRestoreState.gap;
2003
- this.currentInlineTarget.style.minWidth =
2004
- this.currentInlineRestoreState.minWidth;
2201
+ for (const cleanup of this.currentInlineRestoreState.cleanup) {
2202
+ cleanup();
2203
+ }
2204
+ if (restore) {
2205
+ this.currentInlineTarget.replaceChildren(...this.currentInlineRestoreState.childNodes.map((node) => node.cloneNode(true)));
2206
+ this.currentInlineTarget.dataset.einblickValue =
2207
+ serializeBindingValue(this.currentInlineRestoreState.value) ?? '';
2208
+ }
2209
+ this.restoreInlineTargetAttributes(this.currentInlineTarget, this.currentInlineRestoreState);
2005
2210
  delete this.currentInlineTarget.dataset.einblickInlineEditing;
2211
+ delete this.currentInlineTarget.dataset.einblickInlineState;
2212
+ this.currentInlineTarget.removeAttribute('aria-busy');
2006
2213
  }
2007
2214
  this.currentInlineRestoreState = null;
2008
2215
  this.currentInlineTarget = null;
2009
2216
  this.currentInlineBinding = null;
2010
- }
2011
- createInlineControl(binding, target) {
2012
- switch (binding.fieldType) {
2013
- case 'text':
2014
- case 'markdown': {
2015
- const textarea = document.createElement('textarea');
2016
- textarea.setAttribute('data-einblick-inline-textarea', 'true');
2017
- return textarea;
2018
- }
2019
- case 'boolean': {
2020
- const select = document.createElement('select');
2021
- select.setAttribute('data-einblick-inline-select', 'true');
2022
- const trueOption = document.createElement('option');
2023
- trueOption.value = 'true';
2024
- trueOption.textContent = this.messages.values.true;
2025
- const falseOption = document.createElement('option');
2026
- falseOption.value = 'false';
2027
- falseOption.textContent = this.messages.values.false;
2028
- select.append(trueOption, falseOption);
2029
- return select;
2030
- }
2031
- default: {
2032
- const textValue = typeof binding.value === 'string'
2033
- ? binding.value
2034
- : target?.textContent ?? '';
2035
- if (binding.fieldType === 'string' &&
2036
- typeof textValue === 'string' &&
2037
- textValue.includes('\n')) {
2038
- const textarea = document.createElement('textarea');
2039
- textarea.setAttribute('data-einblick-inline-textarea', 'true');
2040
- return textarea;
2041
- }
2042
- const input = document.createElement('input');
2043
- input.setAttribute('data-einblick-inline-control', 'true');
2044
- if (binding.fieldType === 'number') {
2045
- input.type = 'number';
2046
- }
2047
- else if (binding.fieldType === 'date') {
2048
- input.type = 'date';
2049
- }
2050
- else {
2051
- input.type = 'text';
2052
- }
2053
- return input;
2054
- }
2217
+ this.inlineSaveInFlight = null;
2218
+ this.activeOutline?.removeAttribute('data-einblick-inline-active');
2219
+ if (!this.activeElement) {
2220
+ this.activeOutline?.remove();
2221
+ this.activeOutline = null;
2055
2222
  }
2056
2223
  }
2057
- setControlValue(control, binding, value) {
2058
- const currentValue = value ?? binding.value;
2059
- if (control instanceof HTMLSelectElement) {
2060
- control.value = currentValue === true ? 'true' : 'false';
2061
- return;
2224
+ restoreInlineTargetAttributes(target, restoreState) {
2225
+ if (restoreState.contentEditable === null) {
2226
+ target.removeAttribute('contenteditable');
2062
2227
  }
2063
- if (binding.fieldType === 'tags') {
2064
- control.value = Array.isArray(currentValue)
2065
- ? currentValue
2066
- .map((entry) => (typeof entry === 'string' ? entry : String(entry)))
2067
- .join(', ')
2068
- : currentValue === undefined || currentValue === null
2069
- ? ''
2070
- : String(currentValue);
2071
- return;
2228
+ else {
2229
+ target.setAttribute('contenteditable', restoreState.contentEditable);
2072
2230
  }
2073
- if (binding.fieldType === 'date') {
2074
- control.value =
2075
- typeof currentValue === 'string' ? normalizeDateValue(currentValue) : '';
2076
- return;
2231
+ if (restoreState.spellcheck === null) {
2232
+ target.removeAttribute('spellcheck');
2233
+ }
2234
+ else {
2235
+ target.setAttribute('spellcheck', restoreState.spellcheck);
2236
+ }
2237
+ if (restoreState.role === null) {
2238
+ target.removeAttribute('role');
2239
+ }
2240
+ else {
2241
+ target.setAttribute('role', restoreState.role);
2242
+ }
2243
+ if (restoreState.ariaMultiline === null) {
2244
+ target.removeAttribute('aria-multiline');
2245
+ }
2246
+ else {
2247
+ target.setAttribute('aria-multiline', restoreState.ariaMultiline);
2248
+ }
2249
+ if (restoreState.title === null) {
2250
+ target.removeAttribute('title');
2251
+ }
2252
+ else {
2253
+ target.setAttribute('title', restoreState.title);
2254
+ }
2255
+ if (restoreState.hadTabIndex) {
2256
+ target.tabIndex = restoreState.tabIndex;
2257
+ }
2258
+ else {
2259
+ target.removeAttribute('tabindex');
2077
2260
  }
2078
- control.value =
2079
- currentValue === undefined || currentValue === null
2080
- ? ''
2081
- : typeof currentValue === 'string'
2082
- ? currentValue
2083
- : String(currentValue);
2084
2261
  }
2085
- getControlValue(control, binding) {
2086
- const rawValue = control.value;
2262
+ getInlineEditableValue(target, binding) {
2263
+ const rawValue = fieldTypeAllowsMultiline(binding)
2264
+ ? target.innerText.replace(/\n$/, '')
2265
+ : (target.textContent ?? '').replace(/\s+/g, ' ').trim();
2087
2266
  switch (binding.fieldType) {
2088
2267
  case 'number':
2089
2268
  return rawValue.trim().length === 0 ? null : Number(rawValue);
2090
- case 'boolean':
2091
- return rawValue === 'true';
2092
2269
  case 'tags':
2093
2270
  return rawValue
2094
2271
  .split(',')
2095
2272
  .map((entry) => entry.trim())
2096
2273
  .filter(Boolean);
2274
+ case 'date':
2275
+ return normalizeDateValue(rawValue.trim());
2097
2276
  default:
2098
2277
  return rawValue;
2099
2278
  }
2100
2279
  }
2101
- async saveInlineField(binding, value) {
2280
+ async commitInlineEditor() {
2281
+ if (this.inlineSaveInFlight) {
2282
+ return await this.inlineSaveInFlight;
2283
+ }
2284
+ const target = this.currentInlineTarget;
2285
+ const binding = this.currentInlineBinding;
2286
+ if (!target || !binding) {
2287
+ return;
2288
+ }
2289
+ this.inlineSaveInFlight = (async () => {
2290
+ target.setAttribute('aria-busy', 'true');
2291
+ target.dataset.einblickInlineState = 'saving';
2292
+ try {
2293
+ const nextValue = this.getInlineEditableValue(target, binding);
2294
+ const savedValue = await this.saveInlineField(binding, nextValue);
2295
+ this.closeInlineEditor({ restore: false });
2296
+ this.applyValueToElement(target, binding, savedValue);
2297
+ await this.callbacks.onSave?.({
2298
+ binding,
2299
+ value: savedValue,
2300
+ source: 'inline',
2301
+ });
2302
+ }
2303
+ catch (error) {
2304
+ target.dataset.einblickInlineState = 'error';
2305
+ target.removeAttribute('aria-busy');
2306
+ this.inlineSaveInFlight = null;
2307
+ const message = error instanceof Error
2308
+ ? error.message
2309
+ : this.messages.errors.saveFieldFailed;
2310
+ target.title = message;
2311
+ this.callbacks.onError?.(message);
2312
+ }
2313
+ })();
2314
+ await this.inlineSaveInFlight;
2315
+ }
2316
+ insertPlainTextAtSelection(text) {
2317
+ const selection = window.getSelection();
2318
+ if (!selection || selection.rangeCount === 0) {
2319
+ return;
2320
+ }
2321
+ selection.deleteFromDocument();
2322
+ selection.getRangeAt(0).insertNode(document.createTextNode(text));
2323
+ selection.collapseToEnd();
2324
+ }
2325
+ placeCaretAtEnd(target) {
2326
+ const selection = window.getSelection();
2327
+ if (!selection) {
2328
+ return;
2329
+ }
2330
+ const range = document.createRange();
2331
+ range.selectNodeContents(target);
2332
+ range.collapse(false);
2333
+ selection.removeAllRanges();
2334
+ selection.addRange(range);
2335
+ }
2336
+ openMediaPicker(target, binding, button) {
2337
+ if (!this.state.isAuthenticated) {
2338
+ this.callbacks.onError?.(this.messages.errors.signInToEditField);
2339
+ return;
2340
+ }
2341
+ if (!binding.fieldKey) {
2342
+ this.callbacks.onError?.(this.messages.errors.fieldBindingRequired);
2343
+ return;
2344
+ }
2345
+ const input = document.createElement('input');
2346
+ input.type = 'file';
2347
+ input.accept = getMediaAcceptValue(binding);
2348
+ input.style.position = 'fixed';
2349
+ input.style.left = '-9999px';
2350
+ input.style.top = '0';
2351
+ const previousText = button?.textContent ?? '';
2352
+ const resetButton = () => {
2353
+ if (button) {
2354
+ button.disabled = false;
2355
+ button.textContent =
2356
+ previousText || getMediaActionLabel(binding, this.messages);
2357
+ }
2358
+ };
2359
+ input.addEventListener('change', () => {
2360
+ const file = input.files?.[0];
2361
+ input.remove();
2362
+ if (!file) {
2363
+ return;
2364
+ }
2365
+ if (button) {
2366
+ button.disabled = true;
2367
+ button.textContent = this.messages.actions.uploading;
2368
+ }
2369
+ void (async () => {
2370
+ try {
2371
+ const result = await this.saveInlineMediaField(binding, file);
2372
+ this.applyMediaPreviewToElement(target, file);
2373
+ await this.callbacks.onSave?.({
2374
+ binding,
2375
+ value: result.value,
2376
+ source: 'inline',
2377
+ });
2378
+ }
2379
+ catch (error) {
2380
+ const message = error instanceof Error
2381
+ ? error.message
2382
+ : this.messages.errors.saveFieldFailed;
2383
+ this.callbacks.onError?.(message);
2384
+ }
2385
+ finally {
2386
+ resetButton();
2387
+ }
2388
+ })();
2389
+ });
2390
+ input.addEventListener('cancel', () => {
2391
+ input.remove();
2392
+ });
2393
+ document.body.append(input);
2394
+ input.click();
2395
+ }
2396
+ async saveInlineMediaField(binding, file) {
2102
2397
  if (!this.bridgeFrame?.contentWindow) {
2103
2398
  throw new Error(this.messages.errors.bridgeNotConnected);
2104
2399
  }
@@ -2111,15 +2406,68 @@ class EinblickEditRuntime {
2111
2406
  const requestId = createRandomId();
2112
2407
  const promise = new Promise((resolve, reject) => {
2113
2408
  const timeoutId = window.setTimeout(() => {
2114
- this.pendingInlineSaves.delete(requestId);
2409
+ this.pendingInlineMediaSaves.delete(requestId);
2115
2410
  reject(new Error(this.messages.errors.savingTimedOut));
2116
- }, 15000);
2117
- this.pendingInlineSaves.set(requestId, {
2411
+ }, 60000);
2412
+ this.pendingInlineMediaSaves.set(requestId, {
2118
2413
  resolve,
2119
2414
  reject,
2120
2415
  timeoutId,
2121
2416
  });
2122
2417
  });
2418
+ this.bridgeFrame.contentWindow.postMessage({
2419
+ source: EINBLICK_PARENT_SOURCE,
2420
+ type: 'replace-inline-media',
2421
+ nonce: this.bridgeNonce,
2422
+ requestId,
2423
+ payload: {
2424
+ resourceSlug: binding.resourceSlug,
2425
+ recordId: binding.recordId,
2426
+ fieldKey: binding.fieldKey,
2427
+ file,
2428
+ },
2429
+ }, this.appOrigin);
2430
+ return await promise;
2431
+ }
2432
+ applyMediaPreviewToElement(element, file) {
2433
+ if (!file.type.startsWith('image/')) {
2434
+ return;
2435
+ }
2436
+ const image = element instanceof HTMLImageElement
2437
+ ? element
2438
+ : element.querySelector('img');
2439
+ if (!image) {
2440
+ return;
2441
+ }
2442
+ const objectUrl = URL.createObjectURL(file);
2443
+ const previousUrl = image.src;
2444
+ image.addEventListener('load', () => {
2445
+ URL.revokeObjectURL(objectUrl);
2446
+ }, { once: true });
2447
+ image.addEventListener('error', () => {
2448
+ image.src = previousUrl;
2449
+ URL.revokeObjectURL(objectUrl);
2450
+ }, { once: true });
2451
+ image.src = objectUrl;
2452
+ }
2453
+ async saveInlineField(binding, value) {
2454
+ if (!this.bridgeFrame?.contentWindow) {
2455
+ throw new Error(this.messages.errors.bridgeNotConnected);
2456
+ }
2457
+ if (!this.state.isAuthenticated) {
2458
+ throw new Error(this.messages.errors.signInToEditField);
2459
+ }
2460
+ if (!binding.fieldKey) {
2461
+ throw new Error(this.messages.errors.fieldBindingRequired);
2462
+ }
2463
+ const requestId = createRandomId();
2464
+ const promise = new Promise((resolve, reject) => {
2465
+ const timeoutId = window.setTimeout(() => {
2466
+ this.pendingInlineSaves.delete(requestId);
2467
+ reject(new Error(this.messages.errors.savingTimedOut));
2468
+ }, 15000);
2469
+ this.pendingInlineSaves.set(requestId, { resolve, reject, timeoutId });
2470
+ });
2123
2471
  this.bridgeFrame.contentWindow.postMessage({
2124
2472
  source: EINBLICK_PARENT_SOURCE,
2125
2473
  type: 'save-inline-field',