|
@@ -1,10 +1,12 @@
|
|
|
/*
|
|
|
* Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
|
|
|
+ * Copyright (c) 2024, Jelle Raaijmakers <jelle@gmta.nl>
|
|
|
*
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
*/
|
|
|
|
|
|
#include <LibWeb/DOM/Document.h>
|
|
|
+#include <LibWeb/DOM/Event.h>
|
|
|
#include <LibWeb/HTML/FormAssociatedElement.h>
|
|
|
#include <LibWeb/HTML/HTMLButtonElement.h>
|
|
|
#include <LibWeb/HTML/HTMLFieldSetElement.h>
|
|
@@ -17,6 +19,17 @@
|
|
|
|
|
|
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)
|
|
@@ -152,61 +165,261 @@ void FormAssociatedElement::reset_form_owner()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
|
|
|
+String FormAssociatedElement::relevant_value() const
|
|
|
+{
|
|
|
+ auto const& html_element = form_associated_element_to_html_element();
|
|
|
+ if (is<HTMLInputElement>(html_element))
|
|
|
+ return static_cast<HTMLInputElement const&>(html_element).value();
|
|
|
+ if (is<HTMLTextAreaElement>(html_element))
|
|
|
+ return static_cast<HTMLTextAreaElement const&>(html_element).api_value();
|
|
|
+ VERIFY_NOT_REACHED();
|
|
|
+}
|
|
|
+
|
|
|
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
|
|
|
+void FormAssociatedElement::relevant_value_was_changed(JS::GCPtr<DOM::Text> text_node)
|
|
|
+{
|
|
|
+ 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.
|
|
|
+ auto& document = form_associated_element_to_html_element().document();
|
|
|
+ auto const current_cursor_position = document.cursor_position();
|
|
|
+ if (current_cursor_position && text_node
|
|
|
+ && current_cursor_position->node() == text_node
|
|
|
+ && current_cursor_position->offset() > relevant_value_length) {
|
|
|
+ document.set_cursor_position(DOM::Position::create(document.realm(), *text_node, relevant_value_length));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-select
|
|
|
+WebIDL::ExceptionOr<void> FormAssociatedElement::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);
|
|
|
+ // FIXME: implement "or the corresponding control has no selectable text"
|
|
|
+ if (!input_element.select_applies())
|
|
|
+ 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
|
|
|
-WebIDL::UnsignedLong FormAssociatedElement::selection_start() const
|
|
|
+Optional<WebIDL::UnsignedLong> FormAssociatedElement::selection_start() const
|
|
|
{
|
|
|
// 1. If this element is an input element, and selectionStart does not apply to this element, return null.
|
|
|
- // NOTE: This is done by HTMLInputElement before calling this function
|
|
|
+ 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 (auto cursor = form_associated_element_to_html_element().document().cursor_position())
|
|
|
- return cursor->offset();
|
|
|
+ if (m_selection_start == m_selection_end) {
|
|
|
+ if (auto cursor = form_associated_element_to_html_element().document().cursor_position())
|
|
|
+ return cursor->offset();
|
|
|
+ }
|
|
|
|
|
|
- // FIXME: 3. Return the code unit offset within the relevant value to the character that immediately follows the start of
|
|
|
- // the selection.
|
|
|
- return 0;
|
|
|
+ // 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;
|
|
|
}
|
|
|
|
|
|
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection:dom-textarea/input-selectionstart-2
|
|
|
-WebIDL::ExceptionOr<void> FormAssociatedElement::set_selection_start(Optional<WebIDL::UnsignedLong> const&)
|
|
|
+WebIDL::ExceptionOr<void> FormAssociatedElement::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.
|
|
|
- // NOTE: This is done by HTMLInputElement before calling this function
|
|
|
+ // 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"_fly_string);
|
|
|
+ }
|
|
|
|
|
|
- // FIXME: 2. Let end be the value of this element's selectionEnd attribute.
|
|
|
- // FIXME: 3. If end is less than the given value, set end to the given value.
|
|
|
- // FIXME: 4. Set the selection range with the given value, end, and the value of this element's selectionDirection attribute.
|
|
|
+ // 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
|
|
|
-WebIDL::UnsignedLong FormAssociatedElement::selection_end() const
|
|
|
+Optional<WebIDL::UnsignedLong> FormAssociatedElement::selection_end() const
|
|
|
{
|
|
|
- // 1. If this element is an input element, and selectionEnd does not apply to this element, return null.
|
|
|
- // NOTE: This is done by HTMLInputElement before calling this function
|
|
|
+ // 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 (auto cursor = form_associated_element_to_html_element().document().cursor_position())
|
|
|
- return cursor->offset();
|
|
|
+ // 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) {
|
|
|
+ if (auto cursor = form_associated_element_to_html_element().document().cursor_position())
|
|
|
+ return cursor->offset();
|
|
|
+ }
|
|
|
|
|
|
- // FIXME: 3. Return the code unit offset within the relevant value to the character that immediately follows the end of
|
|
|
- // the selection.
|
|
|
- return 0;
|
|
|
+ // 3. Return the code unit offset within the relevant value to the character that immediately
|
|
|
+ // follows the end of the selection.
|
|
|
+ return m_selection_end;
|
|
|
}
|
|
|
|
|
|
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection:dom-textarea/input-selectionend-3
|
|
|
-WebIDL::ExceptionOr<void> FormAssociatedElement::set_selection_end(Optional<WebIDL::UnsignedLong> const&)
|
|
|
+WebIDL::ExceptionOr<void> FormAssociatedElement::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"_fly_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> FormAssociatedElement::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 FormAssociatedElement::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-setselectionrange
|
|
|
+WebIDL::ExceptionOr<void> FormAssociatedElement::set_selection_range(Optional<WebIDL::UnsignedLong> start, Optional<WebIDL::UnsignedLong> end, Optional<String> direction)
|
|
|
{
|
|
|
- // 1. If this element is an input element, and selectionEnd does not apply to this element, throw an
|
|
|
- // "InvalidStateError" DOMException.
|
|
|
- // NOTE: This is done by HTMLInputElement before calling this function
|
|
|
+ // 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"_fly_string);
|
|
|
|
|
|
- // FIXME: 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.
|
|
|
+ // 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 FormAssociatedElement::set_the_selection_range(Optional<WebIDL::UnsignedLong> start, Optional<WebIDL::UnsignedLong> end, SelectionDirection direction)
|
|
|
+{
|
|
|
+ // 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.
|
|
|
+ // AD-HOC: If there is no selection, we do not fire the event. This seems to correspond to how
|
|
|
+ // other browsers behave.
|
|
|
+ if (was_modified && m_selection_start != m_selection_end) {
|
|
|
+ auto& html_element = form_associated_element_to_html_element();
|
|
|
+ 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);
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
}
|