Преглед на файлове

LibWeb: Add input element valueAsDate property

Bastiaan van der Plaat преди 1 година
родител
ревизия
cf69fd0a09

+ 7 - 1
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::Promise&>(@js_name@@js_suffix@.as_object()));
 )~~~");
     } else if (parameter.type->name() == "object") {
-        if (optional) {
+        if (parameter.type->is_nullable()) {
+            scoped_generator.append(R"~~~(
+    Optional<JS::Handle<JS::Object>> @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<JS::Handle<JS::Object>> @cpp_name@;
     if (!@js_name@@js_suffix@.is_undefined())

+ 14 - 0
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. ""

+ 126 - 0
Tests/LibWeb/Text/input/input-date.html

@@ -0,0 +1,126 @@
+<script src="./include.js"></script>
+<script>
+    test(() => {
+        let testCounter = 1;
+        function testPart(part) {
+            try {
+                println(`${testCounter}. ${JSON.stringify(part())}`);
+            } catch (e) {
+                println(`${testCounter}. Exception: ${e.name}`);
+            }
+            testCounter++;
+        }
+
+        // 1. Input date get value as date
+        testPart(() => {
+            const input = document.createElement('input');
+            input.type = 'date';
+            input.value = '2023-12-11';
+            return input.valueAsDate;
+        });
+
+        // 2. Input date invalid get value as date
+        testPart(() => {
+            const input = document.createElement('input');
+            input.type = 'date';
+            input.value = 'invalid';
+            return input.valueAsDate;
+        });
+
+        // 3. Input time get value as date
+        testPart(() => {
+            const input = document.createElement('input');
+            input.type = 'time';
+            input.value = '19:46';
+            return input.valueAsDate;
+        });
+
+        // 4. Input time get value as date
+        testPart(() => {
+            const input = document.createElement('input');
+            input.type = 'time';
+            input.value = '19:46:19';
+            return input.valueAsDate;
+        });
+
+        // 5. Input time invalid get value as date
+        testPart(() => {
+            const input = document.createElement('input');
+            input.type = 'time';
+            input.value = 'invalid';
+            return input.valueAsDate;
+        });
+
+        // 6. Input date set value as date
+        testPart(() => {
+            const input = document.createElement('input');
+            input.type = 'date';
+            input.valueAsDate = new Date(0);
+            return input.value;
+        });
+
+        // 7. Input date set value as date
+        testPart(() => {
+            const input = document.createElement('input');
+            input.type = 'date';
+            input.valueAsDate = new Date(1702320457860);
+            return input.value;
+        });
+
+        // 8. Input date invalid set value as date
+        testPart(() => {
+            const input = document.createElement('input');
+            input.type = 'date';
+            input.valueAsDate = {};
+            return input.value;
+        });
+
+        // 9. Input date null set value as date
+        testPart(() => {
+            const input = document.createElement('input');
+            input.type = 'date';
+            input.valueAsDate = null;
+            return input.value;
+        });
+
+        // 10. Input time set value as date
+        testPart(() => {
+            const input = document.createElement('input');
+            input.type = 'time';
+            input.valueAsDate = new Date(1702320457000);
+            return input.value;
+        });
+
+        // 11. Input time set value as date
+        testPart(() => {
+            const input = document.createElement('input');
+            input.type = 'time';
+            input.valueAsDate = new Date(1702320457100);
+            return input.value;
+        });
+
+        // 12. Input time set value as date
+        testPart(() => {
+            const input = document.createElement('input');
+            input.type = 'time';
+            input.valueAsDate = new Date(1702320457864.5);
+            return input.value;
+        });
+
+        // 13. Input time invalid set value as date
+        testPart(() => {
+            const input = document.createElement('input');
+            input.type = 'time';
+            input.valueAsDate = {};
+            return input.value;
+        });
+
+        // 14. Input time null set value as date
+        testPart(() => {
+            const input = document.createElement('input');
+            input.type = 'time';
+            input.valueAsDate = null;
+            return input.value;
+        });
+    });
+</script>

+ 2 - 3
Userland/Libraries/LibWeb/Animations/KeyframeEffect.cpp

@@ -20,7 +20,7 @@ JS::NonnullGCPtr<KeyframeEffect> KeyframeEffect::create(JS::Realm& realm)
 WebIDL::ExceptionOr<JS::NonnullGCPtr<KeyframeEffect>> KeyframeEffect::construct_impl(
     JS::Realm& realm,
     JS::Handle<DOM::Element> const& target,
-    JS::Handle<JS::Object> const& keyframes,
+    Optional<JS::Handle<JS::Object>> const& keyframes,
     Variant<double, KeyframeEffectOptions> options)
 {
     auto& vm = realm.vm();
@@ -163,10 +163,9 @@ WebIDL::ExceptionOr<Vector<JS::Object*>> KeyframeEffect::get_keyframes() const
 }
 
 // https://www.w3.org/TR/web-animations-1/#dom-keyframeeffect-setkeyframes
-WebIDL::ExceptionOr<void> KeyframeEffect::set_keyframes(JS::Object* keyframe_object)
+WebIDL::ExceptionOr<void> KeyframeEffect::set_keyframes(Optional<JS::Handle<JS::Object>> const&)
 {
     // FIXME: Implement this
-    (void)keyframe_object;
     return {};
 }
 

+ 2 - 2
Userland/Libraries/LibWeb/Animations/KeyframeEffect.h

@@ -47,7 +47,7 @@ public:
     static WebIDL::ExceptionOr<JS::NonnullGCPtr<KeyframeEffect>> construct_impl(
         JS::Realm&,
         JS::Handle<DOM::Element> const& target,
-        JS::Handle<JS::Object> const& keyframes,
+        Optional<JS::Handle<JS::Object>> const& keyframes,
         Variant<double, KeyframeEffectOptions> options = KeyframeEffectOptions {});
 
     static WebIDL::ExceptionOr<JS::NonnullGCPtr<KeyframeEffect>> construct_impl(JS::Realm&, JS::NonnullGCPtr<KeyframeEffect> source);
@@ -62,7 +62,7 @@ public:
     void set_composite(Bindings::CompositeOperation value) { m_composite = value; }
 
     WebIDL::ExceptionOr<Vector<JS::Object*>> get_keyframes() const;
-    WebIDL::ExceptionOr<void> set_keyframes(JS::Object*);
+    WebIDL::ExceptionOr<void> set_keyframes(Optional<JS::Handle<JS::Object>> const&);
 
 private:
     KeyframeEffect(JS::Realm&);

+ 35 - 0
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<JS::NonnullGCPtr<JS::Date>> 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<JS::NonnullGCPtr<JS::Date>> 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 };
+}
+
 }

+ 4 - 0
Userland/Libraries/LibWeb/HTML/Dates.h

@@ -8,6 +8,8 @@
 
 #include <AK/Forward.h>
 #include <AK/String.h>
+#include <LibJS/Runtime/Date.h>
+#include <LibWeb/WebIDL/ExceptionOr.h>
 
 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<JS::NonnullGCPtr<JS::Date>> 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<JS::NonnullGCPtr<JS::Date>> parse_time_string(JS::Realm& realm, StringView value);
 
 }

+ 115 - 1
Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp

@@ -8,6 +8,7 @@
  * SPDX-License-Identifier: BSD-2-Clause
  */
 
+#include <LibJS/Runtime/Date.h>
 #include <LibJS/Runtime/NativeFunction.h>
 #include <LibWeb/CSS/StyleValues/DisplayStyleValue.h>
 #include <LibWeb/CSS/StyleValues/IdentifierStyleValue.h>
@@ -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<JS::GCPtr<JS::Date>> 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<JS::Date> 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<double> 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<void> HTMLInputElement::set_value_as_date(Optional<JS::Handle<JS::Object>> 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<JS::Date>(**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<JS::Date&>(**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<double> 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
 {

+ 8 - 1
Userland/Libraries/LibWeb/HTML/HTMLInputElement.h

@@ -102,7 +102,10 @@ public:
     unsigned size() const;
     WebIDL::ExceptionOr<void> set_size(unsigned value);
 
-    WebIDL::ExceptionOr<double> value_as_number() const;
+    JS::Object* value_as_date() const;
+    WebIDL::ExceptionOr<void> set_value_as_date(Optional<JS::Handle<JS::Object>> const&);
+
+    double value_as_number() const;
     WebIDL::ExceptionOr<void> set_value_as_number(double value);
 
     WebIDL::ExceptionOr<void> 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<double> convert_string_to_number(StringView input) const;
     String covert_number_to_string(double input) const;
 
+    WebIDL::ExceptionOr<JS::GCPtr<JS::Date>> convert_string_to_date(StringView input) const;
+    String covert_date_to_string(JS::NonnullGCPtr<JS::Date> input) const;
+
     Optional<double> min() const;
     Optional<double> max() const;
     double default_step() const;

+ 1 - 1
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;