From 48983a43b19416ce3fe98f36975673868d83faef Mon Sep 17 00:00:00 2001 From: Bastiaan van der Plaat Date: Fri, 19 Jan 2024 18:06:02 +0100 Subject: [PATCH] LibWeb: Add basic input range user interactivity --- .../LibWeb/HTML/HTMLInputElement.cpp | 130 ++++++++++++++---- 1 file changed, 105 insertions(+), 25 deletions(-) diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp index 1721e88da04..1cf42b0bc63 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include @@ -408,8 +409,6 @@ WebIDL::ExceptionOr HTMLInputElement::set_value(String const& value) // 2. Set the element's value to the new value. // NOTE: For the TextNode this is done as part of step 4 below. - if (type_state() == TypeAttributeState::Color && m_color_well_element) - MUST(m_color_well_element->style_for_bindings()->set_property(CSS::PropertyID::BackgroundColor, m_value)); // 3. Set the element's dirty value flag to true. m_dirty_value = true; @@ -422,11 +421,11 @@ WebIDL::ExceptionOr HTMLInputElement::set_value(String const& value) // text control, unselecting any selected text and resetting the selection direction to "none". if (m_value != old_value) { if (m_text_node) { - m_text_node->set_data(m_value); - update_placeholder_visibility(); + m_text_node->set_data(m_value); + update_placeholder_visibility(); - if (auto* browsing_context = document().browsing_context()) - browsing_context->set_cursor_position(DOM::Position::create(realm, *m_text_node, m_text_node->data().bytes().size())); + if (auto* browsing_context = document().browsing_context()) + browsing_context->set_cursor_position(DOM::Position::create(realm, *m_text_node, m_text_node->data().bytes().size())); } if (type_state() == TypeAttributeState::Color && m_color_well_element) @@ -645,12 +644,12 @@ void HTMLInputElement::create_text_input_shadow_tree() auto up_callback_function = JS::NativeFunction::create( realm(), [this](JS::VM&) { - (void)step_up(); + MUST(step_up()); return JS::js_undefined(); }, 0, "", &realm()); auto up_callback = realm().heap().allocate_without_realm(*up_callback_function, Bindings::host_defined_environment_settings_object(realm())); - up_button->add_event_listener_without_options("click"_fly_string, DOM::IDLEventListener::create(realm(), up_callback)); + up_button->add_event_listener_without_options(UIEvents::EventNames::click, DOM::IDLEventListener::create(realm(), up_callback)); // Down button auto down_button = MUST(DOM::create_element(document(), HTML::TagNames::button, Namespace::HTML)); @@ -663,12 +662,12 @@ void HTMLInputElement::create_text_input_shadow_tree() auto down_callback_function = JS::NativeFunction::create( realm(), [this](JS::VM&) { - (void)step_down(); + MUST(step_down()); return JS::js_undefined(); }, 0, "", &realm()); auto down_callback = realm().heap().allocate_without_realm(*down_callback_function, Bindings::host_defined_environment_settings_object(realm())); - down_button->add_event_listener_without_options("click"_fly_string, DOM::IDLEventListener::create(realm(), down_callback)); + down_button->add_event_listener_without_options(UIEvents::EventNames::click, DOM::IDLEventListener::create(realm(), down_callback)); } } @@ -719,21 +718,102 @@ void HTMLInputElement::create_range_input_shadow_tree() m_slider_thumb->set_use_pseudo_element(CSS::Selector::PseudoElement::Type::SliderThumb); MUST(slider_runnable_track->append_child(*m_slider_thumb)); update_slider_thumb_element(); + + //  User event listeners + auto dispatch_input_event = [this]() { + queue_an_element_task(HTML::Task::Source::UserInteraction, [&] { + auto input_event = DOM::Event::create(realm(), HTML::EventNames::input); + input_event->set_bubbles(true); + input_event->set_composed(true); + dispatch_event(*input_event); + }); + }; + + auto keydown_callback_function = JS::NativeFunction::create( + realm(), [this, dispatch_input_event](JS::VM& vm) { + auto key = MUST(vm.argument(0).get(vm, "key")).as_string().utf8_string(); + + if (key == "ArrowLeft" || key == "ArrowDown") + MUST(step_down()); + if (key == "PageDown") + MUST(step_down(10)); + + if (key == "ArrowRight" || key == "ArrowUp") + MUST(step_up()); + if (key == "PageUp") + MUST(step_up(10)); + + dispatch_input_event(); + return JS::js_undefined(); + }, + 0, "", &realm()); + auto keydown_callback = realm().heap().allocate_without_realm(*keydown_callback_function, Bindings::host_defined_environment_settings_object(realm())); + add_event_listener_without_options(UIEvents::EventNames::keydown, DOM::IDLEventListener::create(realm(), keydown_callback)); + + auto wheel_callback_function = JS::NativeFunction::create( + realm(), [this, dispatch_input_event](JS::VM& vm) { + auto deltaY = MUST(vm.argument(0).get(vm, "deltaY")).as_i32(); + if (deltaY > 0) { + MUST(step_down()); + } else { + MUST(step_up()); + } + dispatch_input_event(); + return JS::js_undefined(); + }, + 0, "", &realm()); + auto wheel_callback = realm().heap().allocate_without_realm(*wheel_callback_function, Bindings::host_defined_environment_settings_object(realm())); + add_event_listener_without_options(UIEvents::EventNames::wheel, DOM::IDLEventListener::create(realm(), wheel_callback)); + + auto update_slider_by_mouse = [this, dispatch_input_event](JS::VM& vm) { + auto client_x = MUST(vm.argument(0).get(vm, "clientX")).as_double(); + auto rect = get_bounding_client_rect(); + double minimum = *min(); + double maximum = *max(); + // FIXME: Snap new value to input steps + MUST(set_value_as_number(clamp(round(((client_x - rect->left()) / rect->width()) * (maximum - minimum) + minimum), minimum, maximum))); + dispatch_input_event(); + }; + + auto mousedown_callback_function = JS::NativeFunction::create( + realm(), [this, update_slider_by_mouse](JS::VM& vm) { + update_slider_by_mouse(vm); + + auto mousemove_callback_function = JS::NativeFunction::create( + realm(), [update_slider_by_mouse](JS::VM& vm) { + update_slider_by_mouse(vm); + return JS::js_undefined(); + }, + 0, "", &realm()); + auto mousemove_callback = realm().heap().allocate_without_realm(*mousemove_callback_function, Bindings::host_defined_environment_settings_object(realm())); + auto mousemove_listener = DOM::IDLEventListener::create(realm(), mousemove_callback); + auto& window = static_cast(relevant_global_object(*this)); + window.add_event_listener_without_options(UIEvents::EventNames::mousemove, mousemove_listener); + + auto mouseup_callback_function = JS::NativeFunction::create( + realm(), [this, mousemove_listener](JS::VM&) { + auto& window = static_cast(relevant_global_object(*this)); + window.remove_event_listener_without_options(UIEvents::EventNames::mousemove, mousemove_listener); + return JS::js_undefined(); + }, + 0, "", &realm()); + auto mouseup_callback = realm().heap().allocate_without_realm(*mouseup_callback_function, Bindings::host_defined_environment_settings_object(realm())); + DOM::AddEventListenerOptions mouseup_listener_options; + mouseup_listener_options.once = true; + window.add_event_listener(UIEvents::EventNames::mouseup, DOM::IDLEventListener::create(realm(), mouseup_callback), mouseup_listener_options); + + return JS::js_undefined(); + }, + 0, "", &realm()); + auto mousedown_callback = realm().heap().allocate_without_realm(*mousedown_callback_function, Bindings::host_defined_environment_settings_object(realm())); + add_event_listener_without_options(UIEvents::EventNames::mousedown, DOM::IDLEventListener::create(realm(), mousedown_callback)); } void HTMLInputElement::update_slider_thumb_element() { + double value = value_as_number(); double minimum = *min(); double maximum = *max(); - - double default_value = minimum + (maximum - minimum) / 2; - if (maximum < minimum) - default_value = minimum; - - double value = value_as_number(); - if (!isfinite(value)) - value = default_value; - double position = (value - minimum) / (maximum - minimum) * 100; MUST(m_slider_thumb->style_for_bindings()->set_property(CSS::PropertyID::MarginLeft, MUST(String::formatted("{}%", position)))); } @@ -771,19 +851,19 @@ void HTMLInputElement::attribute_changed(FlyString const& name, Optional } else if (name == HTML::AttributeNames::type) { m_type = parse_type_attribute(value.value_or(String {})); } else if (name == HTML::AttributeNames::value) { - if (!m_dirty_value) { + if (!m_dirty_value) { if (!value.has_value()) { m_value = String {}; - } else { + } else { m_value = value_sanitization_algorithm(*value); } - update_placeholder_visibility(); + update_placeholder_visibility(); - if (type_state() == TypeAttributeState::Color && m_color_well_element) + if (type_state() == TypeAttributeState::Color && m_color_well_element) update_color_well_element(); - if (type_state() == TypeAttributeState::Range && m_slider_thumb) - update_slider_thumb_element(); + if (type_state() == TypeAttributeState::Range && m_slider_thumb) + update_slider_thumb_element(); } } else if (name == HTML::AttributeNames::placeholder) { if (m_placeholder_text_node)