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)