use std::cell::Ref;
use html5ever::{local_name, ns};
use js::context::JSContext;
use markup5ever::QualName;
use script_bindings::codegen::GenericBindings::CharacterDataBinding::CharacterDataMethods;
use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods;
use script_bindings::codegen::GenericBindings::HTMLInputElementBinding::HTMLInputElementMethods;
use script_bindings::codegen::GenericBindings::NodeBinding::NodeMethods;
use script_bindings::inheritance::Castable;
use script_bindings::root::{Dom, DomRoot};
use script_bindings::script_runtime::CanGc;
use style::selector_parser::PseudoElement;
use crate::dom::bindings::cell::DomRefCell;
use crate::dom::characterdata::CharacterData;
use crate::dom::document::Document;
use crate::dom::element::{CustomElementCreationMode, Element, ElementCreator};
use crate::dom::htmlinputelement::HTMLInputElement;
use crate::dom::htmlinputelement::input_type::InputType;
use crate::dom::node::{Node, NodeTraits};
const PASSWORD_REPLACEMENT_CHAR: char = '●';
#[derive(Default, JSTraceable, MallocSizeOf, PartialEq)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
pub(crate) struct TextInputWidget {
shadow_tree: DomRefCell<Option<TextInputWidgetShadowTree>>,
}
impl TextInputWidget {
fn get_or_create_shadow_tree(
&self,
cx: &mut JSContext,
input: &HTMLInputElement,
) -> Ref<'_, TextInputWidgetShadowTree> {
{
if let Ok(shadow_tree) = Ref::filter_map(self.shadow_tree.borrow(), |shadow_tree| {
shadow_tree.as_ref()
}) {
return shadow_tree;
}
}
let element = input.upcast::<Element>();
let shadow_root = element
.shadow_root()
.unwrap_or_else(|| element.attach_ua_shadow_root(cx, true));
let shadow_root = shadow_root.upcast();
*self.shadow_tree.borrow_mut() = Some(TextInputWidgetShadowTree::new(cx, shadow_root));
self.get_or_create_shadow_tree(cx, input)
}
pub(crate) fn update_shadow_tree(&self, cx: &mut JSContext, input: &HTMLInputElement) {
self.get_or_create_shadow_tree(cx, input).update(input)
}
pub(crate) fn update_placeholder_contents(&self, cx: &mut JSContext, input: &HTMLInputElement) {
self.get_or_create_shadow_tree(cx, input)
.update_placeholder(cx, input);
}
}
#[derive(Clone, JSTraceable, MallocSizeOf, PartialEq)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
pub(crate) struct TextInputWidgetShadowTree {
inner_container: Dom<Element>,
text_container: Dom<Element>,
placeholder_container: DomRefCell<Option<Dom<Element>>>,
}
impl TextInputWidgetShadowTree {
pub(crate) fn new(cx: &mut JSContext, shadow_root: &Node) -> Self {
let document = shadow_root.owner_document();
let inner_container = Element::create(
cx,
QualName::new(None, ns!(html), local_name!("div")),
None,
&document,
ElementCreator::ScriptCreated,
CustomElementCreationMode::Asynchronous,
None,
);
Node::replace_all(cx, Some(inner_container.upcast()), shadow_root.upcast());
inner_container
.upcast::<Node>()
.set_implemented_pseudo_element(PseudoElement::ServoTextControlInnerContainer);
let text_container = create_ua_widget_div_with_text_node(
cx,
&document,
inner_container.upcast(),
PseudoElement::ServoTextControlInnerEditor,
false,
);
Self {
inner_container: inner_container.as_traced(),
text_container: text_container.as_traced(),
placeholder_container: DomRefCell::new(None),
}
}
fn init_placeholder_container_if_necessary(
&self,
cx: &mut JSContext,
host: &HTMLInputElement,
) -> Option<DomRoot<Element>> {
if let Some(placeholder_container) = &*self.placeholder_container.borrow() {
return Some(placeholder_container.root_element());
}
if host.placeholder().is_empty() {
return None;
}
let placeholder_container = create_ua_widget_div_with_text_node(
cx,
&host.owner_document(),
self.inner_container.upcast::<Node>(),
PseudoElement::Placeholder,
true,
);
*self.placeholder_container.borrow_mut() = Some(placeholder_container.as_traced());
Some(placeholder_container)
}
fn placeholder_character_data(
&self,
cx: &mut JSContext,
input_element: &HTMLInputElement,
) -> Option<DomRoot<CharacterData>> {
self.init_placeholder_container_if_necessary(cx, input_element)
.and_then(|placeholder_container| {
let first_child = placeholder_container.upcast::<Node>().GetFirstChild()?;
Some(DomRoot::from_ref(first_child.downcast::<CharacterData>()?))
})
}
pub(crate) fn update_placeholder(&self, cx: &mut JSContext, input_element: &HTMLInputElement) {
if let Some(character_data) = self.placeholder_character_data(cx, input_element) {
let placeholder_value = input_element.placeholder().clone();
if character_data.Data() != placeholder_value {
character_data.SetData(placeholder_value);
}
}
}
fn value_character_data(&self) -> Option<DomRoot<CharacterData>> {
Some(DomRoot::from_ref(
self.text_container
.upcast::<Node>()
.GetFirstChild()?
.downcast::<CharacterData>()?,
))
}
pub(crate) fn update(&self, input_element: &HTMLInputElement) {
let value = input_element.Value();
let input_type = &*input_element.input_type();
let value_text = match (value.is_empty(), input_type) {
(false, InputType::Password(_)) => value
.str()
.chars()
.map(|_| PASSWORD_REPLACEMENT_CHAR)
.collect::<String>()
.into(),
(false, _) => value,
(true, _) => "\u{200B}".into(),
};
if let Some(character_data) = self.value_character_data() {
if character_data.Data() != value_text {
character_data.SetData(value_text);
}
}
}
}
fn create_ua_widget_div_with_text_node(
cx: &mut JSContext,
document: &Document,
parent: &Node,
implemented_pseudo: PseudoElement,
as_first_child: bool,
) -> DomRoot<Element> {
let el = Element::create(
cx,
QualName::new(None, ns!(html), local_name!("div")),
None,
document,
ElementCreator::ScriptCreated,
CustomElementCreationMode::Asynchronous,
None,
);
parent
.upcast::<Node>()
.AppendChild(cx, el.upcast::<Node>())
.unwrap();
el.upcast::<Node>()
.set_implemented_pseudo_element(implemented_pseudo);
let text_node = document.CreateTextNode("".into(), CanGc::from_cx(cx));
if !as_first_child {
el.upcast::<Node>()
.AppendChild(cx, text_node.upcast::<Node>())
.unwrap();
} else {
el.upcast::<Node>()
.InsertBefore(
cx,
text_node.upcast::<Node>(),
el.upcast::<Node>().GetFirstChild().as_deref(),
)
.unwrap();
}
el
}