@einblick/sdk 0.5.3 → 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.
@@ -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;
369
- }
370
-
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);
318
+ cursor: text;
396
319
  }
397
320
 
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;
@@ -677,6 +618,7 @@ class EinblickEditRuntime {
677
618
  }
678
619
  this.updateBrandPopover();
679
620
  this.updateBottomBar();
621
+ this.updateHoverChrome();
680
622
  }
681
623
  async login(options) {
682
624
  if (typeof window === 'undefined') {
@@ -738,13 +680,24 @@ class EinblickEditRuntime {
738
680
  this.setActiveElement(null);
739
681
  return;
740
682
  }
683
+ if (this.isSdkChromeTarget(event.target)) {
684
+ this.updateHoverChrome();
685
+ return;
686
+ }
741
687
  const target = this.findEditableTarget(event.target);
742
688
  this.setActiveElement(target);
743
689
  };
744
690
  handleViewportChange = () => {
691
+ if (this.currentInlineTarget?.isConnected) {
692
+ this.ensureActiveOutline();
693
+ this.positionOutline(this.activeOutline, this.currentInlineTarget);
694
+ return;
695
+ }
745
696
  if (this.activeElement && !this.activeElement.isConnected) {
746
697
  this.setActiveElement(null);
698
+ return;
747
699
  }
700
+ this.updateHoverChrome();
748
701
  };
749
702
  handleWindowBlur = () => {
750
703
  this.setBrandPopoverOpen(false);
@@ -785,8 +738,7 @@ class EinblickEditRuntime {
785
738
  this.setActiveElement(null);
786
739
  };
787
740
  handleHoverButtonClick = (event) => {
788
- event.preventDefault();
789
- event.stopPropagation();
741
+ stopEvent(event);
790
742
  if (!this.activeElement) {
791
743
  return;
792
744
  }
@@ -794,11 +746,14 @@ class EinblickEditRuntime {
794
746
  if (!binding) {
795
747
  return;
796
748
  }
749
+ if (binding.fieldKey && isMediaEditableBinding(binding)) {
750
+ this.openMediaPicker(this.activeElement, binding, this.hoverButton);
751
+ return;
752
+ }
797
753
  this.openEditor(binding, this.activeElement);
798
754
  };
799
755
  handleCreateButtonClick = (event) => {
800
- event.preventDefault();
801
- event.stopPropagation();
756
+ stopEvent(event);
802
757
  if (!this.activeElement) {
803
758
  return;
804
759
  }
@@ -815,6 +770,16 @@ class EinblickEditRuntime {
815
770
  this.setBottomBarMenuOpen(false);
816
771
  return;
817
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
+ }
818
783
  if (this.isBrandPopoverOpen() &&
819
784
  !this.brandBadge?.contains(target) &&
820
785
  !this.brandPopover?.contains(target)) {
@@ -889,6 +854,35 @@ class EinblickEditRuntime {
889
854
  }
890
855
  return;
891
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
+ }
892
886
  const pending = this.pendingInlineSaves.get(message.requestId);
893
887
  if (!pending) {
894
888
  return;
@@ -1060,16 +1054,23 @@ class EinblickEditRuntime {
1060
1054
  container.hidden = true;
1061
1055
  const editButton = document.createElement('button');
1062
1056
  editButton.type = 'button';
1063
- editButton.textContent = this.messages.actions.edit;
1057
+ editButton.textContent = this.messages.actions.editElement;
1064
1058
  editButton.setAttribute('data-einblick-edit-button', 'true');
1059
+ editButton.addEventListener('pointerdown', (event) => {
1060
+ event.stopPropagation();
1061
+ });
1065
1062
  editButton.addEventListener('click', this.handleHoverButtonClick);
1066
1063
  const createButton = document.createElement('button');
1067
1064
  createButton.type = 'button';
1068
1065
  createButton.textContent = this.messages.actions.addRecord;
1069
1066
  createButton.hidden = true;
1070
1067
  createButton.setAttribute('data-einblick-create-button', 'true');
1068
+ createButton.addEventListener('pointerdown', (event) => {
1069
+ event.stopPropagation();
1070
+ });
1071
1071
  createButton.addEventListener('click', this.handleCreateButtonClick);
1072
1072
  container.append(editButton, createButton);
1073
+ document.body.append(container);
1073
1074
  this.hoverButtonContainer = container;
1074
1075
  this.hoverButton = editButton;
1075
1076
  this.hoverCreateButton = createButton;
@@ -1621,6 +1622,48 @@ class EinblickEditRuntime {
1621
1622
  siteName: this.state.siteName,
1622
1623
  errorMessage: undefined,
1623
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) {
1624
1667
  const logoutUrl = this.buildBridgeUrl({
1625
1668
  mode: 'logout',
1626
1669
  nonce: createRandomId(),
@@ -1647,28 +1690,11 @@ class EinblickEditRuntime {
1647
1690
  }
1648
1691
  }, 400);
1649
1692
  });
1650
- this.reloadBridge();
1651
1693
  }
1652
1694
  setActiveElement(element) {
1653
1695
  if (this.activeElement === element) {
1654
- if (this.hoverButtonContainer &&
1655
- element &&
1656
- !element.contains(this.hoverButtonContainer)) {
1657
- element.append(this.hoverButtonContainer);
1658
- }
1659
- this.updateHoverButtonVisibility(element);
1660
- return;
1661
- }
1662
- if (this.activeElement) {
1663
- this.activeElement.removeAttribute('data-einblick-editable-active');
1664
- const originalPosition = this.activeElement.dataset.einblickRuntimeOriginalPosition;
1665
- if (originalPosition !== undefined) {
1666
- this.activeElement.style.position =
1667
- originalPosition === RELATIVE_POSITION_SENTINEL
1668
- ? ''
1669
- : originalPosition;
1670
- delete this.activeElement.dataset.einblickRuntimeOriginalPosition;
1671
- }
1696
+ this.updateHoverChrome();
1697
+ return;
1672
1698
  }
1673
1699
  this.activeElement = element;
1674
1700
  if (!element ||
@@ -1677,22 +1703,176 @@ class EinblickEditRuntime {
1677
1703
  !this.state.isAuthenticated) {
1678
1704
  if (this.hoverButtonContainer) {
1679
1705
  this.hoverButtonContainer.hidden = true;
1680
- this.hoverButtonContainer.remove();
1681
1706
  }
1707
+ this.activeOutline?.remove();
1708
+ this.activeOutline = null;
1709
+ this.clearQuickFieldOverlays();
1682
1710
  if (this.hoverCreateButton) {
1683
1711
  this.hoverCreateButton.hidden = true;
1684
1712
  }
1685
1713
  return;
1686
1714
  }
1687
- element.setAttribute('data-einblick-editable-active', 'true');
1688
- if (window.getComputedStyle(element).position === 'static') {
1689
- element.dataset.einblickRuntimeOriginalPosition =
1690
- element.style.position || RELATIVE_POSITION_SENTINEL;
1691
- element.style.position = 'relative';
1692
- }
1715
+ this.ensureActiveOutline();
1716
+ this.hoverButtonContainer.hidden = false;
1717
+ this.updateHoverButtonLabel();
1693
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
+ }
1694
1782
  this.hoverButtonContainer.hidden = false;
1695
- 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;
1696
1876
  }
1697
1877
  reloadBridge() {
1698
1878
  this.clearBridgePing();
@@ -1845,7 +2025,67 @@ class EinblickEditRuntime {
1845
2025
  return null;
1846
2026
  }
1847
2027
  const editableTarget = target.closest('[data-einblick-editable="true"]');
1848
- 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));
1849
2089
  }
1850
2090
  findElementForBinding(binding) {
1851
2091
  const selector = [
@@ -1874,216 +2114,342 @@ class EinblickEditRuntime {
1874
2114
  this.closeDrawer();
1875
2115
  this.closeInlineEditor();
1876
2116
  this.setActiveElement(null);
1877
- const computedDisplay = window.getComputedStyle(target).display;
1878
- const isInlineLayout = computedDisplay === 'inline' ||
1879
- computedDisplay === 'inline-block' ||
1880
- computedDisplay === 'inline-flex' ||
1881
- computedDisplay === 'contents';
1882
- const targetRect = target.getBoundingClientRect();
1883
- this.currentInlineRestoreState = {
1884
- childNodes: Array.from(target.childNodes),
1885
- display: target.style.display,
1886
- flexDirection: target.style.flexDirection,
1887
- alignItems: target.style.alignItems,
1888
- gap: target.style.gap,
1889
- 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: [],
1890
2128
  };
2129
+ this.currentInlineRestoreState = restoreState;
1891
2130
  target.dataset.einblickInlineEditing = 'true';
1892
- target.style.display = isInlineLayout ? 'inline-flex' : 'flex';
1893
- target.style.flexDirection = 'column';
1894
- target.style.alignItems = 'stretch';
1895
- target.style.gap = '0.75rem';
1896
- target.style.minWidth = isInlineLayout
1897
- ? `${Math.min(Math.max(Math.round(targetRect.width), 220), Math.max(window.innerWidth - 32, 220))}px`
1898
- : target.style.minWidth;
1899
- const shellHost = document.createElement('div');
1900
- const shadowRoot = shellHost.attachShadow({ mode: 'open' });
1901
- const shadowStyle = document.createElement('style');
1902
- shadowStyle.textContent = INLINE_EDITOR_SHADOW_STYLES;
1903
- const shell = document.createElement('div');
1904
- shell.setAttribute('data-einblick-inline-shell', 'true');
1905
- const control = this.createInlineControl(binding, target);
1906
- this.setControlValue(control, binding, binding.value);
1907
- const errorMessage = document.createElement('div');
1908
- errorMessage.setAttribute('data-einblick-inline-error', 'true');
1909
- errorMessage.hidden = true;
1910
- const actions = document.createElement('div');
1911
- actions.setAttribute('data-einblick-inline-actions', 'true');
1912
- const cancelButton = document.createElement('button');
1913
- cancelButton.type = 'button';
1914
- cancelButton.textContent = this.messages.actions.cancel;
1915
- cancelButton.setAttribute('data-einblick-edit-action', 'secondary');
1916
- cancelButton.addEventListener('click', () => this.closeInlineEditor());
1917
- const saveButton = document.createElement('button');
1918
- saveButton.type = 'button';
1919
- saveButton.textContent = this.messages.actions.save;
1920
- saveButton.setAttribute('data-einblick-edit-action', 'primary');
1921
- const runSave = async () => {
1922
- errorMessage.hidden = true;
1923
- errorMessage.textContent = '';
1924
- saveButton.disabled = true;
1925
- saveButton.textContent = this.messages.actions.saving;
1926
- cancelButton.disabled = true;
1927
- try {
1928
- const nextValue = this.getControlValue(control, binding);
1929
- const savedValue = await this.saveInlineField(binding, nextValue);
1930
- this.closeInlineEditor();
1931
- this.applyValueToElement(target, binding, savedValue);
1932
- await this.callbacks.onSave?.({
1933
- binding,
1934
- value: savedValue,
1935
- source: 'inline',
1936
- });
1937
- }
1938
- catch (error) {
1939
- errorMessage.hidden = false;
1940
- errorMessage.textContent =
1941
- error instanceof Error
1942
- ? error.message
1943
- : this.messages.errors.saveFieldFailed;
1944
- saveButton.disabled = false;
1945
- saveButton.textContent = this.messages.actions.save;
1946
- cancelButton.disabled = false;
1947
- }
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();
1948
2146
  };
1949
- saveButton.addEventListener('click', () => {
1950
- void runSave();
1951
- });
1952
- control.addEventListener('keydown', (event) => {
1953
- const keyboardEvent = event;
2147
+ const handleKeyDown = (keyboardEvent) => {
1954
2148
  if (keyboardEvent.key === 'Escape') {
1955
2149
  keyboardEvent.preventDefault();
2150
+ keyboardEvent.stopPropagation();
1956
2151
  this.closeInlineEditor();
1957
2152
  return;
1958
2153
  }
1959
2154
  if (keyboardEvent.key === 'Enter' &&
1960
- (!(control instanceof HTMLTextAreaElement) ||
2155
+ (!fieldTypeAllowsMultiline(binding) ||
1961
2156
  keyboardEvent.metaKey ||
1962
2157
  keyboardEvent.ctrlKey)) {
1963
2158
  keyboardEvent.preventDefault();
1964
- void runSave();
2159
+ keyboardEvent.stopPropagation();
2160
+ void this.commitInlineEditor();
2161
+ return;
1965
2162
  }
1966
- });
1967
- actions.append(cancelButton, saveButton);
1968
- shell.append(control, errorMessage, actions);
1969
- shadowRoot.append(shadowStyle, shell);
1970
- 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));
1971
2188
  this.currentInlineTarget = target;
1972
2189
  this.currentInlineBinding = binding;
2190
+ this.ensureActiveOutline();
2191
+ this.activeOutline?.setAttribute('data-einblick-inline-active', 'true');
2192
+ this.positionOutline(this.activeOutline, target);
1973
2193
  window.setTimeout(() => {
1974
- if ('focus' in control && typeof control.focus === 'function') {
1975
- control.focus();
1976
- }
2194
+ target.focus({ preventScroll: true });
2195
+ this.placeCaretAtEnd(target);
1977
2196
  }, 0);
1978
2197
  }
1979
- closeInlineEditor() {
2198
+ closeInlineEditor(options = {}) {
2199
+ const restore = options.restore ?? true;
1980
2200
  if (this.currentInlineTarget && this.currentInlineRestoreState) {
1981
- this.currentInlineTarget.replaceChildren(...this.currentInlineRestoreState.childNodes);
1982
- this.currentInlineTarget.style.display =
1983
- this.currentInlineRestoreState.display;
1984
- this.currentInlineTarget.style.flexDirection =
1985
- this.currentInlineRestoreState.flexDirection;
1986
- this.currentInlineTarget.style.alignItems =
1987
- this.currentInlineRestoreState.alignItems;
1988
- this.currentInlineTarget.style.gap = this.currentInlineRestoreState.gap;
1989
- this.currentInlineTarget.style.minWidth =
1990
- 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);
1991
2210
  delete this.currentInlineTarget.dataset.einblickInlineEditing;
2211
+ delete this.currentInlineTarget.dataset.einblickInlineState;
2212
+ this.currentInlineTarget.removeAttribute('aria-busy');
1992
2213
  }
1993
2214
  this.currentInlineRestoreState = null;
1994
2215
  this.currentInlineTarget = null;
1995
2216
  this.currentInlineBinding = null;
1996
- }
1997
- createInlineControl(binding, target) {
1998
- switch (binding.fieldType) {
1999
- case 'text':
2000
- case 'markdown': {
2001
- const textarea = document.createElement('textarea');
2002
- textarea.setAttribute('data-einblick-inline-textarea', 'true');
2003
- return textarea;
2004
- }
2005
- case 'boolean': {
2006
- const select = document.createElement('select');
2007
- select.setAttribute('data-einblick-inline-select', 'true');
2008
- const trueOption = document.createElement('option');
2009
- trueOption.value = 'true';
2010
- trueOption.textContent = this.messages.values.true;
2011
- const falseOption = document.createElement('option');
2012
- falseOption.value = 'false';
2013
- falseOption.textContent = this.messages.values.false;
2014
- select.append(trueOption, falseOption);
2015
- return select;
2016
- }
2017
- default: {
2018
- const textValue = typeof binding.value === 'string'
2019
- ? binding.value
2020
- : (target?.textContent ?? '');
2021
- if (binding.fieldType === 'string' &&
2022
- typeof textValue === 'string' &&
2023
- textValue.includes('\n')) {
2024
- const textarea = document.createElement('textarea');
2025
- textarea.setAttribute('data-einblick-inline-textarea', 'true');
2026
- return textarea;
2027
- }
2028
- const input = document.createElement('input');
2029
- input.setAttribute('data-einblick-inline-control', 'true');
2030
- if (binding.fieldType === 'number') {
2031
- input.type = 'number';
2032
- }
2033
- else if (binding.fieldType === 'date') {
2034
- input.type = 'date';
2035
- }
2036
- else {
2037
- input.type = 'text';
2038
- }
2039
- return input;
2040
- }
2217
+ this.inlineSaveInFlight = null;
2218
+ this.activeOutline?.removeAttribute('data-einblick-inline-active');
2219
+ if (!this.activeElement) {
2220
+ this.activeOutline?.remove();
2221
+ this.activeOutline = null;
2041
2222
  }
2042
2223
  }
2043
- setControlValue(control, binding, value) {
2044
- const currentValue = value ?? binding.value;
2045
- if (control instanceof HTMLSelectElement) {
2046
- control.value = currentValue === true ? 'true' : 'false';
2047
- return;
2224
+ restoreInlineTargetAttributes(target, restoreState) {
2225
+ if (restoreState.contentEditable === null) {
2226
+ target.removeAttribute('contenteditable');
2048
2227
  }
2049
- if (binding.fieldType === 'tags') {
2050
- control.value = Array.isArray(currentValue)
2051
- ? currentValue
2052
- .map((entry) => (typeof entry === 'string' ? entry : String(entry)))
2053
- .join(', ')
2054
- : currentValue === undefined || currentValue === null
2055
- ? ''
2056
- : String(currentValue);
2057
- return;
2228
+ else {
2229
+ target.setAttribute('contenteditable', restoreState.contentEditable);
2058
2230
  }
2059
- if (binding.fieldType === 'date') {
2060
- control.value =
2061
- typeof currentValue === 'string' ? normalizeDateValue(currentValue) : '';
2062
- 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');
2063
2260
  }
2064
- control.value =
2065
- currentValue === undefined || currentValue === null
2066
- ? ''
2067
- : typeof currentValue === 'string'
2068
- ? currentValue
2069
- : String(currentValue);
2070
2261
  }
2071
- getControlValue(control, binding) {
2072
- 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();
2073
2266
  switch (binding.fieldType) {
2074
2267
  case 'number':
2075
2268
  return rawValue.trim().length === 0 ? null : Number(rawValue);
2076
- case 'boolean':
2077
- return rawValue === 'true';
2078
2269
  case 'tags':
2079
2270
  return rawValue
2080
2271
  .split(',')
2081
2272
  .map((entry) => entry.trim())
2082
2273
  .filter(Boolean);
2274
+ case 'date':
2275
+ return normalizeDateValue(rawValue.trim());
2083
2276
  default:
2084
2277
  return rawValue;
2085
2278
  }
2086
2279
  }
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) {
2397
+ if (!this.bridgeFrame?.contentWindow) {
2398
+ throw new Error(this.messages.errors.bridgeNotConnected);
2399
+ }
2400
+ if (!this.state.isAuthenticated) {
2401
+ throw new Error(this.messages.errors.signInToEditField);
2402
+ }
2403
+ if (!binding.fieldKey) {
2404
+ throw new Error(this.messages.errors.fieldBindingRequired);
2405
+ }
2406
+ const requestId = createRandomId();
2407
+ const promise = new Promise((resolve, reject) => {
2408
+ const timeoutId = window.setTimeout(() => {
2409
+ this.pendingInlineMediaSaves.delete(requestId);
2410
+ reject(new Error(this.messages.errors.savingTimedOut));
2411
+ }, 60000);
2412
+ this.pendingInlineMediaSaves.set(requestId, {
2413
+ resolve,
2414
+ reject,
2415
+ timeoutId,
2416
+ });
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
+ }
2087
2453
  async saveInlineField(binding, value) {
2088
2454
  if (!this.bridgeFrame?.contentWindow) {
2089
2455
  throw new Error(this.messages.errors.bridgeNotConnected);