Serenity Operating System
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}