mirror of
https://github.com/servo/servo.git
synced 2025-08-03 20:50:07 +01:00
Selection interface working for synthetic operations
This commit is contained in:
parent
e697e6cca7
commit
5ef3358951
30 changed files with 812 additions and 8468 deletions
515
components/script/dom/selection.rs
Normal file
515
components/script/dom/selection.rs
Normal file
|
@ -0,0 +1,515 @@
|
|||
/* 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 crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, NodeMethods};
|
||||
use crate::dom::bindings::codegen::Bindings::RangeBinding::RangeMethods;
|
||||
use crate::dom::bindings::codegen::Bindings::SelectionBinding::{SelectionMethods, Wrap};
|
||||
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::{reflect_dom_object, DomObject, Reflector};
|
||||
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::{window_from_node, Node};
|
||||
use crate::dom::range::Range;
|
||||
use crate::task_source::TaskSource;
|
||||
use dom_struct::dom_struct;
|
||||
use std::cell::Cell;
|
||||
|
||||
#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
|
||||
enum Direction {
|
||||
Forwards,
|
||||
Backwards,
|
||||
Directionless,
|
||||
}
|
||||
|
||||
#[dom_struct]
|
||||
pub 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 fn new(document: &Document) -> DomRoot<Selection> {
|
||||
reflect_dom_object(
|
||||
Box::new(Selection::new_inherited(document)),
|
||||
&*document.global(),
|
||||
Wrap,
|
||||
)
|
||||
}
|
||||
|
||||
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 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);
|
||||
let window = window_from_node(&*self.document);
|
||||
window
|
||||
.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"));
|
||||
}),
|
||||
window.upcast(),
|
||||
)
|
||||
.expect("Couldn't queue selectionchange task!");
|
||||
self.task_queued.set(true);
|
||||
}
|
||||
|
||||
fn is_same_root(&self, node: &Node) -> bool {
|
||||
&*node.GetRootNode(&GetRootNodeOptions::empty()) == self.document.upcast::<Node>()
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectionMethods 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.StartContainer()),
|
||||
_ => Some(range.EndContainer()),
|
||||
}
|
||||
} 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.StartOffset(),
|
||||
_ => range.EndOffset(),
|
||||
}
|
||||
} 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.EndContainer()),
|
||||
_ => Some(range.StartContainer()),
|
||||
}
|
||||
} 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.EndOffset(),
|
||||
_ => range.StartOffset(),
|
||||
}
|
||||
} 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)
|
||||
} else if let Some(range) = self.range.get() {
|
||||
Ok(DomRoot::from_ref(&range))
|
||||
} else {
|
||||
Err(Error::IndexSize)
|
||||
}
|
||||
}
|
||||
|
||||
// https://w3c.github.io/selection-api/#dom-selection-addrange
|
||||
fn AddRange(&self, range: &Range) {
|
||||
// Step 1
|
||||
if !self.is_same_root(&*range.StartContainer()) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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) -> ErrorResult {
|
||||
if let Some(node) = node {
|
||||
if node.is_doctype() {
|
||||
// w3c/selection-api#118
|
||||
return Err(Error::InvalidNodeType);
|
||||
}
|
||||
if offset > node.len() {
|
||||
// Step 2
|
||||
return Err(Error::IndexSize);
|
||||
}
|
||||
|
||||
if !self.is_same_root(node) {
|
||||
// Step 3
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Steps 4-5
|
||||
let range = Range::new(&self.document, node, offset, node, offset);
|
||||
|
||||
// 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) -> ErrorResult {
|
||||
self.Collapse(node, offset)
|
||||
}
|
||||
|
||||
// https://w3c.github.io/selection-api/#dom-selection-collapsetostart
|
||||
fn CollapseToStart(&self) -> ErrorResult {
|
||||
if let Some(range) = self.range.get() {
|
||||
self.Collapse(Some(&*range.StartContainer()), range.StartOffset())
|
||||
} else {
|
||||
Err(Error::InvalidState)
|
||||
}
|
||||
}
|
||||
|
||||
// https://w3c.github.io/selection-api/#dom-selection-collapsetoend
|
||||
fn CollapseToEnd(&self) -> ErrorResult {
|
||||
if let Some(range) = self.range.get() {
|
||||
self.Collapse(Some(&*range.EndContainer()), range.EndOffset())
|
||||
} else {
|
||||
Err(Error::InvalidState)
|
||||
}
|
||||
}
|
||||
|
||||
// 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) -> 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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Step 4
|
||||
if !self.is_same_root(&*range.StartContainer()) {
|
||||
// Step 5, and its following 8 and 9
|
||||
self.set_range(&*Range::new(&self.document, node, offset, node, offset));
|
||||
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,
|
||||
));
|
||||
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,
|
||||
));
|
||||
self.direction.set(Direction::Backwards);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// Step 2
|
||||
return Err(Error::InvalidState);
|
||||
}
|
||||
return 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,
|
||||
) -> ErrorResult {
|
||||
// Step 1
|
||||
if anchor_node.is_doctype() || focus_node.is_doctype() {
|
||||
// w3c/selection-api#118
|
||||
return Err(Error::InvalidNodeType);
|
||||
}
|
||||
|
||||
if anchor_offset > anchor_node.len() || focus_offset > focus_node.len() {
|
||||
return Err(Error::IndexSize);
|
||||
}
|
||||
|
||||
// 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,
|
||||
));
|
||||
self.direction.set(Direction::Backwards);
|
||||
} else {
|
||||
self.set_range(&*Range::new(
|
||||
&self.document,
|
||||
anchor_node,
|
||||
anchor_offset,
|
||||
focus_node,
|
||||
focus_offset,
|
||||
));
|
||||
self.direction.set(Direction::Forwards);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// https://w3c.github.io/selection-api/#dom-selection-selectallchildren
|
||||
fn SelectAllChildren(&self, node: &Node) -> ErrorResult {
|
||||
if node.is_doctype() {
|
||||
// w3c/selection-api#118
|
||||
return Err(Error::InvalidNodeType);
|
||||
}
|
||||
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(),
|
||||
));
|
||||
|
||||
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();
|
||||
}
|
||||
return 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.StartContainer();
|
||||
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.EndContainer();
|
||||
if end_node.is_before(node) {
|
||||
return false;
|
||||
}
|
||||
if node == start_node {
|
||||
return range.StartOffset() < node.len();
|
||||
}
|
||||
if node == end_node {
|
||||
return range.EndOffset() > 0;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
if node.is_before(start_node) {
|
||||
return false;
|
||||
}
|
||||
let end_node = &*range.EndContainer();
|
||||
if end_node.is_before(node) {
|
||||
return false;
|
||||
}
|
||||
if node == start_node {
|
||||
return range.StartOffset() == 0;
|
||||
}
|
||||
if node == end_node {
|
||||
return range.EndOffset() == node.len();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// No range
|
||||
return 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("")
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue