turbo-rails 0.8.0 → 0.9.0
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 +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
|