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