turbo-rails 0.5.8 → 0.5.9
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/app/assets/javascripts/turbo.js +660 -585
- data/app/helpers/turbo/streams/action_helper.rb +2 -7
- data/app/models/turbo/streams/tag_builder.rb +2 -1
- data/lib/turbo/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 025ba92afa27783a938ec698ff465685559d71698ab3023b091a54d14f0d6dd1
|
4
|
+
data.tar.gz: ae377ccfdb57c9f9f2f862dc81e5863a5bc614002bbdfe39547cb1ede2aa797a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a7f03c2b76b07d65f64b3170b9f9ed6c1a9c4429c9f7eee3abf8bffbdbed8a320806f239df725c5aad37c826d2171ec2abc0c7787d42f929e2741584fc4d84e3
|
7
|
+
data.tar.gz: b2566bd6203b982d5b3cad1c852517a41b3a11ca8c8f097cac3cf7ff45f8d83db3eab3337807a80299cf61fc21f58990275f2e5dc92af75b03b167bf04f05ecc
|
@@ -132,82 +132,59 @@ function frameLoadingStyleFromString(style) {
|
|
132
132
|
}
|
133
133
|
}
|
134
134
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
}
|
148
|
-
|
149
|
-
return this.wrap(window.location.toString());
|
150
|
-
}
|
151
|
-
static wrap(locatable) {
|
152
|
-
if (typeof locatable == "string") {
|
153
|
-
return new this(locatable);
|
154
|
-
} else if (locatable != null) {
|
155
|
-
return locatable;
|
156
|
-
}
|
157
|
-
}
|
158
|
-
getOrigin() {
|
159
|
-
return this.absoluteURL.split("/", 3).join("/");
|
160
|
-
}
|
161
|
-
getPath() {
|
162
|
-
return (this.requestURL.match(/\/\/[^/]*(\/[^?;]*)/) || [])[1] || "/";
|
163
|
-
}
|
164
|
-
getPathComponents() {
|
165
|
-
return this.getPath().split("/").slice(1);
|
166
|
-
}
|
167
|
-
getLastPathComponent() {
|
168
|
-
return this.getPathComponents().slice(-1)[0];
|
169
|
-
}
|
170
|
-
getExtension() {
|
171
|
-
return (this.getLastPathComponent().match(/\.[^.]*$/) || [])[0] || "";
|
172
|
-
}
|
173
|
-
isHTML() {
|
174
|
-
return !!this.getExtension().match(/^(?:|\.(?:htm|html|xhtml))$/);
|
175
|
-
}
|
176
|
-
isPrefixedBy(location) {
|
177
|
-
const prefixURL = getPrefixURL(location);
|
178
|
-
return this.isEqualTo(location) || stringStartsWith(this.absoluteURL, prefixURL);
|
179
|
-
}
|
180
|
-
isEqualTo(location) {
|
181
|
-
return location && this.absoluteURL === location.absoluteURL;
|
182
|
-
}
|
183
|
-
toCacheKey() {
|
184
|
-
return this.requestURL;
|
185
|
-
}
|
186
|
-
toJSON() {
|
187
|
-
return this.absoluteURL;
|
188
|
-
}
|
189
|
-
toString() {
|
190
|
-
return this.absoluteURL;
|
135
|
+
function expandURL(locatable) {
|
136
|
+
const anchor = document.createElement("a");
|
137
|
+
anchor.href = locatable.toString();
|
138
|
+
return new URL(anchor.href);
|
139
|
+
}
|
140
|
+
|
141
|
+
function getAnchor(url) {
|
142
|
+
let anchorMatch;
|
143
|
+
if (url.hash) {
|
144
|
+
return url.hash.slice(1);
|
145
|
+
} else if (anchorMatch = url.href.match(/#(.*)$/)) {
|
146
|
+
return anchorMatch[1];
|
147
|
+
} else {
|
148
|
+
return "";
|
191
149
|
}
|
192
|
-
|
193
|
-
|
150
|
+
}
|
151
|
+
|
152
|
+
function getExtension(url) {
|
153
|
+
return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
|
154
|
+
}
|
155
|
+
|
156
|
+
function isHTML(url) {
|
157
|
+
return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml))$/);
|
158
|
+
}
|
159
|
+
|
160
|
+
function isPrefixedBy(baseURL, url) {
|
161
|
+
const prefix = getPrefix(url);
|
162
|
+
return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
|
163
|
+
}
|
164
|
+
|
165
|
+
function toCacheKey(url) {
|
166
|
+
const anchorLength = url.hash.length;
|
167
|
+
if (anchorLength < 2) {
|
168
|
+
return url.href;
|
169
|
+
} else {
|
170
|
+
return url.href.slice(0, -anchorLength);
|
194
171
|
}
|
195
172
|
}
|
196
173
|
|
197
|
-
function
|
198
|
-
return
|
174
|
+
function getPathComponents(url) {
|
175
|
+
return url.pathname.split("/").slice(1);
|
199
176
|
}
|
200
177
|
|
201
|
-
function
|
202
|
-
return
|
178
|
+
function getLastPathComponent(url) {
|
179
|
+
return getPathComponents(url).slice(-1)[0];
|
203
180
|
}
|
204
181
|
|
205
|
-
function
|
206
|
-
return
|
182
|
+
function getPrefix(url) {
|
183
|
+
return addTrailingSlash(url.origin + url.pathname);
|
207
184
|
}
|
208
185
|
|
209
|
-
function
|
210
|
-
return
|
186
|
+
function addTrailingSlash(value) {
|
187
|
+
return value.endsWith("/") ? value : value + "/";
|
211
188
|
}
|
212
189
|
|
213
190
|
class FetchResponse {
|
@@ -230,7 +207,7 @@ class FetchResponse {
|
|
230
207
|
return this.response.redirected;
|
231
208
|
}
|
232
209
|
get location() {
|
233
|
-
return
|
210
|
+
return expandURL(this.response.url);
|
234
211
|
}
|
235
212
|
get isHTML() {
|
236
213
|
return this.contentType && this.contentType.match(/^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/);
|
@@ -270,10 +247,18 @@ function nextAnimationFrame() {
|
|
270
247
|
return new Promise((resolve => requestAnimationFrame((() => resolve()))));
|
271
248
|
}
|
272
249
|
|
250
|
+
function nextEventLoopTick() {
|
251
|
+
return new Promise((resolve => setTimeout((() => resolve()), 0)));
|
252
|
+
}
|
253
|
+
|
273
254
|
function nextMicrotask() {
|
274
255
|
return Promise.resolve();
|
275
256
|
}
|
276
257
|
|
258
|
+
function parseHTMLDocument(html = "") {
|
259
|
+
return (new DOMParser).parseFromString(html, "text/html");
|
260
|
+
}
|
261
|
+
|
277
262
|
function unindent(strings, ...values) {
|
278
263
|
const lines = interpolate(strings, values).replace(/^\n/, "").split("\n");
|
279
264
|
const match = lines[0].match(/^\s+/);
|
@@ -334,27 +319,22 @@ function fetchMethodFromString(method) {
|
|
334
319
|
}
|
335
320
|
|
336
321
|
class FetchRequest {
|
337
|
-
constructor(delegate, method, location, body) {
|
322
|
+
constructor(delegate, method, location, body = new URLSearchParams) {
|
338
323
|
this.abortController = new AbortController;
|
339
324
|
this.delegate = delegate;
|
340
325
|
this.method = method;
|
341
|
-
this.
|
342
|
-
|
343
|
-
}
|
344
|
-
get url() {
|
345
|
-
const url = this.location.absoluteURL;
|
346
|
-
const query = this.params.toString();
|
347
|
-
if (this.isIdempotent && query.length) {
|
348
|
-
return [ url, query ].join(url.includes("?") ? "&" : "?");
|
326
|
+
if (this.isIdempotent) {
|
327
|
+
this.url = mergeFormDataEntries(location, [ ...body.entries() ]);
|
349
328
|
} else {
|
350
|
-
|
329
|
+
this.body = body;
|
330
|
+
this.url = location;
|
351
331
|
}
|
352
332
|
}
|
333
|
+
get location() {
|
334
|
+
return this.url;
|
335
|
+
}
|
353
336
|
get params() {
|
354
|
-
return this.
|
355
|
-
params.append(name, value.toString());
|
356
|
-
return params;
|
357
|
-
}), new URLSearchParams);
|
337
|
+
return this.url.searchParams;
|
358
338
|
}
|
359
339
|
get entries() {
|
360
340
|
return this.body ? Array.from(this.body.entries()) : [];
|
@@ -371,7 +351,7 @@ class FetchRequest {
|
|
371
351
|
});
|
372
352
|
try {
|
373
353
|
this.delegate.requestStarted(this);
|
374
|
-
const response = await fetch(this.url, fetchOptions);
|
354
|
+
const response = await fetch(this.url.href, fetchOptions);
|
375
355
|
return await this.receive(response);
|
376
356
|
} catch (error) {
|
377
357
|
this.delegate.requestErrored(this, error);
|
@@ -403,7 +383,7 @@ class FetchRequest {
|
|
403
383
|
credentials: "same-origin",
|
404
384
|
headers: this.headers,
|
405
385
|
redirect: "follow",
|
406
|
-
body: this.
|
386
|
+
body: this.body,
|
407
387
|
signal: this.abortSignal
|
408
388
|
};
|
409
389
|
}
|
@@ -411,20 +391,34 @@ class FetchRequest {
|
|
411
391
|
return this.method == FetchMethod.get;
|
412
392
|
}
|
413
393
|
get headers() {
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
}
|
418
|
-
get additionalHeaders() {
|
419
|
-
if (typeof this.delegate.additionalHeadersForRequest == "function") {
|
420
|
-
return this.delegate.additionalHeadersForRequest(this);
|
421
|
-
} else {
|
422
|
-
return {};
|
394
|
+
const headers = Object.assign({}, this.defaultHeaders);
|
395
|
+
if (typeof this.delegate.prepareHeadersForRequest == "function") {
|
396
|
+
this.delegate.prepareHeadersForRequest(headers, this);
|
423
397
|
}
|
398
|
+
return headers;
|
424
399
|
}
|
425
400
|
get abortSignal() {
|
426
401
|
return this.abortController.signal;
|
427
402
|
}
|
403
|
+
get defaultHeaders() {
|
404
|
+
return {
|
405
|
+
Accept: "text/html, application/xhtml+xml"
|
406
|
+
};
|
407
|
+
}
|
408
|
+
}
|
409
|
+
|
410
|
+
function mergeFormDataEntries(url, entries) {
|
411
|
+
const currentSearchParams = new URLSearchParams(url.search);
|
412
|
+
for (const [name, value] of entries) {
|
413
|
+
if (value instanceof File) continue;
|
414
|
+
if (currentSearchParams.has(name)) {
|
415
|
+
currentSearchParams.delete(name);
|
416
|
+
url.searchParams.set(name, value);
|
417
|
+
} else {
|
418
|
+
url.searchParams.append(name, value);
|
419
|
+
}
|
420
|
+
}
|
421
|
+
return url;
|
428
422
|
}
|
429
423
|
|
430
424
|
class AppearanceObserver {
|
@@ -454,6 +448,41 @@ class AppearanceObserver {
|
|
454
448
|
}
|
455
449
|
}
|
456
450
|
|
451
|
+
class StreamMessage {
|
452
|
+
constructor(html) {
|
453
|
+
this.templateElement = document.createElement("template");
|
454
|
+
this.templateElement.innerHTML = html;
|
455
|
+
}
|
456
|
+
static wrap(message) {
|
457
|
+
if (typeof message == "string") {
|
458
|
+
return new this(message);
|
459
|
+
} else {
|
460
|
+
return message;
|
461
|
+
}
|
462
|
+
}
|
463
|
+
get fragment() {
|
464
|
+
const fragment = document.createDocumentFragment();
|
465
|
+
for (const element of this.foreignElements) {
|
466
|
+
fragment.appendChild(document.importNode(element, true));
|
467
|
+
}
|
468
|
+
return fragment;
|
469
|
+
}
|
470
|
+
get foreignElements() {
|
471
|
+
return this.templateChildren.reduce(((streamElements, child) => {
|
472
|
+
if (child.tagName.toLowerCase() == "turbo-stream") {
|
473
|
+
return [ ...streamElements, child ];
|
474
|
+
} else {
|
475
|
+
return streamElements;
|
476
|
+
}
|
477
|
+
}), []);
|
478
|
+
}
|
479
|
+
get templateChildren() {
|
480
|
+
return Array.from(this.templateElement.content.children);
|
481
|
+
}
|
482
|
+
}
|
483
|
+
|
484
|
+
StreamMessage.contentType = "text/vnd.turbo-stream.html";
|
485
|
+
|
457
486
|
var FormSubmissionState;
|
458
487
|
|
459
488
|
(function(FormSubmissionState) {
|
@@ -465,14 +494,35 @@ var FormSubmissionState;
|
|
465
494
|
FormSubmissionState[FormSubmissionState["stopped"] = 5] = "stopped";
|
466
495
|
})(FormSubmissionState || (FormSubmissionState = {}));
|
467
496
|
|
497
|
+
var FormEnctype;
|
498
|
+
|
499
|
+
(function(FormEnctype) {
|
500
|
+
FormEnctype["urlEncoded"] = "application/x-www-form-urlencoded";
|
501
|
+
FormEnctype["multipart"] = "multipart/form-data";
|
502
|
+
FormEnctype["plain"] = "text/plain";
|
503
|
+
})(FormEnctype || (FormEnctype = {}));
|
504
|
+
|
505
|
+
function formEnctypeFromString(encoding) {
|
506
|
+
switch (encoding.toLowerCase()) {
|
507
|
+
case FormEnctype.multipart:
|
508
|
+
return FormEnctype.multipart;
|
509
|
+
|
510
|
+
case FormEnctype.plain:
|
511
|
+
return FormEnctype.plain;
|
512
|
+
|
513
|
+
default:
|
514
|
+
return FormEnctype.urlEncoded;
|
515
|
+
}
|
516
|
+
}
|
517
|
+
|
468
518
|
class FormSubmission {
|
469
519
|
constructor(delegate, formElement, submitter, mustRedirect = false) {
|
470
520
|
this.state = FormSubmissionState.initialized;
|
471
521
|
this.delegate = delegate;
|
472
522
|
this.formElement = formElement;
|
473
|
-
this.formData = buildFormData(formElement, submitter);
|
474
523
|
this.submitter = submitter;
|
475
|
-
this.
|
524
|
+
this.formData = buildFormData(formElement, submitter);
|
525
|
+
this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body);
|
476
526
|
this.mustRedirect = mustRedirect;
|
477
527
|
}
|
478
528
|
get method() {
|
@@ -485,7 +535,21 @@ class FormSubmission {
|
|
485
535
|
return ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formaction")) || this.formElement.action;
|
486
536
|
}
|
487
537
|
get location() {
|
488
|
-
return
|
538
|
+
return expandURL(this.action);
|
539
|
+
}
|
540
|
+
get body() {
|
541
|
+
if (this.enctype == FormEnctype.urlEncoded || this.method == FetchMethod.get) {
|
542
|
+
return new URLSearchParams(this.stringFormData);
|
543
|
+
} else {
|
544
|
+
return this.formData;
|
545
|
+
}
|
546
|
+
}
|
547
|
+
get enctype() {
|
548
|
+
var _a;
|
549
|
+
return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formenctype")) || this.formElement.enctype);
|
550
|
+
}
|
551
|
+
get stringFormData() {
|
552
|
+
return [ ...this.formData ].reduce(((entries, [name, value]) => entries.concat(typeof value == "string" ? [ [ name, value ] ] : [])), []);
|
489
553
|
}
|
490
554
|
async start() {
|
491
555
|
const {initialized: initialized, requesting: requesting} = FormSubmissionState;
|
@@ -502,15 +566,14 @@ class FormSubmission {
|
|
502
566
|
return true;
|
503
567
|
}
|
504
568
|
}
|
505
|
-
|
506
|
-
|
507
|
-
if (this.method != FetchMethod.get) {
|
569
|
+
prepareHeadersForRequest(headers, request) {
|
570
|
+
if (!request.isIdempotent) {
|
508
571
|
const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token");
|
509
572
|
if (token) {
|
510
573
|
headers["X-CSRF-Token"] = token;
|
511
574
|
}
|
575
|
+
headers["Accept"] = [ StreamMessage.contentType, headers["Accept"] ].join(", ");
|
512
576
|
}
|
513
|
-
return headers;
|
514
577
|
}
|
515
578
|
requestStarted(request) {
|
516
579
|
this.state = FormSubmissionState.waiting;
|
@@ -602,6 +665,37 @@ function responseSucceededWithoutRedirect(response) {
|
|
602
665
|
return response.statusCode == 200 && !response.redirected;
|
603
666
|
}
|
604
667
|
|
668
|
+
class Snapshot {
|
669
|
+
constructor(element) {
|
670
|
+
this.element = element;
|
671
|
+
}
|
672
|
+
get children() {
|
673
|
+
return [ ...this.element.children ];
|
674
|
+
}
|
675
|
+
hasAnchor(anchor) {
|
676
|
+
return this.getElementForAnchor(anchor) != null;
|
677
|
+
}
|
678
|
+
getElementForAnchor(anchor) {
|
679
|
+
try {
|
680
|
+
return this.element.querySelector(`[id='${anchor}'], a[name='${anchor}']`);
|
681
|
+
} catch (_a) {
|
682
|
+
return null;
|
683
|
+
}
|
684
|
+
}
|
685
|
+
get firstAutofocusableElement() {
|
686
|
+
return this.element.querySelector("[autofocus]");
|
687
|
+
}
|
688
|
+
get permanentElements() {
|
689
|
+
return [ ...this.element.querySelectorAll("[id][data-turbo-permanent]") ];
|
690
|
+
}
|
691
|
+
getPermanentElementById(id) {
|
692
|
+
return this.element.querySelector(`#${id}[data-turbo-permanent]`);
|
693
|
+
}
|
694
|
+
getPermanentElementsPresentInSnapshot(snapshot) {
|
695
|
+
return this.permanentElements.filter((({id: id}) => snapshot.getPermanentElementById(id)));
|
696
|
+
}
|
697
|
+
}
|
698
|
+
|
605
699
|
class FormInterceptor {
|
606
700
|
constructor(delegate, element) {
|
607
701
|
this.submitBubbled = event => {
|
@@ -626,6 +720,82 @@ class FormInterceptor {
|
|
626
720
|
}
|
627
721
|
}
|
628
722
|
|
723
|
+
class View {
|
724
|
+
constructor(delegate, element) {
|
725
|
+
this.delegate = delegate;
|
726
|
+
this.element = element;
|
727
|
+
}
|
728
|
+
scrollToAnchor(anchor) {
|
729
|
+
const element = this.snapshot.getElementForAnchor(anchor);
|
730
|
+
if (element) {
|
731
|
+
this.scrollToElement(element);
|
732
|
+
} else {
|
733
|
+
this.scrollToPosition({
|
734
|
+
x: 0,
|
735
|
+
y: 0
|
736
|
+
});
|
737
|
+
}
|
738
|
+
}
|
739
|
+
scrollToElement(element) {
|
740
|
+
element.scrollIntoView();
|
741
|
+
}
|
742
|
+
scrollToPosition({x: x, y: y}) {
|
743
|
+
this.scrollRoot.scrollTo(x, y);
|
744
|
+
}
|
745
|
+
get scrollRoot() {
|
746
|
+
return window;
|
747
|
+
}
|
748
|
+
async render(renderer) {
|
749
|
+
if (this.renderer) {
|
750
|
+
throw new Error("rendering is already in progress");
|
751
|
+
}
|
752
|
+
const {isPreview: isPreview, shouldRender: shouldRender, newSnapshot: snapshot} = renderer;
|
753
|
+
if (shouldRender) {
|
754
|
+
try {
|
755
|
+
this.renderer = renderer;
|
756
|
+
this.prepareToRenderSnapshot(renderer);
|
757
|
+
this.delegate.viewWillRenderSnapshot(snapshot, isPreview);
|
758
|
+
await this.renderSnapshot(renderer);
|
759
|
+
this.delegate.viewRenderedSnapshot(snapshot, isPreview);
|
760
|
+
this.finishRenderingSnapshot(renderer);
|
761
|
+
} finally {
|
762
|
+
delete this.renderer;
|
763
|
+
}
|
764
|
+
} else {
|
765
|
+
this.invalidate();
|
766
|
+
}
|
767
|
+
}
|
768
|
+
invalidate() {
|
769
|
+
this.delegate.viewInvalidated();
|
770
|
+
}
|
771
|
+
prepareToRenderSnapshot(renderer) {
|
772
|
+
this.markAsPreview(renderer.isPreview);
|
773
|
+
renderer.prepareToRender();
|
774
|
+
}
|
775
|
+
markAsPreview(isPreview) {
|
776
|
+
if (isPreview) {
|
777
|
+
this.element.setAttribute("data-turbo-preview", "");
|
778
|
+
} else {
|
779
|
+
this.element.removeAttribute("data-turbo-preview");
|
780
|
+
}
|
781
|
+
}
|
782
|
+
async renderSnapshot(renderer) {
|
783
|
+
await renderer.render();
|
784
|
+
}
|
785
|
+
finishRenderingSnapshot(renderer) {
|
786
|
+
renderer.finishRendering();
|
787
|
+
}
|
788
|
+
}
|
789
|
+
|
790
|
+
class FrameView extends View {
|
791
|
+
invalidate() {
|
792
|
+
this.element.innerHTML = "";
|
793
|
+
}
|
794
|
+
get snapshot() {
|
795
|
+
return new Snapshot(this.element);
|
796
|
+
}
|
797
|
+
}
|
798
|
+
|
629
799
|
class LinkInterceptor {
|
630
800
|
constructor(delegate, element) {
|
631
801
|
this.clickBubbled = event => {
|
@@ -667,10 +837,159 @@ class LinkInterceptor {
|
|
667
837
|
}
|
668
838
|
}
|
669
839
|
|
840
|
+
class Renderer {
|
841
|
+
constructor(currentSnapshot, newSnapshot, isPreview) {
|
842
|
+
this.currentSnapshot = currentSnapshot;
|
843
|
+
this.newSnapshot = newSnapshot;
|
844
|
+
this.isPreview = isPreview;
|
845
|
+
this.promise = new Promise(((resolve, reject) => this.resolvingFunctions = {
|
846
|
+
resolve: resolve,
|
847
|
+
reject: reject
|
848
|
+
}));
|
849
|
+
}
|
850
|
+
get shouldRender() {
|
851
|
+
return true;
|
852
|
+
}
|
853
|
+
prepareToRender() {
|
854
|
+
return;
|
855
|
+
}
|
856
|
+
finishRendering() {
|
857
|
+
if (this.resolvingFunctions) {
|
858
|
+
this.resolvingFunctions.resolve();
|
859
|
+
delete this.resolvingFunctions;
|
860
|
+
}
|
861
|
+
}
|
862
|
+
createScriptElement(element) {
|
863
|
+
if (element.getAttribute("data-turbo-eval") == "false") {
|
864
|
+
return element;
|
865
|
+
} else {
|
866
|
+
const createdScriptElement = document.createElement("script");
|
867
|
+
createdScriptElement.textContent = element.textContent;
|
868
|
+
createdScriptElement.async = false;
|
869
|
+
copyElementAttributes(createdScriptElement, element);
|
870
|
+
return createdScriptElement;
|
871
|
+
}
|
872
|
+
}
|
873
|
+
preservingPermanentElements(callback) {
|
874
|
+
const placeholders = relocatePermanentElements(this.currentSnapshot, this.newSnapshot);
|
875
|
+
callback();
|
876
|
+
replacePlaceholderElementsWithClonedPermanentElements(placeholders);
|
877
|
+
}
|
878
|
+
focusFirstAutofocusableElement() {
|
879
|
+
const element = this.newSnapshot.firstAutofocusableElement;
|
880
|
+
if (elementIsFocusable(element)) {
|
881
|
+
element.focus();
|
882
|
+
}
|
883
|
+
}
|
884
|
+
get currentElement() {
|
885
|
+
return this.currentSnapshot.element;
|
886
|
+
}
|
887
|
+
get newElement() {
|
888
|
+
return this.newSnapshot.element;
|
889
|
+
}
|
890
|
+
}
|
891
|
+
|
892
|
+
function replaceElementWithElement(fromElement, toElement) {
|
893
|
+
const parentElement = fromElement.parentElement;
|
894
|
+
if (parentElement) {
|
895
|
+
return parentElement.replaceChild(toElement, fromElement);
|
896
|
+
}
|
897
|
+
}
|
898
|
+
|
899
|
+
function copyElementAttributes(destinationElement, sourceElement) {
|
900
|
+
for (const {name: name, value: value} of [ ...sourceElement.attributes ]) {
|
901
|
+
destinationElement.setAttribute(name, value);
|
902
|
+
}
|
903
|
+
}
|
904
|
+
|
905
|
+
function createPlaceholderForPermanentElement(permanentElement) {
|
906
|
+
const element = document.createElement("meta");
|
907
|
+
element.setAttribute("name", "turbo-permanent-placeholder");
|
908
|
+
element.setAttribute("content", permanentElement.id);
|
909
|
+
return {
|
910
|
+
element: element,
|
911
|
+
permanentElement: permanentElement
|
912
|
+
};
|
913
|
+
}
|
914
|
+
|
915
|
+
function replacePlaceholderElementsWithClonedPermanentElements(placeholders) {
|
916
|
+
for (const {element: element, permanentElement: permanentElement} of placeholders) {
|
917
|
+
const clonedElement = permanentElement.cloneNode(true);
|
918
|
+
replaceElementWithElement(element, clonedElement);
|
919
|
+
}
|
920
|
+
}
|
921
|
+
|
922
|
+
function relocatePermanentElements(currentSnapshot, newSnapshot) {
|
923
|
+
return currentSnapshot.getPermanentElementsPresentInSnapshot(newSnapshot).reduce(((placeholders, permanentElement) => {
|
924
|
+
const newElement = newSnapshot.getPermanentElementById(permanentElement.id);
|
925
|
+
if (newElement) {
|
926
|
+
const placeholder = createPlaceholderForPermanentElement(permanentElement);
|
927
|
+
replaceElementWithElement(permanentElement, placeholder.element);
|
928
|
+
replaceElementWithElement(newElement, permanentElement);
|
929
|
+
return [ ...placeholders, placeholder ];
|
930
|
+
} else {
|
931
|
+
return placeholders;
|
932
|
+
}
|
933
|
+
}), []);
|
934
|
+
}
|
935
|
+
|
936
|
+
function elementIsFocusable(element) {
|
937
|
+
return element && typeof element.focus == "function";
|
938
|
+
}
|
939
|
+
|
940
|
+
class FrameRenderer extends Renderer {
|
941
|
+
get shouldRender() {
|
942
|
+
return true;
|
943
|
+
}
|
944
|
+
async render() {
|
945
|
+
await nextAnimationFrame();
|
946
|
+
this.preservingPermanentElements((() => {
|
947
|
+
this.loadFrameElement();
|
948
|
+
}));
|
949
|
+
this.scrollFrameIntoView();
|
950
|
+
await nextAnimationFrame();
|
951
|
+
this.focusFirstAutofocusableElement();
|
952
|
+
}
|
953
|
+
loadFrameElement() {
|
954
|
+
var _a;
|
955
|
+
const destinationRange = document.createRange();
|
956
|
+
destinationRange.selectNodeContents(this.currentElement);
|
957
|
+
destinationRange.deleteContents();
|
958
|
+
const frameElement = this.newElement;
|
959
|
+
const sourceRange = (_a = frameElement.ownerDocument) === null || _a === void 0 ? void 0 : _a.createRange();
|
960
|
+
if (sourceRange) {
|
961
|
+
sourceRange.selectNodeContents(frameElement);
|
962
|
+
this.currentElement.appendChild(sourceRange.extractContents());
|
963
|
+
}
|
964
|
+
}
|
965
|
+
scrollFrameIntoView() {
|
966
|
+
if (this.currentElement.autoscroll || this.newElement.autoscroll) {
|
967
|
+
const element = this.currentElement.firstElementChild;
|
968
|
+
const block = readScrollLogicalPosition(this.currentElement.getAttribute("data-autoscroll-block"), "end");
|
969
|
+
if (element) {
|
970
|
+
element.scrollIntoView({
|
971
|
+
block: block
|
972
|
+
});
|
973
|
+
return true;
|
974
|
+
}
|
975
|
+
}
|
976
|
+
return false;
|
977
|
+
}
|
978
|
+
}
|
979
|
+
|
980
|
+
function readScrollLogicalPosition(value, defaultValue) {
|
981
|
+
if (value == "end" || value == "start" || value == "center" || value == "nearest") {
|
982
|
+
return value;
|
983
|
+
} else {
|
984
|
+
return defaultValue;
|
985
|
+
}
|
986
|
+
}
|
987
|
+
|
670
988
|
class FrameController {
|
671
989
|
constructor(element) {
|
672
990
|
this.resolveVisitPromise = () => {};
|
673
991
|
this.element = element;
|
992
|
+
this.view = new FrameView(this, this.element);
|
674
993
|
this.appearanceObserver = new AppearanceObserver(this, this.element);
|
675
994
|
this.linkInterceptor = new LinkInterceptor(this, this.element);
|
676
995
|
this.formInterceptor = new FormInterceptor(this, this.element);
|
@@ -713,14 +1032,17 @@ class FrameController {
|
|
713
1032
|
}
|
714
1033
|
}
|
715
1034
|
async loadResponse(response) {
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
1035
|
+
try {
|
1036
|
+
const html = await response.responseHTML;
|
1037
|
+
if (html) {
|
1038
|
+
const {body: body} = parseHTMLDocument(html);
|
1039
|
+
const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
|
1040
|
+
const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
|
1041
|
+
await this.view.render(renderer);
|
1042
|
+
}
|
1043
|
+
} catch (error) {
|
1044
|
+
console.error(error);
|
1045
|
+
this.view.invalidate();
|
724
1046
|
}
|
725
1047
|
}
|
726
1048
|
elementAppearedInViewport(element) {
|
@@ -741,15 +1063,13 @@ class FrameController {
|
|
741
1063
|
}
|
742
1064
|
this.formSubmission = new FormSubmission(this, element, submitter);
|
743
1065
|
if (this.formSubmission.fetchRequest.isIdempotent) {
|
744
|
-
this.navigateFrame(element, this.formSubmission.fetchRequest.url);
|
1066
|
+
this.navigateFrame(element, this.formSubmission.fetchRequest.url.href);
|
745
1067
|
} else {
|
746
1068
|
this.formSubmission.start();
|
747
1069
|
}
|
748
1070
|
}
|
749
|
-
|
750
|
-
|
751
|
-
"Turbo-Frame": this.id
|
752
|
-
};
|
1071
|
+
prepareHeadersForRequest(headers, request) {
|
1072
|
+
headers["Turbo-Frame"] = this.id;
|
753
1073
|
}
|
754
1074
|
requestStarted(request) {
|
755
1075
|
this.element.setAttribute("busy", "");
|
@@ -782,9 +1102,11 @@ class FrameController {
|
|
782
1102
|
}
|
783
1103
|
formSubmissionErrored(formSubmission, error) {}
|
784
1104
|
formSubmissionFinished(formSubmission) {}
|
1105
|
+
viewWillRenderSnapshot(snapshot, isPreview) {}
|
1106
|
+
viewRenderedSnapshot(snapshot, isPreview) {}
|
1107
|
+
viewInvalidated() {}
|
785
1108
|
async visit(url) {
|
786
|
-
const
|
787
|
-
const request = new FetchRequest(this, FetchMethod.get, location);
|
1109
|
+
const request = new FetchRequest(this, FetchMethod.get, expandURL(url));
|
788
1110
|
return new Promise((resolve => {
|
789
1111
|
this.resolveVisitPromise = () => {
|
790
1112
|
this.resolveVisitPromise = () => {};
|
@@ -799,7 +1121,7 @@ class FrameController {
|
|
799
1121
|
}
|
800
1122
|
findFrameElement(element) {
|
801
1123
|
var _a;
|
802
|
-
const id = element.getAttribute("data-turbo-frame");
|
1124
|
+
const id = element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
|
803
1125
|
return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
|
804
1126
|
}
|
805
1127
|
async extractForeignFrameElement(container) {
|
@@ -815,38 +1137,6 @@ class FrameController {
|
|
815
1137
|
console.error(`Response has no matching <turbo-frame id="${id}"> element`);
|
816
1138
|
return new FrameElement;
|
817
1139
|
}
|
818
|
-
loadFrameElement(frameElement) {
|
819
|
-
var _a;
|
820
|
-
const destinationRange = document.createRange();
|
821
|
-
destinationRange.selectNodeContents(this.element);
|
822
|
-
destinationRange.deleteContents();
|
823
|
-
const sourceRange = (_a = frameElement.ownerDocument) === null || _a === void 0 ? void 0 : _a.createRange();
|
824
|
-
if (sourceRange) {
|
825
|
-
sourceRange.selectNodeContents(frameElement);
|
826
|
-
this.element.appendChild(sourceRange.extractContents());
|
827
|
-
}
|
828
|
-
}
|
829
|
-
focusFirstAutofocusableElement() {
|
830
|
-
const element = this.firstAutofocusableElement;
|
831
|
-
if (element) {
|
832
|
-
element.focus();
|
833
|
-
return true;
|
834
|
-
}
|
835
|
-
return false;
|
836
|
-
}
|
837
|
-
scrollFrameIntoView(frame) {
|
838
|
-
if (this.element.autoscroll || frame.autoscroll) {
|
839
|
-
const element = this.element.firstElementChild;
|
840
|
-
const block = readScrollLogicalPosition(this.element.getAttribute("data-autoscroll-block"), "end");
|
841
|
-
if (element) {
|
842
|
-
element.scrollIntoView({
|
843
|
-
block: block
|
844
|
-
});
|
845
|
-
return true;
|
846
|
-
}
|
847
|
-
}
|
848
|
-
return false;
|
849
|
-
}
|
850
1140
|
shouldInterceptNavigation(element) {
|
851
1141
|
const id = element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
|
852
1142
|
if (!this.enabled || id == "_top") {
|
@@ -860,10 +1150,6 @@ class FrameController {
|
|
860
1150
|
}
|
861
1151
|
return true;
|
862
1152
|
}
|
863
|
-
get firstAutofocusableElement() {
|
864
|
-
const element = this.element.querySelector("[autofocus]");
|
865
|
-
return element instanceof HTMLElement ? element : null;
|
866
|
-
}
|
867
1153
|
get id() {
|
868
1154
|
return this.element.id;
|
869
1155
|
}
|
@@ -893,21 +1179,6 @@ function getFrameElementById(id) {
|
|
893
1179
|
}
|
894
1180
|
}
|
895
1181
|
|
896
|
-
function readScrollLogicalPosition(value, defaultValue) {
|
897
|
-
if (value == "end" || value == "start" || value == "center" || value == "nearest") {
|
898
|
-
return value;
|
899
|
-
} else {
|
900
|
-
return defaultValue;
|
901
|
-
}
|
902
|
-
}
|
903
|
-
|
904
|
-
function fragmentFromHTML(html) {
|
905
|
-
if (html) {
|
906
|
-
const foreignDocument = document.implementation.createHTMLDocument();
|
907
|
-
return foreignDocument.createRange().createContextualFragment(html);
|
908
|
-
}
|
909
|
-
}
|
910
|
-
|
911
1182
|
function activateElement(element) {
|
912
1183
|
if (element && element.ownerDocument !== document) {
|
913
1184
|
element = document.importNode(element, true);
|
@@ -1138,9 +1409,10 @@ class ProgressBar {
|
|
1138
1409
|
|
1139
1410
|
ProgressBar.animationDuration = 300;
|
1140
1411
|
|
1141
|
-
class
|
1142
|
-
constructor(
|
1143
|
-
|
1412
|
+
class HeadSnapshot extends Snapshot {
|
1413
|
+
constructor() {
|
1414
|
+
super(...arguments);
|
1415
|
+
this.detailsByOuterHTML = this.children.reduce(((result, element) => {
|
1144
1416
|
const {outerHTML: outerHTML} = element;
|
1145
1417
|
const details = outerHTML in result ? result[outerHTML] : {
|
1146
1418
|
type: elementType(element),
|
@@ -1154,23 +1426,19 @@ class HeadDetails {
|
|
1154
1426
|
});
|
1155
1427
|
}), {});
|
1156
1428
|
}
|
1157
|
-
|
1158
|
-
const children = headElement ? [ ...headElement.children ] : [];
|
1159
|
-
return new this(children);
|
1160
|
-
}
|
1161
|
-
getTrackedElementSignature() {
|
1429
|
+
get trackedElementSignature() {
|
1162
1430
|
return Object.keys(this.detailsByOuterHTML).filter((outerHTML => this.detailsByOuterHTML[outerHTML].tracked)).join("");
|
1163
1431
|
}
|
1164
|
-
|
1165
|
-
return this.
|
1432
|
+
getScriptElementsNotInSnapshot(snapshot) {
|
1433
|
+
return this.getElementsMatchingTypeNotInSnapshot("script", snapshot);
|
1166
1434
|
}
|
1167
|
-
|
1168
|
-
return this.
|
1435
|
+
getStylesheetElementsNotInSnapshot(snapshot) {
|
1436
|
+
return this.getElementsMatchingTypeNotInSnapshot("stylesheet", snapshot);
|
1169
1437
|
}
|
1170
|
-
|
1171
|
-
return Object.keys(this.detailsByOuterHTML).filter((outerHTML => !(outerHTML in
|
1438
|
+
getElementsMatchingTypeNotInSnapshot(matchedType, snapshot) {
|
1439
|
+
return Object.keys(this.detailsByOuterHTML).filter((outerHTML => !(outerHTML in snapshot.detailsByOuterHTML))).map((outerHTML => this.detailsByOuterHTML[outerHTML])).filter((({type: type}) => type == matchedType)).map((({elements: [element]}) => element));
|
1172
1440
|
}
|
1173
|
-
|
1441
|
+
get provisionalElements() {
|
1174
1442
|
return Object.keys(this.detailsByOuterHTML).reduce(((result, outerHTML) => {
|
1175
1443
|
const {type: type, tracked: tracked, elements: elements} = this.detailsByOuterHTML[outerHTML];
|
1176
1444
|
if (type == null && !tracked) {
|
@@ -1221,75 +1489,45 @@ function elementIsMetaElementWithName(element, name) {
|
|
1221
1489
|
return tagName == "meta" && element.getAttribute("name") == name;
|
1222
1490
|
}
|
1223
1491
|
|
1224
|
-
class Snapshot {
|
1225
|
-
constructor(
|
1226
|
-
|
1227
|
-
this.
|
1228
|
-
}
|
1229
|
-
static wrap(value) {
|
1230
|
-
if (value instanceof this) {
|
1231
|
-
return value;
|
1232
|
-
} else if (typeof value == "string") {
|
1233
|
-
return this.fromHTMLString(value);
|
1234
|
-
} else {
|
1235
|
-
return this.fromHTMLElement(value);
|
1236
|
-
}
|
1237
|
-
}
|
1238
|
-
static fromHTMLString(html) {
|
1239
|
-
const {documentElement: documentElement} = (new DOMParser).parseFromString(html, "text/html");
|
1240
|
-
return this.fromHTMLElement(documentElement);
|
1241
|
-
}
|
1242
|
-
static fromHTMLElement(htmlElement) {
|
1243
|
-
const headElement = htmlElement.querySelector("head");
|
1244
|
-
const bodyElement = htmlElement.querySelector("body") || document.createElement("body");
|
1245
|
-
const headDetails = HeadDetails.fromHeadElement(headElement);
|
1246
|
-
return new this(headDetails, bodyElement);
|
1247
|
-
}
|
1248
|
-
clone() {
|
1249
|
-
const {bodyElement: bodyElement} = Snapshot.fromHTMLString(this.bodyElement.outerHTML);
|
1250
|
-
return new Snapshot(this.headDetails, bodyElement);
|
1251
|
-
}
|
1252
|
-
getRootLocation() {
|
1253
|
-
const root = this.getSetting("root", "/");
|
1254
|
-
return new Location(root);
|
1492
|
+
class PageSnapshot extends Snapshot {
|
1493
|
+
constructor(element, headSnapshot) {
|
1494
|
+
super(element);
|
1495
|
+
this.headSnapshot = headSnapshot;
|
1255
1496
|
}
|
1256
|
-
|
1257
|
-
return this.
|
1497
|
+
static fromHTMLString(html = "") {
|
1498
|
+
return this.fromDocument(parseHTMLDocument(html));
|
1258
1499
|
}
|
1259
|
-
|
1260
|
-
|
1261
|
-
return this.bodyElement.querySelector(`[id='${anchor}'], a[name='${anchor}']`);
|
1262
|
-
} catch (_a) {
|
1263
|
-
return null;
|
1264
|
-
}
|
1500
|
+
static fromElement(element) {
|
1501
|
+
return this.fromDocument(element.ownerDocument);
|
1265
1502
|
}
|
1266
|
-
|
1267
|
-
return
|
1503
|
+
static fromDocument({head: head, body: body}) {
|
1504
|
+
return new this(body, new HeadSnapshot(head));
|
1268
1505
|
}
|
1269
|
-
|
1270
|
-
return this.
|
1506
|
+
clone() {
|
1507
|
+
return new PageSnapshot(this.element.cloneNode(true), this.headSnapshot);
|
1271
1508
|
}
|
1272
|
-
|
1273
|
-
return this.
|
1509
|
+
get headElement() {
|
1510
|
+
return this.headSnapshot.element;
|
1274
1511
|
}
|
1275
|
-
|
1276
|
-
|
1512
|
+
get rootLocation() {
|
1513
|
+
var _a;
|
1514
|
+
const root = (_a = this.getSetting("root")) !== null && _a !== void 0 ? _a : "/";
|
1515
|
+
return expandURL(root);
|
1277
1516
|
}
|
1278
|
-
|
1279
|
-
return this.
|
1517
|
+
get cacheControlValue() {
|
1518
|
+
return this.getSetting("cache-control");
|
1280
1519
|
}
|
1281
|
-
isPreviewable() {
|
1282
|
-
return this.
|
1520
|
+
get isPreviewable() {
|
1521
|
+
return this.cacheControlValue != "no-preview";
|
1283
1522
|
}
|
1284
|
-
isCacheable() {
|
1285
|
-
return this.
|
1523
|
+
get isCacheable() {
|
1524
|
+
return this.cacheControlValue != "no-cache";
|
1286
1525
|
}
|
1287
|
-
isVisitable() {
|
1526
|
+
get isVisitable() {
|
1288
1527
|
return this.getSetting("visit-control") != "reload";
|
1289
1528
|
}
|
1290
|
-
getSetting(name
|
1291
|
-
|
1292
|
-
return value == null ? defaultValue : value;
|
1529
|
+
getSetting(name) {
|
1530
|
+
return this.headSnapshot.getMetaValue(`turbo-${name}`);
|
1293
1531
|
}
|
1294
1532
|
}
|
1295
1533
|
|
@@ -1334,16 +1572,6 @@ class Visit {
|
|
1334
1572
|
this.scrolled = false;
|
1335
1573
|
this.snapshotCached = false;
|
1336
1574
|
this.state = VisitState.initialized;
|
1337
|
-
this.performScroll = () => {
|
1338
|
-
if (!this.scrolled) {
|
1339
|
-
if (this.action == "restore") {
|
1340
|
-
this.scrollToRestoredPosition() || this.scrollToTop();
|
1341
|
-
} else {
|
1342
|
-
this.scrollToAnchor() || this.scrollToTop();
|
1343
|
-
}
|
1344
|
-
this.scrolled = true;
|
1345
|
-
}
|
1346
|
-
};
|
1347
1575
|
this.delegate = delegate;
|
1348
1576
|
this.location = location;
|
1349
1577
|
this.restorationIdentifier = restorationIdentifier || uuid();
|
@@ -1398,8 +1626,9 @@ class Visit {
|
|
1398
1626
|
}
|
1399
1627
|
}
|
1400
1628
|
changeHistory() {
|
1629
|
+
var _a;
|
1401
1630
|
if (!this.historyChanged) {
|
1402
|
-
const actionForHistory = this.location.
|
1631
|
+
const actionForHistory = this.location.href === ((_a = this.referrer) === null || _a === void 0 ? void 0 : _a.href) ? "replace" : this.action;
|
1403
1632
|
const method = this.getHistoryMethodForAction(actionForHistory);
|
1404
1633
|
this.history.update(method, this.location, this.restorationIdentifier);
|
1405
1634
|
this.historyChanged = true;
|
@@ -1442,18 +1671,14 @@ class Visit {
|
|
1442
1671
|
loadResponse() {
|
1443
1672
|
if (this.response) {
|
1444
1673
|
const {statusCode: statusCode, responseHTML: responseHTML} = this.response;
|
1445
|
-
this.render((() => {
|
1674
|
+
this.render((async () => {
|
1446
1675
|
this.cacheSnapshot();
|
1447
1676
|
if (isSuccessful(statusCode) && responseHTML != null) {
|
1448
|
-
this.view.
|
1449
|
-
snapshot: Snapshot.fromHTMLString(responseHTML)
|
1450
|
-
}, this.performScroll);
|
1677
|
+
await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML));
|
1451
1678
|
this.adapter.visitRendered(this);
|
1452
1679
|
this.complete();
|
1453
1680
|
} else {
|
1454
|
-
this.view.
|
1455
|
-
error: responseHTML
|
1456
|
-
}, this.performScroll);
|
1681
|
+
await this.view.renderError(PageSnapshot.fromHTMLString(responseHTML));
|
1457
1682
|
this.adapter.visitRendered(this);
|
1458
1683
|
this.fail();
|
1459
1684
|
}
|
@@ -1462,15 +1687,15 @@ class Visit {
|
|
1462
1687
|
}
|
1463
1688
|
getCachedSnapshot() {
|
1464
1689
|
const snapshot = this.view.getCachedSnapshotForLocation(this.location) || this.getPreloadedSnapshot();
|
1465
|
-
if (snapshot && (!this.location
|
1466
|
-
if (this.action == "restore" || snapshot.isPreviewable
|
1690
|
+
if (snapshot && (!getAnchor(this.location) || snapshot.hasAnchor(getAnchor(this.location)))) {
|
1691
|
+
if (this.action == "restore" || snapshot.isPreviewable) {
|
1467
1692
|
return snapshot;
|
1468
1693
|
}
|
1469
1694
|
}
|
1470
1695
|
}
|
1471
1696
|
getPreloadedSnapshot() {
|
1472
1697
|
if (this.snapshotHTML) {
|
1473
|
-
return
|
1698
|
+
return PageSnapshot.fromHTMLString(this.snapshotHTML);
|
1474
1699
|
}
|
1475
1700
|
}
|
1476
1701
|
hasCachedSnapshot() {
|
@@ -1480,12 +1705,9 @@ class Visit {
|
|
1480
1705
|
const snapshot = this.getCachedSnapshot();
|
1481
1706
|
if (snapshot) {
|
1482
1707
|
const isPreview = this.shouldIssueRequest();
|
1483
|
-
this.render((() => {
|
1708
|
+
this.render((async () => {
|
1484
1709
|
this.cacheSnapshot();
|
1485
|
-
this.view.
|
1486
|
-
snapshot: snapshot,
|
1487
|
-
isPreview: isPreview
|
1488
|
-
}, this.performScroll);
|
1710
|
+
await this.view.renderPage(snapshot);
|
1489
1711
|
this.adapter.visitRendered(this);
|
1490
1712
|
if (!isPreview) {
|
1491
1713
|
this.complete();
|
@@ -1539,6 +1761,16 @@ class Visit {
|
|
1539
1761
|
requestFinished() {
|
1540
1762
|
this.finishRequest();
|
1541
1763
|
}
|
1764
|
+
performScroll() {
|
1765
|
+
if (!this.scrolled) {
|
1766
|
+
if (this.action == "restore") {
|
1767
|
+
this.scrollToRestoredPosition() || this.scrollToTop();
|
1768
|
+
} else {
|
1769
|
+
this.scrollToAnchor() || this.scrollToTop();
|
1770
|
+
}
|
1771
|
+
this.scrolled = true;
|
1772
|
+
}
|
1773
|
+
}
|
1542
1774
|
scrollToRestoredPosition() {
|
1543
1775
|
const {scrollPosition: scrollPosition} = this.restorationData;
|
1544
1776
|
if (scrollPosition) {
|
@@ -1547,8 +1779,8 @@ class Visit {
|
|
1547
1779
|
}
|
1548
1780
|
}
|
1549
1781
|
scrollToAnchor() {
|
1550
|
-
if (this.location
|
1551
|
-
this.view.scrollToAnchor(this.location
|
1782
|
+
if (getAnchor(this.location) != null) {
|
1783
|
+
this.view.scrollToAnchor(getAnchor(this.location));
|
1552
1784
|
return true;
|
1553
1785
|
}
|
1554
1786
|
}
|
@@ -1586,12 +1818,14 @@ class Visit {
|
|
1586
1818
|
this.snapshotCached = true;
|
1587
1819
|
}
|
1588
1820
|
}
|
1589
|
-
render(callback) {
|
1821
|
+
async render(callback) {
|
1590
1822
|
this.cancelRender();
|
1591
|
-
|
1592
|
-
|
1593
|
-
callback.call(this);
|
1823
|
+
await new Promise((resolve => {
|
1824
|
+
this.frame = requestAnimationFrame((() => resolve()));
|
1594
1825
|
}));
|
1826
|
+
callback();
|
1827
|
+
delete this.frame;
|
1828
|
+
this.performScroll();
|
1595
1829
|
}
|
1596
1830
|
cancelRender() {
|
1597
1831
|
if (this.frame) {
|
@@ -1766,11 +2000,10 @@ class History {
|
|
1766
2000
|
if (this.shouldHandlePopState()) {
|
1767
2001
|
const {turbo: turbo} = event.state || {};
|
1768
2002
|
if (turbo) {
|
1769
|
-
|
1770
|
-
this.location = location;
|
2003
|
+
this.location = new URL(window.location.href);
|
1771
2004
|
const {restorationIdentifier: restorationIdentifier} = turbo;
|
1772
2005
|
this.restorationIdentifier = restorationIdentifier;
|
1773
|
-
this.delegate.historyPoppedToLocationWithRestorationIdentifier(location, restorationIdentifier);
|
2006
|
+
this.delegate.historyPoppedToLocationWithRestorationIdentifier(this.location, restorationIdentifier);
|
1774
2007
|
}
|
1775
2008
|
}
|
1776
2009
|
};
|
@@ -1785,7 +2018,7 @@ class History {
|
|
1785
2018
|
addEventListener("popstate", this.onPopState, false);
|
1786
2019
|
addEventListener("load", this.onPageLoad, false);
|
1787
2020
|
this.started = true;
|
1788
|
-
this.replace(
|
2021
|
+
this.replace(new URL(window.location.href));
|
1789
2022
|
}
|
1790
2023
|
}
|
1791
2024
|
stop() {
|
@@ -1807,7 +2040,7 @@ class History {
|
|
1807
2040
|
restorationIdentifier: restorationIdentifier
|
1808
2041
|
}
|
1809
2042
|
};
|
1810
|
-
method.call(history, state, "", location.
|
2043
|
+
method.call(history, state, "", location.href);
|
1811
2044
|
this.location = location;
|
1812
2045
|
this.restorationIdentifier = restorationIdentifier;
|
1813
2046
|
}
|
@@ -1882,7 +2115,7 @@ class LinkClickObserver {
|
|
1882
2115
|
}
|
1883
2116
|
}
|
1884
2117
|
getLocationForLink(link) {
|
1885
|
-
return
|
2118
|
+
return expandURL(link.getAttribute("href") || "");
|
1886
2119
|
}
|
1887
2120
|
}
|
1888
2121
|
|
@@ -1895,9 +2128,9 @@ class Navigator {
|
|
1895
2128
|
this.delegate.visitProposedToLocation(location, options);
|
1896
2129
|
}
|
1897
2130
|
}
|
1898
|
-
startVisit(
|
2131
|
+
startVisit(locatable, restorationIdentifier, options = {}) {
|
1899
2132
|
this.stop();
|
1900
|
-
this.currentVisit = new Visit(this,
|
2133
|
+
this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, Object.assign({
|
1901
2134
|
referrer: this.location
|
1902
2135
|
}, options));
|
1903
2136
|
this.currentVisit.start();
|
@@ -1905,7 +2138,11 @@ class Navigator {
|
|
1905
2138
|
submitForm(form, submitter) {
|
1906
2139
|
this.stop();
|
1907
2140
|
this.formSubmission = new FormSubmission(this, form, submitter, true);
|
1908
|
-
this.formSubmission.
|
2141
|
+
if (this.formSubmission.fetchRequest.isIdempotent) {
|
2142
|
+
this.proposeVisit(this.formSubmission.fetchRequest.url);
|
2143
|
+
} else {
|
2144
|
+
this.formSubmission.start();
|
2145
|
+
}
|
1909
2146
|
}
|
1910
2147
|
stop() {
|
1911
2148
|
if (this.formSubmission) {
|
@@ -1948,10 +2185,8 @@ class Navigator {
|
|
1948
2185
|
async formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
|
1949
2186
|
const responseHTML = await fetchResponse.responseHTML;
|
1950
2187
|
if (responseHTML) {
|
1951
|
-
const snapshot =
|
1952
|
-
this.view.
|
1953
|
-
snapshot: snapshot
|
1954
|
-
}, (() => {}));
|
2188
|
+
const snapshot = PageSnapshot.fromHTMLString(responseHTML);
|
2189
|
+
await this.view.renderPage(snapshot);
|
1955
2190
|
this.view.clearSnapshotCache();
|
1956
2191
|
}
|
1957
2192
|
}
|
@@ -2061,51 +2296,10 @@ class ScrollObserver {
|
|
2061
2296
|
}
|
2062
2297
|
}
|
2063
2298
|
|
2064
|
-
class StreamMessage {
|
2065
|
-
constructor(html) {
|
2066
|
-
this.templateElement = document.createElement("template");
|
2067
|
-
this.templateElement.innerHTML = html;
|
2068
|
-
}
|
2069
|
-
static wrap(message) {
|
2070
|
-
if (typeof message == "string") {
|
2071
|
-
return new this(message);
|
2072
|
-
} else {
|
2073
|
-
return message;
|
2074
|
-
}
|
2075
|
-
}
|
2076
|
-
get fragment() {
|
2077
|
-
const fragment = document.createDocumentFragment();
|
2078
|
-
for (const element of this.foreignElements) {
|
2079
|
-
fragment.appendChild(document.importNode(element, true));
|
2080
|
-
}
|
2081
|
-
return fragment;
|
2082
|
-
}
|
2083
|
-
get foreignElements() {
|
2084
|
-
return this.templateChildren.reduce(((streamElements, child) => {
|
2085
|
-
if (child.tagName.toLowerCase() == "turbo-stream") {
|
2086
|
-
return [ ...streamElements, child ];
|
2087
|
-
} else {
|
2088
|
-
return streamElements;
|
2089
|
-
}
|
2090
|
-
}), []);
|
2091
|
-
}
|
2092
|
-
get templateChildren() {
|
2093
|
-
return Array.from(this.templateElement.content.children);
|
2094
|
-
}
|
2095
|
-
}
|
2096
|
-
|
2097
2299
|
class StreamObserver {
|
2098
2300
|
constructor(delegate) {
|
2099
2301
|
this.sources = new Set;
|
2100
2302
|
this.started = false;
|
2101
|
-
this.prepareFetchRequest = event => {
|
2102
|
-
var _a;
|
2103
|
-
const fetchOptions = (_a = event.detail) === null || _a === void 0 ? void 0 : _a.fetchOptions;
|
2104
|
-
if (fetchOptions) {
|
2105
|
-
const {headers: headers} = fetchOptions;
|
2106
|
-
headers.Accept = [ "text/vnd.turbo-stream.html", headers.Accept ].join(", ");
|
2107
|
-
}
|
2108
|
-
};
|
2109
2303
|
this.inspectFetchResponse = event => {
|
2110
2304
|
const response = fetchResponseFromEvent(event);
|
2111
2305
|
if (response && fetchResponseIsStream(response)) {
|
@@ -2123,14 +2317,12 @@ class StreamObserver {
|
|
2123
2317
|
start() {
|
2124
2318
|
if (!this.started) {
|
2125
2319
|
this.started = true;
|
2126
|
-
addEventListener("turbo:before-fetch-request", this.prepareFetchRequest, true);
|
2127
2320
|
addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
|
2128
2321
|
}
|
2129
2322
|
}
|
2130
2323
|
stop() {
|
2131
2324
|
if (this.started) {
|
2132
2325
|
this.started = false;
|
2133
|
-
removeEventListener("turbo:before-fetch-request", this.prepareFetchRequest, true);
|
2134
2326
|
removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
|
2135
2327
|
}
|
2136
2328
|
}
|
@@ -2171,70 +2363,25 @@ function fetchResponseFromEvent(event) {
|
|
2171
2363
|
function fetchResponseIsStream(response) {
|
2172
2364
|
var _a;
|
2173
2365
|
const contentType = (_a = response.contentType) !== null && _a !== void 0 ? _a : "";
|
2174
|
-
return
|
2366
|
+
return contentType.startsWith(StreamMessage.contentType);
|
2175
2367
|
}
|
2176
2368
|
|
2177
2369
|
function isAction(action) {
|
2178
2370
|
return action == "advance" || action == "replace" || action == "restore";
|
2179
2371
|
}
|
2180
2372
|
|
2181
|
-
class Renderer {
|
2182
|
-
renderView(callback) {
|
2183
|
-
this.delegate.viewWillRender(this.newBody);
|
2184
|
-
callback();
|
2185
|
-
this.delegate.viewRendered(this.newBody);
|
2186
|
-
}
|
2187
|
-
invalidateView() {
|
2188
|
-
this.delegate.viewInvalidated();
|
2189
|
-
}
|
2190
|
-
createScriptElement(element) {
|
2191
|
-
if (element.getAttribute("data-turbo-eval") == "false") {
|
2192
|
-
return element;
|
2193
|
-
} else {
|
2194
|
-
const createdScriptElement = document.createElement("script");
|
2195
|
-
createdScriptElement.textContent = element.textContent;
|
2196
|
-
createdScriptElement.async = false;
|
2197
|
-
copyElementAttributes(createdScriptElement, element);
|
2198
|
-
return createdScriptElement;
|
2199
|
-
}
|
2200
|
-
}
|
2201
|
-
}
|
2202
|
-
|
2203
|
-
function copyElementAttributes(destinationElement, sourceElement) {
|
2204
|
-
for (const {name: name, value: value} of [ ...sourceElement.attributes ]) {
|
2205
|
-
destinationElement.setAttribute(name, value);
|
2206
|
-
}
|
2207
|
-
}
|
2208
|
-
|
2209
2373
|
class ErrorRenderer extends Renderer {
|
2210
|
-
|
2211
|
-
|
2212
|
-
this.
|
2213
|
-
this.htmlElement = (() => {
|
2214
|
-
const htmlElement = document.createElement("html");
|
2215
|
-
htmlElement.innerHTML = html;
|
2216
|
-
return htmlElement;
|
2217
|
-
})();
|
2218
|
-
this.newHead = this.htmlElement.querySelector("head") || document.createElement("head");
|
2219
|
-
this.newBody = this.htmlElement.querySelector("body") || document.createElement("body");
|
2220
|
-
}
|
2221
|
-
static render(delegate, callback, html) {
|
2222
|
-
return new this(delegate, html).render(callback);
|
2223
|
-
}
|
2224
|
-
render(callback) {
|
2225
|
-
this.renderView((() => {
|
2226
|
-
this.replaceHeadAndBody();
|
2227
|
-
this.activateBodyScriptElements();
|
2228
|
-
callback();
|
2229
|
-
}));
|
2374
|
+
async render() {
|
2375
|
+
this.replaceHeadAndBody();
|
2376
|
+
this.activateScriptElements();
|
2230
2377
|
}
|
2231
2378
|
replaceHeadAndBody() {
|
2232
2379
|
const {documentElement: documentElement, head: head, body: body} = document;
|
2233
2380
|
documentElement.replaceChild(this.newHead, head);
|
2234
|
-
documentElement.replaceChild(this.
|
2381
|
+
documentElement.replaceChild(this.newElement, body);
|
2235
2382
|
}
|
2236
|
-
|
2237
|
-
for (const replaceableElement of this.
|
2383
|
+
activateScriptElements() {
|
2384
|
+
for (const replaceableElement of this.scriptElements) {
|
2238
2385
|
const parentNode = replaceableElement.parentNode;
|
2239
2386
|
if (parentNode) {
|
2240
2387
|
const element = this.createScriptElement(replaceableElement);
|
@@ -2242,82 +2389,38 @@ class ErrorRenderer extends Renderer {
|
|
2242
2389
|
}
|
2243
2390
|
}
|
2244
2391
|
}
|
2245
|
-
|
2392
|
+
get newHead() {
|
2393
|
+
return this.newSnapshot.headSnapshot.element;
|
2394
|
+
}
|
2395
|
+
get scriptElements() {
|
2246
2396
|
return [ ...document.documentElement.querySelectorAll("script") ];
|
2247
2397
|
}
|
2248
2398
|
}
|
2249
2399
|
|
2250
|
-
class
|
2251
|
-
|
2252
|
-
this.
|
2253
|
-
this.snapshots = {};
|
2254
|
-
this.size = size;
|
2255
|
-
}
|
2256
|
-
has(location) {
|
2257
|
-
return location.toCacheKey() in this.snapshots;
|
2258
|
-
}
|
2259
|
-
get(location) {
|
2260
|
-
if (this.has(location)) {
|
2261
|
-
const snapshot = this.read(location);
|
2262
|
-
this.touch(location);
|
2263
|
-
return snapshot;
|
2264
|
-
}
|
2265
|
-
}
|
2266
|
-
put(location, snapshot) {
|
2267
|
-
this.write(location, snapshot);
|
2268
|
-
this.touch(location);
|
2269
|
-
return snapshot;
|
2270
|
-
}
|
2271
|
-
clear() {
|
2272
|
-
this.snapshots = {};
|
2273
|
-
}
|
2274
|
-
read(location) {
|
2275
|
-
return this.snapshots[location.toCacheKey()];
|
2400
|
+
class PageRenderer extends Renderer {
|
2401
|
+
get shouldRender() {
|
2402
|
+
return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical;
|
2276
2403
|
}
|
2277
|
-
|
2278
|
-
this.
|
2404
|
+
prepareToRender() {
|
2405
|
+
this.mergeHead();
|
2279
2406
|
}
|
2280
|
-
|
2281
|
-
|
2282
|
-
const index = this.keys.indexOf(key);
|
2283
|
-
if (index > -1) this.keys.splice(index, 1);
|
2284
|
-
this.keys.unshift(key);
|
2285
|
-
this.trim();
|
2407
|
+
async render() {
|
2408
|
+
this.replaceBody();
|
2286
2409
|
}
|
2287
|
-
|
2288
|
-
|
2289
|
-
|
2410
|
+
finishRendering() {
|
2411
|
+
super.finishRendering();
|
2412
|
+
if (this.isPreview) {
|
2413
|
+
this.focusFirstAutofocusableElement();
|
2290
2414
|
}
|
2291
2415
|
}
|
2292
|
-
|
2293
|
-
|
2294
|
-
class SnapshotRenderer extends Renderer {
|
2295
|
-
constructor(delegate, currentSnapshot, newSnapshot, isPreview) {
|
2296
|
-
super();
|
2297
|
-
this.delegate = delegate;
|
2298
|
-
this.currentSnapshot = currentSnapshot;
|
2299
|
-
this.currentHeadDetails = currentSnapshot.headDetails;
|
2300
|
-
this.newSnapshot = newSnapshot;
|
2301
|
-
this.newHeadDetails = newSnapshot.headDetails;
|
2302
|
-
this.newBody = newSnapshot.bodyElement;
|
2303
|
-
this.isPreview = isPreview;
|
2416
|
+
get currentHeadSnapshot() {
|
2417
|
+
return this.currentSnapshot.headSnapshot;
|
2304
2418
|
}
|
2305
|
-
|
2306
|
-
return
|
2419
|
+
get newHeadSnapshot() {
|
2420
|
+
return this.newSnapshot.headSnapshot;
|
2307
2421
|
}
|
2308
|
-
|
2309
|
-
|
2310
|
-
this.mergeHead();
|
2311
|
-
this.renderView((() => {
|
2312
|
-
this.replaceBody();
|
2313
|
-
if (!this.isPreview) {
|
2314
|
-
this.focusFirstAutofocusableElement();
|
2315
|
-
}
|
2316
|
-
callback();
|
2317
|
-
}));
|
2318
|
-
} else {
|
2319
|
-
this.invalidateView();
|
2320
|
-
}
|
2422
|
+
get newElement() {
|
2423
|
+
return this.newSnapshot.element;
|
2321
2424
|
}
|
2322
2425
|
mergeHead() {
|
2323
2426
|
this.copyNewHeadStylesheetElements();
|
@@ -2326,190 +2429,145 @@ class SnapshotRenderer extends Renderer {
|
|
2326
2429
|
this.copyNewHeadProvisionalElements();
|
2327
2430
|
}
|
2328
2431
|
replaceBody() {
|
2329
|
-
|
2330
|
-
|
2331
|
-
|
2332
|
-
|
2333
|
-
}
|
2334
|
-
shouldRender() {
|
2335
|
-
return this.newSnapshot.isVisitable() && this.trackedElementsAreIdentical();
|
2432
|
+
this.preservingPermanentElements((() => {
|
2433
|
+
this.activateNewBody();
|
2434
|
+
this.assignNewBody();
|
2435
|
+
}));
|
2336
2436
|
}
|
2337
|
-
trackedElementsAreIdentical() {
|
2338
|
-
return this.
|
2437
|
+
get trackedElementsAreIdentical() {
|
2438
|
+
return this.currentHeadSnapshot.trackedElementSignature == this.newHeadSnapshot.trackedElementSignature;
|
2339
2439
|
}
|
2340
2440
|
copyNewHeadStylesheetElements() {
|
2341
|
-
for (const element of this.
|
2441
|
+
for (const element of this.newHeadStylesheetElements) {
|
2342
2442
|
document.head.appendChild(element);
|
2343
2443
|
}
|
2344
2444
|
}
|
2345
2445
|
copyNewHeadScriptElements() {
|
2346
|
-
for (const element of this.
|
2446
|
+
for (const element of this.newHeadScriptElements) {
|
2347
2447
|
document.head.appendChild(this.createScriptElement(element));
|
2348
2448
|
}
|
2349
2449
|
}
|
2350
2450
|
removeCurrentHeadProvisionalElements() {
|
2351
|
-
for (const element of this.
|
2451
|
+
for (const element of this.currentHeadProvisionalElements) {
|
2352
2452
|
document.head.removeChild(element);
|
2353
2453
|
}
|
2354
2454
|
}
|
2355
2455
|
copyNewHeadProvisionalElements() {
|
2356
|
-
for (const element of this.
|
2456
|
+
for (const element of this.newHeadProvisionalElements) {
|
2357
2457
|
document.head.appendChild(element);
|
2358
2458
|
}
|
2359
2459
|
}
|
2360
|
-
relocateCurrentBodyPermanentElements() {
|
2361
|
-
return this.getCurrentBodyPermanentElements().reduce(((placeholders, permanentElement) => {
|
2362
|
-
const newElement = this.newSnapshot.getPermanentElementById(permanentElement.id);
|
2363
|
-
if (newElement) {
|
2364
|
-
const placeholder = createPlaceholderForPermanentElement(permanentElement);
|
2365
|
-
replaceElementWithElement(permanentElement, placeholder.element);
|
2366
|
-
replaceElementWithElement(newElement, permanentElement);
|
2367
|
-
return [ ...placeholders, placeholder ];
|
2368
|
-
} else {
|
2369
|
-
return placeholders;
|
2370
|
-
}
|
2371
|
-
}), []);
|
2372
|
-
}
|
2373
|
-
replacePlaceholderElementsWithClonedPermanentElements(placeholders) {
|
2374
|
-
for (const {element: element, permanentElement: permanentElement} of placeholders) {
|
2375
|
-
const clonedElement = permanentElement.cloneNode(true);
|
2376
|
-
replaceElementWithElement(element, clonedElement);
|
2377
|
-
}
|
2378
|
-
}
|
2379
2460
|
activateNewBody() {
|
2380
|
-
document.adoptNode(this.
|
2461
|
+
document.adoptNode(this.newElement);
|
2381
2462
|
this.activateNewBodyScriptElements();
|
2382
2463
|
}
|
2383
2464
|
activateNewBodyScriptElements() {
|
2384
|
-
for (const inertScriptElement of this.
|
2465
|
+
for (const inertScriptElement of this.newBodyScriptElements) {
|
2385
2466
|
const activatedScriptElement = this.createScriptElement(inertScriptElement);
|
2386
2467
|
replaceElementWithElement(inertScriptElement, activatedScriptElement);
|
2387
2468
|
}
|
2388
2469
|
}
|
2389
2470
|
assignNewBody() {
|
2390
|
-
if (document.body) {
|
2391
|
-
replaceElementWithElement(document.body, this.
|
2471
|
+
if (document.body && this.newElement instanceof HTMLBodyElement) {
|
2472
|
+
replaceElementWithElement(document.body, this.newElement);
|
2392
2473
|
} else {
|
2393
|
-
document.documentElement.appendChild(this.
|
2394
|
-
}
|
2395
|
-
}
|
2396
|
-
focusFirstAutofocusableElement() {
|
2397
|
-
const element = this.newSnapshot.findFirstAutofocusableElement();
|
2398
|
-
if (elementIsFocusable(element)) {
|
2399
|
-
element.focus();
|
2474
|
+
document.documentElement.appendChild(this.newElement);
|
2400
2475
|
}
|
2401
2476
|
}
|
2402
|
-
|
2403
|
-
return this.
|
2477
|
+
get newHeadStylesheetElements() {
|
2478
|
+
return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot);
|
2404
2479
|
}
|
2405
|
-
|
2406
|
-
return this.
|
2480
|
+
get newHeadScriptElements() {
|
2481
|
+
return this.newHeadSnapshot.getScriptElementsNotInSnapshot(this.currentHeadSnapshot);
|
2407
2482
|
}
|
2408
|
-
|
2409
|
-
return this.
|
2483
|
+
get currentHeadProvisionalElements() {
|
2484
|
+
return this.currentHeadSnapshot.provisionalElements;
|
2410
2485
|
}
|
2411
|
-
|
2412
|
-
return this.
|
2486
|
+
get newHeadProvisionalElements() {
|
2487
|
+
return this.newHeadSnapshot.provisionalElements;
|
2413
2488
|
}
|
2414
|
-
|
2415
|
-
return this.
|
2489
|
+
get newBodyScriptElements() {
|
2490
|
+
return [ ...this.newElement.querySelectorAll("script") ];
|
2416
2491
|
}
|
2417
|
-
getNewBodyScriptElements() {
|
2418
|
-
return [ ...this.newBody.querySelectorAll("script") ];
|
2419
|
-
}
|
2420
|
-
}
|
2421
|
-
|
2422
|
-
function createPlaceholderForPermanentElement(permanentElement) {
|
2423
|
-
const element = document.createElement("meta");
|
2424
|
-
element.setAttribute("name", "turbo-permanent-placeholder");
|
2425
|
-
element.setAttribute("content", permanentElement.id);
|
2426
|
-
return {
|
2427
|
-
element: element,
|
2428
|
-
permanentElement: permanentElement
|
2429
|
-
};
|
2430
2492
|
}
|
2431
2493
|
|
2432
|
-
|
2433
|
-
|
2434
|
-
|
2435
|
-
|
2494
|
+
class SnapshotCache {
|
2495
|
+
constructor(size) {
|
2496
|
+
this.keys = [];
|
2497
|
+
this.snapshots = {};
|
2498
|
+
this.size = size;
|
2499
|
+
}
|
2500
|
+
has(location) {
|
2501
|
+
return toCacheKey(location) in this.snapshots;
|
2502
|
+
}
|
2503
|
+
get(location) {
|
2504
|
+
if (this.has(location)) {
|
2505
|
+
const snapshot = this.read(location);
|
2506
|
+
this.touch(location);
|
2507
|
+
return snapshot;
|
2508
|
+
}
|
2509
|
+
}
|
2510
|
+
put(location, snapshot) {
|
2511
|
+
this.write(location, snapshot);
|
2512
|
+
this.touch(location);
|
2513
|
+
return snapshot;
|
2514
|
+
}
|
2515
|
+
clear() {
|
2516
|
+
this.snapshots = {};
|
2517
|
+
}
|
2518
|
+
read(location) {
|
2519
|
+
return this.snapshots[toCacheKey(location)];
|
2520
|
+
}
|
2521
|
+
write(location, snapshot) {
|
2522
|
+
this.snapshots[toCacheKey(location)] = snapshot;
|
2523
|
+
}
|
2524
|
+
touch(location) {
|
2525
|
+
const key = toCacheKey(location);
|
2526
|
+
const index = this.keys.indexOf(key);
|
2527
|
+
if (index > -1) this.keys.splice(index, 1);
|
2528
|
+
this.keys.unshift(key);
|
2529
|
+
this.trim();
|
2530
|
+
}
|
2531
|
+
trim() {
|
2532
|
+
for (const key of this.keys.splice(this.size)) {
|
2533
|
+
delete this.snapshots[key];
|
2534
|
+
}
|
2436
2535
|
}
|
2437
2536
|
}
|
2438
2537
|
|
2439
|
-
|
2440
|
-
|
2441
|
-
|
2442
|
-
|
2443
|
-
class View {
|
2444
|
-
constructor(delegate) {
|
2445
|
-
this.htmlElement = document.documentElement;
|
2538
|
+
class PageView extends View {
|
2539
|
+
constructor() {
|
2540
|
+
super(...arguments);
|
2446
2541
|
this.snapshotCache = new SnapshotCache(10);
|
2447
|
-
this.
|
2542
|
+
this.lastRenderedLocation = new URL(location.href);
|
2448
2543
|
}
|
2449
|
-
|
2450
|
-
|
2451
|
-
|
2452
|
-
getElementForAnchor(anchor) {
|
2453
|
-
return this.getSnapshot().getElementForAnchor(anchor);
|
2544
|
+
renderPage(snapshot, isPreview = false) {
|
2545
|
+
const renderer = new PageRenderer(this.snapshot, snapshot, isPreview);
|
2546
|
+
return this.render(renderer);
|
2454
2547
|
}
|
2455
|
-
|
2456
|
-
|
2548
|
+
renderError(snapshot) {
|
2549
|
+
const renderer = new ErrorRenderer(this.snapshot, snapshot, false);
|
2550
|
+
this.render(renderer);
|
2457
2551
|
}
|
2458
2552
|
clearSnapshotCache() {
|
2459
2553
|
this.snapshotCache.clear();
|
2460
2554
|
}
|
2461
|
-
shouldCacheSnapshot() {
|
2462
|
-
return this.getSnapshot().isCacheable();
|
2463
|
-
}
|
2464
2555
|
async cacheSnapshot() {
|
2465
|
-
if (this.shouldCacheSnapshot
|
2556
|
+
if (this.shouldCacheSnapshot) {
|
2466
2557
|
this.delegate.viewWillCacheSnapshot();
|
2467
|
-
const snapshot = this
|
2468
|
-
|
2469
|
-
await nextMicrotask();
|
2558
|
+
const {snapshot: snapshot, lastRenderedLocation: location} = this;
|
2559
|
+
await nextEventLoopTick();
|
2470
2560
|
this.snapshotCache.put(location, snapshot.clone());
|
2471
2561
|
}
|
2472
2562
|
}
|
2473
2563
|
getCachedSnapshotForLocation(location) {
|
2474
2564
|
return this.snapshotCache.get(location);
|
2475
2565
|
}
|
2476
|
-
|
2477
|
-
this.
|
2478
|
-
if (snapshot) {
|
2479
|
-
this.renderSnapshot(snapshot, isPreview, callback);
|
2480
|
-
} else {
|
2481
|
-
this.renderError(error, callback);
|
2482
|
-
}
|
2483
|
-
}
|
2484
|
-
scrollToAnchor(anchor) {
|
2485
|
-
const element = this.getElementForAnchor(anchor);
|
2486
|
-
if (element) {
|
2487
|
-
this.scrollToElement(element);
|
2488
|
-
} else {
|
2489
|
-
this.scrollToPosition({
|
2490
|
-
x: 0,
|
2491
|
-
y: 0
|
2492
|
-
});
|
2493
|
-
}
|
2494
|
-
}
|
2495
|
-
scrollToElement(element) {
|
2496
|
-
element.scrollIntoView();
|
2497
|
-
}
|
2498
|
-
scrollToPosition({x: x, y: y}) {
|
2499
|
-
window.scrollTo(x, y);
|
2500
|
-
}
|
2501
|
-
markAsPreview(isPreview) {
|
2502
|
-
if (isPreview) {
|
2503
|
-
this.htmlElement.setAttribute("data-turbo-preview", "");
|
2504
|
-
} else {
|
2505
|
-
this.htmlElement.removeAttribute("data-turbo-preview");
|
2506
|
-
}
|
2507
|
-
}
|
2508
|
-
renderSnapshot(snapshot, isPreview, callback) {
|
2509
|
-
SnapshotRenderer.render(this.delegate, callback, this.getSnapshot(), snapshot, isPreview || false);
|
2566
|
+
get snapshot() {
|
2567
|
+
return PageSnapshot.fromElement(this.element);
|
2510
2568
|
}
|
2511
|
-
|
2512
|
-
|
2569
|
+
get shouldCacheSnapshot() {
|
2570
|
+
return this.snapshot.isCacheable;
|
2513
2571
|
}
|
2514
2572
|
}
|
2515
2573
|
|
@@ -2517,7 +2575,7 @@ class Session {
|
|
2517
2575
|
constructor() {
|
2518
2576
|
this.navigator = new Navigator(this);
|
2519
2577
|
this.history = new History(this);
|
2520
|
-
this.view = new
|
2578
|
+
this.view = new PageView(this, document.documentElement);
|
2521
2579
|
this.adapter = new BrowserAdapter(this);
|
2522
2580
|
this.pageObserver = new PageObserver(this);
|
2523
2581
|
this.linkClickObserver = new LinkClickObserver(this);
|
@@ -2561,7 +2619,7 @@ class Session {
|
|
2561
2619
|
this.adapter = adapter;
|
2562
2620
|
}
|
2563
2621
|
visit(location, options = {}) {
|
2564
|
-
this.navigator.proposeVisit(
|
2622
|
+
this.navigator.proposeVisit(expandURL(location), options);
|
2565
2623
|
}
|
2566
2624
|
connectStreamSource(source) {
|
2567
2625
|
this.streamObserver.connectStreamSource(source);
|
@@ -2604,7 +2662,7 @@ class Session {
|
|
2604
2662
|
}
|
2605
2663
|
followedLinkToLocation(link, location) {
|
2606
2664
|
const action = this.getActionForLink(link);
|
2607
|
-
this.visit(location, {
|
2665
|
+
this.visit(location.href, {
|
2608
2666
|
action: action
|
2609
2667
|
});
|
2610
2668
|
}
|
@@ -2612,9 +2670,11 @@ class Session {
|
|
2612
2670
|
return this.applicationAllowsVisitingLocation(location);
|
2613
2671
|
}
|
2614
2672
|
visitProposedToLocation(location, options) {
|
2673
|
+
extendURLWithDeprecatedProperties(location);
|
2615
2674
|
this.adapter.visitProposedToLocation(location, options);
|
2616
2675
|
}
|
2617
2676
|
visitStarted(visit) {
|
2677
|
+
extendURLWithDeprecatedProperties(visit.location);
|
2618
2678
|
this.notifyApplicationAfterVisitingLocation(visit.location);
|
2619
2679
|
}
|
2620
2680
|
visitCompleted(visit) {
|
@@ -2639,19 +2699,19 @@ class Session {
|
|
2639
2699
|
receivedMessageFromStream(message) {
|
2640
2700
|
this.renderStreamMessage(message);
|
2641
2701
|
}
|
2642
|
-
|
2643
|
-
this.
|
2702
|
+
viewWillCacheSnapshot() {
|
2703
|
+
this.notifyApplicationBeforeCachingSnapshot();
|
2704
|
+
}
|
2705
|
+
viewWillRenderSnapshot({element: element}, isPreview) {
|
2706
|
+
this.notifyApplicationBeforeRender(element);
|
2644
2707
|
}
|
2645
|
-
|
2708
|
+
viewRenderedSnapshot(snapshot, isPreview) {
|
2646
2709
|
this.view.lastRenderedLocation = this.history.location;
|
2647
2710
|
this.notifyApplicationAfterRender();
|
2648
2711
|
}
|
2649
2712
|
viewInvalidated() {
|
2650
2713
|
this.adapter.pageInvalidated();
|
2651
2714
|
}
|
2652
|
-
viewWillCacheSnapshot() {
|
2653
|
-
this.notifyApplicationBeforeCachingSnapshot();
|
2654
|
-
}
|
2655
2715
|
applicationAllowsFollowingLinkToLocation(link, location) {
|
2656
2716
|
const event = this.notifyApplicationAfterClickingLinkToLocation(link, location);
|
2657
2717
|
return !event.defaultPrevented;
|
@@ -2664,7 +2724,7 @@ class Session {
|
|
2664
2724
|
return dispatch("turbo:click", {
|
2665
2725
|
target: link,
|
2666
2726
|
detail: {
|
2667
|
-
url: location.
|
2727
|
+
url: location.href
|
2668
2728
|
},
|
2669
2729
|
cancelable: true
|
2670
2730
|
});
|
@@ -2672,7 +2732,7 @@ class Session {
|
|
2672
2732
|
notifyApplicationBeforeVisitingLocation(location) {
|
2673
2733
|
return dispatch("turbo:before-visit", {
|
2674
2734
|
detail: {
|
2675
|
-
url: location.
|
2735
|
+
url: location.href
|
2676
2736
|
},
|
2677
2737
|
cancelable: true
|
2678
2738
|
});
|
@@ -2680,7 +2740,7 @@ class Session {
|
|
2680
2740
|
notifyApplicationAfterVisitingLocation(location) {
|
2681
2741
|
return dispatch("turbo:visit", {
|
2682
2742
|
detail: {
|
2683
|
-
url: location.
|
2743
|
+
url: location.href
|
2684
2744
|
}
|
2685
2745
|
});
|
2686
2746
|
}
|
@@ -2700,7 +2760,7 @@ class Session {
|
|
2700
2760
|
notifyApplicationAfterPageLoad(timing = {}) {
|
2701
2761
|
return dispatch("turbo:load", {
|
2702
2762
|
detail: {
|
2703
|
-
url: this.location.
|
2763
|
+
url: this.location.href,
|
2704
2764
|
timing: timing
|
2705
2765
|
}
|
2706
2766
|
});
|
@@ -2718,10 +2778,25 @@ class Session {
|
|
2718
2778
|
}
|
2719
2779
|
}
|
2720
2780
|
locationIsVisitable(location) {
|
2721
|
-
return
|
2781
|
+
return isPrefixedBy(location, this.snapshot.rootLocation) && isHTML(location);
|
2722
2782
|
}
|
2783
|
+
get snapshot() {
|
2784
|
+
return this.view.snapshot;
|
2785
|
+
}
|
2786
|
+
}
|
2787
|
+
|
2788
|
+
function extendURLWithDeprecatedProperties(url) {
|
2789
|
+
Object.defineProperties(url, deprecatedLocationPropertyDescriptors);
|
2723
2790
|
}
|
2724
2791
|
|
2792
|
+
const deprecatedLocationPropertyDescriptors = {
|
2793
|
+
absoluteURL: {
|
2794
|
+
get() {
|
2795
|
+
return this.toString();
|
2796
|
+
}
|
2797
|
+
}
|
2798
|
+
};
|
2799
|
+
|
2725
2800
|
const session = new Session;
|
2726
2801
|
|
2727
2802
|
const {navigator: navigator} = session;
|