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