LibWeb: Implement CloseWatcher API

This implements most of the CloseWatcher API from the html spec.

AbortSignal support is unimplemented.

Integration with dialogs and popovers is also unimplemented.
This commit is contained in:
Luke Warlow 2024-06-21 22:53:05 +01:00 committed by Andreas Kling
parent 527632f416
commit b216046234
Notes: sideshowbarker 2024-07-17 07:31:31 +09:00
15 changed files with 432 additions and 3 deletions

View file

@ -42,6 +42,7 @@ static bool is_platform_object(Type const& type)
"CanvasGradient"sv,
"CanvasPattern"sv,
"CanvasRenderingContext2D"sv,
"CloseWatcher"sv,
"CryptoKey"sv,
"Document"sv,
"DocumentType"sv,

View file

@ -22,6 +22,8 @@ source_set("HTML") {
"CanvasPattern.cpp",
"CanvasRenderingContext2D.cpp",
"CloseEvent.cpp",
"CloseWatcher.cpp",
"CloseWatcherManager.cpp",
"DOMParser.cpp",
"DOMStringMap.cpp",
"DataTransfer.cpp",

View file

@ -114,6 +114,7 @@ standard_idl_files = [
"//Userland/Libraries/LibWeb/HTML/CanvasPattern.idl",
"//Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.idl",
"//Userland/Libraries/LibWeb/HTML/CloseEvent.idl",
"//Userland/Libraries/LibWeb/HTML/CloseWatcher.idl",
"//Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementRegistry.idl",
"//Userland/Libraries/LibWeb/HTML/DataTransfer.idl",
"//Userland/Libraries/LibWeb/HTML/DOMParser.idl",

View file

@ -259,6 +259,8 @@ set(SOURCES
HTML/CanvasPattern.cpp
HTML/CanvasRenderingContext2D.cpp
HTML/CloseEvent.cpp
HTML/CloseWatcher.cpp
HTML/CloseWatcherManager.cpp
HTML/CORSSettingAttribute.cpp
HTML/CrossOrigin/AbstractOperations.cpp
HTML/CrossOrigin/Reporting.cpp

View file

@ -22,6 +22,7 @@
#include <LibWeb/DOM/EventDispatcher.h>
#include <LibWeb/DOM/EventTarget.h>
#include <LibWeb/DOM/IDLEventListener.h>
#include <LibWeb/HTML/CloseWatcherManager.h>
#include <LibWeb/HTML/ErrorEvent.h>
#include <LibWeb/HTML/EventHandler.h>
#include <LibWeb/HTML/EventNames.h>
@ -796,7 +797,9 @@ bool EventTarget::dispatch_event(Event& event)
// FIXME: 3. Extend windows with the active window of each of document's ancestor navigables.
// FIXME: 4. Extend windows with the active window of each of document's descendant navigables,
// filtered to include only those navigables whose active document's origin is same origin with document's origin.
// FIXME: 5. For each window in windows, set window's last activation timestamp to the current high resolution time.
// FIXME: 5. For each window in windows:
// FIXME: 5.1 Set window's last activation timestamp to the current high resolution time.
// FIXME: 5.2 Notify the close watcher manager about user activation given window.
// FIXME: This is ad-hoc, but works for now.
if (is_activation_triggering_input_event()) {
@ -804,11 +807,15 @@ bool EventTarget::dispatch_event(Event& event)
auto current_time = HighResolutionTime::relative_high_resolution_time(unsafe_shared_time, realm().global_object());
if (is<HTML::Window>(this)) {
static_cast<HTML::Window*>(this)->set_last_activation_timestamp(current_time);
auto* window = static_cast<HTML::Window*>(this);
window->set_last_activation_timestamp(current_time);
window->close_watcher_manager()->notify_about_user_activation();
} else if (is<DOM::Element>(this)) {
auto const* element = static_cast<DOM::Element const*>(this);
if (auto window = element->document().window())
if (auto window = element->document().window()) {
window->set_last_activation_timestamp(current_time);
window->close_watcher_manager()->notify_about_user_activation();
}
}
}

View file

@ -344,6 +344,8 @@ class BrowsingContextGroup;
class CanvasRenderingContext2D;
class ClassicScript;
class CloseEvent;
class CloseWatcher;
class CloseWatcherManager;
class CustomElementDefinition;
class CustomElementRegistry;
class DecodedImageData;

View file

@ -0,0 +1,167 @@
/*
* Copyright (c) 2024, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/TypeCasts.h>
#include <LibWeb/Bindings/CloseWatcherPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/EventDispatcher.h>
#include <LibWeb/DOM/IDLEventListener.h>
#include <LibWeb/HTML/CloseWatcher.h>
#include <LibWeb/HTML/CloseWatcherManager.h>
#include <LibWeb/HTML/EventHandler.h>
#include <LibWeb/HTML/Window.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(CloseWatcher);
// https://html.spec.whatwg.org/multipage/interaction.html#establish-a-close-watcher
JS::NonnullGCPtr<CloseWatcher> CloseWatcher::establish(HTML::Window& window)
{
// 1. Assert: window's associated Document is fully active.
VERIFY(window.associated_document().is_fully_active());
// 2. Let closeWatcher be a new close watcher
auto close_watcher = window.heap().allocate<CloseWatcher>(window.realm(), window.realm());
// 3. Let manager be window's associated close watcher manager
auto manager = window.close_watcher_manager();
// 4 - 6. Moved to CloseWatcherManager::add
manager->add(close_watcher);
// 7. Return close_watcher.
return close_watcher;
}
// https://html.spec.whatwg.org/multipage/interaction.html#dom-closewatcher
WebIDL::ExceptionOr<JS::NonnullGCPtr<CloseWatcher>> CloseWatcher::construct_impl(JS::Realm& realm, CloseWatcherOptions const& options)
{
// 1. If this's relevant global object's associated Document is not fully active, then return an "InvalidStateError" DOMException.
// FIXME: Not in spec explicitly, but this should account for detached iframes too. See /close-watcher/frame-removal.html WPT.
auto& window = verify_cast<HTML::Window>(realm.global_object());
if (!window.associated_document().is_fully_active())
return WebIDL::InvalidStateError::create(realm, "The document is not fully active."_fly_string);
// 2. Let close_watcher be the result of establishing a close watcher
auto close_watcher = establish(window);
// 3. If options["signal"] exists, then:
if (options.signal) {
// FIXME: 3.1 If options["signal"]'s aborted, then destroy closeWatcher.
// FIXME: 3.2 Add the following steps to options["signal"]:
}
return close_watcher;
}
CloseWatcher::CloseWatcher(JS::Realm& realm)
: DOM::EventTarget(realm)
{
}
// https://html.spec.whatwg.org/multipage/interaction.html#close-watcher-request-close
bool CloseWatcher::request_close()
{
// 1. If closeWatcher is not active, then return.
if (!m_is_active)
return true;
// 2. If closeWatcher's is running cancel action is true, then return true.
if (m_is_running_cancel_action)
return true;
// 3. Let window be closeWatcher's window.
auto& window = verify_cast<HTML::Window>(realm().global_object());
// 4. If window's associated Document is not fully active, then return true.
if (!window.associated_document().is_fully_active())
return true;
// 5. Let canPreventClose be true if window's close watcher manager's groups's size is less than window's close watcher manager's allowed number of groups,
// and window has history-action activation; otherwise false.
auto manager = window.close_watcher_manager();
bool can_prevent_close = manager->can_prevent_close() && window.has_history_action_activation();
// 6. Set closeWatcher's is running cancel action to true.
m_is_running_cancel_action = true;
// 7. Let shouldContinue be the result of running closeWatcher's cancel action given canPreventClose.
bool should_continue = dispatch_event(DOM::Event::create(realm(), HTML::EventNames::cancel, { .cancelable = can_prevent_close }));
// 8. Set closeWatcher's is running cancel action to false.
m_is_running_cancel_action = false;
// 9. If shouldContinue is false, then:
if (!should_continue) {
// 9.1 Assert: canPreventClose is true.
VERIFY(can_prevent_close);
// 9.2 Consume history-action user activation given window.
window.consume_history_action_user_activation();
return false;
}
// 10. Close closeWatcher.
close();
// 11. Return true.
return true;
}
// https://html.spec.whatwg.org/multipage/interaction.html#close-watcher-close
void CloseWatcher::close()
{
// 1. If closeWatcher is not active, then return.
if (!m_is_active)
return;
// 2. If closeWatcher's window's associated Document is not fully active, then return.
if (!verify_cast<HTML::Window>(realm().global_object()).associated_document().is_fully_active())
return;
// 3. Destroy closeWatcher.
destroy();
// 4. Run closeWatcher's close action.
dispatch_event(DOM::Event::create(realm(), HTML::EventNames::close));
}
// https://html.spec.whatwg.org/multipage/interaction.html#close-watcher-destroy
void CloseWatcher::destroy()
{
// 1. Let manager be closeWatcher's window's close watcher manager.
auto manager = verify_cast<HTML::Window>(realm().global_object()).close_watcher_manager();
// 2-3. Moved to CloseWatcherManager::remove
manager->remove(*this);
m_is_active = false;
}
void CloseWatcher::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(CloseWatcher);
}
void CloseWatcher::set_oncancel(WebIDL::CallbackType* event_handler)
{
set_event_handler_attribute(HTML::EventNames::cancel, event_handler);
}
WebIDL::CallbackType* CloseWatcher::oncancel()
{
return event_handler_attribute(HTML::EventNames::cancel);
}
void CloseWatcher::set_onclose(WebIDL::CallbackType* event_handler)
{
set_event_handler_attribute(HTML::EventNames::close, event_handler);
}
WebIDL::CallbackType* CloseWatcher::onclose()
{
return event_handler_attribute(HTML::EventNames::close);
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2024, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Forward.h>
#include <LibWeb/DOM/EventTarget.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/interaction.html#closewatcheroptions
struct CloseWatcherOptions {
JS::GCPtr<DOM::AbortSignal> signal;
};
// https://html.spec.whatwg.org/multipage/interaction.html#the-closewatcher-interface
class CloseWatcher final : public DOM::EventTarget {
WEB_PLATFORM_OBJECT(CloseWatcher, DOM::EventTarget);
JS_DECLARE_ALLOCATOR(CloseWatcher);
public:
static WebIDL::ExceptionOr<JS::NonnullGCPtr<CloseWatcher>> construct_impl(JS::Realm&, CloseWatcherOptions const& = {});
bool request_close();
void close();
void destroy();
virtual ~CloseWatcher() override = default;
void set_oncancel(WebIDL::CallbackType*);
WebIDL::CallbackType* oncancel();
void set_onclose(WebIDL::CallbackType*);
WebIDL::CallbackType* onclose();
private:
CloseWatcher(JS::Realm&);
[[nodiscard]] static JS::NonnullGCPtr<CloseWatcher> establish(HTML::Window&);
virtual void initialize(JS::Realm&) override;
bool m_is_running_cancel_action { false };
bool m_is_active { true };
};
}

View file

@ -0,0 +1,19 @@
#import <DOM/EventTarget.idl>
#import <DOM/EventHandler.idl>
// https://html.spec.whatwg.org/multipage/interaction.html#closewatcher
[Exposed=Window]
interface CloseWatcher : EventTarget {
constructor(optional CloseWatcherOptions options = {});
undefined requestClose();
undefined close();
undefined destroy();
attribute EventHandler oncancel;
attribute EventHandler onclose;
};
dictionary CloseWatcherOptions {
AbortSignal signal;
};

View file

@ -0,0 +1,120 @@
/*
* Copyright (c) 2024, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/TypeCasts.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/EventDispatcher.h>
#include <LibWeb/DOM/IDLEventListener.h>
#include <LibWeb/HTML/CloseWatcher.h>
#include <LibWeb/HTML/CloseWatcherManager.h>
#include <LibWeb/HTML/Window.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(CloseWatcherManager);
JS::NonnullGCPtr<CloseWatcherManager> CloseWatcherManager::create(JS::Realm& realm)
{
return realm.heap().allocate<CloseWatcherManager>(realm, realm);
}
CloseWatcherManager::CloseWatcherManager(JS::Realm& realm)
: PlatformObject(realm)
{
}
void CloseWatcherManager::add(JS::NonnullGCPtr<CloseWatcher> close_watcher)
{
// If manager's groups's size is less than manager's allowed number of groups
if (m_groups.size() < m_allowed_number_of_groups) {
// then append « closeWatcher » to manager's groups.
JS::MarkedVector<JS::NonnullGCPtr<CloseWatcher>> new_group(realm().heap());
new_group.append(close_watcher);
m_groups.append(move(new_group));
} else {
// Assert: manager's groups's size is at least 1 in this branch, since manager's allowed number of groups is always at least 1.
VERIFY(!m_groups.is_empty());
// Append closeWatcher to manager's groups's last item.
m_groups.last().append(close_watcher);
}
// Set manager's next user interaction allows a new group to true.
m_next_user_interaction_allows_a_new_group = true;
}
void CloseWatcherManager::remove(CloseWatcher const& close_watcher)
{
// 2. For each group of manager's groups: remove closeWatcher from group
for (auto& group : m_groups) {
group.remove_first_matching([&close_watcher](JS::NonnullGCPtr<CloseWatcher>& entry) {
return entry.ptr() == &close_watcher;
});
}
// 3. Remove any item from manager's group that is empty
m_groups.remove_all_matching([](auto& group) { return group.is_empty(); });
}
// https://html.spec.whatwg.org/multipage/interaction.html#process-close-watchers
bool CloseWatcherManager::process_close_watchers()
{
// 1. Let processedACloseWatcher be false.
bool processed_a_close_watcher = false;
// 2. If window's close watcher manager's groups is not empty:
if (!m_groups.is_empty()) {
// 2.1 Let group be the last item in window's close watcher manager's groups.
auto& group = m_groups.last();
// Ambiguous spec wording. We copy the groups to avoid modifying the original while iterating.
// See https://github.com/whatwg/html/issues/10240
JS::MarkedVector<JS::NonnullGCPtr<CloseWatcher>> group_copy(realm().heap());
group_copy.ensure_capacity(group.size());
for (auto& close_watcher : group) {
group_copy.append(close_watcher);
}
// 2.2 For each closeWatcher of group, in reverse order:
for (auto it = group_copy.rbegin(); it != group_copy.rend(); ++it) {
// 2.1.1 Set processedACloseWatcher to true.
processed_a_close_watcher = true;
// 2.1.2 Let shouldProceed be the result of requesting to close closeWatcher.
bool should_proceed = (*it)->request_close();
// 2.1.3 If shouldProceed is false, then break;
if (!should_proceed)
break;
}
}
// 3. If window's close watcher manager's allowed number of groups is greater than 1, decrement it by 1.
if (m_allowed_number_of_groups > 1)
m_allowed_number_of_groups--;
// 4. Return processedACloseWatcher.
return processed_a_close_watcher;
}
// https://html.spec.whatwg.org/multipage/interaction.html#notify-the-close-watcher-manager-about-user-activation
void CloseWatcherManager::notify_about_user_activation()
{
// 1. Let manager be window's close watcher manager.
// 2. If manager's next user interaction allows a new group is true, then increment manager's allowed number of groups.
if (m_next_user_interaction_allows_a_new_group)
m_allowed_number_of_groups++;
// 3. Set manager's next user interaction allows a new group to false.
m_next_user_interaction_allows_a_new_group = false;
}
// https://html.spec.whatwg.org/multipage/interaction.html#close-watcher-request-close
bool CloseWatcherManager::can_prevent_close()
{
// 5. Let canPreventClose be true if window's close watcher manager's groups's size is less than window's close watcher manager's allowed number of groups...
return m_groups.size() < m_allowed_number_of_groups;
}
void CloseWatcherManager::visit_edges(JS::Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_groups);
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2024, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Forward.h>
#include <LibWeb/DOM/EventTarget.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/interaction.html#close-watcher-manager
class CloseWatcherManager final : public Bindings::PlatformObject {
WEB_PLATFORM_OBJECT(CloseWatcherManager, Bindings::PlatformObject);
JS_DECLARE_ALLOCATOR(CloseWatcherManager);
public:
[[nodiscard]] static JS::NonnullGCPtr<CloseWatcherManager> create(JS::Realm&);
void add(JS::NonnullGCPtr<CloseWatcher>);
void remove(CloseWatcher const&);
bool process_close_watchers();
void notify_about_user_activation();
bool can_prevent_close();
private:
explicit CloseWatcherManager(JS::Realm&);
virtual void visit_edges(Cell::Visitor&) override;
Vector<Vector<JS::NonnullGCPtr<CloseWatcher>>> m_groups;
uint32_t m_allowed_number_of_groups { 1 };
bool m_next_user_interaction_allows_a_new_group { true };
};
}

View file

@ -33,6 +33,7 @@
#include <LibWeb/DOM/HTMLCollection.h>
#include <LibWeb/DOMURL/DOMURL.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/CloseWatcherManager.h>
#include <LibWeb/HTML/CustomElements/CustomElementRegistry.h>
#include <LibWeb/HTML/DocumentState.h>
#include <LibWeb/HTML/EventHandler.h>
@ -129,6 +130,7 @@ void Window::visit_edges(JS::Cell::Visitor& visitor)
visitor.visit(m_pdf_viewer_mime_type_objects);
visitor.visit(m_count_queuing_strategy_size_function);
visitor.visit(m_byte_length_queuing_strategy_size_function);
visitor.visit(m_close_watcher_manager);
}
void Window::finalize()
@ -973,6 +975,16 @@ JS::NonnullGCPtr<Navigator> Window::navigator()
return JS::NonnullGCPtr { *m_navigator };
}
// https://html.spec.whatwg.org/multipage/interaction.html#close-watcher-manager
JS::NonnullGCPtr<CloseWatcherManager> Window::close_watcher_manager()
{
auto& realm = this->realm();
if (!m_close_watcher_manager)
m_close_watcher_manager = heap().allocate<CloseWatcherManager>(realm, realm);
return JS::NonnullGCPtr { *m_close_watcher_manager };
}
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-alert
void Window::alert(String const& message)
{

View file

@ -162,6 +162,7 @@ public:
WebIDL::ExceptionOr<JS::GCPtr<WindowProxy>> open(Optional<String> const& url, Optional<String> const& target, Optional<String> const& features);
[[nodiscard]] JS::NonnullGCPtr<Navigator> navigator();
[[nodiscard]] JS::NonnullGCPtr<CloseWatcherManager> close_watcher_manager();
void alert(String const& message = {});
bool confirm(Optional<String> const& message);
@ -269,6 +270,7 @@ private:
JS::GCPtr<CSS::Screen> m_screen;
JS::GCPtr<Navigator> m_navigator;
JS::GCPtr<Location> m_location;
JS::GCPtr<CloseWatcherManager> m_close_watcher_manager;
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window-navigation-api
JS::GCPtr<Navigation> m_navigation;

View file

@ -8,6 +8,7 @@
#include <LibWeb/DOM/Range.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/CloseWatcherManager.h>
#include <LibWeb/HTML/Focus.h>
#include <LibWeb/HTML/HTMLAnchorElement.h>
#include <LibWeb/HTML/HTMLFormElement.h>
@ -778,6 +779,9 @@ bool EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u32 code
return focus_next_element();
}
if (key == UIEvents::KeyCode::Key_Escape)
return document->window()->close_watcher_manager()->process_close_watchers();
auto& realm = document->realm();
if (auto selection = document->get_selection()) {

View file

@ -98,6 +98,7 @@ libweb_js_bindings(HTML/CanvasGradient)
libweb_js_bindings(HTML/CanvasPattern)
libweb_js_bindings(HTML/CanvasRenderingContext2D)
libweb_js_bindings(HTML/CloseEvent)
libweb_js_bindings(HTML/CloseWatcher)
libweb_js_bindings(HTML/CustomElements/CustomElementRegistry)
libweb_js_bindings(HTML/DOMParser)
libweb_js_bindings(HTML/DOMStringMap)