use crate::core::url;
use crate::core::{ElementMaybeSignal, IntoElementMaybeSignal, MaybeRwSignal};
use crate::storage::{use_storage_with_options, StorageType, UseStorageOptions};
use crate::utils::get_header;
use crate::{
sync_signal_with_options, use_cookie_with_options, use_preferred_dark_with_options,
SyncSignalOptions, UseCookieOptions, UsePreferredDarkOptions,
};
use codee::string::FromToStringCodec;
use default_struct_builder::DefaultBuilder;
use leptos::prelude::*;
use leptos::reactive::wrappers::read::Signal;
use std::fmt::{Display, Formatter};
use std::marker::PhantomData;
use std::str::FromStr;
use std::sync::Arc;
use wasm_bindgen::JsCast;
pub fn use_color_mode() -> UseColorModeReturn {
use_color_mode_with_options(UseColorModeOptions::default())
}
pub fn use_color_mode_with_options<El, M>(options: UseColorModeOptions<El, M>) -> UseColorModeReturn
where
El: IntoElementMaybeSignal<web_sys::Element, M>,
M: ?Sized,
{
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,
ssr_color_header_getter,
_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_with_options(UsePreferredDarkOptions {
ssr_color_header_getter,
});
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_assigns(
move |store: &mut ColorMode, cookie: &Option<ColorMode>| {
if let Some(cookie) = cookie {
*store = cookie.clone();
}
},
move |cookie: &mut Option<ColorMode>, store: &ColorMode| {
*cookie = 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.write_untracked() = value;
}
}
let state = Signal::derive(move || {
let value = store.get();
if value == ColorMode::Auto {
system.get()
} else {
value
}
});
let target = target.into_element_maybe_signal();
let update_html_attrs = {
move |target: ElementMaybeSignal<web_sys::Element>, attribute: String, value: ColorMode| {
let el = target.get_untracked();
if let Some(el) = el {
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() {
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, attribute.clone(), mode);
};
let on_changed = move |mode: ColorMode| {
on_changed(mode, Arc::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,
}
}
#[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_with_options::<ColorMode, FromToStringCodec>(
cookie_name,
UseCookieOptions::default().path("/"),
)
} 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, M>
where
El: IntoElementMaybeSignal<web_sys::Element, M>,
M: ?Sized,
{
target: El,
#[builder(into)]
attribute: String,
#[builder(into)]
initial_value: MaybeRwSignal<ColorMode>,
#[builder(into)]
initial_value_from_url_param: Option<String>,
initial_value_from_url_param_to_storage: bool,
custom_modes: Vec<String>,
on_changed: OnChangedFn,
#[builder(into)]
storage_signal: Option<RwSignal<ColorMode>>,
#[builder(into)]
storage_key: String,
storage: StorageType,
storage_enabled: bool,
#[builder(into)]
cookie_name: String,
cookie_enabled: bool,
emit_auto: bool,
transition_enabled: bool,
listen_to_storage_changes: bool,
#[allow(dead_code)]
ssr_color_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
#[builder(skip)]
_marker: PhantomData<M>,
}
type OnChangedFn = Arc<dyn Fn(ColorMode, Arc<dyn Fn(ColorMode) + Send + Sync>) + Send + Sync>;
impl Default for UseColorModeOptions<&'static str, str> {
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: Arc::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,
ssr_color_header_getter: Arc::new(move || {
get_header!(
HeaderName::from_static("sec-ch-prefers-color-scheme"),
use_color_mode,
ssr_color_header_getter
)
}),
_marker: PhantomData,
}
}
}
pub struct UseColorModeReturn {
pub mode: Signal<ColorMode>,
pub set_mode: WriteSignal<ColorMode>,
pub store: Signal<ColorMode>,
pub set_store: WriteSignal<ColorMode>,
pub system: Signal<ColorMode>,
pub state: Signal<ColorMode>,
}