module Coco class Button < Coco::Component include Concerns::Extendable include Concerns::AcceptsOptions include Concerns::WithIcon include Concerns::WithTooltip include Concerns::AcceptsTheme SIZES = [:xs, :sm, :md, :lg, nil] SIZE_ALIASES = { default: [:sm, {xl: :md}] } THEMES = [ "primary", "text-primary", "secondary", "text-secondary", "positive", "text-positive", "negative", "text-negative", "warning", "text-warning", "info", "text-info", "toolbar", "toolbar-floating", "text-toolbar", "neutral-dark", "neutral-light", "text-neutral-light", "text-neutral-dark", "blank", nil ] DEFAULT_THEME = "primary" tag_attr :type, :value, :name, :disabled, :href, :target accepts_option :size, from: SIZES, default: :md accepts_option :theme, from: THEMES, default: DEFAULT_THEME 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 :floating, from: [true, false] accepts_option :state accepts_option :variant 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], private: true dd.accepts_option :flip, from: [true, false], default: true, private: true end renders_one :text, Coco::Content renders_one :dropdown, types: { content: ->(&block) { block.call }, confirmation: ->(**kwargs) do set_option_value(:confirm, true) Coco::ConfirmPanel.new(**kwargs) end } renders_many :states, ->(name = nil, **kwargs) do name ||= kwargs.fetch(:name) @states[name.to_sym] = kwargs.except!(:name) end attr_reader :on_click, :resize, :turbo_frame def initialize(click: nil, resize: nil, states: nil, loading: false, active: false, static: nil, turbo: nil, turbo_frame: nil, **kwargs) @on_click = click @resize = resize.to_h @states = states.to_h @loading = loading @static = static @active = active @turbo = turbo @turbo_frame = turbo_frame end def with_dropdown(...) with_dropdown_content(...) end def with_confirmation(...) with_dropdown_confirmation(...) 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 static? @_static ||= if @static.nil? !(confirm? || dropdown? || tooltip? || @states.any? || on_click.present? || get_option_value(:collapsible) || tag_attrs.key?(:x)) else @static end end def loading? @loading == true end def active? @active == true end def confirm? get_option_value(:confirm) end def tooltip? get_option_value(:tooltip, :content).present? end def disabled? get_option_value(:disabled) end def link? button_tag == :a end def button? button_tag == :button end def icon_only? (@states.none? && button_text.blank?) || !states.find { _2[:text].present? } end def alpine_wrapper_attrs if dropdown? || confirm? { data: x_data("buttonDropdown"), dropdown: jsify_data({placement: get_option_value(:dropdown, :placement), flip: get_option_value(:dropdown, :flip)}.compact), bind: "root" } 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 def turbo_data_attr_value if @turbo == false "false" elsif @turbo == true "true" end 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_circle, 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 before_initialize do |kwargs| if kwargs.key?(:modal) modal_name = (kwargs[:modal] == true) ? "default" : kwargs[:modal] kwargs[:data] = kwargs.fetch(:data, {}).merge(coco_modal_data_attributes(modal_name)) kwargs.delete(:modal) end button_size = kwargs.fetch(:size, :default)&.to_sym if button_size.in?(SIZE_ALIASES.keys) && !kwargs.key?(:resize) kwargs[:size], kwargs[:resize] = 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 confirm? && !dropdown? with_confirmation do |confirm| confirm.with_text { "Are you sure?" } confirm.with_button { "Yes, continue" } end end if (dropdown? || confirm?) && get_option(:dropdown, :placement).blank? set_option_value(:dropdown, :placement, "bottom-start") end end class << self include Coco::ModalHelper end end end