@einblick/sdk 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,916 @@
1
+ import { EINBLICK_AUTH_SOURCE, EINBLICK_BRIDGE_SOURCE, EINBLICK_EDITOR_SOURCE, EINBLICK_PARENT_SOURCE, formatValueForText, getImageUrl, 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
+ state = {
166
+ enabled: true,
167
+ active: true,
168
+ isAuthenticated: false,
169
+ status: 'connecting',
170
+ };
171
+ bridgeFrame = null;
172
+ bridgePingInterval = null;
173
+ activeElement = null;
174
+ hoverButton = null;
175
+ mutationObserver = null;
176
+ destroyed = false;
177
+ started = false;
178
+ inlineEditorBackdrop = null;
179
+ currentInlineTarget = null;
180
+ currentInlineBinding = null;
181
+ editorBackdrop = null;
182
+ editorNonce = null;
183
+ editorTarget = null;
184
+ editorBinding = null;
185
+ pendingInlineSaves = new Map();
186
+ pendingLogin = null;
187
+ constructor(options) {
188
+ this.siteKey = options.siteKey;
189
+ this.appOrigin = resolveAppOrigin(options.appOrigin, options.appUrl);
190
+ this.callbacks = {
191
+ onStateChange: options.onStateChange,
192
+ onSave: options.onSave,
193
+ onError: options.onError,
194
+ };
195
+ }
196
+ start() {
197
+ if (this.started || typeof window === 'undefined' || typeof document === 'undefined') {
198
+ return;
199
+ }
200
+ this.started = true;
201
+ injectRuntimeStyles();
202
+ this.ensureHoverButton();
203
+ window.addEventListener('message', this.handleMessage);
204
+ window.addEventListener('pointermove', this.handlePointerMove, true);
205
+ window.addEventListener('scroll', this.handleViewportChange, true);
206
+ window.addEventListener('resize', this.handleViewportChange);
207
+ window.addEventListener('blur', this.handleWindowBlur);
208
+ window.addEventListener('keydown', this.handleKeyDown, true);
209
+ this.mutationObserver = new MutationObserver(() => {
210
+ if (this.activeElement && !this.activeElement.isConnected) {
211
+ this.setActiveElement(null);
212
+ }
213
+ if (this.editorTarget && !this.editorTarget.isConnected) {
214
+ this.editorTarget = null;
215
+ }
216
+ if (this.currentInlineTarget && !this.currentInlineTarget.isConnected) {
217
+ this.closeInlineEditor();
218
+ }
219
+ });
220
+ this.mutationObserver.observe(document.body, {
221
+ childList: true,
222
+ subtree: true,
223
+ });
224
+ this.reloadBridge();
225
+ }
226
+ destroy() {
227
+ if (this.destroyed) {
228
+ return;
229
+ }
230
+ this.destroyed = true;
231
+ this.started = false;
232
+ window.removeEventListener('message', this.handleMessage);
233
+ window.removeEventListener('pointermove', this.handlePointerMove, true);
234
+ window.removeEventListener('scroll', this.handleViewportChange, true);
235
+ window.removeEventListener('resize', this.handleViewportChange);
236
+ window.removeEventListener('blur', this.handleWindowBlur);
237
+ window.removeEventListener('keydown', this.handleKeyDown, true);
238
+ this.mutationObserver?.disconnect();
239
+ this.mutationObserver = null;
240
+ this.clearBridgePing();
241
+ this.bridgeFrame?.remove();
242
+ this.bridgeFrame = null;
243
+ this.setActiveElement(null);
244
+ this.hoverButton?.remove();
245
+ this.hoverButton = null;
246
+ this.closeInlineEditor();
247
+ this.closeDrawer();
248
+ if (this.pendingLogin) {
249
+ if (this.pendingLogin.closePollId !== null) {
250
+ window.clearInterval(this.pendingLogin.closePollId);
251
+ }
252
+ this.pendingLogin.resolve(false);
253
+ this.pendingLogin = null;
254
+ }
255
+ for (const pending of this.pendingInlineSaves.values()) {
256
+ window.clearTimeout(pending.timeoutId);
257
+ pending.reject(new Error('Einblick editing was interrupted'));
258
+ }
259
+ this.pendingInlineSaves.clear();
260
+ this.setState({
261
+ enabled: true,
262
+ active: false,
263
+ isAuthenticated: false,
264
+ status: 'idle',
265
+ siteName: undefined,
266
+ errorMessage: undefined,
267
+ });
268
+ }
269
+ async login(options) {
270
+ if (typeof window === 'undefined') {
271
+ return false;
272
+ }
273
+ const returnTo = options?.returnTo ?? window.location.href;
274
+ const loginNonce = createRandomId();
275
+ const loginUrl = this.buildBridgeUrl({
276
+ mode: 'login',
277
+ nonce: loginNonce,
278
+ returnTo,
279
+ });
280
+ if (this.pendingLogin) {
281
+ return false;
282
+ }
283
+ return await new Promise((resolve) => {
284
+ let popup = null;
285
+ try {
286
+ popup = window.open(loginUrl, 'einblick-edit-login', 'popup=yes,width=680,height=820,resizable=yes,scrollbars=yes');
287
+ }
288
+ catch {
289
+ popup = null;
290
+ }
291
+ if (!popup) {
292
+ window.location.assign(loginUrl);
293
+ resolve(false);
294
+ return;
295
+ }
296
+ popup.focus();
297
+ const closePollId = window.setInterval(() => {
298
+ if (!popup || popup.closed) {
299
+ window.clearInterval(closePollId);
300
+ if (this.pendingLogin?.nonce === loginNonce) {
301
+ this.pendingLogin.resolve(false);
302
+ this.pendingLogin = null;
303
+ }
304
+ }
305
+ }, 400);
306
+ this.pendingLogin = {
307
+ nonce: loginNonce,
308
+ popup,
309
+ resolve,
310
+ closePollId,
311
+ };
312
+ });
313
+ }
314
+ openEditor(binding, element) {
315
+ const target = element ?? this.findElementForBinding(binding);
316
+ if (shouldUseInlineEditor(binding)) {
317
+ this.openInlineEditor(target, binding);
318
+ return;
319
+ }
320
+ this.openDrawer(target, binding);
321
+ }
322
+ handlePointerMove = (event) => {
323
+ if (!this.state.isAuthenticated || this.inlineEditorBackdrop || this.editorBackdrop) {
324
+ this.setActiveElement(null);
325
+ return;
326
+ }
327
+ const target = this.findEditableTarget(event.target);
328
+ this.setActiveElement(target);
329
+ };
330
+ handleViewportChange = () => {
331
+ if (this.activeElement && !this.activeElement.isConnected) {
332
+ this.setActiveElement(null);
333
+ }
334
+ };
335
+ handleWindowBlur = () => {
336
+ if (!this.inlineEditorBackdrop && !this.editorBackdrop) {
337
+ this.setActiveElement(null);
338
+ }
339
+ };
340
+ handleKeyDown = (event) => {
341
+ if (event.key !== 'Escape') {
342
+ return;
343
+ }
344
+ if (this.inlineEditorBackdrop) {
345
+ event.preventDefault();
346
+ this.closeInlineEditor();
347
+ return;
348
+ }
349
+ if (this.editorBackdrop) {
350
+ event.preventDefault();
351
+ this.closeDrawer();
352
+ return;
353
+ }
354
+ this.setActiveElement(null);
355
+ };
356
+ handleHoverButtonClick = (event) => {
357
+ event.preventDefault();
358
+ event.stopPropagation();
359
+ if (!this.activeElement) {
360
+ return;
361
+ }
362
+ const binding = readBindingFromElement(this.activeElement);
363
+ if (!binding) {
364
+ return;
365
+ }
366
+ this.openEditor(binding, this.activeElement);
367
+ };
368
+ handleMessage = (event) => {
369
+ if (event.origin !== this.appOrigin) {
370
+ return;
371
+ }
372
+ const data = typeof event.data === 'object' && event.data !== null
373
+ ? event.data
374
+ : null;
375
+ if (!data || typeof data.source !== 'string' || typeof data.type !== 'string') {
376
+ return;
377
+ }
378
+ if (data.source === EINBLICK_BRIDGE_SOURCE) {
379
+ this.handleBridgeMessage(data);
380
+ return;
381
+ }
382
+ if (data.source === EINBLICK_AUTH_SOURCE) {
383
+ this.handleAuthMessage(data);
384
+ return;
385
+ }
386
+ if (data.source === EINBLICK_EDITOR_SOURCE) {
387
+ this.handleEditorMessage(data);
388
+ }
389
+ };
390
+ handleBridgeMessage(message) {
391
+ if (message.nonce !== this.bridgeNonce) {
392
+ return;
393
+ }
394
+ if (message.type === 'bridge-ready') {
395
+ this.clearBridgePing();
396
+ if (message.authenticated) {
397
+ this.setState({
398
+ enabled: true,
399
+ active: true,
400
+ isAuthenticated: true,
401
+ status: 'ready',
402
+ siteName: message.siteName,
403
+ errorMessage: undefined,
404
+ });
405
+ return;
406
+ }
407
+ const nextStatus = message.errorCode === 'unauthenticated' ? 'unauthenticated' : 'error';
408
+ this.setState({
409
+ enabled: true,
410
+ active: true,
411
+ isAuthenticated: false,
412
+ status: nextStatus,
413
+ siteName: message.siteName,
414
+ errorMessage: message.message,
415
+ });
416
+ if (message.message) {
417
+ this.callbacks.onError?.(message.message);
418
+ }
419
+ return;
420
+ }
421
+ const pending = this.pendingInlineSaves.get(message.requestId);
422
+ if (!pending) {
423
+ return;
424
+ }
425
+ this.pendingInlineSaves.delete(message.requestId);
426
+ window.clearTimeout(pending.timeoutId);
427
+ if (message.ok) {
428
+ pending.resolve(message.value);
429
+ return;
430
+ }
431
+ const error = new Error(message.error || 'Failed to save field');
432
+ pending.reject(error);
433
+ this.callbacks.onError?.(error.message);
434
+ }
435
+ handleAuthMessage(message) {
436
+ if (!this.pendingLogin || message.nonce !== this.pendingLogin.nonce) {
437
+ return;
438
+ }
439
+ if (this.pendingLogin.closePollId !== null) {
440
+ window.clearInterval(this.pendingLogin.closePollId);
441
+ }
442
+ const pending = this.pendingLogin;
443
+ this.pendingLogin = null;
444
+ if (message.ok) {
445
+ try {
446
+ pending.popup?.close();
447
+ }
448
+ catch { }
449
+ this.reloadBridge();
450
+ pending.resolve(true);
451
+ return;
452
+ }
453
+ const errorMessage = message.message || 'Einblick sign-in failed';
454
+ this.setState({
455
+ ...this.state,
456
+ errorMessage,
457
+ status: 'error',
458
+ });
459
+ this.callbacks.onError?.(errorMessage);
460
+ pending.resolve(false);
461
+ }
462
+ handleEditorMessage(message) {
463
+ if (!this.editorNonce || message.nonce !== this.editorNonce) {
464
+ return;
465
+ }
466
+ if (message.type === 'editor-closed') {
467
+ this.closeDrawer();
468
+ return;
469
+ }
470
+ if (message.type === 'editor-saved') {
471
+ if (this.editorBinding?.fieldKey) {
472
+ const nextValue = message.fields[this.editorBinding.fieldKey];
473
+ if (this.editorTarget && nextValue !== undefined) {
474
+ this.applyValueToElement(this.editorTarget, this.editorBinding, nextValue);
475
+ }
476
+ void this.callbacks.onSave?.({
477
+ binding: this.editorBinding,
478
+ value: nextValue,
479
+ source: 'drawer',
480
+ });
481
+ }
482
+ this.closeDrawer();
483
+ }
484
+ }
485
+ setState(nextState) {
486
+ this.state = nextState;
487
+ this.callbacks.onStateChange(nextState);
488
+ if (!nextState.isAuthenticated) {
489
+ this.setActiveElement(null);
490
+ }
491
+ }
492
+ ensureHoverButton() {
493
+ if (this.hoverButton || typeof document === 'undefined') {
494
+ return;
495
+ }
496
+ const button = document.createElement('button');
497
+ button.type = 'button';
498
+ button.textContent = 'Edit';
499
+ button.setAttribute('data-einblick-edit-button', 'true');
500
+ button.hidden = true;
501
+ button.addEventListener('click', this.handleHoverButtonClick);
502
+ this.hoverButton = button;
503
+ }
504
+ setActiveElement(element) {
505
+ if (this.activeElement === element) {
506
+ if (this.hoverButton && element && !element.contains(this.hoverButton)) {
507
+ element.append(this.hoverButton);
508
+ }
509
+ return;
510
+ }
511
+ if (this.activeElement) {
512
+ this.activeElement.removeAttribute('data-einblick-editable-active');
513
+ const originalPosition = this.activeElement.dataset.einblickRuntimeOriginalPosition;
514
+ if (originalPosition !== undefined) {
515
+ this.activeElement.style.position =
516
+ originalPosition === RELATIVE_POSITION_SENTINEL ? '' : originalPosition;
517
+ delete this.activeElement.dataset.einblickRuntimeOriginalPosition;
518
+ }
519
+ }
520
+ this.activeElement = element;
521
+ if (!element || !this.hoverButton || !this.state.isAuthenticated) {
522
+ if (this.hoverButton) {
523
+ this.hoverButton.hidden = true;
524
+ this.hoverButton.remove();
525
+ }
526
+ return;
527
+ }
528
+ element.setAttribute('data-einblick-editable-active', 'true');
529
+ if (window.getComputedStyle(element).position === 'static') {
530
+ element.dataset.einblickRuntimeOriginalPosition =
531
+ element.style.position || RELATIVE_POSITION_SENTINEL;
532
+ element.style.position = 'relative';
533
+ }
534
+ this.hoverButton.hidden = false;
535
+ element.append(this.hoverButton);
536
+ }
537
+ reloadBridge() {
538
+ this.clearBridgePing();
539
+ this.bridgeFrame?.remove();
540
+ this.bridgeFrame = null;
541
+ this.setState({
542
+ enabled: true,
543
+ active: true,
544
+ isAuthenticated: false,
545
+ status: 'connecting',
546
+ siteName: this.state.siteName,
547
+ errorMessage: undefined,
548
+ });
549
+ const frame = document.createElement('iframe');
550
+ frame.hidden = true;
551
+ frame.tabIndex = -1;
552
+ frame.setAttribute('aria-hidden', 'true');
553
+ frame.src = this.buildBridgeUrl({ mode: 'bridge', nonce: this.bridgeNonce });
554
+ this.bridgeFrame = frame;
555
+ document.body.append(frame);
556
+ const sendPing = () => {
557
+ if (!this.bridgeFrame?.contentWindow) {
558
+ return;
559
+ }
560
+ try {
561
+ this.bridgeFrame.contentWindow.postMessage({
562
+ source: EINBLICK_PARENT_SOURCE,
563
+ type: 'bridge-ping',
564
+ nonce: this.bridgeNonce,
565
+ }, this.appOrigin);
566
+ }
567
+ catch { }
568
+ };
569
+ frame.addEventListener('load', () => {
570
+ sendPing();
571
+ });
572
+ this.bridgePingInterval = window.setInterval(sendPing, 1500);
573
+ }
574
+ clearBridgePing() {
575
+ if (this.bridgePingInterval !== null) {
576
+ window.clearInterval(this.bridgePingInterval);
577
+ this.bridgePingInterval = null;
578
+ }
579
+ }
580
+ buildBridgeUrl(args) {
581
+ const url = new URL('/sdk/bridge', this.appOrigin);
582
+ url.searchParams.set('siteKey', this.siteKey);
583
+ url.searchParams.set('origin', window.location.origin);
584
+ url.searchParams.set('nonce', args.nonce);
585
+ if (args.mode !== 'bridge') {
586
+ url.searchParams.set('mode', args.mode);
587
+ }
588
+ if (args.returnTo) {
589
+ url.searchParams.set('returnTo', args.returnTo);
590
+ }
591
+ return url.toString();
592
+ }
593
+ buildEditorUrl(binding, nonce) {
594
+ const url = new URL('/sdk/editor', this.appOrigin);
595
+ url.searchParams.set('siteKey', this.siteKey);
596
+ url.searchParams.set('origin', window.location.origin);
597
+ url.searchParams.set('nonce', nonce);
598
+ url.searchParams.set('resourceSlug', binding.resourceSlug);
599
+ url.searchParams.set('recordId', binding.recordId);
600
+ if (binding.fieldKey) {
601
+ url.searchParams.set('fieldKey', binding.fieldKey);
602
+ }
603
+ return url.toString();
604
+ }
605
+ findEditableTarget(target) {
606
+ if (!(target instanceof Element)) {
607
+ return null;
608
+ }
609
+ const editableTarget = target.closest('[data-einblick-editable="true"]');
610
+ return editableTarget ?? null;
611
+ }
612
+ findElementForBinding(binding) {
613
+ const selector = [
614
+ '[data-einblick-editable="true"]',
615
+ `[data-einblick-resource="${CSS.escape(binding.resourceSlug)}"]`,
616
+ `[data-einblick-record-id="${CSS.escape(binding.recordId)}"]`,
617
+ binding.fieldKey
618
+ ? `[data-einblick-field-key="${CSS.escape(binding.fieldKey)}"]`
619
+ : '',
620
+ ].join('');
621
+ return document.querySelector(selector);
622
+ }
623
+ openInlineEditor(target, binding) {
624
+ if (!this.state.isAuthenticated) {
625
+ this.callbacks.onError?.('Sign in with Einblick to edit this field');
626
+ return;
627
+ }
628
+ this.closeDrawer();
629
+ this.closeInlineEditor();
630
+ this.setActiveElement(null);
631
+ const backdrop = document.createElement('div');
632
+ backdrop.setAttribute('data-einblick-edit-backdrop', 'true');
633
+ const panel = document.createElement('div');
634
+ panel.setAttribute('data-einblick-edit-panel', 'true');
635
+ const header = document.createElement('div');
636
+ header.setAttribute('data-einblick-edit-header', 'true');
637
+ const title = document.createElement('h2');
638
+ title.textContent = binding.label || binding.fieldKey || 'Edit content';
639
+ title.style.font = '600 1.1rem/1.3 system-ui, sans-serif';
640
+ const subTitle = document.createElement('p');
641
+ subTitle.textContent =
642
+ binding.fieldKey ? `${binding.resourceSlug} / ${binding.fieldKey}` : binding.resourceSlug;
643
+ subTitle.style.marginTop = '0.35rem';
644
+ subTitle.style.color = 'rgb(71, 85, 105)';
645
+ subTitle.style.font = '400 0.85rem/1.4 system-ui, sans-serif';
646
+ header.append(title, subTitle);
647
+ const body = document.createElement('div');
648
+ body.setAttribute('data-einblick-edit-body', 'true');
649
+ const label = document.createElement('label');
650
+ label.setAttribute('data-einblick-edit-label', 'true');
651
+ label.textContent = 'Value';
652
+ const control = this.createInlineControl(binding);
653
+ this.setControlValue(control, binding, binding.value);
654
+ const errorMessage = document.createElement('div');
655
+ errorMessage.setAttribute('data-einblick-edit-error', 'true');
656
+ errorMessage.hidden = true;
657
+ const hint = document.createElement('div');
658
+ hint.setAttribute('data-einblick-edit-hint', 'true');
659
+ hint.textContent =
660
+ shouldUseInlineEditor(binding)
661
+ ? 'Changes are saved directly and the page refreshes afterward.'
662
+ : 'Use the drawer for this field.';
663
+ const actions = document.createElement('div');
664
+ actions.setAttribute('data-einblick-edit-actions', 'true');
665
+ const cancelButton = document.createElement('button');
666
+ cancelButton.type = 'button';
667
+ cancelButton.textContent = 'Cancel';
668
+ cancelButton.setAttribute('data-einblick-edit-action', 'secondary');
669
+ cancelButton.addEventListener('click', () => this.closeInlineEditor());
670
+ const drawerButton = document.createElement('button');
671
+ drawerButton.type = 'button';
672
+ drawerButton.textContent = 'Open drawer';
673
+ drawerButton.setAttribute('data-einblick-edit-action', 'secondary');
674
+ drawerButton.addEventListener('click', () => {
675
+ this.closeInlineEditor();
676
+ this.openDrawer(target, binding);
677
+ });
678
+ const saveButton = document.createElement('button');
679
+ saveButton.type = 'button';
680
+ saveButton.textContent = 'Save';
681
+ saveButton.setAttribute('data-einblick-edit-action', 'primary');
682
+ saveButton.addEventListener('click', async () => {
683
+ errorMessage.hidden = true;
684
+ errorMessage.textContent = '';
685
+ saveButton.disabled = true;
686
+ saveButton.textContent = 'Saving...';
687
+ try {
688
+ const nextValue = this.getControlValue(control, binding);
689
+ const savedValue = await this.saveInlineField(binding, nextValue);
690
+ if (target) {
691
+ this.applyValueToElement(target, binding, savedValue);
692
+ }
693
+ this.closeInlineEditor();
694
+ await this.callbacks.onSave?.({
695
+ binding,
696
+ value: savedValue,
697
+ source: 'inline',
698
+ });
699
+ }
700
+ catch (error) {
701
+ errorMessage.hidden = false;
702
+ errorMessage.textContent =
703
+ error instanceof Error ? error.message : 'Failed to save field';
704
+ saveButton.disabled = false;
705
+ saveButton.textContent = 'Save';
706
+ }
707
+ });
708
+ actions.append(cancelButton, drawerButton, saveButton);
709
+ body.append(label, control, errorMessage, hint, actions);
710
+ panel.append(header, body);
711
+ backdrop.append(panel);
712
+ backdrop.addEventListener('click', (event) => {
713
+ if (event.target === backdrop) {
714
+ this.closeInlineEditor();
715
+ }
716
+ });
717
+ document.body.append(backdrop);
718
+ this.inlineEditorBackdrop = backdrop;
719
+ this.currentInlineTarget = target;
720
+ this.currentInlineBinding = binding;
721
+ window.setTimeout(() => {
722
+ if ('focus' in control && typeof control.focus === 'function') {
723
+ control.focus();
724
+ }
725
+ }, 0);
726
+ }
727
+ closeInlineEditor() {
728
+ this.inlineEditorBackdrop?.remove();
729
+ this.inlineEditorBackdrop = null;
730
+ this.currentInlineTarget = null;
731
+ this.currentInlineBinding = null;
732
+ }
733
+ createInlineControl(binding) {
734
+ switch (binding.fieldType) {
735
+ case 'text':
736
+ case 'markdown': {
737
+ const textarea = document.createElement('textarea');
738
+ textarea.setAttribute('data-einblick-edit-textarea', 'true');
739
+ return textarea;
740
+ }
741
+ case 'boolean': {
742
+ const select = document.createElement('select');
743
+ select.setAttribute('data-einblick-edit-select', 'true');
744
+ const trueOption = document.createElement('option');
745
+ trueOption.value = 'true';
746
+ trueOption.textContent = 'True';
747
+ const falseOption = document.createElement('option');
748
+ falseOption.value = 'false';
749
+ falseOption.textContent = 'False';
750
+ select.append(trueOption, falseOption);
751
+ return select;
752
+ }
753
+ default: {
754
+ const input = document.createElement('input');
755
+ input.setAttribute('data-einblick-edit-input', 'true');
756
+ if (binding.fieldType === 'number') {
757
+ input.type = 'number';
758
+ }
759
+ else if (binding.fieldType === 'date') {
760
+ input.type = 'date';
761
+ }
762
+ else {
763
+ input.type = 'text';
764
+ }
765
+ return input;
766
+ }
767
+ }
768
+ }
769
+ setControlValue(control, binding, value) {
770
+ const currentValue = value ?? binding.value;
771
+ if (control instanceof HTMLSelectElement) {
772
+ control.value = currentValue === true ? 'true' : 'false';
773
+ return;
774
+ }
775
+ if (binding.fieldType === 'tags') {
776
+ control.value = Array.isArray(currentValue)
777
+ ? currentValue
778
+ .map((entry) => (typeof entry === 'string' ? entry : String(entry)))
779
+ .join(', ')
780
+ : currentValue === undefined || currentValue === null
781
+ ? ''
782
+ : String(currentValue);
783
+ return;
784
+ }
785
+ if (binding.fieldType === 'date') {
786
+ control.value =
787
+ typeof currentValue === 'string' ? normalizeDateValue(currentValue) : '';
788
+ return;
789
+ }
790
+ control.value =
791
+ currentValue === undefined || currentValue === null
792
+ ? ''
793
+ : typeof currentValue === 'string'
794
+ ? currentValue
795
+ : String(currentValue);
796
+ }
797
+ getControlValue(control, binding) {
798
+ const rawValue = control.value;
799
+ switch (binding.fieldType) {
800
+ case 'number':
801
+ return rawValue.trim().length === 0 ? null : Number(rawValue);
802
+ case 'boolean':
803
+ return rawValue === 'true';
804
+ case 'tags':
805
+ return rawValue
806
+ .split(',')
807
+ .map((entry) => entry.trim())
808
+ .filter(Boolean);
809
+ default:
810
+ return rawValue;
811
+ }
812
+ }
813
+ async saveInlineField(binding, value) {
814
+ if (!this.bridgeFrame?.contentWindow) {
815
+ throw new Error('Einblick bridge is not connected yet');
816
+ }
817
+ if (!this.state.isAuthenticated) {
818
+ throw new Error('Sign in with Einblick to edit this field');
819
+ }
820
+ if (!binding.fieldKey) {
821
+ throw new Error('Inline editing requires a field binding');
822
+ }
823
+ const requestId = createRandomId();
824
+ const promise = new Promise((resolve, reject) => {
825
+ const timeoutId = window.setTimeout(() => {
826
+ this.pendingInlineSaves.delete(requestId);
827
+ reject(new Error('Saving took too long. Please try again.'));
828
+ }, 15000);
829
+ this.pendingInlineSaves.set(requestId, {
830
+ resolve,
831
+ reject,
832
+ timeoutId,
833
+ });
834
+ });
835
+ this.bridgeFrame.contentWindow.postMessage({
836
+ source: EINBLICK_PARENT_SOURCE,
837
+ type: 'save-inline-field',
838
+ nonce: this.bridgeNonce,
839
+ requestId,
840
+ payload: {
841
+ resourceSlug: binding.resourceSlug,
842
+ recordId: binding.recordId,
843
+ fieldKey: binding.fieldKey,
844
+ value,
845
+ },
846
+ }, this.appOrigin);
847
+ return await promise;
848
+ }
849
+ openDrawer(target, binding) {
850
+ if (!this.state.isAuthenticated) {
851
+ this.callbacks.onError?.('Sign in with Einblick to edit this content');
852
+ return;
853
+ }
854
+ this.closeInlineEditor();
855
+ this.closeDrawer();
856
+ this.setActiveElement(null);
857
+ const nonce = createRandomId();
858
+ const backdrop = document.createElement('div');
859
+ backdrop.setAttribute('data-einblick-edit-backdrop', 'true');
860
+ const iframe = document.createElement('iframe');
861
+ iframe.setAttribute('data-einblick-editor-frame', 'true');
862
+ iframe.src = this.buildEditorUrl(binding, nonce);
863
+ iframe.title = binding.label || 'Einblick editor';
864
+ backdrop.append(iframe);
865
+ backdrop.addEventListener('click', (event) => {
866
+ if (event.target === backdrop) {
867
+ this.closeDrawer();
868
+ }
869
+ });
870
+ document.body.append(backdrop);
871
+ this.editorBackdrop = backdrop;
872
+ this.editorNonce = nonce;
873
+ this.editorTarget = target;
874
+ this.editorBinding = binding;
875
+ }
876
+ closeDrawer() {
877
+ this.editorBackdrop?.remove();
878
+ this.editorBackdrop = null;
879
+ this.editorNonce = null;
880
+ this.editorTarget = null;
881
+ this.editorBinding = null;
882
+ }
883
+ applyValueToElement(element, binding, value) {
884
+ element.dataset.einblickValue = serializeBindingValue(value) ?? '';
885
+ const kind = element.dataset.einblickKind;
886
+ if (kind === 'image' || binding.fieldType === 'image' || binding.fieldType === 'file' || binding.fieldType === 'files') {
887
+ const imageUrl = getImageUrl(value);
888
+ if (!imageUrl) {
889
+ return;
890
+ }
891
+ const img = element.querySelector('img');
892
+ if (!img) {
893
+ return;
894
+ }
895
+ img.setAttribute('src', imageUrl);
896
+ if (img.hasAttribute('srcset')) {
897
+ img.setAttribute('srcset', imageUrl);
898
+ }
899
+ return;
900
+ }
901
+ if (kind !== 'text') {
902
+ return;
903
+ }
904
+ element.textContent = formatValueForText(value);
905
+ }
906
+ }
907
+ export function createEinblickEditRuntime(options) {
908
+ const runtime = new EinblickEditRuntime(options);
909
+ return {
910
+ start: () => runtime.start(),
911
+ destroy: () => runtime.destroy(),
912
+ login: (loginOptions) => runtime.login(loginOptions),
913
+ openEditor: (binding, element) => runtime.openEditor(binding, element),
914
+ };
915
+ }
916
+ //# sourceMappingURL=runtime.js.map