Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cherry-pick "LibWeb+LibWebView: Auto-select subtext when editing DOM nodes/attributes" #25295

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions Base/res/ladybird/inspector.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,14 @@ inspector.loadDOMTree = tree => {

for (let domNode of domNodes) {
domNode.addEventListener("dblclick", event => {
editDOMNode(domNode);
const type = domNode.dataset.nodeType;
const text = event.target.innerText;

if (type === "attribute" && event.target.classList.contains("attribute-value")) {
text = text.substring(1, text.length - 1);
}

editDOMNode(domNode, text);
event.preventDefault();
});
}
Expand Down Expand Up @@ -283,9 +290,6 @@ const createDOMEditor = (onHandleChange, onCancelChange) => {

setTimeout(() => {
input.focus();

// FIXME: Invoke `select` when it isn't just stubbed out.
// input.select();
});

return input;
Expand All @@ -298,7 +302,7 @@ const parseDOMAttributes = value => {
return element.children[0].attributes;
};

const editDOMNode = domNode => {
const editDOMNode = (domNode, textToSelect) => {
if (selectedDOMNode === null) {
return;
}
Expand Down Expand Up @@ -336,6 +340,18 @@ const editDOMNode = domNode => {
editor.value = domNode.innerText;
}

setTimeout(() => {
if (typeof textToSelect !== "undefined") {
const index = editor.value.indexOf(textToSelect);
if (index !== -1) {
editor.setSelectionRange(index, index + textToSelect.length);
return;
}
}

editor.select();
});

domNode.parentNode.replaceChild(editor, domNode);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
12389 PASS (didn't crash)
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ text-input selectionStart: 0 selectionEnd: 18 selectionDirection: none
text-input selectionStart: 2 selectionEnd: 4 selectionDirection: forward
text-input selectionStart: 1 selectionEnd: 4 selectionDirection: forward
text-input selectionStart: 1 selectionEnd: 5 selectionDirection: forward
text-input selectionStart: 18 selectionEnd: 18 selectionDirection: forward
text-input selectionStart: 18 selectionEnd: 18 selectionDirection: backward
text-input selectionStart: 6 selectionEnd: 6 selectionDirection: forward
text-input selectionStart: 6 selectionEnd: 6 selectionDirection: backward
textarea selectionStart: 0 selectionEnd: 9 selectionDirection: none
select event fired: 18 18
select event fired: 9 9
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<input type="text" id="input" value="12389" />
<script src="../include.js"></script>
<script>
test(() => {
let input = document.getElementById("input");

input.focus();
input.select();

input.type = "number";

const rect = input.getBoundingClientRect();
internals.click(rect.x + rect.width / 2, rect.y + rect.height / 2);

println("PASS (didn't crash)");
});
</script>
2 changes: 1 addition & 1 deletion Userland/Libraries/LibWeb/HTML/FormAssociatedElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ void FormAssociatedTextControlElement::set_the_selection_range(Optional<WebIDL::

// AD-HOC: Notify the element that the selection was changed, so it can perform
// element-specific updates.
selection_was_changed();
selection_was_changed(m_selection_start, m_selection_end);
}
}

Expand Down
4 changes: 2 additions & 2 deletions Userland/Libraries/LibWeb/HTML/FormAssociatedElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,6 @@ class FormAssociatedElement {
void form_node_was_removed();
void form_node_attribute_changed(FlyString const&, Optional<String> const&);

virtual void selection_was_changed() { }

private:
void reset_form_owner();

Expand Down Expand Up @@ -159,6 +157,8 @@ class FormAssociatedTextControlElement : public FormAssociatedElement {
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
void relevant_value_was_changed(JS::GCPtr<DOM::Text>);

virtual void selection_was_changed([[maybe_unused]] size_t selection_start, [[maybe_unused]] size_t selection_end) { }

private:
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-selection
WebIDL::UnsignedLong m_selection_start { 0 };
Expand Down
9 changes: 4 additions & 5 deletions Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2385,13 +2385,12 @@ HTMLInputElement::ValueAttributeMode HTMLInputElement::value_attribute_mode() co
VERIFY_NOT_REACHED();
}

void HTMLInputElement::selection_was_changed()
void HTMLInputElement::selection_was_changed(size_t selection_start, size_t selection_end)
{
auto selection = document().get_selection();
if (!selection || selection->range_count() == 0)
return;
document().set_cursor_position(DOM::Position::create(realm(), *m_text_node, selection_end));

MUST(selection->set_base_and_extent(*m_text_node, selection_start().value(), *m_text_node, selection_end().value()));
if (auto selection = document().get_selection())
MUST(selection->set_base_and_extent(*m_text_node, selection_start, *m_text_node, selection_end));
}

}
2 changes: 1 addition & 1 deletion Userland/Libraries/LibWeb/HTML/HTMLInputElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ class HTMLInputElement final
bool selection_or_range_applies() const;

protected:
void selection_was_changed() override;
void selection_was_changed(size_t selection_start, size_t selection_end) override;

private:
HTMLInputElement(DOM::Document&, DOM::QualifiedName);
Expand Down
9 changes: 4 additions & 5 deletions Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -461,13 +461,12 @@ void HTMLTextAreaElement::queue_firing_input_event()
});
}

void HTMLTextAreaElement::selection_was_changed()
void HTMLTextAreaElement::selection_was_changed(size_t selection_start, size_t selection_end)
{
auto selection = document().get_selection();
if (!selection || selection->range_count() == 0)
return;
document().set_cursor_position(DOM::Position::create(realm(), *m_text_node, selection_end));

MUST(selection->set_base_and_extent(*m_text_node, selection_start().value(), *m_text_node, selection_end().value()));
if (auto selection = document().get_selection())
MUST(selection->set_base_and_extent(*m_text_node, selection_start, *m_text_node, selection_end));
}

}
2 changes: 1 addition & 1 deletion Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class HTMLTextAreaElement final
void set_dirty_value_flag(Badge<FormAssociatedElement>, bool flag) { m_dirty_value = flag; }

protected:
void selection_was_changed() override;
void selection_was_changed(size_t selection_start, size_t selection_end) override;

private:
HTMLTextAreaElement(DOM::Document&, DOM::QualifiedName);
Expand Down
26 changes: 19 additions & 7 deletions Userland/Libraries/LibWeb/Internals/Internals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,20 +90,26 @@ void Internals::middle_click(double x, double y)
void Internals::click(double x, double y, UIEvents::MouseButton button)
{
auto& page = global_object().browsing_context()->page();
page.handle_mousedown({ x, y }, { x, y }, button, 0, 0);
page.handle_mouseup({ x, y }, { x, y }, button, 0, 0);

auto position = page.css_to_device_point({ x, y });
page.handle_mousedown(position, position, button, 0, 0);
page.handle_mouseup(position, position, button, 0, 0);
}

void Internals::move_pointer_to(double x, double y)
{
auto& page = global_object().browsing_context()->page();
page.handle_mousemove({ x, y }, { x, y }, 0, 0);

auto position = page.css_to_device_point({ x, y });
page.handle_mousemove(position, position, 0, 0);
}

void Internals::wheel(double x, double y, double delta_x, double delta_y)
{
auto& page = global_object().browsing_context()->page();
page.handle_mousewheel({ x, y }, { x, y }, 0, 0, 0, delta_x, delta_y);

auto position = page.css_to_device_point({ x, y });
page.handle_mousewheel(position, position, 0, 0, 0, delta_x, delta_y);
}

WebIDL::ExceptionOr<bool> Internals::dispatch_user_activated_event(DOM::EventTarget& target, DOM::Event& event)
Expand All @@ -124,19 +130,25 @@ void Internals::simulate_drag_start(double x, double y, String const& name, Stri
files.empend(name.to_byte_string(), MUST(ByteBuffer::copy(contents.bytes())));

auto& page = global_object().browsing_context()->page();
page.handle_drag_and_drop_event(DragEvent::Type::DragStart, { x, y }, { x, y }, UIEvents::MouseButton::Primary, 0, 0, move(files));

auto position = page.css_to_device_point({ x, y });
page.handle_drag_and_drop_event(DragEvent::Type::DragStart, position, position, UIEvents::MouseButton::Primary, 0, 0, move(files));
}

void Internals::simulate_drag_move(double x, double y)
{
auto& page = global_object().browsing_context()->page();
page.handle_drag_and_drop_event(DragEvent::Type::DragMove, { x, y }, { x, y }, UIEvents::MouseButton::Primary, 0, 0, {});

auto position = page.css_to_device_point({ x, y });
page.handle_drag_and_drop_event(DragEvent::Type::DragMove, position, position, UIEvents::MouseButton::Primary, 0, 0, {});
}

void Internals::simulate_drop(double x, double y)
{
auto& page = global_object().browsing_context()->page();
page.handle_drag_and_drop_event(DragEvent::Type::Drop, { x, y }, { x, y }, UIEvents::MouseButton::Primary, 0, 0, {});

auto position = page.css_to_device_point({ x, y });
page.handle_drag_and_drop_event(DragEvent::Type::Drop, position, position, UIEvents::MouseButton::Primary, 0, 0, {});
}

}
46 changes: 21 additions & 25 deletions Userland/Libraries/LibWeb/Page/EventHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/

#include <LibUnicode/Segmenter.h>
#include <LibWeb/DOM/Range.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/HTML/BrowsingContext.h>
Expand Down Expand Up @@ -687,30 +688,16 @@ bool EventHandler::handle_doubleclick(CSSPixelPoint viewport_position, CSSPixelP
auto& hit_dom_node = const_cast<DOM::Text&>(verify_cast<DOM::Text>(*hit_paintable.dom_node()));
auto const& text_for_rendering = hit_paintable.text_for_rendering();

int first_word_break_before = [&] {
// Start from one before the index position to prevent selecting only spaces between words, caused by the addition below.
// This also helps us dealing with cases where index is equal to the string length.
for (int i = result->index_in_node - 1; i >= 0; --i) {
if (is_ascii_space(text_for_rendering.bytes_as_string_view()[i])) {
// Don't include the space in the selection
return i + 1;
}
}
return 0;
}();
auto& segmenter = word_segmenter();
segmenter.set_segmented_text(text_for_rendering);

int first_word_break_after = [&] {
for (size_t i = result->index_in_node; i < text_for_rendering.bytes().size(); ++i) {
if (is_ascii_space(text_for_rendering.bytes_as_string_view()[i]))
return i;
}
return text_for_rendering.bytes().size();
}();
auto previous_boundary = segmenter.previous_boundary(result->index_in_node, Unicode::Segmenter::Inclusive::Yes).value_or(0);
auto next_boundary = segmenter.next_boundary(result->index_in_node).value_or(text_for_rendering.byte_count());

auto& realm = node->document().realm();
document.set_cursor_position(DOM::Position::create(realm, hit_dom_node, first_word_break_after));
document.set_cursor_position(DOM::Position::create(realm, hit_dom_node, next_boundary));
if (auto selection = node->document().get_selection()) {
(void)selection->set_base_and_extent(hit_dom_node, first_word_break_before, hit_dom_node, first_word_break_after);
(void)selection->set_base_and_extent(hit_dom_node, previous_boundary, hit_dom_node, next_boundary);
}
update_selection_range_for_input_or_textarea();
}
Expand Down Expand Up @@ -1184,7 +1171,9 @@ void EventHandler::update_selection_range_for_input_or_textarea()
auto& root = node.root();
if (!root.is_shadow_root())
return;
auto& shadow_host = *root.parent_or_shadow_host();
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();
Expand All @@ -1193,13 +1182,20 @@ void EventHandler::update_selection_range_for_input_or_textarea()
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 (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);
}

Unicode::Segmenter& EventHandler::word_segmenter()
{
if (!m_word_segmenter)
m_word_segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Word);
return *m_word_segmenter;
}

}
5 changes: 5 additions & 0 deletions Userland/Libraries/LibWeb/Page/EventHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <LibGfx/Forward.h>
#include <LibJS/Heap/Cell.h>
#include <LibJS/Heap/GCPtr.h>
#include <LibUnicode/Forward.h>
#include <LibWeb/Forward.h>
#include <LibWeb/Page/InputEvent.h>
#include <LibWeb/PixelUnits.h>
Expand Down Expand Up @@ -41,6 +42,8 @@ class EventHandler {

void visit_edges(JS::Cell::Visitor& visitor) const;

Unicode::Segmenter& word_segmenter();

private:
bool focus_next_element();
bool focus_previous_element();
Expand Down Expand Up @@ -74,6 +77,8 @@ class EventHandler {
WeakPtr<DOM::EventTarget> m_mousedown_target;

Optional<CSSPixelPoint> m_mousemove_previous_screen_position;

OwnPtr<Unicode::Segmenter> m_word_segmenter;
};

}
Loading