script/dom/html/input_element/
text_input_widget.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4use 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    /// Get the shadow tree for this [`HTMLInputElement`], if it is created and valid, otherwise
36    /// recreate the shadow tree and return it.
37    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)]
71/// Contains reference to text control inner editor and placeholder container element in the UA
72/// shadow tree for `text`, `password`, `url`, `tel`, and `email` input. The following is the
73/// structure of the shadow tree.
74///
75/// ```
76/// <input type="text">
77///     #shadow-root
78///         <div id="inner-container">
79///             <div id="input-editor"></div>
80///             <div id="input-placeholder"></div>
81///         </div>
82/// </input>
83/// ```
84///
85// TODO(stevennovaryo): We are trying to use CSS to mimic Chrome and Firefox's layout for the <input> element.
86//                      But, this could be slower in performance and does have some discrepancies. For example,
87//                      they would try to vertically align <input> text baseline with the baseline of other
88//                      TextNode within an inline flow. Another example is the horizontal scroll.
89// FIXME(#38263): Refactor these logics into a TextControl wrapper that would decouple all textual input.
90pub(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    /// Initialize the placeholder container only when it is necessary. This would help the performance of input
130    /// element with shadow dom that is quite bulky.
131    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 there is no placeholder text and we haven't already created one then it is
140        // not necessary to initialize a new placeholder container.
141        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    // TODO(stevennovaryo): The rest of textual input shadow dom structure should act
187    // like an exstension to this one.
188    pub(crate) fn update(&self, input_element: &HTMLInputElement) {
189        // The addition of zero-width space here forces the text input to have an inline formatting
190        // context that might otherwise be trimmed if there's no text. This is important to ensure
191        // that the input element is at least as tall as the line gap of the caret:
192        // <https://drafts.csswg.org/css-ui/#element-with-default-preferred-size>.
193        //
194        // This is also used to ensure that the caret will still be rendered when the input is empty.
195        // TODO: Could append `<br>` element to prevent collapses and avoid this hack, but we would
196        //       need to fix the rendering of caret beforehand.
197        let value = input_element.Value();
198        let input_type = &*input_element.input_type();
199        let value_text = match (value.is_empty(), input_type) {
200            // For a password input, we replace all of the character with its replacement char.
201            (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
219/// Create a div element with a text node within an UA Widget and either append or prepend it to
220/// the designated parent. This is used to create the text container for input elements.
221fn 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}