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;
6
7use dom_struct::dom_struct;
8use js::context::JSContext;
9use script_bindings::reflector::{Reflector, reflect_dom_object_with_cx};
10
11use crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, NodeMethods};
12use crate::dom::bindings::codegen::Bindings::RangeBinding::RangeMethods;
13use crate::dom::bindings::codegen::Bindings::SelectionBinding::SelectionMethods;
14use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
15use crate::dom::bindings::inheritance::Castable;
16use crate::dom::bindings::refcounted::Trusted;
17use crate::dom::bindings::reflector::DomGlobal;
18use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
19use crate::dom::bindings::str::DOMString;
20use crate::dom::document::Document;
21use crate::dom::eventtarget::EventTarget;
22use crate::dom::node::{Node, NodeTraits};
23use crate::dom::range::Range;
24use crate::script_runtime::CanGc;
25
26#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
27enum Direction {
28    Forwards,
29    Backwards,
30    Directionless,
31}
32
33#[dom_struct]
34pub(crate) struct Selection {
35    reflector_: Reflector,
36    document: Dom<Document>,
37    range: MutNullableDom<Range>,
38    direction: Cell<Direction>,
39    /// <https://w3c.github.io/selection-api/#dfn-has-scheduled-selectionchange-event>
40    has_scheduled_selectionchange_event: Cell<bool>,
41}
42
43impl Selection {
44    fn new_inherited(document: &Document) -> Selection {
45        Selection {
46            reflector_: Reflector::new(),
47            document: Dom::from_ref(document),
48            range: MutNullableDom::new(None),
49            direction: Cell::new(Direction::Directionless),
50            has_scheduled_selectionchange_event: Cell::new(false),
51        }
52    }
53
54    pub(crate) fn new(cx: &mut JSContext, document: &Document) -> DomRoot<Selection> {
55        reflect_dom_object_with_cx(
56            Box::new(Selection::new_inherited(document)),
57            &*document.global(),
58            cx,
59        )
60    }
61
62    fn set_range(&self, range: &Range) {
63        // If we are setting to literally the same Range object
64        // (not just the same positions), then there's nothing changing
65        // and no task to queue.
66        if let Some(existing) = self.range.get() &&
67            &*existing == range
68        {
69            return;
70        }
71        self.range.set(Some(range));
72        range.associate_selection(self);
73        self.queue_selectionchange_task();
74    }
75
76    fn clear_range(&self) {
77        // If we already don't have a a Range object, then there's
78        // nothing changing and no task to queue.
79        if let Some(range) = self.range.get() {
80            range.disassociate_selection(self);
81            self.range.set(None);
82            self.queue_selectionchange_task();
83        }
84    }
85
86    /// <https://w3c.github.io/selection-api/#dfn-schedule-a-selectionchange-event>
87    pub(crate) fn queue_selectionchange_task(&self) {
88        // https://w3c.github.io/editing/docs/execCommand/#state-override
89        // https://w3c.github.io/editing/docs/execCommand/#value-override
90        // > Whenever the number of ranges in the selection changes to something different,
91        // > and whenever a boundary point of the range at a given index in the selection changes
92        // > to something different, the state override and value override must be unset for every command.
93        self.document.clear_command_overrides();
94
95        // Step 1. If target's has scheduled selectionchange event is true, abort these steps.
96        if self.has_scheduled_selectionchange_event.get() {
97            return;
98        }
99        // Step 2. Set target's has scheduled selectionchange event to true.
100        self.has_scheduled_selectionchange_event.set(true);
101        // Step 3. Queue a task on the user interaction task source to fire a selectionchange event on target.
102        let this = Trusted::new(self);
103        self.document
104            .owner_global()
105            .task_manager()
106            .user_interaction_task_source() // w3c/selection-api#117
107            .queue(
108                // https://w3c.github.io/selection-api/#firing-selectionchange-event
109                task!(selectionchange_task_steps: move |cx| {
110                    let this = this.root();
111                    // Step 1. Set target's has scheduled selectionchange event to false.
112                    this.has_scheduled_selectionchange_event.set(false);
113                    // Step 2. If target is an element, fire an event named selectionchange, which bubbles and not cancelable, at target.
114                    //
115                    // n/a
116
117                    // Step 3. Otherwise, if target is a document, fire an event named selectionchange,
118                    // which does not bubble and not cancelable, at target.
119                    this.document.upcast::<EventTarget>().fire_event(cx, atom!("selectionchange"));
120                }),
121            );
122    }
123
124    fn is_same_root(&self, node: &Node) -> bool {
125        &*node.GetRootNode(&GetRootNodeOptions::empty()) == self.document.upcast::<Node>()
126    }
127
128    /// <https://w3c.github.io/editing/docs/execCommand/#active-range>
129    pub(crate) fn active_range(&self) -> Option<DomRoot<Range>> {
130        // > The active range is the range of the selection given by calling getSelection() on the context object. (Thus the active range may be null.)
131        self.range.get()
132    }
133
134    pub(crate) fn collapse_current_range(&self, node: &Node, offset: u32) {
135        let range = self.range.get().expect("Must always have a range");
136        range.set_start(node, offset);
137        range.set_end(node, offset);
138    }
139
140    pub(crate) fn extend_current_range(&self, node: &Node, offset: u32) {
141        let range = self.range.get().expect("Must always have a range");
142        assert!(range.collapsed(), "Must only extend after collapsing");
143
144        let anchor_node = range.start_container();
145        if (*anchor_node == *node && range.start_offset() < offset) || anchor_node.is_before(node) {
146            range.set_end(node, offset);
147            self.direction.set(Direction::Forwards);
148        } else {
149            range.set_start(node, offset);
150            self.direction.set(Direction::Backwards);
151        }
152    }
153}
154
155impl SelectionMethods<crate::DomTypeHolder> for Selection {
156    /// <https://w3c.github.io/selection-api/#dom-selection-anchornode>
157    fn GetAnchorNode(&self) -> Option<DomRoot<Node>> {
158        if let Some(range) = self.range.get() {
159            match self.direction.get() {
160                Direction::Forwards => Some(range.start_container()),
161                _ => Some(range.end_container()),
162            }
163        } else {
164            None
165        }
166    }
167
168    /// <https://w3c.github.io/selection-api/#dom-selection-anchoroffset>
169    fn AnchorOffset(&self) -> u32 {
170        if let Some(range) = self.range.get() {
171            match self.direction.get() {
172                Direction::Forwards => range.start_offset(),
173                _ => range.end_offset(),
174            }
175        } else {
176            0
177        }
178    }
179
180    /// <https://w3c.github.io/selection-api/#dom-selection-focusnode>
181    fn GetFocusNode(&self) -> Option<DomRoot<Node>> {
182        if let Some(range) = self.range.get() {
183            match self.direction.get() {
184                Direction::Forwards => Some(range.end_container()),
185                _ => Some(range.start_container()),
186            }
187        } else {
188            None
189        }
190    }
191
192    /// <https://w3c.github.io/selection-api/#dom-selection-focusoffset>
193    fn FocusOffset(&self) -> u32 {
194        if let Some(range) = self.range.get() {
195            match self.direction.get() {
196                Direction::Forwards => range.end_offset(),
197                _ => range.start_offset(),
198            }
199        } else {
200            0
201        }
202    }
203
204    /// <https://w3c.github.io/selection-api/#dom-selection-iscollapsed>
205    fn IsCollapsed(&self) -> bool {
206        if let Some(range) = self.range.get() {
207            range.collapsed()
208        } else {
209            true
210        }
211    }
212
213    /// <https://w3c.github.io/selection-api/#dom-selection-rangecount>
214    fn RangeCount(&self) -> u32 {
215        if self.range.get().is_some() { 1 } else { 0 }
216    }
217
218    /// <https://w3c.github.io/selection-api/#dom-selection-type>
219    fn Type(&self) -> DOMString {
220        if let Some(range) = self.range.get() {
221            if range.collapsed() {
222                DOMString::from("Caret")
223            } else {
224                DOMString::from("Range")
225            }
226        } else {
227            DOMString::from("None")
228        }
229    }
230
231    /// <https://w3c.github.io/selection-api/#dom-selection-getrangeat>
232    fn GetRangeAt(&self, index: u32) -> Fallible<DomRoot<Range>> {
233        if index != 0 {
234            Err(Error::IndexSize(None))
235        } else if let Some(range) = self.range.get() {
236            Ok(DomRoot::from_ref(&range))
237        } else {
238            Err(Error::IndexSize(None))
239        }
240    }
241
242    /// <https://w3c.github.io/selection-api/#dom-selection-addrange>
243    fn AddRange(&self, range: &Range) {
244        // Step 1
245        if !self.is_same_root(&range.start_container()) {
246            return;
247        }
248
249        // Step 2
250        if self.RangeCount() != 0 {
251            return;
252        }
253
254        // Step 3
255        self.set_range(range);
256        // Are we supposed to set Direction here? w3c/selection-api#116
257        self.direction.set(Direction::Forwards);
258    }
259
260    /// <https://w3c.github.io/selection-api/#dom-selection-removerange>
261    fn RemoveRange(&self, range: &Range) -> ErrorResult {
262        if let Some(own_range) = self.range.get() &&
263            &*own_range == range
264        {
265            self.clear_range();
266            return Ok(());
267        }
268        Err(Error::NotFound(None))
269    }
270
271    /// <https://w3c.github.io/selection-api/#dom-selection-removeallranges>
272    fn RemoveAllRanges(&self) {
273        self.clear_range();
274    }
275
276    // https://w3c.github.io/selection-api/#dom-selection-empty
277    // TODO: When implementing actual selection UI, this may be the correct
278    // method to call as the abandon-selection action
279    fn Empty(&self) {
280        self.clear_range();
281    }
282
283    /// <https://w3c.github.io/selection-api/#dom-selection-collapse>
284    fn Collapse(&self, cx: &mut JSContext, node: Option<&Node>, offset: u32) -> ErrorResult {
285        if let Some(node) = node {
286            if node.is_doctype() {
287                // w3c/selection-api#118
288                return Err(Error::InvalidNodeType(None));
289            }
290            if offset > node.len() {
291                // Step 2
292                return Err(Error::IndexSize(None));
293            }
294
295            if !self.is_same_root(node) {
296                // Step 3
297                return Ok(());
298            }
299
300            // Steps 4-5
301            let range = Range::new(
302                &self.document,
303                node,
304                offset,
305                node,
306                offset,
307                CanGc::from_cx(cx),
308            );
309
310            // Step 6
311            self.set_range(&range);
312            // Are we supposed to set Direction here? w3c/selection-api#116
313            //
314            self.direction.set(Direction::Forwards);
315        } else {
316            // Step 1
317            self.clear_range();
318        }
319        Ok(())
320    }
321
322    // https://w3c.github.io/selection-api/#dom-selection-setposition
323    // TODO: When implementing actual selection UI, this may be the correct
324    // method to call as the start-of-selection action, after a
325    // selectstart event has fired and not been cancelled.
326    fn SetPosition(&self, cx: &mut JSContext, node: Option<&Node>, offset: u32) -> ErrorResult {
327        self.Collapse(cx, node, offset)
328    }
329
330    /// <https://w3c.github.io/selection-api/#dom-selection-collapsetostart>
331    fn CollapseToStart(&self, cx: &mut JSContext) -> ErrorResult {
332        if let Some(range) = self.range.get() {
333            self.Collapse(cx, Some(&*range.start_container()), range.start_offset())
334        } else {
335            Err(Error::InvalidState(None))
336        }
337    }
338
339    /// <https://w3c.github.io/selection-api/#dom-selection-collapsetoend>
340    fn CollapseToEnd(&self, cx: &mut JSContext) -> ErrorResult {
341        if let Some(range) = self.range.get() {
342            self.Collapse(cx, Some(&*range.end_container()), range.end_offset())
343        } else {
344            Err(Error::InvalidState(None))
345        }
346    }
347
348    // https://w3c.github.io/selection-api/#dom-selection-extend
349    // TODO: When implementing actual selection UI, this may be the correct
350    // method to call as the continue-selection action
351    fn Extend(&self, cx: &mut JSContext, node: &Node, offset: u32) -> ErrorResult {
352        if !self.is_same_root(node) {
353            // Step 1
354            return Ok(());
355        }
356        if let Some(range) = self.range.get() {
357            if node.is_doctype() {
358                // w3c/selection-api#118
359                return Err(Error::InvalidNodeType(None));
360            }
361
362            if offset > node.len() {
363                // As with is_doctype, not explicit in selection spec steps here
364                // but implied by which exceptions are thrown in WPT tests
365                return Err(Error::IndexSize(None));
366            }
367
368            // Step 4
369            if !self.is_same_root(&range.start_container()) {
370                // Step 5, and its following 8 and 9
371                self.set_range(&Range::new(
372                    &self.document,
373                    node,
374                    offset,
375                    node,
376                    offset,
377                    CanGc::from_cx(cx),
378                ));
379                self.direction.set(Direction::Forwards);
380            } else {
381                let old_anchor_node = &*self.GetAnchorNode().unwrap(); // has range, therefore has anchor node
382                let old_anchor_offset = self.AnchorOffset();
383                let is_old_anchor_before_or_equal = {
384                    if old_anchor_node == node {
385                        old_anchor_offset <= offset
386                    } else {
387                        old_anchor_node.is_before(node)
388                    }
389                };
390                if is_old_anchor_before_or_equal {
391                    // Step 6, and its following 8 and 9
392                    self.set_range(&Range::new(
393                        &self.document,
394                        old_anchor_node,
395                        old_anchor_offset,
396                        node,
397                        offset,
398                        CanGc::from_cx(cx),
399                    ));
400                    self.direction.set(Direction::Forwards);
401                } else {
402                    // Step 7, and its following 8 and 9
403                    self.set_range(&Range::new(
404                        &self.document,
405                        node,
406                        offset,
407                        old_anchor_node,
408                        old_anchor_offset,
409                        CanGc::from_cx(cx),
410                    ));
411                    self.direction.set(Direction::Backwards);
412                }
413            };
414        } else {
415            // Step 2
416            return Err(Error::InvalidState(None));
417        }
418        Ok(())
419    }
420
421    /// <https://w3c.github.io/selection-api/#dom-selection-setbaseandextent>
422    fn SetBaseAndExtent(
423        &self,
424        cx: &mut JSContext,
425        anchor_node: &Node,
426        anchor_offset: u32,
427        focus_node: &Node,
428        focus_offset: u32,
429    ) -> ErrorResult {
430        // Step 1
431        if anchor_node.is_doctype() || focus_node.is_doctype() {
432            // w3c/selection-api#118
433            return Err(Error::InvalidNodeType(None));
434        }
435
436        if anchor_offset > anchor_node.len() || focus_offset > focus_node.len() {
437            return Err(Error::IndexSize(None));
438        }
439
440        // Step 2
441        if !self.is_same_root(anchor_node) || !self.is_same_root(focus_node) {
442            return Ok(());
443        }
444
445        // Steps 5-7
446        let is_focus_before_anchor = {
447            if anchor_node == focus_node {
448                focus_offset < anchor_offset
449            } else {
450                focus_node.is_before(anchor_node)
451            }
452        };
453        if is_focus_before_anchor {
454            self.set_range(&Range::new(
455                &self.document,
456                focus_node,
457                focus_offset,
458                anchor_node,
459                anchor_offset,
460                CanGc::from_cx(cx),
461            ));
462            self.direction.set(Direction::Backwards);
463        } else {
464            self.set_range(&Range::new(
465                &self.document,
466                anchor_node,
467                anchor_offset,
468                focus_node,
469                focus_offset,
470                CanGc::from_cx(cx),
471            ));
472            self.direction.set(Direction::Forwards);
473        }
474        Ok(())
475    }
476
477    /// <https://w3c.github.io/selection-api/#dom-selection-selectallchildren>
478    fn SelectAllChildren(&self, cx: &mut JSContext, node: &Node) -> ErrorResult {
479        if node.is_doctype() {
480            // w3c/selection-api#118
481            return Err(Error::InvalidNodeType(None));
482        }
483        if !self.is_same_root(node) {
484            return Ok(());
485        }
486
487        // Spec wording just says node length here, but WPT specifically
488        // wants number of children (the main difference is that it's 0
489        // for cdata).
490        self.set_range(&Range::new(
491            &self.document,
492            node,
493            0,
494            node,
495            node.children_count(),
496            CanGc::from_cx(cx),
497        ));
498
499        self.direction.set(Direction::Forwards);
500        Ok(())
501    }
502
503    /// <https://w3c.github.io/selection-api/#dom-selection-deletecontents>
504    fn DeleteFromDocument(&self, cx: &mut JSContext) -> ErrorResult {
505        if let Some(range) = self.range.get() {
506            // Since the range is changing, it should trigger a
507            // selectionchange event as it would if if mutated any other way
508            return range.DeleteContents(cx);
509        }
510        Ok(())
511    }
512
513    /// <https://w3c.github.io/selection-api/#dom-selection-containsnode>
514    fn ContainsNode(&self, node: &Node, allow_partial_containment: bool) -> bool {
515        // TODO: Spec requires a "visually equivalent to" check, which is
516        // probably up to a layout query. This is therefore not a full implementation.
517        if !self.is_same_root(node) {
518            return false;
519        }
520        if let Some(range) = self.range.get() {
521            let start_node = &*range.start_container();
522            if !self.is_same_root(start_node) {
523                // node can't be contained in a range with a different root
524                return false;
525            }
526            if allow_partial_containment {
527                // Spec seems to be incorrect here, w3c/selection-api#116
528                if node.is_before(start_node) {
529                    return false;
530                }
531                let end_node = &*range.end_container();
532                if end_node.is_before(node) {
533                    return false;
534                }
535                if node == start_node {
536                    return range.start_offset() < node.len();
537                }
538                if node == end_node {
539                    return range.end_offset() > 0;
540                }
541                true
542            } else {
543                if node.is_before(start_node) {
544                    return false;
545                }
546                let end_node = &*range.end_container();
547                if end_node.is_before(node) {
548                    return false;
549                }
550                if node == start_node {
551                    return range.start_offset() == 0;
552                }
553                if node == end_node {
554                    return range.end_offset() == node.len();
555                }
556                true
557            }
558        } else {
559            // No range
560            false
561        }
562    }
563
564    /// <https://w3c.github.io/selection-api/#dom-selection-stringifier>
565    fn Stringifier(&self) -> DOMString {
566        // The spec as of Jan 31 2020 just says
567        // "See W3C bug 10583." for this method.
568        // Stringifying the range seems at least approximately right
569        // and passes the non-style-dependent case in the WPT tests.
570        if let Some(range) = self.range.get() {
571            range.Stringifier()
572        } else {
573            DOMString::from("")
574        }
575    }
576}