module Coco class Button < Coco::Component include Concerns::Extendable include Concerns::AcceptsOptions include Concerns::WithIcon include Concerns::WithTooltip include Concerns::AcceptsTheme SIZES = [:sm, :md, :lg, nil] SIZE_ALIASES = { default: [:sm, {xl: :md}] } THEMES = [] DEFAULT_THEME = nil BUTTON_ATTRS = %i[type value name disabled href] tag_name :button tag_attr(*BUTTON_ATTRS) accepts_option :disabled, from: [true, false] accepts_option :confirm, from: [true, false, nil], default: nil accepts_option :fit, from: [:auto, :full] accepts_option :collapsible, from: [true, false, nil] accepts_option :toggle, from: [:horizontal, :vertical] accepts_option :state accepts_option :dropdown do |dd| dd.accepts_option :placement, from: %w[top top-start top-end right right-start right-end bottom bottom-start bottom-end left left-start left-end auto auto-start auto-end] end renders_one :text, Coco::Content renders_many :states, ->(name = nil, **kwargs) do name ||= kwargs.fetch(:name) @states[name.to_sym] = kwargs.except!(:name) end before_initialize do |kwargs| button_size = kwargs.fetch(:size, :default)&.to_sym if button_size.in?(self::SIZE_ALIASES.keys) && !kwargs.key?(:resize) kwargs[:size], kwargs[:resize] = self::SIZE_ALIASES.fetch(button_size) end kwargs end before_render do resize.each { set_tag_data_attr("size-#{_1}", _2) } if loading? set_option_value(:state, "loading") elsif active? set_option_value(:state, "active") end @button_attrs = tag_attrs.slice(*BUTTON_ATTRS).merge(@button_element_attrs) tag_attrs.except!(*BUTTON_ATTRS) if disabled? button_attrs[:disabled] = true end if dropdown? set_option_value(:dropdown, :placement, "bottom-start") unless get_option(:dropdown, :placement).present? end unless button_attrs[:type] || link? button_attrs[:type] = "button" end end attr_reader :on_click, :resize, :props, :button_attrs def initialize(click: nil, resize: nil, props: nil, states: nil, loading: false, active: false, button_element: {}, **kwargs) @on_click = click @props = props.to_h @resize = resize.to_h @states = states.to_h @loading = loading @active = active @alpine_data = {} @button_element_attrs = button_element super(**kwargs) end def toggle? toggle_direction.present? && button_text.present? end def toggle_direction get_option_value(:toggle) end def button_tag_name button_attrs[:href].present? ? :a : :button end def button_text text&.to_s || content&.to_s || "" end def loading? @loading == true end def active? @active == true end def confirm? get_option_value(:confirm) end def disabled? get_option_value(:disabled) end def link? button_tag_name == :a end def button? button_tag_name == :button end def icon_only? !states.find { _2[:text].present? } end def dropdown? false end def dropdown_opts if dropdown? jsify_data({ appendTo: "parent", offset: [0, 1], placement: get_option_value(:dropdown, :placement) }.compact) end end def component_classes [ "coco-button", tag_attrs[:class], { "icon-only": icon_only?, "with-icon": (icon? && !icon_only?) } ] end def states @_states ||= begin states = default_states.deep_merge(@states) states.each do |name, props| if props.key?(:icon) if props[:icon] == false props.except!(:icon) # explicitly no icon else props[:icon] = render_icon(props[:icon]) end elsif icon? props[:icon] = icon # no icon specified, use the icon for the default state end end.compact.transform_keys { _1.to_s.camelcase(:lower) } end end def state_tooltips @_tooltips = states.map do |name, props| [name, props[:tooltip]] if props[:tooltip].present? end.compact.to_h end def alpine_data if props.present? || state_tooltips.present? { tooltips: (state_tooltips if state_tooltips.present?), props: (props if props.present?) }.compact end end private def default_states states = { default: { text: button_text, icon: icon, tooltip: get_option_value(:tooltip, :content) } } if @states.key?(:loading) states[:loading] = { text: "Loading...", icon: :loader_2, tooltip: nil } end states end def render_icon(icon) return icon if icon.nil? || icon.is_a?(String) icon = icon.is_a?(Symbol) ? {name: icon} : icon icon.is_a?(Hash) ? render(Coco::Icon.new(**icon)) : icon end end end