use super::*;
const CARET_GLYPH: &str = "\u{25be}";
const NOW_LABEL: &str = "Now";
const CLEAR_LABEL: &str = "Clear";
const TIME_LABELS: [&str; 60] = [
"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15",
"16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31",
"32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47",
"48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59",
];
pub fn parse_time(text: &str) -> Option<(u32, u32)> {
let trimmed: &str = text.trim();
if trimmed.is_empty() {
return None;
}
let mut parts = trimmed.split(':');
let hour_text: &str = parts.next()?;
let minute_text: &str = parts.next()?;
if parts.next().is_some() {
return None;
}
let hour: u32 = parse_clock_part(hour_text)?;
let minute: u32 = parse_clock_part(minute_text)?;
if hour > 23 || minute > 59 {
return None;
}
Some((hour, minute))
}
fn parse_clock_part(text: &str) -> Option<u32> {
if text.is_empty() || text.len() > 2 {
return None;
}
if !text.bytes().all(|byte: u8| byte.is_ascii_digit()) {
return None;
}
text.parse::<u32>().ok()
}
pub fn format_time(hour: u32, minute: u32) -> String {
format!("{hour:02}:{minute:02}")
}
pub fn clamp_hour_range(min_hour: u32, max_hour: u32) -> (u32, u32) {
let low: u32 = min_hour.min(23);
let high: u32 = max_hour.min(23);
if low <= high {
return (low, high);
}
(high, low)
}
pub fn hour_values(min_hour: u32, max_hour: u32) -> Vec<u32> {
let (low, high) = clamp_hour_range(min_hour, max_hour);
(low..=high).collect()
}
pub fn minute_values(minute_step: u32) -> Vec<u32> {
let step: u32 = minute_step.max(1);
let mut minutes: Vec<u32> = Vec::with_capacity((60 / step) as usize + 1);
let mut minute: u32 = 0;
while minute < 60 {
minutes.push(minute);
minute = minute.saturating_add(step);
}
minutes
}
pub fn time_with_hour(current: &str, hour: u32) -> String {
let minute: u32 = parse_time(current)
.map(|(_, minute): (u32, u32)| minute)
.unwrap_or(0);
format_time(hour, minute)
}
pub fn time_with_minute(current: &str, minute: u32, fallback_hour: u32) -> String {
let hour: u32 = parse_time(current)
.map(|(hour, _): (u32, u32)| hour)
.unwrap_or(fallback_hour);
format_time(hour, minute)
}
pub fn current_time() -> Option<(u32, u32)> {
#[cfg(target_arch = "wasm32")]
{
let now: js_sys::Date = js_sys::Date::new_0();
Some((now.get_hours(), now.get_minutes()))
}
#[cfg(not(target_arch = "wasm32"))]
{
None
}
}
pub fn time_picker_toggle_handler<E: 'static>(open: Signal<bool>) -> Option<Rc<dyn Fn(E)>> {
Some(Rc::new(move |_event: E| {
let is_open: bool = open.get();
open.set(!is_open);
}))
}
pub fn time_picker_pick_hour_handler<E: 'static>(
value: Signal<String>,
open: Signal<bool>,
on_change: Option<Rc<dyn Fn(&'static str)>>,
hour: u32,
) -> Option<Rc<dyn Fn(E)>> {
Some(Rc::new(move |_event: E| {
let current: String = value.get();
let formatted: String = time_with_hour(¤t, hour);
let leaked: &'static str = Box::leak(formatted.clone().into_boxed_str());
value.set(formatted);
open.set(false);
if let Some(callback) = &on_change {
callback(leaked);
}
}))
}
pub fn time_picker_pick_minute_handler<E: 'static>(
value: Signal<String>,
open: Signal<bool>,
on_change: Option<Rc<dyn Fn(&'static str)>>,
minute: u32,
fallback_hour: u32,
) -> Option<Rc<dyn Fn(E)>> {
Some(Rc::new(move |_event: E| {
let current: String = value.get();
let formatted: String = time_with_minute(¤t, minute, fallback_hour);
let leaked: &'static str = Box::leak(formatted.clone().into_boxed_str());
value.set(formatted);
open.set(false);
if let Some(callback) = &on_change {
callback(leaked);
}
}))
}
pub fn time_picker_now_handler<E: 'static>(
value: Signal<String>,
open: Signal<bool>,
on_change: Option<Rc<dyn Fn(&'static str)>>,
) -> Rc<dyn Fn(E)> {
Rc::new(move |_event: E| {
let Some((hour, minute)) = current_time() else {
return;
};
let formatted: String = format_time(hour, minute);
let leaked: &'static str = Box::leak(formatted.clone().into_boxed_str());
value.set(formatted);
open.set(false);
if let Some(callback) = &on_change {
callback(leaked);
}
})
}
pub fn time_picker_clear_handler<E: 'static>(
value: Signal<String>,
open: Signal<bool>,
on_change: Option<Rc<dyn Fn(&'static str)>>,
) -> Rc<dyn Fn(E)> {
Rc::new(move |_event: E| {
value.set(String::new());
open.set(false);
if let Some(callback) = &on_change {
callback("");
}
})
}
fn time_picker_cell(
label: &'static str,
is_active: bool,
handler: Option<Rc<dyn Fn(Event)>>,
) -> VirtualNode {
let cell_text: VirtualNode = VirtualNode::Text(TextNode::new(Cow::Borrowed(label), None));
html! {
button {
type: "button"
class: c_euv_time_picker_cell()
class: if is_active {
c_euv_time_picker_cell_active()
} else {
c_euv_time_picker_cell()
}
onclick: handler
cell_text
}
}
}
#[component]
pub fn euv_time_picker(node: VirtualNode<EuvTimePickerProps>) -> VirtualNode {
let EuvTimePickerProps {
value,
placeholder,
label: label_text,
min_hour,
max_hour,
minute_step,
on_change,
}: EuvTimePickerProps = node.try_get_props().unwrap_or_default();
let open: Signal<bool> = HookContext::use_hook(|| Signal::create(false));
let selected_value: String = value.get();
let has_value: bool = !selected_value.is_empty();
let selected: Option<(u32, u32)> = parse_time(&selected_value);
let trigger_text: String = if has_value {
selected_value
} else {
String::from(placeholder)
};
let first_hour: u32 = clamp_hour_range(min_hour, max_hour).0;
let hours: Vec<u32> = hour_values(min_hour, max_hour);
let minutes: Vec<u32> = minute_values(minute_step);
let selected_hour: Option<u32> = selected.map(|(hour, _): (u32, u32)| hour);
let selected_minute: Option<u32> = selected.map(|(_, minute): (u32, u32)| minute);
let hour_cells: Vec<VirtualNode> = hours
.iter()
.map(|hour: &u32| {
time_picker_cell(
TIME_LABELS[*hour as usize],
selected_hour == Some(*hour),
time_picker_pick_hour_handler(value, open, on_change.clone(), *hour),
)
})
.collect();
let minute_cells: Vec<VirtualNode> = minutes
.iter()
.map(|minute: &u32| {
time_picker_cell(
TIME_LABELS[*minute as usize],
selected_minute == Some(*minute),
time_picker_pick_minute_handler(
value,
open,
on_change.clone(),
*minute,
first_hour,
),
)
})
.collect();
html! {
div {
class: c_euv_time_picker()
if !label_text.is_empty() {
label {
class: c_form_label()
label_text
}
}
button {
type: "button"
class: c_euv_time_picker_trigger()
onclick: time_picker_toggle_handler(open)
span {
class: if has_value {
c_euv_time_picker_value()
} else {
c_euv_time_picker_placeholder()
}
trigger_text
}
span {
class: c_euv_time_picker_caret()
CARET_GLYPH
}
}
div {
class: c_euv_time_picker_panel()
class: if { open } {
c_euv_time_picker_panel_open()
} else {
c_euv_time_picker_panel_closed()
}
div {
class: c_euv_time_picker_grid()
div {
class: c_euv_time_picker_column()
hour_cells
}
div {
class: c_euv_time_picker_column()
minute_cells
}
}
div {
class: c_euv_time_picker_footer()
euv_button {
variant: EuvButtonVariant::Outline
label: NOW_LABEL
onclick: Some(time_picker_now_handler(value, open, on_change.clone()))
}
euv_button {
variant: EuvButtonVariant::Outline
label: CLEAR_LABEL
onclick: Some(time_picker_clear_handler(value, open, on_change.clone()))
}
}
}
}
}
}