cable_ready 5.0.0.pre8 → 5.0.0.pre10

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