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