From a2f101c10b2610e7cb09623b34359009c4fa9192 Mon Sep 17 00:00:00 2001 From: Bastiaan van der Plaat Date: Fri, 1 Mar 2024 08:49:04 +0100 Subject: [PATCH] LibWeb: Add input and textarea minlength and maxlength support --- .../LibWeb/Text/expected/input-maxlength.txt | 1 + .../Text/expected/textarea-maxlength.txt | 1 + Tests/LibWeb/Text/input/input-maxlength.html | 7 +++ .../LibWeb/Text/input/textarea-maxlength.html | 7 +++ Userland/Libraries/LibWeb/DOM/Text.h | 4 ++ .../Libraries/LibWeb/HTML/AttributeNames.h | 2 + .../LibWeb/HTML/HTMLInputElement.cpp | 50 ++++++++++++++++++ .../Libraries/LibWeb/HTML/HTMLInputElement.h | 8 +++ .../LibWeb/HTML/HTMLInputElement.idl | 4 +- .../LibWeb/HTML/HTMLTextAreaElement.cpp | 51 +++++++++++++++++++ .../LibWeb/HTML/HTMLTextAreaElement.h | 9 ++++ .../LibWeb/HTML/HTMLTextAreaElement.idl | 4 +- Userland/Libraries/LibWeb/HTML/Numbers.cpp | 7 +++ Userland/Libraries/LibWeb/HTML/Numbers.h | 4 ++ .../LibWeb/Page/EditEventHandler.cpp | 8 ++- 15 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/input-maxlength.txt create mode 100644 Tests/LibWeb/Text/expected/textarea-maxlength.txt create mode 100644 Tests/LibWeb/Text/input/input-maxlength.html create mode 100644 Tests/LibWeb/Text/input/textarea-maxlength.html diff --git a/Tests/LibWeb/Text/expected/input-maxlength.txt b/Tests/LibWeb/Text/expected/input-maxlength.txt new file mode 100644 index 00000000000..a8f9fd84ee0 --- /dev/null +++ b/Tests/LibWeb/Text/expected/input-maxlength.txt @@ -0,0 +1 @@ +Hello diff --git a/Tests/LibWeb/Text/expected/textarea-maxlength.txt b/Tests/LibWeb/Text/expected/textarea-maxlength.txt new file mode 100644 index 00000000000..a8f9fd84ee0 --- /dev/null +++ b/Tests/LibWeb/Text/expected/textarea-maxlength.txt @@ -0,0 +1 @@ +Hello diff --git a/Tests/LibWeb/Text/input/input-maxlength.html b/Tests/LibWeb/Text/input/input-maxlength.html new file mode 100644 index 00000000000..f6d57bc5298 --- /dev/null +++ b/Tests/LibWeb/Text/input/input-maxlength.html @@ -0,0 +1,7 @@ + diff --git a/Tests/LibWeb/Text/input/textarea-maxlength.html b/Tests/LibWeb/Text/input/textarea-maxlength.html new file mode 100644 index 00000000000..3f97d5729fa --- /dev/null +++ b/Tests/LibWeb/Text/input/textarea-maxlength.html @@ -0,0 +1,7 @@ + diff --git a/Userland/Libraries/LibWeb/DOM/Text.h b/Userland/Libraries/LibWeb/DOM/Text.h index a7dae767f84..621273880f9 100644 --- a/Userland/Libraries/LibWeb/DOM/Text.h +++ b/Userland/Libraries/LibWeb/DOM/Text.h @@ -35,6 +35,9 @@ public: void set_always_editable(bool b) { m_always_editable = b; } + Optional max_length() const { return m_max_length; } + void set_max_length(Optional max_length) { m_max_length = move(max_length); } + template T> void set_editable_text_node_owner(Badge, EditableTextNodeOwner& owner_element) { m_owner = &owner_element; } EditableTextNodeOwner* editable_text_node_owner() { return m_owner.ptr(); } @@ -55,6 +58,7 @@ private: JS::GCPtr m_owner; bool m_always_editable { false }; + Optional m_max_length {}; bool m_is_password_input { false }; }; diff --git a/Userland/Libraries/LibWeb/HTML/AttributeNames.h b/Userland/Libraries/LibWeb/HTML/AttributeNames.h index 6bee2a22482..9af36d13741 100644 --- a/Userland/Libraries/LibWeb/HTML/AttributeNames.h +++ b/Userland/Libraries/LibWeb/HTML/AttributeNames.h @@ -103,9 +103,11 @@ namespace AttributeNames { __ENUMERATE_HTML_ATTRIBUTE(marginheight) \ __ENUMERATE_HTML_ATTRIBUTE(marginwidth) \ __ENUMERATE_HTML_ATTRIBUTE(max) \ + __ENUMERATE_HTML_ATTRIBUTE(maxlength) \ __ENUMERATE_HTML_ATTRIBUTE(media) \ __ENUMERATE_HTML_ATTRIBUTE(method) \ __ENUMERATE_HTML_ATTRIBUTE(min) \ + __ENUMERATE_HTML_ATTRIBUTE(minlength) \ __ENUMERATE_HTML_ATTRIBUTE(multiple) \ __ENUMERATE_HTML_ATTRIBUTE(muted) \ __ENUMERATE_HTML_ATTRIBUTE(name) \ diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp index 09cb4db062f..7051630c59f 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -578,6 +578,19 @@ static bool is_allowed_to_be_readonly(HTML::HTMLInputElement::TypeAttributeState } } +// https://html.spec.whatwg.org/multipage/input.html#attr-input-maxlength +void HTMLInputElement::handle_maxlength_attribute() +{ + if (m_text_node) { + auto max_length = this->max_length(); + if (max_length >= 0) { + m_text_node->set_max_length(max_length); + } else { + m_text_node->set_max_length({}); + } + } +} + // https://html.spec.whatwg.org/multipage/input.html#attr-input-readonly void HTMLInputElement::handle_readonly_attribute(Optional const& maybe_value) { @@ -728,6 +741,7 @@ void HTMLInputElement::create_text_input_shadow_tree() m_text_node->set_editable_text_node_owner(Badge {}, *this); if (type_state() == TypeAttributeState::Password) m_text_node->set_is_password_input({}, true); + handle_maxlength_attribute(); MUST(m_inner_text_element->append_child(*m_text_node)); update_placeholder_visibility(); @@ -1024,6 +1038,8 @@ void HTMLInputElement::form_associated_element_attribute_changed(FlyString const } else if (name == HTML::AttributeNames::alt) { if (layout_node() && type_state() == TypeAttributeState::ImageButton) did_update_alt_text(verify_cast(*layout_node())); + } else if (name == HTML::AttributeNames::maxlength) { + handle_maxlength_attribute(); } } @@ -1445,6 +1461,40 @@ void HTMLInputElement::apply_presentational_hints(CSS::StyleProperties& style) c }); } +// https://html.spec.whatwg.org/multipage/input.html#dom-input-maxlength +WebIDL::Long HTMLInputElement::max_length() const +{ + // The maxLength IDL attribute must reflect the maxlength content attribute, limited to only non-negative numbers. + if (auto maxlength_string = get_attribute(HTML::AttributeNames::maxlength); maxlength_string.has_value()) { + if (auto maxlength = parse_non_negative_integer(*maxlength_string); maxlength.has_value()) + return *maxlength; + } + return -1; +} + +WebIDL::ExceptionOr HTMLInputElement::set_max_length(WebIDL::Long value) +{ + // The maxLength IDL attribute must reflect the maxlength content attribute, limited to only non-negative numbers. + return set_attribute(HTML::AttributeNames::maxlength, TRY(convert_non_negative_integer_to_string(realm(), value))); +} + +// https://html.spec.whatwg.org/multipage/input.html#dom-input-minlength +WebIDL::Long HTMLInputElement::min_length() const +{ + // The minLength IDL attribute must reflect the minlength content attribute, limited to only non-negative numbers. + if (auto minlength_string = get_attribute(HTML::AttributeNames::minlength); minlength_string.has_value()) { + if (auto minlength = parse_non_negative_integer(*minlength_string); minlength.has_value()) + return *minlength; + } + return -1; +} + +WebIDL::ExceptionOr HTMLInputElement::set_min_length(WebIDL::Long value) +{ + // The minLength IDL attribute must reflect the minlength content attribute, limited to only non-negative numbers. + return set_attribute(HTML::AttributeNames::minlength, TRY(convert_non_negative_integer_to_string(realm(), value))); +} + // https://html.spec.whatwg.org/multipage/input.html#the-size-attribute unsigned HTMLInputElement::size() const { diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h index 045c6589881..0f87ecbfc94 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h @@ -16,6 +16,7 @@ #include #include #include +#include namespace Web::HTML { @@ -104,6 +105,12 @@ public: // https://html.spec.whatwg.org/multipage/input.html#update-the-file-selection void update_the_file_selection(JS::NonnullGCPtr); + WebIDL::Long max_length() const; + WebIDL::ExceptionOr set_max_length(WebIDL::Long); + + WebIDL::Long min_length() const; + WebIDL::ExceptionOr set_min_length(WebIDL::Long); + unsigned size() const; WebIDL::ExceptionOr set_size(unsigned value); @@ -235,6 +242,7 @@ private: WebIDL::ExceptionOr run_input_activation_behavior(DOM::Event const&); void set_checked_within_group(); + void handle_maxlength_attribute(); void handle_readonly_attribute(Optional const& value); WebIDL::ExceptionOr handle_src_attribute(StringView value); diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.idl b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.idl index 27f19847332..a41d19732f7 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.idl +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.idl @@ -25,9 +25,9 @@ interface HTMLInputElement : HTMLElement { attribute boolean indeterminate; // FIXME: readonly attribute HTMLDataListElement? list; [CEReactions, Reflect] attribute DOMString max; - // FIXME: [CEReactions] attribute long maxLength; + [CEReactions] attribute long maxLength; [CEReactions, Reflect] attribute DOMString min; - // FIXME: [CEReactions] attribute long minLength; + [CEReactions] attribute long minLength; [CEReactions, Reflect] attribute boolean multiple; [CEReactions, Reflect] attribute DOMString name; // FIXME: [CEReactions] attribute DOMString pattern; diff --git a/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp index a3ec6d558bb..0b2e294966d 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp @@ -1,6 +1,7 @@ /* * Copyright (c) 2020, the SerenityOS developers. * Copyright (c) 2023, Sam Atkins + * Copyright (c) 2024, Bastiaan van der Plaat * * SPDX-License-Identifier: BSD-2-Clause */ @@ -147,6 +148,40 @@ u32 HTMLTextAreaElement::text_length() const return Utf16View { utf16_data }.length_in_code_units(); } +// https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-maxlength +WebIDL::Long HTMLTextAreaElement::max_length() const +{ + // The maxLength IDL attribute must reflect the maxlength content attribute, limited to only non-negative numbers. + if (auto maxlength_string = get_attribute(HTML::AttributeNames::maxlength); maxlength_string.has_value()) { + if (auto maxlength = parse_non_negative_integer(*maxlength_string); maxlength.has_value()) + return *maxlength; + } + return -1; +} + +WebIDL::ExceptionOr HTMLTextAreaElement::set_max_length(WebIDL::Long value) +{ + // The maxLength IDL attribute must reflect the maxlength content attribute, limited to only non-negative numbers. + return set_attribute(HTML::AttributeNames::maxlength, TRY(convert_non_negative_integer_to_string(realm(), value))); +} + +// https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-minlength +WebIDL::Long HTMLTextAreaElement::min_length() const +{ + // The minLength IDL attribute must reflect the minlength content attribute, limited to only non-negative numbers. + if (auto minlength_string = get_attribute(HTML::AttributeNames::minlength); minlength_string.has_value()) { + if (auto minlength = parse_non_negative_integer(*minlength_string); minlength.has_value()) + return *minlength; + } + return -1; +} + +WebIDL::ExceptionOr HTMLTextAreaElement::set_min_length(WebIDL::Long value) +{ + // The minLength IDL attribute must reflect the minlength content attribute, limited to only non-negative numbers. + return set_attribute(HTML::AttributeNames::minlength, TRY(convert_non_negative_integer_to_string(realm(), value))); +} + // https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-cols unsigned HTMLTextAreaElement::cols() const { @@ -208,6 +243,7 @@ void HTMLTextAreaElement::create_shadow_tree_if_needed() // NOTE: If `children_changed()` was called before now, `m_raw_value` will hold the text content. // Otherwise, it will get filled in whenever that does get called. m_text_node->set_text_content(m_raw_value); + handle_maxlength_attribute(); MUST(m_inner_text_element->append_child(*m_text_node)); update_placeholder_visibility(); @@ -223,6 +259,19 @@ void HTMLTextAreaElement::handle_readonly_attribute(Optional const& mayb m_text_node->set_always_editable(m_is_mutable); } +// https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-maxlength +void HTMLTextAreaElement::handle_maxlength_attribute() +{ + if (m_text_node) { + auto max_length = this->max_length(); + if (max_length >= 0) { + m_text_node->set_max_length(max_length); + } else { + m_text_node->set_max_length({}); + } + } +} + void HTMLTextAreaElement::update_placeholder_visibility() { if (!m_placeholder_element) @@ -257,6 +306,8 @@ void HTMLTextAreaElement::form_associated_element_attribute_changed(FlyString co m_placeholder_text_node->set_data(value.value_or(String {})); } else if (name == HTML::AttributeNames::readonly) { handle_readonly_attribute(value); + } else if (name == HTML::AttributeNames::maxlength) { + handle_maxlength_attribute(); } } diff --git a/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.h b/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.h index e2d4837d798..76cee0b6a68 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.h @@ -1,6 +1,7 @@ /* * Copyright (c) 2020, the SerenityOS developers. * Copyright (c) 2022, Luke Wilde + * Copyright (c) 2024, Bastiaan van der Plaat * * SPDX-License-Identifier: BSD-2-Clause */ @@ -11,6 +12,7 @@ #include #include #include +#include namespace Web::HTML { @@ -77,6 +79,12 @@ public: u32 text_length() const; + WebIDL::Long max_length() const; + WebIDL::ExceptionOr set_max_length(WebIDL::Long); + + WebIDL::Long min_length() const; + WebIDL::ExceptionOr set_min_length(WebIDL::Long); + unsigned cols() const; WebIDL::ExceptionOr set_cols(unsigned); @@ -95,6 +103,7 @@ private: void create_shadow_tree_if_needed(); void handle_readonly_attribute(Optional const& value); + void handle_maxlength_attribute(); void update_placeholder_visibility(); JS::GCPtr m_placeholder_element; diff --git a/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.idl b/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.idl index 6fe82e9eba9..0929acc7d55 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.idl +++ b/Userland/Libraries/LibWeb/HTML/HTMLTextAreaElement.idl @@ -11,8 +11,8 @@ interface HTMLTextAreaElement : HTMLElement { [CEReactions, Reflect=dirname] attribute DOMString dirName; [CEReactions, Reflect] attribute boolean disabled; readonly attribute HTMLFormElement? form; - // FIXME: [CEReactions] attribute long maxLength; - // FIXME: [CEReactions] attribute long minLength; + [CEReactions] attribute long maxLength; + [CEReactions] attribute long minLength; [CEReactions, Reflect] attribute DOMString name; [CEReactions, Reflect] attribute DOMString placeholder; [CEReactions, Reflect=readonly] attribute boolean readOnly; diff --git a/Userland/Libraries/LibWeb/HTML/Numbers.cpp b/Userland/Libraries/LibWeb/HTML/Numbers.cpp index 4c930e109c7..58d18ccefea 100644 --- a/Userland/Libraries/LibWeb/HTML/Numbers.cpp +++ b/Userland/Libraries/LibWeb/HTML/Numbers.cpp @@ -92,4 +92,11 @@ Optional parse_floating_point_number(StringView string) return maybe_double.value(); } +WebIDL::ExceptionOr convert_non_negative_integer_to_string(JS::Realm& realm, WebIDL::Long value) +{ + if (value < 0) + return WebIDL::IndexSizeError::create(realm, "The attribute is limited to only non-negative numbers"_fly_string); + return MUST(String::number(value)); +} + } diff --git a/Userland/Libraries/LibWeb/HTML/Numbers.h b/Userland/Libraries/LibWeb/HTML/Numbers.h index 83f2aeeca4f..28d9be20e57 100644 --- a/Userland/Libraries/LibWeb/HTML/Numbers.h +++ b/Userland/Libraries/LibWeb/HTML/Numbers.h @@ -8,6 +8,8 @@ #include #include +#include +#include namespace Web::HTML { @@ -17,4 +19,6 @@ Optional parse_non_negative_integer(StringView string); Optional parse_floating_point_number(StringView string); +WebIDL::ExceptionOr convert_non_negative_integer_to_string(JS::Realm&, WebIDL::Long); + } diff --git a/Userland/Libraries/LibWeb/Page/EditEventHandler.cpp b/Userland/Libraries/LibWeb/Page/EditEventHandler.cpp index 9dfe950c00f..5b3dccdcfb2 100644 --- a/Userland/Libraries/LibWeb/Page/EditEventHandler.cpp +++ b/Userland/Libraries/LibWeb/Page/EditEventHandler.cpp @@ -111,8 +111,14 @@ void EditEventHandler::handle_insert(JS::NonnullGCPtr position, u builder.append(node.data().bytes_as_string_view().substring_view(0, position->offset())); builder.append_code_point(code_point); builder.append(node.data().bytes_as_string_view().substring_view(position->offset())); - node.set_data(MUST(builder.to_string())); + // Cut string by max length + // FIXME: Cut by UTF-16 code units instead of raw bytes + if (auto max_length = node.max_length(); max_length.has_value() && builder.string_view().length() > *max_length) { + node.set_data(MUST(String::from_utf8(builder.string_view().substring_view(0, *max_length)))); + } else { + node.set_data(MUST(builder.to_string())); + } node.invalidate_style(); } else { auto& node = *position->node();