@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.
Files changed (48) hide show
  1. package/dist/turbo.es2017-esm.js +647 -574
  2. package/dist/turbo.es2017-esm.js.map +1 -1
  3. package/dist/turbo.es2017-umd.js +647 -574
  4. package/dist/turbo.es2017-umd.js.map +1 -1
  5. package/dist/turbo.es5-umd.js +1153 -843
  6. package/dist/turbo.es5-umd.js.map +1 -1
  7. package/dist/types/core/drive/error_renderer.d.ts +7 -11
  8. package/dist/types/core/drive/form_submission.d.ts +11 -3
  9. package/dist/types/core/drive/head_snapshot.d.ts +21 -0
  10. package/dist/types/core/drive/history.d.ts +5 -6
  11. package/dist/types/core/drive/navigator.d.ts +7 -7
  12. package/dist/types/core/drive/page_renderer.d.ts +26 -0
  13. package/dist/types/core/drive/page_snapshot.d.ts +17 -0
  14. package/dist/types/core/drive/page_view.d.ts +21 -0
  15. package/dist/types/core/drive/snapshot_cache.d.ts +8 -9
  16. package/dist/types/core/drive/visit.d.ts +13 -15
  17. package/dist/types/core/frames/frame_controller.d.ts +11 -10
  18. package/dist/types/core/frames/frame_renderer.d.ts +8 -0
  19. package/dist/types/core/frames/frame_view.d.ts +7 -0
  20. package/dist/types/core/index.d.ts +1 -1
  21. package/dist/types/core/native/adapter.d.ts +1 -2
  22. package/dist/types/core/native/browser_adapter.d.ts +1 -2
  23. package/dist/types/core/renderer.d.ts +26 -0
  24. package/dist/types/core/session.d.ts +23 -21
  25. package/dist/types/core/snapshot.d.ts +11 -0
  26. package/dist/types/core/streams/stream_message.d.ts +1 -0
  27. package/dist/types/core/url.d.ts +7 -0
  28. package/dist/types/core/view.d.ts +27 -0
  29. package/dist/types/http/fetch_request.d.ts +8 -11
  30. package/dist/types/http/fetch_response.d.ts +1 -2
  31. package/dist/types/observers/link_click_observer.d.ts +3 -4
  32. package/dist/types/observers/stream_observer.d.ts +0 -1
  33. package/dist/types/tests/functional/form_submission_tests.d.ts +11 -3
  34. package/dist/types/tests/functional/frame_tests.d.ts +4 -0
  35. package/dist/types/tests/functional/navigation_tests.d.ts +1 -0
  36. package/dist/types/tests/functional/rendering_tests.d.ts +2 -0
  37. package/dist/types/tests/functional/stream_tests.d.ts +0 -2
  38. package/dist/types/tests/helpers/functional_test_case.d.ts +3 -0
  39. package/dist/types/tests/unit/deprecated_adapter_support_test.d.ts +21 -0
  40. package/dist/types/tests/unit/index.d.ts +1 -0
  41. package/dist/types/util.d.ts +2 -0
  42. package/package.json +1 -1
  43. package/dist/types/core/drive/head_details.d.ts +0 -22
  44. package/dist/types/core/drive/renderer.d.ts +0 -13
  45. package/dist/types/core/drive/snapshot.d.ts +0 -24
  46. package/dist/types/core/drive/snapshot_renderer.d.ts +0 -43
  47. package/dist/types/core/drive/view.d.ts +0 -34
  48. package/dist/types/core/location.d.ts +0 -22
@@ -1,5 +1,5 @@
1
1
  /*
2
- Turbo 7.0.0-beta.3
2
+ Turbo 7.0.0-beta.4
3
3
  Copyright © 2021 Basecamp, LLC
4
4
  */
5
5
  (function (global, factory) {
@@ -141,80 +141,53 @@ Copyright © 2021 Basecamp, LLC
141
141
  }
142
142
  }
143
143
 
144
- class Location {
145
- constructor(url) {
146
- const linkWithAnchor = document.createElement("a");
147
- linkWithAnchor.href = url;
148
- this.absoluteURL = linkWithAnchor.href;
149
- const anchorLength = linkWithAnchor.hash.length;
150
- if (anchorLength < 2) {
151
- this.requestURL = this.absoluteURL;
152
- }
153
- else {
154
- this.requestURL = this.absoluteURL.slice(0, -anchorLength);
155
- this.anchor = linkWithAnchor.hash.slice(1);
156
- }
157
- }
158
- static get currentLocation() {
159
- return this.wrap(window.location.toString());
160
- }
161
- static wrap(locatable) {
162
- if (typeof locatable == "string") {
163
- return new this(locatable);
164
- }
165
- else if (locatable != null) {
166
- return locatable;
167
- }
168
- }
169
- getOrigin() {
170
- return this.absoluteURL.split("/", 3).join("/");
171
- }
172
- getPath() {
173
- return (this.requestURL.match(/\/\/[^/]*(\/[^?;]*)/) || [])[1] || "/";
174
- }
175
- getPathComponents() {
176
- return this.getPath().split("/").slice(1);
177
- }
178
- getLastPathComponent() {
179
- return this.getPathComponents().slice(-1)[0];
180
- }
181
- getExtension() {
182
- return (this.getLastPathComponent().match(/\.[^.]*$/) || [])[0] || "";
183
- }
184
- isHTML() {
185
- return !!this.getExtension().match(/^(?:|\.(?:htm|html|xhtml))$/);
186
- }
187
- isPrefixedBy(location) {
188
- const prefixURL = getPrefixURL(location);
189
- return this.isEqualTo(location) || stringStartsWith(this.absoluteURL, prefixURL);
190
- }
191
- isEqualTo(location) {
192
- return location && this.absoluteURL === location.absoluteURL;
144
+ function expandURL(locatable) {
145
+ const anchor = document.createElement("a");
146
+ anchor.href = locatable.toString();
147
+ return new URL(anchor.href);
148
+ }
149
+ function getAnchor(url) {
150
+ let anchorMatch;
151
+ if (url.hash) {
152
+ return url.hash.slice(1);
193
153
  }
194
- toCacheKey() {
195
- return this.requestURL;
154
+ else if (anchorMatch = url.href.match(/#(.*)$/)) {
155
+ return anchorMatch[1];
196
156
  }
197
- toJSON() {
198
- return this.absoluteURL;
157
+ else {
158
+ return "";
199
159
  }
200
- toString() {
201
- return this.absoluteURL;
160
+ }
161
+ function getExtension(url) {
162
+ return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
163
+ }
164
+ function isHTML(url) {
165
+ return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml))$/);
166
+ }
167
+ function isPrefixedBy(baseURL, url) {
168
+ const prefix = getPrefix(url);
169
+ return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
170
+ }
171
+ function toCacheKey(url) {
172
+ const anchorLength = url.hash.length;
173
+ if (anchorLength < 2) {
174
+ return url.href;
202
175
  }
203
- valueOf() {
204
- return this.absoluteURL;
176
+ else {
177
+ return url.href.slice(0, -anchorLength);
205
178
  }
206
179
  }
207
- function getPrefixURL(location) {
208
- return addTrailingSlash(location.getOrigin() + location.getPath());
180
+ function getPathComponents(url) {
181
+ return url.pathname.split("/").slice(1);
209
182
  }
210
- function addTrailingSlash(url) {
211
- return stringEndsWith(url, "/") ? url : url + "/";
183
+ function getLastPathComponent(url) {
184
+ return getPathComponents(url).slice(-1)[0];
212
185
  }
213
- function stringStartsWith(string, prefix) {
214
- return string.slice(0, prefix.length) === prefix;
186
+ function getPrefix(url) {
187
+ return addTrailingSlash(url.origin + url.pathname);
215
188
  }
216
- function stringEndsWith(string, suffix) {
217
- return string.slice(-suffix.length) === suffix;
189
+ function addTrailingSlash(value) {
190
+ return value.endsWith("/") ? value : value + "/";
218
191
  }
219
192
 
220
193
  class FetchResponse {
@@ -237,7 +210,7 @@ Copyright © 2021 Basecamp, LLC
237
210
  return this.response.redirected;
238
211
  }
239
212
  get location() {
240
- return Location.wrap(this.response.url);
213
+ return expandURL(this.response.url);
241
214
  }
242
215
  get isHTML() {
243
216
  return this.contentType && this.contentType.match(/^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/);
@@ -272,9 +245,15 @@ Copyright © 2021 Basecamp, LLC
272
245
  function nextAnimationFrame() {
273
246
  return new Promise(resolve => requestAnimationFrame(() => resolve()));
274
247
  }
248
+ function nextEventLoopTick() {
249
+ return new Promise(resolve => setTimeout(() => resolve(), 0));
250
+ }
275
251
  function nextMicrotask() {
276
252
  return Promise.resolve();
277
253
  }
254
+ function parseHTMLDocument(html = "") {
255
+ return new DOMParser().parseFromString(html, "text/html");
256
+ }
278
257
  function unindent(strings, ...values) {
279
258
  const lines = interpolate(strings, values).replace(/^\n/, "").split("\n");
280
259
  const match = lines[0].match(/^\s+/);
@@ -322,28 +301,23 @@ Copyright © 2021 Basecamp, LLC
322
301
  }
323
302
  }
324
303
  class FetchRequest {
325
- constructor(delegate, method, location, body) {
304
+ constructor(delegate, method, location, body = new URLSearchParams) {
326
305
  this.abortController = new AbortController;
327
306
  this.delegate = delegate;
328
307
  this.method = method;
329
- this.location = location;
330
- this.body = body;
331
- }
332
- get url() {
333
- const url = this.location.absoluteURL;
334
- const query = this.params.toString();
335
- if (this.isIdempotent && query.length) {
336
- return [url, query].join(url.includes("?") ? "&" : "?");
308
+ if (this.isIdempotent) {
309
+ this.url = mergeFormDataEntries(location, [...body.entries()]);
337
310
  }
338
311
  else {
339
- return url;
312
+ this.body = body;
313
+ this.url = location;
340
314
  }
341
315
  }
316
+ get location() {
317
+ return this.url;
318
+ }
342
319
  get params() {
343
- return this.entries.reduce((params, [name, value]) => {
344
- params.append(name, value.toString());
345
- return params;
346
- }, new URLSearchParams);
320
+ return this.url.searchParams;
347
321
  }
348
322
  get entries() {
349
323
  return this.body ? Array.from(this.body.entries()) : [];
@@ -356,7 +330,7 @@ Copyright © 2021 Basecamp, LLC
356
330
  dispatch("turbo:before-fetch-request", { detail: { fetchOptions } });
357
331
  try {
358
332
  this.delegate.requestStarted(this);
359
- const response = await fetch(this.url, fetchOptions);
333
+ const response = await fetch(this.url.href, fetchOptions);
360
334
  return await this.receive(response);
361
335
  }
362
336
  catch (error) {
@@ -387,7 +361,7 @@ Copyright © 2021 Basecamp, LLC
387
361
  credentials: "same-origin",
388
362
  headers: this.headers,
389
363
  redirect: "follow",
390
- body: this.isIdempotent ? undefined : this.body,
364
+ body: this.body,
391
365
  signal: this.abortSignal
392
366
  };
393
367
  }
@@ -395,19 +369,35 @@ Copyright © 2021 Basecamp, LLC
395
369
  return this.method == FetchMethod.get;
396
370
  }
397
371
  get headers() {
398
- return Object.assign({ "Accept": "text/html, application/xhtml+xml" }, this.additionalHeaders);
399
- }
400
- get additionalHeaders() {
401
- if (typeof this.delegate.additionalHeadersForRequest == "function") {
402
- return this.delegate.additionalHeadersForRequest(this);
403
- }
404
- else {
405
- return {};
372
+ const headers = Object.assign({}, this.defaultHeaders);
373
+ if (typeof this.delegate.prepareHeadersForRequest == "function") {
374
+ this.delegate.prepareHeadersForRequest(headers, this);
406
375
  }
376
+ return headers;
407
377
  }
408
378
  get abortSignal() {
409
379
  return this.abortController.signal;
410
380
  }
381
+ get defaultHeaders() {
382
+ return {
383
+ "Accept": "text/html, application/xhtml+xml"
384
+ };
385
+ }
386
+ }
387
+ function mergeFormDataEntries(url, entries) {
388
+ const currentSearchParams = new URLSearchParams(url.search);
389
+ for (const [name, value] of entries) {
390
+ if (value instanceof File)
391
+ continue;
392
+ if (currentSearchParams.has(name)) {
393
+ currentSearchParams.delete(name);
394
+ url.searchParams.set(name, value);
395
+ }
396
+ else {
397
+ url.searchParams.append(name, value);
398
+ }
399
+ }
400
+ return url;
411
401
  }
412
402
 
413
403
  class AppearanceObserver {
@@ -437,6 +427,42 @@ Copyright © 2021 Basecamp, LLC
437
427
  }
438
428
  }
439
429
 
430
+ class StreamMessage {
431
+ constructor(html) {
432
+ this.templateElement = document.createElement("template");
433
+ this.templateElement.innerHTML = html;
434
+ }
435
+ static wrap(message) {
436
+ if (typeof message == "string") {
437
+ return new this(message);
438
+ }
439
+ else {
440
+ return message;
441
+ }
442
+ }
443
+ get fragment() {
444
+ const fragment = document.createDocumentFragment();
445
+ for (const element of this.foreignElements) {
446
+ fragment.appendChild(document.importNode(element, true));
447
+ }
448
+ return fragment;
449
+ }
450
+ get foreignElements() {
451
+ return this.templateChildren.reduce((streamElements, child) => {
452
+ if (child.tagName.toLowerCase() == "turbo-stream") {
453
+ return [...streamElements, child];
454
+ }
455
+ else {
456
+ return streamElements;
457
+ }
458
+ }, []);
459
+ }
460
+ get templateChildren() {
461
+ return Array.from(this.templateElement.content.children);
462
+ }
463
+ }
464
+ StreamMessage.contentType = "text/vnd.turbo-stream.html";
465
+
440
466
  var FormSubmissionState;
441
467
  (function (FormSubmissionState) {
442
468
  FormSubmissionState[FormSubmissionState["initialized"] = 0] = "initialized";
@@ -446,14 +472,27 @@ Copyright © 2021 Basecamp, LLC
446
472
  FormSubmissionState[FormSubmissionState["stopping"] = 4] = "stopping";
447
473
  FormSubmissionState[FormSubmissionState["stopped"] = 5] = "stopped";
448
474
  })(FormSubmissionState || (FormSubmissionState = {}));
475
+ var FormEnctype;
476
+ (function (FormEnctype) {
477
+ FormEnctype["urlEncoded"] = "application/x-www-form-urlencoded";
478
+ FormEnctype["multipart"] = "multipart/form-data";
479
+ FormEnctype["plain"] = "text/plain";
480
+ })(FormEnctype || (FormEnctype = {}));
481
+ function formEnctypeFromString(encoding) {
482
+ switch (encoding.toLowerCase()) {
483
+ case FormEnctype.multipart: return FormEnctype.multipart;
484
+ case FormEnctype.plain: return FormEnctype.plain;
485
+ default: return FormEnctype.urlEncoded;
486
+ }
487
+ }
449
488
  class FormSubmission {
450
489
  constructor(delegate, formElement, submitter, mustRedirect = false) {
451
490
  this.state = FormSubmissionState.initialized;
452
491
  this.delegate = delegate;
453
492
  this.formElement = formElement;
454
- this.formData = buildFormData(formElement, submitter);
455
493
  this.submitter = submitter;
456
- this.fetchRequest = new FetchRequest(this, this.method, this.location, this.formData);
494
+ this.formData = buildFormData(formElement, submitter);
495
+ this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body);
457
496
  this.mustRedirect = mustRedirect;
458
497
  }
459
498
  get method() {
@@ -466,7 +505,24 @@ Copyright © 2021 Basecamp, LLC
466
505
  return ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formaction")) || this.formElement.action;
467
506
  }
468
507
  get location() {
469
- return Location.wrap(this.action);
508
+ return expandURL(this.action);
509
+ }
510
+ get body() {
511
+ if (this.enctype == FormEnctype.urlEncoded || this.method == FetchMethod.get) {
512
+ return new URLSearchParams(this.stringFormData);
513
+ }
514
+ else {
515
+ return this.formData;
516
+ }
517
+ }
518
+ get enctype() {
519
+ var _a;
520
+ return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formenctype")) || this.formElement.enctype);
521
+ }
522
+ get stringFormData() {
523
+ return [...this.formData].reduce((entries, [name, value]) => {
524
+ return entries.concat(typeof value == "string" ? [[name, value]] : []);
525
+ }, []);
470
526
  }
471
527
  async start() {
472
528
  const { initialized, requesting } = FormSubmissionState;
@@ -483,15 +539,14 @@ Copyright © 2021 Basecamp, LLC
483
539
  return true;
484
540
  }
485
541
  }
486
- additionalHeadersForRequest(request) {
487
- const headers = {};
488
- if (this.method != FetchMethod.get) {
542
+ prepareHeadersForRequest(headers, request) {
543
+ if (!request.isIdempotent) {
489
544
  const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token");
490
545
  if (token) {
491
546
  headers["X-CSRF-Token"] = token;
492
547
  }
548
+ headers["Accept"] = [StreamMessage.contentType, headers["Accept"]].join(", ");
493
549
  }
494
- return headers;
495
550
  }
496
551
  requestStarted(request) {
497
552
  this.state = FormSubmissionState.waiting;
@@ -559,6 +614,38 @@ Copyright © 2021 Basecamp, LLC
559
614
  return response.statusCode == 200 && !response.redirected;
560
615
  }
561
616
 
617
+ class Snapshot {
618
+ constructor(element) {
619
+ this.element = element;
620
+ }
621
+ get children() {
622
+ return [...this.element.children];
623
+ }
624
+ hasAnchor(anchor) {
625
+ return this.getElementForAnchor(anchor) != null;
626
+ }
627
+ getElementForAnchor(anchor) {
628
+ try {
629
+ return this.element.querySelector(`[id='${anchor}'], a[name='${anchor}']`);
630
+ }
631
+ catch (_a) {
632
+ return null;
633
+ }
634
+ }
635
+ get firstAutofocusableElement() {
636
+ return this.element.querySelector("[autofocus]");
637
+ }
638
+ get permanentElements() {
639
+ return [...this.element.querySelectorAll("[id][data-turbo-permanent]")];
640
+ }
641
+ getPermanentElementById(id) {
642
+ return this.element.querySelector(`#${id}[data-turbo-permanent]`);
643
+ }
644
+ getPermanentElementsPresentInSnapshot(snapshot) {
645
+ return this.permanentElements.filter(({ id }) => snapshot.getPermanentElementById(id));
646
+ }
647
+ }
648
+
562
649
  class FormInterceptor {
563
650
  constructor(delegate, element) {
564
651
  this.submitBubbled = ((event) => {
@@ -583,6 +670,83 @@ Copyright © 2021 Basecamp, LLC
583
670
  }
584
671
  }
585
672
 
673
+ class View {
674
+ constructor(delegate, element) {
675
+ this.delegate = delegate;
676
+ this.element = element;
677
+ }
678
+ scrollToAnchor(anchor) {
679
+ const element = this.snapshot.getElementForAnchor(anchor);
680
+ if (element) {
681
+ this.scrollToElement(element);
682
+ }
683
+ else {
684
+ this.scrollToPosition({ x: 0, y: 0 });
685
+ }
686
+ }
687
+ scrollToElement(element) {
688
+ element.scrollIntoView();
689
+ }
690
+ scrollToPosition({ x, y }) {
691
+ this.scrollRoot.scrollTo(x, y);
692
+ }
693
+ get scrollRoot() {
694
+ return window;
695
+ }
696
+ async render(renderer) {
697
+ if (this.renderer) {
698
+ throw new Error("rendering is already in progress");
699
+ }
700
+ const { isPreview, shouldRender, newSnapshot: snapshot } = renderer;
701
+ if (shouldRender) {
702
+ try {
703
+ this.renderer = renderer;
704
+ this.prepareToRenderSnapshot(renderer);
705
+ this.delegate.viewWillRenderSnapshot(snapshot, isPreview);
706
+ await this.renderSnapshot(renderer);
707
+ this.delegate.viewRenderedSnapshot(snapshot, isPreview);
708
+ this.finishRenderingSnapshot(renderer);
709
+ }
710
+ finally {
711
+ delete this.renderer;
712
+ }
713
+ }
714
+ else {
715
+ this.invalidate();
716
+ }
717
+ }
718
+ invalidate() {
719
+ this.delegate.viewInvalidated();
720
+ }
721
+ prepareToRenderSnapshot(renderer) {
722
+ this.markAsPreview(renderer.isPreview);
723
+ renderer.prepareToRender();
724
+ }
725
+ markAsPreview(isPreview) {
726
+ if (isPreview) {
727
+ this.element.setAttribute("data-turbo-preview", "");
728
+ }
729
+ else {
730
+ this.element.removeAttribute("data-turbo-preview");
731
+ }
732
+ }
733
+ async renderSnapshot(renderer) {
734
+ await renderer.render();
735
+ }
736
+ finishRenderingSnapshot(renderer) {
737
+ renderer.finishRendering();
738
+ }
739
+ }
740
+
741
+ class FrameView extends View {
742
+ invalidate() {
743
+ this.element.innerHTML = "";
744
+ }
745
+ get snapshot() {
746
+ return new Snapshot(this.element);
747
+ }
748
+ }
749
+
586
750
  class LinkInterceptor {
587
751
  constructor(delegate, element) {
588
752
  this.clickBubbled = (event) => {
@@ -629,10 +793,147 @@ Copyright © 2021 Basecamp, LLC
629
793
  }
630
794
  }
631
795
 
796
+ class Renderer {
797
+ constructor(currentSnapshot, newSnapshot, isPreview) {
798
+ this.currentSnapshot = currentSnapshot;
799
+ this.newSnapshot = newSnapshot;
800
+ this.isPreview = isPreview;
801
+ this.promise = new Promise((resolve, reject) => this.resolvingFunctions = { resolve, reject });
802
+ }
803
+ get shouldRender() {
804
+ return true;
805
+ }
806
+ prepareToRender() {
807
+ return;
808
+ }
809
+ finishRendering() {
810
+ if (this.resolvingFunctions) {
811
+ this.resolvingFunctions.resolve();
812
+ delete this.resolvingFunctions;
813
+ }
814
+ }
815
+ createScriptElement(element) {
816
+ if (element.getAttribute("data-turbo-eval") == "false") {
817
+ return element;
818
+ }
819
+ else {
820
+ const createdScriptElement = document.createElement("script");
821
+ createdScriptElement.textContent = element.textContent;
822
+ createdScriptElement.async = false;
823
+ copyElementAttributes(createdScriptElement, element);
824
+ return createdScriptElement;
825
+ }
826
+ }
827
+ preservingPermanentElements(callback) {
828
+ const placeholders = relocatePermanentElements(this.currentSnapshot, this.newSnapshot);
829
+ callback();
830
+ replacePlaceholderElementsWithClonedPermanentElements(placeholders);
831
+ }
832
+ focusFirstAutofocusableElement() {
833
+ const element = this.newSnapshot.firstAutofocusableElement;
834
+ if (elementIsFocusable(element)) {
835
+ element.focus();
836
+ }
837
+ }
838
+ get currentElement() {
839
+ return this.currentSnapshot.element;
840
+ }
841
+ get newElement() {
842
+ return this.newSnapshot.element;
843
+ }
844
+ }
845
+ function replaceElementWithElement(fromElement, toElement) {
846
+ const parentElement = fromElement.parentElement;
847
+ if (parentElement) {
848
+ return parentElement.replaceChild(toElement, fromElement);
849
+ }
850
+ }
851
+ function copyElementAttributes(destinationElement, sourceElement) {
852
+ for (const { name, value } of [...sourceElement.attributes]) {
853
+ destinationElement.setAttribute(name, value);
854
+ }
855
+ }
856
+ function createPlaceholderForPermanentElement(permanentElement) {
857
+ const element = document.createElement("meta");
858
+ element.setAttribute("name", "turbo-permanent-placeholder");
859
+ element.setAttribute("content", permanentElement.id);
860
+ return { element, permanentElement };
861
+ }
862
+ function replacePlaceholderElementsWithClonedPermanentElements(placeholders) {
863
+ for (const { element, permanentElement } of placeholders) {
864
+ const clonedElement = permanentElement.cloneNode(true);
865
+ replaceElementWithElement(element, clonedElement);
866
+ }
867
+ }
868
+ function relocatePermanentElements(currentSnapshot, newSnapshot) {
869
+ return currentSnapshot.getPermanentElementsPresentInSnapshot(newSnapshot).reduce((placeholders, permanentElement) => {
870
+ const newElement = newSnapshot.getPermanentElementById(permanentElement.id);
871
+ if (newElement) {
872
+ const placeholder = createPlaceholderForPermanentElement(permanentElement);
873
+ replaceElementWithElement(permanentElement, placeholder.element);
874
+ replaceElementWithElement(newElement, permanentElement);
875
+ return [...placeholders, placeholder];
876
+ }
877
+ else {
878
+ return placeholders;
879
+ }
880
+ }, []);
881
+ }
882
+ function elementIsFocusable(element) {
883
+ return element && typeof element.focus == "function";
884
+ }
885
+
886
+ class FrameRenderer extends Renderer {
887
+ get shouldRender() {
888
+ return true;
889
+ }
890
+ async render() {
891
+ await nextAnimationFrame();
892
+ this.preservingPermanentElements(() => {
893
+ this.loadFrameElement();
894
+ });
895
+ this.scrollFrameIntoView();
896
+ await nextAnimationFrame();
897
+ this.focusFirstAutofocusableElement();
898
+ }
899
+ loadFrameElement() {
900
+ var _a;
901
+ const destinationRange = document.createRange();
902
+ destinationRange.selectNodeContents(this.currentElement);
903
+ destinationRange.deleteContents();
904
+ const frameElement = this.newElement;
905
+ const sourceRange = (_a = frameElement.ownerDocument) === null || _a === void 0 ? void 0 : _a.createRange();
906
+ if (sourceRange) {
907
+ sourceRange.selectNodeContents(frameElement);
908
+ this.currentElement.appendChild(sourceRange.extractContents());
909
+ }
910
+ }
911
+ scrollFrameIntoView() {
912
+ if (this.currentElement.autoscroll || this.newElement.autoscroll) {
913
+ const element = this.currentElement.firstElementChild;
914
+ const block = readScrollLogicalPosition(this.currentElement.getAttribute("data-autoscroll-block"), "end");
915
+ if (element) {
916
+ element.scrollIntoView({ block });
917
+ return true;
918
+ }
919
+ }
920
+ return false;
921
+ }
922
+ }
923
+ function readScrollLogicalPosition(value, defaultValue) {
924
+ if (value == "end" || value == "start" || value == "center" || value == "nearest") {
925
+ return value;
926
+ }
927
+ else {
928
+ return defaultValue;
929
+ }
930
+ }
931
+
632
932
  class FrameController {
633
933
  constructor(element) {
634
934
  this.resolveVisitPromise = () => { };
635
935
  this.element = element;
936
+ this.view = new FrameView(this, this.element);
636
937
  this.appearanceObserver = new AppearanceObserver(this, this.element);
637
938
  this.linkInterceptor = new LinkInterceptor(this, this.element);
638
939
  this.formInterceptor = new FormInterceptor(this, this.element);
@@ -677,14 +978,18 @@ Copyright © 2021 Basecamp, LLC
677
978
  }
678
979
  }
679
980
  async loadResponse(response) {
680
- const fragment = fragmentFromHTML(await response.responseHTML);
681
- if (fragment) {
682
- const element = await this.extractForeignFrameElement(fragment);
683
- await nextAnimationFrame();
684
- this.loadFrameElement(element);
685
- this.scrollFrameIntoView(element);
686
- await nextAnimationFrame();
687
- this.focusFirstAutofocusableElement();
981
+ try {
982
+ const html = await response.responseHTML;
983
+ if (html) {
984
+ const { body } = parseHTMLDocument(html);
985
+ const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
986
+ const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
987
+ await this.view.render(renderer);
988
+ }
989
+ }
990
+ catch (error) {
991
+ console.error(error);
992
+ this.view.invalidate();
688
993
  }
689
994
  }
690
995
  elementAppearedInViewport(element) {
@@ -705,14 +1010,14 @@ Copyright © 2021 Basecamp, LLC
705
1010
  }
706
1011
  this.formSubmission = new FormSubmission(this, element, submitter);
707
1012
  if (this.formSubmission.fetchRequest.isIdempotent) {
708
- this.navigateFrame(element, this.formSubmission.fetchRequest.url);
1013
+ this.navigateFrame(element, this.formSubmission.fetchRequest.url.href);
709
1014
  }
710
1015
  else {
711
1016
  this.formSubmission.start();
712
1017
  }
713
1018
  }
714
- additionalHeadersForRequest(request) {
715
- return { "Turbo-Frame": this.id };
1019
+ prepareHeadersForRequest(headers, request) {
1020
+ headers["Turbo-Frame"] = this.id;
716
1021
  }
717
1022
  requestStarted(request) {
718
1023
  this.element.setAttribute("busy", "");
@@ -748,9 +1053,14 @@ Copyright © 2021 Basecamp, LLC
748
1053
  }
749
1054
  formSubmissionFinished(formSubmission) {
750
1055
  }
1056
+ viewWillRenderSnapshot(snapshot, isPreview) {
1057
+ }
1058
+ viewRenderedSnapshot(snapshot, isPreview) {
1059
+ }
1060
+ viewInvalidated() {
1061
+ }
751
1062
  async visit(url) {
752
- const location = Location.wrap(url);
753
- const request = new FetchRequest(this, FetchMethod.get, location);
1063
+ const request = new FetchRequest(this, FetchMethod.get, expandURL(url));
754
1064
  return new Promise(resolve => {
755
1065
  this.resolveVisitPromise = () => {
756
1066
  this.resolveVisitPromise = () => { };
@@ -765,7 +1075,7 @@ Copyright © 2021 Basecamp, LLC
765
1075
  }
766
1076
  findFrameElement(element) {
767
1077
  var _a;
768
- const id = element.getAttribute("data-turbo-frame");
1078
+ const id = element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
769
1079
  return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
770
1080
  }
771
1081
  async extractForeignFrameElement(container) {
@@ -781,36 +1091,6 @@ Copyright © 2021 Basecamp, LLC
781
1091
  console.error(`Response has no matching <turbo-frame id="${id}"> element`);
782
1092
  return new FrameElement();
783
1093
  }
784
- loadFrameElement(frameElement) {
785
- var _a;
786
- const destinationRange = document.createRange();
787
- destinationRange.selectNodeContents(this.element);
788
- destinationRange.deleteContents();
789
- const sourceRange = (_a = frameElement.ownerDocument) === null || _a === void 0 ? void 0 : _a.createRange();
790
- if (sourceRange) {
791
- sourceRange.selectNodeContents(frameElement);
792
- this.element.appendChild(sourceRange.extractContents());
793
- }
794
- }
795
- focusFirstAutofocusableElement() {
796
- const element = this.firstAutofocusableElement;
797
- if (element) {
798
- element.focus();
799
- return true;
800
- }
801
- return false;
802
- }
803
- scrollFrameIntoView(frame) {
804
- if (this.element.autoscroll || frame.autoscroll) {
805
- const element = this.element.firstElementChild;
806
- const block = readScrollLogicalPosition(this.element.getAttribute("data-autoscroll-block"), "end");
807
- if (element) {
808
- element.scrollIntoView({ block });
809
- return true;
810
- }
811
- }
812
- return false;
813
- }
814
1094
  shouldInterceptNavigation(element) {
815
1095
  const id = element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
816
1096
  if (!this.enabled || id == "_top") {
@@ -824,10 +1104,6 @@ Copyright © 2021 Basecamp, LLC
824
1104
  }
825
1105
  return true;
826
1106
  }
827
- get firstAutofocusableElement() {
828
- const element = this.element.querySelector("[autofocus]");
829
- return element instanceof HTMLElement ? element : null;
830
- }
831
1107
  get id() {
832
1108
  return this.element.id;
833
1109
  }
@@ -855,20 +1131,6 @@ Copyright © 2021 Basecamp, LLC
855
1131
  }
856
1132
  }
857
1133
  }
858
- function readScrollLogicalPosition(value, defaultValue) {
859
- if (value == "end" || value == "start" || value == "center" || value == "nearest") {
860
- return value;
861
- }
862
- else {
863
- return defaultValue;
864
- }
865
- }
866
- function fragmentFromHTML(html) {
867
- if (html) {
868
- const foreignDocument = document.implementation.createHTMLDocument();
869
- return foreignDocument.createRange().createContextualFragment(html);
870
- }
871
- }
872
1134
  function activateElement(element) {
873
1135
  if (element && element.ownerDocument !== document) {
874
1136
  element = document.importNode(element, true);
@@ -1098,9 +1360,10 @@ Copyright © 2021 Basecamp, LLC
1098
1360
  }
1099
1361
  ProgressBar.animationDuration = 300;
1100
1362
 
1101
- class HeadDetails {
1102
- constructor(children) {
1103
- this.detailsByOuterHTML = children.reduce((result, element) => {
1363
+ class HeadSnapshot extends Snapshot {
1364
+ constructor() {
1365
+ super(...arguments);
1366
+ this.detailsByOuterHTML = this.children.reduce((result, element) => {
1104
1367
  const { outerHTML } = element;
1105
1368
  const details = outerHTML in result
1106
1369
  ? result[outerHTML]
@@ -1112,29 +1375,25 @@ Copyright © 2021 Basecamp, LLC
1112
1375
  return Object.assign(Object.assign({}, result), { [outerHTML]: Object.assign(Object.assign({}, details), { elements: [...details.elements, element] }) });
1113
1376
  }, {});
1114
1377
  }
1115
- static fromHeadElement(headElement) {
1116
- const children = headElement ? [...headElement.children] : [];
1117
- return new this(children);
1118
- }
1119
- getTrackedElementSignature() {
1378
+ get trackedElementSignature() {
1120
1379
  return Object.keys(this.detailsByOuterHTML)
1121
1380
  .filter(outerHTML => this.detailsByOuterHTML[outerHTML].tracked)
1122
1381
  .join("");
1123
1382
  }
1124
- getScriptElementsNotInDetails(headDetails) {
1125
- return this.getElementsMatchingTypeNotInDetails("script", headDetails);
1383
+ getScriptElementsNotInSnapshot(snapshot) {
1384
+ return this.getElementsMatchingTypeNotInSnapshot("script", snapshot);
1126
1385
  }
1127
- getStylesheetElementsNotInDetails(headDetails) {
1128
- return this.getElementsMatchingTypeNotInDetails("stylesheet", headDetails);
1386
+ getStylesheetElementsNotInSnapshot(snapshot) {
1387
+ return this.getElementsMatchingTypeNotInSnapshot("stylesheet", snapshot);
1129
1388
  }
1130
- getElementsMatchingTypeNotInDetails(matchedType, headDetails) {
1389
+ getElementsMatchingTypeNotInSnapshot(matchedType, snapshot) {
1131
1390
  return Object.keys(this.detailsByOuterHTML)
1132
- .filter(outerHTML => !(outerHTML in headDetails.detailsByOuterHTML))
1391
+ .filter(outerHTML => !(outerHTML in snapshot.detailsByOuterHTML))
1133
1392
  .map(outerHTML => this.detailsByOuterHTML[outerHTML])
1134
1393
  .filter(({ type }) => type == matchedType)
1135
1394
  .map(({ elements: [element] }) => element);
1136
1395
  }
1137
- getProvisionalElements() {
1396
+ get provisionalElements() {
1138
1397
  return Object.keys(this.detailsByOuterHTML).reduce((result, outerHTML) => {
1139
1398
  const { type, tracked, elements } = this.detailsByOuterHTML[outerHTML];
1140
1399
  if (type == null && !tracked) {
@@ -1184,79 +1443,46 @@ Copyright © 2021 Basecamp, LLC
1184
1443
  const tagName = element.tagName.toLowerCase();
1185
1444
  return tagName == "meta" && element.getAttribute("name") == name;
1186
1445
  }
1187
-
1188
- class Snapshot {
1189
- constructor(headDetails, bodyElement) {
1190
- this.headDetails = headDetails;
1191
- this.bodyElement = bodyElement;
1192
- }
1193
- static wrap(value) {
1194
- if (value instanceof this) {
1195
- return value;
1196
- }
1197
- else if (typeof value == "string") {
1198
- return this.fromHTMLString(value);
1199
- }
1200
- else {
1201
- return this.fromHTMLElement(value);
1202
- }
1203
- }
1204
- static fromHTMLString(html) {
1205
- const { documentElement } = new DOMParser().parseFromString(html, "text/html");
1206
- return this.fromHTMLElement(documentElement);
1207
- }
1208
- static fromHTMLElement(htmlElement) {
1209
- const headElement = htmlElement.querySelector("head");
1210
- const bodyElement = htmlElement.querySelector("body") || document.createElement("body");
1211
- const headDetails = HeadDetails.fromHeadElement(headElement);
1212
- return new this(headDetails, bodyElement);
1213
- }
1214
- clone() {
1215
- const { bodyElement } = Snapshot.fromHTMLString(this.bodyElement.outerHTML);
1216
- return new Snapshot(this.headDetails, bodyElement);
1217
- }
1218
- getRootLocation() {
1219
- const root = this.getSetting("root", "/");
1220
- return new Location(root);
1446
+
1447
+ class PageSnapshot extends Snapshot {
1448
+ constructor(element, headSnapshot) {
1449
+ super(element);
1450
+ this.headSnapshot = headSnapshot;
1221
1451
  }
1222
- getCacheControlValue() {
1223
- return this.getSetting("cache-control");
1452
+ static fromHTMLString(html = "") {
1453
+ return this.fromDocument(parseHTMLDocument(html));
1224
1454
  }
1225
- getElementForAnchor(anchor) {
1226
- try {
1227
- return this.bodyElement.querySelector(`[id='${anchor}'], a[name='${anchor}']`);
1228
- }
1229
- catch (_a) {
1230
- return null;
1231
- }
1455
+ static fromElement(element) {
1456
+ return this.fromDocument(element.ownerDocument);
1232
1457
  }
1233
- getPermanentElements() {
1234
- return [...this.bodyElement.querySelectorAll("[id][data-turbo-permanent]")];
1458
+ static fromDocument({ head, body }) {
1459
+ return new this(body, new HeadSnapshot(head));
1235
1460
  }
1236
- getPermanentElementById(id) {
1237
- return this.bodyElement.querySelector(`#${id}[data-turbo-permanent]`);
1461
+ clone() {
1462
+ return new PageSnapshot(this.element.cloneNode(true), this.headSnapshot);
1238
1463
  }
1239
- getPermanentElementsPresentInSnapshot(snapshot) {
1240
- return this.getPermanentElements().filter(({ id }) => snapshot.getPermanentElementById(id));
1464
+ get headElement() {
1465
+ return this.headSnapshot.element;
1241
1466
  }
1242
- findFirstAutofocusableElement() {
1243
- return this.bodyElement.querySelector("[autofocus]");
1467
+ get rootLocation() {
1468
+ var _a;
1469
+ const root = (_a = this.getSetting("root")) !== null && _a !== void 0 ? _a : "/";
1470
+ return expandURL(root);
1244
1471
  }
1245
- hasAnchor(anchor) {
1246
- return this.getElementForAnchor(anchor) != null;
1472
+ get cacheControlValue() {
1473
+ return this.getSetting("cache-control");
1247
1474
  }
1248
- isPreviewable() {
1249
- return this.getCacheControlValue() != "no-preview";
1475
+ get isPreviewable() {
1476
+ return this.cacheControlValue != "no-preview";
1250
1477
  }
1251
- isCacheable() {
1252
- return this.getCacheControlValue() != "no-cache";
1478
+ get isCacheable() {
1479
+ return this.cacheControlValue != "no-cache";
1253
1480
  }
1254
- isVisitable() {
1481
+ get isVisitable() {
1255
1482
  return this.getSetting("visit-control") != "reload";
1256
1483
  }
1257
- getSetting(name, defaultValue) {
1258
- const value = this.headDetails.getMetaValue(`turbo-${name}`);
1259
- return value == null ? defaultValue : value;
1484
+ getSetting(name) {
1485
+ return this.headSnapshot.getMetaValue(`turbo-${name}`);
1260
1486
  }
1261
1487
  }
1262
1488
 
@@ -1294,17 +1520,6 @@ Copyright © 2021 Basecamp, LLC
1294
1520
  this.scrolled = false;
1295
1521
  this.snapshotCached = false;
1296
1522
  this.state = VisitState.initialized;
1297
- this.performScroll = () => {
1298
- if (!this.scrolled) {
1299
- if (this.action == "restore") {
1300
- this.scrollToRestoredPosition() || this.scrollToTop();
1301
- }
1302
- else {
1303
- this.scrollToAnchor() || this.scrollToTop();
1304
- }
1305
- this.scrolled = true;
1306
- }
1307
- };
1308
1523
  this.delegate = delegate;
1309
1524
  this.location = location;
1310
1525
  this.restorationIdentifier = restorationIdentifier || uuid();
@@ -1359,8 +1574,9 @@ Copyright © 2021 Basecamp, LLC
1359
1574
  }
1360
1575
  }
1361
1576
  changeHistory() {
1577
+ var _a;
1362
1578
  if (!this.historyChanged) {
1363
- const actionForHistory = this.location.isEqualTo(this.referrer) ? "replace" : this.action;
1579
+ const actionForHistory = this.location.href === ((_a = this.referrer) === null || _a === void 0 ? void 0 : _a.href) ? "replace" : this.action;
1364
1580
  const method = this.getHistoryMethodForAction(actionForHistory);
1365
1581
  this.history.update(method, this.location, this.restorationIdentifier);
1366
1582
  this.historyChanged = true;
@@ -1405,15 +1621,15 @@ Copyright © 2021 Basecamp, LLC
1405
1621
  loadResponse() {
1406
1622
  if (this.response) {
1407
1623
  const { statusCode, responseHTML } = this.response;
1408
- this.render(() => {
1624
+ this.render(async () => {
1409
1625
  this.cacheSnapshot();
1410
1626
  if (isSuccessful(statusCode) && responseHTML != null) {
1411
- this.view.render({ snapshot: Snapshot.fromHTMLString(responseHTML) }, this.performScroll);
1627
+ await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML));
1412
1628
  this.adapter.visitRendered(this);
1413
1629
  this.complete();
1414
1630
  }
1415
1631
  else {
1416
- this.view.render({ error: responseHTML }, this.performScroll);
1632
+ await this.view.renderError(PageSnapshot.fromHTMLString(responseHTML));
1417
1633
  this.adapter.visitRendered(this);
1418
1634
  this.fail();
1419
1635
  }
@@ -1422,15 +1638,15 @@ Copyright © 2021 Basecamp, LLC
1422
1638
  }
1423
1639
  getCachedSnapshot() {
1424
1640
  const snapshot = this.view.getCachedSnapshotForLocation(this.location) || this.getPreloadedSnapshot();
1425
- if (snapshot && (!this.location.anchor || snapshot.hasAnchor(this.location.anchor))) {
1426
- if (this.action == "restore" || snapshot.isPreviewable()) {
1641
+ if (snapshot && (!getAnchor(this.location) || snapshot.hasAnchor(getAnchor(this.location)))) {
1642
+ if (this.action == "restore" || snapshot.isPreviewable) {
1427
1643
  return snapshot;
1428
1644
  }
1429
1645
  }
1430
1646
  }
1431
1647
  getPreloadedSnapshot() {
1432
1648
  if (this.snapshotHTML) {
1433
- return Snapshot.wrap(this.snapshotHTML);
1649
+ return PageSnapshot.fromHTMLString(this.snapshotHTML);
1434
1650
  }
1435
1651
  }
1436
1652
  hasCachedSnapshot() {
@@ -1440,9 +1656,9 @@ Copyright © 2021 Basecamp, LLC
1440
1656
  const snapshot = this.getCachedSnapshot();
1441
1657
  if (snapshot) {
1442
1658
  const isPreview = this.shouldIssueRequest();
1443
- this.render(() => {
1659
+ this.render(async () => {
1444
1660
  this.cacheSnapshot();
1445
- this.view.render({ snapshot, isPreview }, this.performScroll);
1661
+ await this.view.renderPage(snapshot);
1446
1662
  this.adapter.visitRendered(this);
1447
1663
  if (!isPreview) {
1448
1664
  this.complete();
@@ -1487,6 +1703,17 @@ Copyright © 2021 Basecamp, LLC
1487
1703
  requestFinished() {
1488
1704
  this.finishRequest();
1489
1705
  }
1706
+ performScroll() {
1707
+ if (!this.scrolled) {
1708
+ if (this.action == "restore") {
1709
+ this.scrollToRestoredPosition() || this.scrollToTop();
1710
+ }
1711
+ else {
1712
+ this.scrollToAnchor() || this.scrollToTop();
1713
+ }
1714
+ this.scrolled = true;
1715
+ }
1716
+ }
1490
1717
  scrollToRestoredPosition() {
1491
1718
  const { scrollPosition } = this.restorationData;
1492
1719
  if (scrollPosition) {
@@ -1495,8 +1722,8 @@ Copyright © 2021 Basecamp, LLC
1495
1722
  }
1496
1723
  }
1497
1724
  scrollToAnchor() {
1498
- if (this.location.anchor != null) {
1499
- this.view.scrollToAnchor(this.location.anchor);
1725
+ if (getAnchor(this.location) != null) {
1726
+ this.view.scrollToAnchor(getAnchor(this.location));
1500
1727
  return true;
1501
1728
  }
1502
1729
  }
@@ -1530,12 +1757,14 @@ Copyright © 2021 Basecamp, LLC
1530
1757
  this.snapshotCached = true;
1531
1758
  }
1532
1759
  }
1533
- render(callback) {
1760
+ async render(callback) {
1534
1761
  this.cancelRender();
1535
- this.frame = requestAnimationFrame(() => {
1536
- delete this.frame;
1537
- callback.call(this);
1762
+ await new Promise(resolve => {
1763
+ this.frame = requestAnimationFrame(() => resolve());
1538
1764
  });
1765
+ callback();
1766
+ delete this.frame;
1767
+ this.performScroll();
1539
1768
  }
1540
1769
  cancelRender() {
1541
1770
  if (this.frame) {
@@ -1711,11 +1940,10 @@ Copyright © 2021 Basecamp, LLC
1711
1940
  if (this.shouldHandlePopState()) {
1712
1941
  const { turbo } = event.state || {};
1713
1942
  if (turbo) {
1714
- const location = Location.currentLocation;
1715
- this.location = location;
1943
+ this.location = new URL(window.location.href);
1716
1944
  const { restorationIdentifier } = turbo;
1717
1945
  this.restorationIdentifier = restorationIdentifier;
1718
- this.delegate.historyPoppedToLocationWithRestorationIdentifier(location, restorationIdentifier);
1946
+ this.delegate.historyPoppedToLocationWithRestorationIdentifier(this.location, restorationIdentifier);
1719
1947
  }
1720
1948
  }
1721
1949
  };
@@ -1730,7 +1958,7 @@ Copyright © 2021 Basecamp, LLC
1730
1958
  addEventListener("popstate", this.onPopState, false);
1731
1959
  addEventListener("load", this.onPageLoad, false);
1732
1960
  this.started = true;
1733
- this.replace(Location.currentLocation);
1961
+ this.replace(new URL(window.location.href));
1734
1962
  }
1735
1963
  }
1736
1964
  stop() {
@@ -1748,7 +1976,7 @@ Copyright © 2021 Basecamp, LLC
1748
1976
  }
1749
1977
  update(method, location, restorationIdentifier = uuid()) {
1750
1978
  const state = { turbo: { restorationIdentifier } };
1751
- method.call(history, state, "", location.absoluteURL);
1979
+ method.call(history, state, "", location.href);
1752
1980
  this.location = location;
1753
1981
  this.restorationIdentifier = restorationIdentifier;
1754
1982
  }
@@ -1829,7 +2057,7 @@ Copyright © 2021 Basecamp, LLC
1829
2057
  }
1830
2058
  }
1831
2059
  getLocationForLink(link) {
1832
- return new Location(link.getAttribute("href") || "");
2060
+ return expandURL(link.getAttribute("href") || "");
1833
2061
  }
1834
2062
  }
1835
2063
 
@@ -1842,15 +2070,20 @@ Copyright © 2021 Basecamp, LLC
1842
2070
  this.delegate.visitProposedToLocation(location, options);
1843
2071
  }
1844
2072
  }
1845
- startVisit(location, restorationIdentifier, options = {}) {
2073
+ startVisit(locatable, restorationIdentifier, options = {}) {
1846
2074
  this.stop();
1847
- this.currentVisit = new Visit(this, Location.wrap(location), restorationIdentifier, Object.assign({ referrer: this.location }, options));
2075
+ this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, Object.assign({ referrer: this.location }, options));
1848
2076
  this.currentVisit.start();
1849
2077
  }
1850
2078
  submitForm(form, submitter) {
1851
2079
  this.stop();
1852
2080
  this.formSubmission = new FormSubmission(this, form, submitter, true);
1853
- this.formSubmission.start();
2081
+ if (this.formSubmission.fetchRequest.isIdempotent) {
2082
+ this.proposeVisit(this.formSubmission.fetchRequest.url);
2083
+ }
2084
+ else {
2085
+ this.formSubmission.start();
2086
+ }
1854
2087
  }
1855
2088
  stop() {
1856
2089
  if (this.formSubmission) {
@@ -1889,8 +2122,8 @@ Copyright © 2021 Basecamp, LLC
1889
2122
  async formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
1890
2123
  const responseHTML = await fetchResponse.responseHTML;
1891
2124
  if (responseHTML) {
1892
- const snapshot = Snapshot.fromHTMLString(responseHTML);
1893
- this.view.render({ snapshot }, () => { });
2125
+ const snapshot = PageSnapshot.fromHTMLString(responseHTML);
2126
+ await this.view.renderPage(snapshot);
1894
2127
  this.view.clearSnapshotCache();
1895
2128
  }
1896
2129
  }
@@ -1998,53 +2231,10 @@ Copyright © 2021 Basecamp, LLC
1998
2231
  }
1999
2232
  }
2000
2233
 
2001
- class StreamMessage {
2002
- constructor(html) {
2003
- this.templateElement = document.createElement("template");
2004
- this.templateElement.innerHTML = html;
2005
- }
2006
- static wrap(message) {
2007
- if (typeof message == "string") {
2008
- return new this(message);
2009
- }
2010
- else {
2011
- return message;
2012
- }
2013
- }
2014
- get fragment() {
2015
- const fragment = document.createDocumentFragment();
2016
- for (const element of this.foreignElements) {
2017
- fragment.appendChild(document.importNode(element, true));
2018
- }
2019
- return fragment;
2020
- }
2021
- get foreignElements() {
2022
- return this.templateChildren.reduce((streamElements, child) => {
2023
- if (child.tagName.toLowerCase() == "turbo-stream") {
2024
- return [...streamElements, child];
2025
- }
2026
- else {
2027
- return streamElements;
2028
- }
2029
- }, []);
2030
- }
2031
- get templateChildren() {
2032
- return Array.from(this.templateElement.content.children);
2033
- }
2034
- }
2035
-
2036
2234
  class StreamObserver {
2037
2235
  constructor(delegate) {
2038
2236
  this.sources = new Set;
2039
2237
  this.started = false;
2040
- this.prepareFetchRequest = ((event) => {
2041
- var _a;
2042
- const fetchOptions = (_a = event.detail) === null || _a === void 0 ? void 0 : _a.fetchOptions;
2043
- if (fetchOptions) {
2044
- const { headers } = fetchOptions;
2045
- headers.Accept = ["text/vnd.turbo-stream.html", headers.Accept].join(", ");
2046
- }
2047
- });
2048
2238
  this.inspectFetchResponse = ((event) => {
2049
2239
  const response = fetchResponseFromEvent(event);
2050
2240
  if (response && fetchResponseIsStream(response)) {
@@ -2062,14 +2252,12 @@ Copyright © 2021 Basecamp, LLC
2062
2252
  start() {
2063
2253
  if (!this.started) {
2064
2254
  this.started = true;
2065
- addEventListener("turbo:before-fetch-request", this.prepareFetchRequest, true);
2066
2255
  addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
2067
2256
  }
2068
2257
  }
2069
2258
  stop() {
2070
2259
  if (this.started) {
2071
2260
  this.started = false;
2072
- removeEventListener("turbo:before-fetch-request", this.prepareFetchRequest, true);
2073
2261
  removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
2074
2262
  }
2075
2263
  }
@@ -2108,70 +2296,25 @@ Copyright © 2021 Basecamp, LLC
2108
2296
  function fetchResponseIsStream(response) {
2109
2297
  var _a;
2110
2298
  const contentType = (_a = response.contentType) !== null && _a !== void 0 ? _a : "";
2111
- return /^text\/vnd\.turbo-stream\.html\b/.test(contentType);
2299
+ return contentType.startsWith(StreamMessage.contentType);
2112
2300
  }
2113
2301
 
2114
2302
  function isAction(action) {
2115
2303
  return action == "advance" || action == "replace" || action == "restore";
2116
2304
  }
2117
2305
 
2118
- class Renderer {
2119
- renderView(callback) {
2120
- this.delegate.viewWillRender(this.newBody);
2121
- callback();
2122
- this.delegate.viewRendered(this.newBody);
2123
- }
2124
- invalidateView() {
2125
- this.delegate.viewInvalidated();
2126
- }
2127
- createScriptElement(element) {
2128
- if (element.getAttribute("data-turbo-eval") == "false") {
2129
- return element;
2130
- }
2131
- else {
2132
- const createdScriptElement = document.createElement("script");
2133
- createdScriptElement.textContent = element.textContent;
2134
- createdScriptElement.async = false;
2135
- copyElementAttributes(createdScriptElement, element);
2136
- return createdScriptElement;
2137
- }
2138
- }
2139
- }
2140
- function copyElementAttributes(destinationElement, sourceElement) {
2141
- for (const { name, value } of [...sourceElement.attributes]) {
2142
- destinationElement.setAttribute(name, value);
2143
- }
2144
- }
2145
-
2146
2306
  class ErrorRenderer extends Renderer {
2147
- constructor(delegate, html) {
2148
- super();
2149
- this.delegate = delegate;
2150
- this.htmlElement = (() => {
2151
- const htmlElement = document.createElement("html");
2152
- htmlElement.innerHTML = html;
2153
- return htmlElement;
2154
- })();
2155
- this.newHead = this.htmlElement.querySelector("head") || document.createElement("head");
2156
- this.newBody = this.htmlElement.querySelector("body") || document.createElement("body");
2157
- }
2158
- static render(delegate, callback, html) {
2159
- return new this(delegate, html).render(callback);
2160
- }
2161
- render(callback) {
2162
- this.renderView(() => {
2163
- this.replaceHeadAndBody();
2164
- this.activateBodyScriptElements();
2165
- callback();
2166
- });
2307
+ async render() {
2308
+ this.replaceHeadAndBody();
2309
+ this.activateScriptElements();
2167
2310
  }
2168
2311
  replaceHeadAndBody() {
2169
2312
  const { documentElement, head, body } = document;
2170
2313
  documentElement.replaceChild(this.newHead, head);
2171
- documentElement.replaceChild(this.newBody, body);
2314
+ documentElement.replaceChild(this.newElement, body);
2172
2315
  }
2173
- activateBodyScriptElements() {
2174
- for (const replaceableElement of this.getScriptElements()) {
2316
+ activateScriptElements() {
2317
+ for (const replaceableElement of this.scriptElements) {
2175
2318
  const parentNode = replaceableElement.parentNode;
2176
2319
  if (parentNode) {
2177
2320
  const element = this.createScriptElement(replaceableElement);
@@ -2179,84 +2322,38 @@ Copyright © 2021 Basecamp, LLC
2179
2322
  }
2180
2323
  }
2181
2324
  }
2182
- getScriptElements() {
2325
+ get newHead() {
2326
+ return this.newSnapshot.headSnapshot.element;
2327
+ }
2328
+ get scriptElements() {
2183
2329
  return [...document.documentElement.querySelectorAll("script")];
2184
2330
  }
2185
2331
  }
2186
2332
 
2187
- class SnapshotCache {
2188
- constructor(size) {
2189
- this.keys = [];
2190
- this.snapshots = {};
2191
- this.size = size;
2192
- }
2193
- has(location) {
2194
- return location.toCacheKey() in this.snapshots;
2195
- }
2196
- get(location) {
2197
- if (this.has(location)) {
2198
- const snapshot = this.read(location);
2199
- this.touch(location);
2200
- return snapshot;
2201
- }
2202
- }
2203
- put(location, snapshot) {
2204
- this.write(location, snapshot);
2205
- this.touch(location);
2206
- return snapshot;
2207
- }
2208
- clear() {
2209
- this.snapshots = {};
2210
- }
2211
- read(location) {
2212
- return this.snapshots[location.toCacheKey()];
2333
+ class PageRenderer extends Renderer {
2334
+ get shouldRender() {
2335
+ return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical;
2213
2336
  }
2214
- write(location, snapshot) {
2215
- this.snapshots[location.toCacheKey()] = snapshot;
2337
+ prepareToRender() {
2338
+ this.mergeHead();
2216
2339
  }
2217
- touch(location) {
2218
- const key = location.toCacheKey();
2219
- const index = this.keys.indexOf(key);
2220
- if (index > -1)
2221
- this.keys.splice(index, 1);
2222
- this.keys.unshift(key);
2223
- this.trim();
2340
+ async render() {
2341
+ this.replaceBody();
2224
2342
  }
2225
- trim() {
2226
- for (const key of this.keys.splice(this.size)) {
2227
- delete this.snapshots[key];
2343
+ finishRendering() {
2344
+ super.finishRendering();
2345
+ if (this.isPreview) {
2346
+ this.focusFirstAutofocusableElement();
2228
2347
  }
2229
2348
  }
2230
- }
2231
-
2232
- class SnapshotRenderer extends Renderer {
2233
- constructor(delegate, currentSnapshot, newSnapshot, isPreview) {
2234
- super();
2235
- this.delegate = delegate;
2236
- this.currentSnapshot = currentSnapshot;
2237
- this.currentHeadDetails = currentSnapshot.headDetails;
2238
- this.newSnapshot = newSnapshot;
2239
- this.newHeadDetails = newSnapshot.headDetails;
2240
- this.newBody = newSnapshot.bodyElement;
2241
- this.isPreview = isPreview;
2349
+ get currentHeadSnapshot() {
2350
+ return this.currentSnapshot.headSnapshot;
2242
2351
  }
2243
- static render(delegate, callback, currentSnapshot, newSnapshot, isPreview) {
2244
- return new this(delegate, currentSnapshot, newSnapshot, isPreview).render(callback);
2352
+ get newHeadSnapshot() {
2353
+ return this.newSnapshot.headSnapshot;
2245
2354
  }
2246
- render(callback) {
2247
- if (this.shouldRender()) {
2248
- this.mergeHead();
2249
- this.renderView(() => {
2250
- this.replaceBody();
2251
- if (!this.isPreview) {
2252
- this.focusFirstAutofocusableElement();
2253
- }
2254
- callback();
2255
- });
2256
- }
2257
- else {
2258
- this.invalidateView();
2259
- }
2355
+ get newElement() {
2356
+ return this.newSnapshot.element;
2260
2357
  }
2261
2358
  mergeHead() {
2262
2359
  this.copyNewHeadStylesheetElements();
@@ -2265,186 +2362,147 @@ Copyright © 2021 Basecamp, LLC
2265
2362
  this.copyNewHeadProvisionalElements();
2266
2363
  }
2267
2364
  replaceBody() {
2268
- const placeholders = this.relocateCurrentBodyPermanentElements();
2269
- this.activateNewBody();
2270
- this.assignNewBody();
2271
- this.replacePlaceholderElementsWithClonedPermanentElements(placeholders);
2272
- }
2273
- shouldRender() {
2274
- return this.newSnapshot.isVisitable() && this.trackedElementsAreIdentical();
2365
+ this.preservingPermanentElements(() => {
2366
+ this.activateNewBody();
2367
+ this.assignNewBody();
2368
+ });
2275
2369
  }
2276
- trackedElementsAreIdentical() {
2277
- return this.currentHeadDetails.getTrackedElementSignature() == this.newHeadDetails.getTrackedElementSignature();
2370
+ get trackedElementsAreIdentical() {
2371
+ return this.currentHeadSnapshot.trackedElementSignature == this.newHeadSnapshot.trackedElementSignature;
2278
2372
  }
2279
2373
  copyNewHeadStylesheetElements() {
2280
- for (const element of this.getNewHeadStylesheetElements()) {
2374
+ for (const element of this.newHeadStylesheetElements) {
2281
2375
  document.head.appendChild(element);
2282
2376
  }
2283
2377
  }
2284
2378
  copyNewHeadScriptElements() {
2285
- for (const element of this.getNewHeadScriptElements()) {
2379
+ for (const element of this.newHeadScriptElements) {
2286
2380
  document.head.appendChild(this.createScriptElement(element));
2287
2381
  }
2288
2382
  }
2289
2383
  removeCurrentHeadProvisionalElements() {
2290
- for (const element of this.getCurrentHeadProvisionalElements()) {
2384
+ for (const element of this.currentHeadProvisionalElements) {
2291
2385
  document.head.removeChild(element);
2292
2386
  }
2293
2387
  }
2294
2388
  copyNewHeadProvisionalElements() {
2295
- for (const element of this.getNewHeadProvisionalElements()) {
2389
+ for (const element of this.newHeadProvisionalElements) {
2296
2390
  document.head.appendChild(element);
2297
2391
  }
2298
2392
  }
2299
- relocateCurrentBodyPermanentElements() {
2300
- return this.getCurrentBodyPermanentElements().reduce((placeholders, permanentElement) => {
2301
- const newElement = this.newSnapshot.getPermanentElementById(permanentElement.id);
2302
- if (newElement) {
2303
- const placeholder = createPlaceholderForPermanentElement(permanentElement);
2304
- replaceElementWithElement(permanentElement, placeholder.element);
2305
- replaceElementWithElement(newElement, permanentElement);
2306
- return [...placeholders, placeholder];
2307
- }
2308
- else {
2309
- return placeholders;
2310
- }
2311
- }, []);
2312
- }
2313
- replacePlaceholderElementsWithClonedPermanentElements(placeholders) {
2314
- for (const { element, permanentElement } of placeholders) {
2315
- const clonedElement = permanentElement.cloneNode(true);
2316
- replaceElementWithElement(element, clonedElement);
2317
- }
2318
- }
2319
2393
  activateNewBody() {
2320
- document.adoptNode(this.newBody);
2394
+ document.adoptNode(this.newElement);
2321
2395
  this.activateNewBodyScriptElements();
2322
2396
  }
2323
2397
  activateNewBodyScriptElements() {
2324
- for (const inertScriptElement of this.getNewBodyScriptElements()) {
2398
+ for (const inertScriptElement of this.newBodyScriptElements) {
2325
2399
  const activatedScriptElement = this.createScriptElement(inertScriptElement);
2326
2400
  replaceElementWithElement(inertScriptElement, activatedScriptElement);
2327
2401
  }
2328
2402
  }
2329
2403
  assignNewBody() {
2330
- if (document.body) {
2331
- replaceElementWithElement(document.body, this.newBody);
2404
+ if (document.body && this.newElement instanceof HTMLBodyElement) {
2405
+ replaceElementWithElement(document.body, this.newElement);
2332
2406
  }
2333
2407
  else {
2334
- document.documentElement.appendChild(this.newBody);
2408
+ document.documentElement.appendChild(this.newElement);
2335
2409
  }
2336
2410
  }
2337
- focusFirstAutofocusableElement() {
2338
- const element = this.newSnapshot.findFirstAutofocusableElement();
2339
- if (elementIsFocusable(element)) {
2340
- element.focus();
2341
- }
2411
+ get newHeadStylesheetElements() {
2412
+ return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot);
2342
2413
  }
2343
- getNewHeadStylesheetElements() {
2344
- return this.newHeadDetails.getStylesheetElementsNotInDetails(this.currentHeadDetails);
2414
+ get newHeadScriptElements() {
2415
+ return this.newHeadSnapshot.getScriptElementsNotInSnapshot(this.currentHeadSnapshot);
2345
2416
  }
2346
- getNewHeadScriptElements() {
2347
- return this.newHeadDetails.getScriptElementsNotInDetails(this.currentHeadDetails);
2417
+ get currentHeadProvisionalElements() {
2418
+ return this.currentHeadSnapshot.provisionalElements;
2348
2419
  }
2349
- getCurrentHeadProvisionalElements() {
2350
- return this.currentHeadDetails.getProvisionalElements();
2420
+ get newHeadProvisionalElements() {
2421
+ return this.newHeadSnapshot.provisionalElements;
2351
2422
  }
2352
- getNewHeadProvisionalElements() {
2353
- return this.newHeadDetails.getProvisionalElements();
2423
+ get newBodyScriptElements() {
2424
+ return [...this.newElement.querySelectorAll("script")];
2354
2425
  }
2355
- getCurrentBodyPermanentElements() {
2356
- return this.currentSnapshot.getPermanentElementsPresentInSnapshot(this.newSnapshot);
2426
+ }
2427
+
2428
+ class SnapshotCache {
2429
+ constructor(size) {
2430
+ this.keys = [];
2431
+ this.snapshots = {};
2432
+ this.size = size;
2357
2433
  }
2358
- getNewBodyScriptElements() {
2359
- return [...this.newBody.querySelectorAll("script")];
2434
+ has(location) {
2435
+ return toCacheKey(location) in this.snapshots;
2360
2436
  }
2361
- }
2362
- function createPlaceholderForPermanentElement(permanentElement) {
2363
- const element = document.createElement("meta");
2364
- element.setAttribute("name", "turbo-permanent-placeholder");
2365
- element.setAttribute("content", permanentElement.id);
2366
- return { element, permanentElement };
2367
- }
2368
- function replaceElementWithElement(fromElement, toElement) {
2369
- const parentElement = fromElement.parentElement;
2370
- if (parentElement) {
2371
- return parentElement.replaceChild(toElement, fromElement);
2437
+ get(location) {
2438
+ if (this.has(location)) {
2439
+ const snapshot = this.read(location);
2440
+ this.touch(location);
2441
+ return snapshot;
2442
+ }
2443
+ }
2444
+ put(location, snapshot) {
2445
+ this.write(location, snapshot);
2446
+ this.touch(location);
2447
+ return snapshot;
2448
+ }
2449
+ clear() {
2450
+ this.snapshots = {};
2451
+ }
2452
+ read(location) {
2453
+ return this.snapshots[toCacheKey(location)];
2454
+ }
2455
+ write(location, snapshot) {
2456
+ this.snapshots[toCacheKey(location)] = snapshot;
2457
+ }
2458
+ touch(location) {
2459
+ const key = toCacheKey(location);
2460
+ const index = this.keys.indexOf(key);
2461
+ if (index > -1)
2462
+ this.keys.splice(index, 1);
2463
+ this.keys.unshift(key);
2464
+ this.trim();
2465
+ }
2466
+ trim() {
2467
+ for (const key of this.keys.splice(this.size)) {
2468
+ delete this.snapshots[key];
2469
+ }
2372
2470
  }
2373
- }
2374
- function elementIsFocusable(element) {
2375
- return element && typeof element.focus == "function";
2376
2471
  }
2377
2472
 
2378
- class View {
2379
- constructor(delegate) {
2380
- this.htmlElement = document.documentElement;
2473
+ class PageView extends View {
2474
+ constructor() {
2475
+ super(...arguments);
2381
2476
  this.snapshotCache = new SnapshotCache(10);
2382
- this.delegate = delegate;
2477
+ this.lastRenderedLocation = new URL(location.href);
2383
2478
  }
2384
- getRootLocation() {
2385
- return this.getSnapshot().getRootLocation();
2479
+ renderPage(snapshot, isPreview = false) {
2480
+ const renderer = new PageRenderer(this.snapshot, snapshot, isPreview);
2481
+ return this.render(renderer);
2386
2482
  }
2387
- getElementForAnchor(anchor) {
2388
- return this.getSnapshot().getElementForAnchor(anchor);
2389
- }
2390
- getSnapshot() {
2391
- return Snapshot.fromHTMLElement(this.htmlElement);
2483
+ renderError(snapshot) {
2484
+ const renderer = new ErrorRenderer(this.snapshot, snapshot, false);
2485
+ this.render(renderer);
2392
2486
  }
2393
2487
  clearSnapshotCache() {
2394
2488
  this.snapshotCache.clear();
2395
2489
  }
2396
- shouldCacheSnapshot() {
2397
- return this.getSnapshot().isCacheable();
2398
- }
2399
2490
  async cacheSnapshot() {
2400
- if (this.shouldCacheSnapshot()) {
2491
+ if (this.shouldCacheSnapshot) {
2401
2492
  this.delegate.viewWillCacheSnapshot();
2402
- const snapshot = this.getSnapshot();
2403
- const location = this.lastRenderedLocation || Location.currentLocation;
2404
- await nextMicrotask();
2493
+ const { snapshot, lastRenderedLocation: location } = this;
2494
+ await nextEventLoopTick();
2405
2495
  this.snapshotCache.put(location, snapshot.clone());
2406
2496
  }
2407
2497
  }
2408
2498
  getCachedSnapshotForLocation(location) {
2409
2499
  return this.snapshotCache.get(location);
2410
2500
  }
2411
- render({ snapshot, error, isPreview }, callback) {
2412
- this.markAsPreview(isPreview);
2413
- if (snapshot) {
2414
- this.renderSnapshot(snapshot, isPreview, callback);
2415
- }
2416
- else {
2417
- this.renderError(error, callback);
2418
- }
2419
- }
2420
- scrollToAnchor(anchor) {
2421
- const element = this.getElementForAnchor(anchor);
2422
- if (element) {
2423
- this.scrollToElement(element);
2424
- }
2425
- else {
2426
- this.scrollToPosition({ x: 0, y: 0 });
2427
- }
2428
- }
2429
- scrollToElement(element) {
2430
- element.scrollIntoView();
2431
- }
2432
- scrollToPosition({ x, y }) {
2433
- window.scrollTo(x, y);
2434
- }
2435
- markAsPreview(isPreview) {
2436
- if (isPreview) {
2437
- this.htmlElement.setAttribute("data-turbo-preview", "");
2438
- }
2439
- else {
2440
- this.htmlElement.removeAttribute("data-turbo-preview");
2441
- }
2442
- }
2443
- renderSnapshot(snapshot, isPreview, callback) {
2444
- SnapshotRenderer.render(this.delegate, callback, this.getSnapshot(), snapshot, isPreview || false);
2501
+ get snapshot() {
2502
+ return PageSnapshot.fromElement(this.element);
2445
2503
  }
2446
- renderError(error, callback) {
2447
- ErrorRenderer.render(this.delegate, callback, error || "");
2504
+ get shouldCacheSnapshot() {
2505
+ return this.snapshot.isCacheable;
2448
2506
  }
2449
2507
  }
2450
2508
 
@@ -2452,7 +2510,7 @@ Copyright © 2021 Basecamp, LLC
2452
2510
  constructor() {
2453
2511
  this.navigator = new Navigator(this);
2454
2512
  this.history = new History(this);
2455
- this.view = new View(this);
2513
+ this.view = new PageView(this, document.documentElement);
2456
2514
  this.adapter = new BrowserAdapter(this);
2457
2515
  this.pageObserver = new PageObserver(this);
2458
2516
  this.linkClickObserver = new LinkClickObserver(this);
@@ -2496,7 +2554,7 @@ Copyright © 2021 Basecamp, LLC
2496
2554
  this.adapter = adapter;
2497
2555
  }
2498
2556
  visit(location, options = {}) {
2499
- this.navigator.proposeVisit(Location.wrap(location), options);
2557
+ this.navigator.proposeVisit(expandURL(location), options);
2500
2558
  }
2501
2559
  connectStreamSource(source) {
2502
2560
  this.streamObserver.connectStreamSource(source);
@@ -2537,15 +2595,17 @@ Copyright © 2021 Basecamp, LLC
2537
2595
  }
2538
2596
  followedLinkToLocation(link, location) {
2539
2597
  const action = this.getActionForLink(link);
2540
- this.visit(location, { action });
2598
+ this.visit(location.href, { action });
2541
2599
  }
2542
2600
  allowsVisitingLocation(location) {
2543
2601
  return this.applicationAllowsVisitingLocation(location);
2544
2602
  }
2545
2603
  visitProposedToLocation(location, options) {
2604
+ extendURLWithDeprecatedProperties(location);
2546
2605
  this.adapter.visitProposedToLocation(location, options);
2547
2606
  }
2548
2607
  visitStarted(visit) {
2608
+ extendURLWithDeprecatedProperties(visit.location);
2549
2609
  this.notifyApplicationAfterVisitingLocation(visit.location);
2550
2610
  }
2551
2611
  visitCompleted(visit) {
@@ -2570,19 +2630,19 @@ Copyright © 2021 Basecamp, LLC
2570
2630
  receivedMessageFromStream(message) {
2571
2631
  this.renderStreamMessage(message);
2572
2632
  }
2573
- viewWillRender(newBody) {
2574
- this.notifyApplicationBeforeRender(newBody);
2633
+ viewWillCacheSnapshot() {
2634
+ this.notifyApplicationBeforeCachingSnapshot();
2575
2635
  }
2576
- viewRendered() {
2636
+ viewWillRenderSnapshot({ element }, isPreview) {
2637
+ this.notifyApplicationBeforeRender(element);
2638
+ }
2639
+ viewRenderedSnapshot(snapshot, isPreview) {
2577
2640
  this.view.lastRenderedLocation = this.history.location;
2578
2641
  this.notifyApplicationAfterRender();
2579
2642
  }
2580
2643
  viewInvalidated() {
2581
2644
  this.adapter.pageInvalidated();
2582
2645
  }
2583
- viewWillCacheSnapshot() {
2584
- this.notifyApplicationBeforeCachingSnapshot();
2585
- }
2586
2646
  applicationAllowsFollowingLinkToLocation(link, location) {
2587
2647
  const event = this.notifyApplicationAfterClickingLinkToLocation(link, location);
2588
2648
  return !event.defaultPrevented;
@@ -2592,13 +2652,13 @@ Copyright © 2021 Basecamp, LLC
2592
2652
  return !event.defaultPrevented;
2593
2653
  }
2594
2654
  notifyApplicationAfterClickingLinkToLocation(link, location) {
2595
- return dispatch("turbo:click", { target: link, detail: { url: location.absoluteURL }, cancelable: true });
2655
+ return dispatch("turbo:click", { target: link, detail: { url: location.href }, cancelable: true });
2596
2656
  }
2597
2657
  notifyApplicationBeforeVisitingLocation(location) {
2598
- return dispatch("turbo:before-visit", { detail: { url: location.absoluteURL }, cancelable: true });
2658
+ return dispatch("turbo:before-visit", { detail: { url: location.href }, cancelable: true });
2599
2659
  }
2600
2660
  notifyApplicationAfterVisitingLocation(location) {
2601
- return dispatch("turbo:visit", { detail: { url: location.absoluteURL } });
2661
+ return dispatch("turbo:visit", { detail: { url: location.href } });
2602
2662
  }
2603
2663
  notifyApplicationBeforeCachingSnapshot() {
2604
2664
  return dispatch("turbo:before-cache");
@@ -2610,7 +2670,7 @@ Copyright © 2021 Basecamp, LLC
2610
2670
  return dispatch("turbo:render");
2611
2671
  }
2612
2672
  notifyApplicationAfterPageLoad(timing = {}) {
2613
- return dispatch("turbo:load", { detail: { url: this.location.absoluteURL, timing } });
2673
+ return dispatch("turbo:load", { detail: { url: this.location.href, timing } });
2614
2674
  }
2615
2675
  getActionForLink(link) {
2616
2676
  const action = link.getAttribute("data-turbo-action");
@@ -2626,9 +2686,22 @@ Copyright © 2021 Basecamp, LLC
2626
2686
  }
2627
2687
  }
2628
2688
  locationIsVisitable(location) {
2629
- return location.isPrefixedBy(this.view.getRootLocation()) && location.isHTML();
2689
+ return isPrefixedBy(location, this.snapshot.rootLocation) && isHTML(location);
2630
2690
  }
2691
+ get snapshot() {
2692
+ return this.view.snapshot;
2693
+ }
2694
+ }
2695
+ function extendURLWithDeprecatedProperties(url) {
2696
+ Object.defineProperties(url, deprecatedLocationPropertyDescriptors);
2631
2697
  }
2698
+ const deprecatedLocationPropertyDescriptors = {
2699
+ absoluteURL: {
2700
+ get() {
2701
+ return this.toString();
2702
+ }
2703
+ }
2704
+ };
2632
2705
 
2633
2706
  const session = new Session;
2634
2707
  const { navigator } = session;