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 script_bindings::cell::DomRefCell;
13use servo_base::text::Utf16CodeUnitLength;
14
15use crate::dom::bindings::codegen::Bindings::HTMLFormElementBinding::SelectionMode;
16use crate::dom::bindings::conversions::DerivedFrom;
17use crate::dom::bindings::error::{Error, ErrorResult};
18use crate::dom::bindings::str::DOMString;
19use crate::dom::event::{EventBubbles, EventCancelable};
20use crate::dom::eventtarget::EventTarget;
21use crate::dom::html::form_controls::text_input::{
22 EmbedderClipboardProvider, SelectionDirection, SelectionState, TextInput,
23};
24use crate::dom::node::{Node, NodeTraits};
25use crate::dom::types::Element;
26
27pub(crate) trait TextControlElement:
28 DerivedFrom<EventTarget> + DerivedFrom<Node> + DerivedFrom<Element>
29{
30 fn selection_api_applies(&self) -> bool;
31 fn has_selectable_text(&self) -> bool;
32 fn has_uncollapsed_selection(&self) -> bool;
33 fn set_dirty_value_flag(&self, value: bool);
34 fn select_all(&self);
35 fn maybe_update_shared_selection(&self);
36 fn is_password_field(&self) -> bool {
37 false
38 }
39 fn placeholder_text<'a>(&'a self) -> Ref<'a, DOMString>;
40 fn value_text(&self) -> DOMString;
41}
42
43pub(crate) struct TextControlSelection<'a, E: TextControlElement> {
44 element: &'a E,
45 textinput: &'a DomRefCell<TextInput<EmbedderClipboardProvider>>,
46}
47
48impl<'a, E: TextControlElement> TextControlSelection<'a, E> {
49 pub(crate) fn new(
50 element: &'a E,
51 textinput: &'a DomRefCell<TextInput<EmbedderClipboardProvider>>,
52 ) -> Self {
53 TextControlSelection { element, textinput }
54 }
55
56 /// <https://html.spec.whatwg.org/multipage/#dom-textarea/input-select>
57 pub(crate) fn dom_select(&self) {
58 // Step 1: If this element is an input element, and either select() does not apply
59 // to this element or the corresponding control has no selectable text, return.
60 if !self.element.has_selectable_text() {
61 return;
62 }
63
64 // Step 2 : Set the selection range with 0 and infinity.
65 self.set_range(
66 Some(Utf16CodeUnitLength::zero()),
67 Some(Utf16CodeUnitLength(usize::MAX)),
68 None,
69 None,
70 );
71 }
72
73 // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionstart
74 pub(crate) fn dom_start(&self) -> Option<Utf16CodeUnitLength> {
75 // Step 1
76 if !self.element.selection_api_applies() {
77 return None;
78 }
79
80 // Steps 2-3
81 Some(self.start())
82 }
83
84 // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionstart
85 pub(crate) fn set_dom_start(&self, start: Option<Utf16CodeUnitLength>) -> ErrorResult {
86 // Step 1: If this element is an input element, and selectionStart does not apply
87 // to this element, throw an "InvalidStateError" DOMException.
88 if !self.element.selection_api_applies() {
89 return Err(Error::InvalidState(None));
90 }
91
92 // Step 2: Let end be the value of this element's selectionEnd attribute.
93 let mut end = self.end();
94
95 // Step 3: If end is less than the given value, set end to the given value.
96 match start {
97 Some(start) if end < start => end = start,
98 _ => {},
99 }
100
101 // Step 4: Set the selection range with the given value, end, and the value of
102 // this element's selectionDirection attribute.
103 self.set_range(start, Some(end), Some(self.direction()), None);
104 Ok(())
105 }
106
107 // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionend
108 pub(crate) fn dom_end(&self) -> Option<Utf16CodeUnitLength> {
109 // Step 1: If this element is an input element, and selectionEnd does not apply to
110 // this element, return null.
111 if !self.element.selection_api_applies() {
112 return None;
113 }
114
115 // Step 2: If there is no selection, return the code unit offset within the
116 // relevant value to the character that immediately follows the text entry cursor.
117 // Step 3: Return the code unit offset within the relevant value to the character
118 // that immediately follows the end of the selection.
119 Some(self.end())
120 }
121
122 // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionend
123 pub(crate) fn set_dom_end(&self, end: Option<Utf16CodeUnitLength>) -> ErrorResult {
124 // Step 1: If this element is an input element, and selectionEnd does not apply to
125 // this element, throw an "InvalidStateError" DOMException.
126 if !self.element.selection_api_applies() {
127 return Err(Error::InvalidState(None));
128 }
129
130 // Step 2: Set the selection range with the value of this element's selectionStart
131 // attribute, the given value, and the value of this element's selectionDirection
132 // attribute.
133 self.set_range(Some(self.start()), end, Some(self.direction()), None);
134 Ok(())
135 }
136
137 // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectiondirection
138 pub(crate) fn dom_direction(&self) -> Option<DOMString> {
139 // Step 1
140 if !self.element.selection_api_applies() {
141 return None;
142 }
143
144 Some(DOMString::from(self.direction()))
145 }
146
147 // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectiondirection
148 pub(crate) fn set_dom_direction(&self, direction: Option<DOMString>) -> ErrorResult {
149 // Step 1
150 if !self.element.selection_api_applies() {
151 return Err(Error::InvalidState(None));
152 }
153
154 // Step 2
155 self.set_range(
156 Some(self.start()),
157 Some(self.end()),
158 direction.map(SelectionDirection::from),
159 None,
160 );
161 Ok(())
162 }
163
164 // https://html.spec.whatwg.org/multipage/#dom-textarea/input-setselectionrange
165 pub(crate) fn set_dom_range(
166 &self,
167 start: Utf16CodeUnitLength,
168 end: Utf16CodeUnitLength,
169 direction: Option<DOMString>,
170 ) -> ErrorResult {
171 // Step 1
172 if !self.element.selection_api_applies() {
173 return Err(Error::InvalidState(None));
174 }
175
176 // Step 2
177 self.set_range(
178 Some(start),
179 Some(end),
180 direction.map(SelectionDirection::from),
181 None,
182 );
183 Ok(())
184 }
185
186 // https://html.spec.whatwg.org/multipage/#dom-textarea/input-setrangetext
187 pub(crate) fn set_dom_range_text(
188 &self,
189 replacement: DOMString,
190 start: Option<Utf16CodeUnitLength>,
191 end: Option<Utf16CodeUnitLength>,
192 selection_mode: SelectionMode,
193 ) -> ErrorResult {
194 // Step 1: If this element is an input element, and setRangeText() does not apply
195 // to this element, throw an "InvalidStateError" DOMException.
196 if !self.element.selection_api_applies() {
197 return Err(Error::InvalidState(None));
198 }
199
200 // Step 2: Set this element's dirty value flag to true.
201 self.element.set_dirty_value_flag(true);
202
203 // Step 3: If the method has only one argument, then let start and end have the
204 // values of the selectionStart attribute and the selectionEnd attribute
205 // respectively.
206 //
207 // Otherwise, let start, end have the values of the second and third arguments
208 // respectively.
209 let mut selection_start = self.start();
210 let mut selection_end = self.end();
211 let mut start = start.unwrap_or(selection_start);
212 let mut end = end.unwrap_or(selection_end);
213
214 // Step 4: If start is greater than end, then throw an "IndexSizeError"
215 // DOMException.
216 if start > end {
217 return Err(Error::IndexSize(None));
218 }
219
220 // Save the original selection state to later pass to set_selection_range, because we will
221 // change the selection state in order to replace the text in the range.
222 let original_selection_state = self.textinput.borrow().selection_state();
223
224 // Step 5: If start is greater than the length of the relevant value of the text
225 // control, then set it to the length of the relevant value of the text control.
226 let content_length = self.textinput.borrow().len_utf16();
227 if start > content_length {
228 start = content_length;
229 }
230
231 // Step 6: If end is greater than the length of the relevant value of the text
232 // control, then set it to the length of the relevant value of the text controlV
233 if end > content_length {
234 end = content_length;
235 }
236
237 // Step 7: Let selection start be the current value of the selectionStart
238 // attribute.
239 // Step 8: Let selection end be the current value of the selectionEnd attribute.
240 //
241 // NOTE: These were assigned above.
242
243 {
244 // Step 9: If start is less than end, delete the sequence of code units within
245 // the element's relevant value starting with the code unit at the startth
246 // position and ending with the code unit at the (end-1)th position.
247 //
248 // Step: 10: Insert the value of the first argument into the text of the
249 // relevant value of the text control, immediately before the startth code
250 // unit.
251 let mut textinput = self.textinput.borrow_mut();
252 textinput.set_selection_range_utf16(start, end, SelectionDirection::None);
253 textinput.replace_selection(&replacement);
254 }
255
256 // Step 11: Let *new length* be the length of the value of the first argument.
257 //
258 // Must come before the textinput.replace_selection() call, as replacement gets moved in
259 // that call.
260 let new_length = replacement.len_utf16();
261
262 // Step 12: Let new end be the sum of start and new length.
263 let new_end = start + new_length;
264
265 // Step 13: Run the appropriate set of substeps from the following list:
266 match selection_mode {
267 // ↪ If the fourth argument's value is "select"
268 // Let selection start be start.
269 // Let selection end be new end.
270 SelectionMode::Select => {
271 selection_start = start;
272 selection_end = new_end;
273 },
274
275 // ↪ If the fourth argument's value is "start"
276 // Let selection start and selection end be start.
277 SelectionMode::Start => {
278 selection_start = start;
279 selection_end = start;
280 },
281
282 // ↪ If the fourth argument's value is "end"
283 // Let selection start and selection end be new end
284 SelectionMode::End => {
285 selection_start = new_end;
286 selection_end = new_end;
287 },
288
289 // ↪ If the fourth argument's value is "preserve"
290 // If the method has only one argument
291 SelectionMode::Preserve => {
292 // Sub-step 1: Let old length be end minus start.
293 let old_length = end.saturating_sub(start);
294
295 // Sub-step 2: Let delta be new length minus old length.
296 let delta = (new_length.0 as isize) - (old_length.0 as isize);
297
298 // Sub-step 3: If selection start is greater than end, then increment it
299 // by delta. (If delta is negative, i.e. the new text is shorter than the
300 // old text, then this will decrease the value of selection start.)
301 //
302 // Otherwise: if selection start is greater than start, then set it to
303 // start. (This snaps the start of the selection to the start of the new
304 // text if it was in the middle of the text that it replaced.)
305 if selection_start > end {
306 selection_start =
307 Utf16CodeUnitLength::from((selection_start.0 as isize) + delta);
308 } else if selection_start > start {
309 selection_start = start;
310 }
311
312 // Sub-step 4: If selection end is greater than end, then increment it by
313 // delta in the same way.
314 //
315 // Otherwise: if selection end is greater than start, then set it to new
316 // end. (This snaps the end of the selection to the end of the new text if
317 // it was in the middle of the text that it replaced.)
318 if selection_end > end {
319 selection_end = Utf16CodeUnitLength::from((selection_end.0 as isize) + delta);
320 } else if selection_end > start {
321 selection_end = new_end;
322 }
323 },
324 }
325
326 // Step 14: Set the selection range with selection start and selection end.
327 self.set_range(
328 Some(selection_start),
329 Some(selection_end),
330 None,
331 Some(original_selection_state),
332 );
333 Ok(())
334 }
335
336 fn start(&self) -> Utf16CodeUnitLength {
337 self.textinput.borrow().selection_start_utf16()
338 }
339
340 fn end(&self) -> Utf16CodeUnitLength {
341 self.textinput.borrow().selection_end_utf16()
342 }
343
344 fn direction(&self) -> SelectionDirection {
345 self.textinput.borrow().selection_direction()
346 }
347
348 /// <https://html.spec.whatwg.org/multipage/#set-the-selection-range>
349 fn set_range(
350 &self,
351 start: Option<Utf16CodeUnitLength>,
352 end: Option<Utf16CodeUnitLength>,
353 direction: Option<SelectionDirection>,
354 original_selection_state: Option<SelectionState>,
355 ) {
356 let original_selection_state =
357 original_selection_state.unwrap_or_else(|| self.textinput.borrow().selection_state());
358
359 // To set the selection range with an integer or null start, an integer or null or
360 // the special value infinity end, and optionally a string direction, run the
361 // following steps:
362 //
363 // Step 1: If start is null, let start be 0.
364 let start = start.unwrap_or_default();
365
366 // Step 2: If end is null, let end be 0.
367 let end = end.unwrap_or_default();
368
369 // Step 3: Set the selection of the text control to the sequence of code units
370 // within the relevant value starting with the code unit at the startth position
371 // (in logical order) and ending with the code unit at the (end-1)th position.
372 // Arguments greater than the length of the relevant value of the text control
373 // (including the special value infinity) must be treated as pointing at the end
374 // of the text control. If end is less than or equal to start, then the start of
375 // the selection and the end of the selection must both be placed immediately
376 // before the character with offset end. In UAs where there is no concept of an
377 // empty selection, this must set the cursor to be just before the character with
378 // offset end.
379 //
380 // Step 4: If direction is not identical to either "backward" or "forward", or if
381 // the direction argument was not given, set direction to "none".
382 //
383 // Step 5: Set the selection direction of the text control to direction.
384 self.textinput.borrow_mut().set_selection_range_utf16(
385 start,
386 end,
387 direction.unwrap_or(SelectionDirection::None),
388 );
389
390 // Step 6: If the previous steps caused the selection of the text control to be
391 // modified (in either extent or direction), then queue an element task on the
392 // user interaction task source given the element to fire an event named select at
393 // the element, with the bubbles attribute initialized to true.
394 if self.textinput.borrow().selection_state() == original_selection_state {
395 return;
396 }
397
398 self.element
399 .owner_global()
400 .task_manager()
401 .user_interaction_task_source()
402 .queue_event(
403 self.element.upcast::<EventTarget>(),
404 atom!("select"),
405 EventBubbles::Bubbles,
406 EventCancelable::NotCancelable,
407 );
408 self.element.maybe_update_shared_selection();
409 }
410}