LibWeb: Separate text control input events handling from contenteditable

This input event handling change is intended to address the following
design issues:
- Having `DOM::Position` is unnecessary complexity when `Selection`
  exists because caret position could be described by the selection
  object with a collapsed state. Before this change, we had to
  synchronize those whenever one of them was modified, and there were
  already bugs caused by that, i.e., caret position was not changed when
  selection offset was modified from the JS side.
- Selection API exposes selection offset within `<textarea>` and
  `<input>`, which is not supposed to happen. These objects should
  manage their selection state by themselves and have selection offset
  even when they are not displayed.
- `EventHandler` looks only at `DOM::Text` owned by `DOM::Position`
  while doing text manipulations. It works fine for `<input>` and
  `<textarea>`, but `contenteditable` needs to consider all text
  descendant text nodes; i.e., if the cursor is moved outside of
  `DOM::Text`, we need to look for an adjacent text node to move the
  cursor there.

With this change, `EventHandler` no longer does direct manipulations on
caret position or text content, but instead delegates them to the active
`InputEventsTarget`, which could be either
`FormAssociatedTextControlElement` (for `<input>` and `<textarea>`) or
`EditingHostManager` (for `contenteditable`). The `Selection` object is
used to manage both selection and caret position for `contenteditable`,
and text control elements manage their own selection state that is not
exposed by Selection API.

This change improves text editing on Discord, as now we don't have to
refocus the `contenteditable` element after character input. The problem
was that selection manipulations from the JS side were not propagated
to `DOM::Position`.

I expect this change to make future correctness improvements for
`contenteditable` (and `designMode`) easier, as now it's decoupled from
`<input>` and `<textarea>` and separated from `EventHandler`, which is
quite a busy file.
This commit is contained in:
Aliaksandr Kalenik 2024-10-23 21:26:58 +02:00 committed by Alexander Kalenik
commit a8077f79cc
Notes: github-actions[bot] 2024-10-30 18:30:49 +00:00
35 changed files with 884 additions and 663 deletions

View file

@ -16,7 +16,6 @@
#include <LibJS/Forward.h>
#include <LibJS/Heap/Cell.h>
#include <LibURL/Origin.h>
#include <LibWeb/DOM/Position.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWeb/HTML/NavigableContainer.h>
#include <LibWeb/HTML/SandboxingFlagSet.h>

View file

@ -6,8 +6,11 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibUnicode/CharacterTypes.h>
#include <LibUnicode/Segmenter.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/Position.h>
#include <LibWeb/HTML/FormAssociatedElement.h>
#include <LibWeb/HTML/HTMLButtonElement.h>
#include <LibWeb/HTML/HTMLFieldSetElement.h>
@ -17,6 +20,7 @@
#include <LibWeb/HTML/HTMLSelectElement.h>
#include <LibWeb/HTML/HTMLTextAreaElement.h>
#include <LibWeb/HTML/Parser/HTMLParser.h>
#include <LibWeb/Painting/Paintable.h>
namespace Web::HTML {
@ -196,7 +200,7 @@ WebIDL::ExceptionOr<void> FormAssociatedElement::set_form_action(String const& v
}
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
void FormAssociatedTextControlElement::relevant_value_was_changed(JS::GCPtr<DOM::Text> text_node)
void FormAssociatedTextControlElement::relevant_value_was_changed()
{
auto the_relevant_value = relevant_value();
auto relevant_value_length = the_relevant_value.code_points().length();
@ -223,13 +227,8 @@ void FormAssociatedTextControlElement::relevant_value_was_changed(JS::GCPtr<DOM:
// 2. Otherwise, the element must have a text entry cursor position position. If it is now past
// the end of the relevant value, set it to the end of the relevant value.
auto& document = form_associated_element_to_html_element().document();
auto const current_cursor_position = document.cursor_position();
if (current_cursor_position && text_node
&& current_cursor_position->node() == text_node
&& current_cursor_position->offset() > relevant_value_length) {
document.set_cursor_position(DOM::Position::create(document.realm(), *text_node, relevant_value_length));
}
if (m_selection_start > relevant_value_length)
m_selection_start = relevant_value_length;
}
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-select
@ -263,13 +262,12 @@ Optional<WebIDL::UnsignedLong> FormAssociatedTextControlElement::selection_start
// 2. If there is no selection, return the code unit offset within the relevant value to the character that
// immediately follows the text entry cursor.
if (m_selection_start == m_selection_end) {
if (auto cursor = form_associated_element_to_html_element().document().cursor_position())
return cursor->offset();
return m_selection_start;
}
// 3. Return the code unit offset within the relevant value to the character that immediately follows the start of
// the selection.
return m_selection_start;
return m_selection_start < m_selection_end ? m_selection_start : m_selection_end;
}
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection:dom-textarea/input-selectionstart-2
@ -312,13 +310,12 @@ Optional<WebIDL::UnsignedLong> FormAssociatedTextControlElement::selection_end()
// 2. If there is no selection, return the code unit offset within the relevant value to the
// character that immediately follows the text entry cursor.
if (m_selection_start == m_selection_end) {
if (auto cursor = form_associated_element_to_html_element().document().cursor_position())
return cursor->offset();
return m_selection_start;
}
// 3. Return the code unit offset within the relevant value to the character that immediately
// follows the end of the selection.
return m_selection_end;
return m_selection_start < m_selection_end ? m_selection_end : m_selection_start;
}
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection:dom-textarea/input-selectionend-3
@ -589,4 +586,240 @@ void FormAssociatedTextControlElement::set_the_selection_range(Optional<WebIDL::
}
}
void FormAssociatedTextControlElement::handle_insert(String const& data)
{
auto text_node = form_associated_element_to_text_node();
if (!text_node || !text_node->is_editable())
return;
String data_for_insertion = data;
if (auto max_length = text_node->max_length(); max_length.has_value()) {
auto remaining_length = *max_length - text_node->data().code_points().length();
if (remaining_length < data.code_points().length()) {
data_for_insertion = MUST(data.substring_from_byte_offset(0, remaining_length));
}
}
auto selection_start = this->selection_start();
auto selection_end = this->selection_end();
if (!selection_start.has_value() || !selection_end.has_value()) {
return;
}
MUST(set_range_text(data_for_insertion, selection_start.value(), selection_end.value(), Bindings::SelectionMode::End));
text_node->invalidate_style(DOM::StyleInvalidationReason::EditingInsertion);
text_node->editable_text_node_owner()->did_edit_text_node();
}
void FormAssociatedTextControlElement::handle_delete(DeleteDirection direction)
{
auto text_node = form_associated_element_to_text_node();
if (!text_node || !text_node->is_editable())
return;
auto selection_start = this->selection_start();
auto selection_end = this->selection_end();
if (!selection_start.has_value() || !selection_end.has_value()) {
return;
}
if (selection_start == selection_end) {
if (direction == DeleteDirection::Backward) {
if (selection_start.value() > 0) {
MUST(set_range_text(MUST(String::from_utf8(""sv)), selection_start.value() - 1, selection_end.value(), Bindings::SelectionMode::End));
}
} else {
if (selection_start.value() < text_node->data().code_points().length()) {
MUST(set_range_text(MUST(String::from_utf8(""sv)), selection_start.value(), selection_end.value() + 1, Bindings::SelectionMode::End));
}
}
return;
}
MUST(set_range_text(MUST(String::from_utf8(""sv)), selection_start.value(), selection_end.value(), Bindings::SelectionMode::End));
}
void FormAssociatedTextControlElement::handle_return_key()
{
auto& html_element = form_associated_element_to_html_element();
if (is<HTMLInputElement>(html_element)) {
auto& input_element = static_cast<HTMLInputElement&>(html_element);
if (auto* form = input_element.form()) {
form->implicitly_submit_form().release_value_but_fixme_should_propagate_errors();
return;
}
input_element.commit_pending_changes();
}
}
void FormAssociatedTextControlElement::collapse_selection_to_offset(size_t position)
{
m_selection_start = position;
m_selection_end = position;
}
void FormAssociatedTextControlElement::selection_was_changed()
{
auto text_node = form_associated_element_to_text_node();
if (!text_node)
return;
auto* text_paintable = text_node->paintable();
if (!text_paintable)
return;
if (m_selection_start == m_selection_end) {
text_paintable->set_selected(false);
text_paintable->set_selection_state(Painting::Paintable::SelectionState::None);
text_node->document().reset_cursor_blink_cycle();
} else {
text_paintable->set_selected(true);
text_paintable->set_selection_state(Painting::Paintable::SelectionState::StartAndEnd);
}
text_paintable->set_needs_display();
}
void FormAssociatedTextControlElement::select_all()
{
auto text_node = form_associated_element_to_text_node();
if (!text_node)
return;
set_the_selection_range(0, text_node->length());
selection_was_changed();
}
void FormAssociatedTextControlElement::set_selection_anchor(JS::NonnullGCPtr<DOM::Node> anchor_node, size_t anchor_offset)
{
auto text_node = form_associated_element_to_text_node();
if (!text_node || anchor_node != text_node)
return;
collapse_selection_to_offset(anchor_offset);
selection_was_changed();
}
void FormAssociatedTextControlElement::set_selection_focus(JS::NonnullGCPtr<DOM::Node> focus_node, size_t focus_offset)
{
auto text_node = form_associated_element_to_text_node();
if (!text_node || focus_node != text_node)
return;
m_selection_end = focus_offset;
selection_was_changed();
}
void FormAssociatedTextControlElement::move_cursor_to_start(CollapseSelection collapse)
{
auto text_node = form_associated_element_to_text_node();
if (!text_node)
return;
if (collapse == CollapseSelection::Yes) {
collapse_selection_to_offset(0);
} else {
m_selection_end = 0;
}
selection_was_changed();
}
void FormAssociatedTextControlElement::move_cursor_to_end(CollapseSelection collapse)
{
auto text_node = form_associated_element_to_text_node();
if (!text_node)
return;
if (collapse == CollapseSelection::Yes) {
collapse_selection_to_offset(text_node->length());
} else {
m_selection_end = text_node->length();
}
selection_was_changed();
}
void FormAssociatedTextControlElement::increment_cursor_position_offset(CollapseSelection collapse)
{
auto const text_node = form_associated_element_to_text_node();
if (!text_node)
return;
if (auto offset = text_node->grapheme_segmenter().next_boundary(m_selection_end); offset.has_value()) {
if (collapse == CollapseSelection::Yes) {
collapse_selection_to_offset(*offset);
} else {
m_selection_end = *offset;
}
}
selection_was_changed();
}
void FormAssociatedTextControlElement::decrement_cursor_position_offset(CollapseSelection collapse)
{
auto const text_node = form_associated_element_to_text_node();
if (!text_node)
return;
if (auto offset = text_node->grapheme_segmenter().previous_boundary(m_selection_end); offset.has_value()) {
if (collapse == CollapseSelection::Yes) {
collapse_selection_to_offset(*offset);
} else {
m_selection_end = *offset;
}
}
selection_was_changed();
}
static bool should_continue_beyond_word(Utf8View const& word)
{
for (auto code_point : word) {
if (!Unicode::code_point_has_punctuation_general_category(code_point) && !Unicode::code_point_has_separator_general_category(code_point))
return false;
}
return true;
}
void FormAssociatedTextControlElement::increment_cursor_position_to_next_word(CollapseSelection collapse)
{
auto const text_node = form_associated_element_to_text_node();
if (!text_node)
return;
while (true) {
if (auto offset = text_node->word_segmenter().next_boundary(m_selection_end); offset.has_value()) {
auto word = text_node->data().code_points().substring_view(m_selection_end, *offset - m_selection_end);
if (collapse == CollapseSelection::Yes) {
collapse_selection_to_offset(*offset);
} else {
m_selection_end = *offset;
}
if (should_continue_beyond_word(word))
continue;
}
break;
}
selection_was_changed();
}
void FormAssociatedTextControlElement::decrement_cursor_position_to_previous_word(CollapseSelection collapse)
{
auto const text_node = form_associated_element_to_text_node();
if (!text_node)
return;
while (true) {
if (auto offset = text_node->word_segmenter().previous_boundary(m_selection_end); offset.has_value()) {
auto word = text_node->data().code_points().substring_view(m_selection_end, m_selection_end - *offset);
if (collapse == CollapseSelection::Yes) {
collapse_selection_to_offset(*offset);
} else {
m_selection_end = *offset;
}
if (should_continue_beyond_word(word))
continue;
}
break;
}
selection_was_changed();
}
JS::GCPtr<DOM::Position> FormAssociatedTextControlElement::cursor_position() const
{
auto const node = form_associated_element_to_text_node();
if (!node)
return nullptr;
if (m_selection_start == m_selection_end)
return DOM::Position::create(node->realm(), const_cast<DOM::Text&>(*node), m_selection_start);
return nullptr;
}
}

View file

@ -10,6 +10,7 @@
#include <AK/String.h>
#include <AK/WeakPtr.h>
#include <LibWeb/Bindings/HTMLFormElementPrototype.h>
#include <LibWeb/DOM/InputEventsTarget.h>
#include <LibWeb/Forward.h>
#include <LibWeb/WebIDL/Types.h>
@ -96,7 +97,7 @@ public:
HTMLElement const& form_associated_element_to_html_element() const { return const_cast<FormAssociatedElement&>(*this).form_associated_element_to_html_element(); }
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-form-reset-control
virtual void reset_algorithm() {};
virtual void reset_algorithm() { }
virtual void clear_algorithm();
@ -129,7 +130,8 @@ enum class SelectionSource {
DOM,
};
class FormAssociatedTextControlElement : public FormAssociatedElement {
class FormAssociatedTextControlElement : public FormAssociatedElement
, public InputEventsTarget {
public:
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
virtual String relevant_value() = 0;
@ -168,13 +170,34 @@ public:
bool has_scheduled_selectionchange_event() const { return m_has_scheduled_selectionchange_event; }
void set_scheduled_selectionchange_event(bool value) { m_has_scheduled_selectionchange_event = value; }
virtual JS::GCPtr<DOM::Text> form_associated_element_to_text_node() = 0;
virtual JS::GCPtr<DOM::Text const> form_associated_element_to_text_node() const { return const_cast<FormAssociatedTextControlElement&>(*this).form_associated_element_to_text_node(); }
virtual void handle_insert(String const&) override;
virtual void handle_delete(DeleteDirection) override;
virtual void handle_return_key() override;
virtual void select_all() override;
virtual void set_selection_anchor(JS::NonnullGCPtr<DOM::Node>, size_t offset) override;
virtual void set_selection_focus(JS::NonnullGCPtr<DOM::Node>, size_t offset) override;
virtual void move_cursor_to_start(CollapseSelection) override;
virtual void move_cursor_to_end(CollapseSelection) override;
virtual void increment_cursor_position_offset(CollapseSelection) override;
virtual void decrement_cursor_position_offset(CollapseSelection) override;
virtual void increment_cursor_position_to_next_word(CollapseSelection) override;
virtual void decrement_cursor_position_to_previous_word(CollapseSelection) override;
JS::GCPtr<DOM::Position> cursor_position() const;
protected:
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
void relevant_value_was_changed(JS::GCPtr<DOM::Text>);
void relevant_value_was_changed();
virtual void selection_was_changed([[maybe_unused]] size_t selection_start, [[maybe_unused]] size_t selection_end) { }
private:
void collapse_selection_to_offset(size_t);
void selection_was_changed();
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-selection
WebIDL::UnsignedLong m_selection_start { 0 };
WebIDL::UnsignedLong m_selection_end { 0 };

View file

@ -9,9 +9,11 @@
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/HTMLElementPrototype.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/EditingHostManager.h>
#include <LibWeb/DOM/ElementFactory.h>
#include <LibWeb/DOM/IDLEventListener.h>
#include <LibWeb/DOM/LiveNodeList.h>
#include <LibWeb/DOM/Position.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/CustomElements/CustomElementDefinition.h>
@ -887,6 +889,9 @@ void HTMLElement::did_receive_focus()
if (m_content_editable_state != ContentEditableState::True)
return;
auto editing_host = document().editing_host_manager();
editing_host->set_active_contenteditable_element(this);
DOM::Text* text = nullptr;
for_each_in_inclusive_subtree_of_type<DOM::Text>([&](auto& node) {
text = &node;
@ -894,10 +899,18 @@ void HTMLElement::did_receive_focus()
});
if (!text) {
document().set_cursor_position(DOM::Position::create(realm(), *this, 0));
editing_host->set_selection_anchor(*this, 0);
return;
}
document().set_cursor_position(DOM::Position::create(realm(), *text, text->length()));
editing_host->set_selection_anchor(*text, text->length());
}
void HTMLElement::did_lose_focus()
{
if (m_content_editable_state != ContentEditableState::True)
return;
document().editing_host_manager()->set_active_contenteditable_element(nullptr);
}
// https://html.spec.whatwg.org/multipage/interaction.html#dom-accesskeylabel

View file

@ -95,6 +95,7 @@ private:
// ^HTML::GlobalEventHandlers
virtual JS::GCPtr<DOM::EventTarget> global_event_handlers_to_event_target(FlyString const&) override { return *this; }
virtual void did_receive_focus() override;
virtual void did_lose_focus() override;
[[nodiscard]] String get_the_text_steps();
void append_rendered_text_fragment(StringView input);

View file

@ -429,7 +429,7 @@ WebIDL::ExceptionOr<void> HTMLInputElement::run_input_activation_behavior(DOM::E
return {};
}
void HTMLInputElement::did_edit_text_node(Badge<DOM::Document>)
void HTMLInputElement::did_edit_text_node()
{
// An input element's dirty value flag must be set to true whenever the user interacts with the control in a way that changes the value.
auto old_value = move(m_value);
@ -439,7 +439,7 @@ void HTMLInputElement::did_edit_text_node(Badge<DOM::Document>)
m_has_uncommitted_changes = true;
if (m_value != old_value)
relevant_value_was_changed(m_text_node);
relevant_value_was_changed();
update_placeholder_visibility();
@ -584,7 +584,7 @@ WebIDL::ExceptionOr<void> HTMLInputElement::set_value(String const& value)
// and the element has a text entry cursor position, move the text entry cursor position to the end of the
// text control, unselecting any selected text and resetting the selection direction to "none".
if (m_value != old_value) {
relevant_value_was_changed(m_text_node);
relevant_value_was_changed();
if (m_text_node) {
m_text_node->set_data(m_value);
@ -1178,11 +1178,11 @@ void HTMLInputElement::did_receive_focus()
return;
m_text_node->invalidate_style(DOM::StyleInvalidationReason::DidReceiveFocus);
if (auto* paintable = m_text_node->paintable())
paintable->set_selected(true);
if (m_placeholder_text_node)
m_placeholder_text_node->invalidate_style(DOM::StyleInvalidationReason::DidReceiveFocus);
if (auto cursor = document().cursor_position(); !cursor || m_text_node != cursor->node())
document().set_cursor_position(DOM::Position::create(realm(), *m_text_node, m_text_node->length()));
}
void HTMLInputElement::did_lose_focus()
@ -1190,6 +1190,9 @@ void HTMLInputElement::did_lose_focus()
if (m_text_node)
m_text_node->invalidate_style(DOM::StyleInvalidationReason::DidLoseFocus);
if (auto* paintable = m_text_node->paintable())
paintable->set_selected(false);
if (m_placeholder_text_node)
m_placeholder_text_node->invalidate_style(DOM::StyleInvalidationReason::DidLoseFocus);
@ -1232,7 +1235,7 @@ void HTMLInputElement::form_associated_element_attribute_changed(FlyString const
}
if (m_value != old_value)
relevant_value_was_changed(m_text_node);
relevant_value_was_changed();
update_shadow_tree();
}
@ -1303,7 +1306,6 @@ void HTMLInputElement::type_attribute_changed(TypeAttributeState old_state, Type
// 9. If previouslySelectable is false and nowSelectable is true, set the element's text entry cursor position to the
// beginning of the text control, and set its selection direction to "none".
if (!previously_selectable && now_selectable) {
document().set_cursor_position(DOM::Position::create(realm(), *m_text_node, 0));
set_selection_direction(OptionalNone {});
}
}
@ -1539,7 +1541,7 @@ void HTMLInputElement::reset_algorithm()
m_value = value_sanitization_algorithm(m_value);
if (m_value != old_value)
relevant_value_was_changed(m_text_node);
relevant_value_was_changed();
if (m_text_node) {
m_text_node->set_data(m_value);
@ -1575,7 +1577,7 @@ void HTMLInputElement::clear_algorithm()
user_interaction_did_change_input_value();
if (m_value != old_value)
relevant_value_was_changed(m_text_node);
relevant_value_was_changed();
if (m_text_node) {
m_text_node->set_data(m_value);
@ -2585,15 +2587,4 @@ HTMLInputElement::ValueAttributeMode HTMLInputElement::value_attribute_mode() co
return value_attribute_mode_for_type_state(type_state());
}
void HTMLInputElement::selection_was_changed(size_t selection_start, size_t selection_end)
{
if (!m_text_node || !document().cursor_position() || document().cursor_position()->node() != m_text_node)
return;
document().set_cursor_position(DOM::Position::create(realm(), *m_text_node, selection_end));
if (auto selection = document().get_selection())
MUST(selection->set_base_and_extent(*m_text_node, selection_start, *m_text_node, selection_end));
}
}

View file

@ -151,7 +151,7 @@ public:
WebIDL::ExceptionOr<void> show_picker();
// ^DOM::EditableTextNodeOwner
virtual void did_edit_text_node(Badge<DOM::Document>) override;
virtual void did_edit_text_node() override;
// ^EventTarget
// https://html.spec.whatwg.org/multipage/interaction.html#the-tabindex-attribute:the-input-element
@ -216,8 +216,7 @@ public:
Optional<String> selection_direction_binding() { return selection_direction(); }
protected:
void selection_was_changed(size_t selection_start, size_t selection_end) override;
virtual JS::GCPtr<DOM::Text> form_associated_element_to_text_node() override { return m_text_node; }
private:
HTMLInputElement(DOM::Document&, DOM::QualifiedName);

View file

@ -22,6 +22,7 @@
#include <LibWeb/HTML/Numbers.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/Painting/Paintable.h>
#include <LibWeb/Selection/Selection.h>
namespace Web::HTML {
@ -74,11 +75,11 @@ void HTMLTextAreaElement::did_receive_focus()
return;
m_text_node->invalidate_style(DOM::StyleInvalidationReason::DidReceiveFocus);
if (auto* paintable = m_text_node->paintable())
paintable->set_selected(true);
if (m_placeholder_text_node)
m_placeholder_text_node->invalidate_style(DOM::StyleInvalidationReason::DidReceiveFocus);
if (auto cursor = document().cursor_position(); !cursor || m_text_node != cursor->node())
document().set_cursor_position(DOM::Position::create(realm(), *m_text_node, 0));
}
void HTMLTextAreaElement::did_lose_focus()
@ -86,6 +87,9 @@ void HTMLTextAreaElement::did_lose_focus()
if (m_text_node)
m_text_node->invalidate_style(DOM::StyleInvalidationReason::DidLoseFocus);
if (auto* paintable = m_text_node->paintable())
paintable->set_selected(false);
if (m_placeholder_text_node)
m_placeholder_text_node->invalidate_style(DOM::StyleInvalidationReason::DidLoseFocus);
@ -206,7 +210,7 @@ void HTMLTextAreaElement::set_raw_value(String value)
m_api_value.clear();
if (m_raw_value != old_raw_value)
relevant_value_was_changed(m_text_node);
relevant_value_was_changed();
}
// https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element:concept-fe-api-value-3
@ -448,7 +452,7 @@ void HTMLTextAreaElement::form_associated_element_attribute_changed(FlyString co
}
}
void HTMLTextAreaElement::did_edit_text_node(Badge<DOM::Document>)
void HTMLTextAreaElement::did_edit_text_node()
{
VERIFY(m_text_node);
set_raw_value(m_text_node->data());
@ -474,15 +478,4 @@ void HTMLTextAreaElement::queue_firing_input_event()
});
}
void HTMLTextAreaElement::selection_was_changed(size_t selection_start, size_t selection_end)
{
if (!m_text_node || !document().cursor_position() || document().cursor_position()->node() != m_text_node)
return;
document().set_cursor_position(DOM::Position::create(realm(), *m_text_node, selection_end));
if (auto selection = document().get_selection())
MUST(selection->set_base_and_extent(*m_text_node, selection_start, *m_text_node, selection_end));
}
}

View file

@ -38,7 +38,7 @@ public:
}
// ^DOM::EditableTextNodeOwner
virtual void did_edit_text_node(Badge<DOM::Document>) override;
virtual void did_edit_text_node() override;
// ^EventTarget
// https://html.spec.whatwg.org/multipage/interaction.html#the-tabindex-attribute:the-textarea-element
@ -123,8 +123,7 @@ public:
void set_dirty_value_flag(Badge<FormAssociatedElement>, bool flag) { m_dirty_value = flag; }
protected:
void selection_was_changed(size_t selection_start, size_t selection_end) override;
virtual JS::GCPtr<DOM::Text> form_associated_element_to_text_node() override { return m_text_node; }
private:
HTMLTextAreaElement(DOM::Document&, DOM::QualifiedName);

View file

@ -2189,12 +2189,8 @@ void Navigable::select_all()
if (!selection)
return;
if (auto position = document->cursor_position(); position && position->node()->is_editable()) {
auto& node = *position->node();
auto node_length = node.length();
(void)selection->set_base_and_extent(node, 0, node, node_length);
document->set_cursor_position(DOM::Position::create(document->realm(), node, node_length));
if (auto target = document->active_input_events_target()) {
target->select_all();
} else if (auto* body = document->body()) {
(void)selection->select_all_children(*body);
}