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