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
@@ -3,28 +3,21 @@
3
3
  factory(global.CableReady = {}, global.morphdom));
4
4
  })(this, (function(exports, morphdom) {
5
5
  "use strict";
6
- function _interopDefaultLegacy(e) {
7
- return e && typeof e === "object" && "default" in e ? e : {
8
- default: e
9
- };
10
- }
11
- var morphdom__default = _interopDefaultLegacy(morphdom);
12
6
  var name = "cable_ready";
13
- var version = "5.0.0-pre9";
7
+ var version = "5.0.0-pre10";
14
8
  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.";
15
9
  var keywords = [ "ruby", "rails", "websockets", "actioncable", "cable", "ssr", "stimulus_reflex", "client-side", "dom" ];
16
- var homepage = "https://cableready.stimulusreflex.com/";
17
- var bugs = {
18
- url: "https://github.com/stimulusreflex/cable_ready/issues"
19
- };
20
- var repository = {
21
- type: "git",
22
- url: "git+https://github.com:stimulusreflex/cable_ready.git"
23
- };
10
+ var homepage = "https://cableready.stimulusreflex.com";
11
+ var bugs = "https://github.com/stimulusreflex/cable_ready/issues";
12
+ var repository = "https://github.com/stimulusreflex/cable_ready";
24
13
  var license = "MIT";
25
14
  var author = "Nathan Hopkins <natehop@gmail.com>";
26
- var main = "./dist/cable_ready.umd.min.js";
27
- var module = "./dist/cable_ready.min.js";
15
+ var contributors = [ "Andrew Mason <andrewmcodes@protonmail.com>", "Julian Rubisch <julian@julianrubisch.at>", "Marco Roth <marco.roth@intergga.ch>", "Nathan Hopkins <natehop@gmail.com>" ];
16
+ var main = "./dist/cable_ready.js";
17
+ var module = "./dist/cable_ready.js";
18
+ var browser = "./dist/cable_ready.js";
19
+ var unpkg = "./dist/cable_ready.umd.js";
20
+ var umd = "./dist/cable_ready.umd.js";
28
21
  var files = [ "dist/*", "javascript/*" ];
29
22
  var scripts = {
30
23
  lint: "yarn run prettier-standard:check",
@@ -32,18 +25,23 @@
32
25
  "prettier-standard:check": "yarn run prettier-standard --check ./javascript/**/*.js rollup.config.js",
33
26
  "prettier-standard:format": "yarn run prettier-standard ./javascript/**/*.js rollup.config.js",
34
27
  build: "yarn rollup -c",
35
- watch: "yarn rollup -wc"
28
+ watch: "yarn rollup -wc",
29
+ test: "web-test-runner javascript/test/**/*.test.js"
36
30
  };
37
31
  var dependencies = {
38
- morphdom: "^2.6.1"
32
+ morphdom: "2.6.1"
39
33
  };
40
34
  var devDependencies = {
41
- "@rollup/plugin-commonjs": "^21.0.3",
42
- "@rollup/plugin-json": "^4.1.0",
43
- "@rollup/plugin-node-resolve": "^13.1.3",
35
+ "@open-wc/testing": "^3.1.7",
36
+ "@rollup/plugin-json": "^6.0.0",
37
+ "@rollup/plugin-node-resolve": "^15.0.1",
38
+ "@rollup/plugin-terser": "^0.4.0",
39
+ "@web/dev-server-esbuild": "^0.3.3",
40
+ "@web/dev-server-rollup": "^0.3.21",
41
+ "@web/test-runner": "^0.15.0",
44
42
  "prettier-standard": "^16.4.1",
45
- rollup: "^2.70.1",
46
- "rollup-plugin-terser": "^7.0.2"
43
+ rollup: "^3.15.0",
44
+ sinon: "^15.0.1"
47
45
  };
48
46
  var packageInfo = {
49
47
  name: name,
@@ -55,8 +53,13 @@
55
53
  repository: repository,
56
54
  license: license,
57
55
  author: author,
56
+ contributors: contributors,
58
57
  main: main,
59
58
  module: module,
59
+ browser: browser,
60
+ import: "./dist/cable_ready.js",
61
+ unpkg: unpkg,
62
+ umd: umd,
60
63
  files: files,
61
64
  scripts: scripts,
62
65
  dependencies: dependencies,
@@ -101,49 +104,132 @@
101
104
  activeElement = element;
102
105
  }
103
106
  };
104
- const isTextInput = element => inputTags[element.tagName] && textInputTypes[element.type];
105
- const assignFocus = selector => {
107
+ // Indicates if the passed element is considered a text input.
108
+
109
+ const isTextInput = element => inputTags[element.tagName] && textInputTypes[element.type];
110
+ // Assigns focus to the appropriate element... preferring the explicitly passed selector
111
+
112
+ // * selector - a CSS selector for the element that should have focus
113
+
114
+ const assignFocus = selector => {
106
115
  const element = selector && selector.nodeType === Node.ELEMENT_NODE ? selector : document.querySelector(selector);
107
116
  const focusElement = element || ActiveElement.element;
108
117
  if (focusElement && focusElement.focus) focusElement.focus();
109
118
  };
110
- const dispatch = (element, name, detail = {}) => {
119
+ // Dispatches an event on the passed element
120
+
121
+ // * element - the element
122
+ // * name - the name of the event
123
+ // * detail - the event detail
124
+
125
+ const dispatch = (element, name, detail = {}) => {
111
126
  const init = {
112
127
  bubbles: true,
113
128
  cancelable: true,
114
129
  detail: detail
115
130
  };
116
- const evt = new CustomEvent(name, init);
117
- element.dispatchEvent(evt);
131
+ const event = new CustomEvent(name, init);
132
+ element.dispatchEvent(event);
118
133
  if (window.jQuery) window.jQuery(element).trigger(name, detail);
119
134
  };
120
- const xpathToElement = xpath => document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
121
- const getClassNames = names => Array(names).flat();
135
+ // Accepts an xPath query and returns the element found at that position in the DOM
136
+
137
+ const xpathToElement = xpath => document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
138
+ // Accepts an xPath query and returns all matching elements in the DOM
139
+
140
+ const xpathToElementArray = (xpath, reverse = false) => {
141
+ const snapshotList = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
142
+ const snapshots = [];
143
+ for (let i = 0; i < snapshotList.snapshotLength; i++) {
144
+ snapshots.push(snapshotList.snapshotItem(i));
145
+ }
146
+ return reverse ? snapshots.reverse() : snapshots;
147
+ };
148
+ // Return an array with the class names to be used
149
+
150
+ // * names - could be a string or an array of strings for multiple classes.
151
+
152
+ const getClassNames = names => Array.from(names).flat()
153
+ // Perform operation for either the first or all of the elements returned by CSS selector
154
+
155
+ // * operation - the instruction payload from perform
156
+ // * callback - the operation function to run for each element
157
+
158
+ ;
122
159
  const processElements = (operation, callback) => {
123
160
  Array.from(operation.selectAll ? operation.element : [ operation.element ]).forEach(callback);
124
161
  };
125
- const kebabize = str => str.split("").map(((letter, idx) => letter.toUpperCase() === letter ? `${idx !== 0 ? "-" : ""}${letter.toLowerCase()}` : letter)).join("");
126
- const operate = (operation, callback) => {
162
+ // convert string to kebab-case
163
+ // most other implementations (lodash) are focused on camelCase to kebab-case
164
+ // instead, this uses word token boundaries to produce readable URL slugs and keys
165
+ // this implementation will not support Emoji or other non-ASCII characters
166
+
167
+ const kebabize = createCompounder((function(result, word, index) {
168
+ return result + (index ? "-" : "") + word.toLowerCase();
169
+ }));
170
+ function createCompounder(callback) {
171
+ return function(str) {
172
+ return words(str).reduce(callback, "");
173
+ };
174
+ }
175
+ const words = str => {
176
+ str = str == null ? "" : str;
177
+ return str.match(/([A-Z]{2,}|[0-9]+|[A-Z]?[a-z]+|[A-Z])/g) || [];
178
+ };
179
+ // Provide a standardized pipeline of checks and modifications to all operations based on provided options
180
+ // Currently skips execution if cancelled and implements an optional delay
181
+
182
+ const operate = (operation, callback) => {
127
183
  if (!operation.cancel) {
128
184
  operation.delay ? setTimeout(callback, operation.delay) : callback();
129
185
  return true;
130
186
  }
131
187
  return false;
132
188
  };
133
- const before = (target, operation) => dispatch(target, `cable-ready:before-${kebabize(operation.operation)}`, operation);
189
+ // Dispatch life-cycle events with standardized naming
190
+ const before = (target, operation) => dispatch(target, `cable-ready:before-${kebabize(operation.operation)}`, operation);
134
191
  const after = (target, operation) => dispatch(target, `cable-ready:after-${kebabize(operation.operation)}`, operation);
135
- function debounce(func, timeout) {
192
+ function debounce(fn, delay = 250) {
136
193
  let timer;
137
194
  return (...args) => {
138
- clearTimeout(timer);
139
- timer = setTimeout((() => func.apply(this, args)), timeout);
195
+ const callback = () => fn.apply(this, args);
196
+ if (timer) clearTimeout(timer);
197
+ timer = setTimeout(callback, delay);
140
198
  };
141
199
  }
142
200
  function handleErrors(response) {
143
201
  if (!response.ok) throw Error(response.statusText);
144
202
  return response;
145
203
  }
146
- async function graciouslyFetch(url, additionalHeaders) {
204
+ function safeScalar(val) {
205
+ if (val !== undefined && ![ "string", "number", "boolean" ].includes(typeof val)) console.warn(`Operation expects a string, number or boolean, but got ${val} (${typeof val})`);
206
+ return val != null ? val : "";
207
+ }
208
+ function safeString(str) {
209
+ if (str !== undefined && typeof str !== "string") console.warn(`Operation expects a string, but got ${str} (${typeof str})`);
210
+ return str != null ? String(str) : "";
211
+ }
212
+ function safeArray(arr) {
213
+ if (arr !== undefined && !Array.isArray(arr)) console.warn(`Operation expects an array, but got ${arr} (${typeof arr})`);
214
+ return arr != null ? Array.from(arr) : [];
215
+ }
216
+ function safeObject(obj) {
217
+ if (obj !== undefined && typeof obj !== "object") console.warn(`Operation expects an object, but got ${obj} (${typeof obj})`);
218
+ return obj != null ? Object(obj) : {};
219
+ }
220
+ function safeStringOrArray(elem) {
221
+ if (elem !== undefined && !Array.isArray(elem) && typeof elem !== "string") console.warn(`Operation expects an Array or a String, but got ${elem} (${typeof elem})`);
222
+ return elem == null ? "" : Array.isArray(elem) ? Array.from(elem) : String(elem);
223
+ }
224
+ function fragmentToString(fragment) {
225
+ return (new XMLSerializer).serializeToString(fragment);
226
+ }
227
+ // A proxy method to wrap a fetch call in error handling
228
+
229
+ // * url - the URL to fetch
230
+ // * additionalHeaders - an object of additional headers passed to fetch
231
+
232
+ async function graciouslyFetch(url, additionalHeaders) {
147
233
  try {
148
234
  const response = await fetch(url, {
149
235
  headers: {
@@ -158,29 +244,44 @@
158
244
  console.error(`Could not fetch ${url}`);
159
245
  }
160
246
  }
161
- var utils = Object.freeze({
247
+ var utils = Object.freeze({
162
248
  __proto__: null,
163
- isTextInput: isTextInput,
249
+ after: after,
164
250
  assignFocus: assignFocus,
165
- dispatch: dispatch,
166
- xpathToElement: xpathToElement,
167
- getClassNames: getClassNames,
168
- processElements: processElements,
169
- operate: operate,
170
251
  before: before,
171
- after: after,
172
252
  debounce: debounce,
173
- handleErrors: handleErrors,
253
+ dispatch: dispatch,
254
+ fragmentToString: fragmentToString,
255
+ getClassNames: getClassNames,
174
256
  graciouslyFetch: graciouslyFetch,
175
- kebabize: kebabize
257
+ handleErrors: handleErrors,
258
+ isTextInput: isTextInput,
259
+ kebabize: kebabize,
260
+ operate: operate,
261
+ processElements: processElements,
262
+ safeArray: safeArray,
263
+ safeObject: safeObject,
264
+ safeScalar: safeScalar,
265
+ safeString: safeString,
266
+ safeStringOrArray: safeStringOrArray,
267
+ xpathToElement: xpathToElement,
268
+ xpathToElementArray: xpathToElementArray
176
269
  });
177
- const shouldMorph = operation => (fromEl, toEl) => !shouldMorphCallbacks.map((callback => typeof callback === "function" ? callback(operation, fromEl, toEl) : true)).includes(false);
270
+ // Indicates whether or not we should morph an element via onBeforeElUpdated callback
271
+ // SEE: https://github.com/patrick-steele-idem/morphdom#morphdomfromnode-tonode-options--node
272
+
273
+ const shouldMorph = operation => (fromEl, toEl) => !shouldMorphCallbacks.map((callback => typeof callback === "function" ? callback(operation, fromEl, toEl) : true)).includes(false)
274
+ // Execute any pluggable functions that modify elements after morphing via onElUpdated callback
275
+
276
+ ;
178
277
  const didMorph = operation => el => {
179
278
  didMorphCallbacks.forEach((callback => {
180
279
  if (typeof callback === "function") callback(operation, el);
181
280
  }));
182
281
  };
183
282
  const verifyNotMutable = (detail, fromEl, toEl) => {
283
+ // Skip nodes that are equal:
284
+ // https://github.com/patrick-steele-idem/morphdom#can-i-make-morphdom-blaze-through-the-dom-tree-even-faster-yes
184
285
  if (!mutableTags[fromEl.tagName] && fromEl.isEqualNode(toEl)) return false;
185
286
  return true;
186
287
  };
@@ -192,7 +293,8 @@
192
293
  const {permanentAttributeName: permanentAttributeName} = detail;
193
294
  if (!permanentAttributeName) return true;
194
295
  const permanent = fromEl.closest(`[${permanentAttributeName}]`);
195
- if (!permanent && fromEl === ActiveElement.element && isTextInput(fromEl)) {
296
+ // only morph attributes on the active non-permanent text input
297
+ if (!permanent && fromEl === ActiveElement.element && isTextInput(fromEl)) {
196
298
  const ignore = {
197
299
  value: true
198
300
  };
@@ -205,23 +307,24 @@
205
307
  };
206
308
  const shouldMorphCallbacks = [ verifyNotMutable, verifyNotPermanent, verifyNotContentEditable ];
207
309
  const didMorphCallbacks = [];
208
- var morph_callbacks = Object.freeze({
310
+ var morph_callbacks = Object.freeze({
209
311
  __proto__: null,
210
- shouldMorphCallbacks: shouldMorphCallbacks,
312
+ didMorph: didMorph,
211
313
  didMorphCallbacks: didMorphCallbacks,
212
314
  shouldMorph: shouldMorph,
213
- didMorph: didMorph,
214
- verifyNotMutable: verifyNotMutable,
315
+ shouldMorphCallbacks: shouldMorphCallbacks,
215
316
  verifyNotContentEditable: verifyNotContentEditable,
317
+ verifyNotMutable: verifyNotMutable,
216
318
  verifyNotPermanent: verifyNotPermanent
217
319
  });
218
320
  var Operations = {
321
+ // DOM Mutations
219
322
  append: operation => {
220
323
  processElements(operation, (element => {
221
324
  before(element, operation);
222
325
  operate(operation, (() => {
223
326
  const {html: html, focusSelector: focusSelector} = operation;
224
- element.insertAdjacentHTML("beforeend", html || "");
327
+ element.insertAdjacentHTML("beforeend", safeScalar(html));
225
328
  assignFocus(focusSelector);
226
329
  }));
227
330
  after(element, operation);
@@ -246,7 +349,7 @@
246
349
  before(element, operation);
247
350
  operate(operation, (() => {
248
351
  const {html: html, focusSelector: focusSelector} = operation;
249
- element.innerHTML = html || "";
352
+ element.innerHTML = safeScalar(html);
250
353
  assignFocus(focusSelector);
251
354
  }));
252
355
  after(element, operation);
@@ -257,7 +360,7 @@
257
360
  before(element, operation);
258
361
  operate(operation, (() => {
259
362
  const {html: html, position: position, focusSelector: focusSelector} = operation;
260
- element.insertAdjacentHTML(position || "beforeend", html || "");
363
+ element.insertAdjacentHTML(position || "beforeend", safeScalar(html));
261
364
  assignFocus(focusSelector);
262
365
  }));
263
366
  after(element, operation);
@@ -268,44 +371,23 @@
268
371
  before(element, operation);
269
372
  operate(operation, (() => {
270
373
  const {text: text, position: position, focusSelector: focusSelector} = operation;
271
- element.insertAdjacentText(position || "beforeend", text || "");
374
+ element.insertAdjacentText(position || "beforeend", safeScalar(text));
272
375
  assignFocus(focusSelector);
273
376
  }));
274
377
  after(element, operation);
275
378
  }));
276
379
  },
277
- morph: operation => {
278
- processElements(operation, (element => {
279
- const {html: html} = operation;
280
- const template = document.createElement("template");
281
- template.innerHTML = String(html).trim();
282
- operation.content = template.content;
283
- const parent = element.parentElement;
284
- const ordinal = Array.from(parent.children).indexOf(element);
285
- before(element, operation);
286
- operate(operation, (() => {
287
- const {childrenOnly: childrenOnly, focusSelector: focusSelector} = operation;
288
- morphdom__default["default"](element, childrenOnly ? template.content : template.innerHTML, {
289
- childrenOnly: !!childrenOnly,
290
- onBeforeElUpdated: shouldMorph(operation),
291
- onElUpdated: didMorph(operation)
292
- });
293
- assignFocus(focusSelector);
294
- }));
295
- after(parent.children[ordinal], operation);
296
- }));
297
- },
298
380
  outerHtml: operation => {
299
381
  processElements(operation, (element => {
300
382
  const parent = element.parentElement;
301
- const ordinal = Array.from(parent.children).indexOf(element);
383
+ const idx = parent && Array.from(parent.children).indexOf(element);
302
384
  before(element, operation);
303
385
  operate(operation, (() => {
304
386
  const {html: html, focusSelector: focusSelector} = operation;
305
- element.outerHTML = html || "";
387
+ element.outerHTML = safeScalar(html);
306
388
  assignFocus(focusSelector);
307
389
  }));
308
- after(parent.children[ordinal], operation);
390
+ after(parent ? parent.children[idx] : document.documentElement, operation);
309
391
  }));
310
392
  },
311
393
  prepend: operation => {
@@ -313,7 +395,7 @@
313
395
  before(element, operation);
314
396
  operate(operation, (() => {
315
397
  const {html: html, focusSelector: focusSelector} = operation;
316
- element.insertAdjacentHTML("afterbegin", html || "");
398
+ element.insertAdjacentHTML("afterbegin", safeScalar(html));
317
399
  assignFocus(focusSelector);
318
400
  }));
319
401
  after(element, operation);
@@ -333,14 +415,14 @@
333
415
  replace: operation => {
334
416
  processElements(operation, (element => {
335
417
  const parent = element.parentElement;
336
- const ordinal = Array.from(parent.children).indexOf(element);
418
+ const idx = parent && Array.from(parent.children).indexOf(element);
337
419
  before(element, operation);
338
420
  operate(operation, (() => {
339
421
  const {html: html, focusSelector: focusSelector} = operation;
340
- element.outerHTML = html || "";
422
+ element.outerHTML = safeScalar(html);
341
423
  assignFocus(focusSelector);
342
424
  }));
343
- after(parent.children[ordinal], operation);
425
+ after(parent ? parent.children[idx] : document.documentElement, operation);
344
426
  }));
345
427
  },
346
428
  textContent: operation => {
@@ -348,18 +430,19 @@
348
430
  before(element, operation);
349
431
  operate(operation, (() => {
350
432
  const {text: text, focusSelector: focusSelector} = operation;
351
- element.textContent = text != null ? text : "";
433
+ element.textContent = safeScalar(text);
352
434
  assignFocus(focusSelector);
353
435
  }));
354
436
  after(element, operation);
355
437
  }));
356
438
  },
439
+ // Element Property Mutations
357
440
  addCssClass: operation => {
358
441
  processElements(operation, (element => {
359
442
  before(element, operation);
360
443
  operate(operation, (() => {
361
444
  const {name: name} = operation;
362
- element.classList.add(...getClassNames(name || ""));
445
+ element.classList.add(...getClassNames([ safeStringOrArray(name) ]));
363
446
  }));
364
447
  after(element, operation);
365
448
  }));
@@ -369,7 +452,7 @@
369
452
  before(element, operation);
370
453
  operate(operation, (() => {
371
454
  const {name: name} = operation;
372
- element.removeAttribute(name);
455
+ element.removeAttribute(safeString(name));
373
456
  }));
374
457
  after(element, operation);
375
458
  }));
@@ -379,7 +462,8 @@
379
462
  before(element, operation);
380
463
  operate(operation, (() => {
381
464
  const {name: name} = operation;
382
- element.classList.remove(...getClassNames(name));
465
+ element.classList.remove(...getClassNames([ safeStringOrArray(name) ]));
466
+ if (element.classList.length === 0) element.removeAttribute("class");
383
467
  }));
384
468
  after(element, operation);
385
469
  }));
@@ -389,7 +473,7 @@
389
473
  before(element, operation);
390
474
  operate(operation, (() => {
391
475
  const {name: name, value: value} = operation;
392
- element.setAttribute(name, value || "");
476
+ element.setAttribute(safeString(name), safeScalar(value));
393
477
  }));
394
478
  after(element, operation);
395
479
  }));
@@ -399,7 +483,7 @@
399
483
  before(element, operation);
400
484
  operate(operation, (() => {
401
485
  const {name: name, value: value} = operation;
402
- element.dataset[name] = value || "";
486
+ element.dataset[safeString(name)] = safeScalar(value);
403
487
  }));
404
488
  after(element, operation);
405
489
  }));
@@ -409,7 +493,7 @@
409
493
  before(element, operation);
410
494
  operate(operation, (() => {
411
495
  const {name: name, value: value} = operation;
412
- if (name in element) element[name] = value || "";
496
+ if (name in element) element[safeString(name)] = safeScalar(value);
413
497
  }));
414
498
  after(element, operation);
415
499
  }));
@@ -419,7 +503,7 @@
419
503
  before(element, operation);
420
504
  operate(operation, (() => {
421
505
  const {name: name, value: value} = operation;
422
- element.style[name] = value || "";
506
+ element.style[safeString(name)] = safeScalar(value);
423
507
  }));
424
508
  after(element, operation);
425
509
  }));
@@ -429,7 +513,7 @@
429
513
  before(element, operation);
430
514
  operate(operation, (() => {
431
515
  const {styles: styles} = operation;
432
- for (let [name, value] of Object.entries(styles)) element.style[name] = value || "";
516
+ for (let [name, value] of Object.entries(styles)) element.style[safeString(name)] = safeScalar(value);
433
517
  }));
434
518
  after(element, operation);
435
519
  }));
@@ -439,17 +523,18 @@
439
523
  before(element, operation);
440
524
  operate(operation, (() => {
441
525
  const {value: value} = operation;
442
- element.value = value || "";
526
+ element.value = safeScalar(value);
443
527
  }));
444
528
  after(element, operation);
445
529
  }));
446
530
  },
531
+ // DOM Events and Meta-Operations
447
532
  dispatchEvent: operation => {
448
533
  processElements(operation, (element => {
449
534
  before(element, operation);
450
535
  operate(operation, (() => {
451
536
  const {name: name, detail: detail} = operation;
452
- dispatch(element, name, detail);
537
+ dispatch(element, safeString(name), safeObject(detail));
453
538
  }));
454
539
  after(element, operation);
455
540
  }));
@@ -461,13 +546,22 @@
461
546
  let meta = document.head.querySelector(`meta[name='${name}']`);
462
547
  if (!meta) {
463
548
  meta = document.createElement("meta");
464
- meta.name = name;
549
+ meta.name = safeString(name);
465
550
  document.head.appendChild(meta);
466
551
  }
467
- meta.content = content;
552
+ meta.content = safeScalar(content);
553
+ }));
554
+ after(document, operation);
555
+ },
556
+ setTitle: operation => {
557
+ before(document, operation);
558
+ operate(operation, (() => {
559
+ const {title: title} = operation;
560
+ document.title = safeScalar(title);
468
561
  }));
469
562
  after(document, operation);
470
563
  },
564
+ // Browser Manipulations
471
565
  clearStorage: operation => {
472
566
  before(document, operation);
473
567
  operate(operation, (() => {
@@ -489,22 +583,28 @@
489
583
  before(window, operation);
490
584
  operate(operation, (() => {
491
585
  const {state: state, title: title, url: url} = operation;
492
- history.pushState(state || {}, title || "", url);
586
+ history.pushState(safeObject(state), safeString(title), safeString(url));
493
587
  }));
494
588
  after(window, operation);
495
589
  },
496
590
  redirectTo: operation => {
497
591
  before(window, operation);
498
592
  operate(operation, (() => {
499
- let {url: url, action: action} = operation;
593
+ let {url: url, action: action, turbo: turbo} = operation;
500
594
  action = action || "advance";
501
- if (window.Turbo) window.Turbo.visit(url, {
502
- action: action
503
- });
504
- if (window.Turbolinks) window.Turbolinks.visit(url, {
505
- action: action
506
- });
507
- if (!window.Turbo && !window.Turbolinks) window.location.href = url;
595
+ url = safeString(url);
596
+ if (turbo === undefined) turbo = true;
597
+ if (turbo) {
598
+ if (window.Turbo) window.Turbo.visit(url, {
599
+ action: action
600
+ });
601
+ if (window.Turbolinks) window.Turbolinks.visit(url, {
602
+ action: action
603
+ });
604
+ if (!window.Turbo && !window.Turbolinks) window.location.href = url;
605
+ } else {
606
+ window.location.href = url;
607
+ }
508
608
  }));
509
609
  after(window, operation);
510
610
  },
@@ -520,7 +620,7 @@
520
620
  operate(operation, (() => {
521
621
  const {key: key, type: type} = operation;
522
622
  const storage = type === "session" ? sessionStorage : localStorage;
523
- storage.removeItem(key);
623
+ storage.removeItem(safeString(key));
524
624
  }));
525
625
  after(document, operation);
526
626
  },
@@ -528,7 +628,7 @@
528
628
  before(window, operation);
529
629
  operate(operation, (() => {
530
630
  const {state: state, title: title, url: url} = operation;
531
- history.replaceState(state || {}, title || "", url);
631
+ history.replaceState(safeObject(state), safeString(title), safeString(url));
532
632
  }));
533
633
  after(window, operation);
534
634
  },
@@ -544,7 +644,7 @@
544
644
  before(document, operation);
545
645
  operate(operation, (() => {
546
646
  const {cookie: cookie} = operation;
547
- document.cookie = cookie || "";
647
+ document.cookie = safeScalar(cookie);
548
648
  }));
549
649
  after(document, operation);
550
650
  },
@@ -561,15 +661,16 @@
561
661
  operate(operation, (() => {
562
662
  const {key: key, value: value, type: type} = operation;
563
663
  const storage = type === "session" ? sessionStorage : localStorage;
564
- storage.setItem(key, value || "");
664
+ storage.setItem(safeString(key), safeScalar(value));
565
665
  }));
566
666
  after(document, operation);
567
667
  },
668
+ // Notifications
568
669
  consoleLog: operation => {
569
670
  before(document, operation);
570
671
  operate(operation, (() => {
571
672
  const {message: message, level: level} = operation;
572
- level && [ "warn", "info", "error" ].includes(level) ? console[level](message || "") : console.log(message || "");
673
+ level && [ "warn", "info", "error" ].includes(level) ? console[level](message) : console.log(message);
573
674
  }));
574
675
  after(document, operation);
575
676
  },
@@ -577,7 +678,7 @@
577
678
  before(document, operation);
578
679
  operate(operation, (() => {
579
680
  const {data: data, columns: columns} = operation;
580
- console.table(data, columns || []);
681
+ console.table(data, safeArray(columns));
581
682
  }));
582
683
  after(document, operation);
583
684
  },
@@ -587,10 +688,32 @@
587
688
  const {title: title, options: options} = operation;
588
689
  Notification.requestPermission().then((result => {
589
690
  operation.permission = result;
590
- if (result === "granted") new Notification(title || "", options);
691
+ if (result === "granted") new Notification(safeString(title), safeObject(options));
591
692
  }));
592
693
  }));
593
694
  after(document, operation);
695
+ },
696
+ // Morph operations
697
+ morph: operation => {
698
+ processElements(operation, (element => {
699
+ const {html: html} = operation;
700
+ const template = document.createElement("template");
701
+ template.innerHTML = String(safeScalar(html)).trim();
702
+ operation.content = template.content;
703
+ const parent = element.parentElement;
704
+ const idx = parent && Array.from(parent.children).indexOf(element);
705
+ before(element, operation);
706
+ operate(operation, (() => {
707
+ const {childrenOnly: childrenOnly, focusSelector: focusSelector} = operation;
708
+ morphdom(element, childrenOnly ? template.content : template.innerHTML, {
709
+ childrenOnly: !!childrenOnly,
710
+ onBeforeElUpdated: shouldMorph(operation),
711
+ onElUpdated: didMorph(operation)
712
+ });
713
+ assignFocus(focusSelector);
714
+ }));
715
+ after(parent ? parent.children[idx] : document.documentElement, operation);
716
+ }));
594
717
  }
595
718
  };
596
719
  let operations = Operations;
@@ -613,8 +736,17 @@
613
736
  return operations;
614
737
  }
615
738
  };
739
+ let missingElement = "warn";
740
+ var MissingElement$1 = {
741
+ get behavior() {
742
+ return missingElement;
743
+ },
744
+ set(value) {
745
+ if ([ "warn", "ignore", "event", "exception" ].includes(value)) missingElement = value; else console.warn("Invalid 'onMissingElement' option. Defaulting to 'warn'.");
746
+ }
747
+ };
616
748
  const perform = (operations, options = {
617
- emitMissingElementWarnings: true
749
+ onMissingElement: MissingElement$1.behavior
618
750
  }) => {
619
751
  const batches = {};
620
752
  operations.forEach((operation => {
@@ -624,11 +756,15 @@
624
756
  const name = operation.operation;
625
757
  try {
626
758
  if (operation.selector) {
627
- operation.element = operation.xpath ? xpathToElement(operation.selector) : document[operation.selectAll ? "querySelectorAll" : "querySelector"](operation.selector);
759
+ if (operation.xpath) {
760
+ operation.element = operation.selectAll ? xpathToElementArray(operation.selector) : xpathToElement(operation.selector);
761
+ } else {
762
+ operation.element = operation.selectAll ? document.querySelectorAll(operation.selector) : document.querySelector(operation.selector);
763
+ }
628
764
  } else {
629
765
  operation.element = document;
630
766
  }
631
- if (operation.element || options.emitMissingElementWarnings) {
767
+ if (operation.element || options.onMissingElement !== "ignore") {
632
768
  ActiveElement.set(document.activeElement);
633
769
  const cableReadyOperation = OperationStore.all[name];
634
770
  if (cableReadyOperation) {
@@ -645,13 +781,30 @@
645
781
  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.`);
646
782
  console.error(e);
647
783
  } else {
648
- console.warn(`CableReady ${name || "operation"} failed due to missing DOM element for selector: '${operation.selector}'`);
784
+ const warning = `CableReady ${name || ""} operation failed due to missing DOM element for selector: '${operation.selector}'`;
785
+ switch (options.onMissingElement) {
786
+ case "ignore":
787
+ break;
788
+
789
+ case "event":
790
+ dispatch(document, "cable-ready:missing-element", {
791
+ warning: warning,
792
+ operation: operation
793
+ });
794
+ break;
795
+
796
+ case "exception":
797
+ throw warning;
798
+
799
+ default:
800
+ console.warn(warning);
801
+ }
649
802
  }
650
803
  }
651
804
  }));
652
805
  };
653
806
  const performAsync = (operations, options = {
654
- emitMissingElementWarnings: true
807
+ onMissingElement: MissingElement$1.behavior
655
808
  }) => new Promise(((resolve, reject) => {
656
809
  try {
657
810
  resolve(perform(operations, options));
@@ -659,6 +812,33 @@
659
812
  reject(err);
660
813
  }
661
814
  }));
815
+ class SubscribingElement extends HTMLElement {
816
+ static get tagName() {
817
+ throw new Error("Implement the tagName() getter in the inheriting class");
818
+ }
819
+ static define() {
820
+ if (!customElements.get(this.tagName)) {
821
+ customElements.define(this.tagName, this);
822
+ }
823
+ }
824
+ disconnectedCallback() {
825
+ if (this.channel) this.channel.unsubscribe();
826
+ }
827
+ createSubscription(consumer, channel, receivedCallback) {
828
+ this.channel = consumer.subscriptions.create({
829
+ channel: channel,
830
+ identifier: this.identifier
831
+ }, {
832
+ received: receivedCallback
833
+ });
834
+ }
835
+ get preview() {
836
+ return document.documentElement.hasAttribute("data-turbolinks-preview") || document.documentElement.hasAttribute("data-turbo-preview");
837
+ }
838
+ get identifier() {
839
+ return this.getAttribute("identifier");
840
+ }
841
+ }
662
842
  let consumer;
663
843
  const BACKOFF = [ 25, 50, 75, 100, 200, 250, 500, 800, 1e3, 2e3 ];
664
844
  const wait = ms => new Promise((resolve => setTimeout(resolve, ms)));
@@ -681,41 +861,98 @@
681
861
  return await getConsumerWithRetry();
682
862
  }
683
863
  };
684
- class SubscribingElement extends HTMLElement {
685
- disconnectedCallback() {
686
- if (this.channel) this.channel.unsubscribe();
687
- }
688
- createSubscription(consumer, channel, receivedCallback) {
689
- this.channel = consumer.subscriptions.create({
690
- channel: channel,
691
- identifier: this.identifier
692
- }, {
693
- received: receivedCallback
694
- });
695
- }
696
- get preview() {
697
- return document.documentElement.hasAttribute("data-turbolinks-preview") || document.documentElement.hasAttribute("data-turbo-preview");
698
- }
699
- get identifier() {
700
- return this.getAttribute("identifier");
701
- }
702
- }
703
864
  class StreamFromElement extends SubscribingElement {
865
+ static get tagName() {
866
+ return "cable-ready-stream-from";
867
+ }
704
868
  async connectedCallback() {
705
869
  if (this.preview) return;
706
870
  const consumer = await CableConsumer.getConsumer();
707
871
  if (consumer) {
708
- this.createSubscription(consumer, "CableReady::Stream", this.performOperations);
872
+ this.createSubscription(consumer, "CableReady::Stream", this.performOperations.bind(this));
709
873
  } else {
710
- console.error("The `stream_from` helper cannot connect without an ActionCable consumer.\nPlease run `rails generate cable_ready:helpers` to fix this.");
874
+ console.error("The `cable_ready_stream_from` helper cannot connect. You must initialize CableReady with an Action Cable consumer.");
711
875
  }
712
876
  }
713
877
  performOperations(data) {
714
- if (data.cableReady) perform(data.operations);
878
+ if (data.cableReady) perform(data.operations, {
879
+ onMissingElement: this.onMissingElement
880
+ });
881
+ }
882
+ get onMissingElement() {
883
+ const value = this.getAttribute("missing") || MissingElement$1.behavior;
884
+ // stream_from does not support raising exceptions on missing elements because there's no way to catch them
885
+ if ([ "warn", "ignore", "event" ].includes(value)) return value; else {
886
+ console.warn("Invalid 'missing' attribute. Defaulting to 'warn'.");
887
+ return "warn";
888
+ }
715
889
  }
716
890
  }
891
+ let debugging = false;
892
+ var Debug = {
893
+ get enabled() {
894
+ return debugging;
895
+ },
896
+ get disabled() {
897
+ return !debugging;
898
+ },
899
+ get value() {
900
+ return debugging;
901
+ },
902
+ set(value) {
903
+ debugging = !!value;
904
+ },
905
+ set debug(value) {
906
+ debugging = !!value;
907
+ }
908
+ };
909
+ const request = (data, blocks) => {
910
+ if (Debug.disabled) return;
911
+ console.log(`↑ Updatable request affecting ${blocks.length} element(s): `, {
912
+ elements: blocks.map((b => b.element)),
913
+ identifiers: blocks.map((b => b.element.getAttribute("identifier"))),
914
+ data: data
915
+ });
916
+ };
917
+ const cancel = (timestamp, reason) => {
918
+ if (Debug.disabled) return;
919
+ const duration = new Date - timestamp;
920
+ console.log(`❌ Updatable request canceled after ${duration}ms: ${reason}`);
921
+ };
922
+ const response = (timestamp, element, urls) => {
923
+ if (Debug.disabled) return;
924
+ const duration = new Date - timestamp;
925
+ console.log(`↓ Updatable response: All URLs fetched in ${duration}ms`, {
926
+ element: element,
927
+ urls: urls
928
+ });
929
+ };
930
+ const morphStart = (timestamp, element) => {
931
+ if (Debug.disabled) return;
932
+ const duration = new Date - timestamp;
933
+ console.log(`↻ Updatable morph: starting after ${duration}ms`, {
934
+ element: element
935
+ });
936
+ };
937
+ const morphEnd = (timestamp, element) => {
938
+ if (Debug.disabled) return;
939
+ const duration = new Date - timestamp;
940
+ console.log(`↺ Updatable morph: completed after ${duration}ms`, {
941
+ element: element
942
+ });
943
+ };
944
+ var Log = {
945
+ request: request,
946
+ cancel: cancel,
947
+ response: response,
948
+ morphStart: morphStart,
949
+ morphEnd: morphEnd
950
+ };
717
951
  const template = `\n<style>\n :host {\n display: block;\n }\n</style>\n<slot></slot>\n`;
718
952
  class UpdatesForElement extends SubscribingElement {
953
+ static get tagName() {
954
+ return "cable-ready-updates-for";
955
+ }
719
956
  constructor() {
720
957
  super();
721
958
  const shadowRoot = this.attachShadow({
@@ -730,14 +967,26 @@
730
967
  if (consumer) {
731
968
  this.createSubscription(consumer, "CableReady::Stream", this.update);
732
969
  } else {
733
- console.error("The `updates-for` helper cannot connect without an ActionCable consumer.\nPlease run `rails generate cable_ready:helpers` to fix this.");
970
+ console.error("The `cable_ready_updates_for` helper cannot connect. You must initialize CableReady with an Action Cable consumer.");
734
971
  }
735
972
  }
736
973
  async update(data) {
737
- const blocks = Array.from(document.querySelectorAll(this.query), (element => new Block(element)));
738
- if (blocks[0].element !== this) return;
739
- ActiveElement.set(document.activeElement);
740
- this.html = {};
974
+ this.lastUpdateTimestamp = new Date;
975
+ const blocks = Array.from(document.querySelectorAll(this.query), (element => new Block(element))).filter((block => block.shouldUpdate(data)));
976
+ Log.request(data, blocks);
977
+ if (blocks.length === 0) {
978
+ Log.cancel(this.lastUpdateTimestamp, "All elements filtered out");
979
+ return;
980
+ }
981
+ // first updates-for element in the DOM *at any given moment* updates all of the others
982
+ if (blocks[0].element !== this) {
983
+ Log.cancel(this.lastUpdateTimestamp, "Update already requested");
984
+ return;
985
+ }
986
+ // hold a reference to the active element so that it can be restored after the morph
987
+ ActiveElement.set(document.activeElement);
988
+ // store all retrieved HTML in an object keyed by URL to minimize fetch calls
989
+ this.html = {};
741
990
  const uniqueUrls = [ ...new Set(blocks.map((block => block.url))) ];
742
991
  await Promise.all(uniqueUrls.map((async url => {
743
992
  if (!this.html.hasOwnProperty(url)) {
@@ -747,14 +996,17 @@
747
996
  this.html[url] = await response.text();
748
997
  }
749
998
  })));
750
- this.index = {};
999
+ Log.response(this.lastUpdateTimestamp, this, uniqueUrls);
1000
+ // track current block index for each URL; referred to as fragments
1001
+ this.index = {};
751
1002
  blocks.forEach((block => {
1003
+ // if the block's URL is not in the index, initialize it to 0; otherwise, increment it
752
1004
  this.index.hasOwnProperty(block.url) ? this.index[block.url]++ : this.index[block.url] = 0;
753
- block.process(data, this.html, this.index);
1005
+ block.process(data, this.html, this.index, this.lastUpdateTimestamp);
754
1006
  }));
755
1007
  }
756
1008
  get query() {
757
- return `updates-for[identifier="${this.identifier}"]`;
1009
+ return `${this.tagName}[identifier="${this.identifier}"]`;
758
1010
  }
759
1011
  get identifier() {
760
1012
  return this.getAttribute("identifier");
@@ -767,8 +1019,7 @@
767
1019
  constructor(element) {
768
1020
  this.element = element;
769
1021
  }
770
- async process(data, html, index) {
771
- if (!this.shouldUpdate(data)) return;
1022
+ async process(data, html, index, startTimestamp) {
772
1023
  const blockIndex = index[this.url];
773
1024
  const template = document.createElement("template");
774
1025
  this.element.setAttribute("updating", "updating");
@@ -785,7 +1036,8 @@
785
1036
  permanentAttributeName: "data-ignore-updates"
786
1037
  };
787
1038
  dispatch(this.element, "cable-ready:before-update", operation);
788
- morphdom__default["default"](this.element, fragments[blockIndex], {
1039
+ Log.morphStart(startTimestamp, this.element);
1040
+ morphdom(this.element, fragments[blockIndex], {
789
1041
  childrenOnly: true,
790
1042
  onBeforeElUpdated: shouldMorph(operation),
791
1043
  onElUpdated: _ => {
@@ -794,6 +1046,7 @@
794
1046
  assignFocus(operation.focusSelector);
795
1047
  }
796
1048
  });
1049
+ Log.morphEnd(startTimestamp, this.element);
797
1050
  }
798
1051
  async resolveTurboFrames(documentFragment) {
799
1052
  const reloadingTurboFrames = [ ...documentFragment.querySelectorAll('turbo-frame[src]:not([loading="lazy"])') ];
@@ -804,19 +1057,26 @@
804
1057
  });
805
1058
  const frameTemplate = document.createElement("template");
806
1059
  frameTemplate.innerHTML = await frameResponse.text();
807
- await this.resolveTurboFrames(frameTemplate.content);
808
- documentFragment.querySelector(`turbo-frame#${frame.id}`).innerHTML = String(frameTemplate.content.querySelector(`turbo-frame#${frame.id}`).innerHTML).trim();
1060
+ // recurse here to get all nested eager loaded frames
1061
+ await this.resolveTurboFrames(frameTemplate.content);
1062
+ const selector = `turbo-frame#${frame.id}`;
1063
+ const frameContent = frameTemplate.content.querySelector(selector);
1064
+ const content = frameContent ? frameContent.innerHTML.trim() : "";
1065
+ docFragment.querySelector(selector).innerHTML = content;
809
1066
  resolve();
810
1067
  })))));
811
1068
  }
812
1069
  shouldUpdate(data) {
1070
+ // if everything that could prevent an update is false, update this block
813
1071
  return !this.ignoresInnerUpdates && this.hasChangesSelectedForUpdate(data);
814
1072
  }
815
1073
  hasChangesSelectedForUpdate(data) {
1074
+ // if there's an only attribute, only update if at least one of the attributes changed is in the allow list
816
1075
  const only = this.element.getAttribute("only");
817
1076
  return !(only && data.changed && !only.split(" ").some((attribute => data.changed.includes(attribute))));
818
1077
  }
819
1078
  get ignoresInnerUpdates() {
1079
+ // don't update during a Reflex or Turbolinks redraw
820
1080
  return this.element.hasAttribute("ignore-inner-updates") && this.element.hasAttribute("performing-inner-update");
821
1081
  }
822
1082
  get url() {
@@ -846,35 +1106,51 @@
846
1106
  recursiveUnmarkUpdatesForElements(event.target);
847
1107
  }));
848
1108
  }));
1109
+ document.addEventListener("turbo-boost:command:start", (event => {
1110
+ recursiveMarkUpdatesForElements(event.target);
1111
+ }));
1112
+ document.addEventListener("turbo-boost:command:finish", (event => {
1113
+ setTimeout((() => {
1114
+ recursiveUnmarkUpdatesForElements(event.target);
1115
+ }));
1116
+ }));
1117
+ document.addEventListener("turbo-boost:command:error", (event => {
1118
+ setTimeout((() => {
1119
+ recursiveUnmarkUpdatesForElements(event.target);
1120
+ }));
1121
+ }));
849
1122
  };
850
1123
  const recursiveMarkUpdatesForElements = leaf => {
851
- const closestUpdatesFor = leaf && leaf.parentElement.closest("updates-for");
1124
+ const closestUpdatesFor = leaf && leaf.parentElement && leaf.parentElement.closest("cable-ready-updates-for");
852
1125
  if (closestUpdatesFor) {
853
1126
  closestUpdatesFor.setAttribute("performing-inner-update", "");
854
1127
  recursiveMarkUpdatesForElements(closestUpdatesFor);
855
1128
  }
856
1129
  };
857
1130
  const recursiveUnmarkUpdatesForElements = leaf => {
858
- const closestUpdatesFor = leaf && leaf.parentElement.closest("updates-for");
1131
+ const closestUpdatesFor = leaf && leaf.parentElement && leaf.parentElement.closest("cable-ready-updates-for");
859
1132
  if (closestUpdatesFor) {
860
1133
  closestUpdatesFor.removeAttribute("performing-inner-update");
861
1134
  recursiveUnmarkUpdatesForElements(closestUpdatesFor);
862
1135
  }
863
1136
  };
864
- const initialize = (initializeOptions = {}) => {
865
- const {consumer: consumer} = initializeOptions;
1137
+ const defineElements = () => {
866
1138
  registerInnerUpdates();
1139
+ StreamFromElement.define();
1140
+ UpdatesForElement.define();
1141
+ };
1142
+ const initialize = (initializeOptions = {}) => {
1143
+ const {consumer: consumer, onMissingElement: onMissingElement, debug: debug} = initializeOptions;
1144
+ Debug.set(!!debug);
867
1145
  if (consumer) {
868
1146
  CableConsumer.setConsumer(consumer);
869
1147
  } else {
870
1148
  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 })`.");
871
1149
  }
872
- if (!customElements.get("stream-from")) {
873
- customElements.define("stream-from", StreamFromElement);
874
- }
875
- if (!customElements.get("updates-for")) {
876
- customElements.define("updates-for", UpdatesForElement);
1150
+ if (onMissingElement) {
1151
+ MissingElement.set(onMissingElement);
877
1152
  }
1153
+ defineElements();
878
1154
  };
879
1155
  const global = {
880
1156
  perform: perform,
@@ -903,7 +1179,7 @@
903
1179
  exports.SubscribingElement = SubscribingElement;
904
1180
  exports.UpdatesForElement = UpdatesForElement;
905
1181
  exports.Utils = utils;
906
- exports["default"] = global;
1182
+ exports.default = global;
907
1183
  Object.defineProperty(exports, "__esModule", {
908
1184
  value: true
909
1185
  });