mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-08-12 02:59:45 +00:00
LibWeb: Support autocomplete
attribute on form elements
Implement proper support for the `autocomplete` attribute in `input`, `select` and `textarea` elements.
This commit is contained in:
parent
2d7080ecb3
commit
b8f234719d
Notes:
github-actions[bot]
2025-02-26 07:02:12 +00:00
Author: https://github.com/devgianlu
Commit: b8f234719d
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3517
Reviewed-by: https://github.com/ADKaster
Reviewed-by: https://github.com/tcl3 ✅
11 changed files with 644 additions and 9 deletions
|
@ -298,6 +298,7 @@ set(SOURCES
|
||||||
HTML/AttributeNames.cpp
|
HTML/AttributeNames.cpp
|
||||||
HTML/AudioTrack.cpp
|
HTML/AudioTrack.cpp
|
||||||
HTML/AudioTrackList.cpp
|
HTML/AudioTrackList.cpp
|
||||||
|
HTML/AutocompleteElement.cpp
|
||||||
HTML/BeforeUnloadEvent.cpp
|
HTML/BeforeUnloadEvent.cpp
|
||||||
HTML/BroadcastChannel.cpp
|
HTML/BroadcastChannel.cpp
|
||||||
HTML/BrowsingContext.cpp
|
HTML/BrowsingContext.cpp
|
||||||
|
|
369
Libraries/LibWeb/HTML/AutocompleteElement.cpp
Normal file
369
Libraries/LibWeb/HTML/AutocompleteElement.cpp
Normal file
|
@ -0,0 +1,369 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025, Altomani Gianluca <altomanigianluca@gmail.com>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <LibWeb/HTML/AttributeNames.h>
|
||||||
|
#include <LibWeb/HTML/AutocompleteElement.h>
|
||||||
|
#include <LibWeb/HTML/HTMLElement.h>
|
||||||
|
#include <LibWeb/HTML/HTMLFormElement.h>
|
||||||
|
#include <LibWeb/HTML/HTMLInputElement.h>
|
||||||
|
#include <LibWeb/Infra/CharacterTypes.h>
|
||||||
|
#include <LibWeb/WebIDL/ExceptionOr.h>
|
||||||
|
|
||||||
|
namespace Web::HTML {
|
||||||
|
|
||||||
|
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-expectation-mantle
|
||||||
|
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-anchor-mantle
|
||||||
|
AutocompleteElement::AutofillMantle AutocompleteElement::get_autofill_mantle() const
|
||||||
|
{
|
||||||
|
auto const& element = autocomplete_element_to_html_element();
|
||||||
|
|
||||||
|
// On an input element whose type attribute is in the Hidden state, the autocomplete attribute wears the autofill anchor mantle.
|
||||||
|
if (is<HTMLInputElement>(element)) {
|
||||||
|
auto const& input_element = as<HTMLInputElement>(element);
|
||||||
|
if (input_element.type_state() == HTMLInputElement::TypeAttributeState::Hidden)
|
||||||
|
return AutofillMantle::Anchor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In all other cases, it wears the autofill expectation mantle.
|
||||||
|
return AutofillMantle::Expectation;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector<String> AutocompleteElement::autocomplete_tokens() const
|
||||||
|
{
|
||||||
|
auto autocomplete_value = autocomplete_element_to_html_element().attribute(AttributeNames::autocomplete).value_or({});
|
||||||
|
|
||||||
|
Vector<String> autocomplete_tokens;
|
||||||
|
for (auto& token : autocomplete_value.bytes_as_string_view().split_view_if(Infra::is_ascii_whitespace))
|
||||||
|
autocomplete_tokens.append(MUST(String::from_utf8(token)));
|
||||||
|
return autocomplete_tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
String AutocompleteElement::autocomplete() const
|
||||||
|
{
|
||||||
|
// The autocomplete IDL attribute, on getting, must return the element's IDL-exposed autofill value.
|
||||||
|
auto details = parse_autocomplete_attribute();
|
||||||
|
return details.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
WebIDL::ExceptionOr<void> AutocompleteElement::set_autocomplete(String const& value)
|
||||||
|
{
|
||||||
|
// The autocomplete IDL attribute [...] on setting, must reflect the content attribute of the same name.
|
||||||
|
TRY(autocomplete_element_to_html_element().set_attribute(AttributeNames::autocomplete, value));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Category {
|
||||||
|
Off,
|
||||||
|
Automatic,
|
||||||
|
Normal,
|
||||||
|
Contact,
|
||||||
|
Credential,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CategoryAndMaximumTokens {
|
||||||
|
Optional<Category> category;
|
||||||
|
Optional<size_t> maximum_tokens;
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#determine-a-field's-category
|
||||||
|
static CategoryAndMaximumTokens determine_a_field_category(StringView const& field)
|
||||||
|
{
|
||||||
|
#define CASE_CATEGORY(token, maximum_number_of_tokens, category) \
|
||||||
|
if (field.equals_ignoring_ascii_case(token)) \
|
||||||
|
return CategoryAndMaximumTokens { Category::category, maximum_number_of_tokens };
|
||||||
|
|
||||||
|
// 1. If the field is not an ASCII case-insensitive match for one of the tokens given
|
||||||
|
// in the first column of the following table, return the pair (null, null).
|
||||||
|
// 2. Otherwise, let maximum tokens and category be the values of the cells in the second
|
||||||
|
// and third columns of that row respectively.
|
||||||
|
// 3. Return the pair (category, maximum tokens).
|
||||||
|
CASE_CATEGORY("off"sv, 1, Off);
|
||||||
|
CASE_CATEGORY("on"sv, 1, Automatic);
|
||||||
|
CASE_CATEGORY("name"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("honorific-prefix"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("given-name"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("additional-name"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("family-name"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("honorific-suffix"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("nickname"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("organization-title"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("username"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("new-password"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("current-password"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("one-time-code"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("organization"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("street-address"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("address-line1"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("address-line2"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("address-line3"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("address-level4"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("address-level3"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("address-level2"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("address-level1"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("country"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("country-name"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("postal-code"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("cc-name"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("cc-given-name"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("cc-additional-name"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("cc-family-name"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("cc-number"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("cc-exp"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("cc-exp-month"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("cc-exp-year"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("cc-csc"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("cc-type"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("transaction-currency"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("transaction-amount"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("language"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("bday"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("bday-day"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("bday-month"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("bday-year"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("sex"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("url"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("photo"sv, 3, Normal);
|
||||||
|
CASE_CATEGORY("tel"sv, 4, Contact);
|
||||||
|
CASE_CATEGORY("tel-country-code"sv, 4, Contact);
|
||||||
|
CASE_CATEGORY("tel-national"sv, 4, Contact);
|
||||||
|
CASE_CATEGORY("tel-area-code"sv, 4, Contact);
|
||||||
|
CASE_CATEGORY("tel-local"sv, 4, Contact);
|
||||||
|
CASE_CATEGORY("tel-local-prefix"sv, 4, Contact);
|
||||||
|
CASE_CATEGORY("tel-local-suffix"sv, 4, Contact);
|
||||||
|
CASE_CATEGORY("tel-extension"sv, 4, Contact);
|
||||||
|
CASE_CATEGORY("email"sv, 4, Contact);
|
||||||
|
CASE_CATEGORY("impp"sv, 4, Contact);
|
||||||
|
CASE_CATEGORY("webauthn"sv, 5, Credential);
|
||||||
|
|
||||||
|
#undef CASE_CATEGORY
|
||||||
|
|
||||||
|
return CategoryAndMaximumTokens {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-processing-model
|
||||||
|
AutocompleteElement::AttributeDetails AutocompleteElement::parse_autocomplete_attribute() const
|
||||||
|
{
|
||||||
|
AttributeDetails attr_details {};
|
||||||
|
|
||||||
|
auto step_default = [&] {
|
||||||
|
// 32. Default: Let the element's IDL-exposed autofill value be the empty string, and its autofill hint set and autofill scope be empty.
|
||||||
|
attr_details.value = {};
|
||||||
|
attr_details.hint_set = {};
|
||||||
|
attr_details.scope = {};
|
||||||
|
|
||||||
|
// 33. If the element's autocomplete attribute is wearing the autofill anchor mantle,
|
||||||
|
// then let the element's autofill field name be the empty string and return.
|
||||||
|
if (get_autofill_mantle() == AutofillMantle::Anchor) {
|
||||||
|
attr_details.field_name = {};
|
||||||
|
return attr_details;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 34. Let form be the element's form owner, if any, or null otherwise.
|
||||||
|
auto const* form = as<FormAssociatedElement const>(autocomplete_element_to_html_element()).form();
|
||||||
|
|
||||||
|
// 35. If form is not null and form's autocomplete attribute is in the off state, then let the element's autofill field name be "off".
|
||||||
|
if (form && form->attribute(AttributeNames::autocomplete) == idl_enum_to_string(Bindings::Autocomplete::Off)) {
|
||||||
|
attr_details.field_name = "off"_string;
|
||||||
|
}
|
||||||
|
// Otherwise, let the element's autofill field name be "on".
|
||||||
|
else {
|
||||||
|
attr_details.field_name = "on"_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attr_details;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. If the element has no autocomplete attribute, then jump to the step labeled default.
|
||||||
|
if (!autocomplete_element_to_html_element().has_attribute(AttributeNames::autocomplete))
|
||||||
|
return step_default();
|
||||||
|
|
||||||
|
// 2. Let tokens be the result of splitting the attribute's value on ASCII whitespace.
|
||||||
|
auto tokens = autocomplete_tokens();
|
||||||
|
|
||||||
|
// 3. If tokens is empty, then jump to the step labeled default.
|
||||||
|
if (tokens.is_empty())
|
||||||
|
return step_default();
|
||||||
|
|
||||||
|
// 4. Let index be the index of the last token in tokens.
|
||||||
|
auto index = tokens.size() - 1;
|
||||||
|
|
||||||
|
// 5. Let field be the indexth token in tokens.
|
||||||
|
auto const& field = tokens[index];
|
||||||
|
|
||||||
|
// 6. Set the category, maximum tokens pair to the result of determining a field's category given field.
|
||||||
|
auto [category, maximum_tokens] = determine_a_field_category(field);
|
||||||
|
|
||||||
|
// 7. If category is null, then jump to the step labeled default.
|
||||||
|
if (!category.has_value())
|
||||||
|
return step_default();
|
||||||
|
|
||||||
|
// 8. If the number of tokens in tokens is greater than maximum tokens, then jump to the step labeled default.
|
||||||
|
if (tokens.size() > maximum_tokens.value())
|
||||||
|
return step_default();
|
||||||
|
|
||||||
|
// 9. If category is Off or Automatic but the element's autocomplete attribute is wearing the autofill anchor mantle,
|
||||||
|
// then jump to the step labeled default.
|
||||||
|
if ((category == Category::Off || category == Category::Automatic)
|
||||||
|
&& get_autofill_mantle() == AutofillMantle::Anchor)
|
||||||
|
return step_default();
|
||||||
|
|
||||||
|
// 10. If category is Off, let the element's autofill field name be the string "off", let its autofill hint set be empty,
|
||||||
|
// and let its IDL-exposed autofill value be the string "off". Then, return.
|
||||||
|
if (category == Category::Off) {
|
||||||
|
attr_details.field_name = "off"_string;
|
||||||
|
attr_details.hint_set = {};
|
||||||
|
attr_details.value = "off"_string;
|
||||||
|
return attr_details;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 11. If category is Automatic, let the element's autofill field name be the string "on", let its autofill hint set be empty,
|
||||||
|
// and let its IDL-exposed autofill value be the string "on". Then, return.
|
||||||
|
if (category == Category::Automatic) {
|
||||||
|
attr_details.field_name = "on"_string;
|
||||||
|
attr_details.hint_set = {};
|
||||||
|
attr_details.value = "on"_string;
|
||||||
|
return attr_details;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12. Let scope tokens be an empty list.
|
||||||
|
Vector<String> scope_tokens;
|
||||||
|
|
||||||
|
// 13. Let hint tokens be an empty set.
|
||||||
|
HashTable<String> hint_tokens;
|
||||||
|
|
||||||
|
// 14. Let credential type be null.
|
||||||
|
Optional<String> credential_type;
|
||||||
|
|
||||||
|
// 15. Let IDL value have the same value as field.
|
||||||
|
// NOTE: lowercasing is not mentioned in the spec, but required to pass all WPT tests.
|
||||||
|
auto idl_value = field.to_ascii_lowercase();
|
||||||
|
|
||||||
|
auto step_done = [&] {
|
||||||
|
// 26. Done: Let the element's autofill hint set be hint tokens.
|
||||||
|
attr_details.hint_set = hint_tokens.values();
|
||||||
|
|
||||||
|
// 27. Let the element's non-autofill credential type be credential type.
|
||||||
|
attr_details.credential_type = credential_type;
|
||||||
|
|
||||||
|
// 28. Let the element's autofill scope be scope tokens.
|
||||||
|
attr_details.scope = scope_tokens;
|
||||||
|
|
||||||
|
// 29. Let the element's autofill field name be field.
|
||||||
|
attr_details.field_name = field;
|
||||||
|
|
||||||
|
// 30. Let the element's IDL-exposed autofill value be IDL value.
|
||||||
|
attr_details.value = idl_value;
|
||||||
|
|
||||||
|
// 31. Return.
|
||||||
|
return attr_details;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 16. If category is Credential and the indexth token in tokens is an ASCII case-insensitive match for "webauthn",
|
||||||
|
// then run the substeps that follow:
|
||||||
|
if (category == Category::Credential && tokens[index].equals_ignoring_ascii_case("webauthn"sv)) {
|
||||||
|
// 1. Set credential type to "webauthn".
|
||||||
|
credential_type = "webauthn"_string;
|
||||||
|
|
||||||
|
// 2. If the indexth token in tokens is the first entry, then skip to the step labeled done.
|
||||||
|
if (index == 0)
|
||||||
|
return step_done();
|
||||||
|
|
||||||
|
// 3. Decrement index by one.
|
||||||
|
--index;
|
||||||
|
|
||||||
|
// 4. Set the category, maximum tokens pair to the result of determining a field's category given the indexth token in tokens.
|
||||||
|
auto category_and_maximum_tokens = determine_a_field_category(tokens[index]);
|
||||||
|
category = category_and_maximum_tokens.category;
|
||||||
|
maximum_tokens = category_and_maximum_tokens.maximum_tokens;
|
||||||
|
|
||||||
|
// 5. If category is not Normal and category is not Contact, then jump to the step labeled default.
|
||||||
|
if (category != Category::Normal && category != Category::Contact)
|
||||||
|
return step_default();
|
||||||
|
|
||||||
|
// 6. If index is greater than maximum tokens minus one (i.e. if the number of remaining tokens is greater than maximum tokens),
|
||||||
|
// then jump to the step labeled default.
|
||||||
|
if (index > maximum_tokens.value() - 1)
|
||||||
|
return step_default();
|
||||||
|
|
||||||
|
// 7. Set IDL value to the concatenation of the indexth token in tokens, a U+0020 SPACE character, and the previous value of IDL value.
|
||||||
|
idl_value = MUST(String::formatted("{} {}", tokens[index], idl_value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 17. If the indexth token in tokens is the first entry, then skip to the step labeled done.
|
||||||
|
if (index == 0)
|
||||||
|
return step_done();
|
||||||
|
|
||||||
|
// 18. Decrement index by one.
|
||||||
|
--index;
|
||||||
|
|
||||||
|
// 19. If category is Contact and the indexth token in tokens is an ASCII case-insensitive match for one of the strings in the following list,
|
||||||
|
// then run the substeps that follow:
|
||||||
|
if (category == Category::Contact && tokens[index].to_ascii_lowercase().is_one_of("home", "work", "mobile", "fax", "pager")) {
|
||||||
|
// 1. Let contact be the matching string from the list above.
|
||||||
|
auto contact = tokens[index].to_ascii_lowercase();
|
||||||
|
|
||||||
|
// 2. Insert contact at the start of scope tokens.
|
||||||
|
scope_tokens.prepend(contact);
|
||||||
|
|
||||||
|
// 3. Add contact to hint tokens.
|
||||||
|
hint_tokens.set(contact);
|
||||||
|
|
||||||
|
// 4. Let IDL value be the concatenation of contact, a U+0020 SPACE character, and the previous value of IDL value.
|
||||||
|
idl_value = MUST(String::formatted("{} {}", contact, idl_value));
|
||||||
|
|
||||||
|
// 5. If the indexth entry in tokens is the first entry, then skip to the step labeled done.
|
||||||
|
if (index == 0)
|
||||||
|
return step_done();
|
||||||
|
|
||||||
|
// 6. Decrement index by one.
|
||||||
|
--index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 20. If the indexth token in tokens is an ASCII case-insensitive match for one of the strings in the following list,
|
||||||
|
// then run the substeps that follow:
|
||||||
|
if (tokens[index].to_ascii_lowercase().is_one_of("shipping", "billing")) {
|
||||||
|
// 1. Let mode be the matching string from the list above.
|
||||||
|
auto mode = tokens[index].to_ascii_lowercase();
|
||||||
|
|
||||||
|
// 2. Insert mode at the start of scope tokens.
|
||||||
|
scope_tokens.prepend(mode);
|
||||||
|
|
||||||
|
// 3. Add mode to hint tokens.
|
||||||
|
hint_tokens.set(mode);
|
||||||
|
|
||||||
|
// 4. Let IDL value be the concatenation of mode, a U+0020 SPACE character, and the previous value of IDL value.
|
||||||
|
idl_value = MUST(String::formatted("{} {}", mode, idl_value));
|
||||||
|
|
||||||
|
// 5. If the indexth entry in tokens is the first entry, then skip to the step labeled done.
|
||||||
|
if (index == 0)
|
||||||
|
return step_done();
|
||||||
|
|
||||||
|
// 6. Decrement index by one.
|
||||||
|
--index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 21. If the indexth entry in tokens is not the first entry, then jump to the step labeled default.
|
||||||
|
if (index != 0)
|
||||||
|
return step_default();
|
||||||
|
|
||||||
|
// 22. If the first eight characters of the indexth token in tokens are not an ASCII case-insensitive match for the string "section-",
|
||||||
|
// then jump to the step labeled default.
|
||||||
|
if (!tokens[index].to_ascii_lowercase().starts_with_bytes("section-"sv))
|
||||||
|
return step_default();
|
||||||
|
|
||||||
|
// 23. Let section be the indexth token in tokens, converted to ASCII lowercase.
|
||||||
|
auto section = tokens[index].to_ascii_lowercase();
|
||||||
|
|
||||||
|
// 24. Insert section at the start of scope tokens.
|
||||||
|
scope_tokens.prepend(section);
|
||||||
|
|
||||||
|
// 25. Let IDL value be the concatenation of section, a U+0020 SPACE character, and the previous value of IDL value.
|
||||||
|
idl_value = MUST(String::formatted("{} {}", section, idl_value));
|
||||||
|
|
||||||
|
return step_done();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
54
Libraries/LibWeb/HTML/AutocompleteElement.h
Normal file
54
Libraries/LibWeb/HTML/AutocompleteElement.h
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025, Altomani Gianluca <altomanigianluca@gmail.com>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <LibWeb/HTML/FormAssociatedElement.h>
|
||||||
|
|
||||||
|
namespace Web::HTML {
|
||||||
|
|
||||||
|
#define AUTOCOMPLETE_ELEMENT(ElementBaseClass, ElementClass) \
|
||||||
|
private: \
|
||||||
|
virtual HTMLElement& autocomplete_element_to_html_element() override \
|
||||||
|
{ \
|
||||||
|
static_assert(IsBaseOf<HTMLElement, ElementClass>); \
|
||||||
|
static_assert(IsBaseOf<FormAssociatedElement, ElementClass>); \
|
||||||
|
return *this; \
|
||||||
|
}
|
||||||
|
|
||||||
|
class AutocompleteElement {
|
||||||
|
public:
|
||||||
|
enum class AutofillMantle {
|
||||||
|
Anchor,
|
||||||
|
Expectation,
|
||||||
|
};
|
||||||
|
AutofillMantle get_autofill_mantle() const;
|
||||||
|
|
||||||
|
Vector<String> autocomplete_tokens() const;
|
||||||
|
String autocomplete() const;
|
||||||
|
WebIDL::ExceptionOr<void> set_autocomplete(String const&);
|
||||||
|
|
||||||
|
// Each input element to which the autocomplete attribute applies [...] has
|
||||||
|
// an autofill hint set, an autofill scope, an autofill field name,
|
||||||
|
// a non-autofill credential type, and an IDL-exposed autofill value.
|
||||||
|
struct AttributeDetails {
|
||||||
|
Vector<String> hint_set;
|
||||||
|
Vector<String> scope;
|
||||||
|
String field_name;
|
||||||
|
Optional<String> credential_type;
|
||||||
|
String value;
|
||||||
|
};
|
||||||
|
AttributeDetails parse_autocomplete_attribute() const;
|
||||||
|
|
||||||
|
virtual HTMLElement& autocomplete_element_to_html_element() = 0;
|
||||||
|
HTMLElement const& autocomplete_element_to_html_element() const { return const_cast<AutocompleteElement&>(*this).autocomplete_element_to_html_element(); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
AutocompleteElement() = default;
|
||||||
|
virtual ~AutocompleteElement() = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -12,6 +12,7 @@
|
||||||
#include <LibWeb/DOM/DocumentLoadEventDelayer.h>
|
#include <LibWeb/DOM/DocumentLoadEventDelayer.h>
|
||||||
#include <LibWeb/DOM/Text.h>
|
#include <LibWeb/DOM/Text.h>
|
||||||
#include <LibWeb/FileAPI/FileList.h>
|
#include <LibWeb/FileAPI/FileList.h>
|
||||||
|
#include <LibWeb/HTML/AutocompleteElement.h>
|
||||||
#include <LibWeb/HTML/ColorPickerUpdateState.h>
|
#include <LibWeb/HTML/ColorPickerUpdateState.h>
|
||||||
#include <LibWeb/HTML/FileFilter.h>
|
#include <LibWeb/HTML/FileFilter.h>
|
||||||
#include <LibWeb/HTML/FormAssociatedElement.h>
|
#include <LibWeb/HTML/FormAssociatedElement.h>
|
||||||
|
@ -52,10 +53,12 @@ class HTMLInputElement final
|
||||||
: public HTMLElement
|
: public HTMLElement
|
||||||
, public FormAssociatedTextControlElement
|
, public FormAssociatedTextControlElement
|
||||||
, public Layout::ImageProvider
|
, public Layout::ImageProvider
|
||||||
, public PopoverInvokerElement {
|
, public PopoverInvokerElement
|
||||||
|
, public AutocompleteElement {
|
||||||
WEB_PLATFORM_OBJECT(HTMLInputElement, HTMLElement);
|
WEB_PLATFORM_OBJECT(HTMLInputElement, HTMLElement);
|
||||||
GC_DECLARE_ALLOCATOR(HTMLInputElement);
|
GC_DECLARE_ALLOCATOR(HTMLInputElement);
|
||||||
FORM_ASSOCIATED_ELEMENT(HTMLElement, HTMLInputElement)
|
FORM_ASSOCIATED_ELEMENT(HTMLElement, HTMLInputElement);
|
||||||
|
AUTOCOMPLETE_ELEMENT(HTMLElement, HTMLInputElement);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
virtual ~HTMLInputElement() override;
|
virtual ~HTMLInputElement() override;
|
||||||
|
|
|
@ -11,7 +11,7 @@ interface HTMLInputElement : HTMLElement {
|
||||||
|
|
||||||
[CEReactions, Reflect] attribute DOMString accept;
|
[CEReactions, Reflect] attribute DOMString accept;
|
||||||
[CEReactions, Reflect] attribute DOMString alt;
|
[CEReactions, Reflect] attribute DOMString alt;
|
||||||
[CEReactions, Enumerated=Autocomplete, Reflect] attribute DOMString autocomplete;
|
[CEReactions] attribute DOMString autocomplete;
|
||||||
[CEReactions, Reflect=checked] attribute boolean defaultChecked;
|
[CEReactions, Reflect=checked] attribute boolean defaultChecked;
|
||||||
[ImplementedAs=checked_binding] attribute boolean checked;
|
[ImplementedAs=checked_binding] attribute boolean checked;
|
||||||
[CEReactions, Reflect=dirname] attribute DOMString dirName;
|
[CEReactions, Reflect=dirname] attribute DOMString dirName;
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <LibWeb/HTML/AutocompleteElement.h>
|
||||||
#include <LibWeb/HTML/FormAssociatedElement.h>
|
#include <LibWeb/HTML/FormAssociatedElement.h>
|
||||||
#include <LibWeb/HTML/HTMLElement.h>
|
#include <LibWeb/HTML/HTMLElement.h>
|
||||||
#include <LibWeb/HTML/HTMLOptionsCollection.h>
|
#include <LibWeb/HTML/HTMLOptionsCollection.h>
|
||||||
|
@ -19,10 +20,12 @@ namespace Web::HTML {
|
||||||
|
|
||||||
class HTMLSelectElement final
|
class HTMLSelectElement final
|
||||||
: public HTMLElement
|
: public HTMLElement
|
||||||
, public FormAssociatedElement {
|
, public FormAssociatedElement
|
||||||
|
, public AutocompleteElement {
|
||||||
WEB_PLATFORM_OBJECT(HTMLSelectElement, HTMLElement);
|
WEB_PLATFORM_OBJECT(HTMLSelectElement, HTMLElement);
|
||||||
GC_DECLARE_ALLOCATOR(HTMLSelectElement);
|
GC_DECLARE_ALLOCATOR(HTMLSelectElement);
|
||||||
FORM_ASSOCIATED_ELEMENT(HTMLElement, HTMLSelectElement)
|
FORM_ASSOCIATED_ELEMENT(HTMLElement, HTMLSelectElement);
|
||||||
|
AUTOCOMPLETE_ELEMENT(HTMLElement, HTMLSelectElement);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
virtual ~HTMLSelectElement() override;
|
virtual ~HTMLSelectElement() override;
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
interface HTMLSelectElement : HTMLElement {
|
interface HTMLSelectElement : HTMLElement {
|
||||||
[HTMLConstructor] constructor();
|
[HTMLConstructor] constructor();
|
||||||
|
|
||||||
[CEReactions, Enumerated=Autocomplete, Reflect] attribute DOMString autocomplete;
|
[CEReactions] attribute DOMString autocomplete;
|
||||||
[CEReactions, Reflect] attribute boolean disabled;
|
[CEReactions, Reflect] attribute boolean disabled;
|
||||||
readonly attribute HTMLFormElement? form;
|
readonly attribute HTMLFormElement? form;
|
||||||
[CEReactions, Reflect] attribute boolean multiple;
|
[CEReactions, Reflect] attribute boolean multiple;
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
#include <LibCore/Timer.h>
|
#include <LibCore/Timer.h>
|
||||||
#include <LibWeb/ARIA/Roles.h>
|
#include <LibWeb/ARIA/Roles.h>
|
||||||
#include <LibWeb/DOM/Text.h>
|
#include <LibWeb/DOM/Text.h>
|
||||||
|
#include <LibWeb/HTML/AutocompleteElement.h>
|
||||||
#include <LibWeb/HTML/FormAssociatedElement.h>
|
#include <LibWeb/HTML/FormAssociatedElement.h>
|
||||||
#include <LibWeb/HTML/HTMLElement.h>
|
#include <LibWeb/HTML/HTMLElement.h>
|
||||||
#include <LibWeb/WebIDL/Types.h>
|
#include <LibWeb/WebIDL/Types.h>
|
||||||
|
@ -20,10 +21,12 @@ namespace Web::HTML {
|
||||||
|
|
||||||
class HTMLTextAreaElement final
|
class HTMLTextAreaElement final
|
||||||
: public HTMLElement
|
: public HTMLElement
|
||||||
, public FormAssociatedTextControlElement {
|
, public FormAssociatedTextControlElement
|
||||||
|
, public AutocompleteElement {
|
||||||
WEB_PLATFORM_OBJECT(HTMLTextAreaElement, HTMLElement);
|
WEB_PLATFORM_OBJECT(HTMLTextAreaElement, HTMLElement);
|
||||||
GC_DECLARE_ALLOCATOR(HTMLTextAreaElement);
|
GC_DECLARE_ALLOCATOR(HTMLTextAreaElement);
|
||||||
FORM_ASSOCIATED_ELEMENT(HTMLElement, HTMLTextAreaElement)
|
FORM_ASSOCIATED_ELEMENT(HTMLElement, HTMLTextAreaElement);
|
||||||
|
AUTOCOMPLETE_ELEMENT(HTMLElement, HTMLTextAreaElement);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
virtual ~HTMLTextAreaElement() override;
|
virtual ~HTMLTextAreaElement() override;
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
interface HTMLTextAreaElement : HTMLElement {
|
interface HTMLTextAreaElement : HTMLElement {
|
||||||
[HTMLConstructor] constructor();
|
[HTMLConstructor] constructor();
|
||||||
|
|
||||||
[CEReactions, Enumerated=Autocomplete, Reflect] attribute DOMString autocomplete;
|
[CEReactions] attribute DOMString autocomplete;
|
||||||
[CEReactions] attribute unsigned long cols;
|
[CEReactions] attribute unsigned long cols;
|
||||||
[CEReactions, Reflect=dirname] attribute DOMString dirName;
|
[CEReactions, Reflect=dirname] attribute DOMString dirName;
|
||||||
[CEReactions, Reflect] attribute boolean disabled;
|
[CEReactions, Reflect] attribute boolean disabled;
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
Harness status: OK
|
||||||
|
|
||||||
|
Found 67 tests
|
||||||
|
|
||||||
|
67 Pass
|
||||||
|
Pass form autocomplete attribute missing
|
||||||
|
Pass form autocomplete attribute on
|
||||||
|
Pass form autocomplete attribute off
|
||||||
|
Pass form autocomplete attribute invalid
|
||||||
|
Pass on is an allowed autocomplete field name
|
||||||
|
Pass off is an allowed autocomplete field name
|
||||||
|
Pass name is an allowed autocomplete field name
|
||||||
|
Pass honorific-prefix is an allowed autocomplete field name
|
||||||
|
Pass given-name is an allowed autocomplete field name
|
||||||
|
Pass additional-name is an allowed autocomplete field name
|
||||||
|
Pass family-name is an allowed autocomplete field name
|
||||||
|
Pass honorific-suffix is an allowed autocomplete field name
|
||||||
|
Pass nickname is an allowed autocomplete field name
|
||||||
|
Pass username is an allowed autocomplete field name
|
||||||
|
Pass new-password is an allowed autocomplete field name
|
||||||
|
Pass current-password is an allowed autocomplete field name
|
||||||
|
Pass one-time-code is an allowed autocomplete field name
|
||||||
|
Pass organization-title is an allowed autocomplete field name
|
||||||
|
Pass organization is an allowed autocomplete field name
|
||||||
|
Pass street-address is an allowed autocomplete field name
|
||||||
|
Pass address-line1 is an allowed autocomplete field name
|
||||||
|
Pass address-line2 is an allowed autocomplete field name
|
||||||
|
Pass address-line3 is an allowed autocomplete field name
|
||||||
|
Pass address-level4 is an allowed autocomplete field name
|
||||||
|
Pass address-level3 is an allowed autocomplete field name
|
||||||
|
Pass address-level2 is an allowed autocomplete field name
|
||||||
|
Pass address-level1 is an allowed autocomplete field name
|
||||||
|
Pass country is an allowed autocomplete field name
|
||||||
|
Pass country-name is an allowed autocomplete field name
|
||||||
|
Pass postal-code is an allowed autocomplete field name
|
||||||
|
Pass cc-name is an allowed autocomplete field name
|
||||||
|
Pass cc-given-name is an allowed autocomplete field name
|
||||||
|
Pass cc-additional-name is an allowed autocomplete field name
|
||||||
|
Pass cc-family-name is an allowed autocomplete field name
|
||||||
|
Pass cc-number is an allowed autocomplete field name
|
||||||
|
Pass cc-exp is an allowed autocomplete field name
|
||||||
|
Pass cc-exp-month is an allowed autocomplete field name
|
||||||
|
Pass cc-exp-year is an allowed autocomplete field name
|
||||||
|
Pass cc-csc is an allowed autocomplete field name
|
||||||
|
Pass cc-type is an allowed autocomplete field name
|
||||||
|
Pass transaction-currency is an allowed autocomplete field name
|
||||||
|
Pass transaction-amount is an allowed autocomplete field name
|
||||||
|
Pass language is an allowed autocomplete field name
|
||||||
|
Pass bday is an allowed autocomplete field name
|
||||||
|
Pass bday-day is an allowed autocomplete field name
|
||||||
|
Pass bday-month is an allowed autocomplete field name
|
||||||
|
Pass bday-year is an allowed autocomplete field name
|
||||||
|
Pass sex is an allowed autocomplete field name
|
||||||
|
Pass url is an allowed autocomplete field name
|
||||||
|
Pass photo is an allowed autocomplete field name
|
||||||
|
Pass tel is an allowed autocomplete field name
|
||||||
|
Pass tel-country-code is an allowed autocomplete field name
|
||||||
|
Pass tel-national is an allowed autocomplete field name
|
||||||
|
Pass tel-area-code is an allowed autocomplete field name
|
||||||
|
Pass tel-local is an allowed autocomplete field name
|
||||||
|
Pass tel-local-prefix is an allowed autocomplete field name
|
||||||
|
Pass tel-local-suffix is an allowed autocomplete field name
|
||||||
|
Pass tel-extension is an allowed autocomplete field name
|
||||||
|
Pass email is an allowed autocomplete field name
|
||||||
|
Pass impp is an allowed autocomplete field name
|
||||||
|
Pass webauthn is an allowed autocomplete field name
|
||||||
|
Pass Test whitespace-only attribute value
|
||||||
|
Pass Test maximum number of tokens
|
||||||
|
Pass Unknown field
|
||||||
|
Pass Test 'wearing the autofill anchor mantle' with off/on
|
||||||
|
Pass Serialize combinations of section, mode, contact, and field
|
||||||
|
Pass Serialize combinations of section, mode, contact, field, and credential
|
|
@ -0,0 +1,130 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<meta charset=utf-8>
|
||||||
|
<title>form autocomplete attribute</title>
|
||||||
|
<link rel="author" title="Denis Ah-Kang" href="mailto:denis@w3.org">
|
||||||
|
<link rel=help href="https://html.spec.whatwg.org/multipage/#the-form-element">
|
||||||
|
<link rel=help href="https://html.spec.whatwg.org/multipage/#attr-fe-autocomplete">
|
||||||
|
<script src="../../../../resources/testharness.js"></script>
|
||||||
|
<script src="../../../../resources/testharnessreport.js"></script>
|
||||||
|
<div id="log"></div>
|
||||||
|
<form name="missing_attribute">
|
||||||
|
<input>
|
||||||
|
<input autocomplete="on">
|
||||||
|
<input autocomplete="off">
|
||||||
|
<input autocomplete="foobar">
|
||||||
|
</form>
|
||||||
|
<form name="autocomplete_on" autocomplete="on">
|
||||||
|
<input>
|
||||||
|
<input autocomplete="on">
|
||||||
|
<input autocomplete="off">
|
||||||
|
<input autocomplete="foobar">
|
||||||
|
</form>
|
||||||
|
<form name="autocomplete_off" autocomplete="off">
|
||||||
|
<input>
|
||||||
|
<input autocomplete="on">
|
||||||
|
<input autocomplete="off">
|
||||||
|
<input autocomplete="foobar">
|
||||||
|
</form>
|
||||||
|
<form name="autocomplete_invalid" autocomplete="foobar">
|
||||||
|
<input>
|
||||||
|
<input autocomplete="on">
|
||||||
|
<input autocomplete="off">
|
||||||
|
<input autocomplete="foobar">
|
||||||
|
</form>
|
||||||
|
<script>
|
||||||
|
function autocompletetest(form, expectedValues, desc) {
|
||||||
|
test(function(){
|
||||||
|
assert_equals(form.autocomplete, expectedValues[0]);
|
||||||
|
assert_equals(form.elements[0].autocomplete, expectedValues[1]);
|
||||||
|
assert_equals(form.elements[1].autocomplete, expectedValues[2]);
|
||||||
|
assert_equals(form.elements[2].autocomplete, expectedValues[3]);
|
||||||
|
assert_equals(form.elements[3].autocomplete, expectedValues[4]);
|
||||||
|
}, desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
autocompletetest(document.forms.missing_attribute, ["on", "", "on", "off", ""], "form autocomplete attribute missing");
|
||||||
|
autocompletetest(document.forms.autocomplete_on, ["on", "", "on", "off", ""], "form autocomplete attribute on");
|
||||||
|
autocompletetest(document.forms.autocomplete_off, ["off", "", "on", "off", ""], "form autocomplete attribute off");
|
||||||
|
autocompletetest(document.forms.autocomplete_invalid, ["on", "", "on", "off", ""], "form autocomplete attribute invalid");
|
||||||
|
|
||||||
|
var keywords = [ "on", "off", "name", "honorific-prefix", "given-name", "additional-name", "family-name", "honorific-suffix", "nickname", "username", "new-password", "current-password", "one-time-code", "organization-title", "organization", "street-address", "address-line1", "address-line2", "address-line3", "address-level4", "address-level3", "address-level2", "address-level1", "country", "country-name", "postal-code", "cc-name", "cc-given-name", "cc-additional-name", "cc-family-name", "cc-number", "cc-exp", "cc-exp-month", "cc-exp-year", "cc-csc", "cc-type", "transaction-currency", "transaction-amount", "language", "bday", "bday-day", "bday-month", "bday-year", "sex", "url", "photo", "tel", "tel-country-code", "tel-national", "tel-area-code", "tel-local", "tel-local-prefix", "tel-local-suffix", "tel-extension", "email", "impp", "webauthn" ];
|
||||||
|
|
||||||
|
keywords.forEach(function(keyword) {
|
||||||
|
test(function(){
|
||||||
|
var input = document.createElement("input");
|
||||||
|
// Include whitespace to test splitting tokens on whitespace.
|
||||||
|
// Convert to uppercase to ensure that the tokens are normalized to lowercase.
|
||||||
|
input.setAttribute("autocomplete", " " + keyword.toUpperCase() + "\t");
|
||||||
|
assert_equals(input.autocomplete, keyword);
|
||||||
|
}, keyword + " is an allowed autocomplete field name");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test(() => {
|
||||||
|
const select = document.createElement("select");
|
||||||
|
select.setAttribute("autocomplete", " \n");
|
||||||
|
assert_equals(select.autocomplete, "");
|
||||||
|
}, "Test whitespace-only attribute value");
|
||||||
|
|
||||||
|
test(() => {
|
||||||
|
const select = document.createElement("select");
|
||||||
|
|
||||||
|
select.setAttribute("autocomplete", "foo off");
|
||||||
|
assert_equals(select.autocomplete, "");
|
||||||
|
|
||||||
|
// Normal category; max=3
|
||||||
|
select.setAttribute("autocomplete", "foo section-foo billing name");
|
||||||
|
assert_equals(select.autocomplete, "");
|
||||||
|
|
||||||
|
// Contact category; max=4
|
||||||
|
select.setAttribute("autocomplete", "foo section-bar billing work tel");
|
||||||
|
assert_equals(select.autocomplete, "");
|
||||||
|
|
||||||
|
// Credential category; max=5
|
||||||
|
select.setAttribute("autocomplete", "foo section-bar billing work tel webauthn");
|
||||||
|
assert_equals(select.autocomplete, "");
|
||||||
|
}, "Test maximum number of tokens");
|
||||||
|
|
||||||
|
test(() => {
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
|
||||||
|
textarea.setAttribute("autocomplete", "call-sign");
|
||||||
|
assert_equals(textarea.autocomplete, "");
|
||||||
|
}, "Unknown field");
|
||||||
|
|
||||||
|
test(() => {
|
||||||
|
const hidden = document.createElement("input");
|
||||||
|
hidden.type = "hidden";
|
||||||
|
hidden.setAttribute("autocomplete", "on");
|
||||||
|
assert_equals(hidden.autocomplete, "");
|
||||||
|
hidden.setAttribute("autocomplete", "off");
|
||||||
|
assert_equals(hidden.autocomplete, "");
|
||||||
|
}, "Test 'wearing the autofill anchor mantle' with off/on");
|
||||||
|
|
||||||
|
test(() => {
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
|
||||||
|
textarea.setAttribute("autocomplete", " HOME\ntel");
|
||||||
|
assert_equals(textarea.autocomplete, "home tel");
|
||||||
|
|
||||||
|
textarea.setAttribute("autocomplete", "shipping country");
|
||||||
|
assert_equals(textarea.autocomplete, "shipping country");
|
||||||
|
|
||||||
|
textarea.setAttribute("autocomplete", "billing work email");
|
||||||
|
assert_equals(textarea.autocomplete, "billing work email");
|
||||||
|
|
||||||
|
textarea.setAttribute("autocomplete", " section-FOO bday");
|
||||||
|
assert_equals(textarea.autocomplete, "section-foo bday");
|
||||||
|
}, "Serialize combinations of section, mode, contact, and field");
|
||||||
|
|
||||||
|
test(() => {
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
|
||||||
|
textarea.setAttribute("autocomplete", "\tusername webauthn");
|
||||||
|
assert_equals(textarea.autocomplete, "username webauthn");
|
||||||
|
|
||||||
|
textarea.setAttribute("autocomplete", " section-LOGIN shipping work tel webauthn ");
|
||||||
|
assert_equals(textarea.autocomplete, "section-login shipping work tel webauthn");
|
||||||
|
}, "Serialize combinations of section, mode, contact, field, and credential");
|
||||||
|
|
||||||
|
</script>
|
Loading…
Add table
Add a link
Reference in a new issue