Browse Source

LibWeb: Implement input/textarea selection APIs

For both types of elements, `.selectionStart`, `.selectionEnd`,
`.selectionDirection`, `.setSelectionRange()`, `.select()` and the
`select` event are now implemented.
Jelle Raaijmakers 11 months ago
parent
commit
814ca3267e

+ 14 - 0
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

+ 0 - 7
Tests/LibWeb/Text/expected/input-selection-start-selection-end.txt

@@ -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

+ 46 - 0
Tests/LibWeb/Text/input/DOM/FormAssociatedElement-selection.html

@@ -0,0 +1,46 @@
+<input type="text" id="text-input">
+<input type="date" id="date-input">
+<textarea id="textarea">some
+text</textarea>
+<script src="../include.js"></script>
+<script>
+    test(() => {
+        let textInput = document.getElementById('text-input');
+        let dateInput = document.getElementById('date-input');
+        let textarea = document.getElementById('textarea');
+
+        const dumpSelection = (element) => {
+            println(`${element.id} selectionStart: ${element.selectionStart} selectionEnd: ${element.selectionEnd} selectionDirection: ${element.selectionDirection}`);
+        };
+
+        dumpSelection(textInput);
+        dumpSelection(dateInput);
+        dumpSelection(textarea);
+
+        textInput.value = 'Well hello friends';
+        dumpSelection(textInput);
+
+        try {
+            dateInput.selectionStart = 0;
+        } catch (e) {
+            println(`date input setting selectionStart error: ${e}`);
+        }
+
+        textInput.addEventListener('select', e => println(`select event fired: ${e.target.selectionStart} ${e.target.selectionEnd}`))
+        textInput.select();
+        dumpSelection(textInput);
+        textInput.setSelectionRange(2, 4, 'forward');
+        dumpSelection(textInput);
+        textInput.selectionStart = 1;
+        dumpSelection(textInput);
+        textInput.selectionEnd = 5;
+        dumpSelection(textInput);
+        textInput.selectionStart = 6;
+        dumpSelection(textInput);
+        textInput.selectionDirection = 'backward';
+        dumpSelection(textInput);
+
+        textarea.select();
+        dumpSelection(textarea);
+    });
+</script>

+ 0 - 24
Tests/LibWeb/Text/input/input-selection-start-selection-end.html

@@ -1,24 +0,0 @@
-<input type="text" id="ladybird-text-input"></input>
-<input type="date" id="ladybird-date-input"></input>
-<script src="include.js"></script>
-<script>
-    test(() => {
-        let textInput = document.getElementById("ladybird-text-input");
-        let dateInput = document.getElementById("ladybird-date-input");
-
-        println(`text selectionStart: ${textInput.selectionStart}`);
-        println(`text selectionEnd: ${textInput.selectionEnd}`);
-        println(`date selectionStart: ${dateInput.selectionStart}`);
-        println(`date selectionEnd: ${dateInput.selectionEnd}`);
-
-        textInput.value = "Well hello friends";
-        println(`text selectionStart: ${textInput.selectionStart}`);
-        println(`text selectionEnd: ${textInput.selectionEnd}`);
-
-        try {
-            dateInput.selectionStart = 0;
-        } catch (e) {
-            println(`date input setting selectionStart error: ${e}`);
-        }
-    });
-</script>

+ 243 - 30
Userland/Libraries/LibWeb/HTML/FormAssociatedElement.cpp

@@ -1,10 +1,12 @@
 /*
  * Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2024, Jelle Raaijmakers <jelle@gmta.nl>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
 
 #include <LibWeb/DOM/Document.h>
+#include <LibWeb/DOM/Event.h>
 #include <LibWeb/HTML/FormAssociatedElement.h>
 #include <LibWeb/HTML/HTMLButtonElement.h>
 #include <LibWeb/HTML/HTMLFieldSetElement.h>
@@ -17,6 +19,17 @@
 
 namespace Web::HTML {
 
+static SelectionDirection string_to_selection_direction(Optional<String> 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<HTMLInputElement>(html_element))
+        return static_cast<HTMLInputElement const&>(html_element).value();
+    if (is<HTMLTextAreaElement>(html_element))
+        return static_cast<HTMLTextAreaElement const&>(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<DOM::Text> 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<void> 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<HTMLInputElement>(html_element)) {
+        auto& input_element = static_cast<HTMLInputElement&>(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<WebIDL::UnsignedLong>::max());
+    return {};
+}
+
 // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionstart
-WebIDL::UnsignedLong FormAssociatedElement::selection_start() const
+Optional<WebIDL::UnsignedLong> 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<HTMLInputElement>(html_element)) {
+        auto const& input_element = static_cast<HTMLInputElement const&>(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<void> FormAssociatedElement::set_selection_start(Optional<WebIDL::UnsignedLong> const&)
+WebIDL::ExceptionOr<void> FormAssociatedElement::set_selection_start(Optional<WebIDL::UnsignedLong> 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<HTMLInputElement>(html_element)) {
+        auto& input_element = static_cast<HTMLInputElement&>(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<WebIDL::UnsignedLong> 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<HTMLInputElement>(html_element)) {
+        auto const& input_element = static_cast<HTMLInputElement const&>(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<void> FormAssociatedElement::set_selection_end(Optional<WebIDL::UnsignedLong> const&)
+WebIDL::ExceptionOr<void> FormAssociatedElement::set_selection_end(Optional<WebIDL::UnsignedLong> const& value)
+{
+    // 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<HTMLInputElement>(html_element)) {
+        auto& input_element = static_cast<HTMLInputElement&>(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);
+    }
+
+    // 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<String> 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<HTMLInputElement>(html_element)) {
+        auto const& input_element = static_cast<HTMLInputElement const&>(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<String> 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<void> FormAssociatedElement::set_selection_range(Optional<WebIDL::UnsignedLong> start, Optional<WebIDL::UnsignedLong> end, Optional<String> direction)
 {
-    // 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 setSelectionRange() does not apply to this
+    //    element, throw an "InvalidStateError" DOMException.
+    auto& html_element = form_associated_element_to_html_element();
+    if (is<HTMLInputElement>(html_element) && !static_cast<HTMLInputElement&>(html_element).selection_or_range_applies())
+        return WebIDL::InvalidStateError::create(html_element.realm(), "setSelectionRange 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 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<WebIDL::UnsignedLong> start, Optional<WebIDL::UnsignedLong> 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<DOM::EventTarget*>(&html_element)->dispatch_event(select_event);
+        });
+    }
+}
+
 }

+ 38 - 3
Userland/Libraries/LibWeb/HTML/FormAssociatedElement.h

@@ -1,5 +1,6 @@
 /*
  * Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2024, Jelle Raaijmakers <jelle@gmta.nl>
  *
  * 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<FormAssociatedElement&>(*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<void> select();
+
+    // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionstart
+    Optional<WebIDL::UnsignedLong> selection_start() const;
     WebIDL::ExceptionOr<void> set_selection_start(Optional<WebIDL::UnsignedLong> const&);
 
-    WebIDL::UnsignedLong selection_end() const;
+    // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionend
+    Optional<WebIDL::UnsignedLong> selection_end() const;
     WebIDL::ExceptionOr<void> set_selection_end(Optional<WebIDL::UnsignedLong> const&);
 
+    // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectiondirection
+    Optional<String> selection_direction() const;
+    void set_selection_direction(Optional<String> 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<WebIDL::UnsignedLong> start, Optional<WebIDL::UnsignedLong> end, SelectionDirection direction = SelectionDirection::None);
+
+    // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setselectionrange
+    WebIDL::ExceptionOr<void> set_selection_range(Optional<WebIDL::UnsignedLong> start, Optional<WebIDL::UnsignedLong> end, Optional<String> 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<String> const&);
 
+    // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
+    void relevant_value_was_changed(JS::GCPtr<DOM::Text>);
+
 private:
+    void reset_form_owner();
+
     WeakPtr<HTMLFormElement> 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 };
 };
 
 }

+ 33 - 60
Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp

@@ -4,6 +4,7 @@
  * Copyright (c) 2022, Andrew Kaster <akaster@serenityos.org>
  * Copyright (c) 2023-2024, Shannon Booth <shannon@serenityos.org>
  * Copyright (c) 2023, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
+ * Copyright (c) 2024, Jelle Raaijmakers <jelle@gmta.nl>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
@@ -410,11 +411,15 @@ WebIDL::ExceptionOr<void> HTMLInputElement::run_input_activation_behavior(DOM::E
 void HTMLInputElement::did_edit_text_node(Badge<DOM::Document>)
 {
     // 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<void> 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<void> 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<void> HTMLInputElement::set_selection_range(u32 start, u32 end, Optional<String> 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<void> HTMLInputElement::set_selection_start_for_bindings(Optional<WebIDL::UnsignedLong> 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<WebIDL::UnsignedLong> 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<void> HTMLInputElement::set_selection_end_for_bindings(Optional<WebIDL::UnsignedLong> 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<WebIDL::UnsignedLong> 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<ARIA::Role> 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
 {

+ 2 - 9
Userland/Libraries/LibWeb/HTML/HTMLInputElement.h

@@ -2,6 +2,7 @@
  * Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org>
  * Copyright (c) 2022, Adam Hodgen <ant1441@gmail.com>
  * Copyright (c) 2023, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
+ * Copyright (c) 2024, Jelle Raaijmakers <jelle@gmta.nl>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
@@ -137,9 +138,6 @@ public:
     WebIDL::ExceptionOr<bool> report_validity();
     void set_custom_validity(String const&);
 
-    WebIDL::ExceptionOr<void> select();
-    WebIDL::ExceptionOr<void> set_selection_range(u32 start, u32 end, Optional<String> const& direction = {});
-
     WebIDL::ExceptionOr<void> 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<void> set_selection_start_for_bindings(Optional<WebIDL::UnsignedLong> const&);
-    Optional<WebIDL::UnsignedLong> selection_start_for_bindings() const;
-
-    WebIDL::ExceptionOr<void> set_selection_end_for_bindings(Optional<WebIDL::UnsignedLong> const&);
-    Optional<WebIDL::UnsignedLong> selection_end_for_bindings() const;
-
 private:
     HTMLInputElement(DOM::Document&, DOM::QualifiedName);
 

+ 3 - 3
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);

+ 35 - 0
Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp

@@ -2,6 +2,7 @@
  * Copyright (c) 2020, the SerenityOS developers.
  * Copyright (c) 2023, Sam Atkins <atkinssj@serenityos.org>
  * Copyright (c) 2024, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
+ * Copyright (c) 2024, Jelle Raaijmakers <jelle@gmta.nl>
  *
  * 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<void> 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<void> 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<void> 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())

+ 17 - 1
Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.h

@@ -2,6 +2,7 @@
  * Copyright (c) 2020, the SerenityOS developers.
  * Copyright (c) 2022, Luke Wilde <lukew@serenityos.org>
  * Copyright (c) 2024, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
+ * Copyright (c) 2024, Jelle Raaijmakers <jelle@gmta.nl>
  *
  * 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<void> 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<void> 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<void> 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<DOM::Element> m_placeholder_element;
     JS::GCPtr<DOM::Text> m_placeholder_text_node;
 

+ 5 - 5
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);
 };