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