turbo-rails 2.0.6 → 2.0.13
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.
- checksums.yaml +4 -4
- data/README.md +69 -1
- data/app/assets/javascripts/turbo.js +1165 -1033
- data/app/assets/javascripts/turbo.min.js +7 -7
- data/app/assets/javascripts/turbo.min.js.map +1 -1
- data/app/channels/turbo/streams/broadcasts.rb +17 -6
- data/app/controllers/turbo/native/navigation.rb +11 -8
- data/app/helpers/turbo/streams/action_helper.rb +1 -1
- data/app/helpers/turbo/streams_helper.rb +6 -0
- data/app/javascript/turbo/cable_stream_source_element.js +10 -0
- data/app/models/concerns/turbo/broadcastable.rb +32 -22
- data/app/models/turbo/streams/tag_builder.rb +28 -34
- data/config/routes.rb +3 -3
- data/lib/tasks/turbo_tasks.rake +0 -22
- data/lib/turbo/engine.rb +48 -3
- data/lib/turbo/system_test_helper.rb +128 -0
- data/lib/turbo/version.rb +1 -1
- metadata +9 -23
- data/lib/install/turbo_needs_redis.rb +0 -20
@@ -1,6 +1,6 @@
|
|
1
1
|
/*!
|
2
|
-
Turbo 8.0.
|
3
|
-
Copyright ©
|
2
|
+
Turbo 8.0.13
|
3
|
+
Copyright © 2025 37signals LLC
|
4
4
|
*/
|
5
5
|
(function(prototype) {
|
6
6
|
if (typeof prototype.requestSubmit == "function") return;
|
@@ -116,6 +116,9 @@ class FrameElement extends HTMLElement {
|
|
116
116
|
this.removeAttribute("refresh");
|
117
117
|
}
|
118
118
|
}
|
119
|
+
get shouldReloadWithMorph() {
|
120
|
+
return this.src && this.refresh === "morph";
|
121
|
+
}
|
119
122
|
get loading() {
|
120
123
|
return frameLoadingStyleFromString(this.getAttribute("loading") || "");
|
121
124
|
}
|
@@ -167,122 +170,18 @@ function frameLoadingStyleFromString(style) {
|
|
167
170
|
}
|
168
171
|
}
|
169
172
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
let anchorMatch;
|
176
|
-
if (url.hash) {
|
177
|
-
return url.hash.slice(1);
|
178
|
-
} else if (anchorMatch = url.href.match(/#(.*)$/)) {
|
179
|
-
return anchorMatch[1];
|
180
|
-
}
|
181
|
-
}
|
182
|
-
|
183
|
-
function getAction$1(form, submitter) {
|
184
|
-
const action = submitter?.getAttribute("formaction") || form.getAttribute("action") || form.action;
|
185
|
-
return expandURL(action);
|
186
|
-
}
|
187
|
-
|
188
|
-
function getExtension(url) {
|
189
|
-
return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
|
190
|
-
}
|
191
|
-
|
192
|
-
function isHTML(url) {
|
193
|
-
return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml|php))$/);
|
194
|
-
}
|
195
|
-
|
196
|
-
function isPrefixedBy(baseURL, url) {
|
197
|
-
const prefix = getPrefix(url);
|
198
|
-
return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
|
199
|
-
}
|
200
|
-
|
201
|
-
function locationIsVisitable(location, rootLocation) {
|
202
|
-
return isPrefixedBy(location, rootLocation) && isHTML(location);
|
203
|
-
}
|
204
|
-
|
205
|
-
function getRequestURL(url) {
|
206
|
-
const anchor = getAnchor(url);
|
207
|
-
return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href;
|
208
|
-
}
|
209
|
-
|
210
|
-
function toCacheKey(url) {
|
211
|
-
return getRequestURL(url);
|
212
|
-
}
|
213
|
-
|
214
|
-
function urlsAreEqual(left, right) {
|
215
|
-
return expandURL(left).href == expandURL(right).href;
|
216
|
-
}
|
217
|
-
|
218
|
-
function getPathComponents(url) {
|
219
|
-
return url.pathname.split("/").slice(1);
|
220
|
-
}
|
221
|
-
|
222
|
-
function getLastPathComponent(url) {
|
223
|
-
return getPathComponents(url).slice(-1)[0];
|
224
|
-
}
|
225
|
-
|
226
|
-
function getPrefix(url) {
|
227
|
-
return addTrailingSlash(url.origin + url.pathname);
|
228
|
-
}
|
229
|
-
|
230
|
-
function addTrailingSlash(value) {
|
231
|
-
return value.endsWith("/") ? value : value + "/";
|
232
|
-
}
|
233
|
-
|
234
|
-
class FetchResponse {
|
235
|
-
constructor(response) {
|
236
|
-
this.response = response;
|
237
|
-
}
|
238
|
-
get succeeded() {
|
239
|
-
return this.response.ok;
|
240
|
-
}
|
241
|
-
get failed() {
|
242
|
-
return !this.succeeded;
|
243
|
-
}
|
244
|
-
get clientError() {
|
245
|
-
return this.statusCode >= 400 && this.statusCode <= 499;
|
246
|
-
}
|
247
|
-
get serverError() {
|
248
|
-
return this.statusCode >= 500 && this.statusCode <= 599;
|
249
|
-
}
|
250
|
-
get redirected() {
|
251
|
-
return this.response.redirected;
|
252
|
-
}
|
253
|
-
get location() {
|
254
|
-
return expandURL(this.response.url);
|
255
|
-
}
|
256
|
-
get isHTML() {
|
257
|
-
return this.contentType && this.contentType.match(/^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/);
|
258
|
-
}
|
259
|
-
get statusCode() {
|
260
|
-
return this.response.status;
|
261
|
-
}
|
262
|
-
get contentType() {
|
263
|
-
return this.header("Content-Type");
|
264
|
-
}
|
265
|
-
get responseText() {
|
266
|
-
return this.response.clone().text();
|
267
|
-
}
|
268
|
-
get responseHTML() {
|
269
|
-
if (this.isHTML) {
|
270
|
-
return this.response.clone().text();
|
271
|
-
} else {
|
272
|
-
return Promise.resolve(undefined);
|
273
|
-
}
|
274
|
-
}
|
275
|
-
header(name) {
|
276
|
-
return this.response.headers.get(name);
|
277
|
-
}
|
278
|
-
}
|
173
|
+
const drive = {
|
174
|
+
enabled: true,
|
175
|
+
progressBarDelay: 500,
|
176
|
+
unvisitableExtensions: new Set([ ".7z", ".aac", ".apk", ".avi", ".bmp", ".bz2", ".css", ".csv", ".deb", ".dmg", ".doc", ".docx", ".exe", ".gif", ".gz", ".heic", ".heif", ".ico", ".iso", ".jpeg", ".jpg", ".js", ".json", ".m4a", ".mkv", ".mov", ".mp3", ".mp4", ".mpeg", ".mpg", ".msi", ".ogg", ".ogv", ".pdf", ".pkg", ".png", ".ppt", ".pptx", ".rar", ".rtf", ".svg", ".tar", ".tif", ".tiff", ".txt", ".wav", ".webm", ".webp", ".wma", ".wmv", ".xls", ".xlsx", ".xml", ".zip" ])
|
177
|
+
};
|
279
178
|
|
280
179
|
function activateScriptElement(element) {
|
281
180
|
if (element.getAttribute("data-turbo-eval") == "false") {
|
282
181
|
return element;
|
283
182
|
} else {
|
284
183
|
const createdScriptElement = document.createElement("script");
|
285
|
-
const cspNonce =
|
184
|
+
const cspNonce = getCspNonce();
|
286
185
|
if (cspNonce) {
|
287
186
|
createdScriptElement.nonce = cspNonce;
|
288
187
|
}
|
@@ -320,6 +219,11 @@ function dispatch(eventName, {target: target, cancelable: cancelable, detail: de
|
|
320
219
|
return event;
|
321
220
|
}
|
322
221
|
|
222
|
+
function cancelEvent(event) {
|
223
|
+
event.preventDefault();
|
224
|
+
event.stopImmediatePropagation();
|
225
|
+
}
|
226
|
+
|
323
227
|
function nextRepaint() {
|
324
228
|
if (document.visibilityState === "hidden") {
|
325
229
|
return nextEventLoopTick();
|
@@ -449,6 +353,14 @@ function getMetaContent(name) {
|
|
449
353
|
return element && element.content;
|
450
354
|
}
|
451
355
|
|
356
|
+
function getCspNonce() {
|
357
|
+
const element = getMetaElement("csp-nonce");
|
358
|
+
if (element) {
|
359
|
+
const {nonce: nonce, content: content} = element;
|
360
|
+
return nonce == "" ? content : nonce;
|
361
|
+
}
|
362
|
+
}
|
363
|
+
|
452
364
|
function setMetaContent(name, content) {
|
453
365
|
let element = getMetaElement(name);
|
454
366
|
if (!element) {
|
@@ -513,6 +425,152 @@ function debounce(fn, delay) {
|
|
513
425
|
};
|
514
426
|
}
|
515
427
|
|
428
|
+
const submitter = {
|
429
|
+
"aria-disabled": {
|
430
|
+
beforeSubmit: submitter => {
|
431
|
+
submitter.setAttribute("aria-disabled", "true");
|
432
|
+
submitter.addEventListener("click", cancelEvent);
|
433
|
+
},
|
434
|
+
afterSubmit: submitter => {
|
435
|
+
submitter.removeAttribute("aria-disabled");
|
436
|
+
submitter.removeEventListener("click", cancelEvent);
|
437
|
+
}
|
438
|
+
},
|
439
|
+
disabled: {
|
440
|
+
beforeSubmit: submitter => submitter.disabled = true,
|
441
|
+
afterSubmit: submitter => submitter.disabled = false
|
442
|
+
}
|
443
|
+
};
|
444
|
+
|
445
|
+
class Config {
|
446
|
+
#submitter=null;
|
447
|
+
constructor(config) {
|
448
|
+
Object.assign(this, config);
|
449
|
+
}
|
450
|
+
get submitter() {
|
451
|
+
return this.#submitter;
|
452
|
+
}
|
453
|
+
set submitter(value) {
|
454
|
+
this.#submitter = submitter[value] || value;
|
455
|
+
}
|
456
|
+
}
|
457
|
+
|
458
|
+
const forms = new Config({
|
459
|
+
mode: "on",
|
460
|
+
submitter: "disabled"
|
461
|
+
});
|
462
|
+
|
463
|
+
const config = {
|
464
|
+
drive: drive,
|
465
|
+
forms: forms
|
466
|
+
};
|
467
|
+
|
468
|
+
function expandURL(locatable) {
|
469
|
+
return new URL(locatable.toString(), document.baseURI);
|
470
|
+
}
|
471
|
+
|
472
|
+
function getAnchor(url) {
|
473
|
+
let anchorMatch;
|
474
|
+
if (url.hash) {
|
475
|
+
return url.hash.slice(1);
|
476
|
+
} else if (anchorMatch = url.href.match(/#(.*)$/)) {
|
477
|
+
return anchorMatch[1];
|
478
|
+
}
|
479
|
+
}
|
480
|
+
|
481
|
+
function getAction$1(form, submitter) {
|
482
|
+
const action = submitter?.getAttribute("formaction") || form.getAttribute("action") || form.action;
|
483
|
+
return expandURL(action);
|
484
|
+
}
|
485
|
+
|
486
|
+
function getExtension(url) {
|
487
|
+
return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
|
488
|
+
}
|
489
|
+
|
490
|
+
function isPrefixedBy(baseURL, url) {
|
491
|
+
const prefix = getPrefix(url);
|
492
|
+
return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
|
493
|
+
}
|
494
|
+
|
495
|
+
function locationIsVisitable(location, rootLocation) {
|
496
|
+
return isPrefixedBy(location, rootLocation) && !config.drive.unvisitableExtensions.has(getExtension(location));
|
497
|
+
}
|
498
|
+
|
499
|
+
function getRequestURL(url) {
|
500
|
+
const anchor = getAnchor(url);
|
501
|
+
return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href;
|
502
|
+
}
|
503
|
+
|
504
|
+
function toCacheKey(url) {
|
505
|
+
return getRequestURL(url);
|
506
|
+
}
|
507
|
+
|
508
|
+
function urlsAreEqual(left, right) {
|
509
|
+
return expandURL(left).href == expandURL(right).href;
|
510
|
+
}
|
511
|
+
|
512
|
+
function getPathComponents(url) {
|
513
|
+
return url.pathname.split("/").slice(1);
|
514
|
+
}
|
515
|
+
|
516
|
+
function getLastPathComponent(url) {
|
517
|
+
return getPathComponents(url).slice(-1)[0];
|
518
|
+
}
|
519
|
+
|
520
|
+
function getPrefix(url) {
|
521
|
+
return addTrailingSlash(url.origin + url.pathname);
|
522
|
+
}
|
523
|
+
|
524
|
+
function addTrailingSlash(value) {
|
525
|
+
return value.endsWith("/") ? value : value + "/";
|
526
|
+
}
|
527
|
+
|
528
|
+
class FetchResponse {
|
529
|
+
constructor(response) {
|
530
|
+
this.response = response;
|
531
|
+
}
|
532
|
+
get succeeded() {
|
533
|
+
return this.response.ok;
|
534
|
+
}
|
535
|
+
get failed() {
|
536
|
+
return !this.succeeded;
|
537
|
+
}
|
538
|
+
get clientError() {
|
539
|
+
return this.statusCode >= 400 && this.statusCode <= 499;
|
540
|
+
}
|
541
|
+
get serverError() {
|
542
|
+
return this.statusCode >= 500 && this.statusCode <= 599;
|
543
|
+
}
|
544
|
+
get redirected() {
|
545
|
+
return this.response.redirected;
|
546
|
+
}
|
547
|
+
get location() {
|
548
|
+
return expandURL(this.response.url);
|
549
|
+
}
|
550
|
+
get isHTML() {
|
551
|
+
return this.contentType && this.contentType.match(/^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/);
|
552
|
+
}
|
553
|
+
get statusCode() {
|
554
|
+
return this.response.status;
|
555
|
+
}
|
556
|
+
get contentType() {
|
557
|
+
return this.header("Content-Type");
|
558
|
+
}
|
559
|
+
get responseText() {
|
560
|
+
return this.response.clone().text();
|
561
|
+
}
|
562
|
+
get responseHTML() {
|
563
|
+
if (this.isHTML) {
|
564
|
+
return this.response.clone().text();
|
565
|
+
} else {
|
566
|
+
return Promise.resolve(undefined);
|
567
|
+
}
|
568
|
+
}
|
569
|
+
header(name) {
|
570
|
+
return this.response.headers.get(name);
|
571
|
+
}
|
572
|
+
}
|
573
|
+
|
516
574
|
class LimitedSet extends Set {
|
517
575
|
constructor(maxSize) {
|
518
576
|
super();
|
@@ -861,7 +919,7 @@ const FormSubmissionState = {
|
|
861
919
|
|
862
920
|
class FormSubmission {
|
863
921
|
state=FormSubmissionState.initialized;
|
864
|
-
static confirmMethod(message
|
922
|
+
static confirmMethod(message) {
|
865
923
|
return Promise.resolve(confirm(message));
|
866
924
|
}
|
867
925
|
constructor(delegate, formElement, submitter, mustRedirect = false) {
|
@@ -903,7 +961,8 @@ class FormSubmission {
|
|
903
961
|
const {initialized: initialized, requesting: requesting} = FormSubmissionState;
|
904
962
|
const confirmationMessage = getAttribute("data-turbo-confirm", this.submitter, this.formElement);
|
905
963
|
if (typeof confirmationMessage === "string") {
|
906
|
-
const
|
964
|
+
const confirmMethod = typeof config.forms.confirm === "function" ? config.forms.confirm : FormSubmission.confirmMethod;
|
965
|
+
const answer = await confirmMethod(confirmationMessage, this.formElement, this.submitter);
|
907
966
|
if (!answer) {
|
908
967
|
return;
|
909
968
|
}
|
@@ -934,7 +993,7 @@ class FormSubmission {
|
|
934
993
|
}
|
935
994
|
requestStarted(_request) {
|
936
995
|
this.state = FormSubmissionState.waiting;
|
937
|
-
this.submitter
|
996
|
+
if (this.submitter) config.forms.submitter.beforeSubmit(this.submitter);
|
938
997
|
this.setSubmitsWith();
|
939
998
|
markAsBusy(this.formElement);
|
940
999
|
dispatch("turbo:submit-start", {
|
@@ -986,7 +1045,7 @@ class FormSubmission {
|
|
986
1045
|
}
|
987
1046
|
requestFinished(_request) {
|
988
1047
|
this.state = FormSubmissionState.stopped;
|
989
|
-
this.submitter
|
1048
|
+
if (this.submitter) config.forms.submitter.afterSubmit(this.submitter);
|
990
1049
|
this.resetSubmitterText();
|
991
1050
|
clearBusyState(this.formElement);
|
992
1051
|
dispatch("turbo:submit-end", {
|
@@ -1480,12 +1539,13 @@ function createPlaceholderForPermanentElement(permanentElement) {
|
|
1480
1539
|
|
1481
1540
|
class Renderer {
|
1482
1541
|
#activeElement=null;
|
1483
|
-
|
1542
|
+
static renderElement(currentElement, newElement) {}
|
1543
|
+
constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) {
|
1484
1544
|
this.currentSnapshot = currentSnapshot;
|
1485
1545
|
this.newSnapshot = newSnapshot;
|
1486
1546
|
this.isPreview = isPreview;
|
1487
1547
|
this.willRender = willRender;
|
1488
|
-
this.renderElement = renderElement;
|
1548
|
+
this.renderElement = this.constructor.renderElement;
|
1489
1549
|
this.promise = new Promise(((resolve, reject) => this.resolvingFunctions = {
|
1490
1550
|
resolve: resolve,
|
1491
1551
|
reject: reject
|
@@ -1626,28 +1686,693 @@ function readScrollBehavior(value, defaultValue) {
|
|
1626
1686
|
}
|
1627
1687
|
}
|
1628
1688
|
|
1629
|
-
|
1630
|
-
|
1631
|
-
|
1632
|
-
|
1633
|
-
|
1634
|
-
|
1635
|
-
|
1636
|
-
|
1637
|
-
|
1638
|
-
|
1639
|
-
|
1640
|
-
|
1641
|
-
|
1642
|
-
|
1643
|
-
|
1644
|
-
|
1645
|
-
|
1646
|
-
|
1647
|
-
|
1648
|
-
|
1649
|
-
|
1650
|
-
|
1689
|
+
var Idiomorph = function() {
|
1690
|
+
const noOp = () => {};
|
1691
|
+
const defaults = {
|
1692
|
+
morphStyle: "outerHTML",
|
1693
|
+
callbacks: {
|
1694
|
+
beforeNodeAdded: noOp,
|
1695
|
+
afterNodeAdded: noOp,
|
1696
|
+
beforeNodeMorphed: noOp,
|
1697
|
+
afterNodeMorphed: noOp,
|
1698
|
+
beforeNodeRemoved: noOp,
|
1699
|
+
afterNodeRemoved: noOp,
|
1700
|
+
beforeAttributeUpdated: noOp
|
1701
|
+
},
|
1702
|
+
head: {
|
1703
|
+
style: "merge",
|
1704
|
+
shouldPreserve: elt => elt.getAttribute("im-preserve") === "true",
|
1705
|
+
shouldReAppend: elt => elt.getAttribute("im-re-append") === "true",
|
1706
|
+
shouldRemove: noOp,
|
1707
|
+
afterHeadMorphed: noOp
|
1708
|
+
},
|
1709
|
+
restoreFocus: true
|
1710
|
+
};
|
1711
|
+
function morph(oldNode, newContent, config = {}) {
|
1712
|
+
oldNode = normalizeElement(oldNode);
|
1713
|
+
const newNode = normalizeParent(newContent);
|
1714
|
+
const ctx = createMorphContext(oldNode, newNode, config);
|
1715
|
+
const morphedNodes = saveAndRestoreFocus(ctx, (() => withHeadBlocking(ctx, oldNode, newNode, (ctx => {
|
1716
|
+
if (ctx.morphStyle === "innerHTML") {
|
1717
|
+
morphChildren(ctx, oldNode, newNode);
|
1718
|
+
return Array.from(oldNode.childNodes);
|
1719
|
+
} else {
|
1720
|
+
return morphOuterHTML(ctx, oldNode, newNode);
|
1721
|
+
}
|
1722
|
+
}))));
|
1723
|
+
ctx.pantry.remove();
|
1724
|
+
return morphedNodes;
|
1725
|
+
}
|
1726
|
+
function morphOuterHTML(ctx, oldNode, newNode) {
|
1727
|
+
const oldParent = normalizeParent(oldNode);
|
1728
|
+
let childNodes = Array.from(oldParent.childNodes);
|
1729
|
+
const index = childNodes.indexOf(oldNode);
|
1730
|
+
const rightMargin = childNodes.length - (index + 1);
|
1731
|
+
morphChildren(ctx, oldParent, newNode, oldNode, oldNode.nextSibling);
|
1732
|
+
childNodes = Array.from(oldParent.childNodes);
|
1733
|
+
return childNodes.slice(index, childNodes.length - rightMargin);
|
1734
|
+
}
|
1735
|
+
function saveAndRestoreFocus(ctx, fn) {
|
1736
|
+
if (!ctx.config.restoreFocus) return fn();
|
1737
|
+
let activeElement = document.activeElement;
|
1738
|
+
if (!(activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement)) {
|
1739
|
+
return fn();
|
1740
|
+
}
|
1741
|
+
const {id: activeElementId, selectionStart: selectionStart, selectionEnd: selectionEnd} = activeElement;
|
1742
|
+
const results = fn();
|
1743
|
+
if (activeElementId && activeElementId !== document.activeElement?.id) {
|
1744
|
+
activeElement = ctx.target.querySelector(`#${activeElementId}`);
|
1745
|
+
activeElement?.focus();
|
1746
|
+
}
|
1747
|
+
if (activeElement && !activeElement.selectionEnd && selectionEnd) {
|
1748
|
+
activeElement.setSelectionRange(selectionStart, selectionEnd);
|
1749
|
+
}
|
1750
|
+
return results;
|
1751
|
+
}
|
1752
|
+
const morphChildren = function() {
|
1753
|
+
function morphChildren(ctx, oldParent, newParent, insertionPoint = null, endPoint = null) {
|
1754
|
+
if (oldParent instanceof HTMLTemplateElement && newParent instanceof HTMLTemplateElement) {
|
1755
|
+
oldParent = oldParent.content;
|
1756
|
+
newParent = newParent.content;
|
1757
|
+
}
|
1758
|
+
insertionPoint ||= oldParent.firstChild;
|
1759
|
+
for (const newChild of newParent.childNodes) {
|
1760
|
+
if (insertionPoint && insertionPoint != endPoint) {
|
1761
|
+
const bestMatch = findBestMatch(ctx, newChild, insertionPoint, endPoint);
|
1762
|
+
if (bestMatch) {
|
1763
|
+
if (bestMatch !== insertionPoint) {
|
1764
|
+
removeNodesBetween(ctx, insertionPoint, bestMatch);
|
1765
|
+
}
|
1766
|
+
morphNode(bestMatch, newChild, ctx);
|
1767
|
+
insertionPoint = bestMatch.nextSibling;
|
1768
|
+
continue;
|
1769
|
+
}
|
1770
|
+
}
|
1771
|
+
if (newChild instanceof Element && ctx.persistentIds.has(newChild.id)) {
|
1772
|
+
const movedChild = moveBeforeById(oldParent, newChild.id, insertionPoint, ctx);
|
1773
|
+
morphNode(movedChild, newChild, ctx);
|
1774
|
+
insertionPoint = movedChild.nextSibling;
|
1775
|
+
continue;
|
1776
|
+
}
|
1777
|
+
const insertedNode = createNode(oldParent, newChild, insertionPoint, ctx);
|
1778
|
+
if (insertedNode) {
|
1779
|
+
insertionPoint = insertedNode.nextSibling;
|
1780
|
+
}
|
1781
|
+
}
|
1782
|
+
while (insertionPoint && insertionPoint != endPoint) {
|
1783
|
+
const tempNode = insertionPoint;
|
1784
|
+
insertionPoint = insertionPoint.nextSibling;
|
1785
|
+
removeNode(ctx, tempNode);
|
1786
|
+
}
|
1787
|
+
}
|
1788
|
+
function createNode(oldParent, newChild, insertionPoint, ctx) {
|
1789
|
+
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return null;
|
1790
|
+
if (ctx.idMap.has(newChild)) {
|
1791
|
+
const newEmptyChild = document.createElement(newChild.tagName);
|
1792
|
+
oldParent.insertBefore(newEmptyChild, insertionPoint);
|
1793
|
+
morphNode(newEmptyChild, newChild, ctx);
|
1794
|
+
ctx.callbacks.afterNodeAdded(newEmptyChild);
|
1795
|
+
return newEmptyChild;
|
1796
|
+
} else {
|
1797
|
+
const newClonedChild = document.importNode(newChild, true);
|
1798
|
+
oldParent.insertBefore(newClonedChild, insertionPoint);
|
1799
|
+
ctx.callbacks.afterNodeAdded(newClonedChild);
|
1800
|
+
return newClonedChild;
|
1801
|
+
}
|
1802
|
+
}
|
1803
|
+
const findBestMatch = function() {
|
1804
|
+
function findBestMatch(ctx, node, startPoint, endPoint) {
|
1805
|
+
let softMatch = null;
|
1806
|
+
let nextSibling = node.nextSibling;
|
1807
|
+
let siblingSoftMatchCount = 0;
|
1808
|
+
let cursor = startPoint;
|
1809
|
+
while (cursor && cursor != endPoint) {
|
1810
|
+
if (isSoftMatch(cursor, node)) {
|
1811
|
+
if (isIdSetMatch(ctx, cursor, node)) {
|
1812
|
+
return cursor;
|
1813
|
+
}
|
1814
|
+
if (softMatch === null) {
|
1815
|
+
if (!ctx.idMap.has(cursor)) {
|
1816
|
+
softMatch = cursor;
|
1817
|
+
}
|
1818
|
+
}
|
1819
|
+
}
|
1820
|
+
if (softMatch === null && nextSibling && isSoftMatch(cursor, nextSibling)) {
|
1821
|
+
siblingSoftMatchCount++;
|
1822
|
+
nextSibling = nextSibling.nextSibling;
|
1823
|
+
if (siblingSoftMatchCount >= 2) {
|
1824
|
+
softMatch = undefined;
|
1825
|
+
}
|
1826
|
+
}
|
1827
|
+
if (cursor.contains(document.activeElement)) break;
|
1828
|
+
cursor = cursor.nextSibling;
|
1829
|
+
}
|
1830
|
+
return softMatch || null;
|
1831
|
+
}
|
1832
|
+
function isIdSetMatch(ctx, oldNode, newNode) {
|
1833
|
+
let oldSet = ctx.idMap.get(oldNode);
|
1834
|
+
let newSet = ctx.idMap.get(newNode);
|
1835
|
+
if (!newSet || !oldSet) return false;
|
1836
|
+
for (const id of oldSet) {
|
1837
|
+
if (newSet.has(id)) {
|
1838
|
+
return true;
|
1839
|
+
}
|
1840
|
+
}
|
1841
|
+
return false;
|
1842
|
+
}
|
1843
|
+
function isSoftMatch(oldNode, newNode) {
|
1844
|
+
const oldElt = oldNode;
|
1845
|
+
const newElt = newNode;
|
1846
|
+
return oldElt.nodeType === newElt.nodeType && oldElt.tagName === newElt.tagName && (!oldElt.id || oldElt.id === newElt.id);
|
1847
|
+
}
|
1848
|
+
return findBestMatch;
|
1849
|
+
}();
|
1850
|
+
function removeNode(ctx, node) {
|
1851
|
+
if (ctx.idMap.has(node)) {
|
1852
|
+
moveBefore(ctx.pantry, node, null);
|
1853
|
+
} else {
|
1854
|
+
if (ctx.callbacks.beforeNodeRemoved(node) === false) return;
|
1855
|
+
node.parentNode?.removeChild(node);
|
1856
|
+
ctx.callbacks.afterNodeRemoved(node);
|
1857
|
+
}
|
1858
|
+
}
|
1859
|
+
function removeNodesBetween(ctx, startInclusive, endExclusive) {
|
1860
|
+
let cursor = startInclusive;
|
1861
|
+
while (cursor && cursor !== endExclusive) {
|
1862
|
+
let tempNode = cursor;
|
1863
|
+
cursor = cursor.nextSibling;
|
1864
|
+
removeNode(ctx, tempNode);
|
1865
|
+
}
|
1866
|
+
return cursor;
|
1867
|
+
}
|
1868
|
+
function moveBeforeById(parentNode, id, after, ctx) {
|
1869
|
+
const target = ctx.target.querySelector(`#${id}`) || ctx.pantry.querySelector(`#${id}`);
|
1870
|
+
removeElementFromAncestorsIdMaps(target, ctx);
|
1871
|
+
moveBefore(parentNode, target, after);
|
1872
|
+
return target;
|
1873
|
+
}
|
1874
|
+
function removeElementFromAncestorsIdMaps(element, ctx) {
|
1875
|
+
const id = element.id;
|
1876
|
+
while (element = element.parentNode) {
|
1877
|
+
let idSet = ctx.idMap.get(element);
|
1878
|
+
if (idSet) {
|
1879
|
+
idSet.delete(id);
|
1880
|
+
if (!idSet.size) {
|
1881
|
+
ctx.idMap.delete(element);
|
1882
|
+
}
|
1883
|
+
}
|
1884
|
+
}
|
1885
|
+
}
|
1886
|
+
function moveBefore(parentNode, element, after) {
|
1887
|
+
if (parentNode.moveBefore) {
|
1888
|
+
try {
|
1889
|
+
parentNode.moveBefore(element, after);
|
1890
|
+
} catch (e) {
|
1891
|
+
parentNode.insertBefore(element, after);
|
1892
|
+
}
|
1893
|
+
} else {
|
1894
|
+
parentNode.insertBefore(element, after);
|
1895
|
+
}
|
1896
|
+
}
|
1897
|
+
return morphChildren;
|
1898
|
+
}();
|
1899
|
+
const morphNode = function() {
|
1900
|
+
function morphNode(oldNode, newContent, ctx) {
|
1901
|
+
if (ctx.ignoreActive && oldNode === document.activeElement) {
|
1902
|
+
return null;
|
1903
|
+
}
|
1904
|
+
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) {
|
1905
|
+
return oldNode;
|
1906
|
+
}
|
1907
|
+
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
|
1908
|
+
handleHeadElement(oldNode, newContent, ctx);
|
1909
|
+
} else {
|
1910
|
+
morphAttributes(oldNode, newContent, ctx);
|
1911
|
+
if (!ignoreValueOfActiveElement(oldNode, ctx)) {
|
1912
|
+
morphChildren(ctx, oldNode, newContent);
|
1913
|
+
}
|
1914
|
+
}
|
1915
|
+
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
|
1916
|
+
return oldNode;
|
1917
|
+
}
|
1918
|
+
function morphAttributes(oldNode, newNode, ctx) {
|
1919
|
+
let type = newNode.nodeType;
|
1920
|
+
if (type === 1) {
|
1921
|
+
const oldElt = oldNode;
|
1922
|
+
const newElt = newNode;
|
1923
|
+
const oldAttributes = oldElt.attributes;
|
1924
|
+
const newAttributes = newElt.attributes;
|
1925
|
+
for (const newAttribute of newAttributes) {
|
1926
|
+
if (ignoreAttribute(newAttribute.name, oldElt, "update", ctx)) {
|
1927
|
+
continue;
|
1928
|
+
}
|
1929
|
+
if (oldElt.getAttribute(newAttribute.name) !== newAttribute.value) {
|
1930
|
+
oldElt.setAttribute(newAttribute.name, newAttribute.value);
|
1931
|
+
}
|
1932
|
+
}
|
1933
|
+
for (let i = oldAttributes.length - 1; 0 <= i; i--) {
|
1934
|
+
const oldAttribute = oldAttributes[i];
|
1935
|
+
if (!oldAttribute) continue;
|
1936
|
+
if (!newElt.hasAttribute(oldAttribute.name)) {
|
1937
|
+
if (ignoreAttribute(oldAttribute.name, oldElt, "remove", ctx)) {
|
1938
|
+
continue;
|
1939
|
+
}
|
1940
|
+
oldElt.removeAttribute(oldAttribute.name);
|
1941
|
+
}
|
1942
|
+
}
|
1943
|
+
if (!ignoreValueOfActiveElement(oldElt, ctx)) {
|
1944
|
+
syncInputValue(oldElt, newElt, ctx);
|
1945
|
+
}
|
1946
|
+
}
|
1947
|
+
if (type === 8 || type === 3) {
|
1948
|
+
if (oldNode.nodeValue !== newNode.nodeValue) {
|
1949
|
+
oldNode.nodeValue = newNode.nodeValue;
|
1950
|
+
}
|
1951
|
+
}
|
1952
|
+
}
|
1953
|
+
function syncInputValue(oldElement, newElement, ctx) {
|
1954
|
+
if (oldElement instanceof HTMLInputElement && newElement instanceof HTMLInputElement && newElement.type !== "file") {
|
1955
|
+
let newValue = newElement.value;
|
1956
|
+
let oldValue = oldElement.value;
|
1957
|
+
syncBooleanAttribute(oldElement, newElement, "checked", ctx);
|
1958
|
+
syncBooleanAttribute(oldElement, newElement, "disabled", ctx);
|
1959
|
+
if (!newElement.hasAttribute("value")) {
|
1960
|
+
if (!ignoreAttribute("value", oldElement, "remove", ctx)) {
|
1961
|
+
oldElement.value = "";
|
1962
|
+
oldElement.removeAttribute("value");
|
1963
|
+
}
|
1964
|
+
} else if (oldValue !== newValue) {
|
1965
|
+
if (!ignoreAttribute("value", oldElement, "update", ctx)) {
|
1966
|
+
oldElement.setAttribute("value", newValue);
|
1967
|
+
oldElement.value = newValue;
|
1968
|
+
}
|
1969
|
+
}
|
1970
|
+
} else if (oldElement instanceof HTMLOptionElement && newElement instanceof HTMLOptionElement) {
|
1971
|
+
syncBooleanAttribute(oldElement, newElement, "selected", ctx);
|
1972
|
+
} else if (oldElement instanceof HTMLTextAreaElement && newElement instanceof HTMLTextAreaElement) {
|
1973
|
+
let newValue = newElement.value;
|
1974
|
+
let oldValue = oldElement.value;
|
1975
|
+
if (ignoreAttribute("value", oldElement, "update", ctx)) {
|
1976
|
+
return;
|
1977
|
+
}
|
1978
|
+
if (newValue !== oldValue) {
|
1979
|
+
oldElement.value = newValue;
|
1980
|
+
}
|
1981
|
+
if (oldElement.firstChild && oldElement.firstChild.nodeValue !== newValue) {
|
1982
|
+
oldElement.firstChild.nodeValue = newValue;
|
1983
|
+
}
|
1984
|
+
}
|
1985
|
+
}
|
1986
|
+
function syncBooleanAttribute(oldElement, newElement, attributeName, ctx) {
|
1987
|
+
const newLiveValue = newElement[attributeName], oldLiveValue = oldElement[attributeName];
|
1988
|
+
if (newLiveValue !== oldLiveValue) {
|
1989
|
+
const ignoreUpdate = ignoreAttribute(attributeName, oldElement, "update", ctx);
|
1990
|
+
if (!ignoreUpdate) {
|
1991
|
+
oldElement[attributeName] = newElement[attributeName];
|
1992
|
+
}
|
1993
|
+
if (newLiveValue) {
|
1994
|
+
if (!ignoreUpdate) {
|
1995
|
+
oldElement.setAttribute(attributeName, "");
|
1996
|
+
}
|
1997
|
+
} else {
|
1998
|
+
if (!ignoreAttribute(attributeName, oldElement, "remove", ctx)) {
|
1999
|
+
oldElement.removeAttribute(attributeName);
|
2000
|
+
}
|
2001
|
+
}
|
2002
|
+
}
|
2003
|
+
}
|
2004
|
+
function ignoreAttribute(attr, element, updateType, ctx) {
|
2005
|
+
if (attr === "value" && ctx.ignoreActiveValue && element === document.activeElement) {
|
2006
|
+
return true;
|
2007
|
+
}
|
2008
|
+
return ctx.callbacks.beforeAttributeUpdated(attr, element, updateType) === false;
|
2009
|
+
}
|
2010
|
+
function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
|
2011
|
+
return !!ctx.ignoreActiveValue && possibleActiveElement === document.activeElement && possibleActiveElement !== document.body;
|
2012
|
+
}
|
2013
|
+
return morphNode;
|
2014
|
+
}();
|
2015
|
+
function withHeadBlocking(ctx, oldNode, newNode, callback) {
|
2016
|
+
if (ctx.head.block) {
|
2017
|
+
const oldHead = oldNode.querySelector("head");
|
2018
|
+
const newHead = newNode.querySelector("head");
|
2019
|
+
if (oldHead && newHead) {
|
2020
|
+
const promises = handleHeadElement(oldHead, newHead, ctx);
|
2021
|
+
return Promise.all(promises).then((() => {
|
2022
|
+
const newCtx = Object.assign(ctx, {
|
2023
|
+
head: {
|
2024
|
+
block: false,
|
2025
|
+
ignore: true
|
2026
|
+
}
|
2027
|
+
});
|
2028
|
+
return callback(newCtx);
|
2029
|
+
}));
|
2030
|
+
}
|
2031
|
+
}
|
2032
|
+
return callback(ctx);
|
2033
|
+
}
|
2034
|
+
function handleHeadElement(oldHead, newHead, ctx) {
|
2035
|
+
let added = [];
|
2036
|
+
let removed = [];
|
2037
|
+
let preserved = [];
|
2038
|
+
let nodesToAppend = [];
|
2039
|
+
let srcToNewHeadNodes = new Map;
|
2040
|
+
for (const newHeadChild of newHead.children) {
|
2041
|
+
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
|
2042
|
+
}
|
2043
|
+
for (const currentHeadElt of oldHead.children) {
|
2044
|
+
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
|
2045
|
+
let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
|
2046
|
+
let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
|
2047
|
+
if (inNewContent || isPreserved) {
|
2048
|
+
if (isReAppended) {
|
2049
|
+
removed.push(currentHeadElt);
|
2050
|
+
} else {
|
2051
|
+
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
|
2052
|
+
preserved.push(currentHeadElt);
|
2053
|
+
}
|
2054
|
+
} else {
|
2055
|
+
if (ctx.head.style === "append") {
|
2056
|
+
if (isReAppended) {
|
2057
|
+
removed.push(currentHeadElt);
|
2058
|
+
nodesToAppend.push(currentHeadElt);
|
2059
|
+
}
|
2060
|
+
} else {
|
2061
|
+
if (ctx.head.shouldRemove(currentHeadElt) !== false) {
|
2062
|
+
removed.push(currentHeadElt);
|
2063
|
+
}
|
2064
|
+
}
|
2065
|
+
}
|
2066
|
+
}
|
2067
|
+
nodesToAppend.push(...srcToNewHeadNodes.values());
|
2068
|
+
let promises = [];
|
2069
|
+
for (const newNode of nodesToAppend) {
|
2070
|
+
let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
|
2071
|
+
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
|
2072
|
+
if ("href" in newElt && newElt.href || "src" in newElt && newElt.src) {
|
2073
|
+
let resolve;
|
2074
|
+
let promise = new Promise((function(_resolve) {
|
2075
|
+
resolve = _resolve;
|
2076
|
+
}));
|
2077
|
+
newElt.addEventListener("load", (function() {
|
2078
|
+
resolve();
|
2079
|
+
}));
|
2080
|
+
promises.push(promise);
|
2081
|
+
}
|
2082
|
+
oldHead.appendChild(newElt);
|
2083
|
+
ctx.callbacks.afterNodeAdded(newElt);
|
2084
|
+
added.push(newElt);
|
2085
|
+
}
|
2086
|
+
}
|
2087
|
+
for (const removedElement of removed) {
|
2088
|
+
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
|
2089
|
+
oldHead.removeChild(removedElement);
|
2090
|
+
ctx.callbacks.afterNodeRemoved(removedElement);
|
2091
|
+
}
|
2092
|
+
}
|
2093
|
+
ctx.head.afterHeadMorphed(oldHead, {
|
2094
|
+
added: added,
|
2095
|
+
kept: preserved,
|
2096
|
+
removed: removed
|
2097
|
+
});
|
2098
|
+
return promises;
|
2099
|
+
}
|
2100
|
+
const createMorphContext = function() {
|
2101
|
+
function createMorphContext(oldNode, newContent, config) {
|
2102
|
+
const {persistentIds: persistentIds, idMap: idMap} = createIdMaps(oldNode, newContent);
|
2103
|
+
const mergedConfig = mergeDefaults(config);
|
2104
|
+
const morphStyle = mergedConfig.morphStyle || "outerHTML";
|
2105
|
+
if (![ "innerHTML", "outerHTML" ].includes(morphStyle)) {
|
2106
|
+
throw `Do not understand how to morph style ${morphStyle}`;
|
2107
|
+
}
|
2108
|
+
return {
|
2109
|
+
target: oldNode,
|
2110
|
+
newContent: newContent,
|
2111
|
+
config: mergedConfig,
|
2112
|
+
morphStyle: morphStyle,
|
2113
|
+
ignoreActive: mergedConfig.ignoreActive,
|
2114
|
+
ignoreActiveValue: mergedConfig.ignoreActiveValue,
|
2115
|
+
restoreFocus: mergedConfig.restoreFocus,
|
2116
|
+
idMap: idMap,
|
2117
|
+
persistentIds: persistentIds,
|
2118
|
+
pantry: createPantry(),
|
2119
|
+
callbacks: mergedConfig.callbacks,
|
2120
|
+
head: mergedConfig.head
|
2121
|
+
};
|
2122
|
+
}
|
2123
|
+
function mergeDefaults(config) {
|
2124
|
+
let finalConfig = Object.assign({}, defaults);
|
2125
|
+
Object.assign(finalConfig, config);
|
2126
|
+
finalConfig.callbacks = Object.assign({}, defaults.callbacks, config.callbacks);
|
2127
|
+
finalConfig.head = Object.assign({}, defaults.head, config.head);
|
2128
|
+
return finalConfig;
|
2129
|
+
}
|
2130
|
+
function createPantry() {
|
2131
|
+
const pantry = document.createElement("div");
|
2132
|
+
pantry.hidden = true;
|
2133
|
+
document.body.insertAdjacentElement("afterend", pantry);
|
2134
|
+
return pantry;
|
2135
|
+
}
|
2136
|
+
function findIdElements(root) {
|
2137
|
+
let elements = Array.from(root.querySelectorAll("[id]"));
|
2138
|
+
if (root.id) {
|
2139
|
+
elements.push(root);
|
2140
|
+
}
|
2141
|
+
return elements;
|
2142
|
+
}
|
2143
|
+
function populateIdMapWithTree(idMap, persistentIds, root, elements) {
|
2144
|
+
for (const elt of elements) {
|
2145
|
+
if (persistentIds.has(elt.id)) {
|
2146
|
+
let current = elt;
|
2147
|
+
while (current) {
|
2148
|
+
let idSet = idMap.get(current);
|
2149
|
+
if (idSet == null) {
|
2150
|
+
idSet = new Set;
|
2151
|
+
idMap.set(current, idSet);
|
2152
|
+
}
|
2153
|
+
idSet.add(elt.id);
|
2154
|
+
if (current === root) break;
|
2155
|
+
current = current.parentElement;
|
2156
|
+
}
|
2157
|
+
}
|
2158
|
+
}
|
2159
|
+
}
|
2160
|
+
function createIdMaps(oldContent, newContent) {
|
2161
|
+
const oldIdElements = findIdElements(oldContent);
|
2162
|
+
const newIdElements = findIdElements(newContent);
|
2163
|
+
const persistentIds = createPersistentIds(oldIdElements, newIdElements);
|
2164
|
+
let idMap = new Map;
|
2165
|
+
populateIdMapWithTree(idMap, persistentIds, oldContent, oldIdElements);
|
2166
|
+
const newRoot = newContent.__idiomorphRoot || newContent;
|
2167
|
+
populateIdMapWithTree(idMap, persistentIds, newRoot, newIdElements);
|
2168
|
+
return {
|
2169
|
+
persistentIds: persistentIds,
|
2170
|
+
idMap: idMap
|
2171
|
+
};
|
2172
|
+
}
|
2173
|
+
function createPersistentIds(oldIdElements, newIdElements) {
|
2174
|
+
let duplicateIds = new Set;
|
2175
|
+
let oldIdTagNameMap = new Map;
|
2176
|
+
for (const {id: id, tagName: tagName} of oldIdElements) {
|
2177
|
+
if (oldIdTagNameMap.has(id)) {
|
2178
|
+
duplicateIds.add(id);
|
2179
|
+
} else {
|
2180
|
+
oldIdTagNameMap.set(id, tagName);
|
2181
|
+
}
|
2182
|
+
}
|
2183
|
+
let persistentIds = new Set;
|
2184
|
+
for (const {id: id, tagName: tagName} of newIdElements) {
|
2185
|
+
if (persistentIds.has(id)) {
|
2186
|
+
duplicateIds.add(id);
|
2187
|
+
} else if (oldIdTagNameMap.get(id) === tagName) {
|
2188
|
+
persistentIds.add(id);
|
2189
|
+
}
|
2190
|
+
}
|
2191
|
+
for (const id of duplicateIds) {
|
2192
|
+
persistentIds.delete(id);
|
2193
|
+
}
|
2194
|
+
return persistentIds;
|
2195
|
+
}
|
2196
|
+
return createMorphContext;
|
2197
|
+
}();
|
2198
|
+
const {normalizeElement: normalizeElement, normalizeParent: normalizeParent} = function() {
|
2199
|
+
const generatedByIdiomorph = new WeakSet;
|
2200
|
+
function normalizeElement(content) {
|
2201
|
+
if (content instanceof Document) {
|
2202
|
+
return content.documentElement;
|
2203
|
+
} else {
|
2204
|
+
return content;
|
2205
|
+
}
|
2206
|
+
}
|
2207
|
+
function normalizeParent(newContent) {
|
2208
|
+
if (newContent == null) {
|
2209
|
+
return document.createElement("div");
|
2210
|
+
} else if (typeof newContent === "string") {
|
2211
|
+
return normalizeParent(parseContent(newContent));
|
2212
|
+
} else if (generatedByIdiomorph.has(newContent)) {
|
2213
|
+
return newContent;
|
2214
|
+
} else if (newContent instanceof Node) {
|
2215
|
+
if (newContent.parentNode) {
|
2216
|
+
return createDuckTypedParent(newContent);
|
2217
|
+
} else {
|
2218
|
+
const dummyParent = document.createElement("div");
|
2219
|
+
dummyParent.append(newContent);
|
2220
|
+
return dummyParent;
|
2221
|
+
}
|
2222
|
+
} else {
|
2223
|
+
const dummyParent = document.createElement("div");
|
2224
|
+
for (const elt of [ ...newContent ]) {
|
2225
|
+
dummyParent.append(elt);
|
2226
|
+
}
|
2227
|
+
return dummyParent;
|
2228
|
+
}
|
2229
|
+
}
|
2230
|
+
function createDuckTypedParent(newContent) {
|
2231
|
+
return {
|
2232
|
+
childNodes: [ newContent ],
|
2233
|
+
querySelectorAll: s => {
|
2234
|
+
const elements = newContent.querySelectorAll(s);
|
2235
|
+
return newContent.matches(s) ? [ newContent, ...elements ] : elements;
|
2236
|
+
},
|
2237
|
+
insertBefore: (n, r) => newContent.parentNode.insertBefore(n, r),
|
2238
|
+
moveBefore: (n, r) => newContent.parentNode.moveBefore(n, r),
|
2239
|
+
get __idiomorphRoot() {
|
2240
|
+
return newContent;
|
2241
|
+
}
|
2242
|
+
};
|
2243
|
+
}
|
2244
|
+
function parseContent(newContent) {
|
2245
|
+
let parser = new DOMParser;
|
2246
|
+
let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, "");
|
2247
|
+
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
|
2248
|
+
let content = parser.parseFromString(newContent, "text/html");
|
2249
|
+
if (contentWithSvgsRemoved.match(/<\/html>/)) {
|
2250
|
+
generatedByIdiomorph.add(content);
|
2251
|
+
return content;
|
2252
|
+
} else {
|
2253
|
+
let htmlElement = content.firstChild;
|
2254
|
+
if (htmlElement) {
|
2255
|
+
generatedByIdiomorph.add(htmlElement);
|
2256
|
+
}
|
2257
|
+
return htmlElement;
|
2258
|
+
}
|
2259
|
+
} else {
|
2260
|
+
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
|
2261
|
+
let content = responseDoc.body.querySelector("template").content;
|
2262
|
+
generatedByIdiomorph.add(content);
|
2263
|
+
return content;
|
2264
|
+
}
|
2265
|
+
}
|
2266
|
+
return {
|
2267
|
+
normalizeElement: normalizeElement,
|
2268
|
+
normalizeParent: normalizeParent
|
2269
|
+
};
|
2270
|
+
}();
|
2271
|
+
return {
|
2272
|
+
morph: morph,
|
2273
|
+
defaults: defaults
|
2274
|
+
};
|
2275
|
+
}();
|
2276
|
+
|
2277
|
+
function morphElements(currentElement, newElement, {callbacks: callbacks, ...options} = {}) {
|
2278
|
+
Idiomorph.morph(currentElement, newElement, {
|
2279
|
+
...options,
|
2280
|
+
callbacks: new DefaultIdiomorphCallbacks(callbacks)
|
2281
|
+
});
|
2282
|
+
}
|
2283
|
+
|
2284
|
+
function morphChildren(currentElement, newElement) {
|
2285
|
+
morphElements(currentElement, newElement.childNodes, {
|
2286
|
+
morphStyle: "innerHTML"
|
2287
|
+
});
|
2288
|
+
}
|
2289
|
+
|
2290
|
+
class DefaultIdiomorphCallbacks {
|
2291
|
+
#beforeNodeMorphed;
|
2292
|
+
constructor({beforeNodeMorphed: beforeNodeMorphed} = {}) {
|
2293
|
+
this.#beforeNodeMorphed = beforeNodeMorphed || (() => true);
|
2294
|
+
}
|
2295
|
+
beforeNodeAdded=node => !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id));
|
2296
|
+
beforeNodeMorphed=(currentElement, newElement) => {
|
2297
|
+
if (currentElement instanceof Element) {
|
2298
|
+
if (!currentElement.hasAttribute("data-turbo-permanent") && this.#beforeNodeMorphed(currentElement, newElement)) {
|
2299
|
+
const event = dispatch("turbo:before-morph-element", {
|
2300
|
+
cancelable: true,
|
2301
|
+
target: currentElement,
|
2302
|
+
detail: {
|
2303
|
+
currentElement: currentElement,
|
2304
|
+
newElement: newElement
|
2305
|
+
}
|
2306
|
+
});
|
2307
|
+
return !event.defaultPrevented;
|
2308
|
+
} else {
|
2309
|
+
return false;
|
2310
|
+
}
|
2311
|
+
}
|
2312
|
+
};
|
2313
|
+
beforeAttributeUpdated=(attributeName, target, mutationType) => {
|
2314
|
+
const event = dispatch("turbo:before-morph-attribute", {
|
2315
|
+
cancelable: true,
|
2316
|
+
target: target,
|
2317
|
+
detail: {
|
2318
|
+
attributeName: attributeName,
|
2319
|
+
mutationType: mutationType
|
2320
|
+
}
|
2321
|
+
});
|
2322
|
+
return !event.defaultPrevented;
|
2323
|
+
};
|
2324
|
+
beforeNodeRemoved=node => this.beforeNodeMorphed(node);
|
2325
|
+
afterNodeMorphed=(currentElement, newElement) => {
|
2326
|
+
if (currentElement instanceof Element) {
|
2327
|
+
dispatch("turbo:morph-element", {
|
2328
|
+
target: currentElement,
|
2329
|
+
detail: {
|
2330
|
+
currentElement: currentElement,
|
2331
|
+
newElement: newElement
|
2332
|
+
}
|
2333
|
+
});
|
2334
|
+
}
|
2335
|
+
};
|
2336
|
+
}
|
2337
|
+
|
2338
|
+
class MorphingFrameRenderer extends FrameRenderer {
|
2339
|
+
static renderElement(currentElement, newElement) {
|
2340
|
+
dispatch("turbo:before-frame-morph", {
|
2341
|
+
target: currentElement,
|
2342
|
+
detail: {
|
2343
|
+
currentElement: currentElement,
|
2344
|
+
newElement: newElement
|
2345
|
+
}
|
2346
|
+
});
|
2347
|
+
morphChildren(currentElement, newElement);
|
2348
|
+
}
|
2349
|
+
async preservingPermanentElements(callback) {
|
2350
|
+
return await callback();
|
2351
|
+
}
|
2352
|
+
}
|
2353
|
+
|
2354
|
+
class ProgressBar {
|
2355
|
+
static animationDuration=300;
|
2356
|
+
static get defaultCSS() {
|
2357
|
+
return unindent`
|
2358
|
+
.turbo-progress-bar {
|
2359
|
+
position: fixed;
|
2360
|
+
display: block;
|
2361
|
+
top: 0;
|
2362
|
+
left: 0;
|
2363
|
+
height: 3px;
|
2364
|
+
background: #0076ff;
|
2365
|
+
z-index: 2147483647;
|
2366
|
+
transition:
|
2367
|
+
width ${ProgressBar.animationDuration}ms ease-out,
|
2368
|
+
opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in;
|
2369
|
+
transform: translate3d(0, 0, 0);
|
2370
|
+
}
|
2371
|
+
`;
|
2372
|
+
}
|
2373
|
+
hiding=false;
|
2374
|
+
value=0;
|
2375
|
+
visible=false;
|
1651
2376
|
constructor() {
|
1652
2377
|
this.stylesheetElement = this.createStylesheetElement();
|
1653
2378
|
this.progressElement = this.createProgressElement();
|
@@ -1715,8 +2440,9 @@ class ProgressBar {
|
|
1715
2440
|
const element = document.createElement("style");
|
1716
2441
|
element.type = "text/css";
|
1717
2442
|
element.textContent = ProgressBar.defaultCSS;
|
1718
|
-
|
1719
|
-
|
2443
|
+
const cspNonce = getCspNonce();
|
2444
|
+
if (cspNonce) {
|
2445
|
+
element.nonce = cspNonce;
|
1720
2446
|
}
|
1721
2447
|
return element;
|
1722
2448
|
}
|
@@ -1725,9 +2451,6 @@ class ProgressBar {
|
|
1725
2451
|
element.className = "turbo-progress-bar";
|
1726
2452
|
return element;
|
1727
2453
|
}
|
1728
|
-
get cspNonce() {
|
1729
|
-
return getMetaContent("csp-nonce");
|
1730
|
-
}
|
1731
2454
|
}
|
1732
2455
|
|
1733
2456
|
class HeadSnapshot extends Snapshot {
|
@@ -2230,16 +2953,6 @@ class Visit {
|
|
2230
2953
|
...this.timingMetrics
|
2231
2954
|
};
|
2232
2955
|
}
|
2233
|
-
getHistoryMethodForAction(action) {
|
2234
|
-
switch (action) {
|
2235
|
-
case "replace":
|
2236
|
-
return history.replaceState;
|
2237
|
-
|
2238
|
-
case "advance":
|
2239
|
-
case "restore":
|
2240
|
-
return history.pushState;
|
2241
|
-
}
|
2242
|
-
}
|
2243
2956
|
hasPreloadedResponse() {
|
2244
2957
|
return typeof this.response == "object";
|
2245
2958
|
}
|
@@ -2260,7 +2973,9 @@ class Visit {
|
|
2260
2973
|
}
|
2261
2974
|
async render(callback) {
|
2262
2975
|
this.cancelRender();
|
2263
|
-
|
2976
|
+
await new Promise((resolve => {
|
2977
|
+
this.frame = document.visibilityState === "hidden" ? setTimeout((() => resolve()), 0) : requestAnimationFrame((() => resolve()));
|
2978
|
+
}));
|
2264
2979
|
await callback();
|
2265
2980
|
delete this.frame;
|
2266
2981
|
}
|
@@ -2340,6 +3055,9 @@ class BrowserAdapter {
|
|
2340
3055
|
this.hideVisitProgressBar();
|
2341
3056
|
}
|
2342
3057
|
visitRendered(_visit) {}
|
3058
|
+
linkPrefetchingIsEnabledForLocation(location) {
|
3059
|
+
return true;
|
3060
|
+
}
|
2343
3061
|
formSubmissionStarted(_formSubmission) {
|
2344
3062
|
this.progressBar.setValue(0);
|
2345
3063
|
this.showFormProgressBarAfterDelay();
|
@@ -2796,913 +3514,305 @@ class Navigator {
|
|
2796
3514
|
this.adapter.formSubmissionFinished(formSubmission);
|
2797
3515
|
}
|
2798
3516
|
}
|
2799
|
-
|
2800
|
-
this.
|
2801
|
-
|
2802
|
-
visitCompleted(visit) {
|
2803
|
-
this.delegate.visitCompleted(visit);
|
2804
|
-
delete this.currentVisit;
|
2805
|
-
}
|
2806
|
-
locationWithActionIsSamePage(location, action) {
|
2807
|
-
const anchor = getAnchor(location);
|
2808
|
-
const currentAnchor = getAnchor(this.view.lastRenderedLocation);
|
2809
|
-
const isRestorationToTop = action === "restore" && typeof anchor === "undefined";
|
2810
|
-
return action !== "replace" && getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) && (isRestorationToTop || anchor != null && anchor !== currentAnchor);
|
2811
|
-
}
|
2812
|
-
visitScrolledToSamePageLocation(oldURL, newURL) {
|
2813
|
-
this.delegate.visitScrolledToSamePageLocation(oldURL, newURL);
|
2814
|
-
}
|
2815
|
-
get location() {
|
2816
|
-
return this.history.location;
|
2817
|
-
}
|
2818
|
-
get restorationIdentifier() {
|
2819
|
-
return this.history.restorationIdentifier;
|
2820
|
-
}
|
2821
|
-
#getActionForFormSubmission(formSubmission, fetchResponse) {
|
2822
|
-
const {submitter: submitter, formElement: formElement} = formSubmission;
|
2823
|
-
return getVisitAction(submitter, formElement) || this.#getDefaultAction(fetchResponse);
|
2824
|
-
}
|
2825
|
-
#getDefaultAction(fetchResponse) {
|
2826
|
-
const sameLocationRedirect = fetchResponse.redirected && fetchResponse.location.href === this.location?.href;
|
2827
|
-
return sameLocationRedirect ? "replace" : "advance";
|
2828
|
-
}
|
2829
|
-
}
|
2830
|
-
|
2831
|
-
const PageStage = {
|
2832
|
-
initial: 0,
|
2833
|
-
loading: 1,
|
2834
|
-
interactive: 2,
|
2835
|
-
complete: 3
|
2836
|
-
};
|
2837
|
-
|
2838
|
-
class PageObserver {
|
2839
|
-
stage=PageStage.initial;
|
2840
|
-
started=false;
|
2841
|
-
constructor(delegate) {
|
2842
|
-
this.delegate = delegate;
|
2843
|
-
}
|
2844
|
-
start() {
|
2845
|
-
if (!this.started) {
|
2846
|
-
if (this.stage == PageStage.initial) {
|
2847
|
-
this.stage = PageStage.loading;
|
2848
|
-
}
|
2849
|
-
document.addEventListener("readystatechange", this.interpretReadyState, false);
|
2850
|
-
addEventListener("pagehide", this.pageWillUnload, false);
|
2851
|
-
this.started = true;
|
2852
|
-
}
|
2853
|
-
}
|
2854
|
-
stop() {
|
2855
|
-
if (this.started) {
|
2856
|
-
document.removeEventListener("readystatechange", this.interpretReadyState, false);
|
2857
|
-
removeEventListener("pagehide", this.pageWillUnload, false);
|
2858
|
-
this.started = false;
|
2859
|
-
}
|
2860
|
-
}
|
2861
|
-
interpretReadyState=() => {
|
2862
|
-
const {readyState: readyState} = this;
|
2863
|
-
if (readyState == "interactive") {
|
2864
|
-
this.pageIsInteractive();
|
2865
|
-
} else if (readyState == "complete") {
|
2866
|
-
this.pageIsComplete();
|
2867
|
-
}
|
2868
|
-
};
|
2869
|
-
pageIsInteractive() {
|
2870
|
-
if (this.stage == PageStage.loading) {
|
2871
|
-
this.stage = PageStage.interactive;
|
2872
|
-
this.delegate.pageBecameInteractive();
|
2873
|
-
}
|
2874
|
-
}
|
2875
|
-
pageIsComplete() {
|
2876
|
-
this.pageIsInteractive();
|
2877
|
-
if (this.stage == PageStage.interactive) {
|
2878
|
-
this.stage = PageStage.complete;
|
2879
|
-
this.delegate.pageLoaded();
|
2880
|
-
}
|
2881
|
-
}
|
2882
|
-
pageWillUnload=() => {
|
2883
|
-
this.delegate.pageWillUnload();
|
2884
|
-
};
|
2885
|
-
get readyState() {
|
2886
|
-
return document.readyState;
|
2887
|
-
}
|
2888
|
-
}
|
2889
|
-
|
2890
|
-
class ScrollObserver {
|
2891
|
-
started=false;
|
2892
|
-
constructor(delegate) {
|
2893
|
-
this.delegate = delegate;
|
2894
|
-
}
|
2895
|
-
start() {
|
2896
|
-
if (!this.started) {
|
2897
|
-
addEventListener("scroll", this.onScroll, false);
|
2898
|
-
this.onScroll();
|
2899
|
-
this.started = true;
|
2900
|
-
}
|
2901
|
-
}
|
2902
|
-
stop() {
|
2903
|
-
if (this.started) {
|
2904
|
-
removeEventListener("scroll", this.onScroll, false);
|
2905
|
-
this.started = false;
|
2906
|
-
}
|
2907
|
-
}
|
2908
|
-
onScroll=() => {
|
2909
|
-
this.updatePosition({
|
2910
|
-
x: window.pageXOffset,
|
2911
|
-
y: window.pageYOffset
|
2912
|
-
});
|
2913
|
-
};
|
2914
|
-
updatePosition(position) {
|
2915
|
-
this.delegate.scrollPositionChanged(position);
|
2916
|
-
}
|
2917
|
-
}
|
2918
|
-
|
2919
|
-
class StreamMessageRenderer {
|
2920
|
-
render({fragment: fragment}) {
|
2921
|
-
Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), (() => {
|
2922
|
-
withAutofocusFromFragment(fragment, (() => {
|
2923
|
-
withPreservedFocus((() => {
|
2924
|
-
document.documentElement.appendChild(fragment);
|
2925
|
-
}));
|
2926
|
-
}));
|
2927
|
-
}));
|
2928
|
-
}
|
2929
|
-
enteringBardo(currentPermanentElement, newPermanentElement) {
|
2930
|
-
newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true));
|
2931
|
-
}
|
2932
|
-
leavingBardo() {}
|
2933
|
-
}
|
2934
|
-
|
2935
|
-
function getPermanentElementMapForFragment(fragment) {
|
2936
|
-
const permanentElementsInDocument = queryPermanentElementsAll(document.documentElement);
|
2937
|
-
const permanentElementMap = {};
|
2938
|
-
for (const permanentElementInDocument of permanentElementsInDocument) {
|
2939
|
-
const {id: id} = permanentElementInDocument;
|
2940
|
-
for (const streamElement of fragment.querySelectorAll("turbo-stream")) {
|
2941
|
-
const elementInStream = getPermanentElementById(streamElement.templateElement.content, id);
|
2942
|
-
if (elementInStream) {
|
2943
|
-
permanentElementMap[id] = [ permanentElementInDocument, elementInStream ];
|
2944
|
-
}
|
2945
|
-
}
|
2946
|
-
}
|
2947
|
-
return permanentElementMap;
|
2948
|
-
}
|
2949
|
-
|
2950
|
-
async function withAutofocusFromFragment(fragment, callback) {
|
2951
|
-
const generatedID = `turbo-stream-autofocus-${uuid()}`;
|
2952
|
-
const turboStreams = fragment.querySelectorAll("turbo-stream");
|
2953
|
-
const elementWithAutofocus = firstAutofocusableElementInStreams(turboStreams);
|
2954
|
-
let willAutofocusId = null;
|
2955
|
-
if (elementWithAutofocus) {
|
2956
|
-
if (elementWithAutofocus.id) {
|
2957
|
-
willAutofocusId = elementWithAutofocus.id;
|
2958
|
-
} else {
|
2959
|
-
willAutofocusId = generatedID;
|
2960
|
-
}
|
2961
|
-
elementWithAutofocus.id = willAutofocusId;
|
2962
|
-
}
|
2963
|
-
callback();
|
2964
|
-
await nextRepaint();
|
2965
|
-
const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body;
|
2966
|
-
if (hasNoActiveElement && willAutofocusId) {
|
2967
|
-
const elementToAutofocus = document.getElementById(willAutofocusId);
|
2968
|
-
if (elementIsFocusable(elementToAutofocus)) {
|
2969
|
-
elementToAutofocus.focus();
|
2970
|
-
}
|
2971
|
-
if (elementToAutofocus && elementToAutofocus.id == generatedID) {
|
2972
|
-
elementToAutofocus.removeAttribute("id");
|
2973
|
-
}
|
2974
|
-
}
|
2975
|
-
}
|
2976
|
-
|
2977
|
-
async function withPreservedFocus(callback) {
|
2978
|
-
const [activeElementBeforeRender, activeElementAfterRender] = await around(callback, (() => document.activeElement));
|
2979
|
-
const restoreFocusTo = activeElementBeforeRender && activeElementBeforeRender.id;
|
2980
|
-
if (restoreFocusTo) {
|
2981
|
-
const elementToFocus = document.getElementById(restoreFocusTo);
|
2982
|
-
if (elementIsFocusable(elementToFocus) && elementToFocus != activeElementAfterRender) {
|
2983
|
-
elementToFocus.focus();
|
2984
|
-
}
|
2985
|
-
}
|
2986
|
-
}
|
2987
|
-
|
2988
|
-
function firstAutofocusableElementInStreams(nodeListOfStreamElements) {
|
2989
|
-
for (const streamElement of nodeListOfStreamElements) {
|
2990
|
-
const elementWithAutofocus = queryAutofocusableElement(streamElement.templateElement.content);
|
2991
|
-
if (elementWithAutofocus) return elementWithAutofocus;
|
2992
|
-
}
|
2993
|
-
return null;
|
2994
|
-
}
|
2995
|
-
|
2996
|
-
class StreamObserver {
|
2997
|
-
sources=new Set;
|
2998
|
-
#started=false;
|
2999
|
-
constructor(delegate) {
|
3000
|
-
this.delegate = delegate;
|
3001
|
-
}
|
3002
|
-
start() {
|
3003
|
-
if (!this.#started) {
|
3004
|
-
this.#started = true;
|
3005
|
-
addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
|
3006
|
-
}
|
3007
|
-
}
|
3008
|
-
stop() {
|
3009
|
-
if (this.#started) {
|
3010
|
-
this.#started = false;
|
3011
|
-
removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
|
3012
|
-
}
|
3013
|
-
}
|
3014
|
-
connectStreamSource(source) {
|
3015
|
-
if (!this.streamSourceIsConnected(source)) {
|
3016
|
-
this.sources.add(source);
|
3017
|
-
source.addEventListener("message", this.receiveMessageEvent, false);
|
3018
|
-
}
|
3019
|
-
}
|
3020
|
-
disconnectStreamSource(source) {
|
3021
|
-
if (this.streamSourceIsConnected(source)) {
|
3022
|
-
this.sources.delete(source);
|
3023
|
-
source.removeEventListener("message", this.receiveMessageEvent, false);
|
3024
|
-
}
|
3025
|
-
}
|
3026
|
-
streamSourceIsConnected(source) {
|
3027
|
-
return this.sources.has(source);
|
3028
|
-
}
|
3029
|
-
inspectFetchResponse=event => {
|
3030
|
-
const response = fetchResponseFromEvent(event);
|
3031
|
-
if (response && fetchResponseIsStream(response)) {
|
3032
|
-
event.preventDefault();
|
3033
|
-
this.receiveMessageResponse(response);
|
3034
|
-
}
|
3035
|
-
};
|
3036
|
-
receiveMessageEvent=event => {
|
3037
|
-
if (this.#started && typeof event.data == "string") {
|
3038
|
-
this.receiveMessageHTML(event.data);
|
3039
|
-
}
|
3040
|
-
};
|
3041
|
-
async receiveMessageResponse(response) {
|
3042
|
-
const html = await response.responseHTML;
|
3043
|
-
if (html) {
|
3044
|
-
this.receiveMessageHTML(html);
|
3517
|
+
linkPrefetchingIsEnabledForLocation(location) {
|
3518
|
+
if (typeof this.adapter.linkPrefetchingIsEnabledForLocation === "function") {
|
3519
|
+
return this.adapter.linkPrefetchingIsEnabledForLocation(location);
|
3045
3520
|
}
|
3521
|
+
return true;
|
3046
3522
|
}
|
3047
|
-
|
3048
|
-
this.delegate.
|
3523
|
+
visitStarted(visit) {
|
3524
|
+
this.delegate.visitStarted(visit);
|
3049
3525
|
}
|
3050
|
-
|
3051
|
-
|
3052
|
-
|
3053
|
-
const fetchResponse = event.detail?.fetchResponse;
|
3054
|
-
if (fetchResponse instanceof FetchResponse) {
|
3055
|
-
return fetchResponse;
|
3526
|
+
visitCompleted(visit) {
|
3527
|
+
this.delegate.visitCompleted(visit);
|
3528
|
+
delete this.currentVisit;
|
3056
3529
|
}
|
3057
|
-
|
3058
|
-
|
3059
|
-
|
3060
|
-
|
3061
|
-
|
3062
|
-
}
|
3063
|
-
|
3064
|
-
class ErrorRenderer extends Renderer {
|
3065
|
-
static renderElement(currentElement, newElement) {
|
3066
|
-
const {documentElement: documentElement, body: body} = document;
|
3067
|
-
documentElement.replaceChild(newElement, body);
|
3530
|
+
locationWithActionIsSamePage(location, action) {
|
3531
|
+
const anchor = getAnchor(location);
|
3532
|
+
const currentAnchor = getAnchor(this.view.lastRenderedLocation);
|
3533
|
+
const isRestorationToTop = action === "restore" && typeof anchor === "undefined";
|
3534
|
+
return action !== "replace" && getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) && (isRestorationToTop || anchor != null && anchor !== currentAnchor);
|
3068
3535
|
}
|
3069
|
-
|
3070
|
-
this.
|
3071
|
-
this.activateScriptElements();
|
3536
|
+
visitScrolledToSamePageLocation(oldURL, newURL) {
|
3537
|
+
this.delegate.visitScrolledToSamePageLocation(oldURL, newURL);
|
3072
3538
|
}
|
3073
|
-
|
3074
|
-
|
3075
|
-
documentElement.replaceChild(this.newHead, head);
|
3076
|
-
this.renderElement(this.currentElement, this.newElement);
|
3539
|
+
get location() {
|
3540
|
+
return this.history.location;
|
3077
3541
|
}
|
3078
|
-
|
3079
|
-
|
3080
|
-
const parentNode = replaceableElement.parentNode;
|
3081
|
-
if (parentNode) {
|
3082
|
-
const element = activateScriptElement(replaceableElement);
|
3083
|
-
parentNode.replaceChild(element, replaceableElement);
|
3084
|
-
}
|
3085
|
-
}
|
3542
|
+
get restorationIdentifier() {
|
3543
|
+
return this.history.restorationIdentifier;
|
3086
3544
|
}
|
3087
|
-
|
3088
|
-
|
3545
|
+
#getActionForFormSubmission(formSubmission, fetchResponse) {
|
3546
|
+
const {submitter: submitter, formElement: formElement} = formSubmission;
|
3547
|
+
return getVisitAction(submitter, formElement) || this.#getDefaultAction(fetchResponse);
|
3089
3548
|
}
|
3090
|
-
|
3091
|
-
|
3549
|
+
#getDefaultAction(fetchResponse) {
|
3550
|
+
const sameLocationRedirect = fetchResponse.redirected && fetchResponse.location.href === this.location?.href;
|
3551
|
+
return sameLocationRedirect ? "replace" : "advance";
|
3092
3552
|
}
|
3093
3553
|
}
|
3094
3554
|
|
3095
|
-
|
3096
|
-
|
3097
|
-
|
3098
|
-
|
3099
|
-
|
3100
|
-
|
3101
|
-
|
3102
|
-
|
3103
|
-
|
3104
|
-
|
3105
|
-
|
3106
|
-
|
3107
|
-
},
|
3108
|
-
head: {
|
3109
|
-
style: "merge",
|
3110
|
-
shouldPreserve: function(elt) {
|
3111
|
-
return elt.getAttribute("im-preserve") === "true";
|
3112
|
-
},
|
3113
|
-
shouldReAppend: function(elt) {
|
3114
|
-
return elt.getAttribute("im-re-append") === "true";
|
3115
|
-
},
|
3116
|
-
shouldRemove: noOp,
|
3117
|
-
afterHeadMorphed: noOp
|
3118
|
-
}
|
3119
|
-
};
|
3120
|
-
function morph(oldNode, newContent, config = {}) {
|
3121
|
-
if (oldNode instanceof Document) {
|
3122
|
-
oldNode = oldNode.documentElement;
|
3123
|
-
}
|
3124
|
-
if (typeof newContent === "string") {
|
3125
|
-
newContent = parseContent(newContent);
|
3126
|
-
}
|
3127
|
-
let normalizedContent = normalizeContent(newContent);
|
3128
|
-
let ctx = createMorphContext(oldNode, normalizedContent, config);
|
3129
|
-
return morphNormalizedContent(oldNode, normalizedContent, ctx);
|
3555
|
+
const PageStage = {
|
3556
|
+
initial: 0,
|
3557
|
+
loading: 1,
|
3558
|
+
interactive: 2,
|
3559
|
+
complete: 3
|
3560
|
+
};
|
3561
|
+
|
3562
|
+
class PageObserver {
|
3563
|
+
stage=PageStage.initial;
|
3564
|
+
started=false;
|
3565
|
+
constructor(delegate) {
|
3566
|
+
this.delegate = delegate;
|
3130
3567
|
}
|
3131
|
-
|
3132
|
-
if (
|
3133
|
-
|
3134
|
-
|
3135
|
-
if (oldHead && newHead) {
|
3136
|
-
let promises = handleHeadElement(newHead, oldHead, ctx);
|
3137
|
-
Promise.all(promises).then((function() {
|
3138
|
-
morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
|
3139
|
-
head: {
|
3140
|
-
block: false,
|
3141
|
-
ignore: true
|
3142
|
-
}
|
3143
|
-
}));
|
3144
|
-
}));
|
3145
|
-
return;
|
3146
|
-
}
|
3147
|
-
}
|
3148
|
-
if (ctx.morphStyle === "innerHTML") {
|
3149
|
-
morphChildren(normalizedNewContent, oldNode, ctx);
|
3150
|
-
return oldNode.children;
|
3151
|
-
} else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
|
3152
|
-
let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
|
3153
|
-
let previousSibling = bestMatch?.previousSibling;
|
3154
|
-
let nextSibling = bestMatch?.nextSibling;
|
3155
|
-
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
|
3156
|
-
if (bestMatch) {
|
3157
|
-
return insertSiblings(previousSibling, morphedNode, nextSibling);
|
3158
|
-
} else {
|
3159
|
-
return [];
|
3568
|
+
start() {
|
3569
|
+
if (!this.started) {
|
3570
|
+
if (this.stage == PageStage.initial) {
|
3571
|
+
this.stage = PageStage.loading;
|
3160
3572
|
}
|
3161
|
-
|
3162
|
-
|
3573
|
+
document.addEventListener("readystatechange", this.interpretReadyState, false);
|
3574
|
+
addEventListener("pagehide", this.pageWillUnload, false);
|
3575
|
+
this.started = true;
|
3163
3576
|
}
|
3164
3577
|
}
|
3165
|
-
|
3166
|
-
|
3167
|
-
|
3168
|
-
|
3169
|
-
|
3170
|
-
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
3171
|
-
oldNode.remove();
|
3172
|
-
ctx.callbacks.afterNodeRemoved(oldNode);
|
3173
|
-
return null;
|
3174
|
-
} else if (!isSoftMatch(oldNode, newContent)) {
|
3175
|
-
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
3176
|
-
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
|
3177
|
-
oldNode.parentElement.replaceChild(newContent, oldNode);
|
3178
|
-
ctx.callbacks.afterNodeAdded(newContent);
|
3179
|
-
ctx.callbacks.afterNodeRemoved(oldNode);
|
3180
|
-
return newContent;
|
3181
|
-
} else {
|
3182
|
-
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
|
3183
|
-
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
|
3184
|
-
handleHeadElement(newContent, oldNode, ctx);
|
3185
|
-
} else {
|
3186
|
-
syncNodeFrom(newContent, oldNode, ctx);
|
3187
|
-
if (!ignoreValueOfActiveElement(oldNode, ctx)) {
|
3188
|
-
morphChildren(newContent, oldNode, ctx);
|
3189
|
-
}
|
3190
|
-
}
|
3191
|
-
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
|
3192
|
-
return oldNode;
|
3578
|
+
stop() {
|
3579
|
+
if (this.started) {
|
3580
|
+
document.removeEventListener("readystatechange", this.interpretReadyState, false);
|
3581
|
+
removeEventListener("pagehide", this.pageWillUnload, false);
|
3582
|
+
this.started = false;
|
3193
3583
|
}
|
3194
3584
|
}
|
3195
|
-
|
3196
|
-
|
3197
|
-
|
3198
|
-
|
3199
|
-
|
3200
|
-
|
3201
|
-
nextNewChild = newChild.nextSibling;
|
3202
|
-
if (insertionPoint == null) {
|
3203
|
-
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
3204
|
-
oldParent.appendChild(newChild);
|
3205
|
-
ctx.callbacks.afterNodeAdded(newChild);
|
3206
|
-
removeIdsFromConsideration(ctx, newChild);
|
3207
|
-
continue;
|
3208
|
-
}
|
3209
|
-
if (isIdSetMatch(newChild, insertionPoint, ctx)) {
|
3210
|
-
morphOldNodeTo(insertionPoint, newChild, ctx);
|
3211
|
-
insertionPoint = insertionPoint.nextSibling;
|
3212
|
-
removeIdsFromConsideration(ctx, newChild);
|
3213
|
-
continue;
|
3214
|
-
}
|
3215
|
-
let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
3216
|
-
if (idSetMatch) {
|
3217
|
-
insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
|
3218
|
-
morphOldNodeTo(idSetMatch, newChild, ctx);
|
3219
|
-
removeIdsFromConsideration(ctx, newChild);
|
3220
|
-
continue;
|
3221
|
-
}
|
3222
|
-
let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
3223
|
-
if (softMatch) {
|
3224
|
-
insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
|
3225
|
-
morphOldNodeTo(softMatch, newChild, ctx);
|
3226
|
-
removeIdsFromConsideration(ctx, newChild);
|
3227
|
-
continue;
|
3228
|
-
}
|
3229
|
-
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
3230
|
-
oldParent.insertBefore(newChild, insertionPoint);
|
3231
|
-
ctx.callbacks.afterNodeAdded(newChild);
|
3232
|
-
removeIdsFromConsideration(ctx, newChild);
|
3585
|
+
interpretReadyState=() => {
|
3586
|
+
const {readyState: readyState} = this;
|
3587
|
+
if (readyState == "interactive") {
|
3588
|
+
this.pageIsInteractive();
|
3589
|
+
} else if (readyState == "complete") {
|
3590
|
+
this.pageIsComplete();
|
3233
3591
|
}
|
3234
|
-
|
3235
|
-
|
3236
|
-
|
3237
|
-
|
3592
|
+
};
|
3593
|
+
pageIsInteractive() {
|
3594
|
+
if (this.stage == PageStage.loading) {
|
3595
|
+
this.stage = PageStage.interactive;
|
3596
|
+
this.delegate.pageBecameInteractive();
|
3238
3597
|
}
|
3239
3598
|
}
|
3240
|
-
|
3241
|
-
|
3242
|
-
|
3599
|
+
pageIsComplete() {
|
3600
|
+
this.pageIsInteractive();
|
3601
|
+
if (this.stage == PageStage.interactive) {
|
3602
|
+
this.stage = PageStage.complete;
|
3603
|
+
this.delegate.pageLoaded();
|
3243
3604
|
}
|
3244
|
-
return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
|
3245
3605
|
}
|
3246
|
-
|
3247
|
-
|
3248
|
-
|
3249
|
-
|
3250
|
-
|
3251
|
-
for (const fromAttribute of fromAttributes) {
|
3252
|
-
if (ignoreAttribute(fromAttribute.name, to, "update", ctx)) {
|
3253
|
-
continue;
|
3254
|
-
}
|
3255
|
-
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
|
3256
|
-
to.setAttribute(fromAttribute.name, fromAttribute.value);
|
3257
|
-
}
|
3258
|
-
}
|
3259
|
-
for (let i = toAttributes.length - 1; 0 <= i; i--) {
|
3260
|
-
const toAttribute = toAttributes[i];
|
3261
|
-
if (ignoreAttribute(toAttribute.name, to, "remove", ctx)) {
|
3262
|
-
continue;
|
3263
|
-
}
|
3264
|
-
if (!from.hasAttribute(toAttribute.name)) {
|
3265
|
-
to.removeAttribute(toAttribute.name);
|
3266
|
-
}
|
3267
|
-
}
|
3268
|
-
}
|
3269
|
-
if (type === 8 || type === 3) {
|
3270
|
-
if (to.nodeValue !== from.nodeValue) {
|
3271
|
-
to.nodeValue = from.nodeValue;
|
3272
|
-
}
|
3273
|
-
}
|
3274
|
-
if (!ignoreValueOfActiveElement(to, ctx)) {
|
3275
|
-
syncInputValue(from, to, ctx);
|
3276
|
-
}
|
3606
|
+
pageWillUnload=() => {
|
3607
|
+
this.delegate.pageWillUnload();
|
3608
|
+
};
|
3609
|
+
get readyState() {
|
3610
|
+
return document.readyState;
|
3277
3611
|
}
|
3278
|
-
|
3279
|
-
|
3280
|
-
|
3281
|
-
|
3282
|
-
|
3283
|
-
|
3284
|
-
if (from[attributeName]) {
|
3285
|
-
if (!ignoreUpdate) {
|
3286
|
-
to.setAttribute(attributeName, from[attributeName]);
|
3287
|
-
}
|
3288
|
-
} else {
|
3289
|
-
if (!ignoreAttribute(attributeName, to, "remove", ctx)) {
|
3290
|
-
to.removeAttribute(attributeName);
|
3291
|
-
}
|
3292
|
-
}
|
3293
|
-
}
|
3612
|
+
}
|
3613
|
+
|
3614
|
+
class ScrollObserver {
|
3615
|
+
started=false;
|
3616
|
+
constructor(delegate) {
|
3617
|
+
this.delegate = delegate;
|
3294
3618
|
}
|
3295
|
-
|
3296
|
-
if (
|
3297
|
-
|
3298
|
-
|
3299
|
-
|
3300
|
-
syncBooleanAttribute(from, to, "disabled", ctx);
|
3301
|
-
if (!from.hasAttribute("value")) {
|
3302
|
-
if (!ignoreAttribute("value", to, "remove", ctx)) {
|
3303
|
-
to.value = "";
|
3304
|
-
to.removeAttribute("value");
|
3305
|
-
}
|
3306
|
-
} else if (fromValue !== toValue) {
|
3307
|
-
if (!ignoreAttribute("value", to, "update", ctx)) {
|
3308
|
-
to.setAttribute("value", fromValue);
|
3309
|
-
to.value = fromValue;
|
3310
|
-
}
|
3311
|
-
}
|
3312
|
-
} else if (from instanceof HTMLOptionElement) {
|
3313
|
-
syncBooleanAttribute(from, to, "selected", ctx);
|
3314
|
-
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
|
3315
|
-
let fromValue = from.value;
|
3316
|
-
let toValue = to.value;
|
3317
|
-
if (ignoreAttribute("value", to, "update", ctx)) {
|
3318
|
-
return;
|
3319
|
-
}
|
3320
|
-
if (fromValue !== toValue) {
|
3321
|
-
to.value = fromValue;
|
3322
|
-
}
|
3323
|
-
if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
|
3324
|
-
to.firstChild.nodeValue = fromValue;
|
3325
|
-
}
|
3619
|
+
start() {
|
3620
|
+
if (!this.started) {
|
3621
|
+
addEventListener("scroll", this.onScroll, false);
|
3622
|
+
this.onScroll();
|
3623
|
+
this.started = true;
|
3326
3624
|
}
|
3327
3625
|
}
|
3328
|
-
|
3329
|
-
|
3330
|
-
|
3331
|
-
|
3332
|
-
let nodesToAppend = [];
|
3333
|
-
let headMergeStyle = ctx.head.style;
|
3334
|
-
let srcToNewHeadNodes = new Map;
|
3335
|
-
for (const newHeadChild of newHeadTag.children) {
|
3336
|
-
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
|
3337
|
-
}
|
3338
|
-
for (const currentHeadElt of currentHead.children) {
|
3339
|
-
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
|
3340
|
-
let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
|
3341
|
-
let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
|
3342
|
-
if (inNewContent || isPreserved) {
|
3343
|
-
if (isReAppended) {
|
3344
|
-
removed.push(currentHeadElt);
|
3345
|
-
} else {
|
3346
|
-
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
|
3347
|
-
preserved.push(currentHeadElt);
|
3348
|
-
}
|
3349
|
-
} else {
|
3350
|
-
if (headMergeStyle === "append") {
|
3351
|
-
if (isReAppended) {
|
3352
|
-
removed.push(currentHeadElt);
|
3353
|
-
nodesToAppend.push(currentHeadElt);
|
3354
|
-
}
|
3355
|
-
} else {
|
3356
|
-
if (ctx.head.shouldRemove(currentHeadElt) !== false) {
|
3357
|
-
removed.push(currentHeadElt);
|
3358
|
-
}
|
3359
|
-
}
|
3360
|
-
}
|
3361
|
-
}
|
3362
|
-
nodesToAppend.push(...srcToNewHeadNodes.values());
|
3363
|
-
let promises = [];
|
3364
|
-
for (const newNode of nodesToAppend) {
|
3365
|
-
let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
|
3366
|
-
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
|
3367
|
-
if (newElt.href || newElt.src) {
|
3368
|
-
let resolve = null;
|
3369
|
-
let promise = new Promise((function(_resolve) {
|
3370
|
-
resolve = _resolve;
|
3371
|
-
}));
|
3372
|
-
newElt.addEventListener("load", (function() {
|
3373
|
-
resolve();
|
3374
|
-
}));
|
3375
|
-
promises.push(promise);
|
3376
|
-
}
|
3377
|
-
currentHead.appendChild(newElt);
|
3378
|
-
ctx.callbacks.afterNodeAdded(newElt);
|
3379
|
-
added.push(newElt);
|
3380
|
-
}
|
3381
|
-
}
|
3382
|
-
for (const removedElement of removed) {
|
3383
|
-
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
|
3384
|
-
currentHead.removeChild(removedElement);
|
3385
|
-
ctx.callbacks.afterNodeRemoved(removedElement);
|
3386
|
-
}
|
3626
|
+
stop() {
|
3627
|
+
if (this.started) {
|
3628
|
+
removeEventListener("scroll", this.onScroll, false);
|
3629
|
+
this.started = false;
|
3387
3630
|
}
|
3388
|
-
ctx.head.afterHeadMorphed(currentHead, {
|
3389
|
-
added: added,
|
3390
|
-
kept: preserved,
|
3391
|
-
removed: removed
|
3392
|
-
});
|
3393
|
-
return promises;
|
3394
3631
|
}
|
3395
|
-
|
3396
|
-
|
3397
|
-
|
3398
|
-
|
3399
|
-
|
3400
|
-
|
3401
|
-
|
3402
|
-
|
3403
|
-
finalConfig.head = {};
|
3404
|
-
Object.assign(finalConfig.head, defaults.head);
|
3405
|
-
Object.assign(finalConfig.head, config.head);
|
3406
|
-
return finalConfig;
|
3407
|
-
}
|
3408
|
-
function createMorphContext(oldNode, newContent, config) {
|
3409
|
-
config = mergeDefaults(config);
|
3410
|
-
return {
|
3411
|
-
target: oldNode,
|
3412
|
-
newContent: newContent,
|
3413
|
-
config: config,
|
3414
|
-
morphStyle: config.morphStyle,
|
3415
|
-
ignoreActive: config.ignoreActive,
|
3416
|
-
ignoreActiveValue: config.ignoreActiveValue,
|
3417
|
-
idMap: createIdMap(oldNode, newContent),
|
3418
|
-
deadIds: new Set,
|
3419
|
-
callbacks: config.callbacks,
|
3420
|
-
head: config.head
|
3421
|
-
};
|
3632
|
+
onScroll=() => {
|
3633
|
+
this.updatePosition({
|
3634
|
+
x: window.pageXOffset,
|
3635
|
+
y: window.pageYOffset
|
3636
|
+
});
|
3637
|
+
};
|
3638
|
+
updatePosition(position) {
|
3639
|
+
this.delegate.scrollPositionChanged(position);
|
3422
3640
|
}
|
3423
|
-
|
3424
|
-
|
3425
|
-
|
3426
|
-
|
3427
|
-
|
3428
|
-
|
3429
|
-
|
3430
|
-
|
3431
|
-
|
3432
|
-
}
|
3433
|
-
}
|
3434
|
-
return false;
|
3641
|
+
}
|
3642
|
+
|
3643
|
+
class StreamMessageRenderer {
|
3644
|
+
render({fragment: fragment}) {
|
3645
|
+
Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), (() => {
|
3646
|
+
withAutofocusFromFragment(fragment, (() => {
|
3647
|
+
withPreservedFocus((() => {
|
3648
|
+
document.documentElement.appendChild(fragment);
|
3649
|
+
}));
|
3650
|
+
}));
|
3651
|
+
}));
|
3435
3652
|
}
|
3436
|
-
|
3437
|
-
|
3438
|
-
return false;
|
3439
|
-
}
|
3440
|
-
return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName;
|
3441
|
-
}
|
3442
|
-
function removeNodesBetween(startInclusive, endExclusive, ctx) {
|
3443
|
-
while (startInclusive !== endExclusive) {
|
3444
|
-
let tempNode = startInclusive;
|
3445
|
-
startInclusive = startInclusive.nextSibling;
|
3446
|
-
removeNode(tempNode, ctx);
|
3447
|
-
}
|
3448
|
-
removeIdsFromConsideration(ctx, endExclusive);
|
3449
|
-
return endExclusive.nextSibling;
|
3450
|
-
}
|
3451
|
-
function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
3452
|
-
let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
|
3453
|
-
let potentialMatch = null;
|
3454
|
-
if (newChildPotentialIdCount > 0) {
|
3455
|
-
let potentialMatch = insertionPoint;
|
3456
|
-
let otherMatchCount = 0;
|
3457
|
-
while (potentialMatch != null) {
|
3458
|
-
if (isIdSetMatch(newChild, potentialMatch, ctx)) {
|
3459
|
-
return potentialMatch;
|
3460
|
-
}
|
3461
|
-
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
|
3462
|
-
if (otherMatchCount > newChildPotentialIdCount) {
|
3463
|
-
return null;
|
3464
|
-
}
|
3465
|
-
potentialMatch = potentialMatch.nextSibling;
|
3466
|
-
}
|
3467
|
-
}
|
3468
|
-
return potentialMatch;
|
3653
|
+
enteringBardo(currentPermanentElement, newPermanentElement) {
|
3654
|
+
newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true));
|
3469
3655
|
}
|
3470
|
-
|
3471
|
-
|
3472
|
-
|
3473
|
-
|
3474
|
-
|
3475
|
-
|
3476
|
-
|
3477
|
-
|
3478
|
-
|
3479
|
-
|
3480
|
-
|
3481
|
-
|
3482
|
-
siblingSoftMatchCount++;
|
3483
|
-
nextSibling = nextSibling.nextSibling;
|
3484
|
-
if (siblingSoftMatchCount >= 2) {
|
3485
|
-
return null;
|
3486
|
-
}
|
3656
|
+
leavingBardo() {}
|
3657
|
+
}
|
3658
|
+
|
3659
|
+
function getPermanentElementMapForFragment(fragment) {
|
3660
|
+
const permanentElementsInDocument = queryPermanentElementsAll(document.documentElement);
|
3661
|
+
const permanentElementMap = {};
|
3662
|
+
for (const permanentElementInDocument of permanentElementsInDocument) {
|
3663
|
+
const {id: id} = permanentElementInDocument;
|
3664
|
+
for (const streamElement of fragment.querySelectorAll("turbo-stream")) {
|
3665
|
+
const elementInStream = getPermanentElementById(streamElement.templateElement.content, id);
|
3666
|
+
if (elementInStream) {
|
3667
|
+
permanentElementMap[id] = [ permanentElementInDocument, elementInStream ];
|
3487
3668
|
}
|
3488
|
-
potentialSoftMatch = potentialSoftMatch.nextSibling;
|
3489
3669
|
}
|
3490
|
-
return potentialSoftMatch;
|
3491
3670
|
}
|
3492
|
-
|
3493
|
-
|
3494
|
-
|
3495
|
-
|
3496
|
-
|
3497
|
-
|
3498
|
-
|
3499
|
-
|
3500
|
-
|
3501
|
-
|
3502
|
-
|
3503
|
-
htmlElement.generatedByIdiomorph = true;
|
3504
|
-
return htmlElement;
|
3505
|
-
} else {
|
3506
|
-
return null;
|
3507
|
-
}
|
3508
|
-
}
|
3509
|
-
} else {
|
3510
|
-
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
|
3511
|
-
let content = responseDoc.body.querySelector("template").content;
|
3512
|
-
content.generatedByIdiomorph = true;
|
3513
|
-
return content;
|
3514
|
-
}
|
3515
|
-
}
|
3516
|
-
function normalizeContent(newContent) {
|
3517
|
-
if (newContent == null) {
|
3518
|
-
const dummyParent = document.createElement("div");
|
3519
|
-
return dummyParent;
|
3520
|
-
} else if (newContent.generatedByIdiomorph) {
|
3521
|
-
return newContent;
|
3522
|
-
} else if (newContent instanceof Node) {
|
3523
|
-
const dummyParent = document.createElement("div");
|
3524
|
-
dummyParent.append(newContent);
|
3525
|
-
return dummyParent;
|
3671
|
+
return permanentElementMap;
|
3672
|
+
}
|
3673
|
+
|
3674
|
+
async function withAutofocusFromFragment(fragment, callback) {
|
3675
|
+
const generatedID = `turbo-stream-autofocus-${uuid()}`;
|
3676
|
+
const turboStreams = fragment.querySelectorAll("turbo-stream");
|
3677
|
+
const elementWithAutofocus = firstAutofocusableElementInStreams(turboStreams);
|
3678
|
+
let willAutofocusId = null;
|
3679
|
+
if (elementWithAutofocus) {
|
3680
|
+
if (elementWithAutofocus.id) {
|
3681
|
+
willAutofocusId = elementWithAutofocus.id;
|
3526
3682
|
} else {
|
3527
|
-
|
3528
|
-
for (const elt of [ ...newContent ]) {
|
3529
|
-
dummyParent.append(elt);
|
3530
|
-
}
|
3531
|
-
return dummyParent;
|
3683
|
+
willAutofocusId = generatedID;
|
3532
3684
|
}
|
3685
|
+
elementWithAutofocus.id = willAutofocusId;
|
3533
3686
|
}
|
3534
|
-
|
3535
|
-
|
3536
|
-
|
3537
|
-
|
3538
|
-
|
3539
|
-
|
3540
|
-
|
3541
|
-
|
3542
|
-
|
3543
|
-
|
3544
|
-
morphedNode.parentElement.insertBefore(node, morphedNode);
|
3545
|
-
}
|
3546
|
-
added.push(morphedNode);
|
3547
|
-
while (nextSibling != null) {
|
3548
|
-
stack.push(nextSibling);
|
3549
|
-
added.push(nextSibling);
|
3550
|
-
nextSibling = nextSibling.nextSibling;
|
3551
|
-
}
|
3552
|
-
while (stack.length > 0) {
|
3553
|
-
morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
|
3554
|
-
}
|
3555
|
-
return added;
|
3556
|
-
}
|
3557
|
-
function findBestNodeMatch(newContent, oldNode, ctx) {
|
3558
|
-
let currentElement;
|
3559
|
-
currentElement = newContent.firstChild;
|
3560
|
-
let bestElement = currentElement;
|
3561
|
-
let score = 0;
|
3562
|
-
while (currentElement) {
|
3563
|
-
let newScore = scoreElement(currentElement, oldNode, ctx);
|
3564
|
-
if (newScore > score) {
|
3565
|
-
bestElement = currentElement;
|
3566
|
-
score = newScore;
|
3567
|
-
}
|
3568
|
-
currentElement = currentElement.nextSibling;
|
3687
|
+
callback();
|
3688
|
+
await nextRepaint();
|
3689
|
+
const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body;
|
3690
|
+
if (hasNoActiveElement && willAutofocusId) {
|
3691
|
+
const elementToAutofocus = document.getElementById(willAutofocusId);
|
3692
|
+
if (elementIsFocusable(elementToAutofocus)) {
|
3693
|
+
elementToAutofocus.focus();
|
3694
|
+
}
|
3695
|
+
if (elementToAutofocus && elementToAutofocus.id == generatedID) {
|
3696
|
+
elementToAutofocus.removeAttribute("id");
|
3569
3697
|
}
|
3570
|
-
return bestElement;
|
3571
3698
|
}
|
3572
|
-
|
3573
|
-
|
3574
|
-
|
3699
|
+
}
|
3700
|
+
|
3701
|
+
async function withPreservedFocus(callback) {
|
3702
|
+
const [activeElementBeforeRender, activeElementAfterRender] = await around(callback, (() => document.activeElement));
|
3703
|
+
const restoreFocusTo = activeElementBeforeRender && activeElementBeforeRender.id;
|
3704
|
+
if (restoreFocusTo) {
|
3705
|
+
const elementToFocus = document.getElementById(restoreFocusTo);
|
3706
|
+
if (elementIsFocusable(elementToFocus) && elementToFocus != activeElementAfterRender) {
|
3707
|
+
elementToFocus.focus();
|
3575
3708
|
}
|
3576
|
-
return 0;
|
3577
3709
|
}
|
3578
|
-
|
3579
|
-
|
3580
|
-
|
3581
|
-
|
3582
|
-
|
3710
|
+
}
|
3711
|
+
|
3712
|
+
function firstAutofocusableElementInStreams(nodeListOfStreamElements) {
|
3713
|
+
for (const streamElement of nodeListOfStreamElements) {
|
3714
|
+
const elementWithAutofocus = queryAutofocusableElement(streamElement.templateElement.content);
|
3715
|
+
if (elementWithAutofocus) return elementWithAutofocus;
|
3583
3716
|
}
|
3584
|
-
|
3585
|
-
|
3717
|
+
return null;
|
3718
|
+
}
|
3719
|
+
|
3720
|
+
class StreamObserver {
|
3721
|
+
sources=new Set;
|
3722
|
+
#started=false;
|
3723
|
+
constructor(delegate) {
|
3724
|
+
this.delegate = delegate;
|
3586
3725
|
}
|
3587
|
-
|
3588
|
-
|
3589
|
-
|
3726
|
+
start() {
|
3727
|
+
if (!this.#started) {
|
3728
|
+
this.#started = true;
|
3729
|
+
addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
|
3730
|
+
}
|
3590
3731
|
}
|
3591
|
-
|
3592
|
-
|
3593
|
-
|
3594
|
-
|
3732
|
+
stop() {
|
3733
|
+
if (this.#started) {
|
3734
|
+
this.#started = false;
|
3735
|
+
removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
|
3595
3736
|
}
|
3596
3737
|
}
|
3597
|
-
|
3598
|
-
|
3599
|
-
|
3600
|
-
|
3601
|
-
if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
|
3602
|
-
++matchCount;
|
3603
|
-
}
|
3738
|
+
connectStreamSource(source) {
|
3739
|
+
if (!this.streamSourceIsConnected(source)) {
|
3740
|
+
this.sources.add(source);
|
3741
|
+
source.addEventListener("message", this.receiveMessageEvent, false);
|
3604
3742
|
}
|
3605
|
-
|
3606
|
-
|
3607
|
-
|
3608
|
-
|
3609
|
-
|
3610
|
-
for (const elt of idElements) {
|
3611
|
-
let current = elt;
|
3612
|
-
while (current !== nodeParent && current != null) {
|
3613
|
-
let idSet = idMap.get(current);
|
3614
|
-
if (idSet == null) {
|
3615
|
-
idSet = new Set;
|
3616
|
-
idMap.set(current, idSet);
|
3617
|
-
}
|
3618
|
-
idSet.add(elt.id);
|
3619
|
-
current = current.parentElement;
|
3620
|
-
}
|
3743
|
+
}
|
3744
|
+
disconnectStreamSource(source) {
|
3745
|
+
if (this.streamSourceIsConnected(source)) {
|
3746
|
+
this.sources.delete(source);
|
3747
|
+
source.removeEventListener("message", this.receiveMessageEvent, false);
|
3621
3748
|
}
|
3622
3749
|
}
|
3623
|
-
|
3624
|
-
|
3625
|
-
populateIdMapForNode(oldContent, idMap);
|
3626
|
-
populateIdMapForNode(newContent, idMap);
|
3627
|
-
return idMap;
|
3750
|
+
streamSourceIsConnected(source) {
|
3751
|
+
return this.sources.has(source);
|
3628
3752
|
}
|
3629
|
-
|
3630
|
-
|
3631
|
-
|
3753
|
+
inspectFetchResponse=event => {
|
3754
|
+
const response = fetchResponseFromEvent(event);
|
3755
|
+
if (response && fetchResponseIsStream(response)) {
|
3756
|
+
event.preventDefault();
|
3757
|
+
this.receiveMessageResponse(response);
|
3758
|
+
}
|
3632
3759
|
};
|
3633
|
-
|
3634
|
-
|
3635
|
-
|
3636
|
-
|
3637
|
-
|
3638
|
-
|
3639
|
-
|
3760
|
+
receiveMessageEvent=event => {
|
3761
|
+
if (this.#started && typeof event.data == "string") {
|
3762
|
+
this.receiveMessageHTML(event.data);
|
3763
|
+
}
|
3764
|
+
};
|
3765
|
+
async receiveMessageResponse(response) {
|
3766
|
+
const html = await response.responseHTML;
|
3767
|
+
if (html) {
|
3768
|
+
this.receiveMessageHTML(html);
|
3769
|
+
}
|
3770
|
+
}
|
3771
|
+
receiveMessageHTML(html) {
|
3772
|
+
this.delegate.receivedMessageFromStream(StreamMessage.wrap(html));
|
3773
|
+
}
|
3640
3774
|
}
|
3641
3775
|
|
3642
|
-
function
|
3643
|
-
|
3644
|
-
|
3645
|
-
|
3776
|
+
function fetchResponseFromEvent(event) {
|
3777
|
+
const fetchResponse = event.detail?.fetchResponse;
|
3778
|
+
if (fetchResponse instanceof FetchResponse) {
|
3779
|
+
return fetchResponse;
|
3780
|
+
}
|
3646
3781
|
}
|
3647
3782
|
|
3648
|
-
|
3649
|
-
|
3650
|
-
|
3651
|
-
this.#beforeNodeMorphed = beforeNodeMorphed || (() => true);
|
3652
|
-
}
|
3653
|
-
beforeNodeAdded=node => !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id));
|
3654
|
-
beforeNodeMorphed=(currentElement, newElement) => {
|
3655
|
-
if (currentElement instanceof Element) {
|
3656
|
-
if (!currentElement.hasAttribute("data-turbo-permanent") && this.#beforeNodeMorphed(currentElement, newElement)) {
|
3657
|
-
const event = dispatch("turbo:before-morph-element", {
|
3658
|
-
cancelable: true,
|
3659
|
-
target: currentElement,
|
3660
|
-
detail: {
|
3661
|
-
currentElement: currentElement,
|
3662
|
-
newElement: newElement
|
3663
|
-
}
|
3664
|
-
});
|
3665
|
-
return !event.defaultPrevented;
|
3666
|
-
} else {
|
3667
|
-
return false;
|
3668
|
-
}
|
3669
|
-
}
|
3670
|
-
};
|
3671
|
-
beforeAttributeUpdated=(attributeName, target, mutationType) => {
|
3672
|
-
const event = dispatch("turbo:before-morph-attribute", {
|
3673
|
-
cancelable: true,
|
3674
|
-
target: target,
|
3675
|
-
detail: {
|
3676
|
-
attributeName: attributeName,
|
3677
|
-
mutationType: mutationType
|
3678
|
-
}
|
3679
|
-
});
|
3680
|
-
return !event.defaultPrevented;
|
3681
|
-
};
|
3682
|
-
beforeNodeRemoved=node => this.beforeNodeMorphed(node);
|
3683
|
-
afterNodeMorphed=(currentElement, newElement) => {
|
3684
|
-
if (currentElement instanceof Element) {
|
3685
|
-
dispatch("turbo:morph-element", {
|
3686
|
-
target: currentElement,
|
3687
|
-
detail: {
|
3688
|
-
currentElement: currentElement,
|
3689
|
-
newElement: newElement
|
3690
|
-
}
|
3691
|
-
});
|
3692
|
-
}
|
3693
|
-
};
|
3783
|
+
function fetchResponseIsStream(response) {
|
3784
|
+
const contentType = response.contentType ?? "";
|
3785
|
+
return contentType.startsWith(StreamMessage.contentType);
|
3694
3786
|
}
|
3695
3787
|
|
3696
|
-
class
|
3788
|
+
class ErrorRenderer extends Renderer {
|
3697
3789
|
static renderElement(currentElement, newElement) {
|
3698
|
-
|
3699
|
-
|
3700
|
-
|
3701
|
-
|
3702
|
-
|
3790
|
+
const {documentElement: documentElement, body: body} = document;
|
3791
|
+
documentElement.replaceChild(newElement, body);
|
3792
|
+
}
|
3793
|
+
async render() {
|
3794
|
+
this.replaceHeadAndBody();
|
3795
|
+
this.activateScriptElements();
|
3796
|
+
}
|
3797
|
+
replaceHeadAndBody() {
|
3798
|
+
const {documentElement: documentElement, head: head} = document;
|
3799
|
+
documentElement.replaceChild(this.newHead, head);
|
3800
|
+
this.renderElement(this.currentElement, this.newElement);
|
3801
|
+
}
|
3802
|
+
activateScriptElements() {
|
3803
|
+
for (const replaceableElement of this.scriptElements) {
|
3804
|
+
const parentNode = replaceableElement.parentNode;
|
3805
|
+
if (parentNode) {
|
3806
|
+
const element = activateScriptElement(replaceableElement);
|
3807
|
+
parentNode.replaceChild(element, replaceableElement);
|
3703
3808
|
}
|
3704
|
-
}
|
3705
|
-
|
3809
|
+
}
|
3810
|
+
}
|
3811
|
+
get newHead() {
|
3812
|
+
return this.newSnapshot.headSnapshot.element;
|
3813
|
+
}
|
3814
|
+
get scriptElements() {
|
3815
|
+
return document.documentElement.querySelectorAll("script");
|
3706
3816
|
}
|
3707
3817
|
}
|
3708
3818
|
|
@@ -3882,7 +3992,7 @@ class MorphingPageRenderer extends PageRenderer {
|
|
3882
3992
|
}
|
3883
3993
|
});
|
3884
3994
|
for (const frame of currentElement.querySelectorAll("turbo-frame")) {
|
3885
|
-
if (canRefreshFrame(frame))
|
3995
|
+
if (canRefreshFrame(frame)) frame.reload();
|
3886
3996
|
}
|
3887
3997
|
dispatch("turbo:morph", {
|
3888
3998
|
detail: {
|
@@ -3906,15 +4016,6 @@ function canRefreshFrame(frame) {
|
|
3906
4016
|
return frame instanceof FrameElement && frame.src && frame.refresh === "morph" && !frame.closest("[data-turbo-permanent]");
|
3907
4017
|
}
|
3908
4018
|
|
3909
|
-
function refreshFrame(frame) {
|
3910
|
-
frame.addEventListener("turbo:before-frame-render", (({detail: detail}) => {
|
3911
|
-
detail.render = MorphingFrameRenderer.renderElement;
|
3912
|
-
}), {
|
3913
|
-
once: true
|
3914
|
-
});
|
3915
|
-
frame.reload();
|
3916
|
-
}
|
3917
|
-
|
3918
4019
|
class SnapshotCache {
|
3919
4020
|
keys=[];
|
3920
4021
|
snapshots={};
|
@@ -3969,7 +4070,7 @@ class PageView extends View {
|
|
3969
4070
|
renderPage(snapshot, isPreview = false, willRender = true, visit) {
|
3970
4071
|
const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage;
|
3971
4072
|
const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer;
|
3972
|
-
const renderer = new rendererClass(this.snapshot, snapshot,
|
4073
|
+
const renderer = new rendererClass(this.snapshot, snapshot, isPreview, willRender);
|
3973
4074
|
if (!renderer.shouldRender) {
|
3974
4075
|
this.forceReloaded = true;
|
3975
4076
|
} else {
|
@@ -3979,7 +4080,7 @@ class PageView extends View {
|
|
3979
4080
|
}
|
3980
4081
|
renderError(snapshot, visit) {
|
3981
4082
|
visit?.changeHistory();
|
3982
|
-
const renderer = new ErrorRenderer(this.snapshot, snapshot,
|
4083
|
+
const renderer = new ErrorRenderer(this.snapshot, snapshot, false);
|
3983
4084
|
return this.render(renderer);
|
3984
4085
|
}
|
3985
4086
|
clearSnapshotCache() {
|
@@ -4097,11 +4198,8 @@ class Session {
|
|
4097
4198
|
frameRedirector=new FrameRedirector(this, document.documentElement);
|
4098
4199
|
streamMessageRenderer=new StreamMessageRenderer;
|
4099
4200
|
cache=new Cache(this);
|
4100
|
-
drive=true;
|
4101
4201
|
enabled=true;
|
4102
|
-
progressBarDelay=500;
|
4103
4202
|
started=false;
|
4104
|
-
formMode="on";
|
4105
4203
|
#pageRefreshDebouncePeriod=150;
|
4106
4204
|
constructor(recentRequests) {
|
4107
4205
|
this.recentRequests = recentRequests;
|
@@ -4160,7 +4258,8 @@ class Session {
|
|
4160
4258
|
}
|
4161
4259
|
refresh(url, requestId) {
|
4162
4260
|
const isRecentRequest = requestId && this.recentRequests.has(requestId);
|
4163
|
-
|
4261
|
+
const isCurrentUrl = url === document.baseURI;
|
4262
|
+
if (!isRecentRequest && !this.navigator.currentVisit && isCurrentUrl) {
|
4164
4263
|
this.visit(url, {
|
4165
4264
|
action: "replace",
|
4166
4265
|
shouldCacheSnapshot: false
|
@@ -4180,10 +4279,26 @@ class Session {
|
|
4180
4279
|
this.view.clearSnapshotCache();
|
4181
4280
|
}
|
4182
4281
|
setProgressBarDelay(delay) {
|
4282
|
+
console.warn("Please replace `session.setProgressBarDelay(delay)` with `session.progressBarDelay = delay`. The function is deprecated and will be removed in a future version of Turbo.`");
|
4183
4283
|
this.progressBarDelay = delay;
|
4184
4284
|
}
|
4185
|
-
|
4186
|
-
|
4285
|
+
set progressBarDelay(delay) {
|
4286
|
+
config.drive.progressBarDelay = delay;
|
4287
|
+
}
|
4288
|
+
get progressBarDelay() {
|
4289
|
+
return config.drive.progressBarDelay;
|
4290
|
+
}
|
4291
|
+
set drive(value) {
|
4292
|
+
config.drive.enabled = value;
|
4293
|
+
}
|
4294
|
+
get drive() {
|
4295
|
+
return config.drive.enabled;
|
4296
|
+
}
|
4297
|
+
set formMode(value) {
|
4298
|
+
config.forms.mode = value;
|
4299
|
+
}
|
4300
|
+
get formMode() {
|
4301
|
+
return config.forms.mode;
|
4187
4302
|
}
|
4188
4303
|
get location() {
|
4189
4304
|
return this.history.location;
|
@@ -4233,7 +4348,7 @@ class Session {
|
|
4233
4348
|
}
|
4234
4349
|
submittedFormLinkToLocation() {}
|
4235
4350
|
canPrefetchRequestToLocation(link, location) {
|
4236
|
-
return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation);
|
4351
|
+
return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation) && this.navigator.linkPrefetchingIsEnabledForLocation(location);
|
4237
4352
|
}
|
4238
4353
|
willFollowLinkToLocation(link, location, event) {
|
4239
4354
|
return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation) && this.applicationAllowsFollowingLinkToLocation(link, location, event);
|
@@ -4405,11 +4520,11 @@ class Session {
|
|
4405
4520
|
});
|
4406
4521
|
}
|
4407
4522
|
submissionIsNavigatable(form, submitter) {
|
4408
|
-
if (
|
4523
|
+
if (config.forms.mode == "off") {
|
4409
4524
|
return false;
|
4410
4525
|
} else {
|
4411
4526
|
const submitterIsNavigatable = submitter ? this.elementIsNavigatable(submitter) : true;
|
4412
|
-
if (
|
4527
|
+
if (config.forms.mode == "optin") {
|
4413
4528
|
return submitterIsNavigatable && form.closest('[data-turbo="true"]') != null;
|
4414
4529
|
} else {
|
4415
4530
|
return submitterIsNavigatable && this.elementIsNavigatable(form);
|
@@ -4419,7 +4534,7 @@ class Session {
|
|
4419
4534
|
elementIsNavigatable(element) {
|
4420
4535
|
const container = findClosestRecursively(element, "[data-turbo]");
|
4421
4536
|
const withinFrame = findClosestRecursively(element, "turbo-frame");
|
4422
|
-
if (
|
4537
|
+
if (config.drive.enabled || withinFrame) {
|
4423
4538
|
if (container) {
|
4424
4539
|
return container.getAttribute("data-turbo") != "false";
|
4425
4540
|
} else {
|
@@ -4487,15 +4602,18 @@ function clearCache() {
|
|
4487
4602
|
}
|
4488
4603
|
|
4489
4604
|
function setProgressBarDelay(delay) {
|
4490
|
-
|
4605
|
+
console.warn("Please replace `Turbo.setProgressBarDelay(delay)` with `Turbo.config.drive.progressBarDelay = delay`. The top-level function is deprecated and will be removed in a future version of Turbo.`");
|
4606
|
+
config.drive.progressBarDelay = delay;
|
4491
4607
|
}
|
4492
4608
|
|
4493
4609
|
function setConfirmMethod(confirmMethod) {
|
4494
|
-
|
4610
|
+
console.warn("Please replace `Turbo.setConfirmMethod(confirmMethod)` with `Turbo.config.forms.confirm = confirmMethod`. The top-level function is deprecated and will be removed in a future version of Turbo.`");
|
4611
|
+
config.forms.confirm = confirmMethod;
|
4495
4612
|
}
|
4496
4613
|
|
4497
4614
|
function setFormMode(mode) {
|
4498
|
-
|
4615
|
+
console.warn("Please replace `Turbo.setFormMode(mode)` with `Turbo.config.forms.mode = mode`. The top-level function is deprecated and will be removed in a future version of Turbo.`");
|
4616
|
+
config.forms.mode = mode;
|
4499
4617
|
}
|
4500
4618
|
|
4501
4619
|
var Turbo = Object.freeze({
|
@@ -4507,6 +4625,7 @@ var Turbo = Object.freeze({
|
|
4507
4625
|
PageSnapshot: PageSnapshot,
|
4508
4626
|
FrameRenderer: FrameRenderer,
|
4509
4627
|
fetch: fetchWithTurboHeaders,
|
4628
|
+
config: config,
|
4510
4629
|
start: start,
|
4511
4630
|
registerAdapter: registerAdapter,
|
4512
4631
|
visit: visit,
|
@@ -4528,6 +4647,7 @@ class FrameController {
|
|
4528
4647
|
#connected=false;
|
4529
4648
|
#hasBeenLoaded=false;
|
4530
4649
|
#ignoredAttributes=new Set;
|
4650
|
+
#shouldMorphFrame=false;
|
4531
4651
|
action=null;
|
4532
4652
|
constructor(element) {
|
4533
4653
|
this.element = element;
|
@@ -4575,7 +4695,8 @@ class FrameController {
|
|
4575
4695
|
}
|
4576
4696
|
}
|
4577
4697
|
sourceURLReloaded() {
|
4578
|
-
const {src: src} = this.element;
|
4698
|
+
const {refresh: refresh, src: src} = this.element;
|
4699
|
+
this.#shouldMorphFrame = src && refresh === "morph";
|
4579
4700
|
this.element.removeAttribute("complete");
|
4580
4701
|
this.element.src = null;
|
4581
4702
|
this.element.src = src;
|
@@ -4613,6 +4734,7 @@ class FrameController {
|
|
4613
4734
|
}
|
4614
4735
|
}
|
4615
4736
|
} finally {
|
4737
|
+
this.#shouldMorphFrame = false;
|
4616
4738
|
this.fetchResponseLoaded = () => Promise.resolve();
|
4617
4739
|
}
|
4618
4740
|
}
|
@@ -4725,9 +4847,10 @@ class FrameController {
|
|
4725
4847
|
};
|
4726
4848
|
async #loadFrameResponse(fetchResponse, document) {
|
4727
4849
|
const newFrameElement = await this.extractForeignFrameElement(document.body);
|
4850
|
+
const rendererClass = this.#shouldMorphFrame ? MorphingFrameRenderer : FrameRenderer;
|
4728
4851
|
if (newFrameElement) {
|
4729
4852
|
const snapshot = new Snapshot(newFrameElement);
|
4730
|
-
const renderer = new
|
4853
|
+
const renderer = new rendererClass(this, this.view.snapshot, snapshot, false, false);
|
4731
4854
|
if (this.view.renderPromise) await this.view.renderPromise;
|
4732
4855
|
this.changeHistory();
|
4733
4856
|
await this.view.render(renderer);
|
@@ -5042,9 +5165,9 @@ class StreamElement extends HTMLElement {
|
|
5042
5165
|
this.duplicateChildren.forEach((c => c.remove()));
|
5043
5166
|
}
|
5044
5167
|
get duplicateChildren() {
|
5045
|
-
const existingChildren = this.targetElements.flatMap((e => [ ...e.children ])).filter((c => !!c.id));
|
5046
|
-
const newChildrenIds = [ ...this.templateContent?.children || [] ].filter((c => !!c.id)).map((c => c.id));
|
5047
|
-
return existingChildren.filter((c => newChildrenIds.includes(c.id)));
|
5168
|
+
const existingChildren = this.targetElements.flatMap((e => [ ...e.children ])).filter((c => !!c.getAttribute("id")));
|
5169
|
+
const newChildrenIds = [ ...this.templateContent?.children || [] ].filter((c => !!c.getAttribute("id"))).map((c => c.getAttribute("id")));
|
5170
|
+
return existingChildren.filter((c => newChildrenIds.includes(c.getAttribute("id"))));
|
5048
5171
|
}
|
5049
5172
|
get performAction() {
|
5050
5173
|
if (this.action) {
|
@@ -5200,6 +5323,7 @@ var Turbo$1 = Object.freeze({
|
|
5200
5323
|
StreamSourceElement: StreamSourceElement,
|
5201
5324
|
cache: cache,
|
5202
5325
|
clearCache: clearCache,
|
5326
|
+
config: config,
|
5203
5327
|
connectStreamSource: connectStreamSource,
|
5204
5328
|
disconnectStreamSource: disconnectStreamSource,
|
5205
5329
|
fetch: fetchWithTurboHeaders,
|
@@ -5261,6 +5385,7 @@ function walk(obj) {
|
|
5261
5385
|
}
|
5262
5386
|
|
5263
5387
|
class TurboCableStreamSourceElement extends HTMLElement {
|
5388
|
+
static observedAttributes=[ "channel", "signed-stream-name" ];
|
5264
5389
|
async connectedCallback() {
|
5265
5390
|
connectStreamSource(this);
|
5266
5391
|
this.subscription = await subscribeTo(this.channel, {
|
@@ -5272,6 +5397,13 @@ class TurboCableStreamSourceElement extends HTMLElement {
|
|
5272
5397
|
disconnectedCallback() {
|
5273
5398
|
disconnectStreamSource(this);
|
5274
5399
|
if (this.subscription) this.subscription.unsubscribe();
|
5400
|
+
this.subscriptionDisconnected();
|
5401
|
+
}
|
5402
|
+
attributeChangedCallback() {
|
5403
|
+
if (this.subscription) {
|
5404
|
+
this.disconnectedCallback();
|
5405
|
+
this.connectedCallback();
|
5406
|
+
}
|
5275
5407
|
}
|
5276
5408
|
dispatchMessageEvent(data) {
|
5277
5409
|
const event = new MessageEvent("message", {
|