Skip to main content

wasm_theme/
lib.rs

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}