turbo-rails 1.3.2 → 1.5.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 +48 -3
- data/Rakefile +15 -2
- data/app/assets/javascripts/turbo.js +280 -127
- data/app/assets/javascripts/turbo.min.js +5 -5
- data/app/assets/javascripts/turbo.min.js.map +1 -1
- data/app/controllers/turbo/frames/frame_request.rb +17 -7
- data/app/controllers/turbo/native/navigation.rb +19 -9
- data/app/helpers/turbo/frames_helper.rb +1 -1
- data/app/helpers/turbo/streams/action_helper.rb +17 -5
- data/app/javascript/turbo/cable_stream_source_element.js +17 -2
- data/app/javascript/turbo/fetch_requests.js +43 -3
- data/app/models/concerns/turbo/broadcastable.rb +42 -10
- data/app/models/turbo/streams/tag_builder.rb +2 -0
- data/app/views/layouts/turbo_rails/frame.html.erb +8 -0
- data/config/routes.rb +1 -1
- data/lib/install/turbo_with_bun.rb +9 -0
- data/lib/tasks/turbo_tasks.rake +35 -18
- data/lib/turbo/broadcastable/test_helper.rb +172 -0
- data/lib/turbo/engine.rb +15 -1
- data/lib/turbo/test_assertions/integration_test_assertions.rb +76 -0
- data/lib/turbo/test_assertions.rb +61 -5
- data/lib/turbo/version.rb +1 -1
- data/lib/turbo-rails.rb +2 -0
- metadata +7 -3
@@ -56,13 +56,11 @@ function clickCaptured(event) {
|
|
56
56
|
|
57
57
|
(function() {
|
58
58
|
if ("submitter" in Event.prototype) return;
|
59
|
-
let prototype;
|
59
|
+
let prototype = window.Event.prototype;
|
60
60
|
if ("SubmitEvent" in window && /Apple Computer/.test(navigator.vendor)) {
|
61
61
|
prototype = window.SubmitEvent.prototype;
|
62
62
|
} else if ("SubmitEvent" in window) {
|
63
63
|
return;
|
64
|
-
} else {
|
65
|
-
prototype = window.Event.prototype;
|
66
64
|
}
|
67
65
|
addEventListener("click", clickCaptured, true);
|
68
66
|
Object.defineProperty(prototype, "submitter", {
|
@@ -82,14 +80,14 @@ var FrameLoadingStyle;
|
|
82
80
|
})(FrameLoadingStyle || (FrameLoadingStyle = {}));
|
83
81
|
|
84
82
|
class FrameElement extends HTMLElement {
|
83
|
+
static get observedAttributes() {
|
84
|
+
return [ "disabled", "complete", "loading", "src" ];
|
85
|
+
}
|
85
86
|
constructor() {
|
86
87
|
super();
|
87
88
|
this.loaded = Promise.resolve();
|
88
89
|
this.delegate = new FrameElement.delegateConstructor(this);
|
89
90
|
}
|
90
|
-
static get observedAttributes() {
|
91
|
-
return [ "disabled", "complete", "loading", "src" ];
|
92
|
-
}
|
93
91
|
connectedCallback() {
|
94
92
|
this.delegate.connect();
|
95
93
|
}
|
@@ -282,10 +280,6 @@ class FetchResponse {
|
|
282
280
|
}
|
283
281
|
}
|
284
282
|
|
285
|
-
function isAction(action) {
|
286
|
-
return action == "advance" || action == "replace" || action == "restore";
|
287
|
-
}
|
288
|
-
|
289
283
|
function activateScriptElement(element) {
|
290
284
|
if (element.getAttribute("data-turbo-eval") == "false") {
|
291
285
|
return element;
|
@@ -318,6 +312,7 @@ function dispatch(eventName, {target: target, cancelable: cancelable, detail: de
|
|
318
312
|
const event = new CustomEvent(eventName, {
|
319
313
|
cancelable: cancelable,
|
320
314
|
bubbles: true,
|
315
|
+
composed: true,
|
321
316
|
detail: detail
|
322
317
|
});
|
323
318
|
if (target && target.isConnected) {
|
@@ -431,6 +426,10 @@ function getHistoryMethodForAction(action) {
|
|
431
426
|
}
|
432
427
|
}
|
433
428
|
|
429
|
+
function isAction(action) {
|
430
|
+
return action == "advance" || action == "replace" || action == "restore";
|
431
|
+
}
|
432
|
+
|
434
433
|
function getVisitAction(...elements) {
|
435
434
|
const action = getAttribute("data-turbo-action", ...elements);
|
436
435
|
return isAction(action) ? action : null;
|
@@ -456,6 +455,13 @@ function setMetaContent(name, content) {
|
|
456
455
|
return element;
|
457
456
|
}
|
458
457
|
|
458
|
+
function findClosestRecursively(element, selector) {
|
459
|
+
var _a;
|
460
|
+
if (element instanceof Element) {
|
461
|
+
return element.closest(selector) || findClosestRecursively(element.assignedSlot || ((_a = element.getRootNode()) === null || _a === void 0 ? void 0 : _a.host), selector);
|
462
|
+
}
|
463
|
+
}
|
464
|
+
|
459
465
|
var FetchMethod;
|
460
466
|
|
461
467
|
(function(FetchMethod) {
|
@@ -509,9 +515,8 @@ class FetchRequest {
|
|
509
515
|
this.abortController.abort();
|
510
516
|
}
|
511
517
|
async perform() {
|
512
|
-
var _a, _b;
|
513
518
|
const {fetchOptions: fetchOptions} = this;
|
514
|
-
|
519
|
+
this.delegate.prepareRequest(this);
|
515
520
|
await this.allowRequestToBeIntercepted(fetchOptions);
|
516
521
|
try {
|
517
522
|
this.delegate.requestStarted(this);
|
@@ -553,7 +558,7 @@ class FetchRequest {
|
|
553
558
|
credentials: "same-origin",
|
554
559
|
headers: this.headers,
|
555
560
|
redirect: "follow",
|
556
|
-
body: this.
|
561
|
+
body: this.isSafe ? null : this.body,
|
557
562
|
signal: this.abortSignal,
|
558
563
|
referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
|
559
564
|
};
|
@@ -563,8 +568,8 @@ class FetchRequest {
|
|
563
568
|
Accept: "text/html, application/xhtml+xml"
|
564
569
|
};
|
565
570
|
}
|
566
|
-
get
|
567
|
-
return this.method
|
571
|
+
get isSafe() {
|
572
|
+
return this.method === FetchMethod.get;
|
568
573
|
}
|
569
574
|
get abortSignal() {
|
570
575
|
return this.abortController.signal;
|
@@ -626,9 +631,6 @@ class AppearanceObserver {
|
|
626
631
|
}
|
627
632
|
|
628
633
|
class StreamMessage {
|
629
|
-
constructor(fragment) {
|
630
|
-
this.fragment = importStreamElements(fragment);
|
631
|
-
}
|
632
634
|
static wrap(message) {
|
633
635
|
if (typeof message == "string") {
|
634
636
|
return new this(createDocumentFragment(message));
|
@@ -636,6 +638,9 @@ class StreamMessage {
|
|
636
638
|
return message;
|
637
639
|
}
|
638
640
|
}
|
641
|
+
constructor(fragment) {
|
642
|
+
this.fragment = importStreamElements(fragment);
|
643
|
+
}
|
639
644
|
}
|
640
645
|
|
641
646
|
StreamMessage.contentType = "text/vnd.turbo-stream.html";
|
@@ -684,6 +689,9 @@ function formEnctypeFromString(encoding) {
|
|
684
689
|
}
|
685
690
|
|
686
691
|
class FormSubmission {
|
692
|
+
static confirmMethod(message, _element, _submitter) {
|
693
|
+
return Promise.resolve(confirm(message));
|
694
|
+
}
|
687
695
|
constructor(delegate, formElement, submitter, mustRedirect = false) {
|
688
696
|
this.state = FormSubmissionState.initialized;
|
689
697
|
this.delegate = delegate;
|
@@ -697,9 +705,6 @@ class FormSubmission {
|
|
697
705
|
this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
|
698
706
|
this.mustRedirect = mustRedirect;
|
699
707
|
}
|
700
|
-
static confirmMethod(message, _element, _submitter) {
|
701
|
-
return Promise.resolve(confirm(message));
|
702
|
-
}
|
703
708
|
get method() {
|
704
709
|
var _a;
|
705
710
|
const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
|
@@ -725,8 +730,8 @@ class FormSubmission {
|
|
725
730
|
var _a;
|
726
731
|
return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formenctype")) || this.formElement.enctype);
|
727
732
|
}
|
728
|
-
get
|
729
|
-
return this.fetchRequest.
|
733
|
+
get isSafe() {
|
734
|
+
return this.fetchRequest.isSafe;
|
730
735
|
}
|
731
736
|
get stringFormData() {
|
732
737
|
return [ ...this.formData ].reduce(((entries, [name, value]) => entries.concat(typeof value == "string" ? [ [ name, value ] ] : [])), []);
|
@@ -753,11 +758,11 @@ class FormSubmission {
|
|
753
758
|
return true;
|
754
759
|
}
|
755
760
|
}
|
756
|
-
|
757
|
-
if (!request.
|
761
|
+
prepareRequest(request) {
|
762
|
+
if (!request.isSafe) {
|
758
763
|
const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token");
|
759
764
|
if (token) {
|
760
|
-
headers["X-CSRF-Token"] = token;
|
765
|
+
request.headers["X-CSRF-Token"] = token;
|
761
766
|
}
|
762
767
|
}
|
763
768
|
if (this.requestAcceptsTurboStreamResponse(request)) {
|
@@ -768,6 +773,7 @@ class FormSubmission {
|
|
768
773
|
var _a;
|
769
774
|
this.state = FormSubmissionState.waiting;
|
770
775
|
(_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
|
776
|
+
this.setSubmitsWith();
|
771
777
|
dispatch("turbo:submit-start", {
|
772
778
|
target: this.formElement,
|
773
779
|
detail: {
|
@@ -815,6 +821,7 @@ class FormSubmission {
|
|
815
821
|
var _a;
|
816
822
|
this.state = FormSubmissionState.stopped;
|
817
823
|
(_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
|
824
|
+
this.resetSubmitterText();
|
818
825
|
dispatch("turbo:submit-end", {
|
819
826
|
target: this.formElement,
|
820
827
|
detail: Object.assign({
|
@@ -823,11 +830,35 @@ class FormSubmission {
|
|
823
830
|
});
|
824
831
|
this.delegate.formSubmissionFinished(this);
|
825
832
|
}
|
833
|
+
setSubmitsWith() {
|
834
|
+
if (!this.submitter || !this.submitsWith) return;
|
835
|
+
if (this.submitter.matches("button")) {
|
836
|
+
this.originalSubmitText = this.submitter.innerHTML;
|
837
|
+
this.submitter.innerHTML = this.submitsWith;
|
838
|
+
} else if (this.submitter.matches("input")) {
|
839
|
+
const input = this.submitter;
|
840
|
+
this.originalSubmitText = input.value;
|
841
|
+
input.value = this.submitsWith;
|
842
|
+
}
|
843
|
+
}
|
844
|
+
resetSubmitterText() {
|
845
|
+
if (!this.submitter || !this.originalSubmitText) return;
|
846
|
+
if (this.submitter.matches("button")) {
|
847
|
+
this.submitter.innerHTML = this.originalSubmitText;
|
848
|
+
} else if (this.submitter.matches("input")) {
|
849
|
+
const input = this.submitter;
|
850
|
+
input.value = this.originalSubmitText;
|
851
|
+
}
|
852
|
+
}
|
826
853
|
requestMustRedirect(request) {
|
827
|
-
return !request.
|
854
|
+
return !request.isSafe && this.mustRedirect;
|
828
855
|
}
|
829
856
|
requestAcceptsTurboStreamResponse(request) {
|
830
|
-
return !request.
|
857
|
+
return !request.isSafe || hasAttribute("data-turbo-stream", this.submitter, this.formElement);
|
858
|
+
}
|
859
|
+
get submitsWith() {
|
860
|
+
var _a;
|
861
|
+
return (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("data-turbo-submits-with");
|
831
862
|
}
|
832
863
|
}
|
833
864
|
|
@@ -960,11 +991,15 @@ function submissionDoesNotDismissDialog(form, submitter) {
|
|
960
991
|
}
|
961
992
|
|
962
993
|
function submissionDoesNotTargetIFrame(form, submitter) {
|
963
|
-
|
964
|
-
|
965
|
-
|
994
|
+
if ((submitter === null || submitter === void 0 ? void 0 : submitter.hasAttribute("formtarget")) || form.hasAttribute("target")) {
|
995
|
+
const target = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formtarget")) || form.target;
|
996
|
+
for (const element of document.getElementsByName(target)) {
|
997
|
+
if (element instanceof HTMLIFrameElement) return false;
|
998
|
+
}
|
999
|
+
return true;
|
1000
|
+
} else {
|
1001
|
+
return true;
|
966
1002
|
}
|
967
|
-
return true;
|
968
1003
|
}
|
969
1004
|
|
970
1005
|
class View {
|
@@ -1065,8 +1100,8 @@ class View {
|
|
1065
1100
|
}
|
1066
1101
|
|
1067
1102
|
class FrameView extends View {
|
1068
|
-
|
1069
|
-
this.element.innerHTML = ""
|
1103
|
+
missing() {
|
1104
|
+
this.element.innerHTML = `<strong class="turbo-frame-error">Content missing</strong>`;
|
1070
1105
|
}
|
1071
1106
|
get snapshot() {
|
1072
1107
|
return new Snapshot(this.element);
|
@@ -1153,9 +1188,7 @@ class LinkClickObserver {
|
|
1153
1188
|
return !(event.target && event.target.isContentEditable || event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);
|
1154
1189
|
}
|
1155
1190
|
findLinkFromClickTarget(target) {
|
1156
|
-
|
1157
|
-
return target.closest("a[href]:not([target^=_]):not([download])");
|
1158
|
-
}
|
1191
|
+
return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])");
|
1159
1192
|
}
|
1160
1193
|
getLocationForLink(link) {
|
1161
1194
|
return expandURL(link.getAttribute("href") || "");
|
@@ -1163,10 +1196,14 @@ class LinkClickObserver {
|
|
1163
1196
|
}
|
1164
1197
|
|
1165
1198
|
function doesNotTargetIFrame(anchor) {
|
1166
|
-
|
1167
|
-
|
1199
|
+
if (anchor.hasAttribute("target")) {
|
1200
|
+
for (const element of document.getElementsByName(anchor.target)) {
|
1201
|
+
if (element instanceof HTMLIFrameElement) return false;
|
1202
|
+
}
|
1203
|
+
return true;
|
1204
|
+
} else {
|
1205
|
+
return true;
|
1168
1206
|
}
|
1169
|
-
return true;
|
1170
1207
|
}
|
1171
1208
|
|
1172
1209
|
class FormLinkClickObserver {
|
@@ -1184,16 +1221,26 @@ class FormLinkClickObserver {
|
|
1184
1221
|
return this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) && link.hasAttribute("data-turbo-method");
|
1185
1222
|
}
|
1186
1223
|
followedLinkToLocation(link, location) {
|
1187
|
-
const action = location.href;
|
1188
1224
|
const form = document.createElement("form");
|
1225
|
+
const type = "hidden";
|
1226
|
+
for (const [name, value] of location.searchParams) {
|
1227
|
+
form.append(Object.assign(document.createElement("input"), {
|
1228
|
+
type: type,
|
1229
|
+
name: name,
|
1230
|
+
value: value
|
1231
|
+
}));
|
1232
|
+
}
|
1233
|
+
const action = Object.assign(location, {
|
1234
|
+
search: ""
|
1235
|
+
});
|
1189
1236
|
form.setAttribute("data-turbo", "true");
|
1190
|
-
form.setAttribute("action", action);
|
1237
|
+
form.setAttribute("action", action.href);
|
1191
1238
|
form.setAttribute("hidden", "");
|
1192
1239
|
const method = link.getAttribute("data-turbo-method");
|
1193
1240
|
if (method) form.setAttribute("method", method);
|
1194
1241
|
const turboFrame = link.getAttribute("data-turbo-frame");
|
1195
1242
|
if (turboFrame) form.setAttribute("data-turbo-frame", turboFrame);
|
1196
|
-
const turboAction = link
|
1243
|
+
const turboAction = getVisitAction(link);
|
1197
1244
|
if (turboAction) form.setAttribute("data-turbo-action", turboAction);
|
1198
1245
|
const turboConfirm = link.getAttribute("data-turbo-confirm");
|
1199
1246
|
if (turboConfirm) form.setAttribute("data-turbo-confirm", turboConfirm);
|
@@ -1209,16 +1256,16 @@ class FormLinkClickObserver {
|
|
1209
1256
|
}
|
1210
1257
|
|
1211
1258
|
class Bardo {
|
1212
|
-
|
1213
|
-
this.delegate = delegate;
|
1214
|
-
this.permanentElementMap = permanentElementMap;
|
1215
|
-
}
|
1216
|
-
static preservingPermanentElements(delegate, permanentElementMap, callback) {
|
1259
|
+
static async preservingPermanentElements(delegate, permanentElementMap, callback) {
|
1217
1260
|
const bardo = new this(delegate, permanentElementMap);
|
1218
1261
|
bardo.enter();
|
1219
|
-
callback();
|
1262
|
+
await callback();
|
1220
1263
|
bardo.leave();
|
1221
1264
|
}
|
1265
|
+
constructor(delegate, permanentElementMap) {
|
1266
|
+
this.delegate = delegate;
|
1267
|
+
this.permanentElementMap = permanentElementMap;
|
1268
|
+
}
|
1222
1269
|
enter() {
|
1223
1270
|
for (const id in this.permanentElementMap) {
|
1224
1271
|
const [currentPermanentElement, newPermanentElement] = this.permanentElementMap[id];
|
@@ -1289,8 +1336,8 @@ class Renderer {
|
|
1289
1336
|
delete this.resolvingFunctions;
|
1290
1337
|
}
|
1291
1338
|
}
|
1292
|
-
preservingPermanentElements(callback) {
|
1293
|
-
Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
|
1339
|
+
async preservingPermanentElements(callback) {
|
1340
|
+
await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
|
1294
1341
|
}
|
1295
1342
|
focusFirstAutofocusableElement() {
|
1296
1343
|
const element = this.connectedSnapshot.firstAutofocusableElement;
|
@@ -1329,10 +1376,6 @@ function elementIsFocusable(element) {
|
|
1329
1376
|
}
|
1330
1377
|
|
1331
1378
|
class FrameRenderer extends Renderer {
|
1332
|
-
constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
|
1333
|
-
super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender);
|
1334
|
-
this.delegate = delegate;
|
1335
|
-
}
|
1336
1379
|
static renderElement(currentElement, newElement) {
|
1337
1380
|
var _a;
|
1338
1381
|
const destinationRange = document.createRange();
|
@@ -1345,6 +1388,10 @@ class FrameRenderer extends Renderer {
|
|
1345
1388
|
currentElement.appendChild(sourceRange.extractContents());
|
1346
1389
|
}
|
1347
1390
|
}
|
1391
|
+
constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
|
1392
|
+
super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender);
|
1393
|
+
this.delegate = delegate;
|
1394
|
+
}
|
1348
1395
|
get shouldRender() {
|
1349
1396
|
return true;
|
1350
1397
|
}
|
@@ -1406,18 +1453,6 @@ function readScrollBehavior(value, defaultValue) {
|
|
1406
1453
|
}
|
1407
1454
|
|
1408
1455
|
class ProgressBar {
|
1409
|
-
constructor() {
|
1410
|
-
this.hiding = false;
|
1411
|
-
this.value = 0;
|
1412
|
-
this.visible = false;
|
1413
|
-
this.trickle = () => {
|
1414
|
-
this.setValue(this.value + Math.random() / 100);
|
1415
|
-
};
|
1416
|
-
this.stylesheetElement = this.createStylesheetElement();
|
1417
|
-
this.progressElement = this.createProgressElement();
|
1418
|
-
this.installStylesheetElement();
|
1419
|
-
this.setValue(0);
|
1420
|
-
}
|
1421
1456
|
static get defaultCSS() {
|
1422
1457
|
return unindent`
|
1423
1458
|
.turbo-progress-bar {
|
@@ -1435,6 +1470,18 @@ class ProgressBar {
|
|
1435
1470
|
}
|
1436
1471
|
`;
|
1437
1472
|
}
|
1473
|
+
constructor() {
|
1474
|
+
this.hiding = false;
|
1475
|
+
this.value = 0;
|
1476
|
+
this.visible = false;
|
1477
|
+
this.trickle = () => {
|
1478
|
+
this.setValue(this.value + Math.random() / 100);
|
1479
|
+
};
|
1480
|
+
this.stylesheetElement = this.createStylesheetElement();
|
1481
|
+
this.progressElement = this.createProgressElement();
|
1482
|
+
this.installStylesheetElement();
|
1483
|
+
this.setValue(0);
|
1484
|
+
}
|
1438
1485
|
show() {
|
1439
1486
|
if (!this.visible) {
|
1440
1487
|
this.visible = true;
|
@@ -1603,10 +1650,6 @@ function elementWithoutNonce(element) {
|
|
1603
1650
|
}
|
1604
1651
|
|
1605
1652
|
class PageSnapshot extends Snapshot {
|
1606
|
-
constructor(element, headSnapshot) {
|
1607
|
-
super(element);
|
1608
|
-
this.headSnapshot = headSnapshot;
|
1609
|
-
}
|
1610
1653
|
static fromHTMLString(html = "") {
|
1611
1654
|
return this.fromDocument(parseHTMLDocument(html));
|
1612
1655
|
}
|
@@ -1616,6 +1659,10 @@ class PageSnapshot extends Snapshot {
|
|
1616
1659
|
static fromDocument({head: head, body: body}) {
|
1617
1660
|
return new this(body, new HeadSnapshot(head));
|
1618
1661
|
}
|
1662
|
+
constructor(element, headSnapshot) {
|
1663
|
+
super(element);
|
1664
|
+
this.headSnapshot = headSnapshot;
|
1665
|
+
}
|
1619
1666
|
clone() {
|
1620
1667
|
const clonedElement = this.element.cloneNode(true);
|
1621
1668
|
const selectElements = this.element.querySelectorAll("select");
|
@@ -1873,7 +1920,9 @@ class Visit {
|
|
1873
1920
|
if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
|
1874
1921
|
this.adapter.visitProposedToLocation(this.redirectedToLocation, {
|
1875
1922
|
action: "replace",
|
1876
|
-
response: this.response
|
1923
|
+
response: this.response,
|
1924
|
+
shouldCacheSnapshot: false,
|
1925
|
+
willRender: false
|
1877
1926
|
});
|
1878
1927
|
this.followedRedirect = true;
|
1879
1928
|
}
|
@@ -1888,7 +1937,7 @@ class Visit {
|
|
1888
1937
|
}));
|
1889
1938
|
}
|
1890
1939
|
}
|
1891
|
-
|
1940
|
+
prepareRequest(request) {
|
1892
1941
|
if (this.acceptsStreamResponse) {
|
1893
1942
|
request.acceptResponseType(StreamMessage.contentType);
|
1894
1943
|
}
|
@@ -2118,10 +2167,11 @@ class BrowserAdapter {
|
|
2118
2167
|
|
2119
2168
|
class CacheObserver {
|
2120
2169
|
constructor() {
|
2170
|
+
this.selector = "[data-turbo-temporary]";
|
2171
|
+
this.deprecatedSelector = "[data-turbo-cache=false]";
|
2121
2172
|
this.started = false;
|
2122
|
-
this.
|
2123
|
-
const
|
2124
|
-
for (const element of staleElements) {
|
2173
|
+
this.removeTemporaryElements = _event => {
|
2174
|
+
for (const element of this.temporaryElements) {
|
2125
2175
|
element.remove();
|
2126
2176
|
}
|
2127
2177
|
};
|
@@ -2129,14 +2179,24 @@ class CacheObserver {
|
|
2129
2179
|
start() {
|
2130
2180
|
if (!this.started) {
|
2131
2181
|
this.started = true;
|
2132
|
-
addEventListener("turbo:before-cache", this.
|
2182
|
+
addEventListener("turbo:before-cache", this.removeTemporaryElements, false);
|
2133
2183
|
}
|
2134
2184
|
}
|
2135
2185
|
stop() {
|
2136
2186
|
if (this.started) {
|
2137
2187
|
this.started = false;
|
2138
|
-
removeEventListener("turbo:before-cache", this.
|
2188
|
+
removeEventListener("turbo:before-cache", this.removeTemporaryElements, false);
|
2189
|
+
}
|
2190
|
+
}
|
2191
|
+
get temporaryElements() {
|
2192
|
+
return [ ...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation ];
|
2193
|
+
}
|
2194
|
+
get temporaryElementsWithDeprecation() {
|
2195
|
+
const elements = document.querySelectorAll(this.deprecatedSelector);
|
2196
|
+
if (elements.length) {
|
2197
|
+
console.warn(`The ${this.deprecatedSelector} selector is deprecated and will be removed in a future version. Use ${this.selector} instead.`);
|
2139
2198
|
}
|
2199
|
+
return [ ...elements ];
|
2140
2200
|
}
|
2141
2201
|
}
|
2142
2202
|
|
@@ -2336,7 +2396,7 @@ class Navigator {
|
|
2336
2396
|
if (formSubmission == this.formSubmission) {
|
2337
2397
|
const responseHTML = await fetchResponse.responseHTML;
|
2338
2398
|
if (responseHTML) {
|
2339
|
-
const shouldCacheSnapshot = formSubmission.
|
2399
|
+
const shouldCacheSnapshot = formSubmission.isSafe;
|
2340
2400
|
if (!shouldCacheSnapshot) {
|
2341
2401
|
this.view.clearSnapshotCache();
|
2342
2402
|
}
|
@@ -2397,10 +2457,8 @@ class Navigator {
|
|
2397
2457
|
get restorationIdentifier() {
|
2398
2458
|
return this.history.restorationIdentifier;
|
2399
2459
|
}
|
2400
|
-
getActionForFormSubmission(
|
2401
|
-
|
2402
|
-
const action = getAttribute("data-turbo-action", submitter, formElement);
|
2403
|
-
return isAction(action) ? action : "advance";
|
2460
|
+
getActionForFormSubmission({submitter: submitter, formElement: formElement}) {
|
2461
|
+
return getVisitAction(submitter, formElement) || "advance";
|
2404
2462
|
}
|
2405
2463
|
}
|
2406
2464
|
|
@@ -2648,7 +2706,7 @@ class PageRenderer extends Renderer {
|
|
2648
2706
|
}
|
2649
2707
|
async render() {
|
2650
2708
|
if (this.willRender) {
|
2651
|
-
this.replaceBody();
|
2709
|
+
await this.replaceBody();
|
2652
2710
|
}
|
2653
2711
|
}
|
2654
2712
|
finishRendering() {
|
@@ -2667,16 +2725,16 @@ class PageRenderer extends Renderer {
|
|
2667
2725
|
return this.newSnapshot.element;
|
2668
2726
|
}
|
2669
2727
|
async mergeHead() {
|
2728
|
+
const mergedHeadElements = this.mergeProvisionalElements();
|
2670
2729
|
const newStylesheetElements = this.copyNewHeadStylesheetElements();
|
2671
2730
|
this.copyNewHeadScriptElements();
|
2672
|
-
|
2673
|
-
this.copyNewHeadProvisionalElements();
|
2731
|
+
await mergedHeadElements;
|
2674
2732
|
await newStylesheetElements;
|
2675
2733
|
}
|
2676
|
-
replaceBody() {
|
2677
|
-
this.preservingPermanentElements((() => {
|
2734
|
+
async replaceBody() {
|
2735
|
+
await this.preservingPermanentElements((async () => {
|
2678
2736
|
this.activateNewBody();
|
2679
|
-
this.assignNewBody();
|
2737
|
+
await this.assignNewBody();
|
2680
2738
|
}));
|
2681
2739
|
}
|
2682
2740
|
get trackedElementsAreIdentical() {
|
@@ -2695,6 +2753,35 @@ class PageRenderer extends Renderer {
|
|
2695
2753
|
document.head.appendChild(activateScriptElement(element));
|
2696
2754
|
}
|
2697
2755
|
}
|
2756
|
+
async mergeProvisionalElements() {
|
2757
|
+
const newHeadElements = [ ...this.newHeadProvisionalElements ];
|
2758
|
+
for (const element of this.currentHeadProvisionalElements) {
|
2759
|
+
if (!this.isCurrentElementInElementList(element, newHeadElements)) {
|
2760
|
+
document.head.removeChild(element);
|
2761
|
+
}
|
2762
|
+
}
|
2763
|
+
for (const element of newHeadElements) {
|
2764
|
+
document.head.appendChild(element);
|
2765
|
+
}
|
2766
|
+
}
|
2767
|
+
isCurrentElementInElementList(element, elementList) {
|
2768
|
+
for (const [index, newElement] of elementList.entries()) {
|
2769
|
+
if (element.tagName == "TITLE") {
|
2770
|
+
if (newElement.tagName != "TITLE") {
|
2771
|
+
continue;
|
2772
|
+
}
|
2773
|
+
if (element.innerHTML == newElement.innerHTML) {
|
2774
|
+
elementList.splice(index, 1);
|
2775
|
+
return true;
|
2776
|
+
}
|
2777
|
+
}
|
2778
|
+
if (newElement.isEqualNode(element)) {
|
2779
|
+
elementList.splice(index, 1);
|
2780
|
+
return true;
|
2781
|
+
}
|
2782
|
+
}
|
2783
|
+
return false;
|
2784
|
+
}
|
2698
2785
|
removeCurrentHeadProvisionalElements() {
|
2699
2786
|
for (const element of this.currentHeadProvisionalElements) {
|
2700
2787
|
document.head.removeChild(element);
|
@@ -2715,8 +2802,8 @@ class PageRenderer extends Renderer {
|
|
2715
2802
|
inertScriptElement.replaceWith(activatedScriptElement);
|
2716
2803
|
}
|
2717
2804
|
}
|
2718
|
-
assignNewBody() {
|
2719
|
-
this.renderElement(this.currentElement, this.newElement);
|
2805
|
+
async assignNewBody() {
|
2806
|
+
await this.renderElement(this.currentElement, this.newElement);
|
2720
2807
|
}
|
2721
2808
|
get newHeadStylesheetElements() {
|
2722
2809
|
return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot);
|
@@ -3150,8 +3237,8 @@ class Session {
|
|
3150
3237
|
}
|
3151
3238
|
}
|
3152
3239
|
elementIsNavigatable(element) {
|
3153
|
-
const container = element
|
3154
|
-
const withinFrame = element
|
3240
|
+
const container = findClosestRecursively(element, "[data-turbo]");
|
3241
|
+
const withinFrame = findClosestRecursively(element, "turbo-frame");
|
3155
3242
|
if (this.drive || withinFrame) {
|
3156
3243
|
if (container) {
|
3157
3244
|
return container.getAttribute("data-turbo") != "false";
|
@@ -3167,8 +3254,7 @@ class Session {
|
|
3167
3254
|
}
|
3168
3255
|
}
|
3169
3256
|
getActionForLink(link) {
|
3170
|
-
|
3171
|
-
return isAction(action) ? action : "advance";
|
3257
|
+
return getVisitAction(link) || "advance";
|
3172
3258
|
}
|
3173
3259
|
get snapshot() {
|
3174
3260
|
return this.view.snapshot;
|
@@ -3236,7 +3322,10 @@ const StreamActions = {
|
|
3236
3322
|
this.targetElements.forEach((e => e.replaceWith(this.templateContent)));
|
3237
3323
|
},
|
3238
3324
|
update() {
|
3239
|
-
this.targetElements.forEach((
|
3325
|
+
this.targetElements.forEach((targetElement => {
|
3326
|
+
targetElement.innerHTML = "";
|
3327
|
+
targetElement.append(this.templateContent);
|
3328
|
+
}));
|
3240
3329
|
}
|
3241
3330
|
};
|
3242
3331
|
|
@@ -3308,6 +3397,8 @@ var Turbo = Object.freeze({
|
|
3308
3397
|
StreamActions: StreamActions
|
3309
3398
|
});
|
3310
3399
|
|
3400
|
+
class TurboFrameMissingError extends Error {}
|
3401
|
+
|
3311
3402
|
class FrameController {
|
3312
3403
|
constructor(element) {
|
3313
3404
|
this.fetchResponseLoaded = _fetchResponse => {};
|
@@ -3404,31 +3495,20 @@ class FrameController {
|
|
3404
3495
|
try {
|
3405
3496
|
const html = await fetchResponse.responseHTML;
|
3406
3497
|
if (html) {
|
3407
|
-
const
|
3408
|
-
const
|
3409
|
-
if (
|
3410
|
-
|
3411
|
-
|
3412
|
-
|
3413
|
-
this.changeHistory();
|
3414
|
-
await this.view.render(renderer);
|
3415
|
-
this.complete = true;
|
3416
|
-
session.frameRendered(fetchResponse, this.element);
|
3417
|
-
session.frameLoaded(this.element);
|
3418
|
-
this.fetchResponseLoaded(fetchResponse);
|
3419
|
-
} else if (this.willHandleFrameMissingFromResponse(fetchResponse)) {
|
3420
|
-
console.warn(`A matching frame for #${this.element.id} was missing from the response, transforming into full-page Visit.`);
|
3421
|
-
this.visitResponse(fetchResponse.response);
|
3498
|
+
const document = parseHTMLDocument(html);
|
3499
|
+
const pageSnapshot = PageSnapshot.fromDocument(document);
|
3500
|
+
if (pageSnapshot.isVisitable) {
|
3501
|
+
await this.loadFrameResponse(fetchResponse, document);
|
3502
|
+
} else {
|
3503
|
+
await this.handleUnvisitableFrameResponse(fetchResponse);
|
3422
3504
|
}
|
3423
3505
|
}
|
3424
|
-
} catch (error) {
|
3425
|
-
console.error(error);
|
3426
|
-
this.view.invalidate();
|
3427
3506
|
} finally {
|
3428
3507
|
this.fetchResponseLoaded = () => {};
|
3429
3508
|
}
|
3430
3509
|
}
|
3431
|
-
elementAppearedInViewport(
|
3510
|
+
elementAppearedInViewport(element) {
|
3511
|
+
this.proposeVisitIfNavigatedWithAction(element, element);
|
3432
3512
|
this.loadSourceURL();
|
3433
3513
|
}
|
3434
3514
|
willSubmitFormLinkToLocation(link) {
|
@@ -3453,12 +3533,12 @@ class FrameController {
|
|
3453
3533
|
}
|
3454
3534
|
this.formSubmission = new FormSubmission(this, element, submitter);
|
3455
3535
|
const {fetchRequest: fetchRequest} = this.formSubmission;
|
3456
|
-
this.
|
3536
|
+
this.prepareRequest(fetchRequest);
|
3457
3537
|
this.formSubmission.start();
|
3458
3538
|
}
|
3459
|
-
|
3539
|
+
prepareRequest(request) {
|
3460
3540
|
var _a;
|
3461
|
-
headers["Turbo-Frame"] = this.id;
|
3541
|
+
request.headers["Turbo-Frame"] = this.id;
|
3462
3542
|
if ((_a = this.currentNavigationElement) === null || _a === void 0 ? void 0 : _a.hasAttribute("data-turbo-stream")) {
|
3463
3543
|
request.acceptResponseType(StreamMessage.contentType);
|
3464
3544
|
}
|
@@ -3474,7 +3554,6 @@ class FrameController {
|
|
3474
3554
|
this.resolveVisitPromise();
|
3475
3555
|
}
|
3476
3556
|
async requestFailedWithResponse(request, response) {
|
3477
|
-
console.error(response);
|
3478
3557
|
await this.loadResponse(response);
|
3479
3558
|
this.resolveVisitPromise();
|
3480
3559
|
}
|
@@ -3492,9 +3571,13 @@ class FrameController {
|
|
3492
3571
|
const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
|
3493
3572
|
frame.delegate.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
|
3494
3573
|
frame.delegate.loadResponse(response);
|
3574
|
+
if (!formSubmission.isSafe) {
|
3575
|
+
session.clearCache();
|
3576
|
+
}
|
3495
3577
|
}
|
3496
3578
|
formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
|
3497
3579
|
this.element.delegate.loadResponse(fetchResponse);
|
3580
|
+
session.clearCache();
|
3498
3581
|
}
|
3499
3582
|
formSubmissionErrored(formSubmission, error) {
|
3500
3583
|
console.error(error);
|
@@ -3524,6 +3607,22 @@ class FrameController {
|
|
3524
3607
|
willRenderFrame(currentElement, _newElement) {
|
3525
3608
|
this.previousFrameElement = currentElement.cloneNode(true);
|
3526
3609
|
}
|
3610
|
+
async loadFrameResponse(fetchResponse, document) {
|
3611
|
+
const newFrameElement = await this.extractForeignFrameElement(document.body);
|
3612
|
+
if (newFrameElement) {
|
3613
|
+
const snapshot = new Snapshot(newFrameElement);
|
3614
|
+
const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false);
|
3615
|
+
if (this.view.renderPromise) await this.view.renderPromise;
|
3616
|
+
this.changeHistory();
|
3617
|
+
await this.view.render(renderer);
|
3618
|
+
this.complete = true;
|
3619
|
+
session.frameRendered(fetchResponse, this.element);
|
3620
|
+
session.frameLoaded(this.element);
|
3621
|
+
this.fetchResponseLoaded(fetchResponse);
|
3622
|
+
} else if (this.willHandleFrameMissingFromResponse(fetchResponse)) {
|
3623
|
+
this.handleFrameMissingFromResponse(fetchResponse);
|
3624
|
+
}
|
3625
|
+
}
|
3527
3626
|
async visit(url) {
|
3528
3627
|
var _a;
|
3529
3628
|
const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams, this.element);
|
@@ -3540,7 +3639,6 @@ class FrameController {
|
|
3540
3639
|
}
|
3541
3640
|
navigateFrame(element, url, submitter) {
|
3542
3641
|
const frame = this.findFrameElement(element, submitter);
|
3543
|
-
this.pageSnapshot = PageSnapshot.fromElement(frame).clone();
|
3544
3642
|
frame.delegate.proposeVisitIfNavigatedWithAction(frame, element, submitter);
|
3545
3643
|
this.withCurrentNavigationElement(element, (() => {
|
3546
3644
|
frame.src = url;
|
@@ -3548,7 +3646,8 @@ class FrameController {
|
|
3548
3646
|
}
|
3549
3647
|
proposeVisitIfNavigatedWithAction(frame, element, submitter) {
|
3550
3648
|
this.action = getVisitAction(submitter, element, frame);
|
3551
|
-
if (
|
3649
|
+
if (this.action) {
|
3650
|
+
const pageSnapshot = PageSnapshot.fromElement(frame).clone();
|
3552
3651
|
const {visitCachedSnapshot: visitCachedSnapshot} = frame.delegate;
|
3553
3652
|
frame.delegate.fetchResponseLoaded = fetchResponse => {
|
3554
3653
|
if (frame.src) {
|
@@ -3565,7 +3664,7 @@ class FrameController {
|
|
3565
3664
|
willRender: false,
|
3566
3665
|
updateHistory: false,
|
3567
3666
|
restorationIdentifier: this.restorationIdentifier,
|
3568
|
-
snapshot:
|
3667
|
+
snapshot: pageSnapshot
|
3569
3668
|
};
|
3570
3669
|
if (this.action) options.action = this.action;
|
3571
3670
|
session.visit(frame.src, options);
|
@@ -3579,6 +3678,10 @@ class FrameController {
|
|
3579
3678
|
session.history.update(method, expandURL(this.element.src || ""), this.restorationIdentifier);
|
3580
3679
|
}
|
3581
3680
|
}
|
3681
|
+
async handleUnvisitableFrameResponse(fetchResponse) {
|
3682
|
+
console.warn(`The response (${fetchResponse.statusCode}) from <turbo-frame id="${this.element.id}"> is performing a full page visit due to turbo-visit-control.`);
|
3683
|
+
await this.visitResponse(fetchResponse.response);
|
3684
|
+
}
|
3582
3685
|
willHandleFrameMissingFromResponse(fetchResponse) {
|
3583
3686
|
this.element.setAttribute("complete", "");
|
3584
3687
|
const response = fetchResponse.response;
|
@@ -3599,6 +3702,14 @@ class FrameController {
|
|
3599
3702
|
});
|
3600
3703
|
return !event.defaultPrevented;
|
3601
3704
|
}
|
3705
|
+
handleFrameMissingFromResponse(fetchResponse) {
|
3706
|
+
this.view.missing();
|
3707
|
+
this.throwFrameMissingError(fetchResponse);
|
3708
|
+
}
|
3709
|
+
throwFrameMissingError(fetchResponse) {
|
3710
|
+
const message = `The response (${fetchResponse.statusCode}) did not contain the expected <turbo-frame id="${this.element.id}"> and will be ignored. To perform a full page visit instead, set turbo-visit-control to reload.`;
|
3711
|
+
throw new TurboFrameMissingError(message);
|
3712
|
+
}
|
3602
3713
|
async visitResponse(response) {
|
3603
3714
|
const wrapped = new FetchResponse(response);
|
3604
3715
|
const responseHTML = await wrapped.responseHTML;
|
@@ -3993,7 +4104,9 @@ class TurboCableStreamSourceElement extends HTMLElement {
|
|
3993
4104
|
async connectedCallback() {
|
3994
4105
|
connectStreamSource(this);
|
3995
4106
|
this.subscription = await subscribeTo(this.channel, {
|
3996
|
-
received: this.dispatchMessageEvent.bind(this)
|
4107
|
+
received: this.dispatchMessageEvent.bind(this),
|
4108
|
+
connected: this.subscriptionConnected.bind(this),
|
4109
|
+
disconnected: this.subscriptionDisconnected.bind(this)
|
3997
4110
|
});
|
3998
4111
|
}
|
3999
4112
|
disconnectedCallback() {
|
@@ -4006,6 +4119,12 @@ class TurboCableStreamSourceElement extends HTMLElement {
|
|
4006
4119
|
});
|
4007
4120
|
return this.dispatchEvent(event);
|
4008
4121
|
}
|
4122
|
+
subscriptionConnected() {
|
4123
|
+
this.setAttribute("connected", "");
|
4124
|
+
}
|
4125
|
+
subscriptionDisconnected() {
|
4126
|
+
this.removeAttribute("connected");
|
4127
|
+
}
|
4009
4128
|
get channel() {
|
4010
4129
|
const channel = this.getAttribute("channel");
|
4011
4130
|
const signed_stream_name = this.getAttribute("signed-stream-name");
|
@@ -4019,18 +4138,21 @@ class TurboCableStreamSourceElement extends HTMLElement {
|
|
4019
4138
|
}
|
4020
4139
|
}
|
4021
4140
|
|
4022
|
-
customElements.
|
4141
|
+
if (customElements.get("turbo-cable-stream-source") === undefined) {
|
4142
|
+
customElements.define("turbo-cable-stream-source", TurboCableStreamSourceElement);
|
4143
|
+
}
|
4023
4144
|
|
4024
4145
|
function encodeMethodIntoRequestBody(event) {
|
4025
4146
|
if (event.target instanceof HTMLFormElement) {
|
4026
4147
|
const {target: form, detail: {fetchOptions: fetchOptions}} = event;
|
4027
4148
|
form.addEventListener("turbo:submit-start", (({detail: {formSubmission: {submitter: submitter}}}) => {
|
4028
|
-
const
|
4149
|
+
const body = isBodyInit(fetchOptions.body) ? fetchOptions.body : new URLSearchParams;
|
4150
|
+
const method = determineFetchMethod(submitter, body, form);
|
4029
4151
|
if (!/get/i.test(method)) {
|
4030
4152
|
if (/post/i.test(method)) {
|
4031
|
-
|
4153
|
+
body.delete("_method");
|
4032
4154
|
} else {
|
4033
|
-
|
4155
|
+
body.set("_method", method);
|
4034
4156
|
}
|
4035
4157
|
fetchOptions.method = "post";
|
4036
4158
|
}
|
@@ -4040,6 +4162,37 @@ function encodeMethodIntoRequestBody(event) {
|
|
4040
4162
|
}
|
4041
4163
|
}
|
4042
4164
|
|
4165
|
+
function determineFetchMethod(submitter, body, form) {
|
4166
|
+
const formMethod = determineFormMethod(submitter);
|
4167
|
+
const overrideMethod = body.get("_method");
|
4168
|
+
const method = form.getAttribute("method") || "get";
|
4169
|
+
if (typeof formMethod == "string") {
|
4170
|
+
return formMethod;
|
4171
|
+
} else if (typeof overrideMethod == "string") {
|
4172
|
+
return overrideMethod;
|
4173
|
+
} else {
|
4174
|
+
return method;
|
4175
|
+
}
|
4176
|
+
}
|
4177
|
+
|
4178
|
+
function determineFormMethod(submitter) {
|
4179
|
+
if (submitter instanceof HTMLButtonElement || submitter instanceof HTMLInputElement) {
|
4180
|
+
if (submitter.name === "_method") {
|
4181
|
+
return submitter.value;
|
4182
|
+
} else if (submitter.hasAttribute("formmethod")) {
|
4183
|
+
return submitter.formMethod;
|
4184
|
+
} else {
|
4185
|
+
return null;
|
4186
|
+
}
|
4187
|
+
} else {
|
4188
|
+
return null;
|
4189
|
+
}
|
4190
|
+
}
|
4191
|
+
|
4192
|
+
function isBodyInit(body) {
|
4193
|
+
return body instanceof FormData || body instanceof URLSearchParams;
|
4194
|
+
}
|
4195
|
+
|
4043
4196
|
addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody);
|
4044
4197
|
|
4045
4198
|
var adapters = {
|