turbo-rails 1.3.3 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ecb315ac4b0462119b892a555ba5942ac1de84d8bdc565e31d0e166ac244a6c1
4
- data.tar.gz: a2e2033e73b26803e9b87962ac451b825257e1f052cb86cef013e0b8eb16bbcd
3
+ metadata.gz: 40ccdc123db34929af83c559e6bf6c65ca3027b5fd765546143f6093d34662a7
4
+ data.tar.gz: 7236691e0dbf00c61fb51039212c9316b7d0fc5d03370e939c5c318b56ab70c7
5
5
  SHA512:
6
- metadata.gz: d830701cc6b05ceb930ed1d873998849d870f7b25701ccc3d5cede6f5659eb107e175a46465963a23bf1239fd3aeb798f105155047324644446305e1fea34d71
7
- data.tar.gz: 0bb9370f532eb74a8230320c1aed5ac1d4e5f434873d017454ffe1c005daa124f413ffe1ee11df1cb49f325f85fcd417b9db6bd6909b234d87032d56dcb99284
6
+ metadata.gz: e7b6bdb711d3e712ac96bd52fc6f909d2ea5beaa2ba664d10e4678792afc58bb61ecce45ccb9b745e8f3706e5221917c754c2a461caff4be864e294050aba10d
7
+ data.tar.gz: a6d86dc5249f31161e4f7c3e31cfcda725b1173f0c63348c81f0cd62135688408b4b963540f80564d5eec1fb86f7601090e24c00ed5a55fa5e9c5277a18d75a4
data/Rakefile CHANGED
@@ -9,7 +9,20 @@ load "rails/tasks/statistics.rake"
9
9
  Rake::TestTask.new do |test|
10
10
  test.libs << "test"
11
11
  test.test_files = FileList["test/**/*_test.rb"]
12
- test.warning = false
13
12
  end
14
13
 
15
- task default: :test
14
+ task :test_prereq do
15
+ puts "Installing Ruby dependencies"
16
+ `bundle install`
17
+
18
+ puts "Installing JavaScript dependencies"
19
+ `yarn install`
20
+
21
+ puts "Building JavaScript"
22
+ `yarn build`
23
+
24
+ puts "Preparing test database"
25
+ `cd test/dummy; ./bin/rails db:test:prepare; cd ../..`
26
+ end
27
+
28
+ task default: [:test_prereq, :test]
@@ -56,13 +56,11 @@ function clickCaptured(event) {
56
56
 
57
57
  (function() {
58
58
  if ("submitter" in Event.prototype) return;
59
- let prototype;
59
+ let prototype = window.Event.prototype;
60
60
  if ("SubmitEvent" in window && /Apple Computer/.test(navigator.vendor)) {
61
61
  prototype = window.SubmitEvent.prototype;
62
62
  } else if ("SubmitEvent" in window) {
63
63
  return;
64
- } else {
65
- prototype = window.Event.prototype;
66
64
  }
67
65
  addEventListener("click", clickCaptured, true);
68
66
  Object.defineProperty(prototype, "submitter", {
@@ -82,14 +80,14 @@ var FrameLoadingStyle;
82
80
  })(FrameLoadingStyle || (FrameLoadingStyle = {}));
83
81
 
84
82
  class FrameElement extends HTMLElement {
83
+ static get observedAttributes() {
84
+ return [ "disabled", "complete", "loading", "src" ];
85
+ }
85
86
  constructor() {
86
87
  super();
87
88
  this.loaded = Promise.resolve();
88
89
  this.delegate = new FrameElement.delegateConstructor(this);
89
90
  }
90
- static get observedAttributes() {
91
- return [ "disabled", "complete", "loading", "src" ];
92
- }
93
91
  connectedCallback() {
94
92
  this.delegate.connect();
95
93
  }
@@ -560,7 +558,7 @@ class FetchRequest {
560
558
  credentials: "same-origin",
561
559
  headers: this.headers,
562
560
  redirect: "follow",
563
- body: this.isIdempotent ? null : this.body,
561
+ body: this.isSafe ? null : this.body,
564
562
  signal: this.abortSignal,
565
563
  referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
566
564
  };
@@ -570,8 +568,8 @@ class FetchRequest {
570
568
  Accept: "text/html, application/xhtml+xml"
571
569
  };
572
570
  }
573
- get isIdempotent() {
574
- return this.method == FetchMethod.get;
571
+ get isSafe() {
572
+ return this.method === FetchMethod.get;
575
573
  }
576
574
  get abortSignal() {
577
575
  return this.abortController.signal;
@@ -633,9 +631,6 @@ class AppearanceObserver {
633
631
  }
634
632
 
635
633
  class StreamMessage {
636
- constructor(fragment) {
637
- this.fragment = importStreamElements(fragment);
638
- }
639
634
  static wrap(message) {
640
635
  if (typeof message == "string") {
641
636
  return new this(createDocumentFragment(message));
@@ -643,6 +638,9 @@ class StreamMessage {
643
638
  return message;
644
639
  }
645
640
  }
641
+ constructor(fragment) {
642
+ this.fragment = importStreamElements(fragment);
643
+ }
646
644
  }
647
645
 
648
646
  StreamMessage.contentType = "text/vnd.turbo-stream.html";
@@ -691,6 +689,9 @@ function formEnctypeFromString(encoding) {
691
689
  }
692
690
 
693
691
  class FormSubmission {
692
+ static confirmMethod(message, _element, _submitter) {
693
+ return Promise.resolve(confirm(message));
694
+ }
694
695
  constructor(delegate, formElement, submitter, mustRedirect = false) {
695
696
  this.state = FormSubmissionState.initialized;
696
697
  this.delegate = delegate;
@@ -704,9 +705,6 @@ class FormSubmission {
704
705
  this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
705
706
  this.mustRedirect = mustRedirect;
706
707
  }
707
- static confirmMethod(message, _element, _submitter) {
708
- return Promise.resolve(confirm(message));
709
- }
710
708
  get method() {
711
709
  var _a;
712
710
  const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
@@ -732,8 +730,8 @@ class FormSubmission {
732
730
  var _a;
733
731
  return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formenctype")) || this.formElement.enctype);
734
732
  }
735
- get isIdempotent() {
736
- return this.fetchRequest.isIdempotent;
733
+ get isSafe() {
734
+ return this.fetchRequest.isSafe;
737
735
  }
738
736
  get stringFormData() {
739
737
  return [ ...this.formData ].reduce(((entries, [name, value]) => entries.concat(typeof value == "string" ? [ [ name, value ] ] : [])), []);
@@ -761,7 +759,7 @@ class FormSubmission {
761
759
  }
762
760
  }
763
761
  prepareRequest(request) {
764
- if (!request.isIdempotent) {
762
+ if (!request.isSafe) {
765
763
  const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token");
766
764
  if (token) {
767
765
  request.headers["X-CSRF-Token"] = token;
@@ -775,6 +773,7 @@ class FormSubmission {
775
773
  var _a;
776
774
  this.state = FormSubmissionState.waiting;
777
775
  (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
776
+ this.setSubmitsWith();
778
777
  dispatch("turbo:submit-start", {
779
778
  target: this.formElement,
780
779
  detail: {
@@ -822,6 +821,7 @@ class FormSubmission {
822
821
  var _a;
823
822
  this.state = FormSubmissionState.stopped;
824
823
  (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
824
+ this.resetSubmitterText();
825
825
  dispatch("turbo:submit-end", {
826
826
  target: this.formElement,
827
827
  detail: Object.assign({
@@ -830,11 +830,35 @@ class FormSubmission {
830
830
  });
831
831
  this.delegate.formSubmissionFinished(this);
832
832
  }
833
+ setSubmitsWith() {
834
+ if (!this.submitter || !this.submitsWith) return;
835
+ if (this.submitter.matches("button")) {
836
+ this.originalSubmitText = this.submitter.innerHTML;
837
+ this.submitter.innerHTML = this.submitsWith;
838
+ } else if (this.submitter.matches("input")) {
839
+ const input = this.submitter;
840
+ this.originalSubmitText = input.value;
841
+ input.value = this.submitsWith;
842
+ }
843
+ }
844
+ resetSubmitterText() {
845
+ if (!this.submitter || !this.originalSubmitText) return;
846
+ if (this.submitter.matches("button")) {
847
+ this.submitter.innerHTML = this.originalSubmitText;
848
+ } else if (this.submitter.matches("input")) {
849
+ const input = this.submitter;
850
+ input.value = this.originalSubmitText;
851
+ }
852
+ }
833
853
  requestMustRedirect(request) {
834
- return !request.isIdempotent && this.mustRedirect;
854
+ return !request.isSafe && this.mustRedirect;
835
855
  }
836
856
  requestAcceptsTurboStreamResponse(request) {
837
- return !request.isIdempotent || hasAttribute("data-turbo-stream", this.submitter, this.formElement);
857
+ return !request.isSafe || hasAttribute("data-turbo-stream", this.submitter, this.formElement);
858
+ }
859
+ get submitsWith() {
860
+ var _a;
861
+ return (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("data-turbo-submits-with");
838
862
  }
839
863
  }
840
864
 
@@ -1076,8 +1100,8 @@ class View {
1076
1100
  }
1077
1101
 
1078
1102
  class FrameView extends View {
1079
- invalidate() {
1080
- this.element.innerHTML = "";
1103
+ missing() {
1104
+ this.element.innerHTML = `<strong class="turbo-frame-error">Content missing</strong>`;
1081
1105
  }
1082
1106
  get snapshot() {
1083
1107
  return new Snapshot(this.element);
@@ -1232,16 +1256,16 @@ class FormLinkClickObserver {
1232
1256
  }
1233
1257
 
1234
1258
  class Bardo {
1235
- constructor(delegate, permanentElementMap) {
1236
- this.delegate = delegate;
1237
- this.permanentElementMap = permanentElementMap;
1238
- }
1239
1259
  static async preservingPermanentElements(delegate, permanentElementMap, callback) {
1240
1260
  const bardo = new this(delegate, permanentElementMap);
1241
1261
  bardo.enter();
1242
1262
  await callback();
1243
1263
  bardo.leave();
1244
1264
  }
1265
+ constructor(delegate, permanentElementMap) {
1266
+ this.delegate = delegate;
1267
+ this.permanentElementMap = permanentElementMap;
1268
+ }
1245
1269
  enter() {
1246
1270
  for (const id in this.permanentElementMap) {
1247
1271
  const [currentPermanentElement, newPermanentElement] = this.permanentElementMap[id];
@@ -1352,10 +1376,6 @@ function elementIsFocusable(element) {
1352
1376
  }
1353
1377
 
1354
1378
  class FrameRenderer extends Renderer {
1355
- constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
1356
- super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender);
1357
- this.delegate = delegate;
1358
- }
1359
1379
  static renderElement(currentElement, newElement) {
1360
1380
  var _a;
1361
1381
  const destinationRange = document.createRange();
@@ -1368,6 +1388,10 @@ class FrameRenderer extends Renderer {
1368
1388
  currentElement.appendChild(sourceRange.extractContents());
1369
1389
  }
1370
1390
  }
1391
+ constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
1392
+ super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender);
1393
+ this.delegate = delegate;
1394
+ }
1371
1395
  get shouldRender() {
1372
1396
  return true;
1373
1397
  }
@@ -1429,18 +1453,6 @@ function readScrollBehavior(value, defaultValue) {
1429
1453
  }
1430
1454
 
1431
1455
  class ProgressBar {
1432
- constructor() {
1433
- this.hiding = false;
1434
- this.value = 0;
1435
- this.visible = false;
1436
- this.trickle = () => {
1437
- this.setValue(this.value + Math.random() / 100);
1438
- };
1439
- this.stylesheetElement = this.createStylesheetElement();
1440
- this.progressElement = this.createProgressElement();
1441
- this.installStylesheetElement();
1442
- this.setValue(0);
1443
- }
1444
1456
  static get defaultCSS() {
1445
1457
  return unindent`
1446
1458
  .turbo-progress-bar {
@@ -1458,6 +1470,18 @@ class ProgressBar {
1458
1470
  }
1459
1471
  `;
1460
1472
  }
1473
+ constructor() {
1474
+ this.hiding = false;
1475
+ this.value = 0;
1476
+ this.visible = false;
1477
+ this.trickle = () => {
1478
+ this.setValue(this.value + Math.random() / 100);
1479
+ };
1480
+ this.stylesheetElement = this.createStylesheetElement();
1481
+ this.progressElement = this.createProgressElement();
1482
+ this.installStylesheetElement();
1483
+ this.setValue(0);
1484
+ }
1461
1485
  show() {
1462
1486
  if (!this.visible) {
1463
1487
  this.visible = true;
@@ -1626,10 +1650,6 @@ function elementWithoutNonce(element) {
1626
1650
  }
1627
1651
 
1628
1652
  class PageSnapshot extends Snapshot {
1629
- constructor(element, headSnapshot) {
1630
- super(element);
1631
- this.headSnapshot = headSnapshot;
1632
- }
1633
1653
  static fromHTMLString(html = "") {
1634
1654
  return this.fromDocument(parseHTMLDocument(html));
1635
1655
  }
@@ -1639,6 +1659,10 @@ class PageSnapshot extends Snapshot {
1639
1659
  static fromDocument({head: head, body: body}) {
1640
1660
  return new this(body, new HeadSnapshot(head));
1641
1661
  }
1662
+ constructor(element, headSnapshot) {
1663
+ super(element);
1664
+ this.headSnapshot = headSnapshot;
1665
+ }
1642
1666
  clone() {
1643
1667
  const clonedElement = this.element.cloneNode(true);
1644
1668
  const selectElements = this.element.querySelectorAll("select");
@@ -2143,10 +2167,11 @@ class BrowserAdapter {
2143
2167
 
2144
2168
  class CacheObserver {
2145
2169
  constructor() {
2170
+ this.selector = "[data-turbo-temporary]";
2171
+ this.deprecatedSelector = "[data-turbo-cache=false]";
2146
2172
  this.started = false;
2147
- this.removeStaleElements = _event => {
2148
- const staleElements = [ ...document.querySelectorAll('[data-turbo-cache="false"]') ];
2149
- for (const element of staleElements) {
2173
+ this.removeTemporaryElements = _event => {
2174
+ for (const element of this.temporaryElements) {
2150
2175
  element.remove();
2151
2176
  }
2152
2177
  };
@@ -2154,15 +2179,25 @@ class CacheObserver {
2154
2179
  start() {
2155
2180
  if (!this.started) {
2156
2181
  this.started = true;
2157
- addEventListener("turbo:before-cache", this.removeStaleElements, false);
2182
+ addEventListener("turbo:before-cache", this.removeTemporaryElements, false);
2158
2183
  }
2159
2184
  }
2160
2185
  stop() {
2161
2186
  if (this.started) {
2162
2187
  this.started = false;
2163
- removeEventListener("turbo:before-cache", this.removeStaleElements, false);
2188
+ removeEventListener("turbo:before-cache", this.removeTemporaryElements, false);
2164
2189
  }
2165
2190
  }
2191
+ get temporaryElements() {
2192
+ return [ ...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation ];
2193
+ }
2194
+ get temporaryElementsWithDeprecation() {
2195
+ const elements = document.querySelectorAll(this.deprecatedSelector);
2196
+ if (elements.length) {
2197
+ console.warn(`The ${this.deprecatedSelector} selector is deprecated and will be removed in a future version. Use ${this.selector} instead.`);
2198
+ }
2199
+ return [ ...elements ];
2200
+ }
2166
2201
  }
2167
2202
 
2168
2203
  class FrameRedirector {
@@ -2361,7 +2396,7 @@ class Navigator {
2361
2396
  if (formSubmission == this.formSubmission) {
2362
2397
  const responseHTML = await fetchResponse.responseHTML;
2363
2398
  if (responseHTML) {
2364
- const shouldCacheSnapshot = formSubmission.method == FetchMethod.get;
2399
+ const shouldCacheSnapshot = formSubmission.isSafe;
2365
2400
  if (!shouldCacheSnapshot) {
2366
2401
  this.view.clearSnapshotCache();
2367
2402
  }
@@ -3362,6 +3397,8 @@ var Turbo = Object.freeze({
3362
3397
  StreamActions: StreamActions
3363
3398
  });
3364
3399
 
3400
+ class TurboFrameMissingError extends Error {}
3401
+
3365
3402
  class FrameController {
3366
3403
  constructor(element) {
3367
3404
  this.fetchResponseLoaded = _fetchResponse => {};
@@ -3458,26 +3495,14 @@ class FrameController {
3458
3495
  try {
3459
3496
  const html = await fetchResponse.responseHTML;
3460
3497
  if (html) {
3461
- const {body: body} = parseHTMLDocument(html);
3462
- const newFrameElement = await this.extractForeignFrameElement(body);
3463
- if (newFrameElement) {
3464
- const snapshot = new Snapshot(newFrameElement);
3465
- const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false);
3466
- if (this.view.renderPromise) await this.view.renderPromise;
3467
- this.changeHistory();
3468
- await this.view.render(renderer);
3469
- this.complete = true;
3470
- session.frameRendered(fetchResponse, this.element);
3471
- session.frameLoaded(this.element);
3472
- this.fetchResponseLoaded(fetchResponse);
3473
- } else if (this.willHandleFrameMissingFromResponse(fetchResponse)) {
3474
- console.warn(`A matching frame for #${this.element.id} was missing from the response, transforming into full-page Visit.`);
3475
- this.visitResponse(fetchResponse.response);
3498
+ const document = parseHTMLDocument(html);
3499
+ const pageSnapshot = PageSnapshot.fromDocument(document);
3500
+ if (pageSnapshot.isVisitable) {
3501
+ await this.loadFrameResponse(fetchResponse, document);
3502
+ } else {
3503
+ await this.handleUnvisitableFrameResponse(fetchResponse);
3476
3504
  }
3477
3505
  }
3478
- } catch (error) {
3479
- console.error(error);
3480
- this.view.invalidate();
3481
3506
  } finally {
3482
3507
  this.fetchResponseLoaded = () => {};
3483
3508
  }
@@ -3529,7 +3554,6 @@ class FrameController {
3529
3554
  this.resolveVisitPromise();
3530
3555
  }
3531
3556
  async requestFailedWithResponse(request, response) {
3532
- console.error(response);
3533
3557
  await this.loadResponse(response);
3534
3558
  this.resolveVisitPromise();
3535
3559
  }
@@ -3547,9 +3571,13 @@ class FrameController {
3547
3571
  const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
3548
3572
  frame.delegate.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
3549
3573
  frame.delegate.loadResponse(response);
3574
+ if (!formSubmission.isSafe) {
3575
+ session.clearCache();
3576
+ }
3550
3577
  }
3551
3578
  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
3552
3579
  this.element.delegate.loadResponse(fetchResponse);
3580
+ session.clearCache();
3553
3581
  }
3554
3582
  formSubmissionErrored(formSubmission, error) {
3555
3583
  console.error(error);
@@ -3579,6 +3607,22 @@ class FrameController {
3579
3607
  willRenderFrame(currentElement, _newElement) {
3580
3608
  this.previousFrameElement = currentElement.cloneNode(true);
3581
3609
  }
3610
+ async loadFrameResponse(fetchResponse, document) {
3611
+ const newFrameElement = await this.extractForeignFrameElement(document.body);
3612
+ if (newFrameElement) {
3613
+ const snapshot = new Snapshot(newFrameElement);
3614
+ const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false);
3615
+ if (this.view.renderPromise) await this.view.renderPromise;
3616
+ this.changeHistory();
3617
+ await this.view.render(renderer);
3618
+ this.complete = true;
3619
+ session.frameRendered(fetchResponse, this.element);
3620
+ session.frameLoaded(this.element);
3621
+ this.fetchResponseLoaded(fetchResponse);
3622
+ } else if (this.willHandleFrameMissingFromResponse(fetchResponse)) {
3623
+ this.handleFrameMissingFromResponse(fetchResponse);
3624
+ }
3625
+ }
3582
3626
  async visit(url) {
3583
3627
  var _a;
3584
3628
  const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams, this.element);
@@ -3634,6 +3678,10 @@ class FrameController {
3634
3678
  session.history.update(method, expandURL(this.element.src || ""), this.restorationIdentifier);
3635
3679
  }
3636
3680
  }
3681
+ async handleUnvisitableFrameResponse(fetchResponse) {
3682
+ console.warn(`The response (${fetchResponse.statusCode}) from <turbo-frame id="${this.element.id}"> is performing a full page visit due to turbo-visit-control.`);
3683
+ await this.visitResponse(fetchResponse.response);
3684
+ }
3637
3685
  willHandleFrameMissingFromResponse(fetchResponse) {
3638
3686
  this.element.setAttribute("complete", "");
3639
3687
  const response = fetchResponse.response;
@@ -3654,6 +3702,14 @@ class FrameController {
3654
3702
  });
3655
3703
  return !event.defaultPrevented;
3656
3704
  }
3705
+ handleFrameMissingFromResponse(fetchResponse) {
3706
+ this.view.missing();
3707
+ this.throwFrameMissingError(fetchResponse);
3708
+ }
3709
+ throwFrameMissingError(fetchResponse) {
3710
+ const message = `The response (${fetchResponse.statusCode}) did not contain the expected <turbo-frame id="${this.element.id}"> and will be ignored. To perform a full page visit instead, set turbo-visit-control to reload.`;
3711
+ throw new TurboFrameMissingError(message);
3712
+ }
3657
3713
  async visitResponse(response) {
3658
3714
  const wrapped = new FetchResponse(response);
3659
3715
  const responseHTML = await wrapped.responseHTML;
@@ -4048,7 +4104,9 @@ class TurboCableStreamSourceElement extends HTMLElement {
4048
4104
  async connectedCallback() {
4049
4105
  connectStreamSource(this);
4050
4106
  this.subscription = await subscribeTo(this.channel, {
4051
- received: this.dispatchMessageEvent.bind(this)
4107
+ received: this.dispatchMessageEvent.bind(this),
4108
+ connected: this.subscriptionConnected.bind(this),
4109
+ disconnected: this.subscriptionDisconnected.bind(this)
4052
4110
  });
4053
4111
  }
4054
4112
  disconnectedCallback() {
@@ -4061,6 +4119,12 @@ class TurboCableStreamSourceElement extends HTMLElement {
4061
4119
  });
4062
4120
  return this.dispatchEvent(event);
4063
4121
  }
4122
+ subscriptionConnected() {
4123
+ this.setAttribute("connected", "");
4124
+ }
4125
+ subscriptionDisconnected() {
4126
+ this.removeAttribute("connected");
4127
+ }
4064
4128
  get channel() {
4065
4129
  const channel = this.getAttribute("channel");
4066
4130
  const signed_stream_name = this.getAttribute("signed-stream-name");