mirror of
https://github.com/servo/servo.git
synced 2025-06-08 16:43:28 +00:00
The intention here is to make the flow more explicit. I.e. rather than calling `self.dom_select()` and relying on the programmer to know/realise that this method is provided by a trait, we call `self.selection().dom_select()` and the programmer can inspect the definition of `self.selection()` to follow the code. This came out of a discussion with KiChjang here: https://github.com/servo/servo/pull/19544#discussion_r156167044 Note that I tried to make "selection" be a member field of HTML{Input,TextArea}Element but it opened up a whole can of worms with lifetimes, so it seemed simpler to not do that since it is not essential for this code to work.
296 lines
9.1 KiB
Rust
296 lines
9.1 KiB
Rust
/* 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 http://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 dom::bindings::cell::DomRefCell;
|
|
use dom::bindings::codegen::Bindings::HTMLFormElementBinding::SelectionMode;
|
|
use dom::bindings::conversions::DerivedFrom;
|
|
use dom::bindings::error::{Error, ErrorResult};
|
|
use dom::bindings::str::DOMString;
|
|
use dom::event::{EventBubbles, EventCancelable};
|
|
use dom::eventtarget::EventTarget;
|
|
use dom::node::{Node, NodeDamage, window_from_node};
|
|
use script_traits::ScriptToConstellationChan;
|
|
use textinput::{SelectionDirection, SelectionState, TextInput};
|
|
|
|
pub 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 struct TextControlSelection<'a, E: TextControlElement> {
|
|
element: &'a E,
|
|
textinput: &'a DomRefCell<TextInput<ScriptToConstellationChan>>,
|
|
}
|
|
|
|
impl<'a, E: TextControlElement> TextControlSelection<'a, E> {
|
|
pub fn new(element: &'a E, textinput: &'a DomRefCell<TextInput<ScriptToConstellationChan>>) -> Self {
|
|
TextControlSelection { element, textinput }
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea/input-select
|
|
pub fn dom_select(&self) {
|
|
// Step 1
|
|
if !self.element.has_selectable_text() {
|
|
return;
|
|
}
|
|
|
|
// Step 2
|
|
self.set_range(Some(0), Some(u32::max_value()), None, None);
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionstart
|
|
pub 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 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 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 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 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 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(|d| SelectionDirection::from(d)),
|
|
None
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea/input-setselectionrange
|
|
pub 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(|d| SelectionDirection::from(d)), None);
|
|
Ok(())
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea/input-setrangetext
|
|
pub 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 content_length = self.textinput.borrow().len() 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 {
|
|
self.textinput.borrow().selection_start_offset() as u32
|
|
}
|
|
|
|
fn end(&self) -> u32 {
|
|
self.textinput.borrow().selection_end_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 {
|
|
let window = window_from_node(self.element);
|
|
window.user_interaction_task_source().queue_event(
|
|
&self.element.upcast::<EventTarget>(),
|
|
atom!("select"),
|
|
EventBubbles::Bubbles,
|
|
EventCancelable::NotCancelable,
|
|
&window);
|
|
}
|
|
|
|
self.element.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
|
|
}
|
|
}
|