Skip to main content

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