cable_ready 5.0.0.pre9 → 5.0.0.pre10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -1
  3. data/Gemfile.lock +119 -100
  4. data/README.md +2 -5
  5. data/app/assets/javascripts/cable_ready.js +457 -151
  6. data/app/assets/javascripts/cable_ready.min.js +1 -1
  7. data/app/assets/javascripts/cable_ready.min.js.map +1 -1
  8. data/app/assets/javascripts/cable_ready.umd.js +441 -165
  9. data/app/assets/javascripts/cable_ready.umd.min.js +1 -1
  10. data/app/assets/javascripts/cable_ready.umd.min.js.map +1 -1
  11. data/app/channels/cable_ready/stream.rb +7 -5
  12. data/app/helpers/cable_ready/view_helper.rb +58 -0
  13. data/app/jobs/cable_ready_broadcast_job.rb +9 -8
  14. data/app/models/concerns/cable_ready/updatable/collection_updatable_callbacks.rb +2 -0
  15. data/app/models/concerns/cable_ready/updatable/collections_registry.rb +2 -0
  16. data/app/models/concerns/cable_ready/updatable/model_updatable_callbacks.rb +5 -2
  17. data/app/models/concerns/cable_ready/updatable.rb +67 -19
  18. data/app/models/concerns/extend_has_many.rb +2 -0
  19. data/cable_ready.gemspec +4 -6
  20. data/lib/cable_ready/broadcaster.rb +2 -0
  21. data/lib/cable_ready/cable_car.rb +2 -0
  22. data/lib/cable_ready/channel.rb +12 -4
  23. data/lib/cable_ready/channels.rb +3 -1
  24. data/lib/cable_ready/config.rb +16 -2
  25. data/lib/cable_ready/engine.rb +19 -9
  26. data/lib/cable_ready/identifiable.rb +23 -5
  27. data/lib/cable_ready/importmap.rb +2 -0
  28. data/lib/cable_ready/installer.rb +224 -0
  29. data/lib/cable_ready/operation_builder.rb +1 -1
  30. data/lib/cable_ready/sanity_checker.rb +1 -31
  31. data/lib/cable_ready/version.rb +1 -1
  32. data/lib/cable_ready.rb +3 -8
  33. data/lib/cable_ready_helper.rb +13 -0
  34. data/lib/generators/cable_ready/channel_generator.rb +51 -12
  35. data/lib/generators/cable_ready/templates/config/initializers/cable_ready.rb +10 -6
  36. data/lib/install/action_cable.rb +144 -0
  37. data/lib/install/broadcaster.rb +109 -0
  38. data/lib/install/bundle.rb +54 -0
  39. data/lib/install/compression.rb +51 -0
  40. data/lib/install/config.rb +39 -0
  41. data/lib/install/development.rb +34 -0
  42. data/lib/install/esbuild.rb +101 -0
  43. data/lib/install/importmap.rb +96 -0
  44. data/lib/install/initializers.rb +15 -0
  45. data/lib/install/mrujs.rb +121 -0
  46. data/lib/install/npm_packages.rb +13 -0
  47. data/lib/install/shakapacker.rb +61 -0
  48. data/lib/install/spring.rb +54 -0
  49. data/lib/install/updatable.rb +34 -0
  50. data/lib/install/vite.rb +62 -0
  51. data/lib/install/webpacker.rb +81 -0
  52. data/lib/install/yarn.rb +56 -0
  53. data/lib/tasks/cable_ready/cable_ready.rake +249 -0
  54. data/package.json +28 -18
  55. data/{rollup.config.js → rollup.config.mjs} +9 -8
  56. data/web-test-runner.config.mjs +12 -0
  57. data/yarn.lock +2210 -404
  58. metadata +37 -161
  59. data/LATEST +0 -1
  60. data/app/helpers/cable_ready_helper.rb +0 -26
  61. data/lib/generators/cable_ready/helpers_generator.rb +0 -43
  62. data/lib/generators/cable_ready/initializer_generator.rb +0 -14
  63. data/test/dummy/app/channels/application_cable/channel.rb +0 -4
  64. data/test/dummy/app/channels/application_cable/connection.rb +0 -4
  65. data/test/dummy/app/controllers/application_controller.rb +0 -2
  66. data/test/dummy/app/helpers/application_helper.rb +0 -2
  67. data/test/dummy/app/jobs/application_job.rb +0 -7
  68. data/test/dummy/app/mailers/application_mailer.rb +0 -4
  69. data/test/dummy/app/models/application_record.rb +0 -3
  70. data/test/dummy/app/models/dugong.rb +0 -4
  71. data/test/dummy/app/models/global_idable_entity.rb +0 -16
  72. data/test/dummy/app/models/post.rb +0 -4
  73. data/test/dummy/app/models/section.rb +0 -6
  74. data/test/dummy/app/models/team.rb +0 -6
  75. data/test/dummy/app/models/topic.rb +0 -4
  76. data/test/dummy/app/models/user.rb +0 -7
  77. data/test/dummy/config/application.rb +0 -22
  78. data/test/dummy/config/boot.rb +0 -5
  79. data/test/dummy/config/environment.rb +0 -5
  80. data/test/dummy/config/environments/development.rb +0 -76
  81. data/test/dummy/config/environments/production.rb +0 -120
  82. data/test/dummy/config/environments/test.rb +0 -59
  83. data/test/dummy/config/initializers/application_controller_renderer.rb +0 -8
  84. data/test/dummy/config/initializers/assets.rb +0 -12
  85. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -8
  86. data/test/dummy/config/initializers/cable_ready.rb +0 -18
  87. data/test/dummy/config/initializers/content_security_policy.rb +0 -28
  88. data/test/dummy/config/initializers/cookies_serializer.rb +0 -5
  89. data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -6
  90. data/test/dummy/config/initializers/inflections.rb +0 -16
  91. data/test/dummy/config/initializers/mime_types.rb +0 -4
  92. data/test/dummy/config/initializers/permissions_policy.rb +0 -11
  93. data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
  94. data/test/dummy/config/puma.rb +0 -43
  95. data/test/dummy/config/routes.rb +0 -3
  96. data/test/dummy/db/migrate/20210902154139_create_users.rb +0 -9
  97. data/test/dummy/db/migrate/20210902154153_create_posts.rb +0 -10
  98. data/test/dummy/db/migrate/20210904081930_create_topics.rb +0 -9
  99. data/test/dummy/db/migrate/20210904093607_create_sections.rb +0 -9
  100. data/test/dummy/db/migrate/20210913191735_create_teams.rb +0 -8
  101. data/test/dummy/db/migrate/20210913191759_add_team_reference_to_users.rb +0 -5
  102. data/test/dummy/db/migrate/20220329222959_create_dugongs.rb +0 -8
  103. data/test/dummy/db/migrate/20220329230221_create_active_storage_tables.active_storage.rb +0 -36
  104. data/test/dummy/db/schema.rb +0 -84
  105. data/test/dummy/test/models/dugong_test.rb +0 -7
  106. data/test/dummy/test/models/post_test.rb +0 -7
  107. data/test/dummy/test/models/section_test.rb +0 -7
  108. data/test/dummy/test/models/team_test.rb +0 -7
  109. data/test/dummy/test/models/topic_test.rb +0 -7
  110. data/test/dummy/test/models/user_test.rb +0 -7
  111. data/test/lib/cable_ready/cable_car_test.rb +0 -50
  112. data/test/lib/cable_ready/compoundable_test.rb +0 -26
  113. data/test/lib/cable_ready/helper_test.rb +0 -25
  114. data/test/lib/cable_ready/identifiable_test.rb +0 -69
  115. data/test/lib/cable_ready/operation_builder_test.rb +0 -189
  116. data/test/lib/cable_ready/updatable_test.rb +0 -135
  117. data/test/lib/generators/cable_ready/channel_generator_test.rb +0 -157
  118. data/test/support/generator_test_helpers.rb +0 -28
  119. data/test/test_helper.rb +0 -18
@@ -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
  });