| /* This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ |
| |
| //! This is an abstraction used by `HTMLInputElement` and `HTMLTextAreaElement` to implement the |
| //! text control selection DOM API. |
| //! |
| //! <https://html.spec.whatwg.org/multipage/#textFieldSelection> |
| |
| use crate::clipboard_provider::EmbedderClipboardProvider; |
| use crate::dom::bindings::cell::DomRefCell; |
| use crate::dom::bindings::codegen::Bindings::HTMLFormElementBinding::SelectionMode; |
| use crate::dom::bindings::conversions::DerivedFrom; |
| use crate::dom::bindings::error::{Error, ErrorResult}; |
| use crate::dom::bindings::str::DOMString; |
| use crate::dom::event::{EventBubbles, EventCancelable}; |
| use crate::dom::eventtarget::EventTarget; |
| use crate::dom::node::{Node, NodeDamage, NodeTraits}; |
| use crate::textinput::{SelectionDirection, SelectionState, TextInput, UTF8Bytes}; |
| |
| pub(crate) trait TextControlElement: DerivedFrom<EventTarget> + DerivedFrom<Node> { |
| fn selection_api_applies(&self) -> bool; |
| fn has_selectable_text(&self) -> bool; |
| fn set_dirty_value_flag(&self, value: bool); |
| } |
| |
| pub(crate) struct TextControlSelection<'a, E: TextControlElement> { |
| element: &'a E, |
| textinput: &'a DomRefCell<TextInput<EmbedderClipboardProvider>>, |
| } |
| |
| impl<'a, E: TextControlElement> TextControlSelection<'a, E> { |
| pub(crate) fn new( |
| element: &'a E, |
| textinput: &'a DomRefCell<TextInput<EmbedderClipboardProvider>>, |
| ) -> Self { |
| TextControlSelection { element, textinput } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-textarea/input-select |
| pub(crate) fn dom_select(&self) { |
| // Step 1 |
| if !self.element.has_selectable_text() { |
| return; |
| } |
| |
| // Step 2 |
| self.set_range(Some(0), Some(u32::MAX), None, None); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionstart |
| pub(crate) fn dom_start(&self) -> Option<u32> { |
| // Step 1 |
| if !self.element.selection_api_applies() { |
| return None; |
| } |
| |
| // Steps 2-3 |
| Some(self.start()) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionstart |
| pub(crate) fn set_dom_start(&self, start: Option<u32>) -> ErrorResult { |
| // Step 1 |
| if !self.element.selection_api_applies() { |
| return Err(Error::InvalidState); |
| } |
| |
| // Step 2 |
| let mut end = self.end(); |
| |
| // Step 3 |
| if let Some(s) = start { |
| if end < s { |
| end = s; |
| } |
| } |
| |
| // Step 4 |
| self.set_range(start, Some(end), Some(self.direction()), None); |
| Ok(()) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionend |
| pub(crate) fn dom_end(&self) -> Option<u32> { |
| // Step 1 |
| if !self.element.selection_api_applies() { |
| return None; |
| } |
| |
| // Steps 2-3 |
| Some(self.end()) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionend |
| pub(crate) fn set_dom_end(&self, end: Option<u32>) -> ErrorResult { |
| // Step 1 |
| if !self.element.selection_api_applies() { |
| return Err(Error::InvalidState); |
| } |
| |
| // Step 2 |
| self.set_range(Some(self.start()), end, Some(self.direction()), None); |
| Ok(()) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectiondirection |
| pub(crate) fn dom_direction(&self) -> Option<DOMString> { |
| // Step 1 |
| if !self.element.selection_api_applies() { |
| return None; |
| } |
| |
| Some(DOMString::from(self.direction())) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectiondirection |
| pub(crate) fn set_dom_direction(&self, direction: Option<DOMString>) -> ErrorResult { |
| // Step 1 |
| if !self.element.selection_api_applies() { |
| return Err(Error::InvalidState); |
| } |
| |
| // Step 2 |
| self.set_range( |
| Some(self.start()), |
| Some(self.end()), |
| direction.map(SelectionDirection::from), |
| None, |
| ); |
| Ok(()) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-textarea/input-setselectionrange |
| pub(crate) fn set_dom_range( |
| &self, |
| start: u32, |
| end: u32, |
| direction: Option<DOMString>, |
| ) -> ErrorResult { |
| // Step 1 |
| if !self.element.selection_api_applies() { |
| return Err(Error::InvalidState); |
| } |
| |
| // Step 2 |
| self.set_range( |
| Some(start), |
| Some(end), |
| direction.map(SelectionDirection::from), |
| None, |
| ); |
| Ok(()) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-textarea/input-setrangetext |
| pub(crate) fn set_dom_range_text( |
| &self, |
| replacement: DOMString, |
| start: Option<u32>, |
| end: Option<u32>, |
| selection_mode: SelectionMode, |
| ) -> ErrorResult { |
| // Step 1 |
| if !self.element.selection_api_applies() { |
| return Err(Error::InvalidState); |
| } |
| |
| // Step 2 |
| self.element.set_dirty_value_flag(true); |
| |
| // Step 3 |
| let mut start = start.unwrap_or_else(|| self.start()); |
| let mut end = end.unwrap_or_else(|| self.end()); |
| |
| // Step 4 |
| if start > end { |
| return Err(Error::IndexSize); |
| } |
| |
| // Save the original selection state to later pass to set_selection_range, because we will |
| // change the selection state in order to replace the text in the range. |
| let original_selection_state = self.textinput.borrow().selection_state(); |
| |
| let UTF8Bytes(content_length) = self.textinput.borrow().len_utf8(); |
| let content_length = content_length as u32; |
| |
| // Step 5 |
| if start > content_length { |
| start = content_length; |
| } |
| |
| // Step 6 |
| if end > content_length { |
| end = content_length; |
| } |
| |
| // Step 7 |
| let mut selection_start = self.start(); |
| |
| // Step 8 |
| let mut selection_end = self.end(); |
| |
| // Step 11 |
| // Must come before the textinput.replace_selection() call, as replacement gets moved in |
| // that call. |
| let new_length = replacement.len() as u32; |
| |
| { |
| let mut textinput = self.textinput.borrow_mut(); |
| |
| // Steps 9-10 |
| textinput.set_selection_range(start, end, SelectionDirection::None); |
| textinput.replace_selection(replacement); |
| } |
| |
| // Step 12 |
| let new_end = start + new_length; |
| |
| // Step 13 |
| match selection_mode { |
| SelectionMode::Select => { |
| selection_start = start; |
| selection_end = new_end; |
| }, |
| |
| SelectionMode::Start => { |
| selection_start = start; |
| selection_end = start; |
| }, |
| |
| SelectionMode::End => { |
| selection_start = new_end; |
| selection_end = new_end; |
| }, |
| |
| SelectionMode::Preserve => { |
| // Sub-step 1 |
| let old_length = end - start; |
| |
| // Sub-step 2 |
| let delta = (new_length as isize) - (old_length as isize); |
| |
| // Sub-step 3 |
| if selection_start > end { |
| selection_start = ((selection_start as isize) + delta) as u32; |
| } else if selection_start > start { |
| selection_start = start; |
| } |
| |
| // Sub-step 4 |
| if selection_end > end { |
| selection_end = ((selection_end as isize) + delta) as u32; |
| } else if selection_end > start { |
| selection_end = new_end; |
| } |
| }, |
| } |
| |
| // Step 14 |
| self.set_range( |
| Some(selection_start), |
| Some(selection_end), |
| None, |
| Some(original_selection_state), |
| ); |
| Ok(()) |
| } |
| |
| fn start(&self) -> u32 { |
| let UTF8Bytes(offset) = self.textinput.borrow().selection_start_offset(); |
| offset as u32 |
| } |
| |
| fn end(&self) -> u32 { |
| let UTF8Bytes(offset) = self.textinput.borrow().selection_end_offset(); |
| offset as u32 |
| } |
| |
| fn direction(&self) -> SelectionDirection { |
| self.textinput.borrow().selection_direction() |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#set-the-selection-range |
| fn set_range( |
| &self, |
| start: Option<u32>, |
| end: Option<u32>, |
| direction: Option<SelectionDirection>, |
| original_selection_state: Option<SelectionState>, |
| ) { |
| let mut textinput = self.textinput.borrow_mut(); |
| let original_selection_state = |
| original_selection_state.unwrap_or_else(|| textinput.selection_state()); |
| |
| // Step 1 |
| let start = start.unwrap_or(0); |
| |
| // Step 2 |
| let end = end.unwrap_or(0); |
| |
| // Steps 3-5 |
| textinput.set_selection_range(start, end, direction.unwrap_or(SelectionDirection::None)); |
| |
| // Step 6 |
| if textinput.selection_state() != original_selection_state { |
| self.element |
| .owner_global() |
| .task_manager() |
| .user_interaction_task_source() |
| .queue_event( |
| self.element.upcast::<EventTarget>(), |
| atom!("select"), |
| EventBubbles::Bubbles, |
| EventCancelable::NotCancelable, |
| ); |
| } |
| |
| self.element.upcast::<Node>().dirty(NodeDamage::Other); |
| } |
| } |