cable_ready 5.0.0.pre9 → 5.0.0.rc1

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