From d1aea878896d77f4db1059285331837377e9ad3c Mon Sep 17 00:00:00 2001 From: Andrew Kaster Date: Wed, 23 Aug 2023 18:40:42 -0600 Subject: [PATCH] LibWeb: Add NavigateEvent, the main event of the Navigation API This event is the star of the show, and the main way that web content can react to either programmatic or user-initiated navigation. All of the fun algorithms will have to come later though. --- .../BindingsGenerator/IDLGenerators.cpp | 1 + .../Userland/Libraries/LibWeb/HTML/BUILD.gn | 1 + .../Userland/Libraries/LibWeb/idl_files.gni | 1 + Userland/Libraries/LibWeb/CMakeLists.txt | 1 + Userland/Libraries/LibWeb/Forward.h | 2 + .../Libraries/LibWeb/HTML/NavigateEvent.cpp | 197 ++++++++++++++++++ .../Libraries/LibWeb/HTML/NavigateEvent.h | 148 +++++++++++++ .../Libraries/LibWeb/HTML/NavigateEvent.idl | 56 +++++ Userland/Libraries/LibWeb/idl_files.cmake | 1 + 9 files changed, 408 insertions(+) create mode 100644 Userland/Libraries/LibWeb/HTML/NavigateEvent.cpp create mode 100644 Userland/Libraries/LibWeb/HTML/NavigateEvent.h create mode 100644 Userland/Libraries/LibWeb/HTML/NavigateEvent.idl diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp index fc1d887e5c..d024927297 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp @@ -44,6 +44,7 @@ static bool is_platform_object(Type const& type) "Module"sv, "MutationRecord"sv, "NamedNodeMap"sv, + "NavigationDestination"sv, "NavigationHistoryEntry"sv, "Node"sv, "Path2D"sv, diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn index 8d27c49d76..db329619f7 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn @@ -123,6 +123,7 @@ source_set("HTML") { "MimeTypeArray.cpp", "Navigable.cpp", "NavigableContainer.cpp", + "NavigateEvent.cpp", "Navigation.cpp", "NavigationCurrentEntryChangeEvent.cpp", "NavigationDestination.cpp", diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni b/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni index 2fc876085b..6bcc8611dc 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni @@ -182,6 +182,7 @@ standard_idl_files = [ "//Userland/Libraries/LibWeb/HTML/MessagePort.idl", "//Userland/Libraries/LibWeb/HTML/MimeType.idl", "//Userland/Libraries/LibWeb/HTML/MimeTypeArray.idl", + "//Userland/Libraries/LibWeb/HTML/NavigateEvent.idl", "//Userland/Libraries/LibWeb/HTML/Navigation.idl", "//Userland/Libraries/LibWeb/HTML/NavigationCurrentEntryChangeEvent.idl", "//Userland/Libraries/LibWeb/HTML/NavigationDestination.idl", diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 03d0855bb1..32b6293aca 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -350,6 +350,7 @@ set(SOURCES HTML/MimeTypeArray.cpp HTML/Navigable.cpp HTML/NavigableContainer.cpp + HTML/NavigateEvent.cpp HTML/Navigation.cpp HTML/NavigationDestination.cpp HTML/NavigationCurrentEntryChangeEvent.cpp diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index f9b4ae99b7..82d042e40a 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -414,8 +414,10 @@ class MimeType; class MimeTypeArray; class Navigable; class NavigableContainer; +class NavigateEvent; class Navigation; class NavigationCurrentEntryChangeEvent; +class NavigationDestination; class NavigationHistoryEntry; class NavigationTransition; class Navigator; diff --git a/Userland/Libraries/LibWeb/HTML/NavigateEvent.cpp b/Userland/Libraries/LibWeb/HTML/NavigateEvent.cpp new file mode 100644 index 0000000000..47ca4c5732 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/NavigateEvent.cpp @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2023, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::HTML { + +JS::NonnullGCPtr NavigateEvent::construct_impl(JS::Realm& realm, FlyString const& event_name, NavigateEventInit const& event_init) +{ + return realm.heap().allocate(realm, realm, event_name, event_init); +} + +NavigateEvent::NavigateEvent(JS::Realm& realm, FlyString const& event_name, NavigateEventInit const& event_init) + : DOM::Event(realm, event_name, event_init) + , m_navigation_type(event_init.navigation_type) + , m_destination(*event_init.destination) + , m_can_intercept(event_init.can_intercept) + , m_user_initiated(event_init.user_initiated) + , m_hash_change(event_init.hash_change) + , m_signal(*event_init.signal) + , m_form_data(event_init.form_data) + , m_download_request(event_init.download_request) + , m_info(event_init.info) + , m_has_ua_visual_transition(event_init.has_ua_visual_transition) +{ +} + +NavigateEvent::~NavigateEvent() = default; + +void NavigateEvent::initialize(JS::Realm& realm) +{ + Base::initialize(realm); + set_prototype(&Bindings::ensure_web_prototype(realm, "NavigateEvent")); +} + +void NavigateEvent::visit_edges(JS::Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + for (auto& handler : m_navigation_handler_list) + visitor.visit(handler); + visitor.visit(m_abort_controller); + visitor.visit(m_destination); + visitor.visit(m_signal); + visitor.visit(m_form_data); + visitor.visit(m_info); +} + +// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-intercept +WebIDL::ExceptionOr NavigateEvent::intercept(Optional const& options) +{ + auto& realm = this->realm(); + auto& vm = this->vm(); + // The intercept(options) method steps are: + + // 1. Perform shared checks given this. + TRY(perform_shared_checks()); + + // 2. If this's canIntercept attribute was initialized to false, then throw a "SecurityError" DOMException. + if (!m_can_intercept) + return WebIDL::SecurityError::create(realm, "NavigateEvent cannot be intercepted"); + + // 3. If this's dispatch flag is unset, then throw an "InvalidStateError" DOMException. + if (!this->dispatched()) + return WebIDL::InvalidStateError::create(realm, "NavigationEvent is not dispatched yet"); + + // 4. Assert: this's interception state is either "none" or "intercepted". + VERIFY(m_interception_state == InterceptionState::None || m_interception_state == InterceptionState::Intercepted); + + // 5. Set this's interception state to "intercepted". + m_interception_state = InterceptionState::Intercepted; + + if (options.has_value()) { + // 6. If options["handler"] exists, then append it to this's navigation handler list. + TRY_OR_THROW_OOM(vm, m_navigation_handler_list.try_append(*(options->handler))); + + // 7. If options["focusReset"] exists, then: + // 1. If this's focus reset behavior is not null, and it is not equal to options["focusReset"], + // then the user agent may report a warning to the console indicating that the focusReset option + // for a previous call to intercept() was overridden by this new value, and the previous value + // will be ignored. + if (m_focus_reset_behavior.has_value() && m_focus_reset_behavior.value() != options->focus_reset) { + auto& console = realm.intrinsics().console_object()->console(); + console.output_debug_message(JS::Console::LogLevel::Warn, + TRY_OR_THROW_OOM(vm, String::formatted("focusReset behavior on NavigationEvent overriden (was: {}, now: {})", *m_focus_reset_behavior, options->focus_reset))); + } + + // 2. Set this's focus reset behavior to options["focusReset"]. + m_focus_reset_behavior = options->focus_reset; + + // 8. If options["scroll"] exists, then: + // 1. If this's scroll behavior is not null, and it is not equal to options["scroll"], then the user + // agent may report a warning to the console indicating that the scroll option for a previous call + // to intercept() was overridden by this new value, and the previous value will be ignored. + if (m_scroll_behavior.has_value() && m_scroll_behavior.value() != options->scroll) { + auto& console = realm.intrinsics().console_object()->console(); + console.output_debug_message(JS::Console::LogLevel::Warn, + TRY_OR_THROW_OOM(vm, String::formatted("scroll option on NavigationEvent overriden (was: {}, now: {})", *m_scroll_behavior, options->scroll))); + } + + // 2. Set this's scroll behavior to options["scroll"]. + m_scroll_behavior = options->scroll; + } + + return {}; +} + +// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-scroll +WebIDL::ExceptionOr NavigateEvent::scroll() +{ + // The scroll() method steps are: + // 1. Perform shared checks given this. + TRY(perform_shared_checks()); + + // 2. If this's interception state is not "committed", then throw an "InvalidStateError" DOMException. + if (m_interception_state != InterceptionState::Committed) + return WebIDL::InvalidStateError::create(realm(), "Cannot scroll NavigationEvent that is not committed"); + + // 3. Process scroll behavior given this. + process_scroll_behavior(); + + return {}; +} + +// https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigateevent-perform-shared-checks +WebIDL::ExceptionOr NavigateEvent::perform_shared_checks() +{ + // To perform shared checks for a NavigateEvent event: + + // 1. If event's relevant global object's associated Document is not fully active, + // then throw an "InvalidStateError" DOMException. + auto& associated_document = verify_cast(relevant_global_object(*this)).associated_document(); + if (!associated_document.is_fully_active()) + return WebIDL::InvalidStateError::create(realm(), "Document is not fully active"); + + // 2. If event's isTrusted attribute was initialized to false, then throw a "SecurityError" DOMException. + if (!this->is_trusted()) + return WebIDL::SecurityError::create(realm(), "NavigateEvent is not trusted"); + + // 3. If event's canceled flag is set, then throw an "InvalidStateError" DOMException. + if (this->cancelled()) + return WebIDL::InvalidStateError::create(realm(), "NavigateEvent already cancelled"); + + return {}; +} + +// https://html.spec.whatwg.org/multipage/nav-history-apis.html#process-scroll-behavior +void NavigateEvent::process_scroll_behavior() +{ + // To process scroll behavior given a NavigateEvent event: + + // 1. Assert: event's interception state is "committed". + VERIFY(m_interception_state == InterceptionState::Committed); + + // 2. Set event's interception state to "scrolled". + m_interception_state = InterceptionState::Scrolled; + + // FIXME: 3. If event's navigationType was initialized to "traverse" or "reload", then restore scroll position data + // given event's relevant global object's navigable's active session history entry. + if (m_navigation_type == Bindings::NavigationType::Traverse || m_navigation_type == Bindings::NavigationType::Reload) { + dbgln("FIXME: restore scroll position data after traversal or reload navigation"); + } + + // 4. Otherwise: + else { + // 1. Let document be event's relevant global object's associated Document. + auto& document = verify_cast(relevant_global_object(*this)).associated_document(); + + // 2. If document's indicated part is null, then scroll to the beginning of the document given document. [CSSOMVIEW] + auto indicated_part = document.determine_the_indicated_part(); + if (indicated_part.has() && indicated_part.get() == nullptr) { + document.scroll_to_the_beginning_of_the_document(); + } + + // 3. Otherwise, scroll to the fragment given document. + else { + // FIXME: This will re-determine the indicated part. Can we avoid this extra work? + document.scroll_to_the_fragment(); + } + } +} + +} diff --git a/Userland/Libraries/LibWeb/HTML/NavigateEvent.h b/Userland/Libraries/LibWeb/HTML/NavigateEvent.h new file mode 100644 index 0000000000..bf35198712 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/NavigateEvent.h @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2023, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +namespace Web::HTML { + +// https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigateeventinit +struct NavigateEventInit : public DOM::EventInit { + Bindings::NavigationType navigation_type = Bindings::NavigationType::Push; + JS::GCPtr destination; + bool can_intercept = false; + bool user_initiated = false; + bool hash_change = false; + JS::GCPtr signal; + JS::GCPtr form_data = nullptr; + Optional download_request = {}; + JS::Value info; + bool has_ua_visual_transition = false; +}; + +// https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigationintercepthandler +using NavigationInterceptHandler = JS::NonnullGCPtr; + +// https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigationinterceptoptions +struct NavigationInterceptOptions { + JS::GCPtr handler; + Bindings::NavigationFocusReset focus_reset; + Bindings::NavigationScrollBehavior scroll; +}; + +// https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigateevent +class NavigateEvent : public DOM::Event { + WEB_PLATFORM_OBJECT(NavigateEvent, DOM::Event); + +public: + [[nodiscard]] static JS::NonnullGCPtr construct_impl(JS::Realm&, FlyString const& event_name, NavigateEventInit const&); + + // The navigationType, destination, canIntercept, userInitiated, hashChange, signal, formData, + // downloadRequest, info, and hasUAVisualTransition attributes must return the values they are initialized to. + Bindings::NavigationType navigation_type() const { return m_navigation_type; } + JS::NonnullGCPtr destination() const { return m_destination; } + bool can_intercept() const { return m_can_intercept; } + bool user_initiated() const { return m_user_initiated; } + bool hash_change() const { return m_hash_change; } + JS::NonnullGCPtr signal() const { return m_signal; } + JS::GCPtr form_data() const { return m_form_data; } + Optional download_request() const { return m_download_request; } + JS::Value info() const { return m_info; } + bool has_ua_visual_transition() const { return m_has_ua_visual_transition; } + + WebIDL::ExceptionOr intercept(Optional const& = {}); + WebIDL::ExceptionOr scroll(); + + virtual ~NavigateEvent() override; + +private: + NavigateEvent(JS::Realm&, FlyString const& event_name, NavigateEventInit const& event_init); + + virtual void initialize(JS::Realm&) override; + virtual void visit_edges(Cell::Visitor&) override; + + WebIDL::ExceptionOr perform_shared_checks(); + void process_scroll_behavior(); + + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#concept-navigateevent-interception-state + enum class InterceptionState { + None, + Intercepted, + Committed, + Scrolled, + Finished + }; + InterceptionState m_interception_state = InterceptionState::None; + + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#concept-navigateevent-navigation-handler-list + Vector m_navigation_handler_list; + + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#concept-navigateevent-focusreset + Optional m_focus_reset_behavior = {}; + + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#concept-navigateevent-scroll + Optional m_scroll_behavior = {}; + + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#concept-navigateevent-abort-controller + JS::GCPtr m_abort_controller = { nullptr }; + + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#concept-navigateevent-classic-history-api-state + Optional m_classic_history_api_state = {}; + + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-navigationtype + Bindings::NavigationType m_navigation_type = { Bindings::NavigationType::Push }; + + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-destination + JS::NonnullGCPtr m_destination; + + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-canintercept + bool m_can_intercept = { false }; + + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-userinitiated + bool m_user_initiated = { false }; + + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-hashchange + bool m_hash_change = { false }; + + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-signal + JS::NonnullGCPtr m_signal; + + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-formdata + JS::GCPtr m_form_data; + + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-downloadrequest + Optional m_download_request; + + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-info + JS::Value m_info; + + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-hasuavisualtransition + bool m_has_ua_visual_transition { false }; +}; + +} + +namespace AK { +template<> +struct Formatter : Formatter { + ErrorOr format(FormatBuilder& builder, Web::Bindings::NavigationScrollBehavior const& value) + { + return Formatter::format(builder, Web::Bindings::idl_enum_to_string(value)); + } +}; + +template<> +struct Formatter : Formatter { + ErrorOr format(FormatBuilder& builder, Web::Bindings::NavigationFocusReset const& value) + { + return Formatter::format(builder, Web::Bindings::idl_enum_to_string(value)); + } +}; +} diff --git a/Userland/Libraries/LibWeb/HTML/NavigateEvent.idl b/Userland/Libraries/LibWeb/HTML/NavigateEvent.idl new file mode 100644 index 0000000000..85d925bdad --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/NavigateEvent.idl @@ -0,0 +1,56 @@ +#import +#import +#import +#import +#import + +// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-navigateevent-interface +[Exposed=Window, UseNewAKString] +interface NavigateEvent : Event { + constructor(DOMString type, NavigateEventInit eventInitDict); + + readonly attribute NavigationType navigationType; + readonly attribute NavigationDestination destination; + readonly attribute boolean canIntercept; + readonly attribute boolean userInitiated; + readonly attribute boolean hashChange; + readonly attribute AbortSignal signal; + readonly attribute FormData? formData; + readonly attribute DOMString? downloadRequest; + readonly attribute any info; + readonly attribute boolean hasUAVisualTransition; + + undefined intercept(optional NavigationInterceptOptions options = {}); + undefined scroll(); +}; + +dictionary NavigateEventInit : EventInit { + NavigationType navigationType = "push"; + required NavigationDestination destination; + boolean canIntercept = false; + boolean userInitiated = false; + boolean hashChange = false; + required AbortSignal signal; + FormData? formData = null; + DOMString? downloadRequest = null; + any info; + boolean hasUAVisualTransition = false; +}; + +dictionary NavigationInterceptOptions { + NavigationInterceptHandler handler; + NavigationFocusReset focusReset; + NavigationScrollBehavior scroll; +}; + +enum NavigationFocusReset { + "after-transition", + "manual" +}; + +enum NavigationScrollBehavior { + "after-transition", + "manual" +}; + +callback NavigationInterceptHandler = Promise (); diff --git a/Userland/Libraries/LibWeb/idl_files.cmake b/Userland/Libraries/LibWeb/idl_files.cmake index 1d78332d54..b8068a7024 100644 --- a/Userland/Libraries/LibWeb/idl_files.cmake +++ b/Userland/Libraries/LibWeb/idl_files.cmake @@ -168,6 +168,7 @@ libweb_js_bindings(HTML/MessageEvent) libweb_js_bindings(HTML/MessagePort) libweb_js_bindings(HTML/MimeType) libweb_js_bindings(HTML/MimeTypeArray) +libweb_js_bindings(HTML/NavigateEvent) libweb_js_bindings(HTML/Navigation) libweb_js_bindings(HTML/NavigationCurrentEntryChangeEvent) libweb_js_bindings(HTML/NavigationDestination)