turbo-rails 0.8.2 → 1.0.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 +44 -11
- data/app/assets/javascripts/turbo.js +268 -91
- data/app/assets/javascripts/turbo.min.js +6 -5
- data/app/assets/javascripts/turbo.min.js.map +1 -0
- data/lib/install/turbo_with_importmap.rb +1 -1
- data/lib/turbo/engine.rb +1 -1
- data/lib/turbo/version.rb +1 -1
- metadata +23 -8
@@ -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) {
|
@@ -160,6 +185,11 @@ function getAnchor(url) {
|
|
160
185
|
}
|
161
186
|
}
|
162
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
|
+
|
163
193
|
function getExtension(url) {
|
164
194
|
return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
|
165
195
|
}
|
@@ -173,6 +203,10 @@ function isPrefixedBy(baseURL, url) {
|
|
173
203
|
return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
|
174
204
|
}
|
175
205
|
|
206
|
+
function locationIsVisitable(location, rootLocation) {
|
207
|
+
return isPrefixedBy(location, rootLocation) && isHTML(location);
|
208
|
+
}
|
209
|
+
|
176
210
|
function getRequestURL(url) {
|
177
211
|
const anchor = getAnchor(url);
|
178
212
|
return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href;
|
@@ -308,6 +342,31 @@ function uuid() {
|
|
308
342
|
})).join("");
|
309
343
|
}
|
310
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
|
+
|
311
370
|
var FetchMethod;
|
312
371
|
|
313
372
|
(function(FetchMethod) {
|
@@ -344,12 +403,8 @@ class FetchRequest {
|
|
344
403
|
this.delegate = delegate;
|
345
404
|
this.method = method;
|
346
405
|
this.headers = this.defaultHeaders;
|
347
|
-
|
348
|
-
|
349
|
-
} else {
|
350
|
-
this.body = body;
|
351
|
-
this.url = location;
|
352
|
-
}
|
406
|
+
this.body = body;
|
407
|
+
this.url = location;
|
353
408
|
this.target = target;
|
354
409
|
}
|
355
410
|
get location() {
|
@@ -407,7 +462,7 @@ class FetchRequest {
|
|
407
462
|
credentials: "same-origin",
|
408
463
|
headers: this.headers,
|
409
464
|
redirect: "follow",
|
410
|
-
body: this.body,
|
465
|
+
body: this.isIdempotent ? null : this.body,
|
411
466
|
signal: this.abortSignal,
|
412
467
|
referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
|
413
468
|
};
|
@@ -429,7 +484,7 @@ class FetchRequest {
|
|
429
484
|
cancelable: true,
|
430
485
|
detail: {
|
431
486
|
fetchOptions: fetchOptions,
|
432
|
-
url: this.url
|
487
|
+
url: this.url,
|
433
488
|
resume: this.resolveRequestPromise
|
434
489
|
},
|
435
490
|
target: this.target
|
@@ -438,20 +493,6 @@ class FetchRequest {
|
|
438
493
|
}
|
439
494
|
}
|
440
495
|
|
441
|
-
function mergeFormDataEntries(url, entries) {
|
442
|
-
const currentSearchParams = new URLSearchParams(url.search);
|
443
|
-
for (const [name, value] of entries) {
|
444
|
-
if (value instanceof File) continue;
|
445
|
-
if (currentSearchParams.has(name)) {
|
446
|
-
currentSearchParams.delete(name);
|
447
|
-
url.searchParams.set(name, value);
|
448
|
-
} else {
|
449
|
-
url.searchParams.append(name, value);
|
450
|
-
}
|
451
|
-
}
|
452
|
-
return url;
|
453
|
-
}
|
454
|
-
|
455
496
|
class AppearanceObserver {
|
456
497
|
constructor(delegate, element) {
|
457
498
|
this.started = false;
|
@@ -553,9 +594,16 @@ class FormSubmission {
|
|
553
594
|
this.formElement = formElement;
|
554
595
|
this.submitter = submitter;
|
555
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
|
+
}
|
556
601
|
this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
|
557
602
|
this.mustRedirect = mustRedirect;
|
558
603
|
}
|
604
|
+
static confirmMethod(message, element) {
|
605
|
+
return confirm(message);
|
606
|
+
}
|
559
607
|
get method() {
|
560
608
|
var _a;
|
561
609
|
const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
|
@@ -566,9 +614,6 @@ class FormSubmission {
|
|
566
614
|
const formElementAction = typeof this.formElement.action === "string" ? this.formElement.action : null;
|
567
615
|
return ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formaction")) || this.formElement.getAttribute("action") || formElementAction || "";
|
568
616
|
}
|
569
|
-
get location() {
|
570
|
-
return expandURL(this.action);
|
571
|
-
}
|
572
617
|
get body() {
|
573
618
|
if (this.enctype == FormEnctype.urlEncoded || this.method == FetchMethod.get) {
|
574
619
|
return new URLSearchParams(this.stringFormData);
|
@@ -586,8 +631,20 @@ class FormSubmission {
|
|
586
631
|
get stringFormData() {
|
587
632
|
return [ ...this.formData ].reduce(((entries, [name, value]) => entries.concat(typeof value == "string" ? [ [ name, value ] ] : [])), []);
|
588
633
|
}
|
634
|
+
get confirmationMessage() {
|
635
|
+
return this.formElement.getAttribute("data-turbo-confirm");
|
636
|
+
}
|
637
|
+
get needsConfirmation() {
|
638
|
+
return this.confirmationMessage !== null;
|
639
|
+
}
|
589
640
|
async start() {
|
590
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
|
+
}
|
591
648
|
if (this.state == initialized) {
|
592
649
|
this.state = requesting;
|
593
650
|
return this.fetchRequest.perform();
|
@@ -611,7 +668,9 @@ class FormSubmission {
|
|
611
668
|
}
|
612
669
|
}
|
613
670
|
requestStarted(request) {
|
671
|
+
var _a;
|
614
672
|
this.state = FormSubmissionState.waiting;
|
673
|
+
(_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
|
615
674
|
dispatch("turbo:submit-start", {
|
616
675
|
target: this.formElement,
|
617
676
|
detail: {
|
@@ -656,7 +715,9 @@ class FormSubmission {
|
|
656
715
|
this.delegate.formSubmissionErrored(this, error);
|
657
716
|
}
|
658
717
|
requestFinished(request) {
|
718
|
+
var _a;
|
659
719
|
this.state = FormSubmissionState.stopped;
|
720
|
+
(_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
|
660
721
|
dispatch("turbo:submit-end", {
|
661
722
|
target: this.formElement,
|
662
723
|
detail: Object.assign({
|
@@ -700,6 +761,16 @@ function responseSucceededWithoutRedirect(response) {
|
|
700
761
|
return response.statusCode == 200 && !response.redirected;
|
701
762
|
}
|
702
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
|
+
|
703
774
|
class Snapshot {
|
704
775
|
constructor(element) {
|
705
776
|
this.element = element;
|
@@ -742,9 +813,10 @@ class FormInterceptor {
|
|
742
813
|
constructor(delegate, element) {
|
743
814
|
this.submitBubbled = event => {
|
744
815
|
const form = event.target;
|
745
|
-
if (form instanceof HTMLFormElement && form.closest("turbo-frame, html") == this.element) {
|
816
|
+
if (!event.defaultPrevented && form instanceof HTMLFormElement && form.closest("turbo-frame, html") == this.element) {
|
746
817
|
const submitter = event.submitter || undefined;
|
747
|
-
|
818
|
+
const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
|
819
|
+
if (method != "dialog" && this.delegate.shouldInterceptFormSubmission(form, submitter)) {
|
748
820
|
event.preventDefault();
|
749
821
|
event.stopImmediatePropagation();
|
750
822
|
this.delegate.formSubmissionIntercepted(form, submitter);
|
@@ -955,10 +1027,11 @@ function createPlaceholderForPermanentElement(permanentElement) {
|
|
955
1027
|
}
|
956
1028
|
|
957
1029
|
class Renderer {
|
958
|
-
constructor(currentSnapshot, newSnapshot, isPreview) {
|
1030
|
+
constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) {
|
959
1031
|
this.currentSnapshot = currentSnapshot;
|
960
1032
|
this.newSnapshot = newSnapshot;
|
961
1033
|
this.isPreview = isPreview;
|
1034
|
+
this.willRender = willRender;
|
962
1035
|
this.promise = new Promise(((resolve, reject) => this.resolvingFunctions = {
|
963
1036
|
resolve: resolve,
|
964
1037
|
reject: reject
|
@@ -1340,7 +1413,9 @@ var VisitState;
|
|
1340
1413
|
|
1341
1414
|
const defaultOptions = {
|
1342
1415
|
action: "advance",
|
1343
|
-
historyChanged: false
|
1416
|
+
historyChanged: false,
|
1417
|
+
visitCachedSnapshot: () => {},
|
1418
|
+
willRender: true
|
1344
1419
|
};
|
1345
1420
|
|
1346
1421
|
var SystemStatusCode;
|
@@ -1363,13 +1438,16 @@ class Visit {
|
|
1363
1438
|
this.delegate = delegate;
|
1364
1439
|
this.location = location;
|
1365
1440
|
this.restorationIdentifier = restorationIdentifier || uuid();
|
1366
|
-
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);
|
1367
1442
|
this.action = action;
|
1368
1443
|
this.historyChanged = historyChanged;
|
1369
1444
|
this.referrer = referrer;
|
1370
1445
|
this.snapshotHTML = snapshotHTML;
|
1371
1446
|
this.response = response;
|
1372
1447
|
this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
|
1448
|
+
this.visitCachedSnapshot = visitCachedSnapshot;
|
1449
|
+
this.willRender = willRender;
|
1450
|
+
this.scrolled = !willRender;
|
1373
1451
|
}
|
1374
1452
|
get adapter() {
|
1375
1453
|
return this.delegate.adapter;
|
@@ -1468,7 +1546,7 @@ class Visit {
|
|
1468
1546
|
this.cacheSnapshot();
|
1469
1547
|
if (this.view.renderPromise) await this.view.renderPromise;
|
1470
1548
|
if (isSuccessful(statusCode) && responseHTML != null) {
|
1471
|
-
await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML));
|
1549
|
+
await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML), false, this.willRender);
|
1472
1550
|
this.adapter.visitRendered(this);
|
1473
1551
|
this.complete();
|
1474
1552
|
} else {
|
@@ -1505,7 +1583,7 @@ class Visit {
|
|
1505
1583
|
this.adapter.visitRendered(this);
|
1506
1584
|
} else {
|
1507
1585
|
if (this.view.renderPromise) await this.view.renderPromise;
|
1508
|
-
await this.view.renderPage(snapshot, isPreview);
|
1586
|
+
await this.view.renderPage(snapshot, isPreview, this.willRender);
|
1509
1587
|
this.adapter.visitRendered(this);
|
1510
1588
|
if (!isPreview) {
|
1511
1589
|
this.complete();
|
@@ -1515,7 +1593,8 @@ class Visit {
|
|
1515
1593
|
}
|
1516
1594
|
}
|
1517
1595
|
followRedirect() {
|
1518
|
-
|
1596
|
+
var _a;
|
1597
|
+
if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
|
1519
1598
|
this.adapter.visitProposedToLocation(this.redirectedToLocation, {
|
1520
1599
|
action: "replace",
|
1521
1600
|
response: this.response
|
@@ -1537,34 +1616,41 @@ class Visit {
|
|
1537
1616
|
requestPreventedHandlingResponse(request, response) {}
|
1538
1617
|
async requestSucceededWithResponse(request, response) {
|
1539
1618
|
const responseHTML = await response.responseHTML;
|
1619
|
+
const {redirected: redirected, statusCode: statusCode} = response;
|
1540
1620
|
if (responseHTML == undefined) {
|
1541
1621
|
this.recordResponse({
|
1542
|
-
statusCode: SystemStatusCode.contentTypeMismatch
|
1622
|
+
statusCode: SystemStatusCode.contentTypeMismatch,
|
1623
|
+
redirected: redirected
|
1543
1624
|
});
|
1544
1625
|
} else {
|
1545
1626
|
this.redirectedToLocation = response.redirected ? response.location : undefined;
|
1546
1627
|
this.recordResponse({
|
1547
|
-
statusCode:
|
1548
|
-
responseHTML: responseHTML
|
1628
|
+
statusCode: statusCode,
|
1629
|
+
responseHTML: responseHTML,
|
1630
|
+
redirected: redirected
|
1549
1631
|
});
|
1550
1632
|
}
|
1551
1633
|
}
|
1552
1634
|
async requestFailedWithResponse(request, response) {
|
1553
1635
|
const responseHTML = await response.responseHTML;
|
1636
|
+
const {redirected: redirected, statusCode: statusCode} = response;
|
1554
1637
|
if (responseHTML == undefined) {
|
1555
1638
|
this.recordResponse({
|
1556
|
-
statusCode: SystemStatusCode.contentTypeMismatch
|
1639
|
+
statusCode: SystemStatusCode.contentTypeMismatch,
|
1640
|
+
redirected: redirected
|
1557
1641
|
});
|
1558
1642
|
} else {
|
1559
1643
|
this.recordResponse({
|
1560
|
-
statusCode:
|
1561
|
-
responseHTML: responseHTML
|
1644
|
+
statusCode: statusCode,
|
1645
|
+
responseHTML: responseHTML,
|
1646
|
+
redirected: redirected
|
1562
1647
|
});
|
1563
1648
|
}
|
1564
1649
|
}
|
1565
1650
|
requestErrored(request, error) {
|
1566
1651
|
this.recordResponse({
|
1567
|
-
statusCode: SystemStatusCode.networkFailure
|
1652
|
+
statusCode: SystemStatusCode.networkFailure,
|
1653
|
+
redirected: false
|
1568
1654
|
});
|
1569
1655
|
}
|
1570
1656
|
requestFinished() {
|
@@ -1622,12 +1708,12 @@ class Visit {
|
|
1622
1708
|
} else if (this.action == "restore") {
|
1623
1709
|
return !this.hasCachedSnapshot();
|
1624
1710
|
} else {
|
1625
|
-
return
|
1711
|
+
return this.willRender;
|
1626
1712
|
}
|
1627
1713
|
}
|
1628
1714
|
cacheSnapshot() {
|
1629
1715
|
if (!this.snapshotCached) {
|
1630
|
-
this.view.cacheSnapshot();
|
1716
|
+
this.view.cacheSnapshot().then((snapshot => snapshot && this.visitCachedSnapshot(snapshot)));
|
1631
1717
|
this.snapshotCached = true;
|
1632
1718
|
}
|
1633
1719
|
}
|
@@ -1664,10 +1750,10 @@ class BrowserAdapter {
|
|
1664
1750
|
this.navigator.startVisit(location, uuid(), options);
|
1665
1751
|
}
|
1666
1752
|
visitStarted(visit) {
|
1753
|
+
visit.loadCachedSnapshot();
|
1667
1754
|
visit.issueRequest();
|
1668
1755
|
visit.changeHistory();
|
1669
1756
|
visit.goToSamePageAnchor();
|
1670
|
-
visit.loadCachedSnapshot();
|
1671
1757
|
}
|
1672
1758
|
visitRequestStarted(visit) {
|
1673
1759
|
this.progressBar.setValue(0);
|
@@ -1775,7 +1861,7 @@ class FormSubmitObserver {
|
|
1775
1861
|
const form = event.target instanceof HTMLFormElement ? event.target : undefined;
|
1776
1862
|
const submitter = event.submitter || undefined;
|
1777
1863
|
if (form) {
|
1778
|
-
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");
|
1779
1865
|
if (method != "dialog" && this.delegate.willSubmitForm(form, submitter)) {
|
1780
1866
|
event.preventDefault();
|
1781
1867
|
this.delegate.formSubmitted(form, submitter);
|
@@ -1819,12 +1905,11 @@ class FrameRedirector {
|
|
1819
1905
|
linkClickIntercepted(element, url) {
|
1820
1906
|
const frame = this.findFrameElement(element);
|
1821
1907
|
if (frame) {
|
1822
|
-
frame.
|
1823
|
-
frame.src = url;
|
1908
|
+
frame.delegate.linkClickIntercepted(element, url);
|
1824
1909
|
}
|
1825
1910
|
}
|
1826
1911
|
shouldInterceptFormSubmission(element, submitter) {
|
1827
|
-
return this.
|
1912
|
+
return this.shouldSubmit(element, submitter);
|
1828
1913
|
}
|
1829
1914
|
formSubmissionIntercepted(element, submitter) {
|
1830
1915
|
const frame = this.findFrameElement(element, submitter);
|
@@ -1833,6 +1918,13 @@ class FrameRedirector {
|
|
1833
1918
|
frame.delegate.formSubmissionIntercepted(element, submitter);
|
1834
1919
|
}
|
1835
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
|
+
}
|
1836
1928
|
shouldRedirect(element, submitter) {
|
1837
1929
|
const frame = this.findFrameElement(element, submitter);
|
1838
1930
|
return frame ? frame != element.closest("turbo-frame") : false;
|
@@ -1988,7 +2080,11 @@ class Navigator {
|
|
1988
2080
|
}
|
1989
2081
|
proposeVisit(location, options = {}) {
|
1990
2082
|
if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
|
1991
|
-
|
2083
|
+
if (locationIsVisitable(location, this.view.snapshot.rootLocation)) {
|
2084
|
+
this.delegate.visitProposedToLocation(location, options);
|
2085
|
+
} else {
|
2086
|
+
window.location.href = location.toString();
|
2087
|
+
}
|
1992
2088
|
}
|
1993
2089
|
}
|
1994
2090
|
startVisit(locatable, restorationIdentifier, options = {}) {
|
@@ -2001,13 +2097,7 @@ class Navigator {
|
|
2001
2097
|
submitForm(form, submitter) {
|
2002
2098
|
this.stop();
|
2003
2099
|
this.formSubmission = new FormSubmission(this, form, submitter, true);
|
2004
|
-
|
2005
|
-
this.proposeVisit(this.formSubmission.fetchRequest.url, {
|
2006
|
-
action: this.getActionForFormSubmission(this.formSubmission)
|
2007
|
-
});
|
2008
|
-
} else {
|
2009
|
-
this.formSubmission.start();
|
2010
|
-
}
|
2100
|
+
this.formSubmission.start();
|
2011
2101
|
}
|
2012
2102
|
stop() {
|
2013
2103
|
if (this.formSubmission) {
|
@@ -2040,11 +2130,14 @@ class Navigator {
|
|
2040
2130
|
if (formSubmission.method != FetchMethod.get) {
|
2041
2131
|
this.view.clearSnapshotCache();
|
2042
2132
|
}
|
2043
|
-
const {statusCode: statusCode} = fetchResponse;
|
2133
|
+
const {statusCode: statusCode, redirected: redirected} = fetchResponse;
|
2134
|
+
const action = this.getActionForFormSubmission(formSubmission);
|
2044
2135
|
const visitOptions = {
|
2136
|
+
action: action,
|
2045
2137
|
response: {
|
2046
2138
|
statusCode: statusCode,
|
2047
|
-
responseHTML: responseHTML
|
2139
|
+
responseHTML: responseHTML,
|
2140
|
+
redirected: redirected
|
2048
2141
|
}
|
2049
2142
|
};
|
2050
2143
|
this.proposeVisit(fetchResponse.location, visitOptions);
|
@@ -2095,7 +2188,7 @@ class Navigator {
|
|
2095
2188
|
}
|
2096
2189
|
getActionForFormSubmission(formSubmission) {
|
2097
2190
|
const {formElement: formElement, submitter: submitter} = formSubmission;
|
2098
|
-
const action =
|
2191
|
+
const action = getAttribute("data-turbo-action", submitter, formElement);
|
2099
2192
|
return isAction(action) ? action : "advance";
|
2100
2193
|
}
|
2101
2194
|
}
|
@@ -2295,7 +2388,9 @@ class PageRenderer extends Renderer {
|
|
2295
2388
|
this.mergeHead();
|
2296
2389
|
}
|
2297
2390
|
async render() {
|
2298
|
-
this.
|
2391
|
+
if (this.willRender) {
|
2392
|
+
this.replaceBody();
|
2393
|
+
}
|
2299
2394
|
}
|
2300
2395
|
finishRendering() {
|
2301
2396
|
super.finishRendering();
|
@@ -2431,8 +2526,8 @@ class PageView extends View {
|
|
2431
2526
|
this.snapshotCache = new SnapshotCache(10);
|
2432
2527
|
this.lastRenderedLocation = new URL(location.href);
|
2433
2528
|
}
|
2434
|
-
renderPage(snapshot, isPreview = false) {
|
2435
|
-
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);
|
2436
2531
|
return this.render(renderer);
|
2437
2532
|
}
|
2438
2533
|
renderError(snapshot) {
|
@@ -2447,7 +2542,9 @@ class PageView extends View {
|
|
2447
2542
|
this.delegate.viewWillCacheSnapshot();
|
2448
2543
|
const {snapshot: snapshot, lastRenderedLocation: location} = this;
|
2449
2544
|
await nextEventLoopTick();
|
2450
|
-
|
2545
|
+
const cachedSnapshot = snapshot.clone();
|
2546
|
+
this.snapshotCache.put(location, cachedSnapshot);
|
2547
|
+
return cachedSnapshot;
|
2451
2548
|
}
|
2452
2549
|
}
|
2453
2550
|
getCachedSnapshotForLocation(location) {
|
@@ -2552,7 +2649,7 @@ class Session {
|
|
2552
2649
|
});
|
2553
2650
|
}
|
2554
2651
|
willFollowLinkToLocation(link, location) {
|
2555
|
-
return this.elementDriveEnabled(link) &&
|
2652
|
+
return this.elementDriveEnabled(link) && locationIsVisitable(location, this.snapshot.rootLocation) && this.applicationAllowsFollowingLinkToLocation(link, location);
|
2556
2653
|
}
|
2557
2654
|
followedLinkToLocation(link, location) {
|
2558
2655
|
const action = this.getActionForLink(link);
|
@@ -2561,14 +2658,23 @@ class Session {
|
|
2561
2658
|
});
|
2562
2659
|
}
|
2563
2660
|
convertLinkWithMethodClickToFormSubmission(link) {
|
2564
|
-
var _a;
|
2565
2661
|
const linkMethod = link.getAttribute("data-turbo-method");
|
2566
2662
|
if (linkMethod) {
|
2567
2663
|
const form = document.createElement("form");
|
2568
2664
|
form.method = linkMethod;
|
2569
2665
|
form.action = link.getAttribute("href") || "undefined";
|
2570
2666
|
form.hidden = true;
|
2571
|
-
(
|
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);
|
2572
2678
|
return dispatch("submit", {
|
2573
2679
|
cancelable: true,
|
2574
2680
|
target: form
|
@@ -2600,7 +2706,8 @@ class Session {
|
|
2600
2706
|
this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
|
2601
2707
|
}
|
2602
2708
|
willSubmitForm(form, submitter) {
|
2603
|
-
|
2709
|
+
const action = getAction(form, submitter);
|
2710
|
+
return this.elementDriveEnabled(form) && (!submitter || this.elementDriveEnabled(submitter)) && locationIsVisitable(expandURL(action), this.snapshot.rootLocation);
|
2604
2711
|
}
|
2605
2712
|
formSubmitted(form, submitter) {
|
2606
2713
|
this.navigator.submitForm(form, submitter);
|
@@ -2667,6 +2774,7 @@ class Session {
|
|
2667
2774
|
});
|
2668
2775
|
}
|
2669
2776
|
notifyApplicationAfterVisitingLocation(location, action) {
|
2777
|
+
markAsBusy(document.documentElement);
|
2670
2778
|
return dispatch("turbo:visit", {
|
2671
2779
|
detail: {
|
2672
2780
|
url: location.href,
|
@@ -2690,6 +2798,7 @@ class Session {
|
|
2690
2798
|
return dispatch("turbo:render");
|
2691
2799
|
}
|
2692
2800
|
notifyApplicationAfterPageLoad(timing = {}) {
|
2801
|
+
clearBusyState(document.documentElement);
|
2693
2802
|
return dispatch("turbo:load", {
|
2694
2803
|
detail: {
|
2695
2804
|
url: this.location.href,
|
@@ -2737,8 +2846,16 @@ class Session {
|
|
2737
2846
|
const action = link.getAttribute("data-turbo-action");
|
2738
2847
|
return isAction(action) ? action : "advance";
|
2739
2848
|
}
|
2740
|
-
|
2741
|
-
|
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
|
+
}
|
2742
2859
|
}
|
2743
2860
|
get snapshot() {
|
2744
2861
|
return this.view.snapshot;
|
@@ -2793,6 +2910,10 @@ function setProgressBarDelay(delay) {
|
|
2793
2910
|
session.setProgressBarDelay(delay);
|
2794
2911
|
}
|
2795
2912
|
|
2913
|
+
function setConfirmMethod(confirmMethod) {
|
2914
|
+
FormSubmission.confirmMethod = confirmMethod;
|
2915
|
+
}
|
2916
|
+
|
2796
2917
|
var Turbo = Object.freeze({
|
2797
2918
|
__proto__: null,
|
2798
2919
|
navigator: navigator$1,
|
@@ -2806,11 +2927,14 @@ var Turbo = Object.freeze({
|
|
2806
2927
|
disconnectStreamSource: disconnectStreamSource,
|
2807
2928
|
renderStreamMessage: renderStreamMessage,
|
2808
2929
|
clearCache: clearCache,
|
2809
|
-
setProgressBarDelay: setProgressBarDelay
|
2930
|
+
setProgressBarDelay: setProgressBarDelay,
|
2931
|
+
setConfirmMethod: setConfirmMethod
|
2810
2932
|
});
|
2811
2933
|
|
2812
2934
|
class FrameController {
|
2813
2935
|
constructor(element) {
|
2936
|
+
this.fetchResponseLoaded = fetchResponse => {};
|
2937
|
+
this.currentFetchRequest = null;
|
2814
2938
|
this.resolveVisitPromise = () => {};
|
2815
2939
|
this.connected = false;
|
2816
2940
|
this.hasBeenLoaded = false;
|
@@ -2865,11 +2989,10 @@ class FrameController {
|
|
2865
2989
|
this.currentURL = this.sourceURL;
|
2866
2990
|
if (this.sourceURL) {
|
2867
2991
|
try {
|
2868
|
-
this.element.loaded = this.visit(this.sourceURL);
|
2992
|
+
this.element.loaded = this.visit(expandURL(this.sourceURL));
|
2869
2993
|
this.appearanceObserver.stop();
|
2870
2994
|
await this.element.loaded;
|
2871
2995
|
this.hasBeenLoaded = true;
|
2872
|
-
session.frameLoaded(this.element);
|
2873
2996
|
} catch (error) {
|
2874
2997
|
this.currentURL = previousURL;
|
2875
2998
|
throw error;
|
@@ -2878,7 +3001,7 @@ class FrameController {
|
|
2878
3001
|
}
|
2879
3002
|
}
|
2880
3003
|
async loadResponse(fetchResponse) {
|
2881
|
-
if (fetchResponse.redirected) {
|
3004
|
+
if (fetchResponse.redirected || fetchResponse.succeeded && fetchResponse.isHTML) {
|
2882
3005
|
this.sourceURL = fetchResponse.response.url;
|
2883
3006
|
}
|
2884
3007
|
try {
|
@@ -2886,14 +3009,18 @@ class FrameController {
|
|
2886
3009
|
if (html) {
|
2887
3010
|
const {body: body} = parseHTMLDocument(html);
|
2888
3011
|
const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
|
2889
|
-
const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
|
3012
|
+
const renderer = new FrameRenderer(this.view.snapshot, snapshot, false, false);
|
2890
3013
|
if (this.view.renderPromise) await this.view.renderPromise;
|
2891
3014
|
await this.view.render(renderer);
|
2892
3015
|
session.frameRendered(fetchResponse, this.element);
|
3016
|
+
session.frameLoaded(this.element);
|
3017
|
+
this.fetchResponseLoaded(fetchResponse);
|
2893
3018
|
}
|
2894
3019
|
} catch (error) {
|
2895
3020
|
console.error(error);
|
2896
3021
|
this.view.invalidate();
|
3022
|
+
} finally {
|
3023
|
+
this.fetchResponseLoaded = () => {};
|
2897
3024
|
}
|
2898
3025
|
}
|
2899
3026
|
elementAppearedInViewport(element) {
|
@@ -2919,19 +3046,15 @@ class FrameController {
|
|
2919
3046
|
}
|
2920
3047
|
this.reloadable = false;
|
2921
3048
|
this.formSubmission = new FormSubmission(this, element, submitter);
|
2922
|
-
|
2923
|
-
|
2924
|
-
|
2925
|
-
const {fetchRequest: fetchRequest} = this.formSubmission;
|
2926
|
-
this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
|
2927
|
-
this.formSubmission.start();
|
2928
|
-
}
|
3049
|
+
const {fetchRequest: fetchRequest} = this.formSubmission;
|
3050
|
+
this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
|
3051
|
+
this.formSubmission.start();
|
2929
3052
|
}
|
2930
3053
|
prepareHeadersForRequest(headers, request) {
|
2931
3054
|
headers["Turbo-Frame"] = this.id;
|
2932
3055
|
}
|
2933
3056
|
requestStarted(request) {
|
2934
|
-
this.element
|
3057
|
+
markAsBusy(this.element);
|
2935
3058
|
}
|
2936
3059
|
requestPreventedHandlingResponse(request, response) {
|
2937
3060
|
this.resolveVisitPromise();
|
@@ -2949,14 +3072,14 @@ class FrameController {
|
|
2949
3072
|
this.resolveVisitPromise();
|
2950
3073
|
}
|
2951
3074
|
requestFinished(request) {
|
2952
|
-
this.element
|
3075
|
+
clearBusyState(this.element);
|
2953
3076
|
}
|
2954
|
-
formSubmissionStarted(
|
2955
|
-
|
2956
|
-
frame.setAttribute("busy", "");
|
3077
|
+
formSubmissionStarted({formElement: formElement}) {
|
3078
|
+
markAsBusy(formElement, this.findFrameElement(formElement));
|
2957
3079
|
}
|
2958
3080
|
formSubmissionSucceededWithResponse(formSubmission, response) {
|
2959
3081
|
const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
|
3082
|
+
this.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
|
2960
3083
|
frame.delegate.loadResponse(response);
|
2961
3084
|
}
|
2962
3085
|
formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
|
@@ -2965,9 +3088,8 @@ class FrameController {
|
|
2965
3088
|
formSubmissionErrored(formSubmission, error) {
|
2966
3089
|
console.error(error);
|
2967
3090
|
}
|
2968
|
-
formSubmissionFinished(
|
2969
|
-
|
2970
|
-
frame.removeAttribute("busy");
|
3091
|
+
formSubmissionFinished({formElement: formElement}) {
|
3092
|
+
clearBusyState(formElement, this.findFrameElement(formElement));
|
2971
3093
|
}
|
2972
3094
|
allowsImmediateRender(snapshot, resume) {
|
2973
3095
|
return true;
|
@@ -2975,10 +3097,14 @@ class FrameController {
|
|
2975
3097
|
viewRenderedSnapshot(snapshot, isPreview) {}
|
2976
3098
|
viewInvalidated() {}
|
2977
3099
|
async visit(url) {
|
2978
|
-
|
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;
|
2979
3104
|
return new Promise((resolve => {
|
2980
3105
|
this.resolveVisitPromise = () => {
|
2981
3106
|
this.resolveVisitPromise = () => {};
|
3107
|
+
this.currentFetchRequest = null;
|
2982
3108
|
resolve();
|
2983
3109
|
};
|
2984
3110
|
request.perform();
|
@@ -2986,12 +3112,36 @@ class FrameController {
|
|
2986
3112
|
}
|
2987
3113
|
navigateFrame(element, url, submitter) {
|
2988
3114
|
const frame = this.findFrameElement(element, submitter);
|
3115
|
+
this.proposeVisitIfNavigatedWithAction(frame, element, submitter);
|
2989
3116
|
frame.setAttribute("reloadable", "");
|
2990
3117
|
frame.src = url;
|
2991
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
|
+
}
|
2992
3142
|
findFrameElement(element, submitter) {
|
2993
3143
|
var _a;
|
2994
|
-
const id =
|
3144
|
+
const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
|
2995
3145
|
return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
|
2996
3146
|
}
|
2997
3147
|
async extractForeignFrameElement(container) {
|
@@ -3011,8 +3161,15 @@ class FrameController {
|
|
3011
3161
|
}
|
3012
3162
|
return new FrameElement;
|
3013
3163
|
}
|
3164
|
+
formActionIsVisitable(form, submitter) {
|
3165
|
+
const action = getAction(form, submitter);
|
3166
|
+
return locationIsVisitable(expandURL(action), this.rootLocation);
|
3167
|
+
}
|
3014
3168
|
shouldInterceptNavigation(element, submitter) {
|
3015
|
-
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
|
+
}
|
3016
3173
|
if (!this.enabled || id == "_top") {
|
3017
3174
|
return false;
|
3018
3175
|
}
|
@@ -3068,6 +3225,24 @@ class FrameController {
|
|
3068
3225
|
get isActive() {
|
3069
3226
|
return this.element.isActive && this.connected;
|
3070
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
|
+
}
|
3071
3246
|
}
|
3072
3247
|
|
3073
3248
|
function getFrameElementById(id) {
|
@@ -3090,6 +3265,7 @@ function activateElement(element, currentURL) {
|
|
3090
3265
|
}
|
3091
3266
|
if (element instanceof FrameElement) {
|
3092
3267
|
element.connectedCallback();
|
3268
|
+
element.disconnectedCallback();
|
3093
3269
|
return element;
|
3094
3270
|
}
|
3095
3271
|
}
|
@@ -3274,6 +3450,7 @@ var turbo_es2017Esm = Object.freeze({
|
|
3274
3450
|
registerAdapter: registerAdapter,
|
3275
3451
|
renderStreamMessage: renderStreamMessage,
|
3276
3452
|
session: session,
|
3453
|
+
setConfirmMethod: setConfirmMethod,
|
3277
3454
|
setProgressBarDelay: setProgressBarDelay,
|
3278
3455
|
start: start,
|
3279
3456
|
visit: visit
|