diff --git a/Libraries/LibWeb/CSS/SelectorEngine.cpp b/Libraries/LibWeb/CSS/SelectorEngine.cpp index fb08103f660..420018e19e7 100644 --- a/Libraries/LibWeb/CSS/SelectorEngine.cpp +++ b/Libraries/LibWeb/CSS/SelectorEngine.cpp @@ -411,7 +411,7 @@ static bool matches_read_write_pseudo_class(DOM::Element const& element) return true; } // - elements that are editing hosts or editable and are neither input elements nor textarea elements - return element.is_editable(); + return element.is_editable_or_editing_host(); } // https://www.w3.org/TR/selectors-4/#open-state diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index 5a4eb93b8b8..b852554b78b 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -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(focused_element); if (is(*focused_element)) return static_cast(focused_element); - if (is(*focused_element) && static_cast(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 Document::cursor_position() const { auto const* focused_element = this->focused_element(); - if (!focused_element) { + if (!focused_element) return nullptr; - } Optional target {}; if (is(*focused_element)) @@ -5556,13 +5549,11 @@ GC::Ptr Document::cursor_position() const else if (is(*focused_element)) target = static_cast(*focused_element); - if (target.has_value()) { + if (target.has_value()) return target->cursor_position(); - } - if (is(*focused_element) && static_cast(focused_element)->is_editable()) { + if (focused_element->is_editable_or_editing_host()) return m_selection->cursor_position(); - } return nullptr; } diff --git a/Libraries/LibWeb/DOM/Document.h b/Libraries/LibWeb/DOM/Document.h index 9c5661cf37e..f8cd52a6e88 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -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(); } diff --git a/Libraries/LibWeb/DOM/EditingHostManager.cpp b/Libraries/LibWeb/DOM/EditingHostManager.cpp index f66486833b2..0e1f20e7c9d 100644 --- a/Libraries/LibWeb/DOM/EditingHostManager.cpp +++ b/Libraries/LibWeb/DOM/EditingHostManager.cpp @@ -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(*node)) { auto& realm = node->realm(); diff --git a/Libraries/LibWeb/DOM/Node.cpp b/Libraries/LibWeb/DOM/Node.cpp index a57185b5a54..c349c5f6a7b 100644 --- a/Libraries/LibWeb/DOM/Node.cpp +++ b/Libraries/LibWeb/DOM/Node.cpp @@ -2,6 +2,7 @@ * Copyright (c) 2018-2024, Andreas Kling * Copyright (c) 2021-2022, Linus Groh * Copyright (c) 2021, Luke Wilde + * Copyright (c) 2024, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -11,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -44,16 +44,18 @@ #include #include #include +#include #include #include #include #include #include #include -#include +#include #include #include #include +#include #include #include @@ -1183,9 +1185,48 @@ void Node::set_document(Badge, 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(this) && static_cast(*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(this)) + return true; + + // or it is an svg or math element, + if (is(this) || is(this)) + return true; + + // or it is not an Element and its parent is an HTML element. + return !is(this) && is(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(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(*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(parent()) && static_cast(*parent()).design_mode_enabled_state(); } void Node::set_layout_node(Badge, GC::Ref layout_node) diff --git a/Libraries/LibWeb/DOM/Node.h b/Libraries/LibWeb/DOM/Node.h index 64499ee7c03..f1e509e872c 100644 --- a/Libraries/LibWeb/DOM/Node.h +++ b/Libraries/LibWeb/DOM/Node.h @@ -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; } diff --git a/Libraries/LibWeb/DOM/Text.h b/Libraries/LibWeb/DOM/Text.h index 1010858777b..d7ebcf038ca 100644 --- a/Libraries/LibWeb/DOM/Text.h +++ b/Libraries/LibWeb/DOM/Text.h @@ -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 max_length() const { return m_max_length; } void set_max_length(Optional max_length) { m_max_length = move(max_length); } @@ -51,7 +48,6 @@ protected: private: GC::Ptr m_owner; - bool m_always_editable { false }; Optional m_max_length {}; bool m_is_password_input { false }; }; diff --git a/Libraries/LibWeb/Editing/Commands.cpp b/Libraries/LibWeb/Editing/Commands.cpp index 8b370c382be..0dd3ac71c02 100644 --- a/Libraries/LibWeb/Editing/Commands.cpp +++ b/Libraries/LibWeb/Editing/Commands.cpp @@ -384,7 +384,7 @@ bool command_insert_paragraph_action(DOM::Document& document, String const&) // 2. If the active range's start node is neither editable nor an editing host, return true. auto& active_range = *selection.range(); GC::Ptr node = active_range.start_container(); - if (!node->is_editable() && !is_editing_host(*node)) + if (!node->is_editable_or_editing_host()) return true; // 3. Let node and offset be the active range's start node and offset. diff --git a/Libraries/LibWeb/Editing/ExecCommand.cpp b/Libraries/LibWeb/Editing/ExecCommand.cpp index ae709735709..5d65ead33ef 100644 --- a/Libraries/LibWeb/Editing/ExecCommand.cpp +++ b/Libraries/LibWeb/Editing/ExecCommand.cpp @@ -118,7 +118,7 @@ bool Document::query_command_enabled(FlyString const& command) // its start node is either editable or an editing host, auto start_node = active_range->start_container(); - if (!start_node->is_editable() && !Editing::is_editing_host(start_node)) + if (!start_node->is_editable_or_editing_host()) return false; // FIXME: the editing host of its start node is not an EditContext editing host, @@ -126,7 +126,7 @@ bool Document::query_command_enabled(FlyString const& command) // its end node is either editable or an editing host, auto& end_node = *active_range->end_container(); - if (!end_node.is_editable() && !Editing::is_editing_host(end_node)) + if (!end_node.is_editable_or_editing_host()) return false; // FIXME: the editing host of its end node is not an EditContext editing host, diff --git a/Libraries/LibWeb/Editing/Internal/Algorithms.cpp b/Libraries/LibWeb/Editing/Internal/Algorithms.cpp index 754f03ecd3f..daa6b975ef9 100644 --- a/Libraries/LibWeb/Editing/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/Editing/Internal/Algorithms.cpp @@ -201,7 +201,7 @@ String canonical_space_sequence(u32 length, bool non_breaking_start, bool non_br void canonicalize_whitespace(GC::Ref node, u32 offset, bool fix_collapsed_space) { // 1. If node is neither editable nor an editing host, abort these steps. - if (!node->is_editable() || !is_editing_host(node)) + if (!node->is_editable_or_editing_host()) return; // 2. Let start node equal node and let start offset equal offset. @@ -916,20 +916,6 @@ bool is_collapsed_whitespace_node(GC::Ref node) return false; } -// https://html.spec.whatwg.org/multipage/interaction.html#editing-host -bool is_editing_host(GC::Ref node) -{ - // An editing host is either an HTML element with its contenteditable attribute in the true - // state or plaintext-only state, or a child HTML element of a Document whose design mode - // enabled is true. - if (!is(*node)) - return false; - auto const& html_element = static_cast(*node); - return html_element.content_editable_state() == HTML::ContentEditableState::True - || html_element.content_editable_state() == HTML::ContentEditableState::PlaintextOnly - || node->document().design_mode_enabled_state(); -} - // https://w3c.github.io/editing/docs/execCommand/#element-with-inline-contents bool is_element_with_inline_contents(GC::Ref node) { diff --git a/Libraries/LibWeb/Editing/Internal/Algorithms.h b/Libraries/LibWeb/Editing/Internal/Algorithms.h index 441a708990d..e46b26c6c67 100644 --- a/Libraries/LibWeb/Editing/Internal/Algorithms.h +++ b/Libraries/LibWeb/Editing/Internal/Algorithms.h @@ -34,7 +34,6 @@ bool is_block_end_point(GC::Ref, u32 offset); bool is_block_node(GC::Ref); bool is_block_start_point(GC::Ref, u32 offset); bool is_collapsed_whitespace_node(GC::Ref); -bool is_editing_host(GC::Ref); bool is_element_with_inline_contents(GC::Ref); bool is_extraneous_line_break(GC::Ref); bool is_in_same_editing_host(GC::Ref, GC::Ref); diff --git a/Libraries/LibWeb/HTML/FormAssociatedElement.cpp b/Libraries/LibWeb/HTML/FormAssociatedElement.cpp index a187d32f11a..6927d7812ae 100644 --- a/Libraries/LibWeb/HTML/FormAssociatedElement.cpp +++ b/Libraries/LibWeb/HTML/FormAssociatedElement.cpp @@ -588,7 +588,7 @@ void FormAssociatedTextControlElement::set_the_selection_range(Optionalis_editable()) + if (!text_node || !is_mutable()) return; String data_for_insertion = data; @@ -613,7 +613,7 @@ void FormAssociatedTextControlElement::handle_insert(String const& data) void FormAssociatedTextControlElement::handle_delete(DeleteDirection direction) { auto text_node = form_associated_element_to_text_node(); - if (!text_node || !text_node->is_editable()) + if (!text_node || !is_mutable()) return; auto selection_start = this->selection_start(); auto selection_end = this->selection_end(); diff --git a/Libraries/LibWeb/HTML/FormAssociatedElement.h b/Libraries/LibWeb/HTML/FormAssociatedElement.h index 66e12a26eb7..bf2c53f74e2 100644 --- a/Libraries/LibWeb/HTML/FormAssociatedElement.h +++ b/Libraries/LibWeb/HTML/FormAssociatedElement.h @@ -170,6 +170,9 @@ 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; } + bool is_mutable() const { return m_is_mutable; } + void set_is_mutable(bool is_mutable) { m_is_mutable = is_mutable; } + virtual void did_edit_text_node() = 0; virtual GC::Ptr form_associated_element_to_text_node() = 0; @@ -205,6 +208,9 @@ private: // https://w3c.github.io/selection-api/#dfn-has-scheduled-selectionchange-event bool m_has_scheduled_selectionchange_event { false }; + + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#mutability + bool m_is_mutable { true }; }; } diff --git a/Libraries/LibWeb/HTML/HTMLElement.cpp b/Libraries/LibWeb/HTML/HTMLElement.cpp index bb61449831d..001ee6d104c 100644 --- a/Libraries/LibWeb/HTML/HTMLElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLElement.cpp @@ -86,25 +86,9 @@ void HTMLElement::set_dir(String const& dir) MUST(set_attribute(HTML::AttributeNames::dir, dir)); } -bool HTMLElement::is_editable() const -{ - switch (m_content_editable_state) { - case ContentEditableState::True: - case ContentEditableState::PlaintextOnly: - return true; - case ContentEditableState::False: - return false; - case ContentEditableState::Inherit: - return parent() && parent()->is_editable(); - default: - VERIFY_NOT_REACHED(); - } -} - bool HTMLElement::is_focusable() const { - return m_content_editable_state == ContentEditableState::True - || m_content_editable_state == ContentEditableState::PlaintextOnly; + return is_editing_host(); } // https://html.spec.whatwg.org/multipage/interaction.html#dom-iscontenteditable @@ -112,7 +96,7 @@ bool HTMLElement::is_content_editable() const { // The isContentEditable IDL attribute, on getting, must return true if the element is either an editing host or // editable, and false otherwise. - return is_editable(); + return is_editable_or_editing_host(); } StringView HTMLElement::content_editable() const @@ -1181,7 +1165,7 @@ WebIDL::ExceptionOr HTMLElement::toggle_popover(TogglePopoverOptionsOrForc void HTMLElement::did_receive_focus() { - if (m_content_editable_state != ContentEditableState::True) + if (!first_is_one_of(m_content_editable_state, ContentEditableState::True, ContentEditableState::PlaintextOnly)) return; auto editing_host = document().editing_host_manager(); @@ -1202,7 +1186,7 @@ void HTMLElement::did_receive_focus() void HTMLElement::did_lose_focus() { - if (m_content_editable_state != ContentEditableState::True) + if (!first_is_one_of(m_content_editable_state, ContentEditableState::True, ContentEditableState::PlaintextOnly)) return; document().editing_host_manager()->set_active_contenteditable_element(nullptr); diff --git a/Libraries/LibWeb/HTML/HTMLElement.h b/Libraries/LibWeb/HTML/HTMLElement.h index 77c67f50558..ac52ac3e53c 100644 --- a/Libraries/LibWeb/HTML/HTMLElement.h +++ b/Libraries/LibWeb/HTML/HTMLElement.h @@ -73,7 +73,6 @@ public: StringView dir() const; void set_dir(String const&); - virtual bool is_editable() const final; virtual bool is_focusable() const override; bool is_content_editable() const; StringView content_editable() const; diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Libraries/LibWeb/HTML/HTMLInputElement.cpp index bec7f08eeb1..c4a6d14a9bd 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -336,7 +336,7 @@ WebIDL::ExceptionOr HTMLInputElement::show_picker() // The showPicker() method steps are: // 1. If this is not mutable, then throw an "InvalidStateError" DOMException. - if (!m_is_mutable) + if (!is_mutable()) return WebIDL::InvalidStateError::create(realm(), "Element is not mutable"_string); // 2. If this's relevant settings object's origin is not same origin with this's relevant settings object's top-level origin, @@ -712,10 +712,7 @@ void HTMLInputElement::handle_maxlength_attribute() void HTMLInputElement::handle_readonly_attribute(Optional const& maybe_value) { // The readonly attribute is a boolean attribute that controls whether or not the user can edit the form control. When specified, the element is not mutable. - m_is_mutable = !maybe_value.has_value() || !is_allowed_to_be_readonly(m_type); - - if (m_text_node) - m_text_node->set_always_editable(m_is_mutable); + set_is_mutable(!maybe_value.has_value() || !is_allowed_to_be_readonly(m_type)); } // https://html.spec.whatwg.org/multipage/input.html#the-input-element:attr-input-placeholder-3 @@ -871,12 +868,7 @@ void HTMLInputElement::create_text_input_shadow_tree() MUST(element->append_child(*m_inner_text_element)); m_text_node = realm().create(document(), move(initial_value)); - if (type_state() == TypeAttributeState::FileUpload) { - // NOTE: file upload state is mutable, but we don't allow the text node to be modifed - m_text_node->set_always_editable(false); - } else { - handle_readonly_attribute(attribute(HTML::AttributeNames::readonly)); - } + handle_readonly_attribute(attribute(HTML::AttributeNames::readonly)); if (type_state() == TypeAttributeState::Password) m_text_node->set_is_password_input({}, true); handle_maxlength_attribute(); @@ -907,7 +899,7 @@ void HTMLInputElement::create_text_input_shadow_tree() auto up_callback_function = JS::NativeFunction::create( realm(), [this](JS::VM&) { - if (m_is_mutable) { + if (is_mutable()) { MUST(step_up()); user_interaction_did_change_input_value(); } @@ -929,7 +921,7 @@ void HTMLInputElement::create_text_input_shadow_tree() auto down_callback_function = JS::NativeFunction::create( realm(), [this](JS::VM&) { - if (m_is_mutable) { + if (is_mutable()) { MUST(step_down()); user_interaction_did_change_input_value(); } diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.h b/Libraries/LibWeb/HTML/HTMLInputElement.h index 30369be9abe..bf2df331779 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.h +++ b/Libraries/LibWeb/HTML/HTMLInputElement.h @@ -100,8 +100,6 @@ public: bool indeterminate() const { return m_indeterminate; } void set_indeterminate(bool); - bool is_mutable() const { return m_is_mutable; } - void did_pick_color(Optional picked_color, ColorPickerUpdateState state); enum class MultipleHandling { @@ -339,9 +337,6 @@ private: // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fe-dirty bool m_dirty_value { false }; - // https://html.spec.whatwg.org/multipage/input.html#the-input-element:concept-fe-mutable - bool m_is_mutable { true }; - // https://html.spec.whatwg.org/multipage/input.html#the-input-element:legacy-pre-activation-behavior bool m_before_legacy_pre_activation_behavior_checked { false }; bool m_before_legacy_pre_activation_behavior_indeterminate { false }; diff --git a/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp b/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp index 6aeb1363cc3..89f2bdac319 100644 --- a/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp @@ -394,10 +394,7 @@ void HTMLTextAreaElement::create_shadow_tree_if_needed() void HTMLTextAreaElement::handle_readonly_attribute(Optional const& maybe_value) { // The readonly attribute is a boolean attribute that controls whether or not the user can edit the form control. When specified, the element is not mutable. - m_is_mutable = !maybe_value.has_value(); - - if (m_text_node) - m_text_node->set_always_editable(m_is_mutable); + set_is_mutable(!maybe_value.has_value()); } // https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-maxlength diff --git a/Libraries/LibWeb/HTML/HTMLTextAreaElement.h b/Libraries/LibWeb/HTML/HTMLTextAreaElement.h index e26905cf107..3053f63d2da 100644 --- a/Libraries/LibWeb/HTML/HTMLTextAreaElement.h +++ b/Libraries/LibWeb/HTML/HTMLTextAreaElement.h @@ -157,9 +157,6 @@ private: // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fe-dirty bool m_dirty_value { false }; - // https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element:concept-fe-mutable - bool m_is_mutable { true }; - // https://html.spec.whatwg.org/multipage/form-elements.html#concept-textarea-raw-value String m_raw_value; diff --git a/Libraries/LibWeb/Page/DragAndDropEventHandler.cpp b/Libraries/LibWeb/Page/DragAndDropEventHandler.cpp index 452f77cd6b3..7a966821fbe 100644 --- a/Libraries/LibWeb/Page/DragAndDropEventHandler.cpp +++ b/Libraries/LibWeb/Page/DragAndDropEventHandler.cpp @@ -611,7 +611,7 @@ bool DragAndDropEventHandler::allow_text_drop(GC::Ref node) const if (!m_drag_data_store->has_text_item()) return false; - if (node->is_editable()) + if (node->is_editable_or_editing_host()) return true; if (is(*node)) diff --git a/Libraries/LibWeb/Painting/PaintableBox.cpp b/Libraries/LibWeb/Painting/PaintableBox.cpp index 1ff3e147770..f79d1f41d07 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -564,7 +564,12 @@ void paint_cursor_if_needed(PaintContext& context, TextPaintable const& paintabl 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()) + auto active_element = document.active_element(); + auto active_element_is_editable = is(active_element) + && dynamic_cast(*active_element).is_mutable(); + + auto dom_node = fragment.layout_node().dom_node(); + if (!dom_node || (!dom_node->is_editable() && !active_element_is_editable)) return; auto fragment_rect = fragment.absolute_rect(); diff --git a/Libraries/LibWeb/WebDriver/ElementReference.cpp b/Libraries/LibWeb/WebDriver/ElementReference.cpp index e77518a5dee..5d116a4edb4 100644 --- a/Libraries/LibWeb/WebDriver/ElementReference.cpp +++ b/Libraries/LibWeb/WebDriver/ElementReference.cpp @@ -288,11 +288,7 @@ bool is_element_editable(Web::DOM::Element const& element) bool is_element_mutable(Web::DOM::Element const& element) { // Denotes elements that are editing hosts or content editable. - if (!is(element)) - return false; - - auto const& html_element = static_cast(element); - return html_element.is_editable(); + return element.is_editable_or_editing_host(); } // https://w3c.github.io/webdriver/#dfn-mutable-form-control-element