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