From cf69fd0a09e76f3863adfb68c24a2e8cd44f5d4f Mon Sep 17 00:00:00 2001 From: Bastiaan van der Plaat Date: Fri, 15 Dec 2023 18:23:29 +0100 Subject: [PATCH] LibWeb: Add input element valueAsDate property --- .../BindingsGenerator/IDLGenerators.cpp | 8 +- Tests/LibWeb/Text/expected/input-date.txt | 14 ++ Tests/LibWeb/Text/input/input-date.html | 126 ++++++++++++++++++ .../LibWeb/Animations/KeyframeEffect.cpp | 5 +- .../LibWeb/Animations/KeyframeEffect.h | 4 +- Userland/Libraries/LibWeb/HTML/Dates.cpp | 35 +++++ Userland/Libraries/LibWeb/HTML/Dates.h | 4 + .../LibWeb/HTML/HTMLInputElement.cpp | 116 +++++++++++++++- .../Libraries/LibWeb/HTML/HTMLInputElement.h | 9 +- .../LibWeb/HTML/HTMLInputElement.idl | 2 +- 10 files changed, 314 insertions(+), 9 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/input-date.txt create mode 100644 Tests/LibWeb/Text/input/input-date.html diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp index 432b4cedc9..d1001ef1c5 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp @@ -630,7 +630,13 @@ static void generate_to_cpp(SourceGenerator& generator, ParameterType& parameter auto @cpp_name@ = JS::make_handle(&static_cast(@js_name@@js_suffix@.as_object())); )~~~"); } else if (parameter.type->name() == "object") { - if (optional) { + if (parameter.type->is_nullable()) { + scoped_generator.append(R"~~~( + Optional> @cpp_name@; + if (!@js_name@@js_suffix@.is_null() && !@js_name@@js_suffix@.is_undefined()) + @cpp_name@ = JS::make_handle(TRY(@js_name@@js_suffix@.to_object(vm))); +)~~~"); + } else if (optional) { scoped_generator.append(R"~~~( Optional> @cpp_name@; if (!@js_name@@js_suffix@.is_undefined()) diff --git a/Tests/LibWeb/Text/expected/input-date.txt b/Tests/LibWeb/Text/expected/input-date.txt new file mode 100644 index 0000000000..c05176b030 --- /dev/null +++ b/Tests/LibWeb/Text/expected/input-date.txt @@ -0,0 +1,14 @@ +1. "2023-12-11T00:00:00.000Z" +2. null +3. "1970-01-01T19:46:00.000Z" +4. "1970-01-01T19:46:19.000Z" +5. null +6. "1970-01-01" +7. "2023-12-11" +8. Exception: TypeError +9. "" +10. "18:47:37" +11. "18:47:37.100" +12. "18:47:37.864" +13. Exception: TypeError +14. "" diff --git a/Tests/LibWeb/Text/input/input-date.html b/Tests/LibWeb/Text/input/input-date.html new file mode 100644 index 0000000000..97a1368177 --- /dev/null +++ b/Tests/LibWeb/Text/input/input-date.html @@ -0,0 +1,126 @@ + + diff --git a/Userland/Libraries/LibWeb/Animations/KeyframeEffect.cpp b/Userland/Libraries/LibWeb/Animations/KeyframeEffect.cpp index 5ccda1fa62..85752d514b 100644 --- a/Userland/Libraries/LibWeb/Animations/KeyframeEffect.cpp +++ b/Userland/Libraries/LibWeb/Animations/KeyframeEffect.cpp @@ -20,7 +20,7 @@ JS::NonnullGCPtr KeyframeEffect::create(JS::Realm& realm) WebIDL::ExceptionOr> KeyframeEffect::construct_impl( JS::Realm& realm, JS::Handle const& target, - JS::Handle const& keyframes, + Optional> const& keyframes, Variant options) { auto& vm = realm.vm(); @@ -163,10 +163,9 @@ WebIDL::ExceptionOr> KeyframeEffect::get_keyframes() const } // https://www.w3.org/TR/web-animations-1/#dom-keyframeeffect-setkeyframes -WebIDL::ExceptionOr KeyframeEffect::set_keyframes(JS::Object* keyframe_object) +WebIDL::ExceptionOr KeyframeEffect::set_keyframes(Optional> const&) { // FIXME: Implement this - (void)keyframe_object; return {}; } diff --git a/Userland/Libraries/LibWeb/Animations/KeyframeEffect.h b/Userland/Libraries/LibWeb/Animations/KeyframeEffect.h index a07b6eed43..a3a49c6773 100644 --- a/Userland/Libraries/LibWeb/Animations/KeyframeEffect.h +++ b/Userland/Libraries/LibWeb/Animations/KeyframeEffect.h @@ -47,7 +47,7 @@ public: static WebIDL::ExceptionOr> construct_impl( JS::Realm&, JS::Handle const& target, - JS::Handle const& keyframes, + Optional> const& keyframes, Variant options = KeyframeEffectOptions {}); static WebIDL::ExceptionOr> construct_impl(JS::Realm&, JS::NonnullGCPtr source); @@ -62,7 +62,7 @@ public: void set_composite(Bindings::CompositeOperation value) { m_composite = value; } WebIDL::ExceptionOr> get_keyframes() const; - WebIDL::ExceptionOr set_keyframes(JS::Object*); + WebIDL::ExceptionOr set_keyframes(Optional> const&); private: KeyframeEffect(JS::Realm&); diff --git a/Userland/Libraries/LibWeb/HTML/Dates.cpp b/Userland/Libraries/LibWeb/HTML/Dates.cpp index 627141a74a..25075fffb2 100644 --- a/Userland/Libraries/LibWeb/HTML/Dates.cpp +++ b/Userland/Libraries/LibWeb/HTML/Dates.cpp @@ -128,6 +128,22 @@ bool is_valid_date_string(StringView value) return day >= 1 && day <= AK::days_in_month(year, month); } +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-date-string +WebIDL::ExceptionOr> parse_date_string(JS::Realm& realm, StringView value) +{ + // FIXME: Implement spec compliant date string parsing + auto parts = value.split_view('-'); + if (parts.size() >= 3) { + if (auto year = parts.at(0).to_uint(); year.has_value()) { + if (auto month = parts.at(1).to_uint(); month.has_value()) { + if (auto day_of_month = parts.at(2).to_uint(); day_of_month.has_value()) + return JS::Date::create(realm, JS::make_date(JS::make_day(*year, *month - 1, *day_of_month), 0)); + } + } + } + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Can't parse date string"sv }; +} + // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-local-date-and-time-string bool is_valid_local_date_and_time_string(StringView value) { @@ -197,4 +213,23 @@ bool is_valid_time_string(StringView value) return true; } +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-time-string +WebIDL::ExceptionOr> parse_time_string(JS::Realm& realm, StringView value) +{ + // FIXME: Implement spec compliant time string parsing + auto parts = value.split_view(':'); + if (parts.size() >= 2) { + if (auto hours = parts.at(0).to_uint(); hours.has_value()) { + if (auto minutes = parts.at(1).to_uint(); minutes.has_value()) { + if (parts.size() >= 3) { + if (auto seconds = parts.at(2).to_uint(); seconds.has_value()) + return JS::Date::create(realm, JS::make_time(*hours, *minutes, *seconds, 0)); + } + return JS::Date::create(realm, JS::make_date(0, JS::make_time(*hours, *minutes, 0, 0))); + } + } + } + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Can't parse time string"sv }; +} + } diff --git a/Userland/Libraries/LibWeb/HTML/Dates.h b/Userland/Libraries/LibWeb/HTML/Dates.h index d0f64bd09e..fc3403ea56 100644 --- a/Userland/Libraries/LibWeb/HTML/Dates.h +++ b/Userland/Libraries/LibWeb/HTML/Dates.h @@ -8,6 +8,8 @@ #include #include +#include +#include namespace Web::HTML { @@ -15,8 +17,10 @@ u32 week_number_of_the_last_day(u64 year); bool is_valid_week_string(StringView value); bool is_valid_month_string(StringView value); bool is_valid_date_string(StringView value); +WebIDL::ExceptionOr> parse_date_string(JS::Realm& realm, StringView value); bool is_valid_local_date_and_time_string(StringView value); String normalize_local_date_and_time_string(String const& value); bool is_valid_time_string(StringView value); +WebIDL::ExceptionOr> parse_time_string(JS::Realm& realm, StringView value); } diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp index aef3e4714f..7a5b554b86 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -8,6 +8,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -1117,6 +1118,63 @@ String HTMLInputElement::covert_number_to_string(double input) const return {}; } +// https://html.spec.whatwg.org/multipage/input.html#concept-input-value-string-date +WebIDL::ExceptionOr> HTMLInputElement::convert_string_to_date(StringView input) const +{ + // https://html.spec.whatwg.org/multipage/input.html#date-state-(type=date):concept-input-value-string-date + if (type_state() == TypeAttributeState::Date) { + // If parsing a date from input results in an error, then return an error; + auto maybe_date = parse_date_string(realm(), input); + if (maybe_date.is_exception()) + return maybe_date.exception(); + + // otherwise, return a new Date object representing midnight UTC on the morning of the parsed date. + return maybe_date.value(); + } + + // https://html.spec.whatwg.org/multipage/input.html#time-state-(type=time):concept-input-value-string-date + if (type_state() == TypeAttributeState::Time) { + // If parsing a time from input results in an error, then return an error; + auto maybe_time = parse_time_string(realm(), input); + if (maybe_time.is_exception()) + return maybe_time.exception(); + + // otherwise, return a new Date object representing the parsed time in UTC on 1970-01-01. + return maybe_time.value(); + } + + dbgln("HTMLInputElement::convert_string_to_date() not implemented for input type {}", type()); + return nullptr; +} + +// https://html.spec.whatwg.org/multipage/input.html#concept-input-value-date-string +String HTMLInputElement::covert_date_to_string(JS::NonnullGCPtr input) const +{ + // https://html.spec.whatwg.org/multipage/input.html#date-state-(type=date):concept-input-value-date-string + if (type_state() == TypeAttributeState::Date) { + // Return a valid date string that represents the date current at the time represented by input in the UTC time zone. + // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-date-string + return MUST(String::formatted("{:04d}-{:02d}-{:02d}", JS::year_from_time(input->date_value()), JS::month_from_time(input->date_value()) + 1, JS::date_from_time(input->date_value()))); + } + + // https://html.spec.whatwg.org/multipage/input.html#time-state-(type=time):concept-input-value-string-date + if (type_state() == TypeAttributeState::Time) { + // Return a valid time string that represents the UTC time component that is represented by input. + // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-time-string + auto seconds = JS::sec_from_time(input->date_value()); + auto milliseconds = JS::ms_from_time(input->date_value()); + if (seconds > 0) { + if (milliseconds > 0) + return MUST(String::formatted("{:02d}:{:02d}:{:02d}.{:3d}", JS::hour_from_time(input->date_value()), JS::min_from_time(input->date_value()), seconds, milliseconds)); + return MUST(String::formatted("{:02d}:{:02d}:{:02d}", JS::hour_from_time(input->date_value()), JS::min_from_time(input->date_value()), seconds)); + } + return MUST(String::formatted("{:02d}:{:02d}", JS::hour_from_time(input->date_value()), JS::min_from_time(input->date_value()))); + } + + dbgln("HTMLInputElement::covert_date_to_string() not implemented for input type {}", type()); + return {}; +} + // https://html.spec.whatwg.org/multipage/input.html#attr-input-min Optional HTMLInputElement::min() const { @@ -1234,8 +1292,50 @@ double HTMLInputElement::step_base() const return 0; } +// https://html.spec.whatwg.org/multipage/input.html#dom-input-valueasdate +JS::Object* HTMLInputElement::value_as_date() const +{ + // On getting, if the valueAsDate attribute does not apply, as defined for the input element's type attribute's current state, then return null. + if (!value_as_date_applies()) + return nullptr; + + // Otherwise, run the algorithm to convert a string to a Date object defined for that state to the element's value; + // if the algorithm returned a Date object, then return it, otherwise, return null. + auto maybe_date = convert_string_to_date(value()); + if (!maybe_date.is_exception()) + return maybe_date.value().ptr(); + return nullptr; +} + +// https://html.spec.whatwg.org/multipage/input.html#dom-input-valueasdate +WebIDL::ExceptionOr HTMLInputElement::set_value_as_date(Optional> const& value) +{ + // On setting, if the valueAsDate attribute does not apply, as defined for the input element's type attribute's current state, then throw an "InvalidStateError" DOMException; + if (!value_as_date_applies()) + return WebIDL::InvalidStateError::create(realm(), "valueAsDate: Invalid input type used"_fly_string); + + // otherwise, if the new value is not null and not a Date object throw a TypeError exception; + if (value.has_value() && !is(**value)) + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "valueAsDate: input is not a Date"sv }; + + // otherwise if the new value is null or a Date object representing the NaN time value, then set the value of the element to the empty string; + if (!value.has_value()) { + TRY(set_value(String {})); + return {}; + } + auto& date = static_cast(**value); + if (!isfinite(date.date_value())) { + TRY(set_value(String {})); + return {}; + } + + // otherwise, run the algorithm to convert a Date object to a string, as defined for that state, on the new value, and set the value of the element to the resulting string. + TRY(set_value(covert_date_to_string(date))); + return {}; +} + // https://html.spec.whatwg.org/multipage/input.html#dom-input-valueasnumber -WebIDL::ExceptionOr HTMLInputElement::value_as_number() const +double HTMLInputElement::value_as_number() const { // On getting, if the valueAsNumber attribute does not apply, as defined for the input element's type attribute's current state, then return a Not-a-Number (NaN) value. if (!value_as_number_applies()) @@ -1537,6 +1637,20 @@ bool HTMLInputElement::change_event_applies() const } } +// https://html.spec.whatwg.org/multipage/input.html#the-input-element:dom-input-valueasdate-3 +bool HTMLInputElement::value_as_date_applies() const +{ + switch (type_state()) { + case TypeAttributeState::Date: + case TypeAttributeState::Month: + case TypeAttributeState::Week: + case TypeAttributeState::Time: + return true; + default: + return false; + } +} + // https://html.spec.whatwg.org/multipage/input.html#the-input-element:dom-input-valueasnumber-3 bool HTMLInputElement::value_as_number_applies() const { diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h index 135207d357..4eb95e6bde 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h @@ -102,7 +102,10 @@ public: unsigned size() const; WebIDL::ExceptionOr set_size(unsigned value); - WebIDL::ExceptionOr value_as_number() const; + JS::Object* value_as_date() const; + WebIDL::ExceptionOr set_value_as_date(Optional> const&); + + double value_as_number() const; WebIDL::ExceptionOr set_value_as_number(double value); WebIDL::ExceptionOr step_up(long n = 1); @@ -165,6 +168,7 @@ public: bool has_input_activation_behavior() const; bool change_event_applies() const; + bool value_as_date_applies() const; bool value_as_number_applies() const; bool step_applies() const; bool step_up_or_down_applies() const; @@ -191,6 +195,9 @@ private: Optional convert_string_to_number(StringView input) const; String covert_number_to_string(double input) const; + WebIDL::ExceptionOr> convert_string_to_date(StringView input) const; + String covert_date_to_string(JS::NonnullGCPtr input) const; + Optional min() const; Optional max() const; double default_step() const; diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.idl b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.idl index 539f48640f..27f1984733 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.idl +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.idl @@ -40,7 +40,7 @@ interface HTMLInputElement : HTMLElement { [CEReactions] attribute DOMString type; [CEReactions, Reflect=value] attribute DOMString defaultValue; [CEReactions, LegacyNullToEmptyString] attribute DOMString value; - // FIXME: attribute object? valueAsDate; + attribute object? valueAsDate; attribute unrestricted double valueAsNumber; // FIXME: [CEReactions] attribute unsigned long width;