forked from LadybirdBrowser/ladybird
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
LibWeb: Implement input/textarea selection APIs
For both types of elements, `.selectionStart`, `.selectionEnd`, `.selectionDirection`, `.setSelectionRange()` and `.select()` are now implemented.
- Loading branch information
Showing
8 changed files
with
356 additions
and
112 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
|
@@ -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,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 {}; | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
|
@@ -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; } | ||
|
@@ -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; | ||
|
@@ -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 }; | ||
}; | ||
|
||
} |
Oops, something went wrong.