loco_motion-rails 0.4.0 → 0.5.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.
- checksums.yaml +4 -4
- data/README.md +6 -1
- data/app/components/daisy/actions/button_component.html.haml +2 -2
- data/app/components/daisy/actions/button_component.rb +98 -59
- data/app/components/daisy/actions/dropdown_component.html.haml +1 -2
- data/app/components/daisy/actions/dropdown_component.rb +7 -10
- data/app/components/daisy/actions/modal_component.html.haml +10 -8
- data/app/components/daisy/actions/modal_component.rb +6 -6
- data/app/components/daisy/actions/swap_component.rb +13 -9
- data/app/components/daisy/actions/theme_controller.js +113 -0
- data/app/components/daisy/actions/theme_controller_component.rb +58 -17
- data/app/components/daisy/actions/theme_preview_component.html.haml +5 -0
- data/app/components/daisy/actions/theme_preview_component.rb +68 -0
- data/app/components/daisy/data_display/accordion_component.html.haml +0 -1
- data/app/components/daisy/data_display/accordion_component.rb +10 -3
- data/app/components/daisy/data_display/avatar_component.html.haml +1 -1
- data/app/components/daisy/data_display/avatar_component.rb +17 -7
- data/app/components/daisy/data_display/badge_component.rb +122 -4
- data/app/components/daisy/data_display/card_component.html.haml +1 -1
- data/app/components/daisy/data_display/card_component.rb +20 -6
- data/app/components/daisy/data_display/chat_component.rb +2 -2
- data/app/components/daisy/data_display/collapse_component.rb +9 -5
- data/app/components/daisy/data_display/countdown_component.rb +15 -5
- data/app/components/daisy/data_display/figure_component.rb +8 -3
- data/app/components/daisy/data_display/kbd_component.rb +13 -4
- data/app/components/daisy/data_display/list_component.html.haml +5 -0
- data/app/components/daisy/data_display/list_component.rb +82 -0
- data/app/components/daisy/data_display/list_item_component.rb +39 -0
- data/app/components/daisy/data_display/stat_component.html.haml +5 -6
- data/app/components/daisy/data_display/stat_component.rb +21 -8
- data/app/components/daisy/data_display/status_component.rb +47 -0
- data/app/components/daisy/data_display/timeline_component.rb +1 -1
- data/app/components/daisy/data_input/cally_component.html.haml +14 -0
- data/app/components/daisy/data_input/cally_component.rb +182 -0
- data/app/components/daisy/data_input/cally_input_component.html.haml +5 -0
- data/app/components/daisy/data_input/cally_input_component.rb +165 -0
- data/app/components/daisy/data_input/cally_input_controller.js +235 -0
- data/app/components/daisy/data_input/checkbox_component.html.haml +20 -0
- data/app/components/daisy/data_input/checkbox_component.rb +21 -7
- data/app/components/daisy/data_input/fieldset_component.html.haml +8 -0
- data/app/components/daisy/data_input/fieldset_component.rb +57 -0
- data/app/components/daisy/data_input/file_input_component.rb +6 -0
- data/app/components/daisy/data_input/filter_component.html.haml +3 -0
- data/app/components/daisy/data_input/filter_component.rb +221 -0
- data/app/components/daisy/data_input/label_component.rb +2 -2
- data/app/components/daisy/data_input/radio_button_component.rb +1 -1
- data/app/components/daisy/data_input/rating_component.html.haml +0 -2
- data/app/components/daisy/data_input/rating_component.rb +3 -2
- data/app/components/daisy/data_input/select_component.html.haml +27 -15
- data/app/components/daisy/data_input/select_component.rb +152 -10
- data/app/components/daisy/data_input/text_area_component.rb +11 -8
- data/app/components/daisy/data_input/text_input_component.html.haml +25 -4
- data/app/components/daisy/data_input/text_input_component.rb +38 -36
- data/app/components/daisy/data_input/toggle_component.rb +12 -0
- data/app/components/daisy/feedback/alert_component.html.haml +1 -1
- data/app/components/daisy/feedback/alert_component.rb +86 -2
- data/app/components/daisy/feedback/loading_component.rb +10 -3
- data/app/components/daisy/feedback/skeleton_component.rb +1 -1
- data/app/components/daisy/layout/divider_component.rb +4 -2
- data/app/components/daisy/layout/drawer_component.html.haml +0 -1
- data/app/components/daisy/layout/footer_component.rb +6 -6
- data/app/components/daisy/mockup/device_component.rb +15 -18
- data/app/components/daisy/navigation/breadcrumbs_component.html.haml +0 -1
- data/app/components/daisy/navigation/breadcrumbs_component.rb +84 -9
- data/app/components/daisy/navigation/dock_component.rb +146 -0
- data/app/components/daisy/navigation/link_component.rb +18 -9
- data/app/components/daisy/navigation/menu_component.rb +15 -9
- data/app/components/daisy/navigation/navbar_component.html.haml +1 -1
- data/app/components/daisy/navigation/navbar_component.rb +2 -13
- data/app/components/daisy/navigation/steps_component.rb +6 -6
- data/app/components/daisy/navigation/tabs_component.html.haml +0 -1
- data/app/components/daisy/navigation/tabs_component.rb +26 -16
- data/app/components/hero/icon_component.rb +15 -5
- data/app/helpers/daisy/form_builder_helper.rb +30 -3
- data/app/views/examples/daisy/data_input/filters.html.haml +62 -0
- data/lib/hero.rb +1 -1
- data/lib/loco_motion/base_component.rb +44 -1
- data/lib/loco_motion/component_config.rb +1 -0
- data/lib/loco_motion/concerns/iconable_component.rb +134 -0
- data/lib/loco_motion/concerns/labelable_component.rb +142 -0
- data/lib/loco_motion/concerns/linkable_component.rb +40 -0
- data/lib/loco_motion/concerns/tippable_component.rb +25 -10
- data/lib/loco_motion/helpers.rb +27 -18
- data/lib/loco_motion/patches/view_component/slot_loco_parent_patch.rb +37 -0
- data/lib/loco_motion/patches/view_component/slotable_default_patch.rb +21 -0
- data/lib/loco_motion/version.rb +1 -1
- data/lib/loco_motion.rb +12 -2
- metadata +65 -19
- data/app/components/daisy/actions/theme_controller_component.html.haml +0 -5
- data/app/components/daisy/layout/artboard_component.rb +0 -59
- data/app/components/daisy/navigation/bottom_nav_component.rb +0 -138
@@ -0,0 +1,235 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Controller for handling calendar input interactions.
|
5
|
+
* Manages the popover calendar UI and synchronization between input and calendar elements.
|
6
|
+
*/
|
7
|
+
export default class extends Controller {
|
8
|
+
static targets = ["calendar", "input", "popover"]
|
9
|
+
|
10
|
+
/**
|
11
|
+
* Initializes the controller and sets up event listeners.
|
12
|
+
* Binds all methods to ensure proper 'this' context.
|
13
|
+
*
|
14
|
+
* @returns {void}
|
15
|
+
*/
|
16
|
+
connect() {
|
17
|
+
|
18
|
+
// Save the bound functions so we can remove them later
|
19
|
+
this.boundUpdateInput = this.updateInput.bind(this)
|
20
|
+
this.boundUpdateCalendar = this.updateCalendar.bind(this)
|
21
|
+
this.boundOpenCalendar = this.openCalendar.bind(this)
|
22
|
+
this.boundCloseCalendar = this.closeCalendar.bind(this)
|
23
|
+
this.boundHandleKeydown = this.handleKeydown.bind(this)
|
24
|
+
this.boundHandleToggle = this.handleToggle.bind(this)
|
25
|
+
|
26
|
+
// Bind all of our functions
|
27
|
+
this.calendarTarget.addEventListener("change", this.boundUpdateInput)
|
28
|
+
this.calendarTarget.addEventListener("blur", this.boundCloseCalendar)
|
29
|
+
|
30
|
+
this.inputTarget.addEventListener("change", this.boundUpdateCalendar)
|
31
|
+
this.inputTarget.addEventListener("keyup", this.boundUpdateCalendar)
|
32
|
+
this.inputTarget.addEventListener("click", this.boundOpenCalendar)
|
33
|
+
this.inputTarget.addEventListener("keydown", this.boundHandleKeydown)
|
34
|
+
|
35
|
+
this.popoverTarget.addEventListener("toggle", this.boundHandleToggle)
|
36
|
+
}
|
37
|
+
|
38
|
+
/**
|
39
|
+
* Cleans up event listeners when the controller is disconnected.
|
40
|
+
* Prevents memory leaks by removing all bound event handlers.
|
41
|
+
*
|
42
|
+
* @returns {void}
|
43
|
+
*/
|
44
|
+
disconnect() {
|
45
|
+
this.calendarTarget.removeEventListener("change", this.boundUpdateInput)
|
46
|
+
this.calendarTarget.removeEventListener("blur", this.boundCloseCalendar)
|
47
|
+
|
48
|
+
this.inputTarget.removeEventListener("change", this.boundUpdateCalendar)
|
49
|
+
this.inputTarget.removeEventListener("keyup", this.boundUpdateCalendar)
|
50
|
+
this.inputTarget.removeEventListener("keydown", this.boundHandleKeydown)
|
51
|
+
this.inputTarget.removeEventListener("click", this.boundOpenCalendar)
|
52
|
+
this.popoverTarget.removeEventListener("toggle", this.boundHandleToggle)
|
53
|
+
}
|
54
|
+
|
55
|
+
/**
|
56
|
+
* Opens the calendar popover.
|
57
|
+
*
|
58
|
+
* @returns {void}
|
59
|
+
*/
|
60
|
+
openCalendar() {
|
61
|
+
// Open the popover
|
62
|
+
this.popoverTarget.togglePopover(true)
|
63
|
+
}
|
64
|
+
|
65
|
+
/**
|
66
|
+
* Closes the calendar popover if the blur event is not related to calendar elements.
|
67
|
+
*
|
68
|
+
* @param {FocusEvent} event - The blur event object.
|
69
|
+
*
|
70
|
+
* @returns {void}
|
71
|
+
*/
|
72
|
+
closeCalendar(event) {
|
73
|
+
// Don't close if we're still in the calendar elements
|
74
|
+
if (event?.relatedTarget && this.calendarTarget.contains(event.relatedTarget)) {
|
75
|
+
return
|
76
|
+
}
|
77
|
+
|
78
|
+
this.popoverTarget.togglePopover(false)
|
79
|
+
}
|
80
|
+
|
81
|
+
/**
|
82
|
+
* Opens the calendar if it is closed, or closes it if it is open.
|
83
|
+
*
|
84
|
+
* @returns {void}
|
85
|
+
*/
|
86
|
+
toggleCalendar() {
|
87
|
+
this.popoverTarget.togglePopover()
|
88
|
+
}
|
89
|
+
|
90
|
+
/**
|
91
|
+
* Handles the popover toggle event.
|
92
|
+
* - When opening: Sets a zero-width space in empty inputs to prevent floating label flicker
|
93
|
+
* - When closing: Clears the single-space input to ensure proper floating label behavior
|
94
|
+
*
|
95
|
+
* @param {Event} event - The toggle event object with newState property
|
96
|
+
* @returns {void}
|
97
|
+
*/
|
98
|
+
handleToggle(event) {
|
99
|
+
const hasFloatingLabel = this.inputTarget.parentElement.classList.contains("floating-label")
|
100
|
+
const isOpen = event.newState == 'open'
|
101
|
+
|
102
|
+
if (isOpen) {
|
103
|
+
// Make sure the calendar is visible
|
104
|
+
this.scrollCalendarIntoView()
|
105
|
+
}
|
106
|
+
|
107
|
+
if (hasFloatingLabel) {
|
108
|
+
const ZERO_WIDTH_SPACE = '\u200B'
|
109
|
+
|
110
|
+
if (isOpen) {
|
111
|
+
// Set the input to a zero-width space if it is empty to prevent a floating label
|
112
|
+
// flicker when the calendar loses focus while setting the date
|
113
|
+
if (this.inputTarget.value == null || this.inputTarget.value === '') {
|
114
|
+
this.inputTarget.value = ZERO_WIDTH_SPACE;
|
115
|
+
}
|
116
|
+
} else {
|
117
|
+
// Unset the zero-width space input on close so the floating label works again
|
118
|
+
if (this.inputTarget.value === ZERO_WIDTH_SPACE) {
|
119
|
+
this.inputTarget.value = null
|
120
|
+
}
|
121
|
+
}
|
122
|
+
}
|
123
|
+
}
|
124
|
+
|
125
|
+
/**
|
126
|
+
* Ensures the calendar popover is visible within the viewport.
|
127
|
+
*
|
128
|
+
* If the popover extends beyond the viewport edges, scrolls the window to
|
129
|
+
* bring it into view adding any data-auto-scroll-padding.
|
130
|
+
*
|
131
|
+
* @returns {void}
|
132
|
+
*/
|
133
|
+
scrollCalendarIntoView() {
|
134
|
+
const rect = this.popoverTarget.getBoundingClientRect()
|
135
|
+
const autoScrollPadding = parseInt(this.popoverTarget.dataset.autoScrollPadding, 10)
|
136
|
+
const padding = isNaN(autoScrollPadding) ? 0 : autoScrollPadding
|
137
|
+
|
138
|
+
if (rect.bottom > window.innerHeight) {
|
139
|
+
window.scrollBy({ top: rect.bottom - window.innerHeight + padding, behavior: 'smooth' })
|
140
|
+
} else if (rect.top < 0) {
|
141
|
+
window.scrollBy({ top: rect.top - padding, behavior: 'smooth' })
|
142
|
+
}
|
143
|
+
}
|
144
|
+
|
145
|
+
/**
|
146
|
+
* Updates the input value from the calendar and closes the popover.
|
147
|
+
* Synchronizes the input field with the selected date.
|
148
|
+
*
|
149
|
+
* @returns {void}
|
150
|
+
*/
|
151
|
+
updateInput() {
|
152
|
+
// Update the input value and close the popover
|
153
|
+
this.inputTarget.value = this.calendarTarget.value
|
154
|
+
this.closeCalendar()
|
155
|
+
}
|
156
|
+
|
157
|
+
/**
|
158
|
+
* Handles keyboard navigation for the calendar input.
|
159
|
+
* - SPACE: Toggles the popover
|
160
|
+
* - ENTER: Closes an open popover
|
161
|
+
* - ARROW DOWN / ARROW UP: Opens the popover and focuses the calendar
|
162
|
+
*
|
163
|
+
* @param {KeyboardEvent} event - The keyboard event object.
|
164
|
+
*
|
165
|
+
* @returns {void}
|
166
|
+
*/
|
167
|
+
handleKeydown(event) {
|
168
|
+
const hasModifierKeys = event.ctrlKey || event.shiftKey || event.altKey || event.metaKey;
|
169
|
+
const isOpen = this.popoverTarget.matches(':popover-open')
|
170
|
+
|
171
|
+
//
|
172
|
+
// SPACE - Toggles the popover.
|
173
|
+
//
|
174
|
+
if (event.key === ' ' || event.code === 'Space') {
|
175
|
+
event.preventDefault()
|
176
|
+
|
177
|
+
this.toggleCalendar()
|
178
|
+
}
|
179
|
+
|
180
|
+
//
|
181
|
+
// ENTER - Closes an open popover
|
182
|
+
//
|
183
|
+
else if (isOpen && (event.key === 'Enter' || event.code === 'Enter')) {
|
184
|
+
// Stop the enter key from triggering the form submission
|
185
|
+
event.preventDefault()
|
186
|
+
|
187
|
+
this.closeCalendar()
|
188
|
+
}
|
189
|
+
|
190
|
+
//
|
191
|
+
// ARROW DOWN / ARROW UP - Opens the popover and focuses the calendar
|
192
|
+
//
|
193
|
+
else if ((event.key === 'ArrowDown' || event.code === 'ArrowUp') && !hasModifierKeys) {
|
194
|
+
event.preventDefault()
|
195
|
+
|
196
|
+
// If the calendar is not already open, open it
|
197
|
+
if (!isOpen) {
|
198
|
+
this.openCalendar()
|
199
|
+
}
|
200
|
+
|
201
|
+
// Focus the calendar element
|
202
|
+
this.calendarTarget.focus()
|
203
|
+
|
204
|
+
// Forward the keydown event to the calendar
|
205
|
+
const forwardedEvent = new KeyboardEvent('keydown', {
|
206
|
+
key: event.key,
|
207
|
+
code: event.code,
|
208
|
+
bubbles: true,
|
209
|
+
cancelable: true,
|
210
|
+
})
|
211
|
+
|
212
|
+
// Dispatch the forwarded event to the calendar
|
213
|
+
this.calendarTarget.dispatchEvent(forwardedEvent)
|
214
|
+
}
|
215
|
+
}
|
216
|
+
|
217
|
+
/**
|
218
|
+
* Updates the calendar's value based on the input field.
|
219
|
+
* Only updates if the input contains a valid ISO 8601 date.
|
220
|
+
*
|
221
|
+
* @returns {void}
|
222
|
+
*/
|
223
|
+
updateCalendar() {
|
224
|
+
// Only update the calendar if we have a full / valid ISO 8601 date
|
225
|
+
let newDate = this.inputTarget.value
|
226
|
+
|
227
|
+
if (newDate.length !== 10 || new Date(newDate).toString() === "Invalid Date") {
|
228
|
+
return
|
229
|
+
}
|
230
|
+
|
231
|
+
// Set the calendar value and focused-date attributes
|
232
|
+
this.calendarTarget.value = newDate
|
233
|
+
this.calendarTarget.setAttribute('focused-date', newDate)
|
234
|
+
}
|
235
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
- if has_any_label?
|
2
|
+
= part(:label_wrapper) do
|
3
|
+
- if has_start_label?
|
4
|
+
- if start?
|
5
|
+
= start
|
6
|
+
- else
|
7
|
+
= part(:start) do
|
8
|
+
= @start
|
9
|
+
|
10
|
+
= part(:component)
|
11
|
+
|
12
|
+
- if has_end_label?
|
13
|
+
- if end?
|
14
|
+
= self.send(:end)
|
15
|
+
- else
|
16
|
+
= part(:end) do
|
17
|
+
= @end
|
18
|
+
|
19
|
+
- else
|
20
|
+
= part(:component)
|
@@ -5,6 +5,15 @@
|
|
5
5
|
# It can be used standalone or with a form builder, and supports various styling
|
6
6
|
# options including toggle mode for switch-like appearance.
|
7
7
|
#
|
8
|
+
# @part label_wrapper The wrapper element for labels (when using
|
9
|
+
# start/end/floating labels).
|
10
|
+
# @part start The element that contains the start label (appears before the
|
11
|
+
# checkbox).
|
12
|
+
# @part end The element that contains the end label (appears after the checkbox).
|
13
|
+
#
|
14
|
+
# @slot start Custom content for the start label.
|
15
|
+
# @slot end Custom content for the end label.
|
16
|
+
#
|
8
17
|
# @loco_example Basic Usage
|
9
18
|
# = daisy_checkbox(name: "accept", id: "accept")
|
10
19
|
#
|
@@ -17,7 +26,12 @@
|
|
17
26
|
# @loco_example Disabled Checkbox
|
18
27
|
# = daisy_checkbox(name: "accept", id: "accept", disabled: true)
|
19
28
|
#
|
29
|
+
# @loco_example With End Label (common for checkboxes)
|
30
|
+
# = daisy_checkbox(name: "terms", id: "terms", end: "I agree to the terms and conditions")
|
31
|
+
#
|
20
32
|
class Daisy::DataInput::CheckboxComponent < LocoMotion::BaseComponent
|
33
|
+
include LocoMotion::Concerns::LabelableComponent
|
34
|
+
|
21
35
|
attr_reader :name, :id, :value, :checked, :toggle, :disabled, :required
|
22
36
|
|
23
37
|
#
|
@@ -60,9 +74,16 @@ class Daisy::DataInput::CheckboxComponent < LocoMotion::BaseComponent
|
|
60
74
|
# Calls the {setup_component} method before rendering the component.
|
61
75
|
#
|
62
76
|
def before_render
|
77
|
+
super
|
78
|
+
|
79
|
+
setup_labels
|
63
80
|
setup_component
|
64
81
|
end
|
65
82
|
|
83
|
+
def setup_labels
|
84
|
+
add_css(:label_wrapper, "label") if has_any_label?
|
85
|
+
end
|
86
|
+
|
66
87
|
#
|
67
88
|
# Sets up the component by configuring the tag name, CSS classes, and HTML
|
68
89
|
# attributes.
|
@@ -82,11 +103,4 @@ class Daisy::DataInput::CheckboxComponent < LocoMotion::BaseComponent
|
|
82
103
|
required: @required
|
83
104
|
})
|
84
105
|
end
|
85
|
-
|
86
|
-
#
|
87
|
-
# Renders the component inline with no additional whitespace.
|
88
|
-
#
|
89
|
-
def call
|
90
|
-
part(:component)
|
91
|
-
end
|
92
106
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Daisy
|
4
|
+
module DataInput
|
5
|
+
# Renders a fieldset element, optionally with a legend, to group
|
6
|
+
# related form controls or content.
|
7
|
+
#
|
8
|
+
# @loco_example Basic fieldset
|
9
|
+
# = daisy_fieldset do
|
10
|
+
# Content inside fieldset
|
11
|
+
#
|
12
|
+
# @loco_example Fieldset with legend slot
|
13
|
+
# = daisy_fieldset do |fieldset|
|
14
|
+
# - fieldset.with_legend { "My Legend" }
|
15
|
+
#
|
16
|
+
# Content inside fieldset
|
17
|
+
#
|
18
|
+
# @loco_example Fieldset with legend argument
|
19
|
+
# = daisy_fieldset(legend: "My Legend") do
|
20
|
+
# Content inside fieldset
|
21
|
+
#
|
22
|
+
class FieldsetComponent < LocoMotion::BaseComponent
|
23
|
+
self.component_name = :fieldset
|
24
|
+
|
25
|
+
define_parts :legend
|
26
|
+
|
27
|
+
# The legend (title) for the fieldset. Renders a `<legend>` tag.
|
28
|
+
# Can be provided as a slot or via the `legend` argument.
|
29
|
+
renders_one :legend, LocoMotion::BasicComponent.build(tag_name: :legend, css: "fieldset-legend")
|
30
|
+
|
31
|
+
# @param legend [String] Optional simple text for the legend.
|
32
|
+
# Ignored if the `legend` slot is used.
|
33
|
+
def initialize(legend: nil, **kws)
|
34
|
+
@simple_legend = legend
|
35
|
+
|
36
|
+
super(**kws)
|
37
|
+
end
|
38
|
+
|
39
|
+
def before_render
|
40
|
+
setup_component
|
41
|
+
|
42
|
+
super
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# Sets up default tags and classes for the component and its parts.
|
48
|
+
def setup_component
|
49
|
+
set_tag_name(:component, :fieldset)
|
50
|
+
set_tag_name(:legend, :legend)
|
51
|
+
|
52
|
+
add_css(:legend, "fieldset-legend")
|
53
|
+
add_css(:component, "fieldset")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -5,6 +5,9 @@
|
|
5
5
|
# It can be used standalone or with a form builder, and supports
|
6
6
|
# various styling options including different sizes and variants.
|
7
7
|
#
|
8
|
+
# @note File inputs have a border by default. Use `file-input-ghost` to remove
|
9
|
+
# the border.
|
10
|
+
#
|
8
11
|
# @loco_example Basic Usage
|
9
12
|
# = daisy_file_input(name: "document", id: "document")
|
10
13
|
#
|
@@ -14,6 +17,9 @@
|
|
14
17
|
# @loco_example With Multiple Files
|
15
18
|
# = daisy_file_input(name: "documents[]", id: "documents", multiple: true)
|
16
19
|
#
|
20
|
+
# @loco_example Ghost Style (No Border)
|
21
|
+
# = daisy_file_input(name: "document", id: "document", css: "file-input-ghost")
|
22
|
+
#
|
17
23
|
# @loco_example Disabled File Input
|
18
24
|
# = daisy_file_input(name: "document", id: "document", disabled: true)
|
19
25
|
#
|
@@ -0,0 +1,221 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# The Filter component is a group of radio buttons where choosing one option
|
5
|
+
# hides the others and shows a reset button.
|
6
|
+
#
|
7
|
+
# @loco_example Basic Usage
|
8
|
+
# = daisy_filter(name: "frameworks", options: ["Svelte", "Vue", "React"])
|
9
|
+
#
|
10
|
+
# @loco_example Using Hash Options
|
11
|
+
# = daisy_filter(name: "languages", options: [
|
12
|
+
# { label: "Ruby", value: "ruby" },
|
13
|
+
# { label: "JavaScript", value: "js" },
|
14
|
+
# { label: "Python", value: "py" }
|
15
|
+
# ])
|
16
|
+
#
|
17
|
+
# @loco_example Custom Styling
|
18
|
+
# = daisy_filter(name: "priorities", css: "items-center") do |f|
|
19
|
+
# - f.with_reset_button(css: "btn-accent btn-sm rounded-full")
|
20
|
+
# - f.with_option(label: "Low", css: "btn-outline btn-success")
|
21
|
+
# - f.with_option(label: "Medium", css: "btn-outline btn-warning")
|
22
|
+
# - f.with_option(label: "High", css: "btn-outline btn-error")
|
23
|
+
#
|
24
|
+
# @loco_example Within Form Builder
|
25
|
+
# = form_with(model: @post) do |form|
|
26
|
+
# = form.daisy_filter(:category, options: ["News", "Tech", "Sports"])
|
27
|
+
# = form.submit "Save", class: "btn btn-primary mt-4"
|
28
|
+
#
|
29
|
+
# @loco_example Form Builder With Block
|
30
|
+
# = form_with(model: @user) do |form|
|
31
|
+
# = form.daisy_filter(:role) do |f|
|
32
|
+
# - f.with_option(label: "Admin", value: "admin")
|
33
|
+
# - f.with_option(label: "Editor", value: "editor")
|
34
|
+
# - f.with_option(label: "Viewer", value: "viewer")
|
35
|
+
# = form.submit "Update", class: "btn btn-primary mt-4"
|
36
|
+
#
|
37
|
+
module Daisy
|
38
|
+
module DataInput
|
39
|
+
class FilterComponent < LocoMotion::BaseComponent
|
40
|
+
class FilterOptionComponent < Daisy::DataInput::RadioButtonComponent
|
41
|
+
#
|
42
|
+
# Initialize a new filter option component.
|
43
|
+
#
|
44
|
+
# @param kws [Hash] The keyword arguments for the component.
|
45
|
+
#
|
46
|
+
# @option kws label [String] The aria-label for the radio button.
|
47
|
+
#
|
48
|
+
def initialize(**kws)
|
49
|
+
super(**kws)
|
50
|
+
|
51
|
+
@label = config_option(:label)
|
52
|
+
@index = config_option(:index)
|
53
|
+
@skip_styling = true
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# Setup the component before rendering.
|
58
|
+
#
|
59
|
+
def before_render
|
60
|
+
# Make sure to pull the default name from the parent
|
61
|
+
@name = config_option(:name, loco_parent.name)
|
62
|
+
@id = config_option(:id, "#{loco_parent.id}_#{@index}")
|
63
|
+
|
64
|
+
# Call the parent setup first
|
65
|
+
super
|
66
|
+
|
67
|
+
# Add btn class for styling
|
68
|
+
add_css(:component, "btn")
|
69
|
+
add_html(:component, { id: @id })
|
70
|
+
|
71
|
+
# Add aria-label if specified
|
72
|
+
if @label.present?
|
73
|
+
add_html(:component, { "aria-label": @label })
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class FilterResetComponent < Daisy::DataInput::RadioButtonComponent
|
79
|
+
#
|
80
|
+
# Initialize a new filter reset component.
|
81
|
+
#
|
82
|
+
# @param kws [Hash] The keyword arguments for the component.
|
83
|
+
#
|
84
|
+
# @option kws disabled [Boolean] Whether the reset button is disabled. Defaults to false.
|
85
|
+
#
|
86
|
+
def initialize(**kws)
|
87
|
+
super(**kws)
|
88
|
+
|
89
|
+
@skip_styling = true
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# Setup the component before rendering.
|
94
|
+
#
|
95
|
+
def before_render
|
96
|
+
# Make sure to pull the default name from the parent
|
97
|
+
@name = config_option(:name, loco_parent.name)
|
98
|
+
@id = config_option(:id, "#{loco_parent.id}_reset")
|
99
|
+
|
100
|
+
# Call parent setup first
|
101
|
+
super
|
102
|
+
|
103
|
+
# Add square styling
|
104
|
+
add_css(:component, "where:btn where:btn-square filter-reset")
|
105
|
+
|
106
|
+
# Set type to reset
|
107
|
+
add_html(:component, { type: "radio", id: @id })
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
include ViewComponent::SlotableDefault
|
112
|
+
|
113
|
+
renders_one :reset_button, FilterResetComponent
|
114
|
+
renders_many :options, FilterOptionComponent
|
115
|
+
|
116
|
+
attr_reader :name, :id
|
117
|
+
|
118
|
+
#
|
119
|
+
# Initialize a new filter component.
|
120
|
+
#
|
121
|
+
# @param kws [Hash] The keyword arguments for the component.
|
122
|
+
#
|
123
|
+
# @option kws name [String] Required name attribute for the radio button group.
|
124
|
+
#
|
125
|
+
# @option kws options [Array] An array of options to display in the filter.
|
126
|
+
# Can be an array of strings, symbols, or hashes with :label keys.
|
127
|
+
#
|
128
|
+
# @option kws value [String] The current value of the filter (for form integration).
|
129
|
+
#
|
130
|
+
def initialize(**kws)
|
131
|
+
super(**kws)
|
132
|
+
|
133
|
+
@name = config_option(:name)
|
134
|
+
@id = config_option(:id, SecureRandom.uuid)
|
135
|
+
@options_list = config_option(:options)
|
136
|
+
@value = config_option(:value)
|
137
|
+
end
|
138
|
+
|
139
|
+
#
|
140
|
+
# Setup the component before rendering.
|
141
|
+
#
|
142
|
+
def before_render
|
143
|
+
super
|
144
|
+
|
145
|
+
setup_component
|
146
|
+
end
|
147
|
+
|
148
|
+
def default_reset_button
|
149
|
+
FilterResetComponent.new(name: @name)
|
150
|
+
end
|
151
|
+
|
152
|
+
#
|
153
|
+
# Converts the options array into FilterOptionComponent instances.
|
154
|
+
# Handles both hash options (with label keys) and simple string/symbol options.
|
155
|
+
#
|
156
|
+
# @return [Array<FilterOptionComponent>] Array of option components or empty array if @options_list is nil.
|
157
|
+
#
|
158
|
+
def standard_options
|
159
|
+
return [] unless options_list
|
160
|
+
|
161
|
+
options_list.map.with_index do |option, index|
|
162
|
+
label = option.is_a?(Hash) ? option[:label] : option.to_s
|
163
|
+
value = option.is_a?(Hash) ? option[:value] : option
|
164
|
+
|
165
|
+
# Check if this option should be selected based on the component's value
|
166
|
+
checked = @value.present? && @value.to_s == value.to_s
|
167
|
+
|
168
|
+
Daisy::DataInput::FilterComponent::FilterOptionComponent.new(
|
169
|
+
loco_parent: component_ref,
|
170
|
+
name: @name,
|
171
|
+
label: label,
|
172
|
+
value: value,
|
173
|
+
checked: checked,
|
174
|
+
index: index.to_s # Ensure index is a string
|
175
|
+
)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
#
|
180
|
+
# Renders the filter options based on the configuration.
|
181
|
+
# This method is used by the template to render options consistently.
|
182
|
+
#
|
183
|
+
# @return [String] The HTML for all options in the filter.
|
184
|
+
#
|
185
|
+
def render_filter_options
|
186
|
+
result = ""
|
187
|
+
|
188
|
+
if options?
|
189
|
+
options.each do |option|
|
190
|
+
result += render(option)
|
191
|
+
end
|
192
|
+
elsif standard_options.present?
|
193
|
+
standard_options.each do |option|
|
194
|
+
result += render(option)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
result.html_safe
|
199
|
+
end
|
200
|
+
|
201
|
+
private
|
202
|
+
|
203
|
+
#
|
204
|
+
# Sets up the component by configuring the tag name, CSS classes, and HTML attributes.
|
205
|
+
#
|
206
|
+
def setup_component
|
207
|
+
# Add base component class
|
208
|
+
add_css(:component, "filter")
|
209
|
+
end
|
210
|
+
|
211
|
+
#
|
212
|
+
# Ensures the options list is always an array, even if a single option is provided.
|
213
|
+
#
|
214
|
+
# @return [Array] The list of options as an array.
|
215
|
+
#
|
216
|
+
def options_list
|
217
|
+
[@options_list].flatten.compact
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
@@ -34,11 +34,11 @@ class Daisy::DataInput::LabelComponent < LocoMotion::BaseComponent
|
|
34
34
|
# @option kws required [Boolean] Whether the label is for a required input.
|
35
35
|
# Defaults to false.
|
36
36
|
#
|
37
|
-
def initialize(**kws)
|
37
|
+
def initialize(title = nil, **kws)
|
38
38
|
super
|
39
39
|
|
40
40
|
@for = config_option(:for)
|
41
|
-
@title = config_option(:title)
|
41
|
+
@title = config_option(:title, title)
|
42
42
|
@required = config_option(:required, false)
|
43
43
|
end
|
44
44
|
|
@@ -65,7 +65,7 @@ class Daisy::DataInput::RadioButtonComponent < LocoMotion::BaseComponent
|
|
65
65
|
def setup_component
|
66
66
|
set_tag_name(:component, :input)
|
67
67
|
|
68
|
-
add_css(:component, "radio")
|
68
|
+
add_css(:component, "radio") unless @skip_styling
|
69
69
|
|
70
70
|
add_html(:component, {
|
71
71
|
type: "radio",
|
@@ -30,7 +30,7 @@ class Daisy::DataInput::RatingComponent < LocoMotion::BaseComponent
|
|
30
30
|
#
|
31
31
|
def before_render
|
32
32
|
set_tag_name(:component, :input)
|
33
|
-
add_html(:component, { name: loco_parent
|
33
|
+
add_html(:component, { name: loco_parent.name, type: "radio" })
|
34
34
|
end
|
35
35
|
|
36
36
|
#
|
@@ -120,7 +120,8 @@ class Daisy::DataInput::RatingComponent < LocoMotion::BaseComponent
|
|
120
120
|
def star_items
|
121
121
|
(1..@max).map do |rating|
|
122
122
|
input_attrs = {
|
123
|
-
|
123
|
+
loco_parent: component_ref,
|
124
|
+
css: ["where:mask where:mask-star", @inputs_css].compact.join(" "),
|
124
125
|
html: {
|
125
126
|
name: @name,
|
126
127
|
value: rating,
|