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
110impl SelectionMethods<crate::DomTypeHolder> for Selection {
111    // https://w3c.github.io/selection-api/#dom-selection-anchornode
112    fn GetAnchorNode(&self) -> Option<DomRoot<Node>> {
113        if let Some(range) = self.range.get() {
114            match self.direction.get() {
115                Direction::Forwards => Some(range.start_container()),
116                _ => Some(range.end_container()),
117            }
118        } else {
119            None
120        }
121    }
122
123    // https://w3c.github.io/selection-api/#dom-selection-anchoroffset
124    fn AnchorOffset(&self) -> u32 {
125        if let Some(range) = self.range.get() {
126            match self.direction.get() {
127                Direction::Forwards => range.start_offset(),
128                _ => range.end_offset(),
129            }
130        } else {
131            0
132        }
133    }
134
135    // https://w3c.github.io/selection-api/#dom-selection-focusnode
136    fn GetFocusNode(&self) -> Option<DomRoot<Node>> {
137        if let Some(range) = self.range.get() {
138            match self.direction.get() {
139                Direction::Forwards => Some(range.end_container()),
140                _ => Some(range.start_container()),
141            }
142        } else {
143            None
144        }
145    }
146
147    // https://w3c.github.io/selection-api/#dom-selection-focusoffset
148    fn FocusOffset(&self) -> u32 {
149        if let Some(range) = self.range.get() {
150            match self.direction.get() {
151                Direction::Forwards => range.end_offset(),
152                _ => range.start_offset(),
153            }
154        } else {
155            0
156        }
157    }
158
159    // https://w3c.github.io/selection-api/#dom-selection-iscollapsed
160    fn IsCollapsed(&self) -> bool {
161        if let Some(range) = self.range.get() {
162            range.collapsed()
163        } else {
164            true
165        }
166    }
167
168    // https://w3c.github.io/selection-api/#dom-selection-rangecount
169    fn RangeCount(&self) -> u32 {
170        if self.range.get().is_some() { 1 } else { 0 }
171    }
172
173    // https://w3c.github.io/selection-api/#dom-selection-type
174    fn Type(&self) -> DOMString {
175        if let Some(range) = self.range.get() {
176            if range.collapsed() {
177                DOMString::from("Caret")
178            } else {
179                DOMString::from("Range")
180            }
181        } else {
182            DOMString::from("None")
183        }
184    }
185
186    // https://w3c.github.io/selection-api/#dom-selection-getrangeat
187    fn GetRangeAt(&self, index: u32) -> Fallible<DomRoot<Range>> {
188        if index != 0 {
189            Err(Error::IndexSize)
190        } else if let Some(range) = self.range.get() {
191            Ok(DomRoot::from_ref(&range))
192        } else {
193            Err(Error::IndexSize)
194        }
195    }
196
197    // https://w3c.github.io/selection-api/#dom-selection-addrange
198    fn AddRange(&self, range: &Range) {
199        // Step 1
200        if !self.is_same_root(&range.start_container()) {
201            return;
202        }
203
204        // Step 2
205        if self.RangeCount() != 0 {
206            return;
207        }
208
209        // Step 3
210        self.set_range(range);
211        // Are we supposed to set Direction here? w3c/selection-api#116
212        self.direction.set(Direction::Forwards);
213    }
214
215    // https://w3c.github.io/selection-api/#dom-selection-removerange
216    fn RemoveRange(&self, range: &Range) -> ErrorResult {
217        if let Some(own_range) = self.range.get() {
218            if &*own_range == range {
219                self.clear_range();
220                return Ok(());
221            }
222        }
223        Err(Error::NotFound)
224    }
225
226    // https://w3c.github.io/selection-api/#dom-selection-removeallranges
227    fn RemoveAllRanges(&self) {
228        self.clear_range();
229    }
230
231    // https://w3c.github.io/selection-api/#dom-selection-empty
232    // TODO: When implementing actual selection UI, this may be the correct
233    // method to call as the abandon-selection action
234    fn Empty(&self) {
235        self.clear_range();
236    }
237
238    // https://w3c.github.io/selection-api/#dom-selection-collapse
239    fn Collapse(&self, node: Option<&Node>, offset: u32, can_gc: CanGc) -> ErrorResult {
240        if let Some(node) = node {
241            if node.is_doctype() {
242                // w3c/selection-api#118
243                return Err(Error::InvalidNodeType);
244            }
245            if offset > node.len() {
246                // Step 2
247                return Err(Error::IndexSize);
248            }
249
250            if !self.is_same_root(node) {
251                // Step 3
252                return Ok(());
253            }
254
255            // Steps 4-5
256            let range = Range::new(&self.document, node, offset, node, offset, can_gc);
257
258            // Step 6
259            self.set_range(&range);
260            // Are we supposed to set Direction here? w3c/selection-api#116
261            //
262            self.direction.set(Direction::Forwards);
263        } else {
264            // Step 1
265            self.clear_range();
266        }
267        Ok(())
268    }
269
270    // https://w3c.github.io/selection-api/#dom-selection-setposition
271    // TODO: When implementing actual selection UI, this may be the correct
272    // method to call as the start-of-selection action, after a
273    // selectstart event has fired and not been cancelled.
274    fn SetPosition(&self, node: Option<&Node>, offset: u32, can_gc: CanGc) -> ErrorResult {
275        self.Collapse(node, offset, can_gc)
276    }
277
278    // https://w3c.github.io/selection-api/#dom-selection-collapsetostart
279    fn CollapseToStart(&self, can_gc: CanGc) -> ErrorResult {
280        if let Some(range) = self.range.get() {
281            self.Collapse(
282                Some(&*range.start_container()),
283                range.start_offset(),
284                can_gc,
285            )
286        } else {
287            Err(Error::InvalidState)
288        }
289    }
290
291    // https://w3c.github.io/selection-api/#dom-selection-collapsetoend
292    fn CollapseToEnd(&self, can_gc: CanGc) -> ErrorResult {
293        if let Some(range) = self.range.get() {
294            self.Collapse(Some(&*range.end_container()), range.end_offset(), can_gc)
295        } else {
296            Err(Error::InvalidState)
297        }
298    }
299
300    // https://w3c.github.io/selection-api/#dom-selection-extend
301    // TODO: When implementing actual selection UI, this may be the correct
302    // method to call as the continue-selection action
303    fn Extend(&self, node: &Node, offset: u32, can_gc: CanGc) -> ErrorResult {
304        if !self.is_same_root(node) {
305            // Step 1
306            return Ok(());
307        }
308        if let Some(range) = self.range.get() {
309            if node.is_doctype() {
310                // w3c/selection-api#118
311                return Err(Error::InvalidNodeType);
312            }
313
314            if offset > node.len() {
315                // As with is_doctype, not explicit in selection spec steps here
316                // but implied by which exceptions are thrown in WPT tests
317                return Err(Error::IndexSize);
318            }
319
320            // Step 4
321            if !self.is_same_root(&range.start_container()) {
322                // Step 5, and its following 8 and 9
323                self.set_range(&Range::new(
324                    &self.document,
325                    node,
326                    offset,
327                    node,
328                    offset,
329                    can_gc,
330                ));
331                self.direction.set(Direction::Forwards);
332            } else {
333                let old_anchor_node = &*self.GetAnchorNode().unwrap(); // has range, therefore has anchor node
334                let old_anchor_offset = self.AnchorOffset();
335                let is_old_anchor_before_or_equal = {
336                    if old_anchor_node == node {
337                        old_anchor_offset <= offset
338                    } else {
339                        old_anchor_node.is_before(node)
340                    }
341                };
342                if is_old_anchor_before_or_equal {
343                    // Step 6, and its following 8 and 9
344                    self.set_range(&Range::new(
345                        &self.document,
346                        old_anchor_node,
347                        old_anchor_offset,
348                        node,
349                        offset,
350                        can_gc,
351                    ));
352                    self.direction.set(Direction::Forwards);
353                } else {
354                    // Step 7, and its following 8 and 9
355                    self.set_range(&Range::new(
356                        &self.document,
357                        node,
358                        offset,
359                        old_anchor_node,
360                        old_anchor_offset,
361                        can_gc,
362                    ));
363                    self.direction.set(Direction::Backwards);
364                }
365            };
366        } else {
367            // Step 2
368            return Err(Error::InvalidState);
369        }
370        Ok(())
371    }
372
373    // https://w3c.github.io/selection-api/#dom-selection-setbaseandextent
374    fn SetBaseAndExtent(
375        &self,
376        anchor_node: &Node,
377        anchor_offset: u32,
378        focus_node: &Node,
379        focus_offset: u32,
380        can_gc: CanGc,
381    ) -> ErrorResult {
382        // Step 1
383        if anchor_node.is_doctype() || focus_node.is_doctype() {
384            // w3c/selection-api#118
385            return Err(Error::InvalidNodeType);
386        }
387
388        if anchor_offset > anchor_node.len() || focus_offset > focus_node.len() {
389            return Err(Error::IndexSize);
390        }
391
392        // Step 2
393        if !self.is_same_root(anchor_node) || !self.is_same_root(focus_node) {
394            return Ok(());
395        }
396
397        // Steps 5-7
398        let is_focus_before_anchor = {
399            if anchor_node == focus_node {
400                focus_offset < anchor_offset
401            } else {
402                focus_node.is_before(anchor_node)
403            }
404        };
405        if is_focus_before_anchor {
406            self.set_range(&Range::new(
407                &self.document,
408                focus_node,
409                focus_offset,
410                anchor_node,
411                anchor_offset,
412                can_gc,
413            ));
414            self.direction.set(Direction::Backwards);
415        } else {
416            self.set_range(&Range::new(
417                &self.document,
418                anchor_node,
419                anchor_offset,
420                focus_node,
421                focus_offset,
422                can_gc,
423            ));
424            self.direction.set(Direction::Forwards);
425        }
426        Ok(())
427    }
428
429    // https://w3c.github.io/selection-api/#dom-selection-selectallchildren
430    fn SelectAllChildren(&self, node: &Node, can_gc: CanGc) -> ErrorResult {
431        if node.is_doctype() {
432            // w3c/selection-api#118
433            return Err(Error::InvalidNodeType);
434        }
435        if !self.is_same_root(node) {
436            return Ok(());
437        }
438
439        // Spec wording just says node length here, but WPT specifically
440        // wants number of children (the main difference is that it's 0
441        // for cdata).
442        self.set_range(&Range::new(
443            &self.document,
444            node,
445            0,
446            node,
447            node.children_count(),
448            can_gc,
449        ));
450
451        self.direction.set(Direction::Forwards);
452        Ok(())
453    }
454
455    // https://w3c.github.io/selection-api/#dom-selection-deletecontents
456    fn DeleteFromDocument(&self) -> ErrorResult {
457        if let Some(range) = self.range.get() {
458            // Since the range is changing, it should trigger a
459            // selectionchange event as it would if if mutated any other way
460            return range.DeleteContents();
461        }
462        Ok(())
463    }
464
465    // https://w3c.github.io/selection-api/#dom-selection-containsnode
466    fn ContainsNode(&self, node: &Node, allow_partial_containment: bool) -> bool {
467        // TODO: Spec requires a "visually equivalent to" check, which is
468        // probably up to a layout query. This is therefore not a full implementation.
469        if !self.is_same_root(node) {
470            return false;
471        }
472        if let Some(range) = self.range.get() {
473            let start_node = &*range.start_container();
474            if !self.is_same_root(start_node) {
475                // node can't be contained in a range with a different root
476                return false;
477            }
478            if allow_partial_containment {
479                // Spec seems to be incorrect here, w3c/selection-api#116
480                if node.is_before(start_node) {
481                    return false;
482                }
483                let end_node = &*range.end_container();
484                if end_node.is_before(node) {
485                    return false;
486                }
487                if node == start_node {
488                    return range.start_offset() < node.len();
489                }
490                if node == end_node {
491                    return range.end_offset() > 0;
492                }
493                true
494            } else {
495                if node.is_before(start_node) {
496                    return false;
497                }
498                let end_node = &*range.end_container();
499                if end_node.is_before(node) {
500                    return false;
501                }
502                if node == start_node {
503                    return range.start_offset() == 0;
504                }
505                if node == end_node {
506                    return range.end_offset() == node.len();
507                }
508                true
509            }
510        } else {
511            // No range
512            false
513        }
514    }
515
516    // https://w3c.github.io/selection-api/#dom-selection-stringifier
517    fn Stringifier(&self) -> DOMString {
518        // The spec as of Jan 31 2020 just says
519        // "See W3C bug 10583." for this method.
520        // Stringifying the range seems at least approximately right
521        // and passes the non-style-dependent case in the WPT tests.
522        if let Some(range) = self.range.get() {
523            range.Stringifier()
524        } else {
525            DOMString::from("")
526        }
527    }
528}