From 7edfeb7056c9ea2ba1b9a489df8a2d0abba686b9 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Thu, 30 Nov 2023 12:54:30 -0500 Subject: [PATCH] LibWeb: Support committing changes to elements We currently fire the change event on elements when they lose focus. The spec allows for us to also fire the event when changes are "committed", so long as such an action makes sense for the input type. This patch detects when the return key is entered in an element and uses that as the commit action for text-related types. If no change has occurred since the last commit, no change event is fired. --- Tests/LibWeb/Text/expected/input-commit.txt | 1 + Tests/LibWeb/Text/input/input-commit.html | 15 +++++++ .../LibWeb/HTML/HTMLInputElement.cpp | 39 ++++++++++++++----- .../Libraries/LibWeb/HTML/HTMLInputElement.h | 4 ++ .../Libraries/LibWeb/Internals/Internals.cpp | 22 +++++++++++ .../Libraries/LibWeb/Internals/Internals.h | 3 ++ .../Libraries/LibWeb/Internals/Internals.idl | 5 +++ .../Libraries/LibWeb/Page/EventHandler.cpp | 10 ++++- 8 files changed, 88 insertions(+), 11 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/input-commit.txt create mode 100644 Tests/LibWeb/Text/input/input-commit.html diff --git a/Tests/LibWeb/Text/expected/input-commit.txt b/Tests/LibWeb/Text/expected/input-commit.txt new file mode 100644 index 0000000000..cc7af5f15f --- /dev/null +++ b/Tests/LibWeb/Text/expected/input-commit.txt @@ -0,0 +1 @@ +wfh :^) wfh :^) diff --git a/Tests/LibWeb/Text/input/input-commit.html b/Tests/LibWeb/Text/input/input-commit.html new file mode 100644 index 0000000000..a3140eae07 --- /dev/null +++ b/Tests/LibWeb/Text/input/input-commit.html @@ -0,0 +1,15 @@ + + + diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp index e3113f8d3e..7c397dcd68 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -276,8 +276,6 @@ WebIDL::ExceptionOr HTMLInputElement::run_input_activation_behavior() TRY(form->submit_form(*this)); } else if (type_state() == TypeAttributeState::FileUpload || type_state() == TypeAttributeState::Color) { show_the_picker_if_applicable(*this); - } else { - dispatch_event(DOM::Event::create(realm(), EventNames::change)); } return {}; @@ -289,6 +287,8 @@ void HTMLInputElement::did_edit_text_node(Badge) m_value = value_sanitization_algorithm(m_text_node->data()); m_dirty_value = true; + m_has_uncommitted_changes = true; + update_placeholder_visibility(); // NOTE: This is a bit ad-hoc, but basically implements part of "4.10.5.5 Common event behaviors" @@ -406,6 +406,33 @@ WebIDL::ExceptionOr HTMLInputElement::set_value(String const& value) return {}; } +void HTMLInputElement::commit_pending_changes() +{ + // The change event fires when the value is committed, if that makes sense for the control, + // or else when the control loses focus + switch (type_state()) { + case TypeAttributeState::Email: + case TypeAttributeState::Password: + case TypeAttributeState::Search: + case TypeAttributeState::Telephone: + case TypeAttributeState::Text: + case TypeAttributeState::URL: + if (!m_has_uncommitted_changes) + return; + break; + + default: + break; + } + + m_has_uncommitted_changes = false; + + queue_an_element_task(HTML::Task::Source::UserInteraction, [this] { + auto change_event = DOM::Event::create(realm(), HTML::EventNames::change, { .bubbles = true }); + dispatch_event(change_event); + }); +} + void HTMLInputElement::update_placeholder_visibility() { if (!m_placeholder_element) @@ -615,13 +642,7 @@ void HTMLInputElement::did_receive_focus() void HTMLInputElement::did_lose_focus() { - // The change event fires when the value is committed, if that makes sense for the control, - // or else when the control loses focus - queue_an_element_task(HTML::Task::Source::UserInteraction, [this] { - auto change_event = DOM::Event::create(realm(), HTML::EventNames::change); - change_event->set_bubbles(true); - dispatch_event(change_event); - }); + commit_pending_changes(); } void HTMLInputElement::attribute_changed(FlyString const& name, Optional const& value) diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h index 2c381a9c51..ccb22794d2 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h @@ -69,6 +69,8 @@ public: virtual String value() const override; WebIDL::ExceptionOr set_value(String const&); + void commit_pending_changes(); + Optional placeholder_value() const; bool checked() const { return m_checked; } @@ -215,6 +217,8 @@ private: TypeAttributeState m_type { TypeAttributeState::Text }; String m_value; + + bool m_has_uncommitted_changes { false }; }; } diff --git a/Userland/Libraries/LibWeb/Internals/Internals.cpp b/Userland/Libraries/LibWeb/Internals/Internals.cpp index d8965a1889..75fcd3ffad 100644 --- a/Userland/Libraries/LibWeb/Internals/Internals.cpp +++ b/Userland/Libraries/LibWeb/Internals/Internals.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -62,6 +63,27 @@ JS::Object* Internals::hit_test(double x, double y) return nullptr; } +void Internals::send_text(HTML::HTMLElement& target, String const& text) +{ + auto* page = global_object().browsing_context()->page(); + if (!page) + return; + + target.focus(); + + for (auto code_point : text.code_points()) + page->handle_keydown(code_point_to_key_code(code_point), 0, code_point); +} + +void Internals::commit_text() +{ + auto* page = global_object().browsing_context()->page(); + if (!page) + return; + + page->handle_keydown(Key_Return, 0, 0); +} + WebIDL::ExceptionOr Internals::dispatch_user_activated_event(DOM::EventTarget& target, DOM::Event& event) { event.set_is_trusted(true); diff --git a/Userland/Libraries/LibWeb/Internals/Internals.h b/Userland/Libraries/LibWeb/Internals/Internals.h index b5c1a7fa62..c3f21ac44e 100644 --- a/Userland/Libraries/LibWeb/Internals/Internals.h +++ b/Userland/Libraries/LibWeb/Internals/Internals.h @@ -22,6 +22,9 @@ public: void gc(); JS::Object* hit_test(double x, double y); + void send_text(HTML::HTMLElement&, String const&); + void commit_text(); + WebIDL::ExceptionOr dispatch_user_activated_event(DOM::EventTarget&, DOM::Event& event); private: diff --git a/Userland/Libraries/LibWeb/Internals/Internals.idl b/Userland/Libraries/LibWeb/Internals/Internals.idl index ea7f624cdf..69f3733173 100644 --- a/Userland/Libraries/LibWeb/Internals/Internals.idl +++ b/Userland/Libraries/LibWeb/Internals/Internals.idl @@ -1,10 +1,15 @@ #import +#import [Exposed=Nobody] interface Internals { undefined signalTextTestIsDone(); undefined gc(); object hitTest(double x, double y); + + undefined sendText(HTMLElement target, DOMString text); + undefined commitText(); + boolean dispatchUserActivatedEvent(EventTarget target, Event event); }; diff --git a/Userland/Libraries/LibWeb/Page/EventHandler.cpp b/Userland/Libraries/LibWeb/Page/EventHandler.cpp index 88cdbca3e4..87596dff91 100644 --- a/Userland/Libraries/LibWeb/Page/EventHandler.cpp +++ b/Userland/Libraries/LibWeb/Page/EventHandler.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -769,6 +770,8 @@ bool EventHandler::handle_keydown(KeyCode key, unsigned modifiers, u32 code_poin } if (m_browsing_context->cursor_position() && m_browsing_context->cursor_position()->node()->is_editable()) { + auto& node = verify_cast(*m_browsing_context->cursor_position()->node()); + if (key == KeyCode::Key_Backspace) { if (!m_browsing_context->decrement_cursor_position_offset()) { // FIXME: Move to the previous node and delete the last character there. @@ -799,15 +802,18 @@ bool EventHandler::handle_keydown(KeyCode key, unsigned modifiers, u32 code_poin return true; } if (key == KeyCode::Key_Home) { - auto& node = verify_cast(*m_browsing_context->cursor_position()->node()); m_browsing_context->set_cursor_position(DOM::Position::create(realm, node, 0)); return true; } if (key == KeyCode::Key_End) { - auto& node = verify_cast(*m_browsing_context->cursor_position()->node()); m_browsing_context->set_cursor_position(DOM::Position::create(realm, node, (unsigned)node.data().bytes().size())); return true; } + if (key == KeyCode::Key_Return && is(node.editable_text_node_owner())) { + auto& input_element = static_cast(*node.editable_text_node_owner()); + input_element.commit_pending_changes(); + return true; + } if (!should_ignore_keydown_event(code_point)) { m_edit_event_handler->handle_insert(JS::NonnullGCPtr { *m_browsing_context->cursor_position() }, code_point); m_browsing_context->increment_cursor_position_offset();