cable_ready 4.5.0 → 5.0.0

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