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 tag_attr :type, :value, :name, :disabled, :href, :target 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 set_tag_attr(:disabled, true) if disabled? set_tag_attr(:type, "button") unless tag_attr?(:type) || link? if dropdown? || confirm? && get_option(:dropdown, :placement).blank? set_option_value(:dropdown, :placement, "bottom-start") end end attr_reader :on_click, :resize def initialize(click: nil, resize: nil, states: nil, loading: false, active: false, tooltip: nil, **kwargs) @on_click = click @resize = resize.to_h @states = states.to_h @loading = loading @active = active end def toggle? toggle_direction.present? && button_text.present? end def toggle_direction get_option_value(:toggle) end def button_tag tag_attr(: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 == :a end def button? button_tag == :button end def icon_only? !states.find { _2[:text].present? } end def dropdown? false end def alpine_wrapper_attrs if dropdown? || confirm? { data: x_data("buttonDropdown"), dropdown: jsify_data({offset: [0, 1], placement: get_option_value(:dropdown, :placement)}.compact), "@dropdown:show": ("button.setState('active')" if dropdown?), "@dropdown:hide": ("button.resetState()" if dropdown?), "@confirmation:confirm": ("button.approveAndRun($event)" if confirm?), "@confirmation:cancel": ("button.cancelConfirmation($event)" if confirm?) } end 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 {tooltips: state_tooltips} if state_tooltips.present? end private def default_states { default: default_state, loading: (loading_state if @states&.key?(:loading)) }.compact end def default_state { text: button_text, icon: icon, tooltip: get_option_value(:tooltip, :content) } end def loading_state { text: "Loading...", icon: :loader_2, tooltip: nil } 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