1use super::{Brick, BrickAssertion, BrickBudget, BrickVerification};
19use std::time::Duration;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum EventType {
24 Click,
26 DoubleClick,
28 MouseDown,
30 MouseUp,
32 MouseEnter,
34 MouseLeave,
36 KeyDown,
38 KeyUp,
40 KeyPress,
42 Input,
44 Change,
46 Submit,
48 Focus,
50 Blur,
52 Scroll,
54 TouchStart,
56 TouchEnd,
58 TouchMove,
60 Custom(&'static str),
62}
63
64impl EventType {
65 #[must_use]
67 pub fn js_name(&self) -> &str {
68 match self {
69 Self::Click => "click",
70 Self::DoubleClick => "dblclick",
71 Self::MouseDown => "mousedown",
72 Self::MouseUp => "mouseup",
73 Self::MouseEnter => "mouseenter",
74 Self::MouseLeave => "mouseleave",
75 Self::KeyDown => "keydown",
76 Self::KeyUp => "keyup",
77 Self::KeyPress => "keypress",
78 Self::Input => "input",
79 Self::Change => "change",
80 Self::Submit => "submit",
81 Self::Focus => "focus",
82 Self::Blur => "blur",
83 Self::Scroll => "scroll",
84 Self::TouchStart => "touchstart",
85 Self::TouchEnd => "touchend",
86 Self::TouchMove => "touchmove",
87 Self::Custom(name) => name,
88 }
89 }
90}
91
92#[derive(Debug, Clone)]
94pub enum EventHandler {
95 DispatchState(String),
97
98 CallWasm {
100 function: String,
102 args: Vec<String>,
104 },
105
106 PostMessage {
108 worker: String,
110 message_type: String,
112 fields: Vec<(String, String)>,
114 },
115
116 UpdateElement {
118 selector: String,
120 property: String,
122 value: String,
124 },
125
126 ToggleClass {
128 selector: String,
130 class: String,
132 },
133
134 PreventDefault,
136
137 Chain(Vec<EventHandler>),
139
140 If {
142 condition: String,
144 then: Box<EventHandler>,
146 otherwise: Option<Box<EventHandler>>,
148 },
149}
150
151impl EventHandler {
152 #[must_use]
154 pub fn dispatch_state(state: impl Into<String>) -> Self {
155 Self::DispatchState(state.into())
156 }
157
158 #[must_use]
160 pub fn call_wasm(function: impl Into<String>) -> Self {
161 Self::CallWasm {
162 function: function.into(),
163 args: Vec::new(),
164 }
165 }
166
167 #[must_use]
169 pub fn call_wasm_with_args(function: impl Into<String>, args: Vec<String>) -> Self {
170 Self::CallWasm {
171 function: function.into(),
172 args,
173 }
174 }
175
176 #[must_use]
178 pub fn post_to_worker(worker: impl Into<String>, message_type: impl Into<String>) -> Self {
179 Self::PostMessage {
180 worker: worker.into(),
181 message_type: message_type.into(),
182 fields: Vec::new(),
183 }
184 }
185
186 #[must_use]
188 pub fn update_element(
189 selector: impl Into<String>,
190 property: impl Into<String>,
191 value: impl Into<String>,
192 ) -> Self {
193 Self::UpdateElement {
194 selector: selector.into(),
195 property: property.into(),
196 value: value.into(),
197 }
198 }
199
200 #[must_use]
202 pub fn toggle_class(selector: impl Into<String>, class: impl Into<String>) -> Self {
203 Self::ToggleClass {
204 selector: selector.into(),
205 class: class.into(),
206 }
207 }
208
209 #[must_use]
211 pub fn chain(handlers: Vec<EventHandler>) -> Self {
212 Self::Chain(handlers)
213 }
214
215 #[must_use]
217 pub fn when(
218 condition: impl Into<String>,
219 then: EventHandler,
220 otherwise: Option<EventHandler>,
221 ) -> Self {
222 Self::If {
223 condition: condition.into(),
224 then: Box::new(then),
225 otherwise: otherwise.map(Box::new),
226 }
227 }
228
229 #[must_use]
231 pub fn to_js(&self, indent: usize) -> String {
232 let pad = " ".repeat(indent);
233
234 match self {
235 Self::DispatchState(state) => {
236 format!(
237 "{}window.dispatchEvent(new CustomEvent('state-change', {{ detail: '{}' }}));",
238 pad, state
239 )
240 }
241
242 Self::CallWasm { function, args } => {
243 let args_str = args.join(", ");
244 format!("{}window.wasm.{}({});", pad, function, args_str)
245 }
246
247 Self::PostMessage {
248 worker,
249 message_type,
250 fields,
251 } => {
252 let fields_str = if fields.is_empty() {
253 String::new()
254 } else {
255 let f: Vec<_> = fields
256 .iter()
257 .map(|(k, v)| format!("{}: {}", k, v))
258 .collect();
259 format!(", {}", f.join(", "))
260 };
261 format!(
262 "{}{}.postMessage({{ type: '{}'{} }});",
263 pad, worker, message_type, fields_str
264 )
265 }
266
267 Self::UpdateElement {
268 selector,
269 property,
270 value,
271 } => {
272 format!(
273 "{}document.querySelector('{}').{} = {};",
274 pad, selector, property, value
275 )
276 }
277
278 Self::ToggleClass { selector, class } => {
279 format!(
280 "{}document.querySelector('{}').classList.toggle('{}');",
281 pad, selector, class
282 )
283 }
284
285 Self::PreventDefault => {
286 format!("{}e.preventDefault();\n{}e.stopPropagation();", pad, pad)
287 }
288
289 Self::Chain(handlers) => handlers
290 .iter()
291 .map(|h| h.to_js(indent))
292 .collect::<Vec<_>>()
293 .join("\n"),
294
295 Self::If {
296 condition,
297 then,
298 otherwise,
299 } => {
300 let then_js = then.to_js(indent + 1);
301 let else_js = otherwise
302 .as_ref()
303 .map(|h| format!(" else {{\n{}\n{}}}", h.to_js(indent + 1), pad))
304 .unwrap_or_default();
305
306 format!(
307 "{}if ({}) {{\n{}\n{}}}{}",
308 pad, condition, then_js, pad, else_js
309 )
310 }
311 }
312 }
313}
314
315#[derive(Debug, Clone)]
317pub struct EventBinding {
318 pub selector: String,
320 pub event_type: EventType,
322 pub handler: EventHandler,
324 pub capture: bool,
326 pub once: bool,
328 pub passive: bool,
330}
331
332impl EventBinding {
333 #[must_use]
335 pub fn new(selector: impl Into<String>, event_type: EventType, handler: EventHandler) -> Self {
336 Self {
337 selector: selector.into(),
338 event_type,
339 handler,
340 capture: false,
341 once: false,
342 passive: false,
343 }
344 }
345
346 #[must_use]
348 pub fn capture(mut self) -> Self {
349 self.capture = true;
350 self
351 }
352
353 #[must_use]
355 pub fn once(mut self) -> Self {
356 self.once = true;
357 self
358 }
359
360 #[must_use]
362 pub fn passive(mut self) -> Self {
363 self.passive = true;
364 self
365 }
366
367 #[must_use]
369 pub fn to_js(&self) -> String {
370 let handler_js = self.handler.to_js(2);
371
372 let options = if self.capture || self.once || self.passive {
373 let mut opts = Vec::new();
374 if self.capture {
375 opts.push("capture: true");
376 }
377 if self.once {
378 opts.push("once: true");
379 }
380 if self.passive {
381 opts.push("passive: true");
382 }
383 format!(", {{ {} }}", opts.join(", "))
384 } else {
385 String::new()
386 };
387
388 format!(
389 "document.querySelector('{}').addEventListener('{}', (e) => {{\n{}\n}}{}); ",
390 self.selector,
391 self.event_type.js_name(),
392 handler_js,
393 options
394 )
395 }
396}
397
398#[derive(Debug, Clone, Default)]
400pub struct EventBrick {
401 bindings: Vec<EventBinding>,
403 window_handlers: Vec<(EventType, EventHandler)>,
405}
406
407impl EventBrick {
408 #[must_use]
410 pub fn new() -> Self {
411 Self::default()
412 }
413
414 #[must_use]
416 pub fn on(
417 mut self,
418 selector: impl Into<String>,
419 event_type: EventType,
420 handler: EventHandler,
421 ) -> Self {
422 self.bindings
423 .push(EventBinding::new(selector, event_type, handler));
424 self
425 }
426
427 #[must_use]
429 pub fn on_with(mut self, binding: EventBinding) -> Self {
430 self.bindings.push(binding);
431 self
432 }
433
434 #[must_use]
436 pub fn on_window(mut self, event_type: EventType, handler: EventHandler) -> Self {
437 self.window_handlers.push((event_type, handler));
438 self
439 }
440
441 #[must_use]
443 pub fn to_event_js(&self) -> String {
444 let mut js = String::new();
445
446 js.push_str("// Event Handlers\n");
447 js.push_str("// Generated by probar - DO NOT EDIT MANUALLY\n\n");
448
449 for binding in &self.bindings {
451 js.push_str(&binding.to_js());
452 js.push('\n');
453 }
454
455 for (event_type, handler) in &self.window_handlers {
457 let handler_js = handler.to_js(1);
458 js.push_str(&format!(
459 "window.addEventListener('{}', (e) => {{\n{}\n}});\n",
460 event_type.js_name(),
461 handler_js
462 ));
463 }
464
465 js
466 }
467
468 #[must_use]
470 pub fn selectors(&self) -> Vec<&str> {
471 self.bindings.iter().map(|b| b.selector.as_str()).collect()
472 }
473}
474
475impl Brick for EventBrick {
476 fn brick_name(&self) -> &'static str {
477 "EventBrick"
478 }
479
480 fn assertions(&self) -> &[BrickAssertion] {
481 &[]
482 }
483
484 fn budget(&self) -> BrickBudget {
485 BrickBudget::uniform(100)
486 }
487
488 fn verify(&self) -> BrickVerification {
489 let passed = vec![BrickAssertion::Custom {
490 name: "event_bindings_valid".into(),
491 validator_id: 10,
492 }];
493
494 BrickVerification {
495 passed,
496 failed: Vec::new(),
497 verification_time: Duration::from_micros(50),
498 }
499 }
500
501 fn to_html(&self) -> String {
502 String::new()
503 }
504
505 fn to_css(&self) -> String {
506 String::new()
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513
514 #[test]
515 fn test_event_type_js_name() {
516 assert_eq!(EventType::Click.js_name(), "click");
517 assert_eq!(EventType::KeyDown.js_name(), "keydown");
518 assert_eq!(EventType::Custom("my-event").js_name(), "my-event");
519 }
520
521 #[test]
522 fn test_event_handler_dispatch_state() {
523 let handler = EventHandler::dispatch_state("recording");
524 let js = handler.to_js(0);
525
526 assert!(js.contains("dispatchEvent"));
527 assert!(js.contains("state-change"));
528 assert!(js.contains("recording"));
529 }
530
531 #[test]
532 fn test_event_handler_call_wasm() {
533 let handler = EventHandler::call_wasm("start_recording");
534 let js = handler.to_js(0);
535
536 assert!(js.contains("window.wasm.start_recording()"));
537 }
538
539 #[test]
540 fn test_event_handler_update_element() {
541 let handler = EventHandler::update_element("#status", "textContent", "'Ready'");
542 let js = handler.to_js(0);
543
544 assert!(js.contains("#status"));
545 assert!(js.contains("textContent"));
546 assert!(js.contains("'Ready'"));
547 }
548
549 #[test]
550 fn test_event_binding_basic() {
551 let binding = EventBinding::new(
552 "#button",
553 EventType::Click,
554 EventHandler::dispatch_state("clicked"),
555 );
556
557 let js = binding.to_js();
558
559 assert!(js.contains("#button"));
560 assert!(js.contains("click"));
561 assert!(js.contains("addEventListener"));
562 }
563
564 #[test]
565 fn test_event_binding_options() {
566 let binding = EventBinding::new(
567 "#scroll",
568 EventType::Scroll,
569 EventHandler::call_wasm("on_scroll"),
570 )
571 .passive()
572 .capture();
573
574 let js = binding.to_js();
575
576 assert!(js.contains("passive: true"));
577 assert!(js.contains("capture: true"));
578 }
579
580 #[test]
581 fn test_event_brick_generation() {
582 let events = EventBrick::new()
583 .on(
584 "#record",
585 EventType::Click,
586 EventHandler::dispatch_state("toggle"),
587 )
588 .on("#clear", EventType::Click, EventHandler::call_wasm("clear"));
589
590 let js = events.to_event_js();
591
592 assert!(js.contains("Generated by probar"));
593 assert!(js.contains("#record"));
594 assert!(js.contains("#clear"));
595 }
596
597 #[test]
598 fn test_event_handler_chain() {
599 let handler = EventHandler::chain(vec![
600 EventHandler::PreventDefault,
601 EventHandler::dispatch_state("clicked"),
602 ]);
603
604 let js = handler.to_js(0);
605
606 assert!(js.contains("preventDefault"));
607 assert!(js.contains("dispatchEvent"));
608 }
609
610 #[test]
611 fn test_event_handler_conditional() {
612 let handler = EventHandler::when(
613 "isRecording",
614 EventHandler::dispatch_state("stop"),
615 Some(EventHandler::dispatch_state("start")),
616 );
617
618 let js = handler.to_js(0);
619
620 assert!(js.contains("if (isRecording)"));
621 assert!(js.contains("stop"));
622 assert!(js.contains("else"));
623 assert!(js.contains("start"));
624 }
625}