diff --git a/leptos_hotkeys/src/context.rs b/leptos_hotkeys/src/context.rs index beee3dc..ac89fac 100644 --- a/leptos_hotkeys/src/context.rs +++ b/leptos_hotkeys/src/context.rs @@ -1,13 +1,13 @@ use leptos::html::ElementDescriptor; use leptos::*; -use std::collections::HashSet; +use std::collections::{BTreeMap, HashSet}; #[cfg(not(feature = "ssr"))] use wasm_bindgen::JsCast; #[derive(Clone, Copy)] pub struct HotkeysContext { #[cfg(not(feature = "ssr"))] - pub(crate) pressed_keys: RwSignal>, + pub(crate) keys_pressed: RwSignal, #[cfg(not(feature = "ssr"))] pub active_ref_target: RwSignal>, @@ -20,6 +20,12 @@ pub struct HotkeysContext { pub disable_scope: Callback, pub toggle_scope: Callback, } +#[derive(Debug, Default, Clone)] +#[cfg_attr(feature = "ssr", allow(dead_code))] +pub struct KeyPresses { + pub key_map: BTreeMap, + pub last_key: Option, +} pub fn provide_hotkeys_context( #[cfg_attr(feature = "ssr", allow(unused_variables))] node_ref: NodeRef, @@ -38,8 +44,7 @@ where }); #[cfg(not(feature = "ssr"))] - let pressed_keys: RwSignal> = - RwSignal::new(std::collections::HashMap::new()); + let keys_pressed: RwSignal = RwSignal::new(KeyPresses::default()); let active_scopes: RwSignal> = RwSignal::new(initially_active_scopes); @@ -81,8 +86,15 @@ where #[cfg(all(feature = "debug", not(feature = "ssr")))] create_effect(move |_| { - let pressed_keys_list = move || pressed_keys.get().keys().cloned().collect::>(); - logging::log!("keys pressed: {:?}", pressed_keys_list()); + let keys_pressed_list = move || { + keys_pressed + .get() + .key_map + .keys() + .cloned() + .collect::>() + }; + logging::log!("keys pressed: {:?}", keys_pressed_list()); }); #[cfg(not(feature = "ssr"))] @@ -91,25 +103,23 @@ where if cfg!(feature = "debug") { logging::log!("Window lost focus"); } - pressed_keys.set_untracked(std::collections::HashMap::new()); + keys_pressed.set_untracked(KeyPresses::default()); }) as Box); let keydown_listener = wasm_bindgen::closure::Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| { - pressed_keys.update(|keys| { - match &event.key().eq_ignore_ascii_case(" ") { - true => keys.insert("spacebar".to_string(), event), - false => keys.insert(event.key().to_lowercase(), event), - }; + keys_pressed.update(|keys| { + let key = clean_key(&event); + keys.key_map.insert(key.clone(), event); + keys.last_key = Some(key); }); }) as Box); let keyup_listener = wasm_bindgen::closure::Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| { - pressed_keys.update(|keys| { - match &event.key().eq_ignore_ascii_case(" ") { - true => keys.remove(&"spacebar".to_string()), - false => keys.remove(&event.key().to_lowercase()), - }; + keys_pressed.update(|keys| { + let key = clean_key(&event); + keys.key_map.remove(&key); + keys.last_key = None; }); }) as Box); @@ -156,7 +166,7 @@ where let hotkeys_context = HotkeysContext { #[cfg(not(feature = "ssr"))] - pressed_keys, + keys_pressed, #[cfg(not(feature = "ssr"))] active_ref_target, @@ -177,3 +187,11 @@ where pub fn use_hotkeys_context() -> HotkeysContext { use_context::().expect("expected hotkeys context") } + +#[cfg(not(feature = "ssr"))] +fn clean_key(event: &web_sys::KeyboardEvent) -> String { + match event.key().as_str() { + " " => "spacebar".to_string(), + _ => event.key().to_lowercase(), + } +} diff --git a/leptos_hotkeys/src/hotkey.rs b/leptos_hotkeys/src/hotkey.rs index 4c2664b..a180529 100644 --- a/leptos_hotkeys/src/hotkey.rs +++ b/leptos_hotkeys/src/hotkey.rs @@ -1,6 +1,7 @@ -use crate::types::Keys; use crate::KeyboardModifiers; +use crate::{context::KeyPresses, types::Keys}; use core::str::FromStr; +use std::collections::HashSet; use std::fmt::{Display, Formatter}; #[derive(Debug, PartialEq, Hash, Eq)] @@ -29,6 +30,10 @@ impl Hotkey { pub fn new(key_combination: &str) -> Self { key_combination.parse().unwrap() } + + fn includes_key(&self, key: &String) -> bool { + self.keys.iter().any(|k| k == key) + } } impl FromStr for Hotkey { @@ -77,10 +82,19 @@ impl FromStr for Hotkey { } } +#[cfg_attr(feature = "ssr", allow(dead_code))] +pub(crate) fn is_last_key_match(parsed_keys: &HashSet, pressed_keys: &KeyPresses) -> bool { + pressed_keys.last_key.as_ref().is_some_and(|last_key| { + parsed_keys + .iter() + .any(|hotkey| hotkey.includes_key(last_key)) + }) +} + #[cfg_attr(feature = "ssr", allow(dead_code))] pub(crate) fn is_hotkey_match( hotkey: &Hotkey, - pressed_keyset: &mut std::collections::HashMap, + pressed_keyset: &mut std::collections::BTreeMap, ) -> bool { let mut modifiers_match = true; diff --git a/leptos_hotkeys/src/use_hotkeys.rs b/leptos_hotkeys/src/use_hotkeys.rs index 001a1b2..1a62d54 100644 --- a/leptos_hotkeys/src/use_hotkeys.rs +++ b/leptos_hotkeys/src/use_hotkeys.rs @@ -7,34 +7,39 @@ pub fn use_hotkeys_scoped( ) { #[cfg(not(feature = "ssr"))] { - use crate::hotkey::is_hotkey_match; + use crate::hotkey::{is_hotkey_match, is_last_key_match}; use crate::{use_hotkeys_context, Hotkey}; use std::collections::HashSet; let parsed_keys: HashSet = key_combination.split(',').map(Hotkey::new).collect(); let hotkeys_context = use_hotkeys_context(); - let pressed_keys = hotkeys_context.pressed_keys; create_effect(move |_| { let active_scopes = hotkeys_context.active_scopes.get(); let within_scope = scopes.iter().any(|scope| active_scopes.contains(scope)); - if within_scope { - let mut pressed_keyset = pressed_keys.get(); - if let Some(matching_hotkey) = parsed_keys - .iter() - .find(|hotkey| is_hotkey_match(hotkey, &mut pressed_keyset)) - { - if cfg!(feature = "debug") { - let message = format!("%cfiring hotkey: {}", &matching_hotkey); - web_sys::console::log_2( - &wasm_bindgen::JsValue::from_str(&message), - &wasm_bindgen::JsValue::from_str("color: #39FF14;"), - ); - } - Callable::call(&on_triggered, ()); + if !within_scope { + return; + } + + let mut keys_pressed = hotkeys_context.keys_pressed.get(); + if !is_last_key_match(&parsed_keys, &keys_pressed) { + return; + } + + if let Some(matching_hotkey) = parsed_keys + .iter() + .find(|hotkey| is_hotkey_match(hotkey, &mut keys_pressed.key_map)) + { + if cfg!(feature = "debug") { + let message = format!("%cfiring hotkey: {}", &matching_hotkey); + web_sys::console::log_2( + &wasm_bindgen::JsValue::from_str(&message), + &wasm_bindgen::JsValue::from_str("color: #39FF14;"), + ); } + Callable::call(&on_triggered, ()); } }); } @@ -50,7 +55,7 @@ pub fn use_hotkeys_ref( { #[cfg(not(feature = "ssr"))] create_effect(move |_| { - use crate::hotkey::is_hotkey_match; + use crate::hotkey::{is_hotkey_match, is_last_key_match}; use crate::{use_hotkeys_context, Hotkey}; use leptos::ev::DOMEventResponder; use std::collections::HashSet; @@ -61,23 +66,29 @@ pub fn use_hotkeys_ref( let keydown_closure = move |_event: web_sys::KeyboardEvent| { let hotkeys_context = use_hotkeys_context(); let active_scopes = hotkeys_context.active_scopes.get(); - let mut pressed_keys = hotkeys_context.pressed_keys.get(); + let mut pressed_keys = hotkeys_context.keys_pressed.get(); let within_scope = scopes.iter().any(|scope| active_scopes.contains(scope)); - if within_scope { - if let Some(matching_hotkey) = parsed_keys - .iter() - .find(|hotkey| is_hotkey_match(hotkey, &mut pressed_keys)) - { - if cfg!(feature = "debug") { - let message = format!("%cfiring hotkey: {}", &matching_hotkey); - web_sys::console::log_2( - &wasm_bindgen::JsValue::from_str(&message), - &wasm_bindgen::JsValue::from_str("color: #39FF14;"), - ); - } - Callable::call(&on_triggered, ()); + if !within_scope { + return; + } + + if !is_last_key_match(&parsed_keys, &pressed_keys) { + return; + } + + if let Some(matching_hotkey) = parsed_keys + .iter() + .find(|hotkey| is_hotkey_match(hotkey, &mut pressed_keys.key_map)) + { + if cfg!(feature = "debug") { + let message = format!("%cfiring hotkey: {}", &matching_hotkey); + web_sys::console::log_2( + &wasm_bindgen::JsValue::from_str(&message), + &wasm_bindgen::JsValue::from_str("color: #39FF14;"), + ); } + Callable::call(&on_triggered, ()); } };