cable_ready 5.0.0.pre9 → 5.0.0.pre10

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 (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 = {