stimulus_reflex 3.5.0.pre8 → 3.5.0.pre9

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of stimulus_reflex might be problematic. Click here for more details.

Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -1218
  3. data/Gemfile.lock +109 -152
  4. data/README.md +39 -5
  5. data/Rakefile +0 -8
  6. data/app/assets/javascripts/stimulus_reflex.js +969 -0
  7. data/app/assets/javascripts/stimulus_reflex.min.js +2 -0
  8. data/app/assets/javascripts/stimulus_reflex.min.js.map +1 -0
  9. data/app/assets/javascripts/stimulus_reflex.umd.js +904 -0
  10. data/app/assets/javascripts/stimulus_reflex.umd.min.js +905 -0
  11. data/app/assets/javascripts/stimulus_reflex.umd.min.js.map +1 -0
  12. data/app/channels/stimulus_reflex/channel.rb +20 -1
  13. data/lib/stimulus_reflex/engine.rb +29 -0
  14. data/lib/stimulus_reflex/importmap.rb +4 -0
  15. data/lib/stimulus_reflex/open_struct_fix.rb +29 -0
  16. data/lib/stimulus_reflex/reflex.rb +9 -2
  17. data/lib/stimulus_reflex/reflex_data.rb +4 -0
  18. data/lib/stimulus_reflex/reflex_factory.rb +2 -1
  19. data/lib/stimulus_reflex/utils/logger.rb +2 -0
  20. data/lib/stimulus_reflex/utils/sanity_checker.rb +0 -58
  21. data/lib/stimulus_reflex/version.rb +1 -1
  22. data/lib/stimulus_reflex.rb +2 -6
  23. data/package.json +67 -0
  24. data/rollup.config.js +85 -0
  25. data/stimulus_reflex.gemspec +63 -0
  26. data/test/broadcasters/broadcaster_test_case.rb +1 -1
  27. data/test/broadcasters/nothing_broadcaster_test.rb +2 -1
  28. data/test/broadcasters/page_broadcaster_test.rb +4 -2
  29. data/test/broadcasters/selector_broadcaster_test.rb +12 -6
  30. data/test/callbacks_test.rb +23 -23
  31. data/test/reflex_test.rb +2 -2
  32. data/test/tmp/app/reflexes/application_reflex.rb +3 -10
  33. data/test/tmp/app/reflexes/{demo_reflex.rb → user_reflex.rb} +10 -2
  34. data/web-test-runner.config.mjs +12 -0
  35. data/yarn-error.log +4964 -0
  36. data/yarn.lock +4520 -0
  37. metadata +107 -41
  38. data/lib/generators/USAGE +0 -14
  39. data/lib/generators/stimulus_reflex/templates/app/javascript/controllers/%file_name%_controller.js.tt +0 -101
  40. data/lib/generators/stimulus_reflex/templates/app/javascript/controllers/application_controller.js.tt +0 -60
  41. data/lib/generators/stimulus_reflex/templates/app/reflexes/%file_name%_reflex.rb.tt +0 -41
  42. data/lib/generators/stimulus_reflex/templates/app/reflexes/application_reflex.rb.tt +0 -19
  43. data/lib/tasks/stimulus_reflex/install.rake +0 -116
@@ -0,0 +1,969 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ import CableReady from "cable_ready";
4
+
5
+ import { createConsumer } from "@rails/actioncable";
6
+
7
+ const defaultSchema = {
8
+ reflexAttribute: "data-reflex",
9
+ reflexPermanentAttribute: "data-reflex-permanent",
10
+ reflexRootAttribute: "data-reflex-root",
11
+ reflexSuppressLoggingAttribute: "data-reflex-suppress-logging",
12
+ reflexDatasetAttribute: "data-reflex-dataset",
13
+ reflexDatasetAllAttribute: "data-reflex-dataset-all",
14
+ reflexSerializeFormAttribute: "data-reflex-serialize-form",
15
+ reflexFormSelectorAttribute: "data-reflex-form-selector",
16
+ reflexIncludeInnerHtmlAttribute: "data-reflex-include-inner-html",
17
+ reflexIncludeTextContentAttribute: "data-reflex-include-text-content"
18
+ };
19
+
20
+ let schema = {};
21
+
22
+ var Schema = {
23
+ set(application) {
24
+ schema = {
25
+ ...defaultSchema,
26
+ ...application.schema
27
+ };
28
+ for (const attribute in schema) {
29
+ Object.defineProperty(this, attribute.slice(0, -9), {
30
+ get: () => schema[attribute]
31
+ });
32
+ }
33
+ }
34
+ };
35
+
36
+ let debugging = false;
37
+
38
+ var Debug$1 = {
39
+ get enabled() {
40
+ return debugging;
41
+ },
42
+ get disabled() {
43
+ return !debugging;
44
+ },
45
+ get value() {
46
+ return debugging;
47
+ },
48
+ set(value) {
49
+ debugging = !!value;
50
+ },
51
+ set debug(value) {
52
+ debugging = !!value;
53
+ }
54
+ };
55
+
56
+ const reflexes = {};
57
+
58
+ const request = (reflexId, target, args, controller, element, controllerElement) => {
59
+ const reflex = reflexes[reflexId];
60
+ if (Debug$1.disabled || reflex.promise.data.suppressLogging) return;
61
+ reflex.timestamp = new Date;
62
+ console.log(`↑ stimulus ↑ ${target}`, {
63
+ reflexId: reflexId,
64
+ args: args,
65
+ controller: controller,
66
+ element: element,
67
+ controllerElement: controllerElement
68
+ });
69
+ };
70
+
71
+ const success = (event, halted) => {
72
+ const {detail: detail} = event || {};
73
+ const {selector: selector, payload: payload} = detail || {};
74
+ const {reflexId: reflexId, target: target, morph: morph} = detail.stimulusReflex || {};
75
+ const reflex = reflexes[reflexId];
76
+ if (Debug$1.disabled || reflex.promise.data.suppressLogging) return;
77
+ const progress = reflex.totalOperations > 1 ? ` ${reflex.completedOperations}/${reflex.totalOperations}` : "";
78
+ const duration = reflex.timestamp ? `in ${new Date - reflex.timestamp}ms` : "CLONED";
79
+ const operation = event.type.split(":")[1].split("-").slice(1).join("_");
80
+ console.log(`↓ reflex ↓ ${target} → ${selector || "∞"}${progress} ${duration}`, {
81
+ reflexId: reflexId,
82
+ morph: morph,
83
+ operation: operation,
84
+ halted: halted,
85
+ payload: payload
86
+ });
87
+ };
88
+
89
+ const error$1 = event => {
90
+ const {detail: detail} = event || {};
91
+ const {reflexId: reflexId, target: target, payload: payload} = detail.stimulusReflex || {};
92
+ const reflex = reflexes[reflexId];
93
+ if (Debug$1.disabled || reflex.promise.data.suppressLogging) return;
94
+ const duration = reflex.timestamp ? `in ${new Date - reflex.timestamp}ms` : "CLONED";
95
+ console.log(`↓ reflex ↓ ${target} ${duration} %cERROR: ${event.detail.body}`, "color: #f00;", {
96
+ reflexId: reflexId,
97
+ payload: payload
98
+ });
99
+ };
100
+
101
+ var Log = {
102
+ request: request,
103
+ success: success,
104
+ error: error$1
105
+ };
106
+
107
+ let deprecationWarnings = true;
108
+
109
+ var Deprecate = {
110
+ get enabled() {
111
+ return deprecationWarnings;
112
+ },
113
+ get disabled() {
114
+ return !deprecationWarnings;
115
+ },
116
+ get value() {
117
+ return deprecationWarnings;
118
+ },
119
+ set(value) {
120
+ deprecationWarnings = !!value;
121
+ },
122
+ set deprecate(value) {
123
+ deprecationWarnings = !!value;
124
+ }
125
+ };
126
+
127
+ const uuidv4 = () => {
128
+ const crypto = window.crypto || window.msCrypto;
129
+ return ([ 1e7 ] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)));
130
+ };
131
+
132
+ const serializeForm = (form, options = {}) => {
133
+ if (!form) return "";
134
+ const w = options.w || window;
135
+ const {element: element} = options;
136
+ const formData = new w.FormData(form);
137
+ const data = Array.from(formData, (e => e.map(encodeURIComponent).join("=")));
138
+ const submitButton = form.querySelector("input[type=submit]");
139
+ if (element && element.name && element.nodeName === "INPUT" && element.type === "submit") {
140
+ data.push(`${encodeURIComponent(element.name)}=${encodeURIComponent(element.value)}`);
141
+ } else if (submitButton && submitButton.name) {
142
+ data.push(`${encodeURIComponent(submitButton.name)}=${encodeURIComponent(submitButton.value)}`);
143
+ }
144
+ return Array.from(data).join("&");
145
+ };
146
+
147
+ const camelize = (value, uppercaseFirstLetter = true) => {
148
+ if (typeof value !== "string") return "";
149
+ value = value.replace(/[\s_](.)/g, ($1 => $1.toUpperCase())).replace(/[\s_]/g, "").replace(/^(.)/, ($1 => $1.toLowerCase()));
150
+ if (uppercaseFirstLetter) value = value.substr(0, 1).toUpperCase() + value.substr(1);
151
+ return value;
152
+ };
153
+
154
+ const debounce = (callback, delay = 250) => {
155
+ let timeoutId;
156
+ return (...args) => {
157
+ clearTimeout(timeoutId);
158
+ timeoutId = setTimeout((() => {
159
+ timeoutId = null;
160
+ callback(...args);
161
+ }), delay);
162
+ };
163
+ };
164
+
165
+ const extractReflexName = reflexString => {
166
+ const match = reflexString.match(/(?:.*->)?(.*?)(?:Reflex)?#/);
167
+ return match ? match[1] : "";
168
+ };
169
+
170
+ const emitEvent = (event, detail) => {
171
+ document.dispatchEvent(new CustomEvent(event, {
172
+ bubbles: true,
173
+ cancelable: false,
174
+ detail: detail
175
+ }));
176
+ if (window.jQuery) window.jQuery(document).trigger(event, detail);
177
+ };
178
+
179
+ const elementToXPath = element => {
180
+ if (element.id !== "") return "//*[@id='" + element.id + "']";
181
+ if (element === document.body) return "/html/body";
182
+ let ix = 0;
183
+ const siblings = element?.parentNode ? element.parentNode.childNodes : [];
184
+ for (var i = 0; i < siblings.length; i++) {
185
+ const sibling = siblings[i];
186
+ if (sibling === element) {
187
+ const computedPath = elementToXPath(element.parentNode);
188
+ const tagName = element.tagName.toLowerCase();
189
+ const ixInc = ix + 1;
190
+ return `${computedPath}/${tagName}[${ixInc}]`;
191
+ }
192
+ if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
193
+ ix++;
194
+ }
195
+ }
196
+ };
197
+
198
+ const XPathToElement = xpath => document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
199
+
200
+ const XPathToArray = (xpath, reverse = false) => {
201
+ const snapshotList = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
202
+ const snapshots = [];
203
+ for (let i = 0; i < snapshotList.snapshotLength; i++) {
204
+ snapshots.push(snapshotList.snapshotItem(i));
205
+ }
206
+ return reverse ? snapshots.reverse() : snapshots;
207
+ };
208
+
209
+ const multipleInstances = element => {
210
+ if ([ "checkbox", "radio" ].includes(element.type)) {
211
+ return document.querySelectorAll(`input[type="${element.type}"][name="${element.name}"]`).length > 1;
212
+ }
213
+ return false;
214
+ };
215
+
216
+ const collectCheckedOptions = element => Array.from(element.querySelectorAll("option:checked")).concat(Array.from(document.querySelectorAll(`input[type="${element.type}"][name="${element.name}"]`)).filter((elem => elem.checked))).map((o => o.value));
217
+
218
+ const attributeValue = (values = []) => {
219
+ const value = values.filter((v => v && String(v).length)).map((v => v.trim())).join(" ").trim();
220
+ return value.length ? value : null;
221
+ };
222
+
223
+ const attributeValues = value => {
224
+ if (!value) return [];
225
+ if (!value.length) return [];
226
+ return value.split(" ").filter((v => v.trim().length));
227
+ };
228
+
229
+ const extractElementAttributes = element => {
230
+ let attrs = Array.from(element.attributes).reduce(((memo, attr) => {
231
+ memo[attr.name] = attr.value;
232
+ return memo;
233
+ }), {});
234
+ attrs.checked = !!element.checked;
235
+ attrs.selected = !!element.selected;
236
+ attrs.tag_name = element.tagName;
237
+ if (element.tagName.match(/select/i) || multipleInstances(element)) {
238
+ const collectedOptions = collectCheckedOptions(element);
239
+ attrs.values = collectedOptions;
240
+ attrs.value = collectedOptions.join(",");
241
+ } else {
242
+ attrs.value = element.value;
243
+ }
244
+ return attrs;
245
+ };
246
+
247
+ const getElementsFromTokens = (element, tokens) => {
248
+ if (!tokens || tokens.length === 0) return [];
249
+ let elements = [ element ];
250
+ const xPath = elementToXPath(element);
251
+ tokens.forEach((token => {
252
+ try {
253
+ switch (token) {
254
+ case "combined":
255
+ if (Deprecate.enabled) console.warn("In the next version of StimulusReflex, the 'combined' option to data-reflex-dataset will become 'ancestors'.");
256
+ elements = [ ...elements, ...XPathToArray(`${xPath}/ancestor::*`, true) ];
257
+ break;
258
+
259
+ case "ancestors":
260
+ elements = [ ...elements, ...XPathToArray(`${xPath}/ancestor::*`, true) ];
261
+ break;
262
+
263
+ case "parent":
264
+ elements = [ ...elements, ...XPathToArray(`${xPath}/parent::*`) ];
265
+ break;
266
+
267
+ case "siblings":
268
+ elements = [ ...elements, ...XPathToArray(`${xPath}/preceding-sibling::*|${xPath}/following-sibling::*`) ];
269
+ break;
270
+
271
+ case "children":
272
+ elements = [ ...elements, ...XPathToArray(`${xPath}/child::*`) ];
273
+ break;
274
+
275
+ case "descendants":
276
+ elements = [ ...elements, ...XPathToArray(`${xPath}/descendant::*`) ];
277
+ break;
278
+
279
+ default:
280
+ elements = [ ...elements, ...document.querySelectorAll(token) ];
281
+ }
282
+ } catch (error) {
283
+ if (Debug$1.enabled) console.error(error);
284
+ }
285
+ }));
286
+ return elements;
287
+ };
288
+
289
+ const extractElementDataset = element => {
290
+ const dataset = element.attributes[Schema.reflexDataset];
291
+ const allDataset = element.attributes[Schema.reflexDatasetAll];
292
+ const tokens = dataset && dataset.value.split(" ") || [];
293
+ const allTokens = allDataset && allDataset.value.split(" ") || [];
294
+ const datasetElements = getElementsFromTokens(element, tokens);
295
+ const datasetAllElements = getElementsFromTokens(element, allTokens);
296
+ const datasetAttributes = datasetElements.reduce(((acc, ele) => ({
297
+ ...extractDataAttributes(ele),
298
+ ...acc
299
+ })), {});
300
+ const reflexElementAttributes = extractDataAttributes(element);
301
+ const elementDataset = {
302
+ dataset: {
303
+ ...reflexElementAttributes,
304
+ ...datasetAttributes
305
+ },
306
+ datasetAll: {}
307
+ };
308
+ datasetAllElements.forEach((element => {
309
+ const elementAttributes = extractDataAttributes(element);
310
+ Object.keys(elementAttributes).forEach((key => {
311
+ const value = elementAttributes[key];
312
+ if (elementDataset.datasetAll[key] && Array.isArray(elementDataset.datasetAll[key])) {
313
+ elementDataset.datasetAll[key].push(value);
314
+ } else {
315
+ elementDataset.datasetAll[key] = [ value ];
316
+ }
317
+ }));
318
+ }));
319
+ return elementDataset;
320
+ };
321
+
322
+ const extractDataAttributes = element => {
323
+ let attrs = {};
324
+ if (element && element.attributes) {
325
+ Array.from(element.attributes).forEach((attr => {
326
+ if (attr.name.startsWith("data-")) {
327
+ attrs[attr.name] = attr.value;
328
+ }
329
+ }));
330
+ }
331
+ return attrs;
332
+ };
333
+
334
+ let isolationMode = false;
335
+
336
+ var IsolationMode = {
337
+ get disabled() {
338
+ return !isolationMode;
339
+ },
340
+ set(value) {
341
+ isolationMode = value;
342
+ }
343
+ };
344
+
345
+ const invokeLifecycleMethod = (stage, reflexElement, controllerElement, reflexId, payload) => {
346
+ if (!controllerElement || !controllerElement.reflexData[reflexId]) return;
347
+ const controller = controllerElement.reflexController[reflexId];
348
+ const reflex = controllerElement.reflexData[reflexId].target;
349
+ const reflexMethodName = reflex.split("#")[1];
350
+ const specificLifecycleMethodName = [ "before", "after", "finalize" ].includes(stage) ? `${stage}${camelize(reflexMethodName)}` : `${camelize(reflexMethodName, false)}${camelize(stage)}`;
351
+ const specificLifecycleMethod = controller[specificLifecycleMethodName];
352
+ const genericLifecycleMethodName = [ "before", "after", "finalize" ].includes(stage) ? `${stage}Reflex` : `reflex${camelize(stage)}`;
353
+ const genericLifecycleMethod = controller[genericLifecycleMethodName];
354
+ if (typeof specificLifecycleMethod === "function") {
355
+ specificLifecycleMethod.call(controller, reflexElement, reflex, controllerElement.reflexError[reflexId], reflexId, payload);
356
+ }
357
+ if (typeof genericLifecycleMethod === "function") {
358
+ genericLifecycleMethod.call(controller, reflexElement, reflex, controllerElement.reflexError[reflexId], reflexId, payload);
359
+ }
360
+ if (reflexes[reflexId] && stage === reflexes[reflexId].finalStage) {
361
+ Reflect.deleteProperty(controllerElement.reflexController, reflexId);
362
+ Reflect.deleteProperty(controllerElement.reflexData, reflexId);
363
+ Reflect.deleteProperty(controllerElement.reflexError, reflexId);
364
+ }
365
+ };
366
+
367
+ document.addEventListener("stimulus-reflex:before", (event => invokeLifecycleMethod("before", event.detail.element, event.detail.controller.element, event.detail.reflexId, event.detail.payload)), true);
368
+
369
+ document.addEventListener("stimulus-reflex:success", (event => {
370
+ invokeLifecycleMethod("success", event.detail.element, event.detail.controller.element, event.detail.reflexId, event.detail.payload);
371
+ dispatchLifecycleEvent("after", event.detail.element, event.detail.controller.element, event.detail.reflexId, event.detail.payload);
372
+ }), true);
373
+
374
+ document.addEventListener("stimulus-reflex:nothing", (event => {
375
+ dispatchLifecycleEvent("success", event.detail.element, event.detail.controller.element, event.detail.reflexId, event.detail.payload);
376
+ }), true);
377
+
378
+ document.addEventListener("stimulus-reflex:error", (event => {
379
+ invokeLifecycleMethod("error", event.detail.element, event.detail.controller.element, event.detail.reflexId, event.detail.payload);
380
+ dispatchLifecycleEvent("after", event.detail.element, event.detail.controller.element, event.detail.reflexId, event.detail.payload);
381
+ }), true);
382
+
383
+ document.addEventListener("stimulus-reflex:halted", (event => invokeLifecycleMethod("halted", event.detail.element, event.detail.controller.element, event.detail.reflexId, event.detail.payload)), true);
384
+
385
+ document.addEventListener("stimulus-reflex:after", (event => invokeLifecycleMethod("after", event.detail.element, event.detail.controller.element, event.detail.reflexId, event.detail.payload)), true);
386
+
387
+ document.addEventListener("stimulus-reflex:finalize", (event => invokeLifecycleMethod("finalize", event.detail.element, event.detail.controller.element, event.detail.reflexId, event.detail.payload)), true);
388
+
389
+ const dispatchLifecycleEvent = (stage, reflexElement, controllerElement, reflexId, payload) => {
390
+ if (!controllerElement) {
391
+ if (Debug$1.enabled && !reflexes[reflexId].warned) {
392
+ console.warn(`StimulusReflex was not able execute callbacks or emit events for "${stage}" or later life-cycle stages for this Reflex. The StimulusReflex Controller Element is no longer present in the DOM. Could you move the StimulusReflex Controller to an element higher in your DOM?`);
393
+ reflexes[reflexId].warned = true;
394
+ }
395
+ return;
396
+ }
397
+ if (!controllerElement.reflexController || controllerElement.reflexController && !controllerElement.reflexController[reflexId]) {
398
+ if (Debug$1.enabled && !reflexes[reflexId].warned) {
399
+ console.warn(`StimulusReflex detected that the StimulusReflex Controller responsible for this Reflex has been replaced with a new instance. Callbacks and events for "${stage}" or later life-cycle stages cannot be executed.`);
400
+ reflexes[reflexId].warned = true;
401
+ }
402
+ return;
403
+ }
404
+ const {target: target} = controllerElement.reflexData[reflexId] || {};
405
+ const controller = controllerElement.reflexController[reflexId] || {};
406
+ const event = `stimulus-reflex:${stage}`;
407
+ const action = `${event}:${target.split("#")[1]}`;
408
+ const detail = {
409
+ reflex: target,
410
+ controller: controller,
411
+ reflexId: reflexId,
412
+ element: reflexElement,
413
+ payload: payload
414
+ };
415
+ const options = {
416
+ bubbles: true,
417
+ cancelable: false,
418
+ detail: detail
419
+ };
420
+ controllerElement.dispatchEvent(new CustomEvent(event, options));
421
+ controllerElement.dispatchEvent(new CustomEvent(action, options));
422
+ if (window.jQuery) {
423
+ window.jQuery(controllerElement).trigger(event, detail);
424
+ window.jQuery(controllerElement).trigger(action, detail);
425
+ }
426
+ };
427
+
428
+ const localReflexControllers = (app, element) => attributeValues(element.getAttribute(Schema.controller)).reduce(((memo, name) => {
429
+ const controller = app.getControllerForElementAndIdentifier(element, name);
430
+ if (controller && controller.StimulusReflex) memo.push(controller);
431
+ return memo;
432
+ }), []);
433
+
434
+ const allReflexControllers = (app, element) => {
435
+ let controllers = [];
436
+ while (element) {
437
+ controllers = controllers.concat(localReflexControllers(app, element));
438
+ element = element.parentElement;
439
+ }
440
+ return controllers;
441
+ };
442
+
443
+ const findControllerByReflexName = (reflexName, controllers) => {
444
+ const controller = controllers.find((controller => {
445
+ if (!controller.identifier) return;
446
+ return extractReflexName(reflexName).replace(/([a-z0–9])([A-Z])/g, "$1-$2").replace(/(::)/g, "--").toLowerCase() === controller.identifier;
447
+ }));
448
+ return controller || controllers[0];
449
+ };
450
+
451
+ const received = data => {
452
+ if (!data.cableReady) return;
453
+ if (data.version.replace(".pre", "-pre") !== CableReady.version) {
454
+ if (Debug$1.enabled) console.error(`Reflex failed due to cable_ready gem/NPM package version mismatch. Package versions must match exactly.\nNote that if you are using pre-release builds, gems use the "x.y.z.preN" version format, while NPM packages use "x.y.z-preN".\n\ncable_ready gem: ${data.version}\ncable_ready NPM: ${CableReady.version}`);
455
+ return;
456
+ }
457
+ let reflexOperations = [];
458
+ for (let i = data.operations.length - 1; i >= 0; i--) {
459
+ if (data.operations[i].stimulusReflex) {
460
+ reflexOperations.push(data.operations[i]);
461
+ data.operations.splice(i, 1);
462
+ }
463
+ }
464
+ if (reflexOperations.some((operation => operation.stimulusReflex.url !== location.href))) {
465
+ return;
466
+ }
467
+ let reflexData;
468
+ if (reflexOperations.length) {
469
+ reflexData = reflexOperations[0].stimulusReflex;
470
+ reflexData.payload = reflexOperations[0].payload;
471
+ }
472
+ if (reflexData) {
473
+ const {reflexId: reflexId, payload: payload} = reflexData;
474
+ if (!reflexes[reflexId] && IsolationMode.disabled) {
475
+ const controllerElement = XPathToElement(reflexData.xpathController);
476
+ const reflexElement = XPathToElement(reflexData.xpathElement);
477
+ controllerElement.reflexController = controllerElement.reflexController || {};
478
+ controllerElement.reflexData = controllerElement.reflexData || {};
479
+ controllerElement.reflexError = controllerElement.reflexError || {};
480
+ controllerElement.reflexController[reflexId] = reflexes.app.getControllerForElementAndIdentifier(controllerElement, reflexData.reflexController);
481
+ controllerElement.reflexData[reflexId] = reflexData;
482
+ dispatchLifecycleEvent("before", reflexElement, controllerElement, reflexId, payload);
483
+ registerReflex(reflexData);
484
+ }
485
+ if (reflexes[reflexId]) {
486
+ reflexes[reflexId].totalOperations = reflexOperations.length;
487
+ reflexes[reflexId].pendingOperations = reflexOperations.length;
488
+ reflexes[reflexId].completedOperations = 0;
489
+ reflexes[reflexId].piggybackOperations = data.operations;
490
+ CableReady.perform(reflexOperations);
491
+ }
492
+ } else {
493
+ if (data.operations.length && reflexes[data.operations[0].reflexId]) {
494
+ CableReady.perform(data.operations);
495
+ }
496
+ }
497
+ };
498
+
499
+ const registerReflex = data => {
500
+ const {reflexId: reflexId} = data;
501
+ reflexes[reflexId] = {
502
+ finalStage: "finalize"
503
+ };
504
+ const promise = new Promise(((resolve, reject) => {
505
+ reflexes[reflexId].promise = {
506
+ resolve: resolve,
507
+ reject: reject,
508
+ data: data
509
+ };
510
+ }));
511
+ promise.reflexId = reflexId;
512
+ if (Debug$1.enabled) promise.catch((() => {}));
513
+ return promise;
514
+ };
515
+
516
+ const getReflexRoots = element => {
517
+ let list = [];
518
+ while (list.length === 0 && element) {
519
+ let reflexRoot = element.getAttribute(Schema.reflexRoot);
520
+ if (reflexRoot) {
521
+ if (reflexRoot.length === 0 && element.id) reflexRoot = `#${element.id}`;
522
+ const selectors = reflexRoot.split(",").filter((s => s.trim().length));
523
+ if (Debug$1.enabled && selectors.length === 0) {
524
+ console.error(`No value found for ${Schema.reflexRoot}. Add an #id to the element or provide a value for ${Schema.reflexRoot}.`, element);
525
+ }
526
+ list = list.concat(selectors.filter((s => document.querySelector(s))));
527
+ }
528
+ element = element.parentElement ? element.parentElement.closest(`[${Schema.reflexRoot}]`) : null;
529
+ }
530
+ return list;
531
+ };
532
+
533
+ const setupDeclarativeReflexes = debounce((() => {
534
+ document.querySelectorAll(`[${Schema.reflex}]`).forEach((element => {
535
+ const controllers = attributeValues(element.getAttribute(Schema.controller));
536
+ const reflexAttributeNames = attributeValues(element.getAttribute(Schema.reflex));
537
+ const actions = attributeValues(element.getAttribute(Schema.action));
538
+ reflexAttributeNames.forEach((reflexName => {
539
+ const controller = findControllerByReflexName(reflexName, allReflexControllers(reflexes.app, element));
540
+ let action;
541
+ if (controller) {
542
+ action = `${reflexName.split("->")[0]}->${controller.identifier}#__perform`;
543
+ if (!actions.includes(action)) actions.push(action);
544
+ } else {
545
+ action = `${reflexName.split("->")[0]}->stimulus-reflex#__perform`;
546
+ if (!controllers.includes("stimulus-reflex")) {
547
+ controllers.push("stimulus-reflex");
548
+ }
549
+ if (!actions.includes(action)) actions.push(action);
550
+ }
551
+ }));
552
+ const controllerValue = attributeValue(controllers);
553
+ const actionValue = attributeValue(actions);
554
+ if (controllerValue && element.getAttribute(Schema.controller) != controllerValue) {
555
+ element.setAttribute(Schema.controller, controllerValue);
556
+ }
557
+ if (actionValue && element.getAttribute(Schema.action) != actionValue) element.setAttribute(Schema.action, actionValue);
558
+ }));
559
+ emitEvent("stimulus-reflex:ready");
560
+ }), 20);
561
+
562
+ var version = "3.5.0-pre9";
563
+
564
+ class ReflexData {
565
+ constructor(options, reflexElement, controllerElement, reflexController, permanentAttributeName, target, args, url, tabId) {
566
+ this.options = options;
567
+ this.reflexElement = reflexElement;
568
+ this.controllerElement = controllerElement;
569
+ this.reflexController = reflexController;
570
+ this.permanentAttributeName = permanentAttributeName;
571
+ this.target = target;
572
+ this.args = args;
573
+ this.url = url;
574
+ this.tabId = tabId;
575
+ }
576
+ get attrs() {
577
+ this._attrs = this._attrs || this.options["attrs"] || extractElementAttributes(this.reflexElement);
578
+ return this._attrs;
579
+ }
580
+ get reflexId() {
581
+ this._reflexId = this._reflexId || this.options["reflexId"] || uuidv4();
582
+ return this._reflexId;
583
+ }
584
+ get selectors() {
585
+ this._selectors = this._selectors || this.options["selectors"] || getReflexRoots(this.reflexElement);
586
+ return typeof this._selectors === "string" ? [ this._selectors ] : this._selectors;
587
+ }
588
+ get resolveLate() {
589
+ return this.options["resolveLate"] || false;
590
+ }
591
+ get dataset() {
592
+ this._dataset = this._dataset || extractElementDataset(this.reflexElement);
593
+ return this._dataset;
594
+ }
595
+ get innerHTML() {
596
+ return this.includeInnerHtml ? this.reflexElement.innerHTML : "";
597
+ }
598
+ get textContent() {
599
+ return this.includeTextContent ? this.reflexElement.textContent : "";
600
+ }
601
+ get xpathController() {
602
+ return elementToXPath(this.controllerElement);
603
+ }
604
+ get xpathElement() {
605
+ return elementToXPath(this.reflexElement);
606
+ }
607
+ get formSelector() {
608
+ const attr = this.reflexElement.attributes[Schema.reflexFormSelector] ? this.reflexElement.attributes[Schema.reflexFormSelector].value : undefined;
609
+ return this.options["formSelector"] || attr;
610
+ }
611
+ get includeInnerHtml() {
612
+ const attr = this.reflexElement.attributes[Schema.reflexIncludeInnerHtml] || false;
613
+ return this.options["includeInnerHTML"] || attr ? attr.value !== "false" : false;
614
+ }
615
+ get includeTextContent() {
616
+ const attr = this.reflexElement.attributes[Schema.reflexIncludeTextContent] || false;
617
+ return this.options["includeTextContent"] || attr ? attr.value !== "false" : false;
618
+ }
619
+ get suppressLogging() {
620
+ return this.options["suppressLogging"] || this.reflexElement.attributes[Schema.reflexSuppressLogging] || false;
621
+ }
622
+ valueOf() {
623
+ return {
624
+ attrs: this.attrs,
625
+ dataset: this.dataset,
626
+ selectors: this.selectors,
627
+ reflexId: this.reflexId,
628
+ resolveLate: this.resolveLate,
629
+ suppressLogging: this.suppressLogging,
630
+ xpathController: this.xpathController,
631
+ xpathElement: this.xpathElement,
632
+ inner_html: this.innerHTML,
633
+ text_content: this.textContent,
634
+ formSelector: this.formSelector,
635
+ reflexController: this.reflexController,
636
+ permanentAttributeName: this.permanentAttributeName,
637
+ target: this.target,
638
+ args: this.args,
639
+ url: this.url,
640
+ tabId: this.tabId,
641
+ version: version
642
+ };
643
+ }
644
+ }
645
+
646
+ let consumer;
647
+
648
+ let params;
649
+
650
+ let subscriptionActive;
651
+
652
+ const createSubscription = controller => {
653
+ consumer = consumer || controller.application.consumer || createConsumer();
654
+ const {channel: channel} = controller.StimulusReflex;
655
+ const subscription = {
656
+ channel: channel,
657
+ ...params
658
+ };
659
+ const identifier = JSON.stringify(subscription);
660
+ controller.StimulusReflex.subscription = consumer.subscriptions.findAll(identifier)[0] || consumer.subscriptions.create(subscription, {
661
+ received: received,
662
+ connected: connected,
663
+ rejected: rejected,
664
+ disconnected: disconnected
665
+ });
666
+ };
667
+
668
+ const connected = () => {
669
+ subscriptionActive = true;
670
+ document.body.classList.replace("stimulus-reflex-disconnected", "stimulus-reflex-connected");
671
+ emitEvent("stimulus-reflex:connected");
672
+ emitEvent("stimulus-reflex:action-cable:connected");
673
+ };
674
+
675
+ const rejected = () => {
676
+ subscriptionActive = false;
677
+ document.body.classList.replace("stimulus-reflex-connected", "stimulus-reflex-disconnected");
678
+ emitEvent("stimulus-reflex:rejected");
679
+ emitEvent("stimulus-reflex:action-cable:rejected");
680
+ if (Debug.enabled) console.warn("Channel subscription was rejected.");
681
+ };
682
+
683
+ const disconnected = willAttemptReconnect => {
684
+ subscriptionActive = false;
685
+ document.body.classList.replace("stimulus-reflex-connected", "stimulus-reflex-disconnected");
686
+ emitEvent("stimulus-reflex:disconnected", willAttemptReconnect);
687
+ emitEvent("stimulus-reflex:action-cable:disconnected", willAttemptReconnect);
688
+ };
689
+
690
+ var ActionCableTransport = {
691
+ consumer: consumer,
692
+ params: params,
693
+ get subscriptionActive() {
694
+ return subscriptionActive;
695
+ },
696
+ createSubscription: createSubscription,
697
+ connected: connected,
698
+ rejected: rejected,
699
+ disconnected: disconnected,
700
+ set(consumerValue, paramsValue) {
701
+ consumer = consumerValue;
702
+ params = paramsValue;
703
+ }
704
+ };
705
+
706
+ const beforeDOMUpdate = event => {
707
+ const {stimulusReflex: stimulusReflex, payload: payload} = event.detail || {};
708
+ if (!stimulusReflex) return;
709
+ const {reflexId: reflexId, xpathElement: xpathElement, xpathController: xpathController} = stimulusReflex;
710
+ const controllerElement = XPathToElement(xpathController);
711
+ const reflexElement = XPathToElement(xpathElement);
712
+ const reflex = reflexes[reflexId];
713
+ const {promise: promise} = reflex;
714
+ reflex.pendingOperations--;
715
+ if (reflex.pendingOperations > 0) return;
716
+ if (!stimulusReflex.resolveLate) setTimeout((() => promise.resolve({
717
+ element: reflexElement,
718
+ event: event,
719
+ data: promise.data,
720
+ payload: payload,
721
+ reflexId: reflexId,
722
+ toString: () => ""
723
+ })));
724
+ setTimeout((() => dispatchLifecycleEvent("success", reflexElement, controllerElement, reflexId, payload)));
725
+ };
726
+
727
+ const afterDOMUpdate = event => {
728
+ const {stimulusReflex: stimulusReflex, payload: payload} = event.detail || {};
729
+ if (!stimulusReflex) return;
730
+ const {reflexId: reflexId, xpathElement: xpathElement, xpathController: xpathController} = stimulusReflex;
731
+ const controllerElement = XPathToElement(xpathController);
732
+ const reflexElement = XPathToElement(xpathElement);
733
+ const reflex = reflexes[reflexId];
734
+ const {promise: promise} = reflex;
735
+ reflex.completedOperations++;
736
+ Log.success(event, false);
737
+ if (reflex.completedOperations < reflex.totalOperations) return;
738
+ if (stimulusReflex.resolveLate) setTimeout((() => promise.resolve({
739
+ element: reflexElement,
740
+ event: event,
741
+ data: promise.data,
742
+ payload: payload,
743
+ reflexId: reflexId,
744
+ toString: () => ""
745
+ })));
746
+ setTimeout((() => dispatchLifecycleEvent("finalize", reflexElement, controllerElement, reflexId, payload)));
747
+ if (reflex.piggybackOperations.length) CableReady.perform(reflex.piggybackOperations);
748
+ };
749
+
750
+ const routeReflexEvent = event => {
751
+ const {stimulusReflex: stimulusReflex, payload: payload, name: name, body: body} = event.detail || {};
752
+ const eventType = name.split("-")[2];
753
+ if (!stimulusReflex || ![ "nothing", "halted", "error" ].includes(eventType)) return;
754
+ const {reflexId: reflexId, xpathElement: xpathElement, xpathController: xpathController} = stimulusReflex;
755
+ const reflexElement = XPathToElement(xpathElement);
756
+ const controllerElement = XPathToElement(xpathController);
757
+ const reflex = reflexes[reflexId];
758
+ const {promise: promise} = reflex;
759
+ if (controllerElement) {
760
+ controllerElement.reflexError = controllerElement.reflexError || {};
761
+ if (eventType === "error") controllerElement.reflexError[reflexId] = body;
762
+ }
763
+ switch (eventType) {
764
+ case "nothing":
765
+ nothing(event, payload, promise, reflex, reflexElement);
766
+ break;
767
+
768
+ case "error":
769
+ error(event, payload, promise, reflex, reflexElement);
770
+ break;
771
+
772
+ case "halted":
773
+ halted(event, payload, promise, reflex, reflexElement);
774
+ break;
775
+ }
776
+ setTimeout((() => dispatchLifecycleEvent(eventType, reflexElement, controllerElement, reflexId, payload)));
777
+ if (reflex.piggybackOperations.length) CableReady.perform(reflex.piggybackOperations);
778
+ };
779
+
780
+ const nothing = (event, payload, promise, reflex, reflexElement) => {
781
+ reflex.finalStage = "after";
782
+ Log.success(event, false);
783
+ setTimeout((() => promise.resolve({
784
+ data: promise.data,
785
+ element: reflexElement,
786
+ event: event,
787
+ payload: payload,
788
+ reflexId: promise.data.reflexId,
789
+ toString: () => ""
790
+ })));
791
+ };
792
+
793
+ const halted = (event, payload, promise, reflex, reflexElement) => {
794
+ reflex.finalStage = "halted";
795
+ Log.success(event, true);
796
+ setTimeout((() => promise.resolve({
797
+ data: promise.data,
798
+ element: reflexElement,
799
+ event: event,
800
+ payload: payload,
801
+ reflexId: promise.data.reflexId,
802
+ toString: () => ""
803
+ })));
804
+ };
805
+
806
+ const error = (event, payload, promise, reflex, reflexElement) => {
807
+ reflex.finalStage = "after";
808
+ Log.error(event);
809
+ setTimeout((() => promise.reject({
810
+ data: promise.data,
811
+ element: reflexElement,
812
+ event: event,
813
+ payload: payload,
814
+ reflexId: promise.data.reflexId,
815
+ error: event.detail.body,
816
+ toString: () => event.detail.body
817
+ })));
818
+ };
819
+
820
+ class StimulusReflexController extends Controller {
821
+ constructor(...args) {
822
+ super(...args);
823
+ register(this);
824
+ }
825
+ }
826
+
827
+ const initialize = (application, {controller: controller, consumer: consumer, debug: debug, params: params, isolate: isolate, deprecate: deprecate} = {}) => {
828
+ ActionCableTransport.set(consumer, params);
829
+ document.addEventListener("DOMContentLoaded", (() => {
830
+ document.body.classList.remove("stimulus-reflex-connected");
831
+ document.body.classList.add("stimulus-reflex-disconnected");
832
+ if (Deprecate.enabled && consumer) console.warn("Deprecation warning: the next version of StimulusReflex will obtain a reference to consumer via the Stimulus application object.\nPlease add 'application.consumer = consumer' to your index.js after your Stimulus application has been established, and remove the consumer key from your StimulusReflex initialize() options object.");
833
+ if (Deprecate.enabled && IsolationMode.disabled) console.warn("Deprecation warning: the next version of StimulusReflex will standardize isolation mode, and the isolate option will be removed.\nPlease update your applications to assume that every tab will be isolated.");
834
+ }), {
835
+ once: true
836
+ });
837
+ IsolationMode.set(!!isolate);
838
+ reflexes.app = application;
839
+ Schema.set(application);
840
+ reflexes.app.register("stimulus-reflex", controller || StimulusReflexController);
841
+ Debug$1.set(!!debug);
842
+ if (typeof deprecate !== "undefined") Deprecate.set(deprecate);
843
+ const observer = new MutationObserver(setupDeclarativeReflexes);
844
+ observer.observe(document.documentElement, {
845
+ attributeFilter: [ Schema.reflex, Schema.action ],
846
+ childList: true,
847
+ subtree: true
848
+ });
849
+ };
850
+
851
+ const register = (controller, options = {}) => {
852
+ const channel = "StimulusReflex::Channel";
853
+ controller.StimulusReflex = {
854
+ ...options,
855
+ channel: channel
856
+ };
857
+ ActionCableTransport.createSubscription(controller);
858
+ Object.assign(controller, {
859
+ isActionCableConnectionOpen() {
860
+ return this.StimulusReflex.subscription.consumer.connection.isOpen();
861
+ },
862
+ stimulate() {
863
+ const url = location.href;
864
+ const args = Array.from(arguments);
865
+ const target = args.shift() || "StimulusReflex::Reflex#default_reflex";
866
+ const controllerElement = this.element;
867
+ const reflexElement = args[0] && args[0].nodeType === Node.ELEMENT_NODE ? args.shift() : controllerElement;
868
+ if (reflexElement.type === "number" && reflexElement.validity && reflexElement.validity.badInput) {
869
+ if (Debug$1.enabled) console.warn("Reflex aborted: invalid numeric input");
870
+ return;
871
+ }
872
+ const options = {};
873
+ if (args[0] && typeof args[0] === "object" && Object.keys(args[0]).filter((key => [ "attrs", "selectors", "reflexId", "resolveLate", "serializeForm", "suppressLogging", "includeInnerHTML", "includeTextContent" ].includes(key))).length) {
874
+ const opts = args.shift();
875
+ Object.keys(opts).forEach((o => options[o] = opts[o]));
876
+ }
877
+ const reflexData = new ReflexData(options, reflexElement, controllerElement, this.identifier, Schema.reflexPermanent, target, args, url, tabId);
878
+ const reflexId = reflexData.reflexId;
879
+ if (!this.isActionCableConnectionOpen()) throw "The ActionCable connection is not open! `this.isActionCableConnectionOpen()` must return true before calling `this.stimulate()`";
880
+ if (!ActionCableTransport.subscriptionActive) throw "The ActionCable channel subscription for StimulusReflex was rejected.";
881
+ controllerElement.reflexController = controllerElement.reflexController || {};
882
+ controllerElement.reflexData = controllerElement.reflexData || {};
883
+ controllerElement.reflexError = controllerElement.reflexError || {};
884
+ controllerElement.reflexController[reflexId] = this;
885
+ controllerElement.reflexData[reflexId] = reflexData.valueOf();
886
+ dispatchLifecycleEvent("before", reflexElement, controllerElement, reflexId);
887
+ setTimeout((() => {
888
+ const {params: params} = controllerElement.reflexData[reflexId] || {};
889
+ const check = reflexElement.attributes[Schema.reflexSerializeForm];
890
+ if (check) {
891
+ options["serializeForm"] = check.value !== "false";
892
+ }
893
+ const form = reflexElement.closest(reflexData.formSelector) || document.querySelector(reflexData.formSelector) || reflexElement.closest("form");
894
+ if (Deprecate.enabled && options["serializeForm"] === undefined && form) console.warn(`Deprecation warning: the next version of StimulusReflex will not serialize forms by default.\nPlease set ${Schema.reflexSerializeForm}="true" on your Reflex Controller Element or pass { serializeForm: true } as an option to stimulate.`);
895
+ const formData = options["serializeForm"] === false ? "" : serializeForm(form, {
896
+ element: reflexElement
897
+ });
898
+ controllerElement.reflexData[reflexId] = {
899
+ ...reflexData.valueOf(),
900
+ params: params,
901
+ formData: formData
902
+ };
903
+ this.StimulusReflex.subscription.send(controllerElement.reflexData[reflexId]);
904
+ }));
905
+ const promise = registerReflex(reflexData.valueOf());
906
+ Log.request(reflexId, target, args, this.context.scope.identifier, reflexElement, controllerElement);
907
+ return promise;
908
+ },
909
+ __perform(event) {
910
+ let element = event.target;
911
+ let reflex;
912
+ while (element && !reflex) {
913
+ reflex = element.getAttribute(Schema.reflex);
914
+ if (!reflex || !reflex.trim().length) element = element.parentElement;
915
+ }
916
+ const match = attributeValues(reflex).find((reflex => reflex.split("->")[0] === event.type));
917
+ if (match) {
918
+ event.preventDefault();
919
+ event.stopPropagation();
920
+ this.stimulate(match.split("->")[1], element);
921
+ }
922
+ }
923
+ });
924
+ };
925
+
926
+ const tabId = uuidv4();
927
+
928
+ const useReflex = (controller, options = {}) => {
929
+ register(controller, options);
930
+ };
931
+
932
+ document.addEventListener("cable-ready:after-dispatch-event", routeReflexEvent);
933
+
934
+ document.addEventListener("cable-ready:before-inner-html", beforeDOMUpdate);
935
+
936
+ document.addEventListener("cable-ready:before-morph", beforeDOMUpdate);
937
+
938
+ document.addEventListener("cable-ready:after-inner-html", afterDOMUpdate);
939
+
940
+ document.addEventListener("cable-ready:after-morph", afterDOMUpdate);
941
+
942
+ window.addEventListener("load", setupDeclarativeReflexes);
943
+
944
+ var StimulusReflex = Object.freeze({
945
+ __proto__: null,
946
+ initialize: initialize,
947
+ register: register,
948
+ useReflex: useReflex
949
+ });
950
+
951
+ const global = {
952
+ ...StimulusReflex,
953
+ get debug() {
954
+ return Debug$1.value;
955
+ },
956
+ set debug(value) {
957
+ Debug$1.set(!!value);
958
+ },
959
+ get deprecate() {
960
+ return Deprecate.value;
961
+ },
962
+ set deprecate(value) {
963
+ Deprecate.set(!!value);
964
+ }
965
+ };
966
+
967
+ window.StimulusReflex = global;
968
+
969
+ export { global as default, initialize, register, useReflex };