Skip to main content

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::cell::DomRefCell;
10use script_bindings::codegen::GenericBindings::CharacterDataBinding::CharacterDataMethods;
11use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods;
12use script_bindings::codegen::GenericBindings::NodeBinding::NodeMethods;
13use script_bindings::inheritance::Castable;
14use script_bindings::root::{Dom, DomRoot};
15use style::selector_parser::PseudoElement;
16
17use crate::dom::characterdata::CharacterData;
18use crate::dom::document::Document;
19use crate::dom::element::{CustomElementCreationMode, Element, ElementCreator};
20use crate::dom::node::{Node, NodeTraits};
21use crate::dom::textcontrol::TextControlElement;
22
23const PASSWORD_REPLACEMENT_CHAR: char = '●';
24
25#[derive(Default, JSTraceable, MallocSizeOf, PartialEq)]
26#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
27pub(crate) struct TextInputWidget {
28    shadow_tree: DomRefCell<Option<TextInputWidgetShadowTree>>,
29}
30
31impl TextInputWidget {
32    /// Get the shadow tree for this [`HTMLInputElement`], if it is created and valid, otherwise
33    /// recreate the shadow tree and return it.
34    fn get_or_create_shadow_tree(
35        &self,
36        cx: &mut JSContext,
37        text_control_element: &impl TextControlElement,
38    ) -> Ref<'_, TextInputWidgetShadowTree> {
39        {
40            if let Ok(shadow_tree) = Ref::filter_map(self.shadow_tree.borrow(), |shadow_tree| {
41                shadow_tree.as_ref()
42            }) {
43                return shadow_tree;
44            }
45        }
46
47        let element = text_control_element.upcast::<Element>();
48        let shadow_root = element
49            .shadow_root()
50            .unwrap_or_else(|| element.attach_ua_shadow_root(cx, true));
51        let shadow_root = shadow_root.upcast();
52        *self.shadow_tree.borrow_mut() = Some(TextInputWidgetShadowTree::new(cx, shadow_root));
53        self.get_or_create_shadow_tree(cx, text_control_element)
54    }
55
56    pub(crate) fn update_shadow_tree(&self, cx: &mut JSContext, element: &impl TextControlElement) {
57        self.get_or_create_shadow_tree(cx, element)
58            .update(cx, element)
59    }
60
61    pub(crate) fn update_placeholder_contents(
62        &self,
63        cx: &mut JSContext,
64        element: &impl TextControlElement,
65    ) {
66        self.get_or_create_shadow_tree(cx, element)
67            .update_placeholder(cx, element);
68    }
69}
70
71#[derive(Clone, JSTraceable, MallocSizeOf, PartialEq)]
72#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
73/// Contains reference to text control inner editor and placeholder container element in the UA
74/// shadow tree for `text`, `password`, `url`, `tel`, and `email` input. The following is the
75/// structure of the shadow tree.
76///
77/// ```
78/// <input type="text">
79///     #shadow-root
80///         <div id="inner-container">
81///             <div id="input-editor"></div>
82///             <div id="input-placeholder"></div>
83///         </div>
84/// </input>
85/// ```
86///
87// TODO(stevennovaryo): We are trying to use CSS to mimic Chrome and Firefox's layout for the <input> element.
88//                      But, this could be slower in performance and does have some discrepancies. For example,
89//                      they would try to vertically align <input> text baseline with the baseline of other
90//                      TextNode within an inline flow. Another example is the horizontal scroll.
91// FIXME(#38263): Refactor these logics into a TextControl wrapper that would decouple all textual input.
92pub(crate) struct TextInputWidgetShadowTree {
93    inner_container: Dom<Element>,
94    text_container: Dom<Element>,
95    placeholder_container: DomRefCell<Option<Dom<Element>>>,
96}
97
98impl TextInputWidgetShadowTree {
99    pub(crate) fn new(cx: &mut JSContext, shadow_root: &Node) -> Self {
100        let document = shadow_root.owner_document();
101        let inner_container = Element::create(
102            cx,
103            QualName::new(None, ns!(html), local_name!("div")),
104            None,
105            &document,
106            ElementCreator::ScriptCreated,
107            CustomElementCreationMode::Asynchronous,
108            None,
109        );
110
111        Node::replace_all(cx, Some(inner_container.upcast()), shadow_root.upcast());
112        inner_container
113            .upcast::<Node>()
114            .set_implemented_pseudo_element(PseudoElement::ServoTextControlInnerContainer);
115
116        let text_container = create_ua_widget_div_with_text_node(
117            cx,
118            &document,
119            inner_container.upcast(),
120            PseudoElement::ServoTextControlInnerEditor,
121            false,
122        );
123
124        Self {
125            inner_container: inner_container.as_traced(),
126            text_container: text_container.as_traced(),
127            placeholder_container: DomRefCell::new(None),
128        }
129    }
130
131    /// Initialize the placeholder container only when it is necessary. This would help the performance of input
132    /// element with shadow dom that is quite bulky.
133    fn init_placeholder_container_if_necessary(
134        &self,
135        cx: &mut JSContext,
136        element: &impl TextControlElement,
137    ) -> Option<DomRoot<Element>> {
138        if let Some(placeholder_container) = &*self.placeholder_container.borrow() {
139            return Some(placeholder_container.root_element());
140        }
141        // If there is no placeholder text and we haven't already created one then it is
142        // not necessary to initialize a new placeholder container.
143        let placeholder = element.placeholder_text();
144        if placeholder.is_empty() {
145            return None;
146        }
147
148        let placeholder_container = create_ua_widget_div_with_text_node(
149            cx,
150            &element.owner_document(),
151            self.inner_container.upcast::<Node>(),
152            PseudoElement::Placeholder,
153            true,
154        );
155        *self.placeholder_container.borrow_mut() = Some(placeholder_container.as_traced());
156        Some(placeholder_container)
157    }
158
159    fn placeholder_character_data(
160        &self,
161        cx: &mut JSContext,
162        element: &impl TextControlElement,
163    ) -> Option<DomRoot<CharacterData>> {
164        self.init_placeholder_container_if_necessary(cx, element)
165            .and_then(|placeholder_container| {
166                let first_child = placeholder_container.upcast::<Node>().GetFirstChild()?;
167                Some(DomRoot::from_ref(first_child.downcast::<CharacterData>()?))
168            })
169    }
170
171    pub(crate) fn update_placeholder(&self, cx: &mut JSContext, element: &impl TextControlElement) {
172        if let Some(character_data) = self.placeholder_character_data(cx, element) {
173            let placeholder_value = element.placeholder_text();
174            if character_data.Data() != *placeholder_value {
175                character_data.SetData(cx, placeholder_value.clone());
176            }
177        }
178    }
179
180    fn value_character_data(&self) -> Option<DomRoot<CharacterData>> {
181        Some(DomRoot::from_ref(
182            self.text_container
183                .upcast::<Node>()
184                .GetFirstChild()?
185                .downcast::<CharacterData>()?,
186        ))
187    }
188
189    // TODO(stevennovaryo): The rest of textual input shadow dom structure should act
190    // like an exstension to this one.
191    pub(crate) fn update(&self, cx: &mut JSContext, element: &impl TextControlElement) {
192        // The addition of zero-width space here forces the text input to have an inline formatting
193        // context that might otherwise be trimmed if there's no text. This is important to ensure
194        // that the input element is at least as tall as the line gap of the caret:
195        // <https://drafts.csswg.org/css-ui/#element-with-default-preferred-size>.
196        //
197        // This is also used to ensure that the caret will still be rendered when the input is empty.
198        // TODO: Could append `<br>` element to prevent collapses and avoid this hack, but we would
199        //       need to fix the rendering of caret beforehand.
200        let value = element.value_text();
201        let value_text = match (value.is_empty(), element.is_password_field()) {
202            // For a password input, we replace all of the character with its replacement char.
203            (false, true) => value
204                .str()
205                .chars()
206                .map(|_| PASSWORD_REPLACEMENT_CHAR)
207                .collect::<String>()
208                .into(),
209            (false, _) => value,
210            (true, _) => "\u{200B}".into(),
211        };
212
213        if let Some(character_data) = self.value_character_data() &&
214            character_data.Data() != value_text
215        {
216            character_data.SetData(cx, value_text);
217        }
218    }
219}
220
221/// Create a div element with a text node within an UA Widget and either append or prepend it to
222/// the designated parent. This is used to create the text container for input elements.
223fn create_ua_widget_div_with_text_node(
224    cx: &mut JSContext,
225    document: &Document,
226    parent: &Node,
227    implemented_pseudo: PseudoElement,
228    as_first_child: bool,
229) -> DomRoot<Element> {
230    let el = Element::create(
231        cx,
232        QualName::new(None, ns!(html), local_name!("div")),
233        None,
234        document,
235        ElementCreator::ScriptCreated,
236        CustomElementCreationMode::Asynchronous,
237        None,
238    );
239
240    parent
241        .upcast::<Node>()
242        .AppendChild(cx, el.upcast::<Node>())
243        .unwrap();
244    el.upcast::<Node>()
245        .set_implemented_pseudo_element(implemented_pseudo);
246    let text_node = document.CreateTextNode(cx, "".into());
247
248    if !as_first_child {
249        el.upcast::<Node>()
250            .AppendChild(cx, text_node.upcast::<Node>())
251            .unwrap();
252    } else {
253        el.upcast::<Node>()
254            .InsertBefore(
255                cx,
256                text_node.upcast::<Node>(),
257                el.upcast::<Node>().GetFirstChild().as_deref(),
258            )
259            .unwrap();
260    }
261    el
262}