@einblick/sdk 0.5.3 → 0.6.0

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,33 @@
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
+ function normalizeResourceHints(hints) {
4
+ const normalized = [];
5
+ const seen = new Set();
6
+ for (const hint of hints ?? []) {
7
+ const resourceSlug = hint.resourceSlug?.trim();
8
+ if (!resourceSlug)
9
+ continue;
10
+ const sourceType = hint.sourceType ?? 'cms';
11
+ const key = `${sourceType}:${resourceSlug}`;
12
+ if (seen.has(key))
13
+ continue;
14
+ seen.add(key);
15
+ normalized.push({
16
+ sourceType,
17
+ resourceSlug,
18
+ label: hint.label?.trim() || undefined,
19
+ mode: hint.mode,
20
+ recordIds: hint.recordIds?.filter((id) => id.trim().length > 0),
21
+ showInBottomBar: hint.showInBottomBar,
22
+ });
23
+ }
24
+ return normalized;
25
+ }
3
26
  const RUNTIME_STYLE_ID = 'einblick-edit-runtime-styles';
4
- const RELATIVE_POSITION_SENTINEL = '__einblick_runtime_original_static__';
5
27
  const EINBLICK_ACCENT_RGB = '250, 105, 62';
6
28
  const EINBLICK_ACCENT_COLOR = `rgb(${EINBLICK_ACCENT_RGB})`;
7
29
  const EINBLICK_ACCENT_BORDER = `rgba(${EINBLICK_ACCENT_RGB}, 0.92)`;
8
30
  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
31
  function getBrandBadgeSvg(size) {
97
32
  return `
98
33
  <svg viewBox="0 0 40 40" width="${size}" height="${size}" aria-hidden="true" focusable="false">
@@ -135,6 +70,32 @@ function createRandomId() {
135
70
  }
136
71
  return Math.random().toString(36).slice(2);
137
72
  }
73
+ function stopEvent(event) {
74
+ event.preventDefault();
75
+ event.stopPropagation();
76
+ }
77
+ function clamp(value, min, max) {
78
+ return Math.min(Math.max(value, min), max);
79
+ }
80
+ function isTextInputEvent(event) {
81
+ return (event.key.length === 1 ||
82
+ event.key === 'Enter' ||
83
+ event.key === 'Backspace' ||
84
+ event.key === 'Delete');
85
+ }
86
+ function fieldTypeAllowsMultiline(binding) {
87
+ return binding.fieldType === 'text' || binding.fieldType === 'markdown';
88
+ }
89
+ function getMediaAcceptValue(binding) {
90
+ return binding.fieldType === 'image' || binding.fieldType === 'images'
91
+ ? 'image/*'
92
+ : '';
93
+ }
94
+ function getMediaActionLabel(binding, messages) {
95
+ return binding.fieldType === 'image' || binding.fieldType === 'images'
96
+ ? messages.actions.replaceImage
97
+ : messages.actions.replaceFile;
98
+ }
138
99
  function injectRuntimeStyles() {
139
100
  if (typeof document === 'undefined') {
140
101
  return;
@@ -149,15 +110,27 @@ function injectRuntimeStyles() {
149
110
  scroll-margin-top: 96px;
150
111
  }
151
112
 
152
- [data-einblick-editable-active="true"] {
153
- outline: 2px solid ${EINBLICK_ACCENT_BORDER};
154
- outline-offset: 4px;
113
+ [data-einblick-edit-outline],
114
+ [data-einblick-edit-field-outline] {
115
+ position: fixed;
116
+ z-index: 2147483643;
117
+ box-sizing: border-box;
118
+ pointer-events: none;
119
+ border-radius: var(--einblick-overlay-radius, 0.35rem);
120
+ }
121
+
122
+ [data-einblick-edit-outline] {
123
+ border: 1px solid ${EINBLICK_ACCENT_BORDER};
124
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.7);
125
+ }
126
+
127
+ [data-einblick-edit-field-outline] {
128
+ border: 1px dashed rgba(${EINBLICK_ACCENT_RGB}, 0.78);
129
+ background: rgba(${EINBLICK_ACCENT_RGB}, 0.04);
155
130
  }
156
131
 
157
132
  [data-einblick-edit-controls] {
158
- position: absolute;
159
- top: 0.5rem;
160
- right: 0.5rem;
133
+ position: fixed;
161
134
  z-index: 2147483644;
162
135
  display: inline-flex;
163
136
  align-items: center;
@@ -165,7 +138,8 @@ function injectRuntimeStyles() {
165
138
  }
166
139
 
167
140
  [data-einblick-edit-button],
168
- [data-einblick-create-button] {
141
+ [data-einblick-create-button],
142
+ [data-einblick-field-button] {
169
143
  border: 1px solid ${EINBLICK_ACCENT_COLOR};
170
144
  border-radius: 999px;
171
145
  padding: 0.3rem 0.7rem;
@@ -175,22 +149,46 @@ function injectRuntimeStyles() {
175
149
  box-shadow: 0 8px 20px rgba(15, 23, 42, 0.16);
176
150
  }
177
151
 
178
- [data-einblick-edit-button] {
152
+ [data-einblick-edit-button],
153
+ [data-einblick-field-button="media"] {
179
154
  background: ${EINBLICK_ACCENT_COLOR};
180
155
  color: rgb(255, 255, 255);
181
156
  }
182
157
 
183
- [data-einblick-create-button] {
158
+ [data-einblick-edit-button] {
159
+ padding: 0.42rem 0.85rem;
160
+ font-size: 13px;
161
+ }
162
+
163
+ [data-einblick-create-button],
164
+ [data-einblick-field-button="text"] {
184
165
  background: rgb(255, 255, 255);
185
166
  color: ${EINBLICK_ACCENT_COLOR};
186
167
  }
187
168
 
188
169
  [data-einblick-edit-button]:hover,
189
- [data-einblick-create-button]:hover {
170
+ [data-einblick-create-button]:hover,
171
+ [data-einblick-field-button]:hover {
190
172
  background: rgb(255, 255, 255);
191
173
  color: ${EINBLICK_ACCENT_COLOR};
192
174
  }
193
175
 
176
+ [data-einblick-field-overlays] {
177
+ position: fixed;
178
+ inset: 0;
179
+ z-index: 2147483644;
180
+ pointer-events: none;
181
+ }
182
+
183
+ [data-einblick-field-button] {
184
+ position: fixed;
185
+ z-index: 2147483644;
186
+ pointer-events: auto;
187
+ padding: 0.24rem 0.58rem;
188
+ font-size: 11px;
189
+ box-shadow: 0 8px 18px rgba(15, 23, 42, 0.12);
190
+ }
191
+
194
192
  [data-einblick-brand-badge] {
195
193
  position: fixed;
196
194
  right: 1rem;
@@ -340,64 +338,11 @@ function injectRuntimeStyles() {
340
338
  }
341
339
 
342
340
  [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;
341
+ cursor: text;
381
342
  }
382
343
 
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);
344
+ [data-einblick-inline-editing="true"]:focus {
345
+ outline: none;
401
346
  }
402
347
 
403
348
  [data-einblick-editor-backdrop] {
@@ -472,6 +417,7 @@ class EinblickEditRuntime {
472
417
  callbacks;
473
418
  chrome;
474
419
  reserveBottomSpace;
420
+ resourceHints;
475
421
  locale;
476
422
  messages;
477
423
  bridgeNonce = createRandomId();
@@ -485,9 +431,12 @@ class EinblickEditRuntime {
485
431
  bridgeFrame = null;
486
432
  bridgePingInterval = null;
487
433
  activeElement = null;
434
+ activeOutline = null;
488
435
  hoverButtonContainer = null;
489
436
  hoverButton = null;
490
437
  hoverCreateButton = null;
438
+ quickFieldOverlayContainer = null;
439
+ quickFieldOverlays = [];
491
440
  brandBadge = null;
492
441
  brandPopover = null;
493
442
  brandPopoverSiteName = null;
@@ -497,7 +446,7 @@ class EinblickEditRuntime {
497
446
  brandPopoverLogoutButton = null;
498
447
  bottomBarHost = null;
499
448
  bottomBarSiteName = null;
500
- bottomBarCmsButton = null;
449
+ bottomBarResourceList = null;
501
450
  bottomBarMoreButton = null;
502
451
  bottomBarMoreMenu = null;
503
452
  bottomBarOpenAppButton = null;
@@ -510,6 +459,7 @@ class EinblickEditRuntime {
510
459
  currentInlineTarget = null;
511
460
  currentInlineBinding = null;
512
461
  currentInlineRestoreState = null;
462
+ inlineSaveInFlight = null;
513
463
  editorBackdrop = null;
514
464
  editorNonce = null;
515
465
  editorTarget = null;
@@ -518,14 +468,18 @@ class EinblickEditRuntime {
518
468
  navigatorFrame = null;
519
469
  navigatorNonce = null;
520
470
  navigatorOpen = false;
471
+ navigatorFilter = null;
521
472
  pendingInlineSaves = new Map();
473
+ pendingInlineMediaSaves = new Map();
522
474
  pendingLogin = null;
475
+ pendingLogout = null;
523
476
  constructor(options) {
524
477
  this.siteKey = options.siteKey;
525
478
  this.appOrigin = resolveAppOrigin(options.appOrigin, options.appUrl);
526
479
  this.chrome = options.chrome ?? 'bar';
527
480
  this.reserveBottomSpace =
528
481
  options.reserveBottomSpace ?? this.chrome === 'bar';
482
+ this.resourceHints = normalizeResourceHints(options.resourceHints);
529
483
  this.locale = resolveEinblickSdkLocale(options.locale);
530
484
  this.messages = getEinblickSdkMessages(this.locale);
531
485
  this.session =
@@ -548,6 +502,10 @@ class EinblickEditRuntime {
548
502
  this.messages = getEinblickSdkMessages(nextLocale);
549
503
  this.refreshLocalizedUi();
550
504
  }
505
+ setResourceHints(hints) {
506
+ this.resourceHints = normalizeResourceHints(hints);
507
+ this.updateBottomBarResources();
508
+ }
551
509
  start() {
552
510
  if (this.started ||
553
511
  typeof window === 'undefined' ||
@@ -580,6 +538,7 @@ class EinblickEditRuntime {
580
538
  if (this.currentInlineTarget && !this.currentInlineTarget.isConnected) {
581
539
  this.closeInlineEditor();
582
540
  }
541
+ this.updateBottomBarResources();
583
542
  });
584
543
  this.mutationObserver.observe(document.body, {
585
544
  childList: true,
@@ -603,15 +562,23 @@ class EinblickEditRuntime {
603
562
  this.mutationObserver?.disconnect();
604
563
  this.mutationObserver = null;
605
564
  this.clearBridgePing();
565
+ if (this.pendingLogout) {
566
+ window.clearTimeout(this.pendingLogout.timeoutId);
567
+ this.pendingLogout.resolve(false);
568
+ this.pendingLogout = null;
569
+ }
606
570
  this.bridgeFrame?.remove();
607
571
  this.bridgeFrame = null;
608
572
  this.setActiveElement(null);
573
+ this.activeOutline?.remove();
574
+ this.activeOutline = null;
609
575
  this.hoverButtonContainer?.remove();
610
576
  this.hoverButtonContainer = null;
611
577
  this.hoverButton?.remove();
612
578
  this.hoverButton = null;
613
579
  this.hoverCreateButton?.remove();
614
580
  this.hoverCreateButton = null;
581
+ this.clearQuickFieldOverlays();
615
582
  this.brandBadge?.remove();
616
583
  this.brandBadge = null;
617
584
  this.brandPopover?.remove();
@@ -624,7 +591,7 @@ class EinblickEditRuntime {
624
591
  this.bottomBarHost?.remove();
625
592
  this.bottomBarHost = null;
626
593
  this.bottomBarSiteName = null;
627
- this.bottomBarCmsButton = null;
594
+ this.bottomBarResourceList = null;
628
595
  this.bottomBarMoreButton = null;
629
596
  this.bottomBarMoreMenu = null;
630
597
  this.bottomBarOpenAppButton = null;
@@ -645,6 +612,11 @@ class EinblickEditRuntime {
645
612
  pending.reject(new Error(this.messages.errors.editingInterrupted));
646
613
  }
647
614
  this.pendingInlineSaves.clear();
615
+ for (const pending of this.pendingInlineMediaSaves.values()) {
616
+ window.clearTimeout(pending.timeoutId);
617
+ pending.reject(new Error(this.messages.errors.editingInterrupted));
618
+ }
619
+ this.pendingInlineMediaSaves.clear();
648
620
  this.setState({
649
621
  enabled: true,
650
622
  active: false,
@@ -656,7 +628,7 @@ class EinblickEditRuntime {
656
628
  }
657
629
  refreshLocalizedUi() {
658
630
  if (this.hoverButton) {
659
- this.hoverButton.textContent = this.messages.actions.edit;
631
+ this.updateHoverButtonLabel();
660
632
  }
661
633
  if (this.hoverCreateButton) {
662
634
  this.hoverCreateButton.textContent = this.messages.actions.addRecord;
@@ -677,6 +649,7 @@ class EinblickEditRuntime {
677
649
  }
678
650
  this.updateBrandPopover();
679
651
  this.updateBottomBar();
652
+ this.updateHoverChrome();
680
653
  }
681
654
  async login(options) {
682
655
  if (typeof window === 'undefined') {
@@ -738,13 +711,24 @@ class EinblickEditRuntime {
738
711
  this.setActiveElement(null);
739
712
  return;
740
713
  }
714
+ if (this.isSdkChromeTarget(event.target)) {
715
+ this.updateHoverChrome();
716
+ return;
717
+ }
741
718
  const target = this.findEditableTarget(event.target);
742
719
  this.setActiveElement(target);
743
720
  };
744
721
  handleViewportChange = () => {
722
+ if (this.currentInlineTarget?.isConnected) {
723
+ this.ensureActiveOutline();
724
+ this.positionOutline(this.activeOutline, this.currentInlineTarget);
725
+ return;
726
+ }
745
727
  if (this.activeElement && !this.activeElement.isConnected) {
746
728
  this.setActiveElement(null);
729
+ return;
747
730
  }
731
+ this.updateHoverChrome();
748
732
  };
749
733
  handleWindowBlur = () => {
750
734
  this.setBrandPopoverOpen(false);
@@ -785,8 +769,7 @@ class EinblickEditRuntime {
785
769
  this.setActiveElement(null);
786
770
  };
787
771
  handleHoverButtonClick = (event) => {
788
- event.preventDefault();
789
- event.stopPropagation();
772
+ stopEvent(event);
790
773
  if (!this.activeElement) {
791
774
  return;
792
775
  }
@@ -794,11 +777,14 @@ class EinblickEditRuntime {
794
777
  if (!binding) {
795
778
  return;
796
779
  }
780
+ if (binding.fieldKey && isMediaEditableBinding(binding)) {
781
+ this.openMediaPicker(this.activeElement, binding, this.hoverButton);
782
+ return;
783
+ }
797
784
  this.openEditor(binding, this.activeElement);
798
785
  };
799
786
  handleCreateButtonClick = (event) => {
800
- event.preventDefault();
801
- event.stopPropagation();
787
+ stopEvent(event);
802
788
  if (!this.activeElement) {
803
789
  return;
804
790
  }
@@ -815,6 +801,16 @@ class EinblickEditRuntime {
815
801
  this.setBottomBarMenuOpen(false);
816
802
  return;
817
803
  }
804
+ if (this.currentInlineTarget) {
805
+ if (this.currentInlineTarget.contains(target) ||
806
+ this.isSdkChromeTarget(target)) {
807
+ return;
808
+ }
809
+ event.preventDefault();
810
+ event.stopImmediatePropagation();
811
+ void this.commitInlineEditor();
812
+ return;
813
+ }
818
814
  if (this.isBrandPopoverOpen() &&
819
815
  !this.brandBadge?.contains(target) &&
820
816
  !this.brandPopover?.contains(target)) {
@@ -889,6 +885,35 @@ class EinblickEditRuntime {
889
885
  }
890
886
  return;
891
887
  }
888
+ if (message.type === 'logout-result') {
889
+ const pending = this.pendingLogout;
890
+ if (!pending || pending.requestId !== message.requestId) {
891
+ return;
892
+ }
893
+ window.clearTimeout(pending.timeoutId);
894
+ this.pendingLogout = null;
895
+ if (!message.ok && message.message) {
896
+ this.callbacks.onError?.(message.message);
897
+ }
898
+ pending.resolve(message.ok);
899
+ return;
900
+ }
901
+ if (message.type === 'inline-media-result') {
902
+ const pending = this.pendingInlineMediaSaves.get(message.requestId);
903
+ if (!pending) {
904
+ return;
905
+ }
906
+ this.pendingInlineMediaSaves.delete(message.requestId);
907
+ window.clearTimeout(pending.timeoutId);
908
+ if (message.ok) {
909
+ pending.resolve({ fileId: message.fileId, value: message.value });
910
+ return;
911
+ }
912
+ const error = new Error(message.error || this.messages.errors.saveFieldFailed);
913
+ pending.reject(error);
914
+ this.callbacks.onError?.(error.message);
915
+ return;
916
+ }
892
917
  const pending = this.pendingInlineSaves.get(message.requestId);
893
918
  if (!pending) {
894
919
  return;
@@ -948,6 +973,7 @@ class EinblickEditRuntime {
948
973
  collectionId: message.collectionId,
949
974
  intent: message.intent,
950
975
  recordId: message.recordId,
976
+ sourceType: message.sourceType,
951
977
  resourceSlug: message.resourceSlug,
952
978
  });
953
979
  return;
@@ -956,6 +982,7 @@ class EinblickEditRuntime {
956
982
  if (message.resourceSlug) {
957
983
  void this.callbacks.onSave?.({
958
984
  binding: {
985
+ sourceType: message.sourceType ?? 'cms',
959
986
  resourceSlug: message.resourceSlug,
960
987
  recordId: message.recordId ?? '',
961
988
  resourceMode: 'collection',
@@ -963,6 +990,7 @@ class EinblickEditRuntime {
963
990
  },
964
991
  value: {
965
992
  action: message.action,
993
+ sourceType: message.sourceType ?? 'cms',
966
994
  recordId: message.recordId,
967
995
  collectionId: message.collectionId,
968
996
  },
@@ -994,6 +1022,7 @@ class EinblickEditRuntime {
994
1022
  else if (this.editorBinding) {
995
1023
  const nextBinding = {
996
1024
  ...this.editorBinding,
1025
+ sourceType: message.sourceType ?? this.editorBinding.sourceType ?? 'cms',
997
1026
  recordId: message.recordId ?? this.editorBinding.recordId,
998
1027
  };
999
1028
  void this.callbacks.onSave?.({
@@ -1009,6 +1038,7 @@ class EinblickEditRuntime {
1009
1038
  if (this.editorBinding) {
1010
1039
  const nextBinding = {
1011
1040
  ...this.editorBinding,
1041
+ sourceType: message.sourceType ?? this.editorBinding.sourceType ?? 'cms',
1012
1042
  resourceSlug: message.resourceSlug ?? this.editorBinding.resourceSlug,
1013
1043
  recordId: message.recordId,
1014
1044
  };
@@ -1060,16 +1090,23 @@ class EinblickEditRuntime {
1060
1090
  container.hidden = true;
1061
1091
  const editButton = document.createElement('button');
1062
1092
  editButton.type = 'button';
1063
- editButton.textContent = this.messages.actions.edit;
1093
+ editButton.textContent = this.messages.actions.editElement;
1064
1094
  editButton.setAttribute('data-einblick-edit-button', 'true');
1095
+ editButton.addEventListener('pointerdown', (event) => {
1096
+ event.stopPropagation();
1097
+ });
1065
1098
  editButton.addEventListener('click', this.handleHoverButtonClick);
1066
1099
  const createButton = document.createElement('button');
1067
1100
  createButton.type = 'button';
1068
1101
  createButton.textContent = this.messages.actions.addRecord;
1069
1102
  createButton.hidden = true;
1070
1103
  createButton.setAttribute('data-einblick-create-button', 'true');
1104
+ createButton.addEventListener('pointerdown', (event) => {
1105
+ event.stopPropagation();
1106
+ });
1071
1107
  createButton.addEventListener('click', this.handleCreateButtonClick);
1072
1108
  container.append(editButton, createButton);
1109
+ document.body.append(container);
1073
1110
  this.hoverButtonContainer = container;
1074
1111
  this.hoverButton = editButton;
1075
1112
  this.hoverCreateButton = createButton;
@@ -1189,8 +1226,6 @@ class EinblickEditRuntime {
1189
1226
  height: ${CONTROL_BAR_HEIGHT}px;
1190
1227
  padding: 0 0.75rem;
1191
1228
  background: ${EINBLICK_ACCENT_COLOR};
1192
- border-top: 1px solid rgba(255, 255, 255, 0.24);
1193
- box-shadow: 0 -8px 24px rgba(15, 23, 42, 0.18);
1194
1229
  }
1195
1230
 
1196
1231
  .brand,
@@ -1220,6 +1255,25 @@ class EinblickEditRuntime {
1220
1255
  font: 700 0.84rem/1.1 system-ui, sans-serif;
1221
1256
  }
1222
1257
 
1258
+ .resources {
1259
+ display: inline-flex;
1260
+ align-items: center;
1261
+ gap: 0.35rem;
1262
+ min-width: 0;
1263
+ max-width: min(48rem, 54vw);
1264
+ overflow-x: auto;
1265
+ scrollbar-width: none;
1266
+ }
1267
+
1268
+ .resources::-webkit-scrollbar {
1269
+ display: none;
1270
+ }
1271
+
1272
+ .resource-button {
1273
+ flex: 0 0 auto;
1274
+ max-width: 12rem;
1275
+ }
1276
+
1223
1277
  button {
1224
1278
  appearance: none;
1225
1279
  display: inline-flex;
@@ -1229,10 +1283,9 @@ class EinblickEditRuntime {
1229
1283
  min-width: 0;
1230
1284
  height: 2rem;
1231
1285
  margin: 0;
1232
- border: 1px solid rgba(255, 255, 255, 0.36);
1233
1286
  border-radius: 0.35rem;
1234
- background: rgba(255, 255, 255, 0.13);
1235
- color: rgb(255, 255, 255);
1287
+ background: #ffffff;
1288
+ color: #fa693e;
1236
1289
  padding: 0 0.7rem;
1237
1290
  font: 700 0.78rem/1 system-ui, sans-serif;
1238
1291
  letter-spacing: 0;
@@ -1251,11 +1304,11 @@ class EinblickEditRuntime {
1251
1304
 
1252
1305
  button:hover,
1253
1306
  button[aria-expanded="true"] {
1254
- background: rgba(255, 255, 255, 0.23);
1307
+ opacity: 0.8;
1255
1308
  }
1256
1309
 
1257
1310
  button:focus-visible {
1258
- outline: 2px solid rgba(255, 255, 255, 0.72);
1311
+ outline: 2px solid #ffffff;
1259
1312
  outline-offset: 2px;
1260
1313
  }
1261
1314
 
@@ -1341,14 +1394,8 @@ class EinblickEditRuntime {
1341
1394
  brand.append(mark, siteName);
1342
1395
  const actions = document.createElement('div');
1343
1396
  actions.className = 'actions';
1344
- const cmsButton = document.createElement('button');
1345
- cmsButton.type = 'button';
1346
- cmsButton.addEventListener('click', (event) => {
1347
- event.preventDefault();
1348
- event.stopPropagation();
1349
- this.setBottomBarMenuOpen(false);
1350
- this.setNavigatorOpen(!this.isNavigatorOpen());
1351
- });
1397
+ const resourceList = document.createElement('div');
1398
+ resourceList.className = 'resources';
1352
1399
  const moreButton = document.createElement('button');
1353
1400
  moreButton.type = 'button';
1354
1401
  moreButton.className = 'secondary icon-button';
@@ -1386,13 +1433,13 @@ class EinblickEditRuntime {
1386
1433
  void this.logoutFromEinblick();
1387
1434
  });
1388
1435
  moreMenu.append(openAppButton, logoutButton);
1389
- actions.append(cmsButton, moreButton);
1436
+ actions.append(resourceList, moreButton);
1390
1437
  bar.append(brand, actions, moreMenu);
1391
1438
  shadowRoot.append(style, bar);
1392
1439
  document.body.append(host);
1393
1440
  this.bottomBarHost = host;
1394
1441
  this.bottomBarSiteName = siteName;
1395
- this.bottomBarCmsButton = cmsButton;
1442
+ this.bottomBarResourceList = resourceList;
1396
1443
  this.bottomBarMoreButton = moreButton;
1397
1444
  this.bottomBarMoreMenu = moreMenu;
1398
1445
  this.bottomBarOpenAppButton = openAppButton;
@@ -1429,6 +1476,79 @@ class EinblickEditRuntime {
1429
1476
  ? this.messages.brand.authenticatedHint
1430
1477
  : this.messages.brand.unauthenticatedHint;
1431
1478
  }
1479
+ collectBottomBarResources() {
1480
+ const resources = new Map();
1481
+ for (const hint of this.resourceHints) {
1482
+ const key = `${hint.sourceType}:${hint.resourceSlug}`;
1483
+ if (hint.showInBottomBar === false) {
1484
+ resources.delete(key);
1485
+ continue;
1486
+ }
1487
+ resources.set(key, hint);
1488
+ }
1489
+ if (typeof document !== 'undefined') {
1490
+ const elements = document.querySelectorAll('[data-einblick-editable="true"][data-einblick-resource]');
1491
+ elements.forEach((element) => {
1492
+ const binding = readBindingFromElement(element);
1493
+ if (!binding?.resourceSlug)
1494
+ return;
1495
+ const sourceType = binding.sourceType ?? 'cms';
1496
+ const key = `${sourceType}:${binding.resourceSlug}`;
1497
+ if (resources.has(key)) {
1498
+ const current = resources.get(key);
1499
+ if (binding.recordId &&
1500
+ !current.recordIds?.includes(binding.recordId)) {
1501
+ current.recordIds = [...(current.recordIds ?? []), binding.recordId];
1502
+ }
1503
+ return;
1504
+ }
1505
+ resources.set(key, {
1506
+ sourceType,
1507
+ resourceSlug: binding.resourceSlug,
1508
+ label: binding.label,
1509
+ mode: binding.resourceMode,
1510
+ recordIds: binding.recordId ? [binding.recordId] : undefined,
1511
+ });
1512
+ });
1513
+ }
1514
+ return [...resources.values()];
1515
+ }
1516
+ updateBottomBarResources() {
1517
+ if (!this.bottomBarResourceList) {
1518
+ return;
1519
+ }
1520
+ this.bottomBarResourceList.replaceChildren();
1521
+ const resources = this.collectBottomBarResources();
1522
+ if (resources.length === 0) {
1523
+ const button = document.createElement('button');
1524
+ button.type = 'button';
1525
+ button.className = 'resource-button';
1526
+ button.textContent = this.messages.actions.cmsCollections;
1527
+ button.setAttribute('aria-expanded', this.isNavigatorOpen() ? 'true' : 'false');
1528
+ button.addEventListener('click', (event) => {
1529
+ event.preventDefault();
1530
+ event.stopPropagation();
1531
+ this.setBottomBarMenuOpen(false);
1532
+ this.setNavigatorOpen(!this.isNavigatorOpen());
1533
+ });
1534
+ this.bottomBarResourceList.append(button);
1535
+ return;
1536
+ }
1537
+ for (const resource of resources) {
1538
+ const button = document.createElement('button');
1539
+ button.type = 'button';
1540
+ button.className = 'resource-button';
1541
+ button.textContent = resource.label ?? resource.resourceSlug;
1542
+ button.title = button.textContent;
1543
+ button.addEventListener('click', (event) => {
1544
+ event.preventDefault();
1545
+ event.stopPropagation();
1546
+ this.setBottomBarMenuOpen(false);
1547
+ this.openNavigator(resource);
1548
+ });
1549
+ this.bottomBarResourceList.append(button);
1550
+ }
1551
+ }
1432
1552
  updateBottomBar() {
1433
1553
  if (!this.bottomBarHost) {
1434
1554
  this.applyBottomLayout(false);
@@ -1448,10 +1568,7 @@ class EinblickEditRuntime {
1448
1568
  this.state.siteName?.trim() || this.messages.brand.connectedSite;
1449
1569
  this.bottomBarSiteName.title = this.bottomBarSiteName.textContent;
1450
1570
  }
1451
- if (this.bottomBarCmsButton) {
1452
- this.bottomBarCmsButton.textContent = this.messages.actions.cmsCollections;
1453
- this.bottomBarCmsButton.setAttribute('aria-expanded', this.isNavigatorOpen() ? 'true' : 'false');
1454
- }
1571
+ this.updateBottomBarResources();
1455
1572
  if (this.bottomBarMoreButton) {
1456
1573
  this.bottomBarMoreButton.setAttribute('aria-label', this.messages.actions.moreActions);
1457
1574
  this.bottomBarMoreButton.setAttribute('aria-expanded', this.isBottomBarMenuOpen() ? 'true' : 'false');
@@ -1463,7 +1580,7 @@ class EinblickEditRuntime {
1463
1580
  if (this.bottomBarLogoutButton) {
1464
1581
  this.bottomBarLogoutButton.textContent = this.messages.actions.logout;
1465
1582
  }
1466
- this.ensureNavigatorPanel();
1583
+ this.ensureNavigatorPanel(false, this.navigatorFilter);
1467
1584
  if (this.navigatorFrame) {
1468
1585
  this.navigatorFrame.title = this.messages.actions.cmsCollections;
1469
1586
  }
@@ -1527,7 +1644,7 @@ class EinblickEditRuntime {
1527
1644
  isNavigatorOpen() {
1528
1645
  return this.navigatorOpen && !!this.navigatorPanel;
1529
1646
  }
1530
- ensureNavigatorPanel(reportErrors = false) {
1647
+ ensureNavigatorPanel(reportErrors = false, filter) {
1531
1648
  if (this.navigatorPanel) {
1532
1649
  return true;
1533
1650
  }
@@ -1537,7 +1654,7 @@ class EinblickEditRuntime {
1537
1654
  let navigatorUrl;
1538
1655
  const nonce = createRandomId();
1539
1656
  try {
1540
- navigatorUrl = this.buildNavigatorUrl(nonce);
1657
+ navigatorUrl = this.buildNavigatorUrl(nonce, filter);
1541
1658
  }
1542
1659
  catch (error) {
1543
1660
  if (reportErrors) {
@@ -1564,18 +1681,22 @@ class EinblickEditRuntime {
1564
1681
  this.navigatorPanel = panel;
1565
1682
  this.navigatorFrame = iframe;
1566
1683
  this.navigatorNonce = nonce;
1684
+ this.navigatorFilter = filter ?? null;
1567
1685
  return true;
1568
1686
  }
1569
- openNavigator() {
1687
+ openNavigator(filter) {
1570
1688
  if (!this.state.isAuthenticated) {
1571
1689
  this.callbacks.onError?.(this.messages.errors.signInToEditContent);
1572
1690
  return;
1573
1691
  }
1574
- if (this.isNavigatorOpen()) {
1692
+ if (this.isNavigatorOpen() && !filter) {
1575
1693
  return;
1576
1694
  }
1695
+ if (this.isNavigatorOpen() && filter) {
1696
+ this.removeNavigatorPanel(false);
1697
+ }
1577
1698
  this.setBottomBarMenuOpen(false);
1578
- if (!this.ensureNavigatorPanel(true) || !this.navigatorPanel) {
1699
+ if (!this.ensureNavigatorPanel(true, filter) || !this.navigatorPanel) {
1579
1700
  return;
1580
1701
  }
1581
1702
  this.navigatorOpen = true;
@@ -1602,6 +1723,7 @@ class EinblickEditRuntime {
1602
1723
  this.navigatorPanel = null;
1603
1724
  this.navigatorFrame = null;
1604
1725
  this.navigatorNonce = null;
1726
+ this.navigatorFilter = null;
1605
1727
  this.navigatorOpen = false;
1606
1728
  if (hadNavigator && updateBottomBar) {
1607
1729
  this.updateBottomBar();
@@ -1621,6 +1743,48 @@ class EinblickEditRuntime {
1621
1743
  siteName: this.state.siteName,
1622
1744
  errorMessage: undefined,
1623
1745
  });
1746
+ const loggedOutViaBridge = await this.requestBridgeLogout();
1747
+ if (!loggedOutViaBridge) {
1748
+ await this.logoutViaPopup(sessionToken);
1749
+ }
1750
+ this.reloadBridge();
1751
+ }
1752
+ requestBridgeLogout() {
1753
+ if (typeof window === 'undefined' || this.pendingLogout) {
1754
+ return Promise.resolve(false);
1755
+ }
1756
+ const target = this.bridgeFrame?.contentWindow;
1757
+ if (!target) {
1758
+ return Promise.resolve(false);
1759
+ }
1760
+ const requestId = createRandomId();
1761
+ return new Promise((resolve) => {
1762
+ const timeoutId = window.setTimeout(() => {
1763
+ if (this.pendingLogout?.requestId !== requestId) {
1764
+ return;
1765
+ }
1766
+ this.pendingLogout = null;
1767
+ resolve(false);
1768
+ }, 2500);
1769
+ this.pendingLogout = { requestId, resolve, timeoutId };
1770
+ try {
1771
+ target.postMessage({
1772
+ source: EINBLICK_PARENT_SOURCE,
1773
+ type: 'logout',
1774
+ nonce: this.bridgeNonce,
1775
+ requestId,
1776
+ }, this.appOrigin);
1777
+ }
1778
+ catch {
1779
+ window.clearTimeout(timeoutId);
1780
+ if (this.pendingLogout?.requestId === requestId) {
1781
+ this.pendingLogout = null;
1782
+ }
1783
+ resolve(false);
1784
+ }
1785
+ });
1786
+ }
1787
+ async logoutViaPopup(sessionToken) {
1624
1788
  const logoutUrl = this.buildBridgeUrl({
1625
1789
  mode: 'logout',
1626
1790
  nonce: createRandomId(),
@@ -1647,28 +1811,11 @@ class EinblickEditRuntime {
1647
1811
  }
1648
1812
  }, 400);
1649
1813
  });
1650
- this.reloadBridge();
1651
1814
  }
1652
1815
  setActiveElement(element) {
1653
1816
  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
- }
1817
+ this.updateHoverChrome();
1818
+ return;
1672
1819
  }
1673
1820
  this.activeElement = element;
1674
1821
  if (!element ||
@@ -1677,22 +1824,176 @@ class EinblickEditRuntime {
1677
1824
  !this.state.isAuthenticated) {
1678
1825
  if (this.hoverButtonContainer) {
1679
1826
  this.hoverButtonContainer.hidden = true;
1680
- this.hoverButtonContainer.remove();
1681
1827
  }
1828
+ this.activeOutline?.remove();
1829
+ this.activeOutline = null;
1830
+ this.clearQuickFieldOverlays();
1682
1831
  if (this.hoverCreateButton) {
1683
1832
  this.hoverCreateButton.hidden = true;
1684
1833
  }
1685
1834
  return;
1686
1835
  }
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
- }
1836
+ this.ensureActiveOutline();
1837
+ this.hoverButtonContainer.hidden = false;
1838
+ this.updateHoverButtonLabel();
1693
1839
  this.updateHoverButtonVisibility(element);
1840
+ this.renderQuickFieldOverlays(element);
1841
+ this.updateHoverChrome();
1842
+ }
1843
+ ensureActiveOutline() {
1844
+ if (this.activeOutline || typeof document === 'undefined') {
1845
+ return;
1846
+ }
1847
+ const outline = document.createElement('div');
1848
+ outline.setAttribute('data-einblick-edit-outline', 'true');
1849
+ document.body.append(outline);
1850
+ this.activeOutline = outline;
1851
+ }
1852
+ updateHoverChrome() {
1853
+ if (!this.activeElement || !this.state.isAuthenticated) {
1854
+ this.activeOutline?.remove();
1855
+ this.activeOutline = null;
1856
+ if (this.hoverButtonContainer) {
1857
+ this.hoverButtonContainer.hidden = true;
1858
+ }
1859
+ this.clearQuickFieldOverlays();
1860
+ return;
1861
+ }
1862
+ if (!this.activeElement.isConnected) {
1863
+ this.setActiveElement(null);
1864
+ return;
1865
+ }
1866
+ this.ensureActiveOutline();
1867
+ const rect = this.activeElement.getBoundingClientRect();
1868
+ const isVisible = this.positionOutline(this.activeOutline, this.activeElement);
1869
+ if (!isVisible || rect.width <= 0 || rect.height <= 0) {
1870
+ if (this.hoverButtonContainer) {
1871
+ this.hoverButtonContainer.hidden = true;
1872
+ }
1873
+ this.clearQuickFieldOverlays();
1874
+ return;
1875
+ }
1876
+ this.updateHoverButtonLabel();
1877
+ this.updateHoverButtonVisibility(this.activeElement);
1878
+ this.positionHoverControls(rect);
1879
+ this.updateQuickFieldOverlayPositions();
1880
+ }
1881
+ positionOutline(outline, target) {
1882
+ if (!outline) {
1883
+ return false;
1884
+ }
1885
+ const rect = target.getBoundingClientRect();
1886
+ if (rect.width <= 0 || rect.height <= 0) {
1887
+ outline.hidden = true;
1888
+ return false;
1889
+ }
1890
+ const style = window.getComputedStyle(target);
1891
+ outline.hidden = false;
1892
+ outline.style.setProperty('--einblick-overlay-radius', style.borderRadius || '0.35rem');
1893
+ outline.style.left = `${Math.round(rect.left)}px`;
1894
+ outline.style.top = `${Math.round(rect.top)}px`;
1895
+ outline.style.width = `${Math.round(rect.width)}px`;
1896
+ outline.style.height = `${Math.round(rect.height)}px`;
1897
+ return true;
1898
+ }
1899
+ positionHoverControls(rect) {
1900
+ if (!this.hoverButtonContainer) {
1901
+ return;
1902
+ }
1694
1903
  this.hoverButtonContainer.hidden = false;
1695
- element.append(this.hoverButtonContainer);
1904
+ const margin = 8;
1905
+ const width = this.hoverButtonContainer.offsetWidth || 180;
1906
+ const height = this.hoverButtonContainer.offsetHeight || 32;
1907
+ const left = clamp(rect.right - width - margin, margin, Math.max(margin, window.innerWidth - width - margin));
1908
+ const top = clamp(rect.top + margin, margin, Math.max(margin, window.innerHeight - height - margin));
1909
+ this.hoverButtonContainer.style.left = `${Math.round(left)}px`;
1910
+ this.hoverButtonContainer.style.top = `${Math.round(top)}px`;
1911
+ }
1912
+ clearQuickFieldOverlays() {
1913
+ this.quickFieldOverlayContainer?.remove();
1914
+ this.quickFieldOverlayContainer = null;
1915
+ this.quickFieldOverlays = [];
1916
+ }
1917
+ renderQuickFieldOverlays(element) {
1918
+ this.clearQuickFieldOverlays();
1919
+ const targets = this.getQuickEditableTargets(element);
1920
+ if (targets.length === 0 || typeof document === 'undefined') {
1921
+ return;
1922
+ }
1923
+ const container = document.createElement('div');
1924
+ container.setAttribute('data-einblick-field-overlays', 'true');
1925
+ this.quickFieldOverlays = targets.map((target) => {
1926
+ const binding = readBindingFromElement(target);
1927
+ const outline = document.createElement('div');
1928
+ outline.setAttribute('data-einblick-edit-field-outline', 'true');
1929
+ const button = document.createElement('button');
1930
+ button.type = 'button';
1931
+ button.setAttribute('data-einblick-field-button', binding && isMediaEditableBinding(binding) ? 'media' : 'text');
1932
+ button.textContent =
1933
+ binding && isMediaEditableBinding(binding)
1934
+ ? getMediaActionLabel(binding, this.messages)
1935
+ : this.messages.actions.editText;
1936
+ button.addEventListener('pointerdown', (event) => {
1937
+ event.stopPropagation();
1938
+ });
1939
+ button.addEventListener('click', (event) => {
1940
+ stopEvent(event);
1941
+ const nextBinding = readBindingFromElement(target);
1942
+ if (!nextBinding) {
1943
+ return;
1944
+ }
1945
+ if (isMediaEditableBinding(nextBinding)) {
1946
+ this.openMediaPicker(target, nextBinding, button);
1947
+ return;
1948
+ }
1949
+ this.openInlineEditor(target, nextBinding);
1950
+ });
1951
+ container.append(outline, button);
1952
+ return { target, outline, button };
1953
+ });
1954
+ document.body.append(container);
1955
+ this.quickFieldOverlayContainer = container;
1956
+ this.updateQuickFieldOverlayPositions();
1957
+ }
1958
+ updateQuickFieldOverlayPositions() {
1959
+ if (!this.quickFieldOverlayContainer) {
1960
+ return;
1961
+ }
1962
+ for (const overlay of this.quickFieldOverlays) {
1963
+ if (!overlay.target.isConnected) {
1964
+ overlay.outline.hidden = true;
1965
+ overlay.button.hidden = true;
1966
+ continue;
1967
+ }
1968
+ const rect = overlay.target.getBoundingClientRect();
1969
+ const isVisible = this.positionOutline(overlay.outline, overlay.target);
1970
+ overlay.button.hidden = !isVisible;
1971
+ if (!isVisible) {
1972
+ continue;
1973
+ }
1974
+ const margin = 4;
1975
+ const width = overlay.button.offsetWidth || 96;
1976
+ const height = overlay.button.offsetHeight || 26;
1977
+ const left = clamp(rect.left + margin, margin, Math.max(margin, window.innerWidth - width - margin));
1978
+ const top = clamp(rect.top + margin, margin, Math.max(margin, window.innerHeight - height - margin));
1979
+ overlay.button.style.left = `${Math.round(left)}px`;
1980
+ overlay.button.style.top = `${Math.round(top)}px`;
1981
+ }
1982
+ }
1983
+ updateHoverButtonLabel() {
1984
+ if (!this.hoverButton || !this.activeElement) {
1985
+ return;
1986
+ }
1987
+ const binding = readBindingFromElement(this.activeElement);
1988
+ if (!binding?.fieldKey) {
1989
+ this.hoverButton.textContent = this.messages.actions.editElement;
1990
+ return;
1991
+ }
1992
+ this.hoverButton.textContent = isMediaEditableBinding(binding)
1993
+ ? getMediaActionLabel(binding, this.messages)
1994
+ : shouldUseInlineEditor(binding)
1995
+ ? this.messages.actions.editText
1996
+ : this.messages.actions.edit;
1696
1997
  }
1697
1998
  reloadBridge() {
1698
1999
  this.clearBridgePing();
@@ -1765,6 +2066,7 @@ class EinblickEditRuntime {
1765
2066
  url.searchParams.set('nonce', nonce);
1766
2067
  url.searchParams.set('sessionToken', sessionToken);
1767
2068
  url.searchParams.set('returnTo', window.location.href);
2069
+ url.searchParams.set('sourceType', binding.sourceType ?? 'cms');
1768
2070
  url.searchParams.set('resourceSlug', binding.resourceSlug);
1769
2071
  url.searchParams.set('recordId', binding.recordId);
1770
2072
  if (intent === 'create') {
@@ -1775,7 +2077,7 @@ class EinblickEditRuntime {
1775
2077
  }
1776
2078
  return url.toString();
1777
2079
  }
1778
- buildNavigatorUrl(nonce) {
2080
+ buildNavigatorUrl(nonce, filter) {
1779
2081
  const sessionToken = this.getActiveSessionToken();
1780
2082
  if (!sessionToken) {
1781
2083
  throw new Error(this.messages.errors.editSessionExpired);
@@ -1784,6 +2086,15 @@ class EinblickEditRuntime {
1784
2086
  url.searchParams.set('origin', window.location.origin);
1785
2087
  url.searchParams.set('nonce', nonce);
1786
2088
  url.searchParams.set('sessionToken', sessionToken);
2089
+ if (filter?.sourceType) {
2090
+ url.searchParams.set('sourceType', filter.sourceType);
2091
+ }
2092
+ if (filter?.resourceSlug) {
2093
+ url.searchParams.set('resourceSlug', filter.resourceSlug);
2094
+ }
2095
+ if (filter?.recordIds && filter.recordIds.length > 0) {
2096
+ url.searchParams.set('recordIds', filter.recordIds.join(','));
2097
+ }
1787
2098
  return url.toString();
1788
2099
  }
1789
2100
  buildCollectionEditorUrl(args) {
@@ -1796,7 +2107,11 @@ class EinblickEditRuntime {
1796
2107
  url.searchParams.set('nonce', args.nonce);
1797
2108
  url.searchParams.set('sessionToken', sessionToken);
1798
2109
  url.searchParams.set('returnTo', window.location.href);
2110
+ url.searchParams.set('sourceType', args.sourceType ?? 'cms');
1799
2111
  url.searchParams.set('collectionId', args.collectionId);
2112
+ if (args.resourceSlug) {
2113
+ url.searchParams.set('resourceSlug', args.resourceSlug);
2114
+ }
1800
2115
  url.searchParams.set('intent', args.intent);
1801
2116
  if (args.recordId) {
1802
2117
  url.searchParams.set('recordId', args.recordId);
@@ -1828,6 +2143,7 @@ class EinblickEditRuntime {
1828
2143
  }
1829
2144
  const selector = [
1830
2145
  '[data-einblick-editable="true"]',
2146
+ `[data-einblick-source-type="${CSS.escape(binding.sourceType ?? 'cms')}"]`,
1831
2147
  `[data-einblick-resource="${CSS.escape(binding.resourceSlug)}"]`,
1832
2148
  ].join('');
1833
2149
  const elements = document.querySelectorAll(selector);
@@ -1845,11 +2161,74 @@ class EinblickEditRuntime {
1845
2161
  return null;
1846
2162
  }
1847
2163
  const editableTarget = target.closest('[data-einblick-editable="true"]');
1848
- return editableTarget ?? null;
2164
+ if (!editableTarget) {
2165
+ return null;
2166
+ }
2167
+ const binding = readBindingFromElement(editableTarget);
2168
+ if (!binding?.fieldKey) {
2169
+ return editableTarget;
2170
+ }
2171
+ const entryTarget = this.findEntryTargetForField(editableTarget, binding);
2172
+ return entryTarget ?? editableTarget;
2173
+ }
2174
+ findEntryTargetForField(fieldElement, binding) {
2175
+ let parent = fieldElement.parentElement;
2176
+ while (parent) {
2177
+ const parentBinding = readBindingFromElement(parent);
2178
+ if (parentBinding &&
2179
+ !parentBinding.fieldKey &&
2180
+ (parentBinding.sourceType ?? 'cms') === (binding.sourceType ?? 'cms') &&
2181
+ parentBinding.resourceSlug === binding.resourceSlug &&
2182
+ parentBinding.recordId === binding.recordId) {
2183
+ return parent;
2184
+ }
2185
+ parent = parent.parentElement;
2186
+ }
2187
+ return null;
2188
+ }
2189
+ getQuickEditableTargets(element) {
2190
+ const binding = readBindingFromElement(element);
2191
+ if (!binding) {
2192
+ return [];
2193
+ }
2194
+ if (binding.fieldKey) {
2195
+ return shouldUseInlineEditor(binding) || isMediaEditableBinding(binding)
2196
+ ? [element]
2197
+ : [];
2198
+ }
2199
+ const selector = [
2200
+ '[data-einblick-editable="true"]',
2201
+ `[data-einblick-source-type="${CSS.escape(binding.sourceType ?? 'cms')}"]`,
2202
+ `[data-einblick-resource="${CSS.escape(binding.resourceSlug)}"]`,
2203
+ `[data-einblick-record-id="${CSS.escape(binding.recordId)}"]`,
2204
+ '[data-einblick-field-key]',
2205
+ ].join('');
2206
+ const fields = Array.from(element.querySelectorAll(selector));
2207
+ return fields
2208
+ .filter((candidate) => {
2209
+ const candidateBinding = readBindingFromElement(candidate);
2210
+ return (!!candidateBinding &&
2211
+ (shouldUseInlineEditor(candidateBinding) ||
2212
+ isMediaEditableBinding(candidateBinding)));
2213
+ })
2214
+ .slice(0, 24);
2215
+ }
2216
+ isSdkChromeTarget(target) {
2217
+ if (!(target instanceof Node)) {
2218
+ return false;
2219
+ }
2220
+ return (!!this.hoverButtonContainer?.contains(target) ||
2221
+ !!this.quickFieldOverlayContainer?.contains(target) ||
2222
+ !!this.activeOutline?.contains(target) ||
2223
+ !!this.brandBadge?.contains(target) ||
2224
+ !!this.brandPopover?.contains(target) ||
2225
+ !!this.bottomBarHost?.contains(target) ||
2226
+ !!this.bottomBarMoreMenu?.contains(target));
1849
2227
  }
1850
2228
  findElementForBinding(binding) {
1851
2229
  const selector = [
1852
2230
  '[data-einblick-editable="true"]',
2231
+ `[data-einblick-source-type="${CSS.escape(binding.sourceType ?? 'cms')}"]`,
1853
2232
  `[data-einblick-resource="${CSS.escape(binding.resourceSlug)}"]`,
1854
2233
  `[data-einblick-record-id="${CSS.escape(binding.recordId)}"]`,
1855
2234
  binding.fieldKey
@@ -1874,216 +2253,343 @@ class EinblickEditRuntime {
1874
2253
  this.closeDrawer();
1875
2254
  this.closeInlineEditor();
1876
2255
  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,
2256
+ const restoreState = {
2257
+ childNodes: Array.from(target.childNodes).map((node) => node.cloneNode(true)),
2258
+ contentEditable: target.getAttribute('contenteditable'),
2259
+ spellcheck: target.getAttribute('spellcheck'),
2260
+ tabIndex: target.tabIndex,
2261
+ hadTabIndex: target.hasAttribute('tabindex'),
2262
+ role: target.getAttribute('role'),
2263
+ ariaMultiline: target.getAttribute('aria-multiline'),
2264
+ title: target.getAttribute('title'),
2265
+ value: binding.value,
2266
+ cleanup: [],
1890
2267
  };
2268
+ this.currentInlineRestoreState = restoreState;
1891
2269
  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
- }
2270
+ target.setAttribute('contenteditable', 'plaintext-only');
2271
+ target.setAttribute('spellcheck', 'true');
2272
+ target.setAttribute('role', 'textbox');
2273
+ target.setAttribute('aria-multiline', fieldTypeAllowsMultiline(binding) ? 'true' : 'false');
2274
+ if (!target.hasAttribute('tabindex')) {
2275
+ target.tabIndex = 0;
2276
+ }
2277
+ if ((target.textContent ?? '').trim().length === 0 &&
2278
+ binding.value !== undefined &&
2279
+ binding.value !== null) {
2280
+ target.textContent = formatValueForText(binding.value);
2281
+ }
2282
+ const handleClick = (event) => {
2283
+ event.preventDefault();
2284
+ event.stopPropagation();
1948
2285
  };
1949
- saveButton.addEventListener('click', () => {
1950
- void runSave();
1951
- });
1952
- control.addEventListener('keydown', (event) => {
1953
- const keyboardEvent = event;
2286
+ const handleKeyDown = (keyboardEvent) => {
1954
2287
  if (keyboardEvent.key === 'Escape') {
1955
2288
  keyboardEvent.preventDefault();
2289
+ keyboardEvent.stopPropagation();
1956
2290
  this.closeInlineEditor();
1957
2291
  return;
1958
2292
  }
1959
2293
  if (keyboardEvent.key === 'Enter' &&
1960
- (!(control instanceof HTMLTextAreaElement) ||
2294
+ (!fieldTypeAllowsMultiline(binding) ||
1961
2295
  keyboardEvent.metaKey ||
1962
2296
  keyboardEvent.ctrlKey)) {
1963
2297
  keyboardEvent.preventDefault();
1964
- void runSave();
2298
+ keyboardEvent.stopPropagation();
2299
+ void this.commitInlineEditor();
2300
+ return;
1965
2301
  }
1966
- });
1967
- actions.append(cancelButton, saveButton);
1968
- shell.append(control, errorMessage, actions);
1969
- shadowRoot.append(shadowStyle, shell);
1970
- target.replaceChildren(shellHost);
2302
+ if (isTextInputEvent(keyboardEvent)) {
2303
+ keyboardEvent.stopPropagation();
2304
+ }
2305
+ };
2306
+ const handlePaste = (event) => {
2307
+ const text = event.clipboardData?.getData('text/plain');
2308
+ if (text === undefined) {
2309
+ return;
2310
+ }
2311
+ event.preventDefault();
2312
+ event.stopPropagation();
2313
+ this.insertPlainTextAtSelection(text);
2314
+ };
2315
+ const handleBlur = () => {
2316
+ window.setTimeout(() => {
2317
+ if (this.currentInlineTarget === target) {
2318
+ void this.commitInlineEditor();
2319
+ }
2320
+ }, 0);
2321
+ };
2322
+ target.addEventListener('click', handleClick, true);
2323
+ target.addEventListener('keydown', handleKeyDown, true);
2324
+ target.addEventListener('paste', handlePaste, true);
2325
+ target.addEventListener('blur', handleBlur, true);
2326
+ restoreState.cleanup.push(() => target.removeEventListener('click', handleClick, true), () => target.removeEventListener('keydown', handleKeyDown, true), () => target.removeEventListener('paste', handlePaste, true), () => target.removeEventListener('blur', handleBlur, true));
1971
2327
  this.currentInlineTarget = target;
1972
2328
  this.currentInlineBinding = binding;
2329
+ this.ensureActiveOutline();
2330
+ this.activeOutline?.setAttribute('data-einblick-inline-active', 'true');
2331
+ this.positionOutline(this.activeOutline, target);
1973
2332
  window.setTimeout(() => {
1974
- if ('focus' in control && typeof control.focus === 'function') {
1975
- control.focus();
1976
- }
2333
+ target.focus({ preventScroll: true });
2334
+ this.placeCaretAtEnd(target);
1977
2335
  }, 0);
1978
2336
  }
1979
- closeInlineEditor() {
2337
+ closeInlineEditor(options = {}) {
2338
+ const restore = options.restore ?? true;
1980
2339
  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;
2340
+ for (const cleanup of this.currentInlineRestoreState.cleanup) {
2341
+ cleanup();
2342
+ }
2343
+ if (restore) {
2344
+ this.currentInlineTarget.replaceChildren(...this.currentInlineRestoreState.childNodes.map((node) => node.cloneNode(true)));
2345
+ this.currentInlineTarget.dataset.einblickValue =
2346
+ serializeBindingValue(this.currentInlineRestoreState.value) ?? '';
2347
+ }
2348
+ this.restoreInlineTargetAttributes(this.currentInlineTarget, this.currentInlineRestoreState);
1991
2349
  delete this.currentInlineTarget.dataset.einblickInlineEditing;
2350
+ delete this.currentInlineTarget.dataset.einblickInlineState;
2351
+ this.currentInlineTarget.removeAttribute('aria-busy');
1992
2352
  }
1993
2353
  this.currentInlineRestoreState = null;
1994
2354
  this.currentInlineTarget = null;
1995
2355
  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
- }
2356
+ this.inlineSaveInFlight = null;
2357
+ this.activeOutline?.removeAttribute('data-einblick-inline-active');
2358
+ if (!this.activeElement) {
2359
+ this.activeOutline?.remove();
2360
+ this.activeOutline = null;
2041
2361
  }
2042
2362
  }
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;
2363
+ restoreInlineTargetAttributes(target, restoreState) {
2364
+ if (restoreState.contentEditable === null) {
2365
+ target.removeAttribute('contenteditable');
2048
2366
  }
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;
2367
+ else {
2368
+ target.setAttribute('contenteditable', restoreState.contentEditable);
2058
2369
  }
2059
- if (binding.fieldType === 'date') {
2060
- control.value =
2061
- typeof currentValue === 'string' ? normalizeDateValue(currentValue) : '';
2062
- return;
2370
+ if (restoreState.spellcheck === null) {
2371
+ target.removeAttribute('spellcheck');
2372
+ }
2373
+ else {
2374
+ target.setAttribute('spellcheck', restoreState.spellcheck);
2375
+ }
2376
+ if (restoreState.role === null) {
2377
+ target.removeAttribute('role');
2378
+ }
2379
+ else {
2380
+ target.setAttribute('role', restoreState.role);
2381
+ }
2382
+ if (restoreState.ariaMultiline === null) {
2383
+ target.removeAttribute('aria-multiline');
2384
+ }
2385
+ else {
2386
+ target.setAttribute('aria-multiline', restoreState.ariaMultiline);
2387
+ }
2388
+ if (restoreState.title === null) {
2389
+ target.removeAttribute('title');
2390
+ }
2391
+ else {
2392
+ target.setAttribute('title', restoreState.title);
2393
+ }
2394
+ if (restoreState.hadTabIndex) {
2395
+ target.tabIndex = restoreState.tabIndex;
2396
+ }
2397
+ else {
2398
+ target.removeAttribute('tabindex');
2063
2399
  }
2064
- control.value =
2065
- currentValue === undefined || currentValue === null
2066
- ? ''
2067
- : typeof currentValue === 'string'
2068
- ? currentValue
2069
- : String(currentValue);
2070
2400
  }
2071
- getControlValue(control, binding) {
2072
- const rawValue = control.value;
2401
+ getInlineEditableValue(target, binding) {
2402
+ const rawValue = fieldTypeAllowsMultiline(binding)
2403
+ ? target.innerText.replace(/\n$/, '')
2404
+ : (target.textContent ?? '').replace(/\s+/g, ' ').trim();
2073
2405
  switch (binding.fieldType) {
2074
2406
  case 'number':
2075
2407
  return rawValue.trim().length === 0 ? null : Number(rawValue);
2076
- case 'boolean':
2077
- return rawValue === 'true';
2078
2408
  case 'tags':
2079
2409
  return rawValue
2080
2410
  .split(',')
2081
2411
  .map((entry) => entry.trim())
2082
2412
  .filter(Boolean);
2413
+ case 'date':
2414
+ return normalizeDateValue(rawValue.trim());
2083
2415
  default:
2084
2416
  return rawValue;
2085
2417
  }
2086
2418
  }
2419
+ async commitInlineEditor() {
2420
+ if (this.inlineSaveInFlight) {
2421
+ return await this.inlineSaveInFlight;
2422
+ }
2423
+ const target = this.currentInlineTarget;
2424
+ const binding = this.currentInlineBinding;
2425
+ if (!target || !binding) {
2426
+ return;
2427
+ }
2428
+ this.inlineSaveInFlight = (async () => {
2429
+ target.setAttribute('aria-busy', 'true');
2430
+ target.dataset.einblickInlineState = 'saving';
2431
+ try {
2432
+ const nextValue = this.getInlineEditableValue(target, binding);
2433
+ const savedValue = await this.saveInlineField(binding, nextValue);
2434
+ this.closeInlineEditor({ restore: false });
2435
+ this.applyValueToElement(target, binding, savedValue);
2436
+ await this.callbacks.onSave?.({
2437
+ binding,
2438
+ value: savedValue,
2439
+ source: 'inline',
2440
+ });
2441
+ }
2442
+ catch (error) {
2443
+ target.dataset.einblickInlineState = 'error';
2444
+ target.removeAttribute('aria-busy');
2445
+ this.inlineSaveInFlight = null;
2446
+ const message = error instanceof Error
2447
+ ? error.message
2448
+ : this.messages.errors.saveFieldFailed;
2449
+ target.title = message;
2450
+ this.callbacks.onError?.(message);
2451
+ }
2452
+ })();
2453
+ await this.inlineSaveInFlight;
2454
+ }
2455
+ insertPlainTextAtSelection(text) {
2456
+ const selection = window.getSelection();
2457
+ if (!selection || selection.rangeCount === 0) {
2458
+ return;
2459
+ }
2460
+ selection.deleteFromDocument();
2461
+ selection.getRangeAt(0).insertNode(document.createTextNode(text));
2462
+ selection.collapseToEnd();
2463
+ }
2464
+ placeCaretAtEnd(target) {
2465
+ const selection = window.getSelection();
2466
+ if (!selection) {
2467
+ return;
2468
+ }
2469
+ const range = document.createRange();
2470
+ range.selectNodeContents(target);
2471
+ range.collapse(false);
2472
+ selection.removeAllRanges();
2473
+ selection.addRange(range);
2474
+ }
2475
+ openMediaPicker(target, binding, button) {
2476
+ if (!this.state.isAuthenticated) {
2477
+ this.callbacks.onError?.(this.messages.errors.signInToEditField);
2478
+ return;
2479
+ }
2480
+ if (!binding.fieldKey) {
2481
+ this.callbacks.onError?.(this.messages.errors.fieldBindingRequired);
2482
+ return;
2483
+ }
2484
+ const input = document.createElement('input');
2485
+ input.type = 'file';
2486
+ input.accept = getMediaAcceptValue(binding);
2487
+ input.style.position = 'fixed';
2488
+ input.style.left = '-9999px';
2489
+ input.style.top = '0';
2490
+ const previousText = button?.textContent ?? '';
2491
+ const resetButton = () => {
2492
+ if (button) {
2493
+ button.disabled = false;
2494
+ button.textContent =
2495
+ previousText || getMediaActionLabel(binding, this.messages);
2496
+ }
2497
+ };
2498
+ input.addEventListener('change', () => {
2499
+ const file = input.files?.[0];
2500
+ input.remove();
2501
+ if (!file) {
2502
+ return;
2503
+ }
2504
+ if (button) {
2505
+ button.disabled = true;
2506
+ button.textContent = this.messages.actions.uploading;
2507
+ }
2508
+ void (async () => {
2509
+ try {
2510
+ const result = await this.saveInlineMediaField(binding, file);
2511
+ this.applyMediaPreviewToElement(target, file);
2512
+ await this.callbacks.onSave?.({
2513
+ binding,
2514
+ value: result.value,
2515
+ source: 'inline',
2516
+ });
2517
+ }
2518
+ catch (error) {
2519
+ const message = error instanceof Error
2520
+ ? error.message
2521
+ : this.messages.errors.saveFieldFailed;
2522
+ this.callbacks.onError?.(message);
2523
+ }
2524
+ finally {
2525
+ resetButton();
2526
+ }
2527
+ })();
2528
+ });
2529
+ input.addEventListener('cancel', () => {
2530
+ input.remove();
2531
+ });
2532
+ document.body.append(input);
2533
+ input.click();
2534
+ }
2535
+ async saveInlineMediaField(binding, file) {
2536
+ if (!this.bridgeFrame?.contentWindow) {
2537
+ throw new Error(this.messages.errors.bridgeNotConnected);
2538
+ }
2539
+ if (!this.state.isAuthenticated) {
2540
+ throw new Error(this.messages.errors.signInToEditField);
2541
+ }
2542
+ if (!binding.fieldKey) {
2543
+ throw new Error(this.messages.errors.fieldBindingRequired);
2544
+ }
2545
+ const requestId = createRandomId();
2546
+ const promise = new Promise((resolve, reject) => {
2547
+ const timeoutId = window.setTimeout(() => {
2548
+ this.pendingInlineMediaSaves.delete(requestId);
2549
+ reject(new Error(this.messages.errors.savingTimedOut));
2550
+ }, 60000);
2551
+ this.pendingInlineMediaSaves.set(requestId, {
2552
+ resolve,
2553
+ reject,
2554
+ timeoutId,
2555
+ });
2556
+ });
2557
+ this.bridgeFrame.contentWindow.postMessage({
2558
+ source: EINBLICK_PARENT_SOURCE,
2559
+ type: 'replace-inline-media',
2560
+ nonce: this.bridgeNonce,
2561
+ requestId,
2562
+ payload: {
2563
+ sourceType: binding.sourceType ?? 'cms',
2564
+ resourceSlug: binding.resourceSlug,
2565
+ recordId: binding.recordId,
2566
+ fieldKey: binding.fieldKey,
2567
+ file,
2568
+ },
2569
+ }, this.appOrigin);
2570
+ return await promise;
2571
+ }
2572
+ applyMediaPreviewToElement(element, file) {
2573
+ if (!file.type.startsWith('image/')) {
2574
+ return;
2575
+ }
2576
+ const image = element instanceof HTMLImageElement
2577
+ ? element
2578
+ : element.querySelector('img');
2579
+ if (!image) {
2580
+ return;
2581
+ }
2582
+ const objectUrl = URL.createObjectURL(file);
2583
+ const previousUrl = image.src;
2584
+ image.addEventListener('load', () => {
2585
+ URL.revokeObjectURL(objectUrl);
2586
+ }, { once: true });
2587
+ image.addEventListener('error', () => {
2588
+ image.src = previousUrl;
2589
+ URL.revokeObjectURL(objectUrl);
2590
+ }, { once: true });
2591
+ image.src = objectUrl;
2592
+ }
2087
2593
  async saveInlineField(binding, value) {
2088
2594
  if (!this.bridgeFrame?.contentWindow) {
2089
2595
  throw new Error(this.messages.errors.bridgeNotConnected);
@@ -2108,6 +2614,7 @@ class EinblickEditRuntime {
2108
2614
  nonce: this.bridgeNonce,
2109
2615
  requestId,
2110
2616
  payload: {
2617
+ sourceType: binding.sourceType ?? 'cms',
2111
2618
  resourceSlug: binding.resourceSlug,
2112
2619
  recordId: binding.recordId,
2113
2620
  fieldKey: binding.fieldKey,
@@ -2176,6 +2683,8 @@ class EinblickEditRuntime {
2176
2683
  nonce,
2177
2684
  intent: args.intent,
2178
2685
  recordId: args.recordId,
2686
+ sourceType: args.sourceType,
2687
+ resourceSlug: args.resourceSlug,
2179
2688
  });
2180
2689
  }
2181
2690
  catch (error) {
@@ -2188,6 +2697,7 @@ class EinblickEditRuntime {
2188
2697
  this.editorTarget = null;
2189
2698
  this.editorBinding = args.resourceSlug
2190
2699
  ? {
2700
+ sourceType: args.sourceType ?? 'cms',
2191
2701
  resourceSlug: args.resourceSlug,
2192
2702
  recordId: args.recordId ?? '',
2193
2703
  resourceMode: 'collection',
@@ -2259,6 +2769,7 @@ export function createEinblickEditRuntime(options) {
2259
2769
  login: (loginOptions) => runtime.login(loginOptions),
2260
2770
  openEditor: (binding, element) => runtime.openEditor(binding, element),
2261
2771
  setLocale: (locale) => runtime.setLocale(locale),
2772
+ setResourceHints: (hints) => runtime.setResourceHints(hints),
2262
2773
  };
2263
2774
  }
2264
2775
  //# sourceMappingURL=runtime.js.map