123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825 |
- /*
- * Copyright (c) 2021, Andreas Kling <andreas@ladybird.org>
- * Copyright (c) 2024, Jelle Raaijmakers <jelle@gmta.nl>
- * Copyright (c) 2024, Tim Ledbetter <tim.ledbetter@ladybird.org>
- *
- * SPDX-License-Identifier: BSD-2-Clause
- */
- #include <LibUnicode/CharacterTypes.h>
- #include <LibUnicode/Segmenter.h>
- #include <LibWeb/DOM/Document.h>
- #include <LibWeb/DOM/Event.h>
- #include <LibWeb/DOM/Position.h>
- #include <LibWeb/HTML/FormAssociatedElement.h>
- #include <LibWeb/HTML/HTMLButtonElement.h>
- #include <LibWeb/HTML/HTMLFieldSetElement.h>
- #include <LibWeb/HTML/HTMLFormElement.h>
- #include <LibWeb/HTML/HTMLInputElement.h>
- #include <LibWeb/HTML/HTMLLegendElement.h>
- #include <LibWeb/HTML/HTMLSelectElement.h>
- #include <LibWeb/HTML/HTMLTextAreaElement.h>
- #include <LibWeb/HTML/Parser/HTMLParser.h>
- #include <LibWeb/Painting/Paintable.h>
- namespace Web::HTML {
- static SelectionDirection string_to_selection_direction(Optional<String> value)
- {
- if (!value.has_value())
- return SelectionDirection::None;
- if (value.value() == "forward"sv)
- return SelectionDirection::Forward;
- if (value.value() == "backward"sv)
- return SelectionDirection::Backward;
- return SelectionDirection::None;
- }
- void FormAssociatedElement::set_form(HTMLFormElement* form)
- {
- if (m_form)
- m_form->remove_associated_element({}, form_associated_element_to_html_element());
- m_form = form;
- if (m_form)
- m_form->add_associated_element({}, form_associated_element_to_html_element());
- }
- bool FormAssociatedElement::enabled() const
- {
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fe-disabled
- auto const& html_element = form_associated_element_to_html_element();
- // A form control is disabled if any of the following conditions are met:
- // 1. The element is a button, input, select, textarea, or form-associated custom element, and the disabled attribute is specified on this element (regardless of its value).
- // FIXME: This doesn't check for form-associated custom elements.
- if ((is<HTMLButtonElement>(html_element) || is<HTMLInputElement>(html_element) || is<HTMLSelectElement>(html_element) || is<HTMLTextAreaElement>(html_element)) && html_element.has_attribute(HTML::AttributeNames::disabled))
- return false;
- // 2. The element is a descendant of a fieldset element whose disabled attribute is specified, and is not a descendant of that fieldset element's first legend element child, if any.
- for (auto* fieldset_ancestor = html_element.first_ancestor_of_type<HTMLFieldSetElement>(); fieldset_ancestor; fieldset_ancestor = fieldset_ancestor->first_ancestor_of_type<HTMLFieldSetElement>()) {
- if (fieldset_ancestor->has_attribute(HTML::AttributeNames::disabled)) {
- auto* first_legend_element_child = fieldset_ancestor->first_child_of_type<HTMLLegendElement>();
- if (!first_legend_element_child || !html_element.is_descendant_of(*first_legend_element_child))
- return false;
- }
- }
- return true;
- }
- void FormAssociatedElement::set_parser_inserted(Badge<HTMLParser>)
- {
- m_parser_inserted = true;
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:nodes-are-inserted
- void FormAssociatedElement::form_node_was_inserted()
- {
- // 1. If the form-associated element's parser inserted flag is set, then return.
- if (m_parser_inserted)
- return;
- // 2. Reset the form owner of the form-associated element.
- reset_form_owner();
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:nodes-are-removed
- void FormAssociatedElement::form_node_was_removed()
- {
- // 1. If the form-associated element has a form owner and the form-associated element and its form owner are no longer in the same tree, then reset the form owner of the form-associated element.
- if (m_form && &form_associated_element_to_html_element().root() != &m_form->root())
- reset_form_owner();
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:category-listed-3
- void FormAssociatedElement::form_node_attribute_changed(FlyString const& name, Optional<String> const& value)
- {
- // When a listed form-associated element's form attribute is set, changed, or removed, then the user agent must
- // reset the form owner of that element.
- if (name == HTML::AttributeNames::form) {
- auto& html_element = form_associated_element_to_html_element();
- if (value.has_value())
- html_element.document().add_form_associated_element_with_form_attribute(*this);
- else
- html_element.document().remove_form_associated_element_with_form_attribute(*this);
- reset_form_owner();
- }
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:category-listed-4
- void FormAssociatedElement::element_id_changed(Badge<DOM::Document>)
- {
- // When a listed form-associated element has a form attribute and the ID of any of the elements in the tree changes,
- // then the user agent must reset the form owner of that form-associated element.
- VERIFY(form_associated_element_to_html_element().has_attribute(HTML::AttributeNames::form));
- reset_form_owner();
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:category-listed-5
- void FormAssociatedElement::element_with_id_was_added_or_removed(Badge<DOM::Document>)
- {
- // When a listed form-associated element has a form attribute and an element with an ID is inserted into or removed
- // from the Document, then the user agent must reset the form owner of that form-associated element.
- VERIFY(form_associated_element_to_html_element().has_attribute(HTML::AttributeNames::form));
- reset_form_owner();
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#reset-the-form-owner
- void FormAssociatedElement::reset_form_owner()
- {
- auto& html_element = form_associated_element_to_html_element();
- // 1. Unset element's parser inserted flag.
- m_parser_inserted = false;
- // 2. If all of the following conditions are true
- // - element's form owner is not null
- // - element is not listed or its form content attribute is not present
- // - element's form owner is its nearest form element ancestor after the change to the ancestor chain
- // then do nothing, and return.
- if (m_form
- && (!is_listed() || !html_element.has_attribute(HTML::AttributeNames::form))
- && html_element.first_ancestor_of_type<HTMLFormElement>() == m_form.ptr()) {
- return;
- }
- // 3. Set element's form owner to null.
- set_form(nullptr);
- // 4. If element is listed, has a form content attribute, and is connected, then:
- if (is_listed() && html_element.has_attribute(HTML::AttributeNames::form) && html_element.is_connected()) {
- // 1. If the first element in element's tree, in tree order, to have an ID that is identical to element's form content attribute's value, is a form element, then associate the element with that form element.
- auto form_value = html_element.attribute(HTML::AttributeNames::form);
- html_element.root().for_each_in_inclusive_subtree_of_type<HTMLFormElement>([this, &form_value](HTMLFormElement& form_element) {
- if (form_element.id() == form_value) {
- set_form(&form_element);
- return TraversalDecision::Break;
- }
- return TraversalDecision::Continue;
- });
- }
- // 5. Otherwise, if element has an ancestor form element, then associate element with the nearest such ancestor form element.
- else {
- auto* form_ancestor = html_element.first_ancestor_of_type<HTMLFormElement>();
- if (form_ancestor)
- set_form(form_ancestor);
- }
- }
- // https://w3c.github.io/webdriver/#dfn-clear-algorithm
- void FormAssociatedElement::clear_algorithm()
- {
- // When the clear algorithm is invoked for an element that does not define its own clear algorithm, its reset
- // algorithm must be invoked instead.
- reset_algorithm();
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-fs-formaction
- String FormAssociatedElement::form_action() const
- {
- // The formAction IDL attribute must reflect the formaction content attribute, except that on getting, when the content attribute is missing or its value is the empty string,
- // the element's node document's URL must be returned instead.
- auto& html_element = form_associated_element_to_html_element();
- auto form_action_attribute = html_element.attribute(HTML::AttributeNames::formaction);
- if (!form_action_attribute.has_value() || form_action_attribute.value().is_empty()) {
- return html_element.document().url_string();
- }
- auto document_base_url = html_element.document().base_url();
- return MUST(document_base_url.complete_url(form_action_attribute.value()).to_string());
- }
- WebIDL::ExceptionOr<void> FormAssociatedElement::set_form_action(String const& value)
- {
- auto& html_element = form_associated_element_to_html_element();
- return html_element.set_attribute(HTML::AttributeNames::formaction, value);
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
- void FormAssociatedTextControlElement::relevant_value_was_changed()
- {
- auto the_relevant_value = relevant_value();
- auto relevant_value_length = the_relevant_value.code_points().length();
- // 1. If the element has a selection:
- if (m_selection_start < m_selection_end) {
- // 1. If the start of the selection is now past the end of the relevant value, set it to
- // the end of the relevant value.
- if (m_selection_start > relevant_value_length)
- m_selection_start = relevant_value_length;
- // 2. If the end of the selection is now past the end of the relevant value, set it to the
- // end of the relevant value.
- if (m_selection_end > relevant_value_length)
- m_selection_end = relevant_value_length;
- // 3. If the user agent does not support empty selection, and both the start and end of the
- // selection are now pointing to the end of the relevant value, then instead set the
- // element's text entry cursor position to the end of the relevant value, removing any
- // selection.
- // NOTE: We support empty selections.
- return;
- }
- // 2. Otherwise, the element must have a text entry cursor position position. If it is now past
- // the end of the relevant value, set it to the end of the relevant value.
- if (m_selection_start > relevant_value_length)
- m_selection_start = relevant_value_length;
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-select
- WebIDL::ExceptionOr<void> FormAssociatedTextControlElement::select()
- {
- // 1. If this element is an input element, and either select() does not apply to this element
- // or the corresponding control has no selectable text, return.
- auto& html_element = form_associated_element_to_html_element();
- if (is<HTMLInputElement>(html_element)) {
- auto& input_element = static_cast<HTMLInputElement&>(html_element);
- if (!input_element.select_applies() || !input_element.has_selectable_text())
- return {};
- }
- // 2. Set the selection range with 0 and infinity.
- set_the_selection_range(0, NumericLimits<WebIDL::UnsignedLong>::max());
- return {};
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionstart
- Optional<WebIDL::UnsignedLong> FormAssociatedTextControlElement::selection_start() const
- {
- // 1. If this element is an input element, and selectionStart does not apply to this element, return null.
- auto const& html_element = form_associated_element_to_html_element();
- if (is<HTMLInputElement>(html_element)) {
- auto const& input_element = static_cast<HTMLInputElement const&>(html_element);
- if (!input_element.selection_or_range_applies())
- return {};
- }
- // 2. If there is no selection, return the code unit offset within the relevant value to the character that
- // immediately follows the text entry cursor.
- if (m_selection_start == m_selection_end) {
- return m_selection_start;
- }
- // 3. Return the code unit offset within the relevant value to the character that immediately follows the start of
- // the selection.
- return m_selection_start < m_selection_end ? m_selection_start : m_selection_end;
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection:dom-textarea/input-selectionstart-2
- WebIDL::ExceptionOr<void> FormAssociatedTextControlElement::set_selection_start(Optional<WebIDL::UnsignedLong> const& value)
- {
- // 1. If this element is an input element, and selectionStart does not apply to this element,
- // throw an "InvalidStateError" DOMException.
- auto& html_element = form_associated_element_to_html_element();
- if (is<HTMLInputElement>(html_element)) {
- auto& input_element = static_cast<HTMLInputElement&>(html_element);
- if (!input_element.selection_or_range_applies())
- return WebIDL::InvalidStateError::create(html_element.realm(), "setSelectionStart does not apply to this input type"_string);
- }
- // 2. Let end be the value of this element's selectionEnd attribute.
- auto end = m_selection_end;
- // 3. If end is less than the given value, set end to the given value.
- if (value.has_value() && end < value.value())
- end = value.value();
- // 4. Set the selection range with the given value, end, and the value of this element's
- // selectionDirection attribute.
- set_the_selection_range(value, end, selection_direction_state());
- return {};
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionend
- Optional<WebIDL::UnsignedLong> FormAssociatedTextControlElement::selection_end() const
- {
- // 1. If this element is an input element, and selectionEnd does not apply to this element, return
- // null.
- auto const& html_element = form_associated_element_to_html_element();
- if (is<HTMLInputElement>(html_element)) {
- auto const& input_element = static_cast<HTMLInputElement const&>(html_element);
- if (!input_element.selection_or_range_applies())
- return {};
- }
- // 2. If there is no selection, return the code unit offset within the relevant value to the
- // character that immediately follows the text entry cursor.
- if (m_selection_start == m_selection_end) {
- return m_selection_start;
- }
- // 3. Return the code unit offset within the relevant value to the character that immediately
- // follows the end of the selection.
- return m_selection_start < m_selection_end ? m_selection_end : m_selection_start;
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection:dom-textarea/input-selectionend-3
- WebIDL::ExceptionOr<void> FormAssociatedTextControlElement::set_selection_end(Optional<WebIDL::UnsignedLong> const& value)
- {
- // 1. If this element is an input element, and selectionEnd does not apply to this element,
- // throw an "InvalidStateError" DOMException.
- auto& html_element = form_associated_element_to_html_element();
- if (is<HTMLInputElement>(html_element)) {
- auto& input_element = static_cast<HTMLInputElement&>(html_element);
- if (!input_element.selection_or_range_applies())
- return WebIDL::InvalidStateError::create(html_element.realm(), "setSelectionEnd does not apply to this input type"_string);
- }
- // 2. Set the selection range with the value of this element's selectionStart attribute, the
- // given value, and the value of this element's selectionDirection attribute.
- set_the_selection_range(m_selection_start, value, selection_direction_state());
- return {};
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#selection-direction
- Optional<String> FormAssociatedTextControlElement::selection_direction() const
- {
- // 1. If this element is an input element, and selectionDirection does not apply to this
- // element, return null.
- auto const& html_element = form_associated_element_to_html_element();
- if (is<HTMLInputElement>(html_element)) {
- auto const& input_element = static_cast<HTMLInputElement const&>(html_element);
- if (!input_element.selection_or_range_applies())
- return {};
- }
- // 2. Return this element's selection direction.
- switch (m_selection_direction) {
- case SelectionDirection::Forward:
- return "forward"_string;
- case SelectionDirection::Backward:
- return "backward"_string;
- case SelectionDirection::None:
- return "none"_string;
- default:
- VERIFY_NOT_REACHED();
- }
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#set-the-selection-direction
- void FormAssociatedTextControlElement::set_selection_direction(Optional<String> direction)
- {
- // To set the selection direction of an element to a given direction, update the element's
- // selection direction to the given direction, unless the direction is "none" and the
- // platform does not support that direction; in that case, update the element's selection
- // direction to "forward".
- m_selection_direction = string_to_selection_direction(direction);
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectiondirection
- WebIDL::ExceptionOr<void> FormAssociatedTextControlElement::set_selection_direction_binding(Optional<String> direction)
- {
- // 1. If this element is an input element, and selectionDirection does not apply to this element,
- // throw an "InvalidStateError" DOMException.
- auto const& html_element = form_associated_element_to_html_element();
- if (is<HTMLInputElement>(html_element)) {
- auto const& input_element = static_cast<HTMLInputElement const&>(html_element);
- if (!input_element.selection_direction_applies())
- return WebIDL::InvalidStateError::create(input_element.realm(), "selectionDirection does not apply to element"_string);
- }
- set_the_selection_range(m_selection_start, m_selection_end, string_to_selection_direction(direction));
- return {};
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setrangetext
- WebIDL::ExceptionOr<void> FormAssociatedTextControlElement::set_range_text(String const& replacement)
- {
- return set_range_text(replacement, m_selection_start, m_selection_end);
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setrangetext
- WebIDL::ExceptionOr<void> FormAssociatedTextControlElement::set_range_text(String const& replacement, WebIDL::UnsignedLong start, WebIDL::UnsignedLong end, Bindings::SelectionMode selection_mode)
- {
- // 1. If this element is an input element, and setRangeText() does not apply to this element,
- // throw an "InvalidStateError" DOMException.
- auto& html_element = form_associated_element_to_html_element();
- if (is<HTMLInputElement>(html_element) && !static_cast<HTMLInputElement&>(html_element).selection_or_range_applies())
- return WebIDL::InvalidStateError::create(html_element.realm(), "setRangeText does not apply to this input type"_string);
- // 2. Set this element's dirty value flag to true.
- set_dirty_value_flag(true);
- // 3. If the method has only one argument, then let start and end have the values of the selectionStart attribute and the selectionEnd attribute respectively.
- // Otherwise, let start, end have the values of the second and third arguments respectively.
- // NOTE: This is handled by the caller.
- // 4. If start is greater than end, then throw an "IndexSizeError" DOMException.
- if (start > end)
- return WebIDL::IndexSizeError::create(html_element.realm(), "The start argument must be less than or equal to the end argument"_string);
- // 5. If start is greater than the length of the relevant value of the text control, then set it to the length of the relevant value of the text control.
- auto the_relevant_value = relevant_value();
- auto relevant_value_length = the_relevant_value.code_points().length();
- if (start > relevant_value_length)
- start = relevant_value_length;
- // 6. If end is greater than the length of the relevant value of the text control, then set it to the length of the relevant value of the text control.
- if (end > relevant_value_length)
- end = relevant_value_length;
- // 7. Let selection start be the current value of the selectionStart attribute.
- auto selection_start = m_selection_start;
- // 8. Let selection end be the current value of the selectionEnd attribute.
- auto selection_end = m_selection_end;
- // 9. If start is less than end, delete the sequence of code units within the element's relevant value starting with
- // the code unit at the startth position and ending with the code unit at the (end-1)th position.
- if (start < end) {
- StringBuilder builder;
- auto before_removal_point_view = the_relevant_value.code_points().unicode_substring_view(0, start);
- builder.append(before_removal_point_view.as_string());
- auto after_removal_point_view = the_relevant_value.code_points().unicode_substring_view(end);
- builder.append(after_removal_point_view.as_string());
- the_relevant_value = MUST(builder.to_string());
- }
- // 10. Insert the value of the first argument into the text of the relevant value of the text control, immediately before the startth code unit.
- StringBuilder builder;
- auto before_insertion_point_view = the_relevant_value.code_points().unicode_substring_view(0, start);
- builder.append(before_insertion_point_view.as_string());
- builder.append(replacement);
- auto after_insertion_point_view = the_relevant_value.code_points().unicode_substring_view(start);
- builder.append(after_insertion_point_view.as_string());
- the_relevant_value = MUST(builder.to_string());
- TRY(set_relevant_value(the_relevant_value));
- // 11. Let new length be the length of the value of the first argument.
- i64 new_length = replacement.code_points().length();
- // 12. Let new end be the sum of start and new length.
- auto new_end = start + new_length;
- // 13. Run the appropriate set of substeps from the following list:
- switch (selection_mode) {
- // If the fourth argument's value is "select"
- case Bindings::SelectionMode::Select:
- // Let selection start be start.
- selection_start = start;
- // Let selection end be new end.
- selection_end = new_end;
- break;
- // If the fourth argument's value is "start"
- case Bindings::SelectionMode::Start:
- // Let selection start and selection end be start.
- selection_start = start;
- selection_end = start;
- break;
- // If the fourth argument's value is "end"
- case Bindings::SelectionMode::End:
- selection_start = new_end;
- selection_end = new_end;
- break;
- // If the fourth argument's value is "preserve"
- case Bindings::SelectionMode::Preserve:
- // 1. Let old length be end minus start.
- auto old_length = end - start;
- // 2. Let delta be new length minus old length.
- auto delta = new_length - old_length;
- // 3. If selection start is greater than end, then increment it by delta.
- // (If delta is negative, i.e. the new text is shorter than the old text, then this will decrease the value of selection start.)
- // Otherwise: if selection start is greater than start, then set it to start.
- // (This snaps the start of the selection to the start of the new text if it was in the middle of the text that it replaced.)
- if (selection_start > end)
- selection_start += delta;
- else if (selection_start > start)
- selection_start = start;
- // 4. If selection end is greater than end, then increment it by delta in the same way.
- // Otherwise: if selection end is greater than start, then set it to new end.
- // (This snaps the end of the selection to the end of the new text if it was in the middle of the text that it replaced.)
- if (selection_end > end)
- selection_end += delta;
- else if (selection_end > start)
- selection_end = new_end;
- break;
- }
- // 14. Set the selection range with selection start and selection end.
- set_the_selection_range(selection_start, selection_end);
- return {};
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setselectionrange
- WebIDL::ExceptionOr<void> FormAssociatedTextControlElement::set_selection_range(Optional<WebIDL::UnsignedLong> start, Optional<WebIDL::UnsignedLong> end, Optional<String> direction)
- {
- // 1. If this element is an input element, and setSelectionRange() does not apply to this
- // element, throw an "InvalidStateError" DOMException.
- auto& html_element = form_associated_element_to_html_element();
- if (is<HTMLInputElement>(html_element) && !static_cast<HTMLInputElement&>(html_element).selection_or_range_applies())
- return WebIDL::InvalidStateError::create(html_element.realm(), "setSelectionRange does not apply to this input type"_string);
- // 2. Set the selection range with start, end, and direction.
- set_the_selection_range(start, end, string_to_selection_direction(direction));
- return {};
- }
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#set-the-selection-range
- void FormAssociatedTextControlElement::set_the_selection_range(Optional<WebIDL::UnsignedLong> start, Optional<WebIDL::UnsignedLong> end, SelectionDirection direction, SelectionSource source)
- {
- // 1. If start is null, let start be zero.
- start = start.value_or(0);
- // 2. If end is null, let end be zero.
- end = end.value_or(0);
- // 3. Set the selection of the text control to the sequence of code units within the relevant
- // value starting with the code unit at the startth position (in logical order) and ending
- // with the code unit at the (end-1)th position. Arguments greater than the length of the
- // relevant value of the text control (including the special value infinity) must be treated
- // as pointing at the end of the text control.
- auto the_relevant_value = relevant_value();
- auto relevant_value_length = the_relevant_value.code_points().length();
- auto new_selection_start = AK::min(start.value(), relevant_value_length);
- auto new_selection_end = AK::min(end.value(), relevant_value_length);
- // If end is less than or equal to start then the start of the selection and the end of the
- // selection must both be placed immediately before the character with offset end. In UAs
- // where there is no concept of an empty selection, this must set the cursor to be just
- // before the character with offset end.
- new_selection_start = AK::min(new_selection_start, new_selection_end);
- bool was_modified = m_selection_start != new_selection_start || m_selection_end != new_selection_end;
- m_selection_start = new_selection_start;
- m_selection_end = new_selection_end;
- // 4. If direction is not identical to either "backward" or "forward", or if the direction
- // argument was not given, set direction to "none".
- // NOTE: This is handled by the argument's default value and ::string_to_selection_direction().
- // 5. Set the selection direction of the text control to direction.
- was_modified |= m_selection_direction != direction;
- m_selection_direction = direction;
- // 6. If the previous steps caused the selection of the text control to be modified (in either
- // extent or direction), then queue an element task on the user interaction task source
- // given the element to fire an event named select at the element, with the bubbles attribute
- // initialized to true.
- if (was_modified) {
- auto& html_element = form_associated_element_to_html_element();
- // AD-HOC: We don't fire the event if the user moves the cursor without selecting any text.
- // This is not in the spec but matches how other browsers behave.
- if (source == SelectionSource::DOM || m_selection_start != m_selection_end) {
- html_element.queue_an_element_task(Task::Source::UserInteraction, [&html_element] {
- auto select_event = DOM::Event::create(html_element.realm(), EventNames::select, { .bubbles = true });
- static_cast<DOM::EventTarget*>(&html_element)->dispatch_event(select_event);
- });
- }
- // AD-HOC: Notify the element that the selection was changed, so it can perform
- // element-specific updates.
- selection_was_changed(m_selection_start, m_selection_end);
- }
- }
- void FormAssociatedTextControlElement::handle_insert(String const& data)
- {
- auto text_node = form_associated_element_to_text_node();
- if (!text_node || !text_node->is_editable())
- return;
- String data_for_insertion = data;
- if (auto max_length = text_node->max_length(); max_length.has_value()) {
- auto remaining_length = *max_length - text_node->data().code_points().length();
- if (remaining_length < data.code_points().length()) {
- data_for_insertion = MUST(data.substring_from_byte_offset(0, remaining_length));
- }
- }
- auto selection_start = this->selection_start();
- auto selection_end = this->selection_end();
- if (!selection_start.has_value() || !selection_end.has_value()) {
- return;
- }
- MUST(set_range_text(data_for_insertion, selection_start.value(), selection_end.value(), Bindings::SelectionMode::End));
- text_node->invalidate_style(DOM::StyleInvalidationReason::EditingInsertion);
- text_node->editable_text_node_owner()->did_edit_text_node();
- }
- void FormAssociatedTextControlElement::handle_delete(DeleteDirection direction)
- {
- auto text_node = form_associated_element_to_text_node();
- if (!text_node || !text_node->is_editable())
- return;
- auto selection_start = this->selection_start();
- auto selection_end = this->selection_end();
- if (!selection_start.has_value() || !selection_end.has_value()) {
- return;
- }
- if (selection_start == selection_end) {
- if (direction == DeleteDirection::Backward) {
- if (selection_start.value() > 0) {
- MUST(set_range_text(MUST(String::from_utf8(""sv)), selection_start.value() - 1, selection_end.value(), Bindings::SelectionMode::End));
- }
- } else {
- if (selection_start.value() < text_node->data().code_points().length()) {
- MUST(set_range_text(MUST(String::from_utf8(""sv)), selection_start.value(), selection_end.value() + 1, Bindings::SelectionMode::End));
- }
- }
- return;
- }
- MUST(set_range_text(MUST(String::from_utf8(""sv)), selection_start.value(), selection_end.value(), Bindings::SelectionMode::End));
- }
- void FormAssociatedTextControlElement::handle_return_key()
- {
- auto& html_element = form_associated_element_to_html_element();
- if (is<HTMLInputElement>(html_element)) {
- auto& input_element = static_cast<HTMLInputElement&>(html_element);
- if (auto* form = input_element.form()) {
- form->implicitly_submit_form().release_value_but_fixme_should_propagate_errors();
- return;
- }
- input_element.commit_pending_changes();
- }
- }
- void FormAssociatedTextControlElement::collapse_selection_to_offset(size_t position)
- {
- m_selection_start = position;
- m_selection_end = position;
- }
- void FormAssociatedTextControlElement::selection_was_changed()
- {
- auto text_node = form_associated_element_to_text_node();
- if (!text_node)
- return;
- auto* text_paintable = text_node->paintable();
- if (!text_paintable)
- return;
- if (m_selection_start == m_selection_end) {
- text_paintable->set_selected(false);
- text_paintable->set_selection_state(Painting::Paintable::SelectionState::None);
- text_node->document().reset_cursor_blink_cycle();
- } else {
- text_paintable->set_selected(true);
- text_paintable->set_selection_state(Painting::Paintable::SelectionState::StartAndEnd);
- }
- text_paintable->set_needs_display();
- }
- void FormAssociatedTextControlElement::select_all()
- {
- auto text_node = form_associated_element_to_text_node();
- if (!text_node)
- return;
- set_the_selection_range(0, text_node->length());
- selection_was_changed();
- }
- void FormAssociatedTextControlElement::set_selection_anchor(JS::NonnullGCPtr<DOM::Node> anchor_node, size_t anchor_offset)
- {
- auto text_node = form_associated_element_to_text_node();
- if (!text_node || anchor_node != text_node)
- return;
- collapse_selection_to_offset(anchor_offset);
- selection_was_changed();
- }
- void FormAssociatedTextControlElement::set_selection_focus(JS::NonnullGCPtr<DOM::Node> focus_node, size_t focus_offset)
- {
- auto text_node = form_associated_element_to_text_node();
- if (!text_node || focus_node != text_node)
- return;
- m_selection_end = focus_offset;
- selection_was_changed();
- }
- void FormAssociatedTextControlElement::move_cursor_to_start(CollapseSelection collapse)
- {
- auto text_node = form_associated_element_to_text_node();
- if (!text_node)
- return;
- if (collapse == CollapseSelection::Yes) {
- collapse_selection_to_offset(0);
- } else {
- m_selection_end = 0;
- }
- selection_was_changed();
- }
- void FormAssociatedTextControlElement::move_cursor_to_end(CollapseSelection collapse)
- {
- auto text_node = form_associated_element_to_text_node();
- if (!text_node)
- return;
- if (collapse == CollapseSelection::Yes) {
- collapse_selection_to_offset(text_node->length());
- } else {
- m_selection_end = text_node->length();
- }
- selection_was_changed();
- }
- void FormAssociatedTextControlElement::increment_cursor_position_offset(CollapseSelection collapse)
- {
- auto const text_node = form_associated_element_to_text_node();
- if (!text_node)
- return;
- if (auto offset = text_node->grapheme_segmenter().next_boundary(m_selection_end); offset.has_value()) {
- if (collapse == CollapseSelection::Yes) {
- collapse_selection_to_offset(*offset);
- } else {
- m_selection_end = *offset;
- }
- }
- selection_was_changed();
- }
- void FormAssociatedTextControlElement::decrement_cursor_position_offset(CollapseSelection collapse)
- {
- auto const text_node = form_associated_element_to_text_node();
- if (!text_node)
- return;
- if (auto offset = text_node->grapheme_segmenter().previous_boundary(m_selection_end); offset.has_value()) {
- if (collapse == CollapseSelection::Yes) {
- collapse_selection_to_offset(*offset);
- } else {
- m_selection_end = *offset;
- }
- }
- selection_was_changed();
- }
- static bool should_continue_beyond_word(Utf8View const& word)
- {
- for (auto code_point : word) {
- if (!Unicode::code_point_has_punctuation_general_category(code_point) && !Unicode::code_point_has_separator_general_category(code_point))
- return false;
- }
- return true;
- }
- void FormAssociatedTextControlElement::increment_cursor_position_to_next_word(CollapseSelection collapse)
- {
- auto const text_node = form_associated_element_to_text_node();
- if (!text_node)
- return;
- while (true) {
- if (auto offset = text_node->word_segmenter().next_boundary(m_selection_end); offset.has_value()) {
- auto word = text_node->data().code_points().substring_view(m_selection_end, *offset - m_selection_end);
- if (collapse == CollapseSelection::Yes) {
- collapse_selection_to_offset(*offset);
- } else {
- m_selection_end = *offset;
- }
- if (should_continue_beyond_word(word))
- continue;
- }
- break;
- }
- selection_was_changed();
- }
- void FormAssociatedTextControlElement::decrement_cursor_position_to_previous_word(CollapseSelection collapse)
- {
- auto const text_node = form_associated_element_to_text_node();
- if (!text_node)
- return;
- while (true) {
- if (auto offset = text_node->word_segmenter().previous_boundary(m_selection_end); offset.has_value()) {
- auto word = text_node->data().code_points().substring_view(m_selection_end, m_selection_end - *offset);
- if (collapse == CollapseSelection::Yes) {
- collapse_selection_to_offset(*offset);
- } else {
- m_selection_end = *offset;
- }
- if (should_continue_beyond_word(word))
- continue;
- }
- break;
- }
- selection_was_changed();
- }
- JS::GCPtr<DOM::Position> FormAssociatedTextControlElement::cursor_position() const
- {
- auto const node = form_associated_element_to_text_node();
- if (!node)
- return nullptr;
- if (m_selection_start == m_selection_end)
- return DOM::Position::create(node->realm(), const_cast<DOM::Text&>(*node), m_selection_start);
- return nullptr;
- }
- }
|