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