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 crate::clipboard_provider::EmbedderClipboardProvider;
11use crate::dom::bindings::cell::DomRefCell;
12use crate::dom::bindings::codegen::Bindings::HTMLFormElementBinding::SelectionMode;
13use crate::dom::bindings::conversions::DerivedFrom;
14use crate::dom::bindings::error::{Error, ErrorResult};
15use crate::dom::bindings::str::DOMString;
16use crate::dom::event::{EventBubbles, EventCancelable};
17use crate::dom::eventtarget::EventTarget;
18use crate::dom::node::{Node, NodeDamage, NodeTraits};
19use crate::textinput::{SelectionDirection, SelectionState, TextInput, UTF8Bytes};
20
21pub(crate) trait TextControlElement: DerivedFrom<EventTarget> + DerivedFrom<Node> {
22    fn selection_api_applies(&self) -> bool;
23    fn has_selectable_text(&self) -> bool;
24    fn set_dirty_value_flag(&self, value: bool);
25}
26
27pub(crate) struct TextControlSelection<'a, E: TextControlElement> {
28    element: &'a E,
29    textinput: &'a DomRefCell<TextInput<EmbedderClipboardProvider>>,
30}
31
32impl<'a, E: TextControlElement> TextControlSelection<'a, E> {
33    pub(crate) fn new(
34        element: &'a E,
35        textinput: &'a DomRefCell<TextInput<EmbedderClipboardProvider>>,
36    ) -> Self {
37        TextControlSelection { element, textinput }
38    }
39
40    // https://html.spec.whatwg.org/multipage/#dom-textarea/input-select
41    pub(crate) fn dom_select(&self) {
42        // Step 1
43        if !self.element.has_selectable_text() {
44            return;
45        }
46
47        // Step 2
48        self.set_range(Some(0), Some(u32::MAX), None, None);
49    }
50
51    // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionstart
52    pub(crate) fn dom_start(&self) -> Option<u32> {
53        // Step 1
54        if !self.element.selection_api_applies() {
55            return None;
56        }
57
58        // Steps 2-3
59        Some(self.start())
60    }
61
62    // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionstart
63    pub(crate) fn set_dom_start(&self, start: Option<u32>) -> ErrorResult {
64        // Step 1
65        if !self.element.selection_api_applies() {
66            return Err(Error::InvalidState);
67        }
68
69        // Step 2
70        let mut end = self.end();
71
72        // Step 3
73        if let Some(s) = start {
74            if end < s {
75                end = s;
76            }
77        }
78
79        // Step 4
80        self.set_range(start, Some(end), Some(self.direction()), None);
81        Ok(())
82    }
83
84    // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionend
85    pub(crate) fn dom_end(&self) -> Option<u32> {
86        // Step 1
87        if !self.element.selection_api_applies() {
88            return None;
89        }
90
91        // Steps 2-3
92        Some(self.end())
93    }
94
95    // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionend
96    pub(crate) fn set_dom_end(&self, end: Option<u32>) -> ErrorResult {
97        // Step 1
98        if !self.element.selection_api_applies() {
99            return Err(Error::InvalidState);
100        }
101
102        // Step 2
103        self.set_range(Some(self.start()), end, Some(self.direction()), None);
104        Ok(())
105    }
106
107    // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectiondirection
108    pub(crate) fn dom_direction(&self) -> Option<DOMString> {
109        // Step 1
110        if !self.element.selection_api_applies() {
111            return None;
112        }
113
114        Some(DOMString::from(self.direction()))
115    }
116
117    // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectiondirection
118    pub(crate) fn set_dom_direction(&self, direction: Option<DOMString>) -> ErrorResult {
119        // Step 1
120        if !self.element.selection_api_applies() {
121            return Err(Error::InvalidState);
122        }
123
124        // Step 2
125        self.set_range(
126            Some(self.start()),
127            Some(self.end()),
128            direction.map(SelectionDirection::from),
129            None,
130        );
131        Ok(())
132    }
133
134    // https://html.spec.whatwg.org/multipage/#dom-textarea/input-setselectionrange
135    pub(crate) fn set_dom_range(
136        &self,
137        start: u32,
138        end: u32,
139        direction: Option<DOMString>,
140    ) -> ErrorResult {
141        // Step 1
142        if !self.element.selection_api_applies() {
143            return Err(Error::InvalidState);
144        }
145
146        // Step 2
147        self.set_range(
148            Some(start),
149            Some(end),
150            direction.map(SelectionDirection::from),
151            None,
152        );
153        Ok(())
154    }
155
156    // https://html.spec.whatwg.org/multipage/#dom-textarea/input-setrangetext
157    pub(crate) fn set_dom_range_text(
158        &self,
159        replacement: DOMString,
160        start: Option<u32>,
161        end: Option<u32>,
162        selection_mode: SelectionMode,
163    ) -> ErrorResult {
164        // Step 1
165        if !self.element.selection_api_applies() {
166            return Err(Error::InvalidState);
167        }
168
169        // Step 2
170        self.element.set_dirty_value_flag(true);
171
172        // Step 3
173        let mut start = start.unwrap_or_else(|| self.start());
174        let mut end = end.unwrap_or_else(|| self.end());
175
176        // Step 4
177        if start > end {
178            return Err(Error::IndexSize);
179        }
180
181        // Save the original selection state to later pass to set_selection_range, because we will
182        // change the selection state in order to replace the text in the range.
183        let original_selection_state = self.textinput.borrow().selection_state();
184
185        let UTF8Bytes(content_length) = self.textinput.borrow().len_utf8();
186        let content_length = content_length as u32;
187
188        // Step 5
189        if start > content_length {
190            start = content_length;
191        }
192
193        // Step 6
194        if end > content_length {
195            end = content_length;
196        }
197
198        // Step 7
199        let mut selection_start = self.start();
200
201        // Step 8
202        let mut selection_end = self.end();
203
204        // Step 11
205        // Must come before the textinput.replace_selection() call, as replacement gets moved in
206        // that call.
207        let new_length = replacement.len() as u32;
208
209        {
210            let mut textinput = self.textinput.borrow_mut();
211
212            // Steps 9-10
213            textinput.set_selection_range(start, end, SelectionDirection::None);
214            textinput.replace_selection(replacement);
215        }
216
217        // Step 12
218        let new_end = start + new_length;
219
220        // Step 13
221        match selection_mode {
222            SelectionMode::Select => {
223                selection_start = start;
224                selection_end = new_end;
225            },
226
227            SelectionMode::Start => {
228                selection_start = start;
229                selection_end = start;
230            },
231
232            SelectionMode::End => {
233                selection_start = new_end;
234                selection_end = new_end;
235            },
236
237            SelectionMode::Preserve => {
238                // Sub-step 1
239                let old_length = end - start;
240
241                // Sub-step 2
242                let delta = (new_length as isize) - (old_length as isize);
243
244                // Sub-step 3
245                if selection_start > end {
246                    selection_start = ((selection_start as isize) + delta) as u32;
247                } else if selection_start > start {
248                    selection_start = start;
249                }
250
251                // Sub-step 4
252                if selection_end > end {
253                    selection_end = ((selection_end as isize) + delta) as u32;
254                } else if selection_end > start {
255                    selection_end = new_end;
256                }
257            },
258        }
259
260        // Step 14
261        self.set_range(
262            Some(selection_start),
263            Some(selection_end),
264            None,
265            Some(original_selection_state),
266        );
267        Ok(())
268    }
269
270    fn start(&self) -> u32 {
271        let UTF8Bytes(offset) = self.textinput.borrow().selection_start_offset();
272        offset as u32
273    }
274
275    fn end(&self) -> u32 {
276        let UTF8Bytes(offset) = self.textinput.borrow().selection_end_offset();
277        offset as u32
278    }
279
280    fn direction(&self) -> SelectionDirection {
281        self.textinput.borrow().selection_direction()
282    }
283
284    // https://html.spec.whatwg.org/multipage/#set-the-selection-range
285    fn set_range(
286        &self,
287        start: Option<u32>,
288        end: Option<u32>,
289        direction: Option<SelectionDirection>,
290        original_selection_state: Option<SelectionState>,
291    ) {
292        let mut textinput = self.textinput.borrow_mut();
293        let original_selection_state =
294            original_selection_state.unwrap_or_else(|| textinput.selection_state());
295
296        // Step 1
297        let start = start.unwrap_or(0);
298
299        // Step 2
300        let end = end.unwrap_or(0);
301
302        // Steps 3-5
303        textinput.set_selection_range(start, end, direction.unwrap_or(SelectionDirection::None));
304
305        // Step 6
306        if textinput.selection_state() != original_selection_state {
307            self.element
308                .owner_global()
309                .task_manager()
310                .user_interaction_task_source()
311                .queue_event(
312                    self.element.upcast::<EventTarget>(),
313                    atom!("select"),
314                    EventBubbles::Bubbles,
315                    EventCancelable::NotCancelable,
316                );
317        }
318
319        self.element.upcast::<Node>().dirty(NodeDamage::Other);
320    }
321}