mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 17:52:45 +00:00 
			
		
		
		
	LibWeb: Add input and textarea minlength and maxlength support
This commit is contained in:
		
							parent
							
								
									9b645d20b9
								
							
						
					
					
						commit
						a2f101c10b
					
				
					 15 changed files with 162 additions and 5 deletions
				
			
		
							
								
								
									
										1
									
								
								Tests/LibWeb/Text/expected/input-maxlength.txt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								Tests/LibWeb/Text/expected/input-maxlength.txt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| Hello  | ||||
							
								
								
									
										1
									
								
								Tests/LibWeb/Text/expected/textarea-maxlength.txt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								Tests/LibWeb/Text/expected/textarea-maxlength.txt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| Hello  | ||||
							
								
								
									
										7
									
								
								Tests/LibWeb/Text/input/input-maxlength.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								Tests/LibWeb/Text/input/input-maxlength.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| <input id="input" type="text" maxlength="5"><script src="include.js"></script><script> | ||||
|     test(() => { | ||||
|         const input = document.getElementById('input'); | ||||
|         internals.sendText(input, 'Hello World!'); | ||||
|         internals.commitText(); | ||||
|     }); | ||||
| </script> | ||||
							
								
								
									
										7
									
								
								Tests/LibWeb/Text/input/textarea-maxlength.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								Tests/LibWeb/Text/input/textarea-maxlength.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| <textarea id="textarea" maxlength="5"></textarea><script src="include.js"></script><script> | ||||
|     test(() => { | ||||
|         const textarea = document.getElementById('textarea'); | ||||
|         internals.sendText(textarea, 'Hello World!'); | ||||
|         internals.commitText(); | ||||
|     }); | ||||
| </script> | ||||
|  | @ -35,6 +35,9 @@ public: | |||
| 
 | ||||
|     void set_always_editable(bool b) { m_always_editable = b; } | ||||
| 
 | ||||
|     Optional<size_t> max_length() const { return m_max_length; } | ||||
|     void set_max_length(Optional<size_t> max_length) { m_max_length = move(max_length); } | ||||
| 
 | ||||
|     template<DerivedFrom<EditableTextNodeOwner> T> | ||||
|     void set_editable_text_node_owner(Badge<T>, EditableTextNodeOwner& owner_element) { m_owner = &owner_element; } | ||||
|     EditableTextNodeOwner* editable_text_node_owner() { return m_owner.ptr(); } | ||||
|  | @ -55,6 +58,7 @@ private: | |||
|     JS::GCPtr<EditableTextNodeOwner> m_owner; | ||||
| 
 | ||||
|     bool m_always_editable { false }; | ||||
|     Optional<size_t> m_max_length {}; | ||||
|     bool m_is_password_input { false }; | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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)                       \ | ||||
|  |  | |||
|  | @ -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<String> const& maybe_value) | ||||
| { | ||||
|  | @ -728,6 +741,7 @@ void HTMLInputElement::create_text_input_shadow_tree() | |||
|     m_text_node->set_editable_text_node_owner(Badge<HTMLInputElement> {}, *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::ImageBox>(*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<void> 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<void> 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 | ||||
| { | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ | |||
| #include <LibWeb/HTML/HTMLElement.h> | ||||
| #include <LibWeb/Layout/ImageProvider.h> | ||||
| #include <LibWeb/WebIDL/DOMException.h> | ||||
| #include <LibWeb/WebIDL/Types.h> | ||||
| 
 | ||||
| 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<FileAPI::FileList>); | ||||
| 
 | ||||
|     WebIDL::Long max_length() const; | ||||
|     WebIDL::ExceptionOr<void> set_max_length(WebIDL::Long); | ||||
| 
 | ||||
|     WebIDL::Long min_length() const; | ||||
|     WebIDL::ExceptionOr<void> set_min_length(WebIDL::Long); | ||||
| 
 | ||||
|     unsigned size() const; | ||||
|     WebIDL::ExceptionOr<void> set_size(unsigned value); | ||||
| 
 | ||||
|  | @ -235,6 +242,7 @@ private: | |||
|     WebIDL::ExceptionOr<void> run_input_activation_behavior(DOM::Event const&); | ||||
|     void set_checked_within_group(); | ||||
| 
 | ||||
|     void handle_maxlength_attribute(); | ||||
|     void handle_readonly_attribute(Optional<String> const& value); | ||||
|     WebIDL::ExceptionOr<void> handle_src_attribute(StringView value); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -1,6 +1,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> | ||||
|  * | ||||
|  * 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<void> 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<void> 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<String> 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(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,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> | ||||
|  * | ||||
|  * SPDX-License-Identifier: BSD-2-Clause | ||||
|  */ | ||||
|  | @ -11,6 +12,7 @@ | |||
| #include <LibWeb/DOM/Text.h> | ||||
| #include <LibWeb/HTML/FormAssociatedElement.h> | ||||
| #include <LibWeb/HTML/HTMLElement.h> | ||||
| #include <LibWeb/WebIDL/Types.h> | ||||
| 
 | ||||
| namespace Web::HTML { | ||||
| 
 | ||||
|  | @ -77,6 +79,12 @@ public: | |||
| 
 | ||||
|     u32 text_length() const; | ||||
| 
 | ||||
|     WebIDL::Long max_length() const; | ||||
|     WebIDL::ExceptionOr<void> set_max_length(WebIDL::Long); | ||||
| 
 | ||||
|     WebIDL::Long min_length() const; | ||||
|     WebIDL::ExceptionOr<void> set_min_length(WebIDL::Long); | ||||
| 
 | ||||
|     unsigned cols() const; | ||||
|     WebIDL::ExceptionOr<void> set_cols(unsigned); | ||||
| 
 | ||||
|  | @ -95,6 +103,7 @@ private: | |||
|     void create_shadow_tree_if_needed(); | ||||
| 
 | ||||
|     void handle_readonly_attribute(Optional<String> const& value); | ||||
|     void handle_maxlength_attribute(); | ||||
| 
 | ||||
|     void update_placeholder_visibility(); | ||||
|     JS::GCPtr<DOM::Element> m_placeholder_element; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -92,4 +92,11 @@ Optional<double> parse_floating_point_number(StringView string) | |||
|     return maybe_double.value(); | ||||
| } | ||||
| 
 | ||||
| WebIDL::ExceptionOr<String> 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)); | ||||
| } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -8,6 +8,8 @@ | |||
| 
 | ||||
| #include <AK/Forward.h> | ||||
| #include <AK/String.h> | ||||
| #include <LibWeb/WebIDL/ExceptionOr.h> | ||||
| #include <LibWeb/WebIDL/Types.h> | ||||
| 
 | ||||
| namespace Web::HTML { | ||||
| 
 | ||||
|  | @ -17,4 +19,6 @@ Optional<u32> parse_non_negative_integer(StringView string); | |||
| 
 | ||||
| Optional<double> parse_floating_point_number(StringView string); | ||||
| 
 | ||||
| WebIDL::ExceptionOr<String> convert_non_negative_integer_to_string(JS::Realm&, WebIDL::Long); | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -111,8 +111,14 @@ void EditEventHandler::handle_insert(JS::NonnullGCPtr<DOM::Position> 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(); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Bastiaan van der Plaat
						Bastiaan van der Plaat