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