hakumi_components 0.1.16.pre → 0.1.17.pre
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 +169 -23
- data/app/assets/javascripts/hakumi_components.js +12 -12
- data/app/assets/stylesheets/hakumi_components.css +1 -1
- data/app/components/hakumi/alert/component.html.erb +12 -8
- data/app/components/hakumi/alert/component.rb +18 -62
- data/app/components/hakumi/base_component.rb +13 -0
- data/app/components/hakumi/card/component.html.erb +14 -22
- data/app/components/hakumi/card/component.rb +38 -31
- data/app/components/hakumi/checkbox/component.html.erb +39 -21
- data/app/components/hakumi/checkbox/component.rb +12 -2
- data/app/components/hakumi/collapse/component.html.erb +2 -2
- data/app/components/hakumi/collapse/component.rb +1 -1
- data/app/components/hakumi/collapse/panel/component.rb +9 -0
- data/app/components/hakumi/color_picker/component.rb +0 -4
- data/app/components/hakumi/drawer/component.html.erb +7 -7
- data/app/components/hakumi/drawer/component.rb +12 -19
- data/app/components/hakumi/input/component.rb +0 -2
- data/app/components/hakumi/input/text_area/component.rb +0 -2
- data/app/components/hakumi/input_number/component.rb +3 -4
- data/app/components/hakumi/mentions/component.rb +0 -1
- data/app/components/hakumi/modal/component.html.erb +40 -0
- data/app/components/hakumi/modal/component.rb +24 -102
- data/app/components/hakumi/modal/confirm/component.html.erb +23 -0
- data/app/components/hakumi/modal/confirm/component.rb +23 -41
- data/app/components/hakumi/modal/error/component.rb +12 -11
- data/app/components/hakumi/modal/info/component.rb +12 -11
- data/app/components/hakumi/modal/success/component.rb +12 -11
- data/app/components/hakumi/modal/warning/component.rb +15 -10
- data/app/components/hakumi/popconfirm/component.html.erb +25 -25
- data/app/components/hakumi/popconfirm/component.rb +11 -27
- data/app/components/hakumi/rate/component.rb +0 -1
- data/app/components/hakumi/segmented/component.rb +0 -4
- data/app/components/hakumi/slider/component.rb +2 -6
- data/app/components/hakumi/statistic/component.rb +0 -4
- data/app/components/hakumi/switch/component.html.erb +4 -0
- data/app/components/hakumi/switch/component.rb +1 -2
- data/app/components/hakumi/table/component.rb +3 -229
- data/app/components/hakumi/table/concerns/columns.rb +1 -1
- data/app/components/hakumi/table/concerns/editable.rb +121 -0
- data/app/components/hakumi/table/concerns/ellipsis.rb +63 -0
- data/app/components/hakumi/table/concerns/fixed_columns.rb +87 -0
- data/app/components/hakumi/transfer/component.rb +0 -4
- data/app/controllers/{hakumi_components → hakumi}/components_controller.rb +2 -2
- data/app/form_builders/hakumi/form_builder.rb +217 -175
- data/app/helpers/hakumi/form_helper.rb +39 -0
- data/app/javascript/hakumi_components/controllers/base/registry_controller.js +83 -3
- data/app/javascript/hakumi_components/controllers/hakumi/affix_controller.js +0 -23
- data/app/javascript/hakumi_components/controllers/hakumi/alert_controller.js +2 -1
- data/app/javascript/hakumi_components/controllers/hakumi/button_controller.js +0 -7
- data/app/javascript/hakumi_components/controllers/hakumi/calendar_controller.js +0 -2
- data/app/javascript/hakumi_components/controllers/hakumi/color_picker_controller.js +1 -6
- data/app/javascript/hakumi_components/controllers/hakumi/date_picker_controller.js +28 -34
- data/app/javascript/hakumi_components/controllers/hakumi/drawer_controller.js +2 -1
- data/app/javascript/hakumi_components/controllers/hakumi/form_item_controller.js +9 -63
- data/app/javascript/hakumi_components/controllers/hakumi/mentions_controller.js +4 -11
- data/app/javascript/hakumi_components/controllers/hakumi/message_controller.js +1 -1
- data/app/javascript/hakumi_components/controllers/hakumi/modal_controller.js +4 -20
- data/app/javascript/hakumi_components/controllers/hakumi/notification_controller.js +1 -1
- data/app/javascript/hakumi_components/controllers/hakumi/popconfirm_controller.js +33 -27
- data/app/javascript/hakumi_components/controllers/hakumi/popover_controller.js +2 -23
- data/app/javascript/hakumi_components/controllers/hakumi/qr_code_controller.js +0 -20
- data/app/javascript/hakumi_components/controllers/hakumi/segmented_controller.js +0 -2
- data/app/javascript/hakumi_components/controllers/hakumi/spin_controller.js +1 -19
- data/app/javascript/hakumi_components/controllers/hakumi/statistic_controller.js +0 -2
- data/app/javascript/hakumi_components/controllers/hakumi/table_controller.js +48 -74
- data/app/javascript/hakumi_components/controllers/hakumi/tag_controller.js +15 -14
- data/app/javascript/hakumi_components/controllers/hakumi/tag_group_controller.js +14 -13
- data/app/javascript/hakumi_components/controllers/hakumi/theme_controller.js +24 -1
- data/app/javascript/hakumi_components/controllers/hakumi/time_picker_controller.js +3 -7
- data/app/javascript/hakumi_components/controllers/hakumi/timeline_controller.js +0 -16
- data/app/javascript/hakumi_components/controllers/hakumi/transfer_controller.js +2 -2
- data/app/javascript/hakumi_components/controllers/hakumi/tree_controller.js +0 -2
- data/app/javascript/hakumi_components/controllers/hakumi/tree_select_controller.js +3 -3
- data/app/javascript/hakumi_components/controllers/hakumi/upload_controller.js +12 -26
- data/app/javascript/hakumi_components/core/persistence.js +3 -3
- data/app/javascript/hakumi_components/core/render_component.js +3 -1
- data/app/javascript/lib/validation_manager.js +101 -0
- data/app/javascript/stylesheets/_theme-tokens.scss +2 -1
- data/app/javascript/stylesheets/components/_modal.scss +13 -0
- data/app/services/{hakumi_components → hakumi}/component_handler.rb +1 -1
- data/app/services/hakumi/icon/loader.rb +2 -2
- data/app/services/hakumi/illustrations/loader.rb +3 -3
- data/app/views/hakumi/_drawer.html.erb +21 -0
- data/app/views/hakumi/_modal.html.erb +18 -0
- data/lib/hakumi_components/documentation.rb +127 -0
- data/lib/hakumi_components/engine.rb +13 -4
- data/lib/hakumi_components/rails/attribute_introspection.rb +1 -1
- data/lib/hakumi_components/rails/validation_introspection.rb +5 -5
- data/lib/hakumi_components/rails/validation_mapper.rb +484 -0
- data/lib/hakumi_components/rails.rb +2 -1
- data/lib/hakumi_components/version.rb +2 -2
- data/lib/hakumi_components.rb +3 -1
- data/lib/tasks/coverage.rake +37 -0
- data/sig/hakumi/base_component.rbs +5 -0
- data/sig/hakumi/checkbox/component.rbs +10 -0
- data/sig/hakumi/color_picker/component.rbs +0 -1
- data/sig/hakumi/form_builder.rbs +9 -1
- data/sig/{hakumi_components → hakumi}/rails/attribute_introspection.rbs +1 -1
- data/sig/{hakumi_components → hakumi}/rails/validation_introspection.rbs +1 -1
- data/sig/hakumi/rails/validation_mapper.rbs +53 -0
- data/sig/{hakumi_components → hakumi}/rails.rbs +1 -1
- data/sig/hakumi/segmented/component.rbs +0 -1
- data/sig/hakumi/slider/component.rbs +0 -1
- data/sig/hakumi/statistic/component.rbs +0 -2
- data/sig/hakumi/table/component.rbs +3 -4
- data/sig/hakumi/table/concerns/columns.rbs +2 -1
- data/sig/hakumi/table/concerns/editable.rbs +40 -0
- data/sig/hakumi/table/concerns/ellipsis.rbs +27 -0
- data/sig/hakumi/table/concerns/fixed_columns.rbs +33 -0
- data/sig/hakumi/transfer/component.rbs +0 -1
- data/sig/{hakumi_components.rbs → hakumi.rbs} +20 -3
- data/sig/rails/active_model/validations/comparison_validator.rbs +6 -0
- metadata +44 -29
- data/app/views/hakumi_components/_drawer.html.erb +0 -3
- data/app/views/hakumi_components/_modal.html.erb +0 -3
- /data/app/views/{hakumi_components → hakumi}/_admin_panel.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_affix.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_alert.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_confirm.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_message.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_notification.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_popconfirm.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_popover.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_qr_code.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_result.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_segmented.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_skeleton.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_spin.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_statistic.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_table.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_tag.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_timeline.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_tree.html.erb +0 -0
|
@@ -49,18 +49,8 @@ module Hakumi
|
|
|
49
49
|
# @param options [Hash] Options for the input
|
|
50
50
|
# @return [String] Rendered HTML
|
|
51
51
|
def number_field(method, **options)
|
|
52
|
-
options[:type] = :number
|
|
53
52
|
enhance_options_with_introspection!(method, options)
|
|
54
|
-
|
|
55
|
-
# Apply numericality constraints
|
|
56
|
-
if @object
|
|
57
|
-
constraints = HakumiComponents::Rails::ValidationIntrospection.numericality_constraints(@object, method)
|
|
58
|
-
options[:min] ||= constraints[:min] if constraints[:min]
|
|
59
|
-
options[:max] ||= constraints[:max] if constraints[:max]
|
|
60
|
-
options[:step] ||= constraints[:step] if constraints[:step]
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
render_form_field(Hakumi::Input::Component, method, **options)
|
|
53
|
+
render_form_field(Hakumi::InputNumber::Component, method, **options)
|
|
64
54
|
end
|
|
65
55
|
|
|
66
56
|
# Renders a Hakumi select field
|
|
@@ -73,20 +63,11 @@ module Hakumi
|
|
|
73
63
|
choices ||= [] # steep:ignore UnannotatedEmptyCollection
|
|
74
64
|
enhance_options_with_introspection!(method, options)
|
|
75
65
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
@template.render(Hakumi::Select::Component.new(
|
|
83
|
-
name: field_name,
|
|
84
|
-
options: choices,
|
|
85
|
-
value: value,
|
|
86
|
-
errors: errors,
|
|
87
|
-
standalone: false, # Always render with form-item wrapper in FormBuilder
|
|
88
|
-
**options.merge(id: field_id)
|
|
89
|
-
))
|
|
66
|
+
render_form_field(
|
|
67
|
+
Hakumi::Select::Component,
|
|
68
|
+
method,
|
|
69
|
+
**options.merge(options: choices)
|
|
70
|
+
)
|
|
90
71
|
end
|
|
91
72
|
|
|
92
73
|
# Renders a Hakumi tree select field
|
|
@@ -99,20 +80,11 @@ module Hakumi
|
|
|
99
80
|
choices ||= [] # steep:ignore UnannotatedEmptyCollection
|
|
100
81
|
enhance_options_with_introspection!(method, options)
|
|
101
82
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
@template.render(Hakumi::TreeSelect::Component.new(
|
|
109
|
-
name: field_name,
|
|
110
|
-
options: choices,
|
|
111
|
-
value: value,
|
|
112
|
-
errors: errors,
|
|
113
|
-
standalone: false, # Always render with form-item wrapper in FormBuilder
|
|
114
|
-
**options.merge(id: field_id)
|
|
115
|
-
))
|
|
83
|
+
render_form_field(
|
|
84
|
+
Hakumi::TreeSelect::Component,
|
|
85
|
+
method,
|
|
86
|
+
**options.merge(options: choices)
|
|
87
|
+
)
|
|
116
88
|
end
|
|
117
89
|
|
|
118
90
|
# Renders a Hakumi radio button
|
|
@@ -124,25 +96,24 @@ module Hakumi
|
|
|
124
96
|
def radio_button(method, tag_value, **options)
|
|
125
97
|
enhance_options_with_introspection!(method, options)
|
|
126
98
|
|
|
127
|
-
#
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
# Determine if checked
|
|
132
|
-
current_value = object_value(method)
|
|
133
|
-
checked = current_value.to_s == tag_value.to_s
|
|
99
|
+
# Use tag_value as ID suffix unless explicitly provided
|
|
100
|
+
id_suffix = options[:id] ? nil : tag_value.to_s
|
|
101
|
+
config = field_configuration(method, options, id_suffix: id_suffix)
|
|
134
102
|
|
|
135
|
-
#
|
|
136
|
-
|
|
103
|
+
# Override with explicit ID if provided
|
|
104
|
+
config[:id] = options.delete(:id) if options[:id]
|
|
137
105
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
106
|
+
# Determine if checked
|
|
107
|
+
checked = config[:value].to_s == tag_value.to_s
|
|
108
|
+
config[:value] = tag_value
|
|
109
|
+
config[:checked] = checked
|
|
110
|
+
|
|
111
|
+
render_form_field(
|
|
112
|
+
Hakumi::Radio::Component,
|
|
113
|
+
method,
|
|
114
|
+
config_overrides: config,
|
|
115
|
+
**options
|
|
116
|
+
)
|
|
146
117
|
end
|
|
147
118
|
|
|
148
119
|
# Renders a Hakumi radio group from a collection
|
|
@@ -156,27 +127,19 @@ module Hakumi
|
|
|
156
127
|
def collection_radio_buttons(method, collection, value_method, text_method, **options)
|
|
157
128
|
enhance_options_with_introspection!(method, options)
|
|
158
129
|
|
|
159
|
-
# Convert collection to radio options format
|
|
130
|
+
# Convert collection to radio options format (Hash with :label and :value)
|
|
160
131
|
radio_options = collection.map do |item|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
132
|
+
{
|
|
133
|
+
label: item.send(text_method),
|
|
134
|
+
value: item.send(value_method)
|
|
135
|
+
}
|
|
164
136
|
end
|
|
165
137
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
@template.render(Hakumi::Radio::Group::Component.new(
|
|
173
|
-
name: field_name,
|
|
174
|
-
options: radio_options,
|
|
175
|
-
value: value,
|
|
176
|
-
errors: errors,
|
|
177
|
-
standalone: false, # Always render with form-item wrapper in FormBuilder
|
|
178
|
-
**options.merge(id: field_id)
|
|
179
|
-
))
|
|
138
|
+
render_form_field(
|
|
139
|
+
Hakumi::Radio::Group::Component,
|
|
140
|
+
method,
|
|
141
|
+
**options.merge(options: radio_options)
|
|
142
|
+
)
|
|
180
143
|
end
|
|
181
144
|
|
|
182
145
|
# Renders a Hakumi switch (checkbox) field
|
|
@@ -187,22 +150,16 @@ module Hakumi
|
|
|
187
150
|
def check_box(method, **options)
|
|
188
151
|
enhance_options_with_introspection!(method, options)
|
|
189
152
|
|
|
190
|
-
#
|
|
191
|
-
field_name = @object_name ? "#{@object_name}[#{method}]" : method.to_s
|
|
192
|
-
field_id = @object_name ? "#{@object_name}_#{method}" : method.to_s
|
|
153
|
+
# Convert truthy/falsy value to boolean for checked attribute
|
|
193
154
|
value = object_value(method)
|
|
194
|
-
errors = object_errors(method)
|
|
195
|
-
|
|
196
|
-
# Convert truthy/falsy values to boolean
|
|
197
155
|
checked = value ? true : false
|
|
198
156
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
))
|
|
157
|
+
render_form_field(
|
|
158
|
+
Hakumi::Switch::Component,
|
|
159
|
+
method,
|
|
160
|
+
config_overrides: { checked: checked },
|
|
161
|
+
**options
|
|
162
|
+
)
|
|
206
163
|
end
|
|
207
164
|
|
|
208
165
|
# Renders a Hakumi rate (rating) field
|
|
@@ -212,20 +169,7 @@ module Hakumi
|
|
|
212
169
|
# @return [String] Rendered HTML
|
|
213
170
|
def rate_field(method, **options)
|
|
214
171
|
enhance_options_with_introspection!(method, options)
|
|
215
|
-
|
|
216
|
-
# Get field configuration
|
|
217
|
-
field_name = @object_name ? "#{@object_name}[#{method}]" : method.to_s
|
|
218
|
-
field_id = @object_name ? "#{@object_name}_#{method}" : method.to_s
|
|
219
|
-
value = object_value(method)
|
|
220
|
-
errors = object_errors(method)
|
|
221
|
-
|
|
222
|
-
@template.render(Hakumi::Rate::Component.new(
|
|
223
|
-
name: field_name,
|
|
224
|
-
value: value,
|
|
225
|
-
errors: errors,
|
|
226
|
-
standalone: false, # Always render with form-item wrapper in FormBuilder
|
|
227
|
-
**options.merge(id: field_id)
|
|
228
|
-
))
|
|
172
|
+
render_form_field(Hakumi::Rate::Component, method, **options)
|
|
229
173
|
end
|
|
230
174
|
|
|
231
175
|
# Renders a Hakumi slider field
|
|
@@ -236,19 +180,14 @@ module Hakumi
|
|
|
236
180
|
def slider_field(method, **options)
|
|
237
181
|
enhance_options_with_introspection!(method, options)
|
|
238
182
|
|
|
239
|
-
|
|
240
|
-
field_name = @object_name ? "#{@object_name}[#{method}]" : method.to_s
|
|
241
|
-
field_id = @object_name ? "#{@object_name}_#{method}" : method.to_s
|
|
242
|
-
value = object_value(method)
|
|
243
|
-
errors = object_errors(method)
|
|
183
|
+
config = field_configuration(method, options)
|
|
244
184
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
))
|
|
185
|
+
render_form_field(
|
|
186
|
+
Hakumi::Slider::Component,
|
|
187
|
+
method,
|
|
188
|
+
config_overrides: config,
|
|
189
|
+
**options
|
|
190
|
+
)
|
|
252
191
|
end
|
|
253
192
|
|
|
254
193
|
# Renders a Hakumi date picker field
|
|
@@ -258,20 +197,7 @@ module Hakumi
|
|
|
258
197
|
# @return [String] Rendered HTML
|
|
259
198
|
def date_picker(method, **options)
|
|
260
199
|
enhance_options_with_introspection!(method, options)
|
|
261
|
-
|
|
262
|
-
# Get field configuration
|
|
263
|
-
field_name = @object_name ? "#{@object_name}[#{method}]" : method.to_s
|
|
264
|
-
field_id = @object_name ? "#{@object_name}_#{method}" : method.to_s
|
|
265
|
-
value = options.delete(:value) || object_value(method)
|
|
266
|
-
errors = object_errors(method)
|
|
267
|
-
|
|
268
|
-
@template.render(Hakumi::DatePicker::Component.new(
|
|
269
|
-
name: field_name,
|
|
270
|
-
value: value,
|
|
271
|
-
errors: errors,
|
|
272
|
-
standalone: false, # Always render with form-item wrapper in FormBuilder
|
|
273
|
-
**options.merge(id: field_id)
|
|
274
|
-
))
|
|
200
|
+
render_form_field(Hakumi::DatePicker::Component, method, **options)
|
|
275
201
|
end
|
|
276
202
|
|
|
277
203
|
# Renders a Hakumi mentions field (textarea with @ mention suggestions)
|
|
@@ -286,23 +212,120 @@ module Hakumi
|
|
|
286
212
|
def mentions_field(method, **options)
|
|
287
213
|
enhance_options_with_introspection!(method, options)
|
|
288
214
|
|
|
289
|
-
# Extract mentions-specific options
|
|
215
|
+
# Extract mentions-specific options before field configuration
|
|
290
216
|
mention_options = options.delete(:options) || []
|
|
291
217
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
218
|
+
render_form_field(
|
|
219
|
+
Hakumi::Mentions::Component,
|
|
220
|
+
method,
|
|
221
|
+
**options.merge(options: mention_options)
|
|
222
|
+
)
|
|
223
|
+
end
|
|
297
224
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
225
|
+
# Renders a Hakumi autocomplete field (input with dropdown suggestions)
|
|
226
|
+
#
|
|
227
|
+
# @param method [Symbol] Name of the attribute
|
|
228
|
+
# @param choices [Array] Array of autocomplete options
|
|
229
|
+
# @param options [Hash] Options for the autocomplete component
|
|
230
|
+
# @return [String] Rendered HTML
|
|
231
|
+
def autocomplete_field(method, choices = nil, **options)
|
|
232
|
+
choices ||= [] # steep:ignore UnannotatedEmptyCollection
|
|
233
|
+
enhance_options_with_introspection!(method, options)
|
|
234
|
+
|
|
235
|
+
render_form_field(
|
|
236
|
+
Hakumi::Autocomplete::Component,
|
|
237
|
+
method,
|
|
238
|
+
**options.merge(options: choices)
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Renders a Hakumi cascader field (cascading dropdown selector)
|
|
243
|
+
#
|
|
244
|
+
# @param method [Symbol] Name of the attribute
|
|
245
|
+
# @param choices [Array] Array of cascader options with children
|
|
246
|
+
# @param options [Hash] Options for the cascader component
|
|
247
|
+
# @return [String] Rendered HTML
|
|
248
|
+
def cascader_field(method, choices = nil, **options)
|
|
249
|
+
choices ||= [] # steep:ignore UnannotatedEmptyCollection
|
|
250
|
+
enhance_options_with_introspection!(method, options)
|
|
251
|
+
|
|
252
|
+
render_form_field(
|
|
253
|
+
Hakumi::Cascader::Component,
|
|
254
|
+
method,
|
|
255
|
+
**options.merge(options: choices)
|
|
256
|
+
)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Renders a Hakumi checkbox field (traditional checkbox, different from switch)
|
|
260
|
+
#
|
|
261
|
+
# @param method [Symbol] Name of the attribute
|
|
262
|
+
# @param options [Hash] Options for the checkbox
|
|
263
|
+
# @return [String] Rendered HTML
|
|
264
|
+
def checkbox_field(method, **options)
|
|
265
|
+
enhance_options_with_introspection!(method, options)
|
|
266
|
+
|
|
267
|
+
# Convert truthy/falsy value to boolean for checked attribute
|
|
268
|
+
value = object_value(method)
|
|
269
|
+
checked = value ? true : false
|
|
270
|
+
|
|
271
|
+
render_form_field(
|
|
272
|
+
Hakumi::Checkbox::Component,
|
|
273
|
+
method,
|
|
274
|
+
config_overrides: { checked: checked },
|
|
275
|
+
**options
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Renders a Hakumi color picker field
|
|
280
|
+
#
|
|
281
|
+
# @param method [Symbol] Name of the attribute
|
|
282
|
+
# @param options [Hash] Options for the color picker component
|
|
283
|
+
# @return [String] Rendered HTML
|
|
284
|
+
def color_picker_field(method, **options)
|
|
285
|
+
enhance_options_with_introspection!(method, options)
|
|
286
|
+
render_form_field(Hakumi::ColorPicker::Component, method, **options)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Renders a Hakumi time picker field
|
|
290
|
+
#
|
|
291
|
+
# @param method [Symbol] Name of the attribute
|
|
292
|
+
# @param options [Hash] Options for the time picker component
|
|
293
|
+
# @return [String] Rendered HTML
|
|
294
|
+
def time_picker_field(method, **options)
|
|
295
|
+
enhance_options_with_introspection!(method, options)
|
|
296
|
+
render_form_field(Hakumi::TimePicker::Component, method, **options)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Renders a Hakumi transfer field (dual list box)
|
|
300
|
+
#
|
|
301
|
+
# @param method [Symbol] Name of the attribute
|
|
302
|
+
# @param data_source [Array] Array of items to transfer
|
|
303
|
+
# @param options [Hash] Options for the transfer component
|
|
304
|
+
# @return [String] Rendered HTML
|
|
305
|
+
def transfer_field(method, data_source = nil, **options)
|
|
306
|
+
data_source ||= [] # steep:ignore UnannotatedEmptyCollection
|
|
307
|
+
enhance_options_with_introspection!(method, options)
|
|
308
|
+
|
|
309
|
+
render_form_field(
|
|
310
|
+
Hakumi::Transfer::Component,
|
|
311
|
+
method,
|
|
312
|
+
**options.merge(data_source: data_source)
|
|
313
|
+
)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Renders a Hakumi upload field (file upload)
|
|
317
|
+
#
|
|
318
|
+
# @param method [Symbol] Name of the attribute
|
|
319
|
+
# @param options [Hash] Options for the upload component
|
|
320
|
+
# @option options [String] :action Upload URL (required)
|
|
321
|
+
# @return [String] Rendered HTML
|
|
322
|
+
def upload_field(method, **options)
|
|
323
|
+
enhance_options_with_introspection!(method, options)
|
|
324
|
+
|
|
325
|
+
# Upload requires action parameter
|
|
326
|
+
raise ArgumentError, "action parameter is required for upload_field" unless options[:action]
|
|
327
|
+
|
|
328
|
+
render_form_field(Hakumi::Upload::Component, method, **options)
|
|
306
329
|
end
|
|
307
330
|
|
|
308
331
|
# Renders a Hakumi submit button
|
|
@@ -333,6 +356,29 @@ module Hakumi
|
|
|
333
356
|
|
|
334
357
|
private
|
|
335
358
|
|
|
359
|
+
# Gets standard field configuration (name, id, value, errors)
|
|
360
|
+
#
|
|
361
|
+
# @param method [Symbol] Attribute name
|
|
362
|
+
# @param options [Hash] Options hash (modifies in place by deleting :value)
|
|
363
|
+
# @param id_suffix [String, nil] Optional suffix for field_id (for radio buttons)
|
|
364
|
+
# @return [Hash] Field configuration with :name, :id, :value, :errors
|
|
365
|
+
def field_configuration(method, options, id_suffix: nil)
|
|
366
|
+
field_name = @object_name ? "#{@object_name}[#{method}]" : method.to_s
|
|
367
|
+
|
|
368
|
+
base_id = @object_name ? "#{@object_name}_#{method}" : method.to_s
|
|
369
|
+
field_id = id_suffix ? "#{base_id}_#{id_suffix}".parameterize.underscore : base_id
|
|
370
|
+
|
|
371
|
+
value = options.delete(:value) || object_value(method)
|
|
372
|
+
errors = object_errors(method)
|
|
373
|
+
|
|
374
|
+
{
|
|
375
|
+
name: field_name,
|
|
376
|
+
id: field_id,
|
|
377
|
+
value: value,
|
|
378
|
+
errors: errors
|
|
379
|
+
}
|
|
380
|
+
end
|
|
381
|
+
|
|
336
382
|
# Enhances options hash with automatic detection from Rails validations and I18n
|
|
337
383
|
#
|
|
338
384
|
# @param method [Symbol] Attribute name
|
|
@@ -343,42 +389,50 @@ module Hakumi
|
|
|
343
389
|
|
|
344
390
|
# Auto-detect label from I18n (if not provided)
|
|
345
391
|
if options[:label].nil?
|
|
346
|
-
detected_label =
|
|
392
|
+
detected_label = Hakumi::Rails::AttributeIntrospection.human_attribute_name(@object, method)
|
|
347
393
|
options[:label] = detected_label if detected_label
|
|
348
394
|
end
|
|
349
395
|
|
|
350
|
-
# Auto-detect required
|
|
396
|
+
# Auto-detect required for UI purposes (asterisk display) - NOT for HTML5 validation
|
|
351
397
|
if options[:required].nil?
|
|
352
|
-
options[:required] =
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
# Auto-detect maxlength from length validation (if not provided)
|
|
356
|
-
if options[:maxlength].nil?
|
|
357
|
-
detected_maxlength = HakumiComponents::Rails::ValidationIntrospection.maxlength(@object, method)
|
|
358
|
-
options[:maxlength] = detected_maxlength if detected_maxlength
|
|
398
|
+
options[:required] = Hakumi::Rails::ValidationIntrospection.required?(@object, method)
|
|
359
399
|
end
|
|
360
400
|
|
|
361
401
|
# Auto-detect placeholder from I18n (if not provided)
|
|
362
402
|
if options[:placeholder].nil?
|
|
363
|
-
detected_placeholder =
|
|
403
|
+
detected_placeholder = Hakumi::Rails::AttributeIntrospection.placeholder(@object, method)
|
|
364
404
|
options[:placeholder] = detected_placeholder if detected_placeholder
|
|
365
405
|
end
|
|
366
406
|
|
|
367
407
|
# Auto-detect caption (hint) from I18n (if not provided)
|
|
368
408
|
if options[:caption].nil?
|
|
369
|
-
detected_hint =
|
|
409
|
+
detected_hint = Hakumi::Rails::AttributeIntrospection.hint(@object, method)
|
|
370
410
|
options[:caption] = detected_hint if detected_hint
|
|
371
411
|
end
|
|
372
412
|
|
|
413
|
+
# Auto-generate frontend validation rules from Rails validations
|
|
414
|
+
# If manual rules are provided, merge them with auto-detected rules
|
|
415
|
+
# Manual rules take priority over auto-detected ones for the same validation type
|
|
416
|
+
detected_rules = Hakumi::Rails::ValidationMapper.to_frontend_rules(@object, method)
|
|
417
|
+
manual_rules = options[:rules]
|
|
418
|
+
|
|
419
|
+
if manual_rules.nil?
|
|
420
|
+
options[:rules] = detected_rules unless detected_rules.empty?
|
|
421
|
+
elsif !detected_rules.empty?
|
|
422
|
+
# Merge manual and auto-detected rules, with manual rules taking priority
|
|
423
|
+
options[:rules] = Hakumi::Rails::ValidationMapper.merge_rules(manual_rules, detected_rules)
|
|
424
|
+
end
|
|
425
|
+
# If manual_rules is present but detected_rules is empty, keep manual_rules as-is
|
|
426
|
+
|
|
373
427
|
# Auto-detect input type based on attribute name and validations (if not provided)
|
|
374
428
|
if options[:type].nil?
|
|
375
|
-
if
|
|
429
|
+
if Hakumi::Rails::ValidationIntrospection.email_field?(@object, method)
|
|
376
430
|
options[:type] = :email
|
|
377
|
-
elsif
|
|
431
|
+
elsif Hakumi::Rails::ValidationIntrospection.url_field?(@object, method)
|
|
378
432
|
options[:type] = :url
|
|
379
433
|
else
|
|
380
434
|
# Fallback to column type
|
|
381
|
-
detected_type =
|
|
435
|
+
detected_type = Hakumi::Rails::AttributeIntrospection.input_type_from_column(@object, method)
|
|
382
436
|
options[:type] = detected_type if detected_type && detected_type != :text
|
|
383
437
|
end
|
|
384
438
|
end
|
|
@@ -391,29 +445,17 @@ module Hakumi
|
|
|
391
445
|
# @param component_class [Class] Component class to render
|
|
392
446
|
# @param method [Symbol] Name of the attribute
|
|
393
447
|
# @param options [Hash] Options for the component
|
|
448
|
+
# @param config_overrides [Hash] Override field configuration (for special cases)
|
|
394
449
|
# @return [String] Rendered HTML
|
|
395
|
-
def render_form_field(component_class, method, **options)
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
field_id = @object_name ? "#{@object_name}_#{method}" : method.to_s
|
|
399
|
-
|
|
400
|
-
# Get current value
|
|
401
|
-
value = options.delete(:value) || object_value(method)
|
|
402
|
-
|
|
403
|
-
# Get validation errors (use full_messages_for for better error messages)
|
|
404
|
-
errors = object_errors(method)
|
|
450
|
+
def render_form_field(component_class, method, config_overrides: {}, **options)
|
|
451
|
+
config = field_configuration(method, options)
|
|
452
|
+
config.merge!(config_overrides)
|
|
405
453
|
|
|
406
|
-
# Merge
|
|
407
|
-
|
|
408
|
-
id: field_id,
|
|
409
|
-
name: field_name,
|
|
410
|
-
value: value,
|
|
411
|
-
errors: errors,
|
|
412
|
-
standalone: false # Always render with form-item wrapper in FormBuilder
|
|
413
|
-
)
|
|
454
|
+
# Merge all options with standalone: false
|
|
455
|
+
component_options = config.merge(options).merge(standalone: false)
|
|
414
456
|
|
|
415
|
-
# Render the component
|
|
416
|
-
@template.render(component_class.new(**
|
|
457
|
+
# Render the component with merged configuration
|
|
458
|
+
@template.render(component_class.new(**component_options))
|
|
417
459
|
end
|
|
418
460
|
|
|
419
461
|
# Gets the current value of an attribute from the object
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hakumi
|
|
4
|
+
module FormHelper
|
|
5
|
+
# Creates a form that uses Hakumi components.
|
|
6
|
+
# This is a wrapper around Rails' form_with that automatically sets the Hakumi::FormBuilder.
|
|
7
|
+
#
|
|
8
|
+
# @example Basic usage
|
|
9
|
+
# <%= hakumi_form_with(model: @user) do |f| %>
|
|
10
|
+
# <%= f.text_field :name, label: "Name" %>
|
|
11
|
+
# <%= f.submit "Save" %>
|
|
12
|
+
# <% end %>
|
|
13
|
+
#
|
|
14
|
+
# @example With URL
|
|
15
|
+
# <%= hakumi_form_with(url: "/users", method: :post) do |f| %>
|
|
16
|
+
# <%= f.text_field :email, label: "Email" %>
|
|
17
|
+
# <% end %>
|
|
18
|
+
#
|
|
19
|
+
# @param options [Hash] Options to pass to form_with
|
|
20
|
+
# @option options [Object] :model Model object for the form
|
|
21
|
+
# @option options [String] :url URL to submit the form to
|
|
22
|
+
# @option options [Symbol] :method HTTP method (:get, :post, :patch, :delete)
|
|
23
|
+
# @option options [Class] :builder Custom form builder (defaults to Hakumi::FormBuilder)
|
|
24
|
+
# @option options [Symbol] :layout Form layout (:horizontal, :vertical, :inline)
|
|
25
|
+
# @return [String] HTML form
|
|
26
|
+
def hakumi_form_with(**options, &block)
|
|
27
|
+
options[:builder] ||= Hakumi::FormBuilder
|
|
28
|
+
|
|
29
|
+
layout = options.delete(:layout)
|
|
30
|
+
if layout
|
|
31
|
+
html_options = options[:html] || {}
|
|
32
|
+
html_options[:class] = class_names(html_options[:class], "hakumi-form", "hakumi-form-#{layout}")
|
|
33
|
+
options[:html] = html_options
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
form_with(**options, &block)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -3,6 +3,14 @@ import { ensureId } from "../../core/dom.js"
|
|
|
3
3
|
import { register, unregister } from "../../core/registry.js"
|
|
4
4
|
|
|
5
5
|
export default class RegistryController extends Controller {
|
|
6
|
+
/**
|
|
7
|
+
* Declarative actions supported by this component.
|
|
8
|
+
* Each component must explicitly declare which actions it supports.
|
|
9
|
+
* Actions map to methods: 'close' -> close(), 'confirm' -> handleConfirm()
|
|
10
|
+
* @type {string[]}
|
|
11
|
+
*/
|
|
12
|
+
static declarativeActions = []
|
|
13
|
+
|
|
6
14
|
connect() {
|
|
7
15
|
const isComponentController = this.isHakumiController()
|
|
8
16
|
|
|
@@ -21,6 +29,8 @@ export default class RegistryController extends Controller {
|
|
|
21
29
|
|
|
22
30
|
if (typeof this.setup === "function") this.setup()
|
|
23
31
|
|
|
32
|
+
this.setupDeclarativeActions()
|
|
33
|
+
|
|
24
34
|
this.validateContract()
|
|
25
35
|
register(this.element.id, this.element.hakumiComponent.api)
|
|
26
36
|
|
|
@@ -28,12 +38,17 @@ export default class RegistryController extends Controller {
|
|
|
28
38
|
if (this.element.hakumiComponent.singleton && typeof window !== "undefined" && window.HakumiComponents) {
|
|
29
39
|
window.HakumiComponents[this.element.hakumiComponent.name] = this.element.hakumiComponent.api
|
|
30
40
|
}
|
|
41
|
+
|
|
42
|
+
// Mark component as ready for testing
|
|
43
|
+
this.element.dataset.hakumiReady = "true"
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
|
|
34
47
|
disconnect() {
|
|
35
48
|
if (typeof this.teardown === "function") this.teardown()
|
|
36
49
|
|
|
50
|
+
this.teardownDeclarativeActions()
|
|
51
|
+
|
|
37
52
|
const c = this.element?.hakumiComponent
|
|
38
53
|
if (c?.singleton && typeof window !== "undefined" && window.HakumiComponents) {
|
|
39
54
|
if (window.HakumiComponents[c.name] === c.api) {
|
|
@@ -84,18 +99,83 @@ export default class RegistryController extends Controller {
|
|
|
84
99
|
return String(this.identifier || "").startsWith("hakumi--")
|
|
85
100
|
}
|
|
86
101
|
|
|
87
|
-
|
|
102
|
+
|
|
88
103
|
setup() {
|
|
89
104
|
|
|
90
105
|
}
|
|
91
106
|
|
|
92
|
-
|
|
107
|
+
|
|
93
108
|
teardown() {
|
|
94
109
|
|
|
95
110
|
}
|
|
96
111
|
|
|
97
|
-
|
|
112
|
+
|
|
98
113
|
registerApi() {
|
|
99
114
|
|
|
100
115
|
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Sets up declarative action handling for data-hakumi-action attributes.
|
|
119
|
+
* Listens for clicks on elements with data-hakumi-action and routes to
|
|
120
|
+
* the appropriate handler method (close, open, toggle, confirm, cancel).
|
|
121
|
+
*/
|
|
122
|
+
setupDeclarativeActions() {
|
|
123
|
+
this.boundHandleDeclarativeAction = this.handleDeclarativeAction.bind(this)
|
|
124
|
+
this.element.addEventListener("click", this.boundHandleDeclarativeAction)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Cleans up declarative action event listener.
|
|
129
|
+
*/
|
|
130
|
+
teardownDeclarativeActions() {
|
|
131
|
+
if (this.boundHandleDeclarativeAction) {
|
|
132
|
+
this.element.removeEventListener("click", this.boundHandleDeclarativeAction)
|
|
133
|
+
this.boundHandleDeclarativeAction = null
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Handles clicks on elements with data-hakumi-action attribute.
|
|
139
|
+
* Routes to the appropriate method based on the action value.
|
|
140
|
+
* Supported actions: close, open, toggle, confirm, cancel
|
|
141
|
+
* @param {Event} event - The click event
|
|
142
|
+
*/
|
|
143
|
+
handleDeclarativeAction(event) {
|
|
144
|
+
const actionElement = event.target.closest("[data-hakumi-action]")
|
|
145
|
+
if (!actionElement || !this.element.contains(actionElement)) return
|
|
146
|
+
|
|
147
|
+
const action = actionElement.dataset.hakumiAction
|
|
148
|
+
const actionMethod = this.getActionMethod(action)
|
|
149
|
+
|
|
150
|
+
if (typeof actionMethod === "function") {
|
|
151
|
+
event.preventDefault()
|
|
152
|
+
actionMethod.call(this, event, actionElement)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Returns the method to call for a given action name.
|
|
158
|
+
* Only returns methods for actions declared in static declarativeActions.
|
|
159
|
+
* @param {string} action - The action name
|
|
160
|
+
* @returns {Function|undefined} The method to call
|
|
161
|
+
*/
|
|
162
|
+
getActionMethod(action) {
|
|
163
|
+
const allowedActions = this.constructor.declarativeActions || []
|
|
164
|
+
if (!allowedActions.includes(action)) {
|
|
165
|
+
return undefined
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Try direct method name: close, open, toggle
|
|
169
|
+
if (typeof this[action] === "function") {
|
|
170
|
+
return this[action]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Try handleX pattern: confirm -> handleConfirm, cancel -> handleCancel
|
|
174
|
+
const handlerName = `handle${action.charAt(0).toUpperCase()}${action.slice(1)}`
|
|
175
|
+
if (typeof this[handlerName] === "function") {
|
|
176
|
+
return this[handlerName]
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return undefined
|
|
180
|
+
}
|
|
101
181
|
}
|