use std::option::Option;
use dioxus::prelude::*;
use dioxus_nox_select::{AutoComplete, SelectContext, select};
use crate::components as tag_input;
use crate::hook::{TagInputState, extract_clipboard_text, is_denied};
use crate::tag::TagLike;
#[derive(Clone)]
struct ComboAvailable<T: TagLike + 'static>(Signal<Vec<T>>);
#[derive(Clone)]
struct ComboDisabledValues(Memo<Vec<String>>);
#[derive(Clone, Copy)]
struct ComboConfig {
close_on_select: bool,
}
#[derive(Props, Clone, PartialEq)]
pub struct RootProps<T: TagLike + 'static> {
pub available_tags: Vec<T>,
#[props(default)]
pub initial_selected: Vec<T>,
#[props(default)]
pub disabled: bool,
#[props(default = "Type to search\u{2026}".to_string())]
pub placeholder: String,
#[props(default)]
pub on_create: Option<Callback<String, Option<T>>>,
#[props(default)]
pub max_tags: Option<usize>,
#[props(default)]
pub allow_duplicates: bool,
#[props(default)]
pub deny_list: Option<Vec<String>>,
#[props(default = true)]
pub close_on_select: bool,
#[props(default)]
pub on_add: Option<EventHandler<T>>,
#[props(default)]
pub on_remove: Option<EventHandler<T>>,
#[props(extends = GlobalAttributes)]
pub attributes: Vec<Attribute>,
pub children: Element,
}
#[allow(non_snake_case)]
pub fn Root<T: TagLike>(props: RootProps<T>) -> Element {
let available = props.available_tags.clone();
let initial = props.initial_selected.clone();
let placeholder = props.placeholder.clone();
let deny_list = props.deny_list.clone();
let close_on_select = props.close_on_select;
rsx! {
tag_input::Root::<T> {
available_tags: available.clone(),
initial_selected: initial,
disabled: props.disabled,
max_tags: props.max_tags,
allow_duplicates: props.allow_duplicates,
deny_list: deny_list,
on_create: props.on_create,
on_add: props.on_add,
on_remove: props.on_remove,
select::Root {
multiple: true,
disabled: props.disabled,
autocomplete: AutoComplete::List,
ComboWiring::<T> {
available: available,
placeholder: placeholder,
close_on_select: close_on_select,
{props.children}
}
}
}
}
}
#[component]
fn ComboWiring<T: TagLike>(
available: Vec<T>,
placeholder: String,
#[props(default = true)] close_on_select: bool,
children: Element,
) -> Element {
let mut tag_ctx = use_context::<TagInputState<T>>();
let mut select_ctx = use_context::<SelectContext>();
let available_sig = use_signal(|| available.clone());
use_context_provider(|| ComboAvailable::<T>(available_sig));
use_context_provider(|| ComboConfig { close_on_select });
let avail_for_deny = available.clone();
let denied_values = use_memo(move || {
let deny = tag_ctx.deny_list.read();
match &*deny {
Some(deny_list) => avail_for_deny
.iter()
.filter(|t| is_denied(t.name(), deny_list))
.map(|t| t.id().to_string())
.collect(),
None => Vec::new(),
}
});
use_context_provider(|| ComboDisabledValues(denied_values));
use_effect(move || {
let selected_values = select_ctx.current_values();
let tag_ids: Vec<String> = tag_ctx
.selected_tags
.peek()
.iter()
.map(|t| t.id().to_string())
.collect();
let mut changed = false;
for val in &selected_values {
if !tag_ids.contains(val)
&& let Some(tag) = available.iter().find(|t| t.id() == val.as_str())
{
tag_ctx.add_tag(tag.clone());
changed = true;
}
}
for tag_id in &tag_ids {
if !selected_values.contains(tag_id) {
tag_ctx.remove_tag(tag_id);
changed = true;
}
}
if changed && try_use_context::<ComboConfig>().is_none_or(|c| c.close_on_select) {
select_ctx.set_open(false);
}
});
use_effect(move || {
let tag_ids: Vec<String> = tag_ctx
.selected_tags
.read()
.iter()
.map(|t| t.id().to_string())
.collect();
let select_values = select_ctx.current_values_peek();
for val in &select_values {
if !tag_ids.contains(val) {
select_ctx.toggle_value(val);
}
}
});
rsx! { {children} }
}
#[derive(Props, Clone, PartialEq)]
pub struct InputProps<T: TagLike + 'static> {
#[props(default = "Type to search\u{2026}".to_string())]
pub placeholder: String,
#[props(extends = GlobalAttributes)]
pub attributes: Vec<Attribute>,
#[props(default)]
_phantom: std::marker::PhantomData<T>,
}
fn toggle_highlighted_tag<T: TagLike>(
select_ctx: &mut SelectContext,
tag_ctx: &mut TagInputState<T>,
available: &[T],
) {
let highlighted = select_ctx.highlighted_value();
if let Some(val) = highlighted
&& let Some(tag) = available.iter().find(|t| t.id() == val.as_str())
{
if select_ctx.is_selected(&val) {
tag_ctx.remove_tag(&val);
select_ctx.toggle_value(&val);
} else {
tag_ctx.add_tag(tag.clone());
select_ctx.toggle_value(&val);
}
}
}
#[allow(non_snake_case)]
pub fn Input<T: TagLike>(props: InputProps<T>) -> Element {
let mut tag_ctx = use_context::<TagInputState<T>>();
let mut select_ctx = use_context::<SelectContext>();
let combo_available = use_context::<ComboAvailable<T>>();
use_hook(|| {
select_ctx.mark_has_input();
});
let listbox_id = select_ctx.listbox_id();
let input_id = select_ctx.input_id();
rsx! {
input {
r#type: "text",
id: "{input_id}",
role: "combobox",
disabled: *tag_ctx.is_disabled.read(),
readonly: *tag_ctx.is_readonly.read(),
placeholder: "{props.placeholder}",
value: "{tag_ctx.search_query}",
autocomplete: "off",
aria_autocomplete: select_ctx.autocomplete().as_aria_attr(),
aria_expanded: select_ctx.is_open(),
aria_controls: "{listbox_id}",
aria_activedescendant: select_ctx.active_descendant(),
"data-slot": "input",
"data-select-input": "true",
"data-disabled": *tag_ctx.is_disabled.read(),
"data-readonly": *tag_ctx.is_readonly.read(),
"data-placeholder-shown": tag_ctx.search_query.read().is_empty(),
oninput: move |evt: Event<FormData>| {
let val = evt.value();
tag_ctx.set_query(val.clone());
select_ctx.set_search_query(val);
if !select_ctx.is_open() {
select_ctx.set_open(true);
}
select_ctx.highlight_first();
},
onkeydown: move |evt: Event<KeyboardData>| {
let key = evt.key();
match key {
Key::ArrowDown => {
evt.prevent_default();
if !select_ctx.is_open() {
select_ctx.set_open(true);
select_ctx.highlight_first();
} else {
select_ctx.highlight_next();
}
}
Key::ArrowUp => {
if select_ctx.is_open() {
evt.prevent_default();
select_ctx.highlight_prev();
}
}
Key::Enter => {
evt.prevent_default();
if select_ctx.is_open()
&& select_ctx.has_highlighted()
{
let available = combo_available.0.read();
toggle_highlighted_tag(&mut select_ctx, &mut tag_ctx, &available);
tag_ctx.set_query(String::new());
select_ctx.set_search_query(String::new());
if try_use_context::<ComboConfig>().is_none_or(|c| c.close_on_select) {
select_ctx.set_open(false);
}
} else {
tag_ctx.handle_input_keydown(evt);
}
}
Key::Escape => {
evt.prevent_default();
if select_ctx.is_open() {
select_ctx.set_open(false);
}
tag_ctx.active_pill.set(None);
}
Key::Tab => {
if select_ctx.is_open() {
select_ctx.set_open(false);
}
}
Key::ArrowLeft | Key::Backspace => {
tag_ctx.handle_input_keydown(evt);
}
_ => {
tag_ctx.handle_input_keydown(evt);
}
}
},
onfocus: move |_| {
if select_ctx.open_on_focus() {
select_ctx.set_open(true);
select_ctx.highlight_first();
}
},
onblur: move |_| {
select_ctx.set_open(false);
},
onclick: move |_| {
tag_ctx.handle_click();
if !select_ctx.is_open() {
select_ctx.set_open(true);
}
},
onpaste: move |evt: Event<ClipboardData>| {
if let Some(text) = extract_clipboard_text(&evt) {
evt.prevent_default();
tag_ctx.handle_paste(text);
}
},
..props.attributes,
}
}
}
pub use tag_input::Control;
pub use tag_input::Count;
pub use tag_input::FormValue;
pub use tag_input::LiveRegion;
pub use tag_input::Tag;
pub use tag_input::TagList;
pub use tag_input::TagRemove;
pub use select::Content as Dropdown;
pub use select::Empty;
pub use select::Group as DropdownGroup;
pub use select::Label;
#[allow(non_snake_case)]
pub fn Item(props: ItemProps) -> Element {
let effective_disabled = if let Some(combo_disabled) = try_use_context::<ComboDisabledValues>()
{
props.disabled || combo_disabled.0.read().contains(&props.value)
} else {
props.disabled
};
select::Item(dioxus_nox_select::select::ItemProps {
attributes: props.attributes,
value: props.value,
label: props.label,
keywords: props.keywords,
disabled: effective_disabled,
children: props.children,
})
}
#[derive(Props, Clone, PartialEq)]
pub struct ItemProps {
#[props(extends = GlobalAttributes)]
pub attributes: Vec<Attribute>,
pub value: String,
#[props(default)]
pub label: Option<String>,
#[props(default)]
pub keywords: Option<String>,
#[props(default)]
pub disabled: bool,
pub children: Element,
}