script/dom/html/input_element/
text_input_widget.rs1use std::cell::Ref;
5
6use html5ever::{local_name, ns};
7use js::context::JSContext;
8use markup5ever::QualName;
9use script_bindings::codegen::GenericBindings::CharacterDataBinding::CharacterDataMethods;
10use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods;
11use script_bindings::codegen::GenericBindings::HTMLInputElementBinding::HTMLInputElementMethods;
12use script_bindings::codegen::GenericBindings::NodeBinding::NodeMethods;
13use script_bindings::inheritance::Castable;
14use script_bindings::root::{Dom, DomRoot};
15use script_bindings::script_runtime::CanGc;
16use style::selector_parser::PseudoElement;
17
18use crate::dom::bindings::cell::DomRefCell;
19use crate::dom::characterdata::CharacterData;
20use crate::dom::document::Document;
21use crate::dom::element::{CustomElementCreationMode, Element, ElementCreator};
22use crate::dom::htmlinputelement::HTMLInputElement;
23use crate::dom::htmlinputelement::input_type::InputType;
24use crate::dom::node::{Node, NodeTraits};
25
26const PASSWORD_REPLACEMENT_CHAR: char = '●';
27
28#[derive(Default, JSTraceable, MallocSizeOf, PartialEq)]
29#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
30pub(crate) struct TextInputWidget {
31 shadow_tree: DomRefCell<Option<TextInputWidgetShadowTree>>,
32}
33
34impl TextInputWidget {
35 fn get_or_create_shadow_tree(
38 &self,
39 cx: &mut JSContext,
40 input: &HTMLInputElement,
41 ) -> Ref<'_, TextInputWidgetShadowTree> {
42 {
43 if let Ok(shadow_tree) = Ref::filter_map(self.shadow_tree.borrow(), |shadow_tree| {
44 shadow_tree.as_ref()
45 }) {
46 return shadow_tree;
47 }
48 }
49
50 let element = input.upcast::<Element>();
51 let shadow_root = element
52 .shadow_root()
53 .unwrap_or_else(|| element.attach_ua_shadow_root(cx, true));
54 let shadow_root = shadow_root.upcast();
55 *self.shadow_tree.borrow_mut() = Some(TextInputWidgetShadowTree::new(cx, shadow_root));
56 self.get_or_create_shadow_tree(cx, input)
57 }
58
59 pub(crate) fn update_shadow_tree(&self, cx: &mut JSContext, input: &HTMLInputElement) {
60 self.get_or_create_shadow_tree(cx, input).update(input)
61 }
62
63 pub(crate) fn update_placeholder_contents(&self, cx: &mut JSContext, input: &HTMLInputElement) {
64 self.get_or_create_shadow_tree(cx, input)
65 .update_placeholder(cx, input);
66 }
67}
68
69#[derive(Clone, JSTraceable, MallocSizeOf, PartialEq)]
70#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
71pub(crate) struct TextInputWidgetShadowTree {
91 inner_container: Dom<Element>,
92 text_container: Dom<Element>,
93 placeholder_container: DomRefCell<Option<Dom<Element>>>,
94}
95
96impl TextInputWidgetShadowTree {
97 pub(crate) fn new(cx: &mut JSContext, shadow_root: &Node) -> Self {
98 let document = shadow_root.owner_document();
99 let inner_container = Element::create(
100 cx,
101 QualName::new(None, ns!(html), local_name!("div")),
102 None,
103 &document,
104 ElementCreator::ScriptCreated,
105 CustomElementCreationMode::Asynchronous,
106 None,
107 );
108
109 Node::replace_all(cx, Some(inner_container.upcast()), shadow_root.upcast());
110 inner_container
111 .upcast::<Node>()
112 .set_implemented_pseudo_element(PseudoElement::ServoTextControlInnerContainer);
113
114 let text_container = create_ua_widget_div_with_text_node(
115 cx,
116 &document,
117 inner_container.upcast(),
118 PseudoElement::ServoTextControlInnerEditor,
119 false,
120 );
121
122 Self {
123 inner_container: inner_container.as_traced(),
124 text_container: text_container.as_traced(),
125 placeholder_container: DomRefCell::new(None),
126 }
127 }
128
129 fn init_placeholder_container_if_necessary(
132 &self,
133 cx: &mut JSContext,
134 host: &HTMLInputElement,
135 ) -> Option<DomRoot<Element>> {
136 if let Some(placeholder_container) = &*self.placeholder_container.borrow() {
137 return Some(placeholder_container.root_element());
138 }
139 if host.placeholder().is_empty() {
142 return None;
143 }
144
145 let placeholder_container = create_ua_widget_div_with_text_node(
146 cx,
147 &host.owner_document(),
148 self.inner_container.upcast::<Node>(),
149 PseudoElement::Placeholder,
150 true,
151 );
152 *self.placeholder_container.borrow_mut() = Some(placeholder_container.as_traced());
153 Some(placeholder_container)
154 }
155
156 fn placeholder_character_data(
157 &self,
158 cx: &mut JSContext,
159 input_element: &HTMLInputElement,
160 ) -> Option<DomRoot<CharacterData>> {
161 self.init_placeholder_container_if_necessary(cx, input_element)
162 .and_then(|placeholder_container| {
163 let first_child = placeholder_container.upcast::<Node>().GetFirstChild()?;
164 Some(DomRoot::from_ref(first_child.downcast::<CharacterData>()?))
165 })
166 }
167
168 pub(crate) fn update_placeholder(&self, cx: &mut JSContext, input_element: &HTMLInputElement) {
169 if let Some(character_data) = self.placeholder_character_data(cx, input_element) {
170 let placeholder_value = input_element.placeholder().clone();
171 if character_data.Data() != placeholder_value {
172 character_data.SetData(placeholder_value);
173 }
174 }
175 }
176
177 fn value_character_data(&self) -> Option<DomRoot<CharacterData>> {
178 Some(DomRoot::from_ref(
179 self.text_container
180 .upcast::<Node>()
181 .GetFirstChild()?
182 .downcast::<CharacterData>()?,
183 ))
184 }
185
186 pub(crate) fn update(&self, input_element: &HTMLInputElement) {
189 let value = input_element.Value();
198 let input_type = &*input_element.input_type();
199 let value_text = match (value.is_empty(), input_type) {
200 (false, InputType::Password(_)) => value
202 .str()
203 .chars()
204 .map(|_| PASSWORD_REPLACEMENT_CHAR)
205 .collect::<String>()
206 .into(),
207 (false, _) => value,
208 (true, _) => "\u{200B}".into(),
209 };
210
211 if let Some(character_data) = self.value_character_data() {
212 if character_data.Data() != value_text {
213 character_data.SetData(value_text);
214 }
215 }
216 }
217}
218
219fn create_ua_widget_div_with_text_node(
222 cx: &mut JSContext,
223 document: &Document,
224 parent: &Node,
225 implemented_pseudo: PseudoElement,
226 as_first_child: bool,
227) -> DomRoot<Element> {
228 let el = Element::create(
229 cx,
230 QualName::new(None, ns!(html), local_name!("div")),
231 None,
232 document,
233 ElementCreator::ScriptCreated,
234 CustomElementCreationMode::Asynchronous,
235 None,
236 );
237
238 parent
239 .upcast::<Node>()
240 .AppendChild(cx, el.upcast::<Node>())
241 .unwrap();
242 el.upcast::<Node>()
243 .set_implemented_pseudo_element(implemented_pseudo);
244 let text_node = document.CreateTextNode("".into(), CanGc::from_cx(cx));
245
246 if !as_first_child {
247 el.upcast::<Node>()
248 .AppendChild(cx, text_node.upcast::<Node>())
249 .unwrap();
250 } else {
251 el.upcast::<Node>()
252 .InsertBefore(
253 cx,
254 text_node.upcast::<Node>(),
255 el.upcast::<Node>().GetFirstChild().as_deref(),
256 )
257 .unwrap();
258 }
259 el
260}