use std::borrow::Cow;
use gloo::events::EventListener;
use gloo::utils::window;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{HtmlElement, HtmlInputElement, HtmlTextAreaElement, KeyboardEvent};
use yew::prelude::*;
use super::use_latest;
type KeyFilter = Box<dyn Fn(&str) -> bool>;
#[hook]
pub fn use_start_typing<F>(callback: F)
where
F: Fn(KeyboardEvent) + 'static,
{
use_start_typing_with_options(callback, UseStartTypingOptions::default())
}
pub struct UseStartTypingOptions {
pub event_type: Cow<'static, str>,
pub check_editable: bool,
pub check_modifiers: bool,
pub key_filter: Option<KeyFilter>,
}
impl Default for UseStartTypingOptions {
fn default() -> Self {
Self {
event_type: "keydown".into(),
check_editable: true,
check_modifiers: true,
key_filter: None,
}
}
}
impl Clone for UseStartTypingOptions {
fn clone(&self) -> Self {
Self {
event_type: self.event_type.clone(),
check_editable: self.check_editable,
check_modifiers: self.check_modifiers,
key_filter: None, }
}
}
impl PartialEq for UseStartTypingOptions {
fn eq(&self, other: &Self) -> bool {
self.event_type == other.event_type
&& self.check_editable == other.check_editable
&& self.check_modifiers == other.check_modifiers
&& self.key_filter.is_some() == other.key_filter.is_some()
}
}
impl std::fmt::Debug for UseStartTypingOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UseStartTypingOptions")
.field("event_type", &self.event_type)
.field("check_editable", &self.check_editable)
.field("check_modifiers", &self.check_modifiers)
.field(
"key_filter",
&if self.key_filter.is_some() {
"Some(...)"
} else {
"None"
},
)
.finish()
}
}
#[hook]
pub fn use_start_typing_with_options<F>(callback: F, options: UseStartTypingOptions)
where
F: Fn(KeyboardEvent) + 'static,
{
let callback = use_latest(callback);
let options_ref = use_latest(options);
use_effect_with((), move |_| {
let window = window();
fn is_editable_element(element: &web_sys::Element) -> bool {
if let Ok(input) = element.clone().dyn_into::<HtmlInputElement>() {
!input.disabled() && input.type_() != "hidden"
} else if let Ok(textarea) = element.clone().dyn_into::<HtmlTextAreaElement>() {
!textarea.disabled()
} else if let Ok(html_element) = element.clone().dyn_into::<HtmlElement>() {
html_element
.get_attribute("contenteditable")
.map(|value| value == "true")
.unwrap_or(false)
} else {
false
}
}
let is_editable_element_focused = {
let window = window.clone();
let options_ref = options_ref.clone();
move || {
if !options_ref.current().check_editable {
return false;
}
let document = match window.document() {
Some(doc) => doc,
None => return false,
};
let active_element = document.active_element();
if let Some(element) = active_element {
is_editable_element(&element)
} else {
false
}
}
};
let event_type = options_ref.current().event_type.clone();
let options_ref_clone = options_ref.clone();
let listener = EventListener::new(&window, event_type, move |event| {
let keyboard_event: KeyboardEvent = JsValue::from(event).into();
let options = &*options_ref_clone.current();
if should_trigger_keyboard_event(&keyboard_event, options, &is_editable_element_focused)
{
(*callback.current())(keyboard_event);
}
});
move || drop(listener)
});
}
fn should_trigger_keyboard_event(
keyboard_event: &KeyboardEvent,
options: &UseStartTypingOptions,
is_editable_element_focused: &dyn Fn() -> bool,
) -> bool {
if options.check_modifiers
&& (keyboard_event.ctrl_key()
|| keyboard_event.alt_key()
|| keyboard_event.meta_key()
|| keyboard_event.shift_key())
{
return false;
}
if options.check_editable && is_editable_element_focused() {
return false;
}
let key = keyboard_event.key();
if let Some(filter) = &options.key_filter {
filter(&key)
} else {
if key.len() == 1 {
let c = match key.chars().next() {
Some(c) => c,
None => return false,
};
c.is_ascii_alphanumeric()
} else {
false
}
}
}