Files
servo/components/script/dom/execcommand/contenteditable/htmlelement.rs
Tim van der Lippe ec513ff851 script: Split up contenteditable.rs into multiple files (#44110)
This file was getting way too big and too cluttered. Instead, split it
up into multiple files in a dedicated folder.

Part of #25005
Part of #43709

Testing: It compiles

Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
2026-04-11 15:18:36 +00:00

221 lines
10 KiB
Rust

/* 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 html5ever::local_name;
use js::context::JSContext;
use script_bindings::inheritance::Castable;
use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
use crate::dom::bindings::codegen::Bindings::SelectionBinding::SelectionMethods;
use crate::dom::bindings::inheritance::{ElementTypeId, HTMLElementTypeId, NodeTypeId};
use crate::dom::bindings::root::DomRoot;
use crate::dom::element::Element;
use crate::dom::execcommand::basecommand::{CommandName, CssPropertyName};
use crate::dom::html::htmlanchorelement::HTMLAnchorElement;
use crate::dom::html::htmlelement::HTMLElement;
use crate::dom::html::htmlfontelement::HTMLFontElement;
use crate::dom::node::node::{Node, NodeTraits, ShadowIncluding};
use crate::dom::text::Text;
use crate::script_runtime::CanGc;
impl HTMLElement {
pub(crate) fn local_name(&self) -> &str {
self.upcast::<Element>().local_name()
}
/// <https://w3c.github.io/editing/docs/execCommand/#clear-the-value>
pub(crate) fn clear_the_value(&self, cx: &mut JSContext, command: &CommandName) {
// Step 1. Let command be the current command.
//
// Passed in as argument
let node = self.upcast::<Node>();
let element = self.upcast::<Element>();
// Step 2. If element is not editable, return the empty list.
if !node.is_editable() {
return;
}
// Step 3. If element's specified command value for command is null,
// return the empty list.
if element.specified_command_value(command).is_none() {
return;
}
// Step 4. If element is a simple modifiable element:
if element.is_simple_modifiable_element() {
// Step 4.1. Let children be the children of element.
// Step 4.2. For each child in children, insert child into element's parent immediately before element, preserving ranges.
let element_parent = node.GetParentNode().expect("Must always have a parent");
for child in node.children() {
if element_parent.InsertBefore(cx, &child, Some(node)).is_err() {
unreachable!("Must always be able to insert");
}
}
// Step 4.3. Remove element from its parent.
node.remove_self(cx);
// Step 4.4. Return children.
return;
}
match command {
// Step 5. If command is "strikethrough", and element has a style attribute
// that sets "text-decoration" to some value containing "line-through",
// delete "line-through" from the value.
CommandName::Strikethrough => {
let property = CssPropertyName::TextDecorationLine;
if property.value_for_element(cx, self) == "line-through" {
// TODO: Only remove line-through
property.remove_from_element(cx, self);
}
},
// Step 6. If command is "underline", and element has a style attribute that
// sets "text-decoration" to some value containing "underline", delete "underline" from the value.
CommandName::Underline => {
let property = CssPropertyName::TextDecorationLine;
if property.value_for_element(cx, self) == "underline" {
// TODO: Only remove underline
property.remove_from_element(cx, self);
}
},
_ => {},
}
// Step 7. If the relevant CSS property for command is not null,
// unset that property of element.
if let Some(property) = command.relevant_css_property() {
property.remove_from_element(cx, self);
}
// Step 8. If element is a font element:
if self.is::<HTMLFontElement>() {
match command {
// Step 8.1. If command is "foreColor", unset element's color attribute, if set.
CommandName::ForeColor => {
element.remove_attribute_by_name(&local_name!("color"), CanGc::from_cx(cx));
},
// Step 8.2. If command is "fontName", unset element's face attribute, if set.
CommandName::FontName => {
element.remove_attribute_by_name(&local_name!("face"), CanGc::from_cx(cx));
},
// Step 8.3. If command is "fontSize", unset element's size attribute, if set.
CommandName::FontSize => {
element.remove_attribute_by_name(&local_name!("size"), CanGc::from_cx(cx));
},
_ => {},
}
}
// Step 9. If element is an a element and command is "createLink" or "unlink",
// unset the href property of element.
if self.is::<HTMLAnchorElement>() &&
matches!(command, CommandName::CreateLink | CommandName::Unlink)
{
element.remove_attribute_by_name(&local_name!("href"), CanGc::from_cx(cx));
}
// Step 10. If element's specified command value for command is null,
// return the empty list.
if element.specified_command_value(command).is_none() {
// TODO
}
// Step 11. Set the tag name of element to "span",
// and return the one-node list consisting of the result.
// TODO
}
/// There is no specification for this implementation. Instead, it is
/// reverse-engineered based on the WPT test
/// /selection/contenteditable/initial-selection-on-focus.tentative.html
pub(crate) fn handle_focus_state_for_contenteditable(&self, can_gc: CanGc) {
if !self.is_editing_host() {
return;
}
let document = self.owner_document();
let Some(selection) = document.GetSelection(can_gc) else {
return;
};
let range = self
.upcast::<Element>()
.ensure_contenteditable_selection_range(&document, can_gc);
// If the current range is already associated with this contenteditable
// element, then we shouldn't do anything. This is important when focus
// is lost and regained, but selection was changed beforehand. In that
// case, we should maintain the selection as it were, by not creating
// a new range.
if selection
.active_range()
.is_some_and(|active| active == range)
{
return;
}
let node = self.upcast::<Node>();
let mut selected_node = DomRoot::from_ref(node);
let mut previous_eligible_node = DomRoot::from_ref(node);
let mut previous_node = DomRoot::from_ref(node);
let mut selected_offset = 0;
for child in node.traverse_preorder(ShadowIncluding::Yes) {
if let Some(text) = child.downcast::<Text>() {
// Note that to consider it whitespace, it needs to take more
// into account than simply "it has a non-whitespace" character.
// Therefore, we need to first check if it is not a whitespace
// node and only then can we find what the relevant character is.
if !text.is_whitespace_node() {
// A node with "white-space: pre" set must select its first
// character, regardless if that's a whitespace character or not.
let is_pre_formatted_text_node = child
.GetParentElement()
.and_then(|parent| parent.style())
.is_some_and(|style| {
style.get_inherited_text().white_space_collapse ==
WhiteSpaceCollapse::Preserve
});
if !is_pre_formatted_text_node {
// If it isn't pre-formatted, then we should instead select the
// first non-whitespace character.
selected_offset = text
.data()
.find(|c: char| !c.is_whitespace())
.unwrap_or_default() as u32;
}
selected_node = child;
break;
}
}
// For <input>, <textarea>, <hr> and <br> elements, we should select the previous
// node, regardless if it was a block node or not
if matches!(
child.type_id(),
NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLInputElement,
)) | NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLTextAreaElement,
)) | NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLHRElement,
)) | NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLBRElement,
))
) {
selected_node = previous_node;
break;
}
// When we encounter a non-contenteditable element, we should select the previous
// eligible node
if child
.downcast::<HTMLElement>()
.is_some_and(|el| el.ContentEditable().str() == "false")
{
selected_node = previous_eligible_node;
break;
}
// We can only select block nodes as eligible nodes for the case of non-conenteditable
// nodes
if child.is_block_node() {
previous_eligible_node = child.clone();
}
previous_node = child;
}
range.set_start(&selected_node, selected_offset);
range.set_end(&selected_node, selected_offset);
selection.AddRange(&range);
}
}