diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/Page/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibWeb/Page/BUILD.gn
index a84a192c2a8..07f02692c61 100644
--- a/Meta/gn/secondary/Userland/Libraries/LibWeb/Page/BUILD.gn
+++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/Page/BUILD.gn
@@ -3,7 +3,6 @@ source_set("Page") {
deps = [ "//Userland/Libraries/LibWeb:all_generated" ]
sources = [
"DragAndDropEventHandler.cpp",
- "EditEventHandler.cpp",
"EventHandler.cpp",
"InputEvent.cpp",
"Page.cpp",
diff --git a/Tests/LibWeb/Text/expected/Editing/move-cursor-using-selection-api.txt b/Tests/LibWeb/Text/expected/Editing/move-cursor-using-selection-api.txt
new file mode 100644
index 00000000000..8add6ac532b
--- /dev/null
+++ b/Tests/LibWeb/Text/expected/Editing/move-cursor-using-selection-api.txt
@@ -0,0 +1 @@
+helllo
diff --git a/Tests/LibWeb/Text/input/Editing/move-cursor-using-selection-api.html b/Tests/LibWeb/Text/input/Editing/move-cursor-using-selection-api.html
new file mode 100644
index 00000000000..5abfdc282c8
--- /dev/null
+++ b/Tests/LibWeb/Text/input/Editing/move-cursor-using-selection-api.html
@@ -0,0 +1,11 @@
+
+
heo
+
diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt
index 882b87f560a..e238752c79d 100644
--- a/Userland/Libraries/LibWeb/CMakeLists.txt
+++ b/Userland/Libraries/LibWeb/CMakeLists.txt
@@ -177,6 +177,7 @@ set(SOURCES
DOM/DocumentLoading.cpp
DOM/DocumentObserver.cpp
DOM/DocumentType.cpp
+ DOM/EditingHostManager.cpp
DOM/Element.cpp
DOM/ElementFactory.cpp
DOM/Event.cpp
@@ -567,7 +568,6 @@ set(SOURCES
NavigationTiming/PerformanceNavigation.cpp
NavigationTiming/PerformanceTiming.cpp
Page/DragAndDropEventHandler.cpp
- Page/EditEventHandler.cpp
Page/EventHandler.cpp
Page/InputEvent.cpp
Page/Page.cpp
diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp
index 27467bccd52..88c62bda636 100644
--- a/Userland/Libraries/LibWeb/DOM/Document.cpp
+++ b/Userland/Libraries/LibWeb/DOM/Document.cpp
@@ -49,12 +49,15 @@
#include
#include
#include
+#include
#include
#include
#include
#include
+#include
#include
#include
+#include
#include
#include
#include
@@ -87,10 +90,12 @@
#include
#include
#include
+#include
#include
#include
#include
#include
+#include
#include
#include
#include
@@ -382,6 +387,7 @@ Document::Document(JS::Realm& realm, const URL::URL& url, TemporaryDocumentForFr
, m_style_computer(make(*this))
, m_url(url)
, m_temporary_document_for_fragment_parsing(temporary_document_for_fragment_parsing)
+ , m_editing_host_manager(EditingHostManager::create(realm, *this))
{
m_legacy_platform_object_flags = PlatformObject::LegacyPlatformObjectFlags {
.supports_named_properties = true,
@@ -389,10 +395,11 @@ Document::Document(JS::Realm& realm, const URL::URL& url, TemporaryDocumentForFr
};
m_cursor_blink_timer = Core::Timer::create_repeating(500, [this] {
- if (!m_cursor_position)
+ auto cursor_position = this->cursor_position();
+ if (!cursor_position)
return;
- auto node = m_cursor_position->node();
+ auto node = cursor_position->node();
if (!node)
return;
@@ -522,7 +529,7 @@ void Document::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_top_layer_elements);
visitor.visit(m_top_layer_pending_removals);
visitor.visit(m_console_client);
- visitor.visit(m_cursor_position);
+ visitor.visit(m_editing_host_manager);
}
// https://w3c.github.io/selection-api/#dom-document-getselection
@@ -5512,77 +5519,49 @@ JS::NonnullGCPtr Document::parse_html_unsafe(JS::VM& vm, StringView ht
return document;
}
-void Document::set_cursor_position(JS::NonnullGCPtr position)
+InputEventsTarget* Document::active_input_events_target()
{
- if (m_cursor_position && m_cursor_position->equals(position))
- return;
+ auto* focused_element = this->focused_element();
+ if (!focused_element)
+ return {};
- if (m_cursor_position && m_cursor_position->node()->paintable())
- m_cursor_position->node()->paintable()->set_needs_display();
-
- m_cursor_position = position;
-
- if (m_cursor_position && m_cursor_position->node()->paintable())
- m_cursor_position->node()->paintable()->set_needs_display();
-
- reset_cursor_blink_cycle();
+ if (is(*focused_element))
+ return static_cast(focused_element);
+ if (is(*focused_element))
+ return static_cast(focused_element);
+ if (is(*focused_element) && static_cast(focused_element)->is_editable())
+ return m_editing_host_manager;
+ return nullptr;
}
-bool Document::increment_cursor_position_offset()
+JS::GCPtr Document::cursor_position() const
{
- if (!m_cursor_position->increment_offset())
- return false;
-
- reset_cursor_blink_cycle();
- return true;
-}
-
-bool Document::decrement_cursor_position_offset()
-{
- if (!m_cursor_position->decrement_offset())
- return false;
-
- reset_cursor_blink_cycle();
- return true;
-}
-
-bool Document::increment_cursor_position_to_next_word()
-{
- if (!m_cursor_position->increment_offset_to_next_word())
- return false;
-
- reset_cursor_blink_cycle();
- return true;
-}
-
-bool Document::decrement_cursor_position_to_previous_word()
-{
- if (!m_cursor_position->decrement_offset_to_previous_word())
- return false;
-
- reset_cursor_blink_cycle();
- return true;
-}
-
-void Document::user_did_edit_document_text(Badge)
-{
- reset_cursor_blink_cycle();
-
- if (m_cursor_position && is(*m_cursor_position->node())) {
- auto& text_node = static_cast(*m_cursor_position->node());
-
- if (auto* text_node_owner = text_node.editable_text_node_owner())
- text_node_owner->did_edit_text_node({});
+ auto const* focused_element = this->focused_element();
+ if (!focused_element) {
+ return nullptr;
}
+
+ Optional target {};
+ if (is(*focused_element))
+ target = static_cast(*focused_element);
+ else if (is(*focused_element))
+ target = static_cast(*focused_element);
+
+ if (target.has_value()) {
+ return target->cursor_position();
+ }
+
+ if (is(*focused_element) && static_cast(focused_element)->is_editable()) {
+ return m_selection->cursor_position();
+ }
+
+ return nullptr;
}
void Document::reset_cursor_blink_cycle()
{
m_cursor_blink_state = true;
m_cursor_blink_timer->restart();
-
- if (m_cursor_position && m_cursor_position->node()->paintable())
- m_cursor_position->node()->paintable()->set_needs_display();
}
JS::GCPtr Document::cached_navigable()
diff --git a/Userland/Libraries/LibWeb/DOM/Document.h b/Userland/Libraries/LibWeb/DOM/Document.h
index 30d6c5d3c30..0687b2812a0 100644
--- a/Userland/Libraries/LibWeb/DOM/Document.h
+++ b/Userland/Libraries/LibWeb/DOM/Document.h
@@ -700,16 +700,11 @@ public:
void set_console_client(JS::GCPtr console_client) { m_console_client = console_client; }
JS::GCPtr console_client() const { return m_console_client; }
- JS::GCPtr cursor_position() const { return m_cursor_position; }
- void set_cursor_position(JS::NonnullGCPtr);
- bool increment_cursor_position_offset();
- bool decrement_cursor_position_offset();
- bool increment_cursor_position_to_next_word();
- bool decrement_cursor_position_to_previous_word();
+ InputEventsTarget* active_input_events_target();
+ JS::GCPtr cursor_position() const;
bool cursor_blink_state() const { return m_cursor_blink_state; }
- void user_did_edit_document_text(Badge);
// Cached pointer to the last known node navigable.
// If this document is currently the "active document" of the cached navigable, the cache is still valid.
JS::GCPtr cached_navigable();
@@ -746,6 +741,10 @@ public:
[[nodiscard]] WebIDL::CallbackType* onvisibilitychange();
void set_onvisibilitychange(WebIDL::CallbackType*);
+ void reset_cursor_blink_cycle();
+
+ JS::NonnullGCPtr editing_host_manager() const { return *m_editing_host_manager; }
+
protected:
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
@@ -773,8 +772,6 @@ private:
void dispatch_events_for_animation_if_necessary(JS::NonnullGCPtr);
- void reset_cursor_blink_cycle();
-
JS::NonnullGCPtr m_page;
OwnPtr m_style_computer;
JS::GCPtr m_style_sheets;
@@ -1014,7 +1011,6 @@ private:
JS::GCPtr m_console_client;
- JS::GCPtr m_cursor_position;
RefPtr m_cursor_blink_timer;
bool m_cursor_blink_state { false };
@@ -1030,6 +1026,8 @@ private:
mutable OwnPtr m_grapheme_segmenter;
mutable OwnPtr m_word_segmenter;
+
+ JS::NonnullGCPtr m_editing_host_manager;
};
template<>
diff --git a/Userland/Libraries/LibWeb/DOM/EditingHostManager.cpp b/Userland/Libraries/LibWeb/DOM/EditingHostManager.cpp
new file mode 100644
index 00000000000..a5161be2c99
--- /dev/null
+++ b/Userland/Libraries/LibWeb/DOM/EditingHostManager.cpp
@@ -0,0 +1,257 @@
+/*
+ * Copyright (c) 2024, Aliaksandr Kalenik
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Web::DOM {
+
+JS_DEFINE_ALLOCATOR(EditingHostManager);
+
+void EditingHostManager::handle_insert(String const& data)
+{
+ auto selection = m_document->get_selection();
+
+ auto selection_range = selection->range();
+ if (!selection_range) {
+ return;
+ }
+
+ auto node = selection->anchor_node();
+ if (!node || !node->is_editable()) {
+ return;
+ }
+
+ if (!is(*node)) {
+ auto& realm = node->realm();
+ auto text = realm.heap().allocate(realm, 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);
+}
+
+void EditingHostManager::select_all()
+{
+ if (!m_active_contenteditable_element) {
+ return;
+ }
+ auto selection = m_document->get_selection();
+ if (!selection->anchor_node() || !selection->focus_node()) {
+ return;
+ }
+ MUST(selection->set_base_and_extent(*selection->anchor_node(), 0, *selection->focus_node(), selection->focus_node()->length()));
+}
+
+void EditingHostManager::set_selection_anchor(JS::NonnullGCPtr anchor_node, size_t anchor_offset)
+{
+ auto selection = m_document->get_selection();
+ MUST(selection->collapse(*anchor_node, anchor_offset));
+ m_document->reset_cursor_blink_cycle();
+}
+
+void EditingHostManager::set_selection_focus(JS::NonnullGCPtr focus_node, size_t focus_offset)
+{
+ if (!m_active_contenteditable_element || !m_active_contenteditable_element->is_ancestor_of(*focus_node))
+ return;
+ auto selection = m_document->get_selection();
+ if (!selection->anchor_node())
+ return;
+ MUST(selection->set_base_and_extent(*selection->anchor_node(), selection->anchor_offset(), *focus_node, focus_offset));
+ m_document->reset_cursor_blink_cycle();
+}
+
+void EditingHostManager::move_cursor_to_start(CollapseSelection collapse)
+{
+ auto selection = m_document->get_selection();
+ auto node = selection->anchor_node();
+ if (!node || !is(*node))
+ return;
+
+ if (collapse == CollapseSelection::Yes) {
+ MUST(selection->collapse(node, 0));
+ m_document->reset_cursor_blink_cycle();
+ return;
+ }
+ MUST(selection->set_base_and_extent(*node, selection->anchor_offset(), *node, 0));
+}
+
+void EditingHostManager::move_cursor_to_end(CollapseSelection collapse)
+{
+ auto selection = m_document->get_selection();
+ auto node = selection->anchor_node();
+ if (!node || !is(*node))
+ return;
+
+ if (collapse == CollapseSelection::Yes) {
+ m_document->reset_cursor_blink_cycle();
+ MUST(selection->collapse(node, node->length()));
+ return;
+ }
+ MUST(selection->set_base_and_extent(*node, selection->anchor_offset(), *node, node->length()));
+}
+
+void EditingHostManager::increment_cursor_position_offset(CollapseSelection collapse)
+{
+ auto selection = m_document->get_selection();
+ auto node = selection->anchor_node();
+ if (!node || !is(*node))
+ return;
+
+ auto& text_node = static_cast(*node);
+ if (auto offset = text_node.grapheme_segmenter().next_boundary(selection->focus_offset()); offset.has_value()) {
+ if (collapse == CollapseSelection::Yes) {
+ MUST(selection->collapse(*node, *offset));
+ m_document->reset_cursor_blink_cycle();
+ } else {
+ MUST(selection->set_base_and_extent(*node, selection->anchor_offset(), *node, *offset));
+ }
+ }
+}
+
+void EditingHostManager::decrement_cursor_position_offset(CollapseSelection collapse)
+{
+ auto selection = m_document->get_selection();
+ auto node = selection->anchor_node();
+ if (!node || !is(*node)) {
+ return;
+ }
+
+ auto& text_node = static_cast(*node);
+ if (auto offset = text_node.grapheme_segmenter().previous_boundary(selection->focus_offset()); offset.has_value()) {
+ if (collapse == CollapseSelection::Yes) {
+ MUST(selection->collapse(*node, *offset));
+ m_document->reset_cursor_blink_cycle();
+ } else {
+ MUST(selection->set_base_and_extent(*node, selection->anchor_offset(), *node, *offset));
+ }
+ }
+}
+
+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 EditingHostManager::increment_cursor_position_to_next_word(CollapseSelection collapse)
+{
+ auto selection = m_document->get_selection();
+ auto node = selection->anchor_node();
+ if (!node || !is(*node)) {
+ return;
+ }
+
+ auto& text_node = static_cast(*node);
+
+ while (true) {
+ auto focus_offset = selection->focus_offset();
+ if (focus_offset == text_node.data().bytes_as_string_view().length()) {
+ return;
+ }
+
+ if (auto offset = text_node.word_segmenter().next_boundary(focus_offset); offset.has_value()) {
+ auto word = text_node.data().code_points().substring_view(focus_offset, *offset - focus_offset);
+ if (collapse == CollapseSelection::Yes) {
+ MUST(selection->collapse(node, *offset));
+ m_document->reset_cursor_blink_cycle();
+ } else {
+ MUST(selection->set_base_and_extent(*node, selection->anchor_offset(), *node, *offset));
+ }
+ if (should_continue_beyond_word(word))
+ continue;
+ }
+ break;
+ }
+}
+
+void EditingHostManager::decrement_cursor_position_to_previous_word(CollapseSelection collapse)
+{
+ auto selection = m_document->get_selection();
+ auto node = selection->anchor_node();
+ if (!node || !is(*node)) {
+ return;
+ }
+
+ auto& text_node = static_cast(*node);
+
+ while (true) {
+ auto focus_offset = selection->focus_offset();
+ if (auto offset = text_node.word_segmenter().previous_boundary(focus_offset); offset.has_value()) {
+ auto word = text_node.data().code_points().substring_view(focus_offset, focus_offset - *offset);
+ if (collapse == CollapseSelection::Yes) {
+ MUST(selection->collapse(node, *offset));
+ m_document->reset_cursor_blink_cycle();
+ } else {
+ MUST(selection->set_base_and_extent(*node, selection->anchor_offset(), *node, *offset));
+ }
+ if (should_continue_beyond_word(word))
+ continue;
+ }
+ break;
+ }
+}
+
+void EditingHostManager::handle_delete(DeleteDirection direction)
+{
+ auto selection = m_document->get_selection();
+ auto selection_range = selection->range();
+ if (!selection_range) {
+ return;
+ }
+
+ if (selection->is_collapsed()) {
+ auto node = selection->anchor_node();
+ if (!node || !is(*node)) {
+ return;
+ }
+
+ 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());
+}
+
+void EditingHostManager::handle_return_key()
+{
+ dbgln("FIXME: Implement EditingHostManager::handle_return_key()");
+}
+
+}
diff --git a/Userland/Libraries/LibWeb/DOM/EditingHostManager.h b/Userland/Libraries/LibWeb/DOM/EditingHostManager.h
new file mode 100644
index 00000000000..7d58bcd420e
--- /dev/null
+++ b/Userland/Libraries/LibWeb/DOM/EditingHostManager.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2024, Aliaksandr Kalenik
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Web::DOM {
+
+class EditingHostManager : public JS::Cell
+ , public InputEventsTarget {
+ JS_CELL(EditingHostManager, JS::Cell);
+ JS_DECLARE_ALLOCATOR(EditingHostManager);
+
+public:
+ [[nodiscard]] static JS::NonnullGCPtr create(JS::Realm& realm, JS::NonnullGCPtr document)
+ {
+ return realm.heap().allocate(realm, document);
+ }
+
+ 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, size_t offset) override;
+ virtual void set_selection_focus(JS::NonnullGCPtr, 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;
+
+ virtual void visit_edges(Cell::Visitor& visitor) override
+ {
+ Base::visit_edges(visitor);
+ visitor.visit(m_document);
+ visitor.visit(m_active_contenteditable_element);
+ }
+
+ void set_active_contenteditable_element(JS::GCPtr element)
+ {
+ m_active_contenteditable_element = element;
+ }
+
+ EditingHostManager(JS::NonnullGCPtr document)
+ : m_document(document)
+ {
+ }
+
+private:
+ JS::NonnullGCPtr m_document;
+ JS::GCPtr m_active_contenteditable_element;
+};
+
+}
diff --git a/Userland/Libraries/LibWeb/DOM/InputEventsTarget.h b/Userland/Libraries/LibWeb/DOM/InputEventsTarget.h
new file mode 100644
index 00000000000..bab2a1b3b62
--- /dev/null
+++ b/Userland/Libraries/LibWeb/DOM/InputEventsTarget.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2024, Aliaksandr Kalenik
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include
+
+namespace Web {
+
+class InputEventsTarget {
+public:
+ virtual ~InputEventsTarget() = default;
+
+ virtual void handle_insert(String const&) = 0;
+ virtual void handle_return_key() = 0;
+
+ enum class DeleteDirection {
+ Backward,
+ Forward,
+ };
+ virtual void handle_delete(DeleteDirection) = 0;
+
+ virtual void select_all() = 0;
+ virtual void set_selection_anchor(JS::NonnullGCPtr, size_t offset) = 0;
+ virtual void set_selection_focus(JS::NonnullGCPtr, size_t offset) = 0;
+ enum class CollapseSelection {
+ No,
+ Yes,
+ };
+ virtual void move_cursor_to_start(CollapseSelection) = 0;
+ virtual void move_cursor_to_end(CollapseSelection) = 0;
+ virtual void increment_cursor_position_offset(CollapseSelection) = 0;
+ virtual void decrement_cursor_position_offset(CollapseSelection) = 0;
+ virtual void increment_cursor_position_to_next_word(CollapseSelection) = 0;
+ virtual void decrement_cursor_position_to_previous_word(CollapseSelection) = 0;
+};
+
+}
diff --git a/Userland/Libraries/LibWeb/DOM/Position.cpp b/Userland/Libraries/LibWeb/DOM/Position.cpp
index 390a6e9eb68..61d0d774d61 100644
--- a/Userland/Libraries/LibWeb/DOM/Position.cpp
+++ b/Userland/Libraries/LibWeb/DOM/Position.cpp
@@ -5,9 +5,6 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
-#include
-#include
-#include
#include
#include
#include
@@ -35,100 +32,4 @@ ErrorOr Position::to_string() const
return String::formatted("DOM::Position({} ({})), {})", node()->node_name(), node().ptr(), offset());
}
-bool Position::increment_offset()
-{
- if (!is(*m_node))
- return false;
-
- auto& node = verify_cast(*m_node);
-
- if (auto offset = node.grapheme_segmenter().next_boundary(m_offset); offset.has_value()) {
- m_offset = *offset;
- return true;
- }
-
- // NOTE: Already at end of current node.
- return false;
-}
-
-bool Position::decrement_offset()
-{
- if (!is(*m_node))
- return false;
-
- auto& node = verify_cast(*m_node);
-
- if (auto offset = node.grapheme_segmenter().previous_boundary(m_offset); offset.has_value()) {
- m_offset = *offset;
- return true;
- }
-
- // NOTE: Already at beginning of current node.
- return false;
-}
-
-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;
-}
-
-bool Position::increment_offset_to_next_word()
-{
- if (!is(*m_node) || offset_is_at_end_of_node())
- return false;
-
- auto& node = static_cast(*m_node);
-
- while (true) {
- if (auto offset = node.word_segmenter().next_boundary(m_offset); offset.has_value()) {
- auto word = node.data().code_points().substring_view(m_offset, *offset - m_offset);
- m_offset = *offset;
-
- if (should_continue_beyond_word(word))
- continue;
- }
-
- break;
- }
-
- return true;
-}
-
-bool Position::decrement_offset_to_previous_word()
-{
- if (!is(*m_node) || m_offset == 0)
- return false;
-
- auto& node = static_cast(*m_node);
-
- while (true) {
- if (auto offset = node.word_segmenter().previous_boundary(m_offset); offset.has_value()) {
- auto word = node.data().code_points().substring_view(*offset, m_offset - *offset);
- m_offset = *offset;
-
- if (should_continue_beyond_word(word))
- continue;
- }
-
- break;
- }
-
- return true;
-}
-
-bool Position::offset_is_at_end_of_node() const
-{
- if (!is(*m_node))
- return false;
-
- auto& node = verify_cast(*m_node);
- auto text = node.data();
- return m_offset == text.bytes_as_string_view().length();
-}
-
}
diff --git a/Userland/Libraries/LibWeb/DOM/Position.h b/Userland/Libraries/LibWeb/DOM/Position.h
index 25fae3ef4f8..e05cfa5476b 100644
--- a/Userland/Libraries/LibWeb/DOM/Position.h
+++ b/Userland/Libraries/LibWeb/DOM/Position.h
@@ -28,16 +28,8 @@ public:
JS::GCPtr node() { return m_node; }
JS::GCPtr node() const { return m_node; }
- void set_node(JS::NonnullGCPtr node) { m_node = node; }
unsigned offset() const { return m_offset; }
- bool offset_is_at_end_of_node() const;
- void set_offset(unsigned value) { m_offset = value; }
- bool increment_offset();
- bool decrement_offset();
-
- bool increment_offset_to_next_word();
- bool decrement_offset_to_previous_word();
bool equals(JS::NonnullGCPtr other) const
{
diff --git a/Userland/Libraries/LibWeb/DOM/Range.cpp b/Userland/Libraries/LibWeb/DOM/Range.cpp
index 268cca7f7a1..17b985e2045 100644
--- a/Userland/Libraries/LibWeb/DOM/Range.cpp
+++ b/Userland/Libraries/LibWeb/DOM/Range.cpp
@@ -1257,7 +1257,7 @@ JS::NonnullGCPtr Range::get_client_rects()
auto fragments = paintable_lines.fragments();
auto const& font = paintable->layout_node().first_available_font();
for (auto frag = fragments.begin(); frag != fragments.end(); frag++) {
- auto rect = frag->range_rect(font, *this);
+ auto rect = frag->range_rect(font, start_offset(), end_offset());
if (rect.is_empty())
continue;
rects.append(Geometry::DOMRect::create(realm(),
diff --git a/Userland/Libraries/LibWeb/DOM/Text.h b/Userland/Libraries/LibWeb/DOM/Text.h
index cc5f2ef38d3..7ab72d141d4 100644
--- a/Userland/Libraries/LibWeb/DOM/Text.h
+++ b/Userland/Libraries/LibWeb/DOM/Text.h
@@ -16,7 +16,7 @@ namespace Web::DOM {
class EditableTextNodeOwner {
public:
virtual ~EditableTextNodeOwner() = default;
- virtual void did_edit_text_node(Badge) = 0;
+ virtual void did_edit_text_node() = 0;
};
class Text
diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h
index 96ed5443da3..359c1209a89 100644
--- a/Userland/Libraries/LibWeb/Forward.h
+++ b/Userland/Libraries/LibWeb/Forward.h
@@ -13,8 +13,8 @@
#include
namespace Web {
+class InputEventsTarget;
class DragAndDropEventHandler;
-class EditEventHandler;
class EventHandler;
class LoadRequest;
class Page;
@@ -270,6 +270,7 @@ class DOMTokenList;
class Element;
class Event;
class EventHandler;
+class EditingHostManager;
class EventTarget;
class HTMLCollection;
class HTMLFormControlsCollection;
diff --git a/Userland/Libraries/LibWeb/HTML/BrowsingContext.h b/Userland/Libraries/LibWeb/HTML/BrowsingContext.h
index 12a022d391f..2eff98f111b 100644
--- a/Userland/Libraries/LibWeb/HTML/BrowsingContext.h
+++ b/Userland/Libraries/LibWeb/HTML/BrowsingContext.h
@@ -16,7 +16,6 @@
#include
#include
#include
-#include
#include
#include
#include
diff --git a/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.cpp b/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.cpp
index f37476079ba..122fcacc376 100644
--- a/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.cpp
+++ b/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.cpp
@@ -6,8 +6,11 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
+#include
+#include
#include
#include
+#include
#include
#include
#include
@@ -17,6 +20,7 @@
#include
#include
#include
+#include
namespace Web::HTML {
@@ -196,7 +200,7 @@ WebIDL::ExceptionOr 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 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::GCPtrnode() == 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 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 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(Optionalis_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(html_element)) {
+ auto& input_element = static_cast(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 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 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 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(*node), m_selection_start);
+ return nullptr;
+}
+
}
diff --git a/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.h b/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.h
index de88c022559..b0c52096095 100644
--- a/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.h
+++ b/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.h
@@ -10,6 +10,7 @@
#include
#include
#include
+#include
#include
#include
@@ -96,7 +97,7 @@ public:
HTMLElement const& form_associated_element_to_html_element() const { return const_cast(*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 form_associated_element_to_text_node() = 0;
+ virtual JS::GCPtr form_associated_element_to_text_node() const { return const_cast(*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, size_t offset) override;
+ virtual void set_selection_focus(JS::NonnullGCPtr, 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 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);
+ 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 };
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLElement.cpp
index 0cbe5c6b2b8..f42a308a12d 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLElement.cpp
+++ b/Userland/Libraries/LibWeb/HTML/HTMLElement.cpp
@@ -9,9 +9,11 @@
#include
#include
#include
+#include
#include
#include
#include
+#include
#include
#include
#include
@@ -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([&](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
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLElement.h b/Userland/Libraries/LibWeb/HTML/HTMLElement.h
index ea511b4d115..bf21a04565a 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLElement.h
+++ b/Userland/Libraries/LibWeb/HTML/HTMLElement.h
@@ -95,6 +95,7 @@ private:
// ^HTML::GlobalEventHandlers
virtual JS::GCPtr 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);
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp
index d34793399b4..f647f3dc195 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp
+++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp
@@ -429,7 +429,7 @@ WebIDL::ExceptionOr HTMLInputElement::run_input_activation_behavior(DOM::E
return {};
}
-void HTMLInputElement::did_edit_text_node(Badge)
+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)
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 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));
-}
-
}
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h
index 6789e31e40d..6317e33ca73 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h
+++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h
@@ -151,7 +151,7 @@ public:
WebIDL::ExceptionOr show_picker();
// ^DOM::EditableTextNodeOwner
- virtual void did_edit_text_node(Badge) 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 selection_direction_binding() { return selection_direction(); }
-protected:
- void selection_was_changed(size_t selection_start, size_t selection_end) override;
+ virtual JS::GCPtr form_associated_element_to_text_node() override { return m_text_node; }
private:
HTMLInputElement(DOM::Document&, DOM::QualifiedName);
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp
index a9eb10ad9f1..bdef74113f1 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp
+++ b/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp
@@ -22,6 +22,7 @@
#include
#include
#include
+#include
#include
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)
+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));
-}
-
}
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.h b/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.h
index eea9c3811da..375a54203ad 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.h
+++ b/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.h
@@ -38,7 +38,7 @@ public:
}
// ^DOM::EditableTextNodeOwner
- virtual void did_edit_text_node(Badge) 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, bool flag) { m_dirty_value = flag; }
-protected:
- void selection_was_changed(size_t selection_start, size_t selection_end) override;
+ virtual JS::GCPtr form_associated_element_to_text_node() override { return m_text_node; }
private:
HTMLTextAreaElement(DOM::Document&, DOM::QualifiedName);
diff --git a/Userland/Libraries/LibWeb/HTML/Navigable.cpp b/Userland/Libraries/LibWeb/HTML/Navigable.cpp
index 56e3bd3b090..1b8ce929aea 100644
--- a/Userland/Libraries/LibWeb/HTML/Navigable.cpp
+++ b/Userland/Libraries/LibWeb/HTML/Navigable.cpp
@@ -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);
}
diff --git a/Userland/Libraries/LibWeb/Layout/LineBox.cpp b/Userland/Libraries/LibWeb/Layout/LineBox.cpp
index f1da7e523a1..545783f08e3 100644
--- a/Userland/Libraries/LibWeb/Layout/LineBox.cpp
+++ b/Userland/Libraries/LibWeb/Layout/LineBox.cpp
@@ -7,6 +7,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -45,9 +46,12 @@ void LineBox::trim_trailing_whitespace()
return;
// last_fragment cannot be null from here on down, as m_fragments is not empty.
last_fragment = &m_fragments.last();
- auto dom_node = last_fragment->layout_node().dom_node();
- if (dom_node && dom_node->is_editable() && dom_node->document().cursor_position())
- return;
+ auto const* dom_node = last_fragment->layout_node().dom_node();
+ if (dom_node) {
+ auto cursor_position = dom_node->document().cursor_position();
+ if (cursor_position && cursor_position->node() == dom_node)
+ return;
+ }
if (!should_trim(last_fragment))
return;
if (last_fragment->is_justifiable_whitespace()) {
diff --git a/Userland/Libraries/LibWeb/Page/EditEventHandler.cpp b/Userland/Libraries/LibWeb/Page/EditEventHandler.cpp
deleted file mode 100644
index 183bf97461f..00000000000
--- a/Userland/Libraries/LibWeb/Page/EditEventHandler.cpp
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright (c) 2020-2021, the SerenityOS developers.
- *
- * SPDX-License-Identifier: BSD-2-Clause
- */
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-namespace Web {
-
-void EditEventHandler::handle_delete_character_after(JS::NonnullGCPtr document, JS::NonnullGCPtr cursor_position)
-{
- auto& node = verify_cast(*cursor_position->node());
- auto& text = node.data();
-
- auto next_offset = node.grapheme_segmenter().next_boundary(cursor_position->offset());
- if (!next_offset.has_value()) {
- // FIXME: Move to the next node and delete the first character there.
- return;
- }
-
- StringBuilder builder;
- builder.append(text.bytes_as_string_view().substring_view(0, cursor_position->offset()));
- builder.append(text.bytes_as_string_view().substring_view(*next_offset));
- node.set_data(MUST(builder.to_string()));
-
- document->user_did_edit_document_text({});
-}
-
-// This method is quite convoluted but this is necessary to make editing feel intuitive.
-void EditEventHandler::handle_delete(JS::NonnullGCPtr document, DOM::Range& range)
-{
- auto* start = verify_cast(range.start_container());
- auto* end = verify_cast(range.end_container());
-
- if (start == end) {
- StringBuilder builder;
- builder.append(start->data().bytes_as_string_view().substring_view(0, range.start_offset()));
- builder.append(end->data().bytes_as_string_view().substring_view(range.end_offset()));
-
- start->set_data(MUST(builder.to_string()));
- } else {
- // Remove all the nodes that are fully enclosed in the range.
- HashTable queued_for_deletion;
- for (auto* node = start->next_in_pre_order(); node; node = node->next_in_pre_order()) {
- if (node == end)
- break;
-
- queued_for_deletion.set(node);
- }
- for (auto* parent = start->parent(); parent; parent = parent->parent())
- queued_for_deletion.remove(parent);
- for (auto* parent = end->parent(); parent; parent = parent->parent())
- queued_for_deletion.remove(parent);
- for (auto* node : queued_for_deletion)
- node->remove();
-
- // Join the parent nodes of start and end.
- DOM::Node *insert_after = start, *remove_from = end, *parent_of_end = end->parent();
- while (remove_from) {
- auto* next_sibling = remove_from->next_sibling();
-
- remove_from->remove();
- insert_after->parent()->insert_before(*remove_from, *insert_after);
-
- insert_after = remove_from;
- remove_from = next_sibling;
- }
- if (!parent_of_end->has_children()) {
- if (parent_of_end->parent())
- parent_of_end->remove();
- }
-
- // Join the start and end nodes.
- StringBuilder builder;
- builder.append(start->data().bytes_as_string_view().substring_view(0, range.start_offset()));
- builder.append(end->data().bytes_as_string_view().substring_view(range.end_offset()));
-
- start->set_data(MUST(builder.to_string()));
- end->remove();
- }
-
- document->user_did_edit_document_text({});
-}
-
-void EditEventHandler::handle_insert(JS::NonnullGCPtr document, JS::NonnullGCPtr position, u32 code_point)
-{
- StringBuilder builder;
- builder.append_code_point(code_point);
- handle_insert(document, position, MUST(builder.to_string()));
-}
-
-void EditEventHandler::handle_insert(JS::NonnullGCPtr document, JS::NonnullGCPtr position, String data)
-{
- if (is(*position->node())) {
- auto& node = verify_cast(*position->node());
-
- StringBuilder builder;
- builder.append(node.data().bytes_as_string_view().substring_view(0, position->offset()));
- builder.append(data);
- builder.append(node.data().bytes_as_string_view().substring_view(position->offset()));
-
- // Cut string by max length
- // FIXME: Cut by UTF-16 code units instead of raw bytes
- if (auto max_length = node.max_length(); max_length.has_value() && builder.string_view().length() > *max_length) {
- node.set_data(MUST(String::from_utf8(builder.string_view().substring_view(0, *max_length))));
- } else {
- node.set_data(MUST(builder.to_string()));
- }
- node.invalidate_style(DOM::StyleInvalidationReason::EditingInsertion);
- } else {
- auto& node = *position->node();
- auto& realm = node.realm();
- auto text = realm.heap().allocate(realm, node.document(), data);
- MUST(node.append_child(*text));
- position->set_node(text);
- position->set_offset(1);
- }
-
- document->user_did_edit_document_text({});
-}
-
-}
diff --git a/Userland/Libraries/LibWeb/Page/EditEventHandler.h b/Userland/Libraries/LibWeb/Page/EditEventHandler.h
deleted file mode 100644
index 6fe91d743fb..00000000000
--- a/Userland/Libraries/LibWeb/Page/EditEventHandler.h
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (c) 2020-2021, the SerenityOS developers.
- *
- * SPDX-License-Identifier: BSD-2-Clause
- */
-
-#pragma once
-
-#include
-#include
-#include
-#include
-
-namespace Web {
-
-class EditEventHandler {
-public:
- explicit EditEventHandler()
- {
- }
-
- ~EditEventHandler() = default;
-
- void handle_delete_character_after(JS::NonnullGCPtr, JS::NonnullGCPtr);
- void handle_delete(JS::NonnullGCPtr, DOM::Range&);
- void handle_insert(JS::NonnullGCPtr, JS::NonnullGCPtr, u32 code_point);
- void handle_insert(JS::NonnullGCPtr, JS::NonnullGCPtr, String);
-};
-
-}
diff --git a/Userland/Libraries/LibWeb/Page/EventHandler.cpp b/Userland/Libraries/LibWeb/Page/EventHandler.cpp
index 440d3076cf5..9fef3798d76 100644
--- a/Userland/Libraries/LibWeb/Page/EventHandler.cpp
+++ b/Userland/Libraries/LibWeb/Page/EventHandler.cpp
@@ -7,27 +7,22 @@
#include
#include
-#include