use super::{WebElement, WebEvent};
pub use crate::text_model::{TextSelection, TextSelectionDirection};
use crate::text_validation::{event_data, input_type, locale, text_value};
use std::io;
use wasm_bindgen::JsCast;
use web_sys::{CompositionEvent, HtmlInputElement, HtmlTextAreaElement, InputEvent};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TextInputMetadata {
value: String,
data: Option<String>,
input_type: String,
composing: bool,
selection: TextSelection,
}
impl TextInputMetadata {
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
#[must_use]
pub fn data(&self) -> Option<&str> {
self.data.as_deref()
}
#[must_use]
pub fn input_type(&self) -> &str {
&self.input_type
}
#[must_use]
pub const fn is_composing(&self) -> bool {
self.composing
}
#[must_use]
pub const fn selection(&self) -> TextSelection {
self.selection
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CompositionMetadata {
data: Option<String>,
locale: String,
}
impl CompositionMetadata {
#[must_use]
pub fn data(&self) -> Option<&str> {
self.data.as_deref()
}
#[must_use]
pub fn locale(&self) -> &str {
&self.locale
}
}
impl WebElement {
pub fn text_value(&self) -> io::Result<Option<String>> {
let value = self
.element
.dyn_ref::<HtmlInputElement>()
.map(HtmlInputElement::value)
.or_else(|| {
self.element
.dyn_ref::<HtmlTextAreaElement>()
.map(HtmlTextAreaElement::value)
});
value.map(text_value).transpose()
}
pub fn text_selection(&self) -> io::Result<Option<TextSelection>> {
if let Some(input) = self.element.dyn_ref::<HtmlInputElement>() {
return read_input_selection(input);
}
if let Some(textarea) = self.element.dyn_ref::<HtmlTextAreaElement>() {
return read_textarea_selection(textarea);
}
Ok(None)
}
pub fn set_text_selection(&self, selection: TextSelection) -> io::Result<()> {
let Some(value) = self.text_value()? else {
return Err(invalid_selection_error());
};
if self.text_selection()?.is_none() {
return Err(invalid_selection_error());
}
let value_units = value.encode_utf16().count();
let value_units = u32::try_from(value_units).map_err(|_| invalid_selection_error())?;
if selection.end() > value_units {
return Err(selection_range_error());
}
let direction = match selection.direction() {
TextSelectionDirection::Forward => "forward",
TextSelectionDirection::Backward => "backward",
TextSelectionDirection::None => "none",
TextSelectionDirection::Other => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Browser does not accept an unknown text selection direction",
));
}
};
if let Some(input) = self.element.dyn_ref::<HtmlInputElement>() {
return input
.set_selection_range_with_direction(selection.start(), selection.end(), direction)
.map_err(|_| invalid_selection_error());
}
if let Some(textarea) = self.element.dyn_ref::<HtmlTextAreaElement>() {
return textarea
.set_selection_range_with_direction(selection.start(), selection.end(), direction)
.map_err(|_| invalid_selection_error());
}
Err(invalid_selection_error())
}
}
impl WebEvent {
pub fn text_input_metadata(&self) -> io::Result<Option<TextInputMetadata>> {
let Some(input) = self.event.dyn_ref::<InputEvent>() else {
return Ok(None);
};
let Some(target) = self.target() else {
return Ok(None);
};
let Some(value) = target.text_value()? else {
return Ok(None);
};
let Some(selection) = target.text_selection()? else {
return Ok(None);
};
Ok(Some(TextInputMetadata {
value,
data: event_data(input.data())?,
input_type: input_type(input.input_type())?,
composing: input.is_composing(),
selection,
}))
}
pub fn composition_metadata(&self) -> io::Result<Option<CompositionMetadata>> {
let Some(composition) = self.event.dyn_ref::<CompositionEvent>() else {
return Ok(None);
};
Ok(Some(CompositionMetadata {
data: event_data(composition.data())?,
locale: locale(composition.locale())?,
}))
}
}
fn read_input_selection(input: &HtmlInputElement) -> io::Result<Option<TextSelection>> {
let Some(start) = input
.selection_start()
.map_err(|_| invalid_selection_error())?
else {
return Ok(None);
};
let Some(end) = input
.selection_end()
.map_err(|_| invalid_selection_error())?
else {
return Ok(None);
};
TextSelection::new(
start,
end,
TextSelectionDirection::from_browser(
input
.selection_direction()
.map_err(|_| invalid_selection_error())?
.as_deref(),
),
)
.map(Some)
}
fn read_textarea_selection(textarea: &HtmlTextAreaElement) -> io::Result<Option<TextSelection>> {
let Some(start) = textarea
.selection_start()
.map_err(|_| invalid_selection_error())?
else {
return Ok(None);
};
let Some(end) = textarea
.selection_end()
.map_err(|_| invalid_selection_error())?
else {
return Ok(None);
};
TextSelection::new(
start,
end,
TextSelectionDirection::from_browser(
textarea
.selection_direction()
.map_err(|_| invalid_selection_error())?
.as_deref(),
),
)
.map(Some)
}
fn invalid_selection_error() -> io::Error {
io::Error::new(
io::ErrorKind::InvalidInput,
"Browser rejected text selection access",
)
}
fn selection_range_error() -> io::Error {
io::Error::new(
io::ErrorKind::InvalidInput,
"Text selection exceeds the control value",
)
}