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}