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::NodeBinding::NodeMethods;
use script_bindings::inheritance::Castable;
use script_bindings::root::{Dom, DomRoot};
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::node::{Node, NodeTraits};
use crate::dom::textcontrol::TextControlElement;
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,
text_control_element: &impl TextControlElement,
) -> 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 = text_control_element.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, text_control_element)
}
pub(crate) fn update_shadow_tree(&self, cx: &mut JSContext, element: &impl TextControlElement) {
self.get_or_create_shadow_tree(cx, element).update(element)
}
pub(crate) fn update_placeholder_contents(
&self,
cx: &mut JSContext,
element: &impl TextControlElement,
) {
self.get_or_create_shadow_tree(cx, element)
.update_placeholder(cx, element);
}
}
#[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,
element: &impl TextControlElement,
) -> Option<DomRoot<Element>> {
if let Some(placeholder_container) = &*self.placeholder_container.borrow() {
return Some(placeholder_container.root_element());
}
let placeholder = element.placeholder_text();
if placeholder.is_empty() {
return None;
}
let placeholder_container = create_ua_widget_div_with_text_node(
cx,
&element.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,
element: &impl TextControlElement,
) -> Option<DomRoot<CharacterData>> {
self.init_placeholder_container_if_necessary(cx, 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, element: &impl TextControlElement) {
if let Some(character_data) = self.placeholder_character_data(cx, element) {
let placeholder_value = element.placeholder_text();
if character_data.Data() != *placeholder_value {
character_data.SetData(placeholder_value.clone());
}
}
}
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, element: &impl TextControlElement) {
let value = element.value_text();
let value_text = match (value.is_empty(), element.is_password_field()) {
(false, true) => 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(cx, "".into());
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
}