script/dom/
textcontrol.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
5//! This is an abstraction used by `HTMLInputElement` and `HTMLTextAreaElement` to implement the
6//! text control selection DOM API.
7//!
8//! <https://html.spec.whatwg.org/multipage/#textFieldSelection>
9
10use base::text::Utf16CodeUnitLength;
11
12use crate::clipboard_provider::EmbedderClipboardProvider;
13use crate::dom::bindings::cell::DomRefCell;
14use crate::dom::bindings::codegen::Bindings::HTMLFormElementBinding::SelectionMode;
15use crate::dom::bindings::conversions::DerivedFrom;
16use crate::dom::bindings::error::{Error, ErrorResult};
17use crate::dom::bindings::str::DOMString;
18use crate::dom::event::{EventBubbles, EventCancelable};
19use crate::dom::eventtarget::EventTarget;
20use crate::dom::node::{Node, NodeDamage, NodeTraits};
21use crate::textinput::{SelectionDirection, SelectionState, TextInput};
22
23pub(crate) trait TextControlElement: DerivedFrom<EventTarget> + DerivedFrom<Node> {
24    fn selection_api_applies(&self) -> bool;
25    fn has_selectable_text(&self) -> bool;
26    fn has_selection(&self) -> bool;
27    fn set_dirty_value_flag(&self, value: bool);
28    fn select_all(&self);
29}
30
31pub(crate) struct TextControlSelection<'a, E: TextControlElement> {
32    element: &'a E,
33    textinput: &'a DomRefCell<TextInput<EmbedderClipboardProvider>>,
34}
35
36impl<'a, E: TextControlElement> TextControlSelection<'a, E> {
37    pub(crate) fn new(
38        element: &'a E,
39        textinput: &'a DomRefCell<TextInput<EmbedderClipboardProvider>>,
40    ) -> Self {
41        TextControlSelection { element, textinput }
42    }
43
44    /// <https://html.spec.whatwg.org/multipage/#dom-textarea/input-select>
45    pub(crate) fn dom_select(&self) {
46        // Step 1: If this element is an input element, and either select() does not apply
47        // to this element or the corresponding control has no selectable text, return.
48        if !self.element.has_selectable_text() {
49            return;
50        }
51
52        // Step 2 : Set the selection range with 0 and infinity.
53        self.set_range(
54            Some(Utf16CodeUnitLength::zero()),
55            Some(Utf16CodeUnitLength(usize::MAX)),
56            None,
57            None,
58        );
59    }
60
61    // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionstart
62    pub(crate) fn dom_start(&self) -> Option<Utf16CodeUnitLength> {
63        // Step 1
64        if !self.element.selection_api_applies() {
65            return None;
66        }
67
68        // Steps 2-3
69        Some(self.start())
70    }
71
72    // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionstart
73    pub(crate) fn set_dom_start(&self, start: Option<Utf16CodeUnitLength>) -> ErrorResult {
74        // Step 1: If this element is an input element, and selectionStart does not apply
75        // to this element, throw an "InvalidStateError" DOMException.
76        if !self.element.selection_api_applies() {
77            return Err(Error::InvalidState(None));
78        }
79
80        // Step 2: Let end be the value of this element's selectionEnd attribute.
81        let mut end = self.end();
82
83        // Step 3: If end is less than the given value, set end to the given value.
84        match start {
85            Some(start) if end < start => end = start,
86            _ => {},
87        }
88
89        // Step 4: Set the selection range with the given value, end, and the value of
90        // this element's selectionDirection attribute.
91        self.set_range(start, Some(end), Some(self.direction()), None);
92        Ok(())
93    }
94
95    // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionend
96    pub(crate) fn dom_end(&self) -> Option<Utf16CodeUnitLength> {
97        // Step 1: If this element is an input element, and selectionEnd does not apply to
98        // this element, return null.
99        if !self.element.selection_api_applies() {
100            return None;
101        }
102
103        // Step 2: If there is no selection, return the code unit offset within the
104        // relevant value to the character that immediately follows the text entry cursor.
105        // Step 3: Return the code unit offset within the relevant value to the character
106        // that immediately follows the end of the selection.
107        Some(self.end())
108    }
109
110    // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionend
111    pub(crate) fn set_dom_end(&self, end: Option<Utf16CodeUnitLength>) -> ErrorResult {
112        // Step 1: If this element is an input element, and selectionEnd does not apply to
113        // this element, throw an "InvalidStateError" DOMException.
114        if !self.element.selection_api_applies() {
115            return Err(Error::InvalidState(None));
116        }
117
118        // Step 2: Set the selection range with the value of this element's selectionStart
119        // attribute, the given value, and the value of this element's selectionDirection
120        // attribute.
121        self.set_range(Some(self.start()), end, Some(self.direction()), None);
122        Ok(())
123    }
124
125    // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectiondirection
126    pub(crate) fn dom_direction(&self) -> Option<DOMString> {
127        // Step 1
128        if !self.element.selection_api_applies() {
129            return None;
130        }
131
132        Some(DOMString::from(self.direction()))
133    }
134
135    // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectiondirection
136    pub(crate) fn set_dom_direction(&self, direction: Option<DOMString>) -> ErrorResult {
137        // Step 1
138        if !self.element.selection_api_applies() {
139            return Err(Error::InvalidState(None));
140        }
141
142        // Step 2
143        self.set_range(
144            Some(self.start()),
145            Some(self.end()),
146            direction.map(SelectionDirection::from),
147            None,
148        );
149        Ok(())
150    }
151
152    // https://html.spec.whatwg.org/multipage/#dom-textarea/input-setselectionrange
153    pub(crate) fn set_dom_range(
154        &self,
155        start: Utf16CodeUnitLength,
156        end: Utf16CodeUnitLength,
157        direction: Option<DOMString>,
158    ) -> ErrorResult {
159        // Step 1
160        if !self.element.selection_api_applies() {
161            return Err(Error::InvalidState(None));
162        }
163
164        // Step 2
165        self.set_range(
166            Some(start),
167            Some(end),
168            direction.map(SelectionDirection::from),
169            None,
170        );
171        Ok(())
172    }
173
174    // https://html.spec.whatwg.org/multipage/#dom-textarea/input-setrangetext
175    pub(crate) fn set_dom_range_text(
176        &self,
177        replacement: DOMString,
178        start: Option<Utf16CodeUnitLength>,
179        end: Option<Utf16CodeUnitLength>,
180        selection_mode: SelectionMode,
181    ) -> ErrorResult {
182        // Step 1: If this element is an input element, and setRangeText() does not apply
183        // to this element, throw an "InvalidStateError" DOMException.
184        if !self.element.selection_api_applies() {
185            return Err(Error::InvalidState(None));
186        }
187
188        // Step 2: Set this element's dirty value flag to true.
189        self.element.set_dirty_value_flag(true);
190
191        // Step 3: If the method has only one argument, then let start and end have the
192        // values of the selectionStart attribute and the selectionEnd attribute
193        // respectively.
194        //
195        // Otherwise, let start, end have the values of the second and third arguments
196        // respectively.
197        let mut selection_start = self.start();
198        let mut selection_end = self.end();
199        let mut start = start.unwrap_or(selection_start);
200        let mut end = end.unwrap_or(selection_end);
201
202        // Step 4: If start is greater than end, then throw an "IndexSizeError"
203        // DOMException.
204        if start > end {
205            return Err(Error::IndexSize(None));
206        }
207
208        // Save the original selection state to later pass to set_selection_range, because we will
209        // change the selection state in order to replace the text in the range.
210        let original_selection_state = self.textinput.borrow().selection_state();
211
212        // Step 5: If start is greater than the length of the relevant value of the text
213        // control, then set it to the length of the relevant value of the text control.
214        let content_length = self.textinput.borrow().len_utf16();
215        if start > content_length {
216            start = content_length;
217        }
218
219        // Step 6: If end is greater than the length of the relevant value of the text
220        // control, then set it to the length of the relevant value of the text controlV
221        if end > content_length {
222            end = content_length;
223        }
224
225        // Step 7: Let selection start be the current value of the selectionStart
226        // attribute.
227        // Step 8: Let selection end be the current value of the selectionEnd attribute.
228        //
229        // NOTE: These were assigned above.
230
231        {
232            // Step 9: If start is less than end, delete the sequence of code units within
233            // the element's relevant value starting with the code unit at the startth
234            // position and ending with the code unit at the (end-1)th position.
235            //
236            // Step: 10: Insert the value of the first argument into the text of the
237            // relevant value of the text control, immediately before the startth code
238            // unit.
239            let mut textinput = self.textinput.borrow_mut();
240            textinput.set_selection_range_utf16(start, end, SelectionDirection::None);
241            textinput.replace_selection(&replacement);
242        }
243
244        // Step 11: Let *new length* be the length of the value of the first argument.
245        //
246        // Must come before the textinput.replace_selection() call, as replacement gets moved in
247        // that call.
248        let new_length = replacement.len_utf16();
249
250        // Step 12: Let new end be the sum of start and new length.
251        let new_end = start + new_length;
252
253        // Step 13: Run the appropriate set of substeps from the following list:
254        match selection_mode {
255            // ↪ If the fourth argument's value is "select"
256            //     Let selection start be start.
257            //     Let selection end be new end.
258            SelectionMode::Select => {
259                selection_start = start;
260                selection_end = new_end;
261            },
262
263            // ↪ If the fourth argument's value is "start"
264            //     Let selection start and selection end be start.
265            SelectionMode::Start => {
266                selection_start = start;
267                selection_end = start;
268            },
269
270            // ↪ If the fourth argument's value is "end"
271            //     Let selection start and selection end be new end
272            SelectionMode::End => {
273                selection_start = new_end;
274                selection_end = new_end;
275            },
276
277            //  ↪ If the fourth argument's value is "preserve"
278            // If the method has only one argument
279            SelectionMode::Preserve => {
280                // Sub-step 1: Let old length be end minus start.
281                let old_length = end.saturating_sub(start);
282
283                // Sub-step 2: Let delta be new length minus old length.
284                let delta = (new_length.0 as isize) - (old_length.0 as isize);
285
286                // Sub-step 3: If selection start is greater than end, then increment it
287                // by delta. (If delta is negative, i.e. the new text is shorter than the
288                // old text, then this will decrease the value of selection start.)
289                //
290                // Otherwise: if selection start is greater than start, then set it to
291                // start. (This snaps the start of the selection to the start of the new
292                // text if it was in the middle of the text that it replaced.)
293                if selection_start > end {
294                    selection_start =
295                        Utf16CodeUnitLength::from((selection_start.0 as isize) + delta);
296                } else if selection_start > start {
297                    selection_start = start;
298                }
299
300                // Sub-step 4: If selection end is greater than end, then increment it by
301                // delta in the same way.
302                //
303                // Otherwise: if selection end is greater than start, then set it to new
304                // end. (This snaps the end of the selection to the end of the new text if
305                // it was in the middle of the text that it replaced.)
306                if selection_end > end {
307                    selection_end = Utf16CodeUnitLength::from((selection_end.0 as isize) + delta);
308                } else if selection_end > start {
309                    selection_end = new_end;
310                }
311            },
312        }
313
314        // Step 14: Set the selection range with selection start and selection end.
315        self.set_range(
316            Some(selection_start),
317            Some(selection_end),
318            None,
319            Some(original_selection_state),
320        );
321        Ok(())
322    }
323
324    fn start(&self) -> Utf16CodeUnitLength {
325        self.textinput.borrow().selection_start_utf16()
326    }
327
328    fn end(&self) -> Utf16CodeUnitLength {
329        self.textinput.borrow().selection_end_utf16()
330    }
331
332    fn direction(&self) -> SelectionDirection {
333        self.textinput.borrow().selection_direction()
334    }
335
336    /// <https://html.spec.whatwg.org/multipage/#set-the-selection-range>
337    fn set_range(
338        &self,
339        start: Option<Utf16CodeUnitLength>,
340        end: Option<Utf16CodeUnitLength>,
341        direction: Option<SelectionDirection>,
342        original_selection_state: Option<SelectionState>,
343    ) {
344        let mut textinput = self.textinput.borrow_mut();
345        let original_selection_state =
346            original_selection_state.unwrap_or_else(|| textinput.selection_state());
347
348        // To set the selection range with an integer or null start, an integer or null or
349        // the special value infinity end, and optionally a string direction, run the
350        // following steps:
351        //
352        // Step 1: If start is null, let start be 0.
353        let start = start.unwrap_or_default();
354
355        // Step 2: If end is null, let end be 0.
356        let end = end.unwrap_or_default();
357
358        // Step 3: Set the selection of the text control to the sequence of code units
359        // within the relevant value starting with the code unit at the startth position
360        // (in logical order) and ending with the code unit at the (end-1)th position.
361        // Arguments greater than the length of the relevant value of the text control
362        // (including the special value infinity) must be treated as pointing at the end
363        // of the text control. If end is less than or equal to start, then the start of
364        // the selection and the end of the selection must both be placed immediately
365        // before the character with offset end. In UAs where there is no concept of an
366        // empty selection, this must set the cursor to be just before the character with
367        // offset end.
368        //
369        // Step 4: If direction is not identical to either "backward" or "forward", or if
370        // the direction argument was not given, set direction to "none".
371        //
372        // Step 5: Set the selection direction of the text control to direction.
373        textinput.set_selection_range_utf16(
374            start,
375            end,
376            direction.unwrap_or(SelectionDirection::None),
377        );
378
379        // Step 6: If the previous steps caused the selection of the text control to be
380        // modified (in either extent or direction), then queue an element task on the
381        // user interaction task source given the element to fire an event named select at
382        // the element, with the bubbles attribute initialized to true.
383        if textinput.selection_state() != original_selection_state {
384            self.element
385                .owner_global()
386                .task_manager()
387                .user_interaction_task_source()
388                .queue_event(
389                    self.element.upcast::<EventTarget>(),
390                    atom!("select"),
391                    EventBubbles::Bubbles,
392                    EventCancelable::NotCancelable,
393                );
394        }
395
396        self.element.upcast::<Node>().dirty(NodeDamage::Other);
397    }
398}