mirror of
https://github.com/servo/servo
synced 2026-04-27 09:57:23 +02:00
script: Refactors HTMLInputElement.rs into type specific files (#43325)
Refactors HTMLInputElement.rs to split lots of type specific input code into their own files. These SpecificInputTypes are responsible for their own shadow trees and also for element specific data such as FileInputType's filelist. Testing: Covered by WPTs Fixes: https://github.com/servo/servo/issues/38263 Fixes: https://github.com/servo/servo/issues/43351 Signed-off-by: Luke Warlow <lwarlow@igalia.com> Co-authored-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
260
components/script/dom/html/input_element/text_input_widget.rs
Normal file
260
components/script/dom/html/input_element/text_input_widget.rs
Normal file
@@ -0,0 +1,260 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
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 {
|
||||
/// Get the shadow tree for this [`HTMLInputElement`], if it is created and valid, otherwise
|
||||
/// recreate the shadow tree and return it.
|
||||
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)]
|
||||
/// Contains reference to text control inner editor and placeholder container element in the UA
|
||||
/// shadow tree for `text`, `password`, `url`, `tel`, and `email` input. The following is the
|
||||
/// structure of the shadow tree.
|
||||
///
|
||||
/// ```
|
||||
/// <input type="text">
|
||||
/// #shadow-root
|
||||
/// <div id="inner-container">
|
||||
/// <div id="input-editor"></div>
|
||||
/// <div id="input-placeholder"></div>
|
||||
/// </div>
|
||||
/// </input>
|
||||
/// ```
|
||||
///
|
||||
// TODO(stevennovaryo): We are trying to use CSS to mimic Chrome and Firefox's layout for the <input> element.
|
||||
// But, this could be slower in performance and does have some discrepancies. For example,
|
||||
// they would try to vertically align <input> text baseline with the baseline of other
|
||||
// TextNode within an inline flow. Another example is the horizontal scroll.
|
||||
// FIXME(#38263): Refactor these logics into a TextControl wrapper that would decouple all textual input.
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the placeholder container only when it is necessary. This would help the performance of input
|
||||
/// element with shadow dom that is quite bulky.
|
||||
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 there is no placeholder text and we haven't already created one then it is
|
||||
// not necessary to initialize a new placeholder container.
|
||||
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>()?,
|
||||
))
|
||||
}
|
||||
|
||||
// TODO(stevennovaryo): The rest of textual input shadow dom structure should act
|
||||
// like an exstension to this one.
|
||||
pub(crate) fn update(&self, input_element: &HTMLInputElement) {
|
||||
// The addition of zero-width space here forces the text input to have an inline formatting
|
||||
// context that might otherwise be trimmed if there's no text. This is important to ensure
|
||||
// that the input element is at least as tall as the line gap of the caret:
|
||||
// <https://drafts.csswg.org/css-ui/#element-with-default-preferred-size>.
|
||||
//
|
||||
// This is also used to ensure that the caret will still be rendered when the input is empty.
|
||||
// TODO: Could append `<br>` element to prevent collapses and avoid this hack, but we would
|
||||
// need to fix the rendering of caret beforehand.
|
||||
let value = input_element.Value();
|
||||
let input_type = &*input_element.input_type();
|
||||
let value_text = match (value.is_empty(), input_type) {
|
||||
// For a password input, we replace all of the character with its replacement char.
|
||||
(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a div element with a text node within an UA Widget and either append or prepend it to
|
||||
/// the designated parent. This is used to create the text container for input elements.
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user