cable_ready 5.0.0.pre9 → 5.0.0.pre10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -1
  3. data/Gemfile.lock +119 -100
  4. data/README.md +2 -5
  5. data/app/assets/javascripts/cable_ready.js +457 -151
  6. data/app/assets/javascripts/cable_ready.min.js +1 -1
  7. data/app/assets/javascripts/cable_ready.min.js.map +1 -1
  8. data/app/assets/javascripts/cable_ready.umd.js +441 -165
  9. data/app/assets/javascripts/cable_ready.umd.min.js +1 -1
  10. data/app/assets/javascripts/cable_ready.umd.min.js.map +1 -1
  11. data/app/channels/cable_ready/stream.rb +7 -5
  12. data/app/helpers/cable_ready/view_helper.rb +58 -0
  13. data/app/jobs/cable_ready_broadcast_job.rb +9 -8
  14. data/app/models/concerns/cable_ready/updatable/collection_updatable_callbacks.rb +2 -0
  15. data/app/models/concerns/cable_ready/updatable/collections_registry.rb +2 -0
  16. data/app/models/concerns/cable_ready/updatable/model_updatable_callbacks.rb +5 -2
  17. data/app/models/concerns/cable_ready/updatable.rb +67 -19
  18. data/app/models/concerns/extend_has_many.rb +2 -0
  19. data/cable_ready.gemspec +4 -6
  20. data/lib/cable_ready/broadcaster.rb +2 -0
  21. data/lib/cable_ready/cable_car.rb +2 -0
  22. data/lib/cable_ready/channel.rb +12 -4
  23. data/lib/cable_ready/channels.rb +3 -1
  24. data/lib/cable_ready/config.rb +16 -2
  25. data/lib/cable_ready/engine.rb +19 -9
  26. data/lib/cable_ready/identifiable.rb +23 -5
  27. data/lib/cable_ready/importmap.rb +2 -0
  28. data/lib/cable_ready/installer.rb +224 -0
  29. data/lib/cable_ready/operation_builder.rb +1 -1
  30. data/lib/cable_ready/sanity_checker.rb +1 -31
  31. data/lib/cable_ready/version.rb +1 -1
  32. data/lib/cable_ready.rb +3 -8
  33. data/lib/cable_ready_helper.rb +13 -0
  34. data/lib/generators/cable_ready/channel_generator.rb +51 -12
  35. data/lib/generators/cable_ready/templates/config/initializers/cable_ready.rb +10 -6
  36. data/lib/install/action_cable.rb +144 -0
  37. data/lib/install/broadcaster.rb +109 -0
  38. data/lib/install/bundle.rb +54 -0
  39. data/lib/install/compression.rb +51 -0
  40. data/lib/install/config.rb +39 -0
  41. data/lib/install/development.rb +34 -0
  42. data/lib/install/esbuild.rb +101 -0
  43. data/lib/install/importmap.rb +96 -0
  44. data/lib/install/initializers.rb +15 -0
  45. data/lib/install/mrujs.rb +121 -0
  46. data/lib/install/npm_packages.rb +13 -0
  47. data/lib/install/shakapacker.rb +61 -0
  48. data/lib/install/spring.rb +54 -0
  49. data/lib/install/updatable.rb +34 -0
  50. data/lib/install/vite.rb +62 -0
  51. data/lib/install/webpacker.rb +81 -0
  52. data/lib/install/yarn.rb +56 -0
  53. data/lib/tasks/cable_ready/cable_ready.rake +249 -0
  54. data/package.json +28 -18
  55. data/{rollup.config.js → rollup.config.mjs} +9 -8
  56. data/web-test-runner.config.mjs +12 -0
  57. data/yarn.lock +2210 -404
  58. metadata +37 -161
  59. data/LATEST +0 -1
  60. data/app/helpers/cable_ready_helper.rb +0 -26
  61. data/lib/generators/cable_ready/helpers_generator.rb +0 -43
  62. data/lib/generators/cable_ready/initializer_generator.rb +0 -14
  63. data/test/dummy/app/channels/application_cable/channel.rb +0 -4
  64. data/test/dummy/app/channels/application_cable/connection.rb +0 -4
  65. data/test/dummy/app/controllers/application_controller.rb +0 -2
  66. data/test/dummy/app/helpers/application_helper.rb +0 -2
  67. data/test/dummy/app/jobs/application_job.rb +0 -7
  68. data/test/dummy/app/mailers/application_mailer.rb +0 -4
  69. data/test/dummy/app/models/application_record.rb +0 -3
  70. data/test/dummy/app/models/dugong.rb +0 -4
  71. data/test/dummy/app/models/global_idable_entity.rb +0 -16
  72. data/test/dummy/app/models/post.rb +0 -4
  73. data/test/dummy/app/models/section.rb +0 -6
  74. data/test/dummy/app/models/team.rb +0 -6
  75. data/test/dummy/app/models/topic.rb +0 -4
  76. data/test/dummy/app/models/user.rb +0 -7
  77. data/test/dummy/config/application.rb +0 -22
  78. data/test/dummy/config/boot.rb +0 -5
  79. data/test/dummy/config/environment.rb +0 -5
  80. data/test/dummy/config/environments/development.rb +0 -76
  81. data/test/dummy/config/environments/production.rb +0 -120
  82. data/test/dummy/config/environments/test.rb +0 -59
  83. data/test/dummy/config/initializers/application_controller_renderer.rb +0 -8
  84. data/test/dummy/config/initializers/assets.rb +0 -12
  85. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -8
  86. data/test/dummy/config/initializers/cable_ready.rb +0 -18
  87. data/test/dummy/config/initializers/content_security_policy.rb +0 -28
  88. data/test/dummy/config/initializers/cookies_serializer.rb +0 -5
  89. data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -6
  90. data/test/dummy/config/initializers/inflections.rb +0 -16
  91. data/test/dummy/config/initializers/mime_types.rb +0 -4
  92. data/test/dummy/config/initializers/permissions_policy.rb +0 -11
  93. data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
  94. data/test/dummy/config/puma.rb +0 -43
  95. data/test/dummy/config/routes.rb +0 -3
  96. data/test/dummy/db/migrate/20210902154139_create_users.rb +0 -9
  97. data/test/dummy/db/migrate/20210902154153_create_posts.rb +0 -10
  98. data/test/dummy/db/migrate/20210904081930_create_topics.rb +0 -9
  99. data/test/dummy/db/migrate/20210904093607_create_sections.rb +0 -9
  100. data/test/dummy/db/migrate/20210913191735_create_teams.rb +0 -8
  101. data/test/dummy/db/migrate/20210913191759_add_team_reference_to_users.rb +0 -5
  102. data/test/dummy/db/migrate/20220329222959_create_dugongs.rb +0 -8
  103. data/test/dummy/db/migrate/20220329230221_create_active_storage_tables.active_storage.rb +0 -36
  104. data/test/dummy/db/schema.rb +0 -84
  105. data/test/dummy/test/models/dugong_test.rb +0 -7
  106. data/test/dummy/test/models/post_test.rb +0 -7
  107. data/test/dummy/test/models/section_test.rb +0 -7
  108. data/test/dummy/test/models/team_test.rb +0 -7
  109. data/test/dummy/test/models/topic_test.rb +0 -7
  110. data/test/dummy/test/models/user_test.rb +0 -7
  111. data/test/lib/cable_ready/cable_car_test.rb +0 -50
  112. data/test/lib/cable_ready/compoundable_test.rb +0 -26
  113. data/test/lib/cable_ready/helper_test.rb +0 -25
  114. data/test/lib/cable_ready/identifiable_test.rb +0 -69
  115. data/test/lib/cable_ready/operation_builder_test.rb +0 -189
  116. data/test/lib/cable_ready/updatable_test.rb +0 -135
  117. data/test/lib/generators/cable_ready/channel_generator_test.rb +0 -157
  118. data/test/support/generator_test_helpers.rb +0 -28
  119. data/test/test_helper.rb +0 -18
@@ -2,30 +2,33 @@ import morphdom from "morphdom";
2
2
 
3
3
  var name = "cable_ready";
4
4
 
5
- var version = "5.0.0-pre9";
5
+ var version = "5.0.0-pre10";
6
6
 
7
7
  var description = "CableReady helps you create great real-time user experiences by making it simple to trigger client-side DOM changes from server-side Ruby.";
8
8
 
9
9
  var keywords = [ "ruby", "rails", "websockets", "actioncable", "cable", "ssr", "stimulus_reflex", "client-side", "dom" ];
10
10
 
11
- var homepage = "https://cableready.stimulusreflex.com/";
11
+ var homepage = "https://cableready.stimulusreflex.com";
12
12
 
13
- var bugs = {
14
- url: "https://github.com/stimulusreflex/cable_ready/issues"
15
- };
13
+ var bugs = "https://github.com/stimulusreflex/cable_ready/issues";
16
14
 
17
- var repository = {
18
- type: "git",
19
- url: "git+https://github.com:stimulusreflex/cable_ready.git"
20
- };
15
+ var repository = "https://github.com/stimulusreflex/cable_ready";
21
16
 
22
17
  var license = "MIT";
23
18
 
24
19
  var author = "Nathan Hopkins <natehop@gmail.com>";
25
20
 
26
- var main = "./dist/cable_ready.umd.min.js";
21
+ var contributors = [ "Andrew Mason <andrewmcodes@protonmail.com>", "Julian Rubisch <julian@julianrubisch.at>", "Marco Roth <marco.roth@intergga.ch>", "Nathan Hopkins <natehop@gmail.com>" ];
22
+
23
+ var main = "./dist/cable_ready.js";
24
+
25
+ var module = "./dist/cable_ready.js";
27
26
 
28
- var module = "./dist/cable_ready.min.js";
27
+ var browser = "./dist/cable_ready.js";
28
+
29
+ var unpkg = "./dist/cable_ready.umd.js";
30
+
31
+ var umd = "./dist/cable_ready.umd.js";
29
32
 
30
33
  var files = [ "dist/*", "javascript/*" ];
31
34
 
@@ -35,20 +38,25 @@ var scripts = {
35
38
  "prettier-standard:check": "yarn run prettier-standard --check ./javascript/**/*.js rollup.config.js",
36
39
  "prettier-standard:format": "yarn run prettier-standard ./javascript/**/*.js rollup.config.js",
37
40
  build: "yarn rollup -c",
38
- watch: "yarn rollup -wc"
41
+ watch: "yarn rollup -wc",
42
+ test: "web-test-runner javascript/test/**/*.test.js"
39
43
  };
40
44
 
41
45
  var dependencies = {
42
- morphdom: "^2.6.1"
46
+ morphdom: "2.6.1"
43
47
  };
44
48
 
45
49
  var devDependencies = {
46
- "@rollup/plugin-commonjs": "^21.0.3",
47
- "@rollup/plugin-json": "^4.1.0",
48
- "@rollup/plugin-node-resolve": "^13.1.3",
50
+ "@open-wc/testing": "^3.1.7",
51
+ "@rollup/plugin-json": "^6.0.0",
52
+ "@rollup/plugin-node-resolve": "^15.0.1",
53
+ "@rollup/plugin-terser": "^0.4.0",
54
+ "@web/dev-server-esbuild": "^0.3.3",
55
+ "@web/dev-server-rollup": "^0.3.21",
56
+ "@web/test-runner": "^0.15.0",
49
57
  "prettier-standard": "^16.4.1",
50
- rollup: "^2.70.1",
51
- "rollup-plugin-terser": "^7.0.2"
58
+ rollup: "^3.15.0",
59
+ sinon: "^15.0.1"
52
60
  };
53
61
 
54
62
  var packageInfo = {
@@ -61,8 +69,13 @@ var packageInfo = {
61
69
  repository: repository,
62
70
  license: license,
63
71
  author: author,
72
+ contributors: contributors,
64
73
  main: main,
65
74
  module: module,
75
+ browser: browser,
76
+ import: "./dist/cable_ready.js",
77
+ unpkg: unpkg,
78
+ umd: umd,
66
79
  files: files,
67
80
  scripts: scripts,
68
81
  dependencies: dependencies,
@@ -113,34 +126,90 @@ var ActiveElement = {
113
126
  }
114
127
  };
115
128
 
129
+ // Indicates if the passed element is considered a text input.
130
+
116
131
  const isTextInput = element => inputTags[element.tagName] && textInputTypes[element.type];
117
132
 
133
+ // Assigns focus to the appropriate element... preferring the explicitly passed selector
134
+
135
+ // * selector - a CSS selector for the element that should have focus
136
+
118
137
  const assignFocus = selector => {
119
138
  const element = selector && selector.nodeType === Node.ELEMENT_NODE ? selector : document.querySelector(selector);
120
139
  const focusElement = element || ActiveElement.element;
121
140
  if (focusElement && focusElement.focus) focusElement.focus();
122
141
  };
123
142
 
143
+ // Dispatches an event on the passed element
144
+
145
+ // * element - the element
146
+ // * name - the name of the event
147
+ // * detail - the event detail
148
+
124
149
  const dispatch = (element, name, detail = {}) => {
125
150
  const init = {
126
151
  bubbles: true,
127
152
  cancelable: true,
128
153
  detail: detail
129
154
  };
130
- const evt = new CustomEvent(name, init);
131
- element.dispatchEvent(evt);
155
+ const event = new CustomEvent(name, init);
156
+ element.dispatchEvent(event);
132
157
  if (window.jQuery) window.jQuery(element).trigger(name, detail);
133
158
  };
134
159
 
160
+ // Accepts an xPath query and returns the element found at that position in the DOM
161
+
135
162
  const xpathToElement = xpath => document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
136
163
 
137
- const getClassNames = names => Array(names).flat();
164
+ // Accepts an xPath query and returns all matching elements in the DOM
165
+
166
+ const xpathToElementArray = (xpath, reverse = false) => {
167
+ const snapshotList = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
168
+ const snapshots = [];
169
+ for (let i = 0; i < snapshotList.snapshotLength; i++) {
170
+ snapshots.push(snapshotList.snapshotItem(i));
171
+ }
172
+ return reverse ? snapshots.reverse() : snapshots;
173
+ };
174
+
175
+ // Return an array with the class names to be used
176
+
177
+ // * names - could be a string or an array of strings for multiple classes.
178
+
179
+ const getClassNames = names => Array.from(names).flat()
180
+ // Perform operation for either the first or all of the elements returned by CSS selector
181
+
182
+ // * operation - the instruction payload from perform
183
+ // * callback - the operation function to run for each element
184
+
185
+ ;
138
186
 
139
187
  const processElements = (operation, callback) => {
140
188
  Array.from(operation.selectAll ? operation.element : [ operation.element ]).forEach(callback);
141
189
  };
142
190
 
143
- const kebabize = str => str.split("").map(((letter, idx) => letter.toUpperCase() === letter ? `${idx !== 0 ? "-" : ""}${letter.toLowerCase()}` : letter)).join("");
191
+ // convert string to kebab-case
192
+ // most other implementations (lodash) are focused on camelCase to kebab-case
193
+ // instead, this uses word token boundaries to produce readable URL slugs and keys
194
+ // this implementation will not support Emoji or other non-ASCII characters
195
+
196
+ const kebabize = createCompounder((function(result, word, index) {
197
+ return result + (index ? "-" : "") + word.toLowerCase();
198
+ }));
199
+
200
+ function createCompounder(callback) {
201
+ return function(str) {
202
+ return words(str).reduce(callback, "");
203
+ };
204
+ }
205
+
206
+ const words = str => {
207
+ str = str == null ? "" : str;
208
+ return str.match(/([A-Z]{2,}|[0-9]+|[A-Z]?[a-z]+|[A-Z])/g) || [];
209
+ };
210
+
211
+ // Provide a standardized pipeline of checks and modifications to all operations based on provided options
212
+ // Currently skips execution if cancelled and implements an optional delay
144
213
 
145
214
  const operate = (operation, callback) => {
146
215
  if (!operation.cancel) {
@@ -150,15 +219,17 @@ const operate = (operation, callback) => {
150
219
  return false;
151
220
  };
152
221
 
222
+ // Dispatch life-cycle events with standardized naming
153
223
  const before = (target, operation) => dispatch(target, `cable-ready:before-${kebabize(operation.operation)}`, operation);
154
224
 
155
225
  const after = (target, operation) => dispatch(target, `cable-ready:after-${kebabize(operation.operation)}`, operation);
156
226
 
157
- function debounce(func, timeout) {
227
+ function debounce(fn, delay = 250) {
158
228
  let timer;
159
229
  return (...args) => {
160
- clearTimeout(timer);
161
- timer = setTimeout((() => func.apply(this, args)), timeout);
230
+ const callback = () => fn.apply(this, args);
231
+ if (timer) clearTimeout(timer);
232
+ timer = setTimeout(callback, delay);
162
233
  };
163
234
  }
164
235
 
@@ -167,6 +238,40 @@ function handleErrors(response) {
167
238
  return response;
168
239
  }
169
240
 
241
+ function safeScalar(val) {
242
+ if (val !== undefined && ![ "string", "number", "boolean" ].includes(typeof val)) console.warn(`Operation expects a string, number or boolean, but got ${val} (${typeof val})`);
243
+ return val != null ? val : "";
244
+ }
245
+
246
+ function safeString(str) {
247
+ if (str !== undefined && typeof str !== "string") console.warn(`Operation expects a string, but got ${str} (${typeof str})`);
248
+ return str != null ? String(str) : "";
249
+ }
250
+
251
+ function safeArray(arr) {
252
+ if (arr !== undefined && !Array.isArray(arr)) console.warn(`Operation expects an array, but got ${arr} (${typeof arr})`);
253
+ return arr != null ? Array.from(arr) : [];
254
+ }
255
+
256
+ function safeObject(obj) {
257
+ if (obj !== undefined && typeof obj !== "object") console.warn(`Operation expects an object, but got ${obj} (${typeof obj})`);
258
+ return obj != null ? Object(obj) : {};
259
+ }
260
+
261
+ function safeStringOrArray(elem) {
262
+ if (elem !== undefined && !Array.isArray(elem) && typeof elem !== "string") console.warn(`Operation expects an Array or a String, but got ${elem} (${typeof elem})`);
263
+ return elem == null ? "" : Array.isArray(elem) ? Array.from(elem) : String(elem);
264
+ }
265
+
266
+ function fragmentToString(fragment) {
267
+ return (new XMLSerializer).serializeToString(fragment);
268
+ }
269
+
270
+ // A proxy method to wrap a fetch call in error handling
271
+
272
+ // * url - the URL to fetch
273
+ // * additionalHeaders - an object of additional headers passed to fetch
274
+
170
275
  async function graciouslyFetch(url, additionalHeaders) {
171
276
  try {
172
277
  const response = await fetch(url, {
@@ -183,24 +288,37 @@ async function graciouslyFetch(url, additionalHeaders) {
183
288
  }
184
289
  }
185
290
 
186
- var utils = Object.freeze({
291
+ var utils = Object.freeze({
187
292
  __proto__: null,
188
- isTextInput: isTextInput,
293
+ after: after,
189
294
  assignFocus: assignFocus,
190
- dispatch: dispatch,
191
- xpathToElement: xpathToElement,
192
- getClassNames: getClassNames,
193
- processElements: processElements,
194
- operate: operate,
195
295
  before: before,
196
- after: after,
197
296
  debounce: debounce,
198
- handleErrors: handleErrors,
297
+ dispatch: dispatch,
298
+ fragmentToString: fragmentToString,
299
+ getClassNames: getClassNames,
199
300
  graciouslyFetch: graciouslyFetch,
200
- kebabize: kebabize
301
+ handleErrors: handleErrors,
302
+ isTextInput: isTextInput,
303
+ kebabize: kebabize,
304
+ operate: operate,
305
+ processElements: processElements,
306
+ safeArray: safeArray,
307
+ safeObject: safeObject,
308
+ safeScalar: safeScalar,
309
+ safeString: safeString,
310
+ safeStringOrArray: safeStringOrArray,
311
+ xpathToElement: xpathToElement,
312
+ xpathToElementArray: xpathToElementArray
201
313
  });
202
314
 
203
- const shouldMorph = operation => (fromEl, toEl) => !shouldMorphCallbacks.map((callback => typeof callback === "function" ? callback(operation, fromEl, toEl) : true)).includes(false);
315
+ // Indicates whether or not we should morph an element via onBeforeElUpdated callback
316
+ // SEE: https://github.com/patrick-steele-idem/morphdom#morphdomfromnode-tonode-options--node
317
+
318
+ const shouldMorph = operation => (fromEl, toEl) => !shouldMorphCallbacks.map((callback => typeof callback === "function" ? callback(operation, fromEl, toEl) : true)).includes(false)
319
+ // Execute any pluggable functions that modify elements after morphing via onElUpdated callback
320
+
321
+ ;
204
322
 
205
323
  const didMorph = operation => el => {
206
324
  didMorphCallbacks.forEach((callback => {
@@ -209,6 +327,8 @@ const didMorph = operation => el => {
209
327
  };
210
328
 
211
329
  const verifyNotMutable = (detail, fromEl, toEl) => {
330
+ // Skip nodes that are equal:
331
+ // https://github.com/patrick-steele-idem/morphdom#can-i-make-morphdom-blaze-through-the-dom-tree-even-faster-yes
212
332
  if (!mutableTags[fromEl.tagName] && fromEl.isEqualNode(toEl)) return false;
213
333
  return true;
214
334
  };
@@ -222,7 +342,8 @@ const verifyNotPermanent = (detail, fromEl, toEl) => {
222
342
  const {permanentAttributeName: permanentAttributeName} = detail;
223
343
  if (!permanentAttributeName) return true;
224
344
  const permanent = fromEl.closest(`[${permanentAttributeName}]`);
225
- if (!permanent && fromEl === ActiveElement.element && isTextInput(fromEl)) {
345
+ // only morph attributes on the active non-permanent text input
346
+ if (!permanent && fromEl === ActiveElement.element && isTextInput(fromEl)) {
226
347
  const ignore = {
227
348
  value: true
228
349
  };
@@ -238,24 +359,25 @@ const shouldMorphCallbacks = [ verifyNotMutable, verifyNotPermanent, verifyNotCo
238
359
 
239
360
  const didMorphCallbacks = [];
240
361
 
241
- var morph_callbacks = Object.freeze({
362
+ var morph_callbacks = Object.freeze({
242
363
  __proto__: null,
243
- shouldMorphCallbacks: shouldMorphCallbacks,
364
+ didMorph: didMorph,
244
365
  didMorphCallbacks: didMorphCallbacks,
245
366
  shouldMorph: shouldMorph,
246
- didMorph: didMorph,
247
- verifyNotMutable: verifyNotMutable,
367
+ shouldMorphCallbacks: shouldMorphCallbacks,
248
368
  verifyNotContentEditable: verifyNotContentEditable,
369
+ verifyNotMutable: verifyNotMutable,
249
370
  verifyNotPermanent: verifyNotPermanent
250
371
  });
251
372
 
252
373
  var Operations = {
374
+ // DOM Mutations
253
375
  append: operation => {
254
376
  processElements(operation, (element => {
255
377
  before(element, operation);
256
378
  operate(operation, (() => {
257
379
  const {html: html, focusSelector: focusSelector} = operation;
258
- element.insertAdjacentHTML("beforeend", html || "");
380
+ element.insertAdjacentHTML("beforeend", safeScalar(html));
259
381
  assignFocus(focusSelector);
260
382
  }));
261
383
  after(element, operation);
@@ -280,7 +402,7 @@ var Operations = {
280
402
  before(element, operation);
281
403
  operate(operation, (() => {
282
404
  const {html: html, focusSelector: focusSelector} = operation;
283
- element.innerHTML = html || "";
405
+ element.innerHTML = safeScalar(html);
284
406
  assignFocus(focusSelector);
285
407
  }));
286
408
  after(element, operation);
@@ -291,7 +413,7 @@ var Operations = {
291
413
  before(element, operation);
292
414
  operate(operation, (() => {
293
415
  const {html: html, position: position, focusSelector: focusSelector} = operation;
294
- element.insertAdjacentHTML(position || "beforeend", html || "");
416
+ element.insertAdjacentHTML(position || "beforeend", safeScalar(html));
295
417
  assignFocus(focusSelector);
296
418
  }));
297
419
  after(element, operation);
@@ -302,44 +424,23 @@ var Operations = {
302
424
  before(element, operation);
303
425
  operate(operation, (() => {
304
426
  const {text: text, position: position, focusSelector: focusSelector} = operation;
305
- element.insertAdjacentText(position || "beforeend", text || "");
427
+ element.insertAdjacentText(position || "beforeend", safeScalar(text));
306
428
  assignFocus(focusSelector);
307
429
  }));
308
430
  after(element, operation);
309
431
  }));
310
432
  },
311
- morph: operation => {
312
- processElements(operation, (element => {
313
- const {html: html} = operation;
314
- const template = document.createElement("template");
315
- template.innerHTML = String(html).trim();
316
- operation.content = template.content;
317
- const parent = element.parentElement;
318
- const ordinal = Array.from(parent.children).indexOf(element);
319
- before(element, operation);
320
- operate(operation, (() => {
321
- const {childrenOnly: childrenOnly, focusSelector: focusSelector} = operation;
322
- morphdom(element, childrenOnly ? template.content : template.innerHTML, {
323
- childrenOnly: !!childrenOnly,
324
- onBeforeElUpdated: shouldMorph(operation),
325
- onElUpdated: didMorph(operation)
326
- });
327
- assignFocus(focusSelector);
328
- }));
329
- after(parent.children[ordinal], operation);
330
- }));
331
- },
332
433
  outerHtml: operation => {
333
434
  processElements(operation, (element => {
334
435
  const parent = element.parentElement;
335
- const ordinal = Array.from(parent.children).indexOf(element);
436
+ const idx = parent && Array.from(parent.children).indexOf(element);
336
437
  before(element, operation);
337
438
  operate(operation, (() => {
338
439
  const {html: html, focusSelector: focusSelector} = operation;
339
- element.outerHTML = html || "";
440
+ element.outerHTML = safeScalar(html);
340
441
  assignFocus(focusSelector);
341
442
  }));
342
- after(parent.children[ordinal], operation);
443
+ after(parent ? parent.children[idx] : document.documentElement, operation);
343
444
  }));
344
445
  },
345
446
  prepend: operation => {
@@ -347,7 +448,7 @@ var Operations = {
347
448
  before(element, operation);
348
449
  operate(operation, (() => {
349
450
  const {html: html, focusSelector: focusSelector} = operation;
350
- element.insertAdjacentHTML("afterbegin", html || "");
451
+ element.insertAdjacentHTML("afterbegin", safeScalar(html));
351
452
  assignFocus(focusSelector);
352
453
  }));
353
454
  after(element, operation);
@@ -367,14 +468,14 @@ var Operations = {
367
468
  replace: operation => {
368
469
  processElements(operation, (element => {
369
470
  const parent = element.parentElement;
370
- const ordinal = Array.from(parent.children).indexOf(element);
471
+ const idx = parent && Array.from(parent.children).indexOf(element);
371
472
  before(element, operation);
372
473
  operate(operation, (() => {
373
474
  const {html: html, focusSelector: focusSelector} = operation;
374
- element.outerHTML = html || "";
475
+ element.outerHTML = safeScalar(html);
375
476
  assignFocus(focusSelector);
376
477
  }));
377
- after(parent.children[ordinal], operation);
478
+ after(parent ? parent.children[idx] : document.documentElement, operation);
378
479
  }));
379
480
  },
380
481
  textContent: operation => {
@@ -382,18 +483,19 @@ var Operations = {
382
483
  before(element, operation);
383
484
  operate(operation, (() => {
384
485
  const {text: text, focusSelector: focusSelector} = operation;
385
- element.textContent = text != null ? text : "";
486
+ element.textContent = safeScalar(text);
386
487
  assignFocus(focusSelector);
387
488
  }));
388
489
  after(element, operation);
389
490
  }));
390
491
  },
492
+ // Element Property Mutations
391
493
  addCssClass: operation => {
392
494
  processElements(operation, (element => {
393
495
  before(element, operation);
394
496
  operate(operation, (() => {
395
497
  const {name: name} = operation;
396
- element.classList.add(...getClassNames(name || ""));
498
+ element.classList.add(...getClassNames([ safeStringOrArray(name) ]));
397
499
  }));
398
500
  after(element, operation);
399
501
  }));
@@ -403,7 +505,7 @@ var Operations = {
403
505
  before(element, operation);
404
506
  operate(operation, (() => {
405
507
  const {name: name} = operation;
406
- element.removeAttribute(name);
508
+ element.removeAttribute(safeString(name));
407
509
  }));
408
510
  after(element, operation);
409
511
  }));
@@ -413,7 +515,8 @@ var Operations = {
413
515
  before(element, operation);
414
516
  operate(operation, (() => {
415
517
  const {name: name} = operation;
416
- element.classList.remove(...getClassNames(name));
518
+ element.classList.remove(...getClassNames([ safeStringOrArray(name) ]));
519
+ if (element.classList.length === 0) element.removeAttribute("class");
417
520
  }));
418
521
  after(element, operation);
419
522
  }));
@@ -423,7 +526,7 @@ var Operations = {
423
526
  before(element, operation);
424
527
  operate(operation, (() => {
425
528
  const {name: name, value: value} = operation;
426
- element.setAttribute(name, value || "");
529
+ element.setAttribute(safeString(name), safeScalar(value));
427
530
  }));
428
531
  after(element, operation);
429
532
  }));
@@ -433,7 +536,7 @@ var Operations = {
433
536
  before(element, operation);
434
537
  operate(operation, (() => {
435
538
  const {name: name, value: value} = operation;
436
- element.dataset[name] = value || "";
539
+ element.dataset[safeString(name)] = safeScalar(value);
437
540
  }));
438
541
  after(element, operation);
439
542
  }));
@@ -443,7 +546,7 @@ var Operations = {
443
546
  before(element, operation);
444
547
  operate(operation, (() => {
445
548
  const {name: name, value: value} = operation;
446
- if (name in element) element[name] = value || "";
549
+ if (name in element) element[safeString(name)] = safeScalar(value);
447
550
  }));
448
551
  after(element, operation);
449
552
  }));
@@ -453,7 +556,7 @@ var Operations = {
453
556
  before(element, operation);
454
557
  operate(operation, (() => {
455
558
  const {name: name, value: value} = operation;
456
- element.style[name] = value || "";
559
+ element.style[safeString(name)] = safeScalar(value);
457
560
  }));
458
561
  after(element, operation);
459
562
  }));
@@ -463,7 +566,7 @@ var Operations = {
463
566
  before(element, operation);
464
567
  operate(operation, (() => {
465
568
  const {styles: styles} = operation;
466
- for (let [name, value] of Object.entries(styles)) element.style[name] = value || "";
569
+ for (let [name, value] of Object.entries(styles)) element.style[safeString(name)] = safeScalar(value);
467
570
  }));
468
571
  after(element, operation);
469
572
  }));
@@ -473,17 +576,18 @@ var Operations = {
473
576
  before(element, operation);
474
577
  operate(operation, (() => {
475
578
  const {value: value} = operation;
476
- element.value = value || "";
579
+ element.value = safeScalar(value);
477
580
  }));
478
581
  after(element, operation);
479
582
  }));
480
583
  },
584
+ // DOM Events and Meta-Operations
481
585
  dispatchEvent: operation => {
482
586
  processElements(operation, (element => {
483
587
  before(element, operation);
484
588
  operate(operation, (() => {
485
589
  const {name: name, detail: detail} = operation;
486
- dispatch(element, name, detail);
590
+ dispatch(element, safeString(name), safeObject(detail));
487
591
  }));
488
592
  after(element, operation);
489
593
  }));
@@ -495,13 +599,22 @@ var Operations = {
495
599
  let meta = document.head.querySelector(`meta[name='${name}']`);
496
600
  if (!meta) {
497
601
  meta = document.createElement("meta");
498
- meta.name = name;
602
+ meta.name = safeString(name);
499
603
  document.head.appendChild(meta);
500
604
  }
501
- meta.content = content;
605
+ meta.content = safeScalar(content);
502
606
  }));
503
607
  after(document, operation);
504
608
  },
609
+ setTitle: operation => {
610
+ before(document, operation);
611
+ operate(operation, (() => {
612
+ const {title: title} = operation;
613
+ document.title = safeScalar(title);
614
+ }));
615
+ after(document, operation);
616
+ },
617
+ // Browser Manipulations
505
618
  clearStorage: operation => {
506
619
  before(document, operation);
507
620
  operate(operation, (() => {
@@ -523,22 +636,28 @@ var Operations = {
523
636
  before(window, operation);
524
637
  operate(operation, (() => {
525
638
  const {state: state, title: title, url: url} = operation;
526
- history.pushState(state || {}, title || "", url);
639
+ history.pushState(safeObject(state), safeString(title), safeString(url));
527
640
  }));
528
641
  after(window, operation);
529
642
  },
530
643
  redirectTo: operation => {
531
644
  before(window, operation);
532
645
  operate(operation, (() => {
533
- let {url: url, action: action} = operation;
646
+ let {url: url, action: action, turbo: turbo} = operation;
534
647
  action = action || "advance";
535
- if (window.Turbo) window.Turbo.visit(url, {
536
- action: action
537
- });
538
- if (window.Turbolinks) window.Turbolinks.visit(url, {
539
- action: action
540
- });
541
- if (!window.Turbo && !window.Turbolinks) window.location.href = url;
648
+ url = safeString(url);
649
+ if (turbo === undefined) turbo = true;
650
+ if (turbo) {
651
+ if (window.Turbo) window.Turbo.visit(url, {
652
+ action: action
653
+ });
654
+ if (window.Turbolinks) window.Turbolinks.visit(url, {
655
+ action: action
656
+ });
657
+ if (!window.Turbo && !window.Turbolinks) window.location.href = url;
658
+ } else {
659
+ window.location.href = url;
660
+ }
542
661
  }));
543
662
  after(window, operation);
544
663
  },
@@ -554,7 +673,7 @@ var Operations = {
554
673
  operate(operation, (() => {
555
674
  const {key: key, type: type} = operation;
556
675
  const storage = type === "session" ? sessionStorage : localStorage;
557
- storage.removeItem(key);
676
+ storage.removeItem(safeString(key));
558
677
  }));
559
678
  after(document, operation);
560
679
  },
@@ -562,7 +681,7 @@ var Operations = {
562
681
  before(window, operation);
563
682
  operate(operation, (() => {
564
683
  const {state: state, title: title, url: url} = operation;
565
- history.replaceState(state || {}, title || "", url);
684
+ history.replaceState(safeObject(state), safeString(title), safeString(url));
566
685
  }));
567
686
  after(window, operation);
568
687
  },
@@ -578,7 +697,7 @@ var Operations = {
578
697
  before(document, operation);
579
698
  operate(operation, (() => {
580
699
  const {cookie: cookie} = operation;
581
- document.cookie = cookie || "";
700
+ document.cookie = safeScalar(cookie);
582
701
  }));
583
702
  after(document, operation);
584
703
  },
@@ -595,15 +714,16 @@ var Operations = {
595
714
  operate(operation, (() => {
596
715
  const {key: key, value: value, type: type} = operation;
597
716
  const storage = type === "session" ? sessionStorage : localStorage;
598
- storage.setItem(key, value || "");
717
+ storage.setItem(safeString(key), safeScalar(value));
599
718
  }));
600
719
  after(document, operation);
601
720
  },
721
+ // Notifications
602
722
  consoleLog: operation => {
603
723
  before(document, operation);
604
724
  operate(operation, (() => {
605
725
  const {message: message, level: level} = operation;
606
- level && [ "warn", "info", "error" ].includes(level) ? console[level](message || "") : console.log(message || "");
726
+ level && [ "warn", "info", "error" ].includes(level) ? console[level](message) : console.log(message);
607
727
  }));
608
728
  after(document, operation);
609
729
  },
@@ -611,7 +731,7 @@ var Operations = {
611
731
  before(document, operation);
612
732
  operate(operation, (() => {
613
733
  const {data: data, columns: columns} = operation;
614
- console.table(data, columns || []);
734
+ console.table(data, safeArray(columns));
615
735
  }));
616
736
  after(document, operation);
617
737
  },
@@ -621,10 +741,32 @@ var Operations = {
621
741
  const {title: title, options: options} = operation;
622
742
  Notification.requestPermission().then((result => {
623
743
  operation.permission = result;
624
- if (result === "granted") new Notification(title || "", options);
744
+ if (result === "granted") new Notification(safeString(title), safeObject(options));
625
745
  }));
626
746
  }));
627
747
  after(document, operation);
748
+ },
749
+ // Morph operations
750
+ morph: operation => {
751
+ processElements(operation, (element => {
752
+ const {html: html} = operation;
753
+ const template = document.createElement("template");
754
+ template.innerHTML = String(safeScalar(html)).trim();
755
+ operation.content = template.content;
756
+ const parent = element.parentElement;
757
+ const idx = parent && Array.from(parent.children).indexOf(element);
758
+ before(element, operation);
759
+ operate(operation, (() => {
760
+ const {childrenOnly: childrenOnly, focusSelector: focusSelector} = operation;
761
+ morphdom(element, childrenOnly ? template.content : template.innerHTML, {
762
+ childrenOnly: !!childrenOnly,
763
+ onBeforeElUpdated: shouldMorph(operation),
764
+ onElUpdated: didMorph(operation)
765
+ });
766
+ assignFocus(focusSelector);
767
+ }));
768
+ after(parent ? parent.children[idx] : document.documentElement, operation);
769
+ }));
628
770
  }
629
771
  };
630
772
 
@@ -653,8 +795,19 @@ var OperationStore = {
653
795
  }
654
796
  };
655
797
 
798
+ let missingElement = "warn";
799
+
800
+ var MissingElement$1 = {
801
+ get behavior() {
802
+ return missingElement;
803
+ },
804
+ set(value) {
805
+ if ([ "warn", "ignore", "event", "exception" ].includes(value)) missingElement = value; else console.warn("Invalid 'onMissingElement' option. Defaulting to 'warn'.");
806
+ }
807
+ };
808
+
656
809
  const perform = (operations, options = {
657
- emitMissingElementWarnings: true
810
+ onMissingElement: MissingElement$1.behavior
658
811
  }) => {
659
812
  const batches = {};
660
813
  operations.forEach((operation => {
@@ -664,11 +817,15 @@ const perform = (operations, options = {
664
817
  const name = operation.operation;
665
818
  try {
666
819
  if (operation.selector) {
667
- operation.element = operation.xpath ? xpathToElement(operation.selector) : document[operation.selectAll ? "querySelectorAll" : "querySelector"](operation.selector);
820
+ if (operation.xpath) {
821
+ operation.element = operation.selectAll ? xpathToElementArray(operation.selector) : xpathToElement(operation.selector);
822
+ } else {
823
+ operation.element = operation.selectAll ? document.querySelectorAll(operation.selector) : document.querySelector(operation.selector);
824
+ }
668
825
  } else {
669
826
  operation.element = document;
670
827
  }
671
- if (operation.element || options.emitMissingElementWarnings) {
828
+ if (operation.element || options.onMissingElement !== "ignore") {
672
829
  ActiveElement.set(document.activeElement);
673
830
  const cableReadyOperation = OperationStore.all[name];
674
831
  if (cableReadyOperation) {
@@ -685,14 +842,31 @@ const perform = (operations, options = {
685
842
  console.error(`CableReady detected an error in ${name || "operation"}: ${e.message}. If you need to support older browsers make sure you've included the corresponding polyfills. https://docs.stimulusreflex.com/setup#polyfills-for-ie11.`);
686
843
  console.error(e);
687
844
  } else {
688
- console.warn(`CableReady ${name || "operation"} failed due to missing DOM element for selector: '${operation.selector}'`);
845
+ const warning = `CableReady ${name || ""} operation failed due to missing DOM element for selector: '${operation.selector}'`;
846
+ switch (options.onMissingElement) {
847
+ case "ignore":
848
+ break;
849
+
850
+ case "event":
851
+ dispatch(document, "cable-ready:missing-element", {
852
+ warning: warning,
853
+ operation: operation
854
+ });
855
+ break;
856
+
857
+ case "exception":
858
+ throw warning;
859
+
860
+ default:
861
+ console.warn(warning);
862
+ }
689
863
  }
690
864
  }
691
865
  }));
692
866
  };
693
867
 
694
868
  const performAsync = (operations, options = {
695
- emitMissingElementWarnings: true
869
+ onMissingElement: MissingElement$1.behavior
696
870
  }) => new Promise(((resolve, reject) => {
697
871
  try {
698
872
  resolve(perform(operations, options));
@@ -701,6 +875,34 @@ const performAsync = (operations, options = {
701
875
  }
702
876
  }));
703
877
 
878
+ class SubscribingElement extends HTMLElement {
879
+ static get tagName() {
880
+ throw new Error("Implement the tagName() getter in the inheriting class");
881
+ }
882
+ static define() {
883
+ if (!customElements.get(this.tagName)) {
884
+ customElements.define(this.tagName, this);
885
+ }
886
+ }
887
+ disconnectedCallback() {
888
+ if (this.channel) this.channel.unsubscribe();
889
+ }
890
+ createSubscription(consumer, channel, receivedCallback) {
891
+ this.channel = consumer.subscriptions.create({
892
+ channel: channel,
893
+ identifier: this.identifier
894
+ }, {
895
+ received: receivedCallback
896
+ });
897
+ }
898
+ get preview() {
899
+ return document.documentElement.hasAttribute("data-turbolinks-preview") || document.documentElement.hasAttribute("data-turbo-preview");
900
+ }
901
+ get identifier() {
902
+ return this.getAttribute("identifier");
903
+ }
904
+ }
905
+
704
906
  let consumer;
705
907
 
706
908
  const BACKOFF = [ 25, 50, 75, 100, 200, 250, 500, 800, 1e3, 2e3 ];
@@ -728,44 +930,108 @@ var CableConsumer = {
728
930
  }
729
931
  };
730
932
 
731
- class SubscribingElement extends HTMLElement {
732
- disconnectedCallback() {
733
- if (this.channel) this.channel.unsubscribe();
734
- }
735
- createSubscription(consumer, channel, receivedCallback) {
736
- this.channel = consumer.subscriptions.create({
737
- channel: channel,
738
- identifier: this.identifier
739
- }, {
740
- received: receivedCallback
741
- });
742
- }
743
- get preview() {
744
- return document.documentElement.hasAttribute("data-turbolinks-preview") || document.documentElement.hasAttribute("data-turbo-preview");
745
- }
746
- get identifier() {
747
- return this.getAttribute("identifier");
748
- }
749
- }
750
-
751
933
  class StreamFromElement extends SubscribingElement {
934
+ static get tagName() {
935
+ return "cable-ready-stream-from";
936
+ }
752
937
  async connectedCallback() {
753
938
  if (this.preview) return;
754
939
  const consumer = await CableConsumer.getConsumer();
755
940
  if (consumer) {
756
- this.createSubscription(consumer, "CableReady::Stream", this.performOperations);
941
+ this.createSubscription(consumer, "CableReady::Stream", this.performOperations.bind(this));
757
942
  } else {
758
- console.error("The `stream_from` helper cannot connect without an ActionCable consumer.\nPlease run `rails generate cable_ready:helpers` to fix this.");
943
+ console.error("The `cable_ready_stream_from` helper cannot connect. You must initialize CableReady with an Action Cable consumer.");
759
944
  }
760
945
  }
761
946
  performOperations(data) {
762
- if (data.cableReady) perform(data.operations);
947
+ if (data.cableReady) perform(data.operations, {
948
+ onMissingElement: this.onMissingElement
949
+ });
950
+ }
951
+ get onMissingElement() {
952
+ const value = this.getAttribute("missing") || MissingElement$1.behavior;
953
+ // stream_from does not support raising exceptions on missing elements because there's no way to catch them
954
+ if ([ "warn", "ignore", "event" ].includes(value)) return value; else {
955
+ console.warn("Invalid 'missing' attribute. Defaulting to 'warn'.");
956
+ return "warn";
957
+ }
763
958
  }
764
959
  }
765
960
 
961
+ let debugging = false;
962
+
963
+ var Debug = {
964
+ get enabled() {
965
+ return debugging;
966
+ },
967
+ get disabled() {
968
+ return !debugging;
969
+ },
970
+ get value() {
971
+ return debugging;
972
+ },
973
+ set(value) {
974
+ debugging = !!value;
975
+ },
976
+ set debug(value) {
977
+ debugging = !!value;
978
+ }
979
+ };
980
+
981
+ const request = (data, blocks) => {
982
+ if (Debug.disabled) return;
983
+ console.log(`↑ Updatable request affecting ${blocks.length} element(s): `, {
984
+ elements: blocks.map((b => b.element)),
985
+ identifiers: blocks.map((b => b.element.getAttribute("identifier"))),
986
+ data: data
987
+ });
988
+ };
989
+
990
+ const cancel = (timestamp, reason) => {
991
+ if (Debug.disabled) return;
992
+ const duration = new Date - timestamp;
993
+ console.log(`❌ Updatable request canceled after ${duration}ms: ${reason}`);
994
+ };
995
+
996
+ const response = (timestamp, element, urls) => {
997
+ if (Debug.disabled) return;
998
+ const duration = new Date - timestamp;
999
+ console.log(`↓ Updatable response: All URLs fetched in ${duration}ms`, {
1000
+ element: element,
1001
+ urls: urls
1002
+ });
1003
+ };
1004
+
1005
+ const morphStart = (timestamp, element) => {
1006
+ if (Debug.disabled) return;
1007
+ const duration = new Date - timestamp;
1008
+ console.log(`↻ Updatable morph: starting after ${duration}ms`, {
1009
+ element: element
1010
+ });
1011
+ };
1012
+
1013
+ const morphEnd = (timestamp, element) => {
1014
+ if (Debug.disabled) return;
1015
+ const duration = new Date - timestamp;
1016
+ console.log(`↺ Updatable morph: completed after ${duration}ms`, {
1017
+ element: element
1018
+ });
1019
+ };
1020
+
1021
+ var Log = {
1022
+ request: request,
1023
+ cancel: cancel,
1024
+ response: response,
1025
+ morphStart: morphStart,
1026
+ morphEnd: morphEnd
1027
+ };
1028
+
766
1029
  const template = `\n<style>\n :host {\n display: block;\n }\n</style>\n<slot></slot>\n`;
767
1030
 
768
1031
  class UpdatesForElement extends SubscribingElement {
1032
+ static get tagName() {
1033
+ return "cable-ready-updates-for";
1034
+ }
769
1035
  constructor() {
770
1036
  super();
771
1037
  const shadowRoot = this.attachShadow({
@@ -780,14 +1046,26 @@ class UpdatesForElement extends SubscribingElement {
780
1046
  if (consumer) {
781
1047
  this.createSubscription(consumer, "CableReady::Stream", this.update);
782
1048
  } else {
783
- console.error("The `updates-for` helper cannot connect without an ActionCable consumer.\nPlease run `rails generate cable_ready:helpers` to fix this.");
1049
+ console.error("The `cable_ready_updates_for` helper cannot connect. You must initialize CableReady with an Action Cable consumer.");
784
1050
  }
785
1051
  }
786
1052
  async update(data) {
787
- const blocks = Array.from(document.querySelectorAll(this.query), (element => new Block(element)));
788
- if (blocks[0].element !== this) return;
789
- ActiveElement.set(document.activeElement);
790
- this.html = {};
1053
+ this.lastUpdateTimestamp = new Date;
1054
+ const blocks = Array.from(document.querySelectorAll(this.query), (element => new Block(element))).filter((block => block.shouldUpdate(data)));
1055
+ Log.request(data, blocks);
1056
+ if (blocks.length === 0) {
1057
+ Log.cancel(this.lastUpdateTimestamp, "All elements filtered out");
1058
+ return;
1059
+ }
1060
+ // first updates-for element in the DOM *at any given moment* updates all of the others
1061
+ if (blocks[0].element !== this) {
1062
+ Log.cancel(this.lastUpdateTimestamp, "Update already requested");
1063
+ return;
1064
+ }
1065
+ // hold a reference to the active element so that it can be restored after the morph
1066
+ ActiveElement.set(document.activeElement);
1067
+ // store all retrieved HTML in an object keyed by URL to minimize fetch calls
1068
+ this.html = {};
791
1069
  const uniqueUrls = [ ...new Set(blocks.map((block => block.url))) ];
792
1070
  await Promise.all(uniqueUrls.map((async url => {
793
1071
  if (!this.html.hasOwnProperty(url)) {
@@ -797,14 +1075,17 @@ class UpdatesForElement extends SubscribingElement {
797
1075
  this.html[url] = await response.text();
798
1076
  }
799
1077
  })));
800
- this.index = {};
1078
+ Log.response(this.lastUpdateTimestamp, this, uniqueUrls);
1079
+ // track current block index for each URL; referred to as fragments
1080
+ this.index = {};
801
1081
  blocks.forEach((block => {
1082
+ // if the block's URL is not in the index, initialize it to 0; otherwise, increment it
802
1083
  this.index.hasOwnProperty(block.url) ? this.index[block.url]++ : this.index[block.url] = 0;
803
- block.process(data, this.html, this.index);
1084
+ block.process(data, this.html, this.index, this.lastUpdateTimestamp);
804
1085
  }));
805
1086
  }
806
1087
  get query() {
807
- return `updates-for[identifier="${this.identifier}"]`;
1088
+ return `${this.tagName}[identifier="${this.identifier}"]`;
808
1089
  }
809
1090
  get identifier() {
810
1091
  return this.getAttribute("identifier");
@@ -818,8 +1099,7 @@ class Block {
818
1099
  constructor(element) {
819
1100
  this.element = element;
820
1101
  }
821
- async process(data, html, index) {
822
- if (!this.shouldUpdate(data)) return;
1102
+ async process(data, html, index, startTimestamp) {
823
1103
  const blockIndex = index[this.url];
824
1104
  const template = document.createElement("template");
825
1105
  this.element.setAttribute("updating", "updating");
@@ -836,6 +1116,7 @@ class Block {
836
1116
  permanentAttributeName: "data-ignore-updates"
837
1117
  };
838
1118
  dispatch(this.element, "cable-ready:before-update", operation);
1119
+ Log.morphStart(startTimestamp, this.element);
839
1120
  morphdom(this.element, fragments[blockIndex], {
840
1121
  childrenOnly: true,
841
1122
  onBeforeElUpdated: shouldMorph(operation),
@@ -845,6 +1126,7 @@ class Block {
845
1126
  assignFocus(operation.focusSelector);
846
1127
  }
847
1128
  });
1129
+ Log.morphEnd(startTimestamp, this.element);
848
1130
  }
849
1131
  async resolveTurboFrames(documentFragment) {
850
1132
  const reloadingTurboFrames = [ ...documentFragment.querySelectorAll('turbo-frame[src]:not([loading="lazy"])') ];
@@ -855,19 +1137,26 @@ class Block {
855
1137
  });
856
1138
  const frameTemplate = document.createElement("template");
857
1139
  frameTemplate.innerHTML = await frameResponse.text();
858
- await this.resolveTurboFrames(frameTemplate.content);
859
- documentFragment.querySelector(`turbo-frame#${frame.id}`).innerHTML = String(frameTemplate.content.querySelector(`turbo-frame#${frame.id}`).innerHTML).trim();
1140
+ // recurse here to get all nested eager loaded frames
1141
+ await this.resolveTurboFrames(frameTemplate.content);
1142
+ const selector = `turbo-frame#${frame.id}`;
1143
+ const frameContent = frameTemplate.content.querySelector(selector);
1144
+ const content = frameContent ? frameContent.innerHTML.trim() : "";
1145
+ docFragment.querySelector(selector).innerHTML = content;
860
1146
  resolve();
861
1147
  })))));
862
1148
  }
863
1149
  shouldUpdate(data) {
1150
+ // if everything that could prevent an update is false, update this block
864
1151
  return !this.ignoresInnerUpdates && this.hasChangesSelectedForUpdate(data);
865
1152
  }
866
1153
  hasChangesSelectedForUpdate(data) {
1154
+ // if there's an only attribute, only update if at least one of the attributes changed is in the allow list
867
1155
  const only = this.element.getAttribute("only");
868
1156
  return !(only && data.changed && !only.split(" ").some((attribute => data.changed.includes(attribute))));
869
1157
  }
870
1158
  get ignoresInnerUpdates() {
1159
+ // don't update during a Reflex or Turbolinks redraw
871
1160
  return this.element.hasAttribute("ignore-inner-updates") && this.element.hasAttribute("performing-inner-update");
872
1161
  }
873
1162
  get url() {
@@ -898,10 +1187,23 @@ const registerInnerUpdates = () => {
898
1187
  recursiveUnmarkUpdatesForElements(event.target);
899
1188
  }));
900
1189
  }));
1190
+ document.addEventListener("turbo-boost:command:start", (event => {
1191
+ recursiveMarkUpdatesForElements(event.target);
1192
+ }));
1193
+ document.addEventListener("turbo-boost:command:finish", (event => {
1194
+ setTimeout((() => {
1195
+ recursiveUnmarkUpdatesForElements(event.target);
1196
+ }));
1197
+ }));
1198
+ document.addEventListener("turbo-boost:command:error", (event => {
1199
+ setTimeout((() => {
1200
+ recursiveUnmarkUpdatesForElements(event.target);
1201
+ }));
1202
+ }));
901
1203
  };
902
1204
 
903
1205
  const recursiveMarkUpdatesForElements = leaf => {
904
- const closestUpdatesFor = leaf && leaf.parentElement.closest("updates-for");
1206
+ const closestUpdatesFor = leaf && leaf.parentElement && leaf.parentElement.closest("cable-ready-updates-for");
905
1207
  if (closestUpdatesFor) {
906
1208
  closestUpdatesFor.setAttribute("performing-inner-update", "");
907
1209
  recursiveMarkUpdatesForElements(closestUpdatesFor);
@@ -909,27 +1211,31 @@ const recursiveMarkUpdatesForElements = leaf => {
909
1211
  };
910
1212
 
911
1213
  const recursiveUnmarkUpdatesForElements = leaf => {
912
- const closestUpdatesFor = leaf && leaf.parentElement.closest("updates-for");
1214
+ const closestUpdatesFor = leaf && leaf.parentElement && leaf.parentElement.closest("cable-ready-updates-for");
913
1215
  if (closestUpdatesFor) {
914
1216
  closestUpdatesFor.removeAttribute("performing-inner-update");
915
1217
  recursiveUnmarkUpdatesForElements(closestUpdatesFor);
916
1218
  }
917
1219
  };
918
1220
 
919
- const initialize = (initializeOptions = {}) => {
920
- const {consumer: consumer} = initializeOptions;
1221
+ const defineElements = () => {
921
1222
  registerInnerUpdates();
1223
+ StreamFromElement.define();
1224
+ UpdatesForElement.define();
1225
+ };
1226
+
1227
+ const initialize = (initializeOptions = {}) => {
1228
+ const {consumer: consumer, onMissingElement: onMissingElement, debug: debug} = initializeOptions;
1229
+ Debug.set(!!debug);
922
1230
  if (consumer) {
923
1231
  CableConsumer.setConsumer(consumer);
924
1232
  } else {
925
1233
  console.error("CableReady requires a reference to your Action Cable `consumer` for its helpers to function.\nEnsure that you have imported the `CableReady` package as well as `consumer` from your `channels` folder, then call `CableReady.initialize({ consumer })`.");
926
1234
  }
927
- if (!customElements.get("stream-from")) {
928
- customElements.define("stream-from", StreamFromElement);
929
- }
930
- if (!customElements.get("updates-for")) {
931
- customElements.define("updates-for", UpdatesForElement);
1235
+ if (onMissingElement) {
1236
+ MissingElement.set(onMissingElement);
932
1237
  }
1238
+ defineElements();
933
1239
  };
934
1240
 
935
1241
  const global = {