Skip to main content

script/dom/
selection.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::cmp::Ordering;
7
8use dom_struct::dom_struct;
9use js::context::{JSContext, NoGC};
10use script_bindings::codegen::GenericBindings::ShadowRootBinding::ShadowRootMethods;
11use script_bindings::reflector::{Reflector, reflect_dom_object_with_cx};
12
13use crate::dom::abstractrange::bp_position;
14use crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, NodeMethods};
15use crate::dom::bindings::codegen::Bindings::RangeBinding::RangeMethods;
16use crate::dom::bindings::codegen::Bindings::SelectionBinding::SelectionMethods;
17use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
18use crate::dom::bindings::inheritance::Castable;
19use crate::dom::bindings::refcounted::Trusted;
20use crate::dom::bindings::reflector::DomGlobal;
21use crate::dom::bindings::root::{Dom, DomRoot, LayoutDom, MutNullableDom, ToLayoutOptional};
22use crate::dom::bindings::str::DOMString;
23use crate::dom::document::Document;
24use crate::dom::eventtarget::EventTarget;
25use crate::dom::iterators::PrePostIteration;
26use crate::dom::node::{Node, NodeTraits};
27use crate::dom::range::Range;
28use crate::dom::types::ShadowRoot;
29use crate::dom::{CharacterData, FlatTreeParent, NodeDamage, NodeFlags};
30
31#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
32enum Direction {
33    Forwards,
34    Backwards,
35    Directionless,
36}
37
38#[dom_struct]
39pub(crate) struct Selection {
40    reflector_: Reflector,
41    document: Dom<Document>,
42    range: MutNullableDom<Range>,
43    direction: Cell<Direction>,
44    /// <https://w3c.github.io/selection-api/#dfn-has-scheduled-selectionchange-event>
45    has_scheduled_selectionchange_event: Cell<bool>,
46    /// Whether or not this [`Selection`] needs to remark DOM nodes with selection flags
47    /// after a change to its underlying [`Range`].
48    visible_selection_dirty: Cell<bool>,
49}
50
51impl Selection {
52    fn new_inherited(document: &Document) -> Selection {
53        Selection {
54            reflector_: Reflector::new(),
55            document: Dom::from_ref(document),
56            range: MutNullableDom::new(None),
57            direction: Cell::new(Direction::Directionless),
58            has_scheduled_selectionchange_event: Cell::new(false),
59            visible_selection_dirty: Cell::new(false),
60        }
61    }
62
63    pub(crate) fn new(cx: &mut JSContext, document: &Document) -> DomRoot<Selection> {
64        reflect_dom_object_with_cx(
65            Box::new(Selection::new_inherited(document)),
66            &*document.global(),
67            cx,
68        )
69    }
70
71    pub(crate) fn visible_selection_dirty(&self) -> bool {
72        self.visible_selection_dirty.get()
73    }
74
75    fn set_range(&self, new_range: Option<&Range>) {
76        // If we are setting to literally the same Range object and not just the same
77        // positions, then there's nothing changing and no task to queue.
78        if new_range == self.range.get().as_deref() {
79            return;
80        }
81
82        if let Some(old_range) = self.range.take() {
83            old_range.disassociate_selection(self);
84        }
85
86        if let Some(new_range) = new_range {
87            self.range.set(Some(new_range));
88            new_range.associate_selection(self);
89        }
90
91        self.set_visible_selection_dirty();
92        self.queue_selectionchange_task();
93    }
94
95    fn unset_flags_for_visible_selection(&self, no_gc: &NoGC) {
96        let mut traversal = self
97            .document
98            .upcast::<Node>()
99            .following_flat_tree_nodes_unrooted(no_gc);
100        let mut next = traversal.next();
101        while let Some(node) = next.take() {
102            match node {
103                PrePostIteration::Enter(node) => {
104                    if node.get_flag(NodeFlags::OVERLAPS_DOCUMENT_SELECTION) {
105                        node.set_flag(NodeFlags::OVERLAPS_DOCUMENT_SELECTION, false);
106
107                        // Currently only `CharacterData` nodes show visible selection.
108                        if node.is::<CharacterData>() {
109                            node.dirty(no_gc, NodeDamage::ContentOrHeritage);
110                        }
111
112                        next = traversal.next();
113                    } else {
114                        next = traversal.next_skipping_subtree();
115                    }
116                },
117                PrePostIteration::Leave(_) => next = traversal.next(),
118            }
119        }
120    }
121
122    pub(crate) fn set_flags_for_visible_selection(&self, no_gc: &NoGC) {
123        if !self.visible_selection_dirty.take() {
124            return;
125        }
126
127        self.unset_flags_for_visible_selection(no_gc);
128        let Some(range) = self.range.get() else {
129            return;
130        };
131
132        let start_position = position_in_flat_tree_for_selection(
133            no_gc,
134            range.start_container(),
135            range.start_offset() as usize,
136        );
137        let end_position = position_in_flat_tree_for_selection(
138            no_gc,
139            range.end_container(),
140            range.end_offset() as usize,
141        );
142        let start_node = start_position.node();
143        let end_node = end_position.node();
144
145        // In case the range hasn't changed, but the offsets within the start/end end node
146        // have changed, always dirty the start and end nodes, if they paint selection.
147        // TODO(mrobinson): We should handle changes only to the offsets within a single
148        // boundary node explicitly and not be unsetting and setting flags on the whole
149        // range.
150        if start_node.is::<CharacterData>() {
151            start_node.dirty(no_gc, NodeDamage::ContentOrHeritage);
152        }
153        if end_node.is::<CharacterData>() {
154            end_node.dirty(no_gc, NodeDamage::ContentOrHeritage);
155        }
156
157        let add_selection_flag = |node: &Node| {
158            if !node.get_flag(NodeFlags::OVERLAPS_DOCUMENT_SELECTION) {
159                node.set_flag(NodeFlags::OVERLAPS_DOCUMENT_SELECTION, true);
160
161                // Currently only `CharacterData` nodes show visible selection.
162                if node.is::<CharacterData>() {
163                    node.dirty(no_gc, NodeDamage::ContentOrHeritage);
164                }
165            }
166        };
167
168        // We mark the ancestors of the start node as containing a selection. Two notes:
169        // - The traversal itself will take care of marking ancestors of all other nodes,
170        //   as the in-order tree walk will be guaranteed to walk them.
171        // - We do not need to mark these nodes as dirty as they are guaranteed to not be
172        //   leaves (the only nodes that show visible selection).
173        let mut maybe_parent = start_node.parent_in_flat_tree(no_gc);
174        while let FlatTreeParent::Parent(parent) = maybe_parent {
175            add_selection_flag(&parent);
176            maybe_parent = parent.parent_in_flat_tree(no_gc);
177        }
178
179        let mut traversal = start_node.following_flat_tree_nodes_unrooted(no_gc);
180
181        // If the selection starts after the first node, skip that node and all descendants
182        // before setting flags in the selection range.
183        if matches!(start_position, FlatTreeNodePosition::After(_)) {
184            let leaving_start = traversal.next_skipping_subtree();
185            debug_assert!(
186                matches!(leaving_start, Some(PrePostIteration::Leave(node)) if node == *start_node)
187            );
188        }
189
190        for iteration in traversal {
191            match &iteration {
192                PrePostIteration::Enter(node) => {
193                    if node == end_node && matches!(end_position, FlatTreeNodePosition::Before(_)) {
194                        break;
195                    }
196                    add_selection_flag(node);
197                },
198                PrePostIteration::Leave(node) => {
199                    add_selection_flag(node);
200                    if node == end_node {
201                        break;
202                    }
203                },
204            }
205        }
206    }
207
208    /// <https://w3c.github.io/selection-api/#dfn-schedule-a-selectionchange-event>
209    pub(crate) fn queue_selectionchange_task(&self) {
210        // https://w3c.github.io/editing/docs/execCommand/#state-override
211        // https://w3c.github.io/editing/docs/execCommand/#value-override
212        // > Whenever the number of ranges in the selection changes to something
213        // > different, and whenever a boundary point of the range at a given index in the
214        // > selection changes to something different, the state override and value
215        // > override must be unset for every command.
216        self.document.clear_command_overrides();
217
218        // Step 1. If target's has scheduled selectionchange event is true, abort these steps.
219        if self.has_scheduled_selectionchange_event.get() {
220            return;
221        }
222        // Step 2. Set target's has scheduled selectionchange event to true.
223        self.has_scheduled_selectionchange_event.set(true);
224        // Step 3. Queue a task on the user interaction task source to fire a
225        // selectionchange event on target.
226        let this = Trusted::new(self);
227        self.document
228            .owner_global()
229            .task_manager()
230            .user_interaction_task_source() // w3c/selection-api#117
231            .queue(
232                // https://w3c.github.io/selection-api/#firing-selectionchange-event
233                task!(selectionchange_task_steps: move |cx| {
234                    let this = this.root();
235                    // Step 1. Set target's has scheduled selectionchange event to false.
236                    this.has_scheduled_selectionchange_event.set(false);
237                    // Step 2. If target is an element, fire an event named
238                    // selectionchange, which bubbles and not cancelable, at target.
239                    //
240                    // n/a
241
242                    // Step 3. Otherwise, if target is a document, fire an event named
243                    // selectionchange, which does not bubble and not cancelable, at
244                    // target.
245                    this.document.upcast::<EventTarget>().fire_event(cx, atom!("selectionchange"));
246                }),
247            );
248    }
249
250    fn is_in_document_of_range(&self, node: &Node) -> bool {
251        // TODO(mrobinson): This should eventually allow nodes in the same composed tree (and
252        // not just the same tree), but this requires more work to allow `Selection` to cross
253        // shadow tree boundaries.
254        &*node.GetRootNode(&GetRootNodeOptions { composed: false }) ==
255            self.document.upcast::<Node>()
256    }
257
258    /// <https://w3c.github.io/editing/docs/execCommand/#active-range>
259    pub(crate) fn active_range(&self) -> Option<DomRoot<Range>> {
260        // > The active range is the range of the selection given by calling
261        // > getSelection() on the context object. (Thus the active range may be null.)
262        self.range.get()
263    }
264
265    pub(crate) fn collapse_current_range(&self, node: &Node, offset: u32) {
266        let range = self.range.get().expect("Must always have a range");
267        range.set_start(node, offset);
268        range.set_end(node, offset);
269
270        self.set_visible_selection_dirty();
271    }
272
273    pub(crate) fn extend_current_range(&self, node: &Node, offset: u32) {
274        let range = self.range.get().expect("Must always have a range");
275        assert!(range.collapsed(), "Must only extend after collapsing");
276
277        let anchor_node = range.start_container();
278        if (*anchor_node == *node && range.start_offset() < offset) || anchor_node.is_before(node) {
279            range.set_end(node, offset);
280            self.direction.set(Direction::Forwards);
281        } else {
282            range.set_start(node, offset);
283            self.direction.set(Direction::Backwards);
284        }
285
286        self.set_visible_selection_dirty();
287    }
288
289    pub(crate) fn set_visible_selection_dirty(&self) {
290        self.visible_selection_dirty.set(true);
291    }
292
293    /// <https://w3c.github.io/selection-api/#dfn-anchor>
294    pub(crate) fn anchor_node(&self) -> Option<DomRoot<Node>> {
295        self.range.get().map(|range| match self.direction.get() {
296            Direction::Forwards => range.start_container(),
297            _ => range.end_container(),
298        })
299    }
300
301    /// <https://w3c.github.io/selection-api/#dfn-anchor>
302    pub(crate) fn anchor_offset(&self) -> u32 {
303        self.range
304            .get()
305            .map(|range| match self.direction.get() {
306                Direction::Forwards => range.start_offset(),
307                _ => range.end_offset(),
308            })
309            .unwrap_or(0)
310    }
311
312    /// <https://w3c.github.io/selection-api/#dfn-focus>
313    pub(crate) fn focus_node(&self) -> Option<DomRoot<Node>> {
314        self.range.get().map(|range| match self.direction.get() {
315            Direction::Forwards => range.end_container(),
316            _ => range.start_container(),
317        })
318    }
319
320    /// <https://w3c.github.io/selection-api/#dfn-focus>
321    pub(crate) fn focus_offset(&self) -> u32 {
322        self.range
323            .get()
324            .map(|range| match self.direction.get() {
325                Direction::Forwards => range.end_offset(),
326                _ => range.start_offset(),
327            })
328            .unwrap_or(0)
329    }
330}
331
332impl SelectionMethods<crate::DomTypeHolder> for Selection {
333    /// <https://w3c.github.io/selection-api/#dom-selection-anchornode>
334    fn GetAnchorNode(&self) -> Option<DomRoot<Node>> {
335        // > The attribute must return the anchor node of this, or null if the anchor is
336        // > null or anchor is not in the document tree.
337        let anchor_node = self.anchor_node()?;
338        if !anchor_node.is_in_a_document_tree() {
339            return None;
340        }
341        Some(anchor_node)
342    }
343
344    /// <https://w3c.github.io/selection-api/#dom-selection-anchoroffset>
345    fn AnchorOffset(&self) -> u32 {
346        // > The attribute must return the anchor offset of this, or 0 if the anchor is null
347        // > or anchor is not in the document tree.
348        if self
349            .anchor_node()
350            .is_none_or(|anchor_node| !anchor_node.is_in_a_document_tree())
351        {
352            return 0;
353        }
354        self.anchor_offset()
355    }
356
357    /// <https://w3c.github.io/selection-api/#dom-selection-focusnode>
358    fn GetFocusNode(&self) -> Option<DomRoot<Node>> {
359        // > The attribute must return the focus node of this, or null if the focus is
360        // > null or focus is not in the document tree.
361        let focus_node = self.focus_node()?;
362        if !focus_node.is_in_a_document_tree() {
363            return None;
364        }
365        Some(focus_node)
366    }
367
368    /// <https://w3c.github.io/selection-api/#dom-selection-focusoffset>
369    fn FocusOffset(&self) -> u32 {
370        // > The attribute must return the focus offset of this, or 0 if the focus is null
371        // > or focus is not in the document tree.
372        if self
373            .focus_node()
374            .is_none_or(|focus_node| !focus_node.is_in_a_document_tree())
375        {
376            return 0;
377        }
378        self.focus_offset()
379    }
380
381    /// <https://w3c.github.io/selection-api/#dom-selection-iscollapsed>
382    fn IsCollapsed(&self) -> bool {
383        // > The attribute must return true if and only if the anchor and focus are the
384        // > same (including if both are null). Otherwise it must return false.
385        self.range.get().is_none_or(|range| range.collapsed())
386    }
387
388    /// <https://w3c.github.io/selection-api/#dom-selection-rangecount>
389    fn RangeCount(&self) -> u32 {
390        // > The attribute must return 0 if this is empty or either focus or anchor is not
391        // > in the document tree, and must return 1 otherwise.
392        let Some(range) = self.range.get() else {
393            return 0;
394        };
395        if !range.start_and_end_are_in_document_tree() {
396            return 0;
397        }
398        1
399    }
400
401    /// <https://w3c.github.io/selection-api/#dom-selection-type>
402    fn Type(&self) -> DOMString {
403        // > The attribute must return "None" if this is empty or either focus or anchor
404        // > is not in the document tree, "Caret" if this's range is collapsed, and "Range"
405        // > otherwise.
406        let Some(range) = self.range.get() else {
407            return DOMString::from("None");
408        };
409        if !range.start_and_end_are_in_document_tree() {
410            return DOMString::from("None");
411        }
412
413        if range.collapsed() {
414            DOMString::from("Caret")
415        } else {
416            DOMString::from("Range")
417        }
418    }
419
420    /// <https://w3c.github.io/selection-api/#dom-selection-getrangeat>
421    fn GetRangeAt(&self, index: u32) -> Fallible<DomRoot<Range>> {
422        // > The method must throw an IndexSizeError exception if index is not 0, or if this
423        // > is empty or either focus or anchor is not in the document tree. Otherwise, it
424        // > must return a reference to (not a copy of) this's range.
425        if index != 0 {
426            return Err(Error::IndexSize(None));
427        }
428
429        let Some(range) = self.range.get() else {
430            return Err(Error::IndexSize(None));
431        };
432
433        if !range.start_and_end_are_in_document_tree() {
434            return Err(Error::IndexSize(None));
435        }
436
437        Ok(DomRoot::from_ref(&range))
438    }
439
440    /// <https://w3c.github.io/selection-api/#dom-selection-addrange>
441    fn AddRange(&self, range: &Range) {
442        // Step 1. If the root of the range's boundary points are not the document
443        // associated with this, abort these steps.
444        if !self.is_in_document_of_range(&range.start_container()) {
445            return;
446        }
447
448        // Step 2. If rangeCount is not 0, abort these steps.
449        if self.RangeCount() != 0 {
450            return;
451        }
452
453        // Step 3. Set this's range to range by a strong reference (not by making a copy).
454        self.set_range(Some(range));
455
456        // Are we supposed to set Direction here? w3c/selection-api#116
457        self.direction.set(Direction::Forwards);
458    }
459
460    /// <https://w3c.github.io/selection-api/#dom-selection-removerange>
461    fn RemoveRange(&self, range: &Range) -> ErrorResult {
462        // > The method must make this empty by disassociating its range if this's range
463        // > is range. Otherwise, it must throw a NotFoundError.
464        if let Some(own_range) = self.range.get() &&
465            &*own_range == range
466        {
467            self.set_range(None);
468            return Ok(());
469        }
470        Err(Error::NotFound(None))
471    }
472
473    /// <https://w3c.github.io/selection-api/#dom-selection-removeallranges>
474    fn RemoveAllRanges(&self) {
475        // > The method must make this empty by disassociating its range if this has an
476        // > associated range.
477        self.set_range(None);
478    }
479
480    /// <https://w3c.github.io/selection-api/#dom-selection-empty>
481    fn Empty(&self) {
482        // > The method must be an alias, and behave identically, to removeAllRanges().
483        self.set_range(None);
484    }
485
486    /// <https://w3c.github.io/selection-api/#dom-selection-collapse>
487    fn Collapse(&self, cx: &mut JSContext, node: Option<&Node>, offset: u32) -> ErrorResult {
488        // Step 1. If node is null, this method must behave identically as
489        // removeAllRanges() and abort these steps.
490        let Some(node) = node else {
491            self.set_range(None);
492            return Ok(());
493        };
494
495        // Step 2. If node is a DocumentType, throw an InvalidNodeTypeError exception and
496        // abort these steps.
497        if node.is_doctype() {
498            return Err(Error::InvalidNodeType(None));
499        }
500
501        // Step 3. The method must throw an IndexSizeError exception if offset is longer
502        // than node's length and abort these steps.
503        if offset > node.len() {
504            return Err(Error::IndexSize(None));
505        }
506
507        // Step 4. If document associated with this is not a shadow-including inclusive
508        // ancestor of node, abort these steps.
509        //
510        // TODO(mrobinson): This should eventually allow nodes in the same composed tree (and
511        // not just the same tree), but this requires more work to allow `Selection` to cross
512        // shadow tree boundaries.
513        if &*node.GetRootNode(&GetRootNodeOptions { composed: false }) !=
514            self.document.upcast::<Node>()
515        {
516            return Ok(());
517        }
518
519        // Step 5. Otherwise, let newRange be a new range.
520        // Step 6. Set the start the start and the end of newRange to (node, offset).
521        let new_range = Range::new(cx, &self.document, node, offset, node, offset);
522
523        // Step 7. Set this's range to newRange.
524        self.set_range(Some(&new_range));
525
526        // Are we supposed to set Direction here? w3c/selection-api#116
527        self.direction.set(Direction::Forwards);
528
529        Ok(())
530    }
531
532    /// <https://w3c.github.io/selection-api/#dom-selection-setposition>
533    fn SetPosition(&self, cx: &mut JSContext, node: Option<&Node>, offset: u32) -> ErrorResult {
534        // > The method must be an alias, and behave identically, to collapse().
535        self.Collapse(cx, node, offset)
536    }
537
538    /// <https://w3c.github.io/selection-api/#dom-selection-collapsetostart>
539    fn CollapseToStart(&self, cx: &mut JSContext) -> ErrorResult {
540        // > The method must throw InvalidStateError exception if the this is empty.
541        // > Otherwise, it must create a new range, set the start both its start and end to
542        // > the start of this's range, and then set this's range to the newly-created
543        // > range.
544        if let Some(range) = self.range.get() {
545            self.Collapse(cx, Some(&*range.start_container()), range.start_offset())
546        } else {
547            Err(Error::InvalidState(None))
548        }
549    }
550
551    /// <https://w3c.github.io/selection-api/#dom-selection-collapsetoend>
552    fn CollapseToEnd(&self, cx: &mut JSContext) -> ErrorResult {
553        // > The method must throw InvalidStateError exception if the this is empty.
554        // > Otherwise, it must create a new range, set the start both its start and end to
555        // > the end of this's range, and then set this's range to the newly-created range.
556        if let Some(range) = self.range.get() {
557            self.Collapse(cx, Some(&*range.end_container()), range.end_offset())
558        } else {
559            Err(Error::InvalidState(None))
560        }
561    }
562
563    /// <https://w3c.github.io/selection-api/#dom-selection-extend>
564    fn Extend(&self, cx: &mut JSContext, node: &Node, offset: u32) -> ErrorResult {
565        // Step 1. If the document associated with this is not a shadow-including
566        // inclusive ancestor of node, abort these steps.
567        //
568        // TODO(mrobinson): This should eventually allow nodes in the same composed tree (and
569        // not just the same tree), but this requires more work to allow `Selection` to cross
570        // shadow tree boundaries.
571        if &*node.GetRootNode(&GetRootNodeOptions { composed: false }) !=
572            self.document.upcast::<Node>()
573        {
574            return Ok(());
575        }
576
577        // Step 2. If this is empty, throw an InvalidStateError exception and abort these steps.
578        let Some(range) = self.range.get() else {
579            return Err(Error::InvalidState(None));
580        };
581
582        // This isn't specified, but it appears to be implementation behavior of other
583        // browsers. See w3c/selection-api#118.
584        if node.is_doctype() {
585            return Err(Error::InvalidNodeType(None));
586        }
587
588        // As with is_doctype, this is not explicit in the selection specification steps
589        // here but implied by which exceptions are thrown in WPT tests.
590        if offset > node.len() {
591            return Err(Error::IndexSize(None));
592        }
593
594        // Step 3. Let oldAnchor and oldFocus be the this's anchor and focus, and let
595        // newFocus be the boundary point (node, offset).
596        //
597        // Note: oldFocus is unused, so we do not set it here.
598        let old_anchor_node = &*self
599            .anchor_node()
600            .expect("has range, therefore has anchor node");
601        let old_anchor_offset = self.anchor_offset();
602
603        // Step 4. Let newRange be a new range.
604        let new_range;
605        let direction;
606
607        // Step 5. If node's root is not the same as the this's range's root, set the
608        // start newRange's start and end to newFocus.
609        if !self.is_in_document_of_range(&range.start_container()) {
610            new_range = Range::new(cx, &self.document, node, offset, node, offset);
611            direction = Direction::Forwards;
612        } else {
613            let is_old_anchor_before_or_equal = matches!(
614                bp_position(old_anchor_node, old_anchor_offset, node, offset),
615                Ordering::Less | Ordering::Equal
616            );
617            if is_old_anchor_before_or_equal {
618                // Step 6. Otherwise, if oldAnchor is before or equal to newFocus, set the start
619                // newRange's start to oldAnchor, then set its end to newFocus.
620                new_range = Range::new(
621                    cx,
622                    &self.document,
623                    old_anchor_node,
624                    old_anchor_offset,
625                    node,
626                    offset,
627                );
628                direction = Direction::Forwards;
629            } else {
630                // Step 7. Otherwise, set the start newRange's start to newFocus, then set
631                // its end to oldAnchor.
632                new_range = Range::new(
633                    cx,
634                    &self.document,
635                    node,
636                    offset,
637                    old_anchor_node,
638                    old_anchor_offset,
639                );
640                direction = Direction::Backwards;
641            }
642        }
643
644        // Step 8. Set this's range to newRange.
645        self.set_range(Some(&new_range));
646
647        // Step 9. If newFocus is before oldAnchor, set this's direction to backwards.
648        // Otherwise, set it to forwards.
649        self.direction.set(direction);
650
651        Ok(())
652    }
653
654    /// <https://w3c.github.io/selection-api/#dom-selection-setbaseandextent>
655    fn SetBaseAndExtent(
656        &self,
657        cx: &mut JSContext,
658        anchor_node: &Node,
659        anchor_offset: u32,
660        focus_node: &Node,
661        focus_offset: u32,
662    ) -> ErrorResult {
663        // This isn't specified, but it appears to be implementation behavior of other
664        // browsers. See w3c/selection-api#118.
665        if anchor_node.is_doctype() || focus_node.is_doctype() {
666            return Err(Error::InvalidNodeType(None));
667        }
668
669        // Step 1. If anchorOffset is longer than anchorNode's length or if focusOffset is
670        // longer than focusNode's length, throw an IndexSizeError exception and abort
671        // these steps.
672        if anchor_offset > anchor_node.len() || focus_offset > focus_node.len() {
673            return Err(Error::IndexSize(None));
674        }
675
676        // Step 2. If document associated with this is not a shadow-including inclusive
677        // ancestor of anchorNode or focusNode, abort these steps.
678        //
679        // TODO(mrobinson): This should eventually allow nodes in the same composed tree (and
680        // not just the same tree), but this requires more work to allow `Selection` to cross
681        // shadow tree boundaries.
682        if &*anchor_node.GetRootNode(&GetRootNodeOptions { composed: false }) !=
683            self.document.upcast::<Node>()
684        {
685            return Ok(());
686        }
687        if &*focus_node.GetRootNode(&GetRootNodeOptions { composed: false }) !=
688            self.document.upcast::<Node>()
689        {
690            return Ok(());
691        }
692
693        // Step 3. Let anchor be the boundary point (anchorNode, anchorOffset) and let
694        // focus be the boundary point (focusNode, focusOffset).
695        //
696        // Note: We do not model the boundary point in this way.
697
698        // Step 4. Let newRange be a new range.
699        let new_range;
700        let direction;
701
702        // Step 5. If anchor is before focus, set the start the newRange's start to anchor
703        // and its end to focus. Otherwise, set the start them to focus and anchor
704        // respectively.
705        let is_anchor_before_focus =
706            bp_position(anchor_node, anchor_offset, focus_node, focus_offset) == Ordering::Less;
707        if is_anchor_before_focus {
708            new_range = Range::new(
709                cx,
710                &self.document,
711                anchor_node,
712                anchor_offset,
713                focus_node,
714                focus_offset,
715            );
716            direction = Direction::Forwards;
717        } else {
718            new_range = Range::new(
719                cx,
720                &self.document,
721                focus_node,
722                focus_offset,
723                anchor_node,
724                anchor_offset,
725            );
726            direction = Direction::Backwards;
727        }
728
729        // Step 6. Set this's range to newRange.
730        self.set_range(Some(&new_range));
731
732        // Step 7. If focus is before anchor, set this's direction to backwards.
733        // Otherwise, set it to forwards
734        self.direction.set(direction);
735
736        Ok(())
737    }
738
739    /// <https://w3c.github.io/selection-api/#dom-selection-selectallchildren>
740    fn SelectAllChildren(&self, cx: &mut JSContext, node: &Node) -> ErrorResult {
741        // Step 1. If node is a DocumentType, throw an InvalidNodeTypeError exception and
742        // abort these steps.
743        if node.is_doctype() {
744            return Err(Error::InvalidNodeType(None));
745        }
746
747        // Step 2. If node's root is not the document associated with this, abort these
748        // steps.
749        if !self.is_in_document_of_range(node) {
750            return Ok(());
751        }
752
753        // Let newRange be a new range and childCount be the number of children of node.
754        let child_count = node.children_count();
755
756        // Step 4. Set newRange's start to (node, 0).
757        // Step 5. Set newRange's end to (node, childCount).
758        let new_range = Range::new(cx, &self.document, node, 0, node, child_count);
759
760        // Step 6. Set this's range to newRange.
761        self.set_range(Some(&new_range));
762
763        // Step 7. Set this's direction to forwards.
764        self.direction.set(Direction::Forwards);
765
766        Ok(())
767    }
768
769    /// <https://w3c.github.io/selection-api/#dom-selection-deletecontents>
770    fn DeleteFromDocument(&self, cx: &mut JSContext) -> ErrorResult {
771        // > The method must invoke deleteContents() on this's range if this is not empty
772        // > and both focus and anchor are in the document tree. Otherwise the method must
773        // > do nothing.
774        let Some(range) = self.range.get() else {
775            return Ok(());
776        };
777        if !range.start_and_end_are_in_document_tree() {
778            return Ok(());
779        }
780
781        range.DeleteContents(cx)
782    }
783
784    /// <https://w3c.github.io/selection-api/#dom-selection-containsnode>
785    fn ContainsNode(&self, node: &Node, allow_partial_containment: bool) -> bool {
786        // > The method must return false if this is empty or if node's root is not the document
787        // > associated with this.
788        // >
789        // > Otherwise, if allowPartialContainment is false, the method must return true if and only
790        // > if start of its range is before or visually equivalent to the first boundary point in
791        // > the node *and* end of its range is after or visually equivalent to the last boundary
792        // > point in the node.
793        // >
794        // > If allowPartialContainment is true, the method must return true if and only if start of
795        // > its range is before or visually equivalent to the last boundary point in the node *and*
796        // > end of its range is after or visually equivalent to the first boundary point in the
797        // > node.
798
799        if !self.is_in_document_of_range(node) {
800            return false;
801        }
802        let Some(range) = self.range.get() else {
803            return false;
804        };
805        let start_node = &*range.start_container();
806        if !self.is_in_document_of_range(start_node) {
807            return false;
808        }
809        let end_node = &*range.end_container();
810
811        let first_offset = 0;
812        let last_offset = node.len();
813        let (compare_start_to, compare_end_to) = if allow_partial_containment {
814            (last_offset, first_offset)
815        } else {
816            (first_offset, last_offset)
817        };
818
819        // TODO: find out what "visually equivalent" means for boundary points and implement it.
820        // https://github.com/w3c/selection-api/issues/6
821        // For now it is simplified to "position is equal".
822        matches!(
823            bp_position(start_node, range.start_offset(), node, compare_start_to),
824            Ordering::Less | Ordering::Equal
825        ) && matches!(
826            bp_position(end_node, range.end_offset(), node, compare_end_to),
827            Ordering::Greater | Ordering::Equal
828        )
829    }
830
831    /// <https://w3c.github.io/selection-api/#dom-selection-stringifier>
832    fn Stringifier(&self, no_gc: &NoGC) -> DOMString {
833        // > The stringification must return the string, which is the concatenation of the
834        // > rendered text if there is a range associated with this.
835        // >
836        // > If the selection is within a textarea or input element, it must return the
837        // > selected substring in its value.
838        //
839        // TODO: This implementation should be examined in depth. Does rendered text take
840        // into account `display: none`. The case for textarea and input elements is
841        // completely unhandled here.
842        if let Some(range) = self.range.get() {
843            range.Stringifier(no_gc)
844        } else {
845            DOMString::from("")
846        }
847    }
848}
849
850impl<'dom> LayoutDom<'dom, Selection> {
851    #[expect(unsafe_code)]
852    pub(crate) fn range_for_layout(&self) -> Option<LayoutDom<'dom, Range>> {
853        unsafe { self.unsafe_get().range.to_layout() }
854    }
855}
856
857enum FlatTreeNodePosition {
858    Before(DomRoot<Node>),
859    Inside(DomRoot<Node>),
860    After(DomRoot<Node>),
861}
862
863impl FlatTreeNodePosition {
864    fn node(&self) -> &Node {
865        match self {
866            FlatTreeNodePosition::Before(node) => node,
867            FlatTreeNodePosition::Inside(node) => node,
868            FlatTreeNodePosition::After(node) => node,
869        }
870    }
871}
872
873/// Find the position of a node and offset in the flat tree for the purposes of selection
874/// boundaries. This projects the given position onto the flat tree, accounting for origin
875/// nodes that may not actually be in the flat tree at all.
876fn position_in_flat_tree_for_selection(
877    no_gc: &NoGC,
878    container: DomRoot<Node>,
879    offset: usize,
880) -> FlatTreeNodePosition {
881    if container.is::<CharacterData>() {
882        return FlatTreeNodePosition::Inside(container);
883    }
884
885    let shadow_host_or_node = |node: &Node| {
886        container
887            .downcast::<ShadowRoot>()
888            .map(|shadow_root| DomRoot::upcast(shadow_root.Host()))
889            .unwrap_or(DomRoot::from_ref(node))
890    };
891
892    if let Some(child) = container.children().nth(offset) {
893        if let FlatTreeParent::Parent(_) = child.parent_in_flat_tree(no_gc) {
894            return FlatTreeNodePosition::Before(child);
895        }
896    } else if let Some(last_child) = container.GetLastChild() &&
897        let FlatTreeParent::Parent(_) = last_child.parent_in_flat_tree(no_gc)
898    {
899        return FlatTreeNodePosition::After(shadow_host_or_node(&container));
900    }
901
902    // The container has no child in the flat tree or the child indicated by the index
903    // isn't in the flat tree, so just return a position inside that container.
904    FlatTreeNodePosition::Inside(shadow_host_or_node(&container))
905}