From 814ca3267ed73d7ad8492ef69ff87f95240cf922 Mon Sep 17 00:00:00 2001 From: Jelle Raaijmakers Date: Thu, 22 Aug 2024 15:20:24 +0200 Subject: [PATCH] LibWeb: Implement input/textarea selection APIs For both types of elements, `.selectionStart`, `.selectionEnd`, `.selectionDirection`, `.setSelectionRange()`, `.select()` and the `select` event are now implemented. --- .../DOM/FormAssociatedElement-selection.txt | 14 + .../input-selection-start-selection-end.txt | 7 - .../DOM/FormAssociatedElement-selection.html | 46 +++ .../input-selection-start-selection-end.html | 24 -- .../LibWeb/HTML/FormAssociatedElement.cpp | 273 ++++++++++++++++-- .../LibWeb/HTML/FormAssociatedElement.h | 41 ++- .../LibWeb/HTML/HTMLInputElement.cpp | 93 +++--- .../Libraries/LibWeb/HTML/HTMLInputElement.h | 11 +- .../LibWeb/HTML/HTMLInputElement.idl | 6 +- .../LibWeb/HTML/HTMLTextAreaElement.cpp | 35 +++ .../LibWeb/HTML/HTMLTextAreaElement.h | 18 +- .../LibWeb/HTML/HTMLTextAreaElement.idl | 10 +- 12 files changed, 436 insertions(+), 142 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/DOM/FormAssociatedElement-selection.txt delete mode 100644 Tests/LibWeb/Text/expected/input-selection-start-selection-end.txt create mode 100644 Tests/LibWeb/Text/input/DOM/FormAssociatedElement-selection.html delete mode 100644 Tests/LibWeb/Text/input/input-selection-start-selection-end.html diff --git a/Tests/LibWeb/Text/expected/DOM/FormAssociatedElement-selection.txt b/Tests/LibWeb/Text/expected/DOM/FormAssociatedElement-selection.txt new file mode 100644 index 00000000000..32ea89b6839 --- /dev/null +++ b/Tests/LibWeb/Text/expected/DOM/FormAssociatedElement-selection.txt @@ -0,0 +1,14 @@ +Well hello friends some +text text-input selectionStart: 0 selectionEnd: 0 selectionDirection: none +date-input selectionStart: null selectionEnd: null selectionDirection: null +textarea selectionStart: 0 selectionEnd: 0 selectionDirection: none +text-input selectionStart: 18 selectionEnd: 18 selectionDirection: none +date input setting selectionStart error: InvalidStateError: setSelectionStart does not apply to this input type +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 +textarea selectionStart: 0 selectionEnd: 9 selectionDirection: none +select event fired: 18 18 diff --git a/Tests/LibWeb/Text/expected/input-selection-start-selection-end.txt b/Tests/LibWeb/Text/expected/input-selection-start-selection-end.txt deleted file mode 100644 index f9fa806ddf6..00000000000 --- a/Tests/LibWeb/Text/expected/input-selection-start-selection-end.txt +++ /dev/null @@ -1,7 +0,0 @@ -Well hello friends text selectionStart: 0 -text selectionEnd: 0 -date selectionStart: null -date selectionEnd: null -text selectionStart: 18 -text selectionEnd: 18 -date input setting selectionStart error: InvalidStateError: setSelectionStart does not apply to this input type diff --git a/Tests/LibWeb/Text/input/DOM/FormAssociatedElement-selection.html b/Tests/LibWeb/Text/input/DOM/FormAssociatedElement-selection.html new file mode 100644 index 00000000000..db560c01b9b --- /dev/null +++ b/Tests/LibWeb/Text/input/DOM/FormAssociatedElement-selection.html @@ -0,0 +1,46 @@ + + + + + diff --git a/Tests/LibWeb/Text/input/input-selection-start-selection-end.html b/Tests/LibWeb/Text/input/input-selection-start-selection-end.html deleted file mode 100644 index 6d01fafaaa9..00000000000 --- a/Tests/LibWeb/Text/input/input-selection-start-selection-end.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - diff --git a/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.cpp b/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.cpp index 7e4bf1f8b0e..66ba41d91bc 100644 --- a/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.cpp @@ -1,10 +1,12 @@ /* * Copyright (c) 2021, Andreas Kling + * Copyright (c) 2024, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ #include +#include #include #include #include @@ -17,6 +19,17 @@ namespace Web::HTML { +static SelectionDirection string_to_selection_direction(Optional value) +{ + if (!value.has_value()) + return SelectionDirection::None; + if (value.value() == "forward"sv) + return SelectionDirection::Forward; + if (value.value() == "backward"sv) + return SelectionDirection::Backward; + return SelectionDirection::None; +} + void FormAssociatedElement::set_form(HTMLFormElement* form) { if (m_form) @@ -152,61 +165,261 @@ void FormAssociatedElement::reset_form_owner() } } +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value +String FormAssociatedElement::relevant_value() const +{ + auto const& html_element = form_associated_element_to_html_element(); + if (is(html_element)) + return static_cast(html_element).value(); + if (is(html_element)) + return static_cast(html_element).api_value(); + VERIFY_NOT_REACHED(); +} + +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value +void FormAssociatedElement::relevant_value_was_changed(JS::GCPtr text_node) +{ + auto the_relevant_value = relevant_value(); + auto relevant_value_length = the_relevant_value.code_points().length(); + + // 1. If the element has a selection: + if (m_selection_start < m_selection_end) { + // 1. If the start of the selection is now past the end of the relevant value, set it to + // the end of the relevant value. + if (m_selection_start > relevant_value_length) + m_selection_start = relevant_value_length; + + // 2. If the end of the selection is now past the end of the relevant value, set it to the + // end of the relevant value. + if (m_selection_end > relevant_value_length) + m_selection_end = relevant_value_length; + + // 3. If the user agent does not support empty selection, and both the start and end of the + // selection are now pointing to the end of the relevant value, then instead set the + // element's text entry cursor position to the end of the relevant value, removing any + // selection. + // NOTE: We support empty selections. + return; + } + + // 2. Otherwise, the element must have a text entry cursor position position. If it is now past + // the end of the relevant value, set it to the end of the relevant value. + auto& document = form_associated_element_to_html_element().document(); + auto const current_cursor_position = document.cursor_position(); + if (current_cursor_position && text_node + && current_cursor_position->node() == text_node + && current_cursor_position->offset() > relevant_value_length) { + document.set_cursor_position(DOM::Position::create(document.realm(), *text_node, relevant_value_length)); + } +} + +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-select +WebIDL::ExceptionOr FormAssociatedElement::select() +{ + // 1. If this element is an input element, and either select() does not apply to this element + // or the corresponding control has no selectable text, return. + auto& html_element = form_associated_element_to_html_element(); + if (is(html_element)) { + auto& input_element = static_cast(html_element); + // FIXME: implement "or the corresponding control has no selectable text" + if (!input_element.select_applies()) + return {}; + } + + // 2. Set the selection range with 0 and infinity. + set_the_selection_range(0, NumericLimits::max()); + return {}; +} + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionstart -WebIDL::UnsignedLong FormAssociatedElement::selection_start() const +Optional FormAssociatedElement::selection_start() const { // 1. If this element is an input element, and selectionStart does not apply to this element, return null. - // NOTE: This is done by HTMLInputElement before calling this function + auto const& html_element = form_associated_element_to_html_element(); + if (is(html_element)) { + auto const& input_element = static_cast(html_element); + if (!input_element.selection_or_range_applies()) + return {}; + } // 2. If there is no selection, return the code unit offset within the relevant value to the character that // immediately follows the text entry cursor. - if (auto cursor = form_associated_element_to_html_element().document().cursor_position()) - return cursor->offset(); + if (m_selection_start == m_selection_end) { + if (auto cursor = form_associated_element_to_html_element().document().cursor_position()) + return cursor->offset(); + } - // FIXME: 3. Return the code unit offset within the relevant value to the character that immediately follows the start of - // the selection. - return 0; + // 3. Return the code unit offset within the relevant value to the character that immediately follows the start of + // the selection. + return m_selection_start; } // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection:dom-textarea/input-selectionstart-2 -WebIDL::ExceptionOr FormAssociatedElement::set_selection_start(Optional const&) +WebIDL::ExceptionOr FormAssociatedElement::set_selection_start(Optional const& value) { - // 1. If this element is an input element, and selectionStart does not apply to this element, throw an - // "InvalidStateError" DOMException. - // NOTE: This is done by HTMLInputElement before calling this function + // 1. If this element is an input element, and selectionStart does not apply to this element, + // throw an "InvalidStateError" DOMException. + auto& html_element = form_associated_element_to_html_element(); + if (is(html_element)) { + auto& input_element = static_cast(html_element); + if (!input_element.selection_or_range_applies()) + return WebIDL::InvalidStateError::create(html_element.realm(), "setSelectionStart does not apply to this input type"_fly_string); + } - // FIXME: 2. Let end be the value of this element's selectionEnd attribute. - // FIXME: 3. If end is less than the given value, set end to the given value. - // FIXME: 4. Set the selection range with the given value, end, and the value of this element's selectionDirection attribute. + // 2. Let end be the value of this element's selectionEnd attribute. + auto end = m_selection_end; + + // 3. If end is less than the given value, set end to the given value. + if (value.has_value() && end < value.value()) + end = value.value(); + + // 4. Set the selection range with the given value, end, and the value of this element's + // selectionDirection attribute. + set_the_selection_range(value, end, selection_direction_state()); return {}; } // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionend -WebIDL::UnsignedLong FormAssociatedElement::selection_end() const +Optional FormAssociatedElement::selection_end() const { - // 1. If this element is an input element, and selectionEnd does not apply to this element, return null. - // NOTE: This is done by HTMLInputElement before calling this function + // 1. If this element is an input element, and selectionEnd does not apply to this element, return + // null. + auto const& html_element = form_associated_element_to_html_element(); + if (is(html_element)) { + auto const& input_element = static_cast(html_element); + if (!input_element.selection_or_range_applies()) + return {}; + } - // 2. If there is no selection, return the code unit offset within the relevant value to the character that - // immediately follows the text entry cursor. - if (auto cursor = form_associated_element_to_html_element().document().cursor_position()) - return cursor->offset(); + // 2. If there is no selection, return the code unit offset within the relevant value to the + // character that immediately follows the text entry cursor. + if (m_selection_start == m_selection_end) { + if (auto cursor = form_associated_element_to_html_element().document().cursor_position()) + return cursor->offset(); + } - // FIXME: 3. Return the code unit offset within the relevant value to the character that immediately follows the end of - // the selection. - return 0; + // 3. Return the code unit offset within the relevant value to the character that immediately + // follows the end of the selection. + return m_selection_end; } // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection:dom-textarea/input-selectionend-3 -WebIDL::ExceptionOr FormAssociatedElement::set_selection_end(Optional const&) +WebIDL::ExceptionOr FormAssociatedElement::set_selection_end(Optional const& value) { - // 1. If this element is an input element, and selectionEnd does not apply to this element, throw an - // "InvalidStateError" DOMException. - // NOTE: This is done by HTMLInputElement before calling this function + // 1. If this element is an input element, and selectionEnd does not apply to this element, + // throw an "InvalidStateError" DOMException. + auto& html_element = form_associated_element_to_html_element(); + if (is(html_element)) { + auto& input_element = static_cast(html_element); + if (!input_element.selection_or_range_applies()) + return WebIDL::InvalidStateError::create(html_element.realm(), "setSelectionEnd does not apply to this input type"_fly_string); + } - // FIXME: 2. Set the selection range with the value of this element's selectionStart attribute, the given value, and the - // value of this element's selectionDirection attribute. + // 2. Set the selection range with the value of this element's selectionStart attribute, the + // given value, and the value of this element's selectionDirection attribute. + set_the_selection_range(m_selection_start, value, selection_direction_state()); return {}; } +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#selection-direction +Optional FormAssociatedElement::selection_direction() const +{ + // 1. If this element is an input element, and selectionDirection does not apply to this + // element, return null. + auto const& html_element = form_associated_element_to_html_element(); + if (is(html_element)) { + auto const& input_element = static_cast(html_element); + if (!input_element.selection_or_range_applies()) + return {}; + } + + // 2. Return this element's selection direction. + switch (m_selection_direction) { + case SelectionDirection::Forward: + return "forward"_string; + case SelectionDirection::Backward: + return "backward"_string; + case SelectionDirection::None: + return "none"_string; + default: + VERIFY_NOT_REACHED(); + } +} + +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#set-the-selection-direction +void FormAssociatedElement::set_selection_direction(Optional direction) +{ + // To set the selection direction of an element to a given direction, update the element's + // selection direction to the given direction, unless the direction is "none" and the + // platform does not support that direction; in that case, update the element's selection + // direction to "forward". + m_selection_direction = string_to_selection_direction(direction); +} + +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setselectionrange +WebIDL::ExceptionOr FormAssociatedElement::set_selection_range(Optional start, Optional end, Optional direction) +{ + // 1. If this element is an input element, and setSelectionRange() does not apply to this + // element, throw an "InvalidStateError" DOMException. + auto& html_element = form_associated_element_to_html_element(); + if (is(html_element) && !static_cast(html_element).selection_or_range_applies()) + return WebIDL::InvalidStateError::create(html_element.realm(), "setSelectionRange does not apply to this input type"_fly_string); + + // 2. Set the selection range with start, end, and direction. + set_the_selection_range(start, end, string_to_selection_direction(direction)); + return {}; +} + +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#set-the-selection-range +void FormAssociatedElement::set_the_selection_range(Optional start, Optional end, SelectionDirection direction) +{ + // 1. If start is null, let start be zero. + start = start.value_or(0); + + // 2. If end is null, let end be zero. + end = end.value_or(0); + + // 3. Set the selection of the text control to the sequence of code units within the relevant + // value starting with the code unit at the startth position (in logical order) and ending + // with the code unit at the (end-1)th position. Arguments greater than the length of the + // relevant value of the text control (including the special value infinity) must be treated + // as pointing at the end of the text control. + auto the_relevant_value = relevant_value(); + auto relevant_value_length = the_relevant_value.code_points().length(); + auto new_selection_start = AK::min(start.value(), relevant_value_length); + auto new_selection_end = AK::min(end.value(), relevant_value_length); + + // If end is less than or equal to start then the start of the selection and the end of the + // selection must both be placed immediately before the character with offset end. In UAs + // where there is no concept of an empty selection, this must set the cursor to be just + // before the character with offset end. + new_selection_start = AK::min(new_selection_start, new_selection_end); + + bool was_modified = m_selection_start != new_selection_start || m_selection_end != new_selection_end; + m_selection_start = new_selection_start; + m_selection_end = new_selection_end; + + // 4. If direction is not identical to either "backward" or "forward", or if the direction + // argument was not given, set direction to "none". + // NOTE: This is handled by the argument's default value and ::string_to_selection_direction(). + + // 5. Set the selection direction of the text control to direction. + was_modified |= m_selection_direction != direction; + m_selection_direction = direction; + + // 6. If the previous steps caused the selection of the text control to be modified (in either + // extent or direction), then queue an element task on the user interaction task source + // given the element to fire an event named select at the element, with the bubbles attribute + // initialized to true. + // AD-HOC: If there is no selection, we do not fire the event. This seems to correspond to how + // other browsers behave. + if (was_modified && m_selection_start != m_selection_end) { + auto& html_element = form_associated_element_to_html_element(); + html_element.queue_an_element_task(Task::Source::UserInteraction, [&html_element] { + auto select_event = DOM::Event::create(html_element.realm(), EventNames::select, { .bubbles = true }); + static_cast(&html_element)->dispatch_event(select_event); + }); + } +} + } diff --git a/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.h b/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.h index 8a480473234..eea5b700ffd 100644 --- a/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.h +++ b/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2021, Andreas Kling + * Copyright (c) 2024, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -49,6 +50,13 @@ private: form_associated_element_attribute_changed(name, value); \ } +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#selection-direction +enum class SelectionDirection { + Forward, + Backward, + None, +}; + class FormAssociatedElement { public: HTMLFormElement* form() { return m_form; } @@ -83,18 +91,37 @@ public: virtual String value() const { return String {}; } + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value + String relevant_value() const; + virtual HTMLElement& form_associated_element_to_html_element() = 0; HTMLElement const& form_associated_element_to_html_element() const { return const_cast(*this).form_associated_element_to_html_element(); } // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-form-reset-control virtual void reset_algorithm() {}; - WebIDL::UnsignedLong selection_start() const; + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-select + WebIDL::ExceptionOr select(); + + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionstart + Optional selection_start() const; WebIDL::ExceptionOr set_selection_start(Optional const&); - WebIDL::UnsignedLong selection_end() const; + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionend + Optional selection_end() const; WebIDL::ExceptionOr set_selection_end(Optional const&); + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectiondirection + Optional selection_direction() const; + void set_selection_direction(Optional direction); + SelectionDirection selection_direction_state() const { return m_selection_direction; } + + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setselectionrange + void set_the_selection_range(Optional start, Optional end, SelectionDirection direction = SelectionDirection::None); + + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setselectionrange + WebIDL::ExceptionOr set_selection_range(Optional start, Optional end, Optional direction); + protected: FormAssociatedElement() = default; virtual ~FormAssociatedElement() = default; @@ -107,13 +134,21 @@ protected: void form_node_was_removed(); void form_node_attribute_changed(FlyString const&, Optional const&); + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value + void relevant_value_was_changed(JS::GCPtr); + private: + void reset_form_owner(); + WeakPtr m_form; // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#parser-inserted-flag bool m_parser_inserted { false }; - void reset_form_owner(); + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-selection + WebIDL::UnsignedLong m_selection_start { 0 }; + WebIDL::UnsignedLong m_selection_end { 0 }; + SelectionDirection m_selection_direction { SelectionDirection::None }; }; } diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp index 15705f35f3b..ada592c5e1d 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -4,6 +4,7 @@ * Copyright (c) 2022, Andrew Kaster * Copyright (c) 2023-2024, Shannon Booth * Copyright (c) 2023, Bastiaan van der Plaat + * Copyright (c) 2024, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -410,11 +411,15 @@ WebIDL::ExceptionOr HTMLInputElement::run_input_activation_behavior(DOM::E void HTMLInputElement::did_edit_text_node(Badge) { // An input element's dirty value flag must be set to true whenever the user interacts with the control in a way that changes the value. + auto old_value = move(m_value); m_value = value_sanitization_algorithm(m_text_node->data()); m_dirty_value = true; m_has_uncommitted_changes = true; + if (m_value != old_value) + relevant_value_was_changed(m_text_node); + update_placeholder_visibility(); user_interaction_did_change_input_value(); @@ -550,6 +555,8 @@ WebIDL::ExceptionOr HTMLInputElement::set_value(String const& value) // and the element has a text entry cursor position, move the text entry cursor position to the end of the // text control, unselecting any selected text and resetting the selection direction to "none". if (m_value != old_value) { + relevant_value_was_changed(m_text_node); + if (m_text_node) { m_text_node->set_data(m_value); update_placeholder_visibility(); @@ -1183,12 +1190,16 @@ void HTMLInputElement::form_associated_element_attribute_changed(FlyString const } else if (name == HTML::AttributeNames::value) { if (!m_dirty_value) { + auto old_value = move(m_value); if (!value.has_value()) { m_value = String {}; } else { m_value = value_sanitization_algorithm(*value); } + if (m_value != old_value) + relevant_value_was_changed(m_text_node); + update_shadow_tree(); } } else if (name == HTML::AttributeNames::placeholder) { @@ -1417,6 +1428,7 @@ void HTMLInputElement::reset_algorithm() m_dirty_checkedness = false; // set the value of the element to the value of the value content attribute, if there is one, or the empty string otherwise, + auto old_value = move(m_value); m_value = get_attribute_value(AttributeNames::value); // set the checkedness of the element to true if the element has a checked content attribute and false if it does not, @@ -1428,6 +1440,9 @@ void HTMLInputElement::reset_algorithm() // and then invoke the value sanitization algorithm, if the type attribute's current state defines one. m_value = value_sanitization_algorithm(m_value); + if (m_value != old_value) + relevant_value_was_changed(m_text_node); + if (m_text_node) { m_text_node->set_data(m_value); update_placeholder_visibility(); @@ -2069,66 +2084,6 @@ void HTMLInputElement::set_custom_validity(String const& error) return; } -// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-select -WebIDL::ExceptionOr HTMLInputElement::select() -{ - dbgln("(STUBBED) HTMLInputElement::select(). Called on: {}", debug_description()); - return {}; -} - -// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setselectionrange -WebIDL::ExceptionOr HTMLInputElement::set_selection_range(u32 start, u32 end, Optional const& direction) -{ - dbgln("(STUBBED) HTMLInputElement::set_selection_range(start={}, end={}, direction='{}'). Called on: {}", start, end, direction, debug_description()); - return {}; -} - -// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection:dom-textarea/input-selectionstart-2 -WebIDL::ExceptionOr HTMLInputElement::set_selection_start_for_bindings(Optional const& value) -{ - // 1. If this element is an input element, and selectionStart does not apply to this element, throw an - // "InvalidStateError" DOMException. - if (!selection_or_range_applies()) - return WebIDL::InvalidStateError::create(realm(), "setSelectionStart does not apply to this input type"_fly_string); - - // NOTE: Steps continued below: - return set_selection_start(value); -} - -// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionstart -Optional HTMLInputElement::selection_start_for_bindings() const -{ - // 1. If this element is an input element, and selectionStart does not apply to this element, return null. - if (!selection_or_range_applies()) - return {}; - - // NOTE: Steps continued below: - return selection_start(); -} - -// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection:dom-textarea/input-selectionend-3 -WebIDL::ExceptionOr HTMLInputElement::set_selection_end_for_bindings(Optional const& value) -{ - // 1. If this element is an input element, and selectionEnd does not apply to this element, throw an - // "InvalidStateError" DOMException. - if (!selection_or_range_applies()) - return WebIDL::InvalidStateError::create(realm(), "setSelectionEnd does not apply to this input type"_fly_string); - - // NOTE: Steps continued below: - return set_selection_end(value); -} - -// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionend -Optional HTMLInputElement::selection_end_for_bindings() const -{ - // 1. If this element is an input element, and selectionEnd does not apply to this element, return null. - if (!selection_or_range_applies()) - return {}; - - // NOTE: Steps continued below: - return selection_end(); -} - Optional HTMLInputElement::default_role() const { // https://www.w3.org/TR/html-aria/#el-input-button @@ -2257,6 +2212,24 @@ bool HTMLInputElement::has_input_activation_behavior() const } } +// https://html.spec.whatwg.org/multipage/input.html#do-not-apply +bool HTMLInputElement::select_applies() const +{ + switch (type_state()) { + case TypeAttributeState::Button: + case TypeAttributeState::Checkbox: + case TypeAttributeState::Hidden: + case TypeAttributeState::ImageButton: + case TypeAttributeState::RadioButton: + case TypeAttributeState::Range: + case TypeAttributeState::ResetButton: + case TypeAttributeState::SubmitButton: + return false; + default: + return true; + } +} + // https://html.spec.whatwg.org/multipage/input.html#do-not-apply bool HTMLInputElement::selection_or_range_applies() const { diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h index f5e675dded5..5740e18332c 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h @@ -2,6 +2,7 @@ * Copyright (c) 2018-2022, Andreas Kling * Copyright (c) 2022, Adam Hodgen * Copyright (c) 2023, Bastiaan van der Plaat + * Copyright (c) 2024, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -137,9 +138,6 @@ public: WebIDL::ExceptionOr report_validity(); void set_custom_validity(String const&); - WebIDL::ExceptionOr select(); - WebIDL::ExceptionOr set_selection_range(u32 start, u32 end, Optional const& direction = {}); - WebIDL::ExceptionOr show_picker(); // ^DOM::EditableTextNodeOwner @@ -198,14 +196,9 @@ public: bool value_as_number_applies() const; bool step_applies() const; bool step_up_or_down_applies() const; + bool select_applies() const; bool selection_or_range_applies() const; - WebIDL::ExceptionOr set_selection_start_for_bindings(Optional const&); - Optional selection_start_for_bindings() const; - - WebIDL::ExceptionOr set_selection_end_for_bindings(Optional const&); - Optional selection_end_for_bindings() const; - private: HTMLInputElement(DOM::Document&, DOM::QualifiedName); diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.idl b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.idl index c6ab07ab27c..c94ec2ff314 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.idl +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.idl @@ -58,9 +58,9 @@ interface HTMLInputElement : HTMLElement { readonly attribute NodeList? labels; undefined select(); - [ImplementedAs=selection_start_for_bindings] attribute unsigned long? selectionStart; - [ImplementedAs=selection_end_for_bindings] attribute unsigned long? selectionEnd; - [FIXME] attribute DOMString? selectionDirection; + attribute unsigned long? selectionStart; + attribute unsigned long? selectionEnd; + attribute DOMString? selectionDirection; [FIXME] undefined setRangeText(DOMString replacement); [FIXME] undefined setRangeText(DOMString replacement, unsigned long start, unsigned long end, optional SelectionMode selectionMode = "preserve"); undefined setSelectionRange(unsigned long start, unsigned long end, optional DOMString direction); diff --git a/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp index e9e15a8f5e8..22c6284beab 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp @@ -2,6 +2,7 @@ * Copyright (c) 2020, the SerenityOS developers. * Copyright (c) 2023, Sam Atkins * Copyright (c) 2024, Bastiaan van der Plaat + * Copyright (c) 2024, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -186,8 +187,12 @@ void HTMLTextAreaElement::set_value(String const& value) void HTMLTextAreaElement::set_raw_value(String value) { + auto old_raw_value = move(m_raw_value); m_raw_value = move(value); m_api_value.clear(); + + if (m_raw_value != old_raw_value) + relevant_value_was_changed(m_text_node); } // https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element:concept-fe-api-value-3 @@ -292,6 +297,36 @@ WebIDL::ExceptionOr HTMLTextAreaElement::set_rows(unsigned rows) return set_attribute(HTML::AttributeNames::rows, MUST(String::number(rows))); } +WebIDL::UnsignedLong HTMLTextAreaElement::selection_start_binding() const +{ + return selection_start().value(); +} + +WebIDL::ExceptionOr HTMLTextAreaElement::set_selection_start_binding(WebIDL::UnsignedLong const& value) +{ + return set_selection_start(value); +} + +WebIDL::UnsignedLong HTMLTextAreaElement::selection_end_binding() const +{ + return selection_end().value(); +} + +WebIDL::ExceptionOr HTMLTextAreaElement::set_selection_end_binding(WebIDL::UnsignedLong const& value) +{ + return set_selection_end(value); +} + +String HTMLTextAreaElement::selection_direction_binding() const +{ + return selection_direction().value(); +} + +void HTMLTextAreaElement::set_selection_direction_binding(String direction) +{ + set_selection_direction(direction); +} + void HTMLTextAreaElement::create_shadow_tree_if_needed() { if (shadow_root()) diff --git a/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.h b/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.h index 6260cc18ae3..e69fe45abea 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.h @@ -2,6 +2,7 @@ * Copyright (c) 2020, the SerenityOS developers. * Copyright (c) 2022, Luke Wilde * Copyright (c) 2024, Bastiaan van der Plaat + * Copyright (c) 2024, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -80,6 +81,9 @@ public: String value() const override; void set_value(String const&); + // https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element:concept-fe-api-value-3 + String api_value() const; + u32 text_length() const; bool check_validity(); @@ -98,6 +102,18 @@ public: unsigned rows() const; WebIDL::ExceptionOr set_rows(unsigned); + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionstart + WebIDL::UnsignedLong selection_start_binding() const; + WebIDL::ExceptionOr set_selection_start_binding(WebIDL::UnsignedLong const&); + + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionend + WebIDL::UnsignedLong selection_end_binding() const; + WebIDL::ExceptionOr set_selection_end_binding(WebIDL::UnsignedLong const&); + + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectiondirection + String selection_direction_binding() const; + void set_selection_direction_binding(String direction); + private: HTMLTextAreaElement(DOM::Document&, DOM::QualifiedName); @@ -105,7 +121,6 @@ private: virtual void visit_edges(Cell::Visitor&) override; void set_raw_value(String); - String api_value() const; // ^DOM::Element virtual i32 default_tab_index_value() const override; @@ -118,6 +133,7 @@ private: void queue_firing_input_event(); void update_placeholder_visibility(); + JS::GCPtr m_placeholder_element; JS::GCPtr m_placeholder_text_node; diff --git a/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.idl b/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.idl index 6371de62c21..194212c90ae 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.idl +++ b/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.idl @@ -34,11 +34,11 @@ interface HTMLTextAreaElement : HTMLElement { readonly attribute NodeList labels; - [FIXME] undefined select(); - attribute unsigned long selectionStart; - attribute unsigned long selectionEnd; - [FIXME] attribute DOMString selectionDirection; + undefined select(); + [ImplementedAs=selection_start_binding] attribute unsigned long selectionStart; + [ImplementedAs=selection_end_binding] attribute unsigned long selectionEnd; + [ImplementedAs=selection_direction_binding] attribute DOMString selectionDirection; [FIXME] undefined setRangeText(DOMString replacement); [FIXME] undefined setRangeText(DOMString replacement, unsigned long start, unsigned long end, optional SelectionMode selectionMode = "preserve"); - [FIXME] undefined setSelectionRange(unsigned long start, unsigned long end, optional DOMString direction); + undefined setSelectionRange(unsigned long start, unsigned long end, optional DOMString direction); };