mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-04-21 20:15:17 +00:00
LibWeb: Implement user-select
This implements all values of user-select.
This commit is contained in:
parent
ba43dfe49a
commit
9370990ff2
Notes:
github-actions[bot]
2025-01-08 14:38:35 +00:00
Author: https://github.com/Psychpsyo Commit: https://github.com/LadybirdBrowser/ladybird/commit/9370990ff28 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3176 Reviewed-by: https://github.com/AtkinsSJ ✅
11 changed files with 274 additions and 29 deletions
|
@ -1434,6 +1434,12 @@ Optional<CSS::WritingMode> ComputedProperties::writing_mode() const
|
|||
return keyword_to_writing_mode(value.to_keyword());
|
||||
}
|
||||
|
||||
Optional<CSS::UserSelect> ComputedProperties::user_select() const
|
||||
{
|
||||
auto const& value = property(CSS::PropertyID::UserSelect);
|
||||
return keyword_to_user_select(value.to_keyword());
|
||||
}
|
||||
|
||||
Optional<CSS::MaskType> ComputedProperties::mask_type() const
|
||||
{
|
||||
auto const& value = property(CSS::PropertyID::MaskType);
|
||||
|
|
|
@ -159,6 +159,7 @@ public:
|
|||
Optional<CSS::Direction> direction() const;
|
||||
Optional<CSS::UnicodeBidi> unicode_bidi() const;
|
||||
Optional<CSS::WritingMode> writing_mode() const;
|
||||
Optional<CSS::UserSelect> user_select() const;
|
||||
|
||||
static Vector<CSS::Transformation> transformations_for_style_value(CSSStyleValue const& value);
|
||||
Vector<CSS::Transformation> transformations() const;
|
||||
|
|
|
@ -170,6 +170,7 @@ public:
|
|||
static CSS::Direction direction() { return CSS::Direction::Ltr; }
|
||||
static CSS::UnicodeBidi unicode_bidi() { return CSS::UnicodeBidi::Normal; }
|
||||
static CSS::WritingMode writing_mode() { return CSS::WritingMode::HorizontalTb; }
|
||||
static CSS::UserSelect user_select() { return CSS::UserSelect::Auto; }
|
||||
|
||||
// https://www.w3.org/TR/SVG/geometry.html
|
||||
static LengthPercentage cx() { return CSS::Length::make_px(0); }
|
||||
|
@ -424,6 +425,7 @@ public:
|
|||
CSS::Direction direction() const { return m_inherited.direction; }
|
||||
CSS::UnicodeBidi unicode_bidi() const { return m_noninherited.unicode_bidi; }
|
||||
CSS::WritingMode writing_mode() const { return m_inherited.writing_mode; }
|
||||
CSS::UserSelect user_select() const { return m_noninherited.user_select; }
|
||||
|
||||
CSS::LengthBox const& inset() const { return m_noninherited.inset; }
|
||||
const CSS::LengthBox& margin() const { return m_noninherited.margin; }
|
||||
|
@ -675,6 +677,8 @@ protected:
|
|||
CSS::ObjectFit object_fit { InitialValues::object_fit() };
|
||||
CSS::ObjectPosition object_position { InitialValues::object_position() };
|
||||
CSS::UnicodeBidi unicode_bidi { InitialValues::unicode_bidi() };
|
||||
CSS::UserSelect user_select { InitialValues::user_select() };
|
||||
|
||||
Optional<CSS::Transformation> rotate;
|
||||
Optional<CSS::Transformation> translate;
|
||||
Optional<CSS::Transformation> scale;
|
||||
|
@ -846,6 +850,7 @@ public:
|
|||
void set_direction(CSS::Direction value) { m_inherited.direction = value; }
|
||||
void set_unicode_bidi(CSS::UnicodeBidi value) { m_noninherited.unicode_bidi = value; }
|
||||
void set_writing_mode(CSS::WritingMode value) { m_inherited.writing_mode = value; }
|
||||
void set_user_select(CSS::UserSelect value) { m_noninherited.user_select = value; }
|
||||
|
||||
void set_fill(SVGPaint value) { m_inherited.fill = move(value); }
|
||||
void set_stroke(SVGPaint value) { m_inherited.stroke = move(value); }
|
||||
|
|
|
@ -538,6 +538,13 @@
|
|||
"normal",
|
||||
"plaintext"
|
||||
],
|
||||
"user-select": [
|
||||
"all",
|
||||
"auto",
|
||||
"contain",
|
||||
"none",
|
||||
"text"
|
||||
],
|
||||
"vertical-align": [
|
||||
"baseline",
|
||||
"bottom",
|
||||
|
|
|
@ -2825,12 +2825,8 @@
|
|||
"animation-type": "discrete",
|
||||
"inherited": false,
|
||||
"initial": "auto",
|
||||
"valid-identifiers": [
|
||||
"all",
|
||||
"auto",
|
||||
"contain",
|
||||
"none",
|
||||
"text"
|
||||
"valid-types": [
|
||||
"user-select"
|
||||
]
|
||||
},
|
||||
"vertical-align": {
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
#include <LibWeb/CSS/StyleValues/URLStyleValue.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/Dump.h>
|
||||
#include <LibWeb/HTML/FormAssociatedElement.h>
|
||||
#include <LibWeb/HTML/HTMLHtmlElement.h>
|
||||
#include <LibWeb/Layout/BlockContainer.h>
|
||||
#include <LibWeb/Layout/FormattingContext.h>
|
||||
|
@ -1004,6 +1005,9 @@ void NodeWithStyle::apply_style(const CSS::ComputedProperties& computed_style)
|
|||
if (auto writing_mode = computed_style.writing_mode(); writing_mode.has_value())
|
||||
computed_values.set_writing_mode(writing_mode.value());
|
||||
|
||||
if (auto user_select = computed_style.user_select(); user_select.has_value())
|
||||
computed_values.set_user_select(user_select.value());
|
||||
|
||||
propagate_style_to_anonymous_wrappers();
|
||||
}
|
||||
|
||||
|
@ -1214,4 +1218,53 @@ DOM::Document const& Node::document() const
|
|||
return m_dom_node->document();
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-ui/#propdef-user-select
|
||||
CSS::UserSelect Node::user_select_used_value() const
|
||||
{
|
||||
// The used value is the same as the computed value, except:
|
||||
auto computed_value = computed_values().user_select();
|
||||
|
||||
// 1. on editable elements where the used value is always 'contain' regardless of the computed value
|
||||
|
||||
// 2. when the computed value is 'auto', in which case the used value is one of the other values as defined below
|
||||
|
||||
// For the purpose of this specification, an editable element is either an editing host or a mutable form control with
|
||||
// textual content, such as textarea.
|
||||
auto* form_control = dynamic_cast<HTML::FormAssociatedTextControlElement const*>(dom_node());
|
||||
// FIXME: Check if this needs to exclude input elements with types such as color or range, and if so, which ones exactly.
|
||||
if ((dom_node() && dom_node()->is_editing_host()) || (form_control && form_control->is_mutable())) {
|
||||
return CSS::UserSelect::Contain;
|
||||
} else if (computed_value == CSS::UserSelect::Auto) {
|
||||
// The used value of 'auto' is determined as follows:
|
||||
// - On the '::before' and '::after' pseudo-elements, the used value is 'none'
|
||||
if (is_generated_for_before_pseudo_element() || is_generated_for_after_pseudo_element()) {
|
||||
return CSS::UserSelect::None;
|
||||
}
|
||||
|
||||
// - If the element is an editable element, the used value is 'contain'
|
||||
// NOTE: We already handled this above.
|
||||
|
||||
auto parent_element = parent();
|
||||
if (parent_element) {
|
||||
auto parent_used_value = parent_element->user_select_used_value();
|
||||
|
||||
// - Otherwise, if the used value of user-select on the parent of this element is 'all', the used value is 'all'
|
||||
if (parent_used_value == CSS::UserSelect::All) {
|
||||
return CSS::UserSelect::All;
|
||||
}
|
||||
|
||||
// - Otherwise, if the used value of user-select on the parent of this element is 'none', the used value is
|
||||
// 'none'
|
||||
if (parent_used_value == CSS::UserSelect::None) {
|
||||
return CSS::UserSelect::None;
|
||||
}
|
||||
}
|
||||
|
||||
// - Otherwise, the used value is 'text'
|
||||
return CSS::UserSelect::Text;
|
||||
}
|
||||
|
||||
return computed_value;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -188,6 +188,9 @@ public:
|
|||
return false;
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-ui/#propdef-user-select
|
||||
CSS::UserSelect user_select_used_value() const;
|
||||
|
||||
protected:
|
||||
Node(DOM::Document&, DOM::Node*);
|
||||
|
||||
|
|
|
@ -187,6 +187,139 @@ static CSSPixelPoint compute_mouse_event_offset(CSSPixelPoint position, Painting
|
|||
return offset;
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-ui/#propdef-user-select
|
||||
static void set_user_selection(GC::Ptr<DOM::Node> anchor_node, unsigned anchor_offset, GC::Ptr<DOM::Node> focus_node, unsigned focus_offset, Selection::Selection* selection, CSS::UserSelect user_select)
|
||||
{
|
||||
// https://drafts.csswg.org/css-ui/#valdef-user-select-contain
|
||||
// NOTE: This is clamping the focus node to any node with user-select: contain that stands between it and the anchor node.
|
||||
if (focus_node != anchor_node) {
|
||||
// UAs must not allow a selection which is started in this element to be extended outside of this element.
|
||||
auto potential_contain_node = anchor_node;
|
||||
|
||||
// NOTE: The way we do this is searching up the tree from the anchor, to find 'this element', i.e. its nearest contain ancestor.
|
||||
// We stop the search early when we reach an element that contains both the anchor and the focus node, as this means they
|
||||
// are inside the same contain element, or not in a contain element at all.
|
||||
// This takes care of the "selection trying to escape from a contain" case.
|
||||
while (
|
||||
(!potential_contain_node->is_element() || potential_contain_node->layout_node()->user_select_used_value() != CSS::UserSelect::Contain) && potential_contain_node->parent() && !potential_contain_node->is_inclusive_ancestor_of(*focus_node)) {
|
||||
potential_contain_node = potential_contain_node->parent();
|
||||
}
|
||||
|
||||
if (
|
||||
potential_contain_node->layout_node()->user_select_used_value() == CSS::UserSelect::Contain && !potential_contain_node->is_inclusive_ancestor_of(*focus_node)) {
|
||||
if (focus_node->is_before(*potential_contain_node)) {
|
||||
focus_offset = 0;
|
||||
} else {
|
||||
focus_offset = potential_contain_node->length();
|
||||
}
|
||||
focus_node = potential_contain_node;
|
||||
// NOTE: Prevents this from being handled again further down
|
||||
user_select = CSS::UserSelect::Contain;
|
||||
} else {
|
||||
// A selection started outside of this element must not end in this element. If the user attempts to create such a
|
||||
// selection, the UA must instead end the selection range at the element boundary.
|
||||
|
||||
// NOTE: This branch takes care of the "selection trying to intrude into a contain" case.
|
||||
// This is done by searching up the tree from the focus node, to see if there is a
|
||||
// contain element between it and the common ancestor that also includes the anchor.
|
||||
// We stop once reaching target_node, which is the common ancestor identified in step 1.
|
||||
// If target_node wasn't a common ancestor, we would not be here.
|
||||
auto target_node = potential_contain_node;
|
||||
potential_contain_node = focus_node;
|
||||
while (
|
||||
(!potential_contain_node->is_element() || potential_contain_node->layout_node()->user_select_used_value() != CSS::UserSelect::Contain) && potential_contain_node->parent() && potential_contain_node != target_node) {
|
||||
potential_contain_node = potential_contain_node->parent();
|
||||
}
|
||||
if (
|
||||
potential_contain_node->layout_node()->user_select_used_value() == CSS::UserSelect::Contain && !potential_contain_node->is_inclusive_ancestor_of(*anchor_node)) {
|
||||
if (potential_contain_node->is_before(*anchor_node)) {
|
||||
focus_node = potential_contain_node->next_in_pre_order();
|
||||
while (potential_contain_node->is_inclusive_ancestor_of(*focus_node)) {
|
||||
focus_node = focus_node->next_in_pre_order();
|
||||
}
|
||||
focus_offset = 0;
|
||||
} else {
|
||||
focus_node = potential_contain_node->previous_in_pre_order();
|
||||
while (potential_contain_node->is_inclusive_ancestor_of(*focus_node)) {
|
||||
focus_node = focus_node->previous_in_pre_order();
|
||||
}
|
||||
focus_offset = focus_node->length();
|
||||
}
|
||||
// NOTE: Prevents this from being handled again further down
|
||||
user_select = CSS::UserSelect::Contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (user_select) {
|
||||
case CSS::UserSelect::None:
|
||||
// https://drafts.csswg.org/css-ui/#valdef-user-select-none
|
||||
|
||||
// The UA must not allow selections to be started in this element.
|
||||
if (anchor_node == focus_node) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A selection started outside of this element must not end in this element. If the user attempts to create such a
|
||||
// selection, the UA must instead end the selection range at the element boundary.
|
||||
while (focus_node->parent() && focus_node->parent()->layout_node()->user_select_used_value() == CSS::UserSelect::None) {
|
||||
focus_node = focus_node->parent();
|
||||
}
|
||||
if (focus_node->is_before(*anchor_node)) {
|
||||
auto none_element = focus_node;
|
||||
do {
|
||||
focus_node = focus_node->next_in_pre_order();
|
||||
} while (none_element->is_inclusive_ancestor_of(*focus_node));
|
||||
focus_offset = 0;
|
||||
} else {
|
||||
focus_node = focus_node->previous_in_pre_order();
|
||||
focus_offset = focus_node->length();
|
||||
}
|
||||
break;
|
||||
case CSS::UserSelect::All:
|
||||
// https://drafts.csswg.org/css-ui/#valdef-user-select-all
|
||||
|
||||
// The content of the element must be selected atomically: If a selection would contain part of the element,
|
||||
// then the selection must contain the entire element including all its descendants. If the element is selected
|
||||
// and the used value of 'user-select' on its parent is 'all', then the parent must be included in the selection,
|
||||
// recursively.
|
||||
while (focus_node->parent() && focus_node->parent()->layout_node()->user_select_used_value() == CSS::UserSelect::All) {
|
||||
if (anchor_node == focus_node) {
|
||||
anchor_node = focus_node->parent();
|
||||
}
|
||||
focus_node = focus_node->parent();
|
||||
}
|
||||
|
||||
if (focus_node == anchor_node) {
|
||||
if (anchor_offset > focus_offset) {
|
||||
anchor_offset = focus_node->length();
|
||||
focus_offset = 0;
|
||||
} else {
|
||||
anchor_offset = 0;
|
||||
focus_offset = focus_node->length();
|
||||
}
|
||||
} else if (focus_node->is_before(*anchor_node)) {
|
||||
focus_offset = 0;
|
||||
} else {
|
||||
focus_offset = focus_node->length();
|
||||
}
|
||||
break;
|
||||
case CSS::UserSelect::Contain:
|
||||
// NOTE: This is handled at the start of this function
|
||||
break;
|
||||
case CSS::UserSelect::Text:
|
||||
// https://drafts.csswg.org/css-ui/#valdef-user-select-text
|
||||
|
||||
// The element imposes no constraint on the selection.
|
||||
break;
|
||||
case CSS::UserSelect::Auto:
|
||||
VERIFY_NOT_REACHED();
|
||||
break;
|
||||
}
|
||||
|
||||
(void)selection->set_base_and_extent(*anchor_node, anchor_offset, *focus_node, focus_offset);
|
||||
}
|
||||
|
||||
EventHandler::EventHandler(Badge<HTML::Navigable>, HTML::Navigable& navigable)
|
||||
: m_navigable(navigable)
|
||||
, m_drag_and_drop_event_handler(make<DragAndDropEventHandler>())
|
||||
|
@ -502,23 +635,29 @@ EventResult EventHandler::handle_mousedown(CSSPixelPoint viewport_position, CSSP
|
|||
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) {
|
||||
(void)selection->set_base_and_extent(*anchor_node, selection->anchor_offset(), *dom_node, result->index_in_node);
|
||||
// https://drafts.csswg.org/css-ui/#valdef-user-select-none
|
||||
// Attempting to start a selection in an element where user-select is none, such as by clicking in it or starting
|
||||
// a drag in it, must not cause a pre-existing selection to become unselected or to be affected in any way.
|
||||
auto user_select = paintable->layout_node().user_select_used_value();
|
||||
if (user_select != CSS::UserSelect::None) {
|
||||
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 {
|
||||
(void)selection->set_base_and_extent(*dom_node, result->index_in_node, *dom_node, result->index_in_node);
|
||||
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) {
|
||||
set_user_selection(*anchor_node, selection->anchor_offset(), *dom_node, result->index_in_node, selection, user_select);
|
||||
} else {
|
||||
set_user_selection(*dom_node, result->index_in_node, *dom_node, result->index_in_node, selection, user_select);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -635,9 +774,9 @@ EventResult EventHandler::handle_mousemove(CSSPixelPoint viewport_position, CSSP
|
|||
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);
|
||||
set_user_selection(*anchor_node, selection->anchor_offset(), *hit->paintable->dom_node(), hit->index_in_node, selection, hit->paintable->layout_node().user_select_used_value());
|
||||
} else {
|
||||
(void)selection->set_base_and_extent(*hit->paintable->dom_node(), hit->index_in_node, *hit->paintable->dom_node(), hit->index_in_node);
|
||||
set_user_selection(*hit->paintable->dom_node(), hit->index_in_node, *hit->paintable->dom_node(), hit->index_in_node, selection, hit->paintable->layout_node().user_select_used_value());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -751,7 +890,7 @@ EventResult EventHandler::handle_doubleclick(CSSPixelPoint viewport_position, CS
|
|||
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);
|
||||
set_user_selection(hit_dom_node, previous_boundary, hit_dom_node, next_boundary, selection, hit_paintable.layout_node().user_select_used_value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -315,14 +315,18 @@ void ViewportPaintable::recompute_selection_states(DOM::Range& range)
|
|||
|
||||
// 4. Mark the selection end node as End (if text) or Full (if anything else).
|
||||
if (auto* paintable = end_container->paintable()) {
|
||||
if (is<DOM::Text>(*end_container))
|
||||
if (is<DOM::Text>(*end_container) || end_container->is_ancestor_of(start_container)) {
|
||||
paintable->set_selection_state(SelectionState::End);
|
||||
else
|
||||
paintable->set_selection_state(SelectionState::Full);
|
||||
} else {
|
||||
paintable->for_each_in_inclusive_subtree([&](auto& layout_node) {
|
||||
layout_node.set_selection_state(SelectionState::Full);
|
||||
return TraversalDecision::Continue;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Mark the nodes between start node and end node (in tree order) as Full.
|
||||
for (auto* node = start_container->next_in_pre_order(); node && node != end_container; node = node->next_in_pre_order()) {
|
||||
for (auto* node = start_container->next_in_pre_order(); node && (node->is_before(end_container) || node->is_descendant_of(end_container)); node = node->next_in_pre_order()) {
|
||||
if (auto* paintable = node->paintable())
|
||||
paintable->set_selection_state(SelectionState::Full);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
Harness status: OK
|
||||
|
||||
Found 5 tests
|
||||
|
||||
5 Pass
|
||||
Pass e.style['user-select'] = "auto" should set the property value
|
||||
Pass e.style['user-select'] = "text" should set the property value
|
||||
Pass e.style['user-select'] = "none" should set the property value
|
||||
Pass e.style['user-select'] = "contain" should set the property value
|
||||
Pass e.style['user-select'] = "all" should set the property value
|
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>CSS UI Level 4: parsing user-select with valid values</title>
|
||||
<link rel="help" href="https://drafts.csswg.org/css-ui-4/#content-selection">
|
||||
<meta name="assert" content="user-select supports the full grammar 'auto | text | none | contain | all'.">
|
||||
<script src="../../../resources/testharness.js"></script>
|
||||
<script src="../../../resources/testharnessreport.js"></script>
|
||||
<script src="../../../css/support/parsing-testcommon.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
test_valid_value("user-select", "auto");
|
||||
test_valid_value("user-select", "text");
|
||||
test_valid_value("user-select", "none");
|
||||
test_valid_value("user-select", "contain");
|
||||
test_valid_value("user-select", "all");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Add table
Reference in a new issue