|
| 1 | +defmodule PetalComponents.Carousel do |
| 2 | + @moduledoc """ |
| 3 | + Provides a versatile and customizable carousel component. |
| 4 | +
|
| 5 | + This component enables the creation of carousels with various features such as |
| 6 | + slide indicators, navigation controls, and dynamic slide content. |
| 7 | +
|
| 8 | + ## Features |
| 9 | +
|
| 10 | + - **Slides**: Define multiple slides, each with custom content. |
| 11 | + - **Navigation Controls**: Include previous and next buttons to manually navigate through the slides. |
| 12 | + - **Indicators**: Optional indicators show the current slide and allow direct navigation to any slide. |
| 13 | + - **Transition Types**: Choose between fade and slide transitions. |
| 14 | + - **Responsive Design**: Supports various sizes and padding options to adapt to different screen sizes. |
| 15 | + """ |
| 16 | + |
| 17 | + use Phoenix.Component |
| 18 | + import PetalComponents.Icon, only: [icon: 1] |
| 19 | + import Phoenix.LiveView.Utils, only: [random_id: 0] |
| 20 | + |
| 21 | + @doc """ |
| 22 | + The `carousel` component is used to create interactive carousels with customizable attributes |
| 23 | + such as `size`, `padding`, and `transition_type`. It supports adding multiple slides with different content, |
| 24 | + and includes options for navigation controls and indicators. |
| 25 | +
|
| 26 | + ## Examples |
| 27 | +
|
| 28 | + ```elixir |
| 29 | + <.carousel id="carousel-test-one" transition_type="fade" indicator={true}> |
| 30 | + <:slide title="Slide 1" /> |
| 31 | + <:slide title="Slide 2" /> |
| 32 | + <:slide title="Slide 3" /> |
| 33 | + </.carousel> |
| 34 | + ``` |
| 35 | + """ |
| 36 | + @doc type: :component |
| 37 | + attr :id, :string, doc: "A unique identifier is used to manage state and interaction" |
| 38 | + attr :class, :string, default: nil, doc: "Custom CSS class for additional styling" |
| 39 | + attr :size, :string, default: "large", doc: "Determines the overall size of the elements" |
| 40 | + attr :padding, :string, default: "medium", doc: "Determines padding for items" |
| 41 | + attr :text_position, :string, default: "center", doc: "Determines the element's text position" |
| 42 | + attr :rest, :global, doc: "Global attributes can define defaults" |
| 43 | + attr :indicator, :boolean, default: false, doc: "Specifies whether to show element indicators" |
| 44 | + attr :control, :boolean, default: true, doc: "Determines whether to show navigation controls" |
| 45 | + attr :active_index, :integer, default: 0, doc: "Index of the active slide (starts at 0)" |
| 46 | + attr :autoplay, :boolean, default: false, doc: "Enable or disable autoplay functionality" |
| 47 | + attr :autoplay_interval, :integer, default: 5000, doc: "Time between slides in ms" |
| 48 | + |
| 49 | + attr :transition_type, :string, |
| 50 | + default: "fade", |
| 51 | + doc: "Type of transition between slides (fade or slide)" |
| 52 | + |
| 53 | + attr :transition_duration, :integer, default: 500, doc: "Duration of transition in milliseconds" |
| 54 | + |
| 55 | + slot :slide, required: true do |
| 56 | + attr :title, :string, doc: "Title of the slide" |
| 57 | + attr :description, :string, doc: "Description of the slide" |
| 58 | + attr :class, :string, doc: "Custom CSS class for additional styling" |
| 59 | + end |
| 60 | + |
| 61 | + def carousel(assigns) do |
| 62 | + assigns = |
| 63 | + assigns |
| 64 | + |> assign_new(:id, fn -> "carousel-#{random_id()}" end) |
| 65 | + |> assign_new(:transition_class, fn -> transition_class(assigns.transition_type) end) |
| 66 | + |
| 67 | + ~H""" |
| 68 | + <div |
| 69 | + id={@id} |
| 70 | + phx-hook="CarouselHook" |
| 71 | + phx-update="ignore" |
| 72 | + data-active-index={@active_index} |
| 73 | + data-autoplay={to_string(@autoplay)} |
| 74 | + data-autoplay-interval={@autoplay_interval} |
| 75 | + data-transition-type={@transition_type} |
| 76 | + data-transition-duration={@transition_duration} |
| 77 | + class={[ |
| 78 | + "pc-carousel", |
| 79 | + @transition_class, |
| 80 | + size_class(@size), |
| 81 | + padding_class(@padding), |
| 82 | + text_position_class(@text_position), |
| 83 | + @class |
| 84 | + ]} |
| 85 | + > |
| 86 | + <button |
| 87 | + :if={@control} |
| 88 | + id={"#{@id}-carousel-prev"} |
| 89 | + class="pc-carousel__button pc-carousel__button--prev" |
| 90 | + aria-label="Previous slide" |
| 91 | + > |
| 92 | + <.icon name="hero-chevron-left-solid" class="pc-carousel__icon" /> |
| 93 | + </button> |
| 94 | +
|
| 95 | + <button |
| 96 | + :if={@control} |
| 97 | + id={"#{@id}-carousel-next"} |
| 98 | + class="pc-carousel__button pc-carousel__button--next" |
| 99 | + aria-label="Next slide" |
| 100 | + > |
| 101 | + <.icon name="hero-chevron-right-solid" class="pc-carousel__icon" /> |
| 102 | + </button> |
| 103 | +
|
| 104 | + <div class="pc-carousel__slides"> |
| 105 | + <div |
| 106 | + :for={{slide, index} <- Enum.with_index(@slide)} |
| 107 | + id={"#{@id}-carousel-slide-#{index}"} |
| 108 | + class={[ |
| 109 | + "pc-carousel__slide", |
| 110 | + if(index == @active_index, |
| 111 | + do: "pc-carousel__slide--active", |
| 112 | + else: "pc-carousel__slide--inactive" |
| 113 | + ), |
| 114 | + slide[:class] |
| 115 | + ]} |
| 116 | + style={ |
| 117 | + if @transition_type == "fade" do |
| 118 | + if index == @active_index, |
| 119 | + do: "opacity: 1; z-index: 10;", |
| 120 | + else: "opacity: 0; z-index: 0;" |
| 121 | + else |
| 122 | + if index == @active_index, |
| 123 | + do: "opacity: 1; z-index: 10; transform: translateX(0);", |
| 124 | + else: "opacity: 0; z-index: 0; transform: translateX(100%);" |
| 125 | + end |
| 126 | + } |
| 127 | + > |
| 128 | + <div class="pc-carousel__slide-content"> |
| 129 | + <div class="pc-carousel__content"> |
| 130 | + <div class="pc-carousel__content-wrapper"> |
| 131 | + <div class="pc-carousel__title"> |
| 132 | + {slide[:title] || "Slide #{index + 1}"} |
| 133 | + </div> |
| 134 | + <p :if={!is_nil(slide[:description])} class="pc-carousel__description"> |
| 135 | + {slide[:description]} |
| 136 | + </p> |
| 137 | + </div> |
| 138 | + </div> |
| 139 | + </div> |
| 140 | + </div> |
| 141 | + </div> |
| 142 | +
|
| 143 | + <.slide_indicators :if={@indicator} id={@id} count={length(@slide)} /> |
| 144 | + </div> |
| 145 | + """ |
| 146 | + end |
| 147 | + |
| 148 | + defp slide_indicators(assigns) do |
| 149 | + ~H""" |
| 150 | + <div id={"#{@id}-carousel-slide-indicator"} class="pc-carousel__indicators"> |
| 151 | + <button |
| 152 | + :for={indicator_item <- 1..@count} |
| 153 | + id={"#{@id}-carousel-indicator-#{indicator_item}"} |
| 154 | + data-indicator-index={"#{indicator_item - 1}"} |
| 155 | + class="pc-carousel__indicator" |
| 156 | + aria-label={"Slide #{indicator_item}"} |
| 157 | + /> |
| 158 | + </div> |
| 159 | + """ |
| 160 | + end |
| 161 | + |
| 162 | + defp transition_class("fade") do |
| 163 | + "[&_.pc-carousel__slide]:transition-opacity [&_.pc-carousel__slide]:duration-500 [&_.pc-carousel__slide]:ease-in-out" |
| 164 | + end |
| 165 | + |
| 166 | + defp transition_class("slide") do |
| 167 | + "[&_.pc-carousel__slide]:transition-transform [&_.pc-carousel__slide]:duration-500 [&_.pc-carousel__slide]:ease-in-out" |
| 168 | + end |
| 169 | + |
| 170 | + defp transition_class(_), do: "" |
| 171 | + |
| 172 | + defp size_class("small"), do: "text-sm [&_.description-wrapper]:max-w-96" |
| 173 | + defp size_class("medium"), do: "text-base [&_.description-wrapper]:max-w-xl" |
| 174 | + defp size_class("large"), do: "text-lg [&_.description-wrapper]:max-w-2xl" |
| 175 | + defp size_class(_), do: "" |
| 176 | + |
| 177 | + defp padding_class("small"), do: "[&_.description-wrapper]:p-3" |
| 178 | + defp padding_class("medium"), do: "[&_.description-wrapper]:p-4" |
| 179 | + defp padding_class("large"), do: "[&_.description-wrapper]:p-5" |
| 180 | + defp padding_class(_), do: "" |
| 181 | + |
| 182 | + defp text_position_class("start"), do: "[&_.description-wrapper]:text-start" |
| 183 | + defp text_position_class("center"), do: "[&_.description-wrapper]:text-center" |
| 184 | + defp text_position_class("end"), do: "[&_.description-wrapper]:text-end" |
| 185 | + defp text_position_class(_), do: "" |
| 186 | +end |
0 commit comments