@hotwired/turbo 7.0.0-beta.3 → 7.0.0-beta.4
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.
- package/dist/turbo.es2017-esm.js +647 -574
- package/dist/turbo.es2017-esm.js.map +1 -1
- package/dist/turbo.es2017-umd.js +647 -574
- package/dist/turbo.es2017-umd.js.map +1 -1
- package/dist/turbo.es5-umd.js +1153 -843
- package/dist/turbo.es5-umd.js.map +1 -1
- package/dist/types/core/drive/error_renderer.d.ts +7 -11
- package/dist/types/core/drive/form_submission.d.ts +11 -3
- package/dist/types/core/drive/head_snapshot.d.ts +21 -0
- package/dist/types/core/drive/history.d.ts +5 -6
- package/dist/types/core/drive/navigator.d.ts +7 -7
- package/dist/types/core/drive/page_renderer.d.ts +26 -0
- package/dist/types/core/drive/page_snapshot.d.ts +17 -0
- package/dist/types/core/drive/page_view.d.ts +21 -0
- package/dist/types/core/drive/snapshot_cache.d.ts +8 -9
- package/dist/types/core/drive/visit.d.ts +13 -15
- package/dist/types/core/frames/frame_controller.d.ts +11 -10
- package/dist/types/core/frames/frame_renderer.d.ts +8 -0
- package/dist/types/core/frames/frame_view.d.ts +7 -0
- package/dist/types/core/index.d.ts +1 -1
- package/dist/types/core/native/adapter.d.ts +1 -2
- package/dist/types/core/native/browser_adapter.d.ts +1 -2
- package/dist/types/core/renderer.d.ts +26 -0
- package/dist/types/core/session.d.ts +23 -21
- package/dist/types/core/snapshot.d.ts +11 -0
- package/dist/types/core/streams/stream_message.d.ts +1 -0
- package/dist/types/core/url.d.ts +7 -0
- package/dist/types/core/view.d.ts +27 -0
- package/dist/types/http/fetch_request.d.ts +8 -11
- package/dist/types/http/fetch_response.d.ts +1 -2
- package/dist/types/observers/link_click_observer.d.ts +3 -4
- package/dist/types/observers/stream_observer.d.ts +0 -1
- package/dist/types/tests/functional/form_submission_tests.d.ts +11 -3
- package/dist/types/tests/functional/frame_tests.d.ts +4 -0
- package/dist/types/tests/functional/navigation_tests.d.ts +1 -0
- package/dist/types/tests/functional/rendering_tests.d.ts +2 -0
- package/dist/types/tests/functional/stream_tests.d.ts +0 -2
- package/dist/types/tests/helpers/functional_test_case.d.ts +3 -0
- package/dist/types/tests/unit/deprecated_adapter_support_test.d.ts +21 -0
- package/dist/types/tests/unit/index.d.ts +1 -0
- package/dist/types/util.d.ts +2 -0
- package/package.json +1 -1
- package/dist/types/core/drive/head_details.d.ts +0 -22
- package/dist/types/core/drive/renderer.d.ts +0 -13
- package/dist/types/core/drive/snapshot.d.ts +0 -24
- package/dist/types/core/drive/snapshot_renderer.d.ts +0 -43
- package/dist/types/core/drive/view.d.ts +0 -34
- package/dist/types/core/location.d.ts +0 -22
package/dist/turbo.es2017-esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
Turbo 7.0.0-beta.
|
|
2
|
+
Turbo 7.0.0-beta.4
|
|
3
3
|
Copyright © 2021 Basecamp, LLC
|
|
4
4
|
*/
|
|
5
5
|
(function () {
|
|
@@ -135,80 +135,53 @@ function frameLoadingStyleFromString(style) {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
else {
|
|
148
|
-
this.requestURL = this.absoluteURL.slice(0, -anchorLength);
|
|
149
|
-
this.anchor = linkWithAnchor.hash.slice(1);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
static get currentLocation() {
|
|
153
|
-
return this.wrap(window.location.toString());
|
|
154
|
-
}
|
|
155
|
-
static wrap(locatable) {
|
|
156
|
-
if (typeof locatable == "string") {
|
|
157
|
-
return new this(locatable);
|
|
158
|
-
}
|
|
159
|
-
else if (locatable != null) {
|
|
160
|
-
return locatable;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
getOrigin() {
|
|
164
|
-
return this.absoluteURL.split("/", 3).join("/");
|
|
165
|
-
}
|
|
166
|
-
getPath() {
|
|
167
|
-
return (this.requestURL.match(/\/\/[^/]*(\/[^?;]*)/) || [])[1] || "/";
|
|
168
|
-
}
|
|
169
|
-
getPathComponents() {
|
|
170
|
-
return this.getPath().split("/").slice(1);
|
|
171
|
-
}
|
|
172
|
-
getLastPathComponent() {
|
|
173
|
-
return this.getPathComponents().slice(-1)[0];
|
|
174
|
-
}
|
|
175
|
-
getExtension() {
|
|
176
|
-
return (this.getLastPathComponent().match(/\.[^.]*$/) || [])[0] || "";
|
|
177
|
-
}
|
|
178
|
-
isHTML() {
|
|
179
|
-
return !!this.getExtension().match(/^(?:|\.(?:htm|html|xhtml))$/);
|
|
180
|
-
}
|
|
181
|
-
isPrefixedBy(location) {
|
|
182
|
-
const prefixURL = getPrefixURL(location);
|
|
183
|
-
return this.isEqualTo(location) || stringStartsWith(this.absoluteURL, prefixURL);
|
|
184
|
-
}
|
|
185
|
-
isEqualTo(location) {
|
|
186
|
-
return location && this.absoluteURL === location.absoluteURL;
|
|
138
|
+
function expandURL(locatable) {
|
|
139
|
+
const anchor = document.createElement("a");
|
|
140
|
+
anchor.href = locatable.toString();
|
|
141
|
+
return new URL(anchor.href);
|
|
142
|
+
}
|
|
143
|
+
function getAnchor(url) {
|
|
144
|
+
let anchorMatch;
|
|
145
|
+
if (url.hash) {
|
|
146
|
+
return url.hash.slice(1);
|
|
187
147
|
}
|
|
188
|
-
|
|
189
|
-
return
|
|
148
|
+
else if (anchorMatch = url.href.match(/#(.*)$/)) {
|
|
149
|
+
return anchorMatch[1];
|
|
190
150
|
}
|
|
191
|
-
|
|
192
|
-
return
|
|
151
|
+
else {
|
|
152
|
+
return "";
|
|
193
153
|
}
|
|
194
|
-
|
|
195
|
-
|
|
154
|
+
}
|
|
155
|
+
function getExtension(url) {
|
|
156
|
+
return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
|
|
157
|
+
}
|
|
158
|
+
function isHTML(url) {
|
|
159
|
+
return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml))$/);
|
|
160
|
+
}
|
|
161
|
+
function isPrefixedBy(baseURL, url) {
|
|
162
|
+
const prefix = getPrefix(url);
|
|
163
|
+
return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
|
|
164
|
+
}
|
|
165
|
+
function toCacheKey(url) {
|
|
166
|
+
const anchorLength = url.hash.length;
|
|
167
|
+
if (anchorLength < 2) {
|
|
168
|
+
return url.href;
|
|
196
169
|
}
|
|
197
|
-
|
|
198
|
-
return
|
|
170
|
+
else {
|
|
171
|
+
return url.href.slice(0, -anchorLength);
|
|
199
172
|
}
|
|
200
173
|
}
|
|
201
|
-
function
|
|
202
|
-
return
|
|
174
|
+
function getPathComponents(url) {
|
|
175
|
+
return url.pathname.split("/").slice(1);
|
|
203
176
|
}
|
|
204
|
-
function
|
|
205
|
-
return
|
|
177
|
+
function getLastPathComponent(url) {
|
|
178
|
+
return getPathComponents(url).slice(-1)[0];
|
|
206
179
|
}
|
|
207
|
-
function
|
|
208
|
-
return
|
|
180
|
+
function getPrefix(url) {
|
|
181
|
+
return addTrailingSlash(url.origin + url.pathname);
|
|
209
182
|
}
|
|
210
|
-
function
|
|
211
|
-
return
|
|
183
|
+
function addTrailingSlash(value) {
|
|
184
|
+
return value.endsWith("/") ? value : value + "/";
|
|
212
185
|
}
|
|
213
186
|
|
|
214
187
|
class FetchResponse {
|
|
@@ -231,7 +204,7 @@ class FetchResponse {
|
|
|
231
204
|
return this.response.redirected;
|
|
232
205
|
}
|
|
233
206
|
get location() {
|
|
234
|
-
return
|
|
207
|
+
return expandURL(this.response.url);
|
|
235
208
|
}
|
|
236
209
|
get isHTML() {
|
|
237
210
|
return this.contentType && this.contentType.match(/^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/);
|
|
@@ -266,9 +239,15 @@ function dispatch(eventName, { target, cancelable, detail } = {}) {
|
|
|
266
239
|
function nextAnimationFrame() {
|
|
267
240
|
return new Promise(resolve => requestAnimationFrame(() => resolve()));
|
|
268
241
|
}
|
|
242
|
+
function nextEventLoopTick() {
|
|
243
|
+
return new Promise(resolve => setTimeout(() => resolve(), 0));
|
|
244
|
+
}
|
|
269
245
|
function nextMicrotask() {
|
|
270
246
|
return Promise.resolve();
|
|
271
247
|
}
|
|
248
|
+
function parseHTMLDocument(html = "") {
|
|
249
|
+
return new DOMParser().parseFromString(html, "text/html");
|
|
250
|
+
}
|
|
272
251
|
function unindent(strings, ...values) {
|
|
273
252
|
const lines = interpolate(strings, values).replace(/^\n/, "").split("\n");
|
|
274
253
|
const match = lines[0].match(/^\s+/);
|
|
@@ -316,28 +295,23 @@ function fetchMethodFromString(method) {
|
|
|
316
295
|
}
|
|
317
296
|
}
|
|
318
297
|
class FetchRequest {
|
|
319
|
-
constructor(delegate, method, location, body) {
|
|
298
|
+
constructor(delegate, method, location, body = new URLSearchParams) {
|
|
320
299
|
this.abortController = new AbortController;
|
|
321
300
|
this.delegate = delegate;
|
|
322
301
|
this.method = method;
|
|
323
|
-
this.
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
get url() {
|
|
327
|
-
const url = this.location.absoluteURL;
|
|
328
|
-
const query = this.params.toString();
|
|
329
|
-
if (this.isIdempotent && query.length) {
|
|
330
|
-
return [url, query].join(url.includes("?") ? "&" : "?");
|
|
302
|
+
if (this.isIdempotent) {
|
|
303
|
+
this.url = mergeFormDataEntries(location, [...body.entries()]);
|
|
331
304
|
}
|
|
332
305
|
else {
|
|
333
|
-
|
|
306
|
+
this.body = body;
|
|
307
|
+
this.url = location;
|
|
334
308
|
}
|
|
335
309
|
}
|
|
310
|
+
get location() {
|
|
311
|
+
return this.url;
|
|
312
|
+
}
|
|
336
313
|
get params() {
|
|
337
|
-
return this.
|
|
338
|
-
params.append(name, value.toString());
|
|
339
|
-
return params;
|
|
340
|
-
}, new URLSearchParams);
|
|
314
|
+
return this.url.searchParams;
|
|
341
315
|
}
|
|
342
316
|
get entries() {
|
|
343
317
|
return this.body ? Array.from(this.body.entries()) : [];
|
|
@@ -350,7 +324,7 @@ class FetchRequest {
|
|
|
350
324
|
dispatch("turbo:before-fetch-request", { detail: { fetchOptions } });
|
|
351
325
|
try {
|
|
352
326
|
this.delegate.requestStarted(this);
|
|
353
|
-
const response = await fetch(this.url, fetchOptions);
|
|
327
|
+
const response = await fetch(this.url.href, fetchOptions);
|
|
354
328
|
return await this.receive(response);
|
|
355
329
|
}
|
|
356
330
|
catch (error) {
|
|
@@ -381,7 +355,7 @@ class FetchRequest {
|
|
|
381
355
|
credentials: "same-origin",
|
|
382
356
|
headers: this.headers,
|
|
383
357
|
redirect: "follow",
|
|
384
|
-
body: this.
|
|
358
|
+
body: this.body,
|
|
385
359
|
signal: this.abortSignal
|
|
386
360
|
};
|
|
387
361
|
}
|
|
@@ -389,19 +363,35 @@ class FetchRequest {
|
|
|
389
363
|
return this.method == FetchMethod.get;
|
|
390
364
|
}
|
|
391
365
|
get headers() {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if (typeof this.delegate.additionalHeadersForRequest == "function") {
|
|
396
|
-
return this.delegate.additionalHeadersForRequest(this);
|
|
397
|
-
}
|
|
398
|
-
else {
|
|
399
|
-
return {};
|
|
366
|
+
const headers = Object.assign({}, this.defaultHeaders);
|
|
367
|
+
if (typeof this.delegate.prepareHeadersForRequest == "function") {
|
|
368
|
+
this.delegate.prepareHeadersForRequest(headers, this);
|
|
400
369
|
}
|
|
370
|
+
return headers;
|
|
401
371
|
}
|
|
402
372
|
get abortSignal() {
|
|
403
373
|
return this.abortController.signal;
|
|
404
374
|
}
|
|
375
|
+
get defaultHeaders() {
|
|
376
|
+
return {
|
|
377
|
+
"Accept": "text/html, application/xhtml+xml"
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function mergeFormDataEntries(url, entries) {
|
|
382
|
+
const currentSearchParams = new URLSearchParams(url.search);
|
|
383
|
+
for (const [name, value] of entries) {
|
|
384
|
+
if (value instanceof File)
|
|
385
|
+
continue;
|
|
386
|
+
if (currentSearchParams.has(name)) {
|
|
387
|
+
currentSearchParams.delete(name);
|
|
388
|
+
url.searchParams.set(name, value);
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
url.searchParams.append(name, value);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return url;
|
|
405
395
|
}
|
|
406
396
|
|
|
407
397
|
class AppearanceObserver {
|
|
@@ -431,6 +421,42 @@ class AppearanceObserver {
|
|
|
431
421
|
}
|
|
432
422
|
}
|
|
433
423
|
|
|
424
|
+
class StreamMessage {
|
|
425
|
+
constructor(html) {
|
|
426
|
+
this.templateElement = document.createElement("template");
|
|
427
|
+
this.templateElement.innerHTML = html;
|
|
428
|
+
}
|
|
429
|
+
static wrap(message) {
|
|
430
|
+
if (typeof message == "string") {
|
|
431
|
+
return new this(message);
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
return message;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
get fragment() {
|
|
438
|
+
const fragment = document.createDocumentFragment();
|
|
439
|
+
for (const element of this.foreignElements) {
|
|
440
|
+
fragment.appendChild(document.importNode(element, true));
|
|
441
|
+
}
|
|
442
|
+
return fragment;
|
|
443
|
+
}
|
|
444
|
+
get foreignElements() {
|
|
445
|
+
return this.templateChildren.reduce((streamElements, child) => {
|
|
446
|
+
if (child.tagName.toLowerCase() == "turbo-stream") {
|
|
447
|
+
return [...streamElements, child];
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
return streamElements;
|
|
451
|
+
}
|
|
452
|
+
}, []);
|
|
453
|
+
}
|
|
454
|
+
get templateChildren() {
|
|
455
|
+
return Array.from(this.templateElement.content.children);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
StreamMessage.contentType = "text/vnd.turbo-stream.html";
|
|
459
|
+
|
|
434
460
|
var FormSubmissionState;
|
|
435
461
|
(function (FormSubmissionState) {
|
|
436
462
|
FormSubmissionState[FormSubmissionState["initialized"] = 0] = "initialized";
|
|
@@ -440,14 +466,27 @@ var FormSubmissionState;
|
|
|
440
466
|
FormSubmissionState[FormSubmissionState["stopping"] = 4] = "stopping";
|
|
441
467
|
FormSubmissionState[FormSubmissionState["stopped"] = 5] = "stopped";
|
|
442
468
|
})(FormSubmissionState || (FormSubmissionState = {}));
|
|
469
|
+
var FormEnctype;
|
|
470
|
+
(function (FormEnctype) {
|
|
471
|
+
FormEnctype["urlEncoded"] = "application/x-www-form-urlencoded";
|
|
472
|
+
FormEnctype["multipart"] = "multipart/form-data";
|
|
473
|
+
FormEnctype["plain"] = "text/plain";
|
|
474
|
+
})(FormEnctype || (FormEnctype = {}));
|
|
475
|
+
function formEnctypeFromString(encoding) {
|
|
476
|
+
switch (encoding.toLowerCase()) {
|
|
477
|
+
case FormEnctype.multipart: return FormEnctype.multipart;
|
|
478
|
+
case FormEnctype.plain: return FormEnctype.plain;
|
|
479
|
+
default: return FormEnctype.urlEncoded;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
443
482
|
class FormSubmission {
|
|
444
483
|
constructor(delegate, formElement, submitter, mustRedirect = false) {
|
|
445
484
|
this.state = FormSubmissionState.initialized;
|
|
446
485
|
this.delegate = delegate;
|
|
447
486
|
this.formElement = formElement;
|
|
448
|
-
this.formData = buildFormData(formElement, submitter);
|
|
449
487
|
this.submitter = submitter;
|
|
450
|
-
this.
|
|
488
|
+
this.formData = buildFormData(formElement, submitter);
|
|
489
|
+
this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body);
|
|
451
490
|
this.mustRedirect = mustRedirect;
|
|
452
491
|
}
|
|
453
492
|
get method() {
|
|
@@ -460,7 +499,24 @@ class FormSubmission {
|
|
|
460
499
|
return ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formaction")) || this.formElement.action;
|
|
461
500
|
}
|
|
462
501
|
get location() {
|
|
463
|
-
return
|
|
502
|
+
return expandURL(this.action);
|
|
503
|
+
}
|
|
504
|
+
get body() {
|
|
505
|
+
if (this.enctype == FormEnctype.urlEncoded || this.method == FetchMethod.get) {
|
|
506
|
+
return new URLSearchParams(this.stringFormData);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
return this.formData;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
get enctype() {
|
|
513
|
+
var _a;
|
|
514
|
+
return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formenctype")) || this.formElement.enctype);
|
|
515
|
+
}
|
|
516
|
+
get stringFormData() {
|
|
517
|
+
return [...this.formData].reduce((entries, [name, value]) => {
|
|
518
|
+
return entries.concat(typeof value == "string" ? [[name, value]] : []);
|
|
519
|
+
}, []);
|
|
464
520
|
}
|
|
465
521
|
async start() {
|
|
466
522
|
const { initialized, requesting } = FormSubmissionState;
|
|
@@ -477,15 +533,14 @@ class FormSubmission {
|
|
|
477
533
|
return true;
|
|
478
534
|
}
|
|
479
535
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if (this.method != FetchMethod.get) {
|
|
536
|
+
prepareHeadersForRequest(headers, request) {
|
|
537
|
+
if (!request.isIdempotent) {
|
|
483
538
|
const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token");
|
|
484
539
|
if (token) {
|
|
485
540
|
headers["X-CSRF-Token"] = token;
|
|
486
541
|
}
|
|
542
|
+
headers["Accept"] = [StreamMessage.contentType, headers["Accept"]].join(", ");
|
|
487
543
|
}
|
|
488
|
-
return headers;
|
|
489
544
|
}
|
|
490
545
|
requestStarted(request) {
|
|
491
546
|
this.state = FormSubmissionState.waiting;
|
|
@@ -553,6 +608,38 @@ function responseSucceededWithoutRedirect(response) {
|
|
|
553
608
|
return response.statusCode == 200 && !response.redirected;
|
|
554
609
|
}
|
|
555
610
|
|
|
611
|
+
class Snapshot {
|
|
612
|
+
constructor(element) {
|
|
613
|
+
this.element = element;
|
|
614
|
+
}
|
|
615
|
+
get children() {
|
|
616
|
+
return [...this.element.children];
|
|
617
|
+
}
|
|
618
|
+
hasAnchor(anchor) {
|
|
619
|
+
return this.getElementForAnchor(anchor) != null;
|
|
620
|
+
}
|
|
621
|
+
getElementForAnchor(anchor) {
|
|
622
|
+
try {
|
|
623
|
+
return this.element.querySelector(`[id='${anchor}'], a[name='${anchor}']`);
|
|
624
|
+
}
|
|
625
|
+
catch (_a) {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
get firstAutofocusableElement() {
|
|
630
|
+
return this.element.querySelector("[autofocus]");
|
|
631
|
+
}
|
|
632
|
+
get permanentElements() {
|
|
633
|
+
return [...this.element.querySelectorAll("[id][data-turbo-permanent]")];
|
|
634
|
+
}
|
|
635
|
+
getPermanentElementById(id) {
|
|
636
|
+
return this.element.querySelector(`#${id}[data-turbo-permanent]`);
|
|
637
|
+
}
|
|
638
|
+
getPermanentElementsPresentInSnapshot(snapshot) {
|
|
639
|
+
return this.permanentElements.filter(({ id }) => snapshot.getPermanentElementById(id));
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
556
643
|
class FormInterceptor {
|
|
557
644
|
constructor(delegate, element) {
|
|
558
645
|
this.submitBubbled = ((event) => {
|
|
@@ -577,6 +664,83 @@ class FormInterceptor {
|
|
|
577
664
|
}
|
|
578
665
|
}
|
|
579
666
|
|
|
667
|
+
class View {
|
|
668
|
+
constructor(delegate, element) {
|
|
669
|
+
this.delegate = delegate;
|
|
670
|
+
this.element = element;
|
|
671
|
+
}
|
|
672
|
+
scrollToAnchor(anchor) {
|
|
673
|
+
const element = this.snapshot.getElementForAnchor(anchor);
|
|
674
|
+
if (element) {
|
|
675
|
+
this.scrollToElement(element);
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
this.scrollToPosition({ x: 0, y: 0 });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
scrollToElement(element) {
|
|
682
|
+
element.scrollIntoView();
|
|
683
|
+
}
|
|
684
|
+
scrollToPosition({ x, y }) {
|
|
685
|
+
this.scrollRoot.scrollTo(x, y);
|
|
686
|
+
}
|
|
687
|
+
get scrollRoot() {
|
|
688
|
+
return window;
|
|
689
|
+
}
|
|
690
|
+
async render(renderer) {
|
|
691
|
+
if (this.renderer) {
|
|
692
|
+
throw new Error("rendering is already in progress");
|
|
693
|
+
}
|
|
694
|
+
const { isPreview, shouldRender, newSnapshot: snapshot } = renderer;
|
|
695
|
+
if (shouldRender) {
|
|
696
|
+
try {
|
|
697
|
+
this.renderer = renderer;
|
|
698
|
+
this.prepareToRenderSnapshot(renderer);
|
|
699
|
+
this.delegate.viewWillRenderSnapshot(snapshot, isPreview);
|
|
700
|
+
await this.renderSnapshot(renderer);
|
|
701
|
+
this.delegate.viewRenderedSnapshot(snapshot, isPreview);
|
|
702
|
+
this.finishRenderingSnapshot(renderer);
|
|
703
|
+
}
|
|
704
|
+
finally {
|
|
705
|
+
delete this.renderer;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
this.invalidate();
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
invalidate() {
|
|
713
|
+
this.delegate.viewInvalidated();
|
|
714
|
+
}
|
|
715
|
+
prepareToRenderSnapshot(renderer) {
|
|
716
|
+
this.markAsPreview(renderer.isPreview);
|
|
717
|
+
renderer.prepareToRender();
|
|
718
|
+
}
|
|
719
|
+
markAsPreview(isPreview) {
|
|
720
|
+
if (isPreview) {
|
|
721
|
+
this.element.setAttribute("data-turbo-preview", "");
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
this.element.removeAttribute("data-turbo-preview");
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
async renderSnapshot(renderer) {
|
|
728
|
+
await renderer.render();
|
|
729
|
+
}
|
|
730
|
+
finishRenderingSnapshot(renderer) {
|
|
731
|
+
renderer.finishRendering();
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
class FrameView extends View {
|
|
736
|
+
invalidate() {
|
|
737
|
+
this.element.innerHTML = "";
|
|
738
|
+
}
|
|
739
|
+
get snapshot() {
|
|
740
|
+
return new Snapshot(this.element);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
580
744
|
class LinkInterceptor {
|
|
581
745
|
constructor(delegate, element) {
|
|
582
746
|
this.clickBubbled = (event) => {
|
|
@@ -623,10 +787,147 @@ class LinkInterceptor {
|
|
|
623
787
|
}
|
|
624
788
|
}
|
|
625
789
|
|
|
790
|
+
class Renderer {
|
|
791
|
+
constructor(currentSnapshot, newSnapshot, isPreview) {
|
|
792
|
+
this.currentSnapshot = currentSnapshot;
|
|
793
|
+
this.newSnapshot = newSnapshot;
|
|
794
|
+
this.isPreview = isPreview;
|
|
795
|
+
this.promise = new Promise((resolve, reject) => this.resolvingFunctions = { resolve, reject });
|
|
796
|
+
}
|
|
797
|
+
get shouldRender() {
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
prepareToRender() {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
finishRendering() {
|
|
804
|
+
if (this.resolvingFunctions) {
|
|
805
|
+
this.resolvingFunctions.resolve();
|
|
806
|
+
delete this.resolvingFunctions;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
createScriptElement(element) {
|
|
810
|
+
if (element.getAttribute("data-turbo-eval") == "false") {
|
|
811
|
+
return element;
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
const createdScriptElement = document.createElement("script");
|
|
815
|
+
createdScriptElement.textContent = element.textContent;
|
|
816
|
+
createdScriptElement.async = false;
|
|
817
|
+
copyElementAttributes(createdScriptElement, element);
|
|
818
|
+
return createdScriptElement;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
preservingPermanentElements(callback) {
|
|
822
|
+
const placeholders = relocatePermanentElements(this.currentSnapshot, this.newSnapshot);
|
|
823
|
+
callback();
|
|
824
|
+
replacePlaceholderElementsWithClonedPermanentElements(placeholders);
|
|
825
|
+
}
|
|
826
|
+
focusFirstAutofocusableElement() {
|
|
827
|
+
const element = this.newSnapshot.firstAutofocusableElement;
|
|
828
|
+
if (elementIsFocusable(element)) {
|
|
829
|
+
element.focus();
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
get currentElement() {
|
|
833
|
+
return this.currentSnapshot.element;
|
|
834
|
+
}
|
|
835
|
+
get newElement() {
|
|
836
|
+
return this.newSnapshot.element;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function replaceElementWithElement(fromElement, toElement) {
|
|
840
|
+
const parentElement = fromElement.parentElement;
|
|
841
|
+
if (parentElement) {
|
|
842
|
+
return parentElement.replaceChild(toElement, fromElement);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
function copyElementAttributes(destinationElement, sourceElement) {
|
|
846
|
+
for (const { name, value } of [...sourceElement.attributes]) {
|
|
847
|
+
destinationElement.setAttribute(name, value);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
function createPlaceholderForPermanentElement(permanentElement) {
|
|
851
|
+
const element = document.createElement("meta");
|
|
852
|
+
element.setAttribute("name", "turbo-permanent-placeholder");
|
|
853
|
+
element.setAttribute("content", permanentElement.id);
|
|
854
|
+
return { element, permanentElement };
|
|
855
|
+
}
|
|
856
|
+
function replacePlaceholderElementsWithClonedPermanentElements(placeholders) {
|
|
857
|
+
for (const { element, permanentElement } of placeholders) {
|
|
858
|
+
const clonedElement = permanentElement.cloneNode(true);
|
|
859
|
+
replaceElementWithElement(element, clonedElement);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function relocatePermanentElements(currentSnapshot, newSnapshot) {
|
|
863
|
+
return currentSnapshot.getPermanentElementsPresentInSnapshot(newSnapshot).reduce((placeholders, permanentElement) => {
|
|
864
|
+
const newElement = newSnapshot.getPermanentElementById(permanentElement.id);
|
|
865
|
+
if (newElement) {
|
|
866
|
+
const placeholder = createPlaceholderForPermanentElement(permanentElement);
|
|
867
|
+
replaceElementWithElement(permanentElement, placeholder.element);
|
|
868
|
+
replaceElementWithElement(newElement, permanentElement);
|
|
869
|
+
return [...placeholders, placeholder];
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
return placeholders;
|
|
873
|
+
}
|
|
874
|
+
}, []);
|
|
875
|
+
}
|
|
876
|
+
function elementIsFocusable(element) {
|
|
877
|
+
return element && typeof element.focus == "function";
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
class FrameRenderer extends Renderer {
|
|
881
|
+
get shouldRender() {
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
async render() {
|
|
885
|
+
await nextAnimationFrame();
|
|
886
|
+
this.preservingPermanentElements(() => {
|
|
887
|
+
this.loadFrameElement();
|
|
888
|
+
});
|
|
889
|
+
this.scrollFrameIntoView();
|
|
890
|
+
await nextAnimationFrame();
|
|
891
|
+
this.focusFirstAutofocusableElement();
|
|
892
|
+
}
|
|
893
|
+
loadFrameElement() {
|
|
894
|
+
var _a;
|
|
895
|
+
const destinationRange = document.createRange();
|
|
896
|
+
destinationRange.selectNodeContents(this.currentElement);
|
|
897
|
+
destinationRange.deleteContents();
|
|
898
|
+
const frameElement = this.newElement;
|
|
899
|
+
const sourceRange = (_a = frameElement.ownerDocument) === null || _a === void 0 ? void 0 : _a.createRange();
|
|
900
|
+
if (sourceRange) {
|
|
901
|
+
sourceRange.selectNodeContents(frameElement);
|
|
902
|
+
this.currentElement.appendChild(sourceRange.extractContents());
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
scrollFrameIntoView() {
|
|
906
|
+
if (this.currentElement.autoscroll || this.newElement.autoscroll) {
|
|
907
|
+
const element = this.currentElement.firstElementChild;
|
|
908
|
+
const block = readScrollLogicalPosition(this.currentElement.getAttribute("data-autoscroll-block"), "end");
|
|
909
|
+
if (element) {
|
|
910
|
+
element.scrollIntoView({ block });
|
|
911
|
+
return true;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
function readScrollLogicalPosition(value, defaultValue) {
|
|
918
|
+
if (value == "end" || value == "start" || value == "center" || value == "nearest") {
|
|
919
|
+
return value;
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
return defaultValue;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
626
926
|
class FrameController {
|
|
627
927
|
constructor(element) {
|
|
628
928
|
this.resolveVisitPromise = () => { };
|
|
629
929
|
this.element = element;
|
|
930
|
+
this.view = new FrameView(this, this.element);
|
|
630
931
|
this.appearanceObserver = new AppearanceObserver(this, this.element);
|
|
631
932
|
this.linkInterceptor = new LinkInterceptor(this, this.element);
|
|
632
933
|
this.formInterceptor = new FormInterceptor(this, this.element);
|
|
@@ -671,14 +972,18 @@ class FrameController {
|
|
|
671
972
|
}
|
|
672
973
|
}
|
|
673
974
|
async loadResponse(response) {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
975
|
+
try {
|
|
976
|
+
const html = await response.responseHTML;
|
|
977
|
+
if (html) {
|
|
978
|
+
const { body } = parseHTMLDocument(html);
|
|
979
|
+
const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
|
|
980
|
+
const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
|
|
981
|
+
await this.view.render(renderer);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
catch (error) {
|
|
985
|
+
console.error(error);
|
|
986
|
+
this.view.invalidate();
|
|
682
987
|
}
|
|
683
988
|
}
|
|
684
989
|
elementAppearedInViewport(element) {
|
|
@@ -699,14 +1004,14 @@ class FrameController {
|
|
|
699
1004
|
}
|
|
700
1005
|
this.formSubmission = new FormSubmission(this, element, submitter);
|
|
701
1006
|
if (this.formSubmission.fetchRequest.isIdempotent) {
|
|
702
|
-
this.navigateFrame(element, this.formSubmission.fetchRequest.url);
|
|
1007
|
+
this.navigateFrame(element, this.formSubmission.fetchRequest.url.href);
|
|
703
1008
|
}
|
|
704
1009
|
else {
|
|
705
1010
|
this.formSubmission.start();
|
|
706
1011
|
}
|
|
707
1012
|
}
|
|
708
|
-
|
|
709
|
-
|
|
1013
|
+
prepareHeadersForRequest(headers, request) {
|
|
1014
|
+
headers["Turbo-Frame"] = this.id;
|
|
710
1015
|
}
|
|
711
1016
|
requestStarted(request) {
|
|
712
1017
|
this.element.setAttribute("busy", "");
|
|
@@ -742,9 +1047,14 @@ class FrameController {
|
|
|
742
1047
|
}
|
|
743
1048
|
formSubmissionFinished(formSubmission) {
|
|
744
1049
|
}
|
|
1050
|
+
viewWillRenderSnapshot(snapshot, isPreview) {
|
|
1051
|
+
}
|
|
1052
|
+
viewRenderedSnapshot(snapshot, isPreview) {
|
|
1053
|
+
}
|
|
1054
|
+
viewInvalidated() {
|
|
1055
|
+
}
|
|
745
1056
|
async visit(url) {
|
|
746
|
-
const
|
|
747
|
-
const request = new FetchRequest(this, FetchMethod.get, location);
|
|
1057
|
+
const request = new FetchRequest(this, FetchMethod.get, expandURL(url));
|
|
748
1058
|
return new Promise(resolve => {
|
|
749
1059
|
this.resolveVisitPromise = () => {
|
|
750
1060
|
this.resolveVisitPromise = () => { };
|
|
@@ -759,7 +1069,7 @@ class FrameController {
|
|
|
759
1069
|
}
|
|
760
1070
|
findFrameElement(element) {
|
|
761
1071
|
var _a;
|
|
762
|
-
const id = element.getAttribute("data-turbo-frame");
|
|
1072
|
+
const id = element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
|
|
763
1073
|
return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
|
|
764
1074
|
}
|
|
765
1075
|
async extractForeignFrameElement(container) {
|
|
@@ -775,36 +1085,6 @@ class FrameController {
|
|
|
775
1085
|
console.error(`Response has no matching <turbo-frame id="${id}"> element`);
|
|
776
1086
|
return new FrameElement();
|
|
777
1087
|
}
|
|
778
|
-
loadFrameElement(frameElement) {
|
|
779
|
-
var _a;
|
|
780
|
-
const destinationRange = document.createRange();
|
|
781
|
-
destinationRange.selectNodeContents(this.element);
|
|
782
|
-
destinationRange.deleteContents();
|
|
783
|
-
const sourceRange = (_a = frameElement.ownerDocument) === null || _a === void 0 ? void 0 : _a.createRange();
|
|
784
|
-
if (sourceRange) {
|
|
785
|
-
sourceRange.selectNodeContents(frameElement);
|
|
786
|
-
this.element.appendChild(sourceRange.extractContents());
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
focusFirstAutofocusableElement() {
|
|
790
|
-
const element = this.firstAutofocusableElement;
|
|
791
|
-
if (element) {
|
|
792
|
-
element.focus();
|
|
793
|
-
return true;
|
|
794
|
-
}
|
|
795
|
-
return false;
|
|
796
|
-
}
|
|
797
|
-
scrollFrameIntoView(frame) {
|
|
798
|
-
if (this.element.autoscroll || frame.autoscroll) {
|
|
799
|
-
const element = this.element.firstElementChild;
|
|
800
|
-
const block = readScrollLogicalPosition(this.element.getAttribute("data-autoscroll-block"), "end");
|
|
801
|
-
if (element) {
|
|
802
|
-
element.scrollIntoView({ block });
|
|
803
|
-
return true;
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
return false;
|
|
807
|
-
}
|
|
808
1088
|
shouldInterceptNavigation(element) {
|
|
809
1089
|
const id = element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
|
|
810
1090
|
if (!this.enabled || id == "_top") {
|
|
@@ -818,10 +1098,6 @@ class FrameController {
|
|
|
818
1098
|
}
|
|
819
1099
|
return true;
|
|
820
1100
|
}
|
|
821
|
-
get firstAutofocusableElement() {
|
|
822
|
-
const element = this.element.querySelector("[autofocus]");
|
|
823
|
-
return element instanceof HTMLElement ? element : null;
|
|
824
|
-
}
|
|
825
1101
|
get id() {
|
|
826
1102
|
return this.element.id;
|
|
827
1103
|
}
|
|
@@ -849,20 +1125,6 @@ function getFrameElementById(id) {
|
|
|
849
1125
|
}
|
|
850
1126
|
}
|
|
851
1127
|
}
|
|
852
|
-
function readScrollLogicalPosition(value, defaultValue) {
|
|
853
|
-
if (value == "end" || value == "start" || value == "center" || value == "nearest") {
|
|
854
|
-
return value;
|
|
855
|
-
}
|
|
856
|
-
else {
|
|
857
|
-
return defaultValue;
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
function fragmentFromHTML(html) {
|
|
861
|
-
if (html) {
|
|
862
|
-
const foreignDocument = document.implementation.createHTMLDocument();
|
|
863
|
-
return foreignDocument.createRange().createContextualFragment(html);
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
1128
|
function activateElement(element) {
|
|
867
1129
|
if (element && element.ownerDocument !== document) {
|
|
868
1130
|
element = document.importNode(element, true);
|
|
@@ -1092,9 +1354,10 @@ class ProgressBar {
|
|
|
1092
1354
|
}
|
|
1093
1355
|
ProgressBar.animationDuration = 300;
|
|
1094
1356
|
|
|
1095
|
-
class
|
|
1096
|
-
constructor(
|
|
1097
|
-
|
|
1357
|
+
class HeadSnapshot extends Snapshot {
|
|
1358
|
+
constructor() {
|
|
1359
|
+
super(...arguments);
|
|
1360
|
+
this.detailsByOuterHTML = this.children.reduce((result, element) => {
|
|
1098
1361
|
const { outerHTML } = element;
|
|
1099
1362
|
const details = outerHTML in result
|
|
1100
1363
|
? result[outerHTML]
|
|
@@ -1106,29 +1369,25 @@ class HeadDetails {
|
|
|
1106
1369
|
return Object.assign(Object.assign({}, result), { [outerHTML]: Object.assign(Object.assign({}, details), { elements: [...details.elements, element] }) });
|
|
1107
1370
|
}, {});
|
|
1108
1371
|
}
|
|
1109
|
-
|
|
1110
|
-
const children = headElement ? [...headElement.children] : [];
|
|
1111
|
-
return new this(children);
|
|
1112
|
-
}
|
|
1113
|
-
getTrackedElementSignature() {
|
|
1372
|
+
get trackedElementSignature() {
|
|
1114
1373
|
return Object.keys(this.detailsByOuterHTML)
|
|
1115
1374
|
.filter(outerHTML => this.detailsByOuterHTML[outerHTML].tracked)
|
|
1116
1375
|
.join("");
|
|
1117
1376
|
}
|
|
1118
|
-
|
|
1119
|
-
return this.
|
|
1377
|
+
getScriptElementsNotInSnapshot(snapshot) {
|
|
1378
|
+
return this.getElementsMatchingTypeNotInSnapshot("script", snapshot);
|
|
1120
1379
|
}
|
|
1121
|
-
|
|
1122
|
-
return this.
|
|
1380
|
+
getStylesheetElementsNotInSnapshot(snapshot) {
|
|
1381
|
+
return this.getElementsMatchingTypeNotInSnapshot("stylesheet", snapshot);
|
|
1123
1382
|
}
|
|
1124
|
-
|
|
1383
|
+
getElementsMatchingTypeNotInSnapshot(matchedType, snapshot) {
|
|
1125
1384
|
return Object.keys(this.detailsByOuterHTML)
|
|
1126
|
-
.filter(outerHTML => !(outerHTML in
|
|
1385
|
+
.filter(outerHTML => !(outerHTML in snapshot.detailsByOuterHTML))
|
|
1127
1386
|
.map(outerHTML => this.detailsByOuterHTML[outerHTML])
|
|
1128
1387
|
.filter(({ type }) => type == matchedType)
|
|
1129
1388
|
.map(({ elements: [element] }) => element);
|
|
1130
1389
|
}
|
|
1131
|
-
|
|
1390
|
+
get provisionalElements() {
|
|
1132
1391
|
return Object.keys(this.detailsByOuterHTML).reduce((result, outerHTML) => {
|
|
1133
1392
|
const { type, tracked, elements } = this.detailsByOuterHTML[outerHTML];
|
|
1134
1393
|
if (type == null && !tracked) {
|
|
@@ -1178,79 +1437,46 @@ function elementIsMetaElementWithName(element, name) {
|
|
|
1178
1437
|
const tagName = element.tagName.toLowerCase();
|
|
1179
1438
|
return tagName == "meta" && element.getAttribute("name") == name;
|
|
1180
1439
|
}
|
|
1181
|
-
|
|
1182
|
-
class Snapshot {
|
|
1183
|
-
constructor(
|
|
1184
|
-
|
|
1185
|
-
this.
|
|
1186
|
-
}
|
|
1187
|
-
static wrap(value) {
|
|
1188
|
-
if (value instanceof this) {
|
|
1189
|
-
return value;
|
|
1190
|
-
}
|
|
1191
|
-
else if (typeof value == "string") {
|
|
1192
|
-
return this.fromHTMLString(value);
|
|
1193
|
-
}
|
|
1194
|
-
else {
|
|
1195
|
-
return this.fromHTMLElement(value);
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
static fromHTMLString(html) {
|
|
1199
|
-
const { documentElement } = new DOMParser().parseFromString(html, "text/html");
|
|
1200
|
-
return this.fromHTMLElement(documentElement);
|
|
1201
|
-
}
|
|
1202
|
-
static fromHTMLElement(htmlElement) {
|
|
1203
|
-
const headElement = htmlElement.querySelector("head");
|
|
1204
|
-
const bodyElement = htmlElement.querySelector("body") || document.createElement("body");
|
|
1205
|
-
const headDetails = HeadDetails.fromHeadElement(headElement);
|
|
1206
|
-
return new this(headDetails, bodyElement);
|
|
1207
|
-
}
|
|
1208
|
-
clone() {
|
|
1209
|
-
const { bodyElement } = Snapshot.fromHTMLString(this.bodyElement.outerHTML);
|
|
1210
|
-
return new Snapshot(this.headDetails, bodyElement);
|
|
1211
|
-
}
|
|
1212
|
-
getRootLocation() {
|
|
1213
|
-
const root = this.getSetting("root", "/");
|
|
1214
|
-
return new Location(root);
|
|
1440
|
+
|
|
1441
|
+
class PageSnapshot extends Snapshot {
|
|
1442
|
+
constructor(element, headSnapshot) {
|
|
1443
|
+
super(element);
|
|
1444
|
+
this.headSnapshot = headSnapshot;
|
|
1215
1445
|
}
|
|
1216
|
-
|
|
1217
|
-
return this.
|
|
1446
|
+
static fromHTMLString(html = "") {
|
|
1447
|
+
return this.fromDocument(parseHTMLDocument(html));
|
|
1218
1448
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
return this.bodyElement.querySelector(`[id='${anchor}'], a[name='${anchor}']`);
|
|
1222
|
-
}
|
|
1223
|
-
catch (_a) {
|
|
1224
|
-
return null;
|
|
1225
|
-
}
|
|
1449
|
+
static fromElement(element) {
|
|
1450
|
+
return this.fromDocument(element.ownerDocument);
|
|
1226
1451
|
}
|
|
1227
|
-
|
|
1228
|
-
return
|
|
1452
|
+
static fromDocument({ head, body }) {
|
|
1453
|
+
return new this(body, new HeadSnapshot(head));
|
|
1229
1454
|
}
|
|
1230
|
-
|
|
1231
|
-
return this.
|
|
1455
|
+
clone() {
|
|
1456
|
+
return new PageSnapshot(this.element.cloneNode(true), this.headSnapshot);
|
|
1232
1457
|
}
|
|
1233
|
-
|
|
1234
|
-
return this.
|
|
1458
|
+
get headElement() {
|
|
1459
|
+
return this.headSnapshot.element;
|
|
1235
1460
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1461
|
+
get rootLocation() {
|
|
1462
|
+
var _a;
|
|
1463
|
+
const root = (_a = this.getSetting("root")) !== null && _a !== void 0 ? _a : "/";
|
|
1464
|
+
return expandURL(root);
|
|
1238
1465
|
}
|
|
1239
|
-
|
|
1240
|
-
return this.
|
|
1466
|
+
get cacheControlValue() {
|
|
1467
|
+
return this.getSetting("cache-control");
|
|
1241
1468
|
}
|
|
1242
|
-
isPreviewable() {
|
|
1243
|
-
return this.
|
|
1469
|
+
get isPreviewable() {
|
|
1470
|
+
return this.cacheControlValue != "no-preview";
|
|
1244
1471
|
}
|
|
1245
|
-
isCacheable() {
|
|
1246
|
-
return this.
|
|
1472
|
+
get isCacheable() {
|
|
1473
|
+
return this.cacheControlValue != "no-cache";
|
|
1247
1474
|
}
|
|
1248
|
-
isVisitable() {
|
|
1475
|
+
get isVisitable() {
|
|
1249
1476
|
return this.getSetting("visit-control") != "reload";
|
|
1250
1477
|
}
|
|
1251
|
-
getSetting(name
|
|
1252
|
-
|
|
1253
|
-
return value == null ? defaultValue : value;
|
|
1478
|
+
getSetting(name) {
|
|
1479
|
+
return this.headSnapshot.getMetaValue(`turbo-${name}`);
|
|
1254
1480
|
}
|
|
1255
1481
|
}
|
|
1256
1482
|
|
|
@@ -1288,17 +1514,6 @@ class Visit {
|
|
|
1288
1514
|
this.scrolled = false;
|
|
1289
1515
|
this.snapshotCached = false;
|
|
1290
1516
|
this.state = VisitState.initialized;
|
|
1291
|
-
this.performScroll = () => {
|
|
1292
|
-
if (!this.scrolled) {
|
|
1293
|
-
if (this.action == "restore") {
|
|
1294
|
-
this.scrollToRestoredPosition() || this.scrollToTop();
|
|
1295
|
-
}
|
|
1296
|
-
else {
|
|
1297
|
-
this.scrollToAnchor() || this.scrollToTop();
|
|
1298
|
-
}
|
|
1299
|
-
this.scrolled = true;
|
|
1300
|
-
}
|
|
1301
|
-
};
|
|
1302
1517
|
this.delegate = delegate;
|
|
1303
1518
|
this.location = location;
|
|
1304
1519
|
this.restorationIdentifier = restorationIdentifier || uuid();
|
|
@@ -1353,8 +1568,9 @@ class Visit {
|
|
|
1353
1568
|
}
|
|
1354
1569
|
}
|
|
1355
1570
|
changeHistory() {
|
|
1571
|
+
var _a;
|
|
1356
1572
|
if (!this.historyChanged) {
|
|
1357
|
-
const actionForHistory = this.location.
|
|
1573
|
+
const actionForHistory = this.location.href === ((_a = this.referrer) === null || _a === void 0 ? void 0 : _a.href) ? "replace" : this.action;
|
|
1358
1574
|
const method = this.getHistoryMethodForAction(actionForHistory);
|
|
1359
1575
|
this.history.update(method, this.location, this.restorationIdentifier);
|
|
1360
1576
|
this.historyChanged = true;
|
|
@@ -1399,15 +1615,15 @@ class Visit {
|
|
|
1399
1615
|
loadResponse() {
|
|
1400
1616
|
if (this.response) {
|
|
1401
1617
|
const { statusCode, responseHTML } = this.response;
|
|
1402
|
-
this.render(() => {
|
|
1618
|
+
this.render(async () => {
|
|
1403
1619
|
this.cacheSnapshot();
|
|
1404
1620
|
if (isSuccessful(statusCode) && responseHTML != null) {
|
|
1405
|
-
this.view.
|
|
1621
|
+
await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML));
|
|
1406
1622
|
this.adapter.visitRendered(this);
|
|
1407
1623
|
this.complete();
|
|
1408
1624
|
}
|
|
1409
1625
|
else {
|
|
1410
|
-
this.view.
|
|
1626
|
+
await this.view.renderError(PageSnapshot.fromHTMLString(responseHTML));
|
|
1411
1627
|
this.adapter.visitRendered(this);
|
|
1412
1628
|
this.fail();
|
|
1413
1629
|
}
|
|
@@ -1416,15 +1632,15 @@ class Visit {
|
|
|
1416
1632
|
}
|
|
1417
1633
|
getCachedSnapshot() {
|
|
1418
1634
|
const snapshot = this.view.getCachedSnapshotForLocation(this.location) || this.getPreloadedSnapshot();
|
|
1419
|
-
if (snapshot && (!this.location
|
|
1420
|
-
if (this.action == "restore" || snapshot.isPreviewable
|
|
1635
|
+
if (snapshot && (!getAnchor(this.location) || snapshot.hasAnchor(getAnchor(this.location)))) {
|
|
1636
|
+
if (this.action == "restore" || snapshot.isPreviewable) {
|
|
1421
1637
|
return snapshot;
|
|
1422
1638
|
}
|
|
1423
1639
|
}
|
|
1424
1640
|
}
|
|
1425
1641
|
getPreloadedSnapshot() {
|
|
1426
1642
|
if (this.snapshotHTML) {
|
|
1427
|
-
return
|
|
1643
|
+
return PageSnapshot.fromHTMLString(this.snapshotHTML);
|
|
1428
1644
|
}
|
|
1429
1645
|
}
|
|
1430
1646
|
hasCachedSnapshot() {
|
|
@@ -1434,9 +1650,9 @@ class Visit {
|
|
|
1434
1650
|
const snapshot = this.getCachedSnapshot();
|
|
1435
1651
|
if (snapshot) {
|
|
1436
1652
|
const isPreview = this.shouldIssueRequest();
|
|
1437
|
-
this.render(() => {
|
|
1653
|
+
this.render(async () => {
|
|
1438
1654
|
this.cacheSnapshot();
|
|
1439
|
-
this.view.
|
|
1655
|
+
await this.view.renderPage(snapshot);
|
|
1440
1656
|
this.adapter.visitRendered(this);
|
|
1441
1657
|
if (!isPreview) {
|
|
1442
1658
|
this.complete();
|
|
@@ -1481,6 +1697,17 @@ class Visit {
|
|
|
1481
1697
|
requestFinished() {
|
|
1482
1698
|
this.finishRequest();
|
|
1483
1699
|
}
|
|
1700
|
+
performScroll() {
|
|
1701
|
+
if (!this.scrolled) {
|
|
1702
|
+
if (this.action == "restore") {
|
|
1703
|
+
this.scrollToRestoredPosition() || this.scrollToTop();
|
|
1704
|
+
}
|
|
1705
|
+
else {
|
|
1706
|
+
this.scrollToAnchor() || this.scrollToTop();
|
|
1707
|
+
}
|
|
1708
|
+
this.scrolled = true;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1484
1711
|
scrollToRestoredPosition() {
|
|
1485
1712
|
const { scrollPosition } = this.restorationData;
|
|
1486
1713
|
if (scrollPosition) {
|
|
@@ -1489,8 +1716,8 @@ class Visit {
|
|
|
1489
1716
|
}
|
|
1490
1717
|
}
|
|
1491
1718
|
scrollToAnchor() {
|
|
1492
|
-
if (this.location
|
|
1493
|
-
this.view.scrollToAnchor(this.location
|
|
1719
|
+
if (getAnchor(this.location) != null) {
|
|
1720
|
+
this.view.scrollToAnchor(getAnchor(this.location));
|
|
1494
1721
|
return true;
|
|
1495
1722
|
}
|
|
1496
1723
|
}
|
|
@@ -1524,12 +1751,14 @@ class Visit {
|
|
|
1524
1751
|
this.snapshotCached = true;
|
|
1525
1752
|
}
|
|
1526
1753
|
}
|
|
1527
|
-
render(callback) {
|
|
1754
|
+
async render(callback) {
|
|
1528
1755
|
this.cancelRender();
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
callback.call(this);
|
|
1756
|
+
await new Promise(resolve => {
|
|
1757
|
+
this.frame = requestAnimationFrame(() => resolve());
|
|
1532
1758
|
});
|
|
1759
|
+
callback();
|
|
1760
|
+
delete this.frame;
|
|
1761
|
+
this.performScroll();
|
|
1533
1762
|
}
|
|
1534
1763
|
cancelRender() {
|
|
1535
1764
|
if (this.frame) {
|
|
@@ -1705,11 +1934,10 @@ class History {
|
|
|
1705
1934
|
if (this.shouldHandlePopState()) {
|
|
1706
1935
|
const { turbo } = event.state || {};
|
|
1707
1936
|
if (turbo) {
|
|
1708
|
-
|
|
1709
|
-
this.location = location;
|
|
1937
|
+
this.location = new URL(window.location.href);
|
|
1710
1938
|
const { restorationIdentifier } = turbo;
|
|
1711
1939
|
this.restorationIdentifier = restorationIdentifier;
|
|
1712
|
-
this.delegate.historyPoppedToLocationWithRestorationIdentifier(location, restorationIdentifier);
|
|
1940
|
+
this.delegate.historyPoppedToLocationWithRestorationIdentifier(this.location, restorationIdentifier);
|
|
1713
1941
|
}
|
|
1714
1942
|
}
|
|
1715
1943
|
};
|
|
@@ -1724,7 +1952,7 @@ class History {
|
|
|
1724
1952
|
addEventListener("popstate", this.onPopState, false);
|
|
1725
1953
|
addEventListener("load", this.onPageLoad, false);
|
|
1726
1954
|
this.started = true;
|
|
1727
|
-
this.replace(
|
|
1955
|
+
this.replace(new URL(window.location.href));
|
|
1728
1956
|
}
|
|
1729
1957
|
}
|
|
1730
1958
|
stop() {
|
|
@@ -1742,7 +1970,7 @@ class History {
|
|
|
1742
1970
|
}
|
|
1743
1971
|
update(method, location, restorationIdentifier = uuid()) {
|
|
1744
1972
|
const state = { turbo: { restorationIdentifier } };
|
|
1745
|
-
method.call(history, state, "", location.
|
|
1973
|
+
method.call(history, state, "", location.href);
|
|
1746
1974
|
this.location = location;
|
|
1747
1975
|
this.restorationIdentifier = restorationIdentifier;
|
|
1748
1976
|
}
|
|
@@ -1823,7 +2051,7 @@ class LinkClickObserver {
|
|
|
1823
2051
|
}
|
|
1824
2052
|
}
|
|
1825
2053
|
getLocationForLink(link) {
|
|
1826
|
-
return
|
|
2054
|
+
return expandURL(link.getAttribute("href") || "");
|
|
1827
2055
|
}
|
|
1828
2056
|
}
|
|
1829
2057
|
|
|
@@ -1836,15 +2064,20 @@ class Navigator {
|
|
|
1836
2064
|
this.delegate.visitProposedToLocation(location, options);
|
|
1837
2065
|
}
|
|
1838
2066
|
}
|
|
1839
|
-
startVisit(
|
|
2067
|
+
startVisit(locatable, restorationIdentifier, options = {}) {
|
|
1840
2068
|
this.stop();
|
|
1841
|
-
this.currentVisit = new Visit(this,
|
|
2069
|
+
this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, Object.assign({ referrer: this.location }, options));
|
|
1842
2070
|
this.currentVisit.start();
|
|
1843
2071
|
}
|
|
1844
2072
|
submitForm(form, submitter) {
|
|
1845
2073
|
this.stop();
|
|
1846
2074
|
this.formSubmission = new FormSubmission(this, form, submitter, true);
|
|
1847
|
-
this.formSubmission.
|
|
2075
|
+
if (this.formSubmission.fetchRequest.isIdempotent) {
|
|
2076
|
+
this.proposeVisit(this.formSubmission.fetchRequest.url);
|
|
2077
|
+
}
|
|
2078
|
+
else {
|
|
2079
|
+
this.formSubmission.start();
|
|
2080
|
+
}
|
|
1848
2081
|
}
|
|
1849
2082
|
stop() {
|
|
1850
2083
|
if (this.formSubmission) {
|
|
@@ -1883,8 +2116,8 @@ class Navigator {
|
|
|
1883
2116
|
async formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
|
|
1884
2117
|
const responseHTML = await fetchResponse.responseHTML;
|
|
1885
2118
|
if (responseHTML) {
|
|
1886
|
-
const snapshot =
|
|
1887
|
-
this.view.
|
|
2119
|
+
const snapshot = PageSnapshot.fromHTMLString(responseHTML);
|
|
2120
|
+
await this.view.renderPage(snapshot);
|
|
1888
2121
|
this.view.clearSnapshotCache();
|
|
1889
2122
|
}
|
|
1890
2123
|
}
|
|
@@ -1992,53 +2225,10 @@ class ScrollObserver {
|
|
|
1992
2225
|
}
|
|
1993
2226
|
}
|
|
1994
2227
|
|
|
1995
|
-
class StreamMessage {
|
|
1996
|
-
constructor(html) {
|
|
1997
|
-
this.templateElement = document.createElement("template");
|
|
1998
|
-
this.templateElement.innerHTML = html;
|
|
1999
|
-
}
|
|
2000
|
-
static wrap(message) {
|
|
2001
|
-
if (typeof message == "string") {
|
|
2002
|
-
return new this(message);
|
|
2003
|
-
}
|
|
2004
|
-
else {
|
|
2005
|
-
return message;
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
get fragment() {
|
|
2009
|
-
const fragment = document.createDocumentFragment();
|
|
2010
|
-
for (const element of this.foreignElements) {
|
|
2011
|
-
fragment.appendChild(document.importNode(element, true));
|
|
2012
|
-
}
|
|
2013
|
-
return fragment;
|
|
2014
|
-
}
|
|
2015
|
-
get foreignElements() {
|
|
2016
|
-
return this.templateChildren.reduce((streamElements, child) => {
|
|
2017
|
-
if (child.tagName.toLowerCase() == "turbo-stream") {
|
|
2018
|
-
return [...streamElements, child];
|
|
2019
|
-
}
|
|
2020
|
-
else {
|
|
2021
|
-
return streamElements;
|
|
2022
|
-
}
|
|
2023
|
-
}, []);
|
|
2024
|
-
}
|
|
2025
|
-
get templateChildren() {
|
|
2026
|
-
return Array.from(this.templateElement.content.children);
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
2228
|
class StreamObserver {
|
|
2031
2229
|
constructor(delegate) {
|
|
2032
2230
|
this.sources = new Set;
|
|
2033
2231
|
this.started = false;
|
|
2034
|
-
this.prepareFetchRequest = ((event) => {
|
|
2035
|
-
var _a;
|
|
2036
|
-
const fetchOptions = (_a = event.detail) === null || _a === void 0 ? void 0 : _a.fetchOptions;
|
|
2037
|
-
if (fetchOptions) {
|
|
2038
|
-
const { headers } = fetchOptions;
|
|
2039
|
-
headers.Accept = ["text/vnd.turbo-stream.html", headers.Accept].join(", ");
|
|
2040
|
-
}
|
|
2041
|
-
});
|
|
2042
2232
|
this.inspectFetchResponse = ((event) => {
|
|
2043
2233
|
const response = fetchResponseFromEvent(event);
|
|
2044
2234
|
if (response && fetchResponseIsStream(response)) {
|
|
@@ -2056,14 +2246,12 @@ class StreamObserver {
|
|
|
2056
2246
|
start() {
|
|
2057
2247
|
if (!this.started) {
|
|
2058
2248
|
this.started = true;
|
|
2059
|
-
addEventListener("turbo:before-fetch-request", this.prepareFetchRequest, true);
|
|
2060
2249
|
addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
|
|
2061
2250
|
}
|
|
2062
2251
|
}
|
|
2063
2252
|
stop() {
|
|
2064
2253
|
if (this.started) {
|
|
2065
2254
|
this.started = false;
|
|
2066
|
-
removeEventListener("turbo:before-fetch-request", this.prepareFetchRequest, true);
|
|
2067
2255
|
removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
|
|
2068
2256
|
}
|
|
2069
2257
|
}
|
|
@@ -2102,70 +2290,25 @@ function fetchResponseFromEvent(event) {
|
|
|
2102
2290
|
function fetchResponseIsStream(response) {
|
|
2103
2291
|
var _a;
|
|
2104
2292
|
const contentType = (_a = response.contentType) !== null && _a !== void 0 ? _a : "";
|
|
2105
|
-
return
|
|
2293
|
+
return contentType.startsWith(StreamMessage.contentType);
|
|
2106
2294
|
}
|
|
2107
2295
|
|
|
2108
2296
|
function isAction(action) {
|
|
2109
2297
|
return action == "advance" || action == "replace" || action == "restore";
|
|
2110
2298
|
}
|
|
2111
2299
|
|
|
2112
|
-
class Renderer {
|
|
2113
|
-
renderView(callback) {
|
|
2114
|
-
this.delegate.viewWillRender(this.newBody);
|
|
2115
|
-
callback();
|
|
2116
|
-
this.delegate.viewRendered(this.newBody);
|
|
2117
|
-
}
|
|
2118
|
-
invalidateView() {
|
|
2119
|
-
this.delegate.viewInvalidated();
|
|
2120
|
-
}
|
|
2121
|
-
createScriptElement(element) {
|
|
2122
|
-
if (element.getAttribute("data-turbo-eval") == "false") {
|
|
2123
|
-
return element;
|
|
2124
|
-
}
|
|
2125
|
-
else {
|
|
2126
|
-
const createdScriptElement = document.createElement("script");
|
|
2127
|
-
createdScriptElement.textContent = element.textContent;
|
|
2128
|
-
createdScriptElement.async = false;
|
|
2129
|
-
copyElementAttributes(createdScriptElement, element);
|
|
2130
|
-
return createdScriptElement;
|
|
2131
|
-
}
|
|
2132
|
-
}
|
|
2133
|
-
}
|
|
2134
|
-
function copyElementAttributes(destinationElement, sourceElement) {
|
|
2135
|
-
for (const { name, value } of [...sourceElement.attributes]) {
|
|
2136
|
-
destinationElement.setAttribute(name, value);
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
|
|
2140
2300
|
class ErrorRenderer extends Renderer {
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
this.
|
|
2144
|
-
this.htmlElement = (() => {
|
|
2145
|
-
const htmlElement = document.createElement("html");
|
|
2146
|
-
htmlElement.innerHTML = html;
|
|
2147
|
-
return htmlElement;
|
|
2148
|
-
})();
|
|
2149
|
-
this.newHead = this.htmlElement.querySelector("head") || document.createElement("head");
|
|
2150
|
-
this.newBody = this.htmlElement.querySelector("body") || document.createElement("body");
|
|
2151
|
-
}
|
|
2152
|
-
static render(delegate, callback, html) {
|
|
2153
|
-
return new this(delegate, html).render(callback);
|
|
2154
|
-
}
|
|
2155
|
-
render(callback) {
|
|
2156
|
-
this.renderView(() => {
|
|
2157
|
-
this.replaceHeadAndBody();
|
|
2158
|
-
this.activateBodyScriptElements();
|
|
2159
|
-
callback();
|
|
2160
|
-
});
|
|
2301
|
+
async render() {
|
|
2302
|
+
this.replaceHeadAndBody();
|
|
2303
|
+
this.activateScriptElements();
|
|
2161
2304
|
}
|
|
2162
2305
|
replaceHeadAndBody() {
|
|
2163
2306
|
const { documentElement, head, body } = document;
|
|
2164
2307
|
documentElement.replaceChild(this.newHead, head);
|
|
2165
|
-
documentElement.replaceChild(this.
|
|
2308
|
+
documentElement.replaceChild(this.newElement, body);
|
|
2166
2309
|
}
|
|
2167
|
-
|
|
2168
|
-
for (const replaceableElement of this.
|
|
2310
|
+
activateScriptElements() {
|
|
2311
|
+
for (const replaceableElement of this.scriptElements) {
|
|
2169
2312
|
const parentNode = replaceableElement.parentNode;
|
|
2170
2313
|
if (parentNode) {
|
|
2171
2314
|
const element = this.createScriptElement(replaceableElement);
|
|
@@ -2173,84 +2316,38 @@ class ErrorRenderer extends Renderer {
|
|
|
2173
2316
|
}
|
|
2174
2317
|
}
|
|
2175
2318
|
}
|
|
2176
|
-
|
|
2319
|
+
get newHead() {
|
|
2320
|
+
return this.newSnapshot.headSnapshot.element;
|
|
2321
|
+
}
|
|
2322
|
+
get scriptElements() {
|
|
2177
2323
|
return [...document.documentElement.querySelectorAll("script")];
|
|
2178
2324
|
}
|
|
2179
2325
|
}
|
|
2180
2326
|
|
|
2181
|
-
class
|
|
2182
|
-
|
|
2183
|
-
this.
|
|
2184
|
-
this.snapshots = {};
|
|
2185
|
-
this.size = size;
|
|
2186
|
-
}
|
|
2187
|
-
has(location) {
|
|
2188
|
-
return location.toCacheKey() in this.snapshots;
|
|
2189
|
-
}
|
|
2190
|
-
get(location) {
|
|
2191
|
-
if (this.has(location)) {
|
|
2192
|
-
const snapshot = this.read(location);
|
|
2193
|
-
this.touch(location);
|
|
2194
|
-
return snapshot;
|
|
2195
|
-
}
|
|
2196
|
-
}
|
|
2197
|
-
put(location, snapshot) {
|
|
2198
|
-
this.write(location, snapshot);
|
|
2199
|
-
this.touch(location);
|
|
2200
|
-
return snapshot;
|
|
2201
|
-
}
|
|
2202
|
-
clear() {
|
|
2203
|
-
this.snapshots = {};
|
|
2204
|
-
}
|
|
2205
|
-
read(location) {
|
|
2206
|
-
return this.snapshots[location.toCacheKey()];
|
|
2327
|
+
class PageRenderer extends Renderer {
|
|
2328
|
+
get shouldRender() {
|
|
2329
|
+
return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical;
|
|
2207
2330
|
}
|
|
2208
|
-
|
|
2209
|
-
this.
|
|
2331
|
+
prepareToRender() {
|
|
2332
|
+
this.mergeHead();
|
|
2210
2333
|
}
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
const index = this.keys.indexOf(key);
|
|
2214
|
-
if (index > -1)
|
|
2215
|
-
this.keys.splice(index, 1);
|
|
2216
|
-
this.keys.unshift(key);
|
|
2217
|
-
this.trim();
|
|
2334
|
+
async render() {
|
|
2335
|
+
this.replaceBody();
|
|
2218
2336
|
}
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2337
|
+
finishRendering() {
|
|
2338
|
+
super.finishRendering();
|
|
2339
|
+
if (this.isPreview) {
|
|
2340
|
+
this.focusFirstAutofocusableElement();
|
|
2222
2341
|
}
|
|
2223
2342
|
}
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
class SnapshotRenderer extends Renderer {
|
|
2227
|
-
constructor(delegate, currentSnapshot, newSnapshot, isPreview) {
|
|
2228
|
-
super();
|
|
2229
|
-
this.delegate = delegate;
|
|
2230
|
-
this.currentSnapshot = currentSnapshot;
|
|
2231
|
-
this.currentHeadDetails = currentSnapshot.headDetails;
|
|
2232
|
-
this.newSnapshot = newSnapshot;
|
|
2233
|
-
this.newHeadDetails = newSnapshot.headDetails;
|
|
2234
|
-
this.newBody = newSnapshot.bodyElement;
|
|
2235
|
-
this.isPreview = isPreview;
|
|
2343
|
+
get currentHeadSnapshot() {
|
|
2344
|
+
return this.currentSnapshot.headSnapshot;
|
|
2236
2345
|
}
|
|
2237
|
-
|
|
2238
|
-
return
|
|
2346
|
+
get newHeadSnapshot() {
|
|
2347
|
+
return this.newSnapshot.headSnapshot;
|
|
2239
2348
|
}
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
this.mergeHead();
|
|
2243
|
-
this.renderView(() => {
|
|
2244
|
-
this.replaceBody();
|
|
2245
|
-
if (!this.isPreview) {
|
|
2246
|
-
this.focusFirstAutofocusableElement();
|
|
2247
|
-
}
|
|
2248
|
-
callback();
|
|
2249
|
-
});
|
|
2250
|
-
}
|
|
2251
|
-
else {
|
|
2252
|
-
this.invalidateView();
|
|
2253
|
-
}
|
|
2349
|
+
get newElement() {
|
|
2350
|
+
return this.newSnapshot.element;
|
|
2254
2351
|
}
|
|
2255
2352
|
mergeHead() {
|
|
2256
2353
|
this.copyNewHeadStylesheetElements();
|
|
@@ -2259,186 +2356,147 @@ class SnapshotRenderer extends Renderer {
|
|
|
2259
2356
|
this.copyNewHeadProvisionalElements();
|
|
2260
2357
|
}
|
|
2261
2358
|
replaceBody() {
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
}
|
|
2267
|
-
shouldRender() {
|
|
2268
|
-
return this.newSnapshot.isVisitable() && this.trackedElementsAreIdentical();
|
|
2359
|
+
this.preservingPermanentElements(() => {
|
|
2360
|
+
this.activateNewBody();
|
|
2361
|
+
this.assignNewBody();
|
|
2362
|
+
});
|
|
2269
2363
|
}
|
|
2270
|
-
trackedElementsAreIdentical() {
|
|
2271
|
-
return this.
|
|
2364
|
+
get trackedElementsAreIdentical() {
|
|
2365
|
+
return this.currentHeadSnapshot.trackedElementSignature == this.newHeadSnapshot.trackedElementSignature;
|
|
2272
2366
|
}
|
|
2273
2367
|
copyNewHeadStylesheetElements() {
|
|
2274
|
-
for (const element of this.
|
|
2368
|
+
for (const element of this.newHeadStylesheetElements) {
|
|
2275
2369
|
document.head.appendChild(element);
|
|
2276
2370
|
}
|
|
2277
2371
|
}
|
|
2278
2372
|
copyNewHeadScriptElements() {
|
|
2279
|
-
for (const element of this.
|
|
2373
|
+
for (const element of this.newHeadScriptElements) {
|
|
2280
2374
|
document.head.appendChild(this.createScriptElement(element));
|
|
2281
2375
|
}
|
|
2282
2376
|
}
|
|
2283
2377
|
removeCurrentHeadProvisionalElements() {
|
|
2284
|
-
for (const element of this.
|
|
2378
|
+
for (const element of this.currentHeadProvisionalElements) {
|
|
2285
2379
|
document.head.removeChild(element);
|
|
2286
2380
|
}
|
|
2287
2381
|
}
|
|
2288
2382
|
copyNewHeadProvisionalElements() {
|
|
2289
|
-
for (const element of this.
|
|
2383
|
+
for (const element of this.newHeadProvisionalElements) {
|
|
2290
2384
|
document.head.appendChild(element);
|
|
2291
2385
|
}
|
|
2292
2386
|
}
|
|
2293
|
-
relocateCurrentBodyPermanentElements() {
|
|
2294
|
-
return this.getCurrentBodyPermanentElements().reduce((placeholders, permanentElement) => {
|
|
2295
|
-
const newElement = this.newSnapshot.getPermanentElementById(permanentElement.id);
|
|
2296
|
-
if (newElement) {
|
|
2297
|
-
const placeholder = createPlaceholderForPermanentElement(permanentElement);
|
|
2298
|
-
replaceElementWithElement(permanentElement, placeholder.element);
|
|
2299
|
-
replaceElementWithElement(newElement, permanentElement);
|
|
2300
|
-
return [...placeholders, placeholder];
|
|
2301
|
-
}
|
|
2302
|
-
else {
|
|
2303
|
-
return placeholders;
|
|
2304
|
-
}
|
|
2305
|
-
}, []);
|
|
2306
|
-
}
|
|
2307
|
-
replacePlaceholderElementsWithClonedPermanentElements(placeholders) {
|
|
2308
|
-
for (const { element, permanentElement } of placeholders) {
|
|
2309
|
-
const clonedElement = permanentElement.cloneNode(true);
|
|
2310
|
-
replaceElementWithElement(element, clonedElement);
|
|
2311
|
-
}
|
|
2312
|
-
}
|
|
2313
2387
|
activateNewBody() {
|
|
2314
|
-
document.adoptNode(this.
|
|
2388
|
+
document.adoptNode(this.newElement);
|
|
2315
2389
|
this.activateNewBodyScriptElements();
|
|
2316
2390
|
}
|
|
2317
2391
|
activateNewBodyScriptElements() {
|
|
2318
|
-
for (const inertScriptElement of this.
|
|
2392
|
+
for (const inertScriptElement of this.newBodyScriptElements) {
|
|
2319
2393
|
const activatedScriptElement = this.createScriptElement(inertScriptElement);
|
|
2320
2394
|
replaceElementWithElement(inertScriptElement, activatedScriptElement);
|
|
2321
2395
|
}
|
|
2322
2396
|
}
|
|
2323
2397
|
assignNewBody() {
|
|
2324
|
-
if (document.body) {
|
|
2325
|
-
replaceElementWithElement(document.body, this.
|
|
2398
|
+
if (document.body && this.newElement instanceof HTMLBodyElement) {
|
|
2399
|
+
replaceElementWithElement(document.body, this.newElement);
|
|
2326
2400
|
}
|
|
2327
2401
|
else {
|
|
2328
|
-
document.documentElement.appendChild(this.
|
|
2402
|
+
document.documentElement.appendChild(this.newElement);
|
|
2329
2403
|
}
|
|
2330
2404
|
}
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
if (elementIsFocusable(element)) {
|
|
2334
|
-
element.focus();
|
|
2335
|
-
}
|
|
2405
|
+
get newHeadStylesheetElements() {
|
|
2406
|
+
return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot);
|
|
2336
2407
|
}
|
|
2337
|
-
|
|
2338
|
-
return this.
|
|
2408
|
+
get newHeadScriptElements() {
|
|
2409
|
+
return this.newHeadSnapshot.getScriptElementsNotInSnapshot(this.currentHeadSnapshot);
|
|
2339
2410
|
}
|
|
2340
|
-
|
|
2341
|
-
return this.
|
|
2411
|
+
get currentHeadProvisionalElements() {
|
|
2412
|
+
return this.currentHeadSnapshot.provisionalElements;
|
|
2342
2413
|
}
|
|
2343
|
-
|
|
2344
|
-
return this.
|
|
2414
|
+
get newHeadProvisionalElements() {
|
|
2415
|
+
return this.newHeadSnapshot.provisionalElements;
|
|
2345
2416
|
}
|
|
2346
|
-
|
|
2347
|
-
return this.
|
|
2417
|
+
get newBodyScriptElements() {
|
|
2418
|
+
return [...this.newElement.querySelectorAll("script")];
|
|
2348
2419
|
}
|
|
2349
|
-
|
|
2350
|
-
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
class SnapshotCache {
|
|
2423
|
+
constructor(size) {
|
|
2424
|
+
this.keys = [];
|
|
2425
|
+
this.snapshots = {};
|
|
2426
|
+
this.size = size;
|
|
2351
2427
|
}
|
|
2352
|
-
|
|
2353
|
-
return
|
|
2428
|
+
has(location) {
|
|
2429
|
+
return toCacheKey(location) in this.snapshots;
|
|
2354
2430
|
}
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
}
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
return
|
|
2431
|
+
get(location) {
|
|
2432
|
+
if (this.has(location)) {
|
|
2433
|
+
const snapshot = this.read(location);
|
|
2434
|
+
this.touch(location);
|
|
2435
|
+
return snapshot;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
put(location, snapshot) {
|
|
2439
|
+
this.write(location, snapshot);
|
|
2440
|
+
this.touch(location);
|
|
2441
|
+
return snapshot;
|
|
2442
|
+
}
|
|
2443
|
+
clear() {
|
|
2444
|
+
this.snapshots = {};
|
|
2445
|
+
}
|
|
2446
|
+
read(location) {
|
|
2447
|
+
return this.snapshots[toCacheKey(location)];
|
|
2448
|
+
}
|
|
2449
|
+
write(location, snapshot) {
|
|
2450
|
+
this.snapshots[toCacheKey(location)] = snapshot;
|
|
2451
|
+
}
|
|
2452
|
+
touch(location) {
|
|
2453
|
+
const key = toCacheKey(location);
|
|
2454
|
+
const index = this.keys.indexOf(key);
|
|
2455
|
+
if (index > -1)
|
|
2456
|
+
this.keys.splice(index, 1);
|
|
2457
|
+
this.keys.unshift(key);
|
|
2458
|
+
this.trim();
|
|
2459
|
+
}
|
|
2460
|
+
trim() {
|
|
2461
|
+
for (const key of this.keys.splice(this.size)) {
|
|
2462
|
+
delete this.snapshots[key];
|
|
2463
|
+
}
|
|
2366
2464
|
}
|
|
2367
|
-
}
|
|
2368
|
-
function elementIsFocusable(element) {
|
|
2369
|
-
return element && typeof element.focus == "function";
|
|
2370
2465
|
}
|
|
2371
2466
|
|
|
2372
|
-
class View {
|
|
2373
|
-
constructor(
|
|
2374
|
-
|
|
2467
|
+
class PageView extends View {
|
|
2468
|
+
constructor() {
|
|
2469
|
+
super(...arguments);
|
|
2375
2470
|
this.snapshotCache = new SnapshotCache(10);
|
|
2376
|
-
this.
|
|
2471
|
+
this.lastRenderedLocation = new URL(location.href);
|
|
2377
2472
|
}
|
|
2378
|
-
|
|
2379
|
-
|
|
2473
|
+
renderPage(snapshot, isPreview = false) {
|
|
2474
|
+
const renderer = new PageRenderer(this.snapshot, snapshot, isPreview);
|
|
2475
|
+
return this.render(renderer);
|
|
2380
2476
|
}
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
getSnapshot() {
|
|
2385
|
-
return Snapshot.fromHTMLElement(this.htmlElement);
|
|
2477
|
+
renderError(snapshot) {
|
|
2478
|
+
const renderer = new ErrorRenderer(this.snapshot, snapshot, false);
|
|
2479
|
+
this.render(renderer);
|
|
2386
2480
|
}
|
|
2387
2481
|
clearSnapshotCache() {
|
|
2388
2482
|
this.snapshotCache.clear();
|
|
2389
2483
|
}
|
|
2390
|
-
shouldCacheSnapshot() {
|
|
2391
|
-
return this.getSnapshot().isCacheable();
|
|
2392
|
-
}
|
|
2393
2484
|
async cacheSnapshot() {
|
|
2394
|
-
if (this.shouldCacheSnapshot
|
|
2485
|
+
if (this.shouldCacheSnapshot) {
|
|
2395
2486
|
this.delegate.viewWillCacheSnapshot();
|
|
2396
|
-
const snapshot = this
|
|
2397
|
-
|
|
2398
|
-
await nextMicrotask();
|
|
2487
|
+
const { snapshot, lastRenderedLocation: location } = this;
|
|
2488
|
+
await nextEventLoopTick();
|
|
2399
2489
|
this.snapshotCache.put(location, snapshot.clone());
|
|
2400
2490
|
}
|
|
2401
2491
|
}
|
|
2402
2492
|
getCachedSnapshotForLocation(location) {
|
|
2403
2493
|
return this.snapshotCache.get(location);
|
|
2404
2494
|
}
|
|
2405
|
-
|
|
2406
|
-
this.
|
|
2407
|
-
if (snapshot) {
|
|
2408
|
-
this.renderSnapshot(snapshot, isPreview, callback);
|
|
2409
|
-
}
|
|
2410
|
-
else {
|
|
2411
|
-
this.renderError(error, callback);
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
2414
|
-
scrollToAnchor(anchor) {
|
|
2415
|
-
const element = this.getElementForAnchor(anchor);
|
|
2416
|
-
if (element) {
|
|
2417
|
-
this.scrollToElement(element);
|
|
2418
|
-
}
|
|
2419
|
-
else {
|
|
2420
|
-
this.scrollToPosition({ x: 0, y: 0 });
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2423
|
-
scrollToElement(element) {
|
|
2424
|
-
element.scrollIntoView();
|
|
2425
|
-
}
|
|
2426
|
-
scrollToPosition({ x, y }) {
|
|
2427
|
-
window.scrollTo(x, y);
|
|
2428
|
-
}
|
|
2429
|
-
markAsPreview(isPreview) {
|
|
2430
|
-
if (isPreview) {
|
|
2431
|
-
this.htmlElement.setAttribute("data-turbo-preview", "");
|
|
2432
|
-
}
|
|
2433
|
-
else {
|
|
2434
|
-
this.htmlElement.removeAttribute("data-turbo-preview");
|
|
2435
|
-
}
|
|
2436
|
-
}
|
|
2437
|
-
renderSnapshot(snapshot, isPreview, callback) {
|
|
2438
|
-
SnapshotRenderer.render(this.delegate, callback, this.getSnapshot(), snapshot, isPreview || false);
|
|
2495
|
+
get snapshot() {
|
|
2496
|
+
return PageSnapshot.fromElement(this.element);
|
|
2439
2497
|
}
|
|
2440
|
-
|
|
2441
|
-
|
|
2498
|
+
get shouldCacheSnapshot() {
|
|
2499
|
+
return this.snapshot.isCacheable;
|
|
2442
2500
|
}
|
|
2443
2501
|
}
|
|
2444
2502
|
|
|
@@ -2446,7 +2504,7 @@ class Session {
|
|
|
2446
2504
|
constructor() {
|
|
2447
2505
|
this.navigator = new Navigator(this);
|
|
2448
2506
|
this.history = new History(this);
|
|
2449
|
-
this.view = new
|
|
2507
|
+
this.view = new PageView(this, document.documentElement);
|
|
2450
2508
|
this.adapter = new BrowserAdapter(this);
|
|
2451
2509
|
this.pageObserver = new PageObserver(this);
|
|
2452
2510
|
this.linkClickObserver = new LinkClickObserver(this);
|
|
@@ -2490,7 +2548,7 @@ class Session {
|
|
|
2490
2548
|
this.adapter = adapter;
|
|
2491
2549
|
}
|
|
2492
2550
|
visit(location, options = {}) {
|
|
2493
|
-
this.navigator.proposeVisit(
|
|
2551
|
+
this.navigator.proposeVisit(expandURL(location), options);
|
|
2494
2552
|
}
|
|
2495
2553
|
connectStreamSource(source) {
|
|
2496
2554
|
this.streamObserver.connectStreamSource(source);
|
|
@@ -2531,15 +2589,17 @@ class Session {
|
|
|
2531
2589
|
}
|
|
2532
2590
|
followedLinkToLocation(link, location) {
|
|
2533
2591
|
const action = this.getActionForLink(link);
|
|
2534
|
-
this.visit(location, { action });
|
|
2592
|
+
this.visit(location.href, { action });
|
|
2535
2593
|
}
|
|
2536
2594
|
allowsVisitingLocation(location) {
|
|
2537
2595
|
return this.applicationAllowsVisitingLocation(location);
|
|
2538
2596
|
}
|
|
2539
2597
|
visitProposedToLocation(location, options) {
|
|
2598
|
+
extendURLWithDeprecatedProperties(location);
|
|
2540
2599
|
this.adapter.visitProposedToLocation(location, options);
|
|
2541
2600
|
}
|
|
2542
2601
|
visitStarted(visit) {
|
|
2602
|
+
extendURLWithDeprecatedProperties(visit.location);
|
|
2543
2603
|
this.notifyApplicationAfterVisitingLocation(visit.location);
|
|
2544
2604
|
}
|
|
2545
2605
|
visitCompleted(visit) {
|
|
@@ -2564,19 +2624,19 @@ class Session {
|
|
|
2564
2624
|
receivedMessageFromStream(message) {
|
|
2565
2625
|
this.renderStreamMessage(message);
|
|
2566
2626
|
}
|
|
2567
|
-
|
|
2568
|
-
this.
|
|
2627
|
+
viewWillCacheSnapshot() {
|
|
2628
|
+
this.notifyApplicationBeforeCachingSnapshot();
|
|
2569
2629
|
}
|
|
2570
|
-
|
|
2630
|
+
viewWillRenderSnapshot({ element }, isPreview) {
|
|
2631
|
+
this.notifyApplicationBeforeRender(element);
|
|
2632
|
+
}
|
|
2633
|
+
viewRenderedSnapshot(snapshot, isPreview) {
|
|
2571
2634
|
this.view.lastRenderedLocation = this.history.location;
|
|
2572
2635
|
this.notifyApplicationAfterRender();
|
|
2573
2636
|
}
|
|
2574
2637
|
viewInvalidated() {
|
|
2575
2638
|
this.adapter.pageInvalidated();
|
|
2576
2639
|
}
|
|
2577
|
-
viewWillCacheSnapshot() {
|
|
2578
|
-
this.notifyApplicationBeforeCachingSnapshot();
|
|
2579
|
-
}
|
|
2580
2640
|
applicationAllowsFollowingLinkToLocation(link, location) {
|
|
2581
2641
|
const event = this.notifyApplicationAfterClickingLinkToLocation(link, location);
|
|
2582
2642
|
return !event.defaultPrevented;
|
|
@@ -2586,13 +2646,13 @@ class Session {
|
|
|
2586
2646
|
return !event.defaultPrevented;
|
|
2587
2647
|
}
|
|
2588
2648
|
notifyApplicationAfterClickingLinkToLocation(link, location) {
|
|
2589
|
-
return dispatch("turbo:click", { target: link, detail: { url: location.
|
|
2649
|
+
return dispatch("turbo:click", { target: link, detail: { url: location.href }, cancelable: true });
|
|
2590
2650
|
}
|
|
2591
2651
|
notifyApplicationBeforeVisitingLocation(location) {
|
|
2592
|
-
return dispatch("turbo:before-visit", { detail: { url: location.
|
|
2652
|
+
return dispatch("turbo:before-visit", { detail: { url: location.href }, cancelable: true });
|
|
2593
2653
|
}
|
|
2594
2654
|
notifyApplicationAfterVisitingLocation(location) {
|
|
2595
|
-
return dispatch("turbo:visit", { detail: { url: location.
|
|
2655
|
+
return dispatch("turbo:visit", { detail: { url: location.href } });
|
|
2596
2656
|
}
|
|
2597
2657
|
notifyApplicationBeforeCachingSnapshot() {
|
|
2598
2658
|
return dispatch("turbo:before-cache");
|
|
@@ -2604,7 +2664,7 @@ class Session {
|
|
|
2604
2664
|
return dispatch("turbo:render");
|
|
2605
2665
|
}
|
|
2606
2666
|
notifyApplicationAfterPageLoad(timing = {}) {
|
|
2607
|
-
return dispatch("turbo:load", { detail: { url: this.location.
|
|
2667
|
+
return dispatch("turbo:load", { detail: { url: this.location.href, timing } });
|
|
2608
2668
|
}
|
|
2609
2669
|
getActionForLink(link) {
|
|
2610
2670
|
const action = link.getAttribute("data-turbo-action");
|
|
@@ -2620,9 +2680,22 @@ class Session {
|
|
|
2620
2680
|
}
|
|
2621
2681
|
}
|
|
2622
2682
|
locationIsVisitable(location) {
|
|
2623
|
-
return
|
|
2683
|
+
return isPrefixedBy(location, this.snapshot.rootLocation) && isHTML(location);
|
|
2624
2684
|
}
|
|
2685
|
+
get snapshot() {
|
|
2686
|
+
return this.view.snapshot;
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
function extendURLWithDeprecatedProperties(url) {
|
|
2690
|
+
Object.defineProperties(url, deprecatedLocationPropertyDescriptors);
|
|
2625
2691
|
}
|
|
2692
|
+
const deprecatedLocationPropertyDescriptors = {
|
|
2693
|
+
absoluteURL: {
|
|
2694
|
+
get() {
|
|
2695
|
+
return this.toString();
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
};
|
|
2626
2699
|
|
|
2627
2700
|
const session = new Session;
|
|
2628
2701
|
const { navigator } = session;
|