Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check if last pressed key is part of hotkey combination #120

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 36 additions & 18 deletions leptos_hotkeys/src/context.rs
Original file line number Diff line number Diff line change
@@ -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<std::collections::HashMap<String, web_sys::KeyboardEvent>>,
pub(crate) keys_pressed: RwSignal<KeyPresses>,

#[cfg(not(feature = "ssr"))]
pub active_ref_target: RwSignal<Option<web_sys::EventTarget>>,
Expand All @@ -20,6 +20,12 @@ pub struct HotkeysContext {
pub disable_scope: Callback<String>,
pub toggle_scope: Callback<String>,
}
#[derive(Debug, Default, Clone)]
#[cfg_attr(feature = "ssr", allow(dead_code))]
pub struct KeyPresses {
pub key_map: BTreeMap<String, web_sys::KeyboardEvent>,
pub last_key: Option<String>,
}

pub fn provide_hotkeys_context<T>(
#[cfg_attr(feature = "ssr", allow(unused_variables))] node_ref: NodeRef<T>,
Expand All @@ -38,8 +44,7 @@ where
});

#[cfg(not(feature = "ssr"))]
let pressed_keys: RwSignal<std::collections::HashMap<String, web_sys::KeyboardEvent>> =
RwSignal::new(std::collections::HashMap::new());
let keys_pressed: RwSignal<KeyPresses> = RwSignal::new(KeyPresses::default());

let active_scopes: RwSignal<HashSet<String>> = RwSignal::new(initially_active_scopes);

Expand Down Expand Up @@ -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::<Vec<String>>();
logging::log!("keys pressed: {:?}", pressed_keys_list());
let keys_pressed_list = move || {
keys_pressed
.get()
.key_map
.keys()
.cloned()
.collect::<Vec<String>>()
};
logging::log!("keys pressed: {:?}", keys_pressed_list());
});

#[cfg(not(feature = "ssr"))]
Expand All @@ -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<dyn Fn()>);

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<dyn Fn(_)>);
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<dyn Fn(_)>);

Expand Down Expand Up @@ -156,7 +166,7 @@ where

let hotkeys_context = HotkeysContext {
#[cfg(not(feature = "ssr"))]
pressed_keys,
keys_pressed,

#[cfg(not(feature = "ssr"))]
active_ref_target,
Expand All @@ -177,3 +187,11 @@ where
pub fn use_hotkeys_context() -> HotkeysContext {
use_context::<HotkeysContext>().expect("expected hotkeys context")
}

#[cfg(not(feature = "ssr"))]
fn clean_key(event: &web_sys::KeyboardEvent) -> String {
match event.key().as_str() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@friendlymatthew When I merged in the changes from #121, I noticed that this is the only place where event.key() is referenced. So if you've come up with a feature flag for switching between event.code() and event.key(), I'll add it here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maxbergmark thanks for flagging! Would you be interested in attacking #131? Work is taking up all my time, so I'm afraid I won't have time to get this done ASAP.

On another note, would you be interested in joining as a maintainer? You'd be requested for reviews and help steer this project. Let me know! :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@friendlymatthew I made an attempt at solving #131. I added a feature called event_key and just used it in the clean_key function. That should do the job, but I'd be happy to fix any issues that I might have overlooked (maybe the spacebar handling?). The name of the feature flag was just something I made up, I'll change it if you have a better suggestion.

I'm honored to be invited as a maintainer of this repo, but right now I can't find the time that it would require. I think you're doing great work, and I'll happily keep using the repo, and contribute in discussions and issues when I have some time left over.

" " => "spacebar".to_string(),
_ => event.key().to_lowercase(),
}
}
18 changes: 16 additions & 2 deletions leptos_hotkeys/src/hotkey.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Hotkey>, 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<String, web_sys::KeyboardEvent>,
pressed_keyset: &mut std::collections::BTreeMap<String, web_sys::KeyboardEvent>,
) -> bool {
let mut modifiers_match = true;

Expand Down
73 changes: 42 additions & 31 deletions leptos_hotkeys/src/use_hotkeys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Hotkey> = 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, ());
}
});
}
Expand All @@ -50,7 +55,7 @@ pub fn use_hotkeys_ref<T>(
{
#[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;
Expand All @@ -61,23 +66,29 @@ pub fn use_hotkeys_ref<T>(
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, ());
}
};

Expand Down