@einblick/sdk 0.3.4 → 0.3.6

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.
@@ -0,0 +1,985 @@
1
+ import { EINBLICK_AUTH_SOURCE, EINBLICK_BRIDGE_SOURCE, EINBLICK_EDITOR_SOURCE, EINBLICK_PARENT_SOURCE, formatValueForText, getImageUrl, isEditSessionExpired, readBindingFromElement, resolveAppOrigin, serializeBindingValue, shouldUseInlineEditor, } from './shared.js';
2
+ const RUNTIME_STYLE_ID = 'einblick-edit-runtime-styles';
3
+ const RELATIVE_POSITION_SENTINEL = '__einblick_runtime_original_static__';
4
+ function createRandomId() {
5
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
6
+ return crypto.randomUUID();
7
+ }
8
+ return Math.random().toString(36).slice(2);
9
+ }
10
+ function injectRuntimeStyles() {
11
+ if (typeof document === 'undefined') {
12
+ return;
13
+ }
14
+ if (document.getElementById(RUNTIME_STYLE_ID)) {
15
+ return;
16
+ }
17
+ const style = document.createElement('style');
18
+ style.id = RUNTIME_STYLE_ID;
19
+ style.textContent = `
20
+ [data-einblick-editable="true"] {
21
+ scroll-margin-top: 96px;
22
+ }
23
+
24
+ [data-einblick-editable-active="true"] {
25
+ outline: 2px solid rgba(15, 118, 110, 0.92);
26
+ outline-offset: 4px;
27
+ }
28
+
29
+ [data-einblick-edit-button] {
30
+ position: absolute;
31
+ top: 0.5rem;
32
+ right: 0.5rem;
33
+ z-index: 2147483644;
34
+ border: 1px solid rgba(15, 118, 110, 0.92);
35
+ background: rgba(255, 255, 255, 0.96);
36
+ color: rgb(17, 24, 39);
37
+ border-radius: 999px;
38
+ padding: 0.3rem 0.7rem;
39
+ font: 600 12px/1 system-ui, sans-serif;
40
+ letter-spacing: 0.01em;
41
+ cursor: pointer;
42
+ box-shadow: 0 8px 20px rgba(15, 23, 42, 0.16);
43
+ }
44
+
45
+ [data-einblick-edit-button]:hover {
46
+ background: rgb(15, 118, 110);
47
+ color: rgb(255, 255, 255);
48
+ }
49
+
50
+ [data-einblick-edit-backdrop] {
51
+ position: fixed;
52
+ inset: 0;
53
+ z-index: 2147483645;
54
+ background: rgba(15, 23, 42, 0.38);
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ padding: 1.5rem;
59
+ }
60
+
61
+ [data-einblick-edit-panel] {
62
+ width: min(560px, 100%);
63
+ max-height: min(88vh, 920px);
64
+ overflow: auto;
65
+ border-radius: 1.25rem;
66
+ background: rgb(255, 255, 255);
67
+ color: rgb(15, 23, 42);
68
+ box-shadow: 0 24px 64px rgba(15, 23, 42, 0.3);
69
+ border: 1px solid rgba(148, 163, 184, 0.4);
70
+ }
71
+
72
+ [data-einblick-edit-header] {
73
+ padding: 1.1rem 1.2rem 0;
74
+ }
75
+
76
+ [data-einblick-edit-body] {
77
+ padding: 1rem 1.2rem 1.2rem;
78
+ }
79
+
80
+ [data-einblick-edit-label] {
81
+ display: block;
82
+ margin-bottom: 0.45rem;
83
+ font: 600 0.95rem/1.4 system-ui, sans-serif;
84
+ }
85
+
86
+ [data-einblick-edit-input],
87
+ [data-einblick-edit-textarea],
88
+ [data-einblick-edit-select] {
89
+ width: 100%;
90
+ border-radius: 0.9rem;
91
+ border: 1px solid rgba(148, 163, 184, 0.6);
92
+ background: rgb(255, 255, 255);
93
+ padding: 0.75rem 0.9rem;
94
+ font: 400 0.95rem/1.5 system-ui, sans-serif;
95
+ color: inherit;
96
+ }
97
+
98
+ [data-einblick-edit-textarea] {
99
+ min-height: 10rem;
100
+ resize: vertical;
101
+ }
102
+
103
+ [data-einblick-edit-actions] {
104
+ display: flex;
105
+ flex-wrap: wrap;
106
+ justify-content: flex-end;
107
+ gap: 0.75rem;
108
+ margin-top: 1rem;
109
+ }
110
+
111
+ [data-einblick-edit-action] {
112
+ border-radius: 999px;
113
+ border: 1px solid rgba(148, 163, 184, 0.6);
114
+ background: rgb(255, 255, 255);
115
+ padding: 0.65rem 1rem;
116
+ font: 600 0.9rem/1 system-ui, sans-serif;
117
+ cursor: pointer;
118
+ }
119
+
120
+ [data-einblick-edit-action="primary"] {
121
+ border-color: rgb(15, 118, 110);
122
+ background: rgb(15, 118, 110);
123
+ color: rgb(255, 255, 255);
124
+ }
125
+
126
+ [data-einblick-edit-hint],
127
+ [data-einblick-edit-error] {
128
+ margin-top: 0.75rem;
129
+ font: 400 0.85rem/1.5 system-ui, sans-serif;
130
+ }
131
+
132
+ [data-einblick-edit-hint] {
133
+ color: rgb(71, 85, 105);
134
+ }
135
+
136
+ [data-einblick-edit-error] {
137
+ color: rgb(185, 28, 28);
138
+ }
139
+
140
+ [data-einblick-editor-frame] {
141
+ width: min(1180px, 100%);
142
+ height: min(88vh, 940px);
143
+ border: 0;
144
+ border-radius: 1.25rem;
145
+ background: rgb(255, 255, 255);
146
+ box-shadow: 0 24px 64px rgba(15, 23, 42, 0.32);
147
+ }
148
+ `;
149
+ document.head.append(style);
150
+ }
151
+ function normalizeDateValue(value) {
152
+ if (!value) {
153
+ return '';
154
+ }
155
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
156
+ return value;
157
+ }
158
+ return value.slice(0, 10);
159
+ }
160
+ class EinblickEditRuntime {
161
+ siteKey;
162
+ appOrigin;
163
+ callbacks;
164
+ bridgeNonce = createRandomId();
165
+ session;
166
+ state = {
167
+ enabled: true,
168
+ active: true,
169
+ isAuthenticated: false,
170
+ status: 'connecting',
171
+ };
172
+ bridgeFrame = null;
173
+ bridgePingInterval = null;
174
+ activeElement = null;
175
+ hoverButton = null;
176
+ mutationObserver = null;
177
+ destroyed = false;
178
+ started = false;
179
+ inlineEditorBackdrop = null;
180
+ currentInlineTarget = null;
181
+ currentInlineBinding = null;
182
+ editorPopup = null;
183
+ editorPopupPollId = null;
184
+ editorNonce = null;
185
+ editorTarget = null;
186
+ editorBinding = null;
187
+ pendingInlineSaves = new Map();
188
+ pendingLogin = null;
189
+ constructor(options) {
190
+ this.siteKey = options.siteKey;
191
+ this.appOrigin = resolveAppOrigin(options.appOrigin, options.appUrl);
192
+ this.session =
193
+ options.initialSession && !isEditSessionExpired(options.initialSession)
194
+ ? options.initialSession
195
+ : null;
196
+ this.callbacks = {
197
+ onStateChange: options.onStateChange,
198
+ onSessionChange: options.onSessionChange,
199
+ onSave: options.onSave,
200
+ onError: options.onError,
201
+ };
202
+ }
203
+ start() {
204
+ if (this.started || typeof window === 'undefined' || typeof document === 'undefined') {
205
+ return;
206
+ }
207
+ this.started = true;
208
+ injectRuntimeStyles();
209
+ this.ensureHoverButton();
210
+ window.addEventListener('message', this.handleMessage);
211
+ window.addEventListener('pointermove', this.handlePointerMove, true);
212
+ window.addEventListener('scroll', this.handleViewportChange, true);
213
+ window.addEventListener('resize', this.handleViewportChange);
214
+ window.addEventListener('blur', this.handleWindowBlur);
215
+ window.addEventListener('keydown', this.handleKeyDown, true);
216
+ this.mutationObserver = new MutationObserver(() => {
217
+ if (this.activeElement && !this.activeElement.isConnected) {
218
+ this.setActiveElement(null);
219
+ }
220
+ if (this.editorTarget && !this.editorTarget.isConnected) {
221
+ this.editorTarget = null;
222
+ }
223
+ if (this.currentInlineTarget && !this.currentInlineTarget.isConnected) {
224
+ this.closeInlineEditor();
225
+ }
226
+ });
227
+ this.mutationObserver.observe(document.body, {
228
+ childList: true,
229
+ subtree: true,
230
+ });
231
+ this.reloadBridge();
232
+ }
233
+ destroy() {
234
+ if (this.destroyed) {
235
+ return;
236
+ }
237
+ this.destroyed = true;
238
+ this.started = false;
239
+ window.removeEventListener('message', this.handleMessage);
240
+ window.removeEventListener('pointermove', this.handlePointerMove, true);
241
+ window.removeEventListener('scroll', this.handleViewportChange, true);
242
+ window.removeEventListener('resize', this.handleViewportChange);
243
+ window.removeEventListener('blur', this.handleWindowBlur);
244
+ window.removeEventListener('keydown', this.handleKeyDown, true);
245
+ this.mutationObserver?.disconnect();
246
+ this.mutationObserver = null;
247
+ this.clearBridgePing();
248
+ this.bridgeFrame?.remove();
249
+ this.bridgeFrame = null;
250
+ this.setActiveElement(null);
251
+ this.hoverButton?.remove();
252
+ this.hoverButton = null;
253
+ this.closeInlineEditor();
254
+ this.closeDrawer();
255
+ if (this.pendingLogin) {
256
+ if (this.pendingLogin.closePollId !== null) {
257
+ window.clearInterval(this.pendingLogin.closePollId);
258
+ }
259
+ this.pendingLogin.resolve(false);
260
+ this.pendingLogin = null;
261
+ }
262
+ for (const pending of this.pendingInlineSaves.values()) {
263
+ window.clearTimeout(pending.timeoutId);
264
+ pending.reject(new Error('Einblick editing was interrupted'));
265
+ }
266
+ this.pendingInlineSaves.clear();
267
+ this.setState({
268
+ enabled: true,
269
+ active: false,
270
+ isAuthenticated: false,
271
+ status: 'idle',
272
+ siteName: undefined,
273
+ errorMessage: undefined,
274
+ });
275
+ }
276
+ async login(options) {
277
+ if (typeof window === 'undefined') {
278
+ return false;
279
+ }
280
+ const returnTo = options?.returnTo ?? window.location.href;
281
+ const loginNonce = createRandomId();
282
+ const loginUrl = this.buildBridgeUrl({
283
+ mode: 'login',
284
+ nonce: loginNonce,
285
+ returnTo,
286
+ });
287
+ if (this.pendingLogin) {
288
+ return false;
289
+ }
290
+ return await new Promise((resolve) => {
291
+ let popup = null;
292
+ try {
293
+ popup = window.open(loginUrl, 'einblick-edit-login', 'popup=yes,width=680,height=820,resizable=yes,scrollbars=yes');
294
+ }
295
+ catch {
296
+ popup = null;
297
+ }
298
+ if (!popup) {
299
+ window.location.assign(loginUrl);
300
+ resolve(false);
301
+ return;
302
+ }
303
+ popup.focus();
304
+ const closePollId = window.setInterval(() => {
305
+ if (!popup || popup.closed) {
306
+ window.clearInterval(closePollId);
307
+ if (this.pendingLogin?.nonce === loginNonce) {
308
+ this.pendingLogin.resolve(false);
309
+ this.pendingLogin = null;
310
+ }
311
+ }
312
+ }, 400);
313
+ this.pendingLogin = {
314
+ nonce: loginNonce,
315
+ popup,
316
+ resolve,
317
+ closePollId,
318
+ };
319
+ });
320
+ }
321
+ openEditor(binding, element) {
322
+ const target = element ?? this.findElementForBinding(binding);
323
+ if (shouldUseInlineEditor(binding)) {
324
+ this.openInlineEditor(target, binding);
325
+ return;
326
+ }
327
+ this.openDrawer(target, binding);
328
+ }
329
+ handlePointerMove = (event) => {
330
+ if (!this.state.isAuthenticated || this.inlineEditorBackdrop || this.editorPopup) {
331
+ this.setActiveElement(null);
332
+ return;
333
+ }
334
+ const target = this.findEditableTarget(event.target);
335
+ this.setActiveElement(target);
336
+ };
337
+ handleViewportChange = () => {
338
+ if (this.activeElement && !this.activeElement.isConnected) {
339
+ this.setActiveElement(null);
340
+ }
341
+ };
342
+ handleWindowBlur = () => {
343
+ if (!this.inlineEditorBackdrop && !this.editorPopup) {
344
+ this.setActiveElement(null);
345
+ }
346
+ };
347
+ handleKeyDown = (event) => {
348
+ if (event.key !== 'Escape') {
349
+ return;
350
+ }
351
+ if (this.inlineEditorBackdrop) {
352
+ event.preventDefault();
353
+ this.closeInlineEditor();
354
+ return;
355
+ }
356
+ if (this.editorPopup) {
357
+ event.preventDefault();
358
+ this.closeDrawer();
359
+ return;
360
+ }
361
+ this.setActiveElement(null);
362
+ };
363
+ handleHoverButtonClick = (event) => {
364
+ event.preventDefault();
365
+ event.stopPropagation();
366
+ if (!this.activeElement) {
367
+ return;
368
+ }
369
+ const binding = readBindingFromElement(this.activeElement);
370
+ if (!binding) {
371
+ return;
372
+ }
373
+ this.openEditor(binding, this.activeElement);
374
+ };
375
+ handleMessage = (event) => {
376
+ if (event.origin !== this.appOrigin) {
377
+ return;
378
+ }
379
+ const data = typeof event.data === 'object' && event.data !== null
380
+ ? event.data
381
+ : null;
382
+ if (!data || typeof data.source !== 'string' || typeof data.type !== 'string') {
383
+ return;
384
+ }
385
+ if (data.source === EINBLICK_BRIDGE_SOURCE) {
386
+ this.handleBridgeMessage(data);
387
+ return;
388
+ }
389
+ if (data.source === EINBLICK_AUTH_SOURCE) {
390
+ this.handleAuthMessage(data);
391
+ return;
392
+ }
393
+ if (data.source === EINBLICK_EDITOR_SOURCE) {
394
+ this.handleEditorMessage(data);
395
+ }
396
+ };
397
+ handleBridgeMessage(message) {
398
+ if (message.nonce !== this.bridgeNonce) {
399
+ return;
400
+ }
401
+ if (message.type === 'bridge-ready') {
402
+ this.clearBridgePing();
403
+ if (message.authenticated) {
404
+ this.setState({
405
+ enabled: true,
406
+ active: true,
407
+ isAuthenticated: true,
408
+ status: 'ready',
409
+ siteName: message.siteName,
410
+ errorMessage: undefined,
411
+ });
412
+ return;
413
+ }
414
+ if (this.session) {
415
+ this.setSession(null);
416
+ }
417
+ const nextStatus = message.errorCode === 'unauthenticated' ? 'unauthenticated' : 'error';
418
+ this.setState({
419
+ enabled: true,
420
+ active: true,
421
+ isAuthenticated: false,
422
+ status: nextStatus,
423
+ siteName: message.siteName,
424
+ errorMessage: message.message,
425
+ });
426
+ if (message.message) {
427
+ this.callbacks.onError?.(message.message);
428
+ }
429
+ return;
430
+ }
431
+ const pending = this.pendingInlineSaves.get(message.requestId);
432
+ if (!pending) {
433
+ return;
434
+ }
435
+ this.pendingInlineSaves.delete(message.requestId);
436
+ window.clearTimeout(pending.timeoutId);
437
+ if (message.ok) {
438
+ pending.resolve(message.value);
439
+ return;
440
+ }
441
+ const error = new Error(message.error || 'Failed to save field');
442
+ pending.reject(error);
443
+ this.callbacks.onError?.(error.message);
444
+ }
445
+ handleAuthMessage(message) {
446
+ if (!this.pendingLogin || message.nonce !== this.pendingLogin.nonce) {
447
+ return;
448
+ }
449
+ if (this.pendingLogin.closePollId !== null) {
450
+ window.clearInterval(this.pendingLogin.closePollId);
451
+ }
452
+ const pending = this.pendingLogin;
453
+ this.pendingLogin = null;
454
+ if (message.ok) {
455
+ if (!message.sessionToken) {
456
+ const errorMessage = 'Einblick sign-in did not return an edit session';
457
+ this.setState({
458
+ ...this.state,
459
+ errorMessage,
460
+ status: 'error',
461
+ });
462
+ this.callbacks.onError?.(errorMessage);
463
+ pending.resolve(false);
464
+ return;
465
+ }
466
+ this.setSession({
467
+ sessionToken: message.sessionToken,
468
+ expiresAt: message.expiresAt,
469
+ });
470
+ try {
471
+ pending.popup?.close();
472
+ }
473
+ catch { }
474
+ this.reloadBridge();
475
+ pending.resolve(true);
476
+ return;
477
+ }
478
+ const errorMessage = message.message || 'Einblick sign-in failed';
479
+ this.setState({
480
+ ...this.state,
481
+ errorMessage,
482
+ status: 'error',
483
+ });
484
+ this.callbacks.onError?.(errorMessage);
485
+ pending.resolve(false);
486
+ }
487
+ handleEditorMessage(message) {
488
+ if (!this.editorNonce || message.nonce !== this.editorNonce) {
489
+ return;
490
+ }
491
+ if (message.type === 'editor-closed') {
492
+ this.closeDrawer(false);
493
+ return;
494
+ }
495
+ if (message.type === 'editor-saved') {
496
+ if (this.editorBinding?.fieldKey) {
497
+ const nextValue = message.fields[this.editorBinding.fieldKey];
498
+ if (this.editorTarget && nextValue !== undefined) {
499
+ this.applyValueToElement(this.editorTarget, this.editorBinding, nextValue);
500
+ }
501
+ void this.callbacks.onSave?.({
502
+ binding: this.editorBinding,
503
+ value: nextValue,
504
+ source: 'drawer',
505
+ });
506
+ }
507
+ this.closeDrawer(false);
508
+ }
509
+ }
510
+ setSession(session) {
511
+ this.session = session && !isEditSessionExpired(session) ? session : null;
512
+ this.callbacks.onSessionChange(this.session);
513
+ }
514
+ getActiveSessionToken() {
515
+ if (!this.session) {
516
+ return null;
517
+ }
518
+ if (isEditSessionExpired(this.session)) {
519
+ this.setSession(null);
520
+ return null;
521
+ }
522
+ return this.session.sessionToken;
523
+ }
524
+ setState(nextState) {
525
+ this.state = nextState;
526
+ this.callbacks.onStateChange(nextState);
527
+ if (!nextState.isAuthenticated) {
528
+ this.setActiveElement(null);
529
+ }
530
+ }
531
+ ensureHoverButton() {
532
+ if (this.hoverButton || typeof document === 'undefined') {
533
+ return;
534
+ }
535
+ const button = document.createElement('button');
536
+ button.type = 'button';
537
+ button.textContent = 'Edit';
538
+ button.setAttribute('data-einblick-edit-button', 'true');
539
+ button.hidden = true;
540
+ button.addEventListener('click', this.handleHoverButtonClick);
541
+ this.hoverButton = button;
542
+ }
543
+ setActiveElement(element) {
544
+ if (this.activeElement === element) {
545
+ if (this.hoverButton && element && !element.contains(this.hoverButton)) {
546
+ element.append(this.hoverButton);
547
+ }
548
+ return;
549
+ }
550
+ if (this.activeElement) {
551
+ this.activeElement.removeAttribute('data-einblick-editable-active');
552
+ const originalPosition = this.activeElement.dataset.einblickRuntimeOriginalPosition;
553
+ if (originalPosition !== undefined) {
554
+ this.activeElement.style.position =
555
+ originalPosition === RELATIVE_POSITION_SENTINEL ? '' : originalPosition;
556
+ delete this.activeElement.dataset.einblickRuntimeOriginalPosition;
557
+ }
558
+ }
559
+ this.activeElement = element;
560
+ if (!element || !this.hoverButton || !this.state.isAuthenticated) {
561
+ if (this.hoverButton) {
562
+ this.hoverButton.hidden = true;
563
+ this.hoverButton.remove();
564
+ }
565
+ return;
566
+ }
567
+ element.setAttribute('data-einblick-editable-active', 'true');
568
+ if (window.getComputedStyle(element).position === 'static') {
569
+ element.dataset.einblickRuntimeOriginalPosition =
570
+ element.style.position || RELATIVE_POSITION_SENTINEL;
571
+ element.style.position = 'relative';
572
+ }
573
+ this.hoverButton.hidden = false;
574
+ element.append(this.hoverButton);
575
+ }
576
+ reloadBridge() {
577
+ this.clearBridgePing();
578
+ this.bridgeFrame?.remove();
579
+ this.bridgeFrame = null;
580
+ this.setState({
581
+ enabled: true,
582
+ active: true,
583
+ isAuthenticated: false,
584
+ status: 'connecting',
585
+ siteName: this.state.siteName,
586
+ errorMessage: undefined,
587
+ });
588
+ const frame = document.createElement('iframe');
589
+ frame.hidden = true;
590
+ frame.tabIndex = -1;
591
+ frame.setAttribute('aria-hidden', 'true');
592
+ frame.src = this.buildBridgeUrl({ mode: 'bridge', nonce: this.bridgeNonce });
593
+ this.bridgeFrame = frame;
594
+ document.body.append(frame);
595
+ const sendPing = () => {
596
+ if (!this.bridgeFrame?.contentWindow) {
597
+ return;
598
+ }
599
+ try {
600
+ this.bridgeFrame.contentWindow.postMessage({
601
+ source: EINBLICK_PARENT_SOURCE,
602
+ type: 'bridge-ping',
603
+ nonce: this.bridgeNonce,
604
+ }, this.appOrigin);
605
+ }
606
+ catch { }
607
+ };
608
+ frame.addEventListener('load', () => {
609
+ sendPing();
610
+ });
611
+ this.bridgePingInterval = window.setInterval(sendPing, 1500);
612
+ }
613
+ clearBridgePing() {
614
+ if (this.bridgePingInterval !== null) {
615
+ window.clearInterval(this.bridgePingInterval);
616
+ this.bridgePingInterval = null;
617
+ }
618
+ }
619
+ buildBridgeUrl(args) {
620
+ const url = new URL('/sdk/bridge', this.appOrigin);
621
+ url.searchParams.set('siteKey', this.siteKey);
622
+ url.searchParams.set('origin', window.location.origin);
623
+ url.searchParams.set('nonce', args.nonce);
624
+ if (args.mode !== 'bridge') {
625
+ url.searchParams.set('mode', args.mode);
626
+ }
627
+ const sessionToken = args.mode === 'bridge' ? this.getActiveSessionToken() : null;
628
+ if (sessionToken) {
629
+ url.searchParams.set('sessionToken', sessionToken);
630
+ }
631
+ if (args.returnTo) {
632
+ url.searchParams.set('returnTo', args.returnTo);
633
+ }
634
+ return url.toString();
635
+ }
636
+ buildEditorUrl(binding, nonce) {
637
+ const sessionToken = this.getActiveSessionToken();
638
+ if (!sessionToken) {
639
+ throw new Error('Your edit session expired. Sign in again to continue.');
640
+ }
641
+ const url = new URL('/sdk/editor', this.appOrigin);
642
+ url.searchParams.set('origin', window.location.origin);
643
+ url.searchParams.set('nonce', nonce);
644
+ url.searchParams.set('sessionToken', sessionToken);
645
+ url.searchParams.set('returnTo', window.location.href);
646
+ url.searchParams.set('resourceSlug', binding.resourceSlug);
647
+ url.searchParams.set('recordId', binding.recordId);
648
+ if (binding.fieldKey) {
649
+ url.searchParams.set('fieldKey', binding.fieldKey);
650
+ }
651
+ return url.toString();
652
+ }
653
+ findEditableTarget(target) {
654
+ if (!(target instanceof Element)) {
655
+ return null;
656
+ }
657
+ const editableTarget = target.closest('[data-einblick-editable="true"]');
658
+ return editableTarget ?? null;
659
+ }
660
+ findElementForBinding(binding) {
661
+ const selector = [
662
+ '[data-einblick-editable="true"]',
663
+ `[data-einblick-resource="${CSS.escape(binding.resourceSlug)}"]`,
664
+ `[data-einblick-record-id="${CSS.escape(binding.recordId)}"]`,
665
+ binding.fieldKey
666
+ ? `[data-einblick-field-key="${CSS.escape(binding.fieldKey)}"]`
667
+ : '',
668
+ ].join('');
669
+ return document.querySelector(selector);
670
+ }
671
+ openInlineEditor(target, binding) {
672
+ if (!this.state.isAuthenticated) {
673
+ this.callbacks.onError?.('Sign in with Einblick to edit this field');
674
+ return;
675
+ }
676
+ this.closeDrawer();
677
+ this.closeInlineEditor();
678
+ this.setActiveElement(null);
679
+ const backdrop = document.createElement('div');
680
+ backdrop.setAttribute('data-einblick-edit-backdrop', 'true');
681
+ const panel = document.createElement('div');
682
+ panel.setAttribute('data-einblick-edit-panel', 'true');
683
+ const header = document.createElement('div');
684
+ header.setAttribute('data-einblick-edit-header', 'true');
685
+ const title = document.createElement('h2');
686
+ title.textContent = binding.label || binding.fieldKey || 'Edit content';
687
+ title.style.font = '600 1.1rem/1.3 system-ui, sans-serif';
688
+ const subTitle = document.createElement('p');
689
+ subTitle.textContent =
690
+ binding.fieldKey ? `${binding.resourceSlug} / ${binding.fieldKey}` : binding.resourceSlug;
691
+ subTitle.style.marginTop = '0.35rem';
692
+ subTitle.style.color = 'rgb(71, 85, 105)';
693
+ subTitle.style.font = '400 0.85rem/1.4 system-ui, sans-serif';
694
+ header.append(title, subTitle);
695
+ const body = document.createElement('div');
696
+ body.setAttribute('data-einblick-edit-body', 'true');
697
+ const label = document.createElement('label');
698
+ label.setAttribute('data-einblick-edit-label', 'true');
699
+ label.textContent = 'Value';
700
+ const control = this.createInlineControl(binding);
701
+ this.setControlValue(control, binding, binding.value);
702
+ const errorMessage = document.createElement('div');
703
+ errorMessage.setAttribute('data-einblick-edit-error', 'true');
704
+ errorMessage.hidden = true;
705
+ const hint = document.createElement('div');
706
+ hint.setAttribute('data-einblick-edit-hint', 'true');
707
+ hint.textContent =
708
+ shouldUseInlineEditor(binding)
709
+ ? 'Changes are saved directly and the page refreshes afterward.'
710
+ : 'Use the drawer for this field.';
711
+ const actions = document.createElement('div');
712
+ actions.setAttribute('data-einblick-edit-actions', 'true');
713
+ const cancelButton = document.createElement('button');
714
+ cancelButton.type = 'button';
715
+ cancelButton.textContent = 'Cancel';
716
+ cancelButton.setAttribute('data-einblick-edit-action', 'secondary');
717
+ cancelButton.addEventListener('click', () => this.closeInlineEditor());
718
+ const drawerButton = document.createElement('button');
719
+ drawerButton.type = 'button';
720
+ drawerButton.textContent = 'Open drawer';
721
+ drawerButton.setAttribute('data-einblick-edit-action', 'secondary');
722
+ drawerButton.addEventListener('click', () => {
723
+ this.closeInlineEditor();
724
+ this.openDrawer(target, binding);
725
+ });
726
+ const saveButton = document.createElement('button');
727
+ saveButton.type = 'button';
728
+ saveButton.textContent = 'Save';
729
+ saveButton.setAttribute('data-einblick-edit-action', 'primary');
730
+ saveButton.addEventListener('click', async () => {
731
+ errorMessage.hidden = true;
732
+ errorMessage.textContent = '';
733
+ saveButton.disabled = true;
734
+ saveButton.textContent = 'Saving...';
735
+ try {
736
+ const nextValue = this.getControlValue(control, binding);
737
+ const savedValue = await this.saveInlineField(binding, nextValue);
738
+ if (target) {
739
+ this.applyValueToElement(target, binding, savedValue);
740
+ }
741
+ this.closeInlineEditor();
742
+ await this.callbacks.onSave?.({
743
+ binding,
744
+ value: savedValue,
745
+ source: 'inline',
746
+ });
747
+ }
748
+ catch (error) {
749
+ errorMessage.hidden = false;
750
+ errorMessage.textContent =
751
+ error instanceof Error ? error.message : 'Failed to save field';
752
+ saveButton.disabled = false;
753
+ saveButton.textContent = 'Save';
754
+ }
755
+ });
756
+ actions.append(cancelButton, drawerButton, saveButton);
757
+ body.append(label, control, errorMessage, hint, actions);
758
+ panel.append(header, body);
759
+ backdrop.append(panel);
760
+ backdrop.addEventListener('click', (event) => {
761
+ if (event.target === backdrop) {
762
+ this.closeInlineEditor();
763
+ }
764
+ });
765
+ document.body.append(backdrop);
766
+ this.inlineEditorBackdrop = backdrop;
767
+ this.currentInlineTarget = target;
768
+ this.currentInlineBinding = binding;
769
+ window.setTimeout(() => {
770
+ if ('focus' in control && typeof control.focus === 'function') {
771
+ control.focus();
772
+ }
773
+ }, 0);
774
+ }
775
+ closeInlineEditor() {
776
+ this.inlineEditorBackdrop?.remove();
777
+ this.inlineEditorBackdrop = null;
778
+ this.currentInlineTarget = null;
779
+ this.currentInlineBinding = null;
780
+ }
781
+ createInlineControl(binding) {
782
+ switch (binding.fieldType) {
783
+ case 'text':
784
+ case 'markdown': {
785
+ const textarea = document.createElement('textarea');
786
+ textarea.setAttribute('data-einblick-edit-textarea', 'true');
787
+ return textarea;
788
+ }
789
+ case 'boolean': {
790
+ const select = document.createElement('select');
791
+ select.setAttribute('data-einblick-edit-select', 'true');
792
+ const trueOption = document.createElement('option');
793
+ trueOption.value = 'true';
794
+ trueOption.textContent = 'True';
795
+ const falseOption = document.createElement('option');
796
+ falseOption.value = 'false';
797
+ falseOption.textContent = 'False';
798
+ select.append(trueOption, falseOption);
799
+ return select;
800
+ }
801
+ default: {
802
+ const input = document.createElement('input');
803
+ input.setAttribute('data-einblick-edit-input', 'true');
804
+ if (binding.fieldType === 'number') {
805
+ input.type = 'number';
806
+ }
807
+ else if (binding.fieldType === 'date') {
808
+ input.type = 'date';
809
+ }
810
+ else {
811
+ input.type = 'text';
812
+ }
813
+ return input;
814
+ }
815
+ }
816
+ }
817
+ setControlValue(control, binding, value) {
818
+ const currentValue = value ?? binding.value;
819
+ if (control instanceof HTMLSelectElement) {
820
+ control.value = currentValue === true ? 'true' : 'false';
821
+ return;
822
+ }
823
+ if (binding.fieldType === 'tags') {
824
+ control.value = Array.isArray(currentValue)
825
+ ? currentValue
826
+ .map((entry) => (typeof entry === 'string' ? entry : String(entry)))
827
+ .join(', ')
828
+ : currentValue === undefined || currentValue === null
829
+ ? ''
830
+ : String(currentValue);
831
+ return;
832
+ }
833
+ if (binding.fieldType === 'date') {
834
+ control.value =
835
+ typeof currentValue === 'string' ? normalizeDateValue(currentValue) : '';
836
+ return;
837
+ }
838
+ control.value =
839
+ currentValue === undefined || currentValue === null
840
+ ? ''
841
+ : typeof currentValue === 'string'
842
+ ? currentValue
843
+ : String(currentValue);
844
+ }
845
+ getControlValue(control, binding) {
846
+ const rawValue = control.value;
847
+ switch (binding.fieldType) {
848
+ case 'number':
849
+ return rawValue.trim().length === 0 ? null : Number(rawValue);
850
+ case 'boolean':
851
+ return rawValue === 'true';
852
+ case 'tags':
853
+ return rawValue
854
+ .split(',')
855
+ .map((entry) => entry.trim())
856
+ .filter(Boolean);
857
+ default:
858
+ return rawValue;
859
+ }
860
+ }
861
+ async saveInlineField(binding, value) {
862
+ if (!this.bridgeFrame?.contentWindow) {
863
+ throw new Error('Einblick bridge is not connected yet');
864
+ }
865
+ if (!this.state.isAuthenticated) {
866
+ throw new Error('Sign in with Einblick to edit this field');
867
+ }
868
+ if (!binding.fieldKey) {
869
+ throw new Error('Inline editing requires a field binding');
870
+ }
871
+ const requestId = createRandomId();
872
+ const promise = new Promise((resolve, reject) => {
873
+ const timeoutId = window.setTimeout(() => {
874
+ this.pendingInlineSaves.delete(requestId);
875
+ reject(new Error('Saving took too long. Please try again.'));
876
+ }, 15000);
877
+ this.pendingInlineSaves.set(requestId, {
878
+ resolve,
879
+ reject,
880
+ timeoutId,
881
+ });
882
+ });
883
+ this.bridgeFrame.contentWindow.postMessage({
884
+ source: EINBLICK_PARENT_SOURCE,
885
+ type: 'save-inline-field',
886
+ nonce: this.bridgeNonce,
887
+ requestId,
888
+ payload: {
889
+ resourceSlug: binding.resourceSlug,
890
+ recordId: binding.recordId,
891
+ fieldKey: binding.fieldKey,
892
+ value,
893
+ },
894
+ }, this.appOrigin);
895
+ return await promise;
896
+ }
897
+ openDrawer(target, binding) {
898
+ if (!this.state.isAuthenticated) {
899
+ this.callbacks.onError?.('Sign in with Einblick to edit this content');
900
+ return;
901
+ }
902
+ this.closeInlineEditor();
903
+ this.closeDrawer();
904
+ this.setActiveElement(null);
905
+ const nonce = createRandomId();
906
+ let editorUrl;
907
+ try {
908
+ editorUrl = this.buildEditorUrl(binding, nonce);
909
+ }
910
+ catch (error) {
911
+ this.callbacks.onError?.(error instanceof Error ? error.message : 'Failed to open the editor');
912
+ return;
913
+ }
914
+ let popup = null;
915
+ try {
916
+ popup = window.open(editorUrl, 'einblick-edit-editor', 'popup=yes,width=1320,height=900,resizable=yes,scrollbars=yes');
917
+ }
918
+ catch {
919
+ popup = null;
920
+ }
921
+ this.editorNonce = nonce;
922
+ this.editorTarget = target;
923
+ this.editorBinding = binding;
924
+ if (!popup) {
925
+ window.location.assign(editorUrl);
926
+ return;
927
+ }
928
+ popup.focus();
929
+ this.editorPopup = popup;
930
+ this.editorPopupPollId = window.setInterval(() => {
931
+ if (!this.editorPopup || this.editorPopup.closed) {
932
+ this.closeDrawer(false);
933
+ }
934
+ }, 400);
935
+ }
936
+ closeDrawer(closeWindow = true) {
937
+ if (this.editorPopupPollId !== null) {
938
+ window.clearInterval(this.editorPopupPollId);
939
+ this.editorPopupPollId = null;
940
+ }
941
+ if (closeWindow) {
942
+ try {
943
+ this.editorPopup?.close();
944
+ }
945
+ catch { }
946
+ }
947
+ this.editorPopup = null;
948
+ this.editorNonce = null;
949
+ this.editorTarget = null;
950
+ this.editorBinding = null;
951
+ }
952
+ applyValueToElement(element, binding, value) {
953
+ element.dataset.einblickValue = serializeBindingValue(value) ?? '';
954
+ const kind = element.dataset.einblickKind;
955
+ if (kind === 'image' || binding.fieldType === 'image' || binding.fieldType === 'file' || binding.fieldType === 'files') {
956
+ const imageUrl = getImageUrl(value);
957
+ if (!imageUrl) {
958
+ return;
959
+ }
960
+ const img = element.querySelector('img');
961
+ if (!img) {
962
+ return;
963
+ }
964
+ img.setAttribute('src', imageUrl);
965
+ if (img.hasAttribute('srcset')) {
966
+ img.setAttribute('srcset', imageUrl);
967
+ }
968
+ return;
969
+ }
970
+ if (kind !== 'text') {
971
+ return;
972
+ }
973
+ element.textContent = formatValueForText(value);
974
+ }
975
+ }
976
+ export function createEinblickEditRuntime(options) {
977
+ const runtime = new EinblickEditRuntime(options);
978
+ return {
979
+ start: () => runtime.start(),
980
+ destroy: () => runtime.destroy(),
981
+ login: (loginOptions) => runtime.login(loginOptions),
982
+ openEditor: (binding, element) => runtime.openEditor(binding, element),
983
+ };
984
+ }
985
+ //# sourceMappingURL=runtime.js.map