easy-admin-rails 0.1.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 (203) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/builds/easy_admin.base.js +43505 -0
  6. data/app/assets/builds/easy_admin.base.js.map +7 -0
  7. data/app/assets/builds/easy_admin.css +6141 -0
  8. data/app/assets/config/easy_admin_manifest.js +1 -0
  9. data/app/assets/images/jsoneditor-icons.svg +749 -0
  10. data/app/assets/stylesheets/easy_admin/application.tailwind.css +390 -0
  11. data/app/components/easy_admin/base_component.rb +35 -0
  12. data/app/components/easy_admin/batch_action_bar_component.rb +125 -0
  13. data/app/components/easy_admin/batch_action_form_component.rb +124 -0
  14. data/app/components/easy_admin/combined_filters_component.rb +232 -0
  15. data/app/components/easy_admin/confirmation_modal_component.rb +61 -0
  16. data/app/components/easy_admin/context_menu_component.rb +161 -0
  17. data/app/components/easy_admin/dashboards/base_card_component.rb +152 -0
  18. data/app/components/easy_admin/dashboards/card_error_component.rb +23 -0
  19. data/app/components/easy_admin/dashboards/card_factory.rb +90 -0
  20. data/app/components/easy_admin/dashboards/card_stream_component.rb +22 -0
  21. data/app/components/easy_admin/dashboards/cards/base_card_component.rb +54 -0
  22. data/app/components/easy_admin/dashboards/cards/chart_card_component.rb +175 -0
  23. data/app/components/easy_admin/dashboards/cards/custom_card_component.rb +50 -0
  24. data/app/components/easy_admin/dashboards/cards/metric_card_component.rb +164 -0
  25. data/app/components/easy_admin/dashboards/cards/table_card_component.rb +148 -0
  26. data/app/components/easy_admin/dashboards/chart_card_component.rb +44 -0
  27. data/app/components/easy_admin/dashboards/metric_card_component.rb +56 -0
  28. data/app/components/easy_admin/dashboards/refresh_stream_component.rb +279 -0
  29. data/app/components/easy_admin/dashboards/show_component.rb +163 -0
  30. data/app/components/easy_admin/dashboards/table_card_component.rb +52 -0
  31. data/app/components/easy_admin/date_picker_component.rb +188 -0
  32. data/app/components/easy_admin/fields/base_component.rb +101 -0
  33. data/app/components/easy_admin/fields/belongs_to_edit_modal_component.rb +117 -0
  34. data/app/components/easy_admin/fields/form/belongs_to_component.rb +82 -0
  35. data/app/components/easy_admin/fields/form/boolean_component.rb +100 -0
  36. data/app/components/easy_admin/fields/form/date_component.rb +55 -0
  37. data/app/components/easy_admin/fields/form/datetime_component.rb +55 -0
  38. data/app/components/easy_admin/fields/form/email_component.rb +55 -0
  39. data/app/components/easy_admin/fields/form/file_component.rb +190 -0
  40. data/app/components/easy_admin/fields/form/has_many_component.rb +416 -0
  41. data/app/components/easy_admin/fields/form/json_component.rb +81 -0
  42. data/app/components/easy_admin/fields/form/number_component.rb +55 -0
  43. data/app/components/easy_admin/fields/form/select_component.rb +326 -0
  44. data/app/components/easy_admin/fields/form/text_component.rb +55 -0
  45. data/app/components/easy_admin/fields/form/textarea_component.rb +54 -0
  46. data/app/components/easy_admin/fields/index/belongs_to_component.rb +93 -0
  47. data/app/components/easy_admin/fields/index/boolean_component.rb +29 -0
  48. data/app/components/easy_admin/fields/index/date_component.rb +13 -0
  49. data/app/components/easy_admin/fields/index/datetime_component.rb +13 -0
  50. data/app/components/easy_admin/fields/index/email_component.rb +24 -0
  51. data/app/components/easy_admin/fields/index/filters/base_component.rb +48 -0
  52. data/app/components/easy_admin/fields/index/filters/boolean_component.rb +96 -0
  53. data/app/components/easy_admin/fields/index/filters/date_component.rb +182 -0
  54. data/app/components/easy_admin/fields/index/filters/number_component.rb +30 -0
  55. data/app/components/easy_admin/fields/index/filters/select_component.rb +101 -0
  56. data/app/components/easy_admin/fields/index/filters/string_component.rb +32 -0
  57. data/app/components/easy_admin/fields/index/json_component.rb +23 -0
  58. data/app/components/easy_admin/fields/index/number_component.rb +20 -0
  59. data/app/components/easy_admin/fields/index/select_component.rb +25 -0
  60. data/app/components/easy_admin/fields/index/text_component.rb +20 -0
  61. data/app/components/easy_admin/fields/inline_edit_modal_component.rb +135 -0
  62. data/app/components/easy_admin/fields/inline_edit_trigger_component.rb +144 -0
  63. data/app/components/easy_admin/fields/show/belongs_to_component.rb +93 -0
  64. data/app/components/easy_admin/fields/show/boolean_component.rb +21 -0
  65. data/app/components/easy_admin/fields/show/date_component.rb +13 -0
  66. data/app/components/easy_admin/fields/show/datetime_component.rb +13 -0
  67. data/app/components/easy_admin/fields/show/email_component.rb +19 -0
  68. data/app/components/easy_admin/fields/show/file_component.rb +304 -0
  69. data/app/components/easy_admin/fields/show/has_many_component.rb +192 -0
  70. data/app/components/easy_admin/fields/show/json_component.rb +45 -0
  71. data/app/components/easy_admin/fields/show/number_component.rb +20 -0
  72. data/app/components/easy_admin/fields/show/select_component.rb +25 -0
  73. data/app/components/easy_admin/fields/show/text_component.rb +17 -0
  74. data/app/components/easy_admin/fields/show/textarea_component.rb +26 -0
  75. data/app/components/easy_admin/filters_component.rb +120 -0
  76. data/app/components/easy_admin/form_tabs_component.rb +166 -0
  77. data/app/components/easy_admin/infinite_scroll_component.rb +82 -0
  78. data/app/components/easy_admin/lazy_chart_card_component.rb +128 -0
  79. data/app/components/easy_admin/lazy_metric_card_component.rb +76 -0
  80. data/app/components/easy_admin/modal_frame_component.rb +26 -0
  81. data/app/components/easy_admin/navbar_component.rb +226 -0
  82. data/app/components/easy_admin/notification_component.rb +83 -0
  83. data/app/components/easy_admin/pagination_component.rb +188 -0
  84. data/app/components/easy_admin/quick_filters_component.rb +65 -0
  85. data/app/components/easy_admin/resource_pagination_component.rb +14 -0
  86. data/app/components/easy_admin/resources/index_component.rb +211 -0
  87. data/app/components/easy_admin/resources/index_frame_component.rb +88 -0
  88. data/app/components/easy_admin/resources/show_page_actions_component.rb +324 -0
  89. data/app/components/easy_admin/resources/table_cell_component.rb +145 -0
  90. data/app/components/easy_admin/resources/table_component.rb +206 -0
  91. data/app/components/easy_admin/resources/table_row_component.rb +160 -0
  92. data/app/components/easy_admin/row_action_form_component.rb +127 -0
  93. data/app/components/easy_admin/scopes_component.rb +224 -0
  94. data/app/components/easy_admin/settings_sidebar_component.rb +140 -0
  95. data/app/components/easy_admin/show_layout_component.rb +600 -0
  96. data/app/components/easy_admin/sidebar_component.rb +174 -0
  97. data/app/components/easy_admin/turbo/response_component.rb +40 -0
  98. data/app/components/easy_admin/turbo/stream_component.rb +28 -0
  99. data/app/controllers/easy_admin/application_controller.rb +66 -0
  100. data/app/controllers/easy_admin/batch_actions_controller.rb +166 -0
  101. data/app/controllers/easy_admin/confirmation_modal_controller.rb +20 -0
  102. data/app/controllers/easy_admin/dashboard_controller.rb +6 -0
  103. data/app/controllers/easy_admin/dashboards_controller.rb +123 -0
  104. data/app/controllers/easy_admin/passwords_controller.rb +15 -0
  105. data/app/controllers/easy_admin/registrations_controller.rb +52 -0
  106. data/app/controllers/easy_admin/resources_controller.rb +907 -0
  107. data/app/controllers/easy_admin/row_actions_controller.rb +216 -0
  108. data/app/controllers/easy_admin/sessions_controller.rb +32 -0
  109. data/app/controllers/easy_admin/settings_controller.rb +94 -0
  110. data/app/helpers/easy_admin/application_helper.rb +4 -0
  111. data/app/helpers/easy_admin/dashboards_helper.rb +121 -0
  112. data/app/helpers/easy_admin/fields_helper.rb +27 -0
  113. data/app/helpers/easy_admin/pagy_helper.rb +30 -0
  114. data/app/helpers/easy_admin/resources_helper.rb +39 -0
  115. data/app/javascript/easy_admin/application.js +12 -0
  116. data/app/javascript/easy_admin/controllers/batch_modal_controller.js +66 -0
  117. data/app/javascript/easy_admin/controllers/batch_selection_controller.js +223 -0
  118. data/app/javascript/easy_admin/controllers/chart_controller.js +216 -0
  119. data/app/javascript/easy_admin/controllers/collapsible_filters_controller.js +118 -0
  120. data/app/javascript/easy_admin/controllers/confirmation_modal_controller.js +64 -0
  121. data/app/javascript/easy_admin/controllers/context_menu_controller.js +227 -0
  122. data/app/javascript/easy_admin/controllers/date_picker_controller.js +309 -0
  123. data/app/javascript/easy_admin/controllers/dropdown_controller.js +63 -0
  124. data/app/javascript/easy_admin/controllers/event_emitter_controller.js +19 -0
  125. data/app/javascript/easy_admin/controllers/file_controller.js +121 -0
  126. data/app/javascript/easy_admin/controllers/form_tabs_controller.js +100 -0
  127. data/app/javascript/easy_admin/controllers/has_many_search_controller.js +76 -0
  128. data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +174 -0
  129. data/app/javascript/easy_admin/controllers/ios_alert_controller.js +195 -0
  130. data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +88 -0
  131. data/app/javascript/easy_admin/controllers/modal_controller.js +75 -0
  132. data/app/javascript/easy_admin/controllers/navbar_scroll_controller.js +76 -0
  133. data/app/javascript/easy_admin/controllers/notification_controller.js +48 -0
  134. data/app/javascript/easy_admin/controllers/row_action_controller.js +124 -0
  135. data/app/javascript/easy_admin/controllers/row_modal_controller.js +59 -0
  136. data/app/javascript/easy_admin/controllers/select_field_controller.js +618 -0
  137. data/app/javascript/easy_admin/controllers/settings_button_controller.js +8 -0
  138. data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +186 -0
  139. data/app/javascript/easy_admin/controllers/sidebar_controller.js +102 -0
  140. data/app/javascript/easy_admin/controllers/sidebar_mobile_controller.js +23 -0
  141. data/app/javascript/easy_admin/controllers/sidebar_nav_controller.js +96 -0
  142. data/app/javascript/easy_admin/controllers/table_controller.js +28 -0
  143. data/app/javascript/easy_admin/controllers/table_row_controller.js +16 -0
  144. data/app/javascript/easy_admin/controllers/toggle_switch_controller.js +22 -0
  145. data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +9 -0
  146. data/app/javascript/easy_admin/controllers.js +54 -0
  147. data/app/javascript/easy_admin.base.js +4 -0
  148. data/app/models/easy_admin/admin_user.rb +53 -0
  149. data/app/models/easy_admin/application_record.rb +5 -0
  150. data/app/views/easy_admin/dashboard/index.html.erb +3 -0
  151. data/app/views/easy_admin/dashboards/show.html.erb +7 -0
  152. data/app/views/easy_admin/passwords/edit.html.erb +42 -0
  153. data/app/views/easy_admin/passwords/new.html.erb +41 -0
  154. data/app/views/easy_admin/registrations/new.html.erb +65 -0
  155. data/app/views/easy_admin/resources/_redirect.turbo_stream.erb +3 -0
  156. data/app/views/easy_admin/resources/_table_rows.html.erb +46 -0
  157. data/app/views/easy_admin/resources/edit.html.erb +151 -0
  158. data/app/views/easy_admin/resources/index.html.erb +12 -0
  159. data/app/views/easy_admin/resources/index.turbo_stream.erb +139 -0
  160. data/app/views/easy_admin/resources/index_frame.html.erb +142 -0
  161. data/app/views/easy_admin/resources/new.html.erb +100 -0
  162. data/app/views/easy_admin/resources/show.html.erb +31 -0
  163. data/app/views/easy_admin/sessions/new.html.erb +55 -0
  164. data/app/views/easy_admin/settings/_form.html.erb +51 -0
  165. data/app/views/easy_admin/settings/index.html.erb +53 -0
  166. data/app/views/layouts/easy_admin/application.html.erb +48 -0
  167. data/app/views/layouts/easy_admin/auth.html.erb +34 -0
  168. data/config/initializers/easy_admin_card_factory.rb +27 -0
  169. data/config/initializers/pagy.rb +15 -0
  170. data/config/initializers/rack_mini_profiler.rb +67 -0
  171. data/config/routes.rb +70 -0
  172. data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +45 -0
  173. data/lib/easy-admin.rb +32 -0
  174. data/lib/easy_admin/action.rb +159 -0
  175. data/lib/easy_admin/batch_action.rb +134 -0
  176. data/lib/easy_admin/configuration.rb +75 -0
  177. data/lib/easy_admin/dashboard.rb +110 -0
  178. data/lib/easy_admin/dashboard_registry.rb +30 -0
  179. data/lib/easy_admin/delete_action.rb +22 -0
  180. data/lib/easy_admin/engine.rb +54 -0
  181. data/lib/easy_admin/field.rb +118 -0
  182. data/lib/easy_admin/resource.rb +806 -0
  183. data/lib/easy_admin/resource_registry.rb +22 -0
  184. data/lib/easy_admin/types/json_type.rb +25 -0
  185. data/lib/easy_admin/version.rb +3 -0
  186. data/lib/generators/easy_admin/auth_generator.rb +69 -0
  187. data/lib/generators/easy_admin/card/card_generator.rb +94 -0
  188. data/lib/generators/easy_admin/card/templates/card_component.rb.erb +127 -0
  189. data/lib/generators/easy_admin/card/templates/card_component_spec.rb.erb +122 -0
  190. data/lib/generators/easy_admin/install/templates/easy_admin.rb +31 -0
  191. data/lib/generators/easy_admin/install_generator.rb +25 -0
  192. data/lib/generators/easy_admin/rbac/rbac_generator.rb +244 -0
  193. data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +23 -0
  194. data/lib/generators/easy_admin/rbac/templates/super_admin.rb +34 -0
  195. data/lib/generators/easy_admin/resource_generator.rb +43 -0
  196. data/lib/generators/easy_admin/templates/AUTH_README +35 -0
  197. data/lib/generators/easy_admin/templates/README +27 -0
  198. data/lib/generators/easy_admin/templates/create_easy_admin_admin_users.rb +45 -0
  199. data/lib/generators/easy_admin/templates/devise.rb +267 -0
  200. data/lib/generators/easy_admin/templates/easy_admin.rb +24 -0
  201. data/lib/generators/easy_admin/templates/resource.rb +29 -0
  202. data/lib/tasks/easy_admin_tasks.rake +4 -0
  203. metadata +445 -0
@@ -0,0 +1,227 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { get, post } from "@rails/request.js"
3
+
4
+ export default class extends Controller {
5
+ static targets = ["menu"]
6
+ static values = {
7
+ recordId: String,
8
+ resourceName: String
9
+ }
10
+
11
+ connect() {
12
+ // Hide menu when clicking outside
13
+ this.boundHideMenu = this.hideMenuOnOutsideClick.bind(this)
14
+ document.addEventListener('click', this.boundHideMenu)
15
+
16
+ // Hide menu when scrolling
17
+ this.boundHideOnScroll = this.hideMenu.bind(this)
18
+ document.addEventListener('scroll', this.boundHideOnScroll, true)
19
+ }
20
+
21
+ disconnect() {
22
+ document.removeEventListener('click', this.boundHideMenu)
23
+ document.removeEventListener('scroll', this.boundHideOnScroll, true)
24
+ this.removeExistingMenu()
25
+ }
26
+
27
+ showMenu(event) {
28
+ // Prevent default right-click menu
29
+ event.preventDefault()
30
+ event.stopPropagation()
31
+
32
+ // Remove any existing menu
33
+ this.removeExistingMenu()
34
+
35
+ // Get row data
36
+ const row = event.currentTarget
37
+ const recordId = this.recordIdValue || row.dataset.recordId
38
+ const resourceName = this.resourceNameValue || row.dataset.resourceName
39
+
40
+ if (!recordId || !resourceName) {
41
+ console.error('Missing recordId or resourceName for context menu')
42
+ return
43
+ }
44
+
45
+ // Store position for menu placement
46
+ this.menuX = event.clientX
47
+ this.menuY = event.clientY
48
+
49
+ // Load context menu via Turbo
50
+ this.loadContextMenu(resourceName, recordId)
51
+ }
52
+
53
+ async loadContextMenu(resourceName, recordId) {
54
+ try {
55
+ // Load menu content via Turbo - it will update the context-menu-{recordId} container
56
+ const response = await get(`/admin/${resourceName}/${recordId}/context_menu`, {
57
+ responseKind: 'turbo-stream'
58
+ })
59
+
60
+ // After turbo stream loads, position and show the menu
61
+ setTimeout(() => {
62
+ this.positionAndShowMenu(recordId)
63
+ }, 10)
64
+
65
+ } catch (error) {
66
+ console.error('Failed to load context menu:', error)
67
+ this.removeExistingMenu()
68
+ }
69
+ }
70
+
71
+ positionAndShowMenu(recordId) {
72
+ const menuContainer = document.getElementById(`context-menu-${recordId}`)
73
+ const contextMenu = menuContainer?.querySelector('.context-menu')
74
+
75
+ if (!menuContainer || !contextMenu) return
76
+
77
+ // Get menu dimensions after it's been rendered
78
+ const menuRect = contextMenu.getBoundingClientRect()
79
+ const windowWidth = window.innerWidth
80
+ const windowHeight = window.innerHeight
81
+
82
+ // Calculate position to keep menu within viewport
83
+ let x = this.menuX
84
+ let y = this.menuY
85
+
86
+ // Adjust horizontal position if menu would overflow right edge
87
+ if (x + menuRect.width > windowWidth) {
88
+ x = windowWidth - menuRect.width - 10
89
+ }
90
+
91
+ // Adjust vertical position if menu would overflow bottom edge
92
+ if (y + menuRect.height > windowHeight) {
93
+ y = windowHeight - menuRect.height - 10
94
+ }
95
+
96
+ // Ensure menu doesn't go off left or top edges
97
+ x = Math.max(10, x)
98
+ y = Math.max(10, y)
99
+
100
+ // Apply final position to container and enable pointer events
101
+ menuContainer.style.left = `${x}px`
102
+ menuContainer.style.top = `${y}px`
103
+ menuContainer.classList.remove('pointer-events-none')
104
+
105
+ // Make context menu visible with animation
106
+ contextMenu.classList.add('visible')
107
+ }
108
+
109
+ hideMenu() {
110
+ this.removeExistingMenu()
111
+ }
112
+
113
+ hideMenuOnOutsideClick(event) {
114
+ // Don't hide if clicking inside the menu
115
+ const menuContainer = document.getElementById('context-menu-container')
116
+ if (menuContainer && menuContainer.contains(event.target)) {
117
+ return
118
+ }
119
+
120
+ // Hide the menu
121
+ this.hideMenu()
122
+ }
123
+
124
+ removeExistingMenu() {
125
+ // Hide all context menus
126
+ const allMenus = document.querySelectorAll('[id^="context-menu-"]')
127
+ allMenus.forEach(menu => {
128
+ const contextMenu = menu.querySelector('.context-menu')
129
+ if (contextMenu) {
130
+ contextMenu.classList.remove('visible')
131
+ }
132
+ menu.classList.add('pointer-events-none')
133
+ menu.innerHTML = ''
134
+ })
135
+ }
136
+
137
+ // Action handlers
138
+ executeAction(event) {
139
+ const button = event.currentTarget
140
+ const actionClass = button.dataset.actionClass
141
+ const executionMode = button.dataset.executionMode
142
+ const recordId = button.dataset.recordId
143
+ const resourceName = button.dataset.resourceName
144
+ const confirmMessage = button.dataset.confirm
145
+
146
+ // Hide menu immediately
147
+ this.hideMenu()
148
+
149
+ // For instant actions, show confirmation modal first
150
+ if (executionMode === 'instant' && confirmMessage) {
151
+ this.showConfirmationModal(confirmMessage, resourceName, recordId, actionClass)
152
+ } else {
153
+ // Execute immediately if no confirmation needed
154
+ this.performAction(resourceName, recordId, actionClass)
155
+ }
156
+ }
157
+
158
+ showModal(event) {
159
+ const button = event.currentTarget
160
+ const actionClass = button.dataset.actionClass
161
+ const recordId = button.dataset.recordId
162
+ const resourceName = button.dataset.resourceName
163
+
164
+ // Hide menu immediately
165
+ this.hideMenu()
166
+
167
+ // Load modal form
168
+ this.loadModalForm(resourceName, recordId, actionClass)
169
+ }
170
+
171
+ async showConfirmationModal(message, resourceName, recordId, actionClass) {
172
+ try {
173
+ // Store action details for when confirmation is received
174
+ this.pendingAction = { resourceName, recordId, actionClass }
175
+
176
+ // Listen for confirmation modal events
177
+ document.addEventListener('confirmation-modal:confirmed', this.handleConfirmation.bind(this), { once: true })
178
+ document.addEventListener('confirmation-modal:cancelled', this.handleCancellation.bind(this), { once: true })
179
+
180
+ // Load confirmation modal
181
+ const response = await get('/admin/confirmation_modal', {
182
+ responseKind: 'turbo-stream',
183
+ query: {
184
+ message: message,
185
+ title: 'Confirm Action',
186
+ confirm_text: 'Execute',
187
+ danger: 'true'
188
+ }
189
+ })
190
+ } catch (error) {
191
+ console.error('Failed to load confirmation modal:', error)
192
+ }
193
+ }
194
+
195
+ handleConfirmation() {
196
+ if (this.pendingAction) {
197
+ const { resourceName, recordId, actionClass } = this.pendingAction
198
+ this.performAction(resourceName, recordId, actionClass)
199
+ this.pendingAction = null
200
+ }
201
+ }
202
+
203
+ handleCancellation() {
204
+ this.pendingAction = null
205
+ }
206
+
207
+ async performAction(resourceName, recordId, actionClass) {
208
+ try {
209
+ const response = await post(`/admin/${resourceName}/${recordId}/actions/${actionClass}`, {
210
+ responseKind: 'turbo-stream',
211
+ body: new FormData() // Empty form data for instant actions
212
+ })
213
+ } catch (error) {
214
+ console.error('Failed to execute action:', error)
215
+ }
216
+ }
217
+
218
+ async loadModalForm(resourceName, recordId, actionClass) {
219
+ try {
220
+ const response = await get(`/admin/${resourceName}/${recordId}/actions/${actionClass}/form`, {
221
+ responseKind: 'turbo-stream'
222
+ })
223
+ } catch (error) {
224
+ console.error('Failed to load modal form:', error)
225
+ }
226
+ }
227
+ }
@@ -0,0 +1,309 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["input", "modal", "grid", "monthYear"]
5
+
6
+ connect() {
7
+ this.currentDate = new Date()
8
+ this.selectedDate = this.parseInputValue()
9
+ this.viewDate = new Date(this.selectedDate || this.currentDate)
10
+ this.isOpen = false
11
+
12
+ // Ensure modal starts with correct classes
13
+ this.modalTarget.classList.add('hidden', 'opacity-0', 'scale-95')
14
+
15
+ this.updateDisplay()
16
+ }
17
+
18
+ disconnect() {
19
+ this.close()
20
+ }
21
+
22
+ // Toggle date picker modal
23
+ toggle(event) {
24
+ if (event) {
25
+ event.preventDefault()
26
+ event.stopPropagation()
27
+ }
28
+
29
+ if (this.isOpen) {
30
+ this.close()
31
+ } else {
32
+ this.open()
33
+ }
34
+ }
35
+
36
+ // Open date picker
37
+ open() {
38
+ this.isOpen = true
39
+ this.adjustModalPosition()
40
+ this.modalTarget.classList.remove('hidden')
41
+ // Use setTimeout to ensure the transition works
42
+ setTimeout(() => {
43
+ this.modalTarget.classList.add('opacity-100', 'scale-100')
44
+ this.modalTarget.classList.remove('opacity-0', 'scale-95')
45
+ }, 10)
46
+ this.updateCalendar()
47
+
48
+ // Add click outside listener with a small delay to prevent immediate close
49
+ setTimeout(() => {
50
+ document.addEventListener('click', this.handleClickOutside)
51
+ document.addEventListener('keydown', this.handleKeydown)
52
+ }, 100)
53
+ }
54
+
55
+ // Adjust modal position for mobile
56
+ adjustModalPosition() {
57
+ const inputRect = this.inputTarget.getBoundingClientRect()
58
+ const viewport = {
59
+ width: window.innerWidth,
60
+ height: window.innerHeight
61
+ }
62
+
63
+ // Check if we're on mobile (viewport width < 640px - sm breakpoint)
64
+ if (viewport.width < 640) {
65
+ // On mobile, center the modal horizontally and add some top margin
66
+ this.modalTarget.style.left = '0'
67
+ this.modalTarget.style.right = '0'
68
+ this.modalTarget.style.top = '8px' // mt-2 equivalent
69
+ } else {
70
+ // On desktop, remove any mobile-specific positioning
71
+ this.modalTarget.style.left = ''
72
+ this.modalTarget.style.right = ''
73
+ this.modalTarget.style.top = ''
74
+ }
75
+ }
76
+
77
+ // Close date picker
78
+ close() {
79
+ this.isOpen = false
80
+ this.modalTarget.classList.add('opacity-0', 'scale-95')
81
+ this.modalTarget.classList.remove('opacity-100', 'scale-100')
82
+
83
+ // Wait for animation to complete before hiding
84
+ setTimeout(() => {
85
+ if (!this.isOpen) {
86
+ this.modalTarget.classList.add('hidden')
87
+ }
88
+ }, 200)
89
+
90
+ // Remove listeners
91
+ document.removeEventListener('click', this.handleClickOutside)
92
+ document.removeEventListener('keydown', this.handleKeydown)
93
+ }
94
+
95
+ // Handle click outside (called from data-action on modal)
96
+ clickOutside(event) {
97
+ // Only close if the click target is the modal backdrop itself
98
+ if (event.target === this.modalTarget) {
99
+ this.close()
100
+ }
101
+ }
102
+
103
+ // Prevent modal close when clicking inside
104
+ preventClose(event) {
105
+ event.stopPropagation()
106
+ }
107
+
108
+ // Navigation methods
109
+ previousMonth() {
110
+ this.viewDate.setMonth(this.viewDate.getMonth() - 1)
111
+ this.updateCalendar()
112
+ }
113
+
114
+ nextMonth() {
115
+ this.viewDate.setMonth(this.viewDate.getMonth() + 1)
116
+ this.updateCalendar()
117
+ }
118
+
119
+ // Quick actions
120
+ today() {
121
+ this.selectedDate = new Date()
122
+ this.viewDate = new Date()
123
+ this.updateInput()
124
+ this.updateCalendar()
125
+ }
126
+
127
+ clear() {
128
+ this.selectedDate = null
129
+ this.inputTarget.value = ""
130
+ this.updateCalendar()
131
+ }
132
+
133
+ // Select a date
134
+ selectDate(event) {
135
+ const day = parseInt(event.target.dataset.day)
136
+ if (!day) return
137
+
138
+ this.selectedDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth(), day)
139
+ this.updateInput()
140
+ this.updateCalendar()
141
+ this.close()
142
+ }
143
+
144
+ // Update the calendar display
145
+ updateCalendar() {
146
+ this.updateMonthYear()
147
+ this.updateGrid()
148
+ }
149
+
150
+ // Update month/year display
151
+ updateMonthYear() {
152
+ const monthNames = [
153
+ 'January', 'February', 'March', 'April', 'May', 'June',
154
+ 'July', 'August', 'September', 'October', 'November', 'December'
155
+ ]
156
+
157
+ const month = monthNames[this.viewDate.getMonth()]
158
+ const year = this.viewDate.getFullYear()
159
+
160
+ this.monthYearTarget.textContent = `${month} ${year}`
161
+ }
162
+
163
+ // Update calendar grid
164
+ updateGrid() {
165
+ const year = this.viewDate.getFullYear()
166
+ const month = this.viewDate.getMonth()
167
+
168
+ // First day of the month
169
+ const firstDay = new Date(year, month, 1)
170
+ const lastDay = new Date(year, month + 1, 0)
171
+
172
+ // Start from Sunday of the week containing the first day
173
+ const startDate = new Date(firstDay)
174
+ startDate.setDate(startDate.getDate() - startDate.getDay())
175
+
176
+ // Generate 6 weeks (42 days)
177
+ const days = []
178
+ const currentDate = new Date(startDate)
179
+
180
+ for (let i = 0; i < 42; i++) {
181
+ days.push(new Date(currentDate))
182
+ currentDate.setDate(currentDate.getDate() + 1)
183
+ }
184
+
185
+ // Render days
186
+ this.gridTarget.innerHTML = days.map(date => {
187
+ const day = date.getDate()
188
+ const isCurrentMonth = date.getMonth() === month
189
+ const isToday = this.isSameDay(date, new Date())
190
+ const isSelected = this.selectedDate && this.isSameDay(date, this.selectedDate)
191
+ const isWeekend = date.getDay() === 0 || date.getDay() === 6
192
+
193
+ let classes = ['text-sm', 'p-2', 'sm:p-2', 'py-3', 'sm:py-2', 'rounded-lg', 'transition-all', 'duration-200', 'touch-manipulation', 'min-h-[40px]', 'sm:min-h-0']
194
+
195
+ if (!isCurrentMonth) {
196
+ classes.push('text-gray-300', 'cursor-not-allowed')
197
+ } else if (isSelected) {
198
+ classes.push('bg-blue-600', 'text-white', 'font-semibold', 'shadow-sm')
199
+ } else if (isToday) {
200
+ classes.push('bg-blue-50', 'text-blue-600', 'font-semibold', 'hover:bg-blue-100')
201
+ } else if (isWeekend) {
202
+ classes.push('text-gray-500', 'hover:bg-gray-100')
203
+ } else {
204
+ classes.push('text-gray-700', 'hover:bg-gray-100')
205
+ }
206
+
207
+ return `
208
+ <button type="button"
209
+ class="${classes.join(' ')}"
210
+ data-day="${isCurrentMonth ? day : ''}"
211
+ data-action="click->date-picker#selectDate"
212
+ ${!isCurrentMonth ? 'disabled' : ''}>
213
+ ${day}
214
+ </button>
215
+ `
216
+ }).join('')
217
+ }
218
+
219
+ // Update input field
220
+ updateInput() {
221
+ if (this.selectedDate) {
222
+ this.inputTarget.value = this.formatDate(this.selectedDate)
223
+ }
224
+ }
225
+
226
+ // Update display on connect
227
+ updateDisplay() {
228
+ if (this.selectedDate) {
229
+ this.inputTarget.value = this.formatDate(this.selectedDate)
230
+ }
231
+ }
232
+
233
+ // Parse input value to Date
234
+ parseInputValue() {
235
+ const value = this.inputTarget.value
236
+ if (!value) return null
237
+
238
+ try {
239
+ return new Date(value)
240
+ } catch (e) {
241
+ return null
242
+ }
243
+ }
244
+
245
+ // Format date for display
246
+ formatDate(date) {
247
+ return date.toLocaleDateString('en-US', {
248
+ year: 'numeric',
249
+ month: 'long',
250
+ day: 'numeric'
251
+ })
252
+ }
253
+
254
+ // Check if two dates are the same day
255
+ isSameDay(date1, date2) {
256
+ return date1.getFullYear() === date2.getFullYear() &&
257
+ date1.getMonth() === date2.getMonth() &&
258
+ date1.getDate() === date2.getDate()
259
+ }
260
+
261
+ // Bound methods for event listeners
262
+ handleClickOutside = (event) => {
263
+ // Don't close if clicking the input or inside the modal
264
+ if (!this.element.contains(event.target) && !this.modalTarget.contains(event.target)) {
265
+ this.close()
266
+ }
267
+ }
268
+
269
+ handleKeydown = (event) => {
270
+ if (event.key === 'Escape') {
271
+ this.close()
272
+ } else if (this.isOpen) {
273
+ switch(event.key) {
274
+ case 'ArrowLeft':
275
+ event.preventDefault()
276
+ this.changeSelectedDate(-1)
277
+ break
278
+ case 'ArrowRight':
279
+ event.preventDefault()
280
+ this.changeSelectedDate(1)
281
+ break
282
+ case 'ArrowUp':
283
+ event.preventDefault()
284
+ this.changeSelectedDate(-7)
285
+ break
286
+ case 'ArrowDown':
287
+ event.preventDefault()
288
+ this.changeSelectedDate(7)
289
+ break
290
+ case 'Enter':
291
+ event.preventDefault()
292
+ if (this.selectedDate) {
293
+ this.updateInput()
294
+ this.close()
295
+ }
296
+ break
297
+ }
298
+ }
299
+ }
300
+
301
+ changeSelectedDate(days) {
302
+ if (!this.selectedDate) {
303
+ this.selectedDate = new Date()
304
+ }
305
+ this.selectedDate.setDate(this.selectedDate.getDate() + days)
306
+ this.viewDate = new Date(this.selectedDate)
307
+ this.updateCalendar()
308
+ }
309
+ }
@@ -0,0 +1,63 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["trigger", "menu", "chevron"]
5
+ static classes = ["open"]
6
+
7
+ connect() {
8
+ this.close()
9
+ this.setupClickOutside()
10
+ }
11
+
12
+ disconnect() {
13
+ if (this.clickOutsideHandler) {
14
+ document.removeEventListener('click', this.clickOutsideHandler)
15
+ }
16
+ }
17
+
18
+ setupClickOutside() {
19
+ this.clickOutsideHandler = (event) => {
20
+ if (!this.element.contains(event.target)) {
21
+ this.close()
22
+ }
23
+ }
24
+ document.addEventListener('click', this.clickOutsideHandler)
25
+ }
26
+
27
+ toggle() {
28
+ if (this.isOpen()) {
29
+ this.close()
30
+ } else {
31
+ this.open()
32
+ }
33
+ }
34
+
35
+ open() {
36
+ this.menuTarget.classList.remove('hidden')
37
+ this.menuTarget.classList.add('block')
38
+
39
+ // Rotate chevron up
40
+ if (this.hasChevronTarget) {
41
+ this.chevronTarget.classList.add('rotate-180')
42
+ }
43
+
44
+ // Add a small delay for smooth animation
45
+ setTimeout(() => {
46
+ this.menuTarget.classList.add('animate-fadeIn')
47
+ }, 10)
48
+ }
49
+
50
+ close() {
51
+ this.menuTarget.classList.add('hidden')
52
+ this.menuTarget.classList.remove('block', 'animate-fadeIn')
53
+
54
+ // Rotate chevron back down
55
+ if (this.hasChevronTarget) {
56
+ this.chevronTarget.classList.remove('rotate-180')
57
+ }
58
+ }
59
+
60
+ isOpen() {
61
+ return !this.menuTarget.classList.contains('hidden')
62
+ }
63
+ }
@@ -0,0 +1,19 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static values = { event: String, detail: Object }
5
+
6
+ emit() {
7
+ const eventName = this.eventValue
8
+ const eventDetail = this.hasDetailValue ? this.detailValue : {}
9
+
10
+ console.log(`Event emitter dispatching: ${eventName}`, eventDetail)
11
+
12
+ const customEvent = new CustomEvent(eventName, {
13
+ detail: eventDetail,
14
+ bubbles: true
15
+ })
16
+
17
+ document.dispatchEvent(customEvent)
18
+ }
19
+ }