浏览代码

LibWeb: Implement setRangeText for input and textarea elements

This method replaces range of text in its respective element with a new
string
Tim Ledbetter 11 月之前
父节点
当前提交
2f5b070716

+ 13 - 0
Tests/LibWeb/Text/expected/DOM/FormAssociatedElement-setRangeText.txt

@@ -0,0 +1,13 @@
+   Calling setRangeText() on an element where setRangeText doesn't apply throws: InvalidStateError
+setRangeText("foo") on: textarea - Value: foo
+setRangeText("foo", 1, 3) on: textarea - Value: tfoot
+setRangeText("foo") on: password - Value: footest
+setRangeText("foo", 1, 3) on: passwordInput - Value: tfoot
+setRangeText("foo") on: search - Value: footest
+setRangeText("foo", 1, 3) on: searchInput - Value: tfoot
+setRangeText("foo") on: tel - Value: footest
+setRangeText("foo", 1, 3) on: telInput - Value: tfoot
+setRangeText("foo") on: text - Value: footest
+setRangeText("foo", 1, 3) on: textInput - Value: tfoot
+setRangeText("foo") on: url - Value: footest
+setRangeText("foo", 1, 3) on: urlInput - Value: tfoot

+ 42 - 0
Tests/LibWeb/Text/input/DOM/FormAssociatedElement-setRangeText.html

@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<div id="container">
+    <textarea id="textarea"></textarea>
+    <input id="passwordInput" type="password" value="test">
+    <input id="searchInput" type=search value="test">
+    <input id="telInput" type="tel" value="test">
+    <input id="textInput" type="text" value="test">
+    <input id="urlInput" type="url" value="test">
+    <input id="invalidInput" type="checkbox" value="test">
+</div>
+<script src="../include.js"></script>
+<script>
+     test(() => {
+        const invalidInput = document.getElementById('invalidInput');
+        try {
+            invalidInput.setRangeText('foo');
+            println("FAIL");
+        } catch (e) {
+            println(`Calling setRangeText() on an element where setRangeText doesn't apply throws: ${e.name}`);
+        }
+
+        const validInputs = [
+            document.getElementById('textarea'),
+            document.getElementById('passwordInput'),
+            document.getElementById('searchInput'),
+            document.getElementById('telInput'),
+            document.getElementById('textInput'),
+            document.getElementById('urlInput')
+        ];
+
+        for (let input of validInputs) {
+            input.setRangeText("foo");
+            println(`setRangeText("foo") on: ${input.type} - Value: ${input.value}`);
+
+            input.value = "test";
+            input.setRangeText("foo", 1, 3);
+            println(`setRangeText("foo", 1, 3) on: ${input.id} - Value: ${input.value}`);
+        }
+
+        document.getElementById("container").remove();
+    });
+</script>

+ 127 - 0
Userland/Libraries/LibWeb/HTML/FormAssociatedElement.cpp

@@ -1,6 +1,7 @@
 /*
 /*
  * Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
  * Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
  * Copyright (c) 2024, Jelle Raaijmakers <jelle@gmta.nl>
  * Copyright (c) 2024, Jelle Raaijmakers <jelle@gmta.nl>
+ * Copyright (c) 2024, Tim Ledbetter <tim.ledbetter@ladybird.org>
  *
  *
  * SPDX-License-Identifier: BSD-2-Clause
  * SPDX-License-Identifier: BSD-2-Clause
  */
  */
@@ -345,6 +346,132 @@ void FormAssociatedTextControlElement::set_selection_direction(Optional<String>
     m_selection_direction = string_to_selection_direction(direction);
     m_selection_direction = string_to_selection_direction(direction);
 }
 }
 
 
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setrangetext
+WebIDL::ExceptionOr<void> FormAssociatedTextControlElement::set_range_text(String const& replacement)
+{
+    return set_range_text(replacement, m_selection_start, m_selection_end);
+}
+
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setrangetext
+WebIDL::ExceptionOr<void> FormAssociatedTextControlElement::set_range_text(String const& replacement, WebIDL::UnsignedLong start, WebIDL::UnsignedLong end, Bindings::SelectionMode selection_mode)
+{
+    // 1. If this element is an input element, and setRangeText() 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(), "setRangeText does not apply to this input type"_fly_string);
+
+    // 2. Set this element's dirty value flag to true.
+    set_dirty_value_flag(true);
+
+    // 3. If the method has only one argument, then let start and end have the values of the selectionStart attribute and the selectionEnd attribute respectively.
+    //    Otherwise, let start, end have the values of the second and third arguments respectively.
+    // NOTE: This is handled by the caller.
+
+    // 4. If start is greater than end, then throw an "IndexSizeError" DOMException.
+    if (start > end)
+        return WebIDL::IndexSizeError::create(html_element.realm(), "The start argument must be less than or equal to the end argument"_fly_string);
+
+    // 5. If start is greater than the length of the relevant value of the text control, then set it to the length of the relevant value of the text control.
+    auto the_relevant_value = relevant_value();
+    auto relevant_value_length = the_relevant_value.code_points().length();
+    if (start > relevant_value_length)
+        start = relevant_value_length;
+
+    // 6. If end is greater than the length of the relevant value of the text control, then set it to the length of the relevant value of the text control.
+    if (end > relevant_value_length)
+        end = relevant_value_length;
+
+    // 7. Let selection start be the current value of the selectionStart attribute.
+    auto selection_start = m_selection_start;
+
+    // 8. Let selection end be the current value of the selectionEnd attribute.
+    auto selection_end = m_selection_end;
+
+    // 9. If start is less than end, delete the sequence of code units within the element's relevant value starting with
+    //    the code unit at the startth position and ending with the code unit at the (end-1)th position.
+    if (start < end) {
+        StringBuilder builder;
+        auto before_removal_point_view = the_relevant_value.code_points().unicode_substring_view(0, start);
+        builder.append(before_removal_point_view.as_string());
+        auto after_removal_point_view = the_relevant_value.code_points().unicode_substring_view(end);
+        builder.append(after_removal_point_view.as_string());
+        the_relevant_value = MUST(builder.to_string());
+    }
+
+    // 10. Insert the value of the first argument into the text of the relevant value of the text control, immediately before the startth code unit.
+    StringBuilder builder;
+    auto before_insertion_point_view = the_relevant_value.code_points().unicode_substring_view(0, start);
+    builder.append(before_insertion_point_view.as_string());
+    builder.append(replacement);
+    auto after_insertion_point_view = the_relevant_value.code_points().unicode_substring_view(start);
+    builder.append(after_insertion_point_view.as_string());
+    the_relevant_value = MUST(builder.to_string());
+    TRY(set_relevant_value(the_relevant_value));
+
+    // 11. Let new length be the length of the value of the first argument.
+    i64 new_length = replacement.code_points().length();
+
+    // 12. Let new end be the sum of start and new length.
+    auto new_end = start + new_length;
+
+    // 13. Run the appropriate set of substeps from the following list:
+    switch (selection_mode) {
+    // If the fourth argument's value is "select"
+    case Bindings::SelectionMode::Select:
+        // Let selection start be start.
+        selection_start = start;
+
+        // Let selection end be new end.
+        selection_end = new_end;
+        break;
+
+    // If the fourth argument's value is "start"
+    case Bindings::SelectionMode::Start:
+        // Let selection start and selection end be start.
+        selection_start = start;
+        selection_end = start;
+        break;
+
+    // If the fourth argument's value is "end"
+    case Bindings::SelectionMode::End:
+        selection_start = new_end;
+        selection_end = new_end;
+        break;
+
+    // If the fourth argument's value is "preserve"
+    case Bindings::SelectionMode::Preserve:
+        // 1. Let old length be end minus start.
+        auto old_length = end - start;
+
+        // 2. Let delta be new length minus old length.
+        auto delta = new_length - old_length;
+
+        // 3. If selection start is greater than end, then increment it by delta.
+        //    (If delta is negative, i.e. the new text is shorter than the old text, then this will decrease the value of selection start.)
+        //    Otherwise: if selection start is greater than start, then set it to start.
+        //    (This snaps the start of the selection to the start of the new text if it was in the middle of the text that it replaced.)
+        if (selection_start > end)
+            selection_start += delta;
+        else if (selection_start > start)
+            selection_start = start;
+
+        // 4. If selection end is greater than end, then increment it by delta in the same way.
+        //    Otherwise: if selection end is greater than start, then set it to new end.
+        //    (This snaps the end of the selection to the end of the new text if it was in the middle of the text that it replaced.)
+        if (selection_end > end)
+            selection_end += delta;
+        else if (selection_end > start)
+            selection_end = new_end;
+        break;
+    }
+
+    // 14. Set the selection range with selection start and selection end.
+    set_the_selection_range(selection_start, selection_end);
+
+    return {};
+}
+
 // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setselectionrange
 // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setselectionrange
 WebIDL::ExceptionOr<void> FormAssociatedTextControlElement::set_selection_range(Optional<WebIDL::UnsignedLong> start, Optional<WebIDL::UnsignedLong> end, Optional<String> direction)
 WebIDL::ExceptionOr<void> FormAssociatedTextControlElement::set_selection_range(Optional<WebIDL::UnsignedLong> start, Optional<WebIDL::UnsignedLong> end, Optional<String> direction)
 {
 {

+ 8 - 0
Userland/Libraries/LibWeb/HTML/FormAssociatedElement.h

@@ -9,6 +9,7 @@
 
 
 #include <AK/String.h>
 #include <AK/String.h>
 #include <AK/WeakPtr.h>
 #include <AK/WeakPtr.h>
+#include <LibWeb/Bindings/HTMLFormElementPrototype.h>
 #include <LibWeb/Forward.h>
 #include <LibWeb/Forward.h>
 #include <LibWeb/WebIDL/Types.h>
 #include <LibWeb/WebIDL/Types.h>
 
 
@@ -124,6 +125,9 @@ class FormAssociatedTextControlElement : public FormAssociatedElement {
 public:
 public:
     // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
     // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
     virtual String relevant_value() = 0;
     virtual String relevant_value() = 0;
+    virtual WebIDL::ExceptionOr<void> set_relevant_value(String const&) = 0;
+
+    virtual void set_dirty_value_flag(bool flag) = 0;
 
 
     // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-select
     // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-select
     WebIDL::ExceptionOr<void> select();
     WebIDL::ExceptionOr<void> select();
@@ -141,6 +145,10 @@ public:
     void set_selection_direction(Optional<String> direction);
     void set_selection_direction(Optional<String> direction);
     SelectionDirection selection_direction_state() const { return m_selection_direction; }
     SelectionDirection selection_direction_state() const { return m_selection_direction; }
 
 
+    // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setrangetext
+    WebIDL::ExceptionOr<void> set_range_text(String const& replacement);
+    WebIDL::ExceptionOr<void> set_range_text(String const& replacement, WebIDL::UnsignedLong start, WebIDL::UnsignedLong end, Bindings::SelectionMode = Bindings::SelectionMode::Preserve);
+
     // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setselectionrange
     // 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);
     void set_the_selection_range(Optional<WebIDL::UnsignedLong> start, Optional<WebIDL::UnsignedLong> end, SelectionDirection direction = SelectionDirection::None);
 
 

+ 8 - 0
Userland/Libraries/LibWeb/HTML/HTMLFormElement.idl

@@ -16,6 +16,14 @@ enum EnctypeAttribute {
     "text/plain"
     "text/plain"
 };
 };
 
 
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#selectionmode
+enum SelectionMode {
+    "select",
+    "start",
+    "end",
+    "preserve"
+};
+
 // https://html.spec.whatwg.org/multipage/semantics.html#htmlformelement
 // https://html.spec.whatwg.org/multipage/semantics.html#htmlformelement
 [Exposed=Window, LegacyOverrideBuiltIns, LegacyUnenumerableNamedProperties]
 [Exposed=Window, LegacyOverrideBuiltIns, LegacyUnenumerableNamedProperties]
 interface HTMLFormElement : HTMLElement {
 interface HTMLFormElement : HTMLElement {

+ 3 - 0
Userland/Libraries/LibWeb/HTML/HTMLInputElement.h

@@ -79,6 +79,9 @@ public:
 
 
     // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
     // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
     virtual String relevant_value() override { return value(); }
     virtual String relevant_value() override { return value(); }
+    WebIDL::ExceptionOr<void> set_relevant_value(String const& value) override { return set_value(value); }
+
+    virtual void set_dirty_value_flag(bool flag) override { m_dirty_value = flag; }
 
 
     void commit_pending_changes();
     void commit_pending_changes();
 
 

+ 2 - 2
Userland/Libraries/LibWeb/HTML/HTMLInputElement.idl

@@ -61,8 +61,8 @@ interface HTMLInputElement : HTMLElement {
     attribute unsigned long? selectionStart;
     attribute unsigned long? selectionStart;
     attribute unsigned long? selectionEnd;
     attribute unsigned long? selectionEnd;
     attribute DOMString? selectionDirection;
     attribute DOMString? selectionDirection;
-    [FIXME] undefined setRangeText(DOMString replacement);
-    [FIXME] undefined setRangeText(DOMString replacement, unsigned long start, unsigned long end, optional SelectionMode selectionMode = "preserve");
+    undefined setRangeText(DOMString replacement);
+    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);
     undefined setSelectionRange(unsigned long start, unsigned long end, optional DOMString direction);
 
 
     undefined showPicker();
     undefined showPicker();

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

@@ -205,6 +205,13 @@ String HTMLTextAreaElement::api_value() const
     return *m_api_value;
     return *m_api_value;
 }
 }
 
 
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
+WebIDL::ExceptionOr<void> HTMLTextAreaElement::set_relevant_value(String const& value)
+{
+    set_value(value);
+    return {};
+}
+
 // https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-textlength
 // https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-textlength
 u32 HTMLTextAreaElement::text_length() const
 u32 HTMLTextAreaElement::text_length() const
 {
 {

+ 5 - 0
Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.h

@@ -86,6 +86,9 @@ public:
 
 
     // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
     // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
     virtual String relevant_value() override { return api_value(); }
     virtual String relevant_value() override { return api_value(); }
+    virtual WebIDL::ExceptionOr<void> set_relevant_value(String const& value) override;
+
+    virtual void set_dirty_value_flag(bool flag) override { m_dirty_value = flag; }
 
 
     u32 text_length() const;
     u32 text_length() const;
 
 
@@ -117,6 +120,8 @@ public:
     String selection_direction_binding() const;
     String selection_direction_binding() const;
     void set_selection_direction_binding(String direction);
     void set_selection_direction_binding(String direction);
 
 
+    void set_dirty_value_flag(Badge<FormAssociatedElement>, bool flag) { m_dirty_value = flag; }
+
 protected:
 protected:
     void selection_was_changed() override;
     void selection_was_changed() override;
 
 

+ 2 - 2
Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.idl

@@ -38,7 +38,7 @@ interface HTMLTextAreaElement : HTMLElement {
     [ImplementedAs=selection_start_binding] attribute unsigned long selectionStart;
     [ImplementedAs=selection_start_binding] attribute unsigned long selectionStart;
     [ImplementedAs=selection_end_binding] attribute unsigned long selectionEnd;
     [ImplementedAs=selection_end_binding] attribute unsigned long selectionEnd;
     [ImplementedAs=selection_direction_binding] attribute DOMString selectionDirection;
     [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");
+    undefined setRangeText(DOMString replacement);
+    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);
     undefined setSelectionRange(unsigned long start, unsigned long end, optional DOMString direction);
 };
 };