LibWeb: Fix selectionchange event dispatch on text control elements

With a8077f79cc Selection object is no
longer aware of selection state inside text controls (<textarea> and
<input>), so this change makes them responsible for dispatching
`selectionchange` if their selection state was changed.
This commit is contained in:
Aliaksandr Kalenik 2024-10-31 21:59:19 +01:00 committed by Alexander Kalenik
parent 3a6b698572
commit e915143593
Notes: github-actions[bot] 2024-11-01 14:07:07 +00:00
7 changed files with 91 additions and 70 deletions

View file

@ -0,0 +1,2 @@
selectionchange event dispatched on textarea should bubble: true
Selected range: 6 - 10

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<script src="include.js"></script>
<textarea id="textarea">hello hmmm</textarea>
<script>
asyncTest(done => {
const textarea = document.getElementById("textarea");
textarea.addEventListener("selectionchange", event => {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
println(`selectionchange event dispatched on textarea should bubble: ${event.bubbles}`);
println(`Selected range: ${start} - ${end}`);
done();
});
textarea.setSelectionRange(6, 10);
});
</script>

View file

@ -18,14 +18,12 @@
#include <LibWeb/DOM/Node.h>
#include <LibWeb/DOM/ProcessingInstruction.h>
#include <LibWeb/DOM/Range.h>
#include <LibWeb/DOM/SelectionchangeEventDispatching.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/Geometry/DOMRect.h>
#include <LibWeb/Geometry/DOMRectList.h>
#include <LibWeb/HTML/HTMLHtmlElement.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/HTMLTextAreaElement.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Layout/Viewport.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/Painting/ViewportPaintable.h>
@ -109,57 +107,7 @@ void Range::update_associated_selection()
// When the selection is dissociated with its range, associated with a new range, or the
// associated range's boundary point is mutated either by the user or the content script, the
// user agent must schedule a selectionchange event on document.
schedule_a_selectionchange_event(document);
// When an input or textarea element provide a text selection and its selection changes (in
// either extent or direction), the user agent must schedule a selectionchange event on the
// element.
if (m_start_container == m_end_container) {
if (is<HTML::HTMLInputElement>(*m_start_container))
schedule_a_selectionchange_event(verify_cast<HTML::HTMLInputElement>(*m_start_container));
if (is<HTML::HTMLTextAreaElement>(*m_start_container))
schedule_a_selectionchange_event(verify_cast<HTML::HTMLTextAreaElement>(*m_start_container));
}
}
// https://w3c.github.io/selection-api/#scheduling-selectionhange-event
template<SelectionChangeTarget T>
void Range::schedule_a_selectionchange_event(T& target)
{
// 1. If target's has scheduled selectionchange event is true, abort these steps.
if (target.has_scheduled_selectionchange_event())
return;
// AD-HOC (https://github.com/w3c/selection-api/issues/338):
// Set target's has scheduled selectionchange event to true
target.set_scheduled_selectionchange_event(true);
// 2. Queue a task on the user interaction task source to fire a selectionchange event on
// target.
JS::NonnullGCPtr<Document> document = m_start_container->document();
queue_global_task(HTML::Task::Source::UserInteraction, relevant_global_object(*document), JS::create_heap_function(document->heap(), [&] {
fire_a_selectionchange_event(target);
}));
}
// https://w3c.github.io/selection-api/#firing-selectionhange-event
template<SelectionChangeTarget T>
void Range::fire_a_selectionchange_event(T& target)
{
// 1. Set target's has scheduled selectionchange event to false.
target.set_scheduled_selectionchange_event(false);
// 2. If target is an element, fire an event named selectionchange, which bubbles and not
// cancelable, at target.
// 3. Otherwise, if target is a document, fire an event named selectionchange, which does not
// bubble and not cancelable, at target.
EventInit event_init;
event_init.bubbles = SameAs<T, Element>;
event_init.cancelable = false;
auto& realm = m_start_container->document().realm();
auto event = DOM::Event::create(realm, HTML::EventNames::selectionchange, event_init);
target.dispatch_event(event);
schedule_a_selectionchange_event(document, document);
}
// https://dom.spec.whatwg.org/#concept-range-root

View file

@ -23,13 +23,6 @@ enum class RelativeBoundaryPointPosition {
// https://dom.spec.whatwg.org/#concept-range-bp-position
RelativeBoundaryPointPosition position_of_boundary_point_relative_to_other_boundary_point(Node const& node_a, u32 offset_a, Node const& node_b, u32 offset_b);
// https://w3c.github.io/selection-api/#dfn-has-scheduled-selectionchange-event
template<typename T>
concept SelectionChangeTarget = DerivedFrom<T, EventTarget> && requires(T t) {
{ t.has_scheduled_selectionchange_event() } -> SameAs<bool>;
{ t.set_scheduled_selectionchange_event(bool()) } -> SameAs<void>;
};
class Range final : public AbstractRange {
WEB_PLATFORM_OBJECT(Range, AbstractRange);
JS_DECLARE_ALLOCATOR(Range);
@ -115,10 +108,6 @@ private:
Node const& root() const;
void update_associated_selection();
template<SelectionChangeTarget T>
void schedule_a_selectionchange_event(T&);
template<SelectionChangeTarget T>
void fire_a_selectionchange_event(T&);
enum class StartOrEnd {
Start,

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/EventTarget.h>
namespace Web::DOM {
// https://w3c.github.io/selection-api/#dfn-has-scheduled-selectionchange-event
template<typename T>
concept SelectionChangeTarget = DerivedFrom<T, EventTarget> && requires(T t) {
{ t.has_scheduled_selectionchange_event() } -> SameAs<bool>;
{ t.set_scheduled_selectionchange_event(bool()) } -> SameAs<void>;
};
// https://w3c.github.io/selection-api/#scheduling-selectionhange-event
template<SelectionChangeTarget T>
void schedule_a_selectionchange_event(T& target, Document& document)
{
// 1. If target's has scheduled selectionchange event is true, abort these steps.
if (target.has_scheduled_selectionchange_event())
return;
// AD-HOC (https://github.com/w3c/selection-api/issues/338):
// Set target's has scheduled selectionchange event to true
target.set_scheduled_selectionchange_event(true);
// 2. Queue a task on the user interaction task source to fire a selectionchange event on
// target.
queue_global_task(HTML::Task::Source::UserInteraction, relevant_global_object(document), JS::create_heap_function(document.heap(), [&] {
fire_a_selectionchange_event(target, document);
}));
}
// https://w3c.github.io/selection-api/#firing-selectionhange-event
template<SelectionChangeTarget T>
void fire_a_selectionchange_event(T& target, Document& document)
{
// 1. Set target's has scheduled selectionchange event to false.
target.set_scheduled_selectionchange_event(false);
// 2. If target is an element, fire an event named selectionchange, which bubbles and not
// cancelable, at target.
// 3. Otherwise, if target is a document, fire an event named selectionchange, which does not
// bubble and not cancelable, at target.
EventInit event_init;
event_init.bubbles = DerivedFrom<T, Element>;
event_init.cancelable = false;
auto event = DOM::Event::create(document.realm(), HTML::EventNames::selectionchange, event_init);
target.dispatch_event(event);
}
}

View file

@ -11,6 +11,7 @@
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/Position.h>
#include <LibWeb/DOM/SelectionchangeEventDispatching.h>
#include <LibWeb/HTML/FormAssociatedElement.h>
#include <LibWeb/HTML/HTMLButtonElement.h>
#include <LibWeb/HTML/HTMLFieldSetElement.h>
@ -580,9 +581,7 @@ void FormAssociatedTextControlElement::set_the_selection_range(Optional<WebIDL::
});
}
// 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);
selection_was_changed();
}
}
@ -657,6 +656,15 @@ void FormAssociatedTextControlElement::collapse_selection_to_offset(size_t posit
void FormAssociatedTextControlElement::selection_was_changed()
{
auto& element = form_associated_element_to_html_element();
if (is<HTML::HTMLInputElement>(element)) {
schedule_a_selectionchange_event(static_cast<HTML::HTMLInputElement&>(element), element.document());
} else if (is<HTML::HTMLTextAreaElement>(element)) {
schedule_a_selectionchange_event(static_cast<HTML::HTMLTextAreaElement&>(element), element.document());
} else {
VERIFY_NOT_REACHED();
}
auto text_node = form_associated_element_to_text_node();
if (!text_node)
return;

View file

@ -192,8 +192,6 @@ protected:
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
void relevant_value_was_changed();
virtual void selection_was_changed([[maybe_unused]] size_t selection_start, [[maybe_unused]] size_t selection_end) { }
private:
void collapse_selection_to_offset(size_t);
void selection_was_changed();