mirror of
https://github.com/adoyle0/leptos-use.git
synced 2025-02-02 19:04:15 -05:00
ec6027c59e
# Conflicts: # CHANGELOG.md # Cargo.toml # src/is_err.rs # src/is_none.rs # src/is_ok.rs # src/is_some.rs # src/storage/use_local_storage.rs # src/storage/use_session_storage.rs # src/storage/use_storage.rs # src/use_broadcast_channel.rs # src/use_cookie.rs # src/use_device_pixel_ratio.rs # src/use_event_source.rs # src/use_to_string.rs # src/use_websocket.rs # src/utils/codecs/string/from_to_string.rs # src/utils/codecs/string/json.rs # src/utils/codecs/string/prost.rs
522 lines
17 KiB
Rust
522 lines
17 KiB
Rust
use crate::core::url;
|
|
use crate::core::StorageType;
|
|
use crate::core::{ElementMaybeSignal, MaybeRwSignal};
|
|
use crate::storage::{use_storage_with_options, UseStorageOptions};
|
|
use crate::{sync_signal_with_options, use_cookie, use_preferred_dark, SyncSignalOptions};
|
|
use codee::string::FromToStringCodec;
|
|
use default_struct_builder::DefaultBuilder;
|
|
use leptos::prelude::wrappers::read::Signal;
|
|
use leptos::prelude::*;
|
|
use std::fmt::{Display, Formatter};
|
|
use std::marker::PhantomData;
|
|
use std::rc::Rc;
|
|
use std::str::FromStr;
|
|
use wasm_bindgen::JsCast;
|
|
|
|
/// Reactive color mode (dark / light / customs) with auto data persistence.
|
|
///
|
|
/// ## Demo
|
|
///
|
|
/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_color_mode)
|
|
///
|
|
/// ## Usage
|
|
///
|
|
/// ```
|
|
/// # use leptos::prelude::*;
|
|
/// # use leptos_use::{use_color_mode, UseColorModeReturn};
|
|
/// #
|
|
/// # #[component]
|
|
/// # fn Demo() -> impl IntoView {
|
|
/// let UseColorModeReturn {
|
|
/// mode, // Signal<ColorMode::dark | ColorMode::light>
|
|
/// set_mode,
|
|
/// ..
|
|
/// } = use_color_mode();
|
|
/// #
|
|
/// # view! { }
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// By default, it will match with users' browser preference using [`use_preferred_dark`] (a.k.a. `ColorMode::Auto`).
|
|
/// When reading the signal, it will by default return the current color mode (`ColorMode::Dark`, `ColorMode::Light` or
|
|
/// your custom modes `ColorMode::Custom("some-custom")`). The `ColorMode::Auto` variant can
|
|
/// be included in the returned modes by enabling the `emit_auto` option and using [`use_color_mode_with_options`].
|
|
/// When writing to the signal (`set_mode`), it will trigger DOM updates and persist the color mode to local
|
|
/// storage (or your custom storage). You can pass `ColorMode::Auto` to set back to auto mode.
|
|
///
|
|
/// ```
|
|
/// # use leptos::prelude::*;
|
|
/// # use leptos_use::{ColorMode, use_color_mode, UseColorModeReturn};
|
|
/// #
|
|
/// # #[component]
|
|
/// # fn Demo() -> impl IntoView {
|
|
/// # let UseColorModeReturn { mode, set_mode, .. } = use_color_mode();
|
|
/// #
|
|
/// mode.get(); // ColorMode::Dark or ColorMode::Light
|
|
///
|
|
/// set_mode.set(ColorMode::Dark); // change to dark mode and persist
|
|
///
|
|
/// set_mode.set(ColorMode::Auto); // change to auto mode
|
|
/// #
|
|
/// # view! { }
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// ## Options
|
|
///
|
|
/// ```
|
|
/// # use leptos::prelude::*;
|
|
/// # use leptos_use::{use_color_mode_with_options, UseColorModeOptions, UseColorModeReturn};
|
|
/// #
|
|
/// # #[component]
|
|
/// # fn Demo() -> impl IntoView {
|
|
/// let UseColorModeReturn { mode, set_mode, .. } = use_color_mode_with_options(
|
|
/// UseColorModeOptions::default()
|
|
/// .attribute("theme") // instead of writing to `class`
|
|
/// .custom_modes(vec![
|
|
/// // custom colors in addition to light/dark
|
|
/// "dim".to_string(),
|
|
/// "cafe".to_string(),
|
|
/// ]),
|
|
/// ); // Signal<ColorMode::Dark | ColorMode::Light | ColorMode::Custom("dim") | ColorMode::Custom("cafe")>
|
|
/// #
|
|
/// # view! { }
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// ### Cookies
|
|
///
|
|
/// To persist color mode in a cookie, use `use_cookie_with_options` and specify `.cookie_enabled(true)`.
|
|
///
|
|
/// > Note: To work with SSR you have to add the `axum` or `actix` feature as described in [`use_cookie`].
|
|
///
|
|
/// ```rust
|
|
/// # use leptos::prelude::*;
|
|
/// # use leptos_meta::*;
|
|
/// # use leptos_use::{use_color_mode_with_options, UseColorModeOptions, UseColorModeReturn};
|
|
/// #
|
|
/// # #[component]
|
|
/// # fn Demo() -> impl IntoView {
|
|
/// let UseColorModeReturn { mode, set_mode, .. } = use_color_mode_with_options(
|
|
/// UseColorModeOptions::default()
|
|
/// .cookie_enabled(true),
|
|
/// );
|
|
///
|
|
/// // This adds the color mode class to the `<html>` element even with SSR
|
|
/// view! {
|
|
/// <Html class=move || mode.get().to_string()/>
|
|
/// }
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// For a working example please check out the [ssr example](https://github.com/Synphonyte/leptos-use/blob/main/examples/ssr/src/app.rs).
|
|
///
|
|
/// ## Server-Side Rendering
|
|
///
|
|
/// On the server this will by default return `ColorMode::Light`. Persistence with storage is disabled.
|
|
///
|
|
/// If `cookie_enabled` is set to `true`, cookies will be used and if present this value will be used
|
|
/// on the server as well as on the client. Please note that you have to add the `axum` or `actix`
|
|
/// feature as described in [`use_cookie`].
|
|
///
|
|
/// ## See also
|
|
///
|
|
/// * [`use_preferred_dark`]
|
|
/// * [`use_storage`]
|
|
/// * [`use_cookie`]
|
|
pub fn use_color_mode() -> UseColorModeReturn {
|
|
use_color_mode_with_options(UseColorModeOptions::default())
|
|
}
|
|
|
|
/// Version of [`use_color_mode`] that takes a `UseColorModeOptions`. See [`use_color_mode`] for how to use.
|
|
pub fn use_color_mode_with_options<El, T>(options: UseColorModeOptions<El, T>) -> UseColorModeReturn
|
|
where
|
|
El: Clone,
|
|
El: Into<ElementMaybeSignal<T, web_sys::Element>>,
|
|
T: Into<web_sys::Element> + Clone + 'static,
|
|
{
|
|
let UseColorModeOptions {
|
|
target,
|
|
attribute,
|
|
initial_value,
|
|
initial_value_from_url_param,
|
|
initial_value_from_url_param_to_storage,
|
|
on_changed,
|
|
storage_signal,
|
|
custom_modes,
|
|
storage_key,
|
|
storage,
|
|
storage_enabled,
|
|
cookie_name,
|
|
cookie_enabled,
|
|
emit_auto,
|
|
transition_enabled,
|
|
listen_to_storage_changes,
|
|
_marker,
|
|
} = options;
|
|
|
|
let modes: Vec<String> = custom_modes
|
|
.into_iter()
|
|
.chain(vec![
|
|
ColorMode::Dark.to_string(),
|
|
ColorMode::Light.to_string(),
|
|
])
|
|
.collect();
|
|
|
|
let preferred_dark = use_preferred_dark();
|
|
|
|
let system = Signal::derive(move || {
|
|
if preferred_dark.get() {
|
|
ColorMode::Dark
|
|
} else {
|
|
ColorMode::Light
|
|
}
|
|
});
|
|
|
|
let mut initial_value_from_url = None;
|
|
if let Some(param) = initial_value_from_url_param.as_ref() {
|
|
if let Some(value) = url::params::get(param) {
|
|
initial_value_from_url = ColorMode::from_str(&value).map(MaybeRwSignal::Static).ok()
|
|
}
|
|
}
|
|
|
|
let (store, set_store) = get_store_signal(
|
|
initial_value_from_url.clone().unwrap_or(initial_value),
|
|
storage_signal,
|
|
&storage_key,
|
|
storage_enabled,
|
|
storage,
|
|
listen_to_storage_changes,
|
|
);
|
|
|
|
let (cookie, set_cookie) = get_cookie_signal(&cookie_name, cookie_enabled);
|
|
|
|
if cookie_enabled {
|
|
let _ = sync_signal_with_options(
|
|
(cookie, set_cookie),
|
|
(store, set_store),
|
|
SyncSignalOptions::with_transforms(
|
|
move |cookie: &Option<ColorMode>| {
|
|
cookie.clone().unwrap_or_else(|| store.get_untracked())
|
|
},
|
|
move |store: &ColorMode| Some(store.clone()),
|
|
),
|
|
);
|
|
}
|
|
|
|
if let Some(initial_value_from_url) = initial_value_from_url {
|
|
let value = initial_value_from_url.into_signal().0.get_untracked();
|
|
if initial_value_from_url_param_to_storage {
|
|
set_store.set(value);
|
|
} else {
|
|
set_store.set_untracked(value);
|
|
}
|
|
}
|
|
|
|
let state = Signal::derive(move || {
|
|
let value = store.get();
|
|
if value == ColorMode::Auto {
|
|
system.get()
|
|
} else {
|
|
value
|
|
}
|
|
});
|
|
|
|
let target = target.into();
|
|
|
|
let update_html_attrs = {
|
|
move |target: ElementMaybeSignal<T, web_sys::Element>,
|
|
attribute: String,
|
|
value: ColorMode| {
|
|
let el = target.get_untracked();
|
|
|
|
if let Some(el) = el {
|
|
let el = el.into();
|
|
|
|
let mut style: Option<web_sys::HtmlStyleElement> = None;
|
|
if !transition_enabled {
|
|
if let Ok(styl) = document().create_element("style") {
|
|
if let Some(head) = document().head() {
|
|
let styl: web_sys::HtmlStyleElement = styl.unchecked_into();
|
|
let style_string = "*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}";
|
|
styl.set_text_content(Some(style_string));
|
|
let _ = head.append_child(&styl);
|
|
style = Some(styl);
|
|
}
|
|
}
|
|
}
|
|
|
|
if attribute == "class" {
|
|
for mode in &modes {
|
|
if &value.to_string() == mode {
|
|
let _ = el.class_list().add_1(mode);
|
|
} else {
|
|
let _ = el.class_list().remove_1(mode);
|
|
}
|
|
}
|
|
} else {
|
|
let _ = el.set_attribute(&attribute, &value.to_string());
|
|
}
|
|
|
|
if !transition_enabled {
|
|
if let Some(style) = style {
|
|
if let Some(head) = document().head() {
|
|
// Calling getComputedStyle forces the browser to redraw
|
|
if let Ok(Some(style)) = window().get_computed_style(&style) {
|
|
let _ = style.get_property_value("opacity");
|
|
}
|
|
|
|
let _ = head.remove_child(&style);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
let default_on_changed = move |mode: ColorMode| {
|
|
update_html_attrs(target.clone(), attribute.clone(), mode);
|
|
};
|
|
|
|
let on_changed = move |mode: ColorMode| {
|
|
on_changed(mode, Rc::new(default_on_changed.clone()));
|
|
};
|
|
|
|
Effect::new({
|
|
let on_changed = on_changed.clone();
|
|
|
|
move |_| {
|
|
on_changed.clone()(state.get());
|
|
}
|
|
});
|
|
|
|
on_cleanup(move || {
|
|
on_changed(state.get());
|
|
});
|
|
|
|
let mode = Signal::derive(move || if emit_auto { store.get() } else { state.get() });
|
|
|
|
UseColorModeReturn {
|
|
mode,
|
|
set_mode: set_store,
|
|
store,
|
|
set_store,
|
|
system,
|
|
state,
|
|
}
|
|
}
|
|
|
|
/// Color modes
|
|
#[derive(Clone, Default, PartialEq, Eq, Hash, Debug)]
|
|
pub enum ColorMode {
|
|
#[default]
|
|
Auto,
|
|
Light,
|
|
Dark,
|
|
Custom(String),
|
|
}
|
|
|
|
fn get_cookie_signal(
|
|
cookie_name: &str,
|
|
cookie_enabled: bool,
|
|
) -> (Signal<Option<ColorMode>>, WriteSignal<Option<ColorMode>>) {
|
|
if cookie_enabled {
|
|
use_cookie::<ColorMode, FromToStringCodec>(cookie_name)
|
|
} else {
|
|
let (value, set_value) = signal(None);
|
|
(value.into(), set_value)
|
|
}
|
|
}
|
|
|
|
fn get_store_signal(
|
|
initial_value: MaybeRwSignal<ColorMode>,
|
|
storage_signal: Option<RwSignal<ColorMode>>,
|
|
storage_key: &str,
|
|
storage_enabled: bool,
|
|
storage: StorageType,
|
|
listen_to_storage_changes: bool,
|
|
) -> (Signal<ColorMode>, WriteSignal<ColorMode>) {
|
|
if let Some(storage_signal) = storage_signal {
|
|
let (store, set_store) = storage_signal.split();
|
|
(store.into(), set_store)
|
|
} else if storage_enabled {
|
|
let (store, set_store, _) = use_storage_with_options::<ColorMode, FromToStringCodec>(
|
|
storage,
|
|
storage_key,
|
|
UseStorageOptions::default()
|
|
.listen_to_storage_changes(listen_to_storage_changes)
|
|
.initial_value(initial_value),
|
|
);
|
|
(store, set_store)
|
|
} else {
|
|
initial_value.into_signal()
|
|
}
|
|
}
|
|
|
|
impl Display for ColorMode {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
use ColorMode::*;
|
|
|
|
match self {
|
|
Auto => write!(f, "auto"),
|
|
Light => write!(f, "light"),
|
|
Dark => write!(f, "dark"),
|
|
Custom(v) => write!(f, "{}", v),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<&str> for ColorMode {
|
|
fn from(s: &str) -> Self {
|
|
match s {
|
|
"auto" => ColorMode::Auto,
|
|
"" => ColorMode::Auto,
|
|
"light" => ColorMode::Light,
|
|
"dark" => ColorMode::Dark,
|
|
_ => ColorMode::Custom(s.to_string()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<String> for ColorMode {
|
|
fn from(s: String) -> Self {
|
|
ColorMode::from(s.as_str())
|
|
}
|
|
}
|
|
|
|
impl FromStr for ColorMode {
|
|
type Err = ();
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
Ok(ColorMode::from(s))
|
|
}
|
|
}
|
|
|
|
#[derive(DefaultBuilder)]
|
|
pub struct UseColorModeOptions<El, T>
|
|
where
|
|
El: Clone,
|
|
El: Into<ElementMaybeSignal<T, web_sys::Element>>,
|
|
T: Into<web_sys::Element> + Clone + 'static,
|
|
{
|
|
/// Element that the color mode will be applied to. Defaults to `"html"`.
|
|
target: El,
|
|
|
|
/// HTML attribute applied to the target element. Defaults to `"class"`.
|
|
#[builder(into)]
|
|
attribute: String,
|
|
|
|
/// Initial value of the color mode. Defaults to `"Auto"`.
|
|
#[builder(into)]
|
|
initial_value: MaybeRwSignal<ColorMode>,
|
|
|
|
/// Discover the initial value of the color mode from an URL parameter. Defaults to `None`.
|
|
#[builder(into)]
|
|
initial_value_from_url_param: Option<String>,
|
|
|
|
/// Write the initial value of the discovered color mode from URL parameter to storage.
|
|
/// This only has an effect if `initial_value_from_url_param` is specified.
|
|
/// Defaults to `false`.
|
|
initial_value_from_url_param_to_storage: bool,
|
|
|
|
/// Custom modes that you plan to use as `ColorMode::Custom(x)`. Defaults to `vec![]`.
|
|
custom_modes: Vec<String>,
|
|
|
|
/// Custom handler that is called on updates.
|
|
/// If specified this will override the default behavior.
|
|
/// To get the default behaviour back you can call the provided `default_handler` function.
|
|
/// It takes two parameters:
|
|
/// - `mode: ColorMode`: The color mode to change to.
|
|
/// -`default_handler: Rc<dyn Fn(ColorMode)>`: The default handler that would have been called if the `on_changed` handler had not been specified.
|
|
on_changed: OnChangedFn,
|
|
|
|
/// When provided, `useStorage` will be skipped.
|
|
/// Defaults to `None`.
|
|
#[builder(into)]
|
|
storage_signal: Option<RwSignal<ColorMode>>,
|
|
|
|
/// Key to persist the data into localStorage/sessionStorage.
|
|
/// Defaults to `"leptos-use-color-scheme"`.
|
|
#[builder(into)]
|
|
storage_key: String,
|
|
|
|
/// Storage type, can be `Local` or `Session` or custom.
|
|
/// Defaults to `Local`.
|
|
storage: StorageType,
|
|
|
|
/// If the color mode should be persisted.
|
|
/// Defaults to `true`.
|
|
storage_enabled: bool,
|
|
|
|
/// Name of the cookie that should be used to persist the color mode.
|
|
/// Defaults to `"leptos-use-color-scheme"`.
|
|
#[builder(into)]
|
|
cookie_name: String,
|
|
|
|
/// If the color mode should be persisted through a cookie.
|
|
/// Defaults to `false`.
|
|
cookie_enabled: bool,
|
|
|
|
/// Emit `auto` mode from state
|
|
///
|
|
/// When set to `true`, preferred mode won't be translated into `light` or `dark`.
|
|
/// This is useful when the fact that `auto` mode was selected needs to be known.
|
|
///
|
|
/// Defaults to `false`.
|
|
emit_auto: bool,
|
|
|
|
/// If transitions on color mode change are enabled. Defaults to `false`.
|
|
transition_enabled: bool,
|
|
|
|
/// Listen to changes to this storage key from somewhere else.
|
|
/// Defaults to true.
|
|
listen_to_storage_changes: bool,
|
|
|
|
#[builder(skip)]
|
|
_marker: PhantomData<T>,
|
|
}
|
|
|
|
type OnChangedFn = Rc<dyn Fn(ColorMode, Rc<dyn Fn(ColorMode)>)>;
|
|
|
|
impl Default for UseColorModeOptions<&'static str, web_sys::Element> {
|
|
fn default() -> Self {
|
|
Self {
|
|
target: "html",
|
|
attribute: "class".into(),
|
|
initial_value: ColorMode::Auto.into(),
|
|
initial_value_from_url_param: None,
|
|
initial_value_from_url_param_to_storage: false,
|
|
custom_modes: vec![],
|
|
on_changed: Rc::new(move |mode, default_handler| (default_handler)(mode)),
|
|
storage_signal: None,
|
|
storage_key: "leptos-use-color-scheme".into(),
|
|
storage: StorageType::default(),
|
|
storage_enabled: true,
|
|
cookie_name: "leptos-use-color-scheme".into(),
|
|
cookie_enabled: false,
|
|
emit_auto: false,
|
|
transition_enabled: false,
|
|
listen_to_storage_changes: true,
|
|
_marker: PhantomData,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Return type of [`use_color_mode`]
|
|
pub struct UseColorModeReturn {
|
|
/// Main value signal of the color mode
|
|
pub mode: Signal<ColorMode>,
|
|
/// Main value setter signal of the color mode
|
|
pub set_mode: WriteSignal<ColorMode>,
|
|
|
|
/// Direct access to the returned signal of [`use_storage`] if enabled or [`UseColorModeOptions::storage_signal`] if provided
|
|
pub store: Signal<ColorMode>,
|
|
/// Direct write access to the returned signal of [`use_storage`] if enabled or [`UseColorModeOptions::storage_signal`] if provided
|
|
pub set_store: WriteSignal<ColorMode>,
|
|
|
|
/// Signal of the system's preferred color mode that you would get from a media query
|
|
pub system: Signal<ColorMode>,
|
|
|
|
/// When [`UseColorModeOptions::emit_auto`] is `false` this is the same as `mode`. This will never report `ColorMode::Auto` but always on of the other modes.
|
|
pub state: Signal<ColorMode>,
|
|
}
|