turbo-rails 0.8.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/app/assets/javascripts/turbo.js +281 -97
- data/app/assets/javascripts/turbo.min.js +25 -0
- data/app/assets/javascripts/turbo.min.js.map +1 -0
- data/app/channels/turbo/streams/stream_name.rb +7 -0
- data/app/channels/turbo/streams_channel.rb +30 -2
- data/app/controllers/turbo/frames/frame_request.rb +5 -1
- data/app/helpers/turbo/streams_helper.rb +7 -1
- data/app/models/concerns/turbo/broadcastable.rb +18 -1
- data/lib/install/turbo_needs_redis.rb +2 -1
- data/lib/install/turbo_with_importmap.rb +1 -1
- data/lib/turbo/engine.rb +9 -1
- data/lib/turbo/version.rb +1 -1
- metadata +9 -7
@@ -14,6 +14,31 @@
|
|
14
14
|
Object.setPrototypeOf(HTMLElement, BuiltInHTMLElement);
|
15
15
|
})();
|
16
16
|
|
17
|
+
(function(prototype) {
|
18
|
+
if (typeof prototype.requestSubmit == "function") return;
|
19
|
+
prototype.requestSubmit = function(submitter) {
|
20
|
+
if (submitter) {
|
21
|
+
validateSubmitter(submitter, this);
|
22
|
+
submitter.click();
|
23
|
+
} else {
|
24
|
+
submitter = document.createElement("input");
|
25
|
+
submitter.type = "submit";
|
26
|
+
submitter.hidden = true;
|
27
|
+
this.appendChild(submitter);
|
28
|
+
submitter.click();
|
29
|
+
this.removeChild(submitter);
|
30
|
+
}
|
31
|
+
};
|
32
|
+
function validateSubmitter(submitter, form) {
|
33
|
+
submitter instanceof HTMLElement || raise(TypeError, "parameter 1 is not of type 'HTMLElement'");
|
34
|
+
submitter.type == "submit" || raise(TypeError, "The specified element is not a submit button");
|
35
|
+
submitter.form == form || raise(DOMException, "The specified element is not owned by this form element", "NotFoundError");
|
36
|
+
}
|
37
|
+
function raise(errorConstructor, message, name) {
|
38
|
+
throw new errorConstructor("Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".", name);
|
39
|
+
}
|
40
|
+
})(HTMLFormElement.prototype);
|
41
|
+
|
17
42
|
const submittersByForm = new WeakMap;
|
18
43
|
|
19
44
|
function findSubmitterFromClickTarget(target) {
|
@@ -30,10 +55,17 @@ function clickCaptured(event) {
|
|
30
55
|
}
|
31
56
|
|
32
57
|
(function() {
|
33
|
-
if ("SubmitEvent" in window) return;
|
34
58
|
if ("submitter" in Event.prototype) return;
|
59
|
+
let prototype;
|
60
|
+
if ("SubmitEvent" in window && /Apple Computer/.test(navigator.vendor)) {
|
61
|
+
prototype = window.SubmitEvent.prototype;
|
62
|
+
} else if ("SubmitEvent" in window) {
|
63
|
+
return;
|
64
|
+
} else {
|
65
|
+
prototype = window.Event.prototype;
|
66
|
+
}
|
35
67
|
addEventListener("click", clickCaptured, true);
|
36
|
-
Object.defineProperty(
|
68
|
+
Object.defineProperty(prototype, "submitter", {
|
37
69
|
get() {
|
38
70
|
if (this.type == "submit" && this.target instanceof HTMLFormElement) {
|
39
71
|
return submittersByForm.get(this.target);
|
@@ -153,6 +185,11 @@ function getAnchor(url) {
|
|
153
185
|
}
|
154
186
|
}
|
155
187
|
|
188
|
+
function getAction(form, submitter) {
|
189
|
+
const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formaction")) || form.getAttribute("action") || form.action;
|
190
|
+
return expandURL(action);
|
191
|
+
}
|
192
|
+
|
156
193
|
function getExtension(url) {
|
157
194
|
return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
|
158
195
|
}
|
@@ -166,6 +203,10 @@ function isPrefixedBy(baseURL, url) {
|
|
166
203
|
return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
|
167
204
|
}
|
168
205
|
|
206
|
+
function locationIsVisitable(location, rootLocation) {
|
207
|
+
return isPrefixedBy(location, rootLocation) && isHTML(location);
|
208
|
+
}
|
209
|
+
|
169
210
|
function getRequestURL(url) {
|
170
211
|
const anchor = getAnchor(url);
|
171
212
|
return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href;
|
@@ -301,6 +342,31 @@ function uuid() {
|
|
301
342
|
})).join("");
|
302
343
|
}
|
303
344
|
|
345
|
+
function getAttribute(attributeName, ...elements) {
|
346
|
+
for (const value of elements.map((element => element === null || element === void 0 ? void 0 : element.getAttribute(attributeName)))) {
|
347
|
+
if (typeof value == "string") return value;
|
348
|
+
}
|
349
|
+
return null;
|
350
|
+
}
|
351
|
+
|
352
|
+
function markAsBusy(...elements) {
|
353
|
+
for (const element of elements) {
|
354
|
+
if (element.localName == "turbo-frame") {
|
355
|
+
element.setAttribute("busy", "");
|
356
|
+
}
|
357
|
+
element.setAttribute("aria-busy", "true");
|
358
|
+
}
|
359
|
+
}
|
360
|
+
|
361
|
+
function clearBusyState(...elements) {
|
362
|
+
for (const element of elements) {
|
363
|
+
if (element.localName == "turbo-frame") {
|
364
|
+
element.removeAttribute("busy");
|
365
|
+
}
|
366
|
+
element.removeAttribute("aria-busy");
|
367
|
+
}
|
368
|
+
}
|
369
|
+
|
304
370
|
var FetchMethod;
|
305
371
|
|
306
372
|
(function(FetchMethod) {
|
@@ -337,12 +403,8 @@ class FetchRequest {
|
|
337
403
|
this.delegate = delegate;
|
338
404
|
this.method = method;
|
339
405
|
this.headers = this.defaultHeaders;
|
340
|
-
|
341
|
-
|
342
|
-
} else {
|
343
|
-
this.body = body;
|
344
|
-
this.url = location;
|
345
|
-
}
|
406
|
+
this.body = body;
|
407
|
+
this.url = location;
|
346
408
|
this.target = target;
|
347
409
|
}
|
348
410
|
get location() {
|
@@ -400,7 +462,7 @@ class FetchRequest {
|
|
400
462
|
credentials: "same-origin",
|
401
463
|
headers: this.headers,
|
402
464
|
redirect: "follow",
|
403
|
-
body: this.body,
|
465
|
+
body: this.isIdempotent ? null : this.body,
|
404
466
|
signal: this.abortSignal,
|
405
467
|
referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
|
406
468
|
};
|
@@ -422,7 +484,7 @@ class FetchRequest {
|
|
422
484
|
cancelable: true,
|
423
485
|
detail: {
|
424
486
|
fetchOptions: fetchOptions,
|
425
|
-
url: this.url
|
487
|
+
url: this.url,
|
426
488
|
resume: this.resolveRequestPromise
|
427
489
|
},
|
428
490
|
target: this.target
|
@@ -431,20 +493,6 @@ class FetchRequest {
|
|
431
493
|
}
|
432
494
|
}
|
433
495
|
|
434
|
-
function mergeFormDataEntries(url, entries) {
|
435
|
-
const currentSearchParams = new URLSearchParams(url.search);
|
436
|
-
for (const [name, value] of entries) {
|
437
|
-
if (value instanceof File) continue;
|
438
|
-
if (currentSearchParams.has(name)) {
|
439
|
-
currentSearchParams.delete(name);
|
440
|
-
url.searchParams.set(name, value);
|
441
|
-
} else {
|
442
|
-
url.searchParams.append(name, value);
|
443
|
-
}
|
444
|
-
}
|
445
|
-
return url;
|
446
|
-
}
|
447
|
-
|
448
496
|
class AppearanceObserver {
|
449
497
|
constructor(delegate, element) {
|
450
498
|
this.started = false;
|
@@ -546,9 +594,16 @@ class FormSubmission {
|
|
546
594
|
this.formElement = formElement;
|
547
595
|
this.submitter = submitter;
|
548
596
|
this.formData = buildFormData(formElement, submitter);
|
597
|
+
this.location = expandURL(this.action);
|
598
|
+
if (this.method == FetchMethod.get) {
|
599
|
+
mergeFormDataEntries(this.location, [ ...this.body.entries() ]);
|
600
|
+
}
|
549
601
|
this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
|
550
602
|
this.mustRedirect = mustRedirect;
|
551
603
|
}
|
604
|
+
static confirmMethod(message, element) {
|
605
|
+
return confirm(message);
|
606
|
+
}
|
552
607
|
get method() {
|
553
608
|
var _a;
|
554
609
|
const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
|
@@ -559,9 +614,6 @@ class FormSubmission {
|
|
559
614
|
const formElementAction = typeof this.formElement.action === "string" ? this.formElement.action : null;
|
560
615
|
return ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formaction")) || this.formElement.getAttribute("action") || formElementAction || "";
|
561
616
|
}
|
562
|
-
get location() {
|
563
|
-
return expandURL(this.action);
|
564
|
-
}
|
565
617
|
get body() {
|
566
618
|
if (this.enctype == FormEnctype.urlEncoded || this.method == FetchMethod.get) {
|
567
619
|
return new URLSearchParams(this.stringFormData);
|
@@ -579,8 +631,20 @@ class FormSubmission {
|
|
579
631
|
get stringFormData() {
|
580
632
|
return [ ...this.formData ].reduce(((entries, [name, value]) => entries.concat(typeof value == "string" ? [ [ name, value ] ] : [])), []);
|
581
633
|
}
|
634
|
+
get confirmationMessage() {
|
635
|
+
return this.formElement.getAttribute("data-turbo-confirm");
|
636
|
+
}
|
637
|
+
get needsConfirmation() {
|
638
|
+
return this.confirmationMessage !== null;
|
639
|
+
}
|
582
640
|
async start() {
|
583
641
|
const {initialized: initialized, requesting: requesting} = FormSubmissionState;
|
642
|
+
if (this.needsConfirmation) {
|
643
|
+
const answer = FormSubmission.confirmMethod(this.confirmationMessage, this.formElement);
|
644
|
+
if (!answer) {
|
645
|
+
return;
|
646
|
+
}
|
647
|
+
}
|
584
648
|
if (this.state == initialized) {
|
585
649
|
this.state = requesting;
|
586
650
|
return this.fetchRequest.perform();
|
@@ -604,7 +668,9 @@ class FormSubmission {
|
|
604
668
|
}
|
605
669
|
}
|
606
670
|
requestStarted(request) {
|
671
|
+
var _a;
|
607
672
|
this.state = FormSubmissionState.waiting;
|
673
|
+
(_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
|
608
674
|
dispatch("turbo:submit-start", {
|
609
675
|
target: this.formElement,
|
610
676
|
detail: {
|
@@ -649,7 +715,9 @@ class FormSubmission {
|
|
649
715
|
this.delegate.formSubmissionErrored(this, error);
|
650
716
|
}
|
651
717
|
requestFinished(request) {
|
718
|
+
var _a;
|
652
719
|
this.state = FormSubmissionState.stopped;
|
720
|
+
(_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
|
653
721
|
dispatch("turbo:submit-end", {
|
654
722
|
target: this.formElement,
|
655
723
|
detail: Object.assign({
|
@@ -693,6 +761,16 @@ function responseSucceededWithoutRedirect(response) {
|
|
693
761
|
return response.statusCode == 200 && !response.redirected;
|
694
762
|
}
|
695
763
|
|
764
|
+
function mergeFormDataEntries(url, entries) {
|
765
|
+
const searchParams = new URLSearchParams;
|
766
|
+
for (const [name, value] of entries) {
|
767
|
+
if (value instanceof File) continue;
|
768
|
+
searchParams.append(name, value);
|
769
|
+
}
|
770
|
+
url.search = searchParams.toString();
|
771
|
+
return url;
|
772
|
+
}
|
773
|
+
|
696
774
|
class Snapshot {
|
697
775
|
constructor(element) {
|
698
776
|
this.element = element;
|
@@ -734,10 +812,11 @@ class Snapshot {
|
|
734
812
|
class FormInterceptor {
|
735
813
|
constructor(delegate, element) {
|
736
814
|
this.submitBubbled = event => {
|
737
|
-
|
738
|
-
|
815
|
+
const form = event.target;
|
816
|
+
if (!event.defaultPrevented && form instanceof HTMLFormElement && form.closest("turbo-frame, html") == this.element) {
|
739
817
|
const submitter = event.submitter || undefined;
|
740
|
-
|
818
|
+
const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
|
819
|
+
if (method != "dialog" && this.delegate.shouldInterceptFormSubmission(form, submitter)) {
|
741
820
|
event.preventDefault();
|
742
821
|
event.stopImmediatePropagation();
|
743
822
|
this.delegate.formSubmissionIntercepted(form, submitter);
|
@@ -948,10 +1027,11 @@ function createPlaceholderForPermanentElement(permanentElement) {
|
|
948
1027
|
}
|
949
1028
|
|
950
1029
|
class Renderer {
|
951
|
-
constructor(currentSnapshot, newSnapshot, isPreview) {
|
1030
|
+
constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) {
|
952
1031
|
this.currentSnapshot = currentSnapshot;
|
953
1032
|
this.newSnapshot = newSnapshot;
|
954
1033
|
this.isPreview = isPreview;
|
1034
|
+
this.willRender = willRender;
|
955
1035
|
this.promise = new Promise(((resolve, reject) => this.resolvingFunctions = {
|
956
1036
|
resolve: resolve,
|
957
1037
|
reject: reject
|
@@ -1333,7 +1413,9 @@ var VisitState;
|
|
1333
1413
|
|
1334
1414
|
const defaultOptions = {
|
1335
1415
|
action: "advance",
|
1336
|
-
historyChanged: false
|
1416
|
+
historyChanged: false,
|
1417
|
+
visitCachedSnapshot: () => {},
|
1418
|
+
willRender: true
|
1337
1419
|
};
|
1338
1420
|
|
1339
1421
|
var SystemStatusCode;
|
@@ -1356,13 +1438,16 @@ class Visit {
|
|
1356
1438
|
this.delegate = delegate;
|
1357
1439
|
this.location = location;
|
1358
1440
|
this.restorationIdentifier = restorationIdentifier || uuid();
|
1359
|
-
const {action: action, historyChanged: historyChanged, referrer: referrer, snapshotHTML: snapshotHTML, response: response} = Object.assign(Object.assign({}, defaultOptions), options);
|
1441
|
+
const {action: action, historyChanged: historyChanged, referrer: referrer, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender} = Object.assign(Object.assign({}, defaultOptions), options);
|
1360
1442
|
this.action = action;
|
1361
1443
|
this.historyChanged = historyChanged;
|
1362
1444
|
this.referrer = referrer;
|
1363
1445
|
this.snapshotHTML = snapshotHTML;
|
1364
1446
|
this.response = response;
|
1365
1447
|
this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
|
1448
|
+
this.visitCachedSnapshot = visitCachedSnapshot;
|
1449
|
+
this.willRender = willRender;
|
1450
|
+
this.scrolled = !willRender;
|
1366
1451
|
}
|
1367
1452
|
get adapter() {
|
1368
1453
|
return this.delegate.adapter;
|
@@ -1461,7 +1546,7 @@ class Visit {
|
|
1461
1546
|
this.cacheSnapshot();
|
1462
1547
|
if (this.view.renderPromise) await this.view.renderPromise;
|
1463
1548
|
if (isSuccessful(statusCode) && responseHTML != null) {
|
1464
|
-
await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML));
|
1549
|
+
await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML), false, this.willRender);
|
1465
1550
|
this.adapter.visitRendered(this);
|
1466
1551
|
this.complete();
|
1467
1552
|
} else {
|
@@ -1498,7 +1583,7 @@ class Visit {
|
|
1498
1583
|
this.adapter.visitRendered(this);
|
1499
1584
|
} else {
|
1500
1585
|
if (this.view.renderPromise) await this.view.renderPromise;
|
1501
|
-
await this.view.renderPage(snapshot, isPreview);
|
1586
|
+
await this.view.renderPage(snapshot, isPreview, this.willRender);
|
1502
1587
|
this.adapter.visitRendered(this);
|
1503
1588
|
if (!isPreview) {
|
1504
1589
|
this.complete();
|
@@ -1508,7 +1593,8 @@ class Visit {
|
|
1508
1593
|
}
|
1509
1594
|
}
|
1510
1595
|
followRedirect() {
|
1511
|
-
|
1596
|
+
var _a;
|
1597
|
+
if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
|
1512
1598
|
this.adapter.visitProposedToLocation(this.redirectedToLocation, {
|
1513
1599
|
action: "replace",
|
1514
1600
|
response: this.response
|
@@ -1530,34 +1616,41 @@ class Visit {
|
|
1530
1616
|
requestPreventedHandlingResponse(request, response) {}
|
1531
1617
|
async requestSucceededWithResponse(request, response) {
|
1532
1618
|
const responseHTML = await response.responseHTML;
|
1619
|
+
const {redirected: redirected, statusCode: statusCode} = response;
|
1533
1620
|
if (responseHTML == undefined) {
|
1534
1621
|
this.recordResponse({
|
1535
|
-
statusCode: SystemStatusCode.contentTypeMismatch
|
1622
|
+
statusCode: SystemStatusCode.contentTypeMismatch,
|
1623
|
+
redirected: redirected
|
1536
1624
|
});
|
1537
1625
|
} else {
|
1538
1626
|
this.redirectedToLocation = response.redirected ? response.location : undefined;
|
1539
1627
|
this.recordResponse({
|
1540
|
-
statusCode:
|
1541
|
-
responseHTML: responseHTML
|
1628
|
+
statusCode: statusCode,
|
1629
|
+
responseHTML: responseHTML,
|
1630
|
+
redirected: redirected
|
1542
1631
|
});
|
1543
1632
|
}
|
1544
1633
|
}
|
1545
1634
|
async requestFailedWithResponse(request, response) {
|
1546
1635
|
const responseHTML = await response.responseHTML;
|
1636
|
+
const {redirected: redirected, statusCode: statusCode} = response;
|
1547
1637
|
if (responseHTML == undefined) {
|
1548
1638
|
this.recordResponse({
|
1549
|
-
statusCode: SystemStatusCode.contentTypeMismatch
|
1639
|
+
statusCode: SystemStatusCode.contentTypeMismatch,
|
1640
|
+
redirected: redirected
|
1550
1641
|
});
|
1551
1642
|
} else {
|
1552
1643
|
this.recordResponse({
|
1553
|
-
statusCode:
|
1554
|
-
responseHTML: responseHTML
|
1644
|
+
statusCode: statusCode,
|
1645
|
+
responseHTML: responseHTML,
|
1646
|
+
redirected: redirected
|
1555
1647
|
});
|
1556
1648
|
}
|
1557
1649
|
}
|
1558
1650
|
requestErrored(request, error) {
|
1559
1651
|
this.recordResponse({
|
1560
|
-
statusCode: SystemStatusCode.networkFailure
|
1652
|
+
statusCode: SystemStatusCode.networkFailure,
|
1653
|
+
redirected: false
|
1561
1654
|
});
|
1562
1655
|
}
|
1563
1656
|
requestFinished() {
|
@@ -1615,12 +1708,12 @@ class Visit {
|
|
1615
1708
|
} else if (this.action == "restore") {
|
1616
1709
|
return !this.hasCachedSnapshot();
|
1617
1710
|
} else {
|
1618
|
-
return
|
1711
|
+
return this.willRender;
|
1619
1712
|
}
|
1620
1713
|
}
|
1621
1714
|
cacheSnapshot() {
|
1622
1715
|
if (!this.snapshotCached) {
|
1623
|
-
this.view.cacheSnapshot();
|
1716
|
+
this.view.cacheSnapshot().then((snapshot => snapshot && this.visitCachedSnapshot(snapshot)));
|
1624
1717
|
this.snapshotCached = true;
|
1625
1718
|
}
|
1626
1719
|
}
|
@@ -1657,10 +1750,10 @@ class BrowserAdapter {
|
|
1657
1750
|
this.navigator.startVisit(location, uuid(), options);
|
1658
1751
|
}
|
1659
1752
|
visitStarted(visit) {
|
1753
|
+
visit.loadCachedSnapshot();
|
1660
1754
|
visit.issueRequest();
|
1661
1755
|
visit.changeHistory();
|
1662
1756
|
visit.goToSamePageAnchor();
|
1663
|
-
visit.loadCachedSnapshot();
|
1664
1757
|
}
|
1665
1758
|
visitRequestStarted(visit) {
|
1666
1759
|
this.progressBar.setValue(0);
|
@@ -1768,7 +1861,7 @@ class FormSubmitObserver {
|
|
1768
1861
|
const form = event.target instanceof HTMLFormElement ? event.target : undefined;
|
1769
1862
|
const submitter = event.submitter || undefined;
|
1770
1863
|
if (form) {
|
1771
|
-
const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
|
1864
|
+
const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.getAttribute("method");
|
1772
1865
|
if (method != "dialog" && this.delegate.willSubmitForm(form, submitter)) {
|
1773
1866
|
event.preventDefault();
|
1774
1867
|
this.delegate.formSubmitted(form, submitter);
|
@@ -1812,12 +1905,11 @@ class FrameRedirector {
|
|
1812
1905
|
linkClickIntercepted(element, url) {
|
1813
1906
|
const frame = this.findFrameElement(element);
|
1814
1907
|
if (frame) {
|
1815
|
-
frame.
|
1816
|
-
frame.src = url;
|
1908
|
+
frame.delegate.linkClickIntercepted(element, url);
|
1817
1909
|
}
|
1818
1910
|
}
|
1819
1911
|
shouldInterceptFormSubmission(element, submitter) {
|
1820
|
-
return this.
|
1912
|
+
return this.shouldSubmit(element, submitter);
|
1821
1913
|
}
|
1822
1914
|
formSubmissionIntercepted(element, submitter) {
|
1823
1915
|
const frame = this.findFrameElement(element, submitter);
|
@@ -1826,6 +1918,13 @@ class FrameRedirector {
|
|
1826
1918
|
frame.delegate.formSubmissionIntercepted(element, submitter);
|
1827
1919
|
}
|
1828
1920
|
}
|
1921
|
+
shouldSubmit(form, submitter) {
|
1922
|
+
var _a;
|
1923
|
+
const action = getAction(form, submitter);
|
1924
|
+
const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
|
1925
|
+
const rootLocation = expandURL((_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/");
|
1926
|
+
return this.shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation);
|
1927
|
+
}
|
1829
1928
|
shouldRedirect(element, submitter) {
|
1830
1929
|
const frame = this.findFrameElement(element, submitter);
|
1831
1930
|
return frame ? frame != element.closest("turbo-frame") : false;
|
@@ -1981,7 +2080,11 @@ class Navigator {
|
|
1981
2080
|
}
|
1982
2081
|
proposeVisit(location, options = {}) {
|
1983
2082
|
if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
|
1984
|
-
|
2083
|
+
if (locationIsVisitable(location, this.view.snapshot.rootLocation)) {
|
2084
|
+
this.delegate.visitProposedToLocation(location, options);
|
2085
|
+
} else {
|
2086
|
+
window.location.href = location.toString();
|
2087
|
+
}
|
1985
2088
|
}
|
1986
2089
|
}
|
1987
2090
|
startVisit(locatable, restorationIdentifier, options = {}) {
|
@@ -1994,13 +2097,7 @@ class Navigator {
|
|
1994
2097
|
submitForm(form, submitter) {
|
1995
2098
|
this.stop();
|
1996
2099
|
this.formSubmission = new FormSubmission(this, form, submitter, true);
|
1997
|
-
|
1998
|
-
this.proposeVisit(this.formSubmission.fetchRequest.url, {
|
1999
|
-
action: this.getActionForFormSubmission(this.formSubmission)
|
2000
|
-
});
|
2001
|
-
} else {
|
2002
|
-
this.formSubmission.start();
|
2003
|
-
}
|
2100
|
+
this.formSubmission.start();
|
2004
2101
|
}
|
2005
2102
|
stop() {
|
2006
2103
|
if (this.formSubmission) {
|
@@ -2033,11 +2130,14 @@ class Navigator {
|
|
2033
2130
|
if (formSubmission.method != FetchMethod.get) {
|
2034
2131
|
this.view.clearSnapshotCache();
|
2035
2132
|
}
|
2036
|
-
const {statusCode: statusCode} = fetchResponse;
|
2133
|
+
const {statusCode: statusCode, redirected: redirected} = fetchResponse;
|
2134
|
+
const action = this.getActionForFormSubmission(formSubmission);
|
2037
2135
|
const visitOptions = {
|
2136
|
+
action: action,
|
2038
2137
|
response: {
|
2039
2138
|
statusCode: statusCode,
|
2040
|
-
responseHTML: responseHTML
|
2139
|
+
responseHTML: responseHTML,
|
2140
|
+
redirected: redirected
|
2041
2141
|
}
|
2042
2142
|
};
|
2043
2143
|
this.proposeVisit(fetchResponse.location, visitOptions);
|
@@ -2088,7 +2188,7 @@ class Navigator {
|
|
2088
2188
|
}
|
2089
2189
|
getActionForFormSubmission(formSubmission) {
|
2090
2190
|
const {formElement: formElement, submitter: submitter} = formSubmission;
|
2091
|
-
const action =
|
2191
|
+
const action = getAttribute("data-turbo-action", submitter, formElement);
|
2092
2192
|
return isAction(action) ? action : "advance";
|
2093
2193
|
}
|
2094
2194
|
}
|
@@ -2288,7 +2388,9 @@ class PageRenderer extends Renderer {
|
|
2288
2388
|
this.mergeHead();
|
2289
2389
|
}
|
2290
2390
|
async render() {
|
2291
|
-
this.
|
2391
|
+
if (this.willRender) {
|
2392
|
+
this.replaceBody();
|
2393
|
+
}
|
2292
2394
|
}
|
2293
2395
|
finishRendering() {
|
2294
2396
|
super.finishRendering();
|
@@ -2424,8 +2526,8 @@ class PageView extends View {
|
|
2424
2526
|
this.snapshotCache = new SnapshotCache(10);
|
2425
2527
|
this.lastRenderedLocation = new URL(location.href);
|
2426
2528
|
}
|
2427
|
-
renderPage(snapshot, isPreview = false) {
|
2428
|
-
const renderer = new PageRenderer(this.snapshot, snapshot, isPreview);
|
2529
|
+
renderPage(snapshot, isPreview = false, willRender = true) {
|
2530
|
+
const renderer = new PageRenderer(this.snapshot, snapshot, isPreview, willRender);
|
2429
2531
|
return this.render(renderer);
|
2430
2532
|
}
|
2431
2533
|
renderError(snapshot) {
|
@@ -2440,7 +2542,9 @@ class PageView extends View {
|
|
2440
2542
|
this.delegate.viewWillCacheSnapshot();
|
2441
2543
|
const {snapshot: snapshot, lastRenderedLocation: location} = this;
|
2442
2544
|
await nextEventLoopTick();
|
2443
|
-
|
2545
|
+
const cachedSnapshot = snapshot.clone();
|
2546
|
+
this.snapshotCache.put(location, cachedSnapshot);
|
2547
|
+
return cachedSnapshot;
|
2444
2548
|
}
|
2445
2549
|
}
|
2446
2550
|
getCachedSnapshotForLocation(location) {
|
@@ -2545,7 +2649,7 @@ class Session {
|
|
2545
2649
|
});
|
2546
2650
|
}
|
2547
2651
|
willFollowLinkToLocation(link, location) {
|
2548
|
-
return this.elementDriveEnabled(link) &&
|
2652
|
+
return this.elementDriveEnabled(link) && locationIsVisitable(location, this.snapshot.rootLocation) && this.applicationAllowsFollowingLinkToLocation(link, location);
|
2549
2653
|
}
|
2550
2654
|
followedLinkToLocation(link, location) {
|
2551
2655
|
const action = this.getActionForLink(link);
|
@@ -2554,14 +2658,23 @@ class Session {
|
|
2554
2658
|
});
|
2555
2659
|
}
|
2556
2660
|
convertLinkWithMethodClickToFormSubmission(link) {
|
2557
|
-
var _a;
|
2558
2661
|
const linkMethod = link.getAttribute("data-turbo-method");
|
2559
2662
|
if (linkMethod) {
|
2560
2663
|
const form = document.createElement("form");
|
2561
2664
|
form.method = linkMethod;
|
2562
2665
|
form.action = link.getAttribute("href") || "undefined";
|
2563
2666
|
form.hidden = true;
|
2564
|
-
(
|
2667
|
+
if (link.hasAttribute("data-turbo-confirm")) {
|
2668
|
+
form.setAttribute("data-turbo-confirm", link.getAttribute("data-turbo-confirm"));
|
2669
|
+
}
|
2670
|
+
const frame = this.getTargetFrameForLink(link);
|
2671
|
+
if (frame) {
|
2672
|
+
form.setAttribute("data-turbo-frame", frame);
|
2673
|
+
form.addEventListener("turbo:submit-start", (() => form.remove()));
|
2674
|
+
} else {
|
2675
|
+
form.addEventListener("submit", (() => form.remove()));
|
2676
|
+
}
|
2677
|
+
document.body.appendChild(form);
|
2565
2678
|
return dispatch("submit", {
|
2566
2679
|
cancelable: true,
|
2567
2680
|
target: form
|
@@ -2593,7 +2706,8 @@ class Session {
|
|
2593
2706
|
this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
|
2594
2707
|
}
|
2595
2708
|
willSubmitForm(form, submitter) {
|
2596
|
-
|
2709
|
+
const action = getAction(form, submitter);
|
2710
|
+
return this.elementDriveEnabled(form) && (!submitter || this.elementDriveEnabled(submitter)) && locationIsVisitable(expandURL(action), this.snapshot.rootLocation);
|
2597
2711
|
}
|
2598
2712
|
formSubmitted(form, submitter) {
|
2599
2713
|
this.navigator.submitForm(form, submitter);
|
@@ -2660,6 +2774,7 @@ class Session {
|
|
2660
2774
|
});
|
2661
2775
|
}
|
2662
2776
|
notifyApplicationAfterVisitingLocation(location, action) {
|
2777
|
+
markAsBusy(document.documentElement);
|
2663
2778
|
return dispatch("turbo:visit", {
|
2664
2779
|
detail: {
|
2665
2780
|
url: location.href,
|
@@ -2683,6 +2798,7 @@ class Session {
|
|
2683
2798
|
return dispatch("turbo:render");
|
2684
2799
|
}
|
2685
2800
|
notifyApplicationAfterPageLoad(timing = {}) {
|
2801
|
+
clearBusyState(document.documentElement);
|
2686
2802
|
return dispatch("turbo:load", {
|
2687
2803
|
detail: {
|
2688
2804
|
url: this.location.href,
|
@@ -2730,8 +2846,16 @@ class Session {
|
|
2730
2846
|
const action = link.getAttribute("data-turbo-action");
|
2731
2847
|
return isAction(action) ? action : "advance";
|
2732
2848
|
}
|
2733
|
-
|
2734
|
-
|
2849
|
+
getTargetFrameForLink(link) {
|
2850
|
+
const frame = link.getAttribute("data-turbo-frame");
|
2851
|
+
if (frame) {
|
2852
|
+
return frame;
|
2853
|
+
} else {
|
2854
|
+
const container = link.closest("turbo-frame");
|
2855
|
+
if (container) {
|
2856
|
+
return container.id;
|
2857
|
+
}
|
2858
|
+
}
|
2735
2859
|
}
|
2736
2860
|
get snapshot() {
|
2737
2861
|
return this.view.snapshot;
|
@@ -2752,7 +2876,7 @@ const deprecatedLocationPropertyDescriptors = {
|
|
2752
2876
|
|
2753
2877
|
const session = new Session;
|
2754
2878
|
|
2755
|
-
const {navigator: navigator} = session;
|
2879
|
+
const {navigator: navigator$1} = session;
|
2756
2880
|
|
2757
2881
|
function start() {
|
2758
2882
|
session.start();
|
@@ -2786,9 +2910,13 @@ function setProgressBarDelay(delay) {
|
|
2786
2910
|
session.setProgressBarDelay(delay);
|
2787
2911
|
}
|
2788
2912
|
|
2913
|
+
function setConfirmMethod(confirmMethod) {
|
2914
|
+
FormSubmission.confirmMethod = confirmMethod;
|
2915
|
+
}
|
2916
|
+
|
2789
2917
|
var Turbo = Object.freeze({
|
2790
2918
|
__proto__: null,
|
2791
|
-
navigator: navigator,
|
2919
|
+
navigator: navigator$1,
|
2792
2920
|
session: session,
|
2793
2921
|
PageRenderer: PageRenderer,
|
2794
2922
|
PageSnapshot: PageSnapshot,
|
@@ -2799,11 +2927,14 @@ var Turbo = Object.freeze({
|
|
2799
2927
|
disconnectStreamSource: disconnectStreamSource,
|
2800
2928
|
renderStreamMessage: renderStreamMessage,
|
2801
2929
|
clearCache: clearCache,
|
2802
|
-
setProgressBarDelay: setProgressBarDelay
|
2930
|
+
setProgressBarDelay: setProgressBarDelay,
|
2931
|
+
setConfirmMethod: setConfirmMethod
|
2803
2932
|
});
|
2804
2933
|
|
2805
2934
|
class FrameController {
|
2806
2935
|
constructor(element) {
|
2936
|
+
this.fetchResponseLoaded = fetchResponse => {};
|
2937
|
+
this.currentFetchRequest = null;
|
2807
2938
|
this.resolveVisitPromise = () => {};
|
2808
2939
|
this.connected = false;
|
2809
2940
|
this.hasBeenLoaded = false;
|
@@ -2858,11 +2989,10 @@ class FrameController {
|
|
2858
2989
|
this.currentURL = this.sourceURL;
|
2859
2990
|
if (this.sourceURL) {
|
2860
2991
|
try {
|
2861
|
-
this.element.loaded = this.visit(this.sourceURL);
|
2992
|
+
this.element.loaded = this.visit(expandURL(this.sourceURL));
|
2862
2993
|
this.appearanceObserver.stop();
|
2863
2994
|
await this.element.loaded;
|
2864
2995
|
this.hasBeenLoaded = true;
|
2865
|
-
session.frameLoaded(this.element);
|
2866
2996
|
} catch (error) {
|
2867
2997
|
this.currentURL = previousURL;
|
2868
2998
|
throw error;
|
@@ -2871,7 +3001,7 @@ class FrameController {
|
|
2871
3001
|
}
|
2872
3002
|
}
|
2873
3003
|
async loadResponse(fetchResponse) {
|
2874
|
-
if (fetchResponse.redirected) {
|
3004
|
+
if (fetchResponse.redirected || fetchResponse.succeeded && fetchResponse.isHTML) {
|
2875
3005
|
this.sourceURL = fetchResponse.response.url;
|
2876
3006
|
}
|
2877
3007
|
try {
|
@@ -2879,14 +3009,18 @@ class FrameController {
|
|
2879
3009
|
if (html) {
|
2880
3010
|
const {body: body} = parseHTMLDocument(html);
|
2881
3011
|
const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
|
2882
|
-
const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
|
3012
|
+
const renderer = new FrameRenderer(this.view.snapshot, snapshot, false, false);
|
2883
3013
|
if (this.view.renderPromise) await this.view.renderPromise;
|
2884
3014
|
await this.view.render(renderer);
|
2885
3015
|
session.frameRendered(fetchResponse, this.element);
|
3016
|
+
session.frameLoaded(this.element);
|
3017
|
+
this.fetchResponseLoaded(fetchResponse);
|
2886
3018
|
}
|
2887
3019
|
} catch (error) {
|
2888
3020
|
console.error(error);
|
2889
3021
|
this.view.invalidate();
|
3022
|
+
} finally {
|
3023
|
+
this.fetchResponseLoaded = () => {};
|
2890
3024
|
}
|
2891
3025
|
}
|
2892
3026
|
elementAppearedInViewport(element) {
|
@@ -2912,19 +3046,15 @@ class FrameController {
|
|
2912
3046
|
}
|
2913
3047
|
this.reloadable = false;
|
2914
3048
|
this.formSubmission = new FormSubmission(this, element, submitter);
|
2915
|
-
|
2916
|
-
|
2917
|
-
|
2918
|
-
const {fetchRequest: fetchRequest} = this.formSubmission;
|
2919
|
-
this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
|
2920
|
-
this.formSubmission.start();
|
2921
|
-
}
|
3049
|
+
const {fetchRequest: fetchRequest} = this.formSubmission;
|
3050
|
+
this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
|
3051
|
+
this.formSubmission.start();
|
2922
3052
|
}
|
2923
3053
|
prepareHeadersForRequest(headers, request) {
|
2924
3054
|
headers["Turbo-Frame"] = this.id;
|
2925
3055
|
}
|
2926
3056
|
requestStarted(request) {
|
2927
|
-
this.element
|
3057
|
+
markAsBusy(this.element);
|
2928
3058
|
}
|
2929
3059
|
requestPreventedHandlingResponse(request, response) {
|
2930
3060
|
this.resolveVisitPromise();
|
@@ -2942,14 +3072,14 @@ class FrameController {
|
|
2942
3072
|
this.resolveVisitPromise();
|
2943
3073
|
}
|
2944
3074
|
requestFinished(request) {
|
2945
|
-
this.element
|
3075
|
+
clearBusyState(this.element);
|
2946
3076
|
}
|
2947
|
-
formSubmissionStarted(
|
2948
|
-
|
2949
|
-
frame.setAttribute("busy", "");
|
3077
|
+
formSubmissionStarted({formElement: formElement}) {
|
3078
|
+
markAsBusy(formElement, this.findFrameElement(formElement));
|
2950
3079
|
}
|
2951
3080
|
formSubmissionSucceededWithResponse(formSubmission, response) {
|
2952
3081
|
const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
|
3082
|
+
this.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
|
2953
3083
|
frame.delegate.loadResponse(response);
|
2954
3084
|
}
|
2955
3085
|
formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
|
@@ -2958,9 +3088,8 @@ class FrameController {
|
|
2958
3088
|
formSubmissionErrored(formSubmission, error) {
|
2959
3089
|
console.error(error);
|
2960
3090
|
}
|
2961
|
-
formSubmissionFinished(
|
2962
|
-
|
2963
|
-
frame.removeAttribute("busy");
|
3091
|
+
formSubmissionFinished({formElement: formElement}) {
|
3092
|
+
clearBusyState(formElement, this.findFrameElement(formElement));
|
2964
3093
|
}
|
2965
3094
|
allowsImmediateRender(snapshot, resume) {
|
2966
3095
|
return true;
|
@@ -2968,10 +3097,14 @@ class FrameController {
|
|
2968
3097
|
viewRenderedSnapshot(snapshot, isPreview) {}
|
2969
3098
|
viewInvalidated() {}
|
2970
3099
|
async visit(url) {
|
2971
|
-
|
3100
|
+
var _a;
|
3101
|
+
const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams, this.element);
|
3102
|
+
(_a = this.currentFetchRequest) === null || _a === void 0 ? void 0 : _a.cancel();
|
3103
|
+
this.currentFetchRequest = request;
|
2972
3104
|
return new Promise((resolve => {
|
2973
3105
|
this.resolveVisitPromise = () => {
|
2974
3106
|
this.resolveVisitPromise = () => {};
|
3107
|
+
this.currentFetchRequest = null;
|
2975
3108
|
resolve();
|
2976
3109
|
};
|
2977
3110
|
request.perform();
|
@@ -2979,12 +3112,36 @@ class FrameController {
|
|
2979
3112
|
}
|
2980
3113
|
navigateFrame(element, url, submitter) {
|
2981
3114
|
const frame = this.findFrameElement(element, submitter);
|
3115
|
+
this.proposeVisitIfNavigatedWithAction(frame, element, submitter);
|
2982
3116
|
frame.setAttribute("reloadable", "");
|
2983
3117
|
frame.src = url;
|
2984
3118
|
}
|
3119
|
+
proposeVisitIfNavigatedWithAction(frame, element, submitter) {
|
3120
|
+
const action = getAttribute("data-turbo-action", submitter, element, frame);
|
3121
|
+
if (isAction(action)) {
|
3122
|
+
const {visitCachedSnapshot: visitCachedSnapshot} = new SnapshotSubstitution(frame);
|
3123
|
+
frame.delegate.fetchResponseLoaded = fetchResponse => {
|
3124
|
+
if (frame.src) {
|
3125
|
+
const {statusCode: statusCode, redirected: redirected} = fetchResponse;
|
3126
|
+
const responseHTML = frame.ownerDocument.documentElement.outerHTML;
|
3127
|
+
const response = {
|
3128
|
+
statusCode: statusCode,
|
3129
|
+
redirected: redirected,
|
3130
|
+
responseHTML: responseHTML
|
3131
|
+
};
|
3132
|
+
session.visit(frame.src, {
|
3133
|
+
action: action,
|
3134
|
+
response: response,
|
3135
|
+
visitCachedSnapshot: visitCachedSnapshot,
|
3136
|
+
willRender: false
|
3137
|
+
});
|
3138
|
+
}
|
3139
|
+
};
|
3140
|
+
}
|
3141
|
+
}
|
2985
3142
|
findFrameElement(element, submitter) {
|
2986
3143
|
var _a;
|
2987
|
-
const id =
|
3144
|
+
const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
|
2988
3145
|
return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
|
2989
3146
|
}
|
2990
3147
|
async extractForeignFrameElement(container) {
|
@@ -3004,8 +3161,15 @@ class FrameController {
|
|
3004
3161
|
}
|
3005
3162
|
return new FrameElement;
|
3006
3163
|
}
|
3164
|
+
formActionIsVisitable(form, submitter) {
|
3165
|
+
const action = getAction(form, submitter);
|
3166
|
+
return locationIsVisitable(expandURL(action), this.rootLocation);
|
3167
|
+
}
|
3007
3168
|
shouldInterceptNavigation(element, submitter) {
|
3008
|
-
const id =
|
3169
|
+
const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
|
3170
|
+
if (element instanceof HTMLFormElement && !this.formActionIsVisitable(element, submitter)) {
|
3171
|
+
return false;
|
3172
|
+
}
|
3009
3173
|
if (!this.enabled || id == "_top") {
|
3010
3174
|
return false;
|
3011
3175
|
}
|
@@ -3061,6 +3225,24 @@ class FrameController {
|
|
3061
3225
|
get isActive() {
|
3062
3226
|
return this.element.isActive && this.connected;
|
3063
3227
|
}
|
3228
|
+
get rootLocation() {
|
3229
|
+
var _a;
|
3230
|
+
const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
|
3231
|
+
const root = (_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/";
|
3232
|
+
return expandURL(root);
|
3233
|
+
}
|
3234
|
+
}
|
3235
|
+
|
3236
|
+
class SnapshotSubstitution {
|
3237
|
+
constructor(element) {
|
3238
|
+
this.visitCachedSnapshot = ({element: element}) => {
|
3239
|
+
var _a;
|
3240
|
+
const {id: id, clone: clone} = this;
|
3241
|
+
(_a = element.querySelector("#" + id)) === null || _a === void 0 ? void 0 : _a.replaceWith(clone);
|
3242
|
+
};
|
3243
|
+
this.clone = element.cloneNode(true);
|
3244
|
+
this.id = element.id;
|
3245
|
+
}
|
3064
3246
|
}
|
3065
3247
|
|
3066
3248
|
function getFrameElementById(id) {
|
@@ -3083,6 +3265,7 @@ function activateElement(element, currentURL) {
|
|
3083
3265
|
}
|
3084
3266
|
if (element instanceof FrameElement) {
|
3085
3267
|
element.connectedCallback();
|
3268
|
+
element.disconnectedCallback();
|
3086
3269
|
return element;
|
3087
3270
|
}
|
3088
3271
|
}
|
@@ -3263,10 +3446,11 @@ var turbo_es2017Esm = Object.freeze({
|
|
3263
3446
|
clearCache: clearCache,
|
3264
3447
|
connectStreamSource: connectStreamSource,
|
3265
3448
|
disconnectStreamSource: disconnectStreamSource,
|
3266
|
-
navigator: navigator,
|
3449
|
+
navigator: navigator$1,
|
3267
3450
|
registerAdapter: registerAdapter,
|
3268
3451
|
renderStreamMessage: renderStreamMessage,
|
3269
3452
|
session: session,
|
3453
|
+
setConfirmMethod: setConfirmMethod,
|
3270
3454
|
setProgressBarDelay: setProgressBarDelay,
|
3271
3455
|
start: start,
|
3272
3456
|
visit: visit
|