LibWeb: Implement InputEvent.getTargetRanges()

This returns a list of ranges that would be affected by a change to the
DOM if the input event is not cancelled.
This commit is contained in:
Tim Ledbetter 2025-07-18 13:03:46 +01:00 committed by Shannon Booth
parent ed3d0d76ec
commit 7874f325a8
Notes: github-actions[bot] 2025-07-20 00:35:27 +00:00
6 changed files with 67 additions and 12 deletions

View file

@ -127,7 +127,7 @@ WebIDL::ExceptionOr<bool> Document::exec_command(FlyString const& command, [[may
if (command == Editing::CommandNames::insertText) if (command == Editing::CommandNames::insertText)
event_init.data = value; event_init.data = value;
auto event = realm().create<UIEvents::InputEvent>(realm(), HTML::EventNames::input, event_init); auto event = UIEvents::InputEvent::create_from_platform_event(realm(), HTML::EventNames::input, event_init);
event->set_is_trusted(true); event->set_is_trusted(true);
affected_editing_host->dispatch_event(event); affected_editing_host->dispatch_event(event);
} }

View file

@ -1117,6 +1117,18 @@ static bool is_enter_key_or_interoperable_enter_key_combo(UIEvents::KeyCode key,
return false; return false;
} }
static GC::RootVector<GC::Ref<DOM::StaticRange>> target_ranges_for_input_event(DOM::Document const& document)
{
GC::RootVector<GC::Ref<DOM::StaticRange>> target_ranges { document.heap() };
if (auto selection = document.get_selection(); selection && !selection->is_collapsed()) {
if (auto range = selection->range()) {
auto static_range = document.realm().create<DOM::StaticRange>(range->start_container(), range->start_offset(), range->end_container(), range->end_offset());
target_ranges.append(static_range);
}
}
return target_ranges;
}
EventResult EventHandler::input_event(FlyString const& event_name, FlyString const& input_type, HTML::Navigable& navigable, u32 code_point) EventResult EventHandler::input_event(FlyString const& event_name, FlyString const& input_type, HTML::Navigable& navigable, u32 code_point)
{ {
auto document = navigable.active_document(); auto document = navigable.active_document();
@ -1138,11 +1150,11 @@ EventResult EventHandler::input_event(FlyString const& event_name, FlyString con
return input_event(event_name, input_type, *navigable_container.content_navigable(), code_point); return input_event(event_name, input_type, *navigable_container.content_navigable(), code_point);
} }
auto event = UIEvents::InputEvent::create_from_platform_event(document->realm(), event_name, input_event_init); auto event = UIEvents::InputEvent::create_from_platform_event(document->realm(), event_name, input_event_init, target_ranges_for_input_event(*document));
return focused_element->dispatch_event(event) ? EventResult::Accepted : EventResult::Cancelled; return focused_element->dispatch_event(event) ? EventResult::Accepted : EventResult::Cancelled;
} }
auto event = UIEvents::InputEvent::create_from_platform_event(document->realm(), event_name, input_event_init); auto event = UIEvents::InputEvent::create_from_platform_event(document->realm(), event_name, input_event_init, target_ranges_for_input_event(*document));
if (auto* body = document->body()) if (auto* body = document->body())
return body->dispatch_event(event) ? EventResult::Accepted : EventResult::Cancelled; return body->dispatch_event(event) ? EventResult::Accepted : EventResult::Cancelled;

View file

@ -12,9 +12,9 @@ namespace Web::UIEvents {
GC_DEFINE_ALLOCATOR(InputEvent); GC_DEFINE_ALLOCATOR(InputEvent);
GC::Ref<InputEvent> InputEvent::create_from_platform_event(JS::Realm& realm, FlyString const& event_name, InputEventInit const& event_init) GC::Ref<InputEvent> InputEvent::create_from_platform_event(JS::Realm& realm, FlyString const& event_name, InputEventInit const& event_init, Vector<GC::Ref<DOM::StaticRange>> const& target_ranges)
{ {
auto event = realm.create<InputEvent>(realm, event_name, event_init); auto event = realm.create<InputEvent>(realm, event_name, event_init, target_ranges);
event->set_bubbles(true); event->set_bubbles(true);
if (event_name == "beforeinput"_fly_string) { if (event_name == "beforeinput"_fly_string) {
event->set_cancelable(true); event->set_cancelable(true);
@ -27,11 +27,12 @@ WebIDL::ExceptionOr<GC::Ref<InputEvent>> InputEvent::construct_impl(JS::Realm& r
return realm.create<InputEvent>(realm, event_name, event_init); return realm.create<InputEvent>(realm, event_name, event_init);
} }
InputEvent::InputEvent(JS::Realm& realm, FlyString const& event_name, InputEventInit const& event_init) InputEvent::InputEvent(JS::Realm& realm, FlyString const& event_name, InputEventInit const& event_init, Vector<GC::Ref<DOM::StaticRange>> const& target_ranges)
: UIEvent(realm, event_name, event_init) : UIEvent(realm, event_name, event_init)
, m_data(event_init.data) , m_data(event_init.data)
, m_is_composing(event_init.is_composing) , m_is_composing(event_init.is_composing)
, m_input_type(event_init.input_type) , m_input_type(event_init.input_type)
, m_target_ranges(target_ranges)
{ {
} }
@ -43,10 +44,19 @@ void InputEvent::initialize(JS::Realm& realm)
Base::initialize(realm); Base::initialize(realm);
} }
Vector<DOM::StaticRange> InputEvent::get_target_ranges() const void InputEvent::visit_edges(Visitor& visitor)
{ {
dbgln("FIXME: Implement InputEvent::get_target_ranges()"); Base::visit_edges(visitor);
return {}; visitor.visit(m_target_ranges);
}
// https://w3c.github.io/input-events/#dom-inputevent-gettargetranges
ReadonlySpan<GC::Ref<DOM::StaticRange>> InputEvent::get_target_ranges() const
{
// getTargetRanges() returns an array of StaticRanges representing the content that the event will modify if it is
// not canceled. The returned StaticRanges MUST cover only the code points that the browser would normally replace,
// even if they are only part of a grapheme cluster.
return m_target_ranges;
} }
} }

View file

@ -22,7 +22,7 @@ class InputEvent final : public UIEvent {
GC_DECLARE_ALLOCATOR(InputEvent); GC_DECLARE_ALLOCATOR(InputEvent);
public: public:
[[nodiscard]] static GC::Ref<InputEvent> create_from_platform_event(JS::Realm&, FlyString const& type, InputEventInit const& event_init); [[nodiscard]] static GC::Ref<InputEvent> create_from_platform_event(JS::Realm&, FlyString const& type, InputEventInit const& event_init, Vector<GC::Ref<DOM::StaticRange>> const& target_ranges = {});
static WebIDL::ExceptionOr<GC::Ref<InputEvent>> construct_impl(JS::Realm&, FlyString const& event_name, InputEventInit const& event_init); static WebIDL::ExceptionOr<GC::Ref<InputEvent>> construct_impl(JS::Realm&, FlyString const& event_name, InputEventInit const& event_init);
virtual ~InputEvent() override; virtual ~InputEvent() override;
@ -36,16 +36,18 @@ public:
// https://w3c.github.io/uievents/#dom-inputevent-inputtype // https://w3c.github.io/uievents/#dom-inputevent-inputtype
FlyString input_type() const { return m_input_type; } FlyString input_type() const { return m_input_type; }
Vector<DOM::StaticRange> get_target_ranges() const; ReadonlySpan<GC::Ref<DOM::StaticRange>> get_target_ranges() const;
private: private:
InputEvent(JS::Realm&, FlyString const& event_name, InputEventInit const&); InputEvent(JS::Realm&, FlyString const& event_name, InputEventInit const&, Vector<GC::Ref<DOM::StaticRange>> const& target_ranges = {});
virtual void initialize(JS::Realm&) override; virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Visitor&) override;
Optional<String> m_data; Optional<String> m_data;
bool m_is_composing; bool m_is_composing;
FlyString m_input_type; FlyString m_input_type;
Vector<GC::Ref<DOM::StaticRange>> m_target_ranges;
}; };
} }

View file

@ -0,0 +1,2 @@
Target range count: 1. Start offset: 1, End offset: 3
No target ranges

View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<div contenteditable>text</div>
<script src="../include.js"></script>
<script>
test(() => {
const editableElement = document.querySelector("div");
editableElement.addEventListener("beforeinput", e => {
const targetRanges = e.getTargetRanges();
if (targetRanges.length === 0) {
println("No target ranges");
} else {
println(`Target range count: ${targetRanges.length}. Start offset: ${targetRanges[0].startOffset}, End offset: ${targetRanges[0].endOffset}`);
}
});
editableElement.focus();
const range = document.createRange();
range.setStart(editableElement.firstChild, 1);
range.setEnd(editableElement.firstChild, 3);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
// GetTargetRanges should return a single range representing the selection.
internals.sendKey(editableElement, "Backspace");
// An input event shouldn't be dispatched for events that wouldn't affect the DOM.
internals.sendKey(editableElement, "End");
// GetTargetRanges should be empty because there are no ranges.
internals.sendKey(editableElement, "Backspace");
});
</script>