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) -> ∈
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}