LibWeb/HTML: Dispatch command events

Command events are now dispatched when buttons are activated. The
special commands for popovers and dialogs are also implemented.
This commit is contained in:
Glenn Skrzypczak 2025-04-04 14:30:56 +02:00 committed by Tim Ledbetter
commit c0cbd439a2
Notes: github-actions[bot] 2025-04-18 11:10:59 +00:00
14 changed files with 1037 additions and 4 deletions

View file

@ -151,6 +151,7 @@ namespace AttributeNames {
__ENUMERATE_HTML_ATTRIBUTE(onchange, "onchange") \
__ENUMERATE_HTML_ATTRIBUTE(onclick, "onclick") \
__ENUMERATE_HTML_ATTRIBUTE(onclose, "onclose") \
__ENUMERATE_HTML_ATTRIBUTE(oncommand, "oncommand") \
__ENUMERATE_HTML_ATTRIBUTE(oncontextlost, "oncontextlost") \
__ENUMERATE_HTML_ATTRIBUTE(oncontextmenu, "oncontextmenu") \
__ENUMERATE_HTML_ATTRIBUTE(oncontextrestored, "oncontextrestored") \

View file

@ -31,6 +31,7 @@ namespace Web::HTML::EventNames {
__ENUMERATE_HTML_EVENT(change) \
__ENUMERATE_HTML_EVENT(click) \
__ENUMERATE_HTML_EVENT(close) \
__ENUMERATE_HTML_EVENT(command) \
__ENUMERATE_HTML_EVENT(complete) \
__ENUMERATE_HTML_EVENT(connect) \
__ENUMERATE_HTML_EVENT(contextlost) \

View file

@ -22,6 +22,7 @@
E(onchange, HTML::EventNames::change) \
E(onclick, UIEvents::EventNames::click) \
E(onclose, HTML::EventNames::close) \
E(oncommand, HTML::EventNames::command) \
E(oncontextlost, HTML::EventNames::contextlost) \
E(oncontextmenu, HTML::EventNames::contextmenu) \
E(oncontextrestored, HTML::EventNames::contextrestored) \

View file

@ -7,8 +7,10 @@
#include <LibWeb/Bindings/HTMLButtonElementPrototype.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/HTML/CommandEvent.h>
#include <LibWeb/HTML/HTMLButtonElement.h>
#include <LibWeb/HTML/HTMLFormElement.h>
#include <LibWeb/Namespace.h>
namespace Web::HTML {
@ -153,12 +155,111 @@ void HTMLButtonElement::activation_behavior(DOM::Event const& event)
return;
}
// FIXME: 4. Let target be the result of running element's get the commandfor associated element.
// FIXME: 5. If target is not null:
// ...
// 4. Let target be the result of running element's get the commandfor associated element.
// AD-HOC: Target needs to be an HTML Element in the following steps.
GC::Ptr<HTMLElement> target = as_if<HTMLElement>(m_command_for_element.ptr());
if (!target) {
auto target_id = attribute(AttributeNames::commandfor);
if (target_id.has_value()) {
root().for_each_in_inclusive_subtree_of_type<HTMLElement>([&](auto& candidate) {
if (candidate.attribute(HTML::AttributeNames::id) == target_id.value()) {
target = &candidate;
return TraversalDecision::Break;
}
return TraversalDecision::Continue;
});
}
}
// 5. If target is not null:
if (target) {
// 1. Let command be element's command attribute.
auto command = this->command();
// 2. If command is in the Unknown state, then return.
if (command.is_empty()) {
return;
}
// 3. Let isPopover be true if target's popover attribute is not in the no popover state; otherwise false.
auto is_popover = target->popover().has_value();
// 4. If isPopover is false and command is not in the Custom state:
auto command_is_in_custom_state = command.starts_with_bytes("--"sv);
if (!is_popover && !command.starts_with_bytes("--"sv)) {
// 1. Assert: target's namespace is the HTML namespace.
VERIFY(target->namespace_uri() == Namespace::HTML);
// 2. If this standard does not define is valid invoker command steps for target's local name, then return.
// 3. Otherwise, if the result of running target's corresponding is valid invoker command steps given command is false, then return.
if (!target->is_valid_invoker_command(command))
return;
}
// 5. Let continue be the result of firing an event named command at target, using CommandEvent, with its command attribute initialized to command, its source attribute initialized to element, and its cancelable and composed attributes initialized to true.
// SPEC-NOTE: DOM standard issue #1328 tracks how to better standardize associated event data in a way which makes sense on Events. Currently an event attribute initialized to a value cannot also have a getter, and so an internal slot (or map of additional fields) is required to properly specify this.
CommandEventInit event_init {};
event_init.command = command;
event_init.source = this;
event_init.cancelable = true;
event_init.composed = true;
auto event = CommandEvent::create(realm(), HTML::EventNames::command, move(event_init));
event->set_is_trusted(true);
auto continue_ = target->dispatch_event(event);
// 6. If continue is false, then return.
if (!continue_)
return;
// 7. If target is not connected, then return.
if (!target->is_connected())
return;
// 8. If command is in the Custom state, then return.
if (command_is_in_custom_state)
return;
// AD-HOC: The parameters provided in the spec do not match the function signatures in the following steps.
// The inconsistent parameters were therefore selected ad hoc.
// 9. If command is in the Hide Popover state:
if (command == "hide-popover") {
// 1. If the result of running check popover validity given target, true, false, and null is true, then run the hide popover algorithm given target, true, true, and false.
if (MUST(target->check_popover_validity(ExpectedToBeShowing::Yes, ThrowExceptions::No, nullptr, IgnoreDomState::No))) {
MUST(target->hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::No));
}
}
// 10. Otherwise, if command is in the Toggle Popover state:
else if (command == "toggle-popover") {
// 1. If the result of running check popover validity given target, false, false, and null is true, then run the show popover algorithm given target, true, true, and false.
if (MUST(target->check_popover_validity(ExpectedToBeShowing::No, ThrowExceptions::No, nullptr, IgnoreDomState::No))) {
MUST(target->show_popover(ThrowExceptions::No, this));
}
// 2. Otheriwse, if the result of running check popover validity given target, true, false, and null is true, then run the hide popover algorithm given target, true, true, and false.
else if (MUST(target->check_popover_validity(ExpectedToBeShowing::Yes, ThrowExceptions::No, nullptr, IgnoreDomState::No))) {
MUST(target->hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::No));
}
}
// 11. Otherwise, if command is in the Show Popover state:
else if (command == "show-popover") {
// 1. If the result of running check popover validity given target, false, false, and null is true, then run the show popover algorithm given target, true, true, and false.
if (MUST(target->check_popover_validity(ExpectedToBeShowing::No, ThrowExceptions::No, nullptr, IgnoreDomState::No))) {
MUST(target->show_popover(ThrowExceptions::No, this));
}
}
// 12. Otherwise, if this standard defines invoker command steps for target's local name, then run the corresponding invoker command steps given target, element and command.
else {
target->invoker_command_steps(*this, command);
}
}
// 6. Otherwise, run the popover target attribute activation behavior given element and event's target.
if (event.target() && event.target()->is_dom_node())
else if (event.target() && event.target()->is_dom_node())
PopoverInvokerElement::popover_target_activation_behaviour(*this, as<DOM::Node>(*event.target()));
}

View file

@ -443,4 +443,39 @@ void HTMLDialogElement::set_is_modal(bool is_modal)
invalidate_style(DOM::StyleInvalidationReason::NodeRemove);
}
// https://html.spec.whatwg.org/multipage/interactive-elements.html#the-dialog-element:is-valid-invoker-command-steps
bool HTMLDialogElement::is_valid_invoker_command(String& command)
{
// 1. If command is in the Close state or in the Show Modal state, then return true.
if (command == "close" || command == "show-modal")
return true;
// 2. Return false.
return false;
}
// https://html.spec.whatwg.org/multipage/interactive-elements.html#the-dialog-element:invoker-command-steps
void HTMLDialogElement::invoker_command_steps(DOM::Element& invoker, String& command)
{
// 1. If element is in the popover showing state, then return.
if (popover_visibility_state() == PopoverVisibilityState::Showing) {
return;
}
// 2. If command is in the Close state and element has an open attribute:
if (command == "close" && has_attribute(AttributeNames::open)) {
// 1. Let value be invoker's value.
// FIXME: This assumes invoker is a button.
auto value = invoker.get_attribute(AttributeNames::value);
// 2. Close the dialog element with value.
close_the_dialog(value);
}
// 3. If command is the Show Modal state and element does not have an open attribute, then show a modal dialog given element.
if (command == "show-modal" && !has_attribute(AttributeNames::open)) {
MUST(show_a_modal_dialog(*this));
}
}
}

View file

@ -39,6 +39,9 @@ public:
bool is_modal() const { return m_is_modal; }
void set_is_modal(bool);
bool is_valid_invoker_command(String&) override;
void invoker_command_steps(DOM::Element&, String&) override;
private:
HTMLDialogElement(DOM::Document&, DOM::QualifiedName);

View file

@ -151,6 +151,9 @@ public:
bool is_inert() const { return m_inert; }
virtual bool is_valid_invoker_command(String&) { return false; }
virtual void invoker_command_steps(DOM::Element&, String&) { }
protected:
HTMLElement(DOM::Document&, DOM::QualifiedName);