diff options
Diffstat (limited to 'components/script/dom/selection.rs')
-rw-r--r-- | components/script/dom/selection.rs | 515 |
1 files changed, 515 insertions, 0 deletions
diff --git a/components/script/dom/selection.rs b/components/script/dom/selection.rs new file mode 100644 index 00000000000..e86c366d49c --- /dev/null +++ b/components/script/dom/selection.rs @@ -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("") + } + } +} |