//// //// Gleam UI input select super element. //// import gleam/bool import gleam/list import gleam/option.{type Option, None, Some} import lustre/attribute as a import lustre/element import lustre/element/html import lustre/event import gbr/ui/svg import gbr/ui/svg/icons as svg_icons import gbr/ui/typo import gbr/ui/core/el import gbr/ui/core/model.{type UIRender} type Select = UISelect type Item = UISelectItem type Items = List(Item) type Render(a) = UISelectRender(a) type Text = typo.UITypo type OnChange(a) = fn(String) -> a /// Select super element. /// pub opaque type UISelect { UISelect(el: el.UIEl, items: Items, multi: Bool, label: Option(Text)) } /// Select render type. /// pub type UISelectRender(a) { UISelectRender(in: Select, onchange: Option(OnChange(a))) } /// Select item (option) type. /// pub type UISelectItem { UISelectItem(value: String, label: String, selected: Bool) } /// Constructor of select super element. /// pub fn new(id: String) -> Select { UISelect(el: el.new(id), items: [], multi: False, label: None) } /// Set select title. /// pub fn label(in: Select, label: Text) -> Select { UISelect(..in, label: Some(label)) } pub fn placeholder(in: Select, placeholder: String) -> Select { let el = el.att(in.el, [#("placeholder", placeholder)]) UISelect(..in, el:) } /// Set select items (options). /// pub fn items(in: Select, items) -> Select { UISelect(..in, items:) } /// Set select multi items can selected. /// pub fn multi(in: Select, multi: Bool) -> Select { UISelect(..in, multi:) } /// Update select based by event occurs. /// pub fn selected(in: Select, value: String) -> Select { let UISelect(items:, ..) = in let items = items_toggle(value, items) UISelect(..in, items:) } pub fn at(in: Select) -> Render(a) { UISelectRender(in:, onchange: None) } pub fn onchange(at: Render(a), onchange: OnChange(a)) -> Render(a) { UISelectRender(..at, onchange: Some(onchange)) } /// Render select super element to `lustre/element.{type Element}`. /// pub fn render(at: Render(a)) -> UIRender(a) { let UISelectRender(in:, onchange:) = at let UISelect(el:, items:, multi:, label:) = in let id = el.get_id(in.el) let label = case label { None -> element.none() Some(label) -> html.label( [ a.for(id), a.class(title_class), ], [typo.render(label)], ) } use <- bool.guard(multi, do_multi(id, label, items)) let onchange = option.map(onchange, event.on_change) |> option.unwrap(a.none()) let attrs = el.class(el, select_class) |> el.attrs() html.div([], [ label, html.div([a.class(container_class)], [ html.select([onchange, ..attrs], do_items(items)), html.span([a.class(icon_class)], [ svg.new(20, 20) |> svg_icons.arrow() |> svg.class("stroke-current") |> svg.render(), ]), ]), ]) } // PRIVATE // fn items_toggle(value, items: Items) { use item <- list.map(items) use <- bool.guard(item.value != value, item) UISelectItem(..item, selected: True) } fn do_multi(id, label, items) -> UIRender(a) { html.div([], [ label, html.div([], [ html.select( [ a.id(id), a.class("hidden"), ], do_items(items), ), html.div([a.class("flex flex-col items-center")], [ html.input([a.type_("hidden"), a.name("values")]), html.div([a.class("relative z-20 inline-block w-full")], [ html.div([a.class("relative flex flex-col items-center")], [ html.div([a.class("w-full")], [ html.div( [ a.class( "shadow-theme-xs focus:border-brand-300 focus:shadow-focus-ring dark:focus:border-brand-300 mb-2 flex h-11 rounded-lg border border-gray-300 py-1.5 pr-3 pl-3 outline-hidden transition dark:border-gray-700 dark:bg-gray-900", ), ], [ html.div( [a.class("flex flex-auto flex-wrap gap-2")], list.append(do_items_selected_multi(items), [ html.div([], [ html.input([ a.class(case list.is_empty(items) { True -> "block" False -> "hidden" }), //TODO //a.placeholder(placeholder), a.class( "h-full w-full appearance-none border-0 bg-transparent p-1 pr-2 text-sm outline-hidden placeholder:text-gray-800 focus:border-0 focus:ring-0 focus:outline-hidden dark:placeholder:text-white/90", ), ]), ]), ]), ), html.div([a.class("flex w-7 items-center py-1 pr-1 pl-1")], [ html.button( [ a.class( "h-5 w-5 cursor-pointer text-gray-700 outline-hidden focus:outline-hidden dark:text-gray-400", ), ], [ svg.new(20, 20) |> svg_icons.arrow() |> svg.class("stroke-current") |> svg.render(), ], ), ]), ], ), ]), html.div([a.class("w-full px-4")], [ html.div( [ a.class( "max-h-select absolute top-full left-0 z-40 w-full overflow-y-auto rounded-lg bg-white shadow-sm dark:bg-gray-900", ), ], [ html.div( [a.class("flex w-full flex-col")], do_items_multi(items), ), ], ), ]), ]), ]), ]), ]), ]) } fn do_items(items) { use option <- list.map(items) let UISelectItem(value:, label:, selected:) = option let classes = case selected { True -> option_class <> " " <> option_selected_class False -> option_class } html.option( [ a.class(classes), a.value(value), a.selected(selected), ], label, ) } fn do_items_multi(items) { use option <- list.map(filter_items_selected(items, False)) html.div([], [ html.div( [ a.class( "hover:bg-primary/5 w-full cursor-pointer rounded-t border-b border-gray-200 dark:border-gray-800", ), ], [ html.div( [ a.class( "relative flex w-full items-center border-l-2 border-transparent p-2 pl-2", ), ], [ html.div([a.class("flex w-full items-center")], [ html.div( [ a.class("mx-2 leading-6 text-gray-800 dark:text-white/90"), a.value(option.value), ], [html.text(option.label)], ), ]), ], ), ], ), ]) } fn do_items_selected_multi(items) { use option <- list.map(filter_items_selected(items, True)) html.div( [ a.class( "group flex items-center justify-center rounded-full border-[0.7px] border-transparent bg-gray-100 py-1 pr-2 pl-2.5 text-sm text-gray-800 hover:border-gray-200 dark:bg-gray-800 dark:text-white/90 dark:hover:border-gray-800", ), ], [ html.div( [ a.class("max-w-full flex-initial"), a.value(option.value), ], [ html.text(option.label), ], ), html.div([a.class("flex flex-auto flex-row-reverse")], [ html.div( [ a.class( "cursor-pointer pl-2 text-gray-500 group-hover:text-gray-400 dark:text-gray-400", ), ], [ svg.new(14, 14) |> svg_icons.close() |> svg.render(), ], ), ]), ], ) } fn filter_items_selected(options, is_selected) { use opt <- list.filter(options) let UISelectItem(selected:, ..) = opt selected == is_selected } const title_class = "mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400" const container_class = "relative z-20 bg-transparent" const select_class = "dark:bg-dark-900 shadow-theme-xs focus:border-brand-300 focus:ring-brand-500/10 dark:focus:border-brand-800 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pr-11 text-sm text-gray-800 placeholder:text-gray-400 focus:ring-3 focus:outline-hidden dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30" const option_class = "text-gray-700 dark:bg-gray-900 dark:text-gray-400" const option_selected_class = "text-gray-800 dark:text-white/90" const icon_class = "pointer-events-none absolute top-1/2 right-4 z-30 -translate-y-1/2 text-gray-700 dark:text-gray-400"