Serenity Operating System
1/*
2 * Copyright (c) 2020-2022, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2022, Luke Wilde <lukew@serenityos.org>
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include <AK/StringBuilder.h>
9#include <LibJS/Interpreter.h>
10#include <LibJS/Parser.h>
11#include <LibJS/Runtime/AbstractOperations.h>
12#include <LibJS/Runtime/ECMAScriptFunctionObject.h>
13#include <LibJS/Runtime/NativeFunction.h>
14#include <LibJS/Runtime/ObjectEnvironment.h>
15#include <LibJS/Runtime/VM.h>
16#include <LibWeb/Bindings/EventTargetPrototype.h>
17#include <LibWeb/Bindings/MainThreadVM.h>
18#include <LibWeb/DOM/AbortSignal.h>
19#include <LibWeb/DOM/DOMEventListener.h>
20#include <LibWeb/DOM/Document.h>
21#include <LibWeb/DOM/Event.h>
22#include <LibWeb/DOM/EventDispatcher.h>
23#include <LibWeb/DOM/EventTarget.h>
24#include <LibWeb/DOM/IDLEventListener.h>
25#include <LibWeb/HTML/ErrorEvent.h>
26#include <LibWeb/HTML/EventHandler.h>
27#include <LibWeb/HTML/EventNames.h>
28#include <LibWeb/HTML/FormAssociatedElement.h>
29#include <LibWeb/HTML/HTMLBodyElement.h>
30#include <LibWeb/HTML/HTMLFormElement.h>
31#include <LibWeb/HTML/HTMLFrameSetElement.h>
32#include <LibWeb/HTML/Window.h>
33#include <LibWeb/UIEvents/EventNames.h>
34#include <LibWeb/WebIDL/AbstractOperations.h>
35
36namespace Web::DOM {
37
38EventTarget::EventTarget(JS::Realm& realm)
39 : PlatformObject(realm)
40{
41}
42
43EventTarget::~EventTarget() = default;
44
45void EventTarget::visit_edges(Cell::Visitor& visitor)
46{
47 Base::visit_edges(visitor);
48
49 for (auto& event_listener : m_event_listener_list)
50 visitor.visit(event_listener.ptr());
51
52 for (auto& it : m_event_handler_map)
53 visitor.visit(it.value.ptr());
54}
55
56Vector<JS::Handle<DOMEventListener>> EventTarget::event_listener_list()
57{
58 Vector<JS::Handle<DOMEventListener>> list;
59 for (auto& listener : m_event_listener_list)
60 list.append(*listener);
61 return list;
62}
63
64// https://dom.spec.whatwg.org/#concept-flatten-options
65static bool flatten_event_listener_options(Variant<EventListenerOptions, bool> const& options)
66{
67 // 1. If options is a boolean, then return options.
68 if (options.has<bool>())
69 return options.get<bool>();
70
71 // 2. Return options["capture"].
72 return options.get<EventListenerOptions>().capture;
73}
74
75static bool flatten_event_listener_options(Variant<AddEventListenerOptions, bool> const& options)
76{
77 // 1. If options is a boolean, then return options.
78 if (options.has<bool>())
79 return options.get<bool>();
80
81 // 2. Return options["capture"].
82 return options.get<AddEventListenerOptions>().capture;
83}
84
85struct FlattenedAddEventListenerOptions {
86 bool capture { false };
87 bool passive { false };
88 bool once { false };
89 JS::GCPtr<AbortSignal> signal;
90};
91
92// https://dom.spec.whatwg.org/#event-flatten-more
93static FlattenedAddEventListenerOptions flatten_add_event_listener_options(Variant<AddEventListenerOptions, bool> const& options)
94{
95 // 1. Let capture be the result of flattening options.
96 bool capture = flatten_event_listener_options(options);
97
98 // 2. Let once and passive be false.
99 bool once = false;
100 bool passive = false;
101
102 // 3. Let signal be null.
103 JS::GCPtr<AbortSignal> signal;
104
105 // 4. If options is a dictionary, then:
106 if (options.has<AddEventListenerOptions>()) {
107 auto& add_event_listener_options = options.get<AddEventListenerOptions>();
108
109 // 1. Set passive to options["passive"] and once to options["once"].
110 passive = add_event_listener_options.passive;
111 once = add_event_listener_options.once;
112
113 // 2. If options["signal"] exists, then set signal to options["signal"].
114 if (add_event_listener_options.signal.has_value())
115 signal = add_event_listener_options.signal.value().ptr();
116 }
117
118 // 5. Return capture, passive, once, and signal.
119 return FlattenedAddEventListenerOptions { .capture = capture, .passive = passive, .once = once, .signal = signal.ptr() };
120}
121
122// https://dom.spec.whatwg.org/#dom-eventtarget-addeventlistener
123void EventTarget::add_event_listener(DeprecatedFlyString const& type, IDLEventListener* callback, Variant<AddEventListenerOptions, bool> const& options)
124{
125 // 1. Let capture, passive, once, and signal be the result of flattening more options.
126 auto flattened_options = flatten_add_event_listener_options(options);
127
128 // 2. Add an event listener with this and an event listener whose type is type, callback is callback, capture is capture, passive is passive,
129 // once is once, and signal is signal.
130
131 auto event_listener = heap().allocate_without_realm<DOMEventListener>();
132 event_listener->type = type;
133 event_listener->callback = callback;
134 event_listener->signal = move(flattened_options.signal);
135 event_listener->capture = flattened_options.capture;
136 event_listener->passive = flattened_options.passive;
137 event_listener->once = flattened_options.once;
138 add_an_event_listener(*event_listener);
139}
140
141void EventTarget::add_event_listener_without_options(DeprecatedFlyString const& type, IDLEventListener& callback)
142{
143 add_event_listener(type, &callback, AddEventListenerOptions {});
144}
145
146// https://dom.spec.whatwg.org/#add-an-event-listener
147void EventTarget::add_an_event_listener(DOMEventListener& listener)
148{
149 // FIXME: 1. If eventTarget is a ServiceWorkerGlobalScope object, its service worker’s script resource’s has ever been evaluated flag is set,
150 // and listener’s type matches the type attribute value of any of the service worker events, then report a warning to the console
151 // that this might not give the expected results. [SERVICE-WORKERS]
152
153 // 2. If listener’s signal is not null and is aborted, then return.
154 if (listener.signal && listener.signal->aborted())
155 return;
156
157 // 3. If listener’s callback is null, then return.
158 if (!listener.callback)
159 return;
160
161 // 4. If eventTarget’s event listener list does not contain an event listener whose type is listener’s type, callback is listener’s callback,
162 // and capture is listener’s capture, then append listener to eventTarget’s event listener list.
163 auto it = m_event_listener_list.find_if([&](auto& entry) {
164 return entry->type == listener.type
165 && &entry->callback->callback().callback == &listener.callback->callback().callback
166 && entry->capture == listener.capture;
167 });
168 if (it == m_event_listener_list.end())
169 m_event_listener_list.append(listener);
170
171 // 5. If listener’s signal is not null, then add the following abort steps to it:
172 if (listener.signal) {
173 // NOTE: `this` and `listener` are protected by AbortSignal using JS::SafeFunction.
174 listener.signal->add_abort_algorithm([this, &listener] {
175 // 1. Remove an event listener with eventTarget and listener.
176 remove_an_event_listener(listener);
177 });
178 }
179}
180
181// https://dom.spec.whatwg.org/#dom-eventtarget-removeeventlistener
182void EventTarget::remove_event_listener(DeprecatedFlyString const& type, IDLEventListener* callback, Variant<EventListenerOptions, bool> const& options)
183{
184 // 1. Let capture be the result of flattening options.
185 bool capture = flatten_event_listener_options(options);
186
187 // 2. If this’s event listener list contains an event listener whose type is type, callback is callback, and capture is capture,
188 // then remove an event listener with this and that event listener.
189 auto callbacks_match = [&](DOMEventListener& entry) {
190 if (!entry.callback && !callback)
191 return true;
192 if (!entry.callback || !callback)
193 return false;
194 return &entry.callback->callback().callback == &callback->callback().callback;
195 };
196 auto it = m_event_listener_list.find_if([&](auto& entry) {
197 return entry->type == type
198 && callbacks_match(*entry)
199 && entry->capture == capture;
200 });
201 if (it != m_event_listener_list.end())
202 remove_an_event_listener(**it);
203}
204
205void EventTarget::remove_event_listener_without_options(DeprecatedFlyString const& type, IDLEventListener& callback)
206{
207 remove_event_listener(type, &callback, EventListenerOptions {});
208}
209
210// https://dom.spec.whatwg.org/#remove-an-event-listener
211void EventTarget::remove_an_event_listener(DOMEventListener& listener)
212{
213 // FIXME: 1. If eventTarget is a ServiceWorkerGlobalScope object and its service worker’s set of event types to handle contains type,
214 // then report a warning to the console that this might not give the expected results. [SERVICE-WORKERS]
215
216 // 2. Set listener’s removed to true and remove listener from eventTarget’s event listener list.
217 listener.removed = true;
218 m_event_listener_list.remove_first_matching([&](auto& entry) { return entry.ptr() == &listener; });
219}
220
221void EventTarget::remove_from_event_listener_list(DOMEventListener& listener)
222{
223 m_event_listener_list.remove_first_matching([&](auto& entry) { return entry.ptr() == &listener; });
224}
225
226// https://dom.spec.whatwg.org/#dom-eventtarget-dispatchevent
227WebIDL::ExceptionOr<bool> EventTarget::dispatch_event_binding(Event& event)
228{
229 // 1. If event’s dispatch flag is set, or if its initialized flag is not set, then throw an "InvalidStateError" DOMException.
230 if (event.dispatched())
231 return WebIDL::InvalidStateError::create(realm(), "The event is already being dispatched.");
232
233 if (!event.initialized())
234 return WebIDL::InvalidStateError::create(realm(), "Cannot dispatch an uninitialized event.");
235
236 // 2. Initialize event’s isTrusted attribute to false.
237 event.set_is_trusted(false);
238
239 // 3. Return the result of dispatching event to this.
240 return dispatch_event(event);
241}
242
243// https://html.spec.whatwg.org/multipage/webappapis.html#window-reflecting-body-element-event-handler-set
244bool is_window_reflecting_body_element_event_handler(DeprecatedFlyString const& name)
245{
246 return name.is_one_of(
247 HTML::EventNames::blur,
248 HTML::EventNames::error,
249 HTML::EventNames::focus,
250 HTML::EventNames::load,
251 UIEvents::EventNames::resize,
252 "scroll");
253}
254
255// https://html.spec.whatwg.org/multipage/webappapis.html#windoweventhandlers
256static bool is_window_event_handler(DeprecatedFlyString const& name)
257{
258 return name.is_one_of(
259 HTML::EventNames::afterprint,
260 HTML::EventNames::beforeprint,
261 HTML::EventNames::beforeunload,
262 HTML::EventNames::hashchange,
263 HTML::EventNames::languagechange,
264 HTML::EventNames::message,
265 HTML::EventNames::messageerror,
266 HTML::EventNames::offline,
267 HTML::EventNames::online,
268 HTML::EventNames::pagehide,
269 HTML::EventNames::pageshow,
270 HTML::EventNames::popstate,
271 HTML::EventNames::rejectionhandled,
272 HTML::EventNames::storage,
273 HTML::EventNames::unhandledrejection,
274 HTML::EventNames::unload);
275}
276
277// https://html.spec.whatwg.org/multipage/webappapis.html#determining-the-target-of-an-event-handler
278static EventTarget* determine_target_of_event_handler(EventTarget& event_target, DeprecatedFlyString const& name)
279{
280 // To determine the target of an event handler, given an EventTarget object eventTarget on which the event handler is exposed,
281 // and an event handler name name, the following steps are taken:
282
283 // 1. If eventTarget is not a body element or a frameset element, then return eventTarget.
284 if (!is<HTML::HTMLBodyElement>(event_target) && !is<HTML::HTMLFrameSetElement>(event_target))
285 return &event_target;
286
287 auto& event_target_element = static_cast<HTML::HTMLElement&>(event_target);
288
289 // 2. If name is not the name of an attribute member of the WindowEventHandlers interface mixin and the Window-reflecting
290 // body element event handler set does not contain name, then return eventTarget.
291 if (!is_window_event_handler(name) && !is_window_reflecting_body_element_event_handler(name))
292 return &event_target;
293
294 // 3. If eventTarget's node document is not an active document, then return null.
295 if (!event_target_element.document().is_active())
296 return nullptr;
297
298 // 4. Return eventTarget's node document's relevant global object.
299 return &event_target_element.document().window();
300}
301
302// https://html.spec.whatwg.org/multipage/webappapis.html#event-handler-attributes:event-handler-idl-attributes-2
303WebIDL::CallbackType* EventTarget::event_handler_attribute(DeprecatedFlyString const& name)
304{
305 // 1. Let eventTarget be the result of determining the target of an event handler given this object and name.
306 auto target = determine_target_of_event_handler(*this, name);
307
308 // 2. If eventTarget is null, then return null.
309 if (!target)
310 return nullptr;
311
312 // 3. Return the result of getting the current value of the event handler given eventTarget and name.
313 return target->get_current_value_of_event_handler(name);
314}
315
316// https://html.spec.whatwg.org/multipage/webappapis.html#getting-the-current-value-of-the-event-handler
317WebIDL::CallbackType* EventTarget::get_current_value_of_event_handler(DeprecatedFlyString const& name)
318{
319 // 1. Let handlerMap be eventTarget's event handler map. (NOTE: Not necessary)
320
321 // 2. Let eventHandler be handlerMap[name].
322 auto event_handler_iterator = m_event_handler_map.find(name);
323
324 // Optimization: The spec creates all the event handlers exposed on an object up front and has the initial value of the handler set to null.
325 // If the event handler hasn't been set, null would be returned in step 4.
326 // However, this would be very allocation heavy. For example, each DOM::Element includes GlobalEventHandlers, which defines 60+(!) event handler attributes.
327 // Plus, the vast majority of these allocations would be likely wasted, as I imagine web content will only use a handful of these attributes on certain elements, if any at all.
328 // Thus, we treat the event handler not being in the event handler map as being equivalent to an event handler with an initial null value.
329 if (event_handler_iterator == m_event_handler_map.end())
330 return nullptr;
331
332 auto& event_handler = event_handler_iterator->value;
333
334 // 3. If eventHandler's value is an internal raw uncompiled handler, then:
335 if (event_handler->value.has<DeprecatedString>()) {
336 // 1. If eventTarget is an element, then let element be eventTarget, and document be element's node document.
337 // Otherwise, eventTarget is a Window object, let element be null, and document be eventTarget's associated Document.
338 JS::GCPtr<Element> element;
339 JS::GCPtr<Document> document;
340
341 if (is<Element>(this)) {
342 auto* element_event_target = verify_cast<Element>(this);
343 element = element_event_target;
344 document = &element_event_target->document();
345 } else {
346 VERIFY(is<HTML::Window>(this));
347 auto* window_event_target = verify_cast<HTML::Window>(this);
348 document = &window_event_target->associated_document();
349 }
350
351 VERIFY(document);
352
353 // 2. If scripting is disabled for document, then return null.
354 if (document->is_scripting_disabled())
355 return nullptr;
356
357 // 3. Let body be the uncompiled script body in eventHandler's value.
358 auto& body = event_handler->value.get<DeprecatedString>();
359
360 // FIXME: 4. Let location be the location where the script body originated, as given by eventHandler's value.
361
362 // 5. If element is not null and element has a form owner, let form owner be that form owner. Otherwise, let form owner be null.
363 JS::GCPtr<HTML::HTMLFormElement> form_owner;
364 if (is<HTML::FormAssociatedElement>(element.ptr())) {
365 auto* form_associated_element = dynamic_cast<HTML::FormAssociatedElement*>(element.ptr());
366 VERIFY(form_associated_element);
367
368 if (form_associated_element->form())
369 form_owner = form_associated_element->form();
370 }
371
372 // 6. Let settings object be the relevant settings object of document.
373 auto& settings_object = document->relevant_settings_object();
374
375 // NOTE: ECMAScriptFunctionObject::create expects a parsed body as input, so we must do the spec's sourceText steps here.
376 StringBuilder builder;
377
378 // sourceText
379 if (name == HTML::EventNames::error && is<HTML::Window>(this)) {
380 // -> If name is onerror and eventTarget is a Window object
381 // The string formed by concatenating "function ", name, "(event, source, lineno, colno, error) {", U+000A LF, body, U+000A LF, and "}".
382 builder.appendff("function {}(event, source, lineno, colno, error) {{\n{}\n}}", name, body);
383 } else {
384 // -> Otherwise
385 // The string formed by concatenating "function ", name, "(event) {", U+000A LF, body, U+000A LF, and "}".
386 builder.appendff("function {}(event) {{\n{}\n}}", name, body);
387 }
388
389 auto source_text = builder.to_deprecated_string();
390
391 auto parser = JS::Parser(JS::Lexer(source_text));
392
393 // FIXME: This should only be parsing the `body` instead of `source_text` and therefore use `JS::FunctionBody` instead of `JS::FunctionExpression`.
394 // However, JS::ECMAScriptFunctionObject::create wants parameters and length and JS::FunctionBody does not inherit JS::FunctionNode.
395 auto program = parser.parse_function_node<JS::FunctionExpression>();
396
397 // 7. If body is not parsable as FunctionBody or if parsing detects an early error, then follow these substeps:
398 if (parser.has_errors()) {
399 // 1. Set eventHandler's value to null.
400 // Note: This does not deactivate the event handler, which additionally removes the event handler's listener (if present).
401 m_event_handler_map.remove(event_handler_iterator);
402
403 // FIXME: 2. Report the error for the appropriate script and with the appropriate position (line number and column number) given by location, using settings object's global object.
404 // If the error is still not handled after this, then the error may be reported to a developer console.
405
406 // 3. Return null.
407 return nullptr;
408 }
409
410 auto& vm = Bindings::main_thread_vm();
411
412 // 8. Push settings object's realm execution context onto the JavaScript execution context stack; it is now the running JavaScript execution context.
413 vm.push_execution_context(settings_object.realm_execution_context());
414
415 // 9. Let function be the result of calling OrdinaryFunctionCreate, with arguments:
416 // functionPrototype
417 // %Function.prototype% (This is enforced by using JS::ECMAScriptFunctionObject)
418
419 // sourceText was handled above.
420
421 // ParameterList
422 // If name is onerror and eventTarget is a Window object
423 // Let the function have five arguments, named event, source, lineno, colno, and error.
424 // Otherwise
425 // Let the function have a single argument called event.
426 // (This was handled above for us by the parser using sourceText)
427
428 // body
429 // The result of parsing body above. (This is given by program->body())
430
431 // thisMode
432 // non-lexical-this (For JS::ECMAScriptFunctionObject, this means passing is_arrow_function as false)
433 constexpr bool is_arrow_function = false;
434
435 // scope
436 // 1. Let realm be settings object's Realm.
437 auto& realm = settings_object.realm();
438
439 // 2. Let scope be realm.[[GlobalEnv]].
440 auto scope = JS::NonnullGCPtr<JS::Environment> { realm.global_environment() };
441
442 // 3. If eventHandler is an element's event handler, then set scope to NewObjectEnvironment(document, true, scope).
443 // (Otherwise, eventHandler is a Window object's event handler.)
444 if (is<Element>(this))
445 scope = JS::new_object_environment(*document, true, scope);
446
447 // 4. If form owner is not null, then set scope to NewObjectEnvironment(form owner, true, scope).
448 if (form_owner)
449 scope = JS::new_object_environment(*form_owner, true, scope);
450
451 // 5. If element is not null, then set scope to NewObjectEnvironment(element, true, scope).
452 if (element)
453 scope = JS::new_object_environment(*element, true, scope);
454
455 // 6. Return scope. (NOTE: Not necessary)
456
457 auto function = JS::ECMAScriptFunctionObject::create(realm, name, builder.to_deprecated_string(), program->body(), program->parameters(), program->function_length(), scope, nullptr, JS::FunctionKind::Normal, program->is_strict_mode(), program->might_need_arguments_object(), is_arrow_function);
458
459 // 10. Remove settings object's realm execution context from the JavaScript execution context stack.
460 VERIFY(vm.execution_context_stack().last() == &settings_object.realm_execution_context());
461 vm.pop_execution_context();
462
463 // 11. Set function.[[ScriptOrModule]] to null.
464 function->set_script_or_module({});
465
466 // 12. Set eventHandler's value to the result of creating a Web IDL EventHandler callback function object whose object reference is function and whose callback context is settings object.
467 event_handler->value = realm.heap().allocate_without_realm<WebIDL::CallbackType>(*function, settings_object).ptr();
468 }
469
470 // 4. Return eventHandler's value.
471 VERIFY(event_handler->value.has<WebIDL::CallbackType*>());
472 return *event_handler->value.get_pointer<WebIDL::CallbackType*>();
473}
474
475// https://html.spec.whatwg.org/multipage/webappapis.html#event-handler-attributes:event-handler-idl-attributes-3
476void EventTarget::set_event_handler_attribute(DeprecatedFlyString const& name, WebIDL::CallbackType* value)
477{
478 // 1. Let eventTarget be the result of determining the target of an event handler given this object and name.
479 auto event_target = determine_target_of_event_handler(*this, name);
480
481 // 2. If eventTarget is null, then return.
482 if (!event_target)
483 return;
484
485 // 3. If the given value is null, then deactivate an event handler given eventTarget and name.
486 if (!value) {
487 event_target->deactivate_event_handler(name);
488 return;
489 }
490
491 // 4. Otherwise:
492 // 1. Let handlerMap be eventTarget's event handler map.
493 auto& handler_map = event_target->m_event_handler_map;
494
495 // 2. Let eventHandler be handlerMap[name].
496 auto event_handler_iterator = handler_map.find(name);
497
498 // 3. Set eventHandler's value to the given value.
499 if (event_handler_iterator == handler_map.end()) {
500 // NOTE: See the optimization comment in get_current_value_of_event_handler about why this is done.
501 auto new_event_handler = heap().allocate_without_realm<HTML::EventHandler>(*value);
502
503 // 4. Activate an event handler given eventTarget and name.
504 // Optimization: We pass in the event handler here instead of having activate_event_handler do another hash map lookup just to get the same object.
505 // This handles a new event handler while the other path handles an existing event handler. As such, both paths must have their own
506 // unique call to activate_event_handler.
507 event_target->activate_event_handler(name, *new_event_handler);
508
509 handler_map.set(name, new_event_handler);
510 return;
511 }
512
513 auto& event_handler = event_handler_iterator->value;
514
515 event_handler->value = value;
516
517 // 4. Activate an event handler given eventTarget and name.
518 // NOTE: See the optimization comment above.
519 event_target->activate_event_handler(name, *event_handler);
520}
521
522// https://html.spec.whatwg.org/multipage/webappapis.html#activate-an-event-handler
523void EventTarget::activate_event_handler(DeprecatedFlyString const& name, HTML::EventHandler& event_handler)
524{
525 // 1. Let handlerMap be eventTarget's event handler map.
526 // 2. Let eventHandler be handlerMap[name].
527 // NOTE: These are achieved by using the passed in event handler.
528
529 // 3. If eventHandler's listener is not null, then return.
530 if (event_handler.listener)
531 return;
532
533 JS::Realm& realm = shape().realm();
534
535 // 4. Let callback be the result of creating a Web IDL EventListener instance representing a reference to a function of one argument that executes the steps of the event handler processing algorithm, given eventTarget, name, and its argument.
536 // The EventListener's callback context can be arbitrary; it does not impact the steps of the event handler processing algorithm. [DOM]
537
538 // NOTE: The callback must keep `this` alive. For example:
539 // document.body.onunload = () => { console.log("onunload called!"); }
540 // document.body.remove();
541 // location.reload();
542 // The body element is no longer in the DOM and there is no variable holding onto it. However, the onunload handler is still called, meaning the callback keeps the body element alive.
543 auto callback_function = JS::NativeFunction::create(
544 realm, [event_target = JS::make_handle(*this), name](JS::VM& vm) mutable -> JS::ThrowCompletionOr<JS::Value> {
545 // The event dispatcher should only call this with one argument.
546 VERIFY(vm.argument_count() == 1);
547
548 // The argument must be an object and it must be an Event.
549 auto event_wrapper_argument = vm.argument(0);
550 VERIFY(event_wrapper_argument.is_object());
551 auto& event = verify_cast<DOM::Event>(event_wrapper_argument.as_object());
552
553 TRY(event_target->process_event_handler_for_event(name, event));
554 return JS::js_undefined();
555 },
556 0, "", &realm);
557
558 // NOTE: As per the spec, the callback context is arbitrary.
559 auto callback = realm.heap().allocate_without_realm<WebIDL::CallbackType>(*callback_function, Bindings::host_defined_environment_settings_object(realm));
560
561 // 5. Let listener be a new event listener whose type is the event handler event type corresponding to eventHandler and callback is callback.
562 auto listener = realm.heap().allocate_without_realm<DOMEventListener>();
563 listener->type = name;
564 listener->callback = IDLEventListener::create(realm, *callback).release_value_but_fixme_should_propagate_errors();
565
566 // 6. Add an event listener with eventTarget and listener.
567 add_an_event_listener(*listener);
568
569 // 7. Set eventHandler's listener to listener.
570 event_handler.listener = listener;
571}
572
573void EventTarget::deactivate_event_handler(DeprecatedFlyString const& name)
574{
575 // 1. Let handlerMap be eventTarget's event handler map. (NOTE: Not necessary)
576
577 // 2. Let eventHandler be handlerMap[name].
578 auto event_handler_iterator = m_event_handler_map.find(name);
579
580 // NOTE: See the optimization comment in get_current_value_of_event_handler about why this is done.
581 if (event_handler_iterator == m_event_handler_map.end())
582 return;
583
584 auto& event_handler = event_handler_iterator->value;
585
586 // 4. Let listener be eventHandler's listener. (NOTE: Not necessary)
587
588 // 5. If listener is not null, then remove an event listener with eventTarget and listener.
589 if (event_handler->listener) {
590 remove_an_event_listener(*event_handler->listener);
591 }
592
593 // 6. Set eventHandler's listener to null.
594 event_handler->listener = nullptr;
595
596 // 3. Set eventHandler's value to null.
597 // NOTE: This is done out of order since our equivalent of setting value to null is removing the event handler from the map.
598 // Given that event_handler is a reference to an entry, this would invalidate event_handler if we did it in order.
599 m_event_handler_map.remove(event_handler_iterator);
600}
601
602// https://html.spec.whatwg.org/multipage/webappapis.html#the-event-handler-processing-algorithm
603JS::ThrowCompletionOr<void> EventTarget::process_event_handler_for_event(DeprecatedFlyString const& name, Event& event)
604{
605 // 1. Let callback be the result of getting the current value of the event handler given eventTarget and name.
606 auto* callback = get_current_value_of_event_handler(name);
607
608 // 2. If callback is null, then return.
609 if (!callback)
610 return {};
611
612 // 3. Let special error event handling be true if event is an ErrorEvent object, event's type is error, and event's currentTarget implements the WindowOrWorkerGlobalScope mixin.
613 // Otherwise, let special error event handling be false.
614 // FIXME: This doesn't check for WorkerGlobalScape as we don't currently have it.
615 bool special_error_event_handling = is<HTML::ErrorEvent>(event) && event.type() == HTML::EventNames::error && is<HTML::Window>(event.current_target().ptr());
616
617 // 4. Process the Event object event as follows:
618 JS::Completion return_value_or_error;
619
620 if (special_error_event_handling) {
621 // -> If special error event handling is true
622 // Invoke callback with five arguments, the first one having the value of event's message attribute, the second having the value of event's filename attribute, the third having the value of event's lineno attribute,
623 // the fourth having the value of event's colno attribute, the fifth having the value of event's error attribute, and with the callback this value set to event's currentTarget.
624 // Let return value be the callback's return value. [WEBIDL]
625 auto& error_event = verify_cast<HTML::ErrorEvent>(event);
626 auto wrapped_message = JS::PrimitiveString::create(vm(), error_event.message());
627 auto wrapped_filename = JS::PrimitiveString::create(vm(), error_event.filename());
628 auto wrapped_lineno = JS::Value(error_event.lineno());
629 auto wrapped_colno = JS::Value(error_event.colno());
630
631 // NOTE: error_event.error() is a JS::Value, so it does not require wrapping.
632
633 // NOTE: current_target is always non-null here, as the event dispatcher takes care to make sure it's non-null (and uses it as the this value for the callback!)
634 // FIXME: This is rewrapping the this value of the callback defined in activate_event_handler. While I don't think this is observable as the event dispatcher
635 // calls directly into the callback without considering things such as proxies, it is a waste. However, if it observable, then we must reuse the this_value that was given to the callback.
636 auto* this_value = error_event.current_target().ptr();
637
638 return_value_or_error = WebIDL::invoke_callback(*callback, this_value, wrapped_message, wrapped_filename, wrapped_lineno, wrapped_colno, error_event.error());
639 } else {
640 // -> Otherwise
641 // Invoke callback with one argument, the value of which is the Event object event, with the callback this value set to event's currentTarget. Let return value be the callback's return value. [WEBIDL]
642
643 // FIXME: This has the same rewrapping issue as this_value.
644 auto* wrapped_event = &event;
645
646 // FIXME: The comments about this in the special_error_event_handling path also apply here.
647 auto* this_value = event.current_target().ptr();
648
649 return_value_or_error = WebIDL::invoke_callback(*callback, this_value, wrapped_event);
650 }
651
652 // If an exception gets thrown by the callback, end these steps and allow the exception to propagate. (It will propagate to the DOM event dispatch logic, which will then report the exception.)
653 if (return_value_or_error.is_error())
654 return return_value_or_error.release_error();
655
656 // FIXME: Ideally, invoke_callback would convert JS::Value to the appropriate return type for us as per the spec, but it doesn't currently.
657 auto return_value = *return_value_or_error.value();
658
659 // FIXME: If event is a BeforeUnloadEvent object and event's type is beforeunload
660 // If return value is not null, then: (NOTE: When implementing, if we still return a JS::Value from invoke_callback, use is_nullish instead of is_null, as "null" refers to IDL null, which is JS null or undefined)
661 // 1. Set event's canceled flag.
662 // 2. If event's returnValue attribute's value is the empty string, then set event's returnValue attribute's value to return value.
663
664 if (special_error_event_handling) {
665 // -> If special error event handling is true
666 // If return value is true, then set event's canceled flag.
667 // NOTE: the return type of EventHandler is `any`, so no coercion happens, meaning we have to check if it's a boolean first.
668 if (return_value.is_boolean() && return_value.as_bool())
669 event.set_cancelled(true);
670 } else {
671 // -> Otherwise
672 // If return value is false, then set event's canceled flag.
673 // NOTE: the return type of EventHandler is `any`, so no coercion happens, meaning we have to check if it's a boolean first.
674 if (return_value.is_boolean() && !return_value.as_bool())
675 event.set_cancelled(true);
676 }
677
678 return {};
679}
680
681// https://html.spec.whatwg.org/multipage/webappapis.html#event-handler-attributes:concept-element-attributes-change-ext
682void EventTarget::element_event_handler_attribute_changed(DeprecatedFlyString const& local_name, DeprecatedString const& value)
683{
684 // NOTE: Step 1 of this algorithm was handled in HTMLElement::parse_attribute.
685
686 // 2. Let eventTarget be the result of determining the target of an event handler given element and localName.
687 // NOTE: element is `this`.
688 auto* event_target = determine_target_of_event_handler(*this, local_name);
689
690 // 3. If eventTarget is null, then return.
691 if (!event_target)
692 return;
693
694 // 4. If value is null, then deactivate an event handler given eventTarget and localName.
695 if (value.is_null()) {
696 event_target->deactivate_event_handler(local_name);
697 return;
698 }
699
700 // 5. Otherwise:
701 // FIXME: 1. If the Should element's inline behavior be blocked by Content Security Policy? algorithm returns "Blocked" when executed upon element, "script attribute", and value, then return. [CSP]
702
703 // 2. Let handlerMap be eventTarget's event handler map.
704 auto& handler_map = event_target->m_event_handler_map;
705
706 // 3. Let eventHandler be handlerMap[localName].
707 auto event_handler_iterator = handler_map.find(local_name);
708
709 // FIXME: 4. Let location be the script location that triggered the execution of these steps.
710
711 // FIXME: 5. Set eventHandler's value to the internal raw uncompiled handler value/location.
712 // (This currently sets the value to the uncompiled source code instead of the named struct)
713
714 // NOTE: See the optimization comments in set_event_handler_attribute.
715
716 if (event_handler_iterator == handler_map.end()) {
717 auto new_event_handler = heap().allocate_without_realm<HTML::EventHandler>(value);
718
719 // 6. Activate an event handler given eventTarget and name.
720 event_target->activate_event_handler(local_name, *new_event_handler);
721
722 handler_map.set(local_name, new_event_handler);
723 return;
724 }
725
726 auto& event_handler = event_handler_iterator->value;
727
728 // 6. Activate an event handler given eventTarget and name.
729 event_handler->value = value;
730 event_target->activate_event_handler(local_name, *event_handler);
731}
732
733bool EventTarget::dispatch_event(Event& event)
734{
735 return EventDispatcher::dispatch(*this, event);
736}
737
738bool EventTarget::has_event_listener(DeprecatedFlyString const& type) const
739{
740 for (auto& listener : m_event_listener_list) {
741 if (listener->type == type)
742 return true;
743 }
744 return false;
745}
746
747bool EventTarget::has_event_listeners() const
748{
749 return !m_event_listener_list.is_empty();
750}
751
752}