mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-22 07:30:19 +00:00
LibWeb: Separate text control input events handling from contenteditable
This input event handling change is intended to address the following design issues: - Having `DOM::Position` is unnecessary complexity when `Selection` exists because caret position could be described by the selection object with a collapsed state. Before this change, we had to synchronize those whenever one of them was modified, and there were already bugs caused by that, i.e., caret position was not changed when selection offset was modified from the JS side. - Selection API exposes selection offset within `<textarea>` and `<input>`, which is not supposed to happen. These objects should manage their selection state by themselves and have selection offset even when they are not displayed. - `EventHandler` looks only at `DOM::Text` owned by `DOM::Position` while doing text manipulations. It works fine for `<input>` and `<textarea>`, but `contenteditable` needs to consider all text descendant text nodes; i.e., if the cursor is moved outside of `DOM::Text`, we need to look for an adjacent text node to move the cursor there. With this change, `EventHandler` no longer does direct manipulations on caret position or text content, but instead delegates them to the active `InputEventsTarget`, which could be either `FormAssociatedTextControlElement` (for `<input>` and `<textarea>`) or `EditingHostManager` (for `contenteditable`). The `Selection` object is used to manage both selection and caret position for `contenteditable`, and text control elements manage their own selection state that is not exposed by Selection API. This change improves text editing on Discord, as now we don't have to refocus the `contenteditable` element after character input. The problem was that selection manipulations from the JS side were not propagated to `DOM::Position`. I expect this change to make future correctness improvements for `contenteditable` (and `designMode`) easier, as now it's decoupled from `<input>` and `<textarea>` and separated from `EventHandler`, which is quite a busy file.
This commit is contained in:
parent
380907cd48
commit
a8077f79cc
Notes:
github-actions[bot]
2024-10-30 18:30:49 +00:00
Author: https://github.com/kalenikaliaksandr Commit: https://github.com/LadybirdBrowser/ladybird/commit/a8077f79cc6 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/2024
35 changed files with 884 additions and 663 deletions
|
@ -3,7 +3,6 @@ source_set("Page") {
|
|||
deps = [ "//Userland/Libraries/LibWeb:all_generated" ]
|
||||
sources = [
|
||||
"DragAndDropEventHandler.cpp",
|
||||
"EditEventHandler.cpp",
|
||||
"EventHandler.cpp",
|
||||
"InputEvent.cpp",
|
||||
"Page.cpp",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
helllo
|
|
@ -0,0 +1,11 @@
|
|||
<script src="../include.js"></script>
|
||||
<div id="text" contenteditable="true">heo</div>
|
||||
<script>
|
||||
test(() => {
|
||||
let text = document.getElementById("text");
|
||||
text.focus();
|
||||
document.getSelection().collapse(text.firstChild, 2);
|
||||
internals.sendText(text, "lll");
|
||||
println(text.textContent);
|
||||
});
|
||||
</script>
|
|
@ -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
|
||||
|
|
|
@ -49,12 +49,15 @@
|
|||
#include <LibWeb/DOM/DocumentFragment.h>
|
||||
#include <LibWeb/DOM/DocumentObserver.h>
|
||||
#include <LibWeb/DOM/DocumentType.h>
|
||||
#include <LibWeb/DOM/EditingHostManager.h>
|
||||
#include <LibWeb/DOM/Element.h>
|
||||
#include <LibWeb/DOM/ElementFactory.h>
|
||||
#include <LibWeb/DOM/Event.h>
|
||||
#include <LibWeb/DOM/HTMLCollection.h>
|
||||
#include <LibWeb/DOM/InputEventsTarget.h>
|
||||
#include <LibWeb/DOM/LiveNodeList.h>
|
||||
#include <LibWeb/DOM/NodeIterator.h>
|
||||
#include <LibWeb/DOM/Position.h>
|
||||
#include <LibWeb/DOM/ProcessingInstruction.h>
|
||||
#include <LibWeb/DOM/Range.h>
|
||||
#include <LibWeb/DOM/ShadowRoot.h>
|
||||
|
@ -87,10 +90,12 @@
|
|||
#include <LibWeb/HTML/HTMLHtmlElement.h>
|
||||
#include <LibWeb/HTML/HTMLIFrameElement.h>
|
||||
#include <LibWeb/HTML/HTMLImageElement.h>
|
||||
#include <LibWeb/HTML/HTMLInputElement.h>
|
||||
#include <LibWeb/HTML/HTMLLinkElement.h>
|
||||
#include <LibWeb/HTML/HTMLObjectElement.h>
|
||||
#include <LibWeb/HTML/HTMLScriptElement.h>
|
||||
#include <LibWeb/HTML/HTMLStyleElement.h>
|
||||
#include <LibWeb/HTML/HTMLTextAreaElement.h>
|
||||
#include <LibWeb/HTML/HTMLTitleElement.h>
|
||||
#include <LibWeb/HTML/HashChangeEvent.h>
|
||||
#include <LibWeb/HTML/ListOfAvailableImages.h>
|
||||
|
@ -382,6 +387,7 @@ Document::Document(JS::Realm& realm, const URL::URL& url, TemporaryDocumentForFr
|
|||
, m_style_computer(make<CSS::StyleComputer>(*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> Document::parse_html_unsafe(JS::VM& vm, StringView ht
|
|||
return document;
|
||||
}
|
||||
|
||||
void Document::set_cursor_position(JS::NonnullGCPtr<DOM::Position> 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<HTML::HTMLInputElement>(*focused_element))
|
||||
return static_cast<HTML::HTMLInputElement*>(focused_element);
|
||||
if (is<HTML::HTMLTextAreaElement>(*focused_element))
|
||||
return static_cast<HTML::HTMLTextAreaElement*>(focused_element);
|
||||
if (is<HTML::HTMLElement>(*focused_element) && static_cast<HTML::HTMLElement*>(focused_element)->is_editable())
|
||||
return m_editing_host_manager;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool Document::increment_cursor_position_offset()
|
||||
JS::GCPtr<DOM::Position> Document::cursor_position() const
|
||||
{
|
||||
if (!m_cursor_position->increment_offset())
|
||||
return false;
|
||||
|
||||
reset_cursor_blink_cycle();
|
||||
return true;
|
||||
auto const* focused_element = this->focused_element();
|
||||
if (!focused_element) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool Document::decrement_cursor_position_offset()
|
||||
{
|
||||
if (!m_cursor_position->decrement_offset())
|
||||
return false;
|
||||
Optional<HTML::FormAssociatedTextControlElement const&> target {};
|
||||
if (is<HTML::HTMLInputElement>(*focused_element))
|
||||
target = static_cast<HTML::HTMLInputElement const&>(*focused_element);
|
||||
else if (is<HTML::HTMLTextAreaElement>(*focused_element))
|
||||
target = static_cast<HTML::HTMLTextAreaElement const&>(*focused_element);
|
||||
|
||||
reset_cursor_blink_cycle();
|
||||
return true;
|
||||
if (target.has_value()) {
|
||||
return target->cursor_position();
|
||||
}
|
||||
|
||||
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;
|
||||
if (is<HTML::HTMLElement>(*focused_element) && static_cast<HTML::HTMLElement const*>(focused_element)->is_editable()) {
|
||||
return m_selection->cursor_position();
|
||||
}
|
||||
|
||||
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<EditEventHandler>)
|
||||
{
|
||||
reset_cursor_blink_cycle();
|
||||
|
||||
if (m_cursor_position && is<DOM::Text>(*m_cursor_position->node())) {
|
||||
auto& text_node = static_cast<DOM::Text&>(*m_cursor_position->node());
|
||||
|
||||
if (auto* text_node_owner = text_node.editable_text_node_owner())
|
||||
text_node_owner->did_edit_text_node({});
|
||||
}
|
||||
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<HTML::Navigable> Document::cached_navigable()
|
||||
|
|
|
@ -700,16 +700,11 @@ public:
|
|||
void set_console_client(JS::GCPtr<JS::ConsoleClient> console_client) { m_console_client = console_client; }
|
||||
JS::GCPtr<JS::ConsoleClient> console_client() const { return m_console_client; }
|
||||
|
||||
JS::GCPtr<DOM::Position> cursor_position() const { return m_cursor_position; }
|
||||
void set_cursor_position(JS::NonnullGCPtr<DOM::Position>);
|
||||
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<DOM::Position> cursor_position() const;
|
||||
|
||||
bool cursor_blink_state() const { return m_cursor_blink_state; }
|
||||
|
||||
void user_did_edit_document_text(Badge<EditEventHandler>);
|
||||
// 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<HTML::Navigable> cached_navigable();
|
||||
|
@ -746,6 +741,10 @@ public:
|
|||
[[nodiscard]] WebIDL::CallbackType* onvisibilitychange();
|
||||
void set_onvisibilitychange(WebIDL::CallbackType*);
|
||||
|
||||
void reset_cursor_blink_cycle();
|
||||
|
||||
JS::NonnullGCPtr<EditingHostManager> 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<Animations::Animation>);
|
||||
|
||||
void reset_cursor_blink_cycle();
|
||||
|
||||
JS::NonnullGCPtr<Page> m_page;
|
||||
OwnPtr<CSS::StyleComputer> m_style_computer;
|
||||
JS::GCPtr<CSS::StyleSheetList> m_style_sheets;
|
||||
|
@ -1014,7 +1011,6 @@ private:
|
|||
|
||||
JS::GCPtr<JS::ConsoleClient> m_console_client;
|
||||
|
||||
JS::GCPtr<DOM::Position> m_cursor_position;
|
||||
RefPtr<Core::Timer> m_cursor_blink_timer;
|
||||
bool m_cursor_blink_state { false };
|
||||
|
||||
|
@ -1030,6 +1026,8 @@ private:
|
|||
|
||||
mutable OwnPtr<Unicode::Segmenter> m_grapheme_segmenter;
|
||||
mutable OwnPtr<Unicode::Segmenter> m_word_segmenter;
|
||||
|
||||
JS::NonnullGCPtr<EditingHostManager> m_editing_host_manager;
|
||||
};
|
||||
|
||||
template<>
|
||||
|
|
257
Userland/Libraries/LibWeb/DOM/EditingHostManager.cpp
Normal file
257
Userland/Libraries/LibWeb/DOM/EditingHostManager.cpp
Normal file
|
@ -0,0 +1,257 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibUnicode/CharacterTypes.h>
|
||||
#include <LibUnicode/Segmenter.h>
|
||||
#include <LibWeb/DOM/EditingHostManager.h>
|
||||
#include <LibWeb/DOM/Range.h>
|
||||
#include <LibWeb/DOM/Text.h>
|
||||
#include <LibWeb/Selection/Selection.h>
|
||||
|
||||
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<DOM::Text>(*node)) {
|
||||
auto& realm = node->realm();
|
||||
auto text = realm.heap().allocate<DOM::Text>(realm, node->document(), data);
|
||||
MUST(node->append_child(*text));
|
||||
MUST(selection->collapse(*text, 1));
|
||||
return;
|
||||
}
|
||||
|
||||
auto& text_node = static_cast<DOM::Text&>(*node);
|
||||
|
||||
MUST(selection_range->delete_contents());
|
||||
MUST(text_node.insert_data(selection->anchor_offset(), data));
|
||||
VERIFY(selection->is_collapsed());
|
||||
|
||||
auto utf16_data = MUST(AK::utf8_to_utf16(data));
|
||||
Utf16View const utf16_view { utf16_data };
|
||||
auto length = utf16_view.length_in_code_units();
|
||||
MUST(selection->collapse(*node, selection->anchor_offset() + length));
|
||||
|
||||
text_node.invalidate_style(DOM::StyleInvalidationReason::EditingInsertion);
|
||||
}
|
||||
|
||||
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<DOM::Node> 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<DOM::Node> 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<DOM::Text>(*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<DOM::Text>(*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<DOM::Text>(*node))
|
||||
return;
|
||||
|
||||
auto& text_node = static_cast<DOM::Text&>(*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<DOM::Text>(*node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& text_node = static_cast<DOM::Text&>(*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<DOM::Text>(*node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& text_node = static_cast<DOM::Text&>(*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<DOM::Text>(*node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& text_node = static_cast<DOM::Text&>(*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<DOM::Text>(*node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& text_node = static_cast<DOM::Text&>(*node);
|
||||
if (direction == DeleteDirection::Backward) {
|
||||
if (selection->anchor_offset() > 0) {
|
||||
MUST(text_node.delete_data(selection->anchor_offset() - 1, 1));
|
||||
text_node.invalidate_style(DOM::StyleInvalidationReason::EditingInsertion);
|
||||
}
|
||||
} else {
|
||||
if (selection->anchor_offset() < text_node.data().bytes_as_string_view().length()) {
|
||||
MUST(text_node.delete_data(selection->anchor_offset(), 1));
|
||||
text_node.invalidate_style(DOM::StyleInvalidationReason::EditingInsertion);
|
||||
}
|
||||
}
|
||||
m_document->reset_cursor_blink_cycle();
|
||||
return;
|
||||
}
|
||||
|
||||
MUST(selection_range->delete_contents());
|
||||
}
|
||||
|
||||
void EditingHostManager::handle_return_key()
|
||||
{
|
||||
dbgln("FIXME: Implement EditingHostManager::handle_return_key()");
|
||||
}
|
||||
|
||||
}
|
64
Userland/Libraries/LibWeb/DOM/EditingHostManager.h
Normal file
64
Userland/Libraries/LibWeb/DOM/EditingHostManager.h
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibJS/Heap/Cell.h>
|
||||
#include <LibJS/Heap/CellAllocator.h>
|
||||
#include <LibJS/Runtime/Realm.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/DOM/InputEventsTarget.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
|
||||
namespace Web::DOM {
|
||||
|
||||
class EditingHostManager : public JS::Cell
|
||||
, public InputEventsTarget {
|
||||
JS_CELL(EditingHostManager, JS::Cell);
|
||||
JS_DECLARE_ALLOCATOR(EditingHostManager);
|
||||
|
||||
public:
|
||||
[[nodiscard]] static JS::NonnullGCPtr<EditingHostManager> create(JS::Realm& realm, JS::NonnullGCPtr<Document> document)
|
||||
{
|
||||
return realm.heap().allocate<EditingHostManager>(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<DOM::Node>, size_t offset) override;
|
||||
virtual void set_selection_focus(JS::NonnullGCPtr<DOM::Node>, size_t offset) override;
|
||||
virtual void move_cursor_to_start(CollapseSelection) override;
|
||||
virtual void move_cursor_to_end(CollapseSelection) override;
|
||||
virtual void increment_cursor_position_offset(CollapseSelection) override;
|
||||
virtual void decrement_cursor_position_offset(CollapseSelection) override;
|
||||
virtual void increment_cursor_position_to_next_word(CollapseSelection) override;
|
||||
virtual void decrement_cursor_position_to_previous_word(CollapseSelection) override;
|
||||
|
||||
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<DOM::Node> element)
|
||||
{
|
||||
m_active_contenteditable_element = element;
|
||||
}
|
||||
|
||||
EditingHostManager(JS::NonnullGCPtr<Document> document)
|
||||
: m_document(document)
|
||||
{
|
||||
}
|
||||
|
||||
private:
|
||||
JS::NonnullGCPtr<Document> m_document;
|
||||
JS::GCPtr<DOM::Node> m_active_contenteditable_element;
|
||||
};
|
||||
|
||||
}
|
41
Userland/Libraries/LibWeb/DOM/InputEventsTarget.h
Normal file
41
Userland/Libraries/LibWeb/DOM/InputEventsTarget.h
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibWeb/Forward.h>
|
||||
|
||||
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<DOM::Node>, size_t offset) = 0;
|
||||
virtual void set_selection_focus(JS::NonnullGCPtr<DOM::Node>, 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;
|
||||
};
|
||||
|
||||
}
|
|
@ -5,9 +5,6 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Utf8View.h>
|
||||
#include <LibUnicode/CharacterTypes.h>
|
||||
#include <LibUnicode/Segmenter.h>
|
||||
#include <LibWeb/DOM/Node.h>
|
||||
#include <LibWeb/DOM/Position.h>
|
||||
#include <LibWeb/DOM/Text.h>
|
||||
|
@ -35,100 +32,4 @@ ErrorOr<String> Position::to_string() const
|
|||
return String::formatted("DOM::Position({} ({})), {})", node()->node_name(), node().ptr(), offset());
|
||||
}
|
||||
|
||||
bool Position::increment_offset()
|
||||
{
|
||||
if (!is<DOM::Text>(*m_node))
|
||||
return false;
|
||||
|
||||
auto& node = verify_cast<DOM::Text>(*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<DOM::Text>(*m_node))
|
||||
return false;
|
||||
|
||||
auto& node = verify_cast<DOM::Text>(*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<DOM::Text>(*m_node) || offset_is_at_end_of_node())
|
||||
return false;
|
||||
|
||||
auto& node = static_cast<DOM::Text&>(*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<DOM::Text>(*m_node) || m_offset == 0)
|
||||
return false;
|
||||
|
||||
auto& node = static_cast<DOM::Text&>(*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<DOM::Text>(*m_node))
|
||||
return false;
|
||||
|
||||
auto& node = verify_cast<DOM::Text>(*m_node);
|
||||
auto text = node.data();
|
||||
return m_offset == text.bytes_as_string_view().length();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -28,16 +28,8 @@ public:
|
|||
|
||||
JS::GCPtr<Node> node() { return m_node; }
|
||||
JS::GCPtr<Node const> node() const { return m_node; }
|
||||
void set_node(JS::NonnullGCPtr<Node> 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<Position> other) const
|
||||
{
|
||||
|
|
|
@ -1257,7 +1257,7 @@ JS::NonnullGCPtr<Geometry::DOMRectList> 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(),
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace Web::DOM {
|
|||
class EditableTextNodeOwner {
|
||||
public:
|
||||
virtual ~EditableTextNodeOwner() = default;
|
||||
virtual void did_edit_text_node(Badge<Document>) = 0;
|
||||
virtual void did_edit_text_node() = 0;
|
||||
};
|
||||
|
||||
class Text
|
||||
|
|
|
@ -13,8 +13,8 @@
|
|||
#include <LibJS/Forward.h>
|
||||
|
||||
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;
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
#include <LibJS/Forward.h>
|
||||
#include <LibJS/Heap/Cell.h>
|
||||
#include <LibURL/Origin.h>
|
||||
#include <LibWeb/DOM/Position.h>
|
||||
#include <LibWeb/HTML/ActivateTab.h>
|
||||
#include <LibWeb/HTML/NavigableContainer.h>
|
||||
#include <LibWeb/HTML/SandboxingFlagSet.h>
|
||||
|
|
|
@ -6,8 +6,11 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibUnicode/CharacterTypes.h>
|
||||
#include <LibUnicode/Segmenter.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/DOM/Event.h>
|
||||
#include <LibWeb/DOM/Position.h>
|
||||
#include <LibWeb/HTML/FormAssociatedElement.h>
|
||||
#include <LibWeb/HTML/HTMLButtonElement.h>
|
||||
#include <LibWeb/HTML/HTMLFieldSetElement.h>
|
||||
|
@ -17,6 +20,7 @@
|
|||
#include <LibWeb/HTML/HTMLSelectElement.h>
|
||||
#include <LibWeb/HTML/HTMLTextAreaElement.h>
|
||||
#include <LibWeb/HTML/Parser/HTMLParser.h>
|
||||
#include <LibWeb/Painting/Paintable.h>
|
||||
|
||||
namespace Web::HTML {
|
||||
|
||||
|
@ -196,7 +200,7 @@ WebIDL::ExceptionOr<void> FormAssociatedElement::set_form_action(String const& v
|
|||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
|
||||
void FormAssociatedTextControlElement::relevant_value_was_changed(JS::GCPtr<DOM::Text> text_node)
|
||||
void FormAssociatedTextControlElement::relevant_value_was_changed()
|
||||
{
|
||||
auto the_relevant_value = relevant_value();
|
||||
auto relevant_value_length = the_relevant_value.code_points().length();
|
||||
|
@ -223,13 +227,8 @@ void FormAssociatedTextControlElement::relevant_value_was_changed(JS::GCPtr<DOM:
|
|||
|
||||
// 2. Otherwise, the element must have a text entry cursor position position. If it is now past
|
||||
// the end of the relevant value, set it to the end of the relevant value.
|
||||
auto& document = form_associated_element_to_html_element().document();
|
||||
auto const current_cursor_position = document.cursor_position();
|
||||
if (current_cursor_position && text_node
|
||||
&& current_cursor_position->node() == text_node
|
||||
&& current_cursor_position->offset() > relevant_value_length) {
|
||||
document.set_cursor_position(DOM::Position::create(document.realm(), *text_node, relevant_value_length));
|
||||
}
|
||||
if (m_selection_start > relevant_value_length)
|
||||
m_selection_start = relevant_value_length;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-select
|
||||
|
@ -263,13 +262,12 @@ Optional<WebIDL::UnsignedLong> FormAssociatedTextControlElement::selection_start
|
|||
// 2. If there is no selection, return the code unit offset within the relevant value to the character that
|
||||
// immediately follows the text entry cursor.
|
||||
if (m_selection_start == m_selection_end) {
|
||||
if (auto cursor = form_associated_element_to_html_element().document().cursor_position())
|
||||
return cursor->offset();
|
||||
return m_selection_start;
|
||||
}
|
||||
|
||||
// 3. Return the code unit offset within the relevant value to the character that immediately follows the start of
|
||||
// the selection.
|
||||
return m_selection_start;
|
||||
return m_selection_start < m_selection_end ? m_selection_start : m_selection_end;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection:dom-textarea/input-selectionstart-2
|
||||
|
@ -312,13 +310,12 @@ Optional<WebIDL::UnsignedLong> FormAssociatedTextControlElement::selection_end()
|
|||
// 2. If there is no selection, return the code unit offset within the relevant value to the
|
||||
// character that immediately follows the text entry cursor.
|
||||
if (m_selection_start == m_selection_end) {
|
||||
if (auto cursor = form_associated_element_to_html_element().document().cursor_position())
|
||||
return cursor->offset();
|
||||
return m_selection_start;
|
||||
}
|
||||
|
||||
// 3. Return the code unit offset within the relevant value to the character that immediately
|
||||
// follows the end of the selection.
|
||||
return m_selection_end;
|
||||
return m_selection_start < m_selection_end ? m_selection_end : m_selection_start;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection:dom-textarea/input-selectionend-3
|
||||
|
@ -589,4 +586,240 @@ void FormAssociatedTextControlElement::set_the_selection_range(Optional<WebIDL::
|
|||
}
|
||||
}
|
||||
|
||||
void FormAssociatedTextControlElement::handle_insert(String const& data)
|
||||
{
|
||||
auto text_node = form_associated_element_to_text_node();
|
||||
if (!text_node || !text_node->is_editable())
|
||||
return;
|
||||
|
||||
String data_for_insertion = data;
|
||||
if (auto max_length = text_node->max_length(); max_length.has_value()) {
|
||||
auto remaining_length = *max_length - text_node->data().code_points().length();
|
||||
if (remaining_length < data.code_points().length()) {
|
||||
data_for_insertion = MUST(data.substring_from_byte_offset(0, remaining_length));
|
||||
}
|
||||
}
|
||||
auto selection_start = this->selection_start();
|
||||
auto selection_end = this->selection_end();
|
||||
if (!selection_start.has_value() || !selection_end.has_value()) {
|
||||
return;
|
||||
}
|
||||
MUST(set_range_text(data_for_insertion, selection_start.value(), selection_end.value(), Bindings::SelectionMode::End));
|
||||
|
||||
text_node->invalidate_style(DOM::StyleInvalidationReason::EditingInsertion);
|
||||
text_node->editable_text_node_owner()->did_edit_text_node();
|
||||
}
|
||||
|
||||
void FormAssociatedTextControlElement::handle_delete(DeleteDirection direction)
|
||||
{
|
||||
auto text_node = form_associated_element_to_text_node();
|
||||
if (!text_node || !text_node->is_editable())
|
||||
return;
|
||||
auto selection_start = this->selection_start();
|
||||
auto selection_end = this->selection_end();
|
||||
if (!selection_start.has_value() || !selection_end.has_value()) {
|
||||
return;
|
||||
}
|
||||
if (selection_start == selection_end) {
|
||||
if (direction == DeleteDirection::Backward) {
|
||||
if (selection_start.value() > 0) {
|
||||
MUST(set_range_text(MUST(String::from_utf8(""sv)), selection_start.value() - 1, selection_end.value(), Bindings::SelectionMode::End));
|
||||
}
|
||||
} else {
|
||||
if (selection_start.value() < text_node->data().code_points().length()) {
|
||||
MUST(set_range_text(MUST(String::from_utf8(""sv)), selection_start.value(), selection_end.value() + 1, Bindings::SelectionMode::End));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
MUST(set_range_text(MUST(String::from_utf8(""sv)), selection_start.value(), selection_end.value(), Bindings::SelectionMode::End));
|
||||
}
|
||||
|
||||
void FormAssociatedTextControlElement::handle_return_key()
|
||||
{
|
||||
auto& html_element = form_associated_element_to_html_element();
|
||||
if (is<HTMLInputElement>(html_element)) {
|
||||
auto& input_element = static_cast<HTMLInputElement&>(html_element);
|
||||
if (auto* form = input_element.form()) {
|
||||
form->implicitly_submit_form().release_value_but_fixme_should_propagate_errors();
|
||||
return;
|
||||
}
|
||||
input_element.commit_pending_changes();
|
||||
}
|
||||
}
|
||||
|
||||
void FormAssociatedTextControlElement::collapse_selection_to_offset(size_t position)
|
||||
{
|
||||
m_selection_start = position;
|
||||
m_selection_end = position;
|
||||
}
|
||||
|
||||
void FormAssociatedTextControlElement::selection_was_changed()
|
||||
{
|
||||
auto text_node = form_associated_element_to_text_node();
|
||||
if (!text_node)
|
||||
return;
|
||||
auto* text_paintable = text_node->paintable();
|
||||
if (!text_paintable)
|
||||
return;
|
||||
if (m_selection_start == m_selection_end) {
|
||||
text_paintable->set_selected(false);
|
||||
text_paintable->set_selection_state(Painting::Paintable::SelectionState::None);
|
||||
text_node->document().reset_cursor_blink_cycle();
|
||||
} else {
|
||||
text_paintable->set_selected(true);
|
||||
text_paintable->set_selection_state(Painting::Paintable::SelectionState::StartAndEnd);
|
||||
}
|
||||
text_paintable->set_needs_display();
|
||||
}
|
||||
|
||||
void FormAssociatedTextControlElement::select_all()
|
||||
{
|
||||
auto text_node = form_associated_element_to_text_node();
|
||||
if (!text_node)
|
||||
return;
|
||||
set_the_selection_range(0, text_node->length());
|
||||
selection_was_changed();
|
||||
}
|
||||
|
||||
void FormAssociatedTextControlElement::set_selection_anchor(JS::NonnullGCPtr<DOM::Node> anchor_node, size_t anchor_offset)
|
||||
{
|
||||
auto text_node = form_associated_element_to_text_node();
|
||||
if (!text_node || anchor_node != text_node)
|
||||
return;
|
||||
collapse_selection_to_offset(anchor_offset);
|
||||
selection_was_changed();
|
||||
}
|
||||
|
||||
void FormAssociatedTextControlElement::set_selection_focus(JS::NonnullGCPtr<DOM::Node> focus_node, size_t focus_offset)
|
||||
{
|
||||
auto text_node = form_associated_element_to_text_node();
|
||||
if (!text_node || focus_node != text_node)
|
||||
return;
|
||||
m_selection_end = focus_offset;
|
||||
selection_was_changed();
|
||||
}
|
||||
|
||||
void FormAssociatedTextControlElement::move_cursor_to_start(CollapseSelection collapse)
|
||||
{
|
||||
auto text_node = form_associated_element_to_text_node();
|
||||
if (!text_node)
|
||||
return;
|
||||
if (collapse == CollapseSelection::Yes) {
|
||||
collapse_selection_to_offset(0);
|
||||
} else {
|
||||
m_selection_end = 0;
|
||||
}
|
||||
selection_was_changed();
|
||||
}
|
||||
|
||||
void FormAssociatedTextControlElement::move_cursor_to_end(CollapseSelection collapse)
|
||||
{
|
||||
auto text_node = form_associated_element_to_text_node();
|
||||
if (!text_node)
|
||||
return;
|
||||
if (collapse == CollapseSelection::Yes) {
|
||||
collapse_selection_to_offset(text_node->length());
|
||||
} else {
|
||||
m_selection_end = text_node->length();
|
||||
}
|
||||
selection_was_changed();
|
||||
}
|
||||
|
||||
void FormAssociatedTextControlElement::increment_cursor_position_offset(CollapseSelection collapse)
|
||||
{
|
||||
auto const text_node = form_associated_element_to_text_node();
|
||||
if (!text_node)
|
||||
return;
|
||||
if (auto offset = text_node->grapheme_segmenter().next_boundary(m_selection_end); offset.has_value()) {
|
||||
if (collapse == CollapseSelection::Yes) {
|
||||
collapse_selection_to_offset(*offset);
|
||||
} else {
|
||||
m_selection_end = *offset;
|
||||
}
|
||||
}
|
||||
selection_was_changed();
|
||||
}
|
||||
|
||||
void FormAssociatedTextControlElement::decrement_cursor_position_offset(CollapseSelection collapse)
|
||||
{
|
||||
auto const text_node = form_associated_element_to_text_node();
|
||||
if (!text_node)
|
||||
return;
|
||||
if (auto offset = text_node->grapheme_segmenter().previous_boundary(m_selection_end); offset.has_value()) {
|
||||
if (collapse == CollapseSelection::Yes) {
|
||||
collapse_selection_to_offset(*offset);
|
||||
} else {
|
||||
m_selection_end = *offset;
|
||||
}
|
||||
}
|
||||
selection_was_changed();
|
||||
}
|
||||
|
||||
static bool should_continue_beyond_word(Utf8View const& word)
|
||||
{
|
||||
for (auto code_point : word) {
|
||||
if (!Unicode::code_point_has_punctuation_general_category(code_point) && !Unicode::code_point_has_separator_general_category(code_point))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void FormAssociatedTextControlElement::increment_cursor_position_to_next_word(CollapseSelection collapse)
|
||||
{
|
||||
auto const text_node = form_associated_element_to_text_node();
|
||||
if (!text_node)
|
||||
return;
|
||||
|
||||
while (true) {
|
||||
if (auto offset = text_node->word_segmenter().next_boundary(m_selection_end); offset.has_value()) {
|
||||
auto word = text_node->data().code_points().substring_view(m_selection_end, *offset - m_selection_end);
|
||||
if (collapse == CollapseSelection::Yes) {
|
||||
collapse_selection_to_offset(*offset);
|
||||
} else {
|
||||
m_selection_end = *offset;
|
||||
}
|
||||
if (should_continue_beyond_word(word))
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
selection_was_changed();
|
||||
}
|
||||
|
||||
void FormAssociatedTextControlElement::decrement_cursor_position_to_previous_word(CollapseSelection collapse)
|
||||
{
|
||||
auto const text_node = form_associated_element_to_text_node();
|
||||
if (!text_node)
|
||||
return;
|
||||
|
||||
while (true) {
|
||||
if (auto offset = text_node->word_segmenter().previous_boundary(m_selection_end); offset.has_value()) {
|
||||
auto word = text_node->data().code_points().substring_view(m_selection_end, m_selection_end - *offset);
|
||||
if (collapse == CollapseSelection::Yes) {
|
||||
collapse_selection_to_offset(*offset);
|
||||
} else {
|
||||
m_selection_end = *offset;
|
||||
}
|
||||
if (should_continue_beyond_word(word))
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
selection_was_changed();
|
||||
}
|
||||
|
||||
JS::GCPtr<DOM::Position> FormAssociatedTextControlElement::cursor_position() const
|
||||
{
|
||||
auto const node = form_associated_element_to_text_node();
|
||||
if (!node)
|
||||
return nullptr;
|
||||
if (m_selection_start == m_selection_end)
|
||||
return DOM::Position::create(node->realm(), const_cast<DOM::Text&>(*node), m_selection_start);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <AK/String.h>
|
||||
#include <AK/WeakPtr.h>
|
||||
#include <LibWeb/Bindings/HTMLFormElementPrototype.h>
|
||||
#include <LibWeb/DOM/InputEventsTarget.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
#include <LibWeb/WebIDL/Types.h>
|
||||
|
||||
|
@ -96,7 +97,7 @@ public:
|
|||
HTMLElement const& form_associated_element_to_html_element() const { return const_cast<FormAssociatedElement&>(*this).form_associated_element_to_html_element(); }
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-form-reset-control
|
||||
virtual void reset_algorithm() {};
|
||||
virtual void reset_algorithm() { }
|
||||
|
||||
virtual void clear_algorithm();
|
||||
|
||||
|
@ -129,7 +130,8 @@ enum class SelectionSource {
|
|||
DOM,
|
||||
};
|
||||
|
||||
class FormAssociatedTextControlElement : public FormAssociatedElement {
|
||||
class FormAssociatedTextControlElement : public FormAssociatedElement
|
||||
, public InputEventsTarget {
|
||||
public:
|
||||
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
|
||||
virtual String relevant_value() = 0;
|
||||
|
@ -168,13 +170,34 @@ public:
|
|||
bool has_scheduled_selectionchange_event() const { return m_has_scheduled_selectionchange_event; }
|
||||
void set_scheduled_selectionchange_event(bool value) { m_has_scheduled_selectionchange_event = value; }
|
||||
|
||||
virtual JS::GCPtr<DOM::Text> form_associated_element_to_text_node() = 0;
|
||||
virtual JS::GCPtr<DOM::Text const> form_associated_element_to_text_node() const { return const_cast<FormAssociatedTextControlElement&>(*this).form_associated_element_to_text_node(); }
|
||||
|
||||
virtual void handle_insert(String const&) override;
|
||||
virtual void handle_delete(DeleteDirection) override;
|
||||
virtual void handle_return_key() override;
|
||||
virtual void select_all() override;
|
||||
virtual void set_selection_anchor(JS::NonnullGCPtr<DOM::Node>, size_t offset) override;
|
||||
virtual void set_selection_focus(JS::NonnullGCPtr<DOM::Node>, size_t offset) override;
|
||||
virtual void move_cursor_to_start(CollapseSelection) override;
|
||||
virtual void move_cursor_to_end(CollapseSelection) override;
|
||||
virtual void increment_cursor_position_offset(CollapseSelection) override;
|
||||
virtual void decrement_cursor_position_offset(CollapseSelection) override;
|
||||
virtual void increment_cursor_position_to_next_word(CollapseSelection) override;
|
||||
virtual void decrement_cursor_position_to_previous_word(CollapseSelection) override;
|
||||
|
||||
JS::GCPtr<DOM::Position> cursor_position() const;
|
||||
|
||||
protected:
|
||||
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
|
||||
void relevant_value_was_changed(JS::GCPtr<DOM::Text>);
|
||||
void relevant_value_was_changed();
|
||||
|
||||
virtual void selection_was_changed([[maybe_unused]] size_t selection_start, [[maybe_unused]] size_t selection_end) { }
|
||||
|
||||
private:
|
||||
void collapse_selection_to_offset(size_t);
|
||||
void selection_was_changed();
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-selection
|
||||
WebIDL::UnsignedLong m_selection_start { 0 };
|
||||
WebIDL::UnsignedLong m_selection_end { 0 };
|
||||
|
|
|
@ -9,9 +9,11 @@
|
|||
#include <LibWeb/Bindings/ExceptionOrUtils.h>
|
||||
#include <LibWeb/Bindings/HTMLElementPrototype.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/DOM/EditingHostManager.h>
|
||||
#include <LibWeb/DOM/ElementFactory.h>
|
||||
#include <LibWeb/DOM/IDLEventListener.h>
|
||||
#include <LibWeb/DOM/LiveNodeList.h>
|
||||
#include <LibWeb/DOM/Position.h>
|
||||
#include <LibWeb/DOM/ShadowRoot.h>
|
||||
#include <LibWeb/HTML/BrowsingContext.h>
|
||||
#include <LibWeb/HTML/CustomElements/CustomElementDefinition.h>
|
||||
|
@ -887,6 +889,9 @@ void HTMLElement::did_receive_focus()
|
|||
if (m_content_editable_state != ContentEditableState::True)
|
||||
return;
|
||||
|
||||
auto editing_host = document().editing_host_manager();
|
||||
editing_host->set_active_contenteditable_element(this);
|
||||
|
||||
DOM::Text* text = nullptr;
|
||||
for_each_in_inclusive_subtree_of_type<DOM::Text>([&](auto& node) {
|
||||
text = &node;
|
||||
|
@ -894,10 +899,18 @@ void HTMLElement::did_receive_focus()
|
|||
});
|
||||
|
||||
if (!text) {
|
||||
document().set_cursor_position(DOM::Position::create(realm(), *this, 0));
|
||||
editing_host->set_selection_anchor(*this, 0);
|
||||
return;
|
||||
}
|
||||
document().set_cursor_position(DOM::Position::create(realm(), *text, text->length()));
|
||||
editing_host->set_selection_anchor(*text, text->length());
|
||||
}
|
||||
|
||||
void HTMLElement::did_lose_focus()
|
||||
{
|
||||
if (m_content_editable_state != ContentEditableState::True)
|
||||
return;
|
||||
|
||||
document().editing_host_manager()->set_active_contenteditable_element(nullptr);
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/interaction.html#dom-accesskeylabel
|
||||
|
|
|
@ -95,6 +95,7 @@ private:
|
|||
// ^HTML::GlobalEventHandlers
|
||||
virtual JS::GCPtr<DOM::EventTarget> global_event_handlers_to_event_target(FlyString const&) override { return *this; }
|
||||
virtual void did_receive_focus() override;
|
||||
virtual void did_lose_focus() override;
|
||||
|
||||
[[nodiscard]] String get_the_text_steps();
|
||||
void append_rendered_text_fragment(StringView input);
|
||||
|
|
|
@ -429,7 +429,7 @@ WebIDL::ExceptionOr<void> HTMLInputElement::run_input_activation_behavior(DOM::E
|
|||
return {};
|
||||
}
|
||||
|
||||
void HTMLInputElement::did_edit_text_node(Badge<DOM::Document>)
|
||||
void HTMLInputElement::did_edit_text_node()
|
||||
{
|
||||
// An input element's dirty value flag must be set to true whenever the user interacts with the control in a way that changes the value.
|
||||
auto old_value = move(m_value);
|
||||
|
@ -439,7 +439,7 @@ void HTMLInputElement::did_edit_text_node(Badge<DOM::Document>)
|
|||
m_has_uncommitted_changes = true;
|
||||
|
||||
if (m_value != old_value)
|
||||
relevant_value_was_changed(m_text_node);
|
||||
relevant_value_was_changed();
|
||||
|
||||
update_placeholder_visibility();
|
||||
|
||||
|
@ -584,7 +584,7 @@ WebIDL::ExceptionOr<void> HTMLInputElement::set_value(String const& value)
|
|||
// and the element has a text entry cursor position, move the text entry cursor position to the end of the
|
||||
// text control, unselecting any selected text and resetting the selection direction to "none".
|
||||
if (m_value != old_value) {
|
||||
relevant_value_was_changed(m_text_node);
|
||||
relevant_value_was_changed();
|
||||
|
||||
if (m_text_node) {
|
||||
m_text_node->set_data(m_value);
|
||||
|
@ -1178,11 +1178,11 @@ void HTMLInputElement::did_receive_focus()
|
|||
return;
|
||||
m_text_node->invalidate_style(DOM::StyleInvalidationReason::DidReceiveFocus);
|
||||
|
||||
if (auto* paintable = m_text_node->paintable())
|
||||
paintable->set_selected(true);
|
||||
|
||||
if (m_placeholder_text_node)
|
||||
m_placeholder_text_node->invalidate_style(DOM::StyleInvalidationReason::DidReceiveFocus);
|
||||
|
||||
if (auto cursor = document().cursor_position(); !cursor || m_text_node != cursor->node())
|
||||
document().set_cursor_position(DOM::Position::create(realm(), *m_text_node, m_text_node->length()));
|
||||
}
|
||||
|
||||
void HTMLInputElement::did_lose_focus()
|
||||
|
@ -1190,6 +1190,9 @@ void HTMLInputElement::did_lose_focus()
|
|||
if (m_text_node)
|
||||
m_text_node->invalidate_style(DOM::StyleInvalidationReason::DidLoseFocus);
|
||||
|
||||
if (auto* paintable = m_text_node->paintable())
|
||||
paintable->set_selected(false);
|
||||
|
||||
if (m_placeholder_text_node)
|
||||
m_placeholder_text_node->invalidate_style(DOM::StyleInvalidationReason::DidLoseFocus);
|
||||
|
||||
|
@ -1232,7 +1235,7 @@ void HTMLInputElement::form_associated_element_attribute_changed(FlyString const
|
|||
}
|
||||
|
||||
if (m_value != old_value)
|
||||
relevant_value_was_changed(m_text_node);
|
||||
relevant_value_was_changed();
|
||||
|
||||
update_shadow_tree();
|
||||
}
|
||||
|
@ -1303,7 +1306,6 @@ void HTMLInputElement::type_attribute_changed(TypeAttributeState old_state, Type
|
|||
// 9. If previouslySelectable is false and nowSelectable is true, set the element's text entry cursor position to the
|
||||
// beginning of the text control, and set its selection direction to "none".
|
||||
if (!previously_selectable && now_selectable) {
|
||||
document().set_cursor_position(DOM::Position::create(realm(), *m_text_node, 0));
|
||||
set_selection_direction(OptionalNone {});
|
||||
}
|
||||
}
|
||||
|
@ -1539,7 +1541,7 @@ void HTMLInputElement::reset_algorithm()
|
|||
m_value = value_sanitization_algorithm(m_value);
|
||||
|
||||
if (m_value != old_value)
|
||||
relevant_value_was_changed(m_text_node);
|
||||
relevant_value_was_changed();
|
||||
|
||||
if (m_text_node) {
|
||||
m_text_node->set_data(m_value);
|
||||
|
@ -1575,7 +1577,7 @@ void HTMLInputElement::clear_algorithm()
|
|||
user_interaction_did_change_input_value();
|
||||
|
||||
if (m_value != old_value)
|
||||
relevant_value_was_changed(m_text_node);
|
||||
relevant_value_was_changed();
|
||||
|
||||
if (m_text_node) {
|
||||
m_text_node->set_data(m_value);
|
||||
|
@ -2585,15 +2587,4 @@ HTMLInputElement::ValueAttributeMode HTMLInputElement::value_attribute_mode() co
|
|||
return value_attribute_mode_for_type_state(type_state());
|
||||
}
|
||||
|
||||
void HTMLInputElement::selection_was_changed(size_t selection_start, size_t selection_end)
|
||||
{
|
||||
if (!m_text_node || !document().cursor_position() || document().cursor_position()->node() != m_text_node)
|
||||
return;
|
||||
|
||||
document().set_cursor_position(DOM::Position::create(realm(), *m_text_node, selection_end));
|
||||
|
||||
if (auto selection = document().get_selection())
|
||||
MUST(selection->set_base_and_extent(*m_text_node, selection_start, *m_text_node, selection_end));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -151,7 +151,7 @@ public:
|
|||
WebIDL::ExceptionOr<void> show_picker();
|
||||
|
||||
// ^DOM::EditableTextNodeOwner
|
||||
virtual void did_edit_text_node(Badge<DOM::Document>) override;
|
||||
virtual void did_edit_text_node() override;
|
||||
|
||||
// ^EventTarget
|
||||
// https://html.spec.whatwg.org/multipage/interaction.html#the-tabindex-attribute:the-input-element
|
||||
|
@ -216,8 +216,7 @@ public:
|
|||
|
||||
Optional<String> selection_direction_binding() { return selection_direction(); }
|
||||
|
||||
protected:
|
||||
void selection_was_changed(size_t selection_start, size_t selection_end) override;
|
||||
virtual JS::GCPtr<DOM::Text> form_associated_element_to_text_node() override { return m_text_node; }
|
||||
|
||||
private:
|
||||
HTMLInputElement(DOM::Document&, DOM::QualifiedName);
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
#include <LibWeb/HTML/Numbers.h>
|
||||
#include <LibWeb/Infra/Strings.h>
|
||||
#include <LibWeb/Namespace.h>
|
||||
#include <LibWeb/Painting/Paintable.h>
|
||||
#include <LibWeb/Selection/Selection.h>
|
||||
|
||||
namespace Web::HTML {
|
||||
|
@ -74,11 +75,11 @@ void HTMLTextAreaElement::did_receive_focus()
|
|||
return;
|
||||
m_text_node->invalidate_style(DOM::StyleInvalidationReason::DidReceiveFocus);
|
||||
|
||||
if (auto* paintable = m_text_node->paintable())
|
||||
paintable->set_selected(true);
|
||||
|
||||
if (m_placeholder_text_node)
|
||||
m_placeholder_text_node->invalidate_style(DOM::StyleInvalidationReason::DidReceiveFocus);
|
||||
|
||||
if (auto cursor = document().cursor_position(); !cursor || m_text_node != cursor->node())
|
||||
document().set_cursor_position(DOM::Position::create(realm(), *m_text_node, 0));
|
||||
}
|
||||
|
||||
void HTMLTextAreaElement::did_lose_focus()
|
||||
|
@ -86,6 +87,9 @@ void HTMLTextAreaElement::did_lose_focus()
|
|||
if (m_text_node)
|
||||
m_text_node->invalidate_style(DOM::StyleInvalidationReason::DidLoseFocus);
|
||||
|
||||
if (auto* paintable = m_text_node->paintable())
|
||||
paintable->set_selected(false);
|
||||
|
||||
if (m_placeholder_text_node)
|
||||
m_placeholder_text_node->invalidate_style(DOM::StyleInvalidationReason::DidLoseFocus);
|
||||
|
||||
|
@ -206,7 +210,7 @@ void HTMLTextAreaElement::set_raw_value(String value)
|
|||
m_api_value.clear();
|
||||
|
||||
if (m_raw_value != old_raw_value)
|
||||
relevant_value_was_changed(m_text_node);
|
||||
relevant_value_was_changed();
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element:concept-fe-api-value-3
|
||||
|
@ -448,7 +452,7 @@ void HTMLTextAreaElement::form_associated_element_attribute_changed(FlyString co
|
|||
}
|
||||
}
|
||||
|
||||
void HTMLTextAreaElement::did_edit_text_node(Badge<DOM::Document>)
|
||||
void HTMLTextAreaElement::did_edit_text_node()
|
||||
{
|
||||
VERIFY(m_text_node);
|
||||
set_raw_value(m_text_node->data());
|
||||
|
@ -474,15 +478,4 @@ void HTMLTextAreaElement::queue_firing_input_event()
|
|||
});
|
||||
}
|
||||
|
||||
void HTMLTextAreaElement::selection_was_changed(size_t selection_start, size_t selection_end)
|
||||
{
|
||||
if (!m_text_node || !document().cursor_position() || document().cursor_position()->node() != m_text_node)
|
||||
return;
|
||||
|
||||
document().set_cursor_position(DOM::Position::create(realm(), *m_text_node, selection_end));
|
||||
|
||||
if (auto selection = document().get_selection())
|
||||
MUST(selection->set_base_and_extent(*m_text_node, selection_start, *m_text_node, selection_end));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ public:
|
|||
}
|
||||
|
||||
// ^DOM::EditableTextNodeOwner
|
||||
virtual void did_edit_text_node(Badge<DOM::Document>) override;
|
||||
virtual void did_edit_text_node() override;
|
||||
|
||||
// ^EventTarget
|
||||
// https://html.spec.whatwg.org/multipage/interaction.html#the-tabindex-attribute:the-textarea-element
|
||||
|
@ -123,8 +123,7 @@ public:
|
|||
|
||||
void set_dirty_value_flag(Badge<FormAssociatedElement>, bool flag) { m_dirty_value = flag; }
|
||||
|
||||
protected:
|
||||
void selection_was_changed(size_t selection_start, size_t selection_end) override;
|
||||
virtual JS::GCPtr<DOM::Text> form_associated_element_to_text_node() override { return m_text_node; }
|
||||
|
||||
private:
|
||||
HTMLTextAreaElement(DOM::Document&, DOM::QualifiedName);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#include <AK/CharacterTypes.h>
|
||||
#include <AK/TypeCasts.h>
|
||||
#include <AK/Utf8View.h>
|
||||
#include <LibWeb/DOM/Position.h>
|
||||
#include <LibWeb/Layout/Box.h>
|
||||
#include <LibWeb/Layout/BreakNode.h>
|
||||
#include <LibWeb/Layout/LineBox.h>
|
||||
|
@ -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())
|
||||
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()) {
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020-2021, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <AK/Utf8View.h>
|
||||
#include <LibUnicode/Segmenter.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/DOM/Position.h>
|
||||
#include <LibWeb/DOM/Range.h>
|
||||
#include <LibWeb/DOM/Text.h>
|
||||
#include <LibWeb/HTML/BrowsingContext.h>
|
||||
#include <LibWeb/Layout/Viewport.h>
|
||||
#include <LibWeb/Page/EditEventHandler.h>
|
||||
|
||||
namespace Web {
|
||||
|
||||
void EditEventHandler::handle_delete_character_after(JS::NonnullGCPtr<DOM::Document> document, JS::NonnullGCPtr<DOM::Position> cursor_position)
|
||||
{
|
||||
auto& node = verify_cast<DOM::Text>(*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<DOM::Document> document, DOM::Range& range)
|
||||
{
|
||||
auto* start = verify_cast<DOM::Text>(range.start_container());
|
||||
auto* end = verify_cast<DOM::Text>(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<DOM::Node*> 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<DOM::Document> document, JS::NonnullGCPtr<DOM::Position> 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<DOM::Document> document, JS::NonnullGCPtr<DOM::Position> position, String data)
|
||||
{
|
||||
if (is<DOM::Text>(*position->node())) {
|
||||
auto& node = verify_cast<DOM::Text>(*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<DOM::Text>(realm, node.document(), data);
|
||||
MUST(node.append_child(*text));
|
||||
position->set_node(text);
|
||||
position->set_offset(1);
|
||||
}
|
||||
|
||||
document->user_did_edit_document_text({});
|
||||
}
|
||||
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020-2021, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Types.h>
|
||||
#include <LibJS/Forward.h>
|
||||
#include <LibJS/Heap/GCPtr.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
|
||||
namespace Web {
|
||||
|
||||
class EditEventHandler {
|
||||
public:
|
||||
explicit EditEventHandler()
|
||||
{
|
||||
}
|
||||
|
||||
~EditEventHandler() = default;
|
||||
|
||||
void handle_delete_character_after(JS::NonnullGCPtr<DOM::Document>, JS::NonnullGCPtr<DOM::Position>);
|
||||
void handle_delete(JS::NonnullGCPtr<DOM::Document>, DOM::Range&);
|
||||
void handle_insert(JS::NonnullGCPtr<DOM::Document>, JS::NonnullGCPtr<DOM::Position>, u32 code_point);
|
||||
void handle_insert(JS::NonnullGCPtr<DOM::Document>, JS::NonnullGCPtr<DOM::Position>, String);
|
||||
};
|
||||
|
||||
}
|
|
@ -7,27 +7,22 @@
|
|||
|
||||
#include <LibUnicode/CharacterTypes.h>
|
||||
#include <LibUnicode/Segmenter.h>
|
||||
#include <LibWeb/DOM/Range.h>
|
||||
#include <LibWeb/DOM/Text.h>
|
||||
#include <LibWeb/HTML/BrowsingContext.h>
|
||||
#include <LibWeb/HTML/CloseWatcherManager.h>
|
||||
#include <LibWeb/HTML/Focus.h>
|
||||
#include <LibWeb/HTML/FormAssociatedElement.h>
|
||||
#include <LibWeb/HTML/HTMLAnchorElement.h>
|
||||
#include <LibWeb/HTML/HTMLFormElement.h>
|
||||
#include <LibWeb/HTML/HTMLIFrameElement.h>
|
||||
#include <LibWeb/HTML/HTMLImageElement.h>
|
||||
#include <LibWeb/HTML/HTMLInputElement.h>
|
||||
#include <LibWeb/HTML/HTMLMediaElement.h>
|
||||
#include <LibWeb/HTML/HTMLTextAreaElement.h>
|
||||
#include <LibWeb/HTML/HTMLVideoElement.h>
|
||||
#include <LibWeb/Layout/Viewport.h>
|
||||
#include <LibWeb/Page/DragAndDropEventHandler.h>
|
||||
#include <LibWeb/Page/EditEventHandler.h>
|
||||
#include <LibWeb/Page/EventHandler.h>
|
||||
#include <LibWeb/Page/Page.h>
|
||||
#include <LibWeb/Painting/PaintableBox.h>
|
||||
#include <LibWeb/Painting/TextPaintable.h>
|
||||
#include <LibWeb/Selection/Selection.h>
|
||||
#include <LibWeb/UIEvents/EventNames.h>
|
||||
#include <LibWeb/UIEvents/InputEvent.h>
|
||||
#include <LibWeb/UIEvents/InputTypes.h>
|
||||
|
@ -151,7 +146,6 @@ static CSSPixelPoint compute_mouse_event_offset(CSSPixelPoint position, Layout::
|
|||
|
||||
EventHandler::EventHandler(Badge<HTML::Navigable>, HTML::Navigable& navigable)
|
||||
: m_navigable(navigable)
|
||||
, m_edit_event_handler(make<EditEventHandler>())
|
||||
, m_drag_and_drop_event_handler(make<DragAndDropEventHandler>())
|
||||
{
|
||||
}
|
||||
|
@ -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,26 +580,27 @@ 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 (m_mouse_selection_target) {
|
||||
if (hit.has_value()) {
|
||||
m_mouse_selection_target->set_selection_focus(*hit->paintable->dom_node(), hit->index_in_node);
|
||||
}
|
||||
} 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
|
||||
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 (should_set_cursor_position)
|
||||
document.set_cursor_position(DOM::Position::create(realm, *hit->dom_node(), *start_index));
|
||||
|
||||
document.set_needs_display();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto& page = m_navigable->page();
|
||||
|
||||
|
@ -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<DOM::Range> {
|
||||
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<HTML::HTMLMediaElement>(element)) {
|
||||
auto& media_element = static_cast<HTML::HTMLMediaElement&>(*element);
|
||||
if (media_element.handle_keydown({}, key, modifiers).release_value_but_fixme_should_propagate_errors())
|
||||
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 = [&]() {
|
||||
auto collapse = modifiers & UIEvents::Mod_Shift ? InputEventsTarget::CollapseSelection::No : InputEventsTarget::CollapseSelection::Yes;
|
||||
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();
|
||||
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<DOM::Text&>(*node);
|
||||
if (is<HTML::HTMLInputElement>(text_node.editable_text_node_owner()))
|
||||
input_element = static_cast<HTML::HTMLInputElement*>(text_node.editable_text_node_owner());
|
||||
} else if (node->is_html_input_element()) {
|
||||
input_element = static_cast<HTML::HTMLInputElement*>(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())
|
||||
auto* target = active_document->active_input_events_target();
|
||||
if (!target)
|
||||
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());
|
||||
}
|
||||
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<HTML::FormAssociatedTextControlElement&> target {};
|
||||
if (is<HTML::HTMLInputElement>(*shadow_host))
|
||||
target = static_cast<HTML::HTMLInputElement&>(*shadow_host);
|
||||
else if (is<HTML::HTMLTextAreaElement>(*shadow_host))
|
||||
target = static_cast<HTML::HTMLTextAreaElement&>(*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)
|
||||
|
|
|
@ -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<HTML::Navigable> m_navigable;
|
||||
|
||||
bool m_in_mouse_selection { false };
|
||||
InputEventsTarget* m_mouse_selection_target { nullptr };
|
||||
|
||||
JS::GCPtr<Painting::Paintable> m_mouse_event_tracking_paintable;
|
||||
|
||||
NonnullOwnPtr<EditEventHandler> m_edit_event_handler;
|
||||
NonnullOwnPtr<DragAndDropEventHandler> m_drag_and_drop_event_handler;
|
||||
|
||||
WeakPtr<DOM::EventTarget> m_mousedown_target;
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
#include <LibUnicode/CharacterTypes.h>
|
||||
#include <LibWeb/CSS/SystemColor.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/DOM/Position.h>
|
||||
#include <LibWeb/DOM/Range.h>
|
||||
#include <LibWeb/HTML/HTMLHtmlElement.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
|
@ -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())
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
*/
|
||||
|
||||
#include <LibWeb/DOM/Range.h>
|
||||
#include <LibWeb/HTML/FormAssociatedElement.h>
|
||||
#include <LibWeb/HTML/HTMLInputElement.h>
|
||||
#include <LibWeb/HTML/HTMLTextAreaElement.h>
|
||||
#include <LibWeb/Layout/Viewport.h>
|
||||
#include <LibWeb/Painting/PaintableBox.h>
|
||||
#include <LibWeb/Painting/TextPaintable.h>
|
||||
|
@ -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<HTML::FormAssociatedTextControlElement>(*focused_element)) {
|
||||
HTML::FormAssociatedTextControlElement const* text_control_element = nullptr;
|
||||
if (is<HTML::HTMLInputElement>(*focused_element)) {
|
||||
auto const& input_element = static_cast<HTML::HTMLInputElement const&>(*focused_element);
|
||||
text_control_element = static_cast<HTML::FormAssociatedTextControlElement const*>(&input_element);
|
||||
} else if (is<HTML::HTMLTextAreaElement>(*focused_element)) {
|
||||
auto const& textarea_element = static_cast<HTML::HTMLTextAreaElement const&>(*focused_element);
|
||||
text_control_element = static_cast<HTML::FormAssociatedTextControlElement const*>(&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
|
||||
|
|
|
@ -46,7 +46,7 @@ public:
|
|||
RefPtr<Gfx::GlyphRun> 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(); }
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#include <LibWeb/Bindings/Intrinsics.h>
|
||||
#include <LibWeb/Bindings/SelectionPrototype.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/DOM/Position.h>
|
||||
#include <LibWeb/DOM/Range.h>
|
||||
#include <LibWeb/Selection/Selection.h>
|
||||
|
||||
|
@ -471,4 +472,16 @@ void Selection::set_range(JS::GCPtr<DOM::Range> range)
|
|||
m_range->set_associated_selection({}, this);
|
||||
}
|
||||
|
||||
JS::GCPtr<DOM::Position> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -58,6 +58,9 @@ public:
|
|||
// Non-standard accessor for the selection's document.
|
||||
JS::NonnullGCPtr<DOM::Document> document() const;
|
||||
|
||||
// Non-standard
|
||||
JS::GCPtr<DOM::Position> cursor_position() const;
|
||||
|
||||
private:
|
||||
Selection(JS::NonnullGCPtr<JS::Realm>, JS::NonnullGCPtr<DOM::Document>);
|
||||
|
||||
|
|
|
@ -1737,8 +1737,8 @@ Messages::WebDriverClient::ElementSendKeysResponse WebDriverConnection::element_
|
|||
// -> element is content editable
|
||||
else if (is<Web::HTML::HTMLElement>(*element) && static_cast<Web::HTML::HTMLElement&>(*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<Web::HTML::FormAssociatedTextControlElement>(*element)) {
|
||||
|
|
Loading…
Reference in a new issue