turbo-rails 1.5.0 → 2.0.11
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 +126 -16
- data/app/assets/javascripts/turbo.js +2226 -953
- data/app/assets/javascripts/turbo.min.js +9 -5
- data/app/assets/javascripts/turbo.min.js.map +1 -1
- data/app/channels/turbo/streams/broadcasts.rb +47 -10
- data/app/channels/turbo/streams_channel.rb +15 -15
- data/app/controllers/concerns/turbo/request_id_tracking.rb +12 -0
- data/app/controllers/turbo/frames/frame_request.rb +2 -2
- data/app/controllers/turbo/native/navigation.rb +17 -11
- data/app/helpers/turbo/drive_helper.rb +72 -14
- data/app/helpers/turbo/frames_helper.rb +8 -8
- data/app/helpers/turbo/streams/action_helper.rb +12 -4
- data/app/helpers/turbo/streams_helper.rb +5 -0
- data/app/javascript/turbo/cable_stream_source_element.js +10 -0
- data/app/javascript/turbo/index.js +2 -0
- data/app/jobs/turbo/streams/action_broadcast_job.rb +2 -2
- data/app/jobs/turbo/streams/broadcast_job.rb +1 -1
- data/app/jobs/turbo/streams/broadcast_stream_job.rb +7 -0
- data/app/models/concerns/turbo/broadcastable.rb +201 -42
- data/app/models/turbo/debouncer.rb +24 -0
- data/app/models/turbo/streams/tag_builder.rb +50 -12
- data/app/models/turbo/thread_debouncer.rb +28 -0
- data/config/routes.rb +3 -4
- data/lib/install/turbo_with_importmap.rb +1 -1
- data/lib/tasks/turbo_tasks.rake +0 -22
- data/lib/turbo/broadcastable/test_helper.rb +5 -5
- data/lib/turbo/engine.rb +80 -9
- data/lib/turbo/system_test_helper.rb +128 -0
- data/lib/turbo/test_assertions/integration_test_assertions.rb +2 -2
- data/lib/turbo/test_assertions.rb +2 -2
- data/lib/turbo/version.rb +1 -1
- data/lib/turbo-rails.rb +10 -0
- metadata +10 -19
- data/lib/install/turbo_needs_redis.rb +0 -20
@@ -1,19 +1,7 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
const BuiltInHTMLElement = HTMLElement;
|
6
|
-
const wrapperForTheName = {
|
7
|
-
HTMLElement: function HTMLElement() {
|
8
|
-
return Reflect.construct(BuiltInHTMLElement, [], this.constructor);
|
9
|
-
}
|
10
|
-
};
|
11
|
-
window.HTMLElement = wrapperForTheName["HTMLElement"];
|
12
|
-
HTMLElement.prototype = BuiltInHTMLElement.prototype;
|
13
|
-
HTMLElement.prototype.constructor = HTMLElement;
|
14
|
-
Object.setPrototypeOf(HTMLElement, BuiltInHTMLElement);
|
15
|
-
})();
|
16
|
-
|
1
|
+
/*!
|
2
|
+
Turbo 8.0.12
|
3
|
+
Copyright © 2024 37signals LLC
|
4
|
+
*/
|
17
5
|
(function(prototype) {
|
18
6
|
if (typeof prototype.requestSubmit == "function") return;
|
19
7
|
prototype.requestSubmit = function(submitter) {
|
@@ -44,7 +32,7 @@ const submittersByForm = new WeakMap;
|
|
44
32
|
function findSubmitterFromClickTarget(target) {
|
45
33
|
const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
|
46
34
|
const candidate = element ? element.closest("input, button") : null;
|
47
|
-
return
|
35
|
+
return candidate?.type == "submit" ? candidate : null;
|
48
36
|
}
|
49
37
|
|
50
38
|
function clickCaptured(event) {
|
@@ -57,10 +45,13 @@ function clickCaptured(event) {
|
|
57
45
|
(function() {
|
58
46
|
if ("submitter" in Event.prototype) return;
|
59
47
|
let prototype = window.Event.prototype;
|
60
|
-
if ("SubmitEvent" in window
|
61
|
-
|
62
|
-
|
63
|
-
|
48
|
+
if ("SubmitEvent" in window) {
|
49
|
+
const prototypeOfSubmitEvent = window.SubmitEvent.prototype;
|
50
|
+
if (/Apple Computer/.test(navigator.vendor) && !("submitter" in prototypeOfSubmitEvent)) {
|
51
|
+
prototype = prototypeOfSubmitEvent;
|
52
|
+
} else {
|
53
|
+
return;
|
54
|
+
}
|
64
55
|
}
|
65
56
|
addEventListener("click", clickCaptured, true);
|
66
57
|
Object.defineProperty(prototype, "submitter", {
|
@@ -72,20 +63,19 @@ function clickCaptured(event) {
|
|
72
63
|
});
|
73
64
|
})();
|
74
65
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
FrameLoadingStyle["lazy"] = "lazy";
|
80
|
-
})(FrameLoadingStyle || (FrameLoadingStyle = {}));
|
66
|
+
const FrameLoadingStyle = {
|
67
|
+
eager: "eager",
|
68
|
+
lazy: "lazy"
|
69
|
+
};
|
81
70
|
|
82
71
|
class FrameElement extends HTMLElement {
|
72
|
+
static delegateConstructor=undefined;
|
73
|
+
loaded=Promise.resolve();
|
83
74
|
static get observedAttributes() {
|
84
|
-
return [ "disabled", "
|
75
|
+
return [ "disabled", "loading", "src" ];
|
85
76
|
}
|
86
77
|
constructor() {
|
87
78
|
super();
|
88
|
-
this.loaded = Promise.resolve();
|
89
79
|
this.delegate = new FrameElement.delegateConstructor(this);
|
90
80
|
}
|
91
81
|
connectedCallback() {
|
@@ -100,11 +90,9 @@ class FrameElement extends HTMLElement {
|
|
100
90
|
attributeChangedCallback(name) {
|
101
91
|
if (name == "loading") {
|
102
92
|
this.delegate.loadingStyleChanged();
|
103
|
-
} else if (name == "complete") {
|
104
|
-
this.delegate.completeChanged();
|
105
93
|
} else if (name == "src") {
|
106
94
|
this.delegate.sourceURLChanged();
|
107
|
-
} else {
|
95
|
+
} else if (name == "disabled") {
|
108
96
|
this.delegate.disabledChanged();
|
109
97
|
}
|
110
98
|
}
|
@@ -118,6 +106,19 @@ class FrameElement extends HTMLElement {
|
|
118
106
|
this.removeAttribute("src");
|
119
107
|
}
|
120
108
|
}
|
109
|
+
get refresh() {
|
110
|
+
return this.getAttribute("refresh");
|
111
|
+
}
|
112
|
+
set refresh(value) {
|
113
|
+
if (value) {
|
114
|
+
this.setAttribute("refresh", value);
|
115
|
+
} else {
|
116
|
+
this.removeAttribute("refresh");
|
117
|
+
}
|
118
|
+
}
|
119
|
+
get shouldReloadWithMorph() {
|
120
|
+
return this.src && this.refresh === "morph";
|
121
|
+
}
|
121
122
|
get loading() {
|
122
123
|
return frameLoadingStyleFromString(this.getAttribute("loading") || "");
|
123
124
|
}
|
@@ -155,8 +156,7 @@ class FrameElement extends HTMLElement {
|
|
155
156
|
return this.ownerDocument === document && !this.isPreview;
|
156
157
|
}
|
157
158
|
get isPreview() {
|
158
|
-
|
159
|
-
return (_b = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.documentElement) === null || _b === void 0 ? void 0 : _b.hasAttribute("data-turbo-preview");
|
159
|
+
return this.ownerDocument?.documentElement?.hasAttribute("data-turbo-preview");
|
160
160
|
}
|
161
161
|
}
|
162
162
|
|
@@ -170,122 +170,18 @@ function frameLoadingStyleFromString(style) {
|
|
170
170
|
}
|
171
171
|
}
|
172
172
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
let anchorMatch;
|
179
|
-
if (url.hash) {
|
180
|
-
return url.hash.slice(1);
|
181
|
-
} else if (anchorMatch = url.href.match(/#(.*)$/)) {
|
182
|
-
return anchorMatch[1];
|
183
|
-
}
|
184
|
-
}
|
185
|
-
|
186
|
-
function getAction(form, submitter) {
|
187
|
-
const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formaction")) || form.getAttribute("action") || form.action;
|
188
|
-
return expandURL(action);
|
189
|
-
}
|
190
|
-
|
191
|
-
function getExtension(url) {
|
192
|
-
return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
|
193
|
-
}
|
194
|
-
|
195
|
-
function isHTML(url) {
|
196
|
-
return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml|php))$/);
|
197
|
-
}
|
198
|
-
|
199
|
-
function isPrefixedBy(baseURL, url) {
|
200
|
-
const prefix = getPrefix(url);
|
201
|
-
return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
|
202
|
-
}
|
203
|
-
|
204
|
-
function locationIsVisitable(location, rootLocation) {
|
205
|
-
return isPrefixedBy(location, rootLocation) && isHTML(location);
|
206
|
-
}
|
207
|
-
|
208
|
-
function getRequestURL(url) {
|
209
|
-
const anchor = getAnchor(url);
|
210
|
-
return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href;
|
211
|
-
}
|
212
|
-
|
213
|
-
function toCacheKey(url) {
|
214
|
-
return getRequestURL(url);
|
215
|
-
}
|
216
|
-
|
217
|
-
function urlsAreEqual(left, right) {
|
218
|
-
return expandURL(left).href == expandURL(right).href;
|
219
|
-
}
|
220
|
-
|
221
|
-
function getPathComponents(url) {
|
222
|
-
return url.pathname.split("/").slice(1);
|
223
|
-
}
|
224
|
-
|
225
|
-
function getLastPathComponent(url) {
|
226
|
-
return getPathComponents(url).slice(-1)[0];
|
227
|
-
}
|
228
|
-
|
229
|
-
function getPrefix(url) {
|
230
|
-
return addTrailingSlash(url.origin + url.pathname);
|
231
|
-
}
|
232
|
-
|
233
|
-
function addTrailingSlash(value) {
|
234
|
-
return value.endsWith("/") ? value : value + "/";
|
235
|
-
}
|
236
|
-
|
237
|
-
class FetchResponse {
|
238
|
-
constructor(response) {
|
239
|
-
this.response = response;
|
240
|
-
}
|
241
|
-
get succeeded() {
|
242
|
-
return this.response.ok;
|
243
|
-
}
|
244
|
-
get failed() {
|
245
|
-
return !this.succeeded;
|
246
|
-
}
|
247
|
-
get clientError() {
|
248
|
-
return this.statusCode >= 400 && this.statusCode <= 499;
|
249
|
-
}
|
250
|
-
get serverError() {
|
251
|
-
return this.statusCode >= 500 && this.statusCode <= 599;
|
252
|
-
}
|
253
|
-
get redirected() {
|
254
|
-
return this.response.redirected;
|
255
|
-
}
|
256
|
-
get location() {
|
257
|
-
return expandURL(this.response.url);
|
258
|
-
}
|
259
|
-
get isHTML() {
|
260
|
-
return this.contentType && this.contentType.match(/^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/);
|
261
|
-
}
|
262
|
-
get statusCode() {
|
263
|
-
return this.response.status;
|
264
|
-
}
|
265
|
-
get contentType() {
|
266
|
-
return this.header("Content-Type");
|
267
|
-
}
|
268
|
-
get responseText() {
|
269
|
-
return this.response.clone().text();
|
270
|
-
}
|
271
|
-
get responseHTML() {
|
272
|
-
if (this.isHTML) {
|
273
|
-
return this.response.clone().text();
|
274
|
-
} else {
|
275
|
-
return Promise.resolve(undefined);
|
276
|
-
}
|
277
|
-
}
|
278
|
-
header(name) {
|
279
|
-
return this.response.headers.get(name);
|
280
|
-
}
|
281
|
-
}
|
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
|
+
};
|
282
178
|
|
283
179
|
function activateScriptElement(element) {
|
284
180
|
if (element.getAttribute("data-turbo-eval") == "false") {
|
285
181
|
return element;
|
286
182
|
} else {
|
287
183
|
const createdScriptElement = document.createElement("script");
|
288
|
-
const cspNonce =
|
184
|
+
const cspNonce = getCspNonce();
|
289
185
|
if (cspNonce) {
|
290
186
|
createdScriptElement.nonce = cspNonce;
|
291
187
|
}
|
@@ -323,6 +219,19 @@ function dispatch(eventName, {target: target, cancelable: cancelable, detail: de
|
|
323
219
|
return event;
|
324
220
|
}
|
325
221
|
|
222
|
+
function cancelEvent(event) {
|
223
|
+
event.preventDefault();
|
224
|
+
event.stopImmediatePropagation();
|
225
|
+
}
|
226
|
+
|
227
|
+
function nextRepaint() {
|
228
|
+
if (document.visibilityState === "hidden") {
|
229
|
+
return nextEventLoopTick();
|
230
|
+
} else {
|
231
|
+
return nextAnimationFrame();
|
232
|
+
}
|
233
|
+
}
|
234
|
+
|
326
235
|
function nextAnimationFrame() {
|
327
236
|
return new Promise((resolve => requestAnimationFrame((() => resolve()))));
|
328
237
|
}
|
@@ -370,7 +279,7 @@ function uuid() {
|
|
370
279
|
}
|
371
280
|
|
372
281
|
function getAttribute(attributeName, ...elements) {
|
373
|
-
for (const value of elements.map((element => element
|
282
|
+
for (const value of elements.map((element => element?.getAttribute(attributeName)))) {
|
374
283
|
if (typeof value == "string") return value;
|
375
284
|
}
|
376
285
|
return null;
|
@@ -444,6 +353,14 @@ function getMetaContent(name) {
|
|
444
353
|
return element && element.content;
|
445
354
|
}
|
446
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
|
+
|
447
364
|
function setMetaContent(name, content) {
|
448
365
|
let element = getMetaElement(name);
|
449
366
|
if (!element) {
|
@@ -456,21 +373,233 @@ function setMetaContent(name, content) {
|
|
456
373
|
}
|
457
374
|
|
458
375
|
function findClosestRecursively(element, selector) {
|
459
|
-
var _a;
|
460
376
|
if (element instanceof Element) {
|
461
|
-
return element.closest(selector) || findClosestRecursively(element.assignedSlot ||
|
377
|
+
return element.closest(selector) || findClosestRecursively(element.assignedSlot || element.getRootNode()?.host, selector);
|
378
|
+
}
|
379
|
+
}
|
380
|
+
|
381
|
+
function elementIsFocusable(element) {
|
382
|
+
const inertDisabledOrHidden = "[inert], :disabled, [hidden], details:not([open]), dialog:not([open])";
|
383
|
+
return !!element && element.closest(inertDisabledOrHidden) == null && typeof element.focus == "function";
|
384
|
+
}
|
385
|
+
|
386
|
+
function queryAutofocusableElement(elementOrDocumentFragment) {
|
387
|
+
return Array.from(elementOrDocumentFragment.querySelectorAll("[autofocus]")).find(elementIsFocusable);
|
388
|
+
}
|
389
|
+
|
390
|
+
async function around(callback, reader) {
|
391
|
+
const before = reader();
|
392
|
+
callback();
|
393
|
+
await nextAnimationFrame();
|
394
|
+
const after = reader();
|
395
|
+
return [ before, after ];
|
396
|
+
}
|
397
|
+
|
398
|
+
function doesNotTargetIFrame(name) {
|
399
|
+
if (name === "_blank") {
|
400
|
+
return false;
|
401
|
+
} else if (name) {
|
402
|
+
for (const element of document.getElementsByName(name)) {
|
403
|
+
if (element instanceof HTMLIFrameElement) return false;
|
404
|
+
}
|
405
|
+
return true;
|
406
|
+
} else {
|
407
|
+
return true;
|
408
|
+
}
|
409
|
+
}
|
410
|
+
|
411
|
+
function findLinkFromClickTarget(target) {
|
412
|
+
return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])");
|
413
|
+
}
|
414
|
+
|
415
|
+
function getLocationForLink(link) {
|
416
|
+
return expandURL(link.getAttribute("href") || "");
|
417
|
+
}
|
418
|
+
|
419
|
+
function debounce(fn, delay) {
|
420
|
+
let timeoutId = null;
|
421
|
+
return (...args) => {
|
422
|
+
const callback = () => fn.apply(this, args);
|
423
|
+
clearTimeout(timeoutId);
|
424
|
+
timeoutId = setTimeout(callback, delay);
|
425
|
+
};
|
426
|
+
}
|
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;
|
462
455
|
}
|
463
456
|
}
|
464
457
|
|
465
|
-
|
458
|
+
const forms = new Config({
|
459
|
+
mode: "on",
|
460
|
+
submitter: "disabled"
|
461
|
+
});
|
466
462
|
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
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
|
+
|
574
|
+
class LimitedSet extends Set {
|
575
|
+
constructor(maxSize) {
|
576
|
+
super();
|
577
|
+
this.maxSize = maxSize;
|
578
|
+
}
|
579
|
+
add(value) {
|
580
|
+
if (this.size >= this.maxSize) {
|
581
|
+
const iterator = this.values();
|
582
|
+
const oldestValue = iterator.next().value;
|
583
|
+
this.delete(oldestValue);
|
584
|
+
}
|
585
|
+
super.add(value);
|
586
|
+
}
|
587
|
+
}
|
588
|
+
|
589
|
+
const recentRequests = new LimitedSet(20);
|
590
|
+
|
591
|
+
const nativeFetch = window.fetch;
|
592
|
+
|
593
|
+
function fetchWithTurboHeaders(url, options = {}) {
|
594
|
+
const modifiedHeaders = new Headers(options.headers || {});
|
595
|
+
const requestUID = uuid();
|
596
|
+
recentRequests.add(requestUID);
|
597
|
+
modifiedHeaders.append("X-Turbo-Request-Id", requestUID);
|
598
|
+
return nativeFetch(url, {
|
599
|
+
...options,
|
600
|
+
headers: modifiedHeaders
|
601
|
+
});
|
602
|
+
}
|
474
603
|
|
475
604
|
function fetchMethodFromString(method) {
|
476
605
|
switch (method.toLowerCase()) {
|
@@ -491,16 +620,81 @@ function fetchMethodFromString(method) {
|
|
491
620
|
}
|
492
621
|
}
|
493
622
|
|
623
|
+
const FetchMethod = {
|
624
|
+
get: "get",
|
625
|
+
post: "post",
|
626
|
+
put: "put",
|
627
|
+
patch: "patch",
|
628
|
+
delete: "delete"
|
629
|
+
};
|
630
|
+
|
631
|
+
function fetchEnctypeFromString(encoding) {
|
632
|
+
switch (encoding.toLowerCase()) {
|
633
|
+
case FetchEnctype.multipart:
|
634
|
+
return FetchEnctype.multipart;
|
635
|
+
|
636
|
+
case FetchEnctype.plain:
|
637
|
+
return FetchEnctype.plain;
|
638
|
+
|
639
|
+
default:
|
640
|
+
return FetchEnctype.urlEncoded;
|
641
|
+
}
|
642
|
+
}
|
643
|
+
|
644
|
+
const FetchEnctype = {
|
645
|
+
urlEncoded: "application/x-www-form-urlencoded",
|
646
|
+
multipart: "multipart/form-data",
|
647
|
+
plain: "text/plain"
|
648
|
+
};
|
649
|
+
|
494
650
|
class FetchRequest {
|
495
|
-
|
496
|
-
|
497
|
-
|
651
|
+
abortController=new AbortController;
|
652
|
+
#resolveRequestPromise=_value => {};
|
653
|
+
constructor(delegate, method, location, requestBody = new URLSearchParams, target = null, enctype = FetchEnctype.urlEncoded) {
|
654
|
+
const [url, body] = buildResourceAndBody(expandURL(location), method, requestBody, enctype);
|
498
655
|
this.delegate = delegate;
|
499
|
-
this.
|
500
|
-
this.headers = this.defaultHeaders;
|
501
|
-
this.body = body;
|
502
|
-
this.url = location;
|
656
|
+
this.url = url;
|
503
657
|
this.target = target;
|
658
|
+
this.fetchOptions = {
|
659
|
+
credentials: "same-origin",
|
660
|
+
redirect: "follow",
|
661
|
+
method: method.toUpperCase(),
|
662
|
+
headers: {
|
663
|
+
...this.defaultHeaders
|
664
|
+
},
|
665
|
+
body: body,
|
666
|
+
signal: this.abortSignal,
|
667
|
+
referrer: this.delegate.referrer?.href
|
668
|
+
};
|
669
|
+
this.enctype = enctype;
|
670
|
+
}
|
671
|
+
get method() {
|
672
|
+
return this.fetchOptions.method;
|
673
|
+
}
|
674
|
+
set method(value) {
|
675
|
+
const fetchBody = this.isSafe ? this.url.searchParams : this.fetchOptions.body || new FormData;
|
676
|
+
const fetchMethod = fetchMethodFromString(value) || FetchMethod.get;
|
677
|
+
this.url.search = "";
|
678
|
+
const [url, body] = buildResourceAndBody(this.url, fetchMethod, fetchBody, this.enctype);
|
679
|
+
this.url = url;
|
680
|
+
this.fetchOptions.body = body;
|
681
|
+
this.fetchOptions.method = fetchMethod.toUpperCase();
|
682
|
+
}
|
683
|
+
get headers() {
|
684
|
+
return this.fetchOptions.headers;
|
685
|
+
}
|
686
|
+
set headers(value) {
|
687
|
+
this.fetchOptions.headers = value;
|
688
|
+
}
|
689
|
+
get body() {
|
690
|
+
if (this.isSafe) {
|
691
|
+
return this.url.searchParams;
|
692
|
+
} else {
|
693
|
+
return this.fetchOptions.body;
|
694
|
+
}
|
695
|
+
}
|
696
|
+
set body(value) {
|
697
|
+
this.fetchOptions.body = value;
|
504
698
|
}
|
505
699
|
get location() {
|
506
700
|
return this.url;
|
@@ -517,14 +711,19 @@ class FetchRequest {
|
|
517
711
|
async perform() {
|
518
712
|
const {fetchOptions: fetchOptions} = this;
|
519
713
|
this.delegate.prepareRequest(this);
|
520
|
-
await this
|
714
|
+
const event = await this.#allowRequestToBeIntercepted(fetchOptions);
|
521
715
|
try {
|
522
716
|
this.delegate.requestStarted(this);
|
523
|
-
|
717
|
+
if (event.detail.fetchRequest) {
|
718
|
+
this.response = event.detail.fetchRequest.response;
|
719
|
+
} else {
|
720
|
+
this.response = fetchWithTurboHeaders(this.url.href, fetchOptions);
|
721
|
+
}
|
722
|
+
const response = await this.response;
|
524
723
|
return await this.receive(response);
|
525
724
|
} catch (error) {
|
526
725
|
if (error.name !== "AbortError") {
|
527
|
-
if (this
|
726
|
+
if (this.#willDelegateErrorHandling(error)) {
|
528
727
|
this.delegate.requestErrored(this, error);
|
529
728
|
}
|
530
729
|
throw error;
|
@@ -551,25 +750,13 @@ class FetchRequest {
|
|
551
750
|
}
|
552
751
|
return fetchResponse;
|
553
752
|
}
|
554
|
-
get fetchOptions() {
|
555
|
-
var _a;
|
556
|
-
return {
|
557
|
-
method: FetchMethod[this.method].toUpperCase(),
|
558
|
-
credentials: "same-origin",
|
559
|
-
headers: this.headers,
|
560
|
-
redirect: "follow",
|
561
|
-
body: this.isSafe ? null : this.body,
|
562
|
-
signal: this.abortSignal,
|
563
|
-
referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
|
564
|
-
};
|
565
|
-
}
|
566
753
|
get defaultHeaders() {
|
567
754
|
return {
|
568
755
|
Accept: "text/html, application/xhtml+xml"
|
569
756
|
};
|
570
757
|
}
|
571
758
|
get isSafe() {
|
572
|
-
return this.method
|
759
|
+
return isSafe(this.method);
|
573
760
|
}
|
574
761
|
get abortSignal() {
|
575
762
|
return this.abortController.signal;
|
@@ -577,20 +764,22 @@ class FetchRequest {
|
|
577
764
|
acceptResponseType(mimeType) {
|
578
765
|
this.headers["Accept"] = [ mimeType, this.headers["Accept"] ].join(", ");
|
579
766
|
}
|
580
|
-
async allowRequestToBeIntercepted(fetchOptions) {
|
581
|
-
const requestInterception = new Promise((resolve => this
|
767
|
+
async #allowRequestToBeIntercepted(fetchOptions) {
|
768
|
+
const requestInterception = new Promise((resolve => this.#resolveRequestPromise = resolve));
|
582
769
|
const event = dispatch("turbo:before-fetch-request", {
|
583
770
|
cancelable: true,
|
584
771
|
detail: {
|
585
772
|
fetchOptions: fetchOptions,
|
586
773
|
url: this.url,
|
587
|
-
resume: this
|
774
|
+
resume: this.#resolveRequestPromise
|
588
775
|
},
|
589
776
|
target: this.target
|
590
777
|
});
|
778
|
+
this.url = event.detail.url;
|
591
779
|
if (event.defaultPrevented) await requestInterception;
|
780
|
+
return event;
|
592
781
|
}
|
593
|
-
willDelegateErrorHandling(error) {
|
782
|
+
#willDelegateErrorHandling(error) {
|
594
783
|
const event = dispatch("turbo:fetch-request-error", {
|
595
784
|
target: this.target,
|
596
785
|
cancelable: true,
|
@@ -603,15 +792,38 @@ class FetchRequest {
|
|
603
792
|
}
|
604
793
|
}
|
605
794
|
|
795
|
+
function isSafe(fetchMethod) {
|
796
|
+
return fetchMethodFromString(fetchMethod) == FetchMethod.get;
|
797
|
+
}
|
798
|
+
|
799
|
+
function buildResourceAndBody(resource, method, requestBody, enctype) {
|
800
|
+
const searchParams = Array.from(requestBody).length > 0 ? new URLSearchParams(entriesExcludingFiles(requestBody)) : resource.searchParams;
|
801
|
+
if (isSafe(method)) {
|
802
|
+
return [ mergeIntoURLSearchParams(resource, searchParams), null ];
|
803
|
+
} else if (enctype == FetchEnctype.urlEncoded) {
|
804
|
+
return [ resource, searchParams ];
|
805
|
+
} else {
|
806
|
+
return [ resource, requestBody ];
|
807
|
+
}
|
808
|
+
}
|
809
|
+
|
810
|
+
function entriesExcludingFiles(requestBody) {
|
811
|
+
const entries = [];
|
812
|
+
for (const [name, value] of requestBody) {
|
813
|
+
if (value instanceof File) continue; else entries.push([ name, value ]);
|
814
|
+
}
|
815
|
+
return entries;
|
816
|
+
}
|
817
|
+
|
818
|
+
function mergeIntoURLSearchParams(url, requestBody) {
|
819
|
+
const searchParams = new URLSearchParams(entriesExcludingFiles(requestBody));
|
820
|
+
url.search = searchParams.toString();
|
821
|
+
return url;
|
822
|
+
}
|
823
|
+
|
606
824
|
class AppearanceObserver {
|
825
|
+
started=false;
|
607
826
|
constructor(delegate, element) {
|
608
|
-
this.started = false;
|
609
|
-
this.intersect = entries => {
|
610
|
-
const lastEntry = entries.slice(-1)[0];
|
611
|
-
if (lastEntry === null || lastEntry === void 0 ? void 0 : lastEntry.isIntersecting) {
|
612
|
-
this.delegate.elementAppearedInViewport(this.element);
|
613
|
-
}
|
614
|
-
};
|
615
827
|
this.delegate = delegate;
|
616
828
|
this.element = element;
|
617
829
|
this.intersectionObserver = new IntersectionObserver(this.intersect);
|
@@ -628,9 +840,16 @@ class AppearanceObserver {
|
|
628
840
|
this.intersectionObserver.unobserve(this.element);
|
629
841
|
}
|
630
842
|
}
|
843
|
+
intersect=entries => {
|
844
|
+
const lastEntry = entries.slice(-1)[0];
|
845
|
+
if (lastEntry?.isIntersecting) {
|
846
|
+
this.delegate.elementAppearedInViewport(this.element);
|
847
|
+
}
|
848
|
+
};
|
631
849
|
}
|
632
850
|
|
633
851
|
class StreamMessage {
|
852
|
+
static contentType="text/vnd.turbo-stream.html";
|
634
853
|
static wrap(message) {
|
635
854
|
if (typeof message == "string") {
|
636
855
|
return new this(createDocumentFragment(message));
|
@@ -643,8 +862,6 @@ class StreamMessage {
|
|
643
862
|
}
|
644
863
|
}
|
645
864
|
|
646
|
-
StreamMessage.contentType = "text/vnd.turbo-stream.html";
|
647
|
-
|
648
865
|
function importStreamElements(fragment) {
|
649
866
|
for (const element of fragment.querySelectorAll("turbo-stream")) {
|
650
867
|
const streamElement = document.importNode(element, true);
|
@@ -656,91 +873,96 @@ function importStreamElements(fragment) {
|
|
656
873
|
return fragment;
|
657
874
|
}
|
658
875
|
|
659
|
-
|
660
|
-
|
661
|
-
(function(FormSubmissionState) {
|
662
|
-
FormSubmissionState[FormSubmissionState["initialized"] = 0] = "initialized";
|
663
|
-
FormSubmissionState[FormSubmissionState["requesting"] = 1] = "requesting";
|
664
|
-
FormSubmissionState[FormSubmissionState["waiting"] = 2] = "waiting";
|
665
|
-
FormSubmissionState[FormSubmissionState["receiving"] = 3] = "receiving";
|
666
|
-
FormSubmissionState[FormSubmissionState["stopping"] = 4] = "stopping";
|
667
|
-
FormSubmissionState[FormSubmissionState["stopped"] = 5] = "stopped";
|
668
|
-
})(FormSubmissionState || (FormSubmissionState = {}));
|
669
|
-
|
670
|
-
var FormEnctype;
|
876
|
+
const PREFETCH_DELAY = 100;
|
671
877
|
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
878
|
+
class PrefetchCache {
|
879
|
+
#prefetchTimeout=null;
|
880
|
+
#prefetched=null;
|
881
|
+
get(url) {
|
882
|
+
if (this.#prefetched && this.#prefetched.url === url && this.#prefetched.expire > Date.now()) {
|
883
|
+
return this.#prefetched.request;
|
884
|
+
}
|
885
|
+
}
|
886
|
+
setLater(url, request, ttl) {
|
887
|
+
this.clear();
|
888
|
+
this.#prefetchTimeout = setTimeout((() => {
|
889
|
+
request.perform();
|
890
|
+
this.set(url, request, ttl);
|
891
|
+
this.#prefetchTimeout = null;
|
892
|
+
}), PREFETCH_DELAY);
|
893
|
+
}
|
894
|
+
set(url, request, ttl) {
|
895
|
+
this.#prefetched = {
|
896
|
+
url: url,
|
897
|
+
request: request,
|
898
|
+
expire: new Date((new Date).getTime() + ttl)
|
899
|
+
};
|
900
|
+
}
|
901
|
+
clear() {
|
902
|
+
if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout);
|
903
|
+
this.#prefetched = null;
|
688
904
|
}
|
689
905
|
}
|
690
906
|
|
907
|
+
const cacheTtl = 10 * 1e3;
|
908
|
+
|
909
|
+
const prefetchCache = new PrefetchCache;
|
910
|
+
|
911
|
+
const FormSubmissionState = {
|
912
|
+
initialized: "initialized",
|
913
|
+
requesting: "requesting",
|
914
|
+
waiting: "waiting",
|
915
|
+
receiving: "receiving",
|
916
|
+
stopping: "stopping",
|
917
|
+
stopped: "stopped"
|
918
|
+
};
|
919
|
+
|
691
920
|
class FormSubmission {
|
692
|
-
|
921
|
+
state=FormSubmissionState.initialized;
|
922
|
+
static confirmMethod(message) {
|
693
923
|
return Promise.resolve(confirm(message));
|
694
924
|
}
|
695
925
|
constructor(delegate, formElement, submitter, mustRedirect = false) {
|
696
|
-
|
926
|
+
const method = getMethod(formElement, submitter);
|
927
|
+
const action = getAction(getFormAction(formElement, submitter), method);
|
928
|
+
const body = buildFormData(formElement, submitter);
|
929
|
+
const enctype = getEnctype(formElement, submitter);
|
697
930
|
this.delegate = delegate;
|
698
931
|
this.formElement = formElement;
|
699
932
|
this.submitter = submitter;
|
700
|
-
this.
|
701
|
-
this.location = expandURL(this.action);
|
702
|
-
if (this.method == FetchMethod.get) {
|
703
|
-
mergeFormDataEntries(this.location, [ ...this.body.entries() ]);
|
704
|
-
}
|
705
|
-
this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
|
933
|
+
this.fetchRequest = new FetchRequest(this, method, action, body, formElement, enctype);
|
706
934
|
this.mustRedirect = mustRedirect;
|
707
935
|
}
|
708
936
|
get method() {
|
709
|
-
|
710
|
-
|
711
|
-
|
937
|
+
return this.fetchRequest.method;
|
938
|
+
}
|
939
|
+
set method(value) {
|
940
|
+
this.fetchRequest.method = value;
|
712
941
|
}
|
713
942
|
get action() {
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
} else {
|
719
|
-
return this.formElement.getAttribute("action") || formElementAction || "";
|
720
|
-
}
|
943
|
+
return this.fetchRequest.url.toString();
|
944
|
+
}
|
945
|
+
set action(value) {
|
946
|
+
this.fetchRequest.url = expandURL(value);
|
721
947
|
}
|
722
948
|
get body() {
|
723
|
-
|
724
|
-
return new URLSearchParams(this.stringFormData);
|
725
|
-
} else {
|
726
|
-
return this.formData;
|
727
|
-
}
|
949
|
+
return this.fetchRequest.body;
|
728
950
|
}
|
729
951
|
get enctype() {
|
730
|
-
|
731
|
-
return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formenctype")) || this.formElement.enctype);
|
952
|
+
return this.fetchRequest.enctype;
|
732
953
|
}
|
733
954
|
get isSafe() {
|
734
955
|
return this.fetchRequest.isSafe;
|
735
956
|
}
|
736
|
-
get
|
737
|
-
return
|
957
|
+
get location() {
|
958
|
+
return this.fetchRequest.url;
|
738
959
|
}
|
739
960
|
async start() {
|
740
961
|
const {initialized: initialized, requesting: requesting} = FormSubmissionState;
|
741
962
|
const confirmationMessage = getAttribute("data-turbo-confirm", this.submitter, this.formElement);
|
742
963
|
if (typeof confirmationMessage === "string") {
|
743
|
-
const
|
964
|
+
const confirmMethod = typeof config.forms.confirm === "function" ? config.forms.confirm : FormSubmission.confirmMethod;
|
965
|
+
const answer = await confirmMethod(confirmationMessage, this.formElement, this.submitter);
|
744
966
|
if (!answer) {
|
745
967
|
return;
|
746
968
|
}
|
@@ -770,10 +992,10 @@ class FormSubmission {
|
|
770
992
|
}
|
771
993
|
}
|
772
994
|
requestStarted(_request) {
|
773
|
-
var _a;
|
774
995
|
this.state = FormSubmissionState.waiting;
|
775
|
-
(
|
996
|
+
if (this.submitter) config.forms.submitter.beforeSubmit(this.submitter);
|
776
997
|
this.setSubmitsWith();
|
998
|
+
markAsBusy(this.formElement);
|
777
999
|
dispatch("turbo:submit-start", {
|
778
1000
|
target: this.formElement,
|
779
1001
|
detail: {
|
@@ -783,6 +1005,7 @@ class FormSubmission {
|
|
783
1005
|
this.delegate.formSubmissionStarted(this);
|
784
1006
|
}
|
785
1007
|
requestPreventedHandlingResponse(request, response) {
|
1008
|
+
prefetchCache.clear();
|
786
1009
|
this.result = {
|
787
1010
|
success: response.succeeded,
|
788
1011
|
fetchResponse: response
|
@@ -791,7 +1014,10 @@ class FormSubmission {
|
|
791
1014
|
requestSucceededWithResponse(request, response) {
|
792
1015
|
if (response.clientError || response.serverError) {
|
793
1016
|
this.delegate.formSubmissionFailedWithResponse(this, response);
|
794
|
-
|
1017
|
+
return;
|
1018
|
+
}
|
1019
|
+
prefetchCache.clear();
|
1020
|
+
if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
|
795
1021
|
const error = new Error("Form responses must redirect to another location");
|
796
1022
|
this.delegate.formSubmissionErrored(this, error);
|
797
1023
|
} else {
|
@@ -818,15 +1044,16 @@ class FormSubmission {
|
|
818
1044
|
this.delegate.formSubmissionErrored(this, error);
|
819
1045
|
}
|
820
1046
|
requestFinished(_request) {
|
821
|
-
var _a;
|
822
1047
|
this.state = FormSubmissionState.stopped;
|
823
|
-
(
|
1048
|
+
if (this.submitter) config.forms.submitter.afterSubmit(this.submitter);
|
824
1049
|
this.resetSubmitterText();
|
1050
|
+
clearBusyState(this.formElement);
|
825
1051
|
dispatch("turbo:submit-end", {
|
826
1052
|
target: this.formElement,
|
827
|
-
detail:
|
828
|
-
formSubmission: this
|
829
|
-
|
1053
|
+
detail: {
|
1054
|
+
formSubmission: this,
|
1055
|
+
...this.result
|
1056
|
+
}
|
830
1057
|
});
|
831
1058
|
this.delegate.formSubmissionFinished(this);
|
832
1059
|
}
|
@@ -857,15 +1084,14 @@ class FormSubmission {
|
|
857
1084
|
return !request.isSafe || hasAttribute("data-turbo-stream", this.submitter, this.formElement);
|
858
1085
|
}
|
859
1086
|
get submitsWith() {
|
860
|
-
|
861
|
-
return (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("data-turbo-submits-with");
|
1087
|
+
return this.submitter?.getAttribute("data-turbo-submits-with");
|
862
1088
|
}
|
863
1089
|
}
|
864
1090
|
|
865
1091
|
function buildFormData(formElement, submitter) {
|
866
1092
|
const formData = new FormData(formElement);
|
867
|
-
const name = submitter
|
868
|
-
const value = submitter
|
1093
|
+
const name = submitter?.getAttribute("name");
|
1094
|
+
const value = submitter?.getAttribute("value");
|
869
1095
|
if (name) {
|
870
1096
|
formData.append(name, value || "");
|
871
1097
|
}
|
@@ -887,14 +1113,30 @@ function responseSucceededWithoutRedirect(response) {
|
|
887
1113
|
return response.statusCode == 200 && !response.redirected;
|
888
1114
|
}
|
889
1115
|
|
890
|
-
function
|
891
|
-
const
|
892
|
-
|
893
|
-
|
894
|
-
|
1116
|
+
function getFormAction(formElement, submitter) {
|
1117
|
+
const formElementAction = typeof formElement.action === "string" ? formElement.action : null;
|
1118
|
+
if (submitter?.hasAttribute("formaction")) {
|
1119
|
+
return submitter.getAttribute("formaction") || "";
|
1120
|
+
} else {
|
1121
|
+
return formElement.getAttribute("action") || formElementAction || "";
|
895
1122
|
}
|
896
|
-
|
897
|
-
|
1123
|
+
}
|
1124
|
+
|
1125
|
+
function getAction(formAction, fetchMethod) {
|
1126
|
+
const action = expandURL(formAction);
|
1127
|
+
if (isSafe(fetchMethod)) {
|
1128
|
+
action.search = "";
|
1129
|
+
}
|
1130
|
+
return action;
|
1131
|
+
}
|
1132
|
+
|
1133
|
+
function getMethod(formElement, submitter) {
|
1134
|
+
const method = submitter?.getAttribute("formmethod") || formElement.getAttribute("method") || "";
|
1135
|
+
return fetchMethodFromString(method.toLowerCase()) || FetchMethod.get;
|
1136
|
+
}
|
1137
|
+
|
1138
|
+
function getEnctype(formElement, submitter) {
|
1139
|
+
return fetchEnctypeFromString(submitter?.getAttribute("formenctype") || formElement.enctype);
|
898
1140
|
}
|
899
1141
|
|
900
1142
|
class Snapshot {
|
@@ -917,11 +1159,7 @@ class Snapshot {
|
|
917
1159
|
return this.element.isConnected;
|
918
1160
|
}
|
919
1161
|
get firstAutofocusableElement() {
|
920
|
-
|
921
|
-
for (const element of this.element.querySelectorAll("[autofocus]")) {
|
922
|
-
if (element.closest(inertDisabledOrHidden) == null) return element; else continue;
|
923
|
-
}
|
924
|
-
return null;
|
1162
|
+
return queryAutofocusableElement(this.element);
|
925
1163
|
}
|
926
1164
|
get permanentElements() {
|
927
1165
|
return queryPermanentElementsAll(this.element);
|
@@ -951,23 +1189,8 @@ function queryPermanentElementsAll(node) {
|
|
951
1189
|
}
|
952
1190
|
|
953
1191
|
class FormSubmitObserver {
|
1192
|
+
started=false;
|
954
1193
|
constructor(delegate, eventTarget) {
|
955
|
-
this.started = false;
|
956
|
-
this.submitCaptured = () => {
|
957
|
-
this.eventTarget.removeEventListener("submit", this.submitBubbled, false);
|
958
|
-
this.eventTarget.addEventListener("submit", this.submitBubbled, false);
|
959
|
-
};
|
960
|
-
this.submitBubbled = event => {
|
961
|
-
if (!event.defaultPrevented) {
|
962
|
-
const form = event.target instanceof HTMLFormElement ? event.target : undefined;
|
963
|
-
const submitter = event.submitter || undefined;
|
964
|
-
if (form && submissionDoesNotDismissDialog(form, submitter) && submissionDoesNotTargetIFrame(form, submitter) && this.delegate.willSubmitForm(form, submitter)) {
|
965
|
-
event.preventDefault();
|
966
|
-
event.stopImmediatePropagation();
|
967
|
-
this.delegate.formSubmitted(form, submitter);
|
968
|
-
}
|
969
|
-
}
|
970
|
-
};
|
971
1194
|
this.delegate = delegate;
|
972
1195
|
this.eventTarget = eventTarget;
|
973
1196
|
}
|
@@ -983,29 +1206,37 @@ class FormSubmitObserver {
|
|
983
1206
|
this.started = false;
|
984
1207
|
}
|
985
1208
|
}
|
1209
|
+
submitCaptured=() => {
|
1210
|
+
this.eventTarget.removeEventListener("submit", this.submitBubbled, false);
|
1211
|
+
this.eventTarget.addEventListener("submit", this.submitBubbled, false);
|
1212
|
+
};
|
1213
|
+
submitBubbled=event => {
|
1214
|
+
if (!event.defaultPrevented) {
|
1215
|
+
const form = event.target instanceof HTMLFormElement ? event.target : undefined;
|
1216
|
+
const submitter = event.submitter || undefined;
|
1217
|
+
if (form && submissionDoesNotDismissDialog(form, submitter) && submissionDoesNotTargetIFrame(form, submitter) && this.delegate.willSubmitForm(form, submitter)) {
|
1218
|
+
event.preventDefault();
|
1219
|
+
event.stopImmediatePropagation();
|
1220
|
+
this.delegate.formSubmitted(form, submitter);
|
1221
|
+
}
|
1222
|
+
}
|
1223
|
+
};
|
986
1224
|
}
|
987
1225
|
|
988
1226
|
function submissionDoesNotDismissDialog(form, submitter) {
|
989
|
-
const method =
|
1227
|
+
const method = submitter?.getAttribute("formmethod") || form.getAttribute("method");
|
990
1228
|
return method != "dialog";
|
991
1229
|
}
|
992
1230
|
|
993
1231
|
function submissionDoesNotTargetIFrame(form, submitter) {
|
994
|
-
|
995
|
-
|
996
|
-
for (const element of document.getElementsByName(target)) {
|
997
|
-
if (element instanceof HTMLIFrameElement) return false;
|
998
|
-
}
|
999
|
-
return true;
|
1000
|
-
} else {
|
1001
|
-
return true;
|
1002
|
-
}
|
1232
|
+
const target = submitter?.getAttribute("formtarget") || form.getAttribute("target");
|
1233
|
+
return doesNotTargetIFrame(target);
|
1003
1234
|
}
|
1004
1235
|
|
1005
1236
|
class View {
|
1237
|
+
#resolveRenderPromise=_value => {};
|
1238
|
+
#resolveInterceptionPromise=_value => {};
|
1006
1239
|
constructor(delegate, element) {
|
1007
|
-
this.resolveRenderPromise = _value => {};
|
1008
|
-
this.resolveInterceptionPromise = _value => {};
|
1009
1240
|
this.delegate = delegate;
|
1010
1241
|
this.element = element;
|
1011
1242
|
}
|
@@ -1051,29 +1282,31 @@ class View {
|
|
1051
1282
|
return window;
|
1052
1283
|
}
|
1053
1284
|
async render(renderer) {
|
1054
|
-
const {isPreview: isPreview, shouldRender: shouldRender, newSnapshot: snapshot} = renderer;
|
1285
|
+
const {isPreview: isPreview, shouldRender: shouldRender, willRender: willRender, newSnapshot: snapshot} = renderer;
|
1286
|
+
const shouldInvalidate = willRender;
|
1055
1287
|
if (shouldRender) {
|
1056
1288
|
try {
|
1057
|
-
this.renderPromise = new Promise((resolve => this
|
1289
|
+
this.renderPromise = new Promise((resolve => this.#resolveRenderPromise = resolve));
|
1058
1290
|
this.renderer = renderer;
|
1059
1291
|
await this.prepareToRenderSnapshot(renderer);
|
1060
|
-
const renderInterception = new Promise((resolve => this
|
1292
|
+
const renderInterception = new Promise((resolve => this.#resolveInterceptionPromise = resolve));
|
1061
1293
|
const options = {
|
1062
|
-
resume: this
|
1063
|
-
render: this.renderer.renderElement
|
1294
|
+
resume: this.#resolveInterceptionPromise,
|
1295
|
+
render: this.renderer.renderElement,
|
1296
|
+
renderMethod: this.renderer.renderMethod
|
1064
1297
|
};
|
1065
1298
|
const immediateRender = this.delegate.allowsImmediateRender(snapshot, options);
|
1066
1299
|
if (!immediateRender) await renderInterception;
|
1067
1300
|
await this.renderSnapshot(renderer);
|
1068
|
-
this.delegate.viewRenderedSnapshot(snapshot, isPreview);
|
1301
|
+
this.delegate.viewRenderedSnapshot(snapshot, isPreview, this.renderer.renderMethod);
|
1069
1302
|
this.delegate.preloadOnLoadLinksForView(this.element);
|
1070
1303
|
this.finishRenderingSnapshot(renderer);
|
1071
1304
|
} finally {
|
1072
1305
|
delete this.renderer;
|
1073
|
-
this
|
1306
|
+
this.#resolveRenderPromise(undefined);
|
1074
1307
|
delete this.renderPromise;
|
1075
1308
|
}
|
1076
|
-
} else {
|
1309
|
+
} else if (shouldInvalidate) {
|
1077
1310
|
this.invalidate(renderer.reloadReason);
|
1078
1311
|
}
|
1079
1312
|
}
|
@@ -1091,6 +1324,12 @@ class View {
|
|
1091
1324
|
this.element.removeAttribute("data-turbo-preview");
|
1092
1325
|
}
|
1093
1326
|
}
|
1327
|
+
markVisitDirection(direction) {
|
1328
|
+
this.element.setAttribute("data-turbo-visit-direction", direction);
|
1329
|
+
}
|
1330
|
+
unmarkVisitDirection() {
|
1331
|
+
this.element.removeAttribute("data-turbo-visit-direction");
|
1332
|
+
}
|
1094
1333
|
async renderSnapshot(renderer) {
|
1095
1334
|
await renderer.render();
|
1096
1335
|
}
|
@@ -1110,26 +1349,6 @@ class FrameView extends View {
|
|
1110
1349
|
|
1111
1350
|
class LinkInterceptor {
|
1112
1351
|
constructor(delegate, element) {
|
1113
|
-
this.clickBubbled = event => {
|
1114
|
-
if (this.respondsToEventTarget(event.target)) {
|
1115
|
-
this.clickEvent = event;
|
1116
|
-
} else {
|
1117
|
-
delete this.clickEvent;
|
1118
|
-
}
|
1119
|
-
};
|
1120
|
-
this.linkClicked = event => {
|
1121
|
-
if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
|
1122
|
-
if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
|
1123
|
-
this.clickEvent.preventDefault();
|
1124
|
-
event.preventDefault();
|
1125
|
-
this.delegate.linkClickIntercepted(event.target, event.detail.url, event.detail.originalEvent);
|
1126
|
-
}
|
1127
|
-
}
|
1128
|
-
delete this.clickEvent;
|
1129
|
-
};
|
1130
|
-
this.willVisit = _event => {
|
1131
|
-
delete this.clickEvent;
|
1132
|
-
};
|
1133
1352
|
this.delegate = delegate;
|
1134
1353
|
this.element = element;
|
1135
1354
|
}
|
@@ -1143,32 +1362,36 @@ class LinkInterceptor {
|
|
1143
1362
|
document.removeEventListener("turbo:click", this.linkClicked);
|
1144
1363
|
document.removeEventListener("turbo:before-visit", this.willVisit);
|
1145
1364
|
}
|
1146
|
-
|
1147
|
-
|
1148
|
-
|
1365
|
+
clickBubbled=event => {
|
1366
|
+
if (this.clickEventIsSignificant(event)) {
|
1367
|
+
this.clickEvent = event;
|
1368
|
+
} else {
|
1369
|
+
delete this.clickEvent;
|
1370
|
+
}
|
1371
|
+
};
|
1372
|
+
linkClicked=event => {
|
1373
|
+
if (this.clickEvent && this.clickEventIsSignificant(event)) {
|
1374
|
+
if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
|
1375
|
+
this.clickEvent.preventDefault();
|
1376
|
+
event.preventDefault();
|
1377
|
+
this.delegate.linkClickIntercepted(event.target, event.detail.url, event.detail.originalEvent);
|
1378
|
+
}
|
1379
|
+
}
|
1380
|
+
delete this.clickEvent;
|
1381
|
+
};
|
1382
|
+
willVisit=_event => {
|
1383
|
+
delete this.clickEvent;
|
1384
|
+
};
|
1385
|
+
clickEventIsSignificant(event) {
|
1386
|
+
const target = event.composed ? event.target?.parentElement : event.target;
|
1387
|
+
const element = findLinkFromClickTarget(target) || target;
|
1388
|
+
return element instanceof Element && element.closest("turbo-frame, html") == this.element;
|
1149
1389
|
}
|
1150
1390
|
}
|
1151
1391
|
|
1152
1392
|
class LinkClickObserver {
|
1393
|
+
started=false;
|
1153
1394
|
constructor(delegate, eventTarget) {
|
1154
|
-
this.started = false;
|
1155
|
-
this.clickCaptured = () => {
|
1156
|
-
this.eventTarget.removeEventListener("click", this.clickBubbled, false);
|
1157
|
-
this.eventTarget.addEventListener("click", this.clickBubbled, false);
|
1158
|
-
};
|
1159
|
-
this.clickBubbled = event => {
|
1160
|
-
if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
|
1161
|
-
const target = event.composedPath && event.composedPath()[0] || event.target;
|
1162
|
-
const link = this.findLinkFromClickTarget(target);
|
1163
|
-
if (link && doesNotTargetIFrame(link)) {
|
1164
|
-
const location = this.getLocationForLink(link);
|
1165
|
-
if (this.delegate.willFollowLinkToLocation(link, location, event)) {
|
1166
|
-
event.preventDefault();
|
1167
|
-
this.delegate.followedLinkToLocation(link, location);
|
1168
|
-
}
|
1169
|
-
}
|
1170
|
-
}
|
1171
|
-
};
|
1172
1395
|
this.delegate = delegate;
|
1173
1396
|
this.eventTarget = eventTarget;
|
1174
1397
|
}
|
@@ -1184,26 +1407,26 @@ class LinkClickObserver {
|
|
1184
1407
|
this.started = false;
|
1185
1408
|
}
|
1186
1409
|
}
|
1410
|
+
clickCaptured=() => {
|
1411
|
+
this.eventTarget.removeEventListener("click", this.clickBubbled, false);
|
1412
|
+
this.eventTarget.addEventListener("click", this.clickBubbled, false);
|
1413
|
+
};
|
1414
|
+
clickBubbled=event => {
|
1415
|
+
if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
|
1416
|
+
const target = event.composedPath && event.composedPath()[0] || event.target;
|
1417
|
+
const link = findLinkFromClickTarget(target);
|
1418
|
+
if (link && doesNotTargetIFrame(link.target)) {
|
1419
|
+
const location = getLocationForLink(link);
|
1420
|
+
if (this.delegate.willFollowLinkToLocation(link, location, event)) {
|
1421
|
+
event.preventDefault();
|
1422
|
+
this.delegate.followedLinkToLocation(link, location);
|
1423
|
+
}
|
1424
|
+
}
|
1425
|
+
}
|
1426
|
+
};
|
1187
1427
|
clickEventIsSignificant(event) {
|
1188
1428
|
return !(event.target && event.target.isContentEditable || event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);
|
1189
1429
|
}
|
1190
|
-
findLinkFromClickTarget(target) {
|
1191
|
-
return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])");
|
1192
|
-
}
|
1193
|
-
getLocationForLink(link) {
|
1194
|
-
return expandURL(link.getAttribute("href") || "");
|
1195
|
-
}
|
1196
|
-
}
|
1197
|
-
|
1198
|
-
function doesNotTargetIFrame(anchor) {
|
1199
|
-
if (anchor.hasAttribute("target")) {
|
1200
|
-
for (const element of document.getElementsByName(anchor.target)) {
|
1201
|
-
if (element instanceof HTMLIFrameElement) return false;
|
1202
|
-
}
|
1203
|
-
return true;
|
1204
|
-
} else {
|
1205
|
-
return true;
|
1206
|
-
}
|
1207
1430
|
}
|
1208
1431
|
|
1209
1432
|
class FormLinkClickObserver {
|
@@ -1217,8 +1440,14 @@ class FormLinkClickObserver {
|
|
1217
1440
|
stop() {
|
1218
1441
|
this.linkInterceptor.stop();
|
1219
1442
|
}
|
1443
|
+
canPrefetchRequestToLocation(link, location) {
|
1444
|
+
return false;
|
1445
|
+
}
|
1446
|
+
prefetchAndCacheRequestToLocation(link, location) {
|
1447
|
+
return;
|
1448
|
+
}
|
1220
1449
|
willFollowLinkToLocation(link, location, originalEvent) {
|
1221
|
-
return this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) && link.hasAttribute("data-turbo-method");
|
1450
|
+
return this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) && (link.hasAttribute("data-turbo-method") || link.hasAttribute("data-turbo-stream"));
|
1222
1451
|
}
|
1223
1452
|
followedLinkToLocation(link, location) {
|
1224
1453
|
const form = document.createElement("form");
|
@@ -1291,7 +1520,7 @@ class Bardo {
|
|
1291
1520
|
}
|
1292
1521
|
replacePlaceholderWithPermanentElement(permanentElement) {
|
1293
1522
|
const placeholder = this.getPlaceholderById(permanentElement.id);
|
1294
|
-
placeholder
|
1523
|
+
placeholder?.replaceWith(permanentElement);
|
1295
1524
|
}
|
1296
1525
|
getPlaceholderById(id) {
|
1297
1526
|
return this.placeholders.find((element => element.content == id));
|
@@ -1309,13 +1538,14 @@ function createPlaceholderForPermanentElement(permanentElement) {
|
|
1309
1538
|
}
|
1310
1539
|
|
1311
1540
|
class Renderer {
|
1312
|
-
|
1313
|
-
|
1541
|
+
#activeElement=null;
|
1542
|
+
static renderElement(currentElement, newElement) {}
|
1543
|
+
constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) {
|
1314
1544
|
this.currentSnapshot = currentSnapshot;
|
1315
1545
|
this.newSnapshot = newSnapshot;
|
1316
1546
|
this.isPreview = isPreview;
|
1317
1547
|
this.willRender = willRender;
|
1318
|
-
this.renderElement = renderElement;
|
1548
|
+
this.renderElement = this.constructor.renderElement;
|
1319
1549
|
this.promise = new Promise(((resolve, reject) => this.resolvingFunctions = {
|
1320
1550
|
resolve: resolve,
|
1321
1551
|
reject: reject
|
@@ -1324,135 +1554,757 @@ class Renderer {
|
|
1324
1554
|
get shouldRender() {
|
1325
1555
|
return true;
|
1326
1556
|
}
|
1557
|
+
get shouldAutofocus() {
|
1558
|
+
return true;
|
1559
|
+
}
|
1327
1560
|
get reloadReason() {
|
1328
1561
|
return;
|
1329
1562
|
}
|
1330
1563
|
prepareToRender() {
|
1331
1564
|
return;
|
1332
1565
|
}
|
1566
|
+
render() {}
|
1333
1567
|
finishRendering() {
|
1334
1568
|
if (this.resolvingFunctions) {
|
1335
1569
|
this.resolvingFunctions.resolve();
|
1336
1570
|
delete this.resolvingFunctions;
|
1337
1571
|
}
|
1338
1572
|
}
|
1339
|
-
async preservingPermanentElements(callback) {
|
1340
|
-
await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
|
1341
|
-
}
|
1342
|
-
focusFirstAutofocusableElement() {
|
1343
|
-
|
1344
|
-
|
1345
|
-
element
|
1573
|
+
async preservingPermanentElements(callback) {
|
1574
|
+
await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
|
1575
|
+
}
|
1576
|
+
focusFirstAutofocusableElement() {
|
1577
|
+
if (this.shouldAutofocus) {
|
1578
|
+
const element = this.connectedSnapshot.firstAutofocusableElement;
|
1579
|
+
if (element) {
|
1580
|
+
element.focus();
|
1581
|
+
}
|
1582
|
+
}
|
1583
|
+
}
|
1584
|
+
enteringBardo(currentPermanentElement) {
|
1585
|
+
if (this.#activeElement) return;
|
1586
|
+
if (currentPermanentElement.contains(this.currentSnapshot.activeElement)) {
|
1587
|
+
this.#activeElement = this.currentSnapshot.activeElement;
|
1588
|
+
}
|
1589
|
+
}
|
1590
|
+
leavingBardo(currentPermanentElement) {
|
1591
|
+
if (currentPermanentElement.contains(this.#activeElement) && this.#activeElement instanceof HTMLElement) {
|
1592
|
+
this.#activeElement.focus();
|
1593
|
+
this.#activeElement = null;
|
1594
|
+
}
|
1595
|
+
}
|
1596
|
+
get connectedSnapshot() {
|
1597
|
+
return this.newSnapshot.isConnected ? this.newSnapshot : this.currentSnapshot;
|
1598
|
+
}
|
1599
|
+
get currentElement() {
|
1600
|
+
return this.currentSnapshot.element;
|
1601
|
+
}
|
1602
|
+
get newElement() {
|
1603
|
+
return this.newSnapshot.element;
|
1604
|
+
}
|
1605
|
+
get permanentElementMap() {
|
1606
|
+
return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot);
|
1607
|
+
}
|
1608
|
+
get renderMethod() {
|
1609
|
+
return "replace";
|
1610
|
+
}
|
1611
|
+
}
|
1612
|
+
|
1613
|
+
class FrameRenderer extends Renderer {
|
1614
|
+
static renderElement(currentElement, newElement) {
|
1615
|
+
const destinationRange = document.createRange();
|
1616
|
+
destinationRange.selectNodeContents(currentElement);
|
1617
|
+
destinationRange.deleteContents();
|
1618
|
+
const frameElement = newElement;
|
1619
|
+
const sourceRange = frameElement.ownerDocument?.createRange();
|
1620
|
+
if (sourceRange) {
|
1621
|
+
sourceRange.selectNodeContents(frameElement);
|
1622
|
+
currentElement.appendChild(sourceRange.extractContents());
|
1623
|
+
}
|
1624
|
+
}
|
1625
|
+
constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
|
1626
|
+
super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender);
|
1627
|
+
this.delegate = delegate;
|
1628
|
+
}
|
1629
|
+
get shouldRender() {
|
1630
|
+
return true;
|
1631
|
+
}
|
1632
|
+
async render() {
|
1633
|
+
await nextRepaint();
|
1634
|
+
this.preservingPermanentElements((() => {
|
1635
|
+
this.loadFrameElement();
|
1636
|
+
}));
|
1637
|
+
this.scrollFrameIntoView();
|
1638
|
+
await nextRepaint();
|
1639
|
+
this.focusFirstAutofocusableElement();
|
1640
|
+
await nextRepaint();
|
1641
|
+
this.activateScriptElements();
|
1642
|
+
}
|
1643
|
+
loadFrameElement() {
|
1644
|
+
this.delegate.willRenderFrame(this.currentElement, this.newElement);
|
1645
|
+
this.renderElement(this.currentElement, this.newElement);
|
1646
|
+
}
|
1647
|
+
scrollFrameIntoView() {
|
1648
|
+
if (this.currentElement.autoscroll || this.newElement.autoscroll) {
|
1649
|
+
const element = this.currentElement.firstElementChild;
|
1650
|
+
const block = readScrollLogicalPosition(this.currentElement.getAttribute("data-autoscroll-block"), "end");
|
1651
|
+
const behavior = readScrollBehavior(this.currentElement.getAttribute("data-autoscroll-behavior"), "auto");
|
1652
|
+
if (element) {
|
1653
|
+
element.scrollIntoView({
|
1654
|
+
block: block,
|
1655
|
+
behavior: behavior
|
1656
|
+
});
|
1657
|
+
return true;
|
1658
|
+
}
|
1659
|
+
}
|
1660
|
+
return false;
|
1661
|
+
}
|
1662
|
+
activateScriptElements() {
|
1663
|
+
for (const inertScriptElement of this.newScriptElements) {
|
1664
|
+
const activatedScriptElement = activateScriptElement(inertScriptElement);
|
1665
|
+
inertScriptElement.replaceWith(activatedScriptElement);
|
1666
|
+
}
|
1667
|
+
}
|
1668
|
+
get newScriptElements() {
|
1669
|
+
return this.currentElement.querySelectorAll("script");
|
1670
|
+
}
|
1671
|
+
}
|
1672
|
+
|
1673
|
+
function readScrollLogicalPosition(value, defaultValue) {
|
1674
|
+
if (value == "end" || value == "start" || value == "center" || value == "nearest") {
|
1675
|
+
return value;
|
1676
|
+
} else {
|
1677
|
+
return defaultValue;
|
1678
|
+
}
|
1679
|
+
}
|
1680
|
+
|
1681
|
+
function readScrollBehavior(value, defaultValue) {
|
1682
|
+
if (value == "auto" || value == "smooth") {
|
1683
|
+
return value;
|
1684
|
+
} else {
|
1685
|
+
return defaultValue;
|
1686
|
+
}
|
1687
|
+
}
|
1688
|
+
|
1689
|
+
var Idiomorph = function() {
|
1690
|
+
let EMPTY_SET = new Set;
|
1691
|
+
let 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: function(elt) {
|
1705
|
+
return elt.getAttribute("im-preserve") === "true";
|
1706
|
+
},
|
1707
|
+
shouldReAppend: function(elt) {
|
1708
|
+
return elt.getAttribute("im-re-append") === "true";
|
1709
|
+
},
|
1710
|
+
shouldRemove: noOp,
|
1711
|
+
afterHeadMorphed: noOp
|
1712
|
+
}
|
1713
|
+
};
|
1714
|
+
function morph(oldNode, newContent, config = {}) {
|
1715
|
+
if (oldNode instanceof Document) {
|
1716
|
+
oldNode = oldNode.documentElement;
|
1717
|
+
}
|
1718
|
+
if (typeof newContent === "string") {
|
1719
|
+
newContent = parseContent(newContent);
|
1720
|
+
}
|
1721
|
+
let normalizedContent = normalizeContent(newContent);
|
1722
|
+
let ctx = createMorphContext(oldNode, normalizedContent, config);
|
1723
|
+
return morphNormalizedContent(oldNode, normalizedContent, ctx);
|
1724
|
+
}
|
1725
|
+
function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
|
1726
|
+
if (ctx.head.block) {
|
1727
|
+
let oldHead = oldNode.querySelector("head");
|
1728
|
+
let newHead = normalizedNewContent.querySelector("head");
|
1729
|
+
if (oldHead && newHead) {
|
1730
|
+
let promises = handleHeadElement(newHead, oldHead, ctx);
|
1731
|
+
Promise.all(promises).then((function() {
|
1732
|
+
morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
|
1733
|
+
head: {
|
1734
|
+
block: false,
|
1735
|
+
ignore: true
|
1736
|
+
}
|
1737
|
+
}));
|
1738
|
+
}));
|
1739
|
+
return;
|
1740
|
+
}
|
1741
|
+
}
|
1742
|
+
if (ctx.morphStyle === "innerHTML") {
|
1743
|
+
morphChildren(normalizedNewContent, oldNode, ctx);
|
1744
|
+
return oldNode.children;
|
1745
|
+
} else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
|
1746
|
+
let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
|
1747
|
+
let previousSibling = bestMatch?.previousSibling;
|
1748
|
+
let nextSibling = bestMatch?.nextSibling;
|
1749
|
+
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
|
1750
|
+
if (bestMatch) {
|
1751
|
+
return insertSiblings(previousSibling, morphedNode, nextSibling);
|
1752
|
+
} else {
|
1753
|
+
return [];
|
1754
|
+
}
|
1755
|
+
} else {
|
1756
|
+
throw "Do not understand how to morph style " + ctx.morphStyle;
|
1757
|
+
}
|
1758
|
+
}
|
1759
|
+
function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
|
1760
|
+
return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement && possibleActiveElement !== document.body;
|
1761
|
+
}
|
1762
|
+
function morphOldNodeTo(oldNode, newContent, ctx) {
|
1763
|
+
if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
|
1764
|
+
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
1765
|
+
oldNode.remove();
|
1766
|
+
ctx.callbacks.afterNodeRemoved(oldNode);
|
1767
|
+
return null;
|
1768
|
+
} else if (!isSoftMatch(oldNode, newContent)) {
|
1769
|
+
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
1770
|
+
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
|
1771
|
+
oldNode.parentElement.replaceChild(newContent, oldNode);
|
1772
|
+
ctx.callbacks.afterNodeAdded(newContent);
|
1773
|
+
ctx.callbacks.afterNodeRemoved(oldNode);
|
1774
|
+
return newContent;
|
1775
|
+
} else {
|
1776
|
+
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
|
1777
|
+
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
|
1778
|
+
handleHeadElement(newContent, oldNode, ctx);
|
1779
|
+
} else {
|
1780
|
+
syncNodeFrom(newContent, oldNode, ctx);
|
1781
|
+
if (!ignoreValueOfActiveElement(oldNode, ctx)) {
|
1782
|
+
morphChildren(newContent, oldNode, ctx);
|
1783
|
+
}
|
1784
|
+
}
|
1785
|
+
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
|
1786
|
+
return oldNode;
|
1787
|
+
}
|
1788
|
+
}
|
1789
|
+
function morphChildren(newParent, oldParent, ctx) {
|
1790
|
+
let nextNewChild = newParent.firstChild;
|
1791
|
+
let insertionPoint = oldParent.firstChild;
|
1792
|
+
let newChild;
|
1793
|
+
while (nextNewChild) {
|
1794
|
+
newChild = nextNewChild;
|
1795
|
+
nextNewChild = newChild.nextSibling;
|
1796
|
+
if (insertionPoint == null) {
|
1797
|
+
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
1798
|
+
oldParent.appendChild(newChild);
|
1799
|
+
ctx.callbacks.afterNodeAdded(newChild);
|
1800
|
+
removeIdsFromConsideration(ctx, newChild);
|
1801
|
+
continue;
|
1802
|
+
}
|
1803
|
+
if (isIdSetMatch(newChild, insertionPoint, ctx)) {
|
1804
|
+
morphOldNodeTo(insertionPoint, newChild, ctx);
|
1805
|
+
insertionPoint = insertionPoint.nextSibling;
|
1806
|
+
removeIdsFromConsideration(ctx, newChild);
|
1807
|
+
continue;
|
1808
|
+
}
|
1809
|
+
let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
1810
|
+
if (idSetMatch) {
|
1811
|
+
insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
|
1812
|
+
morphOldNodeTo(idSetMatch, newChild, ctx);
|
1813
|
+
removeIdsFromConsideration(ctx, newChild);
|
1814
|
+
continue;
|
1815
|
+
}
|
1816
|
+
let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
1817
|
+
if (softMatch) {
|
1818
|
+
insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
|
1819
|
+
morphOldNodeTo(softMatch, newChild, ctx);
|
1820
|
+
removeIdsFromConsideration(ctx, newChild);
|
1821
|
+
continue;
|
1822
|
+
}
|
1823
|
+
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
1824
|
+
oldParent.insertBefore(newChild, insertionPoint);
|
1825
|
+
ctx.callbacks.afterNodeAdded(newChild);
|
1826
|
+
removeIdsFromConsideration(ctx, newChild);
|
1827
|
+
}
|
1828
|
+
while (insertionPoint !== null) {
|
1829
|
+
let tempNode = insertionPoint;
|
1830
|
+
insertionPoint = insertionPoint.nextSibling;
|
1831
|
+
removeNode(tempNode, ctx);
|
1832
|
+
}
|
1833
|
+
}
|
1834
|
+
function ignoreAttribute(attr, to, updateType, ctx) {
|
1835
|
+
if (attr === "value" && ctx.ignoreActiveValue && to === document.activeElement) {
|
1836
|
+
return true;
|
1837
|
+
}
|
1838
|
+
return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
|
1839
|
+
}
|
1840
|
+
function syncNodeFrom(from, to, ctx) {
|
1841
|
+
let type = from.nodeType;
|
1842
|
+
if (type === 1) {
|
1843
|
+
const fromAttributes = from.attributes;
|
1844
|
+
const toAttributes = to.attributes;
|
1845
|
+
for (const fromAttribute of fromAttributes) {
|
1846
|
+
if (ignoreAttribute(fromAttribute.name, to, "update", ctx)) {
|
1847
|
+
continue;
|
1848
|
+
}
|
1849
|
+
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
|
1850
|
+
to.setAttribute(fromAttribute.name, fromAttribute.value);
|
1851
|
+
}
|
1852
|
+
}
|
1853
|
+
for (let i = toAttributes.length - 1; 0 <= i; i--) {
|
1854
|
+
const toAttribute = toAttributes[i];
|
1855
|
+
if (ignoreAttribute(toAttribute.name, to, "remove", ctx)) {
|
1856
|
+
continue;
|
1857
|
+
}
|
1858
|
+
if (!from.hasAttribute(toAttribute.name)) {
|
1859
|
+
to.removeAttribute(toAttribute.name);
|
1860
|
+
}
|
1861
|
+
}
|
1862
|
+
}
|
1863
|
+
if (type === 8 || type === 3) {
|
1864
|
+
if (to.nodeValue !== from.nodeValue) {
|
1865
|
+
to.nodeValue = from.nodeValue;
|
1866
|
+
}
|
1867
|
+
}
|
1868
|
+
if (!ignoreValueOfActiveElement(to, ctx)) {
|
1869
|
+
syncInputValue(from, to, ctx);
|
1870
|
+
}
|
1871
|
+
}
|
1872
|
+
function syncBooleanAttribute(from, to, attributeName, ctx) {
|
1873
|
+
if (from[attributeName] !== to[attributeName]) {
|
1874
|
+
let ignoreUpdate = ignoreAttribute(attributeName, to, "update", ctx);
|
1875
|
+
if (!ignoreUpdate) {
|
1876
|
+
to[attributeName] = from[attributeName];
|
1877
|
+
}
|
1878
|
+
if (from[attributeName]) {
|
1879
|
+
if (!ignoreUpdate) {
|
1880
|
+
to.setAttribute(attributeName, from[attributeName]);
|
1881
|
+
}
|
1882
|
+
} else {
|
1883
|
+
if (!ignoreAttribute(attributeName, to, "remove", ctx)) {
|
1884
|
+
to.removeAttribute(attributeName);
|
1885
|
+
}
|
1886
|
+
}
|
1887
|
+
}
|
1888
|
+
}
|
1889
|
+
function syncInputValue(from, to, ctx) {
|
1890
|
+
if (from instanceof HTMLInputElement && to instanceof HTMLInputElement && from.type !== "file") {
|
1891
|
+
let fromValue = from.value;
|
1892
|
+
let toValue = to.value;
|
1893
|
+
syncBooleanAttribute(from, to, "checked", ctx);
|
1894
|
+
syncBooleanAttribute(from, to, "disabled", ctx);
|
1895
|
+
if (!from.hasAttribute("value")) {
|
1896
|
+
if (!ignoreAttribute("value", to, "remove", ctx)) {
|
1897
|
+
to.value = "";
|
1898
|
+
to.removeAttribute("value");
|
1899
|
+
}
|
1900
|
+
} else if (fromValue !== toValue) {
|
1901
|
+
if (!ignoreAttribute("value", to, "update", ctx)) {
|
1902
|
+
to.setAttribute("value", fromValue);
|
1903
|
+
to.value = fromValue;
|
1904
|
+
}
|
1905
|
+
}
|
1906
|
+
} else if (from instanceof HTMLOptionElement) {
|
1907
|
+
syncBooleanAttribute(from, to, "selected", ctx);
|
1908
|
+
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
|
1909
|
+
let fromValue = from.value;
|
1910
|
+
let toValue = to.value;
|
1911
|
+
if (ignoreAttribute("value", to, "update", ctx)) {
|
1912
|
+
return;
|
1913
|
+
}
|
1914
|
+
if (fromValue !== toValue) {
|
1915
|
+
to.value = fromValue;
|
1916
|
+
}
|
1917
|
+
if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
|
1918
|
+
to.firstChild.nodeValue = fromValue;
|
1919
|
+
}
|
1920
|
+
}
|
1921
|
+
}
|
1922
|
+
function handleHeadElement(newHeadTag, currentHead, ctx) {
|
1923
|
+
let added = [];
|
1924
|
+
let removed = [];
|
1925
|
+
let preserved = [];
|
1926
|
+
let nodesToAppend = [];
|
1927
|
+
let headMergeStyle = ctx.head.style;
|
1928
|
+
let srcToNewHeadNodes = new Map;
|
1929
|
+
for (const newHeadChild of newHeadTag.children) {
|
1930
|
+
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
|
1931
|
+
}
|
1932
|
+
for (const currentHeadElt of currentHead.children) {
|
1933
|
+
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
|
1934
|
+
let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
|
1935
|
+
let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
|
1936
|
+
if (inNewContent || isPreserved) {
|
1937
|
+
if (isReAppended) {
|
1938
|
+
removed.push(currentHeadElt);
|
1939
|
+
} else {
|
1940
|
+
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
|
1941
|
+
preserved.push(currentHeadElt);
|
1942
|
+
}
|
1943
|
+
} else {
|
1944
|
+
if (headMergeStyle === "append") {
|
1945
|
+
if (isReAppended) {
|
1946
|
+
removed.push(currentHeadElt);
|
1947
|
+
nodesToAppend.push(currentHeadElt);
|
1948
|
+
}
|
1949
|
+
} else {
|
1950
|
+
if (ctx.head.shouldRemove(currentHeadElt) !== false) {
|
1951
|
+
removed.push(currentHeadElt);
|
1952
|
+
}
|
1953
|
+
}
|
1954
|
+
}
|
1346
1955
|
}
|
1347
|
-
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
1351
|
-
|
1956
|
+
nodesToAppend.push(...srcToNewHeadNodes.values());
|
1957
|
+
let promises = [];
|
1958
|
+
for (const newNode of nodesToAppend) {
|
1959
|
+
let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
|
1960
|
+
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
|
1961
|
+
if (newElt.href || newElt.src) {
|
1962
|
+
let resolve = null;
|
1963
|
+
let promise = new Promise((function(_resolve) {
|
1964
|
+
resolve = _resolve;
|
1965
|
+
}));
|
1966
|
+
newElt.addEventListener("load", (function() {
|
1967
|
+
resolve();
|
1968
|
+
}));
|
1969
|
+
promises.push(promise);
|
1970
|
+
}
|
1971
|
+
currentHead.appendChild(newElt);
|
1972
|
+
ctx.callbacks.afterNodeAdded(newElt);
|
1973
|
+
added.push(newElt);
|
1974
|
+
}
|
1352
1975
|
}
|
1353
|
-
|
1354
|
-
|
1355
|
-
|
1356
|
-
|
1357
|
-
|
1976
|
+
for (const removedElement of removed) {
|
1977
|
+
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
|
1978
|
+
currentHead.removeChild(removedElement);
|
1979
|
+
ctx.callbacks.afterNodeRemoved(removedElement);
|
1980
|
+
}
|
1358
1981
|
}
|
1982
|
+
ctx.head.afterHeadMorphed(currentHead, {
|
1983
|
+
added: added,
|
1984
|
+
kept: preserved,
|
1985
|
+
removed: removed
|
1986
|
+
});
|
1987
|
+
return promises;
|
1988
|
+
}
|
1989
|
+
function noOp() {}
|
1990
|
+
function mergeDefaults(config) {
|
1991
|
+
let finalConfig = {};
|
1992
|
+
Object.assign(finalConfig, defaults);
|
1993
|
+
Object.assign(finalConfig, config);
|
1994
|
+
finalConfig.callbacks = {};
|
1995
|
+
Object.assign(finalConfig.callbacks, defaults.callbacks);
|
1996
|
+
Object.assign(finalConfig.callbacks, config.callbacks);
|
1997
|
+
finalConfig.head = {};
|
1998
|
+
Object.assign(finalConfig.head, defaults.head);
|
1999
|
+
Object.assign(finalConfig.head, config.head);
|
2000
|
+
return finalConfig;
|
2001
|
+
}
|
2002
|
+
function createMorphContext(oldNode, newContent, config) {
|
2003
|
+
config = mergeDefaults(config);
|
2004
|
+
return {
|
2005
|
+
target: oldNode,
|
2006
|
+
newContent: newContent,
|
2007
|
+
config: config,
|
2008
|
+
morphStyle: config.morphStyle,
|
2009
|
+
ignoreActive: config.ignoreActive,
|
2010
|
+
ignoreActiveValue: config.ignoreActiveValue,
|
2011
|
+
idMap: createIdMap(oldNode, newContent),
|
2012
|
+
deadIds: new Set,
|
2013
|
+
callbacks: config.callbacks,
|
2014
|
+
head: config.head
|
2015
|
+
};
|
1359
2016
|
}
|
1360
|
-
|
1361
|
-
|
1362
|
-
|
1363
|
-
|
1364
|
-
|
2017
|
+
function isIdSetMatch(node1, node2, ctx) {
|
2018
|
+
if (node1 == null || node2 == null) {
|
2019
|
+
return false;
|
2020
|
+
}
|
2021
|
+
if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
|
2022
|
+
if (node1.id !== "" && node1.id === node2.id) {
|
2023
|
+
return true;
|
2024
|
+
} else {
|
2025
|
+
return getIdIntersectionCount(ctx, node1, node2) > 0;
|
2026
|
+
}
|
2027
|
+
}
|
2028
|
+
return false;
|
1365
2029
|
}
|
1366
|
-
|
1367
|
-
|
2030
|
+
function isSoftMatch(node1, node2) {
|
2031
|
+
if (node1 == null || node2 == null) {
|
2032
|
+
return false;
|
2033
|
+
}
|
2034
|
+
return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName;
|
2035
|
+
}
|
2036
|
+
function removeNodesBetween(startInclusive, endExclusive, ctx) {
|
2037
|
+
while (startInclusive !== endExclusive) {
|
2038
|
+
let tempNode = startInclusive;
|
2039
|
+
startInclusive = startInclusive.nextSibling;
|
2040
|
+
removeNode(tempNode, ctx);
|
2041
|
+
}
|
2042
|
+
removeIdsFromConsideration(ctx, endExclusive);
|
2043
|
+
return endExclusive.nextSibling;
|
2044
|
+
}
|
2045
|
+
function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
2046
|
+
let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
|
2047
|
+
let potentialMatch = null;
|
2048
|
+
if (newChildPotentialIdCount > 0) {
|
2049
|
+
let potentialMatch = insertionPoint;
|
2050
|
+
let otherMatchCount = 0;
|
2051
|
+
while (potentialMatch != null) {
|
2052
|
+
if (isIdSetMatch(newChild, potentialMatch, ctx)) {
|
2053
|
+
return potentialMatch;
|
2054
|
+
}
|
2055
|
+
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
|
2056
|
+
if (otherMatchCount > newChildPotentialIdCount) {
|
2057
|
+
return null;
|
2058
|
+
}
|
2059
|
+
potentialMatch = potentialMatch.nextSibling;
|
2060
|
+
}
|
2061
|
+
}
|
2062
|
+
return potentialMatch;
|
1368
2063
|
}
|
1369
|
-
|
1370
|
-
|
2064
|
+
function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
2065
|
+
let potentialSoftMatch = insertionPoint;
|
2066
|
+
let nextSibling = newChild.nextSibling;
|
2067
|
+
let siblingSoftMatchCount = 0;
|
2068
|
+
while (potentialSoftMatch != null) {
|
2069
|
+
if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
|
2070
|
+
return null;
|
2071
|
+
}
|
2072
|
+
if (isSoftMatch(newChild, potentialSoftMatch)) {
|
2073
|
+
return potentialSoftMatch;
|
2074
|
+
}
|
2075
|
+
if (isSoftMatch(nextSibling, potentialSoftMatch)) {
|
2076
|
+
siblingSoftMatchCount++;
|
2077
|
+
nextSibling = nextSibling.nextSibling;
|
2078
|
+
if (siblingSoftMatchCount >= 2) {
|
2079
|
+
return null;
|
2080
|
+
}
|
2081
|
+
}
|
2082
|
+
potentialSoftMatch = potentialSoftMatch.nextSibling;
|
2083
|
+
}
|
2084
|
+
return potentialSoftMatch;
|
2085
|
+
}
|
2086
|
+
function parseContent(newContent) {
|
2087
|
+
let parser = new DOMParser;
|
2088
|
+
let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, "");
|
2089
|
+
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
|
2090
|
+
let content = parser.parseFromString(newContent, "text/html");
|
2091
|
+
if (contentWithSvgsRemoved.match(/<\/html>/)) {
|
2092
|
+
content.generatedByIdiomorph = true;
|
2093
|
+
return content;
|
2094
|
+
} else {
|
2095
|
+
let htmlElement = content.firstChild;
|
2096
|
+
if (htmlElement) {
|
2097
|
+
htmlElement.generatedByIdiomorph = true;
|
2098
|
+
return htmlElement;
|
2099
|
+
} else {
|
2100
|
+
return null;
|
2101
|
+
}
|
2102
|
+
}
|
2103
|
+
} else {
|
2104
|
+
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
|
2105
|
+
let content = responseDoc.body.querySelector("template").content;
|
2106
|
+
content.generatedByIdiomorph = true;
|
2107
|
+
return content;
|
2108
|
+
}
|
2109
|
+
}
|
2110
|
+
function normalizeContent(newContent) {
|
2111
|
+
if (newContent == null) {
|
2112
|
+
const dummyParent = document.createElement("div");
|
2113
|
+
return dummyParent;
|
2114
|
+
} else if (newContent.generatedByIdiomorph) {
|
2115
|
+
return newContent;
|
2116
|
+
} else if (newContent instanceof Node) {
|
2117
|
+
const dummyParent = document.createElement("div");
|
2118
|
+
dummyParent.append(newContent);
|
2119
|
+
return dummyParent;
|
2120
|
+
} else {
|
2121
|
+
const dummyParent = document.createElement("div");
|
2122
|
+
for (const elt of [ ...newContent ]) {
|
2123
|
+
dummyParent.append(elt);
|
2124
|
+
}
|
2125
|
+
return dummyParent;
|
2126
|
+
}
|
2127
|
+
}
|
2128
|
+
function insertSiblings(previousSibling, morphedNode, nextSibling) {
|
2129
|
+
let stack = [];
|
2130
|
+
let added = [];
|
2131
|
+
while (previousSibling != null) {
|
2132
|
+
stack.push(previousSibling);
|
2133
|
+
previousSibling = previousSibling.previousSibling;
|
2134
|
+
}
|
2135
|
+
while (stack.length > 0) {
|
2136
|
+
let node = stack.pop();
|
2137
|
+
added.push(node);
|
2138
|
+
morphedNode.parentElement.insertBefore(node, morphedNode);
|
2139
|
+
}
|
2140
|
+
added.push(morphedNode);
|
2141
|
+
while (nextSibling != null) {
|
2142
|
+
stack.push(nextSibling);
|
2143
|
+
added.push(nextSibling);
|
2144
|
+
nextSibling = nextSibling.nextSibling;
|
2145
|
+
}
|
2146
|
+
while (stack.length > 0) {
|
2147
|
+
morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
|
2148
|
+
}
|
2149
|
+
return added;
|
2150
|
+
}
|
2151
|
+
function findBestNodeMatch(newContent, oldNode, ctx) {
|
2152
|
+
let currentElement;
|
2153
|
+
currentElement = newContent.firstChild;
|
2154
|
+
let bestElement = currentElement;
|
2155
|
+
let score = 0;
|
2156
|
+
while (currentElement) {
|
2157
|
+
let newScore = scoreElement(currentElement, oldNode, ctx);
|
2158
|
+
if (newScore > score) {
|
2159
|
+
bestElement = currentElement;
|
2160
|
+
score = newScore;
|
2161
|
+
}
|
2162
|
+
currentElement = currentElement.nextSibling;
|
2163
|
+
}
|
2164
|
+
return bestElement;
|
1371
2165
|
}
|
1372
|
-
|
1373
|
-
|
1374
|
-
|
1375
|
-
return element && typeof element.focus == "function";
|
1376
|
-
}
|
1377
|
-
|
1378
|
-
class FrameRenderer extends Renderer {
|
1379
|
-
static renderElement(currentElement, newElement) {
|
1380
|
-
var _a;
|
1381
|
-
const destinationRange = document.createRange();
|
1382
|
-
destinationRange.selectNodeContents(currentElement);
|
1383
|
-
destinationRange.deleteContents();
|
1384
|
-
const frameElement = newElement;
|
1385
|
-
const sourceRange = (_a = frameElement.ownerDocument) === null || _a === void 0 ? void 0 : _a.createRange();
|
1386
|
-
if (sourceRange) {
|
1387
|
-
sourceRange.selectNodeContents(frameElement);
|
1388
|
-
currentElement.appendChild(sourceRange.extractContents());
|
2166
|
+
function scoreElement(node1, node2, ctx) {
|
2167
|
+
if (isSoftMatch(node1, node2)) {
|
2168
|
+
return .5 + getIdIntersectionCount(ctx, node1, node2);
|
1389
2169
|
}
|
2170
|
+
return 0;
|
1390
2171
|
}
|
1391
|
-
|
1392
|
-
|
1393
|
-
|
2172
|
+
function removeNode(tempNode, ctx) {
|
2173
|
+
removeIdsFromConsideration(ctx, tempNode);
|
2174
|
+
if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
|
2175
|
+
tempNode.remove();
|
2176
|
+
ctx.callbacks.afterNodeRemoved(tempNode);
|
1394
2177
|
}
|
1395
|
-
|
1396
|
-
return
|
2178
|
+
function isIdInConsideration(ctx, id) {
|
2179
|
+
return !ctx.deadIds.has(id);
|
1397
2180
|
}
|
1398
|
-
|
1399
|
-
|
1400
|
-
|
1401
|
-
this.loadFrameElement();
|
1402
|
-
}));
|
1403
|
-
this.scrollFrameIntoView();
|
1404
|
-
await nextAnimationFrame();
|
1405
|
-
this.focusFirstAutofocusableElement();
|
1406
|
-
await nextAnimationFrame();
|
1407
|
-
this.activateScriptElements();
|
2181
|
+
function idIsWithinNode(ctx, id, targetNode) {
|
2182
|
+
let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
|
2183
|
+
return idSet.has(id);
|
1408
2184
|
}
|
1409
|
-
|
1410
|
-
|
1411
|
-
|
2185
|
+
function removeIdsFromConsideration(ctx, node) {
|
2186
|
+
let idSet = ctx.idMap.get(node) || EMPTY_SET;
|
2187
|
+
for (const id of idSet) {
|
2188
|
+
ctx.deadIds.add(id);
|
2189
|
+
}
|
1412
2190
|
}
|
1413
|
-
|
1414
|
-
|
1415
|
-
|
1416
|
-
|
1417
|
-
|
1418
|
-
|
1419
|
-
element.scrollIntoView({
|
1420
|
-
block: block,
|
1421
|
-
behavior: behavior
|
1422
|
-
});
|
1423
|
-
return true;
|
2191
|
+
function getIdIntersectionCount(ctx, node1, node2) {
|
2192
|
+
let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
|
2193
|
+
let matchCount = 0;
|
2194
|
+
for (const id of sourceSet) {
|
2195
|
+
if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
|
2196
|
+
++matchCount;
|
1424
2197
|
}
|
1425
2198
|
}
|
1426
|
-
return
|
1427
|
-
}
|
1428
|
-
|
1429
|
-
|
1430
|
-
|
1431
|
-
|
2199
|
+
return matchCount;
|
2200
|
+
}
|
2201
|
+
function populateIdMapForNode(node, idMap) {
|
2202
|
+
let nodeParent = node.parentElement;
|
2203
|
+
let idElements = node.querySelectorAll("[id]");
|
2204
|
+
for (const elt of idElements) {
|
2205
|
+
let current = elt;
|
2206
|
+
while (current !== nodeParent && current != null) {
|
2207
|
+
let idSet = idMap.get(current);
|
2208
|
+
if (idSet == null) {
|
2209
|
+
idSet = new Set;
|
2210
|
+
idMap.set(current, idSet);
|
2211
|
+
}
|
2212
|
+
idSet.add(elt.id);
|
2213
|
+
current = current.parentElement;
|
2214
|
+
}
|
1432
2215
|
}
|
1433
2216
|
}
|
1434
|
-
|
1435
|
-
|
2217
|
+
function createIdMap(oldContent, newContent) {
|
2218
|
+
let idMap = new Map;
|
2219
|
+
populateIdMapForNode(oldContent, idMap);
|
2220
|
+
populateIdMapForNode(newContent, idMap);
|
2221
|
+
return idMap;
|
1436
2222
|
}
|
2223
|
+
return {
|
2224
|
+
morph: morph,
|
2225
|
+
defaults: defaults
|
2226
|
+
};
|
2227
|
+
}();
|
2228
|
+
|
2229
|
+
function morphElements(currentElement, newElement, {callbacks: callbacks, ...options} = {}) {
|
2230
|
+
Idiomorph.morph(currentElement, newElement, {
|
2231
|
+
...options,
|
2232
|
+
callbacks: new DefaultIdiomorphCallbacks(callbacks)
|
2233
|
+
});
|
1437
2234
|
}
|
1438
2235
|
|
1439
|
-
function
|
1440
|
-
|
1441
|
-
|
1442
|
-
}
|
1443
|
-
return defaultValue;
|
1444
|
-
}
|
2236
|
+
function morphChildren(currentElement, newElement) {
|
2237
|
+
morphElements(currentElement, newElement.children, {
|
2238
|
+
morphStyle: "innerHTML"
|
2239
|
+
});
|
1445
2240
|
}
|
1446
2241
|
|
1447
|
-
|
1448
|
-
|
1449
|
-
|
1450
|
-
|
1451
|
-
|
2242
|
+
class DefaultIdiomorphCallbacks {
|
2243
|
+
#beforeNodeMorphed;
|
2244
|
+
constructor({beforeNodeMorphed: beforeNodeMorphed} = {}) {
|
2245
|
+
this.#beforeNodeMorphed = beforeNodeMorphed || (() => true);
|
2246
|
+
}
|
2247
|
+
beforeNodeAdded=node => !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id));
|
2248
|
+
beforeNodeMorphed=(currentElement, newElement) => {
|
2249
|
+
if (currentElement instanceof Element) {
|
2250
|
+
if (!currentElement.hasAttribute("data-turbo-permanent") && this.#beforeNodeMorphed(currentElement, newElement)) {
|
2251
|
+
const event = dispatch("turbo:before-morph-element", {
|
2252
|
+
cancelable: true,
|
2253
|
+
target: currentElement,
|
2254
|
+
detail: {
|
2255
|
+
currentElement: currentElement,
|
2256
|
+
newElement: newElement
|
2257
|
+
}
|
2258
|
+
});
|
2259
|
+
return !event.defaultPrevented;
|
2260
|
+
} else {
|
2261
|
+
return false;
|
2262
|
+
}
|
2263
|
+
}
|
2264
|
+
};
|
2265
|
+
beforeAttributeUpdated=(attributeName, target, mutationType) => {
|
2266
|
+
const event = dispatch("turbo:before-morph-attribute", {
|
2267
|
+
cancelable: true,
|
2268
|
+
target: target,
|
2269
|
+
detail: {
|
2270
|
+
attributeName: attributeName,
|
2271
|
+
mutationType: mutationType
|
2272
|
+
}
|
2273
|
+
});
|
2274
|
+
return !event.defaultPrevented;
|
2275
|
+
};
|
2276
|
+
beforeNodeRemoved=node => this.beforeNodeMorphed(node);
|
2277
|
+
afterNodeMorphed=(currentElement, newElement) => {
|
2278
|
+
if (currentElement instanceof Element) {
|
2279
|
+
dispatch("turbo:morph-element", {
|
2280
|
+
target: currentElement,
|
2281
|
+
detail: {
|
2282
|
+
currentElement: currentElement,
|
2283
|
+
newElement: newElement
|
2284
|
+
}
|
2285
|
+
});
|
2286
|
+
}
|
2287
|
+
};
|
2288
|
+
}
|
2289
|
+
|
2290
|
+
class MorphingFrameRenderer extends FrameRenderer {
|
2291
|
+
static renderElement(currentElement, newElement) {
|
2292
|
+
dispatch("turbo:before-frame-morph", {
|
2293
|
+
target: currentElement,
|
2294
|
+
detail: {
|
2295
|
+
currentElement: currentElement,
|
2296
|
+
newElement: newElement
|
2297
|
+
}
|
2298
|
+
});
|
2299
|
+
morphChildren(currentElement, newElement);
|
2300
|
+
}
|
2301
|
+
async preservingPermanentElements(callback) {
|
2302
|
+
return await callback();
|
1452
2303
|
}
|
1453
2304
|
}
|
1454
2305
|
|
1455
2306
|
class ProgressBar {
|
2307
|
+
static animationDuration=300;
|
1456
2308
|
static get defaultCSS() {
|
1457
2309
|
return unindent`
|
1458
2310
|
.turbo-progress-bar {
|
@@ -1470,13 +2322,10 @@ class ProgressBar {
|
|
1470
2322
|
}
|
1471
2323
|
`;
|
1472
2324
|
}
|
2325
|
+
hiding=false;
|
2326
|
+
value=0;
|
2327
|
+
visible=false;
|
1473
2328
|
constructor() {
|
1474
|
-
this.hiding = false;
|
1475
|
-
this.value = 0;
|
1476
|
-
this.visible = false;
|
1477
|
-
this.trickle = () => {
|
1478
|
-
this.setValue(this.value + Math.random() / 100);
|
1479
|
-
};
|
1480
2329
|
this.stylesheetElement = this.createStylesheetElement();
|
1481
2330
|
this.progressElement = this.createProgressElement();
|
1482
2331
|
this.installStylesheetElement();
|
@@ -1531,6 +2380,9 @@ class ProgressBar {
|
|
1531
2380
|
window.clearInterval(this.trickleInterval);
|
1532
2381
|
delete this.trickleInterval;
|
1533
2382
|
}
|
2383
|
+
trickle=() => {
|
2384
|
+
this.setValue(this.value + Math.random() / 100);
|
2385
|
+
};
|
1534
2386
|
refresh() {
|
1535
2387
|
requestAnimationFrame((() => {
|
1536
2388
|
this.progressElement.style.width = `${10 + this.value * 90}%`;
|
@@ -1540,8 +2392,9 @@ class ProgressBar {
|
|
1540
2392
|
const element = document.createElement("style");
|
1541
2393
|
element.type = "text/css";
|
1542
2394
|
element.textContent = ProgressBar.defaultCSS;
|
1543
|
-
|
1544
|
-
|
2395
|
+
const cspNonce = getCspNonce();
|
2396
|
+
if (cspNonce) {
|
2397
|
+
element.nonce = cspNonce;
|
1545
2398
|
}
|
1546
2399
|
return element;
|
1547
2400
|
}
|
@@ -1550,30 +2403,24 @@ class ProgressBar {
|
|
1550
2403
|
element.className = "turbo-progress-bar";
|
1551
2404
|
return element;
|
1552
2405
|
}
|
1553
|
-
get cspNonce() {
|
1554
|
-
return getMetaContent("csp-nonce");
|
1555
|
-
}
|
1556
2406
|
}
|
1557
2407
|
|
1558
|
-
ProgressBar.animationDuration = 300;
|
1559
|
-
|
1560
2408
|
class HeadSnapshot extends Snapshot {
|
1561
|
-
|
1562
|
-
|
1563
|
-
|
1564
|
-
|
1565
|
-
|
1566
|
-
|
1567
|
-
|
1568
|
-
|
1569
|
-
|
1570
|
-
|
1571
|
-
|
1572
|
-
|
1573
|
-
|
1574
|
-
|
1575
|
-
|
1576
|
-
}
|
2409
|
+
detailsByOuterHTML=this.children.filter((element => !elementIsNoscript(element))).map((element => elementWithoutNonce(element))).reduce(((result, element) => {
|
2410
|
+
const {outerHTML: outerHTML} = element;
|
2411
|
+
const details = outerHTML in result ? result[outerHTML] : {
|
2412
|
+
type: elementType(element),
|
2413
|
+
tracked: elementIsTracked(element),
|
2414
|
+
elements: []
|
2415
|
+
};
|
2416
|
+
return {
|
2417
|
+
...result,
|
2418
|
+
[outerHTML]: {
|
2419
|
+
...details,
|
2420
|
+
elements: [ ...details.elements, element ]
|
2421
|
+
}
|
2422
|
+
};
|
2423
|
+
}), {});
|
1577
2424
|
get trackedElementSignature() {
|
1578
2425
|
return Object.keys(this.detailsByOuterHTML).filter((outerHTML => this.detailsByOuterHTML[outerHTML].tracked)).join("");
|
1579
2426
|
}
|
@@ -1606,7 +2453,7 @@ class HeadSnapshot extends Snapshot {
|
|
1606
2453
|
return Object.keys(this.detailsByOuterHTML).reduce(((result, outerHTML) => {
|
1607
2454
|
const {elements: [element]} = this.detailsByOuterHTML[outerHTML];
|
1608
2455
|
return elementIsMetaElementWithName(element, name) ? element : result;
|
1609
|
-
}), undefined);
|
2456
|
+
}), undefined | undefined);
|
1610
2457
|
}
|
1611
2458
|
}
|
1612
2459
|
|
@@ -1656,11 +2503,12 @@ class PageSnapshot extends Snapshot {
|
|
1656
2503
|
static fromElement(element) {
|
1657
2504
|
return this.fromDocument(element.ownerDocument);
|
1658
2505
|
}
|
1659
|
-
static fromDocument({
|
1660
|
-
return new this(body, new HeadSnapshot(head));
|
2506
|
+
static fromDocument({documentElement: documentElement, body: body, head: head}) {
|
2507
|
+
return new this(documentElement, body, new HeadSnapshot(head));
|
1661
2508
|
}
|
1662
|
-
constructor(
|
1663
|
-
super(
|
2509
|
+
constructor(documentElement, body, headSnapshot) {
|
2510
|
+
super(body);
|
2511
|
+
this.documentElement = documentElement;
|
1664
2512
|
this.headSnapshot = headSnapshot;
|
1665
2513
|
}
|
1666
2514
|
clone() {
|
@@ -1675,14 +2523,16 @@ class PageSnapshot extends Snapshot {
|
|
1675
2523
|
for (const clonedPasswordInput of clonedElement.querySelectorAll('input[type="password"]')) {
|
1676
2524
|
clonedPasswordInput.value = "";
|
1677
2525
|
}
|
1678
|
-
return new PageSnapshot(clonedElement, this.headSnapshot);
|
2526
|
+
return new PageSnapshot(this.documentElement, clonedElement, this.headSnapshot);
|
2527
|
+
}
|
2528
|
+
get lang() {
|
2529
|
+
return this.documentElement.getAttribute("lang");
|
1679
2530
|
}
|
1680
2531
|
get headElement() {
|
1681
2532
|
return this.headSnapshot.element;
|
1682
2533
|
}
|
1683
2534
|
get rootLocation() {
|
1684
|
-
|
1685
|
-
const root = (_a = this.getSetting("root")) !== null && _a !== void 0 ? _a : "/";
|
2535
|
+
const root = this.getSetting("root") ?? "/";
|
1686
2536
|
return expandURL(root);
|
1687
2537
|
}
|
1688
2538
|
get cacheControlValue() {
|
@@ -1697,29 +2547,38 @@ class PageSnapshot extends Snapshot {
|
|
1697
2547
|
get isVisitable() {
|
1698
2548
|
return this.getSetting("visit-control") != "reload";
|
1699
2549
|
}
|
2550
|
+
get prefersViewTransitions() {
|
2551
|
+
return this.headSnapshot.getMetaValue("view-transition") === "same-origin";
|
2552
|
+
}
|
2553
|
+
get shouldMorphPage() {
|
2554
|
+
return this.getSetting("refresh-method") === "morph";
|
2555
|
+
}
|
2556
|
+
get shouldPreserveScrollPosition() {
|
2557
|
+
return this.getSetting("refresh-scroll") === "preserve";
|
2558
|
+
}
|
1700
2559
|
getSetting(name) {
|
1701
2560
|
return this.headSnapshot.getMetaValue(`turbo-${name}`);
|
1702
2561
|
}
|
1703
2562
|
}
|
1704
2563
|
|
1705
|
-
|
1706
|
-
|
1707
|
-
(
|
1708
|
-
|
1709
|
-
|
1710
|
-
|
1711
|
-
|
1712
|
-
|
1713
|
-
|
1714
|
-
|
1715
|
-
|
1716
|
-
|
1717
|
-
|
1718
|
-
|
1719
|
-
|
1720
|
-
|
1721
|
-
|
1722
|
-
}
|
2564
|
+
class ViewTransitioner {
|
2565
|
+
#viewTransitionStarted=false;
|
2566
|
+
#lastOperation=Promise.resolve();
|
2567
|
+
renderChange(useViewTransition, render) {
|
2568
|
+
if (useViewTransition && this.viewTransitionsAvailable && !this.#viewTransitionStarted) {
|
2569
|
+
this.#viewTransitionStarted = true;
|
2570
|
+
this.#lastOperation = this.#lastOperation.then((async () => {
|
2571
|
+
await document.startViewTransition(render).finished;
|
2572
|
+
}));
|
2573
|
+
} else {
|
2574
|
+
this.#lastOperation = this.#lastOperation.then(render);
|
2575
|
+
}
|
2576
|
+
return this.#lastOperation;
|
2577
|
+
}
|
2578
|
+
get viewTransitionsAvailable() {
|
2579
|
+
return document.startViewTransition;
|
2580
|
+
}
|
2581
|
+
}
|
1723
2582
|
|
1724
2583
|
const defaultOptions = {
|
1725
2584
|
action: "advance",
|
@@ -1731,29 +2590,52 @@ const defaultOptions = {
|
|
1731
2590
|
acceptsStreamResponse: false
|
1732
2591
|
};
|
1733
2592
|
|
1734
|
-
|
2593
|
+
const TimingMetric = {
|
2594
|
+
visitStart: "visitStart",
|
2595
|
+
requestStart: "requestStart",
|
2596
|
+
requestEnd: "requestEnd",
|
2597
|
+
visitEnd: "visitEnd"
|
2598
|
+
};
|
2599
|
+
|
2600
|
+
const VisitState = {
|
2601
|
+
initialized: "initialized",
|
2602
|
+
started: "started",
|
2603
|
+
canceled: "canceled",
|
2604
|
+
failed: "failed",
|
2605
|
+
completed: "completed"
|
2606
|
+
};
|
2607
|
+
|
2608
|
+
const SystemStatusCode = {
|
2609
|
+
networkFailure: 0,
|
2610
|
+
timeoutFailure: -1,
|
2611
|
+
contentTypeMismatch: -2
|
2612
|
+
};
|
1735
2613
|
|
1736
|
-
|
1737
|
-
|
1738
|
-
|
1739
|
-
|
1740
|
-
}
|
2614
|
+
const Direction = {
|
2615
|
+
advance: "forward",
|
2616
|
+
restore: "back",
|
2617
|
+
replace: "none"
|
2618
|
+
};
|
1741
2619
|
|
1742
2620
|
class Visit {
|
2621
|
+
identifier=uuid();
|
2622
|
+
timingMetrics={};
|
2623
|
+
followedRedirect=false;
|
2624
|
+
historyChanged=false;
|
2625
|
+
scrolled=false;
|
2626
|
+
shouldCacheSnapshot=true;
|
2627
|
+
acceptsStreamResponse=false;
|
2628
|
+
snapshotCached=false;
|
2629
|
+
state=VisitState.initialized;
|
2630
|
+
viewTransitioner=new ViewTransitioner;
|
1743
2631
|
constructor(delegate, location, restorationIdentifier, options = {}) {
|
1744
|
-
this.identifier = uuid();
|
1745
|
-
this.timingMetrics = {};
|
1746
|
-
this.followedRedirect = false;
|
1747
|
-
this.historyChanged = false;
|
1748
|
-
this.scrolled = false;
|
1749
|
-
this.shouldCacheSnapshot = true;
|
1750
|
-
this.acceptsStreamResponse = false;
|
1751
|
-
this.snapshotCached = false;
|
1752
|
-
this.state = VisitState.initialized;
|
1753
2632
|
this.delegate = delegate;
|
1754
2633
|
this.location = location;
|
1755
2634
|
this.restorationIdentifier = restorationIdentifier || uuid();
|
1756
|
-
const {action: action, historyChanged: historyChanged, referrer: referrer, snapshot: snapshot, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender, updateHistory: updateHistory, shouldCacheSnapshot: shouldCacheSnapshot, acceptsStreamResponse: acceptsStreamResponse} =
|
2635
|
+
const {action: action, historyChanged: historyChanged, referrer: referrer, snapshot: snapshot, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender, updateHistory: updateHistory, shouldCacheSnapshot: shouldCacheSnapshot, acceptsStreamResponse: acceptsStreamResponse, direction: direction} = {
|
2636
|
+
...defaultOptions,
|
2637
|
+
...options
|
2638
|
+
};
|
1757
2639
|
this.action = action;
|
1758
2640
|
this.historyChanged = historyChanged;
|
1759
2641
|
this.referrer = referrer;
|
@@ -1761,12 +2643,14 @@ class Visit {
|
|
1761
2643
|
this.snapshotHTML = snapshotHTML;
|
1762
2644
|
this.response = response;
|
1763
2645
|
this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
|
2646
|
+
this.isPageRefresh = this.view.isPageRefresh(this);
|
1764
2647
|
this.visitCachedSnapshot = visitCachedSnapshot;
|
1765
2648
|
this.willRender = willRender;
|
1766
2649
|
this.updateHistory = updateHistory;
|
1767
2650
|
this.scrolled = !willRender;
|
1768
2651
|
this.shouldCacheSnapshot = shouldCacheSnapshot;
|
1769
2652
|
this.acceptsStreamResponse = acceptsStreamResponse;
|
2653
|
+
this.direction = direction || Direction[action];
|
1770
2654
|
}
|
1771
2655
|
get adapter() {
|
1772
2656
|
return this.delegate.adapter;
|
@@ -1803,10 +2687,10 @@ class Visit {
|
|
1803
2687
|
complete() {
|
1804
2688
|
if (this.state == VisitState.started) {
|
1805
2689
|
this.recordTimingMetric(TimingMetric.visitEnd);
|
2690
|
+
this.adapter.visitCompleted(this);
|
1806
2691
|
this.state = VisitState.completed;
|
1807
2692
|
this.followRedirect();
|
1808
2693
|
if (!this.followedRedirect) {
|
1809
|
-
this.adapter.visitCompleted(this);
|
1810
2694
|
this.delegate.visitCompleted(this);
|
1811
2695
|
}
|
1812
2696
|
}
|
@@ -1815,12 +2699,12 @@ class Visit {
|
|
1815
2699
|
if (this.state == VisitState.started) {
|
1816
2700
|
this.state = VisitState.failed;
|
1817
2701
|
this.adapter.visitFailed(this);
|
2702
|
+
this.delegate.visitCompleted(this);
|
1818
2703
|
}
|
1819
2704
|
}
|
1820
2705
|
changeHistory() {
|
1821
|
-
var _a;
|
1822
2706
|
if (!this.historyChanged && this.updateHistory) {
|
1823
|
-
const actionForHistory = this.location.href ===
|
2707
|
+
const actionForHistory = this.location.href === this.referrer?.href ? "replace" : this.action;
|
1824
2708
|
const method = getHistoryMethodForAction(actionForHistory);
|
1825
2709
|
this.history.update(method, this.location, this.restorationIdentifier);
|
1826
2710
|
this.historyChanged = true;
|
@@ -1867,8 +2751,8 @@ class Visit {
|
|
1867
2751
|
if (this.shouldCacheSnapshot) this.cacheSnapshot();
|
1868
2752
|
if (this.view.renderPromise) await this.view.renderPromise;
|
1869
2753
|
if (isSuccessful(statusCode) && responseHTML != null) {
|
1870
|
-
|
1871
|
-
this.
|
2754
|
+
const snapshot = PageSnapshot.fromHTMLString(responseHTML);
|
2755
|
+
await this.renderPageSnapshot(snapshot, false);
|
1872
2756
|
this.adapter.visitRendered(this);
|
1873
2757
|
this.complete();
|
1874
2758
|
} else {
|
@@ -1901,12 +2785,11 @@ class Visit {
|
|
1901
2785
|
const isPreview = this.shouldIssueRequest();
|
1902
2786
|
this.render((async () => {
|
1903
2787
|
this.cacheSnapshot();
|
1904
|
-
if (this.isSamePage) {
|
2788
|
+
if (this.isSamePage || this.isPageRefresh) {
|
1905
2789
|
this.adapter.visitRendered(this);
|
1906
2790
|
} else {
|
1907
2791
|
if (this.view.renderPromise) await this.view.renderPromise;
|
1908
|
-
await this.
|
1909
|
-
this.performScroll();
|
2792
|
+
await this.renderPageSnapshot(snapshot, isPreview);
|
1910
2793
|
this.adapter.visitRendered(this);
|
1911
2794
|
if (!isPreview) {
|
1912
2795
|
this.complete();
|
@@ -1916,8 +2799,7 @@ class Visit {
|
|
1916
2799
|
}
|
1917
2800
|
}
|
1918
2801
|
followRedirect() {
|
1919
|
-
|
1920
|
-
if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
|
2802
|
+
if (this.redirectedToLocation && !this.followedRedirect && this.response?.redirected) {
|
1921
2803
|
this.adapter.visitProposedToLocation(this.redirectedToLocation, {
|
1922
2804
|
action: "replace",
|
1923
2805
|
response: this.response,
|
@@ -1989,7 +2871,7 @@ class Visit {
|
|
1989
2871
|
this.finishRequest();
|
1990
2872
|
}
|
1991
2873
|
performScroll() {
|
1992
|
-
if (!this.scrolled && !this.view.forceReloaded) {
|
2874
|
+
if (!this.scrolled && !this.view.forceReloaded && !this.view.shouldPreserveScrollPosition(this)) {
|
1993
2875
|
if (this.action == "restore") {
|
1994
2876
|
this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop();
|
1995
2877
|
} else {
|
@@ -2019,7 +2901,9 @@ class Visit {
|
|
2019
2901
|
this.timingMetrics[metric] = (new Date).getTime();
|
2020
2902
|
}
|
2021
2903
|
getTimingMetrics() {
|
2022
|
-
return
|
2904
|
+
return {
|
2905
|
+
...this.timingMetrics
|
2906
|
+
};
|
2023
2907
|
}
|
2024
2908
|
getHistoryMethodForAction(action) {
|
2025
2909
|
switch (action) {
|
@@ -2052,11 +2936,17 @@ class Visit {
|
|
2052
2936
|
async render(callback) {
|
2053
2937
|
this.cancelRender();
|
2054
2938
|
await new Promise((resolve => {
|
2055
|
-
this.frame = requestAnimationFrame((() => resolve()));
|
2939
|
+
this.frame = document.visibilityState === "hidden" ? setTimeout((() => resolve()), 0) : requestAnimationFrame((() => resolve()));
|
2056
2940
|
}));
|
2057
2941
|
await callback();
|
2058
2942
|
delete this.frame;
|
2059
2943
|
}
|
2944
|
+
async renderPageSnapshot(snapshot, isPreview) {
|
2945
|
+
await this.viewTransitioner.renderChange(this.view.shouldTransitionTo(snapshot), (async () => {
|
2946
|
+
await this.view.renderPage(snapshot, isPreview, this.willRender, this);
|
2947
|
+
this.performScroll();
|
2948
|
+
}));
|
2949
|
+
}
|
2060
2950
|
cancelRender() {
|
2061
2951
|
if (this.frame) {
|
2062
2952
|
cancelAnimationFrame(this.frame);
|
@@ -2070,15 +2960,16 @@ function isSuccessful(statusCode) {
|
|
2070
2960
|
}
|
2071
2961
|
|
2072
2962
|
class BrowserAdapter {
|
2963
|
+
progressBar=new ProgressBar;
|
2073
2964
|
constructor(session) {
|
2074
|
-
this.progressBar = new ProgressBar;
|
2075
|
-
this.showProgressBar = () => {
|
2076
|
-
this.progressBar.show();
|
2077
|
-
};
|
2078
2965
|
this.session = session;
|
2079
2966
|
}
|
2080
2967
|
visitProposedToLocation(location, options) {
|
2081
|
-
|
2968
|
+
if (locationIsVisitable(location, this.navigator.rootLocation)) {
|
2969
|
+
this.navigator.startVisit(location, options?.restorationIdentifier || uuid(), options);
|
2970
|
+
} else {
|
2971
|
+
window.location.href = location.toString();
|
2972
|
+
}
|
2082
2973
|
}
|
2083
2974
|
visitStarted(visit) {
|
2084
2975
|
this.location = visit.location;
|
@@ -2113,15 +3004,18 @@ class BrowserAdapter {
|
|
2113
3004
|
return visit.loadResponse();
|
2114
3005
|
}
|
2115
3006
|
}
|
2116
|
-
visitRequestFinished(_visit) {
|
3007
|
+
visitRequestFinished(_visit) {}
|
3008
|
+
visitCompleted(_visit) {
|
2117
3009
|
this.progressBar.setValue(1);
|
2118
3010
|
this.hideVisitProgressBar();
|
2119
3011
|
}
|
2120
|
-
visitCompleted(_visit) {}
|
2121
3012
|
pageInvalidated(reason) {
|
2122
3013
|
this.reload(reason);
|
2123
3014
|
}
|
2124
|
-
visitFailed(_visit) {
|
3015
|
+
visitFailed(_visit) {
|
3016
|
+
this.progressBar.setValue(1);
|
3017
|
+
this.hideVisitProgressBar();
|
3018
|
+
}
|
2125
3019
|
visitRendered(_visit) {}
|
2126
3020
|
formSubmissionStarted(_formSubmission) {
|
2127
3021
|
this.progressBar.setValue(0);
|
@@ -2153,12 +3047,14 @@ class BrowserAdapter {
|
|
2153
3047
|
delete this.formProgressBarTimeout;
|
2154
3048
|
}
|
2155
3049
|
}
|
3050
|
+
showProgressBar=() => {
|
3051
|
+
this.progressBar.show();
|
3052
|
+
};
|
2156
3053
|
reload(reason) {
|
2157
|
-
var _a;
|
2158
3054
|
dispatch("turbo:reload", {
|
2159
3055
|
detail: reason
|
2160
3056
|
});
|
2161
|
-
window.location.href =
|
3057
|
+
window.location.href = this.location?.toString() || window.location.href;
|
2162
3058
|
}
|
2163
3059
|
get navigator() {
|
2164
3060
|
return this.session.navigator;
|
@@ -2166,16 +3062,9 @@ class BrowserAdapter {
|
|
2166
3062
|
}
|
2167
3063
|
|
2168
3064
|
class CacheObserver {
|
2169
|
-
|
2170
|
-
|
2171
|
-
|
2172
|
-
this.started = false;
|
2173
|
-
this.removeTemporaryElements = _event => {
|
2174
|
-
for (const element of this.temporaryElements) {
|
2175
|
-
element.remove();
|
2176
|
-
}
|
2177
|
-
};
|
2178
|
-
}
|
3065
|
+
selector="[data-turbo-temporary]";
|
3066
|
+
deprecatedSelector="[data-turbo-cache=false]";
|
3067
|
+
started=false;
|
2179
3068
|
start() {
|
2180
3069
|
if (!this.started) {
|
2181
3070
|
this.started = true;
|
@@ -2188,6 +3077,11 @@ class CacheObserver {
|
|
2188
3077
|
removeEventListener("turbo:before-cache", this.removeTemporaryElements, false);
|
2189
3078
|
}
|
2190
3079
|
}
|
3080
|
+
removeTemporaryElements=_event => {
|
3081
|
+
for (const element of this.temporaryElements) {
|
3082
|
+
element.remove();
|
3083
|
+
}
|
3084
|
+
};
|
2191
3085
|
get temporaryElements() {
|
2192
3086
|
return [ ...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation ];
|
2193
3087
|
}
|
@@ -2216,41 +3110,40 @@ class FrameRedirector {
|
|
2216
3110
|
this.formSubmitObserver.stop();
|
2217
3111
|
}
|
2218
3112
|
shouldInterceptLinkClick(element, _location, _event) {
|
2219
|
-
return this
|
3113
|
+
return this.#shouldRedirect(element);
|
2220
3114
|
}
|
2221
3115
|
linkClickIntercepted(element, url, event) {
|
2222
|
-
const frame = this
|
3116
|
+
const frame = this.#findFrameElement(element);
|
2223
3117
|
if (frame) {
|
2224
3118
|
frame.delegate.linkClickIntercepted(element, url, event);
|
2225
3119
|
}
|
2226
3120
|
}
|
2227
3121
|
willSubmitForm(element, submitter) {
|
2228
|
-
return element.closest("turbo-frame") == null && this
|
3122
|
+
return element.closest("turbo-frame") == null && this.#shouldSubmit(element, submitter) && this.#shouldRedirect(element, submitter);
|
2229
3123
|
}
|
2230
3124
|
formSubmitted(element, submitter) {
|
2231
|
-
const frame = this
|
3125
|
+
const frame = this.#findFrameElement(element, submitter);
|
2232
3126
|
if (frame) {
|
2233
3127
|
frame.delegate.formSubmitted(element, submitter);
|
2234
3128
|
}
|
2235
3129
|
}
|
2236
|
-
shouldSubmit(form, submitter) {
|
2237
|
-
|
2238
|
-
const action = getAction(form, submitter);
|
3130
|
+
#shouldSubmit(form, submitter) {
|
3131
|
+
const action = getAction$1(form, submitter);
|
2239
3132
|
const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
|
2240
|
-
const rootLocation = expandURL(
|
2241
|
-
return this
|
3133
|
+
const rootLocation = expandURL(meta?.content ?? "/");
|
3134
|
+
return this.#shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation);
|
2242
3135
|
}
|
2243
|
-
shouldRedirect(element, submitter) {
|
3136
|
+
#shouldRedirect(element, submitter) {
|
2244
3137
|
const isNavigatable = element instanceof HTMLFormElement ? this.session.submissionIsNavigatable(element, submitter) : this.session.elementIsNavigatable(element);
|
2245
3138
|
if (isNavigatable) {
|
2246
|
-
const frame = this
|
3139
|
+
const frame = this.#findFrameElement(element, submitter);
|
2247
3140
|
return frame ? frame != element.closest("turbo-frame") : false;
|
2248
3141
|
} else {
|
2249
3142
|
return false;
|
2250
3143
|
}
|
2251
3144
|
}
|
2252
|
-
findFrameElement(element, submitter) {
|
2253
|
-
const id =
|
3145
|
+
#findFrameElement(element, submitter) {
|
3146
|
+
const id = submitter?.getAttribute("data-turbo-frame") || element.getAttribute("data-turbo-frame");
|
2254
3147
|
if (id && id != "_top") {
|
2255
3148
|
const frame = this.element.querySelector(`#${id}:not([disabled])`);
|
2256
3149
|
if (frame instanceof FrameElement) {
|
@@ -2261,32 +3154,20 @@ class FrameRedirector {
|
|
2261
3154
|
}
|
2262
3155
|
|
2263
3156
|
class History {
|
3157
|
+
location;
|
3158
|
+
restorationIdentifier=uuid();
|
3159
|
+
restorationData={};
|
3160
|
+
started=false;
|
3161
|
+
pageLoaded=false;
|
3162
|
+
currentIndex=0;
|
2264
3163
|
constructor(delegate) {
|
2265
|
-
this.restorationIdentifier = uuid();
|
2266
|
-
this.restorationData = {};
|
2267
|
-
this.started = false;
|
2268
|
-
this.pageLoaded = false;
|
2269
|
-
this.onPopState = event => {
|
2270
|
-
if (this.shouldHandlePopState()) {
|
2271
|
-
const {turbo: turbo} = event.state || {};
|
2272
|
-
if (turbo) {
|
2273
|
-
this.location = new URL(window.location.href);
|
2274
|
-
const {restorationIdentifier: restorationIdentifier} = turbo;
|
2275
|
-
this.restorationIdentifier = restorationIdentifier;
|
2276
|
-
this.delegate.historyPoppedToLocationWithRestorationIdentifier(this.location, restorationIdentifier);
|
2277
|
-
}
|
2278
|
-
}
|
2279
|
-
};
|
2280
|
-
this.onPageLoad = async _event => {
|
2281
|
-
await nextMicrotask();
|
2282
|
-
this.pageLoaded = true;
|
2283
|
-
};
|
2284
3164
|
this.delegate = delegate;
|
2285
3165
|
}
|
2286
3166
|
start() {
|
2287
3167
|
if (!this.started) {
|
2288
3168
|
addEventListener("popstate", this.onPopState, false);
|
2289
3169
|
addEventListener("load", this.onPageLoad, false);
|
3170
|
+
this.currentIndex = history.state?.turbo?.restorationIndex || 0;
|
2290
3171
|
this.started = true;
|
2291
3172
|
this.replace(new URL(window.location.href));
|
2292
3173
|
}
|
@@ -2305,9 +3186,11 @@ class History {
|
|
2305
3186
|
this.update(history.replaceState, location, restorationIdentifier);
|
2306
3187
|
}
|
2307
3188
|
update(method, location, restorationIdentifier = uuid()) {
|
3189
|
+
if (method === history.pushState) ++this.currentIndex;
|
2308
3190
|
const state = {
|
2309
3191
|
turbo: {
|
2310
|
-
restorationIdentifier: restorationIdentifier
|
3192
|
+
restorationIdentifier: restorationIdentifier,
|
3193
|
+
restorationIndex: this.currentIndex
|
2311
3194
|
}
|
2312
3195
|
};
|
2313
3196
|
method.call(history, state, "", location.href);
|
@@ -2320,12 +3203,14 @@ class History {
|
|
2320
3203
|
updateRestorationData(additionalData) {
|
2321
3204
|
const {restorationIdentifier: restorationIdentifier} = this;
|
2322
3205
|
const restorationData = this.restorationData[restorationIdentifier];
|
2323
|
-
this.restorationData[restorationIdentifier] =
|
3206
|
+
this.restorationData[restorationIdentifier] = {
|
3207
|
+
...restorationData,
|
3208
|
+
...additionalData
|
3209
|
+
};
|
2324
3210
|
}
|
2325
3211
|
assumeControlOfScrollRestoration() {
|
2326
|
-
var _a;
|
2327
3212
|
if (!this.previousScrollRestoration) {
|
2328
|
-
this.previousScrollRestoration =
|
3213
|
+
this.previousScrollRestoration = history.scrollRestoration ?? "auto";
|
2329
3214
|
history.scrollRestoration = "manual";
|
2330
3215
|
}
|
2331
3216
|
}
|
@@ -2335,6 +3220,23 @@ class History {
|
|
2335
3220
|
delete this.previousScrollRestoration;
|
2336
3221
|
}
|
2337
3222
|
}
|
3223
|
+
onPopState=event => {
|
3224
|
+
if (this.shouldHandlePopState()) {
|
3225
|
+
const {turbo: turbo} = event.state || {};
|
3226
|
+
if (turbo) {
|
3227
|
+
this.location = new URL(window.location.href);
|
3228
|
+
const {restorationIdentifier: restorationIdentifier, restorationIndex: restorationIndex} = turbo;
|
3229
|
+
this.restorationIdentifier = restorationIdentifier;
|
3230
|
+
const direction = restorationIndex > this.currentIndex ? "forward" : "back";
|
3231
|
+
this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
|
3232
|
+
this.currentIndex = restorationIndex;
|
3233
|
+
}
|
3234
|
+
}
|
3235
|
+
};
|
3236
|
+
onPageLoad=async _event => {
|
3237
|
+
await nextMicrotask();
|
3238
|
+
this.pageLoaded = true;
|
3239
|
+
};
|
2338
3240
|
shouldHandlePopState() {
|
2339
3241
|
return this.pageIsLoaded();
|
2340
3242
|
}
|
@@ -2343,24 +3245,154 @@ class History {
|
|
2343
3245
|
}
|
2344
3246
|
}
|
2345
3247
|
|
3248
|
+
class LinkPrefetchObserver {
|
3249
|
+
started=false;
|
3250
|
+
#prefetchedLink=null;
|
3251
|
+
constructor(delegate, eventTarget) {
|
3252
|
+
this.delegate = delegate;
|
3253
|
+
this.eventTarget = eventTarget;
|
3254
|
+
}
|
3255
|
+
start() {
|
3256
|
+
if (this.started) return;
|
3257
|
+
if (this.eventTarget.readyState === "loading") {
|
3258
|
+
this.eventTarget.addEventListener("DOMContentLoaded", this.#enable, {
|
3259
|
+
once: true
|
3260
|
+
});
|
3261
|
+
} else {
|
3262
|
+
this.#enable();
|
3263
|
+
}
|
3264
|
+
}
|
3265
|
+
stop() {
|
3266
|
+
if (!this.started) return;
|
3267
|
+
this.eventTarget.removeEventListener("mouseenter", this.#tryToPrefetchRequest, {
|
3268
|
+
capture: true,
|
3269
|
+
passive: true
|
3270
|
+
});
|
3271
|
+
this.eventTarget.removeEventListener("mouseleave", this.#cancelRequestIfObsolete, {
|
3272
|
+
capture: true,
|
3273
|
+
passive: true
|
3274
|
+
});
|
3275
|
+
this.eventTarget.removeEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
|
3276
|
+
this.started = false;
|
3277
|
+
}
|
3278
|
+
#enable=() => {
|
3279
|
+
this.eventTarget.addEventListener("mouseenter", this.#tryToPrefetchRequest, {
|
3280
|
+
capture: true,
|
3281
|
+
passive: true
|
3282
|
+
});
|
3283
|
+
this.eventTarget.addEventListener("mouseleave", this.#cancelRequestIfObsolete, {
|
3284
|
+
capture: true,
|
3285
|
+
passive: true
|
3286
|
+
});
|
3287
|
+
this.eventTarget.addEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
|
3288
|
+
this.started = true;
|
3289
|
+
};
|
3290
|
+
#tryToPrefetchRequest=event => {
|
3291
|
+
if (getMetaContent("turbo-prefetch") === "false") return;
|
3292
|
+
const target = event.target;
|
3293
|
+
const isLink = target.matches && target.matches("a[href]:not([target^=_]):not([download])");
|
3294
|
+
if (isLink && this.#isPrefetchable(target)) {
|
3295
|
+
const link = target;
|
3296
|
+
const location = getLocationForLink(link);
|
3297
|
+
if (this.delegate.canPrefetchRequestToLocation(link, location)) {
|
3298
|
+
this.#prefetchedLink = link;
|
3299
|
+
const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams, target);
|
3300
|
+
prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl);
|
3301
|
+
}
|
3302
|
+
}
|
3303
|
+
};
|
3304
|
+
#cancelRequestIfObsolete=event => {
|
3305
|
+
if (event.target === this.#prefetchedLink) this.#cancelPrefetchRequest();
|
3306
|
+
};
|
3307
|
+
#cancelPrefetchRequest=() => {
|
3308
|
+
prefetchCache.clear();
|
3309
|
+
this.#prefetchedLink = null;
|
3310
|
+
};
|
3311
|
+
#tryToUsePrefetchedRequest=event => {
|
3312
|
+
if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
|
3313
|
+
const cached = prefetchCache.get(event.detail.url.toString());
|
3314
|
+
if (cached) {
|
3315
|
+
event.detail.fetchRequest = cached;
|
3316
|
+
}
|
3317
|
+
prefetchCache.clear();
|
3318
|
+
}
|
3319
|
+
};
|
3320
|
+
prepareRequest(request) {
|
3321
|
+
const link = request.target;
|
3322
|
+
request.headers["X-Sec-Purpose"] = "prefetch";
|
3323
|
+
const turboFrame = link.closest("turbo-frame");
|
3324
|
+
const turboFrameTarget = link.getAttribute("data-turbo-frame") || turboFrame?.getAttribute("target") || turboFrame?.id;
|
3325
|
+
if (turboFrameTarget && turboFrameTarget !== "_top") {
|
3326
|
+
request.headers["Turbo-Frame"] = turboFrameTarget;
|
3327
|
+
}
|
3328
|
+
}
|
3329
|
+
requestSucceededWithResponse() {}
|
3330
|
+
requestStarted(fetchRequest) {}
|
3331
|
+
requestErrored(fetchRequest) {}
|
3332
|
+
requestFinished(fetchRequest) {}
|
3333
|
+
requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
|
3334
|
+
requestFailedWithResponse(fetchRequest, fetchResponse) {}
|
3335
|
+
get #cacheTtl() {
|
3336
|
+
return Number(getMetaContent("turbo-prefetch-cache-time")) || cacheTtl;
|
3337
|
+
}
|
3338
|
+
#isPrefetchable(link) {
|
3339
|
+
const href = link.getAttribute("href");
|
3340
|
+
if (!href) return false;
|
3341
|
+
if (unfetchableLink(link)) return false;
|
3342
|
+
if (linkToTheSamePage(link)) return false;
|
3343
|
+
if (linkOptsOut(link)) return false;
|
3344
|
+
if (nonSafeLink(link)) return false;
|
3345
|
+
if (eventPrevented(link)) return false;
|
3346
|
+
return true;
|
3347
|
+
}
|
3348
|
+
}
|
3349
|
+
|
3350
|
+
const unfetchableLink = link => link.origin !== document.location.origin || ![ "http:", "https:" ].includes(link.protocol) || link.hasAttribute("target");
|
3351
|
+
|
3352
|
+
const linkToTheSamePage = link => link.pathname + link.search === document.location.pathname + document.location.search || link.href.startsWith("#");
|
3353
|
+
|
3354
|
+
const linkOptsOut = link => {
|
3355
|
+
if (link.getAttribute("data-turbo-prefetch") === "false") return true;
|
3356
|
+
if (link.getAttribute("data-turbo") === "false") return true;
|
3357
|
+
const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]");
|
3358
|
+
if (turboPrefetchParent && turboPrefetchParent.getAttribute("data-turbo-prefetch") === "false") return true;
|
3359
|
+
return false;
|
3360
|
+
};
|
3361
|
+
|
3362
|
+
const nonSafeLink = link => {
|
3363
|
+
const turboMethod = link.getAttribute("data-turbo-method");
|
3364
|
+
if (turboMethod && turboMethod.toLowerCase() !== "get") return true;
|
3365
|
+
if (isUJS(link)) return true;
|
3366
|
+
if (link.hasAttribute("data-turbo-confirm")) return true;
|
3367
|
+
if (link.hasAttribute("data-turbo-stream")) return true;
|
3368
|
+
return false;
|
3369
|
+
};
|
3370
|
+
|
3371
|
+
const isUJS = link => link.hasAttribute("data-remote") || link.hasAttribute("data-behavior") || link.hasAttribute("data-confirm") || link.hasAttribute("data-method");
|
3372
|
+
|
3373
|
+
const eventPrevented = link => {
|
3374
|
+
const event = dispatch("turbo:before-prefetch", {
|
3375
|
+
target: link,
|
3376
|
+
cancelable: true
|
3377
|
+
});
|
3378
|
+
return event.defaultPrevented;
|
3379
|
+
};
|
3380
|
+
|
2346
3381
|
class Navigator {
|
2347
3382
|
constructor(delegate) {
|
2348
3383
|
this.delegate = delegate;
|
2349
3384
|
}
|
2350
3385
|
proposeVisit(location, options = {}) {
|
2351
3386
|
if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
|
2352
|
-
|
2353
|
-
this.delegate.visitProposedToLocation(location, options);
|
2354
|
-
} else {
|
2355
|
-
window.location.href = location.toString();
|
2356
|
-
}
|
3387
|
+
this.delegate.visitProposedToLocation(location, options);
|
2357
3388
|
}
|
2358
3389
|
}
|
2359
3390
|
startVisit(locatable, restorationIdentifier, options = {}) {
|
2360
3391
|
this.stop();
|
2361
|
-
this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier,
|
2362
|
-
referrer: this.location
|
2363
|
-
|
3392
|
+
this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, {
|
3393
|
+
referrer: this.location,
|
3394
|
+
...options
|
3395
|
+
});
|
2364
3396
|
this.currentVisit.start();
|
2365
3397
|
}
|
2366
3398
|
submitForm(form, submitter) {
|
@@ -2384,6 +3416,9 @@ class Navigator {
|
|
2384
3416
|
get view() {
|
2385
3417
|
return this.delegate.view;
|
2386
3418
|
}
|
3419
|
+
get rootLocation() {
|
3420
|
+
return this.view.snapshot.rootLocation;
|
3421
|
+
}
|
2387
3422
|
get history() {
|
2388
3423
|
return this.delegate.history;
|
2389
3424
|
}
|
@@ -2401,7 +3436,7 @@ class Navigator {
|
|
2401
3436
|
this.view.clearSnapshotCache();
|
2402
3437
|
}
|
2403
3438
|
const {statusCode: statusCode, redirected: redirected} = fetchResponse;
|
2404
|
-
const action = this
|
3439
|
+
const action = this.#getActionForFormSubmission(formSubmission, fetchResponse);
|
2405
3440
|
const visitOptions = {
|
2406
3441
|
action: action,
|
2407
3442
|
shouldCacheSnapshot: shouldCacheSnapshot,
|
@@ -2424,7 +3459,9 @@ class Navigator {
|
|
2424
3459
|
} else {
|
2425
3460
|
await this.view.renderPage(snapshot, false, true, this.currentVisit);
|
2426
3461
|
}
|
2427
|
-
|
3462
|
+
if (!snapshot.shouldPreserveScrollPosition) {
|
3463
|
+
this.view.scrollToTop();
|
3464
|
+
}
|
2428
3465
|
this.view.clearSnapshotCache();
|
2429
3466
|
}
|
2430
3467
|
}
|
@@ -2441,6 +3478,7 @@ class Navigator {
|
|
2441
3478
|
}
|
2442
3479
|
visitCompleted(visit) {
|
2443
3480
|
this.delegate.visitCompleted(visit);
|
3481
|
+
delete this.currentVisit;
|
2444
3482
|
}
|
2445
3483
|
locationWithActionIsSamePage(location, action) {
|
2446
3484
|
const anchor = getAnchor(location);
|
@@ -2457,35 +3495,27 @@ class Navigator {
|
|
2457
3495
|
get restorationIdentifier() {
|
2458
3496
|
return this.history.restorationIdentifier;
|
2459
3497
|
}
|
2460
|
-
getActionForFormSubmission(
|
2461
|
-
|
3498
|
+
#getActionForFormSubmission(formSubmission, fetchResponse) {
|
3499
|
+
const {submitter: submitter, formElement: formElement} = formSubmission;
|
3500
|
+
return getVisitAction(submitter, formElement) || this.#getDefaultAction(fetchResponse);
|
3501
|
+
}
|
3502
|
+
#getDefaultAction(fetchResponse) {
|
3503
|
+
const sameLocationRedirect = fetchResponse.redirected && fetchResponse.location.href === this.location?.href;
|
3504
|
+
return sameLocationRedirect ? "replace" : "advance";
|
2462
3505
|
}
|
2463
3506
|
}
|
2464
3507
|
|
2465
|
-
|
2466
|
-
|
2467
|
-
|
2468
|
-
|
2469
|
-
|
2470
|
-
|
2471
|
-
PageStage[PageStage["complete"] = 3] = "complete";
|
2472
|
-
})(PageStage || (PageStage = {}));
|
3508
|
+
const PageStage = {
|
3509
|
+
initial: 0,
|
3510
|
+
loading: 1,
|
3511
|
+
interactive: 2,
|
3512
|
+
complete: 3
|
3513
|
+
};
|
2473
3514
|
|
2474
3515
|
class PageObserver {
|
3516
|
+
stage=PageStage.initial;
|
3517
|
+
started=false;
|
2475
3518
|
constructor(delegate) {
|
2476
|
-
this.stage = PageStage.initial;
|
2477
|
-
this.started = false;
|
2478
|
-
this.interpretReadyState = () => {
|
2479
|
-
const {readyState: readyState} = this;
|
2480
|
-
if (readyState == "interactive") {
|
2481
|
-
this.pageIsInteractive();
|
2482
|
-
} else if (readyState == "complete") {
|
2483
|
-
this.pageIsComplete();
|
2484
|
-
}
|
2485
|
-
};
|
2486
|
-
this.pageWillUnload = () => {
|
2487
|
-
this.delegate.pageWillUnload();
|
2488
|
-
};
|
2489
3519
|
this.delegate = delegate;
|
2490
3520
|
}
|
2491
3521
|
start() {
|
@@ -2505,6 +3535,14 @@ class PageObserver {
|
|
2505
3535
|
this.started = false;
|
2506
3536
|
}
|
2507
3537
|
}
|
3538
|
+
interpretReadyState=() => {
|
3539
|
+
const {readyState: readyState} = this;
|
3540
|
+
if (readyState == "interactive") {
|
3541
|
+
this.pageIsInteractive();
|
3542
|
+
} else if (readyState == "complete") {
|
3543
|
+
this.pageIsComplete();
|
3544
|
+
}
|
3545
|
+
};
|
2508
3546
|
pageIsInteractive() {
|
2509
3547
|
if (this.stage == PageStage.loading) {
|
2510
3548
|
this.stage = PageStage.interactive;
|
@@ -2518,20 +3556,17 @@ class PageObserver {
|
|
2518
3556
|
this.delegate.pageLoaded();
|
2519
3557
|
}
|
2520
3558
|
}
|
3559
|
+
pageWillUnload=() => {
|
3560
|
+
this.delegate.pageWillUnload();
|
3561
|
+
};
|
2521
3562
|
get readyState() {
|
2522
3563
|
return document.readyState;
|
2523
3564
|
}
|
2524
3565
|
}
|
2525
3566
|
|
2526
3567
|
class ScrollObserver {
|
3568
|
+
started=false;
|
2527
3569
|
constructor(delegate) {
|
2528
|
-
this.started = false;
|
2529
|
-
this.onScroll = () => {
|
2530
|
-
this.updatePosition({
|
2531
|
-
x: window.pageXOffset,
|
2532
|
-
y: window.pageYOffset
|
2533
|
-
});
|
2534
|
-
};
|
2535
3570
|
this.delegate = delegate;
|
2536
3571
|
}
|
2537
3572
|
start() {
|
@@ -2547,6 +3582,12 @@ class ScrollObserver {
|
|
2547
3582
|
this.started = false;
|
2548
3583
|
}
|
2549
3584
|
}
|
3585
|
+
onScroll=() => {
|
3586
|
+
this.updatePosition({
|
3587
|
+
x: window.pageXOffset,
|
3588
|
+
y: window.pageYOffset
|
3589
|
+
});
|
3590
|
+
};
|
2550
3591
|
updatePosition(position) {
|
2551
3592
|
this.delegate.scrollPositionChanged(position);
|
2552
3593
|
}
|
@@ -2554,7 +3595,13 @@ class ScrollObserver {
|
|
2554
3595
|
|
2555
3596
|
class StreamMessageRenderer {
|
2556
3597
|
render({fragment: fragment}) {
|
2557
|
-
Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), (() =>
|
3598
|
+
Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), (() => {
|
3599
|
+
withAutofocusFromFragment(fragment, (() => {
|
3600
|
+
withPreservedFocus((() => {
|
3601
|
+
document.documentElement.appendChild(fragment);
|
3602
|
+
}));
|
3603
|
+
}));
|
3604
|
+
}));
|
2558
3605
|
}
|
2559
3606
|
enteringBardo(currentPermanentElement, newPermanentElement) {
|
2560
3607
|
newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true));
|
@@ -2577,33 +3624,67 @@ function getPermanentElementMapForFragment(fragment) {
|
|
2577
3624
|
return permanentElementMap;
|
2578
3625
|
}
|
2579
3626
|
|
3627
|
+
async function withAutofocusFromFragment(fragment, callback) {
|
3628
|
+
const generatedID = `turbo-stream-autofocus-${uuid()}`;
|
3629
|
+
const turboStreams = fragment.querySelectorAll("turbo-stream");
|
3630
|
+
const elementWithAutofocus = firstAutofocusableElementInStreams(turboStreams);
|
3631
|
+
let willAutofocusId = null;
|
3632
|
+
if (elementWithAutofocus) {
|
3633
|
+
if (elementWithAutofocus.id) {
|
3634
|
+
willAutofocusId = elementWithAutofocus.id;
|
3635
|
+
} else {
|
3636
|
+
willAutofocusId = generatedID;
|
3637
|
+
}
|
3638
|
+
elementWithAutofocus.id = willAutofocusId;
|
3639
|
+
}
|
3640
|
+
callback();
|
3641
|
+
await nextRepaint();
|
3642
|
+
const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body;
|
3643
|
+
if (hasNoActiveElement && willAutofocusId) {
|
3644
|
+
const elementToAutofocus = document.getElementById(willAutofocusId);
|
3645
|
+
if (elementIsFocusable(elementToAutofocus)) {
|
3646
|
+
elementToAutofocus.focus();
|
3647
|
+
}
|
3648
|
+
if (elementToAutofocus && elementToAutofocus.id == generatedID) {
|
3649
|
+
elementToAutofocus.removeAttribute("id");
|
3650
|
+
}
|
3651
|
+
}
|
3652
|
+
}
|
3653
|
+
|
3654
|
+
async function withPreservedFocus(callback) {
|
3655
|
+
const [activeElementBeforeRender, activeElementAfterRender] = await around(callback, (() => document.activeElement));
|
3656
|
+
const restoreFocusTo = activeElementBeforeRender && activeElementBeforeRender.id;
|
3657
|
+
if (restoreFocusTo) {
|
3658
|
+
const elementToFocus = document.getElementById(restoreFocusTo);
|
3659
|
+
if (elementIsFocusable(elementToFocus) && elementToFocus != activeElementAfterRender) {
|
3660
|
+
elementToFocus.focus();
|
3661
|
+
}
|
3662
|
+
}
|
3663
|
+
}
|
3664
|
+
|
3665
|
+
function firstAutofocusableElementInStreams(nodeListOfStreamElements) {
|
3666
|
+
for (const streamElement of nodeListOfStreamElements) {
|
3667
|
+
const elementWithAutofocus = queryAutofocusableElement(streamElement.templateElement.content);
|
3668
|
+
if (elementWithAutofocus) return elementWithAutofocus;
|
3669
|
+
}
|
3670
|
+
return null;
|
3671
|
+
}
|
3672
|
+
|
2580
3673
|
class StreamObserver {
|
3674
|
+
sources=new Set;
|
3675
|
+
#started=false;
|
2581
3676
|
constructor(delegate) {
|
2582
|
-
this.sources = new Set;
|
2583
|
-
this.started = false;
|
2584
|
-
this.inspectFetchResponse = event => {
|
2585
|
-
const response = fetchResponseFromEvent(event);
|
2586
|
-
if (response && fetchResponseIsStream(response)) {
|
2587
|
-
event.preventDefault();
|
2588
|
-
this.receiveMessageResponse(response);
|
2589
|
-
}
|
2590
|
-
};
|
2591
|
-
this.receiveMessageEvent = event => {
|
2592
|
-
if (this.started && typeof event.data == "string") {
|
2593
|
-
this.receiveMessageHTML(event.data);
|
2594
|
-
}
|
2595
|
-
};
|
2596
3677
|
this.delegate = delegate;
|
2597
3678
|
}
|
2598
3679
|
start() {
|
2599
|
-
if (!this
|
2600
|
-
this
|
3680
|
+
if (!this.#started) {
|
3681
|
+
this.#started = true;
|
2601
3682
|
addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
|
2602
3683
|
}
|
2603
3684
|
}
|
2604
3685
|
stop() {
|
2605
|
-
if (this
|
2606
|
-
this
|
3686
|
+
if (this.#started) {
|
3687
|
+
this.#started = false;
|
2607
3688
|
removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
|
2608
3689
|
}
|
2609
3690
|
}
|
@@ -2622,6 +3703,18 @@ class StreamObserver {
|
|
2622
3703
|
streamSourceIsConnected(source) {
|
2623
3704
|
return this.sources.has(source);
|
2624
3705
|
}
|
3706
|
+
inspectFetchResponse=event => {
|
3707
|
+
const response = fetchResponseFromEvent(event);
|
3708
|
+
if (response && fetchResponseIsStream(response)) {
|
3709
|
+
event.preventDefault();
|
3710
|
+
this.receiveMessageResponse(response);
|
3711
|
+
}
|
3712
|
+
};
|
3713
|
+
receiveMessageEvent=event => {
|
3714
|
+
if (this.#started && typeof event.data == "string") {
|
3715
|
+
this.receiveMessageHTML(event.data);
|
3716
|
+
}
|
3717
|
+
};
|
2625
3718
|
async receiveMessageResponse(response) {
|
2626
3719
|
const html = await response.responseHTML;
|
2627
3720
|
if (html) {
|
@@ -2634,16 +3727,14 @@ class StreamObserver {
|
|
2634
3727
|
}
|
2635
3728
|
|
2636
3729
|
function fetchResponseFromEvent(event) {
|
2637
|
-
|
2638
|
-
const fetchResponse = (_a = event.detail) === null || _a === void 0 ? void 0 : _a.fetchResponse;
|
3730
|
+
const fetchResponse = event.detail?.fetchResponse;
|
2639
3731
|
if (fetchResponse instanceof FetchResponse) {
|
2640
3732
|
return fetchResponse;
|
2641
3733
|
}
|
2642
3734
|
}
|
2643
3735
|
|
2644
3736
|
function fetchResponseIsStream(response) {
|
2645
|
-
|
2646
|
-
const contentType = (_a = response.contentType) !== null && _a !== void 0 ? _a : "";
|
3737
|
+
const contentType = response.contentType ?? "";
|
2647
3738
|
return contentType.startsWith(StreamMessage.contentType);
|
2648
3739
|
}
|
2649
3740
|
|
@@ -2702,6 +3793,7 @@ class PageRenderer extends Renderer {
|
|
2702
3793
|
}
|
2703
3794
|
}
|
2704
3795
|
async prepareToRender() {
|
3796
|
+
this.#setLanguage();
|
2705
3797
|
await this.mergeHead();
|
2706
3798
|
}
|
2707
3799
|
async render() {
|
@@ -2724,12 +3816,24 @@ class PageRenderer extends Renderer {
|
|
2724
3816
|
get newElement() {
|
2725
3817
|
return this.newSnapshot.element;
|
2726
3818
|
}
|
3819
|
+
#setLanguage() {
|
3820
|
+
const {documentElement: documentElement} = this.currentSnapshot;
|
3821
|
+
const {lang: lang} = this.newSnapshot;
|
3822
|
+
if (lang) {
|
3823
|
+
documentElement.setAttribute("lang", lang);
|
3824
|
+
} else {
|
3825
|
+
documentElement.removeAttribute("lang");
|
3826
|
+
}
|
3827
|
+
}
|
2727
3828
|
async mergeHead() {
|
2728
3829
|
const mergedHeadElements = this.mergeProvisionalElements();
|
2729
3830
|
const newStylesheetElements = this.copyNewHeadStylesheetElements();
|
2730
3831
|
this.copyNewHeadScriptElements();
|
2731
3832
|
await mergedHeadElements;
|
2732
3833
|
await newStylesheetElements;
|
3834
|
+
if (this.willRender) {
|
3835
|
+
this.removeUnusedDynamicStylesheetElements();
|
3836
|
+
}
|
2733
3837
|
}
|
2734
3838
|
async replaceBody() {
|
2735
3839
|
await this.preservingPermanentElements((async () => {
|
@@ -2753,6 +3857,11 @@ class PageRenderer extends Renderer {
|
|
2753
3857
|
document.head.appendChild(activateScriptElement(element));
|
2754
3858
|
}
|
2755
3859
|
}
|
3860
|
+
removeUnusedDynamicStylesheetElements() {
|
3861
|
+
for (const element of this.unusedDynamicStylesheetElements) {
|
3862
|
+
document.head.removeChild(element);
|
3863
|
+
}
|
3864
|
+
}
|
2756
3865
|
async mergeProvisionalElements() {
|
2757
3866
|
const newHeadElements = [ ...this.newHeadProvisionalElements ];
|
2758
3867
|
for (const element of this.currentHeadProvisionalElements) {
|
@@ -2805,6 +3914,12 @@ class PageRenderer extends Renderer {
|
|
2805
3914
|
async assignNewBody() {
|
2806
3915
|
await this.renderElement(this.currentElement, this.newElement);
|
2807
3916
|
}
|
3917
|
+
get unusedDynamicStylesheetElements() {
|
3918
|
+
return this.oldHeadStylesheetElements.filter((element => element.getAttribute("data-turbo-track") === "dynamic"));
|
3919
|
+
}
|
3920
|
+
get oldHeadStylesheetElements() {
|
3921
|
+
return this.currentHeadSnapshot.getStylesheetElementsNotInSnapshot(this.newHeadSnapshot);
|
3922
|
+
}
|
2808
3923
|
get newHeadStylesheetElements() {
|
2809
3924
|
return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot);
|
2810
3925
|
}
|
@@ -2822,10 +3937,42 @@ class PageRenderer extends Renderer {
|
|
2822
3937
|
}
|
2823
3938
|
}
|
2824
3939
|
|
3940
|
+
class MorphingPageRenderer extends PageRenderer {
|
3941
|
+
static renderElement(currentElement, newElement) {
|
3942
|
+
morphElements(currentElement, newElement, {
|
3943
|
+
callbacks: {
|
3944
|
+
beforeNodeMorphed: element => !canRefreshFrame(element)
|
3945
|
+
}
|
3946
|
+
});
|
3947
|
+
for (const frame of currentElement.querySelectorAll("turbo-frame")) {
|
3948
|
+
if (canRefreshFrame(frame)) frame.reload();
|
3949
|
+
}
|
3950
|
+
dispatch("turbo:morph", {
|
3951
|
+
detail: {
|
3952
|
+
currentElement: currentElement,
|
3953
|
+
newElement: newElement
|
3954
|
+
}
|
3955
|
+
});
|
3956
|
+
}
|
3957
|
+
async preservingPermanentElements(callback) {
|
3958
|
+
return await callback();
|
3959
|
+
}
|
3960
|
+
get renderMethod() {
|
3961
|
+
return "morph";
|
3962
|
+
}
|
3963
|
+
get shouldAutofocus() {
|
3964
|
+
return false;
|
3965
|
+
}
|
3966
|
+
}
|
3967
|
+
|
3968
|
+
function canRefreshFrame(frame) {
|
3969
|
+
return frame instanceof FrameElement && frame.src && frame.refresh === "morph" && !frame.closest("[data-turbo-permanent]");
|
3970
|
+
}
|
3971
|
+
|
2825
3972
|
class SnapshotCache {
|
3973
|
+
keys=[];
|
3974
|
+
snapshots={};
|
2826
3975
|
constructor(size) {
|
2827
|
-
this.keys = [];
|
2828
|
-
this.snapshots = {};
|
2829
3976
|
this.size = size;
|
2830
3977
|
}
|
2831
3978
|
has(location) {
|
@@ -2867,24 +4014,26 @@ class SnapshotCache {
|
|
2867
4014
|
}
|
2868
4015
|
|
2869
4016
|
class PageView extends View {
|
2870
|
-
|
2871
|
-
|
2872
|
-
|
2873
|
-
|
2874
|
-
this.
|
4017
|
+
snapshotCache=new SnapshotCache(10);
|
4018
|
+
lastRenderedLocation=new URL(location.href);
|
4019
|
+
forceReloaded=false;
|
4020
|
+
shouldTransitionTo(newSnapshot) {
|
4021
|
+
return this.snapshot.prefersViewTransitions && newSnapshot.prefersViewTransitions;
|
2875
4022
|
}
|
2876
4023
|
renderPage(snapshot, isPreview = false, willRender = true, visit) {
|
2877
|
-
const
|
4024
|
+
const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage;
|
4025
|
+
const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer;
|
4026
|
+
const renderer = new rendererClass(this.snapshot, snapshot, isPreview, willRender);
|
2878
4027
|
if (!renderer.shouldRender) {
|
2879
4028
|
this.forceReloaded = true;
|
2880
4029
|
} else {
|
2881
|
-
visit
|
4030
|
+
visit?.changeHistory();
|
2882
4031
|
}
|
2883
4032
|
return this.render(renderer);
|
2884
4033
|
}
|
2885
4034
|
renderError(snapshot, visit) {
|
2886
|
-
visit
|
2887
|
-
const renderer = new ErrorRenderer(this.snapshot, snapshot,
|
4035
|
+
visit?.changeHistory();
|
4036
|
+
const renderer = new ErrorRenderer(this.snapshot, snapshot, false);
|
2888
4037
|
return this.render(renderer);
|
2889
4038
|
}
|
2890
4039
|
clearSnapshotCache() {
|
@@ -2903,31 +4052,38 @@ class PageView extends View {
|
|
2903
4052
|
getCachedSnapshotForLocation(location) {
|
2904
4053
|
return this.snapshotCache.get(location);
|
2905
4054
|
}
|
4055
|
+
isPageRefresh(visit) {
|
4056
|
+
return !visit || this.lastRenderedLocation.pathname === visit.location.pathname && visit.action === "replace";
|
4057
|
+
}
|
4058
|
+
shouldPreserveScrollPosition(visit) {
|
4059
|
+
return this.isPageRefresh(visit) && this.snapshot.shouldPreserveScrollPosition;
|
4060
|
+
}
|
2906
4061
|
get snapshot() {
|
2907
4062
|
return PageSnapshot.fromElement(this.element);
|
2908
4063
|
}
|
2909
4064
|
}
|
2910
4065
|
|
2911
4066
|
class Preloader {
|
2912
|
-
|
2913
|
-
|
4067
|
+
selector="a[data-turbo-preload]";
|
4068
|
+
constructor(delegate, snapshotCache) {
|
2914
4069
|
this.delegate = delegate;
|
2915
|
-
|
2916
|
-
get snapshotCache() {
|
2917
|
-
return this.delegate.navigator.view.snapshotCache;
|
4070
|
+
this.snapshotCache = snapshotCache;
|
2918
4071
|
}
|
2919
4072
|
start() {
|
2920
4073
|
if (document.readyState === "loading") {
|
2921
|
-
|
2922
|
-
this.preloadOnLoadLinksForView(document.body);
|
2923
|
-
}));
|
4074
|
+
document.addEventListener("DOMContentLoaded", this.#preloadAll);
|
2924
4075
|
} else {
|
2925
4076
|
this.preloadOnLoadLinksForView(document.body);
|
2926
4077
|
}
|
2927
4078
|
}
|
4079
|
+
stop() {
|
4080
|
+
document.removeEventListener("DOMContentLoaded", this.#preloadAll);
|
4081
|
+
}
|
2928
4082
|
preloadOnLoadLinksForView(element) {
|
2929
4083
|
for (const link of element.querySelectorAll(this.selector)) {
|
2930
|
-
this.
|
4084
|
+
if (this.delegate.shouldPreloadLink(link)) {
|
4085
|
+
this.preloadURL(link);
|
4086
|
+
}
|
2931
4087
|
}
|
2932
4088
|
}
|
2933
4089
|
async preloadURL(link) {
|
@@ -2935,46 +4091,80 @@ class Preloader {
|
|
2935
4091
|
if (this.snapshotCache.has(location)) {
|
2936
4092
|
return;
|
2937
4093
|
}
|
4094
|
+
const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams, link);
|
4095
|
+
await fetchRequest.perform();
|
4096
|
+
}
|
4097
|
+
prepareRequest(fetchRequest) {
|
4098
|
+
fetchRequest.headers["X-Sec-Purpose"] = "prefetch";
|
4099
|
+
}
|
4100
|
+
async requestSucceededWithResponse(fetchRequest, fetchResponse) {
|
2938
4101
|
try {
|
2939
|
-
const
|
2940
|
-
|
2941
|
-
|
2942
|
-
Accept: "text/html"
|
2943
|
-
}
|
2944
|
-
});
|
2945
|
-
const responseText = await response.text();
|
2946
|
-
const snapshot = PageSnapshot.fromHTMLString(responseText);
|
2947
|
-
this.snapshotCache.put(location, snapshot);
|
4102
|
+
const responseHTML = await fetchResponse.responseHTML;
|
4103
|
+
const snapshot = PageSnapshot.fromHTMLString(responseHTML);
|
4104
|
+
this.snapshotCache.put(fetchRequest.url, snapshot);
|
2948
4105
|
} catch (_) {}
|
2949
4106
|
}
|
4107
|
+
requestStarted(fetchRequest) {}
|
4108
|
+
requestErrored(fetchRequest) {}
|
4109
|
+
requestFinished(fetchRequest) {}
|
4110
|
+
requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
|
4111
|
+
requestFailedWithResponse(fetchRequest, fetchResponse) {}
|
4112
|
+
#preloadAll=() => {
|
4113
|
+
this.preloadOnLoadLinksForView(document.body);
|
4114
|
+
};
|
4115
|
+
}
|
4116
|
+
|
4117
|
+
class Cache {
|
4118
|
+
constructor(session) {
|
4119
|
+
this.session = session;
|
4120
|
+
}
|
4121
|
+
clear() {
|
4122
|
+
this.session.clearCache();
|
4123
|
+
}
|
4124
|
+
resetCacheControl() {
|
4125
|
+
this.#setCacheControl("");
|
4126
|
+
}
|
4127
|
+
exemptPageFromCache() {
|
4128
|
+
this.#setCacheControl("no-cache");
|
4129
|
+
}
|
4130
|
+
exemptPageFromPreview() {
|
4131
|
+
this.#setCacheControl("no-preview");
|
4132
|
+
}
|
4133
|
+
#setCacheControl(value) {
|
4134
|
+
setMetaContent("turbo-cache-control", value);
|
4135
|
+
}
|
2950
4136
|
}
|
2951
4137
|
|
2952
4138
|
class Session {
|
2953
|
-
|
2954
|
-
|
2955
|
-
|
2956
|
-
|
2957
|
-
|
2958
|
-
|
2959
|
-
|
2960
|
-
|
2961
|
-
|
2962
|
-
|
2963
|
-
|
2964
|
-
|
2965
|
-
|
2966
|
-
|
2967
|
-
|
2968
|
-
|
2969
|
-
|
2970
|
-
|
2971
|
-
|
2972
|
-
this.
|
4139
|
+
navigator=new Navigator(this);
|
4140
|
+
history=new History(this);
|
4141
|
+
view=new PageView(this, document.documentElement);
|
4142
|
+
adapter=new BrowserAdapter(this);
|
4143
|
+
pageObserver=new PageObserver(this);
|
4144
|
+
cacheObserver=new CacheObserver;
|
4145
|
+
linkPrefetchObserver=new LinkPrefetchObserver(this, document);
|
4146
|
+
linkClickObserver=new LinkClickObserver(this, window);
|
4147
|
+
formSubmitObserver=new FormSubmitObserver(this, document);
|
4148
|
+
scrollObserver=new ScrollObserver(this);
|
4149
|
+
streamObserver=new StreamObserver(this);
|
4150
|
+
formLinkClickObserver=new FormLinkClickObserver(this, document.documentElement);
|
4151
|
+
frameRedirector=new FrameRedirector(this, document.documentElement);
|
4152
|
+
streamMessageRenderer=new StreamMessageRenderer;
|
4153
|
+
cache=new Cache(this);
|
4154
|
+
enabled=true;
|
4155
|
+
started=false;
|
4156
|
+
#pageRefreshDebouncePeriod=150;
|
4157
|
+
constructor(recentRequests) {
|
4158
|
+
this.recentRequests = recentRequests;
|
4159
|
+
this.preloader = new Preloader(this, this.view.snapshotCache);
|
4160
|
+
this.debouncedRefresh = this.refresh;
|
4161
|
+
this.pageRefreshDebouncePeriod = this.pageRefreshDebouncePeriod;
|
2973
4162
|
}
|
2974
4163
|
start() {
|
2975
4164
|
if (!this.started) {
|
2976
4165
|
this.pageObserver.start();
|
2977
4166
|
this.cacheObserver.start();
|
4167
|
+
this.linkPrefetchObserver.start();
|
2978
4168
|
this.formLinkClickObserver.start();
|
2979
4169
|
this.linkClickObserver.start();
|
2980
4170
|
this.formSubmitObserver.start();
|
@@ -2994,6 +4184,7 @@ class Session {
|
|
2994
4184
|
if (this.started) {
|
2995
4185
|
this.pageObserver.stop();
|
2996
4186
|
this.cacheObserver.stop();
|
4187
|
+
this.linkPrefetchObserver.stop();
|
2997
4188
|
this.formLinkClickObserver.stop();
|
2998
4189
|
this.linkClickObserver.stop();
|
2999
4190
|
this.formSubmitObserver.stop();
|
@@ -3001,6 +4192,7 @@ class Session {
|
|
3001
4192
|
this.streamObserver.stop();
|
3002
4193
|
this.frameRedirector.stop();
|
3003
4194
|
this.history.stop();
|
4195
|
+
this.preloader.stop();
|
3004
4196
|
this.started = false;
|
3005
4197
|
}
|
3006
4198
|
}
|
@@ -3010,12 +4202,22 @@ class Session {
|
|
3010
4202
|
visit(location, options = {}) {
|
3011
4203
|
const frameElement = options.frame ? document.getElementById(options.frame) : null;
|
3012
4204
|
if (frameElement instanceof FrameElement) {
|
4205
|
+
const action = options.action || getVisitAction(frameElement);
|
4206
|
+
frameElement.delegate.proposeVisitIfNavigatedWithAction(frameElement, action);
|
3013
4207
|
frameElement.src = location.toString();
|
3014
|
-
frameElement.loaded;
|
3015
4208
|
} else {
|
3016
4209
|
this.navigator.proposeVisit(expandURL(location), options);
|
3017
4210
|
}
|
3018
4211
|
}
|
4212
|
+
refresh(url, requestId) {
|
4213
|
+
const isRecentRequest = requestId && this.recentRequests.has(requestId);
|
4214
|
+
if (!isRecentRequest && !this.navigator.currentVisit) {
|
4215
|
+
this.visit(url, {
|
4216
|
+
action: "replace",
|
4217
|
+
shouldCacheSnapshot: false
|
4218
|
+
});
|
4219
|
+
}
|
4220
|
+
}
|
3019
4221
|
connectStreamSource(source) {
|
3020
4222
|
this.streamObserver.connectStreamSource(source);
|
3021
4223
|
}
|
@@ -3029,10 +4231,26 @@ class Session {
|
|
3029
4231
|
this.view.clearSnapshotCache();
|
3030
4232
|
}
|
3031
4233
|
setProgressBarDelay(delay) {
|
4234
|
+
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.`");
|
3032
4235
|
this.progressBarDelay = delay;
|
3033
4236
|
}
|
3034
|
-
|
3035
|
-
|
4237
|
+
set progressBarDelay(delay) {
|
4238
|
+
config.drive.progressBarDelay = delay;
|
4239
|
+
}
|
4240
|
+
get progressBarDelay() {
|
4241
|
+
return config.drive.progressBarDelay;
|
4242
|
+
}
|
4243
|
+
set drive(value) {
|
4244
|
+
config.drive.enabled = value;
|
4245
|
+
}
|
4246
|
+
get drive() {
|
4247
|
+
return config.drive.enabled;
|
4248
|
+
}
|
4249
|
+
set formMode(value) {
|
4250
|
+
config.forms.mode = value;
|
4251
|
+
}
|
4252
|
+
get formMode() {
|
4253
|
+
return config.forms.mode;
|
3036
4254
|
}
|
3037
4255
|
get location() {
|
3038
4256
|
return this.history.location;
|
@@ -3040,11 +4258,31 @@ class Session {
|
|
3040
4258
|
get restorationIdentifier() {
|
3041
4259
|
return this.history.restorationIdentifier;
|
3042
4260
|
}
|
3043
|
-
|
4261
|
+
get pageRefreshDebouncePeriod() {
|
4262
|
+
return this.#pageRefreshDebouncePeriod;
|
4263
|
+
}
|
4264
|
+
set pageRefreshDebouncePeriod(value) {
|
4265
|
+
this.refresh = debounce(this.debouncedRefresh.bind(this), value);
|
4266
|
+
this.#pageRefreshDebouncePeriod = value;
|
4267
|
+
}
|
4268
|
+
shouldPreloadLink(element) {
|
4269
|
+
const isUnsafe = element.hasAttribute("data-turbo-method");
|
4270
|
+
const isStream = element.hasAttribute("data-turbo-stream");
|
4271
|
+
const frameTarget = element.getAttribute("data-turbo-frame");
|
4272
|
+
const frame = frameTarget == "_top" ? null : document.getElementById(frameTarget) || findClosestRecursively(element, "turbo-frame:not([disabled])");
|
4273
|
+
if (isUnsafe || isStream || frame instanceof FrameElement) {
|
4274
|
+
return false;
|
4275
|
+
} else {
|
4276
|
+
const location = new URL(element.href);
|
4277
|
+
return this.elementIsNavigatable(element) && locationIsVisitable(location, this.snapshot.rootLocation);
|
4278
|
+
}
|
4279
|
+
}
|
4280
|
+
historyPoppedToLocationWithRestorationIdentifierAndDirection(location, restorationIdentifier, direction) {
|
3044
4281
|
if (this.enabled) {
|
3045
4282
|
this.navigator.startVisit(location, restorationIdentifier, {
|
3046
4283
|
action: "restore",
|
3047
|
-
historyChanged: true
|
4284
|
+
historyChanged: true,
|
4285
|
+
direction: direction
|
3048
4286
|
});
|
3049
4287
|
} else {
|
3050
4288
|
this.adapter.pageInvalidated({
|
@@ -3061,6 +4299,9 @@ class Session {
|
|
3061
4299
|
return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation);
|
3062
4300
|
}
|
3063
4301
|
submittedFormLinkToLocation() {}
|
4302
|
+
canPrefetchRequestToLocation(link, location) {
|
4303
|
+
return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation);
|
4304
|
+
}
|
3064
4305
|
willFollowLinkToLocation(link, location, event) {
|
3065
4306
|
return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation) && this.applicationAllowsFollowingLinkToLocation(link, location, event);
|
3066
4307
|
}
|
@@ -3082,6 +4323,7 @@ class Session {
|
|
3082
4323
|
visitStarted(visit) {
|
3083
4324
|
if (!visit.acceptsStreamResponse) {
|
3084
4325
|
markAsBusy(document.documentElement);
|
4326
|
+
this.view.markVisitDirection(visit.direction);
|
3085
4327
|
}
|
3086
4328
|
extendURLWithDeprecatedProperties(visit.location);
|
3087
4329
|
if (!visit.silent) {
|
@@ -3089,6 +4331,7 @@ class Session {
|
|
3089
4331
|
}
|
3090
4332
|
}
|
3091
4333
|
visitCompleted(visit) {
|
4334
|
+
this.view.unmarkVisitDirection();
|
3092
4335
|
clearBusyState(document.documentElement);
|
3093
4336
|
this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
|
3094
4337
|
}
|
@@ -3099,7 +4342,7 @@ class Session {
|
|
3099
4342
|
this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
|
3100
4343
|
}
|
3101
4344
|
willSubmitForm(form, submitter) {
|
3102
|
-
const action = getAction(form, submitter);
|
4345
|
+
const action = getAction$1(form, submitter);
|
3103
4346
|
return this.submissionIsNavigatable(form, submitter) && locationIsVisitable(expandURL(action), this.snapshot.rootLocation);
|
3104
4347
|
}
|
3105
4348
|
formSubmitted(form, submitter) {
|
@@ -3119,8 +4362,7 @@ class Session {
|
|
3119
4362
|
this.renderStreamMessage(message);
|
3120
4363
|
}
|
3121
4364
|
viewWillCacheSnapshot() {
|
3122
|
-
|
3123
|
-
if (!((_a = this.navigator.currentVisit) === null || _a === void 0 ? void 0 : _a.silent)) {
|
4365
|
+
if (!this.navigator.currentVisit?.silent) {
|
3124
4366
|
this.notifyApplicationBeforeCachingSnapshot();
|
3125
4367
|
}
|
3126
4368
|
}
|
@@ -3132,9 +4374,9 @@ class Session {
|
|
3132
4374
|
}
|
3133
4375
|
return !defaultPrevented;
|
3134
4376
|
}
|
3135
|
-
viewRenderedSnapshot(_snapshot, _isPreview) {
|
4377
|
+
viewRenderedSnapshot(_snapshot, _isPreview, renderMethod) {
|
3136
4378
|
this.view.lastRenderedLocation = this.history.location;
|
3137
|
-
this.notifyApplicationAfterRender();
|
4379
|
+
this.notifyApplicationAfterRender(renderMethod);
|
3138
4380
|
}
|
3139
4381
|
preloadOnLoadLinksForView(element) {
|
3140
4382
|
this.preloader.preloadOnLoadLinksForView(element);
|
@@ -3187,14 +4429,19 @@ class Session {
|
|
3187
4429
|
}
|
3188
4430
|
notifyApplicationBeforeRender(newBody, options) {
|
3189
4431
|
return dispatch("turbo:before-render", {
|
3190
|
-
detail:
|
3191
|
-
newBody: newBody
|
3192
|
-
|
4432
|
+
detail: {
|
4433
|
+
newBody: newBody,
|
4434
|
+
...options
|
4435
|
+
},
|
3193
4436
|
cancelable: true
|
3194
4437
|
});
|
3195
4438
|
}
|
3196
|
-
notifyApplicationAfterRender() {
|
3197
|
-
return dispatch("turbo:render"
|
4439
|
+
notifyApplicationAfterRender(renderMethod) {
|
4440
|
+
return dispatch("turbo:render", {
|
4441
|
+
detail: {
|
4442
|
+
renderMethod: renderMethod
|
4443
|
+
}
|
4444
|
+
});
|
3198
4445
|
}
|
3199
4446
|
notifyApplicationAfterPageLoad(timing = {}) {
|
3200
4447
|
return dispatch("turbo:load", {
|
@@ -3225,11 +4472,11 @@ class Session {
|
|
3225
4472
|
});
|
3226
4473
|
}
|
3227
4474
|
submissionIsNavigatable(form, submitter) {
|
3228
|
-
if (
|
4475
|
+
if (config.forms.mode == "off") {
|
3229
4476
|
return false;
|
3230
4477
|
} else {
|
3231
4478
|
const submitterIsNavigatable = submitter ? this.elementIsNavigatable(submitter) : true;
|
3232
|
-
if (
|
4479
|
+
if (config.forms.mode == "optin") {
|
3233
4480
|
return submitterIsNavigatable && form.closest('[data-turbo="true"]') != null;
|
3234
4481
|
} else {
|
3235
4482
|
return submitterIsNavigatable && this.elementIsNavigatable(form);
|
@@ -3239,7 +4486,7 @@ class Session {
|
|
3239
4486
|
elementIsNavigatable(element) {
|
3240
4487
|
const container = findClosestRecursively(element, "[data-turbo]");
|
3241
4488
|
const withinFrame = findClosestRecursively(element, "turbo-frame");
|
3242
|
-
if (
|
4489
|
+
if (config.drive.enabled || withinFrame) {
|
3243
4490
|
if (container) {
|
3244
4491
|
return container.getAttribute("data-turbo") != "false";
|
3245
4492
|
} else {
|
@@ -3273,67 +4520,9 @@ const deprecatedLocationPropertyDescriptors = {
|
|
3273
4520
|
}
|
3274
4521
|
};
|
3275
4522
|
|
3276
|
-
|
3277
|
-
constructor(session) {
|
3278
|
-
this.session = session;
|
3279
|
-
}
|
3280
|
-
clear() {
|
3281
|
-
this.session.clearCache();
|
3282
|
-
}
|
3283
|
-
resetCacheControl() {
|
3284
|
-
this.setCacheControl("");
|
3285
|
-
}
|
3286
|
-
exemptPageFromCache() {
|
3287
|
-
this.setCacheControl("no-cache");
|
3288
|
-
}
|
3289
|
-
exemptPageFromPreview() {
|
3290
|
-
this.setCacheControl("no-preview");
|
3291
|
-
}
|
3292
|
-
setCacheControl(value) {
|
3293
|
-
setMetaContent("turbo-cache-control", value);
|
3294
|
-
}
|
3295
|
-
}
|
3296
|
-
|
3297
|
-
const StreamActions = {
|
3298
|
-
after() {
|
3299
|
-
this.targetElements.forEach((e => {
|
3300
|
-
var _a;
|
3301
|
-
return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e.nextSibling);
|
3302
|
-
}));
|
3303
|
-
},
|
3304
|
-
append() {
|
3305
|
-
this.removeDuplicateTargetChildren();
|
3306
|
-
this.targetElements.forEach((e => e.append(this.templateContent)));
|
3307
|
-
},
|
3308
|
-
before() {
|
3309
|
-
this.targetElements.forEach((e => {
|
3310
|
-
var _a;
|
3311
|
-
return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e);
|
3312
|
-
}));
|
3313
|
-
},
|
3314
|
-
prepend() {
|
3315
|
-
this.removeDuplicateTargetChildren();
|
3316
|
-
this.targetElements.forEach((e => e.prepend(this.templateContent)));
|
3317
|
-
},
|
3318
|
-
remove() {
|
3319
|
-
this.targetElements.forEach((e => e.remove()));
|
3320
|
-
},
|
3321
|
-
replace() {
|
3322
|
-
this.targetElements.forEach((e => e.replaceWith(this.templateContent)));
|
3323
|
-
},
|
3324
|
-
update() {
|
3325
|
-
this.targetElements.forEach((targetElement => {
|
3326
|
-
targetElement.innerHTML = "";
|
3327
|
-
targetElement.append(this.templateContent);
|
3328
|
-
}));
|
3329
|
-
}
|
3330
|
-
};
|
3331
|
-
|
3332
|
-
const session = new Session;
|
3333
|
-
|
3334
|
-
const cache = new Cache(session);
|
4523
|
+
const session = new Session(recentRequests);
|
3335
4524
|
|
3336
|
-
const {navigator: navigator$1} = session;
|
4525
|
+
const {cache: cache, navigator: navigator$1} = session;
|
3337
4526
|
|
3338
4527
|
function start() {
|
3339
4528
|
session.start();
|
@@ -3365,15 +4554,18 @@ function clearCache() {
|
|
3365
4554
|
}
|
3366
4555
|
|
3367
4556
|
function setProgressBarDelay(delay) {
|
3368
|
-
|
4557
|
+
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.`");
|
4558
|
+
config.drive.progressBarDelay = delay;
|
3369
4559
|
}
|
3370
4560
|
|
3371
4561
|
function setConfirmMethod(confirmMethod) {
|
3372
|
-
|
4562
|
+
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.`");
|
4563
|
+
config.forms.confirm = confirmMethod;
|
3373
4564
|
}
|
3374
4565
|
|
3375
4566
|
function setFormMode(mode) {
|
3376
|
-
|
4567
|
+
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.`");
|
4568
|
+
config.forms.mode = mode;
|
3377
4569
|
}
|
3378
4570
|
|
3379
4571
|
var Turbo = Object.freeze({
|
@@ -3384,6 +4576,8 @@ var Turbo = Object.freeze({
|
|
3384
4576
|
PageRenderer: PageRenderer,
|
3385
4577
|
PageSnapshot: PageSnapshot,
|
3386
4578
|
FrameRenderer: FrameRenderer,
|
4579
|
+
fetch: fetchWithTurboHeaders,
|
4580
|
+
config: config,
|
3387
4581
|
start: start,
|
3388
4582
|
registerAdapter: registerAdapter,
|
3389
4583
|
visit: visit,
|
@@ -3393,28 +4587,21 @@ var Turbo = Object.freeze({
|
|
3393
4587
|
clearCache: clearCache,
|
3394
4588
|
setProgressBarDelay: setProgressBarDelay,
|
3395
4589
|
setConfirmMethod: setConfirmMethod,
|
3396
|
-
setFormMode: setFormMode
|
3397
|
-
StreamActions: StreamActions
|
4590
|
+
setFormMode: setFormMode
|
3398
4591
|
});
|
3399
4592
|
|
3400
4593
|
class TurboFrameMissingError extends Error {}
|
3401
4594
|
|
3402
4595
|
class FrameController {
|
4596
|
+
fetchResponseLoaded=_fetchResponse => Promise.resolve();
|
4597
|
+
#currentFetchRequest=null;
|
4598
|
+
#resolveVisitPromise=() => {};
|
4599
|
+
#connected=false;
|
4600
|
+
#hasBeenLoaded=false;
|
4601
|
+
#ignoredAttributes=new Set;
|
4602
|
+
#shouldMorphFrame=false;
|
4603
|
+
action=null;
|
3403
4604
|
constructor(element) {
|
3404
|
-
this.fetchResponseLoaded = _fetchResponse => {};
|
3405
|
-
this.currentFetchRequest = null;
|
3406
|
-
this.resolveVisitPromise = () => {};
|
3407
|
-
this.connected = false;
|
3408
|
-
this.hasBeenLoaded = false;
|
3409
|
-
this.ignoredAttributes = new Set;
|
3410
|
-
this.action = null;
|
3411
|
-
this.visitCachedSnapshot = ({element: element}) => {
|
3412
|
-
const frame = element.querySelector("#" + this.element.id);
|
3413
|
-
if (frame && this.previousFrameElement) {
|
3414
|
-
frame.replaceChildren(...this.previousFrameElement.children);
|
3415
|
-
}
|
3416
|
-
delete this.previousFrameElement;
|
3417
|
-
};
|
3418
4605
|
this.element = element;
|
3419
4606
|
this.view = new FrameView(this, this.element);
|
3420
4607
|
this.appearanceObserver = new AppearanceObserver(this, this.element);
|
@@ -3424,12 +4611,12 @@ class FrameController {
|
|
3424
4611
|
this.formSubmitObserver = new FormSubmitObserver(this, this.element);
|
3425
4612
|
}
|
3426
4613
|
connect() {
|
3427
|
-
if (!this
|
3428
|
-
this
|
4614
|
+
if (!this.#connected) {
|
4615
|
+
this.#connected = true;
|
3429
4616
|
if (this.loadingStyle == FrameLoadingStyle.lazy) {
|
3430
4617
|
this.appearanceObserver.start();
|
3431
4618
|
} else {
|
3432
|
-
this
|
4619
|
+
this.#loadSourceURL();
|
3433
4620
|
}
|
3434
4621
|
this.formLinkClickObserver.start();
|
3435
4622
|
this.linkInterceptor.start();
|
@@ -3437,8 +4624,8 @@ class FrameController {
|
|
3437
4624
|
}
|
3438
4625
|
}
|
3439
4626
|
disconnect() {
|
3440
|
-
if (this
|
3441
|
-
this
|
4627
|
+
if (this.#connected) {
|
4628
|
+
this.#connected = false;
|
3442
4629
|
this.appearanceObserver.stop();
|
3443
4630
|
this.formLinkClickObserver.stop();
|
3444
4631
|
this.linkInterceptor.stop();
|
@@ -3447,45 +4634,40 @@ class FrameController {
|
|
3447
4634
|
}
|
3448
4635
|
disabledChanged() {
|
3449
4636
|
if (this.loadingStyle == FrameLoadingStyle.eager) {
|
3450
|
-
this
|
4637
|
+
this.#loadSourceURL();
|
3451
4638
|
}
|
3452
4639
|
}
|
3453
4640
|
sourceURLChanged() {
|
3454
|
-
if (this
|
4641
|
+
if (this.#isIgnoringChangesTo("src")) return;
|
3455
4642
|
if (this.element.isConnected) {
|
3456
4643
|
this.complete = false;
|
3457
4644
|
}
|
3458
|
-
if (this.loadingStyle == FrameLoadingStyle.eager || this
|
3459
|
-
this
|
4645
|
+
if (this.loadingStyle == FrameLoadingStyle.eager || this.#hasBeenLoaded) {
|
4646
|
+
this.#loadSourceURL();
|
3460
4647
|
}
|
3461
4648
|
}
|
3462
4649
|
sourceURLReloaded() {
|
3463
|
-
const {src: src} = this.element;
|
3464
|
-
this
|
3465
|
-
|
3466
|
-
}));
|
4650
|
+
const {refresh: refresh, src: src} = this.element;
|
4651
|
+
this.#shouldMorphFrame = src && refresh === "morph";
|
4652
|
+
this.element.removeAttribute("complete");
|
3467
4653
|
this.element.src = null;
|
3468
4654
|
this.element.src = src;
|
3469
4655
|
return this.element.loaded;
|
3470
4656
|
}
|
3471
|
-
completeChanged() {
|
3472
|
-
if (this.isIgnoringChangesTo("complete")) return;
|
3473
|
-
this.loadSourceURL();
|
3474
|
-
}
|
3475
4657
|
loadingStyleChanged() {
|
3476
4658
|
if (this.loadingStyle == FrameLoadingStyle.lazy) {
|
3477
4659
|
this.appearanceObserver.start();
|
3478
4660
|
} else {
|
3479
4661
|
this.appearanceObserver.stop();
|
3480
|
-
this
|
4662
|
+
this.#loadSourceURL();
|
3481
4663
|
}
|
3482
4664
|
}
|
3483
|
-
async loadSourceURL() {
|
4665
|
+
async #loadSourceURL() {
|
3484
4666
|
if (this.enabled && this.isActive && !this.complete && this.sourceURL) {
|
3485
|
-
this.element.loaded = this
|
4667
|
+
this.element.loaded = this.#visit(expandURL(this.sourceURL));
|
3486
4668
|
this.appearanceObserver.stop();
|
3487
4669
|
await this.element.loaded;
|
3488
|
-
this
|
4670
|
+
this.#hasBeenLoaded = true;
|
3489
4671
|
}
|
3490
4672
|
}
|
3491
4673
|
async loadResponse(fetchResponse) {
|
@@ -3498,34 +4680,35 @@ class FrameController {
|
|
3498
4680
|
const document = parseHTMLDocument(html);
|
3499
4681
|
const pageSnapshot = PageSnapshot.fromDocument(document);
|
3500
4682
|
if (pageSnapshot.isVisitable) {
|
3501
|
-
await this
|
4683
|
+
await this.#loadFrameResponse(fetchResponse, document);
|
3502
4684
|
} else {
|
3503
|
-
await this
|
4685
|
+
await this.#handleUnvisitableFrameResponse(fetchResponse);
|
3504
4686
|
}
|
3505
4687
|
}
|
3506
4688
|
} finally {
|
3507
|
-
this
|
4689
|
+
this.#shouldMorphFrame = false;
|
4690
|
+
this.fetchResponseLoaded = () => Promise.resolve();
|
3508
4691
|
}
|
3509
4692
|
}
|
3510
4693
|
elementAppearedInViewport(element) {
|
3511
|
-
this.proposeVisitIfNavigatedWithAction(element, element);
|
3512
|
-
this
|
4694
|
+
this.proposeVisitIfNavigatedWithAction(element, getVisitAction(element));
|
4695
|
+
this.#loadSourceURL();
|
3513
4696
|
}
|
3514
4697
|
willSubmitFormLinkToLocation(link) {
|
3515
|
-
return this
|
4698
|
+
return this.#shouldInterceptNavigation(link);
|
3516
4699
|
}
|
3517
4700
|
submittedFormLinkToLocation(link, _location, form) {
|
3518
|
-
const frame = this
|
4701
|
+
const frame = this.#findFrameElement(link);
|
3519
4702
|
if (frame) form.setAttribute("data-turbo-frame", frame.id);
|
3520
4703
|
}
|
3521
4704
|
shouldInterceptLinkClick(element, _location, _event) {
|
3522
|
-
return this
|
4705
|
+
return this.#shouldInterceptNavigation(element);
|
3523
4706
|
}
|
3524
4707
|
linkClickIntercepted(element, location) {
|
3525
|
-
this
|
4708
|
+
this.#navigateFrame(element, location);
|
3526
4709
|
}
|
3527
4710
|
willSubmitForm(element, submitter) {
|
3528
|
-
return element.closest("turbo-frame") == this.element && this
|
4711
|
+
return element.closest("turbo-frame") == this.element && this.#shouldInterceptNavigation(element, submitter);
|
3529
4712
|
}
|
3530
4713
|
formSubmitted(element, submitter) {
|
3531
4714
|
if (this.formSubmission) {
|
@@ -3537,9 +4720,8 @@ class FrameController {
|
|
3537
4720
|
this.formSubmission.start();
|
3538
4721
|
}
|
3539
4722
|
prepareRequest(request) {
|
3540
|
-
var _a;
|
3541
4723
|
request.headers["Turbo-Frame"] = this.id;
|
3542
|
-
if (
|
4724
|
+
if (this.currentNavigationElement?.hasAttribute("data-turbo-stream")) {
|
3543
4725
|
request.acceptResponseType(StreamMessage.contentType);
|
3544
4726
|
}
|
3545
4727
|
}
|
@@ -3547,29 +4729,29 @@ class FrameController {
|
|
3547
4729
|
markAsBusy(this.element);
|
3548
4730
|
}
|
3549
4731
|
requestPreventedHandlingResponse(_request, _response) {
|
3550
|
-
this
|
4732
|
+
this.#resolveVisitPromise();
|
3551
4733
|
}
|
3552
4734
|
async requestSucceededWithResponse(request, response) {
|
3553
4735
|
await this.loadResponse(response);
|
3554
|
-
this
|
4736
|
+
this.#resolveVisitPromise();
|
3555
4737
|
}
|
3556
4738
|
async requestFailedWithResponse(request, response) {
|
3557
4739
|
await this.loadResponse(response);
|
3558
|
-
this
|
4740
|
+
this.#resolveVisitPromise();
|
3559
4741
|
}
|
3560
4742
|
requestErrored(request, error) {
|
3561
4743
|
console.error(error);
|
3562
|
-
this
|
4744
|
+
this.#resolveVisitPromise();
|
3563
4745
|
}
|
3564
4746
|
requestFinished(_request) {
|
3565
4747
|
clearBusyState(this.element);
|
3566
4748
|
}
|
3567
4749
|
formSubmissionStarted({formElement: formElement}) {
|
3568
|
-
markAsBusy(formElement, this
|
4750
|
+
markAsBusy(formElement, this.#findFrameElement(formElement));
|
3569
4751
|
}
|
3570
4752
|
formSubmissionSucceededWithResponse(formSubmission, response) {
|
3571
|
-
const frame = this
|
3572
|
-
frame.delegate.proposeVisitIfNavigatedWithAction(frame, formSubmission.
|
4753
|
+
const frame = this.#findFrameElement(formSubmission.formElement, formSubmission.submitter);
|
4754
|
+
frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(formSubmission.submitter, formSubmission.formElement, frame));
|
3573
4755
|
frame.delegate.loadResponse(response);
|
3574
4756
|
if (!formSubmission.isSafe) {
|
3575
4757
|
session.clearCache();
|
@@ -3583,14 +4765,15 @@ class FrameController {
|
|
3583
4765
|
console.error(error);
|
3584
4766
|
}
|
3585
4767
|
formSubmissionFinished({formElement: formElement}) {
|
3586
|
-
clearBusyState(formElement, this
|
4768
|
+
clearBusyState(formElement, this.#findFrameElement(formElement));
|
3587
4769
|
}
|
3588
4770
|
allowsImmediateRender({element: newFrame}, options) {
|
3589
4771
|
const event = dispatch("turbo:before-frame-render", {
|
3590
4772
|
target: this.element,
|
3591
|
-
detail:
|
3592
|
-
newFrame: newFrame
|
3593
|
-
|
4773
|
+
detail: {
|
4774
|
+
newFrame: newFrame,
|
4775
|
+
...options
|
4776
|
+
},
|
3594
4777
|
cancelable: true
|
3595
4778
|
});
|
3596
4779
|
const {defaultPrevented: defaultPrevented, detail: {render: render}} = event;
|
@@ -3599,7 +4782,7 @@ class FrameController {
|
|
3599
4782
|
}
|
3600
4783
|
return !defaultPrevented;
|
3601
4784
|
}
|
3602
|
-
viewRenderedSnapshot(_snapshot, _isPreview) {}
|
4785
|
+
viewRenderedSnapshot(_snapshot, _isPreview, _renderMethod) {}
|
3603
4786
|
preloadOnLoadLinksForView(element) {
|
3604
4787
|
session.preloadOnLoadLinksForView(element);
|
3605
4788
|
}
|
@@ -3607,52 +4790,59 @@ class FrameController {
|
|
3607
4790
|
willRenderFrame(currentElement, _newElement) {
|
3608
4791
|
this.previousFrameElement = currentElement.cloneNode(true);
|
3609
4792
|
}
|
3610
|
-
|
4793
|
+
visitCachedSnapshot=({element: element}) => {
|
4794
|
+
const frame = element.querySelector("#" + this.element.id);
|
4795
|
+
if (frame && this.previousFrameElement) {
|
4796
|
+
frame.replaceChildren(...this.previousFrameElement.children);
|
4797
|
+
}
|
4798
|
+
delete this.previousFrameElement;
|
4799
|
+
};
|
4800
|
+
async #loadFrameResponse(fetchResponse, document) {
|
3611
4801
|
const newFrameElement = await this.extractForeignFrameElement(document.body);
|
4802
|
+
const rendererClass = this.#shouldMorphFrame ? MorphingFrameRenderer : FrameRenderer;
|
3612
4803
|
if (newFrameElement) {
|
3613
4804
|
const snapshot = new Snapshot(newFrameElement);
|
3614
|
-
const renderer = new
|
4805
|
+
const renderer = new rendererClass(this, this.view.snapshot, snapshot, false, false);
|
3615
4806
|
if (this.view.renderPromise) await this.view.renderPromise;
|
3616
4807
|
this.changeHistory();
|
3617
4808
|
await this.view.render(renderer);
|
3618
4809
|
this.complete = true;
|
3619
4810
|
session.frameRendered(fetchResponse, this.element);
|
3620
4811
|
session.frameLoaded(this.element);
|
3621
|
-
this.fetchResponseLoaded(fetchResponse);
|
3622
|
-
} else if (this
|
3623
|
-
this
|
4812
|
+
await this.fetchResponseLoaded(fetchResponse);
|
4813
|
+
} else if (this.#willHandleFrameMissingFromResponse(fetchResponse)) {
|
4814
|
+
this.#handleFrameMissingFromResponse(fetchResponse);
|
3624
4815
|
}
|
3625
4816
|
}
|
3626
|
-
async visit(url) {
|
3627
|
-
var _a;
|
4817
|
+
async #visit(url) {
|
3628
4818
|
const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams, this.element);
|
3629
|
-
|
3630
|
-
this
|
4819
|
+
this.#currentFetchRequest?.cancel();
|
4820
|
+
this.#currentFetchRequest = request;
|
3631
4821
|
return new Promise((resolve => {
|
3632
|
-
this
|
3633
|
-
this
|
3634
|
-
this
|
4822
|
+
this.#resolveVisitPromise = () => {
|
4823
|
+
this.#resolveVisitPromise = () => {};
|
4824
|
+
this.#currentFetchRequest = null;
|
3635
4825
|
resolve();
|
3636
4826
|
};
|
3637
4827
|
request.perform();
|
3638
4828
|
}));
|
3639
4829
|
}
|
3640
|
-
navigateFrame(element, url, submitter) {
|
3641
|
-
const frame = this
|
3642
|
-
frame.delegate.proposeVisitIfNavigatedWithAction(frame, element,
|
3643
|
-
this
|
4830
|
+
#navigateFrame(element, url, submitter) {
|
4831
|
+
const frame = this.#findFrameElement(element, submitter);
|
4832
|
+
frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(submitter, element, frame));
|
4833
|
+
this.#withCurrentNavigationElement(element, (() => {
|
3644
4834
|
frame.src = url;
|
3645
4835
|
}));
|
3646
4836
|
}
|
3647
|
-
proposeVisitIfNavigatedWithAction(frame,
|
3648
|
-
this.action =
|
4837
|
+
proposeVisitIfNavigatedWithAction(frame, action = null) {
|
4838
|
+
this.action = action;
|
3649
4839
|
if (this.action) {
|
3650
4840
|
const pageSnapshot = PageSnapshot.fromElement(frame).clone();
|
3651
4841
|
const {visitCachedSnapshot: visitCachedSnapshot} = frame.delegate;
|
3652
|
-
frame.delegate.fetchResponseLoaded = fetchResponse => {
|
4842
|
+
frame.delegate.fetchResponseLoaded = async fetchResponse => {
|
3653
4843
|
if (frame.src) {
|
3654
4844
|
const {statusCode: statusCode, redirected: redirected} = fetchResponse;
|
3655
|
-
const responseHTML =
|
4845
|
+
const responseHTML = await fetchResponse.responseHTML;
|
3656
4846
|
const response = {
|
3657
4847
|
statusCode: statusCode,
|
3658
4848
|
redirected: redirected,
|
@@ -3678,16 +4868,16 @@ class FrameController {
|
|
3678
4868
|
session.history.update(method, expandURL(this.element.src || ""), this.restorationIdentifier);
|
3679
4869
|
}
|
3680
4870
|
}
|
3681
|
-
async handleUnvisitableFrameResponse(fetchResponse) {
|
4871
|
+
async #handleUnvisitableFrameResponse(fetchResponse) {
|
3682
4872
|
console.warn(`The response (${fetchResponse.statusCode}) from <turbo-frame id="${this.element.id}"> is performing a full page visit due to turbo-visit-control.`);
|
3683
|
-
await this
|
4873
|
+
await this.#visitResponse(fetchResponse.response);
|
3684
4874
|
}
|
3685
|
-
willHandleFrameMissingFromResponse(fetchResponse) {
|
4875
|
+
#willHandleFrameMissingFromResponse(fetchResponse) {
|
3686
4876
|
this.element.setAttribute("complete", "");
|
3687
4877
|
const response = fetchResponse.response;
|
3688
|
-
const visit = async (url, options
|
4878
|
+
const visit = async (url, options) => {
|
3689
4879
|
if (url instanceof Response) {
|
3690
|
-
this
|
4880
|
+
this.#visitResponse(url);
|
3691
4881
|
} else {
|
3692
4882
|
session.visit(url, options);
|
3693
4883
|
}
|
@@ -3702,15 +4892,15 @@ class FrameController {
|
|
3702
4892
|
});
|
3703
4893
|
return !event.defaultPrevented;
|
3704
4894
|
}
|
3705
|
-
handleFrameMissingFromResponse(fetchResponse) {
|
4895
|
+
#handleFrameMissingFromResponse(fetchResponse) {
|
3706
4896
|
this.view.missing();
|
3707
|
-
this
|
4897
|
+
this.#throwFrameMissingError(fetchResponse);
|
3708
4898
|
}
|
3709
|
-
throwFrameMissingError(fetchResponse) {
|
4899
|
+
#throwFrameMissingError(fetchResponse) {
|
3710
4900
|
const message = `The response (${fetchResponse.statusCode}) did not contain the expected <turbo-frame id="${this.element.id}"> and will be ignored. To perform a full page visit instead, set turbo-visit-control to reload.`;
|
3711
4901
|
throw new TurboFrameMissingError(message);
|
3712
4902
|
}
|
3713
|
-
async visitResponse(response) {
|
4903
|
+
async #visitResponse(response) {
|
3714
4904
|
const wrapped = new FetchResponse(response);
|
3715
4905
|
const responseHTML = await wrapped.responseHTML;
|
3716
4906
|
const {location: location, redirected: redirected, statusCode: statusCode} = wrapped;
|
@@ -3722,10 +4912,9 @@ class FrameController {
|
|
3722
4912
|
}
|
3723
4913
|
});
|
3724
4914
|
}
|
3725
|
-
findFrameElement(element, submitter) {
|
3726
|
-
var _a;
|
4915
|
+
#findFrameElement(element, submitter) {
|
3727
4916
|
const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
|
3728
|
-
return
|
4917
|
+
return getFrameElementById(id) ?? this.element;
|
3729
4918
|
}
|
3730
4919
|
async extractForeignFrameElement(container) {
|
3731
4920
|
let element;
|
@@ -3746,13 +4935,13 @@ class FrameController {
|
|
3746
4935
|
}
|
3747
4936
|
return null;
|
3748
4937
|
}
|
3749
|
-
formActionIsVisitable(form, submitter) {
|
3750
|
-
const action = getAction(form, submitter);
|
4938
|
+
#formActionIsVisitable(form, submitter) {
|
4939
|
+
const action = getAction$1(form, submitter);
|
3751
4940
|
return locationIsVisitable(expandURL(action), this.rootLocation);
|
3752
4941
|
}
|
3753
|
-
shouldInterceptNavigation(element, submitter) {
|
4942
|
+
#shouldInterceptNavigation(element, submitter) {
|
3754
4943
|
const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
|
3755
|
-
if (element instanceof HTMLFormElement && !this
|
4944
|
+
if (element instanceof HTMLFormElement && !this.#formActionIsVisitable(element, submitter)) {
|
3756
4945
|
return false;
|
3757
4946
|
}
|
3758
4947
|
if (!this.enabled || id == "_top") {
|
@@ -3784,46 +4973,43 @@ class FrameController {
|
|
3784
4973
|
}
|
3785
4974
|
}
|
3786
4975
|
set sourceURL(sourceURL) {
|
3787
|
-
this
|
3788
|
-
this.element.src = sourceURL
|
4976
|
+
this.#ignoringChangesToAttribute("src", (() => {
|
4977
|
+
this.element.src = sourceURL ?? null;
|
3789
4978
|
}));
|
3790
4979
|
}
|
3791
4980
|
get loadingStyle() {
|
3792
4981
|
return this.element.loading;
|
3793
4982
|
}
|
3794
4983
|
get isLoading() {
|
3795
|
-
return this.formSubmission !== undefined || this
|
4984
|
+
return this.formSubmission !== undefined || this.#resolveVisitPromise() !== undefined;
|
3796
4985
|
}
|
3797
4986
|
get complete() {
|
3798
4987
|
return this.element.hasAttribute("complete");
|
3799
4988
|
}
|
3800
4989
|
set complete(value) {
|
3801
|
-
|
3802
|
-
|
3803
|
-
|
3804
|
-
|
3805
|
-
|
3806
|
-
}
|
3807
|
-
}));
|
4990
|
+
if (value) {
|
4991
|
+
this.element.setAttribute("complete", "");
|
4992
|
+
} else {
|
4993
|
+
this.element.removeAttribute("complete");
|
4994
|
+
}
|
3808
4995
|
}
|
3809
4996
|
get isActive() {
|
3810
|
-
return this.element.isActive && this
|
4997
|
+
return this.element.isActive && this.#connected;
|
3811
4998
|
}
|
3812
4999
|
get rootLocation() {
|
3813
|
-
var _a;
|
3814
5000
|
const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
|
3815
|
-
const root =
|
5001
|
+
const root = meta?.content ?? "/";
|
3816
5002
|
return expandURL(root);
|
3817
5003
|
}
|
3818
|
-
isIgnoringChangesTo(attributeName) {
|
3819
|
-
return this
|
5004
|
+
#isIgnoringChangesTo(attributeName) {
|
5005
|
+
return this.#ignoredAttributes.has(attributeName);
|
3820
5006
|
}
|
3821
|
-
ignoringChangesToAttribute(attributeName, callback) {
|
3822
|
-
this
|
5007
|
+
#ignoringChangesToAttribute(attributeName, callback) {
|
5008
|
+
this.#ignoredAttributes.add(attributeName);
|
3823
5009
|
callback();
|
3824
|
-
this
|
5010
|
+
this.#ignoredAttributes.delete(attributeName);
|
3825
5011
|
}
|
3826
|
-
withCurrentNavigationElement(element, callback) {
|
5012
|
+
#withCurrentNavigationElement(element, callback) {
|
3827
5013
|
this.currentNavigationElement = element;
|
3828
5014
|
callback();
|
3829
5015
|
delete this.currentNavigationElement;
|
@@ -3856,6 +5042,50 @@ function activateElement(element, currentURL) {
|
|
3856
5042
|
}
|
3857
5043
|
}
|
3858
5044
|
|
5045
|
+
const StreamActions = {
|
5046
|
+
after() {
|
5047
|
+
this.targetElements.forEach((e => e.parentElement?.insertBefore(this.templateContent, e.nextSibling)));
|
5048
|
+
},
|
5049
|
+
append() {
|
5050
|
+
this.removeDuplicateTargetChildren();
|
5051
|
+
this.targetElements.forEach((e => e.append(this.templateContent)));
|
5052
|
+
},
|
5053
|
+
before() {
|
5054
|
+
this.targetElements.forEach((e => e.parentElement?.insertBefore(this.templateContent, e)));
|
5055
|
+
},
|
5056
|
+
prepend() {
|
5057
|
+
this.removeDuplicateTargetChildren();
|
5058
|
+
this.targetElements.forEach((e => e.prepend(this.templateContent)));
|
5059
|
+
},
|
5060
|
+
remove() {
|
5061
|
+
this.targetElements.forEach((e => e.remove()));
|
5062
|
+
},
|
5063
|
+
replace() {
|
5064
|
+
const method = this.getAttribute("method");
|
5065
|
+
this.targetElements.forEach((targetElement => {
|
5066
|
+
if (method === "morph") {
|
5067
|
+
morphElements(targetElement, this.templateContent);
|
5068
|
+
} else {
|
5069
|
+
targetElement.replaceWith(this.templateContent);
|
5070
|
+
}
|
5071
|
+
}));
|
5072
|
+
},
|
5073
|
+
update() {
|
5074
|
+
const method = this.getAttribute("method");
|
5075
|
+
this.targetElements.forEach((targetElement => {
|
5076
|
+
if (method === "morph") {
|
5077
|
+
morphChildren(targetElement, this.templateContent);
|
5078
|
+
} else {
|
5079
|
+
targetElement.innerHTML = "";
|
5080
|
+
targetElement.append(this.templateContent);
|
5081
|
+
}
|
5082
|
+
}));
|
5083
|
+
},
|
5084
|
+
refresh() {
|
5085
|
+
session.refresh(this.baseURI, this.requestId);
|
5086
|
+
}
|
5087
|
+
};
|
5088
|
+
|
3859
5089
|
class StreamElement extends HTMLElement {
|
3860
5090
|
static async renderElement(newElement) {
|
3861
5091
|
await newElement.performAction();
|
@@ -3870,11 +5100,10 @@ class StreamElement extends HTMLElement {
|
|
3870
5100
|
}
|
3871
5101
|
}
|
3872
5102
|
async render() {
|
3873
|
-
|
3874
|
-
return (_a = this.renderPromise) !== null && _a !== void 0 ? _a : this.renderPromise = (async () => {
|
5103
|
+
return this.renderPromise ??= (async () => {
|
3875
5104
|
const event = this.beforeRenderEvent;
|
3876
5105
|
if (this.dispatchEvent(event)) {
|
3877
|
-
await
|
5106
|
+
await nextRepaint();
|
3878
5107
|
await event.detail.render(this);
|
3879
5108
|
}
|
3880
5109
|
})();
|
@@ -3882,15 +5111,14 @@ class StreamElement extends HTMLElement {
|
|
3882
5111
|
disconnect() {
|
3883
5112
|
try {
|
3884
5113
|
this.remove();
|
3885
|
-
} catch
|
5114
|
+
} catch {}
|
3886
5115
|
}
|
3887
5116
|
removeDuplicateTargetChildren() {
|
3888
5117
|
this.duplicateChildren.forEach((c => c.remove()));
|
3889
5118
|
}
|
3890
5119
|
get duplicateChildren() {
|
3891
|
-
var _a;
|
3892
5120
|
const existingChildren = this.targetElements.flatMap((e => [ ...e.children ])).filter((c => !!c.id));
|
3893
|
-
const newChildrenIds = [ ...
|
5121
|
+
const newChildrenIds = [ ...this.templateContent?.children || [] ].filter((c => !!c.id)).map((c => c.id));
|
3894
5122
|
return existingChildren.filter((c => newChildrenIds.includes(c.id)));
|
3895
5123
|
}
|
3896
5124
|
get performAction() {
|
@@ -3899,9 +5127,9 @@ class StreamElement extends HTMLElement {
|
|
3899
5127
|
if (actionFunction) {
|
3900
5128
|
return actionFunction;
|
3901
5129
|
}
|
3902
|
-
this
|
5130
|
+
this.#raise("unknown action");
|
3903
5131
|
}
|
3904
|
-
this
|
5132
|
+
this.#raise("action attribute is missing");
|
3905
5133
|
}
|
3906
5134
|
get targetElements() {
|
3907
5135
|
if (this.target) {
|
@@ -3909,7 +5137,7 @@ class StreamElement extends HTMLElement {
|
|
3909
5137
|
} else if (this.targets) {
|
3910
5138
|
return this.targetElementsByQuery;
|
3911
5139
|
} else {
|
3912
|
-
this
|
5140
|
+
this.#raise("target or targets attribute is missing");
|
3913
5141
|
}
|
3914
5142
|
}
|
3915
5143
|
get templateContent() {
|
@@ -3923,7 +5151,7 @@ class StreamElement extends HTMLElement {
|
|
3923
5151
|
} else if (this.firstElementChild instanceof HTMLTemplateElement) {
|
3924
5152
|
return this.firstElementChild;
|
3925
5153
|
}
|
3926
|
-
this
|
5154
|
+
this.#raise("first child element must be a <template> element");
|
3927
5155
|
}
|
3928
5156
|
get action() {
|
3929
5157
|
return this.getAttribute("action");
|
@@ -3934,12 +5162,14 @@ class StreamElement extends HTMLElement {
|
|
3934
5162
|
get targets() {
|
3935
5163
|
return this.getAttribute("targets");
|
3936
5164
|
}
|
3937
|
-
|
5165
|
+
get requestId() {
|
5166
|
+
return this.getAttribute("request-id");
|
5167
|
+
}
|
5168
|
+
#raise(message) {
|
3938
5169
|
throw new Error(`${this.description}: ${message}`);
|
3939
5170
|
}
|
3940
5171
|
get description() {
|
3941
|
-
|
3942
|
-
return (_b = ((_a = this.outerHTML.match(/<[^>]+>/)) !== null && _a !== void 0 ? _a : [])[0]) !== null && _b !== void 0 ? _b : "<turbo-stream>";
|
5172
|
+
return (this.outerHTML.match(/<[^>]+>/) ?? [])[0] ?? "<turbo-stream>";
|
3943
5173
|
}
|
3944
5174
|
get beforeRenderEvent() {
|
3945
5175
|
return new CustomEvent("turbo:before-stream-render", {
|
@@ -3952,8 +5182,7 @@ class StreamElement extends HTMLElement {
|
|
3952
5182
|
});
|
3953
5183
|
}
|
3954
5184
|
get targetElementsById() {
|
3955
|
-
|
3956
|
-
const element = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.getElementById(this.target);
|
5185
|
+
const element = this.ownerDocument?.getElementById(this.target);
|
3957
5186
|
if (element !== null) {
|
3958
5187
|
return [ element ];
|
3959
5188
|
} else {
|
@@ -3961,8 +5190,7 @@ class StreamElement extends HTMLElement {
|
|
3961
5190
|
}
|
3962
5191
|
}
|
3963
5192
|
get targetElementsByQuery() {
|
3964
|
-
|
3965
|
-
const elements = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.querySelectorAll(this.targets);
|
5193
|
+
const elements = this.ownerDocument?.querySelectorAll(this.targets);
|
3966
5194
|
if (elements.length !== 0) {
|
3967
5195
|
return Array.prototype.slice.call(elements);
|
3968
5196
|
} else {
|
@@ -3972,16 +5200,14 @@ class StreamElement extends HTMLElement {
|
|
3972
5200
|
}
|
3973
5201
|
|
3974
5202
|
class StreamSourceElement extends HTMLElement {
|
3975
|
-
|
3976
|
-
super(...arguments);
|
3977
|
-
this.streamSource = null;
|
3978
|
-
}
|
5203
|
+
streamSource=null;
|
3979
5204
|
connectedCallback() {
|
3980
5205
|
this.streamSource = this.src.match(/^ws{1,2}:/) ? new WebSocket(this.src) : new EventSource(this.src);
|
3981
5206
|
connectStreamSource(this.streamSource);
|
3982
5207
|
}
|
3983
5208
|
disconnectedCallback() {
|
3984
5209
|
if (this.streamSource) {
|
5210
|
+
this.streamSource.close();
|
3985
5211
|
disconnectStreamSource(this.streamSource);
|
3986
5212
|
}
|
3987
5213
|
}
|
@@ -4026,16 +5252,21 @@ if (customElements.get("turbo-stream-source") === undefined) {
|
|
4026
5252
|
}
|
4027
5253
|
})();
|
4028
5254
|
|
4029
|
-
window.Turbo =
|
5255
|
+
window.Turbo = {
|
5256
|
+
...Turbo,
|
5257
|
+
StreamActions: StreamActions
|
5258
|
+
};
|
4030
5259
|
|
4031
5260
|
start();
|
4032
5261
|
|
4033
|
-
var
|
5262
|
+
var Turbo$1 = Object.freeze({
|
4034
5263
|
__proto__: null,
|
5264
|
+
FetchEnctype: FetchEnctype,
|
5265
|
+
FetchMethod: FetchMethod,
|
5266
|
+
FetchRequest: FetchRequest,
|
5267
|
+
FetchResponse: FetchResponse,
|
4035
5268
|
FrameElement: FrameElement,
|
4036
|
-
|
4037
|
-
return FrameLoadingStyle;
|
4038
|
-
},
|
5269
|
+
FrameLoadingStyle: FrameLoadingStyle,
|
4039
5270
|
FrameRenderer: FrameRenderer,
|
4040
5271
|
PageRenderer: PageRenderer,
|
4041
5272
|
PageSnapshot: PageSnapshot,
|
@@ -4044,8 +5275,13 @@ var turbo_es2017Esm = Object.freeze({
|
|
4044
5275
|
StreamSourceElement: StreamSourceElement,
|
4045
5276
|
cache: cache,
|
4046
5277
|
clearCache: clearCache,
|
5278
|
+
config: config,
|
4047
5279
|
connectStreamSource: connectStreamSource,
|
4048
5280
|
disconnectStreamSource: disconnectStreamSource,
|
5281
|
+
fetch: fetchWithTurboHeaders,
|
5282
|
+
fetchEnctypeFromString: fetchEnctypeFromString,
|
5283
|
+
fetchMethodFromString: fetchMethodFromString,
|
5284
|
+
isSafe: isSafe,
|
4049
5285
|
navigator: navigator$1,
|
4050
5286
|
registerAdapter: registerAdapter,
|
4051
5287
|
renderStreamMessage: renderStreamMessage,
|
@@ -4060,14 +5296,14 @@ var turbo_es2017Esm = Object.freeze({
|
|
4060
5296
|
let consumer;
|
4061
5297
|
|
4062
5298
|
async function getConsumer() {
|
4063
|
-
return consumer || setConsumer(createConsumer().then(setConsumer));
|
5299
|
+
return consumer || setConsumer(createConsumer$1().then(setConsumer));
|
4064
5300
|
}
|
4065
5301
|
|
4066
5302
|
function setConsumer(newConsumer) {
|
4067
5303
|
return consumer = newConsumer;
|
4068
5304
|
}
|
4069
5305
|
|
4070
|
-
async function createConsumer() {
|
5306
|
+
async function createConsumer$1() {
|
4071
5307
|
const {createConsumer: createConsumer} = await Promise.resolve().then((function() {
|
4072
5308
|
return index;
|
4073
5309
|
}));
|
@@ -4083,7 +5319,7 @@ var cable = Object.freeze({
|
|
4083
5319
|
__proto__: null,
|
4084
5320
|
getConsumer: getConsumer,
|
4085
5321
|
setConsumer: setConsumer,
|
4086
|
-
createConsumer: createConsumer,
|
5322
|
+
createConsumer: createConsumer$1,
|
4087
5323
|
subscribeTo: subscribeTo
|
4088
5324
|
});
|
4089
5325
|
|
@@ -4101,6 +5337,7 @@ function walk(obj) {
|
|
4101
5337
|
}
|
4102
5338
|
|
4103
5339
|
class TurboCableStreamSourceElement extends HTMLElement {
|
5340
|
+
static observedAttributes=[ "channel", "signed-stream-name" ];
|
4104
5341
|
async connectedCallback() {
|
4105
5342
|
connectStreamSource(this);
|
4106
5343
|
this.subscription = await subscribeTo(this.channel, {
|
@@ -4112,6 +5349,13 @@ class TurboCableStreamSourceElement extends HTMLElement {
|
|
4112
5349
|
disconnectedCallback() {
|
4113
5350
|
disconnectStreamSource(this);
|
4114
5351
|
if (this.subscription) this.subscription.unsubscribe();
|
5352
|
+
this.subscriptionDisconnected();
|
5353
|
+
}
|
5354
|
+
attributeChangedCallback() {
|
5355
|
+
if (this.subscription) {
|
5356
|
+
this.disconnectedCallback();
|
5357
|
+
this.connectedCallback();
|
5358
|
+
}
|
4115
5359
|
}
|
4116
5360
|
dispatchMessageEvent(data) {
|
4117
5361
|
const event = new MessageEvent("message", {
|
@@ -4193,6 +5437,8 @@ function isBodyInit(body) {
|
|
4193
5437
|
return body instanceof FormData || body instanceof URLSearchParams;
|
4194
5438
|
}
|
4195
5439
|
|
5440
|
+
window.Turbo = Turbo$1;
|
5441
|
+
|
4196
5442
|
addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody);
|
4197
5443
|
|
4198
5444
|
var adapters = {
|
@@ -4309,6 +5555,8 @@ ConnectionMonitor.staleThreshold = 6;
|
|
4309
5555
|
|
4310
5556
|
ConnectionMonitor.reconnectionBackoffRate = .15;
|
4311
5557
|
|
5558
|
+
var ConnectionMonitor$1 = ConnectionMonitor;
|
5559
|
+
|
4312
5560
|
var INTERNAL = {
|
4313
5561
|
message_types: {
|
4314
5562
|
welcome: "welcome",
|
@@ -4320,7 +5568,8 @@ var INTERNAL = {
|
|
4320
5568
|
disconnect_reasons: {
|
4321
5569
|
unauthorized: "unauthorized",
|
4322
5570
|
invalid_request: "invalid_request",
|
4323
|
-
server_restart: "server_restart"
|
5571
|
+
server_restart: "server_restart",
|
5572
|
+
remote: "remote"
|
4324
5573
|
},
|
4325
5574
|
default_mount_path: "/cable",
|
4326
5575
|
protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
|
@@ -4337,7 +5586,7 @@ class Connection {
|
|
4337
5586
|
this.open = this.open.bind(this);
|
4338
5587
|
this.consumer = consumer;
|
4339
5588
|
this.subscriptions = this.consumer.subscriptions;
|
4340
|
-
this.monitor = new ConnectionMonitor(this);
|
5589
|
+
this.monitor = new ConnectionMonitor$1(this);
|
4341
5590
|
this.disconnected = true;
|
4342
5591
|
}
|
4343
5592
|
send(data) {
|
@@ -4353,11 +5602,12 @@ class Connection {
|
|
4353
5602
|
logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`);
|
4354
5603
|
return false;
|
4355
5604
|
} else {
|
4356
|
-
|
5605
|
+
const socketProtocols = [ ...protocols, ...this.consumer.subprotocols || [] ];
|
5606
|
+
logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${socketProtocols}`);
|
4357
5607
|
if (this.webSocket) {
|
4358
5608
|
this.uninstallEventHandlers();
|
4359
5609
|
}
|
4360
|
-
this.webSocket = new adapters.WebSocket(this.consumer.url,
|
5610
|
+
this.webSocket = new adapters.WebSocket(this.consumer.url, socketProtocols);
|
4361
5611
|
this.installEventHandlers();
|
4362
5612
|
this.monitor.start();
|
4363
5613
|
return true;
|
@@ -4369,7 +5619,7 @@ class Connection {
|
|
4369
5619
|
if (!allowReconnect) {
|
4370
5620
|
this.monitor.stop();
|
4371
5621
|
}
|
4372
|
-
if (this.
|
5622
|
+
if (this.isOpen()) {
|
4373
5623
|
return this.webSocket.close();
|
4374
5624
|
}
|
4375
5625
|
}
|
@@ -4399,6 +5649,9 @@ class Connection {
|
|
4399
5649
|
isActive() {
|
4400
5650
|
return this.isState("open", "connecting");
|
4401
5651
|
}
|
5652
|
+
triedToReconnect() {
|
5653
|
+
return this.monitor.reconnectAttempts > 0;
|
5654
|
+
}
|
4402
5655
|
isProtocolSupported() {
|
4403
5656
|
return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
|
4404
5657
|
}
|
@@ -4438,6 +5691,9 @@ Connection.prototype.events = {
|
|
4438
5691
|
const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data);
|
4439
5692
|
switch (type) {
|
4440
5693
|
case message_types.welcome:
|
5694
|
+
if (this.triedToReconnect()) {
|
5695
|
+
this.reconnectAttempted = true;
|
5696
|
+
}
|
4441
5697
|
this.monitor.recordConnect();
|
4442
5698
|
return this.subscriptions.reload();
|
4443
5699
|
|
@@ -4452,7 +5708,16 @@ Connection.prototype.events = {
|
|
4452
5708
|
|
4453
5709
|
case message_types.confirmation:
|
4454
5710
|
this.subscriptions.confirmSubscription(identifier);
|
4455
|
-
|
5711
|
+
if (this.reconnectAttempted) {
|
5712
|
+
this.reconnectAttempted = false;
|
5713
|
+
return this.subscriptions.notify(identifier, "connected", {
|
5714
|
+
reconnected: true
|
5715
|
+
});
|
5716
|
+
} else {
|
5717
|
+
return this.subscriptions.notify(identifier, "connected", {
|
5718
|
+
reconnected: false
|
5719
|
+
});
|
5720
|
+
}
|
4456
5721
|
|
4457
5722
|
case message_types.rejection:
|
4458
5723
|
return this.subscriptions.reject(identifier);
|
@@ -4487,6 +5752,8 @@ Connection.prototype.events = {
|
|
4487
5752
|
}
|
4488
5753
|
};
|
4489
5754
|
|
5755
|
+
var Connection$1 = Connection;
|
5756
|
+
|
4490
5757
|
const extend = function(object, properties) {
|
4491
5758
|
if (properties != null) {
|
4492
5759
|
for (let key in properties) {
|
@@ -4556,10 +5823,12 @@ class SubscriptionGuarantor {
|
|
4556
5823
|
}
|
4557
5824
|
}
|
4558
5825
|
|
5826
|
+
var SubscriptionGuarantor$1 = SubscriptionGuarantor;
|
5827
|
+
|
4559
5828
|
class Subscriptions {
|
4560
5829
|
constructor(consumer) {
|
4561
5830
|
this.consumer = consumer;
|
4562
|
-
this.guarantor = new SubscriptionGuarantor(this);
|
5831
|
+
this.guarantor = new SubscriptionGuarantor$1(this);
|
4563
5832
|
this.subscriptions = [];
|
4564
5833
|
}
|
4565
5834
|
create(channelName, mixin) {
|
@@ -4636,7 +5905,8 @@ class Consumer {
|
|
4636
5905
|
constructor(url) {
|
4637
5906
|
this._url = url;
|
4638
5907
|
this.subscriptions = new Subscriptions(this);
|
4639
|
-
this.connection = new Connection(this);
|
5908
|
+
this.connection = new Connection$1(this);
|
5909
|
+
this.subprotocols = [];
|
4640
5910
|
}
|
4641
5911
|
get url() {
|
4642
5912
|
return createWebSocketURL(this._url);
|
@@ -4657,6 +5927,9 @@ class Consumer {
|
|
4657
5927
|
return this.connection.open();
|
4658
5928
|
}
|
4659
5929
|
}
|
5930
|
+
addSubProtocol(subprotocol) {
|
5931
|
+
this.subprotocols = [ ...this.subprotocols, subprotocol ];
|
5932
|
+
}
|
4660
5933
|
}
|
4661
5934
|
|
4662
5935
|
function createWebSocketURL(url) {
|
@@ -4674,7 +5947,7 @@ function createWebSocketURL(url) {
|
|
4674
5947
|
}
|
4675
5948
|
}
|
4676
5949
|
|
4677
|
-
function createConsumer
|
5950
|
+
function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
|
4678
5951
|
return new Consumer(url);
|
4679
5952
|
}
|
4680
5953
|
|
@@ -4687,18 +5960,18 @@ function getConfig(name) {
|
|
4687
5960
|
|
4688
5961
|
var index = Object.freeze({
|
4689
5962
|
__proto__: null,
|
4690
|
-
Connection: Connection,
|
4691
|
-
ConnectionMonitor: ConnectionMonitor,
|
5963
|
+
Connection: Connection$1,
|
5964
|
+
ConnectionMonitor: ConnectionMonitor$1,
|
4692
5965
|
Consumer: Consumer,
|
4693
5966
|
INTERNAL: INTERNAL,
|
4694
5967
|
Subscription: Subscription,
|
4695
5968
|
Subscriptions: Subscriptions,
|
4696
|
-
SubscriptionGuarantor: SubscriptionGuarantor,
|
5969
|
+
SubscriptionGuarantor: SubscriptionGuarantor$1,
|
4697
5970
|
adapters: adapters,
|
4698
5971
|
createWebSocketURL: createWebSocketURL,
|
4699
5972
|
logger: logger,
|
4700
|
-
createConsumer: createConsumer
|
5973
|
+
createConsumer: createConsumer,
|
4701
5974
|
getConfig: getConfig
|
4702
5975
|
});
|
4703
5976
|
|
4704
|
-
export {
|
5977
|
+
export { Turbo$1 as Turbo, cable };
|