Skip to main content

script/dom/document/
editing.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
5use std::cell::RefCell;
6use std::rc::Rc;
7
8use embedder_traits::{
9    ClipboardAction, EditingAction, EditingDirection, EditingMotion, EmbedderMsg, InputEventResult,
10    ModifySelection, MouseButton,
11};
12use js::context::{JSContext, NoGC};
13use keyboard_types::{
14    CompositionEvent, CompositionState, Key, KeyState, KeyboardEvent as KeyboardTypesEvent,
15    Modifiers, NamedKey, ShortcutMatcher,
16};
17use layout_api::QueryMsg;
18use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods;
19use script_bindings::codegen::GenericBindings::EventBinding::EventMethods;
20use script_bindings::codegen::GenericBindings::SelectionBinding::SelectionMethods;
21use script_bindings::codegen::GenericBindings::UIEventBinding::UIEventMethods as _;
22use script_bindings::dom::UnrootedDom;
23use script_bindings::inheritance::Castable;
24use script_bindings::root::DomRoot;
25use script_bindings::str::DOMString;
26use servo_base::generic_channel::GenericCallback;
27
28use crate::dom::clipboardevent::ClipboardEventType;
29use crate::dom::event::{EventBubbles, EventCancelable};
30use crate::dom::execcommand::execcommands::DocumentExecCommandSupport;
31use crate::dom::inputevent::HitTestResult;
32use crate::dom::selection_range::RootedSelectionBoundary;
33use crate::dom::text_control::TextControlElement;
34use crate::dom::text_input::{InputEventType, IsComposing};
35use crate::dom::types::{
36    ClipboardEvent, DataTransfer, Event, EventTarget, HTMLInputElement, HTMLTextAreaElement,
37    MouseEvent, UIEvent,
38};
39use crate::dom::{Document, Node, NodeTraits};
40use crate::drag::document_selection_drag::{
41    DocumentSelectionDragHandler, adjust_anchor_for_user_select,
42};
43use crate::drag::drag_data_store::{DragDataStore, Kind, Mode};
44use crate::drag::drag_gesture::{DragGesture, DragHandler};
45
46#[cfg(target_os = "macos")]
47pub(crate) const CMD_OR_CONTROL: Modifiers = Modifiers::META;
48#[cfg(not(target_os = "macos"))]
49pub(crate) const CMD_OR_CONTROL: Modifiers = Modifiers::CONTROL;
50
51#[cfg(target_os = "macos")]
52pub(crate) const ALT_OR_CONTROL: Modifiers = Modifiers::ALT;
53#[cfg(not(target_os = "macos"))]
54pub(crate) const ALT_OR_CONTROL: Modifiers = Modifiers::CONTROL;
55
56/// A selection granularity to use when selecting via mouse button events.
57#[derive(Clone, Copy, Debug, PartialEq)]
58pub(crate) enum SelectionGranularity {
59    /// Select a single selection focus node position. This is created via single primary click
60    /// or middle clicks.
61    Position,
62    /// Select a word at the target point. This is created via double primary clicks.
63    Word,
64    /// Select to the paragraph boundaries or to a hard line break, whatever comes first.
65    /// This is triggered via triple primary clicks.
66    LineIgnoringSoftWrap,
67}
68
69impl SelectionGranularity {
70    pub(crate) fn from_mouse_event(mouse_event: &MouseEvent) -> Option<Self> {
71        match mouse_event.button() {
72            MouseButton::Primary => {}, // continue below
73            MouseButton::Auxiliary => return Some(Self::Position),
74            _ => return None,
75        }
76
77        // We currently don't do anything for higher click counts, but some platforms do.
78        // For example on desktop linux, 4+ click cycle back starting at same as single click:
79        // ```
80        // const MAX_CLICKS: i32 = 3;
81        // let click_count = (click_count - 1) % MAX_CLICKS + 1;
82        // ```
83        // We should re-examine this when implementing support for platform-specific editing
84        // behaviors.
85        let click_count = mouse_event.upcast::<UIEvent>().Detail();
86        match click_count {
87            1 => Some(Self::Position),
88            2 => Some(Self::Word),
89            3 => Some(Self::LineIgnoringSoftWrap),
90            _ => None,
91        }
92    }
93}
94
95impl Document {
96    pub(crate) fn editing_context(&self, no_gc: &NoGC, node: &Node) -> EditingContext {
97        if let Ok(editing_context) = EditingContext::try_from(node) {
98            return editing_context;
99        }
100
101        let mut current_node = UnrootedDom::from_ref(node, no_gc);
102        while let Some(parent) = current_node.parent_in_flat_tree(no_gc).into_parent() {
103            if let Ok(editing_context) = EditingContext::try_from(&**parent) {
104                return editing_context;
105            }
106            current_node = parent;
107        }
108
109        EditingContext::Document(DomRoot::from_ref(self))
110    }
111
112    /// <https://www.w3.org/TR/clipboard-apis/#clipboard-actions>
113    pub(crate) fn handle_clipboard_action(
114        &self,
115        cx: &mut JSContext,
116        editing_context: &EditingContext,
117        action: ClipboardAction,
118    ) -> InputEventResult {
119        let clipboard_event_type = match action {
120            ClipboardAction::Copy => ClipboardEventType::Copy,
121            ClipboardAction::Cut => ClipboardEventType::Cut,
122            ClipboardAction::Paste => ClipboardEventType::Paste,
123        };
124
125        // The script_triggered flag is set if the action runs because of a script, e.g. document.execCommand()
126        let script_triggered = false;
127
128        // The script_may_access_clipboard flag is set
129        // if action is paste and the script thread is allowed to read from clipboard or
130        // if action is copy or cut and the script thread is allowed to modify the clipboard
131        let script_may_access_clipboard = false;
132
133        // Step 1. If the script-triggered flag is set and the script-may-access-clipboard flag is unset
134        if script_triggered && !script_may_access_clipboard {
135            return InputEventResult::empty();
136        }
137
138        // Step 2. Fire a clipboard event
139        let event_target = editing_context.event_target();
140        let clipboard_event = self.fire_clipboard_event(cx, &event_target, clipboard_event_type);
141
142        let event = clipboard_event.upcast::<Event>();
143        if !event.DefaultPrevented() {
144            // Step 3. If the event was not canceled, then
145            match clipboard_event.clipboard_event_type() {
146                ClipboardEventType::Copy if editing_context.copying_enabled() => {
147                    // Step 3.1. Copy the selected contents, if any, to the clipboard.
148                    // Implementations should create alternate text/html and text/plain
149                    // clipboard formats when content in a web page is selected.
150                    if let Some(selection) = editing_context.selection_content(cx) {
151                        self.send_to_embedder(EmbedderMsg::SetClipboardText(
152                            self.webview_id(),
153                            selection,
154                        ));
155                    }
156                    // Step 3.2. Fire a clipboard event named clipboardchange
157                    self.fire_clipboard_event(cx, &event_target, ClipboardEventType::Change);
158
159                    // This is how `true` is returned from this function.
160                    event.mark_as_handled();
161                },
162                ClipboardEventType::Cut => {
163                    if let Some(selection) = editing_context.selection_content(cx) &&
164                        editing_context.cutting_enabled()
165                    {
166                        let canceled = editing_context.fire_cut_beforeinput_event(cx);
167                        if canceled {
168                            return InputEventResult::empty();
169                        }
170
171                        // Step 3.1. If there is a selection in an editable context where
172                        // cutting is enabled, then
173                        // Step 3.1.1. Copy the selected contents, if any, to the clipboard.
174                        // Implementations should create alternate text/html and text/plain
175                        // clipboard formats when content in a web page is selected.
176                        self.send_to_embedder(EmbedderMsg::SetClipboardText(
177                            self.webview_id(),
178                            selection,
179                        ));
180
181                        // Step 3.1.2. Remove the contents of the selection from the document
182                        // and collapse the selection.
183                        editing_context.remove_the_contents_of_the_selection(cx);
184
185                        // Step 3.1.3. Fire a clipboard event named clipboardchange
186                        self.fire_clipboard_event(cx, &event_target, ClipboardEventType::Change);
187
188                        // Step 3.1.4. Queue tasks to fire any events that should fire due to
189                        // the modification, see §5.3 Integration with other scripts and
190                        // events for details.
191                        editing_context.fire_cut_events();
192
193                        // This is how `true` is returned from this function.
194                        event.mark_as_handled();
195                    }
196                },
197                ClipboardEventType::Paste => {
198                    if editing_context.has_selection_or_cursor() &&
199                        editing_context.pasting_enabled() &&
200                        let Some(text_content) = clipboard_event.text_content()
201                    {
202                        let canceled =
203                            editing_context.fire_paste_beforeinput_event(cx, &text_content);
204                        if canceled {
205                            return InputEventResult::empty();
206                        }
207
208                        // Step 3.1. If there is a selection or cursor in an editable context
209                        // where pasting is enabled, then
210                        // Step 3.1.1. Insert the most suitable content found on the
211                        // clipboard, if any, into the context.
212                        editing_context.insert_content(cx, &text_content);
213
214                        // Step 3.1.2. Queue tasks to fire any events that should fire due to
215                        // the modification, see §5.3 Integration with other scripts and
216                        // events for details.
217                        editing_context.fire_paste_events(&text_content);
218
219                        // This is how `true` is returned from this function.
220                        event.mark_as_handled();
221                    }
222                },
223                _ => (),
224            }
225        } else {
226            // Step 4 If the event was canceled, then
227            match clipboard_event.clipboard_event_type() {
228                ClipboardEventType::Copy => {
229                    // Step 4.1 Call the write content to the clipboard algorithm,
230                    // passing on the DataTransferItemList items, a clear-was-called flag and a types-to-clear list.
231                    if let Some(clipboard_data) = clipboard_event.clipboard_data() {
232                        let drag_data_store =
233                            clipboard_data.data_store().expect("This shouldn't fail");
234                        self.write_content_to_the_clipboard(&drag_data_store);
235                    }
236                },
237                ClipboardEventType::Cut => {
238                    // Step 4.1 Call the write content to the clipboard algorithm,
239                    // passing on the DataTransferItemList items, a clear-was-called flag and a types-to-clear list.
240                    if let Some(clipboard_data) = clipboard_event.clipboard_data() {
241                        let drag_data_store =
242                            clipboard_data.data_store().expect("This shouldn't fail");
243                        self.write_content_to_the_clipboard(&drag_data_store);
244                    }
245
246                    // Step 4.2 Fire a clipboard event named clipboardchange
247                    self.fire_clipboard_event(cx, &event_target, ClipboardEventType::Change);
248                },
249                // Step 4.1 Return false.
250                ClipboardEventType::Paste => (),
251                _ => (),
252            }
253        }
254
255        // Step 5: Return true from the action.
256        // In this case we are returning the `InputEventResult` instead of true or false.
257        event.flags().into()
258    }
259
260    /// <https://www.w3.org/TR/clipboard-apis/#fire-a-clipboard-event>
261    fn fire_clipboard_event(
262        &self,
263        cx: &mut JSContext,
264        target: &EventTarget,
265        clipboard_event_type: ClipboardEventType,
266    ) -> DomRoot<ClipboardEvent> {
267        let clipboard_event = ClipboardEvent::new(
268            cx,
269            self.window(),
270            None,
271            clipboard_event_type,
272            EventBubbles::Bubbles,
273            EventCancelable::Cancelable,
274            None,
275        );
276
277        // Step 1 Let clear_was_called be false
278        // Step 2 Let types_to_clear an empty list
279        let mut drag_data_store = DragDataStore::new();
280
281        // Step 4 let clipboard-entry be the sequence number of clipboard content, null if the OS doesn't support it.
282
283        // Step 5 let trusted be true if the event is generated by the user agent, false otherwise
284        let trusted = true;
285
286        // Step 6 if the context is editable:
287        // Step 6.2 else TODO require Selection see https://github.com/w3c/clipboard-apis/issues/70
288        // Step 7
289        match clipboard_event.clipboard_event_type() {
290            ClipboardEventType::Copy | ClipboardEventType::Cut => {
291                // Step 7.2.1
292                drag_data_store.set_mode(Mode::ReadWrite);
293            },
294            ClipboardEventType::Paste => {
295                let (callback, receiver) =
296                    GenericCallback::new_blocking().expect("Could not create callback");
297                self.send_to_embedder(EmbedderMsg::GetClipboardText(self.webview_id(), callback));
298                let text_contents = receiver
299                    .recv()
300                    .map(Result::unwrap_or_default)
301                    .unwrap_or_default();
302
303                // Step 7.1.1
304                drag_data_store.set_mode(Mode::ReadOnly);
305                // Step 7.1.2 If trusted or the implementation gives script-generated events access to the clipboard
306                if trusted {
307                    // Step 7.1.2.1 For each clipboard-part on the OS clipboard:
308
309                    // Step 7.1.2.1.1 If clipboard-part contains plain text, then
310                    let data = DOMString::from(text_contents);
311                    let type_ = DOMString::from_static("text/plain");
312                    let _ = drag_data_store.add(Kind::Text { data, type_ });
313
314                    // Step 7.1.2.1.2 TODO If clipboard-part represents file references, then for each file reference
315                    // Step 7.1.2.1.3 TODO If clipboard-part contains HTML- or XHTML-formatted text then
316
317                    // Step 7.1.3 Update clipboard-event-data’s files to match clipboard-event-data’s items
318                    // Step 7.1.4 Update clipboard-event-data’s types to match clipboard-event-data’s items
319                }
320            },
321            ClipboardEventType::Change | ClipboardEventType::Other(..) => (),
322        }
323
324        // Step 3
325        let clipboard_event_data = DataTransfer::new(
326            cx,
327            self.window(),
328            Rc::new(RefCell::new(Some(drag_data_store))),
329        );
330
331        // Step 8
332        clipboard_event.set_clipboard_data(Some(&clipboard_event_data));
333
334        // Step 9
335        let event = clipboard_event.upcast::<Event>();
336        event.set_trusted(trusted);
337
338        // Step 10 Set event’s composed to true.
339        event.set_composed(true);
340
341        // Step 11
342        event.dispatch(cx, target, false);
343
344        DomRoot::from(clipboard_event)
345    }
346
347    /// <https://www.w3.org/TR/clipboard-apis/#write-content-to-the-clipboard>
348    fn write_content_to_the_clipboard(&self, drag_data_store: &DragDataStore) {
349        // Step 1
350        if drag_data_store.list_len() > 0 {
351            // Step 1.1 Clear the clipboard.
352            self.send_to_embedder(EmbedderMsg::ClearClipboard(self.webview_id()));
353            // Step 1.2
354            for item in drag_data_store.iter_item_list() {
355                match item {
356                    Kind::Text { data, .. } => {
357                        // Step 1.2.1.1 Ensure encoding is correct per OS and locale conventions
358                        // Step 1.2.1.2 Normalize line endings according to platform conventions
359                        // Step 1.2.1.3
360                        self.send_to_embedder(EmbedderMsg::SetClipboardText(
361                            self.webview_id(),
362                            data.to_string(),
363                        ));
364                    },
365                    Kind::File { .. } => {
366                        // Step 1.2.2 If data is of a type listed in the mandatory data types list, then
367                        // Step 1.2.2.1 Place part on clipboard with the appropriate OS clipboard format description
368                        // Step 1.2.3 Else this is left to the implementation
369                    },
370                }
371            }
372        } else {
373            // Step 2.1
374            if drag_data_store.clear_was_called {
375                // Step 2.1.1 If types-to-clear list is empty, clear the clipboard
376                self.send_to_embedder(EmbedderMsg::ClearClipboard(self.webview_id()));
377                // Step 2.1.2 Else remove the types in the list from the clipboard
378                // As of now this can't be done with Arboard, and it's possible that will be removed from the spec
379            }
380        }
381    }
382
383    /// <https://w3c.github.io/editing/docs/execCommand/#additional-requirements>
384    ///
385    /// This function does not do any checks for whether or not we are actually inside an
386    /// editing host, since those checks are performed by exec_command_for_command_id
387    /// either way.
388    pub(crate) fn perform_editing_action(
389        &self,
390        cx: &mut JSContext,
391        editing_action: &EditingAction,
392    ) -> bool {
393        if !servo_config::pref!(dom_exec_command_enabled) {
394            return false;
395        }
396
397        // This function does not do any checks for whether or not we are actually inside an
398        // editing host, since those checks are performed by exec_command_for_command_id either way.
399        let (command, argument) = match editing_action {
400            // TODO: composition should be supported once editing hosts have selection support
401            EditingAction::MoveCursor(..) |
402            EditingAction::StartComposition |
403            EditingAction::EndComposition(..) => return false,
404            EditingAction::SelectAll | EditingAction::Clipboard(_) => {
405                unreachable!("Should have been handled before this point.")
406            },
407            EditingAction::InsertNewline => {
408                // > When the user instructs the user agent to insert a line break inside an
409                // > editing host without breaking out of the current block, such as by
410                // > pressing Shift-Enter or Option-Enter while the cursor is in an editable
411                // > node, the user agent must call execCommand("insertlinebreak") on the
412                // > relevant document.
413                (DOMString::from_static("insertlinebreak"), DOMString::new())
414            },
415            EditingAction::InsertParagraph => {
416                // > When the user instructs the user agent to insert a line break inside an
417                // > editing host, such as by pressing the Enter key while the cursor is in an
418                // > editable node, the user agent must call execCommand("insertparagraph") on
419                // > the relevant document.
420                (DOMString::from_static("insertparagraph"), DOMString::new())
421            },
422            // > When the user instructs the user agent to delete the previous character inside an
423            // > editing host, such as by pressing the Backspace key while the cursor is in an
424            // > editable node, the user agent must call execCommand("delete") on the relevant
425            // > document.
426            //
427            // TODO: Handle other types of motions here.
428            EditingAction::Backspace(..) => (DOMString::from_static("delete"), DOMString::new()),
429            // > When the user instructs the user agent to delete the next character inside an
430            // > editing host, such as by pressing the Delete key while the cursor is in an
431            // > editable node, the user agent must call execCommand("forwarddelete") on the
432            // > relevant document
433            EditingAction::Delete => (DOMString::from_static("forwarddelete"), DOMString::new()),
434            // > When the user instructs the user agent to insert text inside an editing host, such
435            // > as by typing on the keyboard while the cursor is in an editable node, the user
436            // > agent must call execCommand("inserttext", false, value) on the relevant document,
437            // > with value equal to the text the user provided. If the user inserts multiple
438            // > characters at once or in quick succession, this specification does not define
439            // > whether it is treated as one insertion or several consecutive insertions.
440            // TODO: composition should be supported once editing hosts have selection support
441            EditingAction::InsertText(text) | EditingAction::InsertCompositionText(text) => (
442                DOMString::from_static("inserttext"),
443                DOMString::from(text.as_str()),
444            ),
445        };
446
447        self.exec_command_for_command_id(cx, command, argument)
448    }
449
450    fn handle_mousedown_event(
451        &self,
452        cx: &mut JSContext,
453        mouse_event: &MouseEvent,
454        hit_test_result: &HitTestResult,
455    ) {
456        assert_eq!(mouse_event.upcast::<Event>().type_(), atom!("mousedown"));
457
458        let Some(selection) = self.GetSelection(cx) else {
459            return;
460        };
461
462        if mouse_event.button() == MouseButton::Auxiliary {
463            let _ = selection.Collapse(cx, None, 0);
464            mouse_event.upcast::<Event>().mark_as_handled();
465            return;
466        }
467
468        if mouse_event.button() != MouseButton::Primary {
469            return;
470        }
471
472        // TODO: handle double/triple click:
473        // match SelectionTarget::from_mouse_event(mouse_event) {…}
474
475        // When the hit test cannot find a suitable DOM position for selection, just
476        // use the first offset within the target node of the `mousedown` event. This
477        // is a reasonable place to start the selection from.
478        let boundary = hit_test_result
479            .dom_position_for_selection
480            .clone()
481            .unwrap_or_else(|| RootedSelectionBoundary::start_of(&hit_test_result.node));
482        let Some((boundary, user_select_contain_node)) =
483            adjust_anchor_for_user_select(cx, boundary)
484        else {
485            return;
486        };
487        selection.collapse_to_dom_position(cx, &boundary);
488        self.event_handler().install_drag_gesture(DragGesture::new(
489            DragHandler::DocumentSelection(DocumentSelectionDragHandler::new(
490                user_select_contain_node.as_deref(),
491            )),
492        ));
493        mouse_event.upcast::<Event>().mark_as_handled();
494    }
495}
496
497pub(crate) enum TextControlElementEditingContext {
498    TextArea(DomRoot<HTMLTextAreaElement>),
499    Input(DomRoot<HTMLInputElement>),
500}
501
502impl TextControlElementEditingContext {
503    fn text_control_element(&self) -> &dyn TextControlElement {
504        match self {
505            TextControlElementEditingContext::TextArea(text_area) => &**text_area,
506            TextControlElementEditingContext::Input(input) => &**input,
507        }
508    }
509
510    fn handle_mousedown_event(&self, mouse_event: &MouseEvent, hit_test_result: &HitTestResult) {
511        let text_control = self.text_control_element();
512        if text_control.text_input_mut().handle_mousedown_event(
513            text_control.as_element(),
514            mouse_event,
515            hit_test_result,
516        ) {
517            text_control.maybe_update_shared_selection();
518            mouse_event.upcast::<Event>().mark_as_handled();
519        }
520    }
521}
522
523pub(crate) enum EditingContext {
524    TextControl(TextControlElementEditingContext),
525    Document(DomRoot<Document>),
526}
527
528impl TryFrom<&Node> for EditingContext {
529    type Error = ();
530
531    fn try_from(node: &Node) -> Result<Self, Self::Error> {
532        if let Some(text_area) = node.downcast::<HTMLTextAreaElement>() {
533            return Ok(EditingContext::TextControl(
534                TextControlElementEditingContext::TextArea(DomRoot::from_ref(text_area)),
535            ));
536        }
537        if let Some(input) = node.downcast::<HTMLInputElement>() &&
538            input.is_textual_or_password()
539        {
540            return Ok(EditingContext::TextControl(
541                TextControlElementEditingContext::Input(DomRoot::from_ref(input)),
542            ));
543        }
544        Err(())
545    }
546}
547
548impl EditingContext {
549    pub(crate) fn event_target(&self) -> DomRoot<EventTarget> {
550        match self {
551            EditingContext::TextControl(element) => match element {
552                TextControlElementEditingContext::TextArea(text_area) => {
553                    DomRoot::from_ref(text_area.upcast())
554                },
555                TextControlElementEditingContext::Input(input) => DomRoot::from_ref(input.upcast()),
556            },
557            EditingContext::Document(document) => {
558                document.event_handler().target_for_events_following_focus()
559            },
560        }
561    }
562
563    pub(crate) fn document(&self) -> DomRoot<Document> {
564        match self {
565            EditingContext::TextControl(element) => match element {
566                TextControlElementEditingContext::TextArea(element) => element.owner_document(),
567                TextControlElementEditingContext::Input(element) => element.owner_document(),
568            },
569            EditingContext::Document(document) => document.clone(),
570        }
571    }
572
573    pub(crate) fn selection_content(&self, cx: &mut JSContext) -> Option<String> {
574        match self {
575            EditingContext::TextControl(element) => element
576                .text_control_element()
577                .text_input()
578                .selection_content(),
579            EditingContext::Document(document) => document
580                .selection()
581                .map(|selection| selection.Stringifier(cx).to_string())
582                .filter(|selection| !selection.is_empty()),
583        }
584    }
585
586    pub(crate) fn has_uncollapsed_selection(&self) -> bool {
587        match self {
588            EditingContext::TextControl(element) => {
589                element.text_control_element().has_uncollapsed_selection()
590            },
591            EditingContext::Document(document) => document
592                .selection()
593                .is_some_and(|selection| !selection.collapsed()),
594        }
595    }
596
597    pub(crate) fn has_selectable_text(&self) -> bool {
598        match self {
599            EditingContext::TextControl(element) => {
600                element.text_control_element().has_selectable_text()
601            },
602            EditingContext::Document(..) => true,
603        }
604    }
605
606    pub(crate) fn has_selection_or_cursor(&self) -> bool {
607        match self {
608            EditingContext::TextControl(..) => true,
609            EditingContext::Document(document) => document
610                .selection()
611                .is_some_and(|selection| selection.RangeCount() > 0),
612        }
613    }
614
615    pub(crate) fn copying_enabled(&self) -> bool {
616        match self {
617            EditingContext::TextControl(element) => {
618                !element.text_control_element().is_password_field()
619            },
620            EditingContext::Document(..) => true,
621        }
622    }
623
624    pub(crate) fn cutting_enabled(&self) -> bool {
625        // `pasting_enabled` ensures that the editable content can be modified, which is necessary
626        // for cutting text.
627        self.copying_enabled() && self.pasting_enabled()
628    }
629
630    pub(crate) fn pasting_enabled(&self) -> bool {
631        match self {
632            EditingContext::TextControl(element) => {
633                !element.text_control_element().read_only_or_disabled()
634            },
635            EditingContext::Document(..) => {
636                // TODO(mrobinson): Add support for integration with contenteditable.
637                false
638            },
639        }
640    }
641
642    pub(crate) fn remove_the_contents_of_the_selection(&self, cx: &mut JSContext) {
643        match self {
644            EditingContext::TextControl(element) => {
645                element
646                    .text_control_element()
647                    .remove_the_contents_of_the_selection(cx);
648            },
649            EditingContext::Document(..) => {
650                // TODO(mrobinson): Add support for integration with contenteditable.
651            },
652        }
653    }
654
655    /// Returns true if and only if the cut should be canceled.
656    pub(crate) fn fire_cut_beforeinput_event(&self, cx: &mut JSContext) -> bool {
657        match self {
658            EditingContext::TextControl(element) => {
659                element.text_control_element().fire_beforeinput_event(
660                    cx,
661                    None,
662                    IsComposing::NotComposing,
663                    InputEventType::DeleteByCut,
664                )
665            },
666            EditingContext::Document(..) => false,
667        }
668    }
669
670    /// Returns true if and only if the paste should be canceled.
671    pub(crate) fn fire_paste_beforeinput_event(&self, cx: &mut JSContext, text: &str) -> bool {
672        match self {
673            EditingContext::TextControl(element) => {
674                element.text_control_element().fire_beforeinput_event(
675                    cx,
676                    Some(text),
677                    IsComposing::NotComposing,
678                    InputEventType::InsertFromPaste,
679                )
680            },
681            EditingContext::Document(..) => false,
682        }
683    }
684
685    pub(crate) fn fire_cut_events(&self) {
686        match self {
687            EditingContext::TextControl(element) => {
688                element.text_control_element().queue_input_event(
689                    None,
690                    IsComposing::NotComposing,
691                    InputEventType::DeleteByCut,
692                );
693            },
694            EditingContext::Document(..) => {},
695        }
696    }
697
698    pub(crate) fn insert_content(&self, cx: &mut JSContext, text_content: &str) {
699        match self {
700            EditingContext::TextControl(element) => {
701                element
702                    .text_control_element()
703                    .insert_content(cx, text_content);
704            },
705            EditingContext::Document(..) => {
706                // TODO(mrobinson): Add support for integration with contenteditable.
707            },
708        }
709    }
710
711    pub(crate) fn fire_paste_events(&self, text_content: &str) {
712        match self {
713            EditingContext::TextControl(element) => {
714                element.text_control_element().queue_input_event(
715                    Some(text_content.to_owned()),
716                    IsComposing::NotComposing,
717                    InputEventType::InsertFromPaste,
718                );
719            },
720            EditingContext::Document(..) => {},
721        }
722    }
723
724    pub(crate) fn select_all(&self, cx: &mut JSContext) {
725        match self {
726            EditingContext::TextControl(element) => element.text_control_element().select_all(),
727            EditingContext::Document(document) => {
728                let Some(selection) = document.GetSelection(cx) else {
729                    return;
730                };
731                if let Some(node) = document
732                    .GetBody()
733                    .map(DomRoot::upcast::<Node>)
734                    .or_else(|| document.GetDocumentElement().map(DomRoot::upcast::<Node>))
735                {
736                    let _ = selection.SelectAllChildren(cx, &node);
737                }
738            },
739        }
740    }
741
742    pub(crate) fn perform_editing_action(&self, cx: &mut JSContext, action: EditingAction) -> bool {
743        if let Some(node) = self.event_target().downcast::<Node>() {
744            // A previous event listener, such as keydown or beforeinput, might have
745            // hidden the event target. So we should re-check the layout and see if
746            // the node is still being rendered before performing an editing action
747            // on it.
748            node.owner_window().layout_reflow(QueryMsg::StyleQuery);
749            if !node.is_being_rendered_or_delegates_rendering(None) {
750                return false;
751            }
752        }
753
754        // A couple editing actions can be performed via the external interface of
755        // `EditingContext`. If that's the case we want to do that so that the same
756        // code path is used regardless of how the action was executed.
757        if let EditingAction::Clipboard(clipboard_action) = action {
758            return self
759                .document()
760                .handle_clipboard_action(cx, self, clipboard_action)
761                .contains(InputEventResult::Consumed);
762        }
763
764        if let EditingAction::SelectAll = action {
765            self.select_all(cx);
766            return true;
767        }
768
769        match self {
770            EditingContext::TextControl(element) => element
771                .text_control_element()
772                .perform_editing_action(cx, action),
773            EditingContext::Document(document) => document.perform_editing_action(cx, &action),
774        }
775    }
776
777    pub(crate) fn handle_mousedown_event(
778        &self,
779        cx: &mut JSContext,
780        mouse_event: &MouseEvent,
781        hit_test_result: &HitTestResult,
782    ) {
783        match self {
784            EditingContext::TextControl(element) => {
785                element.handle_mousedown_event(mouse_event, hit_test_result)
786            },
787            EditingContext::Document(document) => {
788                document.handle_mousedown_event(cx, mouse_event, hit_test_result)
789            },
790        }
791    }
792}
793
794pub(crate) fn editing_action_from_keyboard_event(
795    event: &KeyboardTypesEvent,
796) -> Option<EditingAction> {
797    let mut modifiers = event.modifiers;
798    if let Some(action) = ShortcutMatcher::new(event.state, event.key.clone(), modifiers)
799        .shortcut(CMD_OR_CONTROL, 'A', || Some(EditingAction::SelectAll))
800        .shortcut(CMD_OR_CONTROL, 'C', || {
801            Some(EditingAction::Clipboard(ClipboardAction::Copy))
802        })
803        .shortcut(CMD_OR_CONTROL, 'X', || {
804            Some(EditingAction::Clipboard(ClipboardAction::Cut))
805        })
806        .shortcut(CMD_OR_CONTROL, 'V', || {
807            Some(EditingAction::Clipboard(ClipboardAction::Paste))
808        })
809        // TODO: Figure out if the bit about Option+Enter works and whether or not this
810        //       ends up providing the correct behavior on Mac. (i.e. whether or not
811        //       Shift should be accepted in addition to Option.)
812        .shortcut(Modifiers::SHIFT, Key::Named(NamedKey::Enter), || {
813            Some(EditingAction::InsertNewline)
814        })
815        .shortcut(Modifiers::empty(), Key::Named(NamedKey::Enter), || {
816            Some(EditingAction::InsertParagraph)
817        })
818        .otherwise(|| None)
819        .flatten()
820    {
821        return Some(action);
822    };
823
824    let update_selection = if modifiers.contains(Modifiers::SHIFT) {
825        ModifySelection::Yes
826    } else {
827        ModifySelection::No
828    };
829    modifiers.remove(Modifiers::SHIFT);
830
831    ShortcutMatcher::new(KeyState::Down, event.key.clone(), modifiers)
832        .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowLeft), || {
833            Some(EditingAction::MoveCursor(
834                EditingDirection::Backward,
835                EditingMotion::Grapheme,
836                update_selection,
837            ))
838        })
839        .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowRight), || {
840            Some(EditingAction::MoveCursor(
841                EditingDirection::Forward,
842                EditingMotion::Grapheme,
843                update_selection,
844            ))
845        })
846        .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowUp), || {
847            Some(EditingAction::MoveCursor(
848                EditingDirection::Backward,
849                EditingMotion::Line,
850                update_selection,
851            ))
852        })
853        .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowDown), || {
854            Some(EditingAction::MoveCursor(
855                EditingDirection::Forward,
856                EditingMotion::Line,
857                update_selection,
858            ))
859        })
860        .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'B', || {
861            Some(EditingAction::MoveCursor(
862                EditingDirection::Backward,
863                EditingMotion::Word,
864                update_selection,
865            ))
866        })
867        .shortcut(ALT_OR_CONTROL, Key::Named(NamedKey::ArrowLeft), || {
868            Some(EditingAction::MoveCursor(
869                EditingDirection::Backward,
870                EditingMotion::Word,
871                update_selection,
872            ))
873        })
874        .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'F', || {
875            Some(EditingAction::MoveCursor(
876                EditingDirection::Forward,
877                EditingMotion::Word,
878                update_selection,
879            ))
880        })
881        .shortcut(ALT_OR_CONTROL, Key::Named(NamedKey::ArrowRight), || {
882            Some(EditingAction::MoveCursor(
883                EditingDirection::Forward,
884                EditingMotion::Word,
885                update_selection,
886            ))
887        })
888        .shortcut(Modifiers::empty(), Key::Named(NamedKey::Home), || {
889            Some(EditingAction::MoveCursor(
890                EditingDirection::Backward,
891                EditingMotion::LineStartOrEnd,
892                update_selection,
893            ))
894        })
895        .optional_shortcut(cfg!(target_os = "macos"), Modifiers::CONTROL, 'A', || {
896            Some(EditingAction::MoveCursor(
897                EditingDirection::Backward,
898                EditingMotion::LineStartOrEnd,
899                update_selection,
900            ))
901        })
902        .shortcut(Modifiers::empty(), Key::Named(NamedKey::End), || {
903            Some(EditingAction::MoveCursor(
904                EditingDirection::Forward,
905                EditingMotion::LineStartOrEnd,
906                update_selection,
907            ))
908        })
909        .optional_shortcut(cfg!(target_os = "macos"), Modifiers::CONTROL, 'E', || {
910            Some(EditingAction::MoveCursor(
911                EditingDirection::Backward,
912                EditingMotion::LineStartOrEnd,
913                update_selection,
914            ))
915        })
916        .optional_shortcut(
917            cfg!(target_os = "macos"),
918            Modifiers::META,
919            Key::Named(NamedKey::ArrowLeft),
920            || {
921                Some(EditingAction::MoveCursor(
922                    EditingDirection::Backward,
923                    EditingMotion::LineStartOrEnd,
924                    update_selection,
925                ))
926            },
927        )
928        .optional_shortcut(
929            cfg!(target_os = "macos"),
930            Modifiers::META,
931            Key::Named(NamedKey::ArrowRight),
932            || {
933                Some(EditingAction::MoveCursor(
934                    EditingDirection::Forward,
935                    EditingMotion::LineStartOrEnd,
936                    update_selection,
937                ))
938            },
939        )
940        .shortcut(Modifiers::empty(), Key::Named(NamedKey::PageUp), || {
941            Some(EditingAction::MoveCursor(
942                EditingDirection::Backward,
943                EditingMotion::Page,
944                update_selection,
945            ))
946        })
947        .shortcut(Modifiers::empty(), Key::Named(NamedKey::PageDown), || {
948            Some(EditingAction::MoveCursor(
949                EditingDirection::Forward,
950                EditingMotion::Page,
951                update_selection,
952            ))
953        })
954        .shortcut(Modifiers::empty(), Key::Named(NamedKey::Delete), || {
955            Some(EditingAction::Delete)
956        })
957        .shortcut(Modifiers::empty(), Key::Named(NamedKey::Backspace), || {
958            Some(EditingAction::Backspace(EditingMotion::Grapheme))
959        })
960        .shortcut(ALT_OR_CONTROL, Key::Named(NamedKey::Backspace), || {
961            Some(EditingAction::Backspace(EditingMotion::Word))
962        })
963        .otherwise(|| match &event.key {
964            Key::Character(character) if modifiers.is_empty() => {
965                Some(EditingAction::InsertText(character.to_string()))
966            },
967            _ => None,
968        })
969        .flatten()
970}
971
972pub(crate) fn editing_action_from_composition_event(event: &CompositionEvent) -> EditingAction {
973    match event.state {
974        CompositionState::Start => EditingAction::StartComposition,
975        CompositionState::Update => EditingAction::InsertCompositionText(event.data.to_string()),
976        CompositionState::End => EditingAction::EndComposition(event.data.to_string()),
977    }
978}