poetry-core 0.0.2
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 +7 -0
- data/CHANGELOG.md +3 -0
- data/DESIGN.md +145 -0
- data/LICENSE.txt +21 -0
- data/README.md +30 -0
- data/THIRD_PARTY_NOTICES.md +507 -0
- data/app/components/poetry/core/box/component.rb +82 -0
- data/app/components/poetry/core/component.rb +390 -0
- data/app/components/poetry/core/concerns/agent_tools.rb +418 -0
- data/app/components/poetry/core/concerns/declared_attributes.rb +267 -0
- data/app/components/poetry/core/concerns/introspection.rb +533 -0
- data/app/components/poetry/core/concerns/options.rb +262 -0
- data/app/components/poetry/core/concerns/parts.rb +148 -0
- data/app/components/poetry/core/concerns/stimulus.rb +363 -0
- data/app/components/poetry/core/concerns/styles.rb +323 -0
- data/app/components/poetry/core/style.rb +150 -0
- data/app/components/poetry/core/wrapper/component.rb +90 -0
- data/app/javascript/poetry/core/accordion_controller.js +165 -0
- data/app/javascript/poetry/core/action_bar_controller.js +109 -0
- data/app/javascript/poetry/core/autocomplete_controller.js +220 -0
- data/app/javascript/poetry/core/calendar_controller.js +406 -0
- data/app/javascript/poetry/core/carousel_controller.js +167 -0
- data/app/javascript/poetry/core/checkbox_group_controller.js +97 -0
- data/app/javascript/poetry/core/checked_controller.js +208 -0
- data/app/javascript/poetry/core/clipboard_text_controller.js +111 -0
- data/app/javascript/poetry/core/combobox_controller.js +1095 -0
- data/app/javascript/poetry/core/command_controller.js +510 -0
- data/app/javascript/poetry/core/context_menu_controller.js +204 -0
- data/app/javascript/poetry/core/date_field_controller.js +584 -0
- data/app/javascript/poetry/core/date_picker_controller.js +111 -0
- data/app/javascript/poetry/core/deferred_controller.js +111 -0
- data/app/javascript/poetry/core/dialog_controller.js +206 -0
- data/app/javascript/poetry/core/dismissable_controller.js +182 -0
- data/app/javascript/poetry/core/drawer_controller.js +365 -0
- data/app/javascript/poetry/core/file_input_controller.js +191 -0
- data/app/javascript/poetry/core/focus_scope_controller.js +162 -0
- data/app/javascript/poetry/core/helpers/announce.js +228 -0
- data/app/javascript/poetry/core/helpers/breakpoint.js +28 -0
- data/app/javascript/poetry/core/helpers/collection.js +18 -0
- data/app/javascript/poetry/core/helpers/direction.js +15 -0
- data/app/javascript/poetry/core/helpers/escape.js +38 -0
- data/app/javascript/poetry/core/helpers/filter_rank.js +104 -0
- data/app/javascript/poetry/core/helpers/focus_guards.js +46 -0
- data/app/javascript/poetry/core/helpers/hotkey.js +43 -0
- data/app/javascript/poetry/core/helpers/id_integrity.js +65 -0
- data/app/javascript/poetry/core/helpers/incomplete_date.js +328 -0
- data/app/javascript/poetry/core/helpers/mask.js +283 -0
- data/app/javascript/poetry/core/helpers/portal.js +217 -0
- data/app/javascript/poetry/core/helpers/presence.js +210 -0
- data/app/javascript/poetry/core/helpers/registration_guard.js +76 -0
- data/app/javascript/poetry/core/helpers/scroll_lock.js +67 -0
- data/app/javascript/poetry/core/helpers/scroller_geometry.js +422 -0
- data/app/javascript/poetry/core/helpers/state.js +80 -0
- data/app/javascript/poetry/core/helpers/tabbable.js +56 -0
- data/app/javascript/poetry/core/helpers/turbo_cache.js +21 -0
- data/app/javascript/poetry/core/helpers/typeahead.js +85 -0
- data/app/javascript/poetry/core/hotkey_controller.js +55 -0
- data/app/javascript/poetry/core/hover_card_controller.js +431 -0
- data/app/javascript/poetry/core/index.js +218 -0
- data/app/javascript/poetry/core/mask_controller.js +500 -0
- data/app/javascript/poetry/core/menu_controller.js +1050 -0
- data/app/javascript/poetry/core/menubar_controller.js +329 -0
- data/app/javascript/poetry/core/message_scroller_controller.js +930 -0
- data/app/javascript/poetry/core/navigation_menu_controller.js +458 -0
- data/app/javascript/poetry/core/number_field_controller.js +404 -0
- data/app/javascript/poetry/core/optimistic_form_controller.js +56 -0
- data/app/javascript/poetry/core/otp_controller.js +219 -0
- data/app/javascript/poetry/core/popover_controller.js +300 -0
- data/app/javascript/poetry/core/popper_controller.js +439 -0
- data/app/javascript/poetry/core/pressed_controller.js +74 -0
- data/app/javascript/poetry/core/questionnaire_controller.js +468 -0
- data/app/javascript/poetry/core/radio_group_controller.js +197 -0
- data/app/javascript/poetry/core/resizable_controller.js +202 -0
- data/app/javascript/poetry/core/roving_focus_controller.js +215 -0
- data/app/javascript/poetry/core/scroll_spy_controller.js +84 -0
- data/app/javascript/poetry/core/search_field_controller.js +95 -0
- data/app/javascript/poetry/core/select_controller.js +924 -0
- data/app/javascript/poetry/core/sensitive_input_controller.js +195 -0
- data/app/javascript/poetry/core/sheet_controller.js +24 -0
- data/app/javascript/poetry/core/sidebar_controller.js +272 -0
- data/app/javascript/poetry/core/slider_controller.js +419 -0
- data/app/javascript/poetry/core/state_controller.js +82 -0
- data/app/javascript/poetry/core/table_selection_controller.js +153 -0
- data/app/javascript/poetry/core/tabs_controller.js +151 -0
- data/app/javascript/poetry/core/tag_group_controller.js +134 -0
- data/app/javascript/poetry/core/toast_controller.js +228 -0
- data/app/javascript/poetry/core/toast_trigger_controller.js +26 -0
- data/app/javascript/poetry/core/toaster_controller.js +197 -0
- data/app/javascript/poetry/core/toggle_group_controller.js +206 -0
- data/app/javascript/poetry/core/tooltip_controller.js +541 -0
- data/app/javascript/poetry/core/tree_controller.js +265 -0
- data/app/javascript/poetry/core/vendor/floating_ui_core.js +1043 -0
- data/app/javascript/poetry/core/vendor/floating_ui_dom.js +794 -0
- data/app/javascript/poetry/core/vendor/floating_ui_utils.js +141 -0
- data/app/javascript/poetry/core/vendor/floating_ui_utils_dom.js +170 -0
- data/app/views/poetry/core/preview.html.erb +13 -0
- data/config/component_registry.yml +20 -0
- data/config/controllers_manifest.json +1528 -0
- data/config/importmap.rb +11 -0
- data/config/state_vocabulary.json +31 -0
- data/lib/active_model/type/list.rb +33 -0
- data/lib/active_model/type/symbol.rb +36 -0
- data/lib/poetry/core/check/stable_identity.rb +104 -0
- data/lib/poetry/core/check.rb +1445 -0
- data/lib/poetry/core/config.rb +286 -0
- data/lib/poetry/core/contrib/wrapped_helper.rb +68 -0
- data/lib/poetry/core/css/bem_merger.rb +45 -0
- data/lib/poetry/core/css/bem_reference.rb +87 -0
- data/lib/poetry/core/css/override_scan.rb +110 -0
- data/lib/poetry/core/css/resolver.rb +161 -0
- data/lib/poetry/core/css/safelist.rb +41 -0
- data/lib/poetry/core/css/tailwind_merger.rb +102 -0
- data/lib/poetry/core/css/template_classes.rb +102 -0
- data/lib/poetry/core/css/theme_coverage.rb +72 -0
- data/lib/poetry/core/css/var_coverage.rb +75 -0
- data/lib/poetry/core/css/verifier.rb +106 -0
- data/lib/poetry/core/design_lint.rb +720 -0
- data/lib/poetry/core/design_md/import.rb +250 -0
- data/lib/poetry/core/design_md.rb +461 -0
- data/lib/poetry/core/engine.rb +71 -0
- data/lib/poetry/core/errors.rb +53 -0
- data/lib/poetry/core/html/attributes.rb +667 -0
- data/lib/poetry/core/icons.rb +157 -0
- data/lib/poetry/core/llms_text.rb +354 -0
- data/lib/poetry/core/page_architectures.rb +304 -0
- data/lib/poetry/core/part_contract.rb +285 -0
- data/lib/poetry/core/preview/abstract.rb +121 -0
- data/lib/poetry/core/preview/base.rb +227 -0
- data/lib/poetry/core/preview/sidecarable.rb +136 -0
- data/lib/poetry/core/preview/template.rb +186 -0
- data/lib/poetry/core/recipe_items.rb +82 -0
- data/lib/poetry/core/registry.rb +363 -0
- data/lib/poetry/core/registry_address.rb +115 -0
- data/lib/poetry/core/registry_client.rb +217 -0
- data/lib/poetry/core/registry_installer.rb +234 -0
- data/lib/poetry/core/registry_items.rb +184 -0
- data/lib/poetry/core/skill_text.rb +288 -0
- data/lib/poetry/core/stable_id.rb +110 -0
- data/lib/poetry/core/stimulus/builder.rb +395 -0
- data/lib/poetry/core/stimulus/declarations.rb +444 -0
- data/lib/poetry/core/stimulus/manifest.rb +68 -0
- data/lib/poetry/core/stimulus/merger.rb +131 -0
- data/lib/poetry/core/stimulus_contract.rb +263 -0
- data/lib/poetry/core/tag_helper.rb +10 -0
- data/lib/poetry/core/template_compile.rb +74 -0
- data/lib/poetry/core/token_import.rb +317 -0
- data/lib/poetry/core/tokens/color.rb +215 -0
- data/lib/poetry/core/tokens/contrast_gate.rb +120 -0
- data/lib/poetry/core/tokens/generator.rb +211 -0
- data/lib/poetry/core/tokens.rb +117 -0
- data/lib/poetry/core/version.rb +9 -0
- data/lib/poetry/core.rb +52 -0
- data/lib/poetry-core.rb +5 -0
- data/tokens/aliases.css +145 -0
- data/tokens/tailwind-theme.css +47 -0
- data/tokens/tokens.css +78 -0
- data/tokens/tokens.dtcg.json +779 -0
- data/vendor/floating-ui/LICENSE +20 -0
- data/vendor/floating-ui/VENDORED_VERSIONS +15 -0
- data/vendor/shadcn-tailwind/LICENSE +21 -0
- data/vendor/shadcn-tailwind/VENDORED_COMMIT +1 -0
- data/vendor/shadcn-tailwind/tailwind.css +629 -0
- data/vendor/tw-animate-css/LICENSE +21 -0
- data/vendor/tw-animate-css/VENDORED_VERSION +1 -0
- data/vendor/tw-animate-css/tw-animate.css +1 -0
- metadata +267 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Core
|
|
5
|
+
# HTML attribute plumbing: the merge-aware attributes hash.
|
|
6
|
+
module HTML
|
|
7
|
+
# A specialized hash for managing HTML attributes with intelligent merging capabilities.
|
|
8
|
+
#
|
|
9
|
+
# This class extends ActiveSupport::HashWithIndifferentAccess to provide enhanced
|
|
10
|
+
# functionality for handling HTML attributes, particularly CSS classes, Stimulus
|
|
11
|
+
# controllers/actions, data attributes, and ARIA attributes.
|
|
12
|
+
#
|
|
13
|
+
# Features:
|
|
14
|
+
# - Smart merging of CSS classes without duplication
|
|
15
|
+
# - Intelligent merging of Stimulus controllers and actions
|
|
16
|
+
# - Automatic flattening of nested data and aria attributes
|
|
17
|
+
# - Proper handling of HTML boolean attributes
|
|
18
|
+
# - Both mutating (!) and non-mutating versions of merge methods
|
|
19
|
+
#
|
|
20
|
+
# @example Basic usage
|
|
21
|
+
# attrs = Poetry::Core::HTML::Attributes.new(class: "btn", disabled: true)
|
|
22
|
+
# attrs.merge_classes!("btn-primary")
|
|
23
|
+
# attrs.to_attributes # => { "class" => "btn btn-primary", "disabled" => "disabled" }
|
|
24
|
+
#
|
|
25
|
+
# @example Merging Stimulus controllers
|
|
26
|
+
# attrs = Poetry::Core::HTML::Attributes.new
|
|
27
|
+
# attrs.merge_stimulus_controllers!("dropdown", "modal")
|
|
28
|
+
# attrs.to_attributes # => { "data-controller" => "dropdown modal" }
|
|
29
|
+
#
|
|
30
|
+
# @example Working with data attributes
|
|
31
|
+
# attrs = Poetry::Core::HTML::Attributes.new(data: { id: 1, name: "test" })
|
|
32
|
+
# attrs.to_attributes # => { "data-id" => "1", "data-name" => "test" }
|
|
33
|
+
class Attributes < ActiveSupport::HashWithIndifferentAccess
|
|
34
|
+
# The safe way to combine component wiring with caller-supplied
|
|
35
|
+
# attributes into a plain hash for content_tag / button_to: every
|
|
36
|
+
# hash flows through one Attributes instance, so stimulus keys
|
|
37
|
+
# (data-controller / data-action, either spelling) concatenate
|
|
38
|
+
# instead of clobbering, classes tailwind-merge, and to_attributes
|
|
39
|
+
# unifies double-spelled slots deterministically. Plain Hash#merge
|
|
40
|
+
# of wiring with caller options silently drops one side's wiring -
|
|
41
|
+
# never do that; call this.
|
|
42
|
+
#
|
|
43
|
+
# @param hashes [Array<Hash, nil>] wiring first, caller last
|
|
44
|
+
# @return [Hash] flat, render-ready attributes
|
|
45
|
+
def self.merged(*hashes)
|
|
46
|
+
hashes.compact.inject(new) { |merged, hash| merged.merge!(hash) }.to_attributes
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# List of HTML boolean attributes that should be rendered as attribute name only
|
|
50
|
+
# when truthy, or omitted when falsy.
|
|
51
|
+
#
|
|
52
|
+
# @example
|
|
53
|
+
# attrs = Poetry::Core::HTML::Attributes.new(disabled: true, hidden: false)
|
|
54
|
+
# attrs.to_attributes # => { "disabled" => "disabled" }
|
|
55
|
+
# # Note: hidden is omitted because it's false
|
|
56
|
+
BOOLEAN_ATTRIBUTES = %w[
|
|
57
|
+
allowfullscreen allowpaymentrequest async autofocus autoplay checked compact controls declare default
|
|
58
|
+
defaultchecked defaultmuted defaultselected defer disabled enabled formnovalidate hidden indeterminate
|
|
59
|
+
inert ismap itemscope loop multiple muted nohref nomodule noresize noshade novalidate nowrap open
|
|
60
|
+
pauseonexit playsinline readonly required reversed scoped seamless selected sortable truespeed
|
|
61
|
+
typemustmatch visible
|
|
62
|
+
].freeze
|
|
63
|
+
|
|
64
|
+
# Merges CSS classes into the attributes, returning a new instance.
|
|
65
|
+
#
|
|
66
|
+
# This non-mutating method creates a deep copy of the attributes and merges
|
|
67
|
+
# the provided classnames intelligently, avoiding duplicates and handling
|
|
68
|
+
# conditional classes.
|
|
69
|
+
#
|
|
70
|
+
# @param classnames [Array<String, Hash, Array>] One or more classnames to merge.
|
|
71
|
+
# Can be strings, hashes with conditional classes, or arrays.
|
|
72
|
+
# @return [Poetry::Core::HTML::Attributes] A new instance with merged classes
|
|
73
|
+
#
|
|
74
|
+
# @example Merge simple classes
|
|
75
|
+
# attrs = Poetry::Core::HTML::Attributes.new(class: "btn")
|
|
76
|
+
# new_attrs = attrs.merge_classes("btn-primary", "btn-lg")
|
|
77
|
+
# new_attrs["class"] # => "btn btn-primary btn-lg"
|
|
78
|
+
# attrs["class"] # => "btn" (original unchanged)
|
|
79
|
+
def merge_classes(*classnames)
|
|
80
|
+
deep_dup.merge_classes!(*classnames)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Merges CSS classes into the attributes, mutating the current instance.
|
|
84
|
+
#
|
|
85
|
+
# This mutating method modifies the current attributes object by merging
|
|
86
|
+
# the provided classnames.
|
|
87
|
+
#
|
|
88
|
+
# @param classnames [Array<String, Hash, Array>] One or more classnames to merge
|
|
89
|
+
# @return [Poetry::Core::HTML::Attributes] Self for method chaining
|
|
90
|
+
#
|
|
91
|
+
# @example Merge classes in place
|
|
92
|
+
# attrs = Poetry::Core::HTML::Attributes.new(class: "btn")
|
|
93
|
+
# attrs.merge_classes!("btn-primary")
|
|
94
|
+
# attrs["class"] # => "btn btn-primary"
|
|
95
|
+
def merge_classes!(*classnames)
|
|
96
|
+
self["class"] = config.classname_merger.merge(self["class"], *classnames)
|
|
97
|
+
self
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Merges Stimulus controllers into the data-controller attribute, returning a new instance.
|
|
101
|
+
#
|
|
102
|
+
# @param controllers [Array<String, Hash>] One or more controller names or
|
|
103
|
+
# hashes with controller names and options
|
|
104
|
+
# @return [Poetry::Core::HTML::Attributes] A new instance with merged controllers
|
|
105
|
+
#
|
|
106
|
+
# @example Merge controllers
|
|
107
|
+
# attrs = Poetry::Core::HTML::Attributes.new
|
|
108
|
+
# new_attrs = attrs.merge_stimulus_controllers("dropdown", "modal")
|
|
109
|
+
# new_attrs["data"]["controller"] # => "dropdown modal"
|
|
110
|
+
def merge_stimulus_controllers(*controllers)
|
|
111
|
+
deep_dup.merge_stimulus_controllers!(*controllers)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Merges Stimulus controllers into the data-controller attribute, mutating the current instance.
|
|
115
|
+
#
|
|
116
|
+
# @param controllers [Array<String, Hash>] One or more controller names
|
|
117
|
+
# @return [Poetry::Core::HTML::Attributes] Self for method chaining
|
|
118
|
+
#
|
|
119
|
+
# @example Merge controllers in place
|
|
120
|
+
# attrs = Poetry::Core::HTML::Attributes.new(data: { controller: "dropdown" })
|
|
121
|
+
# attrs.merge_stimulus_controllers!("modal")
|
|
122
|
+
# attrs["data"]["controller"] # => "dropdown modal"
|
|
123
|
+
def merge_stimulus_controllers!(*controllers)
|
|
124
|
+
self["data"] ||= {}
|
|
125
|
+
self["data"]["controller"] = config.stimulus_merger.merge_controllers(dig("data", "controller"), *controllers)
|
|
126
|
+
self
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Merges Stimulus actions into the data-action attribute, returning a new instance.
|
|
130
|
+
#
|
|
131
|
+
# @param actions [Array<String, Hash>] One or more action strings or hashes
|
|
132
|
+
# @return [Poetry::Core::HTML::Attributes] A new instance with merged actions
|
|
133
|
+
#
|
|
134
|
+
# @example Merge actions
|
|
135
|
+
# attrs = Poetry::Core::HTML::Attributes.new
|
|
136
|
+
# new_attrs = attrs.merge_stimulus_actions("click->modal#open", "keydown->modal#close")
|
|
137
|
+
# new_attrs["data"]["action"] # => "click->modal#open keydown->modal#close"
|
|
138
|
+
def merge_stimulus_actions(*actions)
|
|
139
|
+
deep_dup.merge_stimulus_actions!(*actions)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Merges Stimulus actions into the data-action attribute, mutating the current instance.
|
|
143
|
+
#
|
|
144
|
+
# @param actions [Array<String, Hash>] One or more action strings
|
|
145
|
+
# @return [Poetry::Core::HTML::Attributes] Self for method chaining
|
|
146
|
+
#
|
|
147
|
+
# @example Merge actions in place
|
|
148
|
+
# attrs = Poetry::Core::HTML::Attributes.new(data: { action: "click->modal#open" })
|
|
149
|
+
# attrs.merge_stimulus_actions!("keydown->modal#close")
|
|
150
|
+
# attrs["data"]["action"] # => "click->modal#open keydown->modal#close"
|
|
151
|
+
def merge_stimulus_actions!(*actions)
|
|
152
|
+
self["data"] ||= {}
|
|
153
|
+
self["data"]["action"] = config.stimulus_merger.merge_actions(dig("data", "action"), *actions)
|
|
154
|
+
self
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Merges Stimulus data attributes, returning a new instance.
|
|
158
|
+
#
|
|
159
|
+
# This method intelligently merges data attributes, handling controllers,
|
|
160
|
+
# actions, and other data attributes appropriately.
|
|
161
|
+
#
|
|
162
|
+
# @param stimulus_hash [Array<Hash>] One or more hashes of Stimulus data attributes
|
|
163
|
+
# @yield [key, old_value, new_value] Optional block for custom merge logic
|
|
164
|
+
# @return [Poetry::Core::HTML::Attributes] A new instance with merged Stimulus data
|
|
165
|
+
#
|
|
166
|
+
# @example Merge Stimulus attributes
|
|
167
|
+
# attrs = Poetry::Core::HTML::Attributes.new(data: { controller: "dropdown" })
|
|
168
|
+
# new_attrs = attrs.merge_stimulus({ action: "click->modal#open", target: "output" })
|
|
169
|
+
# new_attrs["data"] # => { "controller" => "dropdown", "action" => "click->modal#open", "target" => "output" }
|
|
170
|
+
def merge_stimulus(*stimulus_hash, &)
|
|
171
|
+
deep_dup.merge_stimulus!(*stimulus_hash, &)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Merges Stimulus data attributes, mutating the current instance.
|
|
175
|
+
#
|
|
176
|
+
# @param stimulus_hash [Array<Hash>] One or more hashes of Stimulus data attributes
|
|
177
|
+
# @yield [key, old_value, new_value] Optional block for custom merge logic
|
|
178
|
+
# @return [Poetry::Core::HTML::Attributes] Self for method chaining
|
|
179
|
+
#
|
|
180
|
+
# @example Merge Stimulus attributes in place
|
|
181
|
+
# attrs = Poetry::Core::HTML::Attributes.new
|
|
182
|
+
# attrs.merge_stimulus!({ controller: "dropdown", action: "click->dropdown#toggle" })
|
|
183
|
+
# attrs["data"] # => { "controller" => "dropdown", "action" => "click->dropdown#toggle" }
|
|
184
|
+
def merge_stimulus!(*stimulus_hash, &)
|
|
185
|
+
self["data"] = config.stimulus_merger.merge(self["data"], *stimulus_hash, &)
|
|
186
|
+
self
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Merges attributes only if they are not already set, returning a new instance.
|
|
190
|
+
#
|
|
191
|
+
# This method intelligently merges attributes by only adding keys that don't exist
|
|
192
|
+
# in the current attributes. It handles both flat and nested data attributes:
|
|
193
|
+
# - "data-controller" and data: { controller: "..." } are treated as the same
|
|
194
|
+
# - "aria-label" and aria: { label: "..." } are treated as the same
|
|
195
|
+
#
|
|
196
|
+
# @param other_hash [Hash] Hash of attributes to merge if not set
|
|
197
|
+
# @return [Poetry::Core::HTML::Attributes] A new instance with conditionally merged attributes
|
|
198
|
+
#
|
|
199
|
+
# @example Merge defaults that won't override user values
|
|
200
|
+
# attrs = Poetry::Core::HTML::Attributes.new(class: "btn", data: { controller: "dropdown" })
|
|
201
|
+
# defaults = { class: "btn-default", data: { action: "click->modal#open" }, id: "my-btn" }
|
|
202
|
+
# new_attrs = attrs.merge_if_not_set(defaults)
|
|
203
|
+
# # Result: class: "btn", data: { controller: "dropdown", action: "click->modal#open" }, id: "my-btn"
|
|
204
|
+
# # Note: "btn-default" not added because class was already set
|
|
205
|
+
# # Note: action added because it wasn't set, but controller kept original value
|
|
206
|
+
def merge_if_not_set(other_hash)
|
|
207
|
+
deep_dup.merge_if_not_set!(other_hash)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Merges attributes only if they are not already set, mutating the current instance.
|
|
211
|
+
#
|
|
212
|
+
# @param other_hash [Hash] Hash of attributes to merge if not set
|
|
213
|
+
# @return [Poetry::Core::HTML::Attributes] Self for method chaining
|
|
214
|
+
#
|
|
215
|
+
# @example Set defaults without overriding
|
|
216
|
+
# attrs = Poetry::Core::HTML::Attributes.new(data: { controller: "dropdown" })
|
|
217
|
+
# attrs.merge_if_not_set!(class: "btn", data: { controller: "modal", action: "click->dropdown#toggle" })
|
|
218
|
+
# # Result: class: "btn", data: { controller: "dropdown", action: "click->dropdown#toggle" }
|
|
219
|
+
def merge_if_not_set!(other_hash)
|
|
220
|
+
other_hash = convert_value(other_hash)
|
|
221
|
+
|
|
222
|
+
# First, normalize any flat data/aria attributes in self to nested format
|
|
223
|
+
normalize_flat_attributes!
|
|
224
|
+
|
|
225
|
+
other_hash.each do |key, value|
|
|
226
|
+
if key.to_s == "data" && value.is_a?(Hash)
|
|
227
|
+
# Handle nested data attributes
|
|
228
|
+
merge_data_if_not_set!(value)
|
|
229
|
+
elsif key.to_s == "aria" && value.is_a?(Hash)
|
|
230
|
+
# Handle nested aria attributes
|
|
231
|
+
merge_aria_if_not_set!(value)
|
|
232
|
+
elsif key.to_s.start_with?("data-")
|
|
233
|
+
# Handle flat data attributes like "data-controller"
|
|
234
|
+
nested_key = key.to_s.delete_prefix("data-").underscore
|
|
235
|
+
merge_data_key_if_not_set!(nested_key, value)
|
|
236
|
+
elsif key.to_s.start_with?("aria-")
|
|
237
|
+
# Handle flat aria attributes like "aria-label"
|
|
238
|
+
nested_key = key.to_s.delete_prefix("aria-").underscore
|
|
239
|
+
set_if_not_present!("aria", nested_key, value)
|
|
240
|
+
elsif !has_attribute?(key)
|
|
241
|
+
# Only set if the attribute doesn't exist at all
|
|
242
|
+
self[key] = value
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
self
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Checks if an attribute is set, handling both flat and nested formats.
|
|
250
|
+
#
|
|
251
|
+
# This method is smart about data and aria attributes, checking both
|
|
252
|
+
# the nested and flat formats.
|
|
253
|
+
#
|
|
254
|
+
# @param key [String, Symbol] The attribute key to check
|
|
255
|
+
# @param nested_key [String, Symbol, nil] Optional nested key for data/aria attributes
|
|
256
|
+
# @return [Boolean] True if the attribute exists
|
|
257
|
+
#
|
|
258
|
+
# @example Check for regular attributes
|
|
259
|
+
# attrs = Poetry::Core::HTML::Attributes.new(class: "btn", id: "my-btn")
|
|
260
|
+
# attrs.has_attribute?(:class) # => true
|
|
261
|
+
# attrs.has_attribute?(:disabled) # => false
|
|
262
|
+
#
|
|
263
|
+
# @example Check for data attributes (nested format)
|
|
264
|
+
# attrs = Poetry::Core::HTML::Attributes.new(data: { controller: "dropdown" })
|
|
265
|
+
# attrs.has_attribute?("data", "controller") # => true
|
|
266
|
+
# attrs.has_attribute?("data", "action") # => false
|
|
267
|
+
#
|
|
268
|
+
# @example Check for data attributes (flat format)
|
|
269
|
+
# attrs = Poetry::Core::HTML::Attributes.new("data-controller" => "dropdown")
|
|
270
|
+
# attrs.has_attribute?("data", "controller") # => true
|
|
271
|
+
def has_attribute?(key, nested_key = nil)
|
|
272
|
+
if nested_key
|
|
273
|
+
# Check nested format: data: { controller: "..." }
|
|
274
|
+
return true if dig(key, nested_key).present?
|
|
275
|
+
|
|
276
|
+
# Check flat format: "data-controller" => "..."
|
|
277
|
+
flat_key = "#{key}-#{nested_key.to_s.dasherize}"
|
|
278
|
+
return true if self[flat_key].present?
|
|
279
|
+
|
|
280
|
+
false
|
|
281
|
+
else
|
|
282
|
+
# A nil simple attribute is UNSET, not set: to_attributes drops
|
|
283
|
+
# nils, so a nil key must not block merge_if_not_set defaults.
|
|
284
|
+
# (Component#html_attributes always seeds class: - nil when the
|
|
285
|
+
# dictionary base is empty - which silently swallowed Sidebar's
|
|
286
|
+
# wrapper layout classes; the poetry-docs shell caught it.)
|
|
287
|
+
key?(key) && !self[key].nil?
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Gets an attribute value, handling both flat and nested formats.
|
|
292
|
+
#
|
|
293
|
+
# @param key [String, Symbol] The attribute key
|
|
294
|
+
# @param nested_key [String, Symbol, nil] Optional nested key for data/aria attributes
|
|
295
|
+
# @return [Object, nil] The attribute value or nil if not found
|
|
296
|
+
#
|
|
297
|
+
# @example Get nested data attribute
|
|
298
|
+
# attrs = Poetry::Core::HTML::Attributes.new(data: { controller: "dropdown" })
|
|
299
|
+
# attrs.get_attribute("data", "controller") # => "dropdown"
|
|
300
|
+
#
|
|
301
|
+
# @example Get flat data attribute
|
|
302
|
+
# attrs = Poetry::Core::HTML::Attributes.new("data-controller" => "dropdown")
|
|
303
|
+
# attrs.get_attribute("data", "controller") # => "dropdown"
|
|
304
|
+
def get_attribute(key, nested_key = nil)
|
|
305
|
+
if nested_key
|
|
306
|
+
# Try nested format first
|
|
307
|
+
value = dig(key, nested_key)
|
|
308
|
+
return value if value.present?
|
|
309
|
+
|
|
310
|
+
# Try flat format
|
|
311
|
+
flat_key = "#{key}-#{nested_key.to_s.dasherize}"
|
|
312
|
+
self[flat_key]
|
|
313
|
+
else
|
|
314
|
+
self[key]
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Converts the attributes hash to a flat hash suitable for HTML rendering.
|
|
319
|
+
#
|
|
320
|
+
# This method performs several transformations:
|
|
321
|
+
# - Flattens nested data attributes (data: { id: 1 } => "data-id" => "1")
|
|
322
|
+
# - Flattens nested aria attributes (aria: { label: "Close" } => "aria-label" => "Close")
|
|
323
|
+
# - Handles boolean attributes (disabled: true => "disabled" => "disabled")
|
|
324
|
+
# - Skips nil values for all attributes
|
|
325
|
+
# - Converts complex values to JSON strings when appropriate
|
|
326
|
+
#
|
|
327
|
+
# @return [Hash<String, String>] A flat hash of HTML attribute names to values
|
|
328
|
+
#
|
|
329
|
+
# @example Convert to HTML attributes
|
|
330
|
+
# attrs = Poetry::Core::HTML::Attributes.new(
|
|
331
|
+
# class: "btn btn-primary",
|
|
332
|
+
# disabled: true,
|
|
333
|
+
# id: nil,
|
|
334
|
+
# data: { id: 1, controller: "dropdown", target: nil },
|
|
335
|
+
# aria: { label: "Close", hidden: nil }
|
|
336
|
+
# )
|
|
337
|
+
# attrs.to_attributes
|
|
338
|
+
# # => {
|
|
339
|
+
# # "class" => "btn btn-primary",
|
|
340
|
+
# # "disabled" => "disabled",
|
|
341
|
+
# # "data-id" => "1",
|
|
342
|
+
# # "data-controller" => "dropdown",
|
|
343
|
+
# # "aria-label" => "Close"
|
|
344
|
+
# # }
|
|
345
|
+
# When both spellings of the same attribute coexist in the store
|
|
346
|
+
# (flat "data-x" AND nested data: { x: }), the output is
|
|
347
|
+
# deterministic instead of insertion-order roulette: stimulus keys
|
|
348
|
+
# (data-controller / data-action) CONCATENATE so wiring is never
|
|
349
|
+
# lost, and every other duplicate resolves flat-spelling-wins (the
|
|
350
|
+
# normalize_flat_attributes! convention).
|
|
351
|
+
def to_attributes
|
|
352
|
+
each_with_object({}) do |(key, value), hash|
|
|
353
|
+
next if value.nil?
|
|
354
|
+
|
|
355
|
+
if key == "data" && value.is_a?(Hash)
|
|
356
|
+
data_attribute(hash, value)
|
|
357
|
+
elsif key == "aria" && value.is_a?(Hash)
|
|
358
|
+
aria_attribute(hash, value)
|
|
359
|
+
elsif BOOLEAN_ATTRIBUTES.include?(key)
|
|
360
|
+
hash[key] = key if value
|
|
361
|
+
elsif key == "data-controller"
|
|
362
|
+
hash[key] = config.stimulus_merger.merge_controllers(hash[key], value)
|
|
363
|
+
elsif key == "data-action"
|
|
364
|
+
hash[key] = config.stimulus_merger.merge_actions(hash[key], value)
|
|
365
|
+
else
|
|
366
|
+
hash[key] = format_attribute_value(value)
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
private
|
|
372
|
+
|
|
373
|
+
# Returns the configuration object for mergers and other settings.
|
|
374
|
+
#
|
|
375
|
+
# @return [Poetry::Core::Config] A deep duplicate of the current configuration
|
|
376
|
+
# @api private
|
|
377
|
+
def config
|
|
378
|
+
@config ||= Poetry::Core::Config.current.deep_dup
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Normalizes flat data-* and aria-* attributes to nested format.
|
|
382
|
+
#
|
|
383
|
+
# Converts "data-controller" => "dropdown" to data: { controller: "dropdown" }
|
|
384
|
+
# Converts "aria-label" => "Close" to aria: { label: "Close" }
|
|
385
|
+
#
|
|
386
|
+
# @return [void]
|
|
387
|
+
# @api private
|
|
388
|
+
def normalize_flat_attributes!
|
|
389
|
+
flat_data = {}
|
|
390
|
+
flat_aria = {}
|
|
391
|
+
|
|
392
|
+
# Find all flat data-* and aria-* attributes
|
|
393
|
+
keys_to_delete = []
|
|
394
|
+
each do |key, value|
|
|
395
|
+
if key.to_s.start_with?("data-")
|
|
396
|
+
nested_key = key.to_s.delete_prefix("data-").underscore
|
|
397
|
+
flat_data[nested_key] = value
|
|
398
|
+
keys_to_delete << key
|
|
399
|
+
elsif key.to_s.start_with?("aria-")
|
|
400
|
+
nested_key = key.to_s.delete_prefix("aria-").underscore
|
|
401
|
+
flat_aria[nested_key] = value
|
|
402
|
+
keys_to_delete << key
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Remove flat attributes
|
|
407
|
+
keys_to_delete.each { |key| delete(key) }
|
|
408
|
+
|
|
409
|
+
# Merge into nested format. Flat-spelling-wins on collision,
|
|
410
|
+
# except stimulus wiring, which concatenates so a double-spelled
|
|
411
|
+
# controller/action never silently drops tokens.
|
|
412
|
+
if flat_data.any?
|
|
413
|
+
self["data"] ||= {}
|
|
414
|
+
flat_data.each do |key, value|
|
|
415
|
+
self["data"][key] = case key.to_s
|
|
416
|
+
when "controller"
|
|
417
|
+
config.stimulus_merger.merge_controllers(self["data"][key], value)
|
|
418
|
+
when "action"
|
|
419
|
+
config.stimulus_merger.merge_actions(self["data"][key], value)
|
|
420
|
+
else
|
|
421
|
+
value
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
return unless flat_aria.any?
|
|
427
|
+
|
|
428
|
+
self["aria"] ||= {}
|
|
429
|
+
self["aria"].merge!(flat_aria)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Merges data attributes only if they're not already set.
|
|
433
|
+
#
|
|
434
|
+
# @param data_hash [Hash] Hash of data attributes to merge
|
|
435
|
+
# @return [void]
|
|
436
|
+
# @api private
|
|
437
|
+
def merge_data_if_not_set!(data_hash)
|
|
438
|
+
self["data"] ||= {}
|
|
439
|
+
|
|
440
|
+
data_hash.each do |key, value|
|
|
441
|
+
merge_data_key_if_not_set!(key, value)
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Stimulus wiring keys concatenate instead of first-wins: a caller
|
|
446
|
+
# attaching their own data-controller / data-action must not
|
|
447
|
+
# disconnect the wiring the component declares (first-wins here
|
|
448
|
+
# silently dropped the component's controller and broke its JS).
|
|
449
|
+
# Caller tokens stay first (the merge_stimulus_* convention);
|
|
450
|
+
# every other data key remains strictly first-wins.
|
|
451
|
+
#
|
|
452
|
+
# @param key [String, Symbol] The nested data key
|
|
453
|
+
# @param value [Object] The incoming (default) value
|
|
454
|
+
# @return [void]
|
|
455
|
+
# @api private
|
|
456
|
+
def merge_data_key_if_not_set!(key, value)
|
|
457
|
+
case key.to_s
|
|
458
|
+
when "controller"
|
|
459
|
+
merged = config.stimulus_merger.merge_controllers(dig("data", "controller"), value)
|
|
460
|
+
self["data"] ||= {}
|
|
461
|
+
self["data"]["controller"] = merged if merged
|
|
462
|
+
when "action"
|
|
463
|
+
merged = config.stimulus_merger.merge_actions(dig("data", "action"), value)
|
|
464
|
+
self["data"] ||= {}
|
|
465
|
+
self["data"]["action"] = merged if merged
|
|
466
|
+
else
|
|
467
|
+
set_if_not_present!("data", key, value)
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Merges aria attributes only if they're not already set.
|
|
472
|
+
#
|
|
473
|
+
# @param aria_hash [Hash] Hash of aria attributes to merge
|
|
474
|
+
# @return [void]
|
|
475
|
+
# @api private
|
|
476
|
+
def merge_aria_if_not_set!(aria_hash)
|
|
477
|
+
self["aria"] ||= {}
|
|
478
|
+
|
|
479
|
+
aria_hash.each do |key, value|
|
|
480
|
+
set_if_not_present!("aria", key, value)
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Sets a nested attribute only if it's not already present in either format.
|
|
485
|
+
#
|
|
486
|
+
# Checks both nested format (e.g., data: { controller: "..." }) and
|
|
487
|
+
# flat format (e.g., "data-controller" => "...").
|
|
488
|
+
#
|
|
489
|
+
# @param prefix [String] The prefix (e.g., "data", "aria")
|
|
490
|
+
# @param key [String, Symbol] The nested key
|
|
491
|
+
# @param value [Object] The value to set
|
|
492
|
+
# @return [void]
|
|
493
|
+
# @api private
|
|
494
|
+
def set_if_not_present!(prefix, key, value)
|
|
495
|
+
return if has_attribute?(prefix, key)
|
|
496
|
+
|
|
497
|
+
# Set in nested format
|
|
498
|
+
self[prefix] ||= {}
|
|
499
|
+
self[prefix][key] = value
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Flattens a nested data hash into prefixed HTML data attributes.
|
|
503
|
+
#
|
|
504
|
+
# @param attributes [Hash] The target hash to populate with flattened attributes
|
|
505
|
+
# @param data [Hash] The nested data hash to flatten
|
|
506
|
+
# @return [void]
|
|
507
|
+
# @api private
|
|
508
|
+
#
|
|
509
|
+
# @example
|
|
510
|
+
# attributes = {}
|
|
511
|
+
# data_attribute(attributes, { id: 1, user_name: "John" })
|
|
512
|
+
# attributes # => { "data-id" => "1", "data-user-name" => "John" }
|
|
513
|
+
def data_attribute(attributes, data)
|
|
514
|
+
data.each_pair do |key, value|
|
|
515
|
+
next if value.nil?
|
|
516
|
+
|
|
517
|
+
slot = prefixed_attribute(:data, key)
|
|
518
|
+
|
|
519
|
+
# Slot-collision rules (both spellings in one store): stimulus
|
|
520
|
+
# wiring concatenates, everything else is flat-spelling-wins -
|
|
521
|
+
# the nested expansion never overwrites an already-written flat
|
|
522
|
+
# value.
|
|
523
|
+
case slot
|
|
524
|
+
when "data-controller"
|
|
525
|
+
attributes[slot] = config.stimulus_merger.merge_controllers(attributes[slot], value)
|
|
526
|
+
when "data-action"
|
|
527
|
+
attributes[slot] = config.stimulus_merger.merge_actions(attributes[slot], value)
|
|
528
|
+
else
|
|
529
|
+
attributes[slot] = format_attribute_value(value) unless attributes.key?(slot)
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# Flattens a nested aria hash into prefixed HTML aria attributes.
|
|
535
|
+
#
|
|
536
|
+
# Handles special cases for Hash and Array values by flattening them
|
|
537
|
+
# into space-separated strings.
|
|
538
|
+
#
|
|
539
|
+
# @param attributes [Hash] The target hash to populate with flattened attributes
|
|
540
|
+
# @param aria [Hash] The nested aria hash to flatten
|
|
541
|
+
# @return [void]
|
|
542
|
+
# @api private
|
|
543
|
+
#
|
|
544
|
+
# @example
|
|
545
|
+
# attributes = {}
|
|
546
|
+
# aria_attribute(attributes, { label: "Close", hidden: true })
|
|
547
|
+
# attributes # => { "aria-label" => "Close", "aria-hidden" => "true" }
|
|
548
|
+
def aria_attribute(attributes, aria)
|
|
549
|
+
aria.each_pair do |key, value|
|
|
550
|
+
next if value.nil?
|
|
551
|
+
|
|
552
|
+
if value.is_a?(Hash) || value.is_a?(Array)
|
|
553
|
+
values = flatten_attribute_values(value)
|
|
554
|
+
next if values.empty?
|
|
555
|
+
|
|
556
|
+
value = values.join(" ")
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
slot = prefixed_attribute(:aria, key)
|
|
560
|
+
# Flat-spelling-wins on collision, matching data_attribute.
|
|
561
|
+
attributes[slot] = format_attribute_value(value) unless attributes.key?(slot)
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Creates a prefixed and dasherized attribute name.
|
|
566
|
+
#
|
|
567
|
+
# @param prefix [Symbol, String] The prefix (e.g., :data, :aria)
|
|
568
|
+
# @param key [Symbol, String] The attribute key
|
|
569
|
+
# @return [String] The prefixed and dasherized attribute name
|
|
570
|
+
# @api private
|
|
571
|
+
#
|
|
572
|
+
# @example
|
|
573
|
+
# prefixed_attribute(:data, :user_name) # => "data-user-name"
|
|
574
|
+
# prefixed_attribute(:aria, :labelledby) # => "aria-labelledby"
|
|
575
|
+
def prefixed_attribute(prefix, key)
|
|
576
|
+
"#{prefix}-#{key.to_s.dasherize}"
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# Formats an attribute value for HTML output.
|
|
580
|
+
#
|
|
581
|
+
# Simple types (String, Symbol, BigDecimal) are converted to strings.
|
|
582
|
+
# Complex types are converted to JSON.
|
|
583
|
+
#
|
|
584
|
+
# @param value [Object] The value to format
|
|
585
|
+
# @return [String] The formatted value
|
|
586
|
+
# @api private
|
|
587
|
+
#
|
|
588
|
+
# @example
|
|
589
|
+
# format_attribute_value("hello") # => "hello"
|
|
590
|
+
# format_attribute_value(123) # => "123"
|
|
591
|
+
# format_attribute_value({ a: 1 }) # => '{"a":1}'
|
|
592
|
+
def format_attribute_value(value)
|
|
593
|
+
value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal) ? value.to_s : value.to_json
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# Recursively flattens attribute values from hashes and arrays.
|
|
597
|
+
#
|
|
598
|
+
# - Hash values: includes keys where values are truthy
|
|
599
|
+
# - Array values: recursively flattens nested arrays
|
|
600
|
+
# - Other values: includes if present
|
|
601
|
+
#
|
|
602
|
+
# @param args [Array] Values to flatten
|
|
603
|
+
# @return [Array<String>] Flattened array of string values
|
|
604
|
+
# @api private
|
|
605
|
+
#
|
|
606
|
+
# @example
|
|
607
|
+
# flatten_attribute_values({ active: true, hidden: false }, "visible")
|
|
608
|
+
# # => ["active", "visible"]
|
|
609
|
+
def flatten_attribute_values(*args)
|
|
610
|
+
args.each_with_object([]) do |value, tags|
|
|
611
|
+
case value
|
|
612
|
+
when Hash
|
|
613
|
+
value.each { |k, v| tags << k.to_s if v }
|
|
614
|
+
when Array
|
|
615
|
+
tags.concat flatten_attribute_values(*value)
|
|
616
|
+
else
|
|
617
|
+
tags << value.to_s if value.present?
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
# Overrides HashWithIndifferentAccess merge behavior for special attributes.
|
|
623
|
+
#
|
|
624
|
+
# Provides custom merge logic for "class" and "data" attributes.
|
|
625
|
+
#
|
|
626
|
+
# @param other_hash [Hash] The hash to merge
|
|
627
|
+
# @param block [Proc] Optional block for custom merge logic
|
|
628
|
+
# @return [Poetry::Core::HTML::Attributes] The merged result
|
|
629
|
+
# @api private
|
|
630
|
+
def update_with_single_argument(other_hash, block)
|
|
631
|
+
update_block = ->(key, old_value, new_value) { update_single_attribute(key, old_value, new_value, &block) }
|
|
632
|
+
super(other_hash, update_block)
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
# Handles merging of a single attribute with special logic for certain keys.
|
|
636
|
+
#
|
|
637
|
+
# - "class": Uses classname merger for intelligent class merging
|
|
638
|
+
# - "data": Uses stimulus merger for Stimulus attribute merging
|
|
639
|
+
# - Other keys: Uses provided block or defaults to new value
|
|
640
|
+
#
|
|
641
|
+
# @param key [String] The attribute key
|
|
642
|
+
# @param old_value [Object] The existing value
|
|
643
|
+
# @param new_value [Object] The new value to merge
|
|
644
|
+
# @yield [key, old_value, new_value] Optional block for custom merge logic
|
|
645
|
+
# @return [Object] The merged value
|
|
646
|
+
# @api private
|
|
647
|
+
def update_single_attribute(key, old_value, new_value, &block)
|
|
648
|
+
if key == "class"
|
|
649
|
+
config.classname_merger.merge(old_value, new_value)
|
|
650
|
+
elsif key == "data"
|
|
651
|
+
config.stimulus_merger.merge(old_value, convert_value(new_value), &block)
|
|
652
|
+
elsif key == "data-controller"
|
|
653
|
+
# The flat spelling gets the same never-clobber treatment as the
|
|
654
|
+
# nested data: branch - stimulus wiring concatenates on conflict.
|
|
655
|
+
config.stimulus_merger.merge_controllers(old_value, new_value)
|
|
656
|
+
elsif key == "data-action"
|
|
657
|
+
config.stimulus_merger.merge_actions(old_value, new_value)
|
|
658
|
+
elsif block
|
|
659
|
+
yield(key, old_value, new_value)
|
|
660
|
+
else
|
|
661
|
+
new_value
|
|
662
|
+
end
|
|
663
|
+
end
|
|
664
|
+
end
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
end
|