Serenity Operating System
at master 939 lines 43 kB view raw
1/* 2 * Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org> 3 * Copyright (c) 2022, Adam Hodgen <ant1441@gmail.com> 4 * Copyright (c) 2022, Andrew Kaster <akaster@serenityos.org> 5 * 6 * SPDX-License-Identifier: BSD-2-Clause 7 */ 8 9#include <LibWeb/DOM/Document.h> 10#include <LibWeb/DOM/Event.h> 11#include <LibWeb/DOM/ShadowRoot.h> 12#include <LibWeb/DOM/Text.h> 13#include <LibWeb/HTML/BrowsingContext.h> 14#include <LibWeb/HTML/EventNames.h> 15#include <LibWeb/HTML/HTMLFormElement.h> 16#include <LibWeb/HTML/HTMLInputElement.h> 17#include <LibWeb/HTML/Scripting/Environments.h> 18#include <LibWeb/Infra/CharacterTypes.h> 19#include <LibWeb/Layout/BlockContainer.h> 20#include <LibWeb/Layout/ButtonBox.h> 21#include <LibWeb/Layout/CheckBox.h> 22#include <LibWeb/Layout/RadioButton.h> 23#include <LibWeb/WebIDL/DOMException.h> 24#include <LibWeb/WebIDL/ExceptionOr.h> 25 26namespace Web::HTML { 27 28HTMLInputElement::HTMLInputElement(DOM::Document& document, DOM::QualifiedName qualified_name) 29 : HTMLElement(document, move(qualified_name)) 30 , m_value(DeprecatedString::empty()) 31{ 32 activation_behavior = [this](auto&) { 33 // The activation behavior for input elements are these steps: 34 35 // FIXME: 1. If this element is not mutable and is not in the Checkbox state and is not in the Radio state, then return. 36 37 // 2. Run this element's input activation behavior, if any, and do nothing otherwise. 38 run_input_activation_behavior().release_value_but_fixme_should_propagate_errors(); 39 }; 40} 41 42HTMLInputElement::~HTMLInputElement() = default; 43 44JS::ThrowCompletionOr<void> HTMLInputElement::initialize(JS::Realm& realm) 45{ 46 MUST_OR_THROW_OOM(Base::initialize(realm)); 47 set_prototype(&Bindings::ensure_web_prototype<Bindings::HTMLInputElementPrototype>(realm, "HTMLInputElement")); 48 49 return {}; 50} 51 52void HTMLInputElement::visit_edges(Cell::Visitor& visitor) 53{ 54 Base::visit_edges(visitor); 55 visitor.visit(m_text_node.ptr()); 56 visitor.visit(m_legacy_pre_activation_behavior_checked_element_in_group.ptr()); 57 visitor.visit(m_selected_files); 58} 59 60JS::GCPtr<Layout::Node> HTMLInputElement::create_layout_node(NonnullRefPtr<CSS::StyleProperties> style) 61{ 62 if (type_state() == TypeAttributeState::Hidden) 63 return nullptr; 64 65 if (type_state() == TypeAttributeState::SubmitButton || type_state() == TypeAttributeState::Button || type_state() == TypeAttributeState::ResetButton || type_state() == TypeAttributeState::FileUpload) 66 return heap().allocate_without_realm<Layout::ButtonBox>(document(), *this, move(style)); 67 68 if (type_state() == TypeAttributeState::Checkbox) 69 return heap().allocate_without_realm<Layout::CheckBox>(document(), *this, move(style)); 70 71 if (type_state() == TypeAttributeState::RadioButton) 72 return heap().allocate_without_realm<Layout::RadioButton>(document(), *this, move(style)); 73 74 return heap().allocate_without_realm<Layout::BlockContainer>(document(), this, move(style)); 75} 76 77void HTMLInputElement::set_checked(bool checked, ChangeSource change_source) 78{ 79 if (m_checked == checked) 80 return; 81 82 // The dirty checkedness flag must be initially set to false when the element is created, 83 // and must be set to true whenever the user interacts with the control in a way that changes the checkedness. 84 if (change_source == ChangeSource::User) 85 m_dirty_checkedness = true; 86 87 m_checked = checked; 88 set_needs_style_update(true); 89} 90 91void HTMLInputElement::set_checked_binding(bool checked) 92{ 93 if (type_state() == TypeAttributeState::RadioButton) { 94 if (checked) 95 set_checked_within_group(); 96 else 97 set_checked(false, ChangeSource::Programmatic); 98 } else { 99 set_checked(checked, ChangeSource::Programmatic); 100 } 101} 102 103// https://html.spec.whatwg.org/multipage/input.html#dom-input-files 104JS::GCPtr<FileAPI::FileList> HTMLInputElement::files() 105{ 106 // On getting, if the IDL attribute applies, it must return a FileList object that represents the current selected files. 107 // The same object must be returned until the list of selected files changes. 108 // If the IDL attribute does not apply, then it must instead return null. 109 if (m_type != TypeAttributeState::FileUpload) 110 return nullptr; 111 112 if (!m_selected_files) 113 m_selected_files = FileAPI::FileList::create(realm(), {}).release_value_but_fixme_should_propagate_errors(); 114 return m_selected_files; 115} 116 117// https://html.spec.whatwg.org/multipage/input.html#dom-input-files 118void HTMLInputElement::set_files(JS::GCPtr<FileAPI::FileList> files) 119{ 120 // 1. If the IDL attribute does not apply or the given value is null, then return. 121 if (m_type != TypeAttributeState::FileUpload || files == nullptr) 122 return; 123 124 // 2. Replace the element's selected files with the given value. 125 m_selected_files = files; 126} 127 128// https://html.spec.whatwg.org/multipage/input.html#update-the-file-selection 129void HTMLInputElement::update_the_file_selection(JS::NonnullGCPtr<FileAPI::FileList> files) 130{ 131 // 1. Queue an element task on the user interaction task source given element and the following steps: 132 queue_an_element_task(Task::Source::UserInteraction, [this, files] { 133 // 1. Update element's selected files so that it represents the user's selection. 134 this->set_files(files.ptr()); 135 136 // 2. Fire an event named input at the input element, with the bubbles and composed attributes initialized to true. 137 auto input_event = DOM::Event::create(this->realm(), EventNames::input, { .bubbles = true, .composed = true }).release_value_but_fixme_should_propagate_errors(); 138 this->dispatch_event(input_event); 139 140 // 3. Fire an event named change at the input element, with the bubbles attribute initialized to true. 141 auto change_event = DOM::Event::create(this->realm(), EventNames::change, { .bubbles = true }).release_value_but_fixme_should_propagate_errors(); 142 this->dispatch_event(change_event); 143 }); 144} 145 146// https://html.spec.whatwg.org/multipage/input.html#show-the-picker,-if-applicable 147static void show_the_picker_if_applicable(HTMLInputElement& element) 148{ 149 // To show the picker, if applicable for an input element element: 150 151 // 1. If element's relevant global object does not have transient activation, then return. 152 auto& global_object = relevant_global_object(element); 153 if (!is<HTML::Window>(global_object) || !static_cast<HTML::Window&>(global_object).has_transient_activation()) 154 return; 155 156 // FIXME: 2. If element is not mutable, then return. 157 158 // 3. If element's type attribute is in the File Upload state, then run these steps in parallel: 159 if (element.type_state() == HTMLInputElement::TypeAttributeState::FileUpload) { 160 // NOTE: These steps cannot be fully implemented here, and must be done in the PageClient when the response comes back from the PageHost 161 162 // 1. Optionally, wait until any prior execution of this algorithm has terminated. 163 // 2. Display a prompt to the user requesting that the user specify some files. 164 // If the multiple attribute is not set on element, there must be no more than one file selected; otherwise, any number may be selected. 165 // 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. 166 // 3. Wait for the user to have made their selection. 167 // 4. If the user dismissed the prompt without changing their selection, 168 // then queue an element task on the user interaction task source given element to fire an event named cancel at element, 169 // with the bubbles attribute initialized to true. 170 // 5. Otherwise, update the file selection for element. 171 172 bool const multiple = element.has_attribute(HTML::AttributeNames::multiple); 173 auto weak_element = element.make_weak_ptr<DOM::EventTarget>(); 174 175 // FIXME: Pass along accept attribute information https://html.spec.whatwg.org/multipage/input.html#attr-input-accept 176 // The accept attribute may be specified to provide user agents with a hint of what file types will be accepted. 177 element.document().browsing_context()->top_level_browsing_context().page()->client().page_did_request_file_picker(weak_element, multiple); 178 return; 179 } 180 181 // FIXME: show "any relevant user interface" for other type attribute states "in the way [the user agent] normally would" 182 183 // 4. Otherwise, the user agent should show any relevant user interface for selecting a value for element, 184 // 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.) 185 // If such a user interface is shown, it must respect the requirements stated in the relevant parts of the specification for how element 186 // behaves given its type attribute state. (For example, various sections describe restrictions on the resulting value string.) 187 // This step can have side effects, such as closing other pickers that were previously shown by this algorithm. 188 // (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.) 189} 190 191// https://html.spec.whatwg.org/multipage/input.html#dom-input-showpicker 192WebIDL::ExceptionOr<void> HTMLInputElement::show_picker() 193{ 194 // The showPicker() method steps are: 195 196 // FIXME: 1. If this is not mutable, then throw an "InvalidStateError" DOMException. 197 198 // 2. If this's relevant settings object's origin is not same origin with this's relevant settings object's top-level origin, 199 // and this's type attribute is not in the File Upload state or Color state, then throw a "SecurityError" DOMException. 200 // NOTE: File and Color inputs are exempted from this check for historical reason: their input activation behavior also shows their pickers, 201 // and has never been guarded by an origin check. 202 if (!relevant_settings_object(*this).origin().is_same_origin(relevant_settings_object(*this).top_level_origin) 203 && m_type != TypeAttributeState::FileUpload && m_type != TypeAttributeState::Color) { 204 return WebIDL::SecurityError::create(realm(), "Cross origin pickers are not allowed"sv); 205 } 206 207 // 3. If this's relevant global object does not have transient activation, then throw a "NotAllowedError" DOMException. 208 // FIXME: The global object we get here should probably not need casted to Window to check for transient activation 209 auto& global_object = relevant_global_object(*this); 210 if (!is<HTML::Window>(global_object) || !static_cast<HTML::Window&>(global_object).has_transient_activation()) { 211 return WebIDL::NotAllowedError::create(realm(), "Too long since user activation to show picker"sv); 212 } 213 214 // 4. Show the picker, if applicable, for this. 215 show_the_picker_if_applicable(*this); 216 return {}; 217} 218 219// https://html.spec.whatwg.org/multipage/input.html#input-activation-behavior 220ErrorOr<void> HTMLInputElement::run_input_activation_behavior() 221{ 222 if (type_state() == TypeAttributeState::Checkbox || type_state() == TypeAttributeState::RadioButton) { 223 // 1. If the element is not connected, then return. 224 if (!is_connected()) 225 return {}; 226 227 // 2. Fire an event named input at the element with the bubbles and composed attributes initialized to true. 228 auto input_event = DOM::Event::create(realm(), HTML::EventNames::input).release_value_but_fixme_should_propagate_errors(); 229 input_event->set_bubbles(true); 230 input_event->set_composed(true); 231 dispatch_event(input_event); 232 233 // 3. Fire an event named change at the element with the bubbles attribute initialized to true. 234 auto change_event = DOM::Event::create(realm(), HTML::EventNames::change).release_value_but_fixme_should_propagate_errors(); 235 change_event->set_bubbles(true); 236 dispatch_event(*change_event); 237 } else if (type_state() == TypeAttributeState::SubmitButton) { 238 JS::GCPtr<HTMLFormElement> form; 239 // 1. If the element does not have a form owner, then return. 240 if (!(form = this->form())) 241 return {}; 242 243 // 2. If the element's node document is not fully active, then return. 244 if (!document().is_fully_active()) 245 return {}; 246 247 // 3. Submit the form owner from the element. 248 TRY(form->submit_form(this)); 249 } else if (type_state() == TypeAttributeState::FileUpload) { 250 show_the_picker_if_applicable(*this); 251 } else { 252 dispatch_event(DOM::Event::create(realm(), EventNames::change).release_value_but_fixme_should_propagate_errors()); 253 } 254 255 return {}; 256} 257 258void HTMLInputElement::did_edit_text_node(Badge<BrowsingContext>) 259{ 260 // 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. 261 m_value = value_sanitization_algorithm(m_text_node->data()); 262 m_dirty_value = true; 263 264 // NOTE: This is a bit ad-hoc, but basically implements part of "4.10.5.5 Common event behaviors" 265 // https://html.spec.whatwg.org/multipage/input.html#common-input-element-events 266 queue_an_element_task(HTML::Task::Source::UserInteraction, [this] { 267 auto input_event = DOM::Event::create(realm(), HTML::EventNames::input).release_value_but_fixme_should_propagate_errors(); 268 input_event->set_bubbles(true); 269 input_event->set_composed(true); 270 dispatch_event(*input_event); 271 272 // FIXME: This should only fire when the input is "committed", whatever that means. 273 auto change_event = DOM::Event::create(realm(), HTML::EventNames::change).release_value_but_fixme_should_propagate_errors(); 274 change_event->set_bubbles(true); 275 dispatch_event(change_event); 276 }); 277} 278 279DeprecatedString HTMLInputElement::value() const 280{ 281 // https://html.spec.whatwg.org/multipage/input.html#dom-input-value-filename 282 if (type_state() == TypeAttributeState::FileUpload) { 283 // NOTE: This "fakepath" requirement is a sad accident of history. See the example in the File Upload state section for more information. 284 // NOTE: Since path components are not permitted in filenames in the list of selected files, the "\fakepath\" cannot be mistaken for a path component. 285 if (m_selected_files && m_selected_files->item(0)) 286 return DeprecatedString::formatted("C:\\fakepath\\{}", m_selected_files->item(0)->name()); 287 return "C:\\fakepath\\"sv; 288 } 289 290 // https://html.spec.whatwg.org/multipage/input.html#dom-input-value-value 291 // Return the current value of the element. 292 return m_value; 293} 294 295WebIDL::ExceptionOr<void> HTMLInputElement::set_value(DeprecatedString value) 296{ 297 // https://html.spec.whatwg.org/multipage/input.html#dom-input-value-filename 298 if (type_state() == TypeAttributeState::FileUpload) { 299 // On setting, if the new value is the empty string, empty the list of selected files; otherwise, throw an "InvalidStateError" DOMException. 300 if (value != DeprecatedString::empty()) 301 return WebIDL::InvalidStateError::create(realm(), "Setting value of input type file to non-empty string"sv); 302 m_selected_files = nullptr; 303 return {}; 304 } 305 306 // https://html.spec.whatwg.org/multipage/input.html#dom-input-value-value 307 // 1. Let oldValue be the element's value. 308 auto old_value = move(m_value); 309 310 // 2. Set the element's value to the new value. 311 // NOTE: This is done as part of step 4 below. 312 313 // 3. Set the element's dirty value flag to true. 314 m_dirty_value = true; 315 316 // 4. Invoke the value sanitization algorithm, if the element's type attribute's current state defines one. 317 m_value = value_sanitization_algorithm(move(value)); 318 319 // 5. If the element's value (after applying the value sanitization algorithm) is different from oldValue, 320 // and the element has a text entry cursor position, move the text entry cursor position to the end of the 321 // text control, unselecting any selected text and resetting the selection direction to "none". 322 if (m_text_node && (m_value != old_value)) 323 m_text_node->set_data(m_value); 324 325 return {}; 326} 327 328// https://html.spec.whatwg.org/multipage/input.html#the-input-element:attr-input-placeholder-3 329static bool is_allowed_to_have_placeholder(HTML::HTMLInputElement::TypeAttributeState state) 330{ 331 switch (state) { 332 case HTML::HTMLInputElement::TypeAttributeState::Text: 333 case HTML::HTMLInputElement::TypeAttributeState::Search: 334 case HTML::HTMLInputElement::TypeAttributeState::URL: 335 case HTML::HTMLInputElement::TypeAttributeState::Telephone: 336 case HTML::HTMLInputElement::TypeAttributeState::Email: 337 case HTML::HTMLInputElement::TypeAttributeState::Password: 338 case HTML::HTMLInputElement::TypeAttributeState::Number: 339 return true; 340 default: 341 return false; 342 } 343} 344 345// https://html.spec.whatwg.org/multipage/input.html#attr-input-placeholder 346Optional<DeprecatedString> HTMLInputElement::placeholder_value() const 347{ 348 if (!m_text_node || !m_text_node->data().is_empty()) 349 return {}; 350 if (!is_allowed_to_have_placeholder(type_state())) 351 return {}; 352 if (!has_attribute(HTML::AttributeNames::placeholder)) 353 return {}; 354 355 auto placeholder = attribute(HTML::AttributeNames::placeholder); 356 357 if (placeholder.contains('\r') || placeholder.contains('\n')) { 358 StringBuilder builder; 359 for (auto ch : placeholder) { 360 if (ch != '\r' && ch != '\n') 361 builder.append(ch); 362 } 363 placeholder = builder.to_deprecated_string(); 364 } 365 366 return placeholder; 367} 368 369void HTMLInputElement::create_shadow_tree_if_needed() 370{ 371 if (shadow_root_internal()) 372 return; 373 374 // FIXME: This could be better factored. Everything except the below types becomes a text input. 375 switch (type_state()) { 376 case TypeAttributeState::RadioButton: 377 case TypeAttributeState::Checkbox: 378 case TypeAttributeState::Button: 379 case TypeAttributeState::SubmitButton: 380 case TypeAttributeState::ResetButton: 381 case TypeAttributeState::ImageButton: 382 return; 383 default: 384 break; 385 } 386 387 auto shadow_root = heap().allocate<DOM::ShadowRoot>(realm(), document(), *this, Bindings::ShadowRootMode::Closed).release_allocated_value_but_fixme_should_propagate_errors(); 388 auto initial_value = m_value; 389 if (initial_value.is_null()) 390 initial_value = DeprecatedString::empty(); 391 auto element = document().create_element(HTML::TagNames::div).release_value(); 392 MUST(element->set_attribute(HTML::AttributeNames::style, "white-space: pre; padding-top: 1px; padding-bottom: 1px; padding-left: 2px; padding-right: 2px")); 393 m_text_node = heap().allocate<DOM::Text>(realm(), document(), initial_value).release_allocated_value_but_fixme_should_propagate_errors(); 394 m_text_node->set_always_editable(m_type != TypeAttributeState::FileUpload); 395 m_text_node->set_owner_input_element({}, *this); 396 397 if (m_type == TypeAttributeState::Password) 398 m_text_node->set_is_password_input({}, true); 399 400 MUST(element->append_child(*m_text_node)); 401 MUST(shadow_root->append_child(element)); 402 set_shadow_root(shadow_root); 403} 404 405void HTMLInputElement::did_receive_focus() 406{ 407 auto* browsing_context = document().browsing_context(); 408 if (!browsing_context) 409 return; 410 if (!m_text_node) 411 return; 412 browsing_context->set_cursor_position(DOM::Position { *m_text_node, 0 }); 413} 414 415void HTMLInputElement::parse_attribute(DeprecatedFlyString const& name, DeprecatedString const& value) 416{ 417 HTMLElement::parse_attribute(name, value); 418 if (name == HTML::AttributeNames::checked) { 419 // When the checked content attribute is added, if the control does not have dirty checkedness, 420 // the user agent must set the checkedness of the element to true 421 if (!m_dirty_checkedness) 422 set_checked(true, ChangeSource::Programmatic); 423 } else if (name == HTML::AttributeNames::type) { 424 m_type = parse_type_attribute(value); 425 } else if (name == HTML::AttributeNames::value) { 426 if (!m_dirty_value) 427 m_value = value_sanitization_algorithm(value); 428 } 429} 430 431HTMLInputElement::TypeAttributeState HTMLInputElement::parse_type_attribute(StringView type) 432{ 433#define __ENUMERATE_HTML_INPUT_TYPE_ATTRIBUTE(keyword, state) \ 434 if (type.equals_ignoring_ascii_case(#keyword##sv)) \ 435 return HTMLInputElement::TypeAttributeState::state; 436 ENUMERATE_HTML_INPUT_TYPE_ATTRIBUTES 437#undef __ENUMERATE_HTML_INPUT_TYPE_ATTRIBUTE 438 439 // The missing value default and the invalid value default are the Text state. 440 // https://html.spec.whatwg.org/multipage/input.html#the-input-element:missing-value-default 441 // https://html.spec.whatwg.org/multipage/input.html#the-input-element:invalid-value-default 442 return HTMLInputElement::TypeAttributeState::Text; 443} 444 445void HTMLInputElement::did_remove_attribute(DeprecatedFlyString const& name) 446{ 447 HTMLElement::did_remove_attribute(name); 448 if (name == HTML::AttributeNames::checked) { 449 // When the checked content attribute is removed, if the control does not have dirty checkedness, 450 // the user agent must set the checkedness of the element to false. 451 if (!m_dirty_checkedness) 452 set_checked(false, ChangeSource::Programmatic); 453 } else if (name == HTML::AttributeNames::value) { 454 if (!m_dirty_value) 455 m_value = DeprecatedString::empty(); 456 } 457} 458 459DeprecatedString HTMLInputElement::type() const 460{ 461 switch (m_type) { 462#define __ENUMERATE_HTML_INPUT_TYPE_ATTRIBUTE(keyword, state) \ 463 case TypeAttributeState::state: \ 464 return #keyword##sv; 465 ENUMERATE_HTML_INPUT_TYPE_ATTRIBUTES 466#undef __ENUMERATE_HTML_INPUT_TYPE_ATTRIBUTE 467 } 468 469 VERIFY_NOT_REACHED(); 470} 471 472void HTMLInputElement::set_type(DeprecatedString const& type) 473{ 474 MUST(set_attribute(HTML::AttributeNames::type, type)); 475} 476 477// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-simple-colour 478static bool is_valid_simple_color(DeprecatedString const& value) 479{ 480 // if it is exactly seven characters long, 481 if (value.length() != 7) 482 return false; 483 // and the first character is a U+0023 NUMBER SIGN character (#), 484 if (!value.starts_with('#')) 485 return false; 486 // and the remaining six characters are all ASCII hex digits 487 for (size_t i = 1; i < value.length(); i++) 488 if (!is_ascii_hex_digit(value[i])) 489 return false; 490 491 return true; 492} 493 494// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-time-string 495static bool is_valid_time_string(DeprecatedString const& value) 496{ 497 // A string is a valid time string representing an hour hour, a minute minute, and a second second if it consists of the following components in the given order: 498 499 // 1. Two ASCII digits, representing hour, in the range 0 ≤ hour ≤ 23 500 // 2. A U+003A COLON character (:) 501 // 3. Two ASCII digits, representing minute, in the range 0 ≤ minute ≤ 59 502 // 4. If second is nonzero, or optionally if second is zero: 503 // 1. A U+003A COLON character (:) 504 // 2. Two ASCII digits, representing the integer part of second, in the range 0 ≤ s ≤ 59 505 // 3. If second is not an integer, or optionally if second is an integer: 506 // 1. A U+002E FULL STOP character (.) 507 // 2. One, two, or three ASCII digits, representing the fractional part of second 508 auto parts = value.split(':'); 509 if (parts.size() != 2 || parts.size() != 3) 510 return false; 511 if (parts[0].length() != 2) 512 return false; 513 auto hour = (parse_ascii_digit(parts[0][0]) * 10) + parse_ascii_digit(parts[0][1]); 514 if (hour > 23) 515 return false; 516 if (parts[1].length() != 2) 517 return false; 518 auto minute = (parse_ascii_digit(parts[1][0]) * 10) + parse_ascii_digit(parts[1][1]); 519 if (minute > 59) 520 return false; 521 if (parts.size() == 2) 522 return true; 523 524 if (parts[2].length() < 2) 525 return false; 526 auto second = (parse_ascii_digit(parts[2][0]) * 10) + parse_ascii_digit(parts[2][1]); 527 if (second > 59) 528 return false; 529 if (parts[2].length() == 2) 530 return true; 531 auto second_parts = parts[2].split('.'); 532 if (second_parts.size() != 2) 533 return false; 534 if (second_parts[1].length() < 1 || second_parts[1].length() > 3) 535 return false; 536 for (auto digit : second_parts[1]) 537 if (!is_ascii_digit(digit)) 538 return false; 539 540 return true; 541} 542 543// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#week-number-of-the-last-day 544static u32 week_number_of_the_last_day(u64) 545{ 546 // FIXME: sometimes return 53 (!) 547 // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#weeks 548 return 52; 549} 550 551// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-week-string 552static bool is_valid_week_string(DeprecatedString const& value) 553{ 554 // A string is a valid week string representing a week-year year and week week if it consists of the following components in the given order: 555 556 // 1. Four or more ASCII digits, representing year, where year > 0 557 // 2. A U+002D HYPHEN-MINUS character (-) 558 // 3. A U+0057 LATIN CAPITAL LETTER W character (W) 559 // 4. Two ASCII digits, representing the week week, in the range 1 ≤ week ≤ maxweek, where maxweek is the week number of the last day of week-year year 560 auto parts = value.split('-'); 561 if (parts.size() != 2) 562 return false; 563 if (parts[0].length() < 4) 564 return false; 565 for (auto digit : parts[0]) 566 if (!is_ascii_digit(digit)) 567 return false; 568 if (parts[1].length() != 3) 569 return false; 570 571 if (!parts[1].starts_with('W')) 572 return false; 573 if (!is_ascii_digit(parts[1][1])) 574 return false; 575 if (!is_ascii_digit(parts[1][2])) 576 return false; 577 578 u64 year = 0; 579 for (auto d : parts[0]) { 580 year *= 10; 581 year += parse_ascii_digit(d); 582 } 583 auto week = (parse_ascii_digit(parts[1][1]) * 10) + parse_ascii_digit(parts[1][2]); 584 585 return week >= 1 && week <= week_number_of_the_last_day(year); 586} 587 588// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-month-string 589static bool is_valid_month_string(DeprecatedString const& value) 590{ 591 // A string is a valid month string representing a year year and month month if it consists of the following components in the given order: 592 593 // 1. Four or more ASCII digits, representing year, where year > 0 594 // 2. A U+002D HYPHEN-MINUS character (-) 595 // 3. Two ASCII digits, representing the month month, in the range 1 ≤ month ≤ 12 596 597 auto parts = value.split('-'); 598 if (parts.size() != 2) 599 return false; 600 601 if (parts[0].length() < 4) 602 return false; 603 for (auto digit : parts[0]) 604 if (!is_ascii_digit(digit)) 605 return false; 606 607 if (parts[1].length() != 2) 608 return false; 609 610 if (!is_ascii_digit(parts[1][0])) 611 return false; 612 if (!is_ascii_digit(parts[1][1])) 613 return false; 614 615 auto month = (parse_ascii_digit(parts[1][0]) * 10) + parse_ascii_digit(parts[1][1]); 616 return month >= 1 && month <= 12; 617} 618 619// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-date-string 620static bool is_valid_date_string(DeprecatedString const& value) 621{ 622 // A string is a valid date string representing a year year, month month, and day day if it consists of the following components in the given order: 623 624 // 1. A valid month string, representing year and month 625 // 2. A U+002D HYPHEN-MINUS character (-) 626 // 3. Two ASCII digits, representing day, in the range 1 ≤ day ≤ maxday where maxday is the number of days in the month month and year year 627 auto parts = value.split('-'); 628 if (parts.size() != 3) 629 return false; 630 631 if (!is_valid_month_string(DeprecatedString::formatted("{}-{}", parts[0], parts[1]))) 632 return false; 633 634 if (parts[2].length() != 2) 635 return false; 636 637 i64 year = 0; 638 for (auto d : parts[0]) { 639 year *= 10; 640 year += parse_ascii_digit(d); 641 } 642 auto month = (parse_ascii_digit(parts[1][0]) * 10) + parse_ascii_digit(parts[1][1]); 643 i64 day = (parse_ascii_digit(parts[2][0]) * 10) + parse_ascii_digit(parts[2][1]); 644 645 return day >= 1 && day <= AK::days_in_month(year, month); 646} 647 648// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-local-date-and-time-string 649static bool is_valid_local_date_and_time_string(DeprecatedString const& value) 650{ 651 auto parts_split_by_T = value.split('T'); 652 if (parts_split_by_T.size() == 2) 653 return is_valid_date_string(parts_split_by_T[0]) && is_valid_time_string(parts_split_by_T[1]); 654 auto parts_split_by_space = value.split(' '); 655 if (parts_split_by_space.size() == 2) 656 return is_valid_date_string(parts_split_by_space[0]) && is_valid_time_string(parts_split_by_space[1]); 657 658 return false; 659} 660 661// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-normalised-local-date-and-time-string 662static DeprecatedString normalize_local_date_and_time_string(DeprecatedString const& value) 663{ 664 VERIFY(value.count(" "sv) == 1); 665 return value.replace(" "sv, "T"sv, ReplaceMode::FirstOnly); 666} 667 668// https://html.spec.whatwg.org/multipage/input.html#value-sanitization-algorithm 669DeprecatedString HTMLInputElement::value_sanitization_algorithm(DeprecatedString value) const 670{ 671 if (type_state() == HTMLInputElement::TypeAttributeState::Text || type_state() == HTMLInputElement::TypeAttributeState::Search || type_state() == HTMLInputElement::TypeAttributeState::Telephone || type_state() == HTMLInputElement::TypeAttributeState::Password) { 672 // Strip newlines from the value. 673 if (value.contains('\r') || value.contains('\n')) { 674 StringBuilder builder; 675 for (auto c : value) { 676 if (!(c == '\r' || c == '\n')) 677 builder.append(c); 678 } 679 return builder.to_deprecated_string(); 680 } 681 } else if (type_state() == HTMLInputElement::TypeAttributeState::URL) { 682 // Strip newlines from the value, then strip leading and trailing ASCII whitespace from the value. 683 if (value.contains('\r') || value.contains('\n')) { 684 StringBuilder builder; 685 for (auto c : value) { 686 if (!(c == '\r' || c == '\n')) 687 builder.append(c); 688 } 689 return builder.string_view().trim(Infra::ASCII_WHITESPACE); 690 } 691 } else if (type_state() == HTMLInputElement::TypeAttributeState::Email) { 692 // https://html.spec.whatwg.org/multipage/input.html#email-state-(type=email):value-sanitization-algorithm 693 // FIXME: handle the `multiple` attribute 694 // Strip newlines from the value, then strip leading and trailing ASCII whitespace from the value. 695 if (value.contains('\r') || value.contains('\n')) { 696 StringBuilder builder; 697 for (auto c : value) { 698 if (!(c == '\r' || c == '\n')) 699 builder.append(c); 700 } 701 return builder.string_view().trim(Infra::ASCII_WHITESPACE); 702 } 703 } else if (type_state() == HTMLInputElement::TypeAttributeState::Number) { 704 // If the value of the element is not a valid floating-point number, then set it to the empty string instead. 705 // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-floating-point-number-values 706 // 6. Skip ASCII whitespace within input given position. 707 auto maybe_double = value.to_double(TrimWhitespace::Yes); 708 if (!maybe_double.has_value() || !isfinite(maybe_double.value())) 709 return ""; 710 } else if (type_state() == HTMLInputElement::TypeAttributeState::Date) { 711 // https://html.spec.whatwg.org/multipage/input.html#date-state-(type=date):value-sanitization-algorithm 712 if (!is_valid_date_string(value)) 713 return ""; 714 } else if (type_state() == HTMLInputElement::TypeAttributeState::Month) { 715 // https://html.spec.whatwg.org/multipage/input.html#month-state-(type=month):value-sanitization-algorithm 716 if (!is_valid_month_string(value)) 717 return ""; 718 } else if (type_state() == HTMLInputElement::TypeAttributeState::Week) { 719 // https://html.spec.whatwg.org/multipage/input.html#week-state-(type=week):value-sanitization-algorithm 720 if (!is_valid_week_string(value)) 721 return ""; 722 } else if (type_state() == HTMLInputElement::TypeAttributeState::Time) { 723 // https://html.spec.whatwg.org/multipage/input.html#time-state-(type=time):value-sanitization-algorithm 724 if (!is_valid_time_string(value)) 725 return ""; 726 } else if (type_state() == HTMLInputElement::TypeAttributeState::LocalDateAndTime) { 727 // https://html.spec.whatwg.org/multipage/input.html#local-date-and-time-state-(type=datetime-local):value-sanitization-algorithm 728 if (is_valid_local_date_and_time_string(value)) 729 return normalize_local_date_and_time_string(value); 730 return ""; 731 } else if (type_state() == HTMLInputElement::TypeAttributeState::Range) { 732 // https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):value-sanitization-algorithm 733 auto maybe_double = value.to_double(TrimWhitespace::Yes); 734 if (!maybe_double.has_value() || !isfinite(maybe_double.value())) 735 return JS::number_to_deprecated_string(maybe_double.value_or(0)); 736 } else if (type_state() == HTMLInputElement::TypeAttributeState::Color) { 737 // https://html.spec.whatwg.org/multipage/input.html#color-state-(type=color):value-sanitization-algorithm 738 // If the value of the element is a valid simple color, then set it to the value of the element converted to ASCII lowercase; 739 if (is_valid_simple_color(value)) 740 return value.to_lowercase(); 741 // otherwise, set it to the string "#000000". 742 return "#000000"; 743 } 744 return value; 745} 746 747// https://html.spec.whatwg.org/multipage/input.html#the-input-element:concept-form-reset-control 748void HTMLInputElement::reset_algorithm() 749{ 750 // The reset algorithm for input elements is to set the dirty value flag and dirty checkedness flag back to false, 751 m_dirty_value = false; 752 m_dirty_checkedness = false; 753 754 // set the value of the element to the value of the value content attribute, if there is one, or the empty string otherwise, 755 m_value = has_attribute(AttributeNames::value) ? get_attribute(AttributeNames::value) : DeprecatedString::empty(); 756 757 // set the checkedness of the element to true if the element has a checked content attribute and false if it does not, 758 m_checked = has_attribute(AttributeNames::checked); 759 760 // empty the list of selected files, 761 m_selected_files = FileAPI::FileList::create(realm(), {}).release_value_but_fixme_should_propagate_errors(); 762 763 // and then invoke the value sanitization algorithm, if the type attribute's current state defines one. 764 m_value = value_sanitization_algorithm(m_value); 765 if (m_text_node) 766 m_text_node->set_data(m_value); 767} 768 769void HTMLInputElement::form_associated_element_was_inserted() 770{ 771 create_shadow_tree_if_needed(); 772} 773 774void HTMLInputElement::set_checked_within_group() 775{ 776 if (checked()) 777 return; 778 779 set_checked(true, ChangeSource::User); 780 DeprecatedString name = this->name(); 781 782 document().for_each_in_inclusive_subtree_of_type<HTML::HTMLInputElement>([&](auto& element) { 783 if (element.checked() && &element != this && element.name() == name) 784 element.set_checked(false, ChangeSource::User); 785 return IterationDecision::Continue; 786 }); 787} 788 789// https://html.spec.whatwg.org/multipage/input.html#the-input-element:legacy-pre-activation-behavior 790void HTMLInputElement::legacy_pre_activation_behavior() 791{ 792 m_before_legacy_pre_activation_behavior_checked = checked(); 793 794 // 1. If this element's type attribute is in the Checkbox state, then set 795 // this element's checkedness to its opposite value (i.e. true if it is 796 // false, false if it is true) and set this element's indeterminate IDL 797 // attribute to false. 798 // FIXME: Set indeterminate to false when that exists. 799 if (type_state() == TypeAttributeState::Checkbox) { 800 set_checked(!checked(), ChangeSource::User); 801 } 802 803 // 2. If this element's type attribute is in the Radio Button state, then 804 // get a reference to the element in this element's radio button group that 805 // has its checkedness set to true, if any, and then set this element's 806 // checkedness to true. 807 if (type_state() == TypeAttributeState::RadioButton) { 808 DeprecatedString name = this->name(); 809 810 document().for_each_in_inclusive_subtree_of_type<HTML::HTMLInputElement>([&](auto& element) { 811 if (element.checked() && element.name() == name) { 812 m_legacy_pre_activation_behavior_checked_element_in_group = &element; 813 return IterationDecision::Break; 814 } 815 return IterationDecision::Continue; 816 }); 817 818 set_checked_within_group(); 819 } 820} 821 822// https://html.spec.whatwg.org/multipage/input.html#the-input-element:legacy-canceled-activation-behavior 823void HTMLInputElement::legacy_cancelled_activation_behavior() 824{ 825 // 1. If the element's type attribute is in the Checkbox state, then set the 826 // element's checkedness and the element's indeterminate IDL attribute back 827 // to the values they had before the legacy-pre-activation behavior was run. 828 if (type_state() == TypeAttributeState::Checkbox) { 829 set_checked(m_before_legacy_pre_activation_behavior_checked, ChangeSource::Programmatic); 830 } 831 832 // 2. If this element 's type attribute is in the Radio Button state, then 833 // if the element to which a reference was obtained in the 834 // legacy-pre-activation behavior, if any, is still in what is now this 835 // element' s radio button group, if it still has one, and if so, setting 836 // that element 's checkedness to true; or else, if there was no such 837 // element, or that element is no longer in this element' s radio button 838 // group, or if this element no longer has a radio button group, setting 839 // this element's checkedness to false. 840 if (type_state() == TypeAttributeState::RadioButton) { 841 DeprecatedString name = this->name(); 842 bool did_reselect_previous_element = false; 843 if (m_legacy_pre_activation_behavior_checked_element_in_group) { 844 auto& element_in_group = *m_legacy_pre_activation_behavior_checked_element_in_group; 845 if (name == element_in_group.name()) { 846 element_in_group.set_checked_within_group(); 847 did_reselect_previous_element = true; 848 } 849 850 m_legacy_pre_activation_behavior_checked_element_in_group = nullptr; 851 } 852 853 if (!did_reselect_previous_element) 854 set_checked(false, ChangeSource::User); 855 } 856} 857 858void HTMLInputElement::legacy_cancelled_activation_behavior_was_not_called() 859{ 860 m_legacy_pre_activation_behavior_checked_element_in_group = nullptr; 861} 862 863// https://html.spec.whatwg.org/multipage/interaction.html#dom-tabindex 864i32 HTMLInputElement::default_tab_index_value() const 865{ 866 // See the base function for the spec comments. 867 return 0; 868} 869 870// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setselectionrange 871WebIDL::ExceptionOr<void> HTMLInputElement::set_selection_range(u32 start, u32 end, DeprecatedString const& direction) 872{ 873 dbgln("(STUBBED) HTMLInputElement::set_selection_range(start={}, end={}, direction='{}'). Called on: {}", start, end, direction, debug_description()); 874 return {}; 875} 876 877Optional<ARIA::Role> HTMLInputElement::default_role() const 878{ 879 // https://www.w3.org/TR/html-aria/#el-input-button 880 if (type_state() == TypeAttributeState::Button) 881 return ARIA::Role::button; 882 // https://www.w3.org/TR/html-aria/#el-input-checkbox 883 if (type_state() == TypeAttributeState::Checkbox) 884 return ARIA::Role::checkbox; 885 // https://www.w3.org/TR/html-aria/#el-input-email 886 if (type_state() == TypeAttributeState::Email && attribute("list").is_null()) 887 return ARIA::Role::textbox; 888 // https://www.w3.org/TR/html-aria/#el-input-image 889 if (type_state() == TypeAttributeState::ImageButton) 890 return ARIA::Role::button; 891 // https://www.w3.org/TR/html-aria/#el-input-number 892 if (type_state() == TypeAttributeState::Number) 893 return ARIA::Role::spinbutton; 894 // https://www.w3.org/TR/html-aria/#el-input-radio 895 if (type_state() == TypeAttributeState::RadioButton) 896 return ARIA::Role::radio; 897 // https://www.w3.org/TR/html-aria/#el-input-range 898 if (type_state() == TypeAttributeState::Range) 899 return ARIA::Role::slider; 900 // https://www.w3.org/TR/html-aria/#el-input-reset 901 if (type_state() == TypeAttributeState::ResetButton) 902 return ARIA::Role::button; 903 // https://www.w3.org/TR/html-aria/#el-input-text-list 904 if ((type_state() == TypeAttributeState::Text 905 || type_state() == TypeAttributeState::Search 906 || type_state() == TypeAttributeState::Telephone 907 || type_state() == TypeAttributeState::URL 908 || type_state() == TypeAttributeState::Email) 909 && !attribute("list").is_null()) 910 return ARIA::Role::combobox; 911 // https://www.w3.org/TR/html-aria/#el-input-search 912 if (type_state() == TypeAttributeState::Search && attribute("list").is_null()) 913 return ARIA::Role::textbox; 914 // https://www.w3.org/TR/html-aria/#el-input-submit 915 if (type_state() == TypeAttributeState::SubmitButton) 916 return ARIA::Role::button; 917 // https://www.w3.org/TR/html-aria/#el-input-tel 918 if (type_state() == TypeAttributeState::Telephone) 919 return ARIA::Role::textbox; 920 // https://www.w3.org/TR/html-aria/#el-input-text 921 if (type_state() == TypeAttributeState::Text && attribute("list").is_null()) 922 return ARIA::Role::textbox; 923 // https://www.w3.org/TR/html-aria/#el-input-url 924 if (type_state() == TypeAttributeState::URL && attribute("list").is_null()) 925 return ARIA::Role::textbox; 926 927 // https://www.w3.org/TR/html-aria/#el-input-color 928 // https://www.w3.org/TR/html-aria/#el-input-date 929 // https://www.w3.org/TR/html-aria/#el-input-datetime-local 930 // https://www.w3.org/TR/html-aria/#el-input-file 931 // https://www.w3.org/TR/html-aria/#el-input-hidden 932 // https://www.w3.org/TR/html-aria/#el-input-month 933 // https://www.w3.org/TR/html-aria/#el-input-password 934 // https://www.w3.org/TR/html-aria/#el-input-time 935 // https://www.w3.org/TR/html-aria/#el-input-week 936 return {}; 937} 938 939}