1#![forbid(unsafe_code)]
2use wasm_bindgen::prelude::*;
3use wasm_bindgen::{JsCast, UnwrapThrowExt};
4use web_sys::{Document, Window};
5
6fn prefers_color_scheme(window: Window) -> String {
7 let document = window.document().expect("Could not access window document");
8 let storage = window
9 .local_storage()
10 .unwrap_throw()
11 .expect("Can't access local storage");
12 let theme = storage.get_item("theme").unwrap_or(None);
13
14 let document_element = document
15 .document_element()
16 .expect("Expecting an element on document");
17
18 let mut data_theme = "default";
19
20 if let Some(theme) = theme.as_deref() {
21 data_theme = theme;
22 } else if let Ok(Some(scheme)) = window.match_media("(prefers-color-scheme: dark)") {
23 if scheme.matches() {
24 data_theme = "dark";
25 storage.set_item("theme", "dark").unwrap_throw();
26 }
27 } else if let Ok(Some(scheme)) = window.match_media("(prefers-color-scheme: light)") {
28 if scheme.matches() {
29 data_theme = "light";
30 storage.set_item("theme", "light").unwrap_throw();
31 }
32 } else {
33 storage.set_item("theme", "default").unwrap_throw();
34 }
35
36 document_element
37 .set_attribute("data-theme", data_theme)
38 .expect("Failed to set data-theme");
39
40 data_theme.to_string()
41}
42
43fn toggle_callback(window: Window, document: Document) -> Closure<dyn FnMut(web_sys::Event)> {
44 Closure::wrap(Box::new(move |e: web_sys::Event| {
45 let input = e
46 .current_target()
47 .unwrap_throw()
48 .dyn_into::<web_sys::HtmlInputElement>()
49 .unwrap_throw();
50
51 let storage = window
52 .local_storage()
53 .unwrap_throw()
54 .expect("Can't access local storage");
55 let document_element = document
56 .document_element()
57 .expect("Expecting an element on document");
58
59 let value = input.value();
60 let mut itr = value.rsplitn(2, ',');
61 let checked_value = itr.next();
62 let unchecked_value = itr.next();
63
64 if input.checked() {
65 if let Some(checked) = checked_value {
66 document_element
67 .set_attribute("data-theme", checked)
68 .unwrap_throw();
69 storage.set_item("theme", checked).unwrap_throw();
70 } else {
71 document_element
72 .set_attribute("data-theme", &value)
73 .unwrap_throw();
74 storage.set_item("theme", &value).unwrap_throw();
75 }
76 } else if let Some(unchecked) = unchecked_value {
77 document_element
78 .set_attribute("data-theme", unchecked)
79 .unwrap_throw();
80 storage.set_item("theme", unchecked).unwrap_throw();
81 } else {
82 document_element
83 .set_attribute("data-theme", "default")
84 .unwrap_throw();
85 storage.set_item("theme", "default").unwrap_throw();
86 }
87 }) as Box<dyn FnMut(_)>)
88}
89
90#[wasm_bindgen]
91pub fn theme_toggle() {
92 let window = web_sys::window().expect("Could not access window");
93 let document = window.document().expect("Could not access window document");
94 let check_boxes = document
95 .query_selector_all("[name=theme-toggle]")
96 .unwrap_throw();
97 let entries: web_sys::js_sys::Iterator = check_boxes.values();
98 let callback = toggle_callback(window.clone(), document);
99 let prefered = prefers_color_scheme(window);
100
101 for entry in entries {
102 let element = entry
103 .unwrap_throw()
104 .dyn_into::<web_sys::HtmlInputElement>()
105 .unwrap_throw();
106
107 let value = element.value();
108 let mut itr = value.rsplitn(2, ',');
109 let checked_value = itr.next();
110
111 if let Some(checked) = checked_value {
112 if checked == prefered {
113 element.set_checked(true);
114 }
115 } else if element.value() == prefered {
116 element.set_checked(true);
117 }
118
119 element
120 .add_event_listener_with_callback("click", callback.as_ref().unchecked_ref())
121 .unwrap_throw()
122 }
123
124 callback.forget();
125}
126
127fn radio_callback(window: Window, document: Document) -> Closure<dyn FnMut(web_sys::Event)> {
128 Closure::wrap(Box::new(move |e: web_sys::Event| {
129 let input = e
130 .current_target()
131 .unwrap_throw()
132 .dyn_into::<web_sys::HtmlInputElement>()
133 .unwrap_throw();
134 let storage = window
135 .local_storage()
136 .unwrap_throw()
137 .expect("Can't access local storage");
138 let document_element = document
139 .document_element()
140 .expect("Expecting an element on document");
141
142 document_element
143 .set_attribute("data-theme", &input.value())
144 .unwrap_throw();
145 storage.set_item("theme", &input.value()).unwrap_throw();
146 }) as Box<dyn FnMut(_)>)
147}
148
149#[wasm_bindgen]
150pub fn theme_radio() {
151 let window = web_sys::window().expect("Could not access window");
152 let document = window.document().expect("Could not access window document");
153 let radios = document
154 .query_selector_all("[name=theme-radios]")
155 .unwrap_throw();
156 let entries: web_sys::js_sys::Iterator = radios.values();
157 let callback = radio_callback(window.clone(), document);
158 let prefered = prefers_color_scheme(window);
159
160 for entry in entries {
161 let element = entry
162 .unwrap_throw()
163 .dyn_into::<web_sys::HtmlInputElement>()
164 .unwrap_throw();
165
166 if element.value() == prefered {
167 element.set_checked(true);
168 }
169
170 element
171 .add_event_listener_with_callback("click", callback.as_ref().unchecked_ref())
172 .unwrap_throw()
173 }
174
175 callback.forget();
176}
177
178fn button_callback(window: Window, document: Document) -> Closure<dyn FnMut(web_sys::Event)> {
179 Closure::wrap(Box::new(move |e: web_sys::Event| {
180 let button = e
181 .current_target()
182 .unwrap_throw()
183 .dyn_into::<web_sys::HtmlButtonElement>()
184 .unwrap_throw();
185 let storage = window
186 .local_storage()
187 .unwrap_throw()
188 .expect("Can't access local storage");
189 let document_element = document
190 .document_element()
191 .expect("Expecting an element on document");
192
193 document_element
194 .set_attribute("data-theme", &button.value())
195 .unwrap_throw();
196 storage.set_item("theme", &button.value()).unwrap_throw();
197 }) as Box<dyn FnMut(_)>)
198}
199
200#[wasm_bindgen]
201pub fn theme_buttons() {
202 let window = web_sys::window().expect("Could not access window");
203 let document = window.document().expect("Could not access window document");
204 let buttons = document
205 .query_selector_all("[name=theme-button]")
206 .unwrap_throw();
207 let entries: web_sys::js_sys::Iterator = buttons.values();
208 let callback = button_callback(window.clone(), document);
209 let _prefered = prefers_color_scheme(window);
210
211 for entry in entries {
212 let element = entry
213 .unwrap_throw()
214 .dyn_into::<web_sys::HtmlButtonElement>()
215 .unwrap_throw();
216
217 element
218 .add_event_listener_with_callback("click", callback.as_ref().unchecked_ref())
219 .unwrap_throw()
220 }
221
222 callback.forget();
223}
224
225fn select_callback(window: Window, document: Document) -> Closure<dyn FnMut(web_sys::Event)> {
226 Closure::wrap(Box::new(move |e: web_sys::Event| {
227 let select = e
228 .current_target()
229 .unwrap_throw()
230 .dyn_into::<web_sys::HtmlSelectElement>()
231 .unwrap_throw();
232 let storage = window
233 .local_storage()
234 .unwrap_throw()
235 .expect("Can't access local storage");
236 let document_element = document
237 .document_element()
238 .expect("Expecting an element on document");
239
240 document_element
241 .set_attribute("data-theme", &select.value())
242 .unwrap_throw();
243 storage.set_item("theme", &select.value()).unwrap_throw();
244 }) as Box<dyn FnMut(_)>)
245}
246
247#[wasm_bindgen]
248pub fn theme_select() {
249 let window = web_sys::window().expect("Could not access window");
250 let document = window.document().expect("Could not access window document");
251 let radios = document
252 .query_selector_all("[name=theme-select]")
253 .unwrap_throw();
254 let entries: web_sys::js_sys::Iterator = radios.values();
255 let callback = select_callback(window.clone(), document);
256 let prefered = prefers_color_scheme(window);
257
258 for entry in entries {
259 let element = entry
260 .unwrap_throw()
261 .dyn_into::<web_sys::HtmlSelectElement>()
262 .unwrap_throw();
263
264 element.set_value(&prefered);
265
266 element
267 .add_event_listener_with_callback("change", callback.as_ref().unchecked_ref())
268 .unwrap_throw()
269 }
270
271 callback.forget();
272}