2024-02-11 01:03:12 +01:00
use crate ::core ::url ;
2023-06-23 22:04:16 +01:00
use crate ::core ::StorageType ;
2024-01-29 21:29:39 +00:00
use crate ::core ::{ ElementMaybeSignal , MaybeRwSignal } ;
use crate ::storage ::{ use_storage_with_options , UseStorageOptions } ;
2023-06-23 22:04:16 +01:00
use crate ::use_preferred_dark ;
2024-01-29 21:29:39 +00:00
use crate ::utils ::FromToStringCodec ;
2023-06-23 22:04:16 +01:00
use default_struct_builder ::DefaultBuilder ;
use leptos ::* ;
2024-01-29 21:29:39 +00:00
use std ::fmt ::{ Display , Formatter } ;
2023-06-23 22:04:16 +01:00
use std ::marker ::PhantomData ;
2023-08-04 15:58:03 +01:00
use std ::rc ::Rc ;
2024-01-29 21:29:39 +00:00
use std ::str ::FromStr ;
2023-06-23 22:04:16 +01:00
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::*;
2023-12-09 15:20:18 +00:00
/// # use leptos_use::{use_color_mode, UseColorModeReturn};
2023-06-23 22:04:16 +01:00
/// #
/// # #[component]
2023-07-27 18:06:36 +01:00
/// # fn Demo() -> impl IntoView {
2023-06-23 22:04:16 +01:00
/// let UseColorModeReturn {
/// mode, // Signal<ColorMode::dark | ColorMode::light>
/// set_mode,
/// ..
2023-07-27 18:06:36 +01:00
/// } = use_color_mode();
2023-06-23 22:04:16 +01:00
/// #
2023-07-27 18:06:36 +01:00
/// # view! { }
2023-06-23 22:04:16 +01:00
/// # }
/// ```
///
/// 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::*;
2023-12-09 15:20:18 +00:00
/// # use leptos_use::{ColorMode, use_color_mode, UseColorModeReturn};
2023-06-23 22:04:16 +01:00
/// #
/// # #[component]
2023-07-27 18:06:36 +01:00
/// # fn Demo() -> impl IntoView {
/// # let UseColorModeReturn { mode, set_mode, .. } = use_color_mode();
2023-06-23 22:04:16 +01:00
/// #
2023-06-24 01:12:43 +01:00
/// mode.get(); // ColorMode::Dark or ColorMode::Light
2023-06-23 22:04:16 +01:00
///
2023-10-29 15:47:52 +00:00
/// set_mode.set(ColorMode::Dark); // change to dark mode and persist
2023-06-23 22:04:16 +01:00
///
2023-06-24 01:12:43 +01:00
/// set_mode.set(ColorMode::Auto); // change to auto mode
2023-06-23 22:04:16 +01:00
/// #
2023-07-27 18:06:36 +01:00
/// # view! { }
2023-06-23 22:04:16 +01:00
/// # }
/// ```
///
/// ## Options
///
/// ```
/// # use leptos::*;
2023-12-11 19:13:16 +00:00
/// # use leptos_use::{use_color_mode_with_options, UseColorModeOptions, UseColorModeReturn};
2023-06-23 22:04:16 +01:00
/// #
/// # #[component]
2023-07-27 18:06:36 +01:00
/// # fn Demo() -> impl IntoView {
2023-06-23 22:04:16 +01:00
/// 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")>
/// #
2023-07-27 18:06:36 +01:00
/// # view! { }
2023-06-23 22:04:16 +01:00
/// # }
/// ```
///
2023-07-14 22:43:19 +01:00
/// ## Server-Side Rendering
///
/// On the server this will by default return `ColorMode::Light`. Persistence is disabled, of course.
///
2023-06-23 22:04:16 +01:00
/// ## See also
///
/// * [`use_dark`]
/// * [`use_preferred_dark`]
/// * [`use_storage`]
2023-07-27 18:06:36 +01:00
pub fn use_color_mode ( ) -> UseColorModeReturn {
use_color_mode_with_options ( UseColorModeOptions ::default ( ) )
2023-06-23 22:04:16 +01:00
}
/// Version of [`use_color_mode`] that takes a `UseColorModeOptions`. See [`use_color_mode`] for how to use.
2023-07-27 18:06:36 +01:00
pub fn use_color_mode_with_options < El , T > ( options : UseColorModeOptions < El , T > ) -> UseColorModeReturn
2023-06-23 22:04:16 +01:00
where
El : Clone ,
2023-07-27 18:06:36 +01:00
El : Into < ElementMaybeSignal < T , web_sys ::Element > > ,
2023-06-23 22:04:16 +01:00
T : Into < web_sys ::Element > + Clone + 'static ,
{
let UseColorModeOptions {
target ,
attribute ,
initial_value ,
2024-02-11 01:03:12 +01:00
initial_value_from_url_param ,
2024-02-15 22:45:28 +01:00
initial_value_from_url_param_to_storage ,
2023-06-23 22:04:16 +01:00
on_changed ,
storage_signal ,
custom_modes ,
storage_key ,
storage ,
storage_enabled ,
emit_auto ,
transition_enabled ,
listen_to_storage_changes ,
_marker ,
} = options ;
let modes : Vec < String > = custom_modes
. into_iter ( )
2023-07-15 01:14:13 +01:00
. chain ( vec! [
ColorMode ::Dark . to_string ( ) ,
ColorMode ::Light . to_string ( ) ,
] )
2023-06-23 22:04:16 +01:00
. collect ( ) ;
2023-07-27 18:06:36 +01:00
let preferred_dark = use_preferred_dark ( ) ;
2023-06-23 22:04:16 +01:00
2023-07-27 18:06:36 +01:00
let system = Signal ::derive ( move | | {
2023-06-23 22:04:16 +01:00
if preferred_dark . get ( ) {
ColorMode ::Dark
} else {
ColorMode ::Light
}
} ) ;
2024-02-15 22:45:28 +01:00
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 ( )
}
}
2024-02-11 01:03:12 +01:00
2023-06-23 22:04:16 +01:00
let ( store , set_store ) = get_store_signal (
2024-02-15 22:45:28 +01:00
initial_value_from_url . clone ( ) . unwrap_or ( initial_value ) ,
2023-06-23 22:04:16 +01:00
storage_signal ,
& storage_key ,
storage_enabled ,
storage ,
listen_to_storage_changes ,
) ;
2024-02-15 22:45:28 +01:00
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 ) ;
}
}
2023-07-27 18:06:36 +01:00
let state = Signal ::derive ( move | | {
2023-06-23 22:04:16 +01:00
let value = store . get ( ) ;
if value = = ColorMode ::Auto {
system . get ( )
} else {
value
}
} ) ;
2023-10-28 16:18:29 -05:00
let target = target . into ( ) ;
2023-06-23 22:04:16 +01:00
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 {
2023-06-24 01:12:43 +01:00
if & value . to_string ( ) = = mode {
2023-06-23 22:04:16 +01:00
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 | {
2023-08-04 15:58:03 +01:00
on_changed ( mode , Rc ::new ( default_on_changed . clone ( ) ) ) ;
2023-06-23 22:04:16 +01:00
} ;
2023-07-27 18:06:36 +01:00
create_effect ( {
2023-06-23 22:04:16 +01:00
let on_changed = on_changed . clone ( ) ;
move | _ | {
on_changed . clone ( ) ( state . get ( ) ) ;
}
} ) ;
2023-07-27 18:06:36 +01:00
on_cleanup ( move | | {
2023-06-23 22:04:16 +01:00
on_changed ( state . get ( ) ) ;
} ) ;
2023-07-27 18:06:36 +01:00
let mode = Signal ::derive ( move | | if emit_auto { store . get ( ) } else { state . get ( ) } ) ;
2023-06-23 22:04:16 +01:00
UseColorModeReturn {
mode ,
set_mode : set_store ,
2023-07-15 16:48:29 +01:00
store ,
2023-06-23 22:04:16 +01:00
set_store ,
system ,
state ,
}
}
/// Color modes
2023-07-14 22:43:19 +01:00
#[ derive(Clone, Default, PartialEq, Eq, Hash, Debug) ]
2023-06-23 22:04:16 +01:00
pub enum ColorMode {
#[ default ]
Auto ,
Light ,
Dark ,
Custom ( String ) ,
}
2023-10-27 14:27:53 +01:00
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 > ) {
2024-02-15 22:45:28 +01:00
if let Some ( storage_signal ) = storage_signal {
2023-10-27 14:27:53 +01:00
let ( store , set_store ) = storage_signal . split ( ) ;
( store . into ( ) , set_store )
} else if storage_enabled {
2024-02-15 22:45:28 +01:00
let ( store , set_store , _ ) = use_storage_with_options ::< ColorMode , FromToStringCodec > (
2023-10-27 20:46:53 +01:00
storage ,
2023-10-27 14:27:53 +01:00
storage_key ,
2023-10-29 11:49:03 +00:00
UseStorageOptions ::default ( )
2023-10-27 14:27:53 +01:00
. listen_to_storage_changes ( listen_to_storage_changes )
2023-11-04 13:50:41 +00:00
. initial_value ( initial_value ) ,
2023-10-27 20:46:53 +01:00
) ;
( store , set_store )
2023-10-27 14:27:53 +01:00
} else {
initial_value . into_signal ( )
2023-06-23 22:04:16 +01:00
}
2023-10-27 14:27:53 +01:00
}
2023-06-23 22:04:16 +01:00
2023-06-24 01:12:43 +01:00
impl Display for ColorMode {
fn fmt ( & self , f : & mut Formatter < '_ > ) -> std ::fmt ::Result {
2023-06-23 22:04:16 +01:00
use ColorMode ::* ;
match self {
2023-06-24 01:12:43 +01:00
Auto = > write! ( f , " auto " ) ,
Light = > write! ( f , " light " ) ,
Dark = > write! ( f , " dark " ) ,
Custom ( v ) = > write! ( f , " {} " , v ) ,
2023-06-23 22:04:16 +01:00
}
}
}
2023-06-24 01:12:43 +01:00
impl From < & str > for ColorMode {
fn from ( s : & str ) -> Self {
2023-06-23 22:04:16 +01:00
match s {
" auto " = > ColorMode ::Auto ,
" " = > ColorMode ::Auto ,
" light " = > ColorMode ::Light ,
" dark " = > ColorMode ::Dark ,
_ = > ColorMode ::Custom ( s . to_string ( ) ) ,
}
}
}
2023-06-24 01:12:43 +01:00
impl From < String > for ColorMode {
fn from ( s : String ) -> Self {
ColorMode ::from ( s . as_str ( ) )
}
}
2023-10-27 20:46:53 +01:00
impl FromStr for ColorMode {
type Err = ( ) ;
fn from_str ( s : & str ) -> Result < Self , Self ::Err > {
Ok ( ColorMode ::from ( s ) )
}
}
2023-06-23 22:04:16 +01:00
#[ derive(DefaultBuilder) ]
pub struct UseColorModeOptions < El , T >
where
El : Clone ,
2023-07-27 18:06:36 +01:00
El : Into < ElementMaybeSignal < T , web_sys ::Element > > ,
2023-06-23 22:04:16 +01:00
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) ]
2023-07-15 16:48:29 +01:00
initial_value : MaybeRwSignal < ColorMode > ,
2023-06-23 22:04:16 +01:00
2024-02-11 01:03:12 +01:00
/// Discover the initial value of the color mode from an URL parameter. Defaults to `None`.
#[ builder(into) ]
initial_value_from_url_param : Option < String > ,
2024-02-16 02:26:48 +00:00
/// 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.
2024-02-15 22:45:28 +01:00
/// Defaults to `false`.
initial_value_from_url_param_to_storage : bool ,
2023-06-23 22:04:16 +01:00
/// 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.
2023-08-04 15:58:03 +01:00
/// 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 ,
2023-06-23 22:04:16 +01:00
/// 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. If `true` this required the
2023-10-29 15:58:41 +00:00
/// Defaults to `true`.
2023-06-23 22:04:16 +01:00
storage_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 > ,
}
2023-08-04 15:58:03 +01:00
type OnChangedFn = Rc < dyn Fn ( ColorMode , Rc < dyn Fn ( ColorMode ) > ) > ;
2023-06-23 22:04:16 +01:00
impl Default for UseColorModeOptions < & 'static str , web_sys ::Element > {
fn default ( ) -> Self {
Self {
target : " html " ,
attribute : " class " . into ( ) ,
initial_value : ColorMode ::Auto . into ( ) ,
2024-02-11 01:03:12 +01:00
initial_value_from_url_param : None ,
2024-02-15 22:45:28 +01:00
initial_value_from_url_param_to_storage : false ,
2023-06-23 22:04:16 +01:00
custom_modes : vec ! [ ] ,
2023-08-04 15:58:03 +01:00
on_changed : Rc ::new ( move | mode , default_handler | ( default_handler ) ( mode ) ) ,
2023-06-23 22:04:16 +01:00
storage_signal : None ,
storage_key : " leptos-use-color-scheme " . into ( ) ,
storage : StorageType ::default ( ) ,
storage_enabled : true ,
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 > ,
}