From f36d4ef4ca758efebbfb1625f4123df47a72f3bd Mon Sep 17 00:00:00 2001 From: Jelle Raaijmakers Date: Mon, 26 Aug 2024 13:07:57 +0200 Subject: [PATCH] LibWeb: Update input/textarea selection on document selection change This causes UI interactions with the document selection to also update the input and textarea DOM selection state. Note that we switch around the order of focusing a DOM node and setting the selection, so we allow the focus event to override whatever selection we came up with. --- .../Libraries/LibWeb/Page/EventHandler.cpp | 80 ++++++++++++++++--- Userland/Libraries/LibWeb/Page/EventHandler.h | 1 + 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/Userland/Libraries/LibWeb/Page/EventHandler.cpp b/Userland/Libraries/LibWeb/Page/EventHandler.cpp index d29cbf9372dda..92d2a920ea203 100644 --- a/Userland/Libraries/LibWeb/Page/EventHandler.cpp +++ b/Userland/Libraries/LibWeb/Page/EventHandler.cpp @@ -10,12 +10,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -354,8 +356,10 @@ bool EventHandler::handle_mouseup(CSSPixelPoint viewport_position, CSSPixelPoint } after_node_use: - if (button == UIEvents::MouseButton::Primary) + if (button == UIEvents::MouseButton::Primary) { m_in_mouse_selection = false; + update_selection_range_for_input_or_textarea(); + } return handled_event; } @@ -433,25 +437,17 @@ bool EventHandler::handle_mousedown(CSSPixelPoint viewport_position, CSSPixelPoi auto dom_node = paintable->dom_node(); if (dom_node) { // See if we want to focus something. - bool did_focus_something = false; + JS::GCPtr focus_candidate; for (auto candidate = node; candidate; candidate = candidate->parent_or_shadow_host()) { if (candidate->is_focusable()) { - // When a user activates a click focusable focusable area, the user agent must run the focusing steps on the focusable area with focus trigger set to "click". - // Spec Note: Note that focusing is not an activation behavior, i.e. calling the click() method on an element or dispatching a synthetic click event on it won't cause the element to get focused. - HTML::run_focusing_steps(candidate.ptr(), nullptr, "click"sv); - did_focus_something = true; + focus_candidate = candidate; break; } } - if (!did_focus_something) { - if (auto* focused_element = document->focused_element()) - HTML::run_unfocusing_steps(focused_element); - } - // If we didn't focus anything, place the document text cursor at the mouse position. // FIXME: This is all rather strange. Find a better solution. - if (!did_focus_something || dom_node->is_editable()) { + if (!focus_candidate || dom_node->is_editable()) { auto& realm = document->realm(); document->set_cursor_position(DOM::Position::create(realm, *dom_node, result->index_in_node)); if (auto selection = document->get_selection()) { @@ -462,8 +458,16 @@ bool EventHandler::handle_mousedown(CSSPixelPoint viewport_position, CSSPixelPoi (void)selection->set_base_and_extent(*dom_node, result->index_in_node, *dom_node, result->index_in_node); } } + update_selection_range_for_input_or_textarea(); m_in_mouse_selection = true; } + + // When a user activates a click focusable focusable area, the user agent must run the focusing steps on the focusable area with focus trigger set to "click". + // Spec Note: Note that focusing is not an activation behavior, i.e. calling the click() method on an element or dispatching a synthetic click event on it won't cause the element to get focused. + if (focus_candidate) + HTML::run_focusing_steps(focus_candidate, nullptr, "click"sv); + else if (auto* focused_element = document->focused_element()) + HTML::run_unfocusing_steps(focused_element); } } } @@ -706,6 +710,7 @@ bool EventHandler::handle_doubleclick(CSSPixelPoint viewport_position, CSSPixelP if (auto selection = node->document().get_selection()) { (void)selection->set_base_and_extent(hit_dom_node, first_word_break_before, hit_dom_node, first_word_break_after); } + update_selection_range_for_input_or_textarea(); } } @@ -978,6 +983,8 @@ bool EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u32 code } } + update_selection_range_for_input_or_textarea(); + // FIXME: Implement scroll by line and by page instead of approximating the behavior of other browsers. auto arrow_key_scroll_distance = 100; auto page_scroll_distance = document->window()->inner_height() - (document->window()->outer_height() - document->window()->inner_height()); @@ -1113,4 +1120,53 @@ void EventHandler::visit_edges(JS::Cell::Visitor& visitor) const visitor.visit(m_mouse_event_tracking_paintable); } +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection:set-the-selection-range +void EventHandler::update_selection_range_for_input_or_textarea() +{ + // Where possible, user interface features for changing the text selection in input and + // textarea elements must be implemented using the set the selection range algorithm so that, + // e.g., all the same events fire. + + // NOTE: It seems like only new selections are registered with the respective elements. I.e. + // existing selections in other elements are not cleared, so we only need to set the + // selection range for the element with the current selection. + + // Get the active selection + auto active_document = m_navigable->active_document(); + if (!active_document) + return; + auto selection = active_document->get_selection(); + if (!selection) + return; + + // Do we have a range within the same node? + auto range = selection->range(); + if (!range || range->start_container() != range->end_container()) + return; + + // We are only interested in text nodes with a shadow root + auto& node = *range->start_container(); + if (!node.is_text()) + return; + auto& root = node.root(); + if (!root.is_shadow_root()) + return; + auto& shadow_host = *root.parent_or_shadow_host(); + + // Invoke "set the selection range" on the form associated element + auto selection_start = range->start_offset(); + auto selection_end = range->end_offset(); + // FIXME: support selection directions other than ::Forward + auto direction = HTML::SelectionDirection::Forward; + + Optional target {}; + if (is(shadow_host)) + target = static_cast(shadow_host); + else if (is(shadow_host)) + target = static_cast(shadow_host); + + if (target.has_value()) + target.value().set_the_selection_range(selection_start, selection_end, direction); +} + } diff --git a/Userland/Libraries/LibWeb/Page/EventHandler.h b/Userland/Libraries/LibWeb/Page/EventHandler.h index c76e95fa1c8c1..d6c5392c18656 100644 --- a/Userland/Libraries/LibWeb/Page/EventHandler.h +++ b/Userland/Libraries/LibWeb/Page/EventHandler.h @@ -60,6 +60,7 @@ class EventHandler { Painting::PaintableBox const* paint_root() const; bool should_ignore_device_input_event() const; + void update_selection_range_for_input_or_textarea(); JS::NonnullGCPtr m_navigable;