diff --git a/Cargo.toml b/Cargo.toml index 380d248..b9640f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ icondata = { version = "0.1.0", features = [ "AiPlusOutlined", "AiMinusOutlined", "AiRightOutlined", + "AiClockCircleOutlined", ] } icondata_core = "0.0.2" uuid = { version = "1.5.0", features = ["v4"] } diff --git a/demo/src/app.rs b/demo/src/app.rs index 3f8427f..81ad58e 100644 --- a/demo/src/app.rs +++ b/demo/src/app.rs @@ -77,6 +77,7 @@ fn TheRouter(is_routing: RwSignal) -> impl IntoView { + diff --git a/demo/src/pages/components.rs b/demo/src/pages/components.rs index d6d788d..6c92faf 100644 --- a/demo/src/pages/components.rs +++ b/demo/src/pages/components.rs @@ -150,6 +150,10 @@ pub(crate) fn gen_menu_data() -> Vec { value: "switch".into(), label: "Switch".into(), }, + MenuItemOption { + value: "time-picker".into(), + label: "Time Picker".into(), + }, MenuItemOption { value: "upload".into(), label: "Upload".into(), diff --git a/demo/src/pages/mod.rs b/demo/src/pages/mod.rs index 9d3c425..27aea7f 100644 --- a/demo/src/pages/mod.rs +++ b/demo/src/pages/mod.rs @@ -36,6 +36,7 @@ mod table; mod tabs; mod tag; mod theme; +mod time_picker; mod toast; mod typography; mod upload; @@ -78,6 +79,7 @@ pub use table::*; pub use tabs::*; pub use tag::*; pub use theme::*; +pub use time_picker::*; pub use toast::*; pub use typography::*; pub use upload::*; diff --git a/demo/src/pages/time_picker/mod.rs b/demo/src/pages/time_picker/mod.rs new file mode 100644 index 0000000..dfe3b87 --- /dev/null +++ b/demo/src/pages/time_picker/mod.rs @@ -0,0 +1,52 @@ +use crate::components::{Demo, DemoCode}; +use leptos::*; +use prisms::highlight_str; +use thaw::chrono::prelude::*; +use thaw::*; + +#[component] +pub fn TimePickerPage() -> impl IntoView { + let value = create_rw_signal(Some(Local::now().time())); + view! { +
+

"Time Picker"

+ + + + + {highlight_str!( + r#" + use thaw::chrono::prelude::*; + + let value = create_rw_signal(Local::now().time()); + view! { + + } + "#, + "rust" + )} + + + +

"TimePicker Props"

+ + + + + + + + + + + + + + + + + +
"Name""Type""Default""Description"
"value""RwSignal
+
+ } +} diff --git a/src/lib.rs b/src/lib.rs index 2993690..783010e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,7 @@ mod table; mod tabs; mod tag; mod theme; +mod time_picker; mod typography; mod upload; mod utils; @@ -73,6 +74,7 @@ pub use table::*; pub use tabs::*; pub use tag::*; pub use theme::*; +pub use time_picker::*; pub use typography::*; pub use upload::*; pub use utils::SignalWatch; diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 8e107d7..058dd6f 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -5,7 +5,8 @@ use crate::{ mobile::{NavBarTheme, TabbarTheme}, AlertTheme, AutoCompleteTheme, AvatarTheme, BreadcrumbTheme, ButtonTheme, CalendarTheme, ColorPickerTheme, InputTheme, MenuTheme, MessageTheme, ProgressTheme, SelectTheme, - SkeletionTheme, SliderTheme, SwitchTheme, TableTheme, TagTheme, TypographyTheme, UploadTheme, + SkeletionTheme, SliderTheme, SwitchTheme, TableTheme, TagTheme, TimePickerTheme, + TypographyTheme, UploadTheme, }; use leptos::*; @@ -39,6 +40,7 @@ pub struct Theme { pub progress: ProgressTheme, pub typograph: TypographyTheme, pub calendar: CalendarTheme, + pub time_picker: TimePickerTheme, } impl Theme { @@ -67,6 +69,7 @@ impl Theme { progress: ProgressTheme::light(), typograph: TypographyTheme::light(), calendar: CalendarTheme::light(), + time_picker: TimePickerTheme::light(), } } pub fn dark() -> Self { @@ -94,6 +97,7 @@ impl Theme { progress: ProgressTheme::dark(), typograph: TypographyTheme::dark(), calendar: CalendarTheme::dark(), + time_picker: TimePickerTheme::dark(), } } } diff --git a/src/time_picker/mod.rs b/src/time_picker/mod.rs new file mode 100644 index 0000000..68a655f --- /dev/null +++ b/src/time_picker/mod.rs @@ -0,0 +1,332 @@ +mod theme; + +use crate::{ + chrono::{Local, NaiveTime, Timelike}, + components::{Binder, Follower, FollowerPlacement}, + use_theme, + utils::{mount_style, ComponentRef}, + AiIcon, Button, ButtonSize, ButtonVariant, Icon, Input, InputSuffix, SignalWatch, Theme, +}; +use leptos::*; +pub use theme::TimePickerTheme; + +#[component] +pub fn TimePicker(#[prop(optional, into)] value: RwSignal>) -> impl IntoView { + mount_style("time-picker", include_str!("./time-picker.css")); + let time_picker_ref = create_node_ref::(); + let panel_ref = ComponentRef::::default(); + let is_show_panel = create_rw_signal(false); + let show_time_format = "%H:%M:%S"; + let show_time_text = create_rw_signal(String::new()); + let update_show_time_text = move || { + value.with_untracked(move |time| { + let text = time.as_ref().map_or(String::new(), |time| { + time.format(show_time_format).to_string() + }); + show_time_text.set(text); + }); + }; + update_show_time_text(); + let panel_selected_time = create_rw_signal(None::); + _ = panel_selected_time.watch(move |time| { + let text = time.as_ref().map_or(String::new(), |time| { + time.format(show_time_format).to_string() + }); + show_time_text.set(text); + }); + + let on_input_blur = Callback::new(move |_| { + if let Ok(time) = + NaiveTime::parse_from_str(&show_time_text.get_untracked(), show_time_format) + { + if value.get_untracked() != Some(time) { + value.set(Some(time)); + update_show_time_text(); + } + } else { + update_show_time_text(); + } + }); + let close_panel = Callback::new(move |time: Option| { + if value.get_untracked() != time { + if time.is_some() { + value.set(time); + } + update_show_time_text(); + } + is_show_panel.set(false); + }); + + let open_panel = Callback::new(move |_| { + panel_selected_time.set(value.get_untracked()); + is_show_panel.set(true); + request_animation_frame(move || { + if let Some(panel_ref) = panel_ref.get_untracked() { + panel_ref.scroll_into_view(); + } + }); + }); + + view! { + +
+ + + + + +
+ + + +
+ } +} + +#[component] +fn Panel( + selected_time: RwSignal>, + time_picker_ref: NodeRef, + close_panel: Callback>, + comp_ref: ComponentRef, +) -> impl IntoView { + let theme = use_theme(Theme::light); + let css_vars = create_memo(move |_| { + let mut css_vars = String::new(); + theme.with(|theme| { + css_vars.push_str(&format!( + "--thaw-item-font-color: {};", + theme.common.color_primary + )); + css_vars.push_str(&format!( + "--thaw-background-color: {};", + theme.time_picker.panel_background_color + )); + css_vars.push_str(&format!( + "--thaw-item-background-color-hover: {};", + theme.time_picker.panel_time_item_background_color_hover + )); + css_vars.push_str(&format!( + "--thaw-item-border-color: {};", + theme.time_picker.panel_border_color + )); + }); + css_vars + }); + let now = move |_| { + close_panel.call(Some(now_time())); + }; + let ok = move |_| { + close_panel.call(selected_time.get_untracked()); + }; + + let panel_ref = create_node_ref::(); + #[cfg(any(feature = "csr", feature = "hydrate"))] + { + use leptos::wasm_bindgen::__rt::IntoJsResult; + let handle = window_event_listener(ev::click, move |ev| { + let el = ev.target(); + let mut el: Option = + el.into_js_result().map_or(None, |el| Some(el.into())); + let body = document().body().unwrap(); + while let Some(current_el) = el { + if current_el == *body { + break; + }; + if panel_ref.get().is_none() { + return; + } + if current_el == ***panel_ref.get_untracked().unwrap() + || current_el == ***time_picker_ref.get_untracked().unwrap() + { + return; + } + el = current_el.parent_element(); + } + close_panel.call(None); + }); + on_cleanup(move || handle.remove()); + } + #[cfg(not(any(feature = "csr", feature = "hydrate")))] + { + _ = time_picker_ref; + } + + let hour_ref = create_node_ref::(); + let minute_ref = create_node_ref::(); + let second_ref = create_node_ref::(); + comp_ref.load(PanelRef { + hour_ref, + minute_ref, + second_ref, + }); + + view! { +
+
+
+ + {(0..24) + .into_iter() + .map(|hour| { + let comp_ref = ComponentRef::::default(); + let on_click = move |_| { + selected_time + .update(move |time| { + *time = if let Some(time) = time { + time.with_hour(hour) + } else { + NaiveTime::from_hms_opt(hour, 0, 0) + } + }); + comp_ref.get_untracked().unwrap().scroll_into_view(); + }; + let is_selected = Memo::new(move |_| { + selected_time.get().map_or(false, |v| v.hour() == hour) + }); + view! { + + } + }) + .collect_view()}
+
+
+ + {(0..60) + .into_iter() + .map(|minute| { + let comp_ref = ComponentRef::::default(); + let on_click = move |_| { + selected_time + .update(move |time| { + *time = if let Some(time) = time { + time.with_minute(minute) + } else { + NaiveTime::from_hms_opt(now_time().hour(), minute, 0) + } + }); + comp_ref.get_untracked().unwrap().scroll_into_view(); + }; + let is_selected = Memo::new(move |_| { + selected_time.get().map_or(false, |v| v.minute() == minute) + }); + view! { + + } + }) + .collect_view()}
+
+
+ + {(0..60) + .into_iter() + .map(|second| { + let comp_ref = ComponentRef::::default(); + let on_click = move |_| { + selected_time + .update(move |time| { + *time = if let Some(time) = time { + time.with_second(second) + } else { + now_time().with_second(second) + } + }); + comp_ref.get_untracked().unwrap().scroll_into_view(); + }; + let is_selected = Memo::new(move |_| { + selected_time.get().map_or(false, |v| v.second() == second) + }); + view! { + + } + }) + .collect_view()}
+
+
+ +
+ } +} + +#[derive(Clone)] +struct PanelRef { + hour_ref: NodeRef, + minute_ref: NodeRef, + second_ref: NodeRef, +} + +impl PanelRef { + fn scroll_top(el: HtmlElement) { + if let Ok(Some(slected_el)) = + el.query_selector(".thaw-time-picker-panel__time-item--slected") + { + use wasm_bindgen::JsCast; + if let Ok(slected_el) = slected_el.dyn_into::() { + el.set_scroll_top(slected_el.offset_top()); + } + } + } + + fn scroll_into_view(&self) { + if let Some(hour_el) = self.hour_ref.get_untracked() { + Self::scroll_top(hour_el); + } + if let Some(minute_el) = self.minute_ref.get_untracked() { + Self::scroll_top(minute_el); + } + if let Some(second_el) = self.second_ref.get_untracked() { + Self::scroll_top(second_el); + } + } +} + +#[component] +fn PanelTimeItem( + value: u32, + is_selected: Memo, + comp_ref: ComponentRef, +) -> impl IntoView { + let item_ref = create_node_ref(); + item_ref.on_load(move |_| { + let item_ref = PanelTimeItemRef { item_ref }; + comp_ref.load(item_ref); + }); + view! { +
+ + {format!("{value:02}")} + +
+ } +} + +#[derive(Clone)] +struct PanelTimeItemRef { + item_ref: NodeRef, +} + +impl PanelTimeItemRef { + fn scroll_into_view(&self) { + if let Some(item_ref) = self.item_ref.get_untracked() { + item_ref.scroll_into_view_with_bool(true); + } + } +} + +fn now_time() -> NaiveTime { + Local::now().time() +} diff --git a/src/time_picker/theme.rs b/src/time_picker/theme.rs new file mode 100644 index 0000000..12cc7ac --- /dev/null +++ b/src/time_picker/theme.rs @@ -0,0 +1,26 @@ +use crate::theme::ThemeMethod; + +#[derive(Clone)] +pub struct TimePickerTheme { + pub panel_background_color: String, + pub panel_time_item_background_color_hover: String, + pub panel_border_color: String, +} + +impl ThemeMethod for TimePickerTheme { + fn light() -> Self { + Self { + panel_background_color: "#fff".into(), + panel_time_item_background_color_hover: "#f1f3f5".into(), + panel_border_color: "#e0e0e6".into(), + } + } + + fn dark() -> Self { + Self { + panel_background_color: "#48484e".into(), + panel_time_item_background_color_hover: "#ffffff1a".into(), + panel_border_color: "#ffffff3d".into(), + } + } +} diff --git a/src/time_picker/time-picker.css b/src/time_picker/time-picker.css new file mode 100644 index 0000000..7c22948 --- /dev/null +++ b/src/time_picker/time-picker.css @@ -0,0 +1,53 @@ +.thaw-time-picker-panel { + width: 160px; + height: 260px; + background-color: var(--thaw-background-color); + border-radius: 3px; + box-sizing: border-box; + box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), + 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); +} + +.thaw-time-picker-panel__time { + display: flex; + height: calc(100% - 40px); +} + +.thaw-time-picker-panel__time-hour, +.thaw-time-picker-panel__time-minute, +.thaw-time-picker-panel__time-second { + flex: 1; + overflow-y: auto; +} + +.thaw-time-picker-panel__time-item { + height: 34px; + margin: 2px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 3px; +} + +.thaw-time-picker-panel__time-padding { + height: 184px; +} + +.thaw-time-picker-panel__time-item--slected, +.thaw-time-picker-panel__time-item:hover { + background-color: var(--thaw-item-background-color-hover); +} + +.thaw-time-picker-panel__time-item--slected { + color: var(--thaw-item-font-color); +} + +.thaw-time-picker-panel__footer { + display: flex; + padding: 0 2px 2px; + height: 38px; + justify-content: space-evenly; + align-items: center; + border-top: 1px solid var(--thaw-item-border-color); +}