LibWeb: Basic implementation of global event handlers :^)

Document and HTMLElement now inherit from HTML::GlobalEventHandlers
which allows them to support "onfoo" event handler attributes.

These are assignable both via IDL attributes and content attributes.

Event listeners constructed this way get a special "attribute" flag
on them so we know which one to replace if you reassign them.
This also allows them to coexist with EventTarget.addEventListener().

This is all a bit sloppy, but it works decently for a first cut.
The Window object should also inherit GlobalEventHandlers, but since
we don't generate it from IDL, I haven't taken that step here.

Also this would be a lot nicer if we supported IDL mixins.
This commit is contained in:
Andreas Kling 2021-02-03 22:47:50 +01:00
parent b43db4cc50
commit a59b1825ce
Notes: sideshowbarker 2024-07-18 22:36:16 +09:00
15 changed files with 391 additions and 2 deletions

View file

@ -55,6 +55,7 @@ set(SOURCES
HTML/AttributeNames.cpp
HTML/CanvasRenderingContext2D.cpp
HTML/EventNames.cpp
HTML/GlobalEventHandlers.cpp
HTML/HTMLAnchorElement.cpp
HTML/HTMLAreaElement.cpp
HTML/HTMLAudioElement.cpp

View file

@ -904,6 +904,7 @@ void generate_prototype_implementation(const IDL::Interface& interface)
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/EventListener.h>
#include <LibWeb/DOM/Window.h>
#include <LibWeb/HTML/EventHandler.h>
#include <LibWeb/HTML/HTMLElement.h>
#include <LibWeb/NavigationTiming/PerformanceTiming.h>
#include <LibWeb/Origin.h>
@ -1096,6 +1097,18 @@ static @fully_qualified_name@* impl_from(JS::VM& vm, JS::GlobalObject& global_ob
auto @cpp_name@ = @js_name@@js_suffix@.to_u32(global_object);
if (vm.exception())
@return_statement@
)~~~");
} else if (parameter.type.name == "EventHandler") {
// x.onfoo = function() { ... }
scoped_generator.append(R"~~~(
HTML::EventHandler @cpp_name@;
if (@js_name@@js_suffix@.is_function()) {
@cpp_name@.callback = JS::make_handle(&@js_name@@js_suffix@.as_function());
} else if (@js_name@@js_suffix@.is_string()) {
@cpp_name@.string = @js_name@@js_suffix@.as_string().string();
} else {
@return_statement@
}
)~~~");
} else {
dbgln("Unimplemented JS-to-C++ conversion: {}", parameter.type.name);
@ -1173,6 +1186,10 @@ static @fully_qualified_name@* impl_from(JS::VM& vm, JS::GlobalObject& global_ob
} else if (return_type.name == "Uint8ClampedArray") {
scoped_generator.append(R"~~~(
return retval;
)~~~");
} else if (return_type.name == "EventHandler") {
scoped_generator.append(R"~~~(
return retval.callback.cell();
)~~~");
} else {
scoped_generator.append(R"~~~(

View file

@ -55,6 +55,7 @@ enum class QuirksMode {
class Document
: public ParentNode
, public NonElementParentNode<Document>
, public HTML::GlobalEventHandlers
, public Bindings::ScriptExecutionContext {
public:
using WrapperType = Bindings::DocumentWrapper;
@ -233,8 +234,12 @@ public:
private:
explicit Document(const URL&);
// ^DOM::Node
virtual RefPtr<Layout::Node> create_layout_node() override;
// ^HTML::GlobalEventHandlers
virtual EventTarget& global_event_handlers_to_event_target() final { return *this; }
void tear_down_layout_tree();
void increment_referencing_node_count()

View file

@ -36,4 +36,75 @@ interface Document : Node {
attribute DOMString title;
// FIXME: These should all come from a GlobalEventHandlers mixin
attribute EventHandler onabort;
attribute EventHandler onauxclick;
attribute EventHandler onblur;
attribute EventHandler oncancel;
attribute EventHandler oncanplay;
attribute EventHandler oncanplaythrough;
attribute EventHandler onchange;
attribute EventHandler onclick;
attribute EventHandler onclose;
attribute EventHandler oncontextmenu;
attribute EventHandler oncuechange;
attribute EventHandler ondblclick;
attribute EventHandler ondrag;
attribute EventHandler ondragend;
attribute EventHandler ondragenter;
attribute EventHandler ondragleave;
attribute EventHandler ondragover;
attribute EventHandler ondragstart;
attribute EventHandler ondrop;
attribute EventHandler ondurationchange;
attribute EventHandler onemptied;
attribute EventHandler onended;
// FIXME: Should be an OnErrorEventHandler
attribute EventHandler onerror;
attribute EventHandler onfocus;
attribute EventHandler onformdata;
attribute EventHandler oninput;
attribute EventHandler oninvalid;
attribute EventHandler onkeydown;
attribute EventHandler onkeypress;
attribute EventHandler onkeyup;
attribute EventHandler onload;
attribute EventHandler onloadeddata;
attribute EventHandler onloadedmetadata;
attribute EventHandler onloadstart;
attribute EventHandler onmousedown;
[LegacyLenientThis] attribute EventHandler onmouseenter;
[LegacyLenientThis] attribute EventHandler onmouseleave;
attribute EventHandler onmousemove;
attribute EventHandler onmouseout;
attribute EventHandler onmouseover;
attribute EventHandler onmouseup;
attribute EventHandler onpause;
attribute EventHandler onplay;
attribute EventHandler onplaying;
attribute EventHandler onprogress;
attribute EventHandler onratechange;
attribute EventHandler onreset;
attribute EventHandler onresize;
attribute EventHandler onscroll;
attribute EventHandler onsecuritypolicyviolation;
attribute EventHandler onseeked;
attribute EventHandler onseeking;
attribute EventHandler onselect;
attribute EventHandler onslotchange;
attribute EventHandler onstalled;
attribute EventHandler onsubmit;
attribute EventHandler onsuspend;
attribute EventHandler ontimeupdate;
attribute EventHandler ontoggle;
attribute EventHandler onvolumechange;
attribute EventHandler onwaiting;
attribute EventHandler onwebkitanimationend;
attribute EventHandler onwebkitanimationiteration;
attribute EventHandler onwebkitanimationstart;
attribute EventHandler onwebkittransitionend;
attribute EventHandler onwheel;
};

View file

@ -38,8 +38,9 @@ class EventListener
public:
using WrapperType = Bindings::EventListenerWrapper;
explicit EventListener(JS::Handle<JS::Function> function)
explicit EventListener(JS::Handle<JS::Function> function, bool is_attribute = false)
: m_function(move(function))
, m_attribute(is_attribute)
{
}
@ -60,6 +61,8 @@ public:
bool removed() const { return m_removed; }
void set_removed(bool removed) { m_removed = removed; }
bool is_attribute() const { return m_attribute; }
private:
FlyString m_type;
JS::Handle<JS::Function> m_function;
@ -67,6 +70,7 @@ private:
bool m_passive { false };
bool m_once { false };
bool m_removed { false };
bool m_attribute { false };
};
}

View file

@ -61,6 +61,7 @@ public:
NonnullRefPtr<EventListener> listener;
};
Vector<EventListenerRegistration>& listeners() { return m_listeners; }
const Vector<EventListenerRegistration>& listeners() const { return m_listeners; }
Function<void(const Event&)> activation_behaviour;

View file

@ -62,6 +62,7 @@ enum class QuirksMode;
namespace Web::HTML {
class CanvasRenderingContext2D;
class EventHandler;
class HTMLAnchorElement;
class HTMLAreaElement;
class HTMLAudioElement;

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/String.h>
#include <LibJS/Heap/Handle.h>
#include <LibJS/Runtime/Function.h>
namespace Web::HTML {
struct EventHandler {
EventHandler()
{
}
EventHandler(String s)
: string(move(s))
{
}
EventHandler(JS::Handle<JS::Function> c)
: callback(move(c))
{
}
String string;
JS::Handle<JS::Function> callback;
};
}

View file

@ -0,0 +1,99 @@
/*
* Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <LibJS/Interpreter.h>
#include <LibJS/Parser.h>
#include <LibJS/Runtime/ScriptFunction.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/EventListener.h>
#include <LibWeb/HTML/EventHandler.h>
#include <LibWeb/HTML/EventNames.h>
#include <LibWeb/HTML/GlobalEventHandlers.h>
#include <LibWeb/UIEvents/EventNames.h>
namespace Web::HTML {
#undef __ENUMERATE
#define __ENUMERATE(attribute_name, event_name) \
void GlobalEventHandlers::set_##attribute_name(HTML::EventHandler value) \
{ \
set_event_handler_attribute(event_name, move(value)); \
} \
HTML::EventHandler GlobalEventHandlers::attribute_name() \
{ \
return get_event_handler_attribute(event_name); \
}
ENUMERATE_GLOBAL_EVENT_HANDLERS(__ENUMERATE)
#undef __ENUMERATE
GlobalEventHandlers::~GlobalEventHandlers()
{
}
void GlobalEventHandlers::set_event_handler_attribute(const FlyString& name, HTML::EventHandler value)
{
auto& self = global_event_handlers_to_event_target();
RefPtr<DOM::EventListener> listener;
if (!value.callback.is_null()) {
listener = adopt(*new DOM::EventListener(move(value.callback)));
} else {
StringBuilder builder;
builder.appendff("function {}(event) {{\n{}\n}}", name, value.string);
auto parser = JS::Parser(JS::Lexer(builder.string_view()));
auto program = parser.parse_function_node<JS::FunctionExpression>();
if (parser.has_errors()) {
dbgln("Failed to parse script in event handler attribute '{}'", name);
return;
}
auto* function = JS::ScriptFunction::create(self.script_execution_context()->interpreter().global_object(), name, program->body(), program->parameters(), program->function_length(), nullptr, false, false);
ASSERT(function);
listener = adopt(*new DOM::EventListener(JS::make_handle(static_cast<JS::Function*>(function))));
}
if (listener) {
for (auto& registered_listener : self.listeners()) {
if (registered_listener.event_name == name && registered_listener.listener->is_attribute()) {
self.remove_event_listener(name, registered_listener.listener);
break;
}
}
self.add_event_listener(name, listener.release_nonnull());
}
}
HTML::EventHandler GlobalEventHandlers::get_event_handler_attribute(const FlyString& name)
{
auto& self = global_event_handlers_to_event_target();
for (auto& listener : self.listeners()) {
if (listener.event_name == name && listener.listener->is_attribute()) {
return HTML::EventHandler { JS::make_handle(&listener.listener->function()) };
}
}
return {};
}
}

View file

@ -26,6 +26,9 @@
#pragma once
#include <AK/Forward.h>
#include <LibWeb/Forward.h>
#define ENUMERATE_GLOBAL_EVENT_HANDLERS(E) \
E(onabort, HTML::EventNames::abort) \
E(onauxclick, "auxclick") \
@ -93,3 +96,25 @@
E(onwebkitanimationstart, "webkitanimationstart") \
E(onwebkittransitionend, "webkittransitionend") \
E(onwheel, "wheel")
namespace Web::HTML {
class GlobalEventHandlers {
public:
virtual ~GlobalEventHandlers();
#undef __ENUMERATE
#define __ENUMERATE(attribute_name, event_name) \
void set_##attribute_name(HTML::EventHandler); \
HTML::EventHandler attribute_name();
ENUMERATE_GLOBAL_EVENT_HANDLERS(__ENUMERATE)
#undef __ENUMERATE
void set_event_handler_attribute(const FlyString& name, HTML::EventHandler);
HTML::EventHandler get_event_handler_attribute(const FlyString& name);
protected:
virtual DOM::EventTarget& global_event_handlers_to_event_target() = 0;
};
}

View file

@ -27,6 +27,7 @@
#include <LibWeb/CSS/StyleProperties.h>
#include <LibWeb/CSS/StyleValue.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Window.h>
#include <LibWeb/HTML/HTMLBodyElement.h>
namespace Web::HTML {
@ -78,4 +79,10 @@ void HTMLBodyElement::parse_attribute(const FlyString& name, const String& value
}
}
DOM::EventTarget& HTMLBodyElement::global_event_handlers_to_event_target()
{
// NOTE: This is a little weird, but IIUC document.body.onload actually refers to window.onload
return document().window();
}
}

View file

@ -41,6 +41,9 @@ public:
virtual void apply_presentational_hints(CSS::StyleProperties&) const override;
private:
// ^HTML::GlobalEventHandlers
virtual EventTarget& global_event_handlers_to_event_target() override;
RefPtr<CSS::ImageStyleValue> m_background_style_value;
};

View file

@ -25,11 +25,17 @@
*/
#include <AK/StringBuilder.h>
#include <LibJS/Interpreter.h>
#include <LibJS/Parser.h>
#include <LibJS/Runtime/ScriptFunction.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/EventListener.h>
#include <LibWeb/HTML/EventHandler.h>
#include <LibWeb/HTML/HTMLAnchorElement.h>
#include <LibWeb/HTML/HTMLElement.h>
#include <LibWeb/Layout/BreakNode.h>
#include <LibWeb/Layout/TextNode.h>
#include <LibWeb/UIEvents/EventNames.h>
namespace Web::HTML {
@ -138,4 +144,17 @@ bool HTMLElement::cannot_navigate() const
return !is<HTML::HTMLAnchorElement>(this) && !is_connected();
}
void HTMLElement::parse_attribute(const FlyString& name, const String& value)
{
Element::parse_attribute(name, value);
#undef __ENUMERATE
#define __ENUMERATE(attribute_name, event_name) \
if (name == HTML::AttributeNames::attribute_name) { \
set_event_handler_attribute(event_name, EventHandler { value }); \
}
ENUMERATE_GLOBAL_EVENT_HANDLERS(__ENUMERATE)
#undef __ENUMERATE
}
}

View file

@ -27,10 +27,14 @@
#pragma once
#include <LibWeb/DOM/Element.h>
#include <LibWeb/HTML/EventNames.h>
#include <LibWeb/HTML/GlobalEventHandlers.h>
namespace Web::HTML {
class HTMLElement : public DOM::Element {
class HTMLElement
: public DOM::Element
, public HTML::GlobalEventHandlers {
public:
using WrapperType = Bindings::HTMLElementWrapper;
@ -48,7 +52,13 @@ public:
bool cannot_navigate() const;
protected:
virtual void parse_attribute(const FlyString& name, const String& value) override;
private:
// ^HTML::GlobalEventHandlers
virtual EventTarget& global_event_handlers_to_event_target() override { return *this; }
enum class ContentEditableState {
True,
False,

View file

@ -8,4 +8,76 @@ interface HTMLElement : Element {
attribute DOMString contentEditable;
[LegacyNullToEmptyString] attribute DOMString innerText;
// FIXME: These should all come from a GlobalEventHandlers mixin
attribute EventHandler onabort;
attribute EventHandler onauxclick;
attribute EventHandler onblur;
attribute EventHandler oncancel;
attribute EventHandler oncanplay;
attribute EventHandler oncanplaythrough;
attribute EventHandler onchange;
attribute EventHandler onclick;
attribute EventHandler onclose;
attribute EventHandler oncontextmenu;
attribute EventHandler oncuechange;
attribute EventHandler ondblclick;
attribute EventHandler ondrag;
attribute EventHandler ondragend;
attribute EventHandler ondragenter;
attribute EventHandler ondragleave;
attribute EventHandler ondragover;
attribute EventHandler ondragstart;
attribute EventHandler ondrop;
attribute EventHandler ondurationchange;
attribute EventHandler onemptied;
attribute EventHandler onended;
// FIXME: Should be an OnErrorEventHandler
attribute EventHandler onerror;
attribute EventHandler onfocus;
attribute EventHandler onformdata;
attribute EventHandler oninput;
attribute EventHandler oninvalid;
attribute EventHandler onkeydown;
attribute EventHandler onkeypress;
attribute EventHandler onkeyup;
attribute EventHandler onload;
attribute EventHandler onloadeddata;
attribute EventHandler onloadedmetadata;
attribute EventHandler onloadstart;
attribute EventHandler onmousedown;
[LegacyLenientThis] attribute EventHandler onmouseenter;
[LegacyLenientThis] attribute EventHandler onmouseleave;
attribute EventHandler onmousemove;
attribute EventHandler onmouseout;
attribute EventHandler onmouseover;
attribute EventHandler onmouseup;
attribute EventHandler onpause;
attribute EventHandler onplay;
attribute EventHandler onplaying;
attribute EventHandler onprogress;
attribute EventHandler onratechange;
attribute EventHandler onreset;
attribute EventHandler onresize;
attribute EventHandler onscroll;
attribute EventHandler onsecuritypolicyviolation;
attribute EventHandler onseeked;
attribute EventHandler onseeking;
attribute EventHandler onselect;
attribute EventHandler onslotchange;
attribute EventHandler onstalled;
attribute EventHandler onsubmit;
attribute EventHandler onsuspend;
attribute EventHandler ontimeupdate;
attribute EventHandler ontoggle;
attribute EventHandler onvolumechange;
attribute EventHandler onwaiting;
attribute EventHandler onwebkitanimationend;
attribute EventHandler onwebkitanimationiteration;
attribute EventHandler onwebkitanimationstart;
attribute EventHandler onwebkittransitionend;
attribute EventHandler onwheel;
};