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