/* * Copyright (c) 2018-2023, Andreas Kling * Copyright (c) 2022, Adam Hodgen * Copyright (c) 2022, Andrew Kaster * Copyright (c) 2023, Shannon Booth * Copyright (c) 2023, Bastiaan van der Plaat * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Web::HTML { JS_DEFINE_ALLOCATOR(HTMLInputElement); HTMLInputElement::HTMLInputElement(DOM::Document& document, DOM::QualifiedName qualified_name) : HTMLElement(document, move(qualified_name)) { } HTMLInputElement::~HTMLInputElement() = default; void HTMLInputElement::initialize(JS::Realm& realm) { Base::initialize(realm); set_prototype(&Bindings::ensure_web_prototype(realm, "HTMLInputElement"_fly_string)); } void HTMLInputElement::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); visitor.visit(m_inner_text_element); visitor.visit(m_text_node); visitor.visit(m_placeholder_element); visitor.visit(m_placeholder_text_node); visitor.visit(m_color_well_element); visitor.visit(m_legacy_pre_activation_behavior_checked_element_in_group); visitor.visit(m_selected_files); visitor.visit(m_slider_thumb); } JS::GCPtr HTMLInputElement::create_layout_node(NonnullRefPtr style) { if (type_state() == TypeAttributeState::Hidden) return nullptr; if (type_state() == TypeAttributeState::SubmitButton || type_state() == TypeAttributeState::Button || type_state() == TypeAttributeState::ResetButton || type_state() == TypeAttributeState::FileUpload) return heap().allocate_without_realm(document(), *this, move(style)); if (type_state() == TypeAttributeState::Checkbox) return heap().allocate_without_realm(document(), *this, move(style)); if (type_state() == TypeAttributeState::RadioButton) return heap().allocate_without_realm(document(), *this, move(style)); // AD-HOC: We rewrite `display: inline` to `display: inline-block`. // This is required for the internal shadow tree to work correctly in layout. if (style->display().is_inline_outside() && style->display().is_flow_inside()) style->set_property(CSS::PropertyID::Display, CSS::DisplayStyleValue::create(CSS::Display::from_short(CSS::Display::Short::InlineBlock))); return Element::create_layout_node_for_display_type(document(), style->display(), style, this); } void HTMLInputElement::set_checked(bool checked, ChangeSource change_source) { if (m_checked == checked) return; // The dirty checkedness flag must be initially set to false when the element is created, // and must be set to true whenever the user interacts with the control in a way that changes the checkedness. if (change_source == ChangeSource::User) m_dirty_checkedness = true; m_checked = checked; // This element's :checked pseudo-class could be used in a sibling's sibling-selector, // so we need to invalidate the style of all siblings. if (parent()) { parent()->for_each_child([&](auto& child) { child.invalidate_style(); }); } } void HTMLInputElement::set_checked_binding(bool checked) { if (type_state() == TypeAttributeState::RadioButton) { if (checked) set_checked_within_group(); else set_checked(false, ChangeSource::Programmatic); } else { set_checked(checked, ChangeSource::Programmatic); } } // https://html.spec.whatwg.org/multipage/input.html#dom-input-indeterminate void HTMLInputElement::set_indeterminate(bool value) { // On setting, it must be set to the new value. It has no effect except for changing the appearance of checkbox controls. m_indeterminate = value; } // https://html.spec.whatwg.org/multipage/input.html#dom-input-files JS::GCPtr HTMLInputElement::files() { // On getting, if the IDL attribute applies, it must return a FileList object that represents the current selected files. // The same object must be returned until the list of selected files changes. // If the IDL attribute does not apply, then it must instead return null. if (m_type != TypeAttributeState::FileUpload) return nullptr; if (!m_selected_files) m_selected_files = FileAPI::FileList::create(realm(), {}); return m_selected_files; } // https://html.spec.whatwg.org/multipage/input.html#dom-input-files void HTMLInputElement::set_files(JS::GCPtr files) { // 1. If the IDL attribute does not apply or the given value is null, then return. if (m_type != TypeAttributeState::FileUpload || files == nullptr) return; // 2. Replace the element's selected files with the given value. m_selected_files = files; } // https://html.spec.whatwg.org/multipage/input.html#update-the-file-selection void HTMLInputElement::update_the_file_selection(JS::NonnullGCPtr files) { // 1. Queue an element task on the user interaction task source given element and the following steps: queue_an_element_task(Task::Source::UserInteraction, [this, files] { // 1. Update element's selected files so that it represents the user's selection. this->set_files(files.ptr()); // 2. Fire an event named input at the input element, with the bubbles and composed attributes initialized to true. auto input_event = DOM::Event::create(this->realm(), EventNames::input, { .bubbles = true, .composed = true }); this->dispatch_event(input_event); // 3. Fire an event named change at the input element, with the bubbles attribute initialized to true. auto change_event = DOM::Event::create(this->realm(), EventNames::change, { .bubbles = true }); this->dispatch_event(change_event); }); } // https://html.spec.whatwg.org/multipage/input.html#show-the-picker,-if-applicable static void show_the_picker_if_applicable(HTMLInputElement& element) { // To show the picker, if applicable for an input element element: // 1. If element's relevant global object does not have transient activation, then return. auto& global_object = relevant_global_object(element); if (!is(global_object) || !static_cast(global_object).has_transient_activation()) return; // 2. If element is not mutable, then return. if (!element.is_mutable()) return; // 3. If element's type attribute is in the File Upload state, then run these steps in parallel: if (element.type_state() == HTMLInputElement::TypeAttributeState::FileUpload) { // NOTE: These steps cannot be fully implemented here, and must be done in the PageClient when the response comes back from the PageHost // 1. Optionally, wait until any prior execution of this algorithm has terminated. // 2. Display a prompt to the user requesting that the user specify some files. // If the multiple attribute is not set on element, there must be no more than one file selected; otherwise, any number may be selected. // Files can be from the filesystem or created on the fly, e.g., a picture taken from a camera connected to the user's device. // 3. Wait for the user to have made their selection. // 4. If the user dismissed the prompt without changing their selection, // then queue an element task on the user interaction task source given element to fire an event named cancel at element, // with the bubbles attribute initialized to true. // 5. Otherwise, update the file selection for element. bool const multiple = element.has_attribute(HTML::AttributeNames::multiple); auto weak_element = element.make_weak_ptr(); // FIXME: Pass along accept attribute information https://html.spec.whatwg.org/multipage/input.html#attr-input-accept // The accept attribute may be specified to provide user agents with a hint of what file types will be accepted. element.document().browsing_context()->top_level_browsing_context()->page().client().page_did_request_file_picker(weak_element, multiple); return; } // 4. Otherwise, the user agent should show any relevant user interface for selecting a value for element, // in the way it normally would when the user interacts with the control. (If no such UI applies to element, then this step does nothing.) // If such a user interface is shown, it must respect the requirements stated in the relevant parts of the specification for how element // behaves given its type attribute state. (For example, various sections describe restrictions on the resulting value string.) // This step can have side effects, such as closing other pickers that were previously shown by this algorithm. // (If this closes a file selection picker, then per the above that will lead to firing either input and change events, or a cancel event.) if (element.type_state() == HTMLInputElement::TypeAttributeState::Color) { auto weak_element = element.make_weak_ptr(); element.document().browsing_context()->top_level_browsing_context()->page().did_request_color_picker(weak_element, Color::from_string(element.value()).value_or(Color(0, 0, 0))); } } // https://html.spec.whatwg.org/multipage/input.html#dom-input-showpicker WebIDL::ExceptionOr HTMLInputElement::show_picker() { // The showPicker() method steps are: // 1. If this is not mutable, then throw an "InvalidStateError" DOMException. if (!m_is_mutable) return WebIDL::InvalidStateError::create(realm(), "Element is not mutable"_fly_string); // 2. If this's relevant settings object's origin is not same origin with this's relevant settings object's top-level origin, // and this's type attribute is not in the File Upload state or Color state, then throw a "SecurityError" DOMException. // NOTE: File and Color inputs are exempted from this check for historical reason: their input activation behavior also shows their pickers, // and has never been guarded by an origin check. if (!relevant_settings_object(*this).origin().is_same_origin(relevant_settings_object(*this).top_level_origin) && m_type != TypeAttributeState::FileUpload && m_type != TypeAttributeState::Color) { return WebIDL::SecurityError::create(realm(), "Cross origin pickers are not allowed"_fly_string); } // 3. If this's relevant global object does not have transient activation, then throw a "NotAllowedError" DOMException. // FIXME: The global object we get here should probably not need casted to Window to check for transient activation auto& global_object = relevant_global_object(*this); if (!is(global_object) || !static_cast(global_object).has_transient_activation()) { return WebIDL::NotAllowedError::create(realm(), "Too long since user activation to show picker"_fly_string); } // 4. Show the picker, if applicable, for this. show_the_picker_if_applicable(*this); return {}; } // https://html.spec.whatwg.org/multipage/input.html#input-activation-behavior WebIDL::ExceptionOr HTMLInputElement::run_input_activation_behavior() { if (type_state() == TypeAttributeState::Checkbox || type_state() == TypeAttributeState::RadioButton) { // 1. If the element is not connected, then return. if (!is_connected()) return {}; // 2. Fire an event named input at the element with the bubbles and composed attributes initialized to true. auto input_event = DOM::Event::create(realm(), HTML::EventNames::input); input_event->set_bubbles(true); input_event->set_composed(true); dispatch_event(input_event); // 3. Fire an event named change at the element with the bubbles attribute initialized to true. auto change_event = DOM::Event::create(realm(), HTML::EventNames::change); change_event->set_bubbles(true); dispatch_event(*change_event); } else if (type_state() == TypeAttributeState::SubmitButton) { JS::GCPtr form; // 1. If the element does not have a form owner, then return. if (!(form = this->form())) return {}; // 2. If the element's node document is not fully active, then return. if (!document().is_fully_active()) return {}; // 3. Submit the form owner from the element. TRY(form->submit_form(*this)); } else if (type_state() == TypeAttributeState::FileUpload || type_state() == TypeAttributeState::Color) { show_the_picker_if_applicable(*this); } return {}; } void HTMLInputElement::did_edit_text_node(Badge) { // An input element's dirty value flag must be set to true whenever the user interacts with the control in a way that changes the value. m_value = value_sanitization_algorithm(m_text_node->data()); m_dirty_value = true; m_has_uncommitted_changes = true; update_placeholder_visibility(); // NOTE: This is a bit ad-hoc, but basically implements part of "4.10.5.5 Common event behaviors" // https://html.spec.whatwg.org/multipage/input.html#common-input-element-events queue_an_element_task(HTML::Task::Source::UserInteraction, [this] { auto input_event = DOM::Event::create(realm(), HTML::EventNames::input); input_event->set_bubbles(true); input_event->set_composed(true); dispatch_event(*input_event); }); } void HTMLInputElement::did_pick_color(Optional picked_color) { // https://html.spec.whatwg.org/multipage/input.html#common-input-element-events // For input elements without a defined input activation behavior, but to which these events apply // and for which the user interface involves both interactive manipulation and an explicit commit action if (type_state() == TypeAttributeState::Color && picked_color.has_value()) { // then when the user changes the element's value m_value = value_sanitization_algorithm(picked_color.value().to_string_without_alpha()); m_dirty_value = true; if (m_color_well_element) MUST(m_color_well_element->style_for_bindings()->set_property(CSS::PropertyID::BackgroundColor, m_value)); // the user agent must queue an element task on the user interaction task source queue_an_element_task(HTML::Task::Source::UserInteraction, [this] { // given the input element to fire an event named input at the input element, with the bubbles and composed attributes initialized to true auto input_event = DOM::Event::create(realm(), HTML::EventNames::input); input_event->set_bubbles(true); input_event->set_composed(true); dispatch_event(*input_event); }); // and any time the user commits the change, the user agent must queue an element task on the user interaction task source queue_an_element_task(HTML::Task::Source::UserInteraction, [this] { // given the input element // FIXME: to set its user interacted to true // and fire an event named change at the input element, with the bubbles attribute initialized to true. auto change_event = DOM::Event::create(realm(), HTML::EventNames::change); change_event->set_bubbles(true); dispatch_event(*change_event); }); } } String HTMLInputElement::value() const { // https://html.spec.whatwg.org/multipage/input.html#dom-input-value-filename if (type_state() == TypeAttributeState::FileUpload) { // NOTE: This "fakepath" requirement is a sad accident of history. See the example in the File Upload state section for more information. // NOTE: Since path components are not permitted in filenames in the list of selected files, the "\fakepath\" cannot be mistaken for a path component. // On getting, return the string "C:\fakepath\" followed by the name of the first file in the list of selected files, if any, or the empty string if the list is empty. if (m_selected_files && m_selected_files->item(0)) return MUST(String::formatted("C:\\fakepath\\{}", m_selected_files->item(0)->name())); return String {}; } // https://html.spec.whatwg.org/multipage/input.html#dom-input-value-default-on if (type_state() == TypeAttributeState::Checkbox || type_state() == TypeAttributeState::RadioButton) { // On getting, if the element has a value content attribute, return that attribute's value; otherwise, return the string "on". return get_attribute(AttributeNames::value).value_or("on"_string); } // https://html.spec.whatwg.org/multipage/input.html#dom-input-value-default if (type_state() == TypeAttributeState::Hidden || type_state() == TypeAttributeState::SubmitButton || type_state() == TypeAttributeState::ImageButton || type_state() == TypeAttributeState::ResetButton || type_state() == TypeAttributeState::Button) { // On getting, if the element has a value content attribute, return that attribute's value; otherwise, return the empty string. return get_attribute(AttributeNames::value).value_or(String {}); } // https://html.spec.whatwg.org/multipage/input.html#dom-input-value-value // Return the current value of the element. return m_value; } WebIDL::ExceptionOr HTMLInputElement::set_value(String const& value) { auto& realm = this->realm(); // https://html.spec.whatwg.org/multipage/input.html#dom-input-value-filename if (type_state() == TypeAttributeState::FileUpload) { // On setting, if the new value is the empty string, empty the list of selected files; otherwise, throw an "InvalidStateError" DOMException. if (!value.is_empty()) return WebIDL::InvalidStateError::create(realm, "Setting value of input type file to non-empty string"_fly_string); m_selected_files = nullptr; return {}; } // https://html.spec.whatwg.org/multipage/input.html#dom-input-value-value // 1. Let oldValue be the element's value. auto old_value = move(m_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; // 4. Invoke the value sanitization algorithm, if the element's type attribute's current state defines one. m_value = value_sanitization_algorithm(value); // 5. If the element's value (after applying the value sanitization algorithm) is different from oldValue, // and the element has a text entry cursor position, move the text entry cursor position to the end of the // text control, unselecting any selected text and resetting the selection direction to "none". if (m_text_node && (m_value != old_value)) { 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())); } return {}; } void HTMLInputElement::commit_pending_changes() { // The change event fires when the value is committed, if that makes sense for the control, // or else when the control loses focus switch (type_state()) { case TypeAttributeState::Email: case TypeAttributeState::Password: case TypeAttributeState::Search: case TypeAttributeState::Telephone: case TypeAttributeState::Text: case TypeAttributeState::URL: if (!m_has_uncommitted_changes) return; break; default: break; } m_has_uncommitted_changes = false; auto change_event = DOM::Event::create(realm(), HTML::EventNames::change, { .bubbles = true }); dispatch_event(change_event); } void HTMLInputElement::update_placeholder_visibility() { if (!m_placeholder_element) return; auto placeholder_text = this->placeholder_value(); if (placeholder_text.has_value()) { MUST(m_placeholder_element->style_for_bindings()->set_property(CSS::PropertyID::Display, "block"sv)); } else { MUST(m_placeholder_element->style_for_bindings()->set_property(CSS::PropertyID::Display, "none"sv)); } } // https://html.spec.whatwg.org/multipage/input.html#the-input-element:attr-input-readonly-3 static bool is_allowed_to_be_readonly(HTML::HTMLInputElement::TypeAttributeState state) { switch (state) { case HTML::HTMLInputElement::TypeAttributeState::Text: case HTML::HTMLInputElement::TypeAttributeState::Search: case HTML::HTMLInputElement::TypeAttributeState::Telephone: case HTML::HTMLInputElement::TypeAttributeState::URL: case HTML::HTMLInputElement::TypeAttributeState::Email: case HTML::HTMLInputElement::TypeAttributeState::Password: case HTML::HTMLInputElement::TypeAttributeState::Date: case HTML::HTMLInputElement::TypeAttributeState::Month: case HTML::HTMLInputElement::TypeAttributeState::Week: case HTML::HTMLInputElement::TypeAttributeState::Time: case HTML::HTMLInputElement::TypeAttributeState::LocalDateAndTime: case HTML::HTMLInputElement::TypeAttributeState::Number: return true; default: return false; } } // https://html.spec.whatwg.org/multipage/input.html#attr-input-readonly void HTMLInputElement::handle_readonly_attribute(Optional const& maybe_value) { // The readonly attribute is a boolean attribute that controls whether or not the user can edit the form control. When specified, the element is not mutable. m_is_mutable = !maybe_value.has_value() || !is_allowed_to_be_readonly(m_type); if (m_text_node) m_text_node->set_always_editable(m_is_mutable); } // https://html.spec.whatwg.org/multipage/input.html#the-input-element:attr-input-placeholder-3 static bool is_allowed_to_have_placeholder(HTML::HTMLInputElement::TypeAttributeState state) { switch (state) { case HTML::HTMLInputElement::TypeAttributeState::Text: case HTML::HTMLInputElement::TypeAttributeState::Search: case HTML::HTMLInputElement::TypeAttributeState::URL: case HTML::HTMLInputElement::TypeAttributeState::Telephone: case HTML::HTMLInputElement::TypeAttributeState::Email: case HTML::HTMLInputElement::TypeAttributeState::Password: case HTML::HTMLInputElement::TypeAttributeState::Number: return true; default: return false; } } // https://html.spec.whatwg.org/multipage/input.html#attr-input-placeholder String HTMLInputElement::placeholder() const { auto maybe_placeholder = get_attribute(HTML::AttributeNames::placeholder); if (!maybe_placeholder.has_value()) return String {}; auto placeholder = *maybe_placeholder; // The attribute, if specified, must have a value that contains no U+000A LINE FEED (LF) or U+000D CARRIAGE RETURN (CR) characters. StringBuilder builder; for (auto c : placeholder.bytes_as_string_view()) { if (c != '\r' && c != '\n') builder.append(c); } return MUST(builder.to_string()); } // https://html.spec.whatwg.org/multipage/input.html#attr-input-placeholder Optional HTMLInputElement::placeholder_value() const { if (!m_text_node || !m_text_node->data().is_empty()) return {}; if (!is_allowed_to_have_placeholder(type_state())) return {}; if (!has_attribute(HTML::AttributeNames::placeholder)) return {}; return placeholder().to_byte_string(); } void HTMLInputElement::create_shadow_tree_if_needed() { if (shadow_root_internal()) return; switch (type_state()) { case TypeAttributeState::Hidden: case TypeAttributeState::RadioButton: case TypeAttributeState::Checkbox: case TypeAttributeState::Button: case TypeAttributeState::SubmitButton: case TypeAttributeState::ResetButton: case TypeAttributeState::ImageButton: break; case TypeAttributeState::Color: create_color_input_shadow_tree(); break; case TypeAttributeState::Range: create_range_input_shadow_tree(); break; // FIXME: This could be better factored. Everything except the above types becomes a text input. default: create_text_input_shadow_tree(); break; } } void HTMLInputElement::create_text_input_shadow_tree() { auto shadow_root = heap().allocate(realm(), document(), *this, Bindings::ShadowRootMode::Closed); set_shadow_root(shadow_root); auto initial_value = m_value; auto element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML)); MUST(element->set_attribute(HTML::AttributeNames::style, R"~~~( display: flex; height: 100%; align-items: center; white-space: pre; border: none; padding: 1px 2px; )~~~"_string)); MUST(shadow_root->append_child(element)); m_placeholder_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML)); m_placeholder_element->set_use_pseudo_element(CSS::Selector::PseudoElement::Type::Placeholder); MUST(m_placeholder_element->set_attribute(HTML::AttributeNames::style, R"~~~( flex: 1; height: 1lh; )~~~"_string)); MUST(element->append_child(*m_placeholder_element)); m_placeholder_text_node = heap().allocate(realm(), document(), String {}); m_placeholder_text_node->set_data(placeholder()); m_placeholder_text_node->set_editable_text_node_owner(Badge {}, *this); MUST(m_placeholder_element->append_child(*m_placeholder_text_node)); m_inner_text_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML)); MUST(m_inner_text_element->set_attribute(HTML::AttributeNames::style, R"~~~( flex: 1; height: 1lh; )~~~"_string)); MUST(element->append_child(*m_inner_text_element)); m_text_node = heap().allocate(realm(), document(), move(initial_value)); if (type_state() == TypeAttributeState::FileUpload) { // NOTE: file upload state is mutable, but we don't allow the text node to be modifed m_text_node->set_always_editable(false); } else { handle_readonly_attribute(attribute(HTML::AttributeNames::readonly)); } m_text_node->set_editable_text_node_owner(Badge {}, *this); if (type_state() == TypeAttributeState::Password) m_text_node->set_is_password_input({}, true); MUST(m_inner_text_element->append_child(*m_text_node)); update_placeholder_visibility(); if (type_state() == TypeAttributeState::Number) { // Up button auto up_button = MUST(DOM::create_element(document(), HTML::TagNames::button, Namespace::HTML)); // FIXME: This cursor property doesn't work MUST(up_button->set_attribute(HTML::AttributeNames::style, R"~~~( padding: 0; cursor: default; )~~~"_string)); MUST(up_button->set_inner_html(""sv)); MUST(element->append_child(up_button)); auto up_callback_function = JS::NativeFunction::create( realm(), [this](JS::VM&) { (void)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)); // Down button auto down_button = MUST(DOM::create_element(document(), HTML::TagNames::button, Namespace::HTML)); MUST(down_button->set_attribute(HTML::AttributeNames::style, R"~~~( padding: 0; cursor: default; )~~~"_string)); MUST(down_button->set_inner_html(""sv)); MUST(element->append_child(down_button)); auto down_callback_function = JS::NativeFunction::create( realm(), [this](JS::VM&) { (void)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)); } } void HTMLInputElement::create_color_input_shadow_tree() { auto shadow_root = heap().allocate(realm(), document(), *this, Bindings::ShadowRootMode::Closed); auto color = value_sanitization_algorithm(m_value); auto border = DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML).release_value_but_fixme_should_propagate_errors(); MUST(border->set_attribute(HTML::AttributeNames::style, R"~~~( width: fit-content; height: fit-content; padding: 4px; border: 1px solid ButtonBorder; background-color: ButtonFace; )~~~"_string)); m_color_well_element = DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML).release_value_but_fixme_should_propagate_errors(); MUST(m_color_well_element->set_attribute(HTML::AttributeNames::style, R"~~~( width: 24px; height: 24px; border: 1px solid ButtonBorder; box-sizing: border-box; )~~~"_string)); MUST(m_color_well_element->style_for_bindings()->set_property(CSS::PropertyID::BackgroundColor, color)); MUST(border->append_child(*m_color_well_element)); MUST(shadow_root->append_child(border)); set_shadow_root(shadow_root); } void HTMLInputElement::create_range_input_shadow_tree() { auto shadow_root = heap().allocate(realm(), document(), *this, Bindings::ShadowRootMode::Closed); set_shadow_root(shadow_root); auto slider_runnable_track = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML)); slider_runnable_track->set_use_pseudo_element(CSS::Selector::PseudoElement::Type::SliderRunnableTrack); MUST(shadow_root->append_child(slider_runnable_track)); m_slider_thumb = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML)); 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(); } void HTMLInputElement::update_slider_thumb_element() { double minimum = *min(); double maximum = *max(); double default_value = minimum + (maximum - minimum) / 2; if (maximum < minimum) default_value = minimum; double value = MUST(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)))); } void HTMLInputElement::did_receive_focus() { auto* browsing_context = document().browsing_context(); if (!browsing_context) return; if (!m_text_node) return; browsing_context->set_cursor_position(DOM::Position::create(realm(), *m_text_node, 0)); } void HTMLInputElement::did_lose_focus() { commit_pending_changes(); } void HTMLInputElement::attribute_changed(FlyString const& name, Optional const& value) { HTMLElement::attribute_changed(name, value); if (name == HTML::AttributeNames::checked) { if (!value.has_value()) { // When the checked content attribute is removed, if the control does not have dirty checkedness, // the user agent must set the checkedness of the element to false. if (!m_dirty_checkedness) set_checked(false, ChangeSource::Programmatic); } else { // When the checked content attribute is added, if the control does not have dirty checkedness, // the user agent must set the checkedness of the element to true if (!m_dirty_checkedness) set_checked(true, ChangeSource::Programmatic); } } else if (name == HTML::AttributeNames::type) { m_type = parse_type_attribute(value.value_or(String {})); } else if (name == HTML::AttributeNames::value) { if (!value.has_value()) { if (!m_dirty_value) { m_value = String {}; update_placeholder_visibility(); if (type_state() == TypeAttributeState::Color && m_color_well_element) MUST(m_color_well_element->style_for_bindings()->set_property(CSS::PropertyID::BackgroundColor, m_value)); if (type_state() == TypeAttributeState::Range && m_slider_thumb) update_slider_thumb_element(); } } else { if (!m_dirty_value) { m_value = value_sanitization_algorithm(*value); update_placeholder_visibility(); if (type_state() == TypeAttributeState::Color && m_color_well_element) MUST(m_color_well_element->style_for_bindings()->set_property(CSS::PropertyID::BackgroundColor, m_value)); if (type_state() == TypeAttributeState::Range && m_slider_thumb) update_slider_thumb_element(); } } } else if (name == HTML::AttributeNames::placeholder) { if (m_placeholder_text_node) m_placeholder_text_node->set_data(placeholder()); } else if (name == HTML::AttributeNames::readonly) { handle_readonly_attribute(value); } } HTMLInputElement::TypeAttributeState HTMLInputElement::parse_type_attribute(StringView type) { #define __ENUMERATE_HTML_INPUT_TYPE_ATTRIBUTE(keyword, state) \ if (type.equals_ignoring_ascii_case(#keyword##sv)) \ return HTMLInputElement::TypeAttributeState::state; ENUMERATE_HTML_INPUT_TYPE_ATTRIBUTES #undef __ENUMERATE_HTML_INPUT_TYPE_ATTRIBUTE // The missing value default and the invalid value default are the Text state. // https://html.spec.whatwg.org/multipage/input.html#the-input-element:missing-value-default // https://html.spec.whatwg.org/multipage/input.html#the-input-element:invalid-value-default return HTMLInputElement::TypeAttributeState::Text; } StringView HTMLInputElement::type() const { // FIXME: This should probably be `Reflect` in the IDL. switch (m_type) { #define __ENUMERATE_HTML_INPUT_TYPE_ATTRIBUTE(keyword, state) \ case TypeAttributeState::state: \ return #keyword##sv; ENUMERATE_HTML_INPUT_TYPE_ATTRIBUTES #undef __ENUMERATE_HTML_INPUT_TYPE_ATTRIBUTE } VERIFY_NOT_REACHED(); } WebIDL::ExceptionOr HTMLInputElement::set_type(String const& type) { return set_attribute(HTML::AttributeNames::type, type); } // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-simple-colour static bool is_valid_simple_color(StringView value) { // if it is exactly seven characters long, if (value.length() != 7) return false; // and the first character is a U+0023 NUMBER SIGN character (#), if (!value.starts_with('#')) return false; // and the remaining six characters are all ASCII hex digits for (size_t i = 1; i < value.length(); i++) if (!is_ascii_hex_digit(value[i])) return false; return true; } // https://html.spec.whatwg.org/multipage/input.html#value-sanitization-algorithm String HTMLInputElement::value_sanitization_algorithm(String const& value) const { if (type_state() == HTMLInputElement::TypeAttributeState::Text || type_state() == HTMLInputElement::TypeAttributeState::Search || type_state() == HTMLInputElement::TypeAttributeState::Telephone || type_state() == HTMLInputElement::TypeAttributeState::Password) { // Strip newlines from the value. if (value.bytes_as_string_view().contains('\r') || value.bytes_as_string_view().contains('\n')) { StringBuilder builder; for (auto c : value.bytes_as_string_view()) { if (c != '\r' && c != '\n') builder.append(c); } return MUST(builder.to_string()); } } else if (type_state() == HTMLInputElement::TypeAttributeState::URL) { // Strip newlines from the value, then strip leading and trailing ASCII whitespace from the value. if (value.bytes_as_string_view().contains('\r') || value.bytes_as_string_view().contains('\n')) { StringBuilder builder; for (auto c : value.bytes_as_string_view()) { if (c != '\r' && c != '\n') builder.append(c); } return MUST(String::from_utf8(builder.string_view().trim(Infra::ASCII_WHITESPACE))); } } else if (type_state() == HTMLInputElement::TypeAttributeState::Email) { // https://html.spec.whatwg.org/multipage/input.html#email-state-(type=email):value-sanitization-algorithm // FIXME: handle the `multiple` attribute // Strip newlines from the value, then strip leading and trailing ASCII whitespace from the value. if (value.bytes_as_string_view().contains('\r') || value.bytes_as_string_view().contains('\n')) { StringBuilder builder; for (auto c : value.bytes_as_string_view()) { if (c != '\r' && c != '\n') builder.append(c); } return MUST(String::from_utf8(builder.string_view().trim(Infra::ASCII_WHITESPACE))); } } else if (type_state() == HTMLInputElement::TypeAttributeState::Number) { // If the value of the element is not a valid floating-point number, then set it to the empty string instead. auto maybe_value = parse_floating_point_number(value); if (!maybe_value.has_value() || !isfinite(maybe_value.value())) return String {}; } else if (type_state() == HTMLInputElement::TypeAttributeState::Date) { // https://html.spec.whatwg.org/multipage/input.html#date-state-(type=date):value-sanitization-algorithm if (!is_valid_date_string(value)) return String {}; } else if (type_state() == HTMLInputElement::TypeAttributeState::Month) { // https://html.spec.whatwg.org/multipage/input.html#month-state-(type=month):value-sanitization-algorithm if (!is_valid_month_string(value)) return String {}; } else if (type_state() == HTMLInputElement::TypeAttributeState::Week) { // https://html.spec.whatwg.org/multipage/input.html#week-state-(type=week):value-sanitization-algorithm if (!is_valid_week_string(value)) return String {}; } else if (type_state() == HTMLInputElement::TypeAttributeState::Time) { // https://html.spec.whatwg.org/multipage/input.html#time-state-(type=time):value-sanitization-algorithm if (!is_valid_time_string(value)) return String {}; } else if (type_state() == HTMLInputElement::TypeAttributeState::LocalDateAndTime) { // https://html.spec.whatwg.org/multipage/input.html#local-date-and-time-state-(type=datetime-local):value-sanitization-algorithm if (is_valid_local_date_and_time_string(value)) return normalize_local_date_and_time_string(value); return String {}; } else if (type_state() == HTMLInputElement::TypeAttributeState::Range) { // https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):value-sanitization-algorithm // If the value of the element is not a valid floating-point number, then set it to the best representation, as a floating-point number, of the default value. auto maybe_value = parse_floating_point_number(value); if (!maybe_value.has_value() || !isfinite(maybe_value.value())) { // The default value is the minimum plus half the difference between the minimum and the maximum, unless the maximum is less than the minimum, in which case the default value is the minimum. auto min = parse_floating_point_number(get_attribute(HTML::AttributeNames::min).value_or("0"_string)).value_or(0); auto max = parse_floating_point_number(get_attribute(HTML::AttributeNames::max).value_or("1"_string)).value_or(1); if (max > min) return JS::number_to_string(min); return JS::number_to_string(min + (max - min) / 2); } } else if (type_state() == HTMLInputElement::TypeAttributeState::Color) { // https://html.spec.whatwg.org/multipage/input.html#color-state-(type=color):value-sanitization-algorithm // If the value of the element is a valid simple color, then set it to the value of the element converted to ASCII lowercase; if (is_valid_simple_color(value)) return MUST(Infra::to_ascii_lowercase(value)); // otherwise, set it to the string "#000000". return "#000000"_string; } return value; } // https://html.spec.whatwg.org/multipage/input.html#the-input-element:concept-form-reset-control void HTMLInputElement::reset_algorithm() { // The reset algorithm for input elements is to set the dirty value flag and dirty checkedness flag back to false, m_dirty_value = false; m_dirty_checkedness = false; // set the value of the element to the value of the value content attribute, if there is one, or the empty string otherwise, m_value = get_attribute(AttributeNames::value).value_or(String {}); // set the checkedness of the element to true if the element has a checked content attribute and false if it does not, m_checked = has_attribute(AttributeNames::checked); // empty the list of selected files, m_selected_files = FileAPI::FileList::create(realm(), {}); // and then invoke the value sanitization algorithm, if the type attribute's current state defines one. m_value = value_sanitization_algorithm(m_value); if (m_text_node) { m_text_node->set_data(m_value); update_placeholder_visibility(); } if (type_state() == TypeAttributeState::Color && m_color_well_element) MUST(m_color_well_element->style_for_bindings()->set_property(CSS::PropertyID::BackgroundColor, m_value)); } void HTMLInputElement::form_associated_element_was_inserted() { create_shadow_tree_if_needed(); } void HTMLInputElement::form_associated_element_was_removed(DOM::Node*) { set_shadow_root(nullptr); } // https://html.spec.whatwg.org/multipage/input.html#radio-button-group static bool is_in_same_radio_button_group(HTML::HTMLInputElement const& a, HTML::HTMLInputElement const& b) { auto non_empty_equals = [](auto const& value_a, auto const& value_b) { return !value_a.is_empty() && value_a == value_b; }; // The radio button group that contains an input element a also contains all the // other input elements b that fulfill all of the following conditions: return ( // - Both a and b are in the same tree. // - The input element b's type attribute is in the Radio Button state. a.type_state() == b.type_state() && b.type_state() == HTMLInputElement::TypeAttributeState::RadioButton // - Either a and b have the same form owner, or they both have no form owner. && a.form() == b.form() // - They both have a name attribute, their name attributes are not empty, and the // value of a's name attribute equals the value of b's name attribute. && a.has_attribute(HTML::AttributeNames::name) && b.has_attribute(HTML::AttributeNames::name) && non_empty_equals(a.name(), b.name())); } // https://html.spec.whatwg.org/multipage/input.html#radio-button-state-(type=radio) void HTMLInputElement::set_checked_within_group() { if (checked()) return; set_checked(true, ChangeSource::User); // No point iterating the tree if we have an empty name. auto name = this->name(); if (name.is_empty()) return; document().for_each_in_inclusive_subtree_of_type([&](auto& element) { if (element.checked() && &element != this && is_in_same_radio_button_group(*this, element)) element.set_checked(false, ChangeSource::User); return IterationDecision::Continue; }); } // https://html.spec.whatwg.org/multipage/input.html#the-input-element:legacy-pre-activation-behavior void HTMLInputElement::legacy_pre_activation_behavior() { m_before_legacy_pre_activation_behavior_checked = checked(); m_before_legacy_pre_activation_behavior_indeterminate = indeterminate(); // 1. If this element's type attribute is in the Checkbox state, then set // this element's checkedness to its opposite value (i.e. true if it is // false, false if it is true) and set this element's indeterminate IDL // attribute to false. if (type_state() == TypeAttributeState::Checkbox) { set_checked(!checked(), ChangeSource::User); set_indeterminate(false); } // 2. If this element's type attribute is in the Radio Button state, then // get a reference to the element in this element's radio button group that // has its checkedness set to true, if any, and then set this element's // checkedness to true. if (type_state() == TypeAttributeState::RadioButton) { ByteString name = this->name(); document().for_each_in_inclusive_subtree_of_type([&](auto& element) { if (element.checked() && is_in_same_radio_button_group(*this, element)) { m_legacy_pre_activation_behavior_checked_element_in_group = &element; return IterationDecision::Break; } return IterationDecision::Continue; }); set_checked_within_group(); } } // https://html.spec.whatwg.org/multipage/input.html#the-input-element:legacy-canceled-activation-behavior void HTMLInputElement::legacy_cancelled_activation_behavior() { // 1. If the element's type attribute is in the Checkbox state, then set the // element's checkedness and the element's indeterminate IDL attribute back // to the values they had before the legacy-pre-activation behavior was run. if (type_state() == TypeAttributeState::Checkbox) { set_checked(m_before_legacy_pre_activation_behavior_checked, ChangeSource::Programmatic); set_indeterminate(m_before_legacy_pre_activation_behavior_indeterminate); } // 2. If this element 's type attribute is in the Radio Button state, then // if the element to which a reference was obtained in the // legacy-pre-activation behavior, if any, is still in what is now this // element' s radio button group, if it still has one, and if so, setting // that element 's checkedness to true; or else, if there was no such // element, or that element is no longer in this element' s radio button // group, or if this element no longer has a radio button group, setting // this element's checkedness to false. if (type_state() == TypeAttributeState::RadioButton) { bool did_reselect_previous_element = false; if (m_legacy_pre_activation_behavior_checked_element_in_group) { auto& element_in_group = *m_legacy_pre_activation_behavior_checked_element_in_group; if (is_in_same_radio_button_group(*this, element_in_group)) { element_in_group.set_checked_within_group(); did_reselect_previous_element = true; } m_legacy_pre_activation_behavior_checked_element_in_group = nullptr; } if (!did_reselect_previous_element) set_checked(false, ChangeSource::User); } } void HTMLInputElement::legacy_cancelled_activation_behavior_was_not_called() { m_legacy_pre_activation_behavior_checked_element_in_group = nullptr; } // https://html.spec.whatwg.org/multipage/interaction.html#dom-tabindex i32 HTMLInputElement::default_tab_index_value() const { // See the base function for the spec comments. return 0; } // https://html.spec.whatwg.org/multipage/input.html#the-size-attribute unsigned HTMLInputElement::size() const { // The size IDL attribute is limited to only positive numbers and has a default value of 20. if (auto size_string = get_attribute(HTML::AttributeNames::size); size_string.has_value()) { if (auto size = parse_non_negative_integer(*size_string); size.has_value()) return *size; } return 20; } WebIDL::ExceptionOr HTMLInputElement::set_size(unsigned value) { return set_attribute(HTML::AttributeNames::size, MUST(String::number(value))); } // https://html.spec.whatwg.org/multipage/input.html#concept-input-value-string-number Optional HTMLInputElement::convert_string_to_number(StringView input) const { // https://html.spec.whatwg.org/multipage/input.html#number-state-(type=number):concept-input-value-string-number if (type_state() == TypeAttributeState::Number) return parse_floating_point_number(input); // https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):concept-input-value-string-number if (type_state() == TypeAttributeState::Range) return parse_floating_point_number(input); dbgln("HTMLInputElement::convert_string_to_number() not implemented for input type {}", type()); return {}; } // https://html.spec.whatwg.org/multipage/input.html#concept-input-value-string-number String HTMLInputElement::covert_number_to_string(double input) const { // https://html.spec.whatwg.org/multipage/input.html#number-state-(type=number):concept-input-value-number-string if (type_state() == TypeAttributeState::Number) return MUST(String::number(input)); // https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):concept-input-value-number-string if (type_state() == TypeAttributeState::Range) return MUST(String::number(input)); dbgln("HTMLInputElement::covert_number_to_string() not implemented for input type {}", type()); return {}; } // https://html.spec.whatwg.org/multipage/input.html#attr-input-min Optional HTMLInputElement::min() const { // If the element has a min attribute, and the result of applying the algorithm to convert a string to a number to // the value of the min attribute is a number, then that number is the element's minimum; otherwise, if the type // attribute's current state defines a default minimum, then that is the minimum; otherwise, the element has no minimum. if (auto min_string = get_attribute(HTML::AttributeNames::min); min_string.has_value()) { if (auto min = convert_string_to_number(*min_string); min.has_value()) return *min; } // https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):concept-input-min-default if (type_state() == TypeAttributeState::Range) return 0; return {}; } // https://html.spec.whatwg.org/multipage/input.html#attr-input-max Optional HTMLInputElement::max() const { // If the element has a max attribute, and the result of applying the algorithm to convert a string to a number to the // value of the max attribute is a number, then that number is the element's maximum; otherwise, if the type attribute's // current state defines a default maximum, then that is the maximum; otherwise, the element has no maximum. if (auto max_string = get_attribute(HTML::AttributeNames::max); max_string.has_value()) { if (auto max = convert_string_to_number(*max_string); max.has_value()) return *max; } // https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):concept-input-max-default if (type_state() == TypeAttributeState::Range) return 100; return {}; } // https://html.spec.whatwg.org/multipage/input.html#concept-input-step-default double HTMLInputElement::default_step() const { // https://html.spec.whatwg.org/multipage/input.html#number-state-(type=number):concept-input-step-default if (type_state() == TypeAttributeState::Number) return 1; // https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):concept-input-step-default if (type_state() == TypeAttributeState::Range) return 1; dbgln("HTMLInputElement::default_step() not implemented for input type {}", type()); return 0; } // https://html.spec.whatwg.org/multipage/input.html#concept-input-step-scale double HTMLInputElement::step_scale_factor() const { // https://html.spec.whatwg.org/multipage/input.html#number-state-(type=number):concept-input-step-scale if (type_state() == TypeAttributeState::Number) return 1; // https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):concept-input-step-scale if (type_state() == TypeAttributeState::Range) return 1; dbgln("HTMLInputElement::step_scale_factor() not implemented for input type {}", type()); return 0; } // https://html.spec.whatwg.org/multipage/input.html#concept-input-step Optional HTMLInputElement::allowed_value_step() const { // 1. If the attribute does not apply, then there is no allowed value step. if (!step_applies()) return {}; // 2. Otherwise, if the attribute is absent, then the allowed value step is the default step multiplied by the step scale factor. auto maybe_step_string = get_attribute(AttributeNames::step); if (!maybe_step_string.has_value()) return default_step() * step_scale_factor(); auto step_string = *maybe_step_string; // 3. Otherwise, if the attribute's value is an ASCII case-insensitive match for the string "any", then there is no allowed value step. if (Infra::is_ascii_case_insensitive_match(step_string, "any"_string)) return {}; // 4. Otherwise, if the rules for parsing floating-point number values, when they are applied to the attribute's value, return an error, // zero, or a number less than zero, then the allowed value step is the default step multiplied by the step scale factor. auto maybe_step = parse_floating_point_number(step_string); if (!maybe_step.has_value() || *maybe_step == 0 || *maybe_step < 0) return default_step() * step_scale_factor(); // 5. Otherwise, the allowed value step is the number returned by the rules for parsing floating-point number values when they are applied // to the attribute's value, multiplied by the step scale factor. return *maybe_step * step_scale_factor(); } // https://html.spec.whatwg.org/multipage/input.html#concept-input-min-zero double HTMLInputElement::step_base() const { // 1. If the element has a min content attribute, and the result of applying the algorithm to convert a string to a number to the value of // the min content attribute is not an error, then return that result. if (auto min = this->min(); min.has_value()) return *min; // 2. If the element has a value content attribute, and the result of applying the algorithm to convert a string to a number to the value of // the value content attribute is not an error, then return that result. if (auto value = convert_string_to_number(this->value()); value.has_value()) return *value; // 3. If a default step base is defined for this element given its type attribute's state, then return it. if (type_state() == TypeAttributeState::Week) { // The default step base is −259,200,000 (the start of week 1970-W01). return -259'200'000; } // 4. Return zero. return 0; } // https://html.spec.whatwg.org/multipage/input.html#dom-input-valueasnumber WebIDL::ExceptionOr 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()) return NAN; // Otherwise, run the algorithm to convert a string to a number defined for that state to the element's value; // if the algorithm returned a number, then return it, otherwise, return a Not-a-Number (NaN) value. return convert_string_to_number(value()).value_or(NAN); } // https://html.spec.whatwg.org/multipage/input.html#dom-input-valueasnumber WebIDL::ExceptionOr HTMLInputElement::set_value_as_number(double value) { // On setting, if the new value is infinite, then throw a TypeError exception. if (!isfinite(value)) return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "valueAsNumber: Value is infinite"sv }; // Otherwise, if the valueAsNumber attribute does not apply, as defined for the input element's type attribute's current state, then throw an "InvalidStateError" DOMException. if (!value_as_number_applies()) return WebIDL::InvalidStateError::create(realm(), "valueAsNumber: Invalid input type used"_fly_string); // Otherwise, if the new value is a Not-a-Number (NaN) value, then set the value of the element to the empty string. if (value == NAN) { TRY(set_value(String {})); return {}; } // Otherwise, run the algorithm to convert a number 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_number_to_string(value))); return {}; } // https://html.spec.whatwg.org/multipage/input.html#dom-input-stepup WebIDL::ExceptionOr HTMLInputElement::step_up(long n) { return step_up_or_down(false, n); } // https://html.spec.whatwg.org/multipage/input.html#dom-input-stepdown WebIDL::ExceptionOr HTMLInputElement::step_down(long n) { return step_up_or_down(true, n); } // https://html.spec.whatwg.org/multipage/input.html#dom-input-stepup WebIDL::ExceptionOr HTMLInputElement::step_up_or_down(bool is_down, long n) { // 1. If the stepDown() and stepUp() methods do not apply, as defined for the input element's type attribute's current state, then throw an "InvalidStateError" DOMException. if (!step_up_or_down_applies()) return WebIDL::InvalidStateError::create(realm(), MUST(String::formatted("{}: Invalid input type used", is_down ? "stepDown()" : "stepUp()"))); // 2. If the element has no allowed value step, then throw an "InvalidStateError" DOMException. auto maybe_allowed_value_step = allowed_value_step(); if (!maybe_allowed_value_step.has_value()) return WebIDL::InvalidStateError::create(realm(), "element has no allowed value step"_fly_string); double allowed_value_step = *maybe_allowed_value_step; // 3. If the element has a minimum and a maximum and the minimum is greater than the maximum, then return. auto maybe_minimum = min(); auto maybe_maximum = max(); if (maybe_minimum.has_value() && maybe_maximum.has_value() && *maybe_minimum > *maybe_maximum) return {}; // FIXME: 4. If the element has a minimum and a maximum and there is no value greater than or equal to the element's minimum and less than // or equal to the element's maximum that, when subtracted from the step base, is an integral multiple of the allowed value step, then return. // 5. If applying the algorithm to convert a string to a number to the string given by the element's value does not result in an error, // then let value be the result of that algorithm. Otherwise, let value be zero. double value = convert_string_to_number(this->value()).value_or(0); // 6. Let valueBeforeStepping be value. double value_before_stepping = value; // 7. If value subtracted from the step base is not an integral multiple of the allowed value step, then set value to the nearest value that, // when subtracted from the step base, is an integral multiple of the allowed value step, and that is less than value if the method invoked was the stepDown() method, and more than value otherwise. if (fmod(step_base() - value, allowed_value_step) != 0) { double diff = step_base() - value; if (is_down) { value = diff - fmod(diff, allowed_value_step); } else { value = diff + fmod(diff, allowed_value_step); } } else { // 1. Let n be the argument. // 2. Let delta be the allowed value step multiplied by n. double delta = allowed_value_step * n; // 3. If the method invoked was the stepDown() method, negate delta. if (is_down) delta = -delta; // 4. Let value be the result of adding delta to value. value += delta; } // 8. If the element has a minimum, and value is less than that minimum, then set value to the smallest value that, // when subtracted from the step base, is an integral multiple of the allowed value step, and that is more than or equal to minimum. if (maybe_minimum.has_value() && value < *maybe_minimum) { value = AK::max(value, *maybe_minimum); } // 9. If the element has a maximum, and value is greater than that maximum, then set value to the largest value that, // when subtracted from the step base, is an integral multiple of the allowed value step, and that is less than or equal to maximum. if (maybe_maximum.has_value() && value > *maybe_maximum) { value = AK::min(value, *maybe_maximum); } // 10. If either the method invoked was the stepDown() method and value is greater than valueBeforeStepping, // or the method invoked was the stepUp() method and value is less than valueBeforeStepping, then return. if (is_down) { if (value > value_before_stepping) return {}; } else { if (value < value_before_stepping) return {}; } // 11. Let value as string be the result of running the algorithm to convert a number to a string, // as defined for the input element's type attribute's current state, on value. auto value_as_string = covert_number_to_string(value); // 12. Set the value of the element to value as string. TRY(set_value(value_as_string)); return {}; } // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-cva-checkvalidity WebIDL::ExceptionOr HTMLInputElement::check_validity() { dbgln("(STUBBED) HTMLInputElement::check_validity(). Called on: {}", debug_description()); return true; } // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-cva-reportvalidity WebIDL::ExceptionOr HTMLInputElement::report_validity() { dbgln("(STUBBED) HTMLInputElement::report_validity(). Called on: {}", debug_description()); return true; } // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-cva-setcustomvalidity void HTMLInputElement::set_custom_validity(String const& error) { dbgln("(STUBBED) HTMLInputElement::set_custom_validity(error={}). Called on: {}", error, debug_description()); return; } // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-select WebIDL::ExceptionOr HTMLInputElement::select() { dbgln("(STUBBED) HTMLInputElement::select(). Called on: {}", debug_description()); return {}; } // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setselectionrange WebIDL::ExceptionOr HTMLInputElement::set_selection_range(u32 start, u32 end, Optional const& direction) { dbgln("(STUBBED) HTMLInputElement::set_selection_range(start={}, end={}, direction='{}'). Called on: {}", start, end, direction, debug_description()); return {}; } Optional HTMLInputElement::default_role() const { // https://www.w3.org/TR/html-aria/#el-input-button if (type_state() == TypeAttributeState::Button) return ARIA::Role::button; // https://www.w3.org/TR/html-aria/#el-input-checkbox if (type_state() == TypeAttributeState::Checkbox) return ARIA::Role::checkbox; // https://www.w3.org/TR/html-aria/#el-input-email if (type_state() == TypeAttributeState::Email && !has_attribute(AttributeNames::list)) return ARIA::Role::textbox; // https://www.w3.org/TR/html-aria/#el-input-image if (type_state() == TypeAttributeState::ImageButton) return ARIA::Role::button; // https://www.w3.org/TR/html-aria/#el-input-number if (type_state() == TypeAttributeState::Number) return ARIA::Role::spinbutton; // https://www.w3.org/TR/html-aria/#el-input-radio if (type_state() == TypeAttributeState::RadioButton) return ARIA::Role::radio; // https://www.w3.org/TR/html-aria/#el-input-range if (type_state() == TypeAttributeState::Range) return ARIA::Role::slider; // https://www.w3.org/TR/html-aria/#el-input-reset if (type_state() == TypeAttributeState::ResetButton) return ARIA::Role::button; // https://www.w3.org/TR/html-aria/#el-input-text-list if ((type_state() == TypeAttributeState::Text || type_state() == TypeAttributeState::Search || type_state() == TypeAttributeState::Telephone || type_state() == TypeAttributeState::URL || type_state() == TypeAttributeState::Email) && has_attribute(AttributeNames::list)) return ARIA::Role::combobox; // https://www.w3.org/TR/html-aria/#el-input-search if (type_state() == TypeAttributeState::Search && !has_attribute(AttributeNames::list)) return ARIA::Role::textbox; // https://www.w3.org/TR/html-aria/#el-input-submit if (type_state() == TypeAttributeState::SubmitButton) return ARIA::Role::button; // https://www.w3.org/TR/html-aria/#el-input-tel if (type_state() == TypeAttributeState::Telephone) return ARIA::Role::textbox; // https://www.w3.org/TR/html-aria/#el-input-text if (type_state() == TypeAttributeState::Text && !has_attribute(AttributeNames::list)) return ARIA::Role::textbox; // https://www.w3.org/TR/html-aria/#el-input-url if (type_state() == TypeAttributeState::URL && !has_attribute(AttributeNames::list)) return ARIA::Role::textbox; // https://www.w3.org/TR/html-aria/#el-input-color // https://www.w3.org/TR/html-aria/#el-input-date // https://www.w3.org/TR/html-aria/#el-input-datetime-local // https://www.w3.org/TR/html-aria/#el-input-file // https://www.w3.org/TR/html-aria/#el-input-hidden // https://www.w3.org/TR/html-aria/#el-input-month // https://www.w3.org/TR/html-aria/#el-input-password // https://www.w3.org/TR/html-aria/#el-input-time // https://www.w3.org/TR/html-aria/#el-input-week return {}; } bool HTMLInputElement::is_button() const { // https://html.spec.whatwg.org/multipage/input.html#submit-button-state-(type=submit):concept-button // https://html.spec.whatwg.org/multipage/input.html#image-button-state-(type=image):concept-button // https://html.spec.whatwg.org/multipage/input.html#reset-button-state-(type=reset):concept-button // https://html.spec.whatwg.org/multipage/input.html#button-state-(type=button):concept-button return type_state() == TypeAttributeState::SubmitButton || type_state() == TypeAttributeState::ImageButton || type_state() == TypeAttributeState::ResetButton || type_state() == TypeAttributeState::Button; } bool HTMLInputElement::is_submit_button() const { // https://html.spec.whatwg.org/multipage/input.html#submit-button-state-(type=submit):concept-submit-button // https://html.spec.whatwg.org/multipage/input.html#image-button-state-(type=image):concept-submit-button return type_state() == TypeAttributeState::SubmitButton || type_state() == TypeAttributeState::ImageButton; } bool HTMLInputElement::has_activation_behavior() const { return true; } void HTMLInputElement::activation_behavior(DOM::Event const&) { // The activation behavior for input elements are these steps: // FIXME: 1. If this element is not mutable and is not in the Checkbox state and is not in the Radio state, then return. // 2. Run this element's input activation behavior, if any, and do nothing otherwise. run_input_activation_behavior().release_value_but_fixme_should_propagate_errors(); } bool HTMLInputElement::has_input_activation_behavior() const { switch (type_state()) { case TypeAttributeState::Checkbox: case TypeAttributeState::Color: case TypeAttributeState::FileUpload: case TypeAttributeState::ImageButton: case TypeAttributeState::RadioButton: case TypeAttributeState::ResetButton: case TypeAttributeState::SubmitButton: return true; default: return false; } } // https://html.spec.whatwg.org/multipage/input.html#the-input-element:event-change-2 bool HTMLInputElement::change_event_applies() const { switch (type_state()) { case TypeAttributeState::Checkbox: case TypeAttributeState::Color: case TypeAttributeState::Date: case TypeAttributeState::Email: case TypeAttributeState::FileUpload: case TypeAttributeState::LocalDateAndTime: case TypeAttributeState::Month: case TypeAttributeState::Number: case TypeAttributeState::Password: case TypeAttributeState::RadioButton: case TypeAttributeState::Range: case TypeAttributeState::Search: case TypeAttributeState::Telephone: case TypeAttributeState::Text: case TypeAttributeState::Time: case TypeAttributeState::URL: case TypeAttributeState::Week: 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 { switch (type_state()) { case TypeAttributeState::Date: case TypeAttributeState::Month: case TypeAttributeState::Week: case TypeAttributeState::Time: case TypeAttributeState::LocalDateAndTime: case TypeAttributeState::Number: case TypeAttributeState::Range: return true; default: return false; } } // https://html.spec.whatwg.org/multipage/input.html#the-input-element:attr-input-step-3 bool HTMLInputElement::step_applies() const { return value_as_number_applies(); } // https://html.spec.whatwg.org/multipage/input.html#the-input-element:dom-input-stepup-3 bool HTMLInputElement::step_up_or_down_applies() const { return value_as_number_applies(); } }