mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-28 19:59:17 +00:00
LibWeb: Start integrating the editing API with user keyboard input
This reworks EventHandler so text insertion, backspace, delete and return actions are now handled by the Editing API. This was the whole point of the execCommand spec, to provide an implementation of both editing commands and the expected editing behavior on user input. Responsibility of firing the `input` event is moved from EventHandler to the Editing API, which also gets rid of duplicate events whenever dealing with `<input>` or `<textarea>` events. The `beforeinput` event still needs to be fired by `EventHandler` however, since that is never fired by `execCommand()`.
This commit is contained in:
parent
564f5ca2cc
commit
ac46ec0b2e
Notes:
github-actions[bot]
2025-05-16 22:30:34 +00:00
Author: https://github.com/gmta
Commit: ac46ec0b2e
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/4779
9 changed files with 84 additions and 80 deletions
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
|
||||
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
@ -8,7 +9,9 @@
|
|||
#include <LibWeb/DOM/EditingHostManager.h>
|
||||
#include <LibWeb/DOM/Range.h>
|
||||
#include <LibWeb/DOM/Text.h>
|
||||
#include <LibWeb/Editing/CommandNames.h>
|
||||
#include <LibWeb/Selection/Selection.h>
|
||||
#include <LibWeb/UIEvents/InputTypes.h>
|
||||
|
||||
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<DOM::Text>(*node)) {
|
||||
auto& realm = node->realm();
|
||||
auto text = realm.create<DOM::Text>(node->document(), data);
|
||||
MUST(node->append_child(*text));
|
||||
MUST(selection->collapse(*text, 1));
|
||||
return;
|
||||
}
|
||||
|
||||
auto& text_node = static_cast<DOM::Text&>(*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<DOM::Text>(*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<DOM::Text&>(*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()");
|
||||
// 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<DOM::Node>, size_t offset) override;
|
||||
virtual void set_selection_focus(GC::Ref<DOM::Node>, size_t offset) override;
|
||||
|
|
|
@ -19,7 +19,7 @@ public:
|
|||
virtual GC::Ref<JS::Cell> 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,
|
||||
|
|
|
@ -122,6 +122,11 @@ WebIDL::ExceptionOr<bool> 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<UIEvents::InputEvent>(realm(), HTML::EventNames::input, event_init);
|
||||
event->set_is_trusted(true);
|
||||
affected_editing_host->dispatch_event(event);
|
||||
|
|
|
@ -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<HTMLInputElement>(form_associated_element_to_html_element());
|
||||
if (!input_element)
|
||||
|
|
|
@ -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<DOM::Node>, size_t offset) override;
|
||||
virtual void set_selection_focus(GC::Ref<DOM::Node>, size_t offset) override;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* Copyright (c) 2020-2021, Andreas Kling <andreas@ladybird.org>
|
||||
* Copyright (c) 2021, Max Wipfli <mail@maxwipfli.ch>
|
||||
* Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
|
||||
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
@ -9,6 +10,7 @@
|
|||
#include <LibUnicode/CharacterTypes.h>
|
||||
#include <LibUnicode/Segmenter.h>
|
||||
#include <LibWeb/DOM/Text.h>
|
||||
#include <LibWeb/Editing/Internal/Algorithms.h>
|
||||
#include <LibWeb/HTML/CloseWatcherManager.h>
|
||||
#include <LibWeb/HTML/Focus.h>
|
||||
#include <LibWeb/HTML/HTMLAnchorElement.h>
|
||||
|
@ -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<HTML::HTMLMediaElement>(element)) {
|
||||
auto& media_element = static_cast<HTML::HTMLMediaElement&>(*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<HTML::HTMLMediaElement>(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<HTML::HTMLElement>(*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()) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue