mirror of
https://github.com/servo/servo
synced 2026-04-25 17:15:48 +02:00
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>
This commit is contained in:
committed by
GitHub
parent
93c9918cd5
commit
ec513ff851
@@ -13,7 +13,6 @@ use script_bindings::script_runtime::CanGc;
|
||||
use servo_constellation_traits::ScriptToConstellationMessage;
|
||||
|
||||
use crate::dom::bindings::cell::DomRefCell;
|
||||
use crate::dom::execcommand::contenteditable::ContentEditableRange;
|
||||
use crate::dom::focusevent::FocusEventType;
|
||||
use crate::dom::types::{Element, EventTarget, FocusEvent, HTMLElement, HTMLIFrameElement, Window};
|
||||
use crate::dom::{Document, Event, EventBubbles, EventCancelable, Node, NodeTraits};
|
||||
|
||||
@@ -7,9 +7,8 @@ use script_bindings::inheritance::Castable;
|
||||
use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
|
||||
use crate::dom::bindings::codegen::Bindings::SelectionBinding::SelectionMethods;
|
||||
use crate::dom::document::Document;
|
||||
use crate::dom::execcommand::contenteditable::{
|
||||
NodeExecCommandSupport, SelectionDeleteDirection, SelectionExecCommandSupport, split_the_parent,
|
||||
};
|
||||
use crate::dom::execcommand::contenteditable::node::split_the_parent;
|
||||
use crate::dom::execcommand::contenteditable::selection::SelectionDeleteDirection;
|
||||
use crate::dom::html::htmlanchorelement::HTMLAnchorElement;
|
||||
use crate::dom::html::htmlbrelement::HTMLBRElement;
|
||||
use crate::dom::html::htmlhrelement::HTMLHRElement;
|
||||
|
||||
@@ -12,9 +12,6 @@ use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
|
||||
use crate::dom::bindings::str::DOMString;
|
||||
use crate::dom::document::Document;
|
||||
use crate::dom::execcommand::basecommand::CommandName;
|
||||
use crate::dom::execcommand::contenteditable::{
|
||||
NodeExecCommandSupport, SelectionExecCommandSupport,
|
||||
};
|
||||
use crate::dom::selection::Selection;
|
||||
use crate::script_runtime::CanGc;
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/* 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 js::context::JSContext;
|
||||
|
||||
use crate::dom::bindings::codegen::Bindings::DocumentBinding::{
|
||||
DocumentMethods, ElementCreationOptions,
|
||||
};
|
||||
use crate::dom::bindings::codegen::UnionTypes::StringOrElementCreationOptions;
|
||||
use crate::dom::bindings::root::DomRoot;
|
||||
use crate::dom::document::Document;
|
||||
use crate::dom::element::Element;
|
||||
|
||||
impl Document {
|
||||
pub(crate) fn create_element(&self, cx: &mut JSContext, name: &str) -> DomRoot<Element> {
|
||||
let element_options =
|
||||
StringOrElementCreationOptions::ElementCreationOptions(ElementCreationOptions {
|
||||
is: None,
|
||||
});
|
||||
self.CreateElement(cx, name.into(), element_options)
|
||||
.expect("Must always be able to create element")
|
||||
}
|
||||
}
|
||||
227
components/script/dom/execcommand/contenteditable/element.rs
Normal file
227
components/script/dom/execcommand/contenteditable/element.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
/* 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 script_bindings::inheritance::Castable;
|
||||
use style::attr::AttrValue;
|
||||
use style::properties::{LonghandId, PropertyDeclaration, PropertyDeclarationId};
|
||||
use style::values::specified::TextDecorationLine;
|
||||
use style::values::specified::box_::DisplayOutside;
|
||||
|
||||
use crate::dom::bindings::inheritance::{ElementTypeId, HTMLElementTypeId, NodeTypeId};
|
||||
use crate::dom::bindings::str::DOMString;
|
||||
use crate::dom::element::Element;
|
||||
use crate::dom::execcommand::basecommand::{CommandName, CssPropertyName};
|
||||
use crate::dom::execcommand::commands::fontsize::font_size_to_css_font;
|
||||
use crate::dom::html::htmlfontelement::HTMLFontElement;
|
||||
use crate::dom::node::node::{Node, NodeTraits};
|
||||
|
||||
impl Element {
|
||||
pub(crate) fn resolved_display_value(&self) -> Option<DisplayOutside> {
|
||||
self.style().map(|style| style.get_box().display.outside())
|
||||
}
|
||||
|
||||
/// <https://w3c.github.io/editing/docs/execCommand/#specified-command-value>
|
||||
pub(crate) fn specified_command_value(&self, command: &CommandName) -> Option<DOMString> {
|
||||
match command {
|
||||
// Step 1. If command is "backColor" or "hiliteColor" and the Element's display property does not have resolved value "inline", return null.
|
||||
CommandName::BackColor | CommandName::HiliteColor => {
|
||||
// TODO
|
||||
},
|
||||
// Step 2. If command is "createLink" or "unlink":
|
||||
CommandName::CreateLink | CommandName::Unlink => {
|
||||
// TODO
|
||||
},
|
||||
// Step 3. If command is "subscript" or "superscript":
|
||||
CommandName::Subscript | CommandName::Superscript => {
|
||||
// TODO
|
||||
},
|
||||
CommandName::Strikethrough => {
|
||||
// Step 4. If command is "strikethrough", and element has a style attribute set, and that attribute sets "text-decoration":
|
||||
// TODO
|
||||
// Step 5. If command is "strikethrough" and element is an s or strike element, return "line-through".
|
||||
// TODO
|
||||
},
|
||||
CommandName::Underline => {
|
||||
// Step 6. If command is "underline", and element has a style attribute set, and that attribute sets "text-decoration":
|
||||
// TODO
|
||||
// Step 7. If command is "underline" and element is a u element, return "underline".
|
||||
// TODO
|
||||
},
|
||||
_ => {},
|
||||
};
|
||||
// Step 8. Let property be the relevant CSS property for command.
|
||||
// Step 9. If property is null, return null.
|
||||
let property = command.relevant_css_property()?;
|
||||
// Step 10. If element has a style attribute set, and that attribute has the effect of setting property,
|
||||
// return the value that it sets property to.
|
||||
if let Some(value) = property.value_set_for_style(self) {
|
||||
return Some(value);
|
||||
}
|
||||
// Step 11. If element is a font element that has an attribute whose effect is to create a presentational hint for property,
|
||||
// return the value that the hint sets property to. (For a size of 7, this will be the non-CSS value "xxx-large".)
|
||||
if self.is::<HTMLFontElement>() {
|
||||
if let Some(font_size) = self.get_attribute(&local_name!("size")) {
|
||||
if let AttrValue::UInt(_, value) = *font_size.value() {
|
||||
return Some(font_size_to_css_font(&value).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 12. If element is in the following list, and property is equal to the CSS property name listed for it,
|
||||
// return the string listed for it.
|
||||
let element_name = self.local_name();
|
||||
match property {
|
||||
CssPropertyName::FontWeight
|
||||
if element_name == &local_name!("b") || element_name == &local_name!("strong") =>
|
||||
{
|
||||
Some("bold".into())
|
||||
},
|
||||
CssPropertyName::FontStyle
|
||||
if element_name == &local_name!("i") || element_name == &local_name!("em") =>
|
||||
{
|
||||
Some("italic".into())
|
||||
},
|
||||
// Step 13. Return null.
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// <https://w3c.github.io/editing/docs/execCommand/#simple-modifiable-element>
|
||||
pub(crate) fn is_simple_modifiable_element(&self) -> bool {
|
||||
let attrs = self.attrs();
|
||||
let attr_count = attrs.len();
|
||||
let type_id = self.upcast::<Node>().type_id();
|
||||
|
||||
if matches!(
|
||||
type_id,
|
||||
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
||||
HTMLElementTypeId::HTMLAnchorElement,
|
||||
)) | NodeTypeId::Element(ElementTypeId::HTMLElement(
|
||||
HTMLElementTypeId::HTMLFontElement,
|
||||
)) | NodeTypeId::Element(ElementTypeId::HTMLElement(
|
||||
HTMLElementTypeId::HTMLSpanElement,
|
||||
))
|
||||
) {
|
||||
// > It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u element with no attributes.
|
||||
//
|
||||
// TODO: All elements that are HTMLElement rather than a specific one
|
||||
if attr_count == 0 {
|
||||
return true;
|
||||
}
|
||||
|
||||
// > It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u element
|
||||
// > with exactly one attribute, which is style,
|
||||
// > which sets no CSS properties (including invalid or unrecognized properties).
|
||||
if attr_count == 1 &&
|
||||
self.attrs().first().expect("Size is 1").local_name() == &local_name!("style")
|
||||
{
|
||||
let style_attribute = self.style_attribute().borrow();
|
||||
if style_attribute.as_ref().is_some_and(|declarations| {
|
||||
let document = self.owner_document();
|
||||
let shared_lock = document.style_shared_lock();
|
||||
let read_lock = shared_lock.read();
|
||||
let style = declarations.read_with(&read_lock);
|
||||
|
||||
style.is_empty()
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if attr_count != 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let only_attribute = attrs.first().expect("Size is 1").local_name();
|
||||
|
||||
// > It is an a element with exactly one attribute, which is href.
|
||||
if matches!(
|
||||
type_id,
|
||||
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
||||
HTMLElementTypeId::HTMLAnchorElement,
|
||||
))
|
||||
) {
|
||||
return only_attribute == &local_name!("href");
|
||||
}
|
||||
|
||||
// > It is a font element with exactly one attribute, which is either color, face, or size.
|
||||
if matches!(
|
||||
type_id,
|
||||
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
||||
HTMLElementTypeId::HTMLFontElement,
|
||||
))
|
||||
) {
|
||||
return only_attribute == &local_name!("color") ||
|
||||
only_attribute == &local_name!("face") ||
|
||||
only_attribute == &local_name!("size");
|
||||
}
|
||||
|
||||
// > It is a b or strong element with exactly one attribute, which is style,
|
||||
// > and the style attribute sets exactly one CSS property
|
||||
// > (including invalid or unrecognized properties), which is "font-weight".
|
||||
// TODO
|
||||
|
||||
// > It is an i or em element with exactly one attribute, which is style,
|
||||
// > and the style attribute sets exactly one CSS property (including invalid or unrecognized properties),
|
||||
// > which is "font-style".
|
||||
// TODO
|
||||
|
||||
// > It is an a, font, or span element with exactly one attribute, which is style,
|
||||
// > and the style attribute sets exactly one CSS property (including invalid or unrecognized properties),
|
||||
// > and that property is not "text-decoration".
|
||||
if matches!(
|
||||
type_id,
|
||||
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
||||
HTMLElementTypeId::HTMLAnchorElement,
|
||||
)) | NodeTypeId::Element(ElementTypeId::HTMLElement(
|
||||
HTMLElementTypeId::HTMLFontElement,
|
||||
)) | NodeTypeId::Element(ElementTypeId::HTMLElement(
|
||||
HTMLElementTypeId::HTMLSpanElement,
|
||||
))
|
||||
) {
|
||||
if only_attribute != &local_name!("style") {
|
||||
return false;
|
||||
}
|
||||
let style_attribute = self.style_attribute().borrow();
|
||||
let Some(declarations) = style_attribute.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
let document = self.owner_document();
|
||||
let shared_lock = document.style_shared_lock();
|
||||
let read_lock = shared_lock.read();
|
||||
let style = declarations.read_with(&read_lock);
|
||||
|
||||
if style.len() == 1 {
|
||||
if let Some((text_decoration, _)) = style.get(PropertyDeclarationId::Longhand(
|
||||
LonghandId::TextDecorationLine,
|
||||
)) {
|
||||
// > It is an a, font, s, span, strike, or u element with exactly one attribute,
|
||||
// > which is style, and the style attribute sets exactly one CSS property
|
||||
// > (including invalid or unrecognized properties), which is "text-decoration",
|
||||
// > which is set to "line-through" or "underline" or "overline" or "none".
|
||||
//
|
||||
// TODO: Also the other element types
|
||||
return matches!(
|
||||
text_decoration,
|
||||
PropertyDeclaration::TextDecorationLine(
|
||||
TextDecorationLine::LINE_THROUGH |
|
||||
TextDecorationLine::UNDERLINE |
|
||||
TextDecorationLine::OVERLINE |
|
||||
TextDecorationLine::NONE
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// > It is an a, font, or span element with exactly one attribute, which is style,
|
||||
// > and the style attribute sets exactly one CSS property (including invalid or unrecognized properties),
|
||||
// > and that property is not "text-decoration".
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
220
components/script/dom/execcommand/contenteditable/htmlelement.rs
Normal file
220
components/script/dom/execcommand/contenteditable/htmlelement.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
/* 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);
|
||||
}
|
||||
}
|
||||
11
components/script/dom/execcommand/contenteditable/mod.rs
Normal file
11
components/script/dom/execcommand/contenteditable/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
/* 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/. */
|
||||
|
||||
mod document;
|
||||
mod element;
|
||||
pub(crate) mod htmlelement;
|
||||
pub(crate) mod node;
|
||||
mod range;
|
||||
pub(crate) mod selection;
|
||||
mod text;
|
||||
File diff suppressed because it is too large
Load Diff
256
components/script/dom/execcommand/contenteditable/range.rs
Normal file
256
components/script/dom/execcommand/contenteditable/range.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
/* 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 js::context::JSContext;
|
||||
use script_bindings::inheritance::Castable;
|
||||
|
||||
use crate::dom::bindings::codegen::Bindings::RangeBinding::RangeMethods;
|
||||
use crate::dom::bindings::root::DomRoot;
|
||||
use crate::dom::bindings::str::DOMString;
|
||||
use crate::dom::document::Document;
|
||||
use crate::dom::execcommand::basecommand::CommandName;
|
||||
use crate::dom::execcommand::commands::fontsize::legacy_font_size_for;
|
||||
use crate::dom::node::{Node, ShadowIncluding};
|
||||
use crate::dom::range::Range;
|
||||
use crate::dom::selection::Selection;
|
||||
use crate::dom::text::Text;
|
||||
|
||||
enum BoolOrOptionalString {
|
||||
Bool(bool),
|
||||
OptionalString(Option<DOMString>),
|
||||
}
|
||||
|
||||
impl From<Option<DOMString>> for BoolOrOptionalString {
|
||||
fn from(optional_string: Option<DOMString>) -> Self {
|
||||
Self::OptionalString(optional_string)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for BoolOrOptionalString {
|
||||
fn from(bool_: bool) -> Self {
|
||||
Self::Bool(bool_)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RecordedStateOfNode {
|
||||
command: CommandName,
|
||||
value: BoolOrOptionalString,
|
||||
}
|
||||
|
||||
impl RecordedStateOfNode {
|
||||
fn for_command_node(command: CommandName, node: &Node) -> Self {
|
||||
let value = node.effective_command_value(&command).into();
|
||||
Self { command, value }
|
||||
}
|
||||
}
|
||||
|
||||
impl Range {
|
||||
/// <https://w3c.github.io/editing/docs/execCommand/#effectively-contained>
|
||||
fn is_effectively_contained_node(&self, node: &Node) -> bool {
|
||||
// > A node node is effectively contained in a range range if range is not collapsed,
|
||||
if self.collapsed() {
|
||||
return false;
|
||||
}
|
||||
// > and at least one of the following holds:
|
||||
// > node is range's start node, it is a Text node, and its length is different from range's start offset.
|
||||
let start_container = self.start_container();
|
||||
if *start_container == *node && node.is::<Text>() && node.len() != self.start_offset() {
|
||||
return true;
|
||||
}
|
||||
// > node is range's end node, it is a Text node, and range's end offset is not 0.
|
||||
let end_container = self.end_container();
|
||||
if *end_container == *node && node.is::<Text>() && self.end_offset() != 0 {
|
||||
return true;
|
||||
}
|
||||
// > node is contained in range.
|
||||
if self.contains(node) {
|
||||
return true;
|
||||
}
|
||||
// > node has at least one child; and all its children are effectively contained in range;
|
||||
node.children_count() > 0 && node.children().all(|child| self.is_effectively_contained_node(&child))
|
||||
// > and either range's start node is not a descendant of node or is not a Text node or range's start offset is zero;
|
||||
&& (!node.is_ancestor_of(&start_container) || !start_container.is::<Text>() || self.start_offset() == 0)
|
||||
// > and either range's end node is not a descendant of node or is not a Text node or range's end offset is its end node's length.
|
||||
&& (!node.is_ancestor_of(&end_container) || !end_container.is::<Text>() || self.end_offset() == end_container.len())
|
||||
}
|
||||
|
||||
pub(crate) fn first_formattable_contained_node(&self) -> Option<DomRoot<Node>> {
|
||||
if self.collapsed() {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.CommonAncestorContainer()
|
||||
.traverse_preorder(ShadowIncluding::No)
|
||||
.find(|child| child.is_formattable() && self.is_effectively_contained_node(child))
|
||||
}
|
||||
|
||||
pub(crate) fn for_each_effectively_contained_child<Callback: FnMut(&Node)>(
|
||||
&self,
|
||||
mut callback: Callback,
|
||||
) {
|
||||
if self.collapsed() {
|
||||
return;
|
||||
}
|
||||
|
||||
for child in self
|
||||
.CommonAncestorContainer()
|
||||
.traverse_preorder(ShadowIncluding::No)
|
||||
{
|
||||
if self.is_effectively_contained_node(&child) {
|
||||
callback(&child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <https://w3c.github.io/editing/docs/execCommand/#record-current-states-and-values>
|
||||
pub(crate) fn record_current_states_and_values(&self) -> Vec<RecordedStateOfNode> {
|
||||
// Step 1. Let overrides be a list of (string, string or boolean) ordered pairs, initially empty.
|
||||
//
|
||||
// We return the vec in one go for the relevant values
|
||||
|
||||
// Step 2. Let node be the first formattable node effectively contained in the active range,
|
||||
// or null if there is none.
|
||||
let Some(node) = self.first_formattable_contained_node() else {
|
||||
// Step 3. If node is null, return overrides.
|
||||
return vec![];
|
||||
};
|
||||
// Step 8. Return overrides.
|
||||
vec![
|
||||
// Step 4. Add ("createLink", node's effective command value for "createLink") to overrides.
|
||||
RecordedStateOfNode::for_command_node(CommandName::CreateLink, &node),
|
||||
// Step 5. For each command in the list
|
||||
// "bold", "italic", "strikethrough", "subscript", "superscript", "underline", in order:
|
||||
// if node's effective command value for command is one of its inline command activated values,
|
||||
// add (command, true) to overrides, and otherwise add (command, false) to overrides.
|
||||
// TODO
|
||||
|
||||
// Step 6. For each command in the list "fontName", "foreColor", "hiliteColor", in order:
|
||||
// add (command, command's value) to overrides.
|
||||
// TODO
|
||||
|
||||
// Step 7. Add ("fontSize", node's effective command value for "fontSize") to overrides.
|
||||
RecordedStateOfNode::for_command_node(CommandName::FontSize, &node),
|
||||
]
|
||||
}
|
||||
|
||||
/// <https://w3c.github.io/editing/docs/execCommand/#restore-states-and-values>
|
||||
pub(crate) fn restore_states_and_values(
|
||||
&self,
|
||||
cx: &mut JSContext,
|
||||
selection: &Selection,
|
||||
context_object: &Document,
|
||||
overrides: Vec<RecordedStateOfNode>,
|
||||
) {
|
||||
// Step 1. Let node be the first formattable node effectively contained in the active range,
|
||||
// or null if there is none.
|
||||
let mut first_formattable_contained_node = self.first_formattable_contained_node();
|
||||
for override_state in overrides {
|
||||
// Step 2. If node is not null, then for each (command, override) pair in overrides, in order:
|
||||
if let Some(ref node) = first_formattable_contained_node {
|
||||
match override_state.value {
|
||||
// Step 2.1. If override is a boolean, and queryCommandState(command)
|
||||
// returns something different from override, take the action for command,
|
||||
// with value equal to the empty string.
|
||||
BoolOrOptionalString::Bool(bool_)
|
||||
if override_state
|
||||
.command
|
||||
.current_state(context_object)
|
||||
.is_some_and(|value| value != bool_) =>
|
||||
{
|
||||
override_state
|
||||
.command
|
||||
.execute(cx, context_object, selection, "".into());
|
||||
},
|
||||
BoolOrOptionalString::OptionalString(optional_string) => {
|
||||
match override_state.command {
|
||||
// Step 2.3. Otherwise, if override is a string; and command is "createLink";
|
||||
// and either there is a value override for "createLink" that is not equal to override,
|
||||
// or there is no value override for "createLink" and node's effective command value
|
||||
// for "createLink" is not equal to override: take the action for "createLink", with value equal to override.
|
||||
CommandName::CreateLink => {
|
||||
let value_override =
|
||||
context_object.value_override(&CommandName::CreateLink);
|
||||
if value_override != optional_string {
|
||||
CommandName::CreateLink.execute(
|
||||
cx,
|
||||
context_object,
|
||||
selection,
|
||||
optional_string.unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
},
|
||||
// Step 2.4. Otherwise, if override is a string; and command is "fontSize";
|
||||
// and either there is a value override for "fontSize" that is not equal to override,
|
||||
// or there is no value override for "fontSize" and node's effective command value for "fontSize"
|
||||
// is not loosely equivalent to override:
|
||||
CommandName::FontSize => {
|
||||
let value_override =
|
||||
context_object.value_override(&CommandName::FontSize);
|
||||
if value_override != optional_string ||
|
||||
(value_override.is_none() &&
|
||||
!CommandName::FontSize.are_loosely_equivalent_values(
|
||||
node.effective_command_value(&CommandName::FontSize)
|
||||
.as_ref(),
|
||||
optional_string.as_ref(),
|
||||
))
|
||||
{
|
||||
// Step 2.5. Convert override to an integer number of pixels,
|
||||
// and set override to the legacy font size for the result.
|
||||
let pixels = optional_string
|
||||
.and_then(|value| value.parse::<i32>().ok())
|
||||
.map(|value| {
|
||||
legacy_font_size_for(value as f32, context_object)
|
||||
})
|
||||
.unwrap_or("7".into());
|
||||
// Step 2.6. Take the action for "fontSize", with value equal to override.
|
||||
CommandName::FontSize.execute(
|
||||
cx,
|
||||
context_object,
|
||||
selection,
|
||||
pixels,
|
||||
);
|
||||
}
|
||||
},
|
||||
// Step 2.2. Otherwise, if override is a string, and command is neither "createLink" nor "fontSize",
|
||||
// and queryCommandValue(command) returns something not equivalent to override,
|
||||
// take the action for command, with value equal to override.
|
||||
command
|
||||
if command.current_value(cx, context_object) != optional_string =>
|
||||
{
|
||||
command.execute(
|
||||
cx,
|
||||
context_object,
|
||||
selection,
|
||||
optional_string.unwrap_or_default(),
|
||||
);
|
||||
},
|
||||
// Step 2.5. Otherwise, continue this loop from the beginning.
|
||||
_ => {
|
||||
continue;
|
||||
},
|
||||
}
|
||||
},
|
||||
// Step 2.5. Otherwise, continue this loop from the beginning.
|
||||
_ => {
|
||||
continue;
|
||||
},
|
||||
}
|
||||
// Step 2.6. Set node to the first formattable node effectively contained in the active range, if there is one.
|
||||
first_formattable_contained_node = self.first_formattable_contained_node();
|
||||
} else {
|
||||
// Step 3. Otherwise, for each (command, override) pair in overrides, in order:
|
||||
// Step 3.1. If override is a boolean, set the state override for command to override.
|
||||
match override_state.value {
|
||||
BoolOrOptionalString::Bool(bool_) => {
|
||||
context_object.set_state_override(override_state.command, Some(bool_))
|
||||
},
|
||||
// Step 3.2. If override is a string, set the value override for command to override.
|
||||
BoolOrOptionalString::OptionalString(optional_string) => {
|
||||
context_object.set_value_override(override_state.command, optional_string)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
890
components/script/dom/execcommand/contenteditable/selection.rs
Normal file
890
components/script/dom/execcommand/contenteditable/selection.rs
Normal file
@@ -0,0 +1,890 @@
|
||||
/* 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::cmp::Ordering;
|
||||
|
||||
use js::context::JSContext;
|
||||
use script_bindings::inheritance::Castable;
|
||||
|
||||
use crate::dom::abstractrange::bp_position;
|
||||
use crate::dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterDataMethods;
|
||||
use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
|
||||
use crate::dom::bindings::codegen::Bindings::SelectionBinding::SelectionMethods;
|
||||
use crate::dom::bindings::codegen::Bindings::TextBinding::TextMethods;
|
||||
use crate::dom::bindings::root::{DomRoot, DomSlice};
|
||||
use crate::dom::bindings::str::DOMString;
|
||||
use crate::dom::characterdata::CharacterData;
|
||||
use crate::dom::document::Document;
|
||||
use crate::dom::execcommand::basecommand::CommandName;
|
||||
use crate::dom::execcommand::contenteditable::node::{
|
||||
NodeOrString, is_allowed_child, move_preserving_ranges, split_the_parent,
|
||||
};
|
||||
use crate::dom::html::htmlbrelement::HTMLBRElement;
|
||||
use crate::dom::html::htmlelement::HTMLElement;
|
||||
use crate::dom::html::htmltablecellelement::HTMLTableCellElement;
|
||||
use crate::dom::html::htmltablerowelement::HTMLTableRowElement;
|
||||
use crate::dom::html::htmltablesectionelement::HTMLTableSectionElement;
|
||||
use crate::dom::node::Node;
|
||||
use crate::dom::selection::Selection;
|
||||
use crate::dom::text::Text;
|
||||
use crate::script_runtime::CanGc;
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
pub(crate) enum SelectionDeletionBlockMerging {
|
||||
#[default]
|
||||
Merge,
|
||||
Skip,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
pub(crate) enum SelectionDeletionStripWrappers {
|
||||
#[default]
|
||||
Strip,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
pub(crate) enum SelectionDeleteDirection {
|
||||
#[default]
|
||||
Forward,
|
||||
Backward,
|
||||
}
|
||||
|
||||
trait EquivalentPoint {
|
||||
fn previous_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)>;
|
||||
fn next_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)>;
|
||||
fn first_equivalent_point(self) -> (DomRoot<Node>, u32);
|
||||
fn last_equivalent_point(self) -> (DomRoot<Node>, u32);
|
||||
}
|
||||
|
||||
impl EquivalentPoint for (DomRoot<Node>, u32) {
|
||||
/// <https://w3c.github.io/editing/docs/execCommand/#previous-equivalent-point>
|
||||
fn previous_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)> {
|
||||
let (node, offset) = self;
|
||||
// Step 1. If node's length is zero, return null.
|
||||
let len = node.len();
|
||||
if len == 0 {
|
||||
return None;
|
||||
}
|
||||
// Step 2. If offset is 0, and node's parent is not null, and node is an inline node,
|
||||
// return (node's parent, node's index).
|
||||
if *offset == 0 && node.is_inline_node() {
|
||||
if let Some(parent) = node.GetParentNode() {
|
||||
return Some((parent, node.index()));
|
||||
}
|
||||
}
|
||||
// Step 3. If node has a child with index offset − 1, and that child's length is not zero,
|
||||
// and that child is an inline node, return (that child, that child's length).
|
||||
if *offset > 0 {
|
||||
if let Some(child) = node.children().nth(*offset as usize - 1) {
|
||||
if !child.is_empty() && child.is_inline_node() {
|
||||
let len = child.len();
|
||||
return Some((child, len));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4. Return null.
|
||||
None
|
||||
}
|
||||
|
||||
/// <https://w3c.github.io/editing/docs/execCommand/#next-equivalent-point>
|
||||
fn next_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)> {
|
||||
let (node, offset) = self;
|
||||
// Step 1. If node's length is zero, return null.
|
||||
let len = node.len();
|
||||
if len == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Step 2.
|
||||
//
|
||||
// This step does not exist in the spec
|
||||
|
||||
// Step 3. If offset is node's length, and node's parent is not null, and node is an inline node,
|
||||
// return (node's parent, 1 + node's index).
|
||||
if *offset == len && node.is_inline_node() {
|
||||
if let Some(parent) = node.GetParentNode() {
|
||||
return Some((parent, node.index() + 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4.
|
||||
//
|
||||
// This step does not exist in the spec
|
||||
|
||||
// Step 5. If node has a child with index offset, and that child's length is not zero,
|
||||
// and that child is an inline node, return (that child, 0).
|
||||
if let Some(child) = node.children().nth(*offset as usize) {
|
||||
if !child.is_empty() && child.is_inline_node() {
|
||||
return Some((child, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6.
|
||||
//
|
||||
// This step does not exist in the spec
|
||||
|
||||
// Step 7. Return null.
|
||||
None
|
||||
}
|
||||
|
||||
/// <https://w3c.github.io/editing/docs/execCommand/#first-equivalent-point>
|
||||
fn first_equivalent_point(self) -> (DomRoot<Node>, u32) {
|
||||
let mut previous_equivalent_point = self;
|
||||
// Step 1. While (node, offset)'s previous equivalent point is not null, set (node, offset) to its previous equivalent point.
|
||||
loop {
|
||||
if let Some(next) = previous_equivalent_point.previous_equivalent_point() {
|
||||
previous_equivalent_point = next;
|
||||
} else {
|
||||
// Step 2. Return (node, offset).
|
||||
return previous_equivalent_point;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <https://w3c.github.io/editing/docs/execCommand/#last-equivalent-point>
|
||||
fn last_equivalent_point(self) -> (DomRoot<Node>, u32) {
|
||||
let mut next_equivalent_point = self;
|
||||
// Step 1. While (node, offset)'s next equivalent point is not null, set (node, offset) to its next equivalent point.
|
||||
loop {
|
||||
if let Some(next) = next_equivalent_point.next_equivalent_point() {
|
||||
next_equivalent_point = next;
|
||||
} else {
|
||||
// Step 2. Return (node, offset).
|
||||
return next_equivalent_point;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
/// <https://w3c.github.io/editing/docs/execCommand/#delete-the-selection>
|
||||
pub(crate) fn delete_the_selection(
|
||||
&self,
|
||||
cx: &mut JSContext,
|
||||
context_object: &Document,
|
||||
block_merging: SelectionDeletionBlockMerging,
|
||||
strip_wrappers: SelectionDeletionStripWrappers,
|
||||
direction: SelectionDeleteDirection,
|
||||
) {
|
||||
// Step 1. If the active range is null, abort these steps and do nothing.
|
||||
let Some(active_range) = self.active_range() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Step 2. Canonicalize whitespace at the active range's start.
|
||||
active_range
|
||||
.start_container()
|
||||
.canonicalize_whitespace(active_range.start_offset(), true);
|
||||
|
||||
// Step 3. Canonicalize whitespace at the active range's end.
|
||||
active_range
|
||||
.end_container()
|
||||
.canonicalize_whitespace(active_range.end_offset(), true);
|
||||
|
||||
// Step 4. Let (start node, start offset) be the last equivalent point for the active range's start.
|
||||
let (mut start_node, mut start_offset) =
|
||||
(active_range.start_container(), active_range.start_offset()).last_equivalent_point();
|
||||
|
||||
// Step 5. Let (end node, end offset) be the first equivalent point for the active range's end.
|
||||
let (mut end_node, mut end_offset) =
|
||||
(active_range.end_container(), active_range.end_offset()).first_equivalent_point();
|
||||
|
||||
// Step 6. If (end node, end offset) is not after (start node, start offset):
|
||||
if bp_position(&end_node, end_offset, &start_node, start_offset) != Some(Ordering::Greater)
|
||||
{
|
||||
// Step 6.1. If direction is "forward", call collapseToStart() on the context object's selection.
|
||||
if direction == SelectionDeleteDirection::Forward {
|
||||
if self.CollapseToStart(CanGc::from_cx(cx)).is_err() {
|
||||
unreachable!("Should be able to collapse to start");
|
||||
}
|
||||
} else {
|
||||
// Step 6.2. Otherwise, call collapseToEnd() on the context object's selection.
|
||||
if self.CollapseToEnd(CanGc::from_cx(cx)).is_err() {
|
||||
unreachable!("Should be able to collapse to end");
|
||||
}
|
||||
}
|
||||
// Step 6.3. Abort these steps.
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 7. If start node is a Text node and start offset is 0, set start offset to the index of start node,
|
||||
// then set start node to its parent.
|
||||
if start_node.is::<Text>() && start_offset == 0 {
|
||||
start_offset = start_node.index();
|
||||
start_node = start_node
|
||||
.GetParentNode()
|
||||
.expect("Must always have a parent");
|
||||
}
|
||||
|
||||
// Step 8. If end node is a Text node and end offset is its length, set end offset to one plus the index of end node,
|
||||
// then set end node to its parent.
|
||||
if end_node.is::<Text>() && end_offset == end_node.len() {
|
||||
end_offset = end_node.index() + 1;
|
||||
end_node = end_node.GetParentNode().expect("Must always have a parent");
|
||||
}
|
||||
|
||||
// Step 9. Call collapse(start node, start offset) on the context object's selection.
|
||||
if self
|
||||
.Collapse(Some(&start_node), start_offset, CanGc::from_cx(cx))
|
||||
.is_err()
|
||||
{
|
||||
unreachable!("Must always be able to collapse");
|
||||
}
|
||||
|
||||
// Step 10. Call extend(end node, end offset) on the context object's selection.
|
||||
if self
|
||||
.Extend(&end_node, end_offset, CanGc::from_cx(cx))
|
||||
.is_err()
|
||||
{
|
||||
unreachable!("Must always be able to extend");
|
||||
}
|
||||
|
||||
// Step 11.
|
||||
//
|
||||
// This step does not exist in the spec
|
||||
|
||||
// Step 12. Let start block be the active range's start node.
|
||||
let Some(active_range) = self.active_range() else {
|
||||
return;
|
||||
};
|
||||
let mut start_block = active_range.start_container();
|
||||
|
||||
// Step 13. While start block's parent is in the same editing host and start block is an inline node,
|
||||
// set start block to its parent.
|
||||
loop {
|
||||
if start_block.is_inline_node() {
|
||||
if let Some(parent) = start_block.GetParentNode() {
|
||||
if parent.same_editing_host(&start_node) {
|
||||
start_block = parent;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Step 14. If start block is neither a block node nor an editing host,
|
||||
// or "span" is not an allowed child of start block,
|
||||
// or start block is a td or th, set start block to null.
|
||||
let start_block = if (!start_block.is_block_node() && !start_block.is_editing_host()) ||
|
||||
!is_allowed_child(
|
||||
NodeOrString::String("span".to_owned()),
|
||||
NodeOrString::Node(start_block.clone()),
|
||||
) ||
|
||||
start_block.is::<HTMLTableCellElement>()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(start_block)
|
||||
};
|
||||
|
||||
// Step 15. Let end block be the active range's end node.
|
||||
let mut end_block = active_range.end_container();
|
||||
|
||||
// Step 16. While end block's parent is in the same editing host and end block is an inline node, set end block to its parent.
|
||||
loop {
|
||||
if end_block.is_inline_node() {
|
||||
if let Some(parent) = end_block.GetParentNode() {
|
||||
if parent.same_editing_host(&end_block) {
|
||||
end_block = parent;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Step 17. If end block is neither a block node nor an editing host, or "span" is not an allowed child of end block,
|
||||
// or end block is a td or th, set end block to null.
|
||||
let end_block = if (!end_block.is_block_node() && !end_block.is_editing_host()) ||
|
||||
!is_allowed_child(
|
||||
NodeOrString::String("span".to_owned()),
|
||||
NodeOrString::Node(end_block.clone()),
|
||||
) ||
|
||||
end_block.is::<HTMLTableCellElement>()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(end_block)
|
||||
};
|
||||
|
||||
// Step 18.
|
||||
//
|
||||
// This step does not exist in the spec
|
||||
|
||||
// Step 19. Record current states and values, and let overrides be the result.
|
||||
let overrides = active_range.record_current_states_and_values();
|
||||
|
||||
// Step 20.
|
||||
//
|
||||
// This step does not exist in the spec
|
||||
|
||||
// Step 21. If start node and end node are the same, and start node is an editable Text node:
|
||||
if start_node == end_node && start_node.is_editable() {
|
||||
if let Some(start_text) = start_node.downcast::<Text>() {
|
||||
// Step 21.1. Call deleteData(start offset, end offset − start offset) on start node.
|
||||
if start_text
|
||||
.upcast::<CharacterData>()
|
||||
.DeleteData(start_offset, end_offset - start_offset)
|
||||
.is_err()
|
||||
{
|
||||
unreachable!("Must always be able to delete");
|
||||
}
|
||||
// Step 21.2. Canonicalize whitespace at (start node, start offset), with fix collapsed space false.
|
||||
start_node.canonicalize_whitespace(start_offset, false);
|
||||
// Step 21.3. If direction is "forward", call collapseToStart() on the context object's selection.
|
||||
if direction == SelectionDeleteDirection::Forward {
|
||||
if self.CollapseToStart(CanGc::from_cx(cx)).is_err() {
|
||||
unreachable!("Should be able to collapse to start");
|
||||
}
|
||||
} else {
|
||||
// Step 21.4. Otherwise, call collapseToEnd() on the context object's selection.
|
||||
if self.CollapseToEnd(CanGc::from_cx(cx)).is_err() {
|
||||
unreachable!("Should be able to collapse to end");
|
||||
}
|
||||
}
|
||||
// Step 21.5. Restore states and values from overrides.
|
||||
active_range.restore_states_and_values(cx, self, context_object, overrides);
|
||||
|
||||
// Step 21.6. Abort these steps.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 22. If start node is an editable Text node, call deleteData() on it, with start offset as
|
||||
// the first argument and (length of start node − start offset) as the second argument.
|
||||
if start_node.is_editable() {
|
||||
if let Some(start_text) = start_node.downcast::<Text>() {
|
||||
if start_text
|
||||
.upcast::<CharacterData>()
|
||||
.DeleteData(start_offset, start_node.len() - start_offset)
|
||||
.is_err()
|
||||
{
|
||||
unreachable!("Must always be able to delete");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 23. Let node list be a list of nodes, initially empty.
|
||||
rooted_vec!(let mut node_list);
|
||||
|
||||
// Step 24. For each node contained in the active range, append node to node list if the
|
||||
// last member of node list (if any) is not an ancestor of node; node is editable;
|
||||
// and node is not a thead, tbody, tfoot, tr, th, or td.
|
||||
let Ok(contained_children) = active_range.contained_children() else {
|
||||
unreachable!("Must always have contained children");
|
||||
};
|
||||
for node in contained_children.contained_children {
|
||||
// This type is only used to tell the compiler how to handle the type of `node_list.last()`.
|
||||
// It is not allowed to add a `& DomRoot<Node>` annotation, as test-tidy disallows that.
|
||||
// However, if we omit the type, the compiler doesn't know what it is, since we also
|
||||
// aren't allowed to add a type annotation to `node_list` itself, as that is handled
|
||||
// by the `rooted_vec` macro. Lastly, we also can't make it `&Node`, since then the compiler
|
||||
// thinks that the contents of the `RootedVec` is `Node`, whereas it is should be
|
||||
// `RootedVec<DomRoot<Node>>`. The type alias here doesn't upset test-tidy,
|
||||
// while also providing the necessary information to the compiler to work.
|
||||
type DomRootNode = DomRoot<Node>;
|
||||
if node.is_editable() &&
|
||||
!(node.is::<HTMLTableSectionElement>() ||
|
||||
node.is::<HTMLTableRowElement>() ||
|
||||
node.is::<HTMLTableCellElement>()) &&
|
||||
node_list
|
||||
.last()
|
||||
.is_none_or(|last: &DomRootNode| !last.is_ancestor_of(&node))
|
||||
{
|
||||
node_list.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 25. For each node in node list:
|
||||
for node in node_list.iter() {
|
||||
// Step 25.1. Let parent be the parent of node.
|
||||
let parent = node.GetParentNode().expect("Must always have a parent");
|
||||
// Step 25.2. Remove node from parent.
|
||||
assert!(node.has_parent());
|
||||
node.remove_self(cx);
|
||||
// Step 25.3. If the block node of parent has no visible children, and parent is editable or an editing host,
|
||||
// call createElement("br") on the context object and append the result as the last child of parent.
|
||||
if parent
|
||||
.block_node_of()
|
||||
.is_some_and(|block_node| block_node.children().all(|child| child.is_invisible())) &&
|
||||
parent.is_editable_or_editing_host()
|
||||
{
|
||||
let br = context_object.create_element(cx, "br");
|
||||
if parent.AppendChild(cx, br.upcast()).is_err() {
|
||||
unreachable!("Must always be able to append");
|
||||
}
|
||||
}
|
||||
// Step 25.4. If strip wrappers is true or parent is not an inclusive ancestor of start node,
|
||||
// while parent is an editable inline node with length 0, let grandparent be the parent of parent,
|
||||
// then remove parent from grandparent, then set parent to grandparent.
|
||||
if strip_wrappers == SelectionDeletionStripWrappers::Strip ||
|
||||
!parent.is_inclusive_ancestor_of(&start_node)
|
||||
{
|
||||
let mut parent = parent;
|
||||
loop {
|
||||
if parent.is_editable() && parent.is_inline_node() && parent.is_empty() {
|
||||
let grand_parent =
|
||||
parent.GetParentNode().expect("Must always have a parent");
|
||||
assert!(parent.has_parent());
|
||||
parent.remove_self(cx);
|
||||
parent = grand_parent;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 26. If end node is an editable Text node, call deleteData(0, end offset) on it.
|
||||
if end_node.is_editable() {
|
||||
if let Some(end_text) = end_node.downcast::<Text>() {
|
||||
if end_text
|
||||
.upcast::<CharacterData>()
|
||||
.DeleteData(0, end_offset)
|
||||
.is_err()
|
||||
{
|
||||
unreachable!("Must always be able to delete");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 27. Canonicalize whitespace at the active range's start, with fix collapsed space false.
|
||||
active_range
|
||||
.start_container()
|
||||
.canonicalize_whitespace(active_range.start_offset(), false);
|
||||
|
||||
// Step 28. Canonicalize whitespace at the active range's end, with fix collapsed space false.
|
||||
active_range
|
||||
.end_container()
|
||||
.canonicalize_whitespace(active_range.end_offset(), false);
|
||||
|
||||
// Step 29.
|
||||
//
|
||||
// This step does not exist in the spec
|
||||
|
||||
// Step 30. If block merging is false, or start block or end block is null, or start block is not
|
||||
// in the same editing host as end block, or start block and end block are the same:
|
||||
if block_merging == SelectionDeletionBlockMerging::Skip ||
|
||||
start_block.as_ref().zip(end_block.as_ref()).is_none_or(
|
||||
|(start_block, end_block)| {
|
||||
start_block == end_block || !start_block.same_editing_host(end_block)
|
||||
},
|
||||
)
|
||||
{
|
||||
// Step 30.1. If direction is "forward", call collapseToStart() on the context object's selection.
|
||||
if direction == SelectionDeleteDirection::Forward {
|
||||
if self.CollapseToStart(CanGc::from_cx(cx)).is_err() {
|
||||
unreachable!("Should be able to collapse to start");
|
||||
}
|
||||
} else {
|
||||
// Step 30.2. Otherwise, call collapseToEnd() on the context object's selection.
|
||||
if self.CollapseToEnd(CanGc::from_cx(cx)).is_err() {
|
||||
unreachable!("Should be able to collapse to end");
|
||||
}
|
||||
}
|
||||
// Step 30.3. Restore states and values from overrides.
|
||||
active_range.restore_states_and_values(cx, self, context_object, overrides);
|
||||
|
||||
// Step 30.4. Abort these steps.
|
||||
return;
|
||||
}
|
||||
let start_block = start_block.expect("Already checked for None in previous statement");
|
||||
let end_block = end_block.expect("Already checked for None in previous statement");
|
||||
|
||||
// Step 31. If start block has one child, which is a collapsed block prop, remove its child from it.
|
||||
if start_block.children_count() == 1 {
|
||||
let Some(child) = start_block.children().nth(0) else {
|
||||
unreachable!("Must always have a single child");
|
||||
};
|
||||
if child.is_collapsed_block_prop() {
|
||||
assert!(child.has_parent());
|
||||
child.remove_self(cx);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 32. If start block is an ancestor of end block:
|
||||
if start_block.is_ancestor_of(&end_block) {
|
||||
// Step 32.1. Let reference node be end block.
|
||||
let mut reference_node = end_block.clone();
|
||||
// Step 32.2. While reference node is not a child of start block, set reference node to its parent.
|
||||
loop {
|
||||
if start_block.children().all(|child| child != reference_node) {
|
||||
reference_node = reference_node
|
||||
.GetParentNode()
|
||||
.expect("Must always have a parent, at least start_block");
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Step 32.3. Call collapse() on the context object's selection,
|
||||
// with first argument start block and second argument the index of reference node.
|
||||
if self
|
||||
.Collapse(
|
||||
Some(&start_block),
|
||||
reference_node.index(),
|
||||
CanGc::from_cx(cx),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
unreachable!("Must always be able to collapse");
|
||||
}
|
||||
// Step 32.4. If end block has no children:
|
||||
if end_block.children_count() == 0 {
|
||||
let mut end_block = end_block;
|
||||
// Step 32.4.1. While end block is editable and is the only child of its parent and is not a child of start block,
|
||||
// let parent equal end block, then remove end block from parent, then set end block to parent.
|
||||
loop {
|
||||
if end_block.is_editable() &&
|
||||
start_block.children().all(|child| child != end_block)
|
||||
{
|
||||
if let Some(parent) = end_block.GetParentNode() {
|
||||
if parent.children_count() == 1 {
|
||||
assert!(end_block.has_parent());
|
||||
end_block.remove_self(cx);
|
||||
end_block = parent;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Step 32.4.2. If end block is editable and is not an inline node,
|
||||
// and its previousSibling and nextSibling are both inline nodes,
|
||||
// call createElement("br") on the context object and insert it into end block's parent immediately after end block.
|
||||
if end_block.is_editable() &&
|
||||
!end_block.is_inline_node() &&
|
||||
end_block
|
||||
.GetPreviousSibling()
|
||||
.is_some_and(|previous| previous.is_inline_node())
|
||||
{
|
||||
if let Some(next_of_end_block) = end_block.GetNextSibling() {
|
||||
if next_of_end_block.is_inline_node() {
|
||||
let br = context_object.create_element(cx, "br");
|
||||
let parent = end_block
|
||||
.GetParentNode()
|
||||
.expect("Must always have a parent");
|
||||
if parent
|
||||
.InsertBefore(cx, br.upcast(), Some(&next_of_end_block))
|
||||
.is_err()
|
||||
{
|
||||
unreachable!("Must always be able to insert into parent");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Step 32.4.3. If end block is editable, remove it from its parent.
|
||||
if end_block.is_editable() {
|
||||
assert!(end_block.has_parent());
|
||||
end_block.remove_self(cx);
|
||||
}
|
||||
// Step 32.4.4. Restore states and values from overrides.
|
||||
active_range.restore_states_and_values(cx, self, context_object, overrides);
|
||||
|
||||
// Step 32.4.5. Abort these steps.
|
||||
return;
|
||||
}
|
||||
let first_child = end_block
|
||||
.children()
|
||||
.nth(0)
|
||||
.expect("Already checked at least 1 child in previous statement");
|
||||
// Step 32.5. If end block's firstChild is not an inline node,
|
||||
// restore states and values from record, then abort these steps.
|
||||
if !first_child.is_inline_node() {
|
||||
// TODO: Restore state
|
||||
return;
|
||||
}
|
||||
// Step 32.6. Let children be a list of nodes, initially empty.
|
||||
rooted_vec!(let mut children);
|
||||
// Step 32.7. Append the first child of end block to children.
|
||||
children.push(first_child.as_traced());
|
||||
// Step 32.8. While children's last member is not a br,
|
||||
// and children's last member's nextSibling is an inline node,
|
||||
// append children's last member's nextSibling to children.
|
||||
loop {
|
||||
let Some(last) = children.last() else {
|
||||
break;
|
||||
};
|
||||
if last.is::<HTMLBRElement>() {
|
||||
break;
|
||||
}
|
||||
let Some(next) = last.GetNextSibling() else {
|
||||
break;
|
||||
};
|
||||
if next.is_inline_node() {
|
||||
children.push(next.as_traced());
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Step 32.9. Record the values of children, and let values be the result.
|
||||
// TODO
|
||||
|
||||
// Step 32.10. While children's first member's parent is not start block,
|
||||
// split the parent of children.
|
||||
loop {
|
||||
if children
|
||||
.first()
|
||||
.and_then(|child| child.GetParentNode())
|
||||
.is_some_and(|parent_of_child| parent_of_child != start_block)
|
||||
{
|
||||
split_the_parent(cx, children.r());
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Step 32.11. If children's first member's previousSibling is an editable br,
|
||||
// remove that br from its parent.
|
||||
if let Some(first) = children.first() {
|
||||
if let Some(previous_of_first) = first.GetPreviousSibling() {
|
||||
if previous_of_first.is_editable() && previous_of_first.is::<HTMLBRElement>() {
|
||||
assert!(previous_of_first.has_parent());
|
||||
previous_of_first.remove_self(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Step 33. Otherwise, if start block is a descendant of end block:
|
||||
} else if end_block.is_ancestor_of(&start_block) {
|
||||
// Step 33.1. Call collapse() on the context object's selection,
|
||||
// with first argument start block and second argument start block's length.
|
||||
if self
|
||||
.Collapse(Some(&start_block), start_block.len(), CanGc::from_cx(cx))
|
||||
.is_err()
|
||||
{
|
||||
unreachable!("Must always be able to collapse");
|
||||
}
|
||||
// Step 33.2. Let reference node be start block.
|
||||
let mut reference_node = start_block.clone();
|
||||
// Step 33.3. While reference node is not a child of end block, set reference node to its parent.
|
||||
loop {
|
||||
if end_block.children().all(|child| child != reference_node) {
|
||||
if let Some(parent) = reference_node.GetParentNode() {
|
||||
reference_node = parent;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Step 33.4. If reference node's nextSibling is an inline node and start block's lastChild is a br,
|
||||
// remove start block's lastChild from it.
|
||||
if reference_node
|
||||
.GetNextSibling()
|
||||
.is_some_and(|next| next.is_inline_node())
|
||||
{
|
||||
if let Some(last) = start_block.children().last() {
|
||||
if last.is::<HTMLBRElement>() {
|
||||
assert!(last.has_parent());
|
||||
last.remove_self(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Step 33.5. Let nodes to move be a list of nodes, initially empty.
|
||||
rooted_vec!(let mut nodes_to_move);
|
||||
// Step 33.6. If reference node's nextSibling is neither null nor a block node,
|
||||
// append it to nodes to move.
|
||||
if let Some(next) = reference_node.GetNextSibling() {
|
||||
if !next.is_block_node() {
|
||||
nodes_to_move.push(next);
|
||||
}
|
||||
}
|
||||
// Step 33.7. While nodes to move is nonempty and its last member isn't a br
|
||||
// and its last member's nextSibling is neither null nor a block node,
|
||||
// append its last member's nextSibling to nodes to move.
|
||||
loop {
|
||||
if let Some(last) = nodes_to_move.last() {
|
||||
if !last.is::<HTMLBRElement>() {
|
||||
if let Some(next_of_last) = last.GetNextSibling() {
|
||||
if !next_of_last.is_block_node() {
|
||||
nodes_to_move.push(next_of_last);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Step 33.8. Record the values of nodes to move, and let values be the result.
|
||||
// TODO
|
||||
|
||||
// Step 33.9. For each node in nodes to move,
|
||||
// append node as the last child of start block, preserving ranges.
|
||||
for node in nodes_to_move.iter() {
|
||||
move_preserving_ranges(cx, node, |cx| start_block.AppendChild(cx, node));
|
||||
}
|
||||
// Step 34. Otherwise:
|
||||
} else {
|
||||
// Step 34.1. Call collapse() on the context object's selection,
|
||||
// with first argument start block and second argument start block's length.
|
||||
if self
|
||||
.Collapse(Some(&start_block), start_block.len(), CanGc::from_cx(cx))
|
||||
.is_err()
|
||||
{
|
||||
unreachable!("Must always be able to collapse");
|
||||
}
|
||||
// Step 34.2. If end block's firstChild is an inline node and start block's lastChild is a br,
|
||||
// remove start block's lastChild from it.
|
||||
if end_block
|
||||
.children()
|
||||
.nth(0)
|
||||
.is_some_and(|next| next.is_inline_node())
|
||||
{
|
||||
if let Some(last) = start_block.children().last() {
|
||||
if last.is::<HTMLBRElement>() {
|
||||
assert!(last.has_parent());
|
||||
last.remove_self(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Step 34.3. Record the values of end block's children, and let values be the result.
|
||||
// TODO
|
||||
|
||||
// Step 34.4. While end block has children,
|
||||
// append the first child of end block to start block, preserving ranges.
|
||||
loop {
|
||||
if let Some(first_child) = end_block.children().nth(0) {
|
||||
move_preserving_ranges(cx, &first_child, |cx| {
|
||||
start_block.AppendChild(cx, &first_child)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Step 34.5. While end block has no children,
|
||||
// let parent be the parent of end block, then remove end block from parent,
|
||||
// then set end block to parent.
|
||||
let mut end_block = end_block;
|
||||
loop {
|
||||
if end_block.children_count() == 0 {
|
||||
if let Some(parent) = end_block.GetParentNode() {
|
||||
assert!(end_block.has_parent());
|
||||
end_block.remove_self(cx);
|
||||
end_block = parent;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 35.
|
||||
//
|
||||
// This step does not exist in the spec
|
||||
|
||||
// Step 36. Let ancestor be start block.
|
||||
// TODO
|
||||
|
||||
// Step 37. While ancestor has an inclusive ancestor ol in the same editing host whose nextSibling is
|
||||
// also an ol in the same editing host, or an inclusive ancestor ul in the same editing host whose nextSibling
|
||||
// is also a ul in the same editing host:
|
||||
// TODO
|
||||
|
||||
// Step 38. Restore the values from values.
|
||||
// TODO
|
||||
|
||||
// Step 39. If start block has no children, call createElement("br") on the context object and
|
||||
// append the result as the last child of start block.
|
||||
if start_block.children_count() == 0 {
|
||||
let br = context_object.create_element(cx, "br");
|
||||
if start_block.AppendChild(cx, br.upcast()).is_err() {
|
||||
unreachable!("Must always be able to append");
|
||||
}
|
||||
}
|
||||
|
||||
// Step 40. Remove extraneous line breaks at the end of start block.
|
||||
start_block.remove_extraneous_line_breaks_at_the_end_of(cx);
|
||||
|
||||
// Step 41. Restore states and values from overrides.
|
||||
active_range.restore_states_and_values(cx, self, context_object, overrides);
|
||||
}
|
||||
|
||||
/// <https://w3c.github.io/editing/docs/execCommand/#set-the-selection%27s-value>
|
||||
pub(crate) fn set_the_selection_value(
|
||||
&self,
|
||||
cx: &mut JSContext,
|
||||
new_value: Option<DOMString>,
|
||||
command: CommandName,
|
||||
context_object: &Document,
|
||||
) {
|
||||
let active_range = self
|
||||
.active_range()
|
||||
.expect("Must always have an active range");
|
||||
|
||||
// Step 1. Let command be the current command.
|
||||
//
|
||||
// Passed as argument
|
||||
|
||||
// Step 2. If there is no formattable node effectively contained in the active range:
|
||||
if active_range.first_formattable_contained_node().is_none() {
|
||||
// Step 2.1. If command has inline command activated values, set the state override to true if new value is among them and false if it's not.
|
||||
// TODO
|
||||
|
||||
// Step 2.2. If command is "subscript", unset the state override for "superscript".
|
||||
if command == CommandName::Subscript {
|
||||
context_object.set_state_override(CommandName::Superscript, None);
|
||||
}
|
||||
// Step 2.3. If command is "superscript", unset the state override for "subscript".
|
||||
if command == CommandName::Superscript {
|
||||
context_object.set_state_override(CommandName::Subscript, None);
|
||||
}
|
||||
// Step 2.4. If new value is null, unset the value override (if any).
|
||||
// Step 2.5. Otherwise, if command is "createLink" or it has a value specified, set the value override to new value.
|
||||
context_object.set_value_override(command, new_value);
|
||||
// Step 2.6. Abort these steps.
|
||||
return;
|
||||
}
|
||||
// Step 3. If the active range's start node is an editable Text node,
|
||||
// and its start offset is neither zero nor its start node's length,
|
||||
// call splitText() on the active range's start node,
|
||||
// with argument equal to the active range's start offset.
|
||||
// Then set the active range's start node to the result, and its start offset to zero.
|
||||
let start_node = active_range.start_container();
|
||||
let start_offset = active_range.start_offset();
|
||||
if start_node.is_editable() && start_offset != 0 && start_offset != start_node.len() {
|
||||
if let Some(start_text) = start_node.downcast::<Text>() {
|
||||
let Ok(start_text) = start_text.SplitText(cx, start_offset) else {
|
||||
unreachable!("Must always be able to split");
|
||||
};
|
||||
active_range.set_start(start_text.upcast(), 0);
|
||||
}
|
||||
}
|
||||
// Step 4. If the active range's end node is an editable Text node,
|
||||
// and its end offset is neither zero nor its end node's length,
|
||||
// call splitText() on the active range's end node,
|
||||
// with argument equal to the active range's end offset.
|
||||
let end_node = active_range.end_container();
|
||||
let end_offset = active_range.end_offset();
|
||||
if end_node.is_editable() && end_offset != 0 && end_offset != end_node.len() {
|
||||
if let Some(end_text) = end_node.downcast::<Text>() {
|
||||
if end_text.SplitText(cx, end_offset).is_err() {
|
||||
unreachable!("Must always be able to split");
|
||||
};
|
||||
}
|
||||
}
|
||||
// Step 5. Let element list be all editable Elements effectively contained in the active range.
|
||||
// Step 6. For each element in element list, clear the value of element.
|
||||
active_range.for_each_effectively_contained_child(|child| {
|
||||
if child.is_editable() {
|
||||
if let Some(element_child) = child.downcast::<HTMLElement>() {
|
||||
element_child.clear_the_value(cx, &command);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Step 7. Let node list be all editable nodes effectively contained in the active range.
|
||||
// Step 8. For each node in node list:
|
||||
active_range.for_each_effectively_contained_child(|child| {
|
||||
if child.is_editable() {
|
||||
// Step 8.1. Push down values on node.
|
||||
child.push_down_values(cx, &command, new_value.clone());
|
||||
// Step 8.2. If node is an allowed child of "span", force the value of node.
|
||||
if is_allowed_child(
|
||||
NodeOrString::Node(DomRoot::from_ref(child)),
|
||||
NodeOrString::String("span".to_owned()),
|
||||
) {
|
||||
child.force_the_value(cx, &command, new_value.as_ref());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
157
components/script/dom/execcommand/contenteditable/text.rs
Normal file
157
components/script/dom/execcommand/contenteditable/text.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
/* 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 script_bindings::inheritance::Castable;
|
||||
use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
|
||||
|
||||
use crate::dom::bindings::cell::Ref;
|
||||
use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
|
||||
use crate::dom::characterdata::CharacterData;
|
||||
use crate::dom::element::Element;
|
||||
use crate::dom::html::htmlbrelement::HTMLBRElement;
|
||||
use crate::dom::html::htmlimageelement::HTMLImageElement;
|
||||
use crate::dom::node::Node;
|
||||
use crate::dom::text::Text;
|
||||
|
||||
impl Text {
|
||||
/// <https://dom.spec.whatwg.org/#concept-cd-data>
|
||||
pub(crate) fn data(&self) -> Ref<'_, String> {
|
||||
self.upcast::<CharacterData>().data()
|
||||
}
|
||||
|
||||
/// <https://w3c.github.io/editing/docs/execCommand/#whitespace-node>
|
||||
pub(crate) fn is_whitespace_node(&self) -> bool {
|
||||
// > A whitespace node is either a Text node whose data is the empty string;
|
||||
let data = self.data();
|
||||
if data.is_empty() {
|
||||
return true;
|
||||
}
|
||||
// > or a Text node whose data consists only of one or more tabs (0x0009), line feeds (0x000A),
|
||||
// > carriage returns (0x000D), and/or spaces (0x0020),
|
||||
// > and whose parent is an Element whose resolved value for "white-space" is "normal" or "nowrap";
|
||||
let Some(parent) = self.upcast::<Node>().GetParentElement() else {
|
||||
return false;
|
||||
};
|
||||
// TODO: Optimize the below to only do a traversal once and in the match handle the expected collapse value
|
||||
let Some(style) = parent.style() else {
|
||||
return false;
|
||||
};
|
||||
let white_space_collapse = style.get_inherited_text().white_space_collapse;
|
||||
if data
|
||||
.bytes()
|
||||
.all(|byte| matches!(byte, b'\t' | b'\n' | b'\r' | b' ')) &&
|
||||
// Note that for "normal" and "nowrap", the longhand "white-space-collapse: collapse" applies
|
||||
// https://www.w3.org/TR/css-text-4/#white-space-property
|
||||
white_space_collapse == WhiteSpaceCollapse::Collapse
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// > or a Text node whose data consists only of one or more tabs (0x0009), carriage returns (0x000D),
|
||||
// > and/or spaces (0x0020), and whose parent is an Element whose resolved value for "white-space" is "pre-line".
|
||||
data.bytes()
|
||||
.all(|byte| matches!(byte, b'\t' | b'\r' | b' ')) &&
|
||||
// Note that for "pre-line", the longhand "white-space-collapse: preserve-breaks" applies
|
||||
// https://www.w3.org/TR/css-text-4/#white-space-property
|
||||
white_space_collapse == WhiteSpaceCollapse::PreserveBreaks
|
||||
}
|
||||
|
||||
/// <https://w3c.github.io/editing/docs/execCommand/#collapsed-whitespace-node>
|
||||
pub(crate) fn is_collapsed_whitespace_node(&self) -> bool {
|
||||
// Step 1. If node is not a whitespace node, return false.
|
||||
if !self.is_whitespace_node() {
|
||||
return false;
|
||||
}
|
||||
// Step 2. If node's data is the empty string, return true.
|
||||
if self.data().is_empty() {
|
||||
return true;
|
||||
}
|
||||
// Step 3. Let ancestor be node's parent.
|
||||
let node = self.upcast::<Node>();
|
||||
let Some(ancestor) = node.GetParentNode() else {
|
||||
// Step 4. If ancestor is null, return true.
|
||||
return true;
|
||||
};
|
||||
let mut resolved_ancestor = ancestor.clone();
|
||||
for parent in ancestor.ancestors() {
|
||||
// Step 5. If the "display" property of some ancestor of node has resolved value "none", return true.
|
||||
if parent
|
||||
.downcast::<Element>()
|
||||
.is_some_and(Element::is_display_none)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// Step 6. While ancestor is not a block node and its parent is not null, set ancestor to its parent.
|
||||
//
|
||||
// Note that the spec is written as "while not". Since this is the end-condition, we need to invert
|
||||
// the condition to decide when to stop.
|
||||
if parent.is_block_node() {
|
||||
break;
|
||||
}
|
||||
resolved_ancestor = parent;
|
||||
}
|
||||
// Step 7. Let reference be node.
|
||||
// Step 8. While reference is a descendant of ancestor:
|
||||
// Step 8.1. Let reference be the node before it in tree order.
|
||||
for reference in node.preceding_nodes(&resolved_ancestor) {
|
||||
// Step 8.2. If reference is a block node or a br, return true.
|
||||
if reference.is_block_node() || reference.is::<HTMLBRElement>() {
|
||||
return true;
|
||||
}
|
||||
// Step 8.3. If reference is a Text node that is not a whitespace node, or is an img, break from this loop.
|
||||
if reference
|
||||
.downcast::<Text>()
|
||||
.is_some_and(|text| !text.is_whitespace_node()) ||
|
||||
reference.is::<HTMLImageElement>()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Step 9. Let reference be node.
|
||||
// Step 10. While reference is a descendant of ancestor:
|
||||
// Step 10.1. Let reference be the node after it in tree order, or null if there is no such node.
|
||||
for reference in node.following_nodes(&resolved_ancestor) {
|
||||
// Step 10.2. If reference is a block node or a br, return true.
|
||||
if reference.is_block_node() || reference.is::<HTMLBRElement>() {
|
||||
return true;
|
||||
}
|
||||
// Step 10.3. If reference is a Text node that is not a whitespace node, or is an img, break from this loop.
|
||||
if reference
|
||||
.downcast::<Text>()
|
||||
.is_some_and(|text| !text.is_whitespace_node()) ||
|
||||
reference.is::<HTMLImageElement>()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Step 11. Return false.
|
||||
false
|
||||
}
|
||||
|
||||
/// Part of <https://w3c.github.io/editing/docs/execCommand/#canonicalize-whitespace>
|
||||
/// and deduplicated here, since we need to do this for both start and end nodes
|
||||
pub(crate) fn has_whitespace_and_has_parent_with_whitespace_preserve(
|
||||
&self,
|
||||
offset: u32,
|
||||
space_characters: &'static [&'static char],
|
||||
) -> bool {
|
||||
// if node is a Text node and its parent's resolved value for "white-space" is neither "pre" nor "pre-wrap"
|
||||
// and start offset is not zero and the (start offset − 1)st code unit of start node's data is a space (0x0020) or
|
||||
// non-breaking space (0x00A0)
|
||||
let has_preserve_space = self
|
||||
.upcast::<Node>()
|
||||
.GetParentNode()
|
||||
.and_then(|parent_node| parent_node.downcast::<Element>().and_then(Element::style))
|
||||
.is_some_and(|style| {
|
||||
// Note that for "pre" and "pre-wrap", the longhand "white-space-collapse: preserve" applies
|
||||
// https://www.w3.org/TR/css-text-4/#white-space-property
|
||||
style.get_inherited_text().white_space_collapse != WhiteSpaceCollapse::Preserve
|
||||
});
|
||||
let has_space_character = self
|
||||
.data()
|
||||
.chars()
|
||||
.nth(offset as usize)
|
||||
.is_some_and(|c| space_characters.contains(&&c));
|
||||
has_preserve_space && has_space_character
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user