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 #include -#include #include #include -#include #include #include #include #include -#include #include -#include #include #include #include -#include #include #include #include #include +#include #include #include #include @@ -151,7 +146,6 @@ static CSSPixelPoint compute_mouse_event_offset(CSSPixelPoint position, Layout:: EventHandler::EventHandler(Badge, HTML::Navigable& navigable) : m_navigable(navigable) - , m_edit_event_handler(make()) , m_drag_and_drop_event_handler(make()) { } @@ -368,7 +362,7 @@ EventResult EventHandler::handle_mouseup(CSSPixelPoint viewport_position, CSSPix after_node_use: if (button == UIEvents::MouseButton::Primary) { m_in_mouse_selection = false; - update_selection_range_for_input_or_textarea(); + m_mouse_selection_target = nullptr; } return handled_event; } @@ -455,11 +449,24 @@ EventResult EventHandler::handle_mousedown(CSSPixelPoint viewport_position, CSSP } } - // If we didn't focus anything, place the document text cursor at the mouse position. - // FIXME: This is all rather strange. Find a better solution. - if (!focus_candidate || dom_node->is_editable()) { - auto& realm = document->realm(); - document->set_cursor_position(DOM::Position::create(realm, *dom_node, result->index_in_node)); + // When a user activates a click focusable focusable area, the user agent must run the focusing steps on the focusable area with focus trigger set to "click". + // Spec Note: Note that focusing is not an activation behavior, i.e. calling the click() method on an element or dispatching a synthetic click event on it won't cause the element to get focused. + if (focus_candidate) + HTML::run_focusing_steps(focus_candidate, nullptr, "click"sv); + else if (auto* focused_element = document->focused_element()) + HTML::run_unfocusing_steps(focused_element); + + auto target = document->active_input_events_target(); + if (target) { + m_in_mouse_selection = true; + m_mouse_selection_target = target; + if (modifiers & UIEvents::KeyModifier::Mod_Shift) { + target->set_selection_focus(*dom_node, result->index_in_node); + } else { + target->set_selection_anchor(*dom_node, result->index_in_node); + } + } else if (!focus_candidate) { + m_in_mouse_selection = true; if (auto selection = document->get_selection()) { auto anchor_node = selection->anchor_node(); if (anchor_node && modifiers & UIEvents::KeyModifier::Mod_Shift) { @@ -468,16 +475,7 @@ EventResult EventHandler::handle_mousedown(CSSPixelPoint viewport_position, CSSP (void)selection->set_base_and_extent(*dom_node, result->index_in_node, *dom_node, result->index_in_node); } } - update_selection_range_for_input_or_textarea(); - m_in_mouse_selection = true; } - - // When a user activates a click focusable focusable area, the user agent must run the focusing steps on the focusable area with focus trigger set to "click". - // Spec Note: Note that focusing is not an activation behavior, i.e. calling the click() method on an element or dispatching a synthetic click event on it won't cause the element to get focused. - if (focus_candidate) - HTML::run_focusing_steps(focus_candidate, nullptr, "click"sv); - else if (auto* focused_element = document->focused_element()) - HTML::run_unfocusing_steps(focused_element); } } } @@ -503,7 +501,6 @@ EventResult EventHandler::handle_mousemove(CSSPixelPoint viewport_position, CSSP return EventResult::Dropped; auto& document = *m_navigable->active_document(); - auto& realm = document.realm(); bool hovered_node_changed = false; bool is_hovering_link = false; @@ -583,23 +580,24 @@ EventResult EventHandler::handle_mousemove(CSSPixelPoint viewport_position, CSSP if (m_in_mouse_selection) { auto hit = paint_root()->hit_test(position, Painting::HitTestType::TextCursor); - auto should_set_cursor_position = true; - if (start_index.has_value() && hit.has_value() && hit->dom_node()) { - if (auto selection = document.get_selection()) { - auto anchor_node = selection->anchor_node(); - if (anchor_node) { - if (&anchor_node->root() == &hit->dom_node()->root()) - (void)selection->set_base_and_extent(*anchor_node, selection->anchor_offset(), *hit->paintable->dom_node(), hit->index_in_node); - else - should_set_cursor_position = false; - } else { - (void)selection->set_base_and_extent(*hit->paintable->dom_node(), hit->index_in_node, *hit->paintable->dom_node(), hit->index_in_node); - } + if (m_mouse_selection_target) { + if (hit.has_value()) { + m_mouse_selection_target->set_selection_focus(*hit->paintable->dom_node(), hit->index_in_node); } - if (should_set_cursor_position) - document.set_cursor_position(DOM::Position::create(realm, *hit->dom_node(), *start_index)); + } else { + if (start_index.has_value() && hit.has_value() && hit->dom_node()) { + if (auto selection = document.get_selection()) { + auto anchor_node = selection->anchor_node(); + if (anchor_node) { + if (&anchor_node->root() == &hit->dom_node()->root()) + (void)selection->set_base_and_extent(*anchor_node, selection->anchor_offset(), *hit->paintable->dom_node(), hit->index_in_node); + } else { + (void)selection->set_base_and_extent(*hit->paintable->dom_node(), hit->index_in_node, *hit->paintable->dom_node(), hit->index_in_node); + } + } - document.set_needs_display(); + document.set_needs_display(); + } } } } @@ -697,12 +695,13 @@ EventResult EventHandler::handle_doubleclick(CSSPixelPoint viewport_position, CS auto previous_boundary = hit_dom_node.word_segmenter().previous_boundary(result->index_in_node, Unicode::Segmenter::Inclusive::Yes).value_or(0); auto next_boundary = hit_dom_node.word_segmenter().next_boundary(result->index_in_node).value_or(hit_dom_node.length()); - auto& realm = node->document().realm(); - document.set_cursor_position(DOM::Position::create(realm, hit_dom_node, next_boundary)); - if (auto selection = node->document().get_selection()) { + auto target = document.active_input_events_target(); + if (target) { + target->set_selection_anchor(hit_dom_node, previous_boundary); + target->set_selection_focus(hit_dom_node, next_boundary); + } else if (auto selection = node->document().get_selection()) { (void)selection->set_base_and_extent(hit_dom_node, previous_boundary, hit_dom_node, next_boundary); } - update_selection_range_for_input_or_textarea(); } } @@ -946,72 +945,24 @@ 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. } - auto& realm = document->realm(); - - auto selection = document->get_selection(); - auto range = [&]() -> JS::GCPtr { - if (selection) { - if (auto range = selection->range(); range && !range->collapsed()) - return range; - } - return nullptr; - }(); - - if (selection && range && range->start_container()->is_editable()) { - auto clear_selection = [&]() { - selection->remove_all_ranges(); - - // FIXME: This doesn't work for some reason? - document->set_cursor_position(DOM::Position::create(realm, *range->start_container(), range->start_offset())); - }; - - if (key == UIEvents::KeyCode::Key_Backspace || key == UIEvents::KeyCode::Key_Delete) { - clear_selection(); - m_edit_event_handler->handle_delete(document, *range); - return EventResult::Handled; - } - - // FIXME: Text editing shortcut keys (copy/paste etc.) should be handled here. - if (!should_ignore_keydown_event(code_point, modifiers)) { - clear_selection(); - m_edit_event_handler->handle_delete(document, *range); - m_edit_event_handler->handle_insert(document, JS::NonnullGCPtr { *document->cursor_position() }, code_point); - document->increment_cursor_position_offset(); - return EventResult::Handled; - } - } - if (auto* element = m_navigable->active_document()->focused_element(); is(element)) { auto& media_element = static_cast(*element); if (media_element.handle_keydown({}, key, modifiers).release_value_but_fixme_should_propagate_errors()) return EventResult::Handled; } - if (document->cursor_position()) { - auto& node = *document->cursor_position()->node(); - - if (key == UIEvents::KeyCode::Key_Backspace && node.is_editable()) { + auto* target = document->active_input_events_target(); + if (target) { + if (key == UIEvents::KeyCode::Key_Backspace) { FIRE(input_event(UIEvents::EventNames::beforeinput, UIEvents::InputTypes::deleteContentBackward, m_navigable, code_point)); - - if (!document->decrement_cursor_position_offset()) { - // FIXME: Move to the previous node and delete the last character there. - return EventResult::Handled; - } - - m_edit_event_handler->handle_delete_character_after(document, *document->cursor_position()); + 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 && node.is_editable()) { + if (key == UIEvents::KeyCode::Key_Delete) { FIRE(input_event(UIEvents::EventNames::beforeinput, UIEvents::InputTypes::deleteContentForward, m_navigable, code_point)); - - if (document->cursor_position()->offset_is_at_end_of_node()) { - // FIXME: Move to the next node and delete the first character there. - return EventResult::Handled; - } - - m_edit_event_handler->handle_delete_character_after(document, *document->cursor_position()); + target->handle_delete(InputEventsTarget::DeleteDirection::Forward); FIRE(input_event(UIEvents::EventNames::input, UIEvents::InputTypes::deleteContentForward, m_navigable, code_point)); return EventResult::Handled; } @@ -1030,99 +981,50 @@ EventResult EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u #endif if (key == UIEvents::KeyCode::Key_Left || key == UIEvents::KeyCode::Key_Right) { - auto increment_or_decrement_cursor = [&]() { - if ((modifiers & UIEvents::Mod_PlatformWordJump) == 0) { - if (key == UIEvents::KeyCode::Key_Left) - return document->decrement_cursor_position_offset(); - return document->increment_cursor_position_offset(); - } - - if (key == UIEvents::KeyCode::Key_Left) - return document->decrement_cursor_position_to_previous_word(); - return document->increment_cursor_position_to_next_word(); - }; - - if ((modifiers & UIEvents::Mod_Shift) != 0) { - auto previous_position = document->cursor_position()->offset(); - auto should_udpdate_selection = increment_or_decrement_cursor(); - - if (should_udpdate_selection && selection) { - auto selection_start = range ? selection->anchor_offset() : previous_position; - auto selection_end = document->cursor_position()->offset(); - - (void)selection->set_base_and_extent(node, selection_start, node, selection_end); - } - } else if (node.is_editable()) { - if (selection && range) { - auto cursor_edge = key == UIEvents::KeyCode::Key_Left ? range->start_offset() : range->end_offset(); - - document->set_cursor_position(DOM::Position::create(document->realm(), node, cursor_edge)); - selection->remove_all_ranges(); + auto collapse = modifiers & UIEvents::Mod_Shift ? InputEventsTarget::CollapseSelection::No : InputEventsTarget::CollapseSelection::Yes; + if ((modifiers & UIEvents::Mod_PlatformWordJump) == 0) { + if (key == UIEvents::KeyCode::Key_Left) { + target->decrement_cursor_position_offset(collapse); } else { - increment_or_decrement_cursor(); + target->increment_cursor_position_offset(collapse); + } + } else { + if (key == UIEvents::KeyCode::Key_Left) { + target->decrement_cursor_position_to_previous_word(collapse); + } else { + target->increment_cursor_position_to_next_word(collapse); } } - return EventResult::Handled; } - if (key == UIEvents::KeyCode::Key_Home || key == UIEvents::KeyCode::Key_End) { - auto cursor_edge = key == UIEvents::KeyCode::Key_Home ? 0uz : node.length(); - - if ((modifiers & UIEvents::Mod_Shift) != 0) { - auto previous_position = document->cursor_position()->offset(); - auto should_udpdate_selection = previous_position != cursor_edge; - - if (should_udpdate_selection && selection) { - auto selection_start = range ? selection->anchor_offset() : previous_position; - (void)selection->set_base_and_extent(node, selection_start, node, cursor_edge); - } - } else if (node.is_editable()) { - if (selection && range) - selection->remove_all_ranges(); - } - - document->set_cursor_position(DOM::Position::create(realm, node, cursor_edge)); + if (key == UIEvents::KeyCode::Key_Home) { + auto collapse = modifiers & UIEvents::Mod_Shift ? InputEventsTarget::CollapseSelection::No : InputEventsTarget::CollapseSelection::Yes; + target->move_cursor_to_start(collapse); return EventResult::Handled; } - if (key == UIEvents::KeyCode::Key_Return && node.is_editable()) { + if (key == UIEvents::KeyCode::Key_End) { + auto collapse = modifiers & UIEvents::Mod_Shift ? InputEventsTarget::CollapseSelection::No : InputEventsTarget::CollapseSelection::Yes; + target->move_cursor_to_end(collapse); + return EventResult::Handled; + } + + if (key == UIEvents::KeyCode::Key_Return) { FIRE(input_event(UIEvents::EventNames::beforeinput, UIEvents::InputTypes::insertParagraph, m_navigable, code_point)); - HTML::HTMLInputElement* input_element = nullptr; - if (auto node = document->cursor_position()->node()) { - if (node->is_text()) { - auto& text_node = static_cast(*node); - if (is(text_node.editable_text_node_owner())) - input_element = static_cast(text_node.editable_text_node_owner()); - } else if (node->is_html_input_element()) { - input_element = static_cast(node.ptr()); - } - } - if (input_element) { - if (auto* form = input_element->form()) { - form->implicitly_submit_form().release_value_but_fixme_should_propagate_errors(); - return EventResult::Handled; - } - - input_element->commit_pending_changes(); - FIRE(input_event(UIEvents::EventNames::input, UIEvents::InputTypes::insertParagraph, m_navigable, code_point)); - return EventResult::Handled; - } + target->handle_return_key(); FIRE(input_event(UIEvents::EventNames::input, UIEvents::InputTypes::insertParagraph, m_navigable, code_point)); } // FIXME: Text editing shortcut keys (copy/paste etc.) should be handled here. - if (!should_ignore_keydown_event(code_point, modifiers) && node.is_editable()) { + if (!should_ignore_keydown_event(code_point, modifiers)) { FIRE(input_event(UIEvents::EventNames::beforeinput, UIEvents::InputTypes::insertText, m_navigable, code_point)); - m_edit_event_handler->handle_insert(document, JS::NonnullGCPtr { *document->cursor_position() }, code_point); - document->increment_cursor_position_offset(); + 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; } } - update_selection_range_for_input_or_textarea(); - // FIXME: Implement scroll by line and by page instead of approximating the behavior of other browsers. auto arrow_key_scroll_distance = 100; auto page_scroll_distance = document->window()->inner_height() - (document->window()->outer_height() - document->window()->inner_height()); @@ -1183,13 +1085,10 @@ void EventHandler::handle_paste(String const& text) if (!active_document->is_fully_active()) return; - if (auto cursor_position = active_document->cursor_position()) { - if (!cursor_position->node()->is_editable()) - return; - active_document->update_layout(); - m_edit_event_handler->handle_insert(*active_document, *cursor_position, text); - cursor_position->set_offset(cursor_position->offset() + text.code_points().length()); - } + auto* target = active_document->active_input_events_target(); + if (!target) + return; + target->handle_insert(text); } void EventHandler::set_mouse_event_tracking_paintable(Painting::Paintable* paintable) @@ -1262,57 +1161,6 @@ void EventHandler::visit_edges(JS::Cell::Visitor& visitor) const visitor.visit(m_mouse_event_tracking_paintable); } -// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection:set-the-selection-range -void EventHandler::update_selection_range_for_input_or_textarea() -{ - // Where possible, user interface features for changing the text selection in input and - // textarea elements must be implemented using the set the selection range algorithm so that, - // e.g., all the same events fire. - - // NOTE: It seems like only new selections are registered with the respective elements. I.e. - // existing selections in other elements are not cleared, so we only need to set the - // selection range for the element with the current selection. - - // Get the active selection - auto active_document = m_navigable->active_document(); - if (!active_document) - return; - auto selection = active_document->get_selection(); - if (!selection) - return; - - // Do we have a range within the same node? - auto range = selection->range(); - if (!range || range->start_container() != range->end_container()) - return; - - // We are only interested in text nodes with a shadow root - auto& node = *range->start_container(); - if (!node.is_text()) - return; - auto& root = node.root(); - if (!root.is_shadow_root()) - return; - auto* shadow_host = root.parent_or_shadow_host(); - if (!shadow_host) - return; - - // Invoke "set the selection range" on the form associated element - auto selection_start = range->start_offset(); - auto selection_end = range->end_offset(); - // FIXME: support selection directions other than ::Forward - auto direction = HTML::SelectionDirection::Forward; - - Optional target {}; - if (is(*shadow_host)) - target = static_cast(*shadow_host); - else if (is(*shadow_host)) - target = static_cast(*shadow_host); - - if (target.has_value()) - target.value().set_the_selection_range(selection_start, selection_end, direction, HTML::SelectionSource::UI); -} - Unicode::Segmenter& EventHandler::word_segmenter() { if (!m_word_segmenter) diff --git a/Userland/Libraries/LibWeb/Page/EventHandler.h b/Userland/Libraries/LibWeb/Page/EventHandler.h index e32091ed5dd..a9049204d23 100644 --- a/Userland/Libraries/LibWeb/Page/EventHandler.h +++ b/Userland/Libraries/LibWeb/Page/EventHandler.h @@ -65,15 +65,14 @@ private: Painting::PaintableBox const* paint_root() const; bool should_ignore_device_input_event() const; - void update_selection_range_for_input_or_textarea(); JS::NonnullGCPtr m_navigable; bool m_in_mouse_selection { false }; + InputEventsTarget* m_mouse_selection_target { nullptr }; JS::GCPtr m_mouse_event_tracking_paintable; - NonnullOwnPtr m_edit_event_handler; NonnullOwnPtr m_drag_and_drop_event_handler; WeakPtr m_mousedown_target; diff --git a/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp b/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp index 2f77f57a1da..da6d6f3ca15 100644 --- a/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -543,11 +544,15 @@ void paint_cursor_if_needed(PaintContext& context, TextPaintable const& paintabl if (!document.cursor_blink_state()) return; - if (document.cursor_position()->node() != paintable.dom_node()) + auto cursor_position = document.cursor_position(); + if (!cursor_position || !cursor_position->node()) + return; + + if (cursor_position->node() != paintable.dom_node()) return; // NOTE: This checks if the cursor is before the start or after the end of the fragment. If it is at the end, after all text, it should still be painted. - if (document.cursor_position()->offset() < (unsigned)fragment.start() || document.cursor_position()->offset() > (unsigned)(fragment.start() + fragment.length())) + if (cursor_position->offset() < (unsigned)fragment.start() || cursor_position->offset() > (unsigned)(fragment.start() + fragment.length())) return; if (!fragment.layout_node().dom_node() || !fragment.layout_node().dom_node()->is_editable()) diff --git a/Userland/Libraries/LibWeb/Painting/PaintableFragment.cpp b/Userland/Libraries/LibWeb/Painting/PaintableFragment.cpp index 1c65066376a..4960afb9c9e 100644 --- a/Userland/Libraries/LibWeb/Painting/PaintableFragment.cpp +++ b/Userland/Libraries/LibWeb/Painting/PaintableFragment.cpp @@ -5,6 +5,9 @@ */ #include +#include +#include +#include #include #include #include @@ -56,7 +59,7 @@ int PaintableFragment::text_index_at(CSSPixels x) const return m_start + m_length; } -CSSPixelRect PaintableFragment::range_rect(Gfx::Font const& font, DOM::Range const& range) const +CSSPixelRect PaintableFragment::range_rect(Gfx::Font const& font, size_t start_offset, size_t end_offset) const { if (paintable().selection_state() == Paintable::SelectionState::None) return {}; @@ -72,16 +75,16 @@ CSSPixelRect PaintableFragment::range_rect(Gfx::Font const& font, DOM::Range con if (paintable().selection_state() == Paintable::SelectionState::StartAndEnd) { // we are in the start/end node (both the same) - if (start_index > range.end_offset()) + if (start_index > end_offset) return {}; - if (end_index < range.start_offset()) + if (end_index < start_offset) return {}; - if (range.start_offset() == range.end_offset()) + if (start_offset == end_offset) return {}; - auto selection_start_in_this_fragment = max(0, range.start_offset() - m_start); - auto selection_end_in_this_fragment = min(m_length, range.end_offset() - m_start); + auto selection_start_in_this_fragment = max(0, start_offset - m_start); + auto selection_end_in_this_fragment = min(m_length, end_offset - m_start); auto pixel_distance_to_first_selected_character = CSSPixels::nearest_value_for(font.width(text.substring_view(0, selection_start_in_this_fragment))); auto pixel_width_of_selection = CSSPixels::nearest_value_for(font.width(text.substring_view(selection_start_in_this_fragment, selection_end_in_this_fragment - selection_start_in_this_fragment))) + 1; @@ -93,10 +96,10 @@ CSSPixelRect PaintableFragment::range_rect(Gfx::Font const& font, DOM::Range con } if (paintable().selection_state() == Paintable::SelectionState::Start) { // we are in the start node - if (end_index < range.start_offset()) + if (end_index < start_offset) return {}; - auto selection_start_in_this_fragment = max(0, range.start_offset() - m_start); + auto selection_start_in_this_fragment = max(0, start_offset - m_start); auto selection_end_in_this_fragment = m_length; auto pixel_distance_to_first_selected_character = CSSPixels::nearest_value_for(font.width(text.substring_view(0, selection_start_in_this_fragment))); auto pixel_width_of_selection = CSSPixels::nearest_value_for(font.width(text.substring_view(selection_start_in_this_fragment, selection_end_in_this_fragment - selection_start_in_this_fragment))) + 1; @@ -109,11 +112,11 @@ CSSPixelRect PaintableFragment::range_rect(Gfx::Font const& font, DOM::Range con } if (paintable().selection_state() == Paintable::SelectionState::End) { // we are in the end node - if (start_index > range.end_offset()) + if (start_index > end_offset) return {}; auto selection_start_in_this_fragment = 0; - auto selection_end_in_this_fragment = min(range.end_offset() - m_start, m_length); + auto selection_end_in_this_fragment = min(end_offset - m_start, m_length); auto pixel_distance_to_first_selected_character = CSSPixels::nearest_value_for(font.width(text.substring_view(0, selection_start_in_this_fragment))); auto pixel_width_of_selection = CSSPixels::nearest_value_for(font.width(text.substring_view(selection_start_in_this_fragment, selection_end_in_this_fragment - selection_start_in_this_fragment))) + 1; @@ -131,6 +134,23 @@ CSSPixelRect PaintableFragment::selection_rect(Gfx::Font const& font) const if (!paintable().is_selected()) return {}; + if (auto const* focused_element = paintable().document().focused_element(); focused_element && is(*focused_element)) { + HTML::FormAssociatedTextControlElement const* text_control_element = nullptr; + if (is(*focused_element)) { + auto const& input_element = static_cast(*focused_element); + text_control_element = static_cast(&input_element); + } else if (is(*focused_element)) { + auto const& textarea_element = static_cast(*focused_element); + text_control_element = static_cast(&textarea_element); + } else { + VERIFY_NOT_REACHED(); + } + auto selection_start = text_control_element->selection_start(); + auto selection_end = text_control_element->selection_end(); + if (!selection_start.has_value() || !selection_end.has_value()) + return {}; + return range_rect(font, selection_start.value(), selection_end.value()); + } auto selection = paintable().document().get_selection(); if (!selection) return {}; @@ -138,7 +158,7 @@ CSSPixelRect PaintableFragment::selection_rect(Gfx::Font const& font) const if (!range) return {}; - return range_rect(font, *range); + return range_rect(font, range->start_offset(), range->end_offset()); } StringView PaintableFragment::string_view() const diff --git a/Userland/Libraries/LibWeb/Painting/PaintableFragment.h b/Userland/Libraries/LibWeb/Painting/PaintableFragment.h index c4f47509ac3..80e2a60784f 100644 --- a/Userland/Libraries/LibWeb/Painting/PaintableFragment.h +++ b/Userland/Libraries/LibWeb/Painting/PaintableFragment.h @@ -46,7 +46,7 @@ public: RefPtr glyph_run() const { return m_glyph_run; } CSSPixelRect selection_rect(Gfx::Font const&) const; - CSSPixelRect range_rect(Gfx::Font const&, DOM::Range const&) const; + CSSPixelRect range_rect(Gfx::Font const&, size_t start_offset, size_t end_offset) const; CSSPixels width() const { return m_size.width(); } CSSPixels height() const { return m_size.height(); } diff --git a/Userland/Libraries/LibWeb/Selection/Selection.cpp b/Userland/Libraries/LibWeb/Selection/Selection.cpp index 2223a4789aa..7cd5e7c07f2 100644 --- a/Userland/Libraries/LibWeb/Selection/Selection.cpp +++ b/Userland/Libraries/LibWeb/Selection/Selection.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -471,4 +472,16 @@ void Selection::set_range(JS::GCPtr range) m_range->set_associated_selection({}, this); } +JS::GCPtr Selection::cursor_position() const +{ + if (!m_range) + return nullptr; + + if (is_collapsed()) { + return DOM::Position::create(m_document->realm(), *m_range->start_container(), m_range->start_offset()); + } + + return nullptr; +} + } diff --git a/Userland/Libraries/LibWeb/Selection/Selection.h b/Userland/Libraries/LibWeb/Selection/Selection.h index bc9207a5eea..06b5d229278 100644 --- a/Userland/Libraries/LibWeb/Selection/Selection.h +++ b/Userland/Libraries/LibWeb/Selection/Selection.h @@ -58,6 +58,9 @@ public: // Non-standard accessor for the selection's document. JS::NonnullGCPtr document() const; + // Non-standard + JS::GCPtr cursor_position() const; + private: Selection(JS::NonnullGCPtr, JS::NonnullGCPtr); diff --git a/Userland/Services/WebContent/WebDriverConnection.cpp b/Userland/Services/WebContent/WebDriverConnection.cpp index 1cda8802f8b..536c71c782c 100644 --- a/Userland/Services/WebContent/WebDriverConnection.cpp +++ b/Userland/Services/WebContent/WebDriverConnection.cpp @@ -1737,8 +1737,8 @@ Messages::WebDriverClient::ElementSendKeysResponse WebDriverConnection::element_ // -> element is content editable else if (is(*element) && static_cast(*element).is_content_editable()) { // If element does not currently have focus, set the text insertion caret after any child content. - if (!element->is_focused()) - element->document().set_cursor_position(Web::DOM::Position::create(element->realm(), *element, element->length())); + auto* document = current_browsing_context().active_document(); + document->set_focused_element(element); } // -> otherwise else if (is(*element)) {