mirror of
https://github.com/LadybirdBrowser/ladybird
synced 2026-04-25 17:25:08 +02:00
LibWeb: Selection toString focused text control delegation
Allows selected text in form controls to be copied to clipboard.
This commit is contained in:
committed by
Shannon Booth
parent
451177f1f4
commit
7385569a02
Notes:
github-actions[bot]
2026-01-02 17:41:12 +00:00
Author: https://github.com/jonbgamble Commit: https://github.com/LadybirdBrowser/ladybird/commit/7385569a024 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/7292 Reviewed-by: https://github.com/shannonbooth ✅
@@ -890,6 +890,18 @@ void FormAssociatedTextControlElement::handle_delete(DeleteDirection direction)
|
||||
did_edit_text_node();
|
||||
}
|
||||
|
||||
Optional<Utf16String> FormAssociatedTextControlElement::selected_text_for_stringifier() const
|
||||
{
|
||||
// https://w3c.github.io/selection-api/#dom-selection-stringifier
|
||||
// Used for clipboard copy and window.getSelection().toString() when this element is active.
|
||||
size_t start = this->selection_start();
|
||||
size_t end = this->selection_end();
|
||||
if (start >= end)
|
||||
return {};
|
||||
|
||||
return Utf16String::from_utf16(relevant_value().substring_view(start, end - start));
|
||||
}
|
||||
|
||||
void FormAssociatedTextControlElement::collapse_selection_to_offset(size_t position)
|
||||
{
|
||||
m_selection_start = position;
|
||||
|
||||
@@ -197,8 +197,9 @@ class WEB_API FormAssociatedTextControlElement
|
||||
, public InputEventsTarget {
|
||||
public:
|
||||
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
|
||||
virtual Utf16String relevant_value() = 0;
|
||||
virtual Utf16String relevant_value() const = 0;
|
||||
virtual WebIDL::ExceptionOr<void> set_relevant_value(Utf16String const&) = 0;
|
||||
virtual Optional<Utf16String> selected_text_for_stringifier() const;
|
||||
|
||||
virtual void set_dirty_value_flag(bool flag) = 0;
|
||||
|
||||
|
||||
@@ -1447,6 +1447,9 @@ void HTMLInputElement::did_receive_focus()
|
||||
|
||||
if (m_placeholder_text_node)
|
||||
m_placeholder_text_node->invalidate_style(DOM::StyleInvalidationReason::DidReceiveFocus);
|
||||
|
||||
if (has_selectable_text())
|
||||
document().get_selection()->remove_all_ranges();
|
||||
}
|
||||
|
||||
void HTMLInputElement::did_lose_focus()
|
||||
@@ -3298,6 +3301,7 @@ bool HTMLInputElement::has_selectable_text() const
|
||||
case TypeAttributeState::Time:
|
||||
case TypeAttributeState::LocalDateAndTime:
|
||||
case TypeAttributeState::Number:
|
||||
case TypeAttributeState::Email:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -3790,4 +3794,31 @@ bool HTMLInputElement::uses_button_layout() const
|
||||
TypeAttributeState::Button, TypeAttributeState::Color, TypeAttributeState::FileUpload);
|
||||
}
|
||||
|
||||
Optional<Utf16String> HTMLInputElement::selected_text_for_stringifier() const
|
||||
{
|
||||
// https://w3c.github.io/selection-api/#dom-selection-stringifier
|
||||
// Used for clipboard copy and window.getSelection().toString() when this element is active.
|
||||
if (!has_selectable_text())
|
||||
return {};
|
||||
|
||||
size_t start = this->selection_start();
|
||||
size_t end = this->selection_end();
|
||||
if (start >= end)
|
||||
return {};
|
||||
|
||||
switch (type_state()) {
|
||||
case TypeAttributeState::Text:
|
||||
case TypeAttributeState::Search:
|
||||
case TypeAttributeState::Telephone:
|
||||
case TypeAttributeState::URL:
|
||||
case TypeAttributeState::Email:
|
||||
return Utf16String::from_utf16(relevant_value().substring_view(start, end - start));
|
||||
|
||||
case TypeAttributeState::Password:
|
||||
return Utf16String::repeated(0x2022, end - start); // 0x2022 is BULLET character
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -85,8 +85,9 @@ public:
|
||||
WebIDL::ExceptionOr<void> set_value(Utf16String const&);
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
|
||||
virtual Utf16String relevant_value() override { return value(); }
|
||||
virtual Utf16String relevant_value() const override { return value(); }
|
||||
WebIDL::ExceptionOr<void> set_relevant_value(Utf16String const& value) override { return set_value(value); }
|
||||
virtual Optional<Utf16String> selected_text_for_stringifier() const override;
|
||||
|
||||
virtual void set_dirty_value_flag(bool flag) override { m_dirty_value = flag; }
|
||||
|
||||
|
||||
@@ -81,6 +81,8 @@ void HTMLTextAreaElement::did_receive_focus()
|
||||
|
||||
if (m_placeholder_text_node)
|
||||
m_placeholder_text_node->invalidate_style(DOM::StyleInvalidationReason::DidReceiveFocus);
|
||||
|
||||
document().get_selection()->remove_all_ranges();
|
||||
}
|
||||
|
||||
void HTMLTextAreaElement::did_lose_focus()
|
||||
|
||||
@@ -88,7 +88,7 @@ public:
|
||||
Utf16String api_value() const;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
|
||||
virtual Utf16String relevant_value() override { return api_value(); }
|
||||
virtual Utf16String relevant_value() const override { return api_value(); }
|
||||
virtual WebIDL::ExceptionOr<void> set_relevant_value(Utf16String const& value) override;
|
||||
|
||||
virtual void set_dirty_value_flag(bool flag) override { m_dirty_value = flag; }
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <LibWeb/HTML/BrowsingContextGroup.h>
|
||||
#include <LibWeb/HTML/DocumentState.h>
|
||||
#include <LibWeb/HTML/HTMLIFrameElement.h>
|
||||
#include <LibWeb/HTML/HTMLInputElement.h>
|
||||
#include <LibWeb/HTML/HistoryHandlingBehavior.h>
|
||||
#include <LibWeb/HTML/Navigable.h>
|
||||
#include <LibWeb/HTML/Navigation.h>
|
||||
@@ -2630,7 +2631,16 @@ String Navigable::selected_text() const
|
||||
auto document = active_document();
|
||||
if (!document)
|
||||
return String {};
|
||||
|
||||
auto const* input_element = as_if<HTML::HTMLInputElement>(document->active_element());
|
||||
if (input_element && input_element->type_state() == HTML::HTMLInputElement::TypeAttributeState::Password) {
|
||||
// Apparently nobody wants bullet characters. We leave the clipboard alone here like other browsers.
|
||||
return String {};
|
||||
}
|
||||
auto selection = document->get_selection();
|
||||
if (auto form_text = selection->try_form_control_selected_text_for_stringifier(); form_text.has_value())
|
||||
return form_text->to_utf8();
|
||||
|
||||
auto range = selection->range();
|
||||
if (!range)
|
||||
return String {};
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <LibWeb/DOM/Range.h>
|
||||
#include <LibWeb/DOM/Text.h>
|
||||
#include <LibWeb/GraphemeEdgeTracker.h>
|
||||
#include <LibWeb/HTML/FormAssociatedElement.h>
|
||||
#include <LibWeb/Selection/Selection.h>
|
||||
|
||||
namespace Web::Selection {
|
||||
@@ -505,8 +506,27 @@ bool Selection::contains_node(GC::Ref<DOM::Node> node, bool allow_partial_contai
|
||||
&& (end_relative_position == DOM::RelativeBoundaryPointPosition::Equal || end_relative_position == DOM::RelativeBoundaryPointPosition::After);
|
||||
}
|
||||
|
||||
Optional<Utf16String> Selection::try_form_control_selected_text_for_stringifier() const
|
||||
{
|
||||
// FIXME: According to https://bugzilla.mozilla.org/show_bug.cgi?id=85686#c69,
|
||||
// sometimes you want the selection from a previously focused form text, probably
|
||||
// when a button or context menu has temporarily stolen focus but page scripts
|
||||
// still expect window.getSelection() to have the goodies.
|
||||
|
||||
auto const* form_element = as_if<HTML::FormAssociatedTextControlElement>(m_document->active_element());
|
||||
if (!form_element)
|
||||
return {};
|
||||
return form_element->selected_text_for_stringifier();
|
||||
}
|
||||
|
||||
Utf16String Selection::to_string() const
|
||||
{
|
||||
// https://w3c.github.io/selection-api/#dom-selection-stringifier
|
||||
// If the selection is within a textarea or input element, it must return the
|
||||
// selected substring in its value.
|
||||
if (auto form_text = try_form_control_selected_text_for_stringifier(); form_text.has_value())
|
||||
return form_text.release_value();
|
||||
|
||||
// FIXME: This needs more work to be compatible with other engines.
|
||||
// See https://www.w3.org/Bugs/Public/show_bug.cgi?id=10583
|
||||
if (!m_range)
|
||||
|
||||
@@ -56,6 +56,7 @@ public:
|
||||
|
||||
// Non-standard convenience accessor for the selection's range.
|
||||
GC::Ptr<DOM::Range> range() const;
|
||||
Optional<Utf16String> try_form_control_selected_text_for_stringifier() const;
|
||||
|
||||
// Non-standard accessor for the selection's document.
|
||||
GC::Ref<DOM::Document> document() const;
|
||||
|
||||
@@ -763,7 +763,8 @@ void Application::initialize_actions()
|
||||
|
||||
m_copy_selection_action = Action::create("Copy"sv, ActionID::CopySelection, [this]() {
|
||||
if (auto view = active_web_view(); view.has_value())
|
||||
insert_clipboard_entry({ view->selected_text(), "text/plain"_string });
|
||||
if (!view->selected_text().is_empty())
|
||||
insert_clipboard_entry({ view->selected_text(), "text/plain"_string });
|
||||
});
|
||||
m_paste_action = Action::create("Paste"sv, ActionID::Paste, [this]() {
|
||||
if (auto view = active_web_view(); view.has_value())
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
initial, window.getSelection().toString() is ""
|
||||
after selecting foo, window.getSelection().toString() is "foo"
|
||||
after selecting bar, window.getSelection().toString() is "bar"
|
||||
after unselecting bar, window.getSelection().toString() is ""
|
||||
after selecting 'z' from normal text input, window.getSelection().toString() is "z"
|
||||
after selecting 'boo' from password input, window.getSelection().toString() is "•••"
|
||||
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Selection reflects textarea selection</title>
|
||||
<script src="./include.js"></script>
|
||||
<script>
|
||||
test(() => {
|
||||
function verify(expected, label) {
|
||||
let s = window.getSelection().toString();
|
||||
println(`${label}, window.getSelection().toString() is "${s}"`);
|
||||
if (s !== expected)
|
||||
throw new Error(`${label}: expected "${expected}", got "${s}"`);
|
||||
}
|
||||
|
||||
verify("", "initial");
|
||||
|
||||
let foo = document.getElementById("foo");
|
||||
let r = document.createRange();
|
||||
r.selectNodeContents(foo);
|
||||
let sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(r);
|
||||
verify("foo", "after selecting foo");
|
||||
|
||||
let textarea = document.getElementById("bar");
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
verify("bar", "after selecting bar");
|
||||
|
||||
textarea.setSelectionRange(0, 0);
|
||||
verify("", "after unselecting bar");
|
||||
|
||||
let input = document.getElementById("baz");
|
||||
input.focus();
|
||||
input.setSelectionRange(2, 3);
|
||||
verify("z", "after selecting 'z' from normal text input");
|
||||
let password = document.getElementById("boo");
|
||||
password.focus();
|
||||
password.setSelectionRange(0, 3);
|
||||
verify("•••", "after selecting 'boo' from password input");
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p id="foo">foo</p>
|
||||
<textarea id="bar">bar</textarea>
|
||||
<input id="baz" type="text" value="baz">
|
||||
<input id="boo" type="password" value="boo!">
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user