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