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