turbo-rails 1.3.2 → 1.5.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 +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 = {
|