Files
servo/components/script/dom/selection.rs
Tim van der Lippe 92e2d00083 script: Implement check for supported and enabled commands (#42634)
The first step of `execCommand` commands is to figure out
if they are supported and enabled. Therefore, implement
these two pieces with only 1 command: delete.

The implementation of `delete` is currently mostly dummy,
to have at least something going. But the main part of
this change is to setup the infrastructure to figure out
when commands are supported and enabled.

For the first part, its simply the list of commands we
currently have implemented, which is only delete.

For the second part, we need to consider the active range
of the current selection and do various checks, as well as
check the presence of `contenteditable`.

Part of #25005

Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
2026-02-17 13:19:41 +00:00

535 lines
18 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 std::cell::Cell;
use dom_struct::dom_struct;
use crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, NodeMethods};
use crate::dom::bindings::codegen::Bindings::RangeBinding::RangeMethods;
use crate::dom::bindings::codegen::Bindings::SelectionBinding::SelectionMethods;
use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::refcounted::Trusted;
use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
use crate::dom::bindings::str::DOMString;
use crate::dom::document::Document;
use crate::dom::eventtarget::EventTarget;
use crate::dom::node::{Node, NodeTraits};
use crate::dom::range::Range;
use crate::script_runtime::CanGc;
#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
enum Direction {
Forwards,
Backwards,
Directionless,
}
#[dom_struct]
pub(crate) struct Selection {
reflector_: Reflector,
document: Dom<Document>,
range: MutNullableDom<Range>,
direction: Cell<Direction>,
task_queued: Cell<bool>,
}
impl Selection {
fn new_inherited(document: &Document) -> Selection {
Selection {
reflector_: Reflector::new(),
document: Dom::from_ref(document),
range: MutNullableDom::new(None),
direction: Cell::new(Direction::Directionless),
task_queued: Cell::new(false),
}
}
pub(crate) fn new(document: &Document, can_gc: CanGc) -> DomRoot<Selection> {
reflect_dom_object(
Box::new(Selection::new_inherited(document)),
&*document.global(),
can_gc,
)
}
fn set_range(&self, range: &Range) {
// If we are setting to literally the same Range object
// (not just the same positions), then there's nothing changing
// and no task to queue.
if let Some(existing) = self.range.get() {
if &*existing == range {
return;
}
}
self.range.set(Some(range));
range.associate_selection(self);
self.queue_selectionchange_task();
}
fn clear_range(&self) {
// If we already don't have a a Range object, then there's
// nothing changing and no task to queue.
if let Some(range) = self.range.get() {
range.disassociate_selection(self);
self.range.set(None);
self.queue_selectionchange_task();
}
}
pub(crate) fn queue_selectionchange_task(&self) {
if self.task_queued.get() {
// Spec doesn't specify not to queue multiple tasks,
// but it's much easier to code range operations if
// change notifications within a method are idempotent.
return;
}
let this = Trusted::new(self);
self.document
.owner_global()
.task_manager()
.user_interaction_task_source() // w3c/selection-api#117
.queue(
task!(selectionchange_task_steps: move || {
let this = this.root();
this.task_queued.set(false);
this.document.upcast::<EventTarget>().fire_event(atom!("selectionchange"), CanGc::note());
})
);
self.task_queued.set(true);
}
fn is_same_root(&self, node: &Node) -> bool {
&*node.GetRootNode(&GetRootNodeOptions::empty()) == self.document.upcast::<Node>()
}
/// <https://w3c.github.io/editing/docs/execCommand/#active-range>
pub(crate) fn active_range(&self) -> Option<DomRoot<Range>> {
// > The active range is the range of the selection given by calling getSelection() on the context object. (Thus the active range may be null.)
self.range.get()
}
}
impl SelectionMethods<crate::DomTypeHolder> for Selection {
/// <https://w3c.github.io/selection-api/#dom-selection-anchornode>
fn GetAnchorNode(&self) -> Option<DomRoot<Node>> {
if let Some(range) = self.range.get() {
match self.direction.get() {
Direction::Forwards => Some(range.start_container()),
_ => Some(range.end_container()),
}
} else {
None
}
}
/// <https://w3c.github.io/selection-api/#dom-selection-anchoroffset>
fn AnchorOffset(&self) -> u32 {
if let Some(range) = self.range.get() {
match self.direction.get() {
Direction::Forwards => range.start_offset(),
_ => range.end_offset(),
}
} else {
0
}
}
/// <https://w3c.github.io/selection-api/#dom-selection-focusnode>
fn GetFocusNode(&self) -> Option<DomRoot<Node>> {
if let Some(range) = self.range.get() {
match self.direction.get() {
Direction::Forwards => Some(range.end_container()),
_ => Some(range.start_container()),
}
} else {
None
}
}
/// <https://w3c.github.io/selection-api/#dom-selection-focusoffset>
fn FocusOffset(&self) -> u32 {
if let Some(range) = self.range.get() {
match self.direction.get() {
Direction::Forwards => range.end_offset(),
_ => range.start_offset(),
}
} else {
0
}
}
/// <https://w3c.github.io/selection-api/#dom-selection-iscollapsed>
fn IsCollapsed(&self) -> bool {
if let Some(range) = self.range.get() {
range.collapsed()
} else {
true
}
}
/// <https://w3c.github.io/selection-api/#dom-selection-rangecount>
fn RangeCount(&self) -> u32 {
if self.range.get().is_some() { 1 } else { 0 }
}
/// <https://w3c.github.io/selection-api/#dom-selection-type>
fn Type(&self) -> DOMString {
if let Some(range) = self.range.get() {
if range.collapsed() {
DOMString::from("Caret")
} else {
DOMString::from("Range")
}
} else {
DOMString::from("None")
}
}
/// <https://w3c.github.io/selection-api/#dom-selection-getrangeat>
fn GetRangeAt(&self, index: u32) -> Fallible<DomRoot<Range>> {
if index != 0 {
Err(Error::IndexSize(None))
} else if let Some(range) = self.range.get() {
Ok(DomRoot::from_ref(&range))
} else {
Err(Error::IndexSize(None))
}
}
/// <https://w3c.github.io/selection-api/#dom-selection-addrange>
fn AddRange(&self, range: &Range) {
// Step 1
if !self.is_same_root(&range.start_container()) {
return;
}
// Step 2
if self.RangeCount() != 0 {
return;
}
// Step 3
self.set_range(range);
// Are we supposed to set Direction here? w3c/selection-api#116
self.direction.set(Direction::Forwards);
}
/// <https://w3c.github.io/selection-api/#dom-selection-removerange>
fn RemoveRange(&self, range: &Range) -> ErrorResult {
if let Some(own_range) = self.range.get() {
if &*own_range == range {
self.clear_range();
return Ok(());
}
}
Err(Error::NotFound(None))
}
/// <https://w3c.github.io/selection-api/#dom-selection-removeallranges>
fn RemoveAllRanges(&self) {
self.clear_range();
}
// https://w3c.github.io/selection-api/#dom-selection-empty
// TODO: When implementing actual selection UI, this may be the correct
// method to call as the abandon-selection action
fn Empty(&self) {
self.clear_range();
}
/// <https://w3c.github.io/selection-api/#dom-selection-collapse>
fn Collapse(&self, node: Option<&Node>, offset: u32, can_gc: CanGc) -> ErrorResult {
if let Some(node) = node {
if node.is_doctype() {
// w3c/selection-api#118
return Err(Error::InvalidNodeType(None));
}
if offset > node.len() {
// Step 2
return Err(Error::IndexSize(None));
}
if !self.is_same_root(node) {
// Step 3
return Ok(());
}
// Steps 4-5
let range = Range::new(&self.document, node, offset, node, offset, can_gc);
// Step 6
self.set_range(&range);
// Are we supposed to set Direction here? w3c/selection-api#116
//
self.direction.set(Direction::Forwards);
} else {
// Step 1
self.clear_range();
}
Ok(())
}
// https://w3c.github.io/selection-api/#dom-selection-setposition
// TODO: When implementing actual selection UI, this may be the correct
// method to call as the start-of-selection action, after a
// selectstart event has fired and not been cancelled.
fn SetPosition(&self, node: Option<&Node>, offset: u32, can_gc: CanGc) -> ErrorResult {
self.Collapse(node, offset, can_gc)
}
/// <https://w3c.github.io/selection-api/#dom-selection-collapsetostart>
fn CollapseToStart(&self, can_gc: CanGc) -> ErrorResult {
if let Some(range) = self.range.get() {
self.Collapse(
Some(&*range.start_container()),
range.start_offset(),
can_gc,
)
} else {
Err(Error::InvalidState(None))
}
}
/// <https://w3c.github.io/selection-api/#dom-selection-collapsetoend>
fn CollapseToEnd(&self, can_gc: CanGc) -> ErrorResult {
if let Some(range) = self.range.get() {
self.Collapse(Some(&*range.end_container()), range.end_offset(), can_gc)
} else {
Err(Error::InvalidState(None))
}
}
// https://w3c.github.io/selection-api/#dom-selection-extend
// TODO: When implementing actual selection UI, this may be the correct
// method to call as the continue-selection action
fn Extend(&self, node: &Node, offset: u32, can_gc: CanGc) -> ErrorResult {
if !self.is_same_root(node) {
// Step 1
return Ok(());
}
if let Some(range) = self.range.get() {
if node.is_doctype() {
// w3c/selection-api#118
return Err(Error::InvalidNodeType(None));
}
if offset > node.len() {
// As with is_doctype, not explicit in selection spec steps here
// but implied by which exceptions are thrown in WPT tests
return Err(Error::IndexSize(None));
}
// Step 4
if !self.is_same_root(&range.start_container()) {
// Step 5, and its following 8 and 9
self.set_range(&Range::new(
&self.document,
node,
offset,
node,
offset,
can_gc,
));
self.direction.set(Direction::Forwards);
} else {
let old_anchor_node = &*self.GetAnchorNode().unwrap(); // has range, therefore has anchor node
let old_anchor_offset = self.AnchorOffset();
let is_old_anchor_before_or_equal = {
if old_anchor_node == node {
old_anchor_offset <= offset
} else {
old_anchor_node.is_before(node)
}
};
if is_old_anchor_before_or_equal {
// Step 6, and its following 8 and 9
self.set_range(&Range::new(
&self.document,
old_anchor_node,
old_anchor_offset,
node,
offset,
can_gc,
));
self.direction.set(Direction::Forwards);
} else {
// Step 7, and its following 8 and 9
self.set_range(&Range::new(
&self.document,
node,
offset,
old_anchor_node,
old_anchor_offset,
can_gc,
));
self.direction.set(Direction::Backwards);
}
};
} else {
// Step 2
return Err(Error::InvalidState(None));
}
Ok(())
}
/// <https://w3c.github.io/selection-api/#dom-selection-setbaseandextent>
fn SetBaseAndExtent(
&self,
anchor_node: &Node,
anchor_offset: u32,
focus_node: &Node,
focus_offset: u32,
can_gc: CanGc,
) -> ErrorResult {
// Step 1
if anchor_node.is_doctype() || focus_node.is_doctype() {
// w3c/selection-api#118
return Err(Error::InvalidNodeType(None));
}
if anchor_offset > anchor_node.len() || focus_offset > focus_node.len() {
return Err(Error::IndexSize(None));
}
// Step 2
if !self.is_same_root(anchor_node) || !self.is_same_root(focus_node) {
return Ok(());
}
// Steps 5-7
let is_focus_before_anchor = {
if anchor_node == focus_node {
focus_offset < anchor_offset
} else {
focus_node.is_before(anchor_node)
}
};
if is_focus_before_anchor {
self.set_range(&Range::new(
&self.document,
focus_node,
focus_offset,
anchor_node,
anchor_offset,
can_gc,
));
self.direction.set(Direction::Backwards);
} else {
self.set_range(&Range::new(
&self.document,
anchor_node,
anchor_offset,
focus_node,
focus_offset,
can_gc,
));
self.direction.set(Direction::Forwards);
}
Ok(())
}
/// <https://w3c.github.io/selection-api/#dom-selection-selectallchildren>
fn SelectAllChildren(&self, node: &Node, can_gc: CanGc) -> ErrorResult {
if node.is_doctype() {
// w3c/selection-api#118
return Err(Error::InvalidNodeType(None));
}
if !self.is_same_root(node) {
return Ok(());
}
// Spec wording just says node length here, but WPT specifically
// wants number of children (the main difference is that it's 0
// for cdata).
self.set_range(&Range::new(
&self.document,
node,
0,
node,
node.children_count(),
can_gc,
));
self.direction.set(Direction::Forwards);
Ok(())
}
/// <https://w3c.github.io/selection-api/#dom-selection-deletecontents>
fn DeleteFromDocument(&self) -> ErrorResult {
if let Some(range) = self.range.get() {
// Since the range is changing, it should trigger a
// selectionchange event as it would if if mutated any other way
return range.DeleteContents();
}
Ok(())
}
/// <https://w3c.github.io/selection-api/#dom-selection-containsnode>
fn ContainsNode(&self, node: &Node, allow_partial_containment: bool) -> bool {
// TODO: Spec requires a "visually equivalent to" check, which is
// probably up to a layout query. This is therefore not a full implementation.
if !self.is_same_root(node) {
return false;
}
if let Some(range) = self.range.get() {
let start_node = &*range.start_container();
if !self.is_same_root(start_node) {
// node can't be contained in a range with a different root
return false;
}
if allow_partial_containment {
// Spec seems to be incorrect here, w3c/selection-api#116
if node.is_before(start_node) {
return false;
}
let end_node = &*range.end_container();
if end_node.is_before(node) {
return false;
}
if node == start_node {
return range.start_offset() < node.len();
}
if node == end_node {
return range.end_offset() > 0;
}
true
} else {
if node.is_before(start_node) {
return false;
}
let end_node = &*range.end_container();
if end_node.is_before(node) {
return false;
}
if node == start_node {
return range.start_offset() == 0;
}
if node == end_node {
return range.end_offset() == node.len();
}
true
}
} else {
// No range
false
}
}
/// <https://w3c.github.io/selection-api/#dom-selection-stringifier>
fn Stringifier(&self) -> DOMString {
// The spec as of Jan 31 2020 just says
// "See W3C bug 10583." for this method.
// Stringifying the range seems at least approximately right
// and passes the non-style-dependent case in the WPT tests.
if let Some(range) = self.range.get() {
range.Stringifier()
} else {
DOMString::from("")
}
}
}