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::reflector::{Reflector, reflect_dom_object_with_cx};
11
12use crate::dom::abstractrange::bp_position;
13use crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, NodeMethods};
14use crate::dom::bindings::codegen::Bindings::RangeBinding::RangeMethods;
15use crate::dom::bindings::codegen::Bindings::SelectionBinding::SelectionMethods;
16use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
17use crate::dom::bindings::inheritance::Castable;
18use crate::dom::bindings::refcounted::Trusted;
19use crate::dom::bindings::reflector::DomGlobal;
20use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
21use crate::dom::bindings::str::DOMString;
22use crate::dom::document::Document;
23use crate::dom::eventtarget::EventTarget;
24use crate::dom::node::{Node, NodeTraits};
25use crate::dom::range::Range;
26
27#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
28enum Direction {
29 Forwards,
30 Backwards,
31 Directionless,
32}
33
34#[dom_struct]
35pub(crate) struct Selection {
36 reflector_: Reflector,
37 document: Dom<Document>,
38 range: MutNullableDom<Range>,
39 direction: Cell<Direction>,
40 /// <https://w3c.github.io/selection-api/#dfn-has-scheduled-selectionchange-event>
41 has_scheduled_selectionchange_event: Cell<bool>,
42}
43
44impl Selection {
45 fn new_inherited(document: &Document) -> Selection {
46 Selection {
47 reflector_: Reflector::new(),
48 document: Dom::from_ref(document),
49 range: MutNullableDom::new(None),
50 direction: Cell::new(Direction::Directionless),
51 has_scheduled_selectionchange_event: Cell::new(false),
52 }
53 }
54
55 pub(crate) fn new(cx: &mut JSContext, document: &Document) -> DomRoot<Selection> {
56 reflect_dom_object_with_cx(
57 Box::new(Selection::new_inherited(document)),
58 &*document.global(),
59 cx,
60 )
61 }
62
63 fn set_range(&self, range: &Range) {
64 // If we are setting to literally the same Range object
65 // (not just the same positions), then there's nothing changing
66 // and no task to queue.
67 if let Some(existing) = self.range.get() &&
68 &*existing == range
69 {
70 return;
71 }
72 self.range.set(Some(range));
73 range.associate_selection(self);
74 self.queue_selectionchange_task();
75 }
76
77 fn clear_range(&self) {
78 // If we already don't have a a Range object, then there's
79 // nothing changing and no task to queue.
80 if let Some(range) = self.range.get() {
81 range.disassociate_selection(self);
82 self.range.set(None);
83 self.queue_selectionchange_task();
84 }
85 }
86
87 /// <https://w3c.github.io/selection-api/#dfn-schedule-a-selectionchange-event>
88 pub(crate) fn queue_selectionchange_task(&self) {
89 // https://w3c.github.io/editing/docs/execCommand/#state-override
90 // https://w3c.github.io/editing/docs/execCommand/#value-override
91 // > Whenever the number of ranges in the selection changes to something
92 // > different, and whenever a boundary point of the range at a given index in the
93 // > selection changes to something different, the state override and value
94 // > override must be unset for every command.
95 self.document.clear_command_overrides();
96
97 // Step 1. If target's has scheduled selectionchange event is true, abort these steps.
98 if self.has_scheduled_selectionchange_event.get() {
99 return;
100 }
101 // Step 2. Set target's has scheduled selectionchange event to true.
102 self.has_scheduled_selectionchange_event.set(true);
103 // Step 3. Queue a task on the user interaction task source to fire a
104 // selectionchange event on target.
105 let this = Trusted::new(self);
106 self.document
107 .owner_global()
108 .task_manager()
109 .user_interaction_task_source() // w3c/selection-api#117
110 .queue(
111 // https://w3c.github.io/selection-api/#firing-selectionchange-event
112 task!(selectionchange_task_steps: move |cx| {
113 let this = this.root();
114 // Step 1. Set target's has scheduled selectionchange event to false.
115 this.has_scheduled_selectionchange_event.set(false);
116 // Step 2. If target is an element, fire an event named
117 // selectionchange, which bubbles and not cancelable, at target.
118 //
119 // n/a
120
121 // Step 3. Otherwise, if target is a document, fire an event named
122 // selectionchange, which does not bubble and not cancelable, at
123 // target.
124 this.document.upcast::<EventTarget>().fire_event(cx, atom!("selectionchange"));
125 }),
126 );
127 }
128
129 fn is_in_document_of_range(&self, node: &Node) -> bool {
130 // TODO(mrobinson): This should eventually allow nodes in the same composed tree (and
131 // not just the same tree), but this requires more work to allow `Selection` to cross
132 // shadow tree boundaries.
133 &*node.GetRootNode(&GetRootNodeOptions { composed: false }) ==
134 self.document.upcast::<Node>()
135 }
136
137 /// <https://w3c.github.io/editing/docs/execCommand/#active-range>
138 pub(crate) fn active_range(&self) -> Option<DomRoot<Range>> {
139 // > The active range is the range of the selection given by calling
140 // > getSelection() on the context object. (Thus the active range may be null.)
141 self.range.get()
142 }
143
144 pub(crate) fn collapse_current_range(&self, node: &Node, offset: u32) {
145 let range = self.range.get().expect("Must always have a range");
146 range.set_start(node, offset);
147 range.set_end(node, offset);
148 }
149
150 pub(crate) fn extend_current_range(&self, node: &Node, offset: u32) {
151 let range = self.range.get().expect("Must always have a range");
152 assert!(range.collapsed(), "Must only extend after collapsing");
153
154 let anchor_node = range.start_container();
155 if (*anchor_node == *node && range.start_offset() < offset) || anchor_node.is_before(node) {
156 range.set_end(node, offset);
157 self.direction.set(Direction::Forwards);
158 } else {
159 range.set_start(node, offset);
160 self.direction.set(Direction::Backwards);
161 }
162 }
163
164 /// <https://w3c.github.io/selection-api/#dfn-anchor>
165 pub(crate) fn anchor_node(&self) -> Option<DomRoot<Node>> {
166 self.range.get().map(|range| match self.direction.get() {
167 Direction::Forwards => range.start_container(),
168 _ => range.end_container(),
169 })
170 }
171
172 /// <https://w3c.github.io/selection-api/#dfn-anchor>
173 pub(crate) fn anchor_offset(&self) -> u32 {
174 self.range
175 .get()
176 .map(|range| match self.direction.get() {
177 Direction::Forwards => range.start_offset(),
178 _ => range.end_offset(),
179 })
180 .unwrap_or(0)
181 }
182
183 /// <https://w3c.github.io/selection-api/#dfn-focus>
184 pub(crate) fn focus_node(&self) -> Option<DomRoot<Node>> {
185 self.range.get().map(|range| match self.direction.get() {
186 Direction::Forwards => range.end_container(),
187 _ => range.start_container(),
188 })
189 }
190
191 /// <https://w3c.github.io/selection-api/#dfn-focus>
192 pub(crate) fn focus_offset(&self) -> u32 {
193 self.range
194 .get()
195 .map(|range| match self.direction.get() {
196 Direction::Forwards => range.end_offset(),
197 _ => range.start_offset(),
198 })
199 .unwrap_or(0)
200 }
201}
202
203impl SelectionMethods<crate::DomTypeHolder> for Selection {
204 /// <https://w3c.github.io/selection-api/#dom-selection-anchornode>
205 fn GetAnchorNode(&self) -> Option<DomRoot<Node>> {
206 // > The attribute must return the anchor node of this, or null if the anchor is
207 // > null or anchor is not in the document tree.
208 let anchor_node = self.anchor_node()?;
209 if !anchor_node.is_in_a_document_tree() {
210 return None;
211 }
212 Some(anchor_node)
213 }
214
215 /// <https://w3c.github.io/selection-api/#dom-selection-anchoroffset>
216 fn AnchorOffset(&self) -> u32 {
217 // > The attribute must return the anchor offset of this, or 0 if the anchor is null
218 // > or anchor is not in the document tree.
219 if self
220 .anchor_node()
221 .is_none_or(|anchor_node| !anchor_node.is_in_a_document_tree())
222 {
223 return 0;
224 }
225 self.anchor_offset()
226 }
227
228 /// <https://w3c.github.io/selection-api/#dom-selection-focusnode>
229 fn GetFocusNode(&self) -> Option<DomRoot<Node>> {
230 // > The attribute must return the focus node of this, or null if the focus is
231 // > null or focus is not in the document tree.
232 let focus_node = self.focus_node()?;
233 if !focus_node.is_in_a_document_tree() {
234 return None;
235 }
236 Some(focus_node)
237 }
238
239 /// <https://w3c.github.io/selection-api/#dom-selection-focusoffset>
240 fn FocusOffset(&self) -> u32 {
241 // > The attribute must return the focus offset of this, or 0 if the focus is null
242 // > or focus is not in the document tree.
243 if self
244 .focus_node()
245 .is_none_or(|focus_node| !focus_node.is_in_a_document_tree())
246 {
247 return 0;
248 }
249 self.focus_offset()
250 }
251
252 /// <https://w3c.github.io/selection-api/#dom-selection-iscollapsed>
253 fn IsCollapsed(&self) -> bool {
254 // > The attribute must return true if and only if the anchor and focus are the
255 // > same (including if both are null). Otherwise it must return false.
256 self.range.get().is_none_or(|range| range.collapsed())
257 }
258
259 /// <https://w3c.github.io/selection-api/#dom-selection-rangecount>
260 fn RangeCount(&self) -> u32 {
261 // > The attribute must return 0 if this is empty or either focus or anchor is not
262 // > in the document tree, and must return 1 otherwise.
263 let Some(range) = self.range.get() else {
264 return 0;
265 };
266 if !range.start_and_end_are_in_document_tree() {
267 return 0;
268 }
269 1
270 }
271
272 /// <https://w3c.github.io/selection-api/#dom-selection-type>
273 fn Type(&self) -> DOMString {
274 // > The attribute must return "None" if this is empty or either focus or anchor
275 // > is not in the document tree, "Caret" if this's range is collapsed, and "Range"
276 // > otherwise.
277 let Some(range) = self.range.get() else {
278 return DOMString::from("None");
279 };
280 if !range.start_and_end_are_in_document_tree() {
281 return DOMString::from("None");
282 }
283
284 if range.collapsed() {
285 DOMString::from("Caret")
286 } else {
287 DOMString::from("Range")
288 }
289 }
290
291 /// <https://w3c.github.io/selection-api/#dom-selection-getrangeat>
292 fn GetRangeAt(&self, index: u32) -> Fallible<DomRoot<Range>> {
293 // > The method must throw an IndexSizeError exception if index is not 0, or if this
294 // > is empty or either focus or anchor is not in the document tree. Otherwise, it
295 // > must return a reference to (not a copy of) this's range.
296 if index != 0 {
297 return Err(Error::IndexSize(None));
298 }
299
300 let Some(range) = self.range.get() else {
301 return Err(Error::IndexSize(None));
302 };
303
304 if !range.start_and_end_are_in_document_tree() {
305 return Err(Error::IndexSize(None));
306 }
307
308 Ok(DomRoot::from_ref(&range))
309 }
310
311 /// <https://w3c.github.io/selection-api/#dom-selection-addrange>
312 fn AddRange(&self, range: &Range) {
313 // Step 1. If the root of the range's boundary points are not the document
314 // associated with this, abort these steps.
315 if !self.is_in_document_of_range(&range.start_container()) {
316 return;
317 }
318
319 // Step 2. If rangeCount is not 0, abort these steps.
320 if self.RangeCount() != 0 {
321 return;
322 }
323
324 // Step 3. Set this's range to range by a strong reference (not by making a copy).
325 self.set_range(range);
326
327 // Are we supposed to set Direction here? w3c/selection-api#116
328 self.direction.set(Direction::Forwards);
329 }
330
331 /// <https://w3c.github.io/selection-api/#dom-selection-removerange>
332 fn RemoveRange(&self, range: &Range) -> ErrorResult {
333 // > The method must make this empty by disassociating its range if this's range
334 // > is range. Otherwise, it must throw a NotFoundError.
335 if let Some(own_range) = self.range.get() &&
336 &*own_range == range
337 {
338 self.clear_range();
339 return Ok(());
340 }
341 Err(Error::NotFound(None))
342 }
343
344 /// <https://w3c.github.io/selection-api/#dom-selection-removeallranges>
345 fn RemoveAllRanges(&self) {
346 // > The method must make this empty by disassociating its range if this has an
347 // > associated range.
348 self.clear_range();
349 }
350
351 /// <https://w3c.github.io/selection-api/#dom-selection-empty>
352 fn Empty(&self) {
353 // > The method must be an alias, and behave identically, to removeAllRanges().
354 self.clear_range();
355 }
356
357 /// <https://w3c.github.io/selection-api/#dom-selection-collapse>
358 fn Collapse(&self, cx: &mut JSContext, node: Option<&Node>, offset: u32) -> ErrorResult {
359 // Step 1. If node is null, this method must behave identically as
360 // removeAllRanges() and abort these steps.
361 let Some(node) = node else {
362 self.clear_range();
363 return Ok(());
364 };
365
366 // Step 2. If node is a DocumentType, throw an InvalidNodeTypeError exception and
367 // abort these steps.
368 if node.is_doctype() {
369 return Err(Error::InvalidNodeType(None));
370 }
371
372 // Step 3. The method must throw an IndexSizeError exception if offset is longer
373 // than node's length and abort these steps.
374 if offset > node.len() {
375 return Err(Error::IndexSize(None));
376 }
377
378 // Step 4. If document associated with this is not a shadow-including inclusive
379 // ancestor of node, abort these steps.
380 //
381 // TODO(mrobinson): This should eventually allow nodes in the same composed tree (and
382 // not just the same tree), but this requires more work to allow `Selection` to cross
383 // shadow tree boundaries.
384 if &*node.GetRootNode(&GetRootNodeOptions { composed: false }) !=
385 self.document.upcast::<Node>()
386 {
387 return Ok(());
388 }
389
390 // Step 5. Otherwise, let newRange be a new range.
391 // Step 6. Set the start the start and the end of newRange to (node, offset).
392 let new_range = Range::new(cx, &self.document, node, offset, node, offset);
393
394 // Step 7. Set this's range to newRange.
395 self.set_range(&new_range);
396
397 // Are we supposed to set Direction here? w3c/selection-api#116
398 self.direction.set(Direction::Forwards);
399
400 Ok(())
401 }
402
403 /// <https://w3c.github.io/selection-api/#dom-selection-setposition>
404 fn SetPosition(&self, cx: &mut JSContext, node: Option<&Node>, offset: u32) -> ErrorResult {
405 // > The method must be an alias, and behave identically, to collapse().
406 self.Collapse(cx, node, offset)
407 }
408
409 /// <https://w3c.github.io/selection-api/#dom-selection-collapsetostart>
410 fn CollapseToStart(&self, cx: &mut JSContext) -> ErrorResult {
411 // > The method must throw InvalidStateError exception if the this is empty.
412 // > Otherwise, it must create a new range, set the start both its start and end to
413 // > the start of this's range, and then set this's range to the newly-created
414 // > range.
415 if let Some(range) = self.range.get() {
416 self.Collapse(cx, Some(&*range.start_container()), range.start_offset())
417 } else {
418 Err(Error::InvalidState(None))
419 }
420 }
421
422 /// <https://w3c.github.io/selection-api/#dom-selection-collapsetoend>
423 fn CollapseToEnd(&self, cx: &mut JSContext) -> ErrorResult {
424 // > The method must throw InvalidStateError exception if the this is empty.
425 // > Otherwise, it must create a new range, set the start both its start and end to
426 // > the end of this's range, and then set this's range to the newly-created range.
427 if let Some(range) = self.range.get() {
428 self.Collapse(cx, Some(&*range.end_container()), range.end_offset())
429 } else {
430 Err(Error::InvalidState(None))
431 }
432 }
433
434 /// <https://w3c.github.io/selection-api/#dom-selection-extend>
435 fn Extend(&self, cx: &mut JSContext, node: &Node, offset: u32) -> ErrorResult {
436 // Step 1. If the document associated with this is not a shadow-including
437 // inclusive ancestor of node, abort these steps.
438 //
439 // TODO(mrobinson): This should eventually allow nodes in the same composed tree (and
440 // not just the same tree), but this requires more work to allow `Selection` to cross
441 // shadow tree boundaries.
442 if &*node.GetRootNode(&GetRootNodeOptions { composed: false }) !=
443 self.document.upcast::<Node>()
444 {
445 return Ok(());
446 }
447
448 // Step 2. If this is empty, throw an InvalidStateError exception and abort these steps.
449 let Some(range) = self.range.get() else {
450 return Err(Error::InvalidState(None));
451 };
452
453 // This isn't specified, but it appears to be implementation behavior of other
454 // browsers. See w3c/selection-api#118.
455 if node.is_doctype() {
456 return Err(Error::InvalidNodeType(None));
457 }
458
459 // As with is_doctype, this is not explicit in the selection specification steps
460 // here but implied by which exceptions are thrown in WPT tests.
461 if offset > node.len() {
462 return Err(Error::IndexSize(None));
463 }
464
465 // Step 3. Let oldAnchor and oldFocus be the this's anchor and focus, and let
466 // newFocus be the boundary point (node, offset).
467 //
468 // Note: oldFocus is unused, so we do not set it here.
469 let old_anchor_node = &*self
470 .anchor_node()
471 .expect("has range, therefore has anchor node");
472 let old_anchor_offset = self.anchor_offset();
473
474 // Step 4. Let newRange be a new range.
475 let new_range;
476 let direction;
477
478 // Step 5. If node's root is not the same as the this's range's root, set the
479 // start newRange's start and end to newFocus.
480 if !self.is_in_document_of_range(&range.start_container()) {
481 new_range = Range::new(cx, &self.document, node, offset, node, offset);
482 direction = Direction::Forwards;
483 } else {
484 let is_old_anchor_before_or_equal = matches!(
485 bp_position(old_anchor_node, old_anchor_offset, node, offset),
486 Ordering::Less | Ordering::Equal
487 );
488 if is_old_anchor_before_or_equal {
489 // Step 6. Otherwise, if oldAnchor is before or equal to newFocus, set the start
490 // newRange's start to oldAnchor, then set its end to newFocus.
491 new_range = Range::new(
492 cx,
493 &self.document,
494 old_anchor_node,
495 old_anchor_offset,
496 node,
497 offset,
498 );
499 direction = Direction::Forwards;
500 } else {
501 // Step 7. Otherwise, set the start newRange's start to newFocus, then set
502 // its end to oldAnchor.
503 new_range = Range::new(
504 cx,
505 &self.document,
506 node,
507 offset,
508 old_anchor_node,
509 old_anchor_offset,
510 );
511 direction = Direction::Backwards;
512 }
513 }
514
515 // Step 8. Set this's range to newRange.
516 self.set_range(&new_range);
517
518 // Step 9. If newFocus is before oldAnchor, set this's direction to backwards.
519 // Otherwise, set it to forwards.
520 self.direction.set(direction);
521
522 Ok(())
523 }
524
525 /// <https://w3c.github.io/selection-api/#dom-selection-setbaseandextent>
526 fn SetBaseAndExtent(
527 &self,
528 cx: &mut JSContext,
529 anchor_node: &Node,
530 anchor_offset: u32,
531 focus_node: &Node,
532 focus_offset: u32,
533 ) -> ErrorResult {
534 // This isn't specified, but it appears to be implementation behavior of other
535 // browsers. See w3c/selection-api#118.
536 if anchor_node.is_doctype() || focus_node.is_doctype() {
537 return Err(Error::InvalidNodeType(None));
538 }
539
540 // Step 1. If anchorOffset is longer than anchorNode's length or if focusOffset is
541 // longer than focusNode's length, throw an IndexSizeError exception and abort
542 // these steps.
543 if anchor_offset > anchor_node.len() || focus_offset > focus_node.len() {
544 return Err(Error::IndexSize(None));
545 }
546
547 // Step 2. If document associated with this is not a shadow-including inclusive
548 // ancestor of anchorNode or focusNode, abort these steps.
549 //
550 // TODO(mrobinson): This should eventually allow nodes in the same composed tree (and
551 // not just the same tree), but this requires more work to allow `Selection` to cross
552 // shadow tree boundaries.
553 if &*anchor_node.GetRootNode(&GetRootNodeOptions { composed: false }) !=
554 self.document.upcast::<Node>()
555 {
556 return Ok(());
557 }
558 if &*focus_node.GetRootNode(&GetRootNodeOptions { composed: false }) !=
559 self.document.upcast::<Node>()
560 {
561 return Ok(());
562 }
563
564 // Step 3. Let anchor be the boundary point (anchorNode, anchorOffset) and let
565 // focus be the boundary point (focusNode, focusOffset).
566 //
567 // Note: We do not model the boundary point in this way.
568
569 // Step 4. Let newRange be a new range.
570 let new_range;
571 let direction;
572
573 // Step 5. If anchor is before focus, set the start the newRange's start to anchor
574 // and its end to focus. Otherwise, set the start them to focus and anchor
575 // respectively.
576 let is_anchor_before_focus =
577 bp_position(anchor_node, anchor_offset, focus_node, focus_offset) == Ordering::Less;
578 if is_anchor_before_focus {
579 new_range = Range::new(
580 cx,
581 &self.document,
582 anchor_node,
583 anchor_offset,
584 focus_node,
585 focus_offset,
586 );
587 direction = Direction::Forwards;
588 } else {
589 new_range = Range::new(
590 cx,
591 &self.document,
592 focus_node,
593 focus_offset,
594 anchor_node,
595 anchor_offset,
596 );
597 direction = Direction::Backwards;
598 }
599
600 // Step 6. Set this's range to newRange.
601 self.set_range(&new_range);
602
603 // Step 7. If focus is before anchor, set this's direction to backwards.
604 // Otherwise, set it to forwards
605 self.direction.set(direction);
606
607 Ok(())
608 }
609
610 /// <https://w3c.github.io/selection-api/#dom-selection-selectallchildren>
611 fn SelectAllChildren(&self, cx: &mut JSContext, node: &Node) -> ErrorResult {
612 // Step 1. If node is a DocumentType, throw an InvalidNodeTypeError exception and
613 // abort these steps.
614 if node.is_doctype() {
615 return Err(Error::InvalidNodeType(None));
616 }
617
618 // Step 2. If node's root is not the document associated with this, abort these
619 // steps.
620 if !self.is_in_document_of_range(node) {
621 return Ok(());
622 }
623
624 // Let newRange be a new range and childCount be the number of children of node.
625 let child_count = node.children_count();
626
627 // Step 4. Set newRange's start to (node, 0).
628 // Step 5. Set newRange's end to (node, childCount).
629 let new_range = Range::new(cx, &self.document, node, 0, node, child_count);
630
631 // Step 6. Set this's range to newRange.
632 self.set_range(&new_range);
633
634 // Step 7. Set this's direction to forwards.
635 self.direction.set(Direction::Forwards);
636
637 Ok(())
638 }
639
640 /// <https://w3c.github.io/selection-api/#dom-selection-deletecontents>
641 fn DeleteFromDocument(&self, cx: &mut JSContext) -> ErrorResult {
642 // > The method must invoke deleteContents() on this's range if this is not empty
643 // > and both focus and anchor are in the document tree. Otherwise the method must
644 // > do nothing.
645 let Some(range) = self.range.get() else {
646 return Ok(());
647 };
648 if !range.start_and_end_are_in_document_tree() {
649 return Ok(());
650 }
651
652 range.DeleteContents(cx)
653 }
654
655 /// <https://w3c.github.io/selection-api/#dom-selection-containsnode>
656 fn ContainsNode(&self, node: &Node, allow_partial_containment: bool) -> bool {
657 // > The method must return false if this is empty or if node's root is not the document
658 // > associated with this.
659 // >
660 // > Otherwise, if allowPartialContainment is false, the method must return true if and only
661 // > if start of its range is before or visually equivalent to the first boundary point in
662 // > the node *and* end of its range is after or visually equivalent to the last boundary
663 // > point in the node.
664 // >
665 // > If allowPartialContainment is true, the method must return true if and only if start of
666 // > its range is before or visually equivalent to the last boundary point in the node *and*
667 // > end of its range is after or visually equivalent to the first boundary point in the
668 // > node.
669
670 if !self.is_in_document_of_range(node) {
671 return false;
672 }
673 let Some(range) = self.range.get() else {
674 return false;
675 };
676 let start_node = &*range.start_container();
677 if !self.is_in_document_of_range(start_node) {
678 return false;
679 }
680 let end_node = &*range.end_container();
681
682 let first_offset = 0;
683 let last_offset = node.len();
684 let (compare_start_to, compare_end_to) = if allow_partial_containment {
685 (last_offset, first_offset)
686 } else {
687 (first_offset, last_offset)
688 };
689
690 // TODO: find out what "visually equivalent" means for boundary points and implement it.
691 // https://github.com/w3c/selection-api/issues/6
692 // For now it is simplified to "position is equal".
693 matches!(
694 bp_position(start_node, range.start_offset(), node, compare_start_to),
695 Ordering::Less | Ordering::Equal
696 ) && matches!(
697 bp_position(end_node, range.end_offset(), node, compare_end_to),
698 Ordering::Greater | Ordering::Equal
699 )
700 }
701
702 /// <https://w3c.github.io/selection-api/#dom-selection-stringifier>
703 fn Stringifier(&self, no_gc: &NoGC) -> DOMString {
704 // > The stringification must return the string, which is the concatenation of the
705 // > rendered text if there is a range associated with this.
706 // >
707 // > If the selection is within a textarea or input element, it must return the
708 // > selected substring in its value.
709 //
710 // TODO: This implementation should be examined in depth. Does rendered text take
711 // into account `display: none`. The case for textarea and input elements is
712 // completely unhandled here.
713 if let Some(range) = self.range.get() {
714 range.Stringifier(no_gc)
715 } else {
716 DOMString::from("")
717 }
718 }
719}