@hotwired/turbo 7.2.5 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/turbo.es2017-esm.js +135 -75
  2. package/dist/turbo.es2017-umd.js +135 -75
  3. package/dist/types/core/drive/form_submission.d.ts +8 -4
  4. package/dist/types/core/drive/head_snapshot.d.ts +3 -3
  5. package/dist/types/core/drive/history.d.ts +3 -3
  6. package/dist/types/core/drive/navigator.d.ts +1 -1
  7. package/dist/types/core/drive/page_view.d.ts +2 -2
  8. package/dist/types/core/drive/visit.d.ts +3 -3
  9. package/dist/types/core/errors.d.ts +2 -0
  10. package/dist/types/core/frames/frame_controller.d.ts +6 -2
  11. package/dist/types/core/frames/frame_view.d.ts +2 -2
  12. package/dist/types/core/index.d.ts +5 -4
  13. package/dist/types/core/native/browser_adapter.d.ts +1 -1
  14. package/dist/types/core/renderer.d.ts +1 -1
  15. package/dist/types/core/session.d.ts +12 -12
  16. package/dist/types/core/snapshot.d.ts +1 -1
  17. package/dist/types/core/streams/stream_actions.d.ts +2 -2
  18. package/dist/types/core/types.d.ts +3 -3
  19. package/dist/types/core/url.d.ts +1 -1
  20. package/dist/types/core/view.d.ts +1 -1
  21. package/dist/types/elements/frame_element.d.ts +1 -1
  22. package/dist/types/elements/stream_element.d.ts +2 -2
  23. package/dist/types/http/fetch_request.d.ts +7 -7
  24. package/dist/types/http/index.d.ts +1 -1
  25. package/dist/types/observers/cache_observer.d.ts +5 -1
  26. package/dist/types/observers/form_link_click_observer.d.ts +1 -1
  27. package/dist/types/polyfills/custom-elements-native-shim.d.ts +1 -0
  28. package/dist/types/tests/helpers/dom_test_case.d.ts +1 -2
  29. package/dist/types/tests/helpers/page.d.ts +10 -10
  30. package/dist/types/tests/unit/deprecated_adapter_support_tests.d.ts +1 -0
  31. package/dist/types/tests/unit/export_tests.d.ts +1 -5
  32. package/dist/types/tests/unit/stream_element_tests.d.ts +0 -10
  33. package/dist/types/util.d.ts +1 -1
  34. package/package.json +16 -10
  35. package/dist/types/tests/helpers/intern_test_case.d.ts +0 -19
  36. package/dist/types/tests/unit/deprecated_adapter_support_test.d.ts +0 -24
  37. package/dist/types/tests/unit/index.d.ts +0 -3
@@ -1,5 +1,5 @@
1
1
  /*
2
- Turbo 7.2.5
2
+ Turbo 7.3.0
3
3
  Copyright © 2023 37signals LLC
4
4
  */
5
5
  (function () {
@@ -87,16 +87,13 @@ function clickCaptured(event) {
87
87
  (function () {
88
88
  if ("submitter" in Event.prototype)
89
89
  return;
90
- let prototype;
90
+ let prototype = window.Event.prototype;
91
91
  if ("SubmitEvent" in window && /Apple Computer/.test(navigator.vendor)) {
92
92
  prototype = window.SubmitEvent.prototype;
93
93
  }
94
94
  else if ("SubmitEvent" in window) {
95
95
  return;
96
96
  }
97
- else {
98
- prototype = window.Event.prototype;
99
- }
100
97
  addEventListener("click", clickCaptured, true);
101
98
  Object.defineProperty(prototype, "submitter", {
102
99
  get() {
@@ -113,14 +110,14 @@ var FrameLoadingStyle;
113
110
  FrameLoadingStyle["lazy"] = "lazy";
114
111
  })(FrameLoadingStyle || (FrameLoadingStyle = {}));
115
112
  class FrameElement extends HTMLElement {
113
+ static get observedAttributes() {
114
+ return ["disabled", "complete", "loading", "src"];
115
+ }
116
116
  constructor() {
117
117
  super();
118
118
  this.loaded = Promise.resolve();
119
119
  this.delegate = new FrameElement.delegateConstructor(this);
120
120
  }
121
- static get observedAttributes() {
122
- return ["disabled", "complete", "loading", "src"];
123
- }
124
121
  connectedCallback() {
125
122
  this.delegate.connect();
126
123
  }
@@ -560,7 +557,7 @@ class FetchRequest {
560
557
  credentials: "same-origin",
561
558
  headers: this.headers,
562
559
  redirect: "follow",
563
- body: this.isIdempotent ? null : this.body,
560
+ body: this.isSafe ? null : this.body,
564
561
  signal: this.abortSignal,
565
562
  referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href,
566
563
  };
@@ -570,8 +567,8 @@ class FetchRequest {
570
567
  Accept: "text/html, application/xhtml+xml",
571
568
  };
572
569
  }
573
- get isIdempotent() {
574
- return this.method == FetchMethod.get;
570
+ get isSafe() {
571
+ return this.method === FetchMethod.get;
575
572
  }
576
573
  get abortSignal() {
577
574
  return this.abortController.signal;
@@ -631,9 +628,6 @@ class AppearanceObserver {
631
628
  }
632
629
 
633
630
  class StreamMessage {
634
- constructor(fragment) {
635
- this.fragment = importStreamElements(fragment);
636
- }
637
631
  static wrap(message) {
638
632
  if (typeof message == "string") {
639
633
  return new this(createDocumentFragment(message));
@@ -642,6 +636,9 @@ class StreamMessage {
642
636
  return message;
643
637
  }
644
638
  }
639
+ constructor(fragment) {
640
+ this.fragment = importStreamElements(fragment);
641
+ }
645
642
  }
646
643
  StreamMessage.contentType = "text/vnd.turbo-stream.html";
647
644
  function importStreamElements(fragment) {
@@ -681,6 +678,9 @@ function formEnctypeFromString(encoding) {
681
678
  }
682
679
  }
683
680
  class FormSubmission {
681
+ static confirmMethod(message, _element, _submitter) {
682
+ return Promise.resolve(confirm(message));
683
+ }
684
684
  constructor(delegate, formElement, submitter, mustRedirect = false) {
685
685
  this.state = FormSubmissionState.initialized;
686
686
  this.delegate = delegate;
@@ -694,9 +694,6 @@ class FormSubmission {
694
694
  this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
695
695
  this.mustRedirect = mustRedirect;
696
696
  }
697
- static confirmMethod(message, _element, _submitter) {
698
- return Promise.resolve(confirm(message));
699
- }
700
697
  get method() {
701
698
  var _a;
702
699
  const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
@@ -724,8 +721,8 @@ class FormSubmission {
724
721
  var _a;
725
722
  return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formenctype")) || this.formElement.enctype);
726
723
  }
727
- get isIdempotent() {
728
- return this.fetchRequest.isIdempotent;
724
+ get isSafe() {
725
+ return this.fetchRequest.isSafe;
729
726
  }
730
727
  get stringFormData() {
731
728
  return [...this.formData].reduce((entries, [name, value]) => {
@@ -755,7 +752,7 @@ class FormSubmission {
755
752
  }
756
753
  }
757
754
  prepareRequest(request) {
758
- if (!request.isIdempotent) {
755
+ if (!request.isSafe) {
759
756
  const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token");
760
757
  if (token) {
761
758
  request.headers["X-CSRF-Token"] = token;
@@ -769,6 +766,7 @@ class FormSubmission {
769
766
  var _a;
770
767
  this.state = FormSubmissionState.waiting;
771
768
  (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
769
+ this.setSubmitsWith();
772
770
  dispatch("turbo:submit-start", {
773
771
  target: this.formElement,
774
772
  detail: { formSubmission: this },
@@ -804,17 +802,46 @@ class FormSubmission {
804
802
  var _a;
805
803
  this.state = FormSubmissionState.stopped;
806
804
  (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
805
+ this.resetSubmitterText();
807
806
  dispatch("turbo:submit-end", {
808
807
  target: this.formElement,
809
808
  detail: Object.assign({ formSubmission: this }, this.result),
810
809
  });
811
810
  this.delegate.formSubmissionFinished(this);
812
811
  }
812
+ setSubmitsWith() {
813
+ if (!this.submitter || !this.submitsWith)
814
+ return;
815
+ if (this.submitter.matches("button")) {
816
+ this.originalSubmitText = this.submitter.innerHTML;
817
+ this.submitter.innerHTML = this.submitsWith;
818
+ }
819
+ else if (this.submitter.matches("input")) {
820
+ const input = this.submitter;
821
+ this.originalSubmitText = input.value;
822
+ input.value = this.submitsWith;
823
+ }
824
+ }
825
+ resetSubmitterText() {
826
+ if (!this.submitter || !this.originalSubmitText)
827
+ return;
828
+ if (this.submitter.matches("button")) {
829
+ this.submitter.innerHTML = this.originalSubmitText;
830
+ }
831
+ else if (this.submitter.matches("input")) {
832
+ const input = this.submitter;
833
+ input.value = this.originalSubmitText;
834
+ }
835
+ }
813
836
  requestMustRedirect(request) {
814
- return !request.isIdempotent && this.mustRedirect;
837
+ return !request.isSafe && this.mustRedirect;
815
838
  }
816
839
  requestAcceptsTurboStreamResponse(request) {
817
- return !request.isIdempotent || hasAttribute("data-turbo-stream", this.submitter, this.formElement);
840
+ return !request.isSafe || hasAttribute("data-turbo-stream", this.submitter, this.formElement);
841
+ }
842
+ get submitsWith() {
843
+ var _a;
844
+ return (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("data-turbo-submits-with");
818
845
  }
819
846
  }
820
847
  function buildFormData(formElement, submitter) {
@@ -1054,8 +1081,8 @@ class View {
1054
1081
  }
1055
1082
 
1056
1083
  class FrameView extends View {
1057
- invalidate() {
1058
- this.element.innerHTML = "";
1084
+ missing() {
1085
+ this.element.innerHTML = `<strong class="turbo-frame-error">Content missing</strong>`;
1059
1086
  }
1060
1087
  get snapshot() {
1061
1088
  return new Snapshot(this.element);
@@ -1216,16 +1243,16 @@ class FormLinkClickObserver {
1216
1243
  }
1217
1244
 
1218
1245
  class Bardo {
1219
- constructor(delegate, permanentElementMap) {
1220
- this.delegate = delegate;
1221
- this.permanentElementMap = permanentElementMap;
1222
- }
1223
1246
  static async preservingPermanentElements(delegate, permanentElementMap, callback) {
1224
1247
  const bardo = new this(delegate, permanentElementMap);
1225
1248
  bardo.enter();
1226
1249
  await callback();
1227
1250
  bardo.leave();
1228
1251
  }
1252
+ constructor(delegate, permanentElementMap) {
1253
+ this.delegate = delegate;
1254
+ this.permanentElementMap = permanentElementMap;
1255
+ }
1229
1256
  enter() {
1230
1257
  for (const id in this.permanentElementMap) {
1231
1258
  const [currentPermanentElement, newPermanentElement] = this.permanentElementMap[id];
@@ -1332,10 +1359,6 @@ function elementIsFocusable(element) {
1332
1359
  }
1333
1360
 
1334
1361
  class FrameRenderer extends Renderer {
1335
- constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
1336
- super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender);
1337
- this.delegate = delegate;
1338
- }
1339
1362
  static renderElement(currentElement, newElement) {
1340
1363
  var _a;
1341
1364
  const destinationRange = document.createRange();
@@ -1348,6 +1371,10 @@ class FrameRenderer extends Renderer {
1348
1371
  currentElement.appendChild(sourceRange.extractContents());
1349
1372
  }
1350
1373
  }
1374
+ constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
1375
+ super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender);
1376
+ this.delegate = delegate;
1377
+ }
1351
1378
  get shouldRender() {
1352
1379
  return true;
1353
1380
  }
@@ -1406,18 +1433,6 @@ function readScrollBehavior(value, defaultValue) {
1406
1433
  }
1407
1434
 
1408
1435
  class ProgressBar {
1409
- constructor() {
1410
- this.hiding = false;
1411
- this.value = 0;
1412
- this.visible = false;
1413
- this.trickle = () => {
1414
- this.setValue(this.value + Math.random() / 100);
1415
- };
1416
- this.stylesheetElement = this.createStylesheetElement();
1417
- this.progressElement = this.createProgressElement();
1418
- this.installStylesheetElement();
1419
- this.setValue(0);
1420
- }
1421
1436
  static get defaultCSS() {
1422
1437
  return unindent `
1423
1438
  .turbo-progress-bar {
@@ -1435,6 +1450,18 @@ class ProgressBar {
1435
1450
  }
1436
1451
  `;
1437
1452
  }
1453
+ constructor() {
1454
+ this.hiding = false;
1455
+ this.value = 0;
1456
+ this.visible = false;
1457
+ this.trickle = () => {
1458
+ this.setValue(this.value + Math.random() / 100);
1459
+ };
1460
+ this.stylesheetElement = this.createStylesheetElement();
1461
+ this.progressElement = this.createProgressElement();
1462
+ this.installStylesheetElement();
1463
+ this.setValue(0);
1464
+ }
1438
1465
  show() {
1439
1466
  if (!this.visible) {
1440
1467
  this.visible = true;
@@ -1605,10 +1632,6 @@ function elementWithoutNonce(element) {
1605
1632
  }
1606
1633
 
1607
1634
  class PageSnapshot extends Snapshot {
1608
- constructor(element, headSnapshot) {
1609
- super(element);
1610
- this.headSnapshot = headSnapshot;
1611
- }
1612
1635
  static fromHTMLString(html = "") {
1613
1636
  return this.fromDocument(parseHTMLDocument(html));
1614
1637
  }
@@ -1618,6 +1641,10 @@ class PageSnapshot extends Snapshot {
1618
1641
  static fromDocument({ head, body }) {
1619
1642
  return new this(body, new HeadSnapshot(head));
1620
1643
  }
1644
+ constructor(element, headSnapshot) {
1645
+ super(element);
1646
+ this.headSnapshot = headSnapshot;
1647
+ }
1621
1648
  clone() {
1622
1649
  const clonedElement = this.element.cloneNode(true);
1623
1650
  const selectElements = this.element.querySelectorAll("select");
@@ -2117,10 +2144,11 @@ class BrowserAdapter {
2117
2144
 
2118
2145
  class CacheObserver {
2119
2146
  constructor() {
2147
+ this.selector = "[data-turbo-temporary]";
2148
+ this.deprecatedSelector = "[data-turbo-cache=false]";
2120
2149
  this.started = false;
2121
- this.removeStaleElements = ((_event) => {
2122
- const staleElements = [...document.querySelectorAll('[data-turbo-cache="false"]')];
2123
- for (const element of staleElements) {
2150
+ this.removeTemporaryElements = ((_event) => {
2151
+ for (const element of this.temporaryElements) {
2124
2152
  element.remove();
2125
2153
  }
2126
2154
  });
@@ -2128,15 +2156,25 @@ class CacheObserver {
2128
2156
  start() {
2129
2157
  if (!this.started) {
2130
2158
  this.started = true;
2131
- addEventListener("turbo:before-cache", this.removeStaleElements, false);
2159
+ addEventListener("turbo:before-cache", this.removeTemporaryElements, false);
2132
2160
  }
2133
2161
  }
2134
2162
  stop() {
2135
2163
  if (this.started) {
2136
2164
  this.started = false;
2137
- removeEventListener("turbo:before-cache", this.removeStaleElements, false);
2165
+ removeEventListener("turbo:before-cache", this.removeTemporaryElements, false);
2138
2166
  }
2139
2167
  }
2168
+ get temporaryElements() {
2169
+ return [...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation];
2170
+ }
2171
+ get temporaryElementsWithDeprecation() {
2172
+ const elements = document.querySelectorAll(this.deprecatedSelector);
2173
+ if (elements.length) {
2174
+ console.warn(`The ${this.deprecatedSelector} selector is deprecated and will be removed in a future version. Use ${this.selector} instead.`);
2175
+ }
2176
+ return [...elements];
2177
+ }
2140
2178
  }
2141
2179
 
2142
2180
  class FrameRedirector {
@@ -2335,7 +2373,7 @@ class Navigator {
2335
2373
  if (formSubmission == this.formSubmission) {
2336
2374
  const responseHTML = await fetchResponse.responseHTML;
2337
2375
  if (responseHTML) {
2338
- const shouldCacheSnapshot = formSubmission.method == FetchMethod.get;
2376
+ const shouldCacheSnapshot = formSubmission.isSafe;
2339
2377
  if (!shouldCacheSnapshot) {
2340
2378
  this.view.clearSnapshotCache();
2341
2379
  }
@@ -3295,6 +3333,9 @@ var Turbo = /*#__PURE__*/Object.freeze({
3295
3333
  StreamActions: StreamActions
3296
3334
  });
3297
3335
 
3336
+ class TurboFrameMissingError extends Error {
3337
+ }
3338
+
3298
3339
  class FrameController {
3299
3340
  constructor(element) {
3300
3341
  this.fetchResponseLoaded = (_fetchResponse) => { };
@@ -3395,30 +3436,16 @@ class FrameController {
3395
3436
  try {
3396
3437
  const html = await fetchResponse.responseHTML;
3397
3438
  if (html) {
3398
- const { body } = parseHTMLDocument(html);
3399
- const newFrameElement = await this.extractForeignFrameElement(body);
3400
- if (newFrameElement) {
3401
- const snapshot = new Snapshot(newFrameElement);
3402
- const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false);
3403
- if (this.view.renderPromise)
3404
- await this.view.renderPromise;
3405
- this.changeHistory();
3406
- await this.view.render(renderer);
3407
- this.complete = true;
3408
- session.frameRendered(fetchResponse, this.element);
3409
- session.frameLoaded(this.element);
3410
- this.fetchResponseLoaded(fetchResponse);
3439
+ const document = parseHTMLDocument(html);
3440
+ const pageSnapshot = PageSnapshot.fromDocument(document);
3441
+ if (pageSnapshot.isVisitable) {
3442
+ await this.loadFrameResponse(fetchResponse, document);
3411
3443
  }
3412
- else if (this.willHandleFrameMissingFromResponse(fetchResponse)) {
3413
- console.warn(`A matching frame for #${this.element.id} was missing from the response, transforming into full-page Visit.`);
3414
- this.visitResponse(fetchResponse.response);
3444
+ else {
3445
+ await this.handleUnvisitableFrameResponse(fetchResponse);
3415
3446
  }
3416
3447
  }
3417
3448
  }
3418
- catch (error) {
3419
- console.error(error);
3420
- this.view.invalidate();
3421
- }
3422
3449
  finally {
3423
3450
  this.fetchResponseLoaded = () => { };
3424
3451
  }
@@ -3471,7 +3498,6 @@ class FrameController {
3471
3498
  this.resolveVisitPromise();
3472
3499
  }
3473
3500
  async requestFailedWithResponse(request, response) {
3474
- console.error(response);
3475
3501
  await this.loadResponse(response);
3476
3502
  this.resolveVisitPromise();
3477
3503
  }
@@ -3489,9 +3515,13 @@ class FrameController {
3489
3515
  const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
3490
3516
  frame.delegate.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
3491
3517
  frame.delegate.loadResponse(response);
3518
+ if (!formSubmission.isSafe) {
3519
+ session.clearCache();
3520
+ }
3492
3521
  }
3493
3522
  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
3494
3523
  this.element.delegate.loadResponse(fetchResponse);
3524
+ session.clearCache();
3495
3525
  }
3496
3526
  formSubmissionErrored(formSubmission, error) {
3497
3527
  console.error(error);
@@ -3519,6 +3549,24 @@ class FrameController {
3519
3549
  willRenderFrame(currentElement, _newElement) {
3520
3550
  this.previousFrameElement = currentElement.cloneNode(true);
3521
3551
  }
3552
+ async loadFrameResponse(fetchResponse, document) {
3553
+ const newFrameElement = await this.extractForeignFrameElement(document.body);
3554
+ if (newFrameElement) {
3555
+ const snapshot = new Snapshot(newFrameElement);
3556
+ const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false);
3557
+ if (this.view.renderPromise)
3558
+ await this.view.renderPromise;
3559
+ this.changeHistory();
3560
+ await this.view.render(renderer);
3561
+ this.complete = true;
3562
+ session.frameRendered(fetchResponse, this.element);
3563
+ session.frameLoaded(this.element);
3564
+ this.fetchResponseLoaded(fetchResponse);
3565
+ }
3566
+ else if (this.willHandleFrameMissingFromResponse(fetchResponse)) {
3567
+ this.handleFrameMissingFromResponse(fetchResponse);
3568
+ }
3569
+ }
3522
3570
  async visit(url) {
3523
3571
  var _a;
3524
3572
  const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams(), this.element);
@@ -3571,6 +3619,10 @@ class FrameController {
3571
3619
  session.history.update(method, expandURL(this.element.src || ""), this.restorationIdentifier);
3572
3620
  }
3573
3621
  }
3622
+ async handleUnvisitableFrameResponse(fetchResponse) {
3623
+ console.warn(`The response (${fetchResponse.statusCode}) from <turbo-frame id="${this.element.id}"> is performing a full page visit due to turbo-visit-control.`);
3624
+ await this.visitResponse(fetchResponse.response);
3625
+ }
3574
3626
  willHandleFrameMissingFromResponse(fetchResponse) {
3575
3627
  this.element.setAttribute("complete", "");
3576
3628
  const response = fetchResponse.response;
@@ -3589,6 +3641,14 @@ class FrameController {
3589
3641
  });
3590
3642
  return !event.defaultPrevented;
3591
3643
  }
3644
+ handleFrameMissingFromResponse(fetchResponse) {
3645
+ this.view.missing();
3646
+ this.throwFrameMissingError(fetchResponse);
3647
+ }
3648
+ throwFrameMissingError(fetchResponse) {
3649
+ const message = `The response (${fetchResponse.statusCode}) did not contain the expected <turbo-frame id="${this.element.id}"> and will be ignored. To perform a full page visit instead, set turbo-visit-control to reload.`;
3650
+ throw new TurboFrameMissingError(message);
3651
+ }
3592
3652
  async visitResponse(response) {
3593
3653
  const wrapped = new FetchResponse(response);
3594
3654
  const responseHTML = await wrapped.responseHTML;