LibWeb: Refactor "editable" and "editing host" concepts

The DOM spec defines what it means for an element to be an "editing
host", and the Editing spec does the same for the "editable" concept.
Replace our `Node::is_editable()` implementation with these
spec-compliant algorithms.

An editing host is an element that has the properties to make its
contents effectively editable. Editable elements are descendants of an
editing host. Concepts like the inheritable contenteditable attribute
are propagated through the editable algorithm.
This commit is contained in:
Jelle Raaijmakers 2024-12-06 11:41:20 +01:00 committed by Jelle Raaijmakers
commit 1c55153d43
Notes: github-actions[bot] 2024-12-10 13:55:36 +00:00
22 changed files with 85 additions and 102 deletions

View file

@ -2000,11 +2000,6 @@ String const& Document::compat_mode() const
return css1_compat;
}
bool Document::is_editable() const
{
return m_editable;
}
// https://html.spec.whatwg.org/multipage/interaction.html#dom-documentorshadowroot-activeelement
void Document::update_active_element()
{
@ -2060,9 +2055,8 @@ void Document::set_focused_element(Element* element)
if (auto* invalidation_target = find_common_ancestor(old_focused_element, m_focused_element) ?: this)
invalidation_target->invalidate_style(StyleInvalidationReason::FocusedElementChange);
if (m_focused_element) {
if (m_focused_element)
m_focused_element->did_receive_focus();
}
if (paintable())
paintable()->set_needs_display();
@ -5538,7 +5532,7 @@ InputEventsTarget* Document::active_input_events_target()
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())
if (focused_element->is_editable_or_editing_host())
return m_editing_host_manager;
return nullptr;
}
@ -5546,9 +5540,8 @@ InputEventsTarget* Document::active_input_events_target()
GC::Ptr<DOM::Position> Document::cursor_position() const
{
auto const* focused_element = this->focused_element();
if (!focused_element) {
if (!focused_element)
return nullptr;
}
Optional<HTML::FormAssociatedTextControlElement const&> target {};
if (is<HTML::HTMLInputElement>(*focused_element))
@ -5556,13 +5549,11 @@ GC::Ptr<DOM::Position> Document::cursor_position() const
else if (is<HTML::HTMLTextAreaElement>(*focused_element))
target = static_cast<HTML::HTMLTextAreaElement const&>(*focused_element);
if (target.has_value()) {
if (target.has_value())
return target->cursor_position();
}
if (is<HTML::HTMLElement>(*focused_element) && static_cast<HTML::HTMLElement const*>(focused_element)->is_editable()) {
if (focused_element->is_editable_or_editing_host())
return m_selection->cursor_position();
}
return nullptr;
}

View file

@ -335,7 +335,6 @@ public:
String const& compat_mode() const;
void set_editable(bool editable) { m_editable = editable; }
virtual bool is_editable() const final;
Element* focused_element() { return m_focused_element.ptr(); }
Element const* focused_element() const { return m_focused_element.ptr(); }

View file

@ -36,14 +36,12 @@ void EditingHostManager::handle_insert(String const& data)
auto selection = m_document->get_selection();
auto selection_range = selection->range();
if (!selection_range) {
if (!selection_range)
return;
}
auto node = selection->anchor_node();
if (!node || !node->is_editable()) {
if (!node || !node->is_editable_or_editing_host())
return;
}
if (!is<DOM::Text>(*node)) {
auto& realm = node->realm();

View file

@ -2,6 +2,7 @@
* Copyright (c) 2018-2024, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021-2022, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
* Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -11,7 +12,6 @@
#include <LibGC/DeferGC.h>
#include <LibJS/Runtime/FunctionObject.h>
#include <LibRegex/Regex.h>
#include <LibURL/Origin.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/Bindings/NodePrototype.h>
#include <LibWeb/DOM/Attr.h>
@ -44,16 +44,18 @@
#include <LibWeb/HTML/HTMLSlotElement.h>
#include <LibWeb/HTML/HTMLStyleElement.h>
#include <LibWeb/HTML/HTMLTableElement.h>
#include <LibWeb/HTML/HTMLTextAreaElement.h>
#include <LibWeb/HTML/Navigable.h>
#include <LibWeb/HTML/NavigableContainer.h>
#include <LibWeb/HTML/Parser/HTMLParser.h>
#include <LibWeb/Infra/CharacterTypes.h>
#include <LibWeb/Layout/Node.h>
#include <LibWeb/Layout/TextNode.h>
#include <LibWeb/Layout/Viewport.h>
#include <LibWeb/MathML/MathMLElement.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/Painting/Paintable.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/SVG/SVGElement.h>
#include <LibWeb/SVG/SVGTitleElement.h>
#include <LibWeb/XLink/AttributeNames.h>
@ -1183,9 +1185,48 @@ void Node::set_document(Badge<Document>, Document& document)
}
}
// https://w3c.github.io/editing/docs/execCommand/#editable
bool Node::is_editable() const
{
return parent() && parent()->is_editable();
// Something is editable if it is a node; it is not an editing host;
if (is_editing_host())
return false;
// it does not have a contenteditable attribute set to the false state;
if (is<HTML::HTMLElement>(this) && static_cast<HTML::HTMLElement const&>(*this).content_editable_state() == HTML::ContentEditableState::False)
return false;
// its parent is an editing host or editable;
if (!parent() || !parent()->is_editable_or_editing_host())
return false;
// and either it is an HTML element,
if (is<HTML::HTMLElement>(this))
return true;
// or it is an svg or math element,
if (is<SVG::SVGElement>(this) || is<MathML::MathMLElement>(this))
return true;
// or it is not an Element and its parent is an HTML element.
return !is<Element>(this) && is<HTML::HTMLElement>(parent());
}
// https://html.spec.whatwg.org/multipage/interaction.html#editing-host
bool Node::is_editing_host() const
{
// NOTE: Both conditions below require this to be an HTML element.
if (!is<HTML::HTMLElement>(this))
return false;
// An editing host is either an HTML element with its contenteditable attribute in the true state or
// plaintext-only state,
auto state = static_cast<HTML::HTMLElement const&>(*this).content_editable_state();
if (state == HTML::ContentEditableState::True || state == HTML::ContentEditableState::PlaintextOnly)
return true;
// or a child HTML element of a Document whose design mode enabled is true.
return is<Document>(parent()) && static_cast<Document const&>(*parent()).design_mode_enabled_state();
}
void Node::set_layout_node(Badge<Layout::Node>, GC::Ref<Layout::Node> layout_node)

View file

@ -137,7 +137,9 @@ public:
// NOTE: This is intended for the JS bindings.
u16 node_type() const { return (u16)m_type; }
virtual bool is_editable() const;
bool is_editable() const;
bool is_editing_host() const;
bool is_editable_or_editing_host() const { return is_editable() || is_editing_host(); }
virtual bool is_dom_node() const final { return true; }
virtual bool is_html_element() const { return false; }

View file

@ -26,9 +26,6 @@ public:
// ^Node
virtual FlyString node_name() const override { return "#text"_fly_string; }
virtual bool is_editable() const override { return m_always_editable || CharacterData::is_editable(); }
void set_always_editable(bool b) { m_always_editable = b; }
Optional<size_t> max_length() const { return m_max_length; }
void set_max_length(Optional<size_t> max_length) { m_max_length = move(max_length); }
@ -51,7 +48,6 @@ protected:
private:
GC::Ptr<Element> m_owner;
bool m_always_editable { false };
Optional<size_t> m_max_length {};
bool m_is_password_input { false };
};