turbo-rails 1.3.0 → 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 +366 -191
- 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
|
}
|
@@ -97,11 +95,7 @@ class FrameElement extends HTMLElement {
|
|
97
95
|
this.delegate.disconnect();
|
98
96
|
}
|
99
97
|
reload() {
|
100
|
-
|
101
|
-
this.removeAttribute("complete");
|
102
|
-
this.src = null;
|
103
|
-
this.src = src;
|
104
|
-
return this.loaded;
|
98
|
+
return this.delegate.sourceURLReloaded();
|
105
99
|
}
|
106
100
|
attributeChangedCallback(name) {
|
107
101
|
if (name == "loading") {
|
@@ -286,10 +280,6 @@ class FetchResponse {
|
|
286
280
|
}
|
287
281
|
}
|
288
282
|
|
289
|
-
function isAction(action) {
|
290
|
-
return action == "advance" || action == "replace" || action == "restore";
|
291
|
-
}
|
292
|
-
|
293
283
|
function activateScriptElement(element) {
|
294
284
|
if (element.getAttribute("data-turbo-eval") == "false") {
|
295
285
|
return element;
|
@@ -322,6 +312,7 @@ function dispatch(eventName, {target: target, cancelable: cancelable, detail: de
|
|
322
312
|
const event = new CustomEvent(eventName, {
|
323
313
|
cancelable: cancelable,
|
324
314
|
bubbles: true,
|
315
|
+
composed: true,
|
325
316
|
detail: detail
|
326
317
|
});
|
327
318
|
if (target && target.isConnected) {
|
@@ -435,6 +426,10 @@ function getHistoryMethodForAction(action) {
|
|
435
426
|
}
|
436
427
|
}
|
437
428
|
|
429
|
+
function isAction(action) {
|
430
|
+
return action == "advance" || action == "replace" || action == "restore";
|
431
|
+
}
|
432
|
+
|
438
433
|
function getVisitAction(...elements) {
|
439
434
|
const action = getAttribute("data-turbo-action", ...elements);
|
440
435
|
return isAction(action) ? action : null;
|
@@ -460,6 +455,13 @@ function setMetaContent(name, content) {
|
|
460
455
|
return element;
|
461
456
|
}
|
462
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
|
+
|
463
465
|
var FetchMethod;
|
464
466
|
|
465
467
|
(function(FetchMethod) {
|
@@ -513,9 +515,8 @@ class FetchRequest {
|
|
513
515
|
this.abortController.abort();
|
514
516
|
}
|
515
517
|
async perform() {
|
516
|
-
var _a, _b;
|
517
518
|
const {fetchOptions: fetchOptions} = this;
|
518
|
-
|
519
|
+
this.delegate.prepareRequest(this);
|
519
520
|
await this.allowRequestToBeIntercepted(fetchOptions);
|
520
521
|
try {
|
521
522
|
this.delegate.requestStarted(this);
|
@@ -557,7 +558,7 @@ class FetchRequest {
|
|
557
558
|
credentials: "same-origin",
|
558
559
|
headers: this.headers,
|
559
560
|
redirect: "follow",
|
560
|
-
body: this.
|
561
|
+
body: this.isSafe ? null : this.body,
|
561
562
|
signal: this.abortSignal,
|
562
563
|
referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
|
563
564
|
};
|
@@ -567,8 +568,8 @@ class FetchRequest {
|
|
567
568
|
Accept: "text/html, application/xhtml+xml"
|
568
569
|
};
|
569
570
|
}
|
570
|
-
get
|
571
|
-
return this.method
|
571
|
+
get isSafe() {
|
572
|
+
return this.method === FetchMethod.get;
|
572
573
|
}
|
573
574
|
get abortSignal() {
|
574
575
|
return this.abortController.signal;
|
@@ -630,9 +631,6 @@ class AppearanceObserver {
|
|
630
631
|
}
|
631
632
|
|
632
633
|
class StreamMessage {
|
633
|
-
constructor(fragment) {
|
634
|
-
this.fragment = importStreamElements(fragment);
|
635
|
-
}
|
636
634
|
static wrap(message) {
|
637
635
|
if (typeof message == "string") {
|
638
636
|
return new this(createDocumentFragment(message));
|
@@ -640,6 +638,9 @@ class StreamMessage {
|
|
640
638
|
return message;
|
641
639
|
}
|
642
640
|
}
|
641
|
+
constructor(fragment) {
|
642
|
+
this.fragment = importStreamElements(fragment);
|
643
|
+
}
|
643
644
|
}
|
644
645
|
|
645
646
|
StreamMessage.contentType = "text/vnd.turbo-stream.html";
|
@@ -688,6 +689,9 @@ function formEnctypeFromString(encoding) {
|
|
688
689
|
}
|
689
690
|
|
690
691
|
class FormSubmission {
|
692
|
+
static confirmMethod(message, _element, _submitter) {
|
693
|
+
return Promise.resolve(confirm(message));
|
694
|
+
}
|
691
695
|
constructor(delegate, formElement, submitter, mustRedirect = false) {
|
692
696
|
this.state = FormSubmissionState.initialized;
|
693
697
|
this.delegate = delegate;
|
@@ -701,9 +705,6 @@ class FormSubmission {
|
|
701
705
|
this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
|
702
706
|
this.mustRedirect = mustRedirect;
|
703
707
|
}
|
704
|
-
static confirmMethod(message, _element, _submitter) {
|
705
|
-
return Promise.resolve(confirm(message));
|
706
|
-
}
|
707
708
|
get method() {
|
708
709
|
var _a;
|
709
710
|
const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
|
@@ -729,8 +730,8 @@ class FormSubmission {
|
|
729
730
|
var _a;
|
730
731
|
return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formenctype")) || this.formElement.enctype);
|
731
732
|
}
|
732
|
-
get
|
733
|
-
return this.fetchRequest.
|
733
|
+
get isSafe() {
|
734
|
+
return this.fetchRequest.isSafe;
|
734
735
|
}
|
735
736
|
get stringFormData() {
|
736
737
|
return [ ...this.formData ].reduce(((entries, [name, value]) => entries.concat(typeof value == "string" ? [ [ name, value ] ] : [])), []);
|
@@ -757,11 +758,11 @@ class FormSubmission {
|
|
757
758
|
return true;
|
758
759
|
}
|
759
760
|
}
|
760
|
-
|
761
|
-
if (!request.
|
761
|
+
prepareRequest(request) {
|
762
|
+
if (!request.isSafe) {
|
762
763
|
const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token");
|
763
764
|
if (token) {
|
764
|
-
headers["X-CSRF-Token"] = token;
|
765
|
+
request.headers["X-CSRF-Token"] = token;
|
765
766
|
}
|
766
767
|
}
|
767
768
|
if (this.requestAcceptsTurboStreamResponse(request)) {
|
@@ -772,6 +773,7 @@ class FormSubmission {
|
|
772
773
|
var _a;
|
773
774
|
this.state = FormSubmissionState.waiting;
|
774
775
|
(_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
|
776
|
+
this.setSubmitsWith();
|
775
777
|
dispatch("turbo:submit-start", {
|
776
778
|
target: this.formElement,
|
777
779
|
detail: {
|
@@ -819,6 +821,7 @@ class FormSubmission {
|
|
819
821
|
var _a;
|
820
822
|
this.state = FormSubmissionState.stopped;
|
821
823
|
(_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
|
824
|
+
this.resetSubmitterText();
|
822
825
|
dispatch("turbo:submit-end", {
|
823
826
|
target: this.formElement,
|
824
827
|
detail: Object.assign({
|
@@ -827,11 +830,35 @@ class FormSubmission {
|
|
827
830
|
});
|
828
831
|
this.delegate.formSubmissionFinished(this);
|
829
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
|
+
}
|
830
853
|
requestMustRedirect(request) {
|
831
|
-
return !request.
|
854
|
+
return !request.isSafe && this.mustRedirect;
|
832
855
|
}
|
833
856
|
requestAcceptsTurboStreamResponse(request) {
|
834
|
-
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");
|
835
862
|
}
|
836
863
|
}
|
837
864
|
|
@@ -936,6 +963,7 @@ class FormSubmitObserver {
|
|
936
963
|
const submitter = event.submitter || undefined;
|
937
964
|
if (form && submissionDoesNotDismissDialog(form, submitter) && submissionDoesNotTargetIFrame(form, submitter) && this.delegate.willSubmitForm(form, submitter)) {
|
938
965
|
event.preventDefault();
|
966
|
+
event.stopImmediatePropagation();
|
939
967
|
this.delegate.formSubmitted(form, submitter);
|
940
968
|
}
|
941
969
|
}
|
@@ -963,11 +991,15 @@ function submissionDoesNotDismissDialog(form, submitter) {
|
|
963
991
|
}
|
964
992
|
|
965
993
|
function submissionDoesNotTargetIFrame(form, submitter) {
|
966
|
-
|
967
|
-
|
968
|
-
|
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;
|
969
1002
|
}
|
970
|
-
return true;
|
971
1003
|
}
|
972
1004
|
|
973
1005
|
class View {
|
@@ -1068,14 +1100,55 @@ class View {
|
|
1068
1100
|
}
|
1069
1101
|
|
1070
1102
|
class FrameView extends View {
|
1071
|
-
|
1072
|
-
this.element.innerHTML = ""
|
1103
|
+
missing() {
|
1104
|
+
this.element.innerHTML = `<strong class="turbo-frame-error">Content missing</strong>`;
|
1073
1105
|
}
|
1074
1106
|
get snapshot() {
|
1075
1107
|
return new Snapshot(this.element);
|
1076
1108
|
}
|
1077
1109
|
}
|
1078
1110
|
|
1111
|
+
class LinkInterceptor {
|
1112
|
+
constructor(delegate, element) {
|
1113
|
+
this.clickBubbled = event => {
|
1114
|
+
if (this.respondsToEventTarget(event.target)) {
|
1115
|
+
this.clickEvent = event;
|
1116
|
+
} else {
|
1117
|
+
delete this.clickEvent;
|
1118
|
+
}
|
1119
|
+
};
|
1120
|
+
this.linkClicked = event => {
|
1121
|
+
if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
|
1122
|
+
if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
|
1123
|
+
this.clickEvent.preventDefault();
|
1124
|
+
event.preventDefault();
|
1125
|
+
this.delegate.linkClickIntercepted(event.target, event.detail.url, event.detail.originalEvent);
|
1126
|
+
}
|
1127
|
+
}
|
1128
|
+
delete this.clickEvent;
|
1129
|
+
};
|
1130
|
+
this.willVisit = _event => {
|
1131
|
+
delete this.clickEvent;
|
1132
|
+
};
|
1133
|
+
this.delegate = delegate;
|
1134
|
+
this.element = element;
|
1135
|
+
}
|
1136
|
+
start() {
|
1137
|
+
this.element.addEventListener("click", this.clickBubbled);
|
1138
|
+
document.addEventListener("turbo:click", this.linkClicked);
|
1139
|
+
document.addEventListener("turbo:before-visit", this.willVisit);
|
1140
|
+
}
|
1141
|
+
stop() {
|
1142
|
+
this.element.removeEventListener("click", this.clickBubbled);
|
1143
|
+
document.removeEventListener("turbo:click", this.linkClicked);
|
1144
|
+
document.removeEventListener("turbo:before-visit", this.willVisit);
|
1145
|
+
}
|
1146
|
+
respondsToEventTarget(target) {
|
1147
|
+
const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
|
1148
|
+
return element && element.closest("turbo-frame, html") == this.element;
|
1149
|
+
}
|
1150
|
+
}
|
1151
|
+
|
1079
1152
|
class LinkClickObserver {
|
1080
1153
|
constructor(delegate, eventTarget) {
|
1081
1154
|
this.started = false;
|
@@ -1115,9 +1188,7 @@ class LinkClickObserver {
|
|
1115
1188
|
return !(event.target && event.target.isContentEditable || event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);
|
1116
1189
|
}
|
1117
1190
|
findLinkFromClickTarget(target) {
|
1118
|
-
|
1119
|
-
return target.closest("a[href]:not([target^=_]):not([download])");
|
1120
|
-
}
|
1191
|
+
return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])");
|
1121
1192
|
}
|
1122
1193
|
getLocationForLink(link) {
|
1123
1194
|
return expandURL(link.getAttribute("href") || "");
|
@@ -1125,37 +1196,51 @@ class LinkClickObserver {
|
|
1125
1196
|
}
|
1126
1197
|
|
1127
1198
|
function doesNotTargetIFrame(anchor) {
|
1128
|
-
|
1129
|
-
|
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;
|
1130
1206
|
}
|
1131
|
-
return true;
|
1132
1207
|
}
|
1133
1208
|
|
1134
1209
|
class FormLinkClickObserver {
|
1135
1210
|
constructor(delegate, element) {
|
1136
1211
|
this.delegate = delegate;
|
1137
|
-
this.
|
1212
|
+
this.linkInterceptor = new LinkClickObserver(this, element);
|
1138
1213
|
}
|
1139
1214
|
start() {
|
1140
|
-
this.
|
1215
|
+
this.linkInterceptor.start();
|
1141
1216
|
}
|
1142
1217
|
stop() {
|
1143
|
-
this.
|
1218
|
+
this.linkInterceptor.stop();
|
1144
1219
|
}
|
1145
1220
|
willFollowLinkToLocation(link, location, originalEvent) {
|
1146
1221
|
return this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) && link.hasAttribute("data-turbo-method");
|
1147
1222
|
}
|
1148
1223
|
followedLinkToLocation(link, location) {
|
1149
|
-
const action = location.href;
|
1150
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
|
+
});
|
1151
1236
|
form.setAttribute("data-turbo", "true");
|
1152
|
-
form.setAttribute("action", action);
|
1237
|
+
form.setAttribute("action", action.href);
|
1153
1238
|
form.setAttribute("hidden", "");
|
1154
1239
|
const method = link.getAttribute("data-turbo-method");
|
1155
1240
|
if (method) form.setAttribute("method", method);
|
1156
1241
|
const turboFrame = link.getAttribute("data-turbo-frame");
|
1157
1242
|
if (turboFrame) form.setAttribute("data-turbo-frame", turboFrame);
|
1158
|
-
const turboAction = link
|
1243
|
+
const turboAction = getVisitAction(link);
|
1159
1244
|
if (turboAction) form.setAttribute("data-turbo-action", turboAction);
|
1160
1245
|
const turboConfirm = link.getAttribute("data-turbo-confirm");
|
1161
1246
|
if (turboConfirm) form.setAttribute("data-turbo-confirm", turboConfirm);
|
@@ -1171,16 +1256,16 @@ class FormLinkClickObserver {
|
|
1171
1256
|
}
|
1172
1257
|
|
1173
1258
|
class Bardo {
|
1174
|
-
|
1175
|
-
this.delegate = delegate;
|
1176
|
-
this.permanentElementMap = permanentElementMap;
|
1177
|
-
}
|
1178
|
-
static preservingPermanentElements(delegate, permanentElementMap, callback) {
|
1259
|
+
static async preservingPermanentElements(delegate, permanentElementMap, callback) {
|
1179
1260
|
const bardo = new this(delegate, permanentElementMap);
|
1180
1261
|
bardo.enter();
|
1181
|
-
callback();
|
1262
|
+
await callback();
|
1182
1263
|
bardo.leave();
|
1183
1264
|
}
|
1265
|
+
constructor(delegate, permanentElementMap) {
|
1266
|
+
this.delegate = delegate;
|
1267
|
+
this.permanentElementMap = permanentElementMap;
|
1268
|
+
}
|
1184
1269
|
enter() {
|
1185
1270
|
for (const id in this.permanentElementMap) {
|
1186
1271
|
const [currentPermanentElement, newPermanentElement] = this.permanentElementMap[id];
|
@@ -1251,8 +1336,8 @@ class Renderer {
|
|
1251
1336
|
delete this.resolvingFunctions;
|
1252
1337
|
}
|
1253
1338
|
}
|
1254
|
-
preservingPermanentElements(callback) {
|
1255
|
-
Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
|
1339
|
+
async preservingPermanentElements(callback) {
|
1340
|
+
await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
|
1256
1341
|
}
|
1257
1342
|
focusFirstAutofocusableElement() {
|
1258
1343
|
const element = this.connectedSnapshot.firstAutofocusableElement;
|
@@ -1291,10 +1376,6 @@ function elementIsFocusable(element) {
|
|
1291
1376
|
}
|
1292
1377
|
|
1293
1378
|
class FrameRenderer extends Renderer {
|
1294
|
-
constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
|
1295
|
-
super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender);
|
1296
|
-
this.delegate = delegate;
|
1297
|
-
}
|
1298
1379
|
static renderElement(currentElement, newElement) {
|
1299
1380
|
var _a;
|
1300
1381
|
const destinationRange = document.createRange();
|
@@ -1307,6 +1388,10 @@ class FrameRenderer extends Renderer {
|
|
1307
1388
|
currentElement.appendChild(sourceRange.extractContents());
|
1308
1389
|
}
|
1309
1390
|
}
|
1391
|
+
constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
|
1392
|
+
super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender);
|
1393
|
+
this.delegate = delegate;
|
1394
|
+
}
|
1310
1395
|
get shouldRender() {
|
1311
1396
|
return true;
|
1312
1397
|
}
|
@@ -1368,18 +1453,6 @@ function readScrollBehavior(value, defaultValue) {
|
|
1368
1453
|
}
|
1369
1454
|
|
1370
1455
|
class ProgressBar {
|
1371
|
-
constructor() {
|
1372
|
-
this.hiding = false;
|
1373
|
-
this.value = 0;
|
1374
|
-
this.visible = false;
|
1375
|
-
this.trickle = () => {
|
1376
|
-
this.setValue(this.value + Math.random() / 100);
|
1377
|
-
};
|
1378
|
-
this.stylesheetElement = this.createStylesheetElement();
|
1379
|
-
this.progressElement = this.createProgressElement();
|
1380
|
-
this.installStylesheetElement();
|
1381
|
-
this.setValue(0);
|
1382
|
-
}
|
1383
1456
|
static get defaultCSS() {
|
1384
1457
|
return unindent`
|
1385
1458
|
.turbo-progress-bar {
|
@@ -1397,6 +1470,18 @@ class ProgressBar {
|
|
1397
1470
|
}
|
1398
1471
|
`;
|
1399
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
|
+
}
|
1400
1485
|
show() {
|
1401
1486
|
if (!this.visible) {
|
1402
1487
|
this.visible = true;
|
@@ -1565,10 +1650,6 @@ function elementWithoutNonce(element) {
|
|
1565
1650
|
}
|
1566
1651
|
|
1567
1652
|
class PageSnapshot extends Snapshot {
|
1568
|
-
constructor(element, headSnapshot) {
|
1569
|
-
super(element);
|
1570
|
-
this.headSnapshot = headSnapshot;
|
1571
|
-
}
|
1572
1653
|
static fromHTMLString(html = "") {
|
1573
1654
|
return this.fromDocument(parseHTMLDocument(html));
|
1574
1655
|
}
|
@@ -1578,6 +1659,10 @@ class PageSnapshot extends Snapshot {
|
|
1578
1659
|
static fromDocument({head: head, body: body}) {
|
1579
1660
|
return new this(body, new HeadSnapshot(head));
|
1580
1661
|
}
|
1662
|
+
constructor(element, headSnapshot) {
|
1663
|
+
super(element);
|
1664
|
+
this.headSnapshot = headSnapshot;
|
1665
|
+
}
|
1581
1666
|
clone() {
|
1582
1667
|
const clonedElement = this.element.cloneNode(true);
|
1583
1668
|
const selectElements = this.element.querySelectorAll("select");
|
@@ -1668,10 +1753,11 @@ class Visit {
|
|
1668
1753
|
this.delegate = delegate;
|
1669
1754
|
this.location = location;
|
1670
1755
|
this.restorationIdentifier = restorationIdentifier || uuid();
|
1671
|
-
const {action: action, historyChanged: historyChanged, referrer: referrer, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender, updateHistory: updateHistory, shouldCacheSnapshot: shouldCacheSnapshot, acceptsStreamResponse: acceptsStreamResponse} = Object.assign(Object.assign({}, defaultOptions), options);
|
1756
|
+
const {action: action, historyChanged: historyChanged, referrer: referrer, snapshot: snapshot, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender, updateHistory: updateHistory, shouldCacheSnapshot: shouldCacheSnapshot, acceptsStreamResponse: acceptsStreamResponse} = Object.assign(Object.assign({}, defaultOptions), options);
|
1672
1757
|
this.action = action;
|
1673
1758
|
this.historyChanged = historyChanged;
|
1674
1759
|
this.referrer = referrer;
|
1760
|
+
this.snapshot = snapshot;
|
1675
1761
|
this.snapshotHTML = snapshotHTML;
|
1676
1762
|
this.response = response;
|
1677
1763
|
this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
|
@@ -1834,7 +1920,9 @@ class Visit {
|
|
1834
1920
|
if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
|
1835
1921
|
this.adapter.visitProposedToLocation(this.redirectedToLocation, {
|
1836
1922
|
action: "replace",
|
1837
|
-
response: this.response
|
1923
|
+
response: this.response,
|
1924
|
+
shouldCacheSnapshot: false,
|
1925
|
+
willRender: false
|
1838
1926
|
});
|
1839
1927
|
this.followedRedirect = true;
|
1840
1928
|
}
|
@@ -1844,11 +1932,12 @@ class Visit {
|
|
1844
1932
|
this.render((async () => {
|
1845
1933
|
this.cacheSnapshot();
|
1846
1934
|
this.performScroll();
|
1935
|
+
this.changeHistory();
|
1847
1936
|
this.adapter.visitRendered(this);
|
1848
1937
|
}));
|
1849
1938
|
}
|
1850
1939
|
}
|
1851
|
-
|
1940
|
+
prepareRequest(request) {
|
1852
1941
|
if (this.acceptsStreamResponse) {
|
1853
1942
|
request.acceptResponseType(StreamMessage.contentType);
|
1854
1943
|
}
|
@@ -1956,7 +2045,7 @@ class Visit {
|
|
1956
2045
|
}
|
1957
2046
|
cacheSnapshot() {
|
1958
2047
|
if (!this.snapshotCached) {
|
1959
|
-
this.view.cacheSnapshot().then((snapshot => snapshot && this.visitCachedSnapshot(snapshot)));
|
2048
|
+
this.view.cacheSnapshot(this.snapshot).then((snapshot => snapshot && this.visitCachedSnapshot(snapshot)));
|
1960
2049
|
this.snapshotCached = true;
|
1961
2050
|
}
|
1962
2051
|
}
|
@@ -2065,11 +2154,11 @@ class BrowserAdapter {
|
|
2065
2154
|
}
|
2066
2155
|
}
|
2067
2156
|
reload(reason) {
|
2157
|
+
var _a;
|
2068
2158
|
dispatch("turbo:reload", {
|
2069
2159
|
detail: reason
|
2070
2160
|
});
|
2071
|
-
|
2072
|
-
window.location.href = this.location.toString();
|
2161
|
+
window.location.href = ((_a = this.location) === null || _a === void 0 ? void 0 : _a.toString()) || window.location.href;
|
2073
2162
|
}
|
2074
2163
|
get navigator() {
|
2075
2164
|
return this.session.navigator;
|
@@ -2078,10 +2167,11 @@ class BrowserAdapter {
|
|
2078
2167
|
|
2079
2168
|
class CacheObserver {
|
2080
2169
|
constructor() {
|
2170
|
+
this.selector = "[data-turbo-temporary]";
|
2171
|
+
this.deprecatedSelector = "[data-turbo-cache=false]";
|
2081
2172
|
this.started = false;
|
2082
|
-
this.
|
2083
|
-
const
|
2084
|
-
for (const element of staleElements) {
|
2173
|
+
this.removeTemporaryElements = _event => {
|
2174
|
+
for (const element of this.temporaryElements) {
|
2085
2175
|
element.remove();
|
2086
2176
|
}
|
2087
2177
|
};
|
@@ -2089,14 +2179,24 @@ class CacheObserver {
|
|
2089
2179
|
start() {
|
2090
2180
|
if (!this.started) {
|
2091
2181
|
this.started = true;
|
2092
|
-
addEventListener("turbo:before-cache", this.
|
2182
|
+
addEventListener("turbo:before-cache", this.removeTemporaryElements, false);
|
2093
2183
|
}
|
2094
2184
|
}
|
2095
2185
|
stop() {
|
2096
2186
|
if (this.started) {
|
2097
2187
|
this.started = false;
|
2098
|
-
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.`);
|
2099
2198
|
}
|
2199
|
+
return [ ...elements ];
|
2100
2200
|
}
|
2101
2201
|
}
|
2102
2202
|
|
@@ -2104,24 +2204,24 @@ class FrameRedirector {
|
|
2104
2204
|
constructor(session, element) {
|
2105
2205
|
this.session = session;
|
2106
2206
|
this.element = element;
|
2107
|
-
this.
|
2207
|
+
this.linkInterceptor = new LinkInterceptor(this, element);
|
2108
2208
|
this.formSubmitObserver = new FormSubmitObserver(this, element);
|
2109
2209
|
}
|
2110
2210
|
start() {
|
2111
|
-
this.
|
2211
|
+
this.linkInterceptor.start();
|
2112
2212
|
this.formSubmitObserver.start();
|
2113
2213
|
}
|
2114
2214
|
stop() {
|
2115
|
-
this.
|
2215
|
+
this.linkInterceptor.stop();
|
2116
2216
|
this.formSubmitObserver.stop();
|
2117
2217
|
}
|
2118
|
-
|
2119
|
-
return this.shouldRedirect(element)
|
2218
|
+
shouldInterceptLinkClick(element, _location, _event) {
|
2219
|
+
return this.shouldRedirect(element);
|
2120
2220
|
}
|
2121
|
-
|
2221
|
+
linkClickIntercepted(element, url, event) {
|
2122
2222
|
const frame = this.findFrameElement(element);
|
2123
2223
|
if (frame) {
|
2124
|
-
frame.delegate.
|
2224
|
+
frame.delegate.linkClickIntercepted(element, url, event);
|
2125
2225
|
}
|
2126
2226
|
}
|
2127
2227
|
willSubmitForm(element, submitter) {
|
@@ -2133,17 +2233,6 @@ class FrameRedirector {
|
|
2133
2233
|
frame.delegate.formSubmitted(element, submitter);
|
2134
2234
|
}
|
2135
2235
|
}
|
2136
|
-
frameAllowsVisitingLocation(target, {href: url}, originalEvent) {
|
2137
|
-
const event = dispatch("turbo:click", {
|
2138
|
-
target: target,
|
2139
|
-
detail: {
|
2140
|
-
url: url,
|
2141
|
-
originalEvent: originalEvent
|
2142
|
-
},
|
2143
|
-
cancelable: true
|
2144
|
-
});
|
2145
|
-
return !event.defaultPrevented;
|
2146
|
-
}
|
2147
2236
|
shouldSubmit(form, submitter) {
|
2148
2237
|
var _a;
|
2149
2238
|
const action = getAction(form, submitter);
|
@@ -2268,7 +2357,6 @@ class Navigator {
|
|
2268
2357
|
}
|
2269
2358
|
}
|
2270
2359
|
startVisit(locatable, restorationIdentifier, options = {}) {
|
2271
|
-
this.lastVisit = this.currentVisit;
|
2272
2360
|
this.stop();
|
2273
2361
|
this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, Object.assign({
|
2274
2362
|
referrer: this.location
|
@@ -2308,7 +2396,7 @@ class Navigator {
|
|
2308
2396
|
if (formSubmission == this.formSubmission) {
|
2309
2397
|
const responseHTML = await fetchResponse.responseHTML;
|
2310
2398
|
if (responseHTML) {
|
2311
|
-
const shouldCacheSnapshot = formSubmission.
|
2399
|
+
const shouldCacheSnapshot = formSubmission.isSafe;
|
2312
2400
|
if (!shouldCacheSnapshot) {
|
2313
2401
|
this.view.clearSnapshotCache();
|
2314
2402
|
}
|
@@ -2355,12 +2443,10 @@ class Navigator {
|
|
2355
2443
|
this.delegate.visitCompleted(visit);
|
2356
2444
|
}
|
2357
2445
|
locationWithActionIsSamePage(location, action) {
|
2358
|
-
var _a;
|
2359
2446
|
const anchor = getAnchor(location);
|
2360
|
-
const
|
2361
|
-
const currentAnchor = getAnchor(lastLocation);
|
2447
|
+
const currentAnchor = getAnchor(this.view.lastRenderedLocation);
|
2362
2448
|
const isRestorationToTop = action === "restore" && typeof anchor === "undefined";
|
2363
|
-
return action !== "replace" && getRequestURL(location) === getRequestURL(
|
2449
|
+
return action !== "replace" && getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) && (isRestorationToTop || anchor != null && anchor !== currentAnchor);
|
2364
2450
|
}
|
2365
2451
|
visitScrolledToSamePageLocation(oldURL, newURL) {
|
2366
2452
|
this.delegate.visitScrolledToSamePageLocation(oldURL, newURL);
|
@@ -2371,10 +2457,8 @@ class Navigator {
|
|
2371
2457
|
get restorationIdentifier() {
|
2372
2458
|
return this.history.restorationIdentifier;
|
2373
2459
|
}
|
2374
|
-
getActionForFormSubmission(
|
2375
|
-
|
2376
|
-
const action = getAttribute("data-turbo-action", submitter, formElement);
|
2377
|
-
return isAction(action) ? action : "advance";
|
2460
|
+
getActionForFormSubmission({submitter: submitter, formElement: formElement}) {
|
2461
|
+
return getVisitAction(submitter, formElement) || "advance";
|
2378
2462
|
}
|
2379
2463
|
}
|
2380
2464
|
|
@@ -2622,7 +2706,7 @@ class PageRenderer extends Renderer {
|
|
2622
2706
|
}
|
2623
2707
|
async render() {
|
2624
2708
|
if (this.willRender) {
|
2625
|
-
this.replaceBody();
|
2709
|
+
await this.replaceBody();
|
2626
2710
|
}
|
2627
2711
|
}
|
2628
2712
|
finishRendering() {
|
@@ -2641,16 +2725,16 @@ class PageRenderer extends Renderer {
|
|
2641
2725
|
return this.newSnapshot.element;
|
2642
2726
|
}
|
2643
2727
|
async mergeHead() {
|
2728
|
+
const mergedHeadElements = this.mergeProvisionalElements();
|
2644
2729
|
const newStylesheetElements = this.copyNewHeadStylesheetElements();
|
2645
2730
|
this.copyNewHeadScriptElements();
|
2646
|
-
|
2647
|
-
this.copyNewHeadProvisionalElements();
|
2731
|
+
await mergedHeadElements;
|
2648
2732
|
await newStylesheetElements;
|
2649
2733
|
}
|
2650
|
-
replaceBody() {
|
2651
|
-
this.preservingPermanentElements((() => {
|
2734
|
+
async replaceBody() {
|
2735
|
+
await this.preservingPermanentElements((async () => {
|
2652
2736
|
this.activateNewBody();
|
2653
|
-
this.assignNewBody();
|
2737
|
+
await this.assignNewBody();
|
2654
2738
|
}));
|
2655
2739
|
}
|
2656
2740
|
get trackedElementsAreIdentical() {
|
@@ -2669,6 +2753,35 @@ class PageRenderer extends Renderer {
|
|
2669
2753
|
document.head.appendChild(activateScriptElement(element));
|
2670
2754
|
}
|
2671
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
|
+
}
|
2672
2785
|
removeCurrentHeadProvisionalElements() {
|
2673
2786
|
for (const element of this.currentHeadProvisionalElements) {
|
2674
2787
|
document.head.removeChild(element);
|
@@ -2689,8 +2802,8 @@ class PageRenderer extends Renderer {
|
|
2689
2802
|
inertScriptElement.replaceWith(activatedScriptElement);
|
2690
2803
|
}
|
2691
2804
|
}
|
2692
|
-
assignNewBody() {
|
2693
|
-
this.renderElement(this.currentElement, this.newElement);
|
2805
|
+
async assignNewBody() {
|
2806
|
+
await this.renderElement(this.currentElement, this.newElement);
|
2694
2807
|
}
|
2695
2808
|
get newHeadStylesheetElements() {
|
2696
2809
|
return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot);
|
@@ -2777,10 +2890,10 @@ class PageView extends View {
|
|
2777
2890
|
clearSnapshotCache() {
|
2778
2891
|
this.snapshotCache.clear();
|
2779
2892
|
}
|
2780
|
-
async cacheSnapshot() {
|
2781
|
-
if (
|
2893
|
+
async cacheSnapshot(snapshot = this.snapshot) {
|
2894
|
+
if (snapshot.isCacheable) {
|
2782
2895
|
this.delegate.viewWillCacheSnapshot();
|
2783
|
-
const {
|
2896
|
+
const {lastRenderedLocation: location} = this;
|
2784
2897
|
await nextEventLoopTick();
|
2785
2898
|
const cachedSnapshot = snapshot.clone();
|
2786
2899
|
this.snapshotCache.put(location, cachedSnapshot);
|
@@ -2793,9 +2906,6 @@ class PageView extends View {
|
|
2793
2906
|
get snapshot() {
|
2794
2907
|
return PageSnapshot.fromElement(this.element);
|
2795
2908
|
}
|
2796
|
-
get shouldCacheSnapshot() {
|
2797
|
-
return this.snapshot.isCacheable;
|
2798
|
-
}
|
2799
2909
|
}
|
2800
2910
|
|
2801
2911
|
class Preloader {
|
@@ -3127,8 +3237,8 @@ class Session {
|
|
3127
3237
|
}
|
3128
3238
|
}
|
3129
3239
|
elementIsNavigatable(element) {
|
3130
|
-
const container = element
|
3131
|
-
const withinFrame = element
|
3240
|
+
const container = findClosestRecursively(element, "[data-turbo]");
|
3241
|
+
const withinFrame = findClosestRecursively(element, "turbo-frame");
|
3132
3242
|
if (this.drive || withinFrame) {
|
3133
3243
|
if (container) {
|
3134
3244
|
return container.getAttribute("data-turbo") != "false";
|
@@ -3144,8 +3254,7 @@ class Session {
|
|
3144
3254
|
}
|
3145
3255
|
}
|
3146
3256
|
getActionForLink(link) {
|
3147
|
-
|
3148
|
-
return isAction(action) ? action : "advance";
|
3257
|
+
return getVisitAction(link) || "advance";
|
3149
3258
|
}
|
3150
3259
|
get snapshot() {
|
3151
3260
|
return this.view.snapshot;
|
@@ -3213,7 +3322,10 @@ const StreamActions = {
|
|
3213
3322
|
this.targetElements.forEach((e => e.replaceWith(this.templateContent)));
|
3214
3323
|
},
|
3215
3324
|
update() {
|
3216
|
-
this.targetElements.forEach((
|
3325
|
+
this.targetElements.forEach((targetElement => {
|
3326
|
+
targetElement.innerHTML = "";
|
3327
|
+
targetElement.append(this.templateContent);
|
3328
|
+
}));
|
3217
3329
|
}
|
3218
3330
|
};
|
3219
3331
|
|
@@ -3285,6 +3397,8 @@ var Turbo = Object.freeze({
|
|
3285
3397
|
StreamActions: StreamActions
|
3286
3398
|
});
|
3287
3399
|
|
3400
|
+
class TurboFrameMissingError extends Error {}
|
3401
|
+
|
3288
3402
|
class FrameController {
|
3289
3403
|
constructor(element) {
|
3290
3404
|
this.fetchResponseLoaded = _fetchResponse => {};
|
@@ -3305,7 +3419,7 @@ class FrameController {
|
|
3305
3419
|
this.view = new FrameView(this, this.element);
|
3306
3420
|
this.appearanceObserver = new AppearanceObserver(this, this.element);
|
3307
3421
|
this.formLinkClickObserver = new FormLinkClickObserver(this, this.element);
|
3308
|
-
this.
|
3422
|
+
this.linkInterceptor = new LinkInterceptor(this, this.element);
|
3309
3423
|
this.restorationIdentifier = uuid();
|
3310
3424
|
this.formSubmitObserver = new FormSubmitObserver(this, this.element);
|
3311
3425
|
}
|
@@ -3318,7 +3432,7 @@ class FrameController {
|
|
3318
3432
|
this.loadSourceURL();
|
3319
3433
|
}
|
3320
3434
|
this.formLinkClickObserver.start();
|
3321
|
-
this.
|
3435
|
+
this.linkInterceptor.start();
|
3322
3436
|
this.formSubmitObserver.start();
|
3323
3437
|
}
|
3324
3438
|
}
|
@@ -3327,7 +3441,7 @@ class FrameController {
|
|
3327
3441
|
this.connected = false;
|
3328
3442
|
this.appearanceObserver.stop();
|
3329
3443
|
this.formLinkClickObserver.stop();
|
3330
|
-
this.
|
3444
|
+
this.linkInterceptor.stop();
|
3331
3445
|
this.formSubmitObserver.stop();
|
3332
3446
|
}
|
3333
3447
|
}
|
@@ -3345,6 +3459,15 @@ class FrameController {
|
|
3345
3459
|
this.loadSourceURL();
|
3346
3460
|
}
|
3347
3461
|
}
|
3462
|
+
sourceURLReloaded() {
|
3463
|
+
const {src: src} = this.element;
|
3464
|
+
this.ignoringChangesToAttribute("complete", (() => {
|
3465
|
+
this.element.removeAttribute("complete");
|
3466
|
+
}));
|
3467
|
+
this.element.src = null;
|
3468
|
+
this.element.src = src;
|
3469
|
+
return this.element.loaded;
|
3470
|
+
}
|
3348
3471
|
completeChanged() {
|
3349
3472
|
if (this.isIgnoringChangesTo("complete")) return;
|
3350
3473
|
this.loadSourceURL();
|
@@ -3372,45 +3495,34 @@ class FrameController {
|
|
3372
3495
|
try {
|
3373
3496
|
const html = await fetchResponse.responseHTML;
|
3374
3497
|
if (html) {
|
3375
|
-
const
|
3376
|
-
const
|
3377
|
-
if (
|
3378
|
-
|
3379
|
-
|
3380
|
-
|
3381
|
-
this.changeHistory();
|
3382
|
-
await this.view.render(renderer);
|
3383
|
-
this.complete = true;
|
3384
|
-
session.frameRendered(fetchResponse, this.element);
|
3385
|
-
session.frameLoaded(this.element);
|
3386
|
-
this.fetchResponseLoaded(fetchResponse);
|
3387
|
-
} else if (this.willHandleFrameMissingFromResponse(fetchResponse)) {
|
3388
|
-
console.warn(`A matching frame for #${this.element.id} was missing from the response, transforming into full-page Visit.`);
|
3389
|
-
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);
|
3390
3504
|
}
|
3391
3505
|
}
|
3392
|
-
} catch (error) {
|
3393
|
-
console.error(error);
|
3394
|
-
this.view.invalidate();
|
3395
3506
|
} finally {
|
3396
3507
|
this.fetchResponseLoaded = () => {};
|
3397
3508
|
}
|
3398
3509
|
}
|
3399
|
-
elementAppearedInViewport(
|
3510
|
+
elementAppearedInViewport(element) {
|
3511
|
+
this.proposeVisitIfNavigatedWithAction(element, element);
|
3400
3512
|
this.loadSourceURL();
|
3401
3513
|
}
|
3402
3514
|
willSubmitFormLinkToLocation(link) {
|
3403
|
-
return
|
3515
|
+
return this.shouldInterceptNavigation(link);
|
3404
3516
|
}
|
3405
3517
|
submittedFormLinkToLocation(link, _location, form) {
|
3406
3518
|
const frame = this.findFrameElement(link);
|
3407
3519
|
if (frame) form.setAttribute("data-turbo-frame", frame.id);
|
3408
3520
|
}
|
3409
|
-
|
3410
|
-
return this.shouldInterceptNavigation(element)
|
3521
|
+
shouldInterceptLinkClick(element, _location, _event) {
|
3522
|
+
return this.shouldInterceptNavigation(element);
|
3411
3523
|
}
|
3412
|
-
|
3413
|
-
this.navigateFrame(element, location
|
3524
|
+
linkClickIntercepted(element, location) {
|
3525
|
+
this.navigateFrame(element, location);
|
3414
3526
|
}
|
3415
3527
|
willSubmitForm(element, submitter) {
|
3416
3528
|
return element.closest("turbo-frame") == this.element && this.shouldInterceptNavigation(element, submitter);
|
@@ -3421,12 +3533,12 @@ class FrameController {
|
|
3421
3533
|
}
|
3422
3534
|
this.formSubmission = new FormSubmission(this, element, submitter);
|
3423
3535
|
const {fetchRequest: fetchRequest} = this.formSubmission;
|
3424
|
-
this.
|
3536
|
+
this.prepareRequest(fetchRequest);
|
3425
3537
|
this.formSubmission.start();
|
3426
3538
|
}
|
3427
|
-
|
3539
|
+
prepareRequest(request) {
|
3428
3540
|
var _a;
|
3429
|
-
headers["Turbo-Frame"] = this.id;
|
3541
|
+
request.headers["Turbo-Frame"] = this.id;
|
3430
3542
|
if ((_a = this.currentNavigationElement) === null || _a === void 0 ? void 0 : _a.hasAttribute("data-turbo-stream")) {
|
3431
3543
|
request.acceptResponseType(StreamMessage.contentType);
|
3432
3544
|
}
|
@@ -3442,7 +3554,6 @@ class FrameController {
|
|
3442
3554
|
this.resolveVisitPromise();
|
3443
3555
|
}
|
3444
3556
|
async requestFailedWithResponse(request, response) {
|
3445
|
-
console.error(response);
|
3446
3557
|
await this.loadResponse(response);
|
3447
3558
|
this.resolveVisitPromise();
|
3448
3559
|
}
|
@@ -3458,11 +3569,15 @@ class FrameController {
|
|
3458
3569
|
}
|
3459
3570
|
formSubmissionSucceededWithResponse(formSubmission, response) {
|
3460
3571
|
const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
|
3461
|
-
|
3572
|
+
frame.delegate.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
|
3462
3573
|
frame.delegate.loadResponse(response);
|
3574
|
+
if (!formSubmission.isSafe) {
|
3575
|
+
session.clearCache();
|
3576
|
+
}
|
3463
3577
|
}
|
3464
3578
|
formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
|
3465
3579
|
this.element.delegate.loadResponse(fetchResponse);
|
3580
|
+
session.clearCache();
|
3466
3581
|
}
|
3467
3582
|
formSubmissionErrored(formSubmission, error) {
|
3468
3583
|
console.error(error);
|
@@ -3492,6 +3607,22 @@ class FrameController {
|
|
3492
3607
|
willRenderFrame(currentElement, _newElement) {
|
3493
3608
|
this.previousFrameElement = currentElement.cloneNode(true);
|
3494
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
|
+
}
|
3495
3626
|
async visit(url) {
|
3496
3627
|
var _a;
|
3497
3628
|
const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams, this.element);
|
@@ -3508,15 +3639,15 @@ class FrameController {
|
|
3508
3639
|
}
|
3509
3640
|
navigateFrame(element, url, submitter) {
|
3510
3641
|
const frame = this.findFrameElement(element, submitter);
|
3511
|
-
|
3642
|
+
frame.delegate.proposeVisitIfNavigatedWithAction(frame, element, submitter);
|
3512
3643
|
this.withCurrentNavigationElement(element, (() => {
|
3513
3644
|
frame.src = url;
|
3514
3645
|
}));
|
3515
3646
|
}
|
3516
3647
|
proposeVisitIfNavigatedWithAction(frame, element, submitter) {
|
3517
3648
|
this.action = getVisitAction(submitter, element, frame);
|
3518
|
-
this.
|
3519
|
-
|
3649
|
+
if (this.action) {
|
3650
|
+
const pageSnapshot = PageSnapshot.fromElement(frame).clone();
|
3520
3651
|
const {visitCachedSnapshot: visitCachedSnapshot} = frame.delegate;
|
3521
3652
|
frame.delegate.fetchResponseLoaded = fetchResponse => {
|
3522
3653
|
if (frame.src) {
|
@@ -3532,7 +3663,8 @@ class FrameController {
|
|
3532
3663
|
visitCachedSnapshot: visitCachedSnapshot,
|
3533
3664
|
willRender: false,
|
3534
3665
|
updateHistory: false,
|
3535
|
-
restorationIdentifier: this.restorationIdentifier
|
3666
|
+
restorationIdentifier: this.restorationIdentifier,
|
3667
|
+
snapshot: pageSnapshot
|
3536
3668
|
};
|
3537
3669
|
if (this.action) options.action = this.action;
|
3538
3670
|
session.visit(frame.src, options);
|
@@ -3541,11 +3673,15 @@ class FrameController {
|
|
3541
3673
|
}
|
3542
3674
|
}
|
3543
3675
|
changeHistory() {
|
3544
|
-
if (this.action
|
3676
|
+
if (this.action) {
|
3545
3677
|
const method = getHistoryMethodForAction(this.action);
|
3546
|
-
session.history.update(method, expandURL(this.
|
3678
|
+
session.history.update(method, expandURL(this.element.src || ""), this.restorationIdentifier);
|
3547
3679
|
}
|
3548
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
|
+
}
|
3549
3685
|
willHandleFrameMissingFromResponse(fetchResponse) {
|
3550
3686
|
this.element.setAttribute("complete", "");
|
3551
3687
|
const response = fetchResponse.response;
|
@@ -3566,6 +3702,14 @@ class FrameController {
|
|
3566
3702
|
});
|
3567
3703
|
return !event.defaultPrevented;
|
3568
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
|
+
}
|
3569
3713
|
async visitResponse(response) {
|
3570
3714
|
const wrapped = new FetchResponse(response);
|
3571
3715
|
const responseHTML = await wrapped.responseHTML;
|
@@ -3671,17 +3815,6 @@ class FrameController {
|
|
3671
3815
|
const root = (_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/";
|
3672
3816
|
return expandURL(root);
|
3673
3817
|
}
|
3674
|
-
frameAllowsVisitingLocation(target, {href: url}, originalEvent) {
|
3675
|
-
const event = dispatch("turbo:click", {
|
3676
|
-
target: target,
|
3677
|
-
detail: {
|
3678
|
-
url: url,
|
3679
|
-
originalEvent: originalEvent
|
3680
|
-
},
|
3681
|
-
cancelable: true
|
3682
|
-
});
|
3683
|
-
return !event.defaultPrevented;
|
3684
|
-
}
|
3685
3818
|
isIgnoringChangesTo(attributeName) {
|
3686
3819
|
return this.ignoredAttributes.has(attributeName);
|
3687
3820
|
}
|
@@ -3971,7 +4104,9 @@ class TurboCableStreamSourceElement extends HTMLElement {
|
|
3971
4104
|
async connectedCallback() {
|
3972
4105
|
connectStreamSource(this);
|
3973
4106
|
this.subscription = await subscribeTo(this.channel, {
|
3974
|
-
received: this.dispatchMessageEvent.bind(this)
|
4107
|
+
received: this.dispatchMessageEvent.bind(this),
|
4108
|
+
connected: this.subscriptionConnected.bind(this),
|
4109
|
+
disconnected: this.subscriptionDisconnected.bind(this)
|
3975
4110
|
});
|
3976
4111
|
}
|
3977
4112
|
disconnectedCallback() {
|
@@ -3984,6 +4119,12 @@ class TurboCableStreamSourceElement extends HTMLElement {
|
|
3984
4119
|
});
|
3985
4120
|
return this.dispatchEvent(event);
|
3986
4121
|
}
|
4122
|
+
subscriptionConnected() {
|
4123
|
+
this.setAttribute("connected", "");
|
4124
|
+
}
|
4125
|
+
subscriptionDisconnected() {
|
4126
|
+
this.removeAttribute("connected");
|
4127
|
+
}
|
3987
4128
|
get channel() {
|
3988
4129
|
const channel = this.getAttribute("channel");
|
3989
4130
|
const signed_stream_name = this.getAttribute("signed-stream-name");
|
@@ -3997,18 +4138,21 @@ class TurboCableStreamSourceElement extends HTMLElement {
|
|
3997
4138
|
}
|
3998
4139
|
}
|
3999
4140
|
|
4000
|
-
customElements.
|
4141
|
+
if (customElements.get("turbo-cable-stream-source") === undefined) {
|
4142
|
+
customElements.define("turbo-cable-stream-source", TurboCableStreamSourceElement);
|
4143
|
+
}
|
4001
4144
|
|
4002
4145
|
function encodeMethodIntoRequestBody(event) {
|
4003
4146
|
if (event.target instanceof HTMLFormElement) {
|
4004
4147
|
const {target: form, detail: {fetchOptions: fetchOptions}} = event;
|
4005
4148
|
form.addEventListener("turbo:submit-start", (({detail: {formSubmission: {submitter: submitter}}}) => {
|
4006
|
-
const
|
4149
|
+
const body = isBodyInit(fetchOptions.body) ? fetchOptions.body : new URLSearchParams;
|
4150
|
+
const method = determineFetchMethod(submitter, body, form);
|
4007
4151
|
if (!/get/i.test(method)) {
|
4008
4152
|
if (/post/i.test(method)) {
|
4009
|
-
|
4153
|
+
body.delete("_method");
|
4010
4154
|
} else {
|
4011
|
-
|
4155
|
+
body.set("_method", method);
|
4012
4156
|
}
|
4013
4157
|
fetchOptions.method = "post";
|
4014
4158
|
}
|
@@ -4018,6 +4162,37 @@ function encodeMethodIntoRequestBody(event) {
|
|
4018
4162
|
}
|
4019
4163
|
}
|
4020
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
|
+
|
4021
4196
|
addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody);
|
4022
4197
|
|
4023
4198
|
var adapters = {
|