1#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq, Default)]
2#[serde(rename_all = "camelCase")]
3pub enum Header {
4 #[default]
5 Title,
6 Previous,
7 Next,
8 Today,
9 PreviousYear,
10 NextYear,
11}
12impl AsRef<str> for Header {
13 fn as_ref(&self) -> &str {
14 match self {
15 Self::Title => "title",
16 Self::Previous => "prev",
17 Self::Next => "next",
18 Self::Today => "today",
19 Self::PreviousYear => "prevYear",
20 Self::NextYear => "nextYear",
21 }
22 }
23}
24
25#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
26pub struct HeaderSections {
27 pub left: String,
28 pub center: String,
29 pub right: String,
30}
31
32impl Default for HeaderSections {
33 fn default() -> Self {
34 Self {
35 left: Header::Title.as_ref().to_string(),
36 center: String::new(),
37 right: format!(
38 "{} {},{}",
39 Header::Today.as_ref(),
40 Header::Previous.as_ref(),
41 Header::Next.as_ref()
42 ),
43 }
44 }
45}
46
47#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
48#[serde(untagged)]
49pub enum HeaderOptions {
50 Active(bool),
51 Options(HeaderSections),
52}
53
54impl Default for HeaderOptions {
55 fn default() -> Self {
56 Self::Options(HeaderSections::default())
57 }
58}
59
60#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
61pub struct EventTimeFormat {
62 pub hour: DateFormat,
63 pub minute: DateFormat,
64 pub meridiem: DateFormat,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub omit_zero_minute: Option<bool>,
67}
68
69impl Default for EventTimeFormat {
70 fn default() -> Self {
71 Self {
72 hour: DateFormat::Numeric,
73 minute: DateFormat::TwoDigit,
74 meridiem: DateFormat::Short,
75 omit_zero_minute: None,
76 }
77 }
78}
79
80#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq, Default)]
81#[serde(rename_all = "camelCase")]
82pub enum DateFormat {
83 #[default]
84 Short,
85 Numeric,
86 Long,
87 #[serde(rename = "2-digit")]
88 TwoDigit,
89 Narrow,
90 #[serde(untagged)]
91 False(bool),
92}
93
94impl AsRef<str> for DateFormat {
95 fn as_ref(&self) -> &str {
96 match self {
97 Self::Short => "short",
98 Self::Numeric => "numeric",
99 Self::Long => "long",
100 Self::TwoDigit => "2-digit",
101 Self::Narrow => "narrow",
102 Self::False(_) => "false",
103 }
104 }
105}
106
107#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq, Default)]
108#[serde(rename_all = "camelCase")]
109pub struct DayHeaderFormat {
110 #[serde(skip_serializing_if = "Option::is_none")]
111 weekday: Option<DateFormat>,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 month: Option<DateFormat>,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 day: Option<DateFormat>,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 omit_commas: Option<bool>,
118}
119
120impl DayHeaderFormat {
121 #[must_use]
122 pub const fn weekday(mut self, weekday: DateFormat) -> Self {
123 self.weekday = Some(weekday);
124 self
125 }
126 #[must_use]
127 pub const fn month(mut self, month: DateFormat) -> Self {
128 self.month = Some(month);
129 self
130 }
131 #[must_use]
132 pub const fn day(mut self, day: DateFormat) -> Self {
133 self.day = Some(day);
134 self
135 }
136 #[must_use]
137 pub const fn omit_commas(mut self, omit_commas: bool) -> Self {
138 self.omit_commas = Some(omit_commas);
139 self
140 }
141}
142
143#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq, Default, serde::Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub enum InitialView {
146 #[default]
147 DayGridMonth,
148 TimeGridWeek,
149 TimeGridDay,
150 ListMonth,
151 ListWeek,
152 ListDay,
153 ListYear,
154}
155impl AsRef<str> for InitialView {
156 fn as_ref(&self) -> &str {
157 match self {
158 Self::DayGridMonth => "dayGridMonth",
159 Self::TimeGridWeek => "timeGridWeek",
160 Self::TimeGridDay => "timeGridDay",
161 Self::ListMonth => "listMonth",
162 Self::ListWeek => "listWeek",
163 Self::ListDay => "listDay",
164 Self::ListYear => "listYear",
165 }
166 }
167}
168impl std::str::FromStr for InitialView {
169 type Err = String;
170 fn from_str(s: &str) -> Result<Self, Self::Err> {
171 match s {
172 "dayGridMonth" => Ok(Self::DayGridMonth),
173 "timeGridWeek" => Ok(Self::TimeGridWeek),
174 "timeGridDay" => Ok(Self::TimeGridDay),
175 "listMonth" => Ok(Self::ListMonth),
176 "listWeek" => Ok(Self::ListWeek),
177 "listDay" => Ok(Self::ListDay),
178 "listYear" => Ok(Self::ListYear),
179 _ => Err(format!("Invalid view name: {s}")),
180 }
181 }
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, Default)]
185#[serde(rename_all = "camelCase")]
186pub enum Locale {
187 #[default]
188 #[serde(rename = "en")]
189 En,
190 #[serde(rename = "es")]
191 Es,
192 #[serde(rename = "fr")]
193 Fr,
194}
195
196impl AsRef<str> for Locale {
197 fn as_ref(&self) -> &str {
198 match self {
199 Self::En => "en",
200 Self::Es => "es",
201 Self::Fr => "fr",
202 }
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, Default, serde::Deserialize)]
207pub enum TimeZone {
208 #[default]
209 #[serde(rename = "local")]
210 Local,
211 #[serde(rename = "UTC")]
212 Utc,
213 #[serde(untagged)]
214 NamedTimeZone(String),
215}
216
217impl std::str::FromStr for TimeZone {
218 type Err = String;
219 fn from_str(s: &str) -> Result<Self, Self::Err> {
220 Ok(match s {
221 "local" => Self::Local,
222 "UTC" => Self::Utc,
223 s => Self::NamedTimeZone(s.to_string()),
224 })
225 }
226}
227
228#[derive(Debug, Clone, serde::Serialize, PartialEq, Default)]
229#[serde(rename_all = "camelCase")]
230pub struct Options {
231 pub initial_view: InitialView,
232 #[serde(skip_serializing_if = "Option::is_none")]
233 pub initial_date: Option<crate::EventDate>,
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub locale: Option<Locale>,
236 #[serde(skip_serializing_if = "Option::is_none")]
237 pub time_zone: Option<TimeZone>,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub expand_rows: Option<bool>,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub all_day_slot: Option<bool>,
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub selectable: Option<bool>,
244 #[serde(skip_serializing_if = "Option::is_none")]
245 pub first_day: Option<u32>,
246 #[serde(skip_serializing_if = "Option::is_none")]
247 pub slot_duration: Option<crate::EventDuration>,
248 #[serde(skip_serializing_if = "Option::is_none")]
249 pub header_toolbar: Option<HeaderOptions>,
250 #[serde(skip_serializing_if = "Option::is_none")]
251 pub select_long_press_delay: Option<u32>,
252 #[serde(skip_serializing_if = "Option::is_none")]
253 pub display_event_time: Option<bool>,
254 #[serde(skip_serializing_if = "Option::is_none")]
255 pub event_time_format: Option<EventTimeFormat>,
256 #[serde(skip_serializing_if = "Option::is_none")]
257 pub day_headers: Option<bool>,
258 #[serde(skip_serializing_if = "Option::is_none")]
259 pub day_header_format: Option<DayHeaderFormat>,
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub slot_label_format: Option<EventTimeFormat>,
262 #[serde(skip)]
263 event_click_handler: Option<web_sys::js_sys::Function>,
264 #[serde(skip)]
265 select_handler: Option<web_sys::js_sys::Function>,
266 #[serde(skip)]
267 date_click_handler: Option<web_sys::js_sys::Function>,
268 #[serde(skip)]
269 date_set_handler: Option<web_sys::js_sys::Function>,
270}
271impl Options {
272 #[must_use]
273 pub fn new() -> Self {
274 Self::default()
275 }
276 #[must_use]
277 pub const fn with_initial_view(mut self, initial_view: InitialView) -> Self {
278 self.initial_view = initial_view;
279 self
280 }
281 #[must_use]
282 pub fn with_initial_date(mut self, initial_date: crate::EventDate) -> Self {
283 self.initial_date = Some(initial_date);
284 self
285 }
286 #[must_use]
287 pub const fn with_locale(mut self, locale: Locale) -> Self {
288 self.locale = Some(locale);
289 self
290 }
291 #[must_use]
292 pub fn with_time_zone(mut self, time_zone: TimeZone) -> Self {
293 self.time_zone.replace(time_zone);
294 self
295 }
296 #[must_use]
297 pub const fn with_expand_rows(mut self, expand_rows: bool) -> Self {
298 self.expand_rows = Some(expand_rows);
299 self
300 }
301 #[must_use]
302 pub const fn with_all_day_slot(mut self, all_day_slot: bool) -> Self {
303 self.all_day_slot = Some(all_day_slot);
304 self
305 }
306 #[must_use]
307 pub const fn with_selectable(mut self, selectable: bool) -> Self {
308 self.selectable = Some(selectable);
309 self
310 }
311 #[must_use]
312 pub const fn with_first_day(mut self, first_day: u32) -> Self {
313 self.first_day = Some(first_day);
314 self
315 }
316 #[must_use]
317 pub fn with_slot_duration(mut self, slot_duration: crate::EventDuration) -> Self {
318 self.slot_duration = Some(slot_duration);
319 self
320 }
321 #[must_use]
322 pub fn with_header_toolbar(mut self, header_toolbar: HeaderOptions) -> Self {
323 self.header_toolbar = Some(header_toolbar);
324 self
325 }
326 #[must_use]
327 pub const fn with_select_long_press_delay(mut self, select_long_press_delay: u32) -> Self {
328 self.select_long_press_delay = Some(select_long_press_delay);
329 self
330 }
331 #[must_use]
332 pub const fn with_display_event_time(mut self, display_event_time: bool) -> Self {
333 self.display_event_time = Some(display_event_time);
334 self
335 }
336 #[must_use]
337 pub const fn with_event_time_format(mut self, event_time_format: EventTimeFormat) -> Self {
338 self.event_time_format = Some(event_time_format);
339 self
340 }
341 #[must_use]
342 pub const fn with_day_headers(mut self, day_headers: bool) -> Self {
343 self.day_headers = Some(day_headers);
344 self
345 }
346 #[must_use]
347 pub const fn with_day_header_format(mut self, day_header_format: DayHeaderFormat) -> Self {
348 self.day_header_format = Some(day_header_format);
349 self
350 }
351 #[must_use]
352 pub const fn with_slot_label_format(mut self, slot_label_format: EventTimeFormat) -> Self {
353 self.slot_label_format = Some(slot_label_format);
354 self
355 }
356
357 #[must_use]
358 pub fn with_event_click<F>(mut self, f: F) -> Self
359 where
360 F: Fn(crate::bindings::EventClickInfo) + 'static,
361 {
362 let closure = web_sys::wasm_bindgen::closure::Closure::<
363 dyn Fn(crate::bindings::EventClickInfo),
364 >::new(f);
365 self.event_click_handler = Some(web_sys::wasm_bindgen::JsCast::unchecked_into(
366 closure.into_js_value(),
367 ));
368 self
369 }
370
371 #[must_use]
372 pub fn with_select<F>(mut self, f: F) -> Self
373 where
374 F: Fn(crate::bindings::SelectionInfo) + 'static,
375 {
376 let closure = web_sys::wasm_bindgen::closure::Closure::<
377 dyn Fn(crate::bindings::SelectionInfo),
378 >::new(f);
379 self.select_handler = Some(web_sys::wasm_bindgen::JsCast::unchecked_into(
380 closure.into_js_value(),
381 ));
382 self
383 }
384
385 #[must_use]
386 pub fn with_date_click<F>(mut self, f: F) -> Self
387 where
388 F: Fn(crate::bindings::DateClickInfo) + 'static,
389 {
390 let closure = web_sys::wasm_bindgen::closure::Closure::<
391 dyn Fn(crate::bindings::DateClickInfo),
392 >::new(f);
393 self.date_click_handler = Some(web_sys::wasm_bindgen::JsCast::unchecked_into(
394 closure.into_js_value(),
395 ));
396
397 self
398 }
399
400 #[must_use]
401 pub fn with_date_set<F>(mut self, f: F) -> Self
402 where
403 F: Fn(crate::bindings::DateSetEvent) + 'static,
404 {
405 let closure = web_sys::wasm_bindgen::closure::Closure::<
406 dyn Fn(crate::bindings::DateSetEvent),
407 >::new(f);
408 self.date_set_handler = Some(web_sys::wasm_bindgen::JsCast::unchecked_into(
409 closure.into_js_value(),
410 ));
411 self
412 }
413 fn validate_handler(&self) -> Result<(), web_sys::wasm_bindgen::JsValue> {
414 let validate_fn = |handler: &web_sys::js_sys::Function,
415 name: &str|
416 -> Result<(), web_sys::wasm_bindgen::JsValue> {
417 if web_sys::js_sys::Reflect::get(handler, &"call".into())?.is_undefined() {
418 return Err(web_sys::wasm_bindgen::JsValue::from_str(&format!(
419 "Invalid {name} handler"
420 )));
421 }
422 Ok(())
423 };
424
425 if let Some(handler) = &self.event_click_handler {
426 validate_fn(handler, "event click")?;
427 }
428 if let Some(handler) = &self.select_handler {
429 validate_fn(handler, "select")?;
430 }
431 if let Some(handler) = &self.date_click_handler {
432 validate_fn(handler, "date click")?;
433 }
434 if let Some(handler) = &self.date_set_handler {
435 validate_fn(handler, "date set")?;
436 }
437 Ok(())
438 }
439
440 pub fn build(self) -> Result<web_sys::wasm_bindgen::JsValue, web_sys::wasm_bindgen::JsValue> {
446 self.validate_handler()?;
447
448 let obj = web_sys::js_sys::Object::new();
449
450 let base_options = serde_wasm_bindgen::to_value(&self)?;
452
453 let base_obj: web_sys::js_sys::Object = base_options.into();
455 let keys = web_sys::js_sys::Object::keys(&base_obj);
456 for i in 0..keys.length() {
457 let key = keys.get(i);
458 let value = web_sys::js_sys::Reflect::get(&base_obj, &key)?;
459 web_sys::js_sys::Reflect::set(&obj, &key, &value)?;
460 }
461 if let Some(handler) = &self.event_click_handler {
463 web_sys::js_sys::Reflect::set(
464 &obj,
465 &web_sys::wasm_bindgen::JsValue::from_str("eventClick"),
466 handler,
467 )?;
468 }
469 if let Some(handler) = &self.select_handler {
470 web_sys::js_sys::Reflect::set(
471 &obj,
472 &web_sys::wasm_bindgen::JsValue::from_str("select"),
473 handler,
474 )?;
475 }
476 if let Some(handler) = &self.date_click_handler {
477 web_sys::js_sys::Reflect::set(
478 &obj,
479 &web_sys::wasm_bindgen::JsValue::from_str("dateClick"),
480 handler,
481 )?;
482 }
483
484 if let Some(handler) = &self.date_set_handler {
485 web_sys::js_sys::Reflect::set(
486 &obj,
487 &web_sys::wasm_bindgen::JsValue::from_str("datesSet"),
488 handler,
489 )?;
490 }
491
492 Ok(obj.into())
493 }
494}