Skip to content

Commit

Permalink
LibWeb: Implement input/textarea selection APIs
Browse files Browse the repository at this point in the history
For both types of elements, `.selectionStart`, `.selectionEnd`,
`.selectionDirection`, `.setSelectionRange()` and `.select()` are now
implemented.
  • Loading branch information
gmta committed Aug 22, 2024
1 parent cc55c9e commit b613536
Show file tree
Hide file tree
Showing 8 changed files with 356 additions and 112 deletions.
257 changes: 226 additions & 31 deletions Userland/Libraries/LibWeb/HTML/FormAssociatedElement.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/*
* Copyright (c) 2021, Andreas Kling <[email protected]>
* Copyright (c) 2024, Jelle Raaijmakers <[email protected]>
*
* 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>
Expand All @@ -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)
Expand Down Expand Up @@ -152,60 +165,242 @@ 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& 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();
ASSERT_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);
if (!input_element.select_applies() || input_element.value().is_empty())
return {};
}

// 2. Set the selection range with 0 and infinity.
return set_selection_range(0, NumericLimits<WebIDL::UnsignedLong>::max(), {});
}

// 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.
return {};
// 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.
return set_selection_range(value, end, selection_direction());
}

// 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.
// 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,
// 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.
return set_selection_range(m_selection_start, value, selection_direction());
}

// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#selection-direction
Optional<String> FormAssociatedElement::selection_direction() const
{
switch (m_selection_direction) {
case SelectionDirection::Forward:
return "forward"_string;
case SelectionDirection::Backward:
return "backward"_string;
case SelectionDirection::None:
return "none"_string;
default:
ASSERT_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);
}

WebIDL::ExceptionOr<void> FormAssociatedElement::set_selection_range(Optional<WebIDL::UnsignedLong> start, Optional<WebIDL::UnsignedLong> end, Optional<String> direction)
{
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setselectionrange

// 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);

// 2. Set the selection range with start, end, and direction.

// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#set-the-selection-range

// 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".
auto new_direction = string_to_selection_direction(direction);

// 5. Set the selection direction of the text control to direction.
was_modified |= m_selection_direction != new_direction;
m_selection_direction = new_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) {
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);
});
}

// 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.
return {};
}

Expand Down
38 changes: 35 additions & 3 deletions Userland/Libraries/LibWeb/HTML/FormAssociatedElement.h
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/*
* Copyright (c) 2021, Andreas Kling <[email protected]>
* Copyright (c) 2024, Jelle Raaijmakers <[email protected]>
*
* SPDX-License-Identifier: BSD-2-Clause
*/

#pragma once

#include <AK/FlyString.h>
#include <AK/String.h>
#include <AK/WeakPtr.h>
#include <LibWeb/Forward.h>
Expand Down Expand Up @@ -49,6 +51,13 @@ private:
form_associated_element_attribute_changed(name, value); \
}

// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#selection-direction
enum class SelectionDirection {
Forward,
Backward,
None,
};

class FormAssociatedElement {
public:
HTMLFormElement* form() { return m_form; }
Expand Down Expand Up @@ -83,18 +92,33 @@ class FormAssociatedElement {

virtual String value() const { return String {}; }

// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
String relevant_value() const;

virtual HTMLElement& form_associated_element_to_html_element() = 0;
HTMLElement const& form_associated_element_to_html_element() const { return const_cast<FormAssociatedElement&>(*this).form_associated_element_to_html_element(); }

// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-form-reset-control
virtual void reset_algorithm() {};

WebIDL::UnsignedLong selection_start() const;
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-select
WebIDL::ExceptionOr<void> select();

// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionstart
Optional<WebIDL::UnsignedLong> selection_start() const;
WebIDL::ExceptionOr<void> set_selection_start(Optional<WebIDL::UnsignedLong> const&);

WebIDL::UnsignedLong selection_end() const;
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionend
Optional<WebIDL::UnsignedLong> selection_end() const;
WebIDL::ExceptionOr<void> set_selection_end(Optional<WebIDL::UnsignedLong> const&);

// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectiondirection
Optional<String> selection_direction() const;
void set_selection_direction(Optional<String> direction);

// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setselectionrange
WebIDL::ExceptionOr<void> set_selection_range(Optional<WebIDL::UnsignedLong> start, Optional<WebIDL::UnsignedLong> end, Optional<String> direction = {});

protected:
FormAssociatedElement() = default;
virtual ~FormAssociatedElement() = default;
Expand All @@ -107,13 +131,21 @@ class FormAssociatedElement {
void form_node_was_removed();
void form_node_attribute_changed(FlyString const&, Optional<String> const&);

// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
void relevant_value_was_changed(JS::GCPtr<DOM::Text>);

private:
void reset_form_owner();

WeakPtr<HTMLFormElement> m_form;

// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#parser-inserted-flag
bool m_parser_inserted { false };

void reset_form_owner();
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-selection
WebIDL::UnsignedLong m_selection_start { 0 };
WebIDL::UnsignedLong m_selection_end { 0 };
SelectionDirection m_selection_direction { SelectionDirection::None };
};

}
Loading

0 comments on commit b613536

Please sign in to comment.