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