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