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();