rapier-embed 1.2.45 → 1.2.48
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.
- package/README.md +111 -53
- package/contract.mjs +16 -3
- package/embed.d.mts +14 -6
- package/embed.mjs +262 -91
- package/package.json +1 -1
- package/rapier-plugins.json +200 -200
package/embed.mjs
CHANGED
|
@@ -163,14 +163,23 @@ function assetURL(value) {
|
|
|
163
163
|
return url.href;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
function pictureCeiling(limit) {
|
|
167
|
+
if (!Number.isSafeInteger(limit) || limit < 1) throw refusal('asset_bytes_invalid');
|
|
168
|
+
return Math.min(limit, LIMITS.pictureBytes);
|
|
169
|
+
}
|
|
166
170
|
function assetBytes(value, limit = LIMITS.pictureBytes) {
|
|
167
171
|
if (!(value instanceof Uint8Array) || !value.byteLength) throw refusal('asset_bytes_invalid');
|
|
168
|
-
if (value.byteLength >
|
|
172
|
+
if (value.byteLength > pictureCeiling(limit)) throw refusal('picture_too_large');
|
|
169
173
|
return value;
|
|
170
174
|
}
|
|
171
175
|
async function bytesHash(bytes) {
|
|
172
176
|
return Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)), n => n.toString(16).padStart(2, '0')).join('');
|
|
173
177
|
}
|
|
178
|
+
function ownedPicture(bytes) {
|
|
179
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
180
|
+
copy.set(bytes);
|
|
181
|
+
return copy;
|
|
182
|
+
}
|
|
174
183
|
function externalImage(raw, asset, url) {
|
|
175
184
|
if (!url) return raw;
|
|
176
185
|
const token = '[' + asset.label + ']', at = raw.lastIndexOf(token);
|
|
@@ -188,15 +197,19 @@ function createAssetBroker({current, send}) {
|
|
|
188
197
|
if (identity !== now.identity) { retained.clear(); retainedBytes = 0; identity = now.identity; }
|
|
189
198
|
return now;
|
|
190
199
|
}
|
|
200
|
+
function admitted(now) {
|
|
201
|
+
return {identity: now.identity, generation: now.generation, connected: now.connected,
|
|
202
|
+
granted: now.granted, pictureBytes: now.pictureBytes, baseRevision: now.baseRevision};
|
|
203
|
+
}
|
|
191
204
|
function cancel(code = 'asset_disconnected') { pending?.finish({code}); }
|
|
192
205
|
async function request(asset) {
|
|
193
|
-
const state = scope(), bytes = assetBytes(asset.bytes, state.pictureBytes);
|
|
206
|
+
const state = admitted(scope()), bytes = assetBytes(asset.bytes, state.pictureBytes);
|
|
194
207
|
if (!state.granted) return {code: 'asset_not_granted'};
|
|
195
208
|
if (!state.connected) return {code: 'asset_disconnected'};
|
|
196
209
|
if (pending) return {code: 'asset_busy'};
|
|
197
210
|
if (!ASSET_TYPES.includes(asset.codec)) throw refusal('asset_media_type_invalid');
|
|
198
211
|
if (retained.size >= 1024 || retainedBytes + bytes.byteLength > 25 * 1024 * 1024) return {code: 'asset_cache_full'};
|
|
199
|
-
const kept = {...asset, bytes: bytes
|
|
212
|
+
const kept = {...asset, bytes: ownedPicture(bytes)};
|
|
200
213
|
let resolve;
|
|
201
214
|
const job = new Promise(yes => { resolve = yes; });
|
|
202
215
|
const row = {state, asset: kept, requestId: crypto.randomUUID(), timer: null, bytesSha256: '',
|
|
@@ -258,7 +271,7 @@ function pictureBase(value) {
|
|
|
258
271
|
return {FEATURES, LIMITS, ASSET_TYPES, refusal, coded, normalizeSettings, snapshotSettings, pickDeviceFile, createFilePicker, assetURL, assetBytes, bytesHash, externalImage, createAssetBroker, pictureBase};
|
|
259
272
|
})();
|
|
260
273
|
const BUILDS = Object.freeze({full: 'https://rapier.website/embed/rapier.html', reader: 'https://rapier.website/embed/rapier-reader.html'});
|
|
261
|
-
const PACKAGE_VERSION = '1.2.
|
|
274
|
+
const PACKAGE_VERSION = '1.2.48';
|
|
262
275
|
const OPTIONS = ['build', 'src', 'plugins', 'approvePlugins', 'sessionId', 'documentId', 'agent', 'agentName', 'theme', 'save', 'assets', 'openFile', 'load',
|
|
263
276
|
'onState', 'compare', 'onClose', 'settings', 'style', 'title', 'onError', 'timeout'];
|
|
264
277
|
const READER_FEATURES = ['find', 'readAloud', 'share'];
|
|
@@ -268,14 +281,15 @@ const identifier = value => typeof value === 'string' && value.length > 0 && val
|
|
|
268
281
|
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
269
282
|
const themeValue = value => ['light', 'dark', 'system'].includes(value);
|
|
270
283
|
const mint = prefix => prefix + '-' + (globalThis.crypto?.randomUUID?.() || Math.random().toString(36).slice(2));
|
|
271
|
-
const
|
|
284
|
+
const slotLive = new Map();
|
|
272
285
|
const fnv = text => { let h = 0x811c9dc5; for (let i = 0; i < text.length; i++) { h ^= text.charCodeAt(i); h = Math.imul(h, 0x01000193); } return (h >>> 0).toString(36); };
|
|
273
|
-
function autoIdentity(options) {
|
|
286
|
+
function autoIdentity(options, location, fieldId) {
|
|
274
287
|
const load = options.load;
|
|
275
288
|
if (!options.save || !load || typeof load !== 'object' || typeof load.content !== 'string') return null;
|
|
276
289
|
const base = fnv(location.origin + location.pathname) + '-' + fnv(String(load.filename ?? '') + '|' + load.content.length + '|' + load.content.slice(0, 2048));
|
|
290
|
+
if (fieldId) return base + '-field-' + fnv(fieldId);
|
|
277
291
|
let at = 0;
|
|
278
|
-
while (
|
|
292
|
+
while (slotLive.get(JSON.stringify(['page-' + base + '-' + at, 'doc-' + base + '-' + at]))?.()) at++;
|
|
279
293
|
return base + '-' + at;
|
|
280
294
|
}
|
|
281
295
|
function deferred() {
|
|
@@ -361,8 +375,9 @@ function mount(target, options = {}) {
|
|
|
361
375
|
source.searchParams.set('plugins', directory.href);
|
|
362
376
|
}
|
|
363
377
|
if (options.approvePlugins === true) source.searchParams.set('approvePlugins', '1');
|
|
364
|
-
const origin = source.origin, auto = options.sessionId === undefined && options.documentId === undefined ? autoIdentity(options) : null;
|
|
365
|
-
const
|
|
378
|
+
const origin = source.origin, auto = options.sessionId === undefined && options.documentId === undefined ? autoIdentity(options, host.location) : null;
|
|
379
|
+
const pageSession = options.documentId !== undefined && options.save ? 'page-' + fnv(host.location.origin + host.location.pathname) : null;
|
|
380
|
+
const askedSession = options.sessionId ?? (auto ? 'page-' + auto : pageSession ?? mint('session')), askedDocument = options.documentId ?? (auto ? 'doc-' + auto : mint('document'));
|
|
366
381
|
if (!identifier(askedSession) || !identifier(askedDocument)) throw coded('invalid_id', TypeError, 'Session and document IDs must be nonempty strings of at most 256 code units');
|
|
367
382
|
const sessionId = askedSession, documentId = askedDocument, pairKey = JSON.stringify([sessionId, documentId]), openElsewhere = !!slotLive.get(pairKey)?.();
|
|
368
383
|
const agentName = options.agentName;
|
|
@@ -377,7 +392,7 @@ function mount(target, options = {}) {
|
|
|
377
392
|
const unread = reader ? (settings?.features || []).filter(name => !READER_FEATURES.includes(name)) : [];
|
|
378
393
|
if (unread.length) throw Object.assign(new TypeError('The reader has no ' + unread.join(', ') + '; its features are ' + READER_FEATURES.join(', ')), {code: 'settings_feature_unavailable'});
|
|
379
394
|
const assetAnswers = new Map();
|
|
380
|
-
let assetPending =
|
|
395
|
+
let assetPending = null, assetAbort = new AbortController();
|
|
381
396
|
const chooseFile = options.openFile || (({signal}) => pickDeviceFile(document, signal)), openAnswers = new Map();
|
|
382
397
|
let openPending = null;
|
|
383
398
|
const capabilities = [
|
|
@@ -388,8 +403,9 @@ function mount(target, options = {}) {
|
|
|
388
403
|
if (options.agent && (!capabilities.includes('open') || !capabilities.includes('read')))
|
|
389
404
|
throw coded('invalid_option', TypeError, 'Agent access needs both load and save');
|
|
390
405
|
const ids = {sessionId, documentId}, answers = new Map(), waiting = new Map(), listeners = new Map();
|
|
391
|
-
let watcher = null, port = null, posted = false, connected = false, ended = false, endError = null, sawLoad = false, seq = 0, activeSave = null, activeClose = null;
|
|
392
|
-
let currentTheme = options.theme, lastSaved = null, readOnly = false, connectedWait = deferred(), boot, bootTimer = 0, loadTimer = 0, timedOut = false, frameFacts = {};
|
|
406
|
+
let watcher = null, port = null, posted = false, connected = false, ended = false, terminal = false, endError = null, sawLoad = false, seq = 0, activeSave = null, activeClose = null;
|
|
407
|
+
let currentTheme = options.theme, lastSaved = null, readOnly = false, connectedWait = deferred(), boot, bootAttempt, bootTimer = 0, loadTimer = 0, timedOut = false, frameFacts = {};
|
|
408
|
+
const initialLoad = record(options.load) ? {...options.load} : options.load;
|
|
393
409
|
let held = null;
|
|
394
410
|
const ready = deferred();
|
|
395
411
|
const emit = (type, value) => { for (const fn of listeners.get(type) || []) fn(value); };
|
|
@@ -414,8 +430,9 @@ function mount(target, options = {}) {
|
|
|
414
430
|
return pending.promise;
|
|
415
431
|
}
|
|
416
432
|
function requestCapture() {
|
|
417
|
-
|
|
418
|
-
|
|
433
|
+
const capture = activeSave;
|
|
434
|
+
if (capture) capture.commandId = 'host-' + (seq + 1);
|
|
435
|
+
send('save', undefined, undefined, true).catch(error => { if (activeSave === capture) { capture?.reject(error); activeSave = null; } });
|
|
419
436
|
}
|
|
420
437
|
async function answerSave(message, channel) {
|
|
421
438
|
const {requestId, baseRevision, payload} = message;
|
|
@@ -439,6 +456,7 @@ function mount(target, options = {}) {
|
|
|
439
456
|
if (!answers.has(requestId)) {
|
|
440
457
|
const job = Promise.resolve().then(async () => {
|
|
441
458
|
try {
|
|
459
|
+
if (ended || channel !== port) throw coded('disconnected', Error, 'The ' + noun + ' connection has ended');
|
|
442
460
|
const result = await options.save({...payload, requestId, baseRevision});
|
|
443
461
|
if (!revision(result?.revision) || result.revision === baseRevision || typeof result.revision === 'number' && typeof baseRevision === 'number' && result.revision <= baseRevision)
|
|
444
462
|
throw coded('save_not_advancing', TypeError, 'save must return an advancing durable revision');
|
|
@@ -455,7 +473,7 @@ function mount(target, options = {}) {
|
|
|
455
473
|
if (ended || channel !== port) return;
|
|
456
474
|
channel.postMessage(reply);
|
|
457
475
|
if (reply.type === 'save-ack') {
|
|
458
|
-
lastSaved = reply.payload; held = {revision: reply.payload.revision, content: payload.content, filename: payload.filename};
|
|
476
|
+
lastSaved = reply.payload; held = {...held, revision: reply.payload.revision, content: payload.content, filename: payload.filename};
|
|
459
477
|
if (activeSave) activeSave.acknowledged = true;
|
|
460
478
|
} else if (activeSave) { activeSave.reject(refused(reply.payload)); activeSave = null; }
|
|
461
479
|
}
|
|
@@ -477,7 +495,7 @@ function mount(target, options = {}) {
|
|
|
477
495
|
if (assetPending) throw refusal('asset_busy');
|
|
478
496
|
if (assetAnswers.size >= 48) assetAnswers.delete(assetAnswers.keys().next().value);
|
|
479
497
|
const signal = assetAbort.signal;
|
|
480
|
-
row = {signature, job: null}; assetAnswers.set(requestId, row); assetPending =
|
|
498
|
+
row = {signature, job: null}; assetAnswers.set(requestId, row); assetPending = row;
|
|
481
499
|
row.job = Promise.resolve().then(async () => {
|
|
482
500
|
try {
|
|
483
501
|
if (await bytesHash(bytes) !== payload.bytesSha256) throw refusal('asset_hash_mismatch');
|
|
@@ -488,7 +506,7 @@ function mount(target, options = {}) {
|
|
|
488
506
|
} catch (error) {
|
|
489
507
|
return {type: 'asset-nack', payload: {code: typeof error?.code === 'string' && error.code.length > 0 && error.code.length <= 64 ? error.code : 'asset_failed',
|
|
490
508
|
reason: String(error?.message || error).slice(0, 500)}};
|
|
491
|
-
} finally { if (assetPending ===
|
|
509
|
+
} finally { if (assetPending === row) assetPending = null; }
|
|
492
510
|
});
|
|
493
511
|
} else if (await bytesHash(bytes) !== payload.bytesSha256) throw refusal('asset_hash_mismatch');
|
|
494
512
|
const answer = await row.job;
|
|
@@ -500,6 +518,7 @@ function mount(target, options = {}) {
|
|
|
500
518
|
function cancelOpen(code = 'open_cancelled') {
|
|
501
519
|
const row = openPending;
|
|
502
520
|
if (!row || row.abort.signal.aborted) return;
|
|
521
|
+
openPending = null;
|
|
503
522
|
row.answer = {type: 'open-nack', payload: {code, reason: 'File opening was canceled.'}};
|
|
504
523
|
row.abort.abort();
|
|
505
524
|
}
|
|
@@ -567,7 +586,8 @@ function mount(target, options = {}) {
|
|
|
567
586
|
|| !(message.requestId === null || identifier(message.requestId)) || !(message.baseRevision == null || revision(message.baseRevision))) return;
|
|
568
587
|
const {type, payload, requestId} = message, pending = waiting.get(requestId);
|
|
569
588
|
if (type === 'protocol-error' && !connected) {
|
|
570
|
-
|
|
589
|
+
clearTimeout(bootTimer); clearTimeout(loadTimer);
|
|
590
|
+
connectedWait.reject(refused(payload)); return;
|
|
571
591
|
}
|
|
572
592
|
if (type === 'connected') {
|
|
573
593
|
if (connected) return;
|
|
@@ -578,8 +598,8 @@ function mount(target, options = {}) {
|
|
|
578
598
|
}
|
|
579
599
|
if (!connected) return;
|
|
580
600
|
if (type === 'open-request' || type === 'open-cancel') { answerOpen(message, channel); return; }
|
|
581
|
-
if (type === 'asset-request') { answerAsset(message, channel).catch(fail); return; }
|
|
582
|
-
if (type === 'save-request') { answerSave(message, channel).catch(fail); return; }
|
|
601
|
+
if (type === 'asset-request') { answerAsset(message, channel).catch(error => { if (!ended && channel === port) fail(error); }); return; }
|
|
602
|
+
if (type === 'save-request') { answerSave(message, channel).catch(error => { if (!ended && channel === port) fail(error); }); return; }
|
|
583
603
|
if (type === 'document-state' && capabilities.includes('changes')) {
|
|
584
604
|
if (!['loaded', 'dirty', 'saving', 'closing', 'readOnly'].every(key => typeof payload[key] === 'boolean')
|
|
585
605
|
|| Object.keys(payload).some(key => !['loaded', 'dirty', 'saving', 'closing', 'readOnly', 'filename', 'docKind'].includes(key))) return;
|
|
@@ -595,12 +615,15 @@ function mount(target, options = {}) {
|
|
|
595
615
|
return;
|
|
596
616
|
}
|
|
597
617
|
if (type === 'close-request' && options.onClose) {
|
|
598
|
-
Promise.resolve().then(() =>
|
|
618
|
+
Promise.resolve().then(() => {
|
|
619
|
+
if (ended || channel !== port) throw coded('disconnected', Error, 'The ' + noun + ' connection has ended');
|
|
620
|
+
return options.onClose({dirty: !!payload.dirty});
|
|
621
|
+
}).then(decision => {
|
|
599
622
|
if (!['save', 'discard', 'cancel'].includes(decision)) throw coded('invalid_option', TypeError, 'Close needs save, discard or cancel');
|
|
600
623
|
if (channel !== port || ended) return;
|
|
601
624
|
channel.postMessage({type: 'close-decision', ...ids, requestId, baseRevision: message.baseRevision, payload: {decision}});
|
|
602
625
|
if (decision === 'cancel') { activeClose?.resolve({cancelled: true}); activeClose = null; }
|
|
603
|
-
}).catch(error => { activeClose?.reject(error); activeClose = null; fail(error); });
|
|
626
|
+
}).catch(error => { if (channel !== port || ended) return; activeClose?.reject(error); activeClose = null; fail(error); });
|
|
604
627
|
emit('close-request', payload); return;
|
|
605
628
|
}
|
|
606
629
|
if (type === 'close-ready' && options.onClose) { activeClose?.resolve(payload); activeClose = null; emit('closed', payload); return; }
|
|
@@ -620,17 +643,40 @@ function mount(target, options = {}) {
|
|
|
620
643
|
}
|
|
621
644
|
}
|
|
622
645
|
function prepareBoot() {
|
|
623
|
-
|
|
646
|
+
bootAttempt?.cancel(coded('disconnected', Error, 'The ' + noun + ' reconnected'));
|
|
647
|
+
const attempt = {...deferred(), stopped: false, recovered: false, timer: 0};
|
|
648
|
+
attempt.cancel = error => { attempt.stopped = true; clearTimeout(attempt.timer); attempt.reject(error); };
|
|
649
|
+
bootAttempt = attempt; boot = attempt.promise;
|
|
650
|
+
const current = () => bootAttempt === attempt && !attempt.stopped && !ended;
|
|
651
|
+
connectedWait.promise.then(async () => {
|
|
652
|
+
if (!current()) return;
|
|
653
|
+
attempt.timer = setTimeout(() => {
|
|
654
|
+
if (!current()) return;
|
|
655
|
+
const error = coded('load_timeout', Error, 'The ' + noun + ' did not finish its first document within ' + loadTimeout / 1000 + ' seconds');
|
|
656
|
+
attempt.cancel(error); rejectPending(error);
|
|
657
|
+
}, loadTimeout);
|
|
624
658
|
if (currentStyle) await send('style', currentStyle, undefined, true);
|
|
625
|
-
if (
|
|
626
|
-
const
|
|
627
|
-
if (!
|
|
628
|
-
|
|
629
|
-
|
|
659
|
+
if (initialLoad !== undefined) {
|
|
660
|
+
const input = typeof initialLoad === 'function' ? await initialLoad() : held || initialLoad;
|
|
661
|
+
if (!current()) return;
|
|
662
|
+
if (!record(input) || typeof input.content !== 'string' || !revision(input.revision)) throw coded('invalid_load', TypeError, 'load needs content and its revision');
|
|
663
|
+
const value = {...input};
|
|
664
|
+
await send('load', value, null, false, loadTimeout);
|
|
665
|
+
if (!current()) return;
|
|
666
|
+
held = value;
|
|
630
667
|
}
|
|
668
|
+
if (!current()) return;
|
|
669
|
+
clearTimeout(attempt.timer); attempt.resolve();
|
|
631
670
|
ready.resolve(handle);
|
|
632
|
-
});
|
|
633
|
-
boot.catch(error => { ready.reject(error);
|
|
671
|
+
}).catch(error => { if (current()) attempt.cancel(error); });
|
|
672
|
+
boot.catch(error => { if (bootAttempt === attempt && !ended) { ready.reject(error); fail(error); } });
|
|
673
|
+
}
|
|
674
|
+
async function whenBooted(retryLoad = false) {
|
|
675
|
+
const attempt = bootAttempt;
|
|
676
|
+
try { await attempt.promise; }
|
|
677
|
+
catch (error) { if ((!retryLoad && !attempt.recovered) || ended || !connected || attempt !== bootAttempt) throw error; }
|
|
678
|
+
if (ended || attempt !== bootAttempt) throw endError || coded('disconnected', Error, 'The ' + noun + ' connection has ended');
|
|
679
|
+
return attempt;
|
|
634
680
|
}
|
|
635
681
|
function connect() {
|
|
636
682
|
if (posted || ended) return;
|
|
@@ -639,7 +685,8 @@ function mount(target, options = {}) {
|
|
|
639
685
|
port.onmessage = event => onPortMessage(event, channel.port1);
|
|
640
686
|
port.onmessageerror = () => {
|
|
641
687
|
if (channel.port1 !== port || ended) return;
|
|
642
|
-
|
|
688
|
+
const error = coded('disconnected', Error, 'The ' + noun + ' connection was lost');
|
|
689
|
+
end(true, error); fail(error);
|
|
643
690
|
};
|
|
644
691
|
try { iframe.contentWindow.postMessage({type: 'rapier-connect', ...ids, capabilities, contract: 1, ...(settings ? {settings} : {}), ...(agentName ? {agent: {name: agentName}} : {}), ...(currentTheme ? {theme: currentTheme} : {})}, origin, [channel.port2]); }
|
|
645
692
|
catch (error) { port.close(); port = null; posted = false; fail(error); }
|
|
@@ -652,11 +699,24 @@ function mount(target, options = {}) {
|
|
|
652
699
|
}
|
|
653
700
|
const connectTimeoutReason = () => 'The ' + noun + ' page loaded but did not answer within ' + bootTimeout / 1000 + ' seconds. The address may not be a Rapier page (a missing file loads an error page), the page may not allow this parent (frame-ancestors or X-Frame-Options), or the device may be too slow; a slow device that answers later still connects.';
|
|
654
701
|
function onLoad() {
|
|
702
|
+
if (terminal) return;
|
|
703
|
+
if (ended) {
|
|
704
|
+
if (!iframe.isConnected) return;
|
|
705
|
+
if (slotLive.get(pairKey)?.()) {
|
|
706
|
+
endError = coded('document_open_elsewhere', Error, 'Another editor already has this document open');
|
|
707
|
+
fail(endError); return;
|
|
708
|
+
}
|
|
709
|
+
ended = false; endError = null; connected = false; posted = false; readOnly = false; sawLoad = false; timedOut = false;
|
|
710
|
+
assetAbort = new AbortController(); connectedWait = deferred(); prepareBoot();
|
|
711
|
+
host.addEventListener('message', onWindowMessage);
|
|
712
|
+
slotLive.set(pairKey, live); watch();
|
|
713
|
+
}
|
|
655
714
|
if (sawLoad && posted) {
|
|
656
715
|
cancelOpen('open_disconnected');
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
716
|
+
const error = coded('disconnected', Error, 'The ' + noun + ' reconnected');
|
|
717
|
+
bootAttempt.cancel(error); rejectPending(error); port?.close(); port = null;
|
|
718
|
+
assetAbort.abort(refusal('asset_disconnected')); assetPending = null; assetAbort = new AbortController();
|
|
719
|
+
connected = false; posted = false; readOnly = false; timedOut = false; connectedWait = deferred(); prepareBoot();
|
|
660
720
|
}
|
|
661
721
|
sawLoad = true;
|
|
662
722
|
clearTimeout(bootTimer); clearTimeout(loadTimer);
|
|
@@ -664,27 +724,43 @@ function mount(target, options = {}) {
|
|
|
664
724
|
connect();
|
|
665
725
|
}
|
|
666
726
|
function focusBeside(forward) {
|
|
667
|
-
const
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
727
|
+
const flat = [], around = new Set();
|
|
728
|
+
const parent = node => node.assignedSlot || node.parentNode || node.host;
|
|
729
|
+
const walk = element => {
|
|
730
|
+
flat.push(element);
|
|
731
|
+
const assigned = element.localName === 'slot' ? element.assignedElements({flatten: true}) : [];
|
|
732
|
+
for (const child of assigned.length ? assigned : (element.shadowRoot || element).children) walk(child);
|
|
733
|
+
};
|
|
734
|
+
for (const child of document.children) walk(child);
|
|
735
|
+
for (let node = iframe; node; node = parent(node)) around.add(node);
|
|
736
|
+
const available = element => {
|
|
737
|
+
if (around.has(element) || element.tabIndex < 0 || !element.matches('a[href],button,input,select,textarea,iframe,summary,[tabindex],[contenteditable=""],[contenteditable="true"]') ||
|
|
738
|
+
element.matches(':disabled') || !element.getClientRects().length || ['hidden', 'collapse'].includes(host.getComputedStyle(element).visibility)) return false;
|
|
739
|
+
for (let node = element; node; node = parent(node))
|
|
740
|
+
if (node.nodeType === 1 && (node.matches('[hidden],[inert]') || node.localName.includes('-') && node.hasAttribute('disabled'))) return false;
|
|
741
|
+
return true;
|
|
742
|
+
};
|
|
743
|
+
const at = flat.indexOf(iframe), side = forward ? flat.slice(at + 1) : flat.slice(0, at).reverse();
|
|
744
|
+
const next = at < 0 ? null : side.find(available);
|
|
672
745
|
if (next) next.focus(); else iframe.blur();
|
|
673
746
|
}
|
|
674
|
-
function end(removed) {
|
|
675
|
-
if (ended) return;
|
|
676
|
-
if (connected && !removed) send('disconnect', undefined, undefined, true).catch(() => {});
|
|
677
|
-
ended = true; connected = false; clearTimeout(bootTimer); clearTimeout(loadTimer); watcher?.disconnect();
|
|
747
|
+
function end(removed, error) {
|
|
748
|
+
if (terminal || ended && removed) return;
|
|
749
|
+
if (connected && (!removed || error)) send('disconnect', undefined, undefined, true).catch(() => {});
|
|
750
|
+
terminal = !removed; ended = true; connected = false; clearTimeout(bootTimer); clearTimeout(loadTimer); watcher?.disconnect(); watcher = null;
|
|
678
751
|
cancelOpen('open_disconnected'); openAnswers.clear();
|
|
679
|
-
assetAbort.abort(refusal('asset_disconnected')); assetAnswers.clear();
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
rejectPending(error); connectedWait.reject(error); ready.reject(error);
|
|
683
|
-
host.removeEventListener('message', onWindowMessage);
|
|
684
|
-
|
|
752
|
+
assetAbort.abort(refusal('asset_disconnected')); assetPending = null; assetAnswers.clear();
|
|
753
|
+
error ??= removed ? coded('frame_removed', Error, 'The ' + noun + ' was removed from the page') : coded('disconnected', Error, 'The ' + noun + ' connection has ended');
|
|
754
|
+
endError = error;
|
|
755
|
+
bootAttempt.cancel(error); rejectPending(error); connectedWait.reject(error); ready.reject(error);
|
|
756
|
+
host.removeEventListener('message', onWindowMessage);
|
|
757
|
+
if (!removed) { iframe.removeEventListener('load', onLoad); listeners.clear(); answers.clear(); }
|
|
758
|
+
port?.close(); port = null;
|
|
759
|
+
if (slotLive.get(pairKey) === live) slotLive.delete(pairKey);
|
|
685
760
|
if (!removed && iframe !== target) iframe.remove();
|
|
686
761
|
}
|
|
687
762
|
function onWindowMessage(event) {
|
|
763
|
+
if (ended) return;
|
|
688
764
|
if (event.source !== iframe.contentWindow || event.origin !== origin) return;
|
|
689
765
|
if (event.data?.type === 'rapier-ready') connect();
|
|
690
766
|
else if (event.data?.type === 'rapier-boot-failed') bootFailed('boot_failed', 'The ' + noun + ' could not start');
|
|
@@ -703,19 +779,21 @@ function mount(target, options = {}) {
|
|
|
703
779
|
if (typeof content !== 'string') throw Object.assign(new TypeError('load needs the document text as a string'), {code: 'invalid_payload'});
|
|
704
780
|
let {filename, revision: version, readOnly, title, baseUrl} = options;
|
|
705
781
|
cancelOpen('open_stale');
|
|
706
|
-
await
|
|
782
|
+
const attempt = await whenBooted(true);
|
|
707
783
|
if (version === undefined) version = typeof held?.revision === 'number' ? held.revision + 1 : 1;
|
|
708
784
|
const sameDocument = held && (filename === undefined || held.filename === undefined || filename === held.filename);
|
|
709
785
|
if (sameDocument && typeof version === 'number' && typeof held.revision === 'number' && version < held.revision)
|
|
710
786
|
throw Object.assign(new Error('Revision ' + version + ' is older than the frame\'s ' + held.revision), {code: 'revision_stale', currentRevision: held.revision});
|
|
711
787
|
if (sameDocument && version === held.revision && held.content !== null && content !== held.content)
|
|
712
788
|
throw Object.assign(new Error('Revision ' + version + ' already names other content'), {code: 'revision_conflict', currentRevision: held.revision});
|
|
713
|
-
const
|
|
714
|
-
|
|
789
|
+
const value = {content, revision: version, ...(filename !== undefined ? {filename} : {}), ...(readOnly !== undefined ? {readOnly} : {}), ...(title !== undefined ? {title} : {}), ...(baseUrl !== undefined ? {baseUrl} : {})};
|
|
790
|
+
const loaded = await send('load', value, null, false, loadTimeout);
|
|
791
|
+
if (attempt !== bootAttempt || ended) throw endError || coded('disconnected', Error, 'The ' + noun + ' reconnected');
|
|
792
|
+
held = value; attempt.recovered = true;
|
|
715
793
|
return loaded;
|
|
716
794
|
},
|
|
717
795
|
async save() {
|
|
718
|
-
await
|
|
796
|
+
await whenBooted();
|
|
719
797
|
if (ended) throw endError || coded('disconnected', Error, 'The ' + noun + ' connection has ended');
|
|
720
798
|
if (!options.save) throw coded('save_unavailable', Error, 'No save callback was granted');
|
|
721
799
|
if (readOnly) throw coded('read_only', Error, 'The document is read-only');
|
|
@@ -723,24 +801,27 @@ function mount(target, options = {}) {
|
|
|
723
801
|
const promise = activeSave.promise;
|
|
724
802
|
requestCapture(); return promise;
|
|
725
803
|
},
|
|
726
|
-
async compare(content, {filename} = {}) { await
|
|
804
|
+
async compare(content, {filename} = {}) { await whenBooted(); return send('compare', {content, ...(filename !== undefined ? {filename} : {})}); },
|
|
727
805
|
async close() {
|
|
728
|
-
await
|
|
806
|
+
await whenBooted();
|
|
729
807
|
if (ended) throw endError || coded('disconnected', Error, 'The ' + noun + ' connection has ended');
|
|
730
808
|
if (!options.onClose) throw coded('close_unavailable', Error, 'No close callback was granted');
|
|
731
|
-
if (!activeClose) {
|
|
809
|
+
if (!activeClose) {
|
|
810
|
+
const closing = activeClose = deferred();
|
|
811
|
+
send('close', undefined, undefined, true).catch(error => { if (activeClose === closing) { closing.reject(error); activeClose = null; } });
|
|
812
|
+
}
|
|
732
813
|
return activeClose.promise;
|
|
733
814
|
},
|
|
734
815
|
async theme(value) {
|
|
735
816
|
if (!themeValue(value)) throw coded('invalid_theme', TypeError, 'Theme must be light, dark or system');
|
|
736
817
|
currentTheme = value;
|
|
737
|
-
try { await
|
|
818
|
+
try { await whenBooted(); return await send('theme', {theme: value}, undefined, true); } catch (error) { if (error?.code === 'disconnected') return; throw error; }
|
|
738
819
|
},
|
|
739
820
|
async style(value) {
|
|
740
821
|
if (!reader) throw coded('style_unavailable', TypeError, 'style restyles the reader build');
|
|
741
822
|
if (!capabilities.includes('open')) throw coded('load_required', TypeError, 'Reader styling requires load');
|
|
742
823
|
currentStyle = readerStyle(value);
|
|
743
|
-
try { await
|
|
824
|
+
try { await whenBooted(); return await send('style', currentStyle, undefined, true); } catch (error) { if (error?.code === 'disconnected') return; throw error; }
|
|
744
825
|
},
|
|
745
826
|
disconnect() { end(false); },
|
|
746
827
|
on(type, fn) {
|
|
@@ -749,18 +830,18 @@ function mount(target, options = {}) {
|
|
|
749
830
|
return () => listeners.get(type)?.delete(fn);
|
|
750
831
|
},
|
|
751
832
|
});
|
|
752
|
-
|
|
753
|
-
if (!openElsewhere) slotLive.set(pairKey,
|
|
833
|
+
const live = () => iframe.isConnected && !ended;
|
|
834
|
+
if (!openElsewhere) slotLive.set(pairKey, live);
|
|
754
835
|
prepareBoot();
|
|
836
|
+
if (openElsewhere) {
|
|
837
|
+
queueMicrotask(() => bootFailed('document_open_elsewhere', 'Another editor on this page already has this document open (the same sessionId and documentId). Give each editor its own documentId.'));
|
|
838
|
+
return handle;
|
|
839
|
+
}
|
|
755
840
|
if (iframe !== target) { iframe.setAttribute('width', '100%'); iframe.setAttribute('height', '100%'); iframe.setAttribute('frameborder', '0'); }
|
|
756
841
|
iframe.setAttribute('title', options.title || (reader ? 'Markdown reader' : 'Markdown editor'));
|
|
757
842
|
iframe.setAttribute('allow', options.agent ? 'clipboard-write; tools' : 'clipboard-write');
|
|
758
843
|
const ownSandbox = iframe === target && iframe.hasAttribute('sandbox');
|
|
759
844
|
if (!ownSandbox) iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-downloads');
|
|
760
|
-
if (openElsewhere) {
|
|
761
|
-
queueMicrotask(() => bootFailed('document_open_elsewhere', 'Another editor on this page already has this document open (the same sessionId and documentId). Give each editor its own documentId.'));
|
|
762
|
-
return handle;
|
|
763
|
-
}
|
|
764
845
|
const blocked = framingProblem(host, source, ownSandbox ? iframe.sandbox : null, noun);
|
|
765
846
|
if (blocked) {
|
|
766
847
|
if (iframe !== target) target.append(iframe);
|
|
@@ -770,13 +851,26 @@ function mount(target, options = {}) {
|
|
|
770
851
|
host.addEventListener('message', onWindowMessage); iframe.addEventListener('load', onLoad);
|
|
771
852
|
iframe.src = source.href;
|
|
772
853
|
if (iframe !== target) target.append(iframe);
|
|
773
|
-
|
|
854
|
+
function watch() {
|
|
855
|
+
if (!host.MutationObserver) return;
|
|
856
|
+
watcher?.disconnect();
|
|
774
857
|
let attached = iframe.isConnected;
|
|
775
|
-
watcher = new host.MutationObserver(() => {
|
|
858
|
+
watcher = new host.MutationObserver(() => {
|
|
859
|
+
if (!iframe.isConnected) { if (attached) end(true); return; }
|
|
860
|
+
attached = true;
|
|
861
|
+
observeRoots();
|
|
862
|
+
});
|
|
863
|
+
const observeRoots = () => {
|
|
864
|
+
for (let node = iframe; node;) {
|
|
865
|
+
const root = node.getRootNode();
|
|
866
|
+
if (root !== host.document && root.nodeType === 11) watcher.observe(root, {childList: true, subtree: true});
|
|
867
|
+
node = root.host;
|
|
868
|
+
}
|
|
869
|
+
};
|
|
776
870
|
watcher.observe(host.document, {childList: true, subtree: true});
|
|
777
|
-
|
|
778
|
-
if (root !== host.document && root.nodeType === 11) watcher.observe(root, {childList: true, subtree: true});
|
|
871
|
+
observeRoots();
|
|
779
872
|
}
|
|
873
|
+
watch();
|
|
780
874
|
loadTimer = setTimeout(() => bootFailed('load_timeout', 'The ' + noun + ' did not finish loading within ' + loadTimeout / 1000 + ' seconds. Check the address and the network.'), loadTimeout);
|
|
781
875
|
return handle;
|
|
782
876
|
}
|
|
@@ -807,12 +901,19 @@ function joinForm(field, form) {
|
|
|
807
901
|
} catch (error) { if (current()) for (const {field} of captured) field._error(error); }
|
|
808
902
|
finally { group.pending = false; group.releasing = false; }
|
|
809
903
|
};
|
|
810
|
-
|
|
904
|
+
forms.set(form, group);
|
|
905
|
+
}
|
|
906
|
+
const root = form.getRootNode?.();
|
|
907
|
+
const next = !root || root === form.ownerDocument ? form.ownerDocument.defaultView : root;
|
|
908
|
+
if (group.view !== next) {
|
|
909
|
+
group.view?.removeEventListener('submit', group.submit, true);
|
|
910
|
+
group.view = next;
|
|
911
|
+
next?.addEventListener('submit', group.submit, true);
|
|
811
912
|
}
|
|
812
913
|
group.fields.add(field);
|
|
813
914
|
return () => {
|
|
814
915
|
group.fields.delete(field);
|
|
815
|
-
if (!group.fields.size) { group.view
|
|
916
|
+
if (!group.fields.size) { group.view?.removeEventListener('submit', group.submit, true); forms.delete(form); }
|
|
816
917
|
};
|
|
817
918
|
}
|
|
818
919
|
|
|
@@ -846,23 +947,62 @@ export class RapierEditor extends (globalThis.HTMLElement || class {}) {
|
|
|
846
947
|
this._initialized = true;
|
|
847
948
|
}
|
|
848
949
|
this._disabled = this.matches(':disabled'); this.tabIndex = this._disabled ? -1 : 0;
|
|
849
|
-
|
|
850
|
-
|
|
950
|
+
if (!this._onPageHide) {
|
|
951
|
+
this._onPageHide = () => {
|
|
952
|
+
if (this._disabled) return;
|
|
953
|
+
this._setFormValue();
|
|
954
|
+
};
|
|
955
|
+
this._onPageShow = () => { if (!this._disabled) this._updateValue(); };
|
|
956
|
+
}
|
|
957
|
+
const bindPageLifecycle = () => {
|
|
958
|
+
if (!this._onPageHide) return;
|
|
959
|
+
const view = this.isConnected ? this.ownerDocument.defaultView : null;
|
|
960
|
+
if (this._view === view) return;
|
|
961
|
+
this._view?.removeEventListener('pagehide', this._onPageHide);
|
|
962
|
+
this._view?.removeEventListener('pageshow', this._onPageShow);
|
|
963
|
+
this._media?.removeEventListener('change', this._onMedia);
|
|
964
|
+
this._view = view;
|
|
965
|
+
this._media = view ? view.matchMedia('(max-width:600px)') : null;
|
|
966
|
+
view?.addEventListener('pagehide', this._onPageHide);
|
|
967
|
+
view?.addEventListener('pageshow', this._onPageShow);
|
|
968
|
+
if (this._media) {
|
|
969
|
+
this._onMedia ||= () => this._layout();
|
|
970
|
+
this._media.addEventListener('change', this._onMedia);
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
bindPageLifecycle();
|
|
974
|
+
queueMicrotask(bindPageLifecycle);
|
|
851
975
|
this._bindForm(); this._updateValue(); this._layout(); if (this._panel.open) this._start();
|
|
852
976
|
}
|
|
853
977
|
disconnectedCallback() {
|
|
854
978
|
this._formAuthority++;
|
|
855
979
|
queueMicrotask(() => {
|
|
856
980
|
if (this.isConnected) return;
|
|
857
|
-
this._leaveForm?.(); this._leaveForm = null;
|
|
858
|
-
this.
|
|
981
|
+
this._leaveForm?.(); this._leaveForm = null;
|
|
982
|
+
this._media?.removeEventListener('change', this._onMedia);
|
|
983
|
+
this._media = null;
|
|
984
|
+
this._view?.removeEventListener('pagehide', this._onPageHide);
|
|
985
|
+
this._view?.removeEventListener('pageshow', this._onPageShow);
|
|
986
|
+
this._view = null;
|
|
987
|
+
this._onPageHide = null; this._onPageShow = null;
|
|
988
|
+
const editor = this._editor;
|
|
989
|
+
this._generation++; this._editor = null; this._sync = Promise.resolve();
|
|
990
|
+
editor?.disconnect(); this._mount.replaceChildren();
|
|
859
991
|
});
|
|
860
992
|
}
|
|
861
993
|
attributeChangedCallback(name) {
|
|
862
994
|
if (!this._initialized) return;
|
|
863
995
|
if (name === 'disabled') this.formDisabledCallback(this.matches(':disabled'));
|
|
864
|
-
else if (name === 'theme')
|
|
865
|
-
|
|
996
|
+
else if (name === 'theme') {
|
|
997
|
+
const editor = this._editor;
|
|
998
|
+
editor?.theme(this.getAttribute('theme') || 'system').catch(error => { if (this._editor === editor) this._error(error); });
|
|
999
|
+
}
|
|
1000
|
+
else if (name === 'src') {
|
|
1001
|
+
const editor = this._editor, generation = this._generation, src = this.getAttribute('src');
|
|
1002
|
+
const current = () => this._editor === editor && this._generation === generation && this.getAttribute('src') === src;
|
|
1003
|
+
(editor?.connected ? this.save() : Promise.resolve()).then(() => { if (current()) this._remount(); })
|
|
1004
|
+
.catch(error => { if (current()) this._error(error); });
|
|
1005
|
+
}
|
|
866
1006
|
else this._updateValue();
|
|
867
1007
|
}
|
|
868
1008
|
get value() { return this._value; }
|
|
@@ -884,7 +1024,7 @@ export class RapierEditor extends (globalThis.HTMLElement || class {}) {
|
|
|
884
1024
|
_bindForm() { this._formAuthority++; this._leaveForm?.(); this._leaveForm = joinForm(this, this.form); }
|
|
885
1025
|
formAssociatedCallback() { if (this._initialized && this.isConnected) this._bindForm(); }
|
|
886
1026
|
formResetCallback() { this._replace(this._defaultValue); }
|
|
887
|
-
formStateRestoreCallback(state) { if (typeof state === 'string') this._replace(state); }
|
|
1027
|
+
formStateRestoreCallback(state, mode) { if (mode === 'restore' && typeof state === 'string') this._replace(state); }
|
|
888
1028
|
formDisabledCallback(disabled) {
|
|
889
1029
|
const wasDisabled = this._disabled; this._disabled = disabled;
|
|
890
1030
|
if (disabled !== wasDisabled) this._formAuthority++;
|
|
@@ -901,9 +1041,17 @@ export class RapierEditor extends (globalThis.HTMLElement || class {}) {
|
|
|
901
1041
|
}).catch(error => { if (this._editor === editor) this._error(error); });
|
|
902
1042
|
}
|
|
903
1043
|
_filename() { return (this.getAttribute('name') || 'document').slice(0, 240) + '.md'; }
|
|
1044
|
+
_submissionFile() {
|
|
1045
|
+
const File = this.ownerDocument.defaultView?.File;
|
|
1046
|
+
return File ? new File([this._value], this._filename(), {type: 'text/markdown'}) : null;
|
|
1047
|
+
}
|
|
1048
|
+
_setFormValue() {
|
|
1049
|
+
const file = this._submissionFile();
|
|
1050
|
+
if (!file) return;
|
|
1051
|
+
this._internals.setFormValue(file, null);
|
|
1052
|
+
}
|
|
904
1053
|
_updateValue() {
|
|
905
|
-
|
|
906
|
-
this._internals.setFormValue(new File([this._value], this._filename(), {type: 'text/markdown'}), this._value);
|
|
1054
|
+
this._setFormValue();
|
|
907
1055
|
this._preview.textContent = this._value || 'Edit document'; this._preview.disabled = this._disabled;
|
|
908
1056
|
if (this._fallback) this._fallback.value = this._value;
|
|
909
1057
|
const missing = this.required && !this._value && !this._dirty;
|
|
@@ -911,34 +1059,52 @@ export class RapierEditor extends (globalThis.HTMLElement || class {}) {
|
|
|
911
1059
|
this._internals.setValidity(error ? (this._customValidity ? {customError: true} : {valueMissing: true}) : {}, error, this._preview);
|
|
912
1060
|
}
|
|
913
1061
|
_replace(value) {
|
|
914
|
-
this._value = value; this._dirty = false; this._revision = 0;
|
|
915
|
-
this.
|
|
1062
|
+
this._value = value; this._dirty = false; this._revision = 0;
|
|
1063
|
+
this._ids = {sessionId: mint('field'), documentId: mint('field')};
|
|
1064
|
+
this._remount();
|
|
1065
|
+
}
|
|
1066
|
+
_remount() {
|
|
1067
|
+
const editor = this._editor;
|
|
1068
|
+
this._generation++; this._editor = null; this._sync = Promise.resolve();
|
|
1069
|
+
editor?.disconnect(); this._mount.replaceChildren(); this._updateValue();
|
|
916
1070
|
if (this._initialized && this.isConnected && this._panel.open) this._start();
|
|
917
1071
|
}
|
|
918
1072
|
_start() {
|
|
919
1073
|
if (this._editor) return;
|
|
1074
|
+
const load = {content: this._value, filename: this._filename(), revision: this._revision, readOnly: this._disabled};
|
|
1075
|
+
if (!this._ids) {
|
|
1076
|
+
const here = this.ownerDocument.defaultView.location;
|
|
1077
|
+
const auto = autoIdentity({load, save: true}, here, this.id);
|
|
1078
|
+
this._ids = {sessionId: 'page-' + auto, documentId: 'doc-' + auto};
|
|
1079
|
+
}
|
|
920
1080
|
const generation = this._generation;
|
|
921
|
-
|
|
1081
|
+
let editor;
|
|
1082
|
+
const current = () => this._generation === generation && this._editor === editor;
|
|
1083
|
+
editor = Rapier.mount(this._mount, {...this._ids,
|
|
922
1084
|
src: this.getAttribute('src') || DEFAULT_SRC, theme: this.getAttribute('theme') || 'system',
|
|
923
|
-
load
|
|
1085
|
+
load,
|
|
924
1086
|
save: ({content, baseRevision}) => {
|
|
925
|
-
if (
|
|
1087
|
+
if (!current()) throw coded('field_replaced', Error, 'The form field was replaced');
|
|
926
1088
|
if (baseRevision !== this._revision) throw conflict(this._revision);
|
|
927
1089
|
this._value = content; this._revision++; this._dirty = false; this._updateValue();
|
|
1090
|
+
const revision = this._revision;
|
|
928
1091
|
this.dispatchEvent(new Event('change', {bubbles: true, composed: true}));
|
|
929
|
-
return {revision
|
|
1092
|
+
return {revision};
|
|
930
1093
|
},
|
|
931
|
-
onState: state => { this._dirty = state.dirty; this._updateValue(); },
|
|
932
|
-
onError: error => this._error(error),
|
|
1094
|
+
onState: state => { if (current()) { this._dirty = state.dirty; this._updateValue(); } },
|
|
1095
|
+
onError: error => { if (current()) this._error(error); },
|
|
933
1096
|
});
|
|
1097
|
+
this._editor = editor;
|
|
934
1098
|
this._mount.inert = this._disabled;
|
|
935
1099
|
}
|
|
936
1100
|
_layout() {
|
|
937
|
-
const narrow = this._media
|
|
1101
|
+
const narrow = this._media?.matches === true;
|
|
938
1102
|
if (this._narrow === narrow) return;
|
|
939
1103
|
this._narrow = narrow;
|
|
940
1104
|
if (this._panel.open) this._panel.close();
|
|
941
|
-
if (
|
|
1105
|
+
if (narrow || !this.ownerDocument.defaultView) return;
|
|
1106
|
+
this._panel.setAttribute('open', '');
|
|
1107
|
+
this._start();
|
|
942
1108
|
}
|
|
943
1109
|
focus(options) {
|
|
944
1110
|
if (this._disabled) return;
|
|
@@ -946,13 +1112,18 @@ export class RapierEditor extends (globalThis.HTMLElement || class {}) {
|
|
|
946
1112
|
this._start(); this._editor.iframe.focus(options);
|
|
947
1113
|
}
|
|
948
1114
|
async save() {
|
|
1115
|
+
const generation = this._generation;
|
|
949
1116
|
await this._sync;
|
|
1117
|
+
if (generation !== this._generation) throw coded('field_replaced', Error, 'The form field was replaced');
|
|
950
1118
|
if (this._editor && !this._disabled) await this._editor.save();
|
|
1119
|
+
if (generation !== this._generation) throw coded('field_replaced', Error, 'The form field was replaced');
|
|
951
1120
|
return this._value;
|
|
952
1121
|
}
|
|
953
1122
|
async _finish() {
|
|
954
|
-
|
|
955
|
-
|
|
1123
|
+
const generation = this._generation, authority = this._formAuthority, narrow = this._narrow;
|
|
1124
|
+
const current = () => this._generation === generation && this._formAuthority === authority && this._narrow === narrow && this.isConnected;
|
|
1125
|
+
try { await this.save(); if (current()) { this._panel.close(); if (!this._disabled) this._preview.focus(); } }
|
|
1126
|
+
catch (error) { if (current()) this._error(error); }
|
|
956
1127
|
}
|
|
957
1128
|
_error(error) { this.dispatchEvent(new CustomEvent('error', {detail: error, bubbles: true, composed: true})); }
|
|
958
1129
|
}
|