diff --git a/Libraries/LibWeb/DOM/EditingHostManager.cpp b/Libraries/LibWeb/DOM/EditingHostManager.cpp index 6e2e341753a..86157bcee5b 100644 --- a/Libraries/LibWeb/DOM/EditingHostManager.cpp +++ b/Libraries/LibWeb/DOM/EditingHostManager.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2024, Aliaksandr Kalenik + * Copyright (c) 2025, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -8,7 +9,9 @@ #include #include #include +#include #include +#include namespace Web::DOM { @@ -31,38 +34,18 @@ void EditingHostManager::visit_edges(Cell::Visitor& visitor) visitor.visit(m_active_contenteditable_element); } -void EditingHostManager::handle_insert(String const& data) +void EditingHostManager::handle_insert(String const& value) { - auto selection = m_document->get_selection(); + // https://w3c.github.io/editing/docs/execCommand/#additional-requirements + // When the user instructs the user agent to insert text inside an editing host, such as by typing on the keyboard + // while the cursor is in an editable node, the user agent must call execCommand("inserttext", false, value) on the + // relevant document, with value equal to the text the user provided. If the user inserts multiple characters at + // once or in quick succession, this specification does not define whether it is treated as one insertion or several + // consecutive insertions. - auto selection_range = selection->range(); - if (!selection_range) - return; - - auto node = selection->anchor_node(); - if (!node || !node->is_editable_or_editing_host()) - return; - - if (!is(*node)) { - auto& realm = node->realm(); - auto text = realm.create(node->document(), data); - MUST(node->append_child(*text)); - MUST(selection->collapse(*text, 1)); - return; - } - - auto& text_node = static_cast(*node); - - MUST(selection_range->delete_contents()); - MUST(text_node.insert_data(selection->anchor_offset(), data)); - VERIFY(selection->is_collapsed()); - - auto utf16_data = MUST(AK::utf8_to_utf16(data)); - Utf16View const utf16_view { utf16_data }; - auto length = utf16_view.length_in_code_units(); - MUST(selection->collapse(*node, selection->anchor_offset() + length)); - - text_node.invalidate_style(DOM::StyleInvalidationReason::EditingInsertion); + auto editing_result = m_document->exec_command(Editing::CommandNames::insertText, false, value); + if (editing_result.is_exception()) + dbgln("handle_insert(): editing resulted in exception: {}", editing_result.exception()); } void EditingHostManager::select_all() @@ -159,41 +142,49 @@ void EditingHostManager::decrement_cursor_position_to_previous_word(CollapseSele void EditingHostManager::handle_delete(DeleteDirection direction) { - auto selection = m_document->get_selection(); - auto selection_range = selection->range(); - if (!selection_range) { - return; - } + // https://w3c.github.io/editing/docs/execCommand/#additional-requirements + // When the user instructs the user agent to delete the previous character inside an editing host, such as by + // pressing the Backspace key while the cursor is in an editable node, the user agent must call + // execCommand("delete") on the relevant document. + // When the user instructs the user agent to delete the next character inside an editing host, such as by pressing + // the Delete key while the cursor is in an editable node, the user agent must call execCommand("forwarddelete") on + // the relevant document. - if (selection->is_collapsed()) { - auto node = selection->anchor_node(); - if (!node || !is(*node)) { - return; - } + auto editing_result = [&] { + if (direction == DeleteDirection::Backward) + return m_document->exec_command(Editing::CommandNames::delete_, false, {}); + if (direction == DeleteDirection::Forward) + return m_document->exec_command(Editing::CommandNames::forwardDelete, false, {}); + VERIFY_NOT_REACHED(); + }(); - auto& text_node = static_cast(*node); - if (direction == DeleteDirection::Backward) { - if (selection->anchor_offset() > 0) { - MUST(text_node.delete_data(selection->anchor_offset() - 1, 1)); - text_node.invalidate_style(DOM::StyleInvalidationReason::EditingInsertion); - } - } else { - if (selection->anchor_offset() < text_node.data().bytes_as_string_view().length()) { - MUST(text_node.delete_data(selection->anchor_offset(), 1)); - text_node.invalidate_style(DOM::StyleInvalidationReason::EditingInsertion); - } - } - m_document->reset_cursor_blink_cycle(); - return; - } - - MUST(selection_range->delete_contents()); + if (editing_result.is_exception()) + dbgln("handle_delete(): editing resulted in exception: {}", editing_result.exception()); } -EventResult EditingHostManager::handle_return_key() +EventResult EditingHostManager::handle_return_key(FlyString const& ui_input_type) { - dbgln("FIXME: Implement EditingHostManager::handle_return_key()"); - return EventResult::Dropped; + // https://w3c.github.io/editing/docs/execCommand/#additional-requirements + // When the user instructs the user agent to insert a line break inside an editing host, such as by pressing the + // Enter key while the cursor is in an editable node, the user agent must call execCommand("insertparagraph") on the + // relevant document. + // When the user instructs the user agent to insert a line break inside an editing host without breaking out of the + // current block, such as by pressing Shift-Enter or Option-Enter while the cursor is in an editable node, the user + // agent must call execCommand("insertlinebreak") on the relevant document. + auto editing_result = [&] { + if (ui_input_type == UIEvents::InputTypes::insertParagraph) + return m_document->exec_command(Editing::CommandNames::insertParagraph, false, {}); + if (ui_input_type == UIEvents::InputTypes::insertLineBreak) + return m_document->exec_command(Editing::CommandNames::insertLineBreak, false, {}); + VERIFY_NOT_REACHED(); + }(); + + if (editing_result.is_exception()) { + dbgln("handle_return_key(): editing resulted in exception: {}", editing_result.exception()); + return EventResult::Dropped; + } + + return editing_result.value() ? EventResult::Handled : EventResult::Dropped; } } diff --git a/Libraries/LibWeb/DOM/EditingHostManager.h b/Libraries/LibWeb/DOM/EditingHostManager.h index fe629dcb60c..a460f6bf6b1 100644 --- a/Libraries/LibWeb/DOM/EditingHostManager.h +++ b/Libraries/LibWeb/DOM/EditingHostManager.h @@ -25,7 +25,7 @@ public: virtual void handle_insert(String const&) override; virtual void handle_delete(DeleteDirection) override; - virtual EventResult handle_return_key() override; + virtual EventResult handle_return_key(FlyString const& ui_input_type) override; virtual void select_all() override; virtual void set_selection_anchor(GC::Ref, size_t offset) override; virtual void set_selection_focus(GC::Ref, size_t offset) override; diff --git a/Libraries/LibWeb/DOM/InputEventsTarget.h b/Libraries/LibWeb/DOM/InputEventsTarget.h index 668699fbcba..5eefa8d2567 100644 --- a/Libraries/LibWeb/DOM/InputEventsTarget.h +++ b/Libraries/LibWeb/DOM/InputEventsTarget.h @@ -19,7 +19,7 @@ public: virtual GC::Ref as_cell() = 0; virtual void handle_insert(String const&) = 0; - virtual EventResult handle_return_key() = 0; + virtual EventResult handle_return_key(FlyString const& ui_input_type) = 0; enum class DeleteDirection { Backward, diff --git a/Libraries/LibWeb/Editing/ExecCommand.cpp b/Libraries/LibWeb/Editing/ExecCommand.cpp index e9c59b6fbdf..0af95fafc30 100644 --- a/Libraries/LibWeb/Editing/ExecCommand.cpp +++ b/Libraries/LibWeb/Editing/ExecCommand.cpp @@ -122,6 +122,11 @@ WebIDL::ExceptionOr Document::exec_command(FlyString const& command, [[may UIEvents::InputEventInit event_init {}; event_init.bubbles = true; event_init.input_type = command_definition.mapped_value; + + // AD-HOC: For insertText, we do what other browsers do and set data to value. + if (command == Editing::CommandNames::insertText) + event_init.data = value; + auto event = realm().create(realm(), HTML::EventNames::input, event_init); event->set_is_trusted(true); affected_editing_host->dispatch_event(event); diff --git a/Libraries/LibWeb/HTML/FormAssociatedElement.cpp b/Libraries/LibWeb/HTML/FormAssociatedElement.cpp index 4803015e4b8..8dec7c1c3f7 100644 --- a/Libraries/LibWeb/HTML/FormAssociatedElement.cpp +++ b/Libraries/LibWeb/HTML/FormAssociatedElement.cpp @@ -820,7 +820,7 @@ void FormAssociatedTextControlElement::handle_delete(DeleteDirection direction) MUST(set_range_text(String {}, selection_start, selection_end, Bindings::SelectionMode::End)); } -EventResult FormAssociatedTextControlElement::handle_return_key() +EventResult FormAssociatedTextControlElement::handle_return_key(FlyString const&) { auto* input_element = as_if(form_associated_element_to_html_element()); if (!input_element) diff --git a/Libraries/LibWeb/HTML/FormAssociatedElement.h b/Libraries/LibWeb/HTML/FormAssociatedElement.h index 250a8ddf4cd..6941e64ecce 100644 --- a/Libraries/LibWeb/HTML/FormAssociatedElement.h +++ b/Libraries/LibWeb/HTML/FormAssociatedElement.h @@ -226,7 +226,7 @@ public: virtual void handle_insert(String const&) override; virtual void handle_delete(DeleteDirection) override; - virtual EventResult handle_return_key() override; + virtual EventResult handle_return_key(FlyString const& ui_input_type) override; virtual void select_all() override; virtual void set_selection_anchor(GC::Ref, size_t offset) override; virtual void set_selection_focus(GC::Ref, size_t offset) override; diff --git a/Libraries/LibWeb/Page/EventHandler.cpp b/Libraries/LibWeb/Page/EventHandler.cpp index 8b851dc288e..a6f01d87a67 100644 --- a/Libraries/LibWeb/Page/EventHandler.cpp +++ b/Libraries/LibWeb/Page/EventHandler.cpp @@ -2,6 +2,7 @@ * Copyright (c) 2020-2021, Andreas Kling * Copyright (c) 2021, Max Wipfli * Copyright (c) 2024, Aliaksandr Kalenik + * Copyright (c) 2025, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -9,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -1192,9 +1194,9 @@ EventResult EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u // instead interpret this interaction as some other action, instead of interpreting it as a close request. } - if (auto* element = m_navigable->active_document()->focused_element(); is(element)) { - auto& media_element = static_cast(*element); - if (media_element.handle_keydown({}, key, modifiers).release_value_but_fixme_should_propagate_errors()) + auto* focused_element = m_navigable->active_document()->focused_element(); + if (auto* media_element = as_if(focused_element)) { + if (media_element->handle_keydown({}, key, modifiers).release_value_but_fixme_should_propagate_errors()) return EventResult::Handled; } @@ -1203,14 +1205,12 @@ EventResult EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u if (key == UIEvents::KeyCode::Key_Backspace) { FIRE(input_event(UIEvents::EventNames::beforeinput, UIEvents::InputTypes::deleteContentBackward, m_navigable, code_point)); target->handle_delete(InputEventsTarget::DeleteDirection::Backward); - FIRE(input_event(UIEvents::EventNames::input, UIEvents::InputTypes::deleteContentBackward, m_navigable, code_point)); return EventResult::Handled; } if (key == UIEvents::KeyCode::Key_Delete) { FIRE(input_event(UIEvents::EventNames::beforeinput, UIEvents::InputTypes::deleteContentForward, m_navigable, code_point)); target->handle_delete(InputEventsTarget::DeleteDirection::Forward); - FIRE(input_event(UIEvents::EventNames::input, UIEvents::InputTypes::deleteContentForward, m_navigable, code_point)); return EventResult::Handled; } @@ -1257,13 +1257,20 @@ EventResult EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u return EventResult::Handled; } - if (key == UIEvents::KeyCode::Key_Return) { - FIRE(input_event(UIEvents::EventNames::beforeinput, UIEvents::InputTypes::insertParagraph, m_navigable, code_point)); + if (key == UIEvents::KeyCode::Key_Return && (modifiers == UIEvents::Mod_None || modifiers == UIEvents::Mod_Shift)) { + auto input_type = modifiers == UIEvents::Mod_Shift ? UIEvents::InputTypes::insertLineBreak : UIEvents::InputTypes::insertParagraph; - if (target->handle_return_key() != EventResult::Handled) + // If the editing host is contenteditable="plaintext-only", we force a line break. + if (focused_element) { + if (auto editing_host = Editing::editing_host_of_node(*focused_element); editing_host + && as(*editing_host).content_editable_state() == HTML::ContentEditableState::PlaintextOnly) + input_type = UIEvents::InputTypes::insertLineBreak; + } + + FIRE(input_event(UIEvents::EventNames::beforeinput, input_type, m_navigable, code_point)); + if (target->handle_return_key(input_type) != EventResult::Handled) target->handle_insert(String::from_code_point(code_point)); - FIRE(input_event(UIEvents::EventNames::input, UIEvents::InputTypes::insertParagraph, m_navigable, code_point)); return EventResult::Handled; } @@ -1271,7 +1278,6 @@ EventResult EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u if (!should_ignore_keydown_event(code_point, modifiers)) { FIRE(input_event(UIEvents::EventNames::beforeinput, UIEvents::InputTypes::insertText, m_navigable, code_point)); target->handle_insert(String::from_code_point(code_point)); - FIRE(input_event(UIEvents::EventNames::input, UIEvents::InputTypes::insertText, m_navigable, code_point)); return EventResult::Handled; } } else if (auto selection = document->get_selection(); selection && !selection->is_collapsed()) { diff --git a/Libraries/LibWeb/UIEvents/InputTypes.h b/Libraries/LibWeb/UIEvents/InputTypes.h index 437cf671651..7d62b6c5214 100644 --- a/Libraries/LibWeb/UIEvents/InputTypes.h +++ b/Libraries/LibWeb/UIEvents/InputTypes.h @@ -12,10 +12,11 @@ namespace Web::UIEvents::InputTypes { // https://w3c.github.io/input-events/#interface-InputEvent-Attributes #define ENUMERATE_INPUT_TYPES \ - __ENUMERATE_INPUT_TYPE(insertText) \ - __ENUMERATE_INPUT_TYPE(insertParagraph) \ __ENUMERATE_INPUT_TYPE(deleteContentBackward) \ - __ENUMERATE_INPUT_TYPE(deleteContentForward) + __ENUMERATE_INPUT_TYPE(deleteContentForward) \ + __ENUMERATE_INPUT_TYPE(insertLineBreak) \ + __ENUMERATE_INPUT_TYPE(insertParagraph) \ + __ENUMERATE_INPUT_TYPE(insertText) #define __ENUMERATE_INPUT_TYPE(name) extern FlyString name; ENUMERATE_INPUT_TYPES diff --git a/Libraries/LibWeb/UIEvents/KeyCode.h b/Libraries/LibWeb/UIEvents/KeyCode.h index 1cb45726e1c..dc507de5a33 100644 --- a/Libraries/LibWeb/UIEvents/KeyCode.h +++ b/Libraries/LibWeb/UIEvents/KeyCode.h @@ -229,6 +229,10 @@ inline KeyCode code_point_to_key_code(u32 code_point) #define MATCH_KEY(name, character) \ case character: \ return KeyCode::Key_##name; + MATCH_KEY(Backspace, '\b') + MATCH_KEY(Tab, '\t') + MATCH_KEY(Return, '\n') + MATCH_KEY(Space, ' ') MATCH_KEY(ExclamationPoint, '!') MATCH_KEY(DoubleQuote, '"') MATCH_KEY(Hashtag, '#') @@ -271,9 +275,6 @@ inline KeyCode code_point_to_key_code(u32 code_point) MATCH_KEY(Pipe, '|') MATCH_KEY(Tilde, '~') MATCH_KEY(Backtick, '`') - MATCH_KEY(Space, ' ') - MATCH_KEY(Tab, '\t') - MATCH_KEY(Backspace, '\b') #undef MATCH_KEY default: