fluxbit_view_components 0.3.0 → 0.4.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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -0
  3. data/app/assets/javascripts/fluxbit_view_components/assigner_controller.js +49 -0
  4. data/app/assets/javascripts/fluxbit_view_components/auto_submit_controller.js +39 -0
  5. data/app/assets/javascripts/fluxbit_view_components/drawer_controller.js +135 -0
  6. data/app/assets/javascripts/fluxbit_view_components/index.js +56 -0
  7. data/app/assets/javascripts/fluxbit_view_components/method_link_controller.js +143 -0
  8. data/app/assets/javascripts/fluxbit_view_components/modal_controller.js +118 -0
  9. data/app/assets/javascripts/fluxbit_view_components/password_controller.js +170 -0
  10. data/app/assets/javascripts/fluxbit_view_components/progress_controller.js +374 -0
  11. data/app/assets/javascripts/fluxbit_view_components/row_click_controller.js +32 -0
  12. data/app/assets/javascripts/fluxbit_view_components/select_all_controller.js +122 -0
  13. data/app/assets/javascripts/fluxbit_view_components/spinner_percent_controller.js +174 -0
  14. data/app/assets/javascripts/fluxbit_view_components/theme_button_controller.js +90 -0
  15. data/app/assets/javascripts/fluxbit_view_components.js +1175 -0
  16. data/app/components/fluxbit/accordion_component.rb +125 -0
  17. data/app/components/fluxbit/alert_component.rb +8 -8
  18. data/app/components/fluxbit/avatar_component.rb +11 -12
  19. data/app/components/fluxbit/avatar_group_component.rb +1 -1
  20. data/app/components/fluxbit/badge_component.rb +8 -7
  21. data/app/components/fluxbit/banner_component.rb +139 -0
  22. data/app/components/fluxbit/bottom_navigation_component.rb +437 -0
  23. data/app/components/fluxbit/breadcrumb_component.rb +66 -0
  24. data/app/components/fluxbit/button_component.rb +39 -11
  25. data/app/components/fluxbit/button_group_component.rb +1 -1
  26. data/app/components/fluxbit/card_component.rb +26 -23
  27. data/app/components/fluxbit/carousel_component.rb +154 -0
  28. data/app/components/fluxbit/component.rb +24 -3
  29. data/app/components/fluxbit/drawer_component.html.erb +30 -0
  30. data/app/components/fluxbit/drawer_component.rb +125 -0
  31. data/app/components/fluxbit/dropdown_component.rb +41 -0
  32. data/app/components/fluxbit/dropdown_item_component.rb +68 -0
  33. data/app/components/fluxbit/flex_component.rb +1 -1
  34. data/app/components/fluxbit/form/component.rb +15 -8
  35. data/app/components/fluxbit/form/dropzone_component.rb +3 -3
  36. data/app/components/fluxbit/form/field_component.rb +4 -2
  37. data/app/components/fluxbit/form/help_text_component.rb +1 -1
  38. data/app/components/fluxbit/form/label_component.rb +10 -3
  39. data/app/components/fluxbit/form/password_component.rb +247 -0
  40. data/app/components/fluxbit/form/radio_group_button_component.rb +126 -0
  41. data/app/components/fluxbit/form/select_component.rb +108 -11
  42. data/app/components/fluxbit/form/text_field_component.rb +40 -23
  43. data/app/components/fluxbit/form/toggle_component.rb +2 -2
  44. data/app/components/fluxbit/form/upload_image_component.html.erb +3 -3
  45. data/app/components/fluxbit/form/upload_image_component.rb +12 -1
  46. data/app/components/fluxbit/gravatar_component.rb +7 -0
  47. data/app/components/fluxbit/icon_helpers.rb +167 -0
  48. data/app/components/fluxbit/link_component.rb +42 -0
  49. data/app/components/fluxbit/modal_component.rb +28 -31
  50. data/app/components/fluxbit/pagination_component.rb +206 -0
  51. data/app/components/fluxbit/popover_component.rb +14 -14
  52. data/app/components/fluxbit/progress_component.rb +196 -0
  53. data/app/components/fluxbit/skeleton_component.rb +237 -0
  54. data/app/components/fluxbit/speed_dial_action_component.html.erb +30 -0
  55. data/app/components/fluxbit/speed_dial_action_component.rb +59 -0
  56. data/app/components/fluxbit/speed_dial_component.html.erb +33 -0
  57. data/app/components/fluxbit/speed_dial_component.rb +73 -0
  58. data/app/components/fluxbit/spinner_component.rb +71 -0
  59. data/app/components/fluxbit/spinner_percent_component.rb +174 -0
  60. data/app/components/fluxbit/stepper_component.rb +223 -0
  61. data/app/components/fluxbit/tab_component.rb +44 -25
  62. data/app/components/fluxbit/table_component.rb +186 -0
  63. data/app/components/fluxbit/table_group_component.rb +28 -0
  64. data/app/components/fluxbit/theme_button_component.rb +64 -0
  65. data/app/components/fluxbit/timeline_component.rb +63 -0
  66. data/app/components/fluxbit/timeline_item_component.html.erb +64 -0
  67. data/app/components/fluxbit/timeline_item_component.rb +78 -0
  68. data/app/components/fluxbit/tooltip_component.rb +2 -2
  69. data/app/helpers/fluxbit/components_helper.rb +74 -4
  70. data/app/helpers/fluxbit/form_builder.rb +64 -15
  71. data/app/helpers/fluxbit/view_helper.rb +71 -0
  72. data/config/locales/en.yml +37 -4
  73. data/config/locales/pt-BR.yml +36 -0
  74. data/lib/fluxbit/config/accordion_component.rb +73 -0
  75. data/lib/fluxbit/config/avatar_component.rb +11 -11
  76. data/lib/fluxbit/config/badge_component.rb +14 -11
  77. data/lib/fluxbit/config/banner_component.rb +60 -0
  78. data/lib/fluxbit/config/bottom_navigation_component.rb +74 -0
  79. data/lib/fluxbit/config/breadcrumb_component.rb +24 -0
  80. data/lib/fluxbit/config/button_component.rb +6 -4
  81. data/lib/fluxbit/config/card_component.rb +23 -12
  82. data/lib/fluxbit/config/carousel_component.rb +33 -0
  83. data/lib/fluxbit/config/drawer_component.rb +48 -0
  84. data/lib/fluxbit/config/dropdown_component.rb +29 -0
  85. data/lib/fluxbit/config/form/check_box_component.rb +1 -1
  86. data/lib/fluxbit/config/form/dropzone_component.rb +1 -1
  87. data/lib/fluxbit/config/form/help_text_component.rb +1 -1
  88. data/lib/fluxbit/config/form/label_component.rb +3 -2
  89. data/lib/fluxbit/config/form/password_component.rb +19 -0
  90. data/lib/fluxbit/config/form/radio_group_button_component.rb +24 -0
  91. data/lib/fluxbit/config/form/text_field_component.rb +11 -11
  92. data/lib/fluxbit/config/form/toggle_component.rb +5 -5
  93. data/lib/fluxbit/config/link_component.rb +24 -0
  94. data/lib/fluxbit/config/modal_component.rb +1 -1
  95. data/lib/fluxbit/config/pagination_component.rb +31 -0
  96. data/lib/fluxbit/config/popover_component.rb +1 -1
  97. data/lib/fluxbit/config/progress_component.rb +63 -0
  98. data/lib/fluxbit/config/skeleton_component.rb +82 -0
  99. data/lib/fluxbit/config/speed_dial_component.rb +50 -0
  100. data/lib/fluxbit/config/spinner_component.rb +30 -0
  101. data/lib/fluxbit/config/spinner_percent_component.rb +61 -0
  102. data/lib/fluxbit/config/stepper_component.rb +299 -0
  103. data/lib/fluxbit/config/tab_component.rb +6 -0
  104. data/lib/fluxbit/config/table_component.rb +75 -0
  105. data/lib/fluxbit/config/theme_button_component.rb +19 -0
  106. data/lib/fluxbit/config/timeline_component.rb +77 -0
  107. data/lib/fluxbit/view_components/engine.rb +11 -3
  108. data/lib/fluxbit/view_components/version.rb +1 -1
  109. data/lib/fluxbit/view_components.rb +20 -0
  110. data/lib/generators/fluxbit/devise_views_generator.rb +116 -0
  111. data/lib/generators/fluxbit/pagy_generator.rb +39 -0
  112. data/lib/generators/fluxbit/scaffold_generator.rb +165 -0
  113. data/lib/generators/fluxbit/templates/_alert.html.erb.tt +1 -0
  114. data/lib/generators/fluxbit/templates/_flash.html.erb.tt +15 -0
  115. data/lib/generators/fluxbit/templates/_form.html.erb.tt +38 -0
  116. data/lib/generators/fluxbit/templates/_metadata.html.erb.tt +44 -0
  117. data/lib/generators/fluxbit/templates/controller.rb.tt +406 -0
  118. data/lib/generators/fluxbit/templates/create.turbo_stream.erb.tt +7 -0
  119. data/lib/generators/fluxbit/templates/destroy.turbo_stream.erb.tt +3 -0
  120. data/lib/generators/fluxbit/templates/destroy_all.turbo_stream.erb.tt +9 -0
  121. data/lib/generators/fluxbit/templates/devise_views/confirmations/new.html.erb +11 -0
  122. data/lib/generators/fluxbit/templates/devise_views/layouts/devise.html.erb +64 -0
  123. data/lib/generators/fluxbit/templates/devise_views/mailer/confirmation_instructions.html.erb +5 -0
  124. data/lib/generators/fluxbit/templates/devise_views/mailer/email_changed.html.erb +7 -0
  125. data/lib/generators/fluxbit/templates/devise_views/mailer/password_changed.html.erb +3 -0
  126. data/lib/generators/fluxbit/templates/devise_views/mailer/reset_password_instructions.html.erb +8 -0
  127. data/lib/generators/fluxbit/templates/devise_views/mailer/unlock_instructions.html.erb +7 -0
  128. data/lib/generators/fluxbit/templates/devise_views/passwords/edit.html.erb +29 -0
  129. data/lib/generators/fluxbit/templates/devise_views/passwords/new.html.erb +11 -0
  130. data/lib/generators/fluxbit/templates/devise_views/registrations/edit.html.erb +43 -0
  131. data/lib/generators/fluxbit/templates/devise_views/registrations/new.html.erb +34 -0
  132. data/lib/generators/fluxbit/templates/devise_views/sessions/new.html.erb +15 -0
  133. data/lib/generators/fluxbit/templates/devise_views/shared/_error_messages.html.erb +14 -0
  134. data/lib/generators/fluxbit/templates/devise_views/shared/_links.html.erb +25 -0
  135. data/lib/generators/fluxbit/templates/devise_views/unlocks/new.html.erb +11 -0
  136. data/lib/generators/fluxbit/templates/edit.html.erb.tt +47 -0
  137. data/lib/generators/fluxbit/templates/fluxbit_pagy.css +27 -0
  138. data/lib/generators/fluxbit/templates/i18n.en.yml.tt +121 -0
  139. data/lib/generators/fluxbit/templates/i18n.pt-BR.yml.tt +121 -0
  140. data/lib/generators/fluxbit/templates/index.html.erb.tt +254 -0
  141. data/lib/generators/fluxbit/templates/index.json.jbuilder.tt +33 -0
  142. data/lib/generators/fluxbit/templates/new.html.erb.tt +47 -0
  143. data/lib/generators/fluxbit/templates/partial.html.erb.tt +61 -0
  144. data/lib/generators/fluxbit/templates/policy.rb.tt +36 -0
  145. data/lib/generators/fluxbit/templates/send_alert_via_drawer.erb.tt +10 -0
  146. data/lib/generators/fluxbit/templates/show.html.erb.tt +44 -0
  147. data/lib/generators/fluxbit/templates/show.json.jbuilder.tt +6 -0
  148. data/lib/generators/fluxbit/templates/update.turbo_stream.erb.tt +10 -0
  149. data/lib/generators/fluxbit/templates/update_all.turbo_stream.erb.tt +20 -0
  150. data/lib/install/install.rb +58 -0
  151. metadata +107 -18
  152. data/app/helpers/fluxbit/classes_helper.rb +0 -9
@@ -0,0 +1,1175 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ class FxAssigner extends Controller {
4
+ assign(event) {
5
+ if (event.params["preventDefault"] === "true") {
6
+ event.preventDefault();
7
+ event.stopPropagation();
8
+ }
9
+ Object.keys(event.params["change"]).forEach(el => {
10
+ const targetElement = document.querySelector(el);
11
+ if (!targetElement) {
12
+ console.error(`fx-assigner: Target element "${el}" not found.`);
13
+ return;
14
+ }
15
+ Object.keys(event.params["change"][el]).forEach(attr => {
16
+ let value = "";
17
+ if (typeof event.params["change"][el][attr] == "object") {
18
+ const element = event.params["change"][el][attr]["element"];
19
+ const attribute = event.params["change"][el][attr]["attribute"];
20
+ const fromElement = document.querySelector(element);
21
+ if (!fromElement) {
22
+ console.error(`fx-assigner: Element "${element}" not found.`);
23
+ return;
24
+ }
25
+ if (attribute === "innerHTML") value = fromElement.innerHTML; else if (attribute === "value") value = fromElement.value; else if (attribute === "textContent") value = fromElement.textContent; else value = fromElement.getAttribute(attribute);
26
+ } else value = event.params["change"][el][attr];
27
+ if (attr === "innerHTML") targetElement.innerHTML = value; else if (attr === "value") targetElement.value = value; else if (attr === "textContent") targetElement.textContent = value; else targetElement.setAttribute(attr, value);
28
+ });
29
+ });
30
+ }
31
+ }
32
+
33
+ class FxAutoSubmit extends Controller {
34
+ static values={
35
+ delay: {
36
+ type: Number,
37
+ default: 150
38
+ }
39
+ };
40
+ connect() {
41
+ this.timeout = null;
42
+ }
43
+ submit(event) {
44
+ if (this.timeout) {
45
+ clearTimeout(this.timeout);
46
+ }
47
+ this.timeout = setTimeout(() => {
48
+ if (event.params["formId"]) {
49
+ const form = document.getElementById(event.params["formId"]);
50
+ if (!form) {
51
+ console.error(`fx-auto-submit: Form with ID "${event.params["formId"]}" not found.`);
52
+ return;
53
+ }
54
+ form.requestSubmit();
55
+ return;
56
+ }
57
+ this.element.requestSubmit();
58
+ }, this.delayValue);
59
+ }
60
+ disconnect() {
61
+ if (this.timeout) {
62
+ clearTimeout(this.timeout);
63
+ }
64
+ }
65
+ }
66
+
67
+ class FxDrawer extends Controller {
68
+ static targets=[ "drawer" ];
69
+ static values={
70
+ autoShow: false,
71
+ placement: "left",
72
+ backdrop: true,
73
+ bodyScrolling: false,
74
+ edge: false,
75
+ edgeOffset: String,
76
+ backdropClasses: "bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30",
77
+ onHide: Object,
78
+ onShow: Object,
79
+ onToggle: Object
80
+ };
81
+ connect() {
82
+ this.drawers = {};
83
+ if (this.drawerTargets.length === 0) {
84
+ if (!this.element.id) this.element.id = "drawer-" + Math.random().toString(36).substring(2, 15);
85
+ this._initDrawer(this.element, this._optionsFromElements());
86
+ this._addListeners(this.element);
87
+ if (this.autoShowValue) this.drawers[this.element.id].show();
88
+ } else {
89
+ this.drawerTargets.forEach(target => {
90
+ if (!target.id) target.id = "drawer-" + Math.random().toString(36).substring(2, 15);
91
+ this._initDrawer(target, this._optionsFromElements(target));
92
+ this._addListeners(target);
93
+ if (this.autoShowValue) this.drawers[target.id].show();
94
+ });
95
+ }
96
+ }
97
+ async _ensureDrawerLoaded() {
98
+ if (typeof Drawer === "undefined") {
99
+ const module = await import("flowbite");
100
+ window.Drawer = module.Drawer;
101
+ }
102
+ }
103
+ _initDrawer(target, options = {}) {
104
+ this.drawers[target.id] = new Drawer(target, options);
105
+ }
106
+ _addListeners(target) {
107
+ if (!this._drawerListeners) this._drawerListeners = new Set;
108
+ [ "show", "hide", "toggle" ].forEach(action => {
109
+ const eventName = `${action}Drawer:${target.id}`;
110
+ if (!this._drawerListeners.has(eventName)) {
111
+ document.addEventListener(eventName, () => {
112
+ if (this.drawers[target.id]) this.drawers[target.id][action]();
113
+ });
114
+ this._drawerListeners.add(eventName);
115
+ }
116
+ });
117
+ }
118
+ disconnect() {
119
+ Object.entries(this.drawers).forEach(([id, drawer]) => {
120
+ if (drawer) {
121
+ drawer.hide();
122
+ drawer.destroy();
123
+ delete this.drawers[id];
124
+ }
125
+ });
126
+ }
127
+ _optionsFromElements(target = null) {
128
+ let options = {};
129
+ if (this.hasPlacementValue) options["placement"] = this.placementValue;
130
+ if (this.hasBackdropValue) options["backdrop"] = this.backdropValue;
131
+ if (this.hasBodyScrollingValue) options["bodyScrolling"] = this.bodyScrollingValue;
132
+ if (this.hasEdgeValue) options["edge"] = this.edgeValue;
133
+ if (this.hasEdgeOffsetValue) options["edgeOffset"] = this.edgeOffsetValue;
134
+ if (this.hasBackdropClassesValue) options["backdropClasses"] = this.backdropClassesValue;
135
+ if (this.hasOnHideValue) options["onHide"] = this.onHideValue;
136
+ if (this.hasOnShowValue) options["onShow"] = this.onShowValue;
137
+ if (this.hasOnToggleValue) options["onToggle"] = this.onToggleValue;
138
+ if (target) {
139
+ if (target.dataset.autoShow !== undefined) options["autoShow"] = target.dataset.autoShow === "true";
140
+ if (target.dataset.placement) options["placement"] = target.dataset.placement;
141
+ if (target.dataset.backdrop !== undefined) options["backdrop"] = target.dataset.backdrop === "true";
142
+ if (target.dataset.bodyScrolling !== undefined) options["bodyScrolling"] = target.dataset.bodyScrolling === "true";
143
+ if (target.dataset.edge !== undefined) options["edge"] = target.dataset.edge === "true";
144
+ if (target.dataset.edgeOffset) options["edgeOffset"] = target.dataset.edgeOffset;
145
+ if (target.dataset.backdropClasses) options["backdropClasses"] = target.dataset.backdropClasses;
146
+ if (target.dataset.onHide) options["onHide"] = target.dataset.onHide;
147
+ if (target.dataset.onShow) options["onShow"] = target.dataset.onShow;
148
+ if (target.dataset.onToggle) options["onToggle"] = target.dataset.onToggle;
149
+ }
150
+ return options;
151
+ }
152
+ _toCamelCase(str) {
153
+ return str.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
154
+ }
155
+ toggle(event) {
156
+ const targetId = event.target.dataset[this._toCamelCase(this.identifier + "-id")];
157
+ if (targetId) {
158
+ if (this.drawers[targetId]) this.drawers[targetId].toggle(); else console.warn(`Drawer with id ${targetId} not found.`);
159
+ } else Object.entries(this.drawers).forEach(([_id, drawer]) => {
160
+ if (drawer) drawer.toggle();
161
+ });
162
+ }
163
+ show(event) {
164
+ const targetId = event.target.dataset[this._toCamelCase(this.identifier + "-id")];
165
+ if (targetId) {
166
+ if (this.drawers[targetId]) this.drawers[targetId].show(); else console.warn(`Drawer with id ${targetId} not found.`);
167
+ } else Object.entries(this.drawers).forEach(([_id, drawer]) => {
168
+ if (drawer) drawer.show();
169
+ });
170
+ }
171
+ hide(event) {
172
+ const targetId = event.target.dataset[this._toCamelCase(this.identifier + "-id")];
173
+ if (targetId) {
174
+ if (this.drawers[targetId]) this.drawers[targetId].hide(); else console.warn(`Drawer with id ${targetId} not found.`);
175
+ } else Object.entries(this.drawers).forEach(([_id, drawer]) => {
176
+ if (drawer) drawer.hide();
177
+ });
178
+ }
179
+ }
180
+
181
+ class FxMethodLink extends Controller {
182
+ static values={
183
+ method: "get",
184
+ url: String,
185
+ params: Object,
186
+ formDataId: String,
187
+ debug: false,
188
+ eventType: "click"
189
+ };
190
+ connect() {
191
+ this.element.addEventListener(this.eventTypeValue, this.click.bind(this));
192
+ }
193
+ disconnect() {
194
+ this.element.removeEventListener(this.eventTypeValue, this.click.bind(this));
195
+ }
196
+ click(event) {
197
+ event.preventDefault();
198
+ event.stopPropagation();
199
+ const formData = this.createFormData();
200
+ if (this.hasParamsValue) this.addParams(formData);
201
+ const url = this.hasUrlValue ? this.urlValue : this.element.href;
202
+ this.submitRequest(url, formData);
203
+ }
204
+ createFormData() {
205
+ const formData = new FormData;
206
+ if (this.hasFormDataIdValue) {
207
+ const existingForm = document.getElementById(this.formDataIdValue);
208
+ const formElements = existingForm.querySelectorAll("input, select, textarea");
209
+ formElements.forEach(element => {
210
+ if (element.name && (element.type !== "checkbox" || element.checked)) formData.append(element.name, element.value);
211
+ });
212
+ }
213
+ formData.append("_method", (this.methodValue || formData.method).toUpperCase());
214
+ formData.append("authenticity_token", this.getCSRFToken());
215
+ return formData;
216
+ }
217
+ addParams(formData) {
218
+ this.appendNestedParams(formData, this.paramsValue, "");
219
+ }
220
+ appendNestedParams(formData, obj, prefix) {
221
+ Object.entries(obj).forEach(([key, value]) => {
222
+ const fullKey = prefix ? `${prefix}[${key}]` : key;
223
+ if (typeof value === "object" && value !== null) {
224
+ if (this.isElementReference(value)) {
225
+ const resolvedValue = this.resolveElementValue(value);
226
+ formData.append(fullKey, resolvedValue);
227
+ } else {
228
+ this.appendNestedParams(formData, value, fullKey);
229
+ }
230
+ } else {
231
+ formData.append(fullKey, value);
232
+ }
233
+ });
234
+ }
235
+ isElementReference(obj) {
236
+ return obj.hasOwnProperty("element") && obj.hasOwnProperty("attribute");
237
+ }
238
+ resolveElementValue(elementRef) {
239
+ const {element: element, attribute: attribute} = elementRef;
240
+ const domElement = document.querySelector(element);
241
+ if (!domElement) {
242
+ console.error(`fx-method-link: Element "${element}" not found.`);
243
+ return "";
244
+ }
245
+ switch (attribute) {
246
+ case "innerHTML":
247
+ return domElement.innerHTML;
248
+
249
+ case "value":
250
+ return domElement.value;
251
+
252
+ case "textContent":
253
+ return domElement.textContent;
254
+
255
+ default:
256
+ return domElement.getAttribute(attribute) || "";
257
+ }
258
+ }
259
+ submitRequest(url, formData) {
260
+ if (this.debugValue) {
261
+ console.log("Submitting form data:");
262
+ for (let [key, value] of formData.entries()) {
263
+ console.log(`${key}: ${value}`);
264
+ }
265
+ }
266
+ fetch(url, {
267
+ method: "POST",
268
+ body: formData,
269
+ headers: {
270
+ Accept: "text/vnd.turbo-stream.html, text/html",
271
+ "X-Requested-With": "XMLHttpRequest"
272
+ }
273
+ }).then(response => {
274
+ if (response.ok) {
275
+ if (response.headers.get("Content-Type")?.includes("turbo-stream")) {
276
+ return response.text().then(html => {
277
+ Turbo.renderStreamMessage(html);
278
+ });
279
+ } else {
280
+ window.location.reload();
281
+ }
282
+ } else {
283
+ console.error("Request failed:", response.statusText);
284
+ }
285
+ }).catch(error => {
286
+ console.error("Network error:", error);
287
+ });
288
+ }
289
+ getCSRFToken() {
290
+ const token = document.querySelector('meta[name="csrf-token"]');
291
+ return token ? token.getAttribute("content") : "";
292
+ }
293
+ }
294
+
295
+ class FxModal extends Controller {
296
+ static targets=[ "modal" ];
297
+ static values={
298
+ autoShow: false,
299
+ placement: "bottom-right",
300
+ backdrop: "dynamic",
301
+ backdropClasses: "bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30",
302
+ closable: false,
303
+ onHide: Object,
304
+ onShow: Object,
305
+ onToggle: Object
306
+ };
307
+ connect() {
308
+ this.modals = {};
309
+ if (this.modalTargets.length === 0) {
310
+ if (!this.element.id) this.element.id = "modal-" + Math.random().toString(36).substring(2, 15);
311
+ this._initModal(this.element, this._optionsFromElements());
312
+ this._addListeners(this.element);
313
+ if (this.autoShowValue) this.modals[this.element.id].show();
314
+ } else {
315
+ this.modalTargets.forEach(target => {
316
+ if (!target.id) target.id = "modal-" + Math.random().toString(36).substring(2, 15);
317
+ this._initModal(target, this._optionsFromElements(target));
318
+ this._addListeners(target);
319
+ if (this.autoShowValue) this.modals[target.id].show();
320
+ });
321
+ }
322
+ }
323
+ async _ensureModalLoaded() {
324
+ if (typeof Modal === "undefined") {
325
+ const module = await import("flowbite");
326
+ window.Modal = module.Modal;
327
+ }
328
+ }
329
+ async _initModal(target, options = {}) {
330
+ await this._ensureModalLoaded();
331
+ this.modals[target.id] = new Modal(target, options);
332
+ }
333
+ _addListeners(target) {
334
+ if (!this._modalListeners) this._modalListeners = new Set;
335
+ [ "show", "hide", "toggle" ].forEach(action => {
336
+ const eventName = `${action}Modal:${target.id}`;
337
+ if (!this._modalListeners.has(eventName)) {
338
+ document.addEventListener(eventName, () => {
339
+ this.modals[target.id][action]();
340
+ });
341
+ this._modalListeners.add(eventName);
342
+ }
343
+ });
344
+ }
345
+ _optionsFromElements(target = null) {
346
+ let options = {};
347
+ if (this.hasPlacementValue) options["placement"] = this.placementValue;
348
+ if (this.hasBackdropValue) options["backdrop"] = this.backdropValue;
349
+ if (this.hasBackdropClassesValue) options["backdropClasses"] = this.backdropClassesValue;
350
+ if (this.hasClosableValue) options["closable"] = this.closableValue;
351
+ if (this.hasOnHideValue) options["onHide"] = this.onHideValue;
352
+ if (this.hasOnShowValue) options["onShow"] = this.onShowValue;
353
+ if (this.hasOnToggleValue) options["onToggle"] = this.onToggleValue;
354
+ if (target) {
355
+ if (target.dataset.placement) options["placement"] = target.dataset.placement;
356
+ if (target.dataset.backdrop !== undefined) options["backdrop"] = target.dataset.backdrop === "true";
357
+ if (target.dataset.backdropClasses) options["backdropClasses"] = target.dataset.backdropClasses;
358
+ if (target.dataset.closable) options["closable"] = target.dataset.edgeOffset;
359
+ if (target.dataset.onHide) options["onHide"] = target.dataset.onHide;
360
+ if (target.dataset.onShow) options["onShow"] = target.dataset.onShow;
361
+ if (target.dataset.onToggle) options["onToggle"] = target.dataset.onToggle;
362
+ }
363
+ return options;
364
+ }
365
+ _toCamelCase(str) {
366
+ return str.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
367
+ }
368
+ toggle(event) {
369
+ const targetId = event.target.dataset[this._toCamelCase(this.identifier + "-id")];
370
+ if (targetId) {
371
+ if (this.modals[targetId]) this.modals[targetId].toggle(); else console.warn(`Modal with id ${targetId} not found.`);
372
+ } else Object.entries(this.modals).forEach(([_id, modal]) => {
373
+ if (modal) modal.toggle();
374
+ });
375
+ }
376
+ show(event) {
377
+ const targetId = event.target.dataset[this._toCamelCase(this.identifier + "-id")];
378
+ if (targetId) {
379
+ if (this.modals[targetId]) this.modals[targetId].show(); else console.warn(`Modal with id ${targetId} not found.`);
380
+ } else Object.entries(this.modals).forEach(([_id, modal]) => {
381
+ if (modal) modal.show();
382
+ });
383
+ }
384
+ hide(event) {
385
+ const targetId = event.target.dataset[this._toCamelCase(this.identifier + "-id")];
386
+ if (targetId) {
387
+ if (this.modals[targetId]) this.modals[targetId].hide(); else console.warn(`Modal with id ${targetId} not found.`);
388
+ } else Object.entries(this.modals).forEach(([_id, modal]) => {
389
+ if (modal) modal.hide();
390
+ });
391
+ }
392
+ }
393
+
394
+ class FxPassword extends Controller {
395
+ static targets=[ "eyeIcon", "eyeSlashIcon", "inputWrapper", "strengthIndicator", "strengthBar", "checkLength", "checkLengthPass", "checkLengthFail", "checkUppercase", "checkUppercasePass", "checkUppercaseFail", "checkLowercase", "checkLowercasePass", "checkLowercaseFail", "checkNumbers", "checkNumbersPass", "checkNumbersFail", "checkSpecial", "checkSpecialPass", "checkSpecialFail" ];
396
+ static values={
397
+ minLength: {
398
+ type: Number,
399
+ default: 8
400
+ },
401
+ requireUppercase: {
402
+ type: Boolean,
403
+ default: true
404
+ },
405
+ requireLowercase: {
406
+ type: Boolean,
407
+ default: true
408
+ },
409
+ requireNumbers: {
410
+ type: Boolean,
411
+ default: true
412
+ },
413
+ requireSpecial: {
414
+ type: Boolean,
415
+ default: true
416
+ }
417
+ };
418
+ connect() {
419
+ this.passwordVisible = false;
420
+ this.passwordInput = this.element.querySelector('input[type="password"]');
421
+ if (!this.passwordInput) {
422
+ this.passwordInput = this.element.querySelector('input[type="text"]');
423
+ }
424
+ this.maxLength = this.passwordInput.getAttribute("maxlength");
425
+ }
426
+ toggleVisibility(event) {
427
+ event.preventDefault();
428
+ event.stopPropagation();
429
+ this.passwordVisible = !this.passwordVisible;
430
+ if (this.passwordVisible) {
431
+ this.passwordInput.type = "text";
432
+ this.showEyeSlash();
433
+ } else {
434
+ this.passwordInput.type = "password";
435
+ this.showEye();
436
+ }
437
+ if (this.maxLength) {
438
+ this.passwordInput.setAttribute("maxlength", this.maxLength);
439
+ }
440
+ }
441
+ showEye() {
442
+ if (!this.hasEyeIconTarget || !this.hasEyeSlashIconTarget) return;
443
+ this.eyeIconTarget.classList.remove("hidden");
444
+ this.eyeSlashIconTarget.classList.add("hidden");
445
+ }
446
+ showEyeSlash() {
447
+ if (!this.hasEyeIconTarget || !this.hasEyeSlashIconTarget) return;
448
+ this.eyeIconTarget.classList.add("hidden");
449
+ this.eyeSlashIconTarget.classList.remove("hidden");
450
+ }
451
+ validate() {
452
+ if (!this.hasStrengthIndicatorTarget) return;
453
+ const password = this.passwordInput.value;
454
+ const checks = {
455
+ length: password.length >= this.minLengthValue,
456
+ uppercase: this.requireUppercaseValue ? /[A-Z]/.test(password) : true,
457
+ lowercase: this.requireLowercaseValue ? /[a-z]/.test(password) : true,
458
+ numbers: this.requireNumbersValue ? /[0-9]/.test(password) : true,
459
+ special: this.requireSpecialValue ? /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?`~]/.test(password) : true
460
+ };
461
+ this.updateCheck("length", checks.length);
462
+ if (this.requireUppercaseValue) this.updateCheck("uppercase", checks.uppercase);
463
+ if (this.requireLowercaseValue) this.updateCheck("lowercase", checks.lowercase);
464
+ if (this.requireNumbersValue) this.updateCheck("numbers", checks.numbers);
465
+ if (this.requireSpecialValue) this.updateCheck("special", checks.special);
466
+ const totalChecks = Object.values(checks).length;
467
+ const passedChecks = Object.values(checks).filter(Boolean).length;
468
+ const strengthPercentage = passedChecks / totalChecks * 100;
469
+ this.updateStrengthBar(strengthPercentage);
470
+ }
471
+ updateCheck(type, passed) {
472
+ const capitalizedType = type.charAt(0).toUpperCase() + type.slice(1);
473
+ const checkTarget = `check${capitalizedType}Target`;
474
+ const passTarget = `check${capitalizedType}PassTarget`;
475
+ const failTarget = `check${capitalizedType}FailTarget`;
476
+ if (!this[`has${checkTarget.charAt(0).toUpperCase() + checkTarget.slice(1)}`]) return;
477
+ if (!this[`has${passTarget.charAt(0).toUpperCase() + passTarget.slice(1)}`]) return;
478
+ if (!this[`has${failTarget.charAt(0).toUpperCase() + failTarget.slice(1)}`]) return;
479
+ const checkElement = this[checkTarget];
480
+ const passIcon = this[passTarget];
481
+ const failIcon = this[failTarget];
482
+ const iconContainer = checkElement.querySelector(".flex-shrink-0");
483
+ if (passed) {
484
+ passIcon.classList.remove("hidden");
485
+ failIcon.classList.add("hidden");
486
+ iconContainer.classList.remove("text-red-500", "dark:text-red-400");
487
+ iconContainer.classList.add("text-green-500", "dark:text-green-400");
488
+ checkElement.classList.remove("text-slate-600", "dark:text-slate-400");
489
+ checkElement.classList.add("text-green-600", "dark:text-green-400");
490
+ } else {
491
+ passIcon.classList.add("hidden");
492
+ failIcon.classList.remove("hidden");
493
+ iconContainer.classList.remove("text-green-500", "dark:text-green-400");
494
+ iconContainer.classList.add("text-red-500", "dark:text-red-400");
495
+ checkElement.classList.remove("text-green-600", "dark:text-green-400");
496
+ checkElement.classList.add("text-slate-600", "dark:text-slate-400");
497
+ }
498
+ }
499
+ updateStrengthBar(percentage) {
500
+ if (!this.hasStrengthBarTarget) return;
501
+ this.strengthBarTarget.style.width = `${percentage}%`;
502
+ this.strengthBarTarget.classList.remove("bg-red-500", "dark:bg-red-400", "bg-yellow-500", "dark:bg-yellow-400", "bg-green-500", "dark:bg-green-400", "bg-slate-300", "dark:bg-slate-600");
503
+ if (percentage === 0) {
504
+ this.strengthBarTarget.classList.add("bg-slate-300", "dark:bg-slate-600");
505
+ } else if (percentage < 50) {
506
+ this.strengthBarTarget.classList.add("bg-red-500", "dark:bg-red-400");
507
+ } else if (percentage < 100) {
508
+ this.strengthBarTarget.classList.add("bg-yellow-500", "dark:bg-yellow-400");
509
+ } else {
510
+ this.strengthBarTarget.classList.add("bg-green-500", "dark:bg-green-400");
511
+ }
512
+ }
513
+ }
514
+
515
+ class FxProgress extends Controller {
516
+ static targets=[ "bar", "textLabel", "progressLabel" ];
517
+ static values={
518
+ progress: {
519
+ type: Number,
520
+ default: 0
521
+ },
522
+ animate: {
523
+ type: Boolean,
524
+ default: true
525
+ },
526
+ speed: {
527
+ type: String,
528
+ default: "normal"
529
+ }
530
+ };
531
+ connect() {
532
+ this.updateProgress();
533
+ this.updateBarTransition();
534
+ }
535
+ progressValueChanged() {
536
+ this.updateProgress();
537
+ }
538
+ animateValueChanged() {
539
+ this.updateBarTransition();
540
+ }
541
+ speedValueChanged() {
542
+ this.updateBarTransition();
543
+ }
544
+ updateProgress() {
545
+ const clampedProgress = Math.max(0, Math.min(100, this.progressValue));
546
+ if (this.hasBarTarget) {
547
+ this.barTarget.style.width = `${clampedProgress}%`;
548
+ this.barTarget.setAttribute("aria-valuenow", clampedProgress);
549
+ }
550
+ if (this.hasProgressLabelTarget) {
551
+ this.progressLabelTarget.textContent = `${clampedProgress}%`;
552
+ }
553
+ this.element.setAttribute("aria-valuenow", clampedProgress);
554
+ }
555
+ updateBarTransition() {
556
+ if (!this.hasBarTarget) return;
557
+ if (this.animateValue) {
558
+ let duration;
559
+ switch (this.speedValue) {
560
+ case "slow":
561
+ duration = "2s";
562
+ break;
563
+
564
+ case "fast":
565
+ duration = "0.3s";
566
+ break;
567
+
568
+ case "very_fast":
569
+ duration = "0.1s";
570
+ break;
571
+
572
+ case "normal":
573
+ default:
574
+ duration = "0.6s";
575
+ break;
576
+ }
577
+ this.barTarget.style.transition = `width ${duration} ease-out`;
578
+ } else {
579
+ this.barTarget.style.transition = "none";
580
+ }
581
+ }
582
+ setProgress(progress) {
583
+ this.progressValue = progress;
584
+ }
585
+ incrementProgress(amount = 1) {
586
+ this.progressValue = Math.min(100, this.progressValue + amount);
587
+ }
588
+ decrementProgress(amount = 1) {
589
+ this.progressValue = Math.max(0, this.progressValue - amount);
590
+ }
591
+ reset() {
592
+ this.progressValue = 0;
593
+ }
594
+ complete() {
595
+ this.progressValue = 100;
596
+ }
597
+ setSpeed(speed) {
598
+ this.speedValue = speed;
599
+ }
600
+ setAnimate(animate) {
601
+ this.animateValue = animate;
602
+ }
603
+ animateToProgress(targetProgress, duration = 1e3) {
604
+ const startProgress = this.progressValue;
605
+ const difference = targetProgress - startProgress;
606
+ const startTime = performance.now();
607
+ if (this.animationId) {
608
+ cancelAnimationFrame(this.animationId);
609
+ }
610
+ const animate = currentTime => {
611
+ const elapsed = currentTime - startTime;
612
+ const progress = Math.min(elapsed / duration, 1);
613
+ const easeOut = 1 - Math.pow(1 - progress, 3);
614
+ const currentProgress = startProgress + difference * easeOut;
615
+ this.updateProgressDirect(Math.round(currentProgress));
616
+ if (progress < 1) {
617
+ this.animationId = requestAnimationFrame(animate);
618
+ } else {
619
+ this.progressValue = targetProgress;
620
+ this.animationId = null;
621
+ }
622
+ };
623
+ this.animationId = requestAnimationFrame(animate);
624
+ }
625
+ updateProgressDirect(progress) {
626
+ const clampedProgress = Math.max(0, Math.min(100, progress));
627
+ if (this.hasBarTarget) {
628
+ this.barTarget.style.width = `${clampedProgress}%`;
629
+ this.barTarget.setAttribute("aria-valuenow", clampedProgress);
630
+ }
631
+ if (this.hasProgressLabelTarget) {
632
+ this.progressLabelTarget.textContent = `${clampedProgress}%`;
633
+ }
634
+ this.element.setAttribute("aria-valuenow", clampedProgress);
635
+ }
636
+ increment(event) {
637
+ const amount = parseInt(event.params?.amount) || 1;
638
+ const progressId = event.params?.id;
639
+ if (progressId) {
640
+ this.updateProgressById(progressId, controller => controller.incrementProgress(amount));
641
+ } else {
642
+ this.incrementProgress(amount);
643
+ }
644
+ }
645
+ decrement(event) {
646
+ const amount = parseInt(event.params?.amount) || 1;
647
+ const progressId = event.params?.id;
648
+ if (progressId) {
649
+ this.updateProgressById(progressId, controller => controller.decrementProgress(amount));
650
+ } else {
651
+ this.decrementProgress(amount);
652
+ }
653
+ }
654
+ resetProgress(event) {
655
+ const progressId = event.params?.id;
656
+ if (progressId) {
657
+ this.updateProgressById(progressId, controller => controller.reset());
658
+ } else {
659
+ this.reset();
660
+ }
661
+ }
662
+ completeProgress(event) {
663
+ const progressId = event.params?.id;
664
+ if (progressId) {
665
+ this.updateProgressById(progressId, controller => controller.complete());
666
+ } else {
667
+ this.complete();
668
+ }
669
+ }
670
+ animateTo(event) {
671
+ const target = parseInt(event.params?.target) || 100;
672
+ const duration = parseInt(event.params?.duration) || 1e3;
673
+ const progressId = event.params?.id;
674
+ if (progressId) {
675
+ this.updateProgressById(progressId, controller => controller.animateToProgress(target, duration));
676
+ } else {
677
+ this.animateToProgress(target, duration);
678
+ }
679
+ }
680
+ updateSpeed(event) {
681
+ const speed = event.params?.speed || "normal";
682
+ const progressId = event.params?.id;
683
+ if (progressId) {
684
+ this.updateProgressById(progressId, controller => controller.setSpeed(speed));
685
+ } else {
686
+ this.setSpeed(speed);
687
+ }
688
+ }
689
+ updateProgressById(progressId, callback) {
690
+ const progressContainer = this.element.querySelector(`[data-progress-id="${progressId}"]`);
691
+ if (!progressContainer) return;
692
+ const progressBar = progressContainer.querySelector('div[style*="width"]');
693
+ const textLabel = progressContainer.parentElement.querySelector("span:first-child");
694
+ const progressLabel = progressContainer.parentElement.querySelector("span:last-child");
695
+ if (!progressBar) return;
696
+ const tempController = {
697
+ barElement: progressBar,
698
+ textLabelElement: textLabel,
699
+ progressLabelElement: progressLabel,
700
+ containerElement: progressContainer,
701
+ setProgress: progress => {
702
+ const clampedProgress = Math.max(0, Math.min(100, progress));
703
+ this.updateSpecificProgress(progressBar, progressLabel, clampedProgress);
704
+ },
705
+ incrementProgress: (amount = 1) => {
706
+ const currentProgress = this.getCurrentProgress(progressBar);
707
+ const newProgress = Math.min(100, currentProgress + amount);
708
+ this.updateSpecificProgress(progressBar, progressLabel, newProgress);
709
+ },
710
+ decrementProgress: (amount = 1) => {
711
+ const currentProgress = this.getCurrentProgress(progressBar);
712
+ const newProgress = Math.max(0, currentProgress - amount);
713
+ this.updateSpecificProgress(progressBar, progressLabel, newProgress);
714
+ },
715
+ reset: () => {
716
+ this.updateSpecificProgress(progressBar, progressLabel, 0);
717
+ },
718
+ complete: () => {
719
+ this.updateSpecificProgress(progressBar, progressLabel, 100);
720
+ },
721
+ animateToProgress: (targetProgress, duration = 1e3) => {
722
+ const currentProgress = this.getCurrentProgress(progressBar);
723
+ this.animateSpecificProgress(progressBar, progressLabel, currentProgress, targetProgress, duration);
724
+ },
725
+ setSpeed: speed => {
726
+ let duration;
727
+ switch (speed) {
728
+ case "slow":
729
+ duration = "2s";
730
+ break;
731
+
732
+ case "fast":
733
+ duration = "0.3s";
734
+ break;
735
+
736
+ case "very_fast":
737
+ duration = "0.1s";
738
+ break;
739
+
740
+ default:
741
+ duration = "0.6s";
742
+ break;
743
+ }
744
+ progressBar.style.transition = `width ${duration} ease-out`;
745
+ }
746
+ };
747
+ if (callback) {
748
+ callback(tempController);
749
+ }
750
+ }
751
+ getCurrentProgress(progressBar) {
752
+ const widthStyle = progressBar.style.width;
753
+ return parseInt(widthStyle.replace("%", "")) || 0;
754
+ }
755
+ updateSpecificProgress(progressBar, progressLabel, progress) {
756
+ const clampedProgress = Math.max(0, Math.min(100, progress));
757
+ progressBar.style.width = `${clampedProgress}%`;
758
+ progressBar.setAttribute("aria-valuenow", clampedProgress);
759
+ if (progressLabel) {
760
+ progressLabel.textContent = `${clampedProgress}%`;
761
+ }
762
+ }
763
+ animateSpecificProgress(progressBar, progressLabel, startProgress, targetProgress, duration) {
764
+ const difference = targetProgress - startProgress;
765
+ const startTime = performance.now();
766
+ const animate = currentTime => {
767
+ const elapsed = currentTime - startTime;
768
+ const progress = Math.min(elapsed / duration, 1);
769
+ const easeOut = 1 - Math.pow(1 - progress, 3);
770
+ const currentProgress = startProgress + difference * easeOut;
771
+ this.updateSpecificProgress(progressBar, progressLabel, Math.round(currentProgress));
772
+ if (progress < 1) {
773
+ requestAnimationFrame(animate);
774
+ } else {
775
+ this.updateSpecificProgress(progressBar, progressLabel, targetProgress);
776
+ }
777
+ };
778
+ requestAnimationFrame(animate);
779
+ }
780
+ disconnect() {
781
+ if (this.animationId) {
782
+ cancelAnimationFrame(this.animationId);
783
+ this.animationId = null;
784
+ }
785
+ }
786
+ static getController(element) {
787
+ if (typeof element === "string") {
788
+ element = document.querySelector(element);
789
+ }
790
+ if (!element) return null;
791
+ const controllerElement = element.closest('[data-controller*="fx-progress"]');
792
+ if (!controllerElement) return null;
793
+ return window.Stimulus?.getControllerForElementAndIdentifier(controllerElement, "fx-progress") || application?.getControllerForElementAndIdentifier(controllerElement, "fx-progress");
794
+ }
795
+ static updateProgress(selector, progress) {
796
+ const controller = this.getController(selector);
797
+ if (controller) controller.setProgress(progress);
798
+ }
799
+ static incrementProgress(selector, amount = 10) {
800
+ const controller = this.getController(selector);
801
+ if (controller) controller.incrementProgress(amount);
802
+ }
803
+ static animateProgress(selector, target, duration = 1e3) {
804
+ const controller = this.getController(selector);
805
+ if (controller) controller.animateToProgress(target, duration);
806
+ }
807
+ static resetProgress(selector) {
808
+ const controller = this.getController(selector);
809
+ if (controller) controller.reset();
810
+ }
811
+ static completeProgress(selector) {
812
+ const controller = this.getController(selector);
813
+ if (controller) controller.complete();
814
+ }
815
+ static updateProgressById(selector, progressId, callback) {
816
+ const controller = this.getController(selector);
817
+ if (controller) controller.updateProgressById(progressId, callback);
818
+ }
819
+ }
820
+
821
+ class FxRowClick extends Controller {
822
+ static values={
823
+ url: String,
824
+ frame: String
825
+ };
826
+ connect() {
827
+ if (this.element.tagName !== "TR") {
828
+ console.warn("row-click controller should only be used on <tr> tags");
829
+ return;
830
+ }
831
+ this.element.addEventListener("click", this.navigate.bind(this));
832
+ }
833
+ disconnect() {
834
+ this.element.removeEventListener("click", this.navigate.bind(this));
835
+ }
836
+ navigate(event) {
837
+ if (event.target === event.currentTarget || !event.target.closest("button, a, input, select, textarea")) {
838
+ if (this.frameValue) {
839
+ window.Turbo.visit(this.urlValue, {
840
+ frame: this.frameValue
841
+ });
842
+ } else {
843
+ window.Turbo.visit(this.urlValue);
844
+ }
845
+ }
846
+ }
847
+ }
848
+
849
+ class FxSelectAll extends Controller {
850
+ static targets=[ "selectAll", "select", "disableOnEmptySelect", "enableOnEmptySelect", "hideOnEmptySelect", "showOnEmptySelect", "count" ];
851
+ static values={
852
+ disableIndeterminate: {
853
+ type: Boolean,
854
+ default: false
855
+ }
856
+ };
857
+ initialize() {
858
+ this.toggle = this.toggle.bind(this);
859
+ this.refresh = this.refresh.bind(this);
860
+ }
861
+ selectAllTargetConnected(checkbox) {
862
+ checkbox.addEventListener("change", this.toggle);
863
+ this.refresh();
864
+ }
865
+ selectTargetConnected(checkbox) {
866
+ checkbox.addEventListener("change", this.refresh);
867
+ this.refresh();
868
+ }
869
+ selectAllTargetDisconnected(checkbox) {
870
+ checkbox.removeEventListener("change", this.toggle);
871
+ this.refresh();
872
+ }
873
+ selectTargetDisconnected(checkbox) {
874
+ checkbox.removeEventListener("change", this.refresh);
875
+ this.refresh();
876
+ }
877
+ toggle(e) {
878
+ e.preventDefault();
879
+ this.selectTargets.forEach(checkbox => {
880
+ checkbox.checked = e.target.checked;
881
+ this.triggerInputEvent(checkbox);
882
+ });
883
+ this.refresh();
884
+ }
885
+ refresh() {
886
+ const checkboxesCount = this.selectTargets.length;
887
+ const checkboxesCheckedCount = this.checked.length;
888
+ if (this.disableIndeterminateValue) {
889
+ this.selectAllTarget.checked = checkboxesCheckedCount === checkboxesCount;
890
+ } else {
891
+ this.selectAllTarget.checked = checkboxesCheckedCount > 0;
892
+ this.selectAllTarget.indeterminate = checkboxesCheckedCount > 0 && checkboxesCheckedCount < checkboxesCount;
893
+ }
894
+ this.updateVisibility();
895
+ this.updateDisabledState();
896
+ this.updateCount();
897
+ }
898
+ updateVisibility() {
899
+ const hasChecked = this.checkedCount() > 0;
900
+ this.hideOnEmptySelectTargets.forEach(el => {
901
+ el.classList.toggle("hidden", hasChecked);
902
+ });
903
+ this.showOnEmptySelectTargets.forEach(el => {
904
+ el.classList.toggle("hidden", !hasChecked);
905
+ });
906
+ }
907
+ updateDisabledState() {
908
+ const hasChecked = this.checkedCount() > 0;
909
+ this.disableOnEmptySelectTargets.forEach(el => {
910
+ el.disabled = !hasChecked;
911
+ });
912
+ this.enableOnEmptySelectTargets.forEach(el => {
913
+ el.disabled = hasChecked;
914
+ });
915
+ }
916
+ updateCount() {
917
+ this.countTargets.forEach(el => {
918
+ el.textContent = this.checkedCount().toString();
919
+ });
920
+ }
921
+ triggerInputEvent(checkbox) {
922
+ const event = new Event("input", {
923
+ bubbles: false,
924
+ cancelable: true
925
+ });
926
+ checkbox.dispatchEvent(event);
927
+ }
928
+ checkedCount() {
929
+ return this.checked.length;
930
+ }
931
+ get checked() {
932
+ return this.selectTargets.filter(checkbox => checkbox.checked);
933
+ }
934
+ get unchecked() {
935
+ return this.selectTargets.filter(checkbox => !checkbox.checked);
936
+ }
937
+ }
938
+
939
+ class FxSpinnerPercent extends Controller {
940
+ static targets=[ "progress", "text" ];
941
+ static values={
942
+ percent: {
943
+ type: Number,
944
+ default: 0
945
+ },
946
+ animate: {
947
+ type: Boolean,
948
+ default: false
949
+ },
950
+ speed: {
951
+ type: String,
952
+ default: "normal"
953
+ },
954
+ hasCustomText: {
955
+ type: Boolean,
956
+ default: false
957
+ }
958
+ };
959
+ connect() {
960
+ this.circumference = 2 * Math.PI * 45;
961
+ this.updateProgress();
962
+ this.updateAnimation();
963
+ }
964
+ percentValueChanged() {
965
+ this.updateProgress();
966
+ }
967
+ animateValueChanged() {
968
+ this.updateAnimation();
969
+ }
970
+ speedValueChanged() {
971
+ this.updateAnimation();
972
+ }
973
+ updateProgress() {
974
+ const clampedPercent = Math.max(0, Math.min(100, this.percentValue));
975
+ const offset = this.circumference - clampedPercent / 100 * this.circumference;
976
+ if (this.hasProgressTarget) {
977
+ this.progressTarget.style.strokeDashoffset = offset;
978
+ this.progressTarget.setAttribute("aria-valuenow", clampedPercent);
979
+ }
980
+ if (this.hasTextTarget && !this.hasCustomTextValue) {
981
+ this.textTarget.textContent = `${clampedPercent}%`;
982
+ }
983
+ this.element.setAttribute("aria-valuenow", clampedPercent);
984
+ }
985
+ updateAnimation() {
986
+ const svg = this.element.querySelector("svg");
987
+ if (!svg) return;
988
+ if (this.animateValue) {
989
+ let duration;
990
+ switch (this.speedValue) {
991
+ case "slow":
992
+ duration = "3s";
993
+ break;
994
+
995
+ case "fast":
996
+ duration = "0.5s";
997
+ break;
998
+
999
+ case "very_fast":
1000
+ duration = "0.3s";
1001
+ break;
1002
+
1003
+ case "normal":
1004
+ default:
1005
+ duration = "1s";
1006
+ break;
1007
+ }
1008
+ svg.style.animation = `spin ${duration} linear infinite`;
1009
+ } else {
1010
+ svg.style.animation = "";
1011
+ }
1012
+ }
1013
+ setPercent(percent) {
1014
+ this.percentValue = percent;
1015
+ }
1016
+ setAnimate(animate) {
1017
+ this.animateValue = animate;
1018
+ }
1019
+ startAnimation() {
1020
+ this.animateValue = true;
1021
+ }
1022
+ stopAnimation() {
1023
+ this.animateValue = false;
1024
+ }
1025
+ setSpeed(speed) {
1026
+ this.speedValue = speed;
1027
+ }
1028
+ animateToPercent(targetPercent, duration = 1e3) {
1029
+ const startPercent = this.percentValue;
1030
+ const difference = targetPercent - startPercent;
1031
+ const startTime = performance.now();
1032
+ if (this.animationId) {
1033
+ cancelAnimationFrame(this.animationId);
1034
+ }
1035
+ const animate = currentTime => {
1036
+ const elapsed = currentTime - startTime;
1037
+ const progress = Math.min(elapsed / duration, 1);
1038
+ const easeOut = 1 - Math.pow(1 - progress, 3);
1039
+ const currentPercent = startPercent + difference * easeOut;
1040
+ this.updateProgressDirect(Math.round(currentPercent));
1041
+ if (progress < 1) {
1042
+ this.animationId = requestAnimationFrame(animate);
1043
+ } else {
1044
+ this.percentValue = targetPercent;
1045
+ this.animationId = null;
1046
+ }
1047
+ };
1048
+ this.animationId = requestAnimationFrame(animate);
1049
+ }
1050
+ updateProgressDirect(percent) {
1051
+ const clampedPercent = Math.max(0, Math.min(100, percent));
1052
+ const offset = this.circumference - clampedPercent / 100 * this.circumference;
1053
+ if (this.hasProgressTarget) {
1054
+ this.progressTarget.style.strokeDashoffset = offset;
1055
+ this.progressTarget.setAttribute("aria-valuenow", clampedPercent);
1056
+ }
1057
+ if (this.hasTextTarget && !this.hasCustomTextValue) {
1058
+ this.textTarget.textContent = `${clampedPercent}%`;
1059
+ }
1060
+ this.element.setAttribute("aria-valuenow", clampedPercent);
1061
+ }
1062
+ disconnect() {
1063
+ if (this.animationId) {
1064
+ cancelAnimationFrame(this.animationId);
1065
+ this.animationId = null;
1066
+ }
1067
+ }
1068
+ }
1069
+
1070
+ class FxThemeButton extends Controller {
1071
+ static targets=[ "lightIcon", "darkIcon", "systemIcon" ];
1072
+ static values={
1073
+ theme: {
1074
+ type: String,
1075
+ default: "system"
1076
+ }
1077
+ };
1078
+ connect() {
1079
+ this.themeValue = this.getSavedTheme() || "system";
1080
+ this.applyTheme(this.themeValue);
1081
+ this.updateIcon();
1082
+ }
1083
+ toggle() {
1084
+ const themes = [ "light", "dark", "system" ];
1085
+ const currentIndex = themes.indexOf(this.themeValue);
1086
+ const nextIndex = (currentIndex + 1) % themes.length;
1087
+ this.themeValue = themes[nextIndex];
1088
+ this.applyTheme(this.themeValue);
1089
+ this.saveTheme(this.themeValue);
1090
+ this.updateIcon();
1091
+ this.dispatch("changed", {
1092
+ detail: {
1093
+ theme: this.themeValue
1094
+ }
1095
+ });
1096
+ }
1097
+ applyTheme(theme) {
1098
+ const html = document.documentElement;
1099
+ if (theme === "system") {
1100
+ localStorage.removeItem("theme");
1101
+ if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
1102
+ html.classList.add("dark");
1103
+ } else {
1104
+ html.classList.remove("dark");
1105
+ }
1106
+ } else if (theme === "dark") {
1107
+ localStorage.setItem("theme", "dark");
1108
+ html.classList.add("dark");
1109
+ } else {
1110
+ localStorage.setItem("theme", "light");
1111
+ html.classList.remove("dark");
1112
+ }
1113
+ }
1114
+ getSavedTheme() {
1115
+ const saved = localStorage.getItem("theme");
1116
+ if (saved) return saved;
1117
+ if (document.documentElement.classList.contains("dark")) {
1118
+ return "dark";
1119
+ }
1120
+ return "system";
1121
+ }
1122
+ saveTheme(theme) {
1123
+ if (theme === "system") {
1124
+ localStorage.removeItem("theme");
1125
+ } else {
1126
+ localStorage.setItem("theme", theme);
1127
+ }
1128
+ }
1129
+ updateIcon() {
1130
+ this.lightIconTargets.forEach(icon => icon.classList.add("hidden"));
1131
+ this.darkIconTargets.forEach(icon => icon.classList.add("hidden"));
1132
+ this.systemIconTargets.forEach(icon => icon.classList.add("hidden"));
1133
+ if (this.themeValue === "light" && this.hasLightIconTarget) {
1134
+ this.lightIconTargets.forEach(icon => icon.classList.remove("hidden"));
1135
+ } else if (this.themeValue === "dark" && this.hasDarkIconTarget) {
1136
+ this.darkIconTargets.forEach(icon => icon.classList.remove("hidden"));
1137
+ } else if (this.themeValue === "system" && this.hasSystemIconTarget) {
1138
+ this.systemIconTargets.forEach(icon => icon.classList.remove("hidden"));
1139
+ }
1140
+ }
1141
+ themeValueChanged() {
1142
+ this.updateIcon();
1143
+ }
1144
+ }
1145
+
1146
+ function registerFluxbitControllers(application) {
1147
+ application.register("fx-assigner", FxAssigner);
1148
+ application.register("fx-auto-submit", FxAutoSubmit);
1149
+ application.register("fx-drawer", FxDrawer);
1150
+ application.register("fx-method-link", FxMethodLink);
1151
+ application.register("fx-modal", FxModal);
1152
+ application.register("fx-password", FxPassword);
1153
+ application.register("fx-progress", FxProgress);
1154
+ application.register("fx-row-click", FxRowClick);
1155
+ application.register("fx-select-all", FxSelectAll);
1156
+ application.register("fx-spinner-percent", FxSpinnerPercent);
1157
+ application.register("fx-theme-button", FxThemeButton);
1158
+ if (typeof window !== "undefined") {
1159
+ window.FluxbitControllers = {
1160
+ FxAssigner: FxAssigner,
1161
+ FxAutoSubmit: FxAutoSubmit,
1162
+ FxDrawer: FxDrawer,
1163
+ FxMethodLink: FxMethodLink,
1164
+ FxModal: FxModal,
1165
+ FxPassword: FxPassword,
1166
+ FxProgress: FxProgress,
1167
+ FxRowClick: FxRowClick,
1168
+ FxSelectAll: FxSelectAll,
1169
+ FxSpinnerPercent: FxSpinnerPercent,
1170
+ FxThemeButton: FxThemeButton
1171
+ };
1172
+ }
1173
+ }
1174
+
1175
+ export { FxAssigner, FxAutoSubmit, FxDrawer, FxMethodLink, FxModal, FxPassword, FxProgress, FxRowClick, FxSelectAll, FxSpinnerPercent, FxThemeButton, registerFluxbitControllers };