ladybird/Userland/Libraries/LibWeb/HTML/NavigateEvent.cpp
Andrew Kaster d1aea87889 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.
2023-08-24 11:03:57 -06:00

197 lines
8.6 KiB
C++

/*
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Console.h>
#include <LibJS/Heap/Heap.h>
#include <LibJS/Runtime/ConsoleObject.h>
#include <LibJS/Runtime/Promise.h>
#include <LibJS/Runtime/Realm.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/NavigateEventPrototype.h>
#include <LibWeb/DOM/AbortController.h>
#include <LibWeb/DOM/AbortSignal.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/HTML/NavigateEvent.h>
#include <LibWeb/HTML/NavigationDestination.h>
#include <LibWeb/XHR/FormData.h>
namespace Web::HTML {
JS::NonnullGCPtr<NavigateEvent> NavigateEvent::construct_impl(JS::Realm& realm, FlyString const& event_name, NavigateEventInit const& event_init)
{
return realm.heap().allocate<NavigateEvent>(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<Bindings::NavigateEventPrototype>(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<void> NavigateEvent::intercept(Optional<NavigationInterceptOptions> 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<void> 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<void> 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<HTML::Window>(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<HTML::Window>(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<DOM::Element*>() && indicated_part.get<DOM::Element*>() == 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();
}
}
}
}