2023-08-23 18:40:42 -06:00
|
|
|
/*
|
|
|
|
|
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
|
|
|
|
*
|
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
|
*/
|
|
|
|
|
|
2024-11-15 04:01:23 +13:00
|
|
|
#include <LibGC/Heap.h>
|
2023-08-23 18:40:42 -06:00
|
|
|
#include <LibJS/Console.h>
|
|
|
|
|
#include <LibJS/Runtime/ConsoleObject.h>
|
|
|
|
|
#include <LibJS/Runtime/Promise.h>
|
|
|
|
|
#include <LibJS/Runtime/Realm.h>
|
|
|
|
|
#include <LibWeb/Bindings/Intrinsics.h>
|
2026-04-18 10:54:06 +02:00
|
|
|
#include <LibWeb/Bindings/NavigateEvent.h>
|
2023-08-23 18:40:42 -06:00
|
|
|
#include <LibWeb/DOM/AbortController.h>
|
|
|
|
|
#include <LibWeb/DOM/AbortSignal.h>
|
|
|
|
|
#include <LibWeb/DOM/Document.h>
|
2023-09-22 18:24:28 -06:00
|
|
|
#include <LibWeb/HTML/Focus.h>
|
2023-08-23 18:40:42 -06:00
|
|
|
#include <LibWeb/HTML/NavigateEvent.h>
|
2023-09-22 18:24:28 -06:00
|
|
|
#include <LibWeb/HTML/Navigation.h>
|
2023-08-23 18:40:42 -06:00
|
|
|
#include <LibWeb/HTML/NavigationDestination.h>
|
2023-10-08 11:59:40 +02:00
|
|
|
#include <LibWeb/HTML/Window.h>
|
2023-08-23 18:40:42 -06:00
|
|
|
#include <LibWeb/XHR/FormData.h>
|
|
|
|
|
|
|
|
|
|
namespace Web::HTML {
|
|
|
|
|
|
2024-11-15 04:01:23 +13:00
|
|
|
GC_DEFINE_ALLOCATOR(NavigateEvent);
|
2023-11-19 19:47:52 +01:00
|
|
|
|
2026-05-01 19:01:23 +02:00
|
|
|
GC::Ref<NavigateEvent> NavigateEvent::create(JS::Realm& realm, FlyString const& event_name, Bindings::NavigateEventInit const& event_init)
|
2025-04-26 15:44:16 +12:00
|
|
|
{
|
|
|
|
|
auto event = realm.create<NavigateEvent>(realm, event_name, event_init);
|
|
|
|
|
event->set_is_trusted(true);
|
|
|
|
|
return event;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 19:01:23 +02:00
|
|
|
GC::Ref<NavigateEvent> NavigateEvent::construct_impl(JS::Realm& realm, FlyString const& event_name, Bindings::NavigateEventInit const& event_init)
|
2023-08-23 18:40:42 -06:00
|
|
|
{
|
2024-11-14 05:50:17 +13:00
|
|
|
return realm.create<NavigateEvent>(realm, event_name, event_init);
|
2023-08-23 18:40:42 -06:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 19:01:23 +02:00
|
|
|
NavigateEvent::NavigateEvent(JS::Realm& realm, FlyString const& event_name, Bindings::NavigateEventInit const& event_init)
|
2023-08-23 18:40:42 -06:00
|
|
|
: 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)
|
2023-08-24 16:15:54 -06:00
|
|
|
, m_info(event_init.info.value_or(JS::js_undefined()))
|
2023-08-23 18:40:42 -06:00
|
|
|
, m_has_ua_visual_transition(event_init.has_ua_visual_transition)
|
2025-01-30 14:14:13 +00:00
|
|
|
, m_source_element(event_init.source_element)
|
2023-08-23 18:40:42 -06:00
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NavigateEvent::~NavigateEvent() = default;
|
|
|
|
|
|
|
|
|
|
void NavigateEvent::initialize(JS::Realm& realm)
|
|
|
|
|
{
|
2024-03-16 13:13:08 +01:00
|
|
|
WEB_SET_PROTOTYPE_FOR_INTERFACE(NavigateEvent);
|
2025-04-20 16:22:57 +02:00
|
|
|
Base::initialize(realm);
|
2023-08-23 18:40:42 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void NavigateEvent::visit_edges(JS::Cell::Visitor& visitor)
|
|
|
|
|
{
|
|
|
|
|
Base::visit_edges(visitor);
|
2024-04-15 13:58:21 +02:00
|
|
|
visitor.visit(m_navigation_handler_list);
|
2023-08-23 18:40:42 -06:00
|
|
|
visitor.visit(m_abort_controller);
|
|
|
|
|
visitor.visit(m_destination);
|
|
|
|
|
visitor.visit(m_signal);
|
|
|
|
|
visitor.visit(m_form_data);
|
|
|
|
|
visitor.visit(m_info);
|
2025-01-30 14:14:13 +00:00
|
|
|
visitor.visit(m_source_element);
|
2023-08-23 18:40:42 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-intercept
|
2026-05-01 19:01:23 +02:00
|
|
|
WebIDL::ExceptionOr<void> NavigateEvent::intercept(Bindings::NavigationInterceptOptions const& options)
|
2023-08-23 18:40:42 -06:00
|
|
|
{
|
|
|
|
|
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)
|
2025-08-07 19:31:52 -04:00
|
|
|
return WebIDL::SecurityError::create(realm, "NavigateEvent cannot be intercepted"_utf16);
|
2023-08-23 18:40:42 -06:00
|
|
|
|
|
|
|
|
// 3. If this's dispatch flag is unset, then throw an "InvalidStateError" DOMException.
|
|
|
|
|
if (!this->dispatched())
|
2025-08-07 19:31:52 -04:00
|
|
|
return WebIDL::InvalidStateError::create(realm, "NavigationEvent is not dispatched yet"_utf16);
|
2023-08-23 18:40:42 -06:00
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
2023-08-24 16:15:54 -06:00
|
|
|
// 6. If options["handler"] exists, then append it to this's navigation handler list.
|
2026-05-20 20:58:46 +02:00
|
|
|
if (options.handler)
|
2023-08-24 16:15:54 -06:00
|
|
|
TRY_OR_THROW_OOM(vm, m_navigation_handler_list.try_append(*options.handler));
|
|
|
|
|
|
|
|
|
|
// 7. If options["focusReset"] exists, then:
|
|
|
|
|
if (options.focus_reset.has_value()) {
|
|
|
|
|
// 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 != *options.focus_reset) {
|
2023-08-23 18:40:42 -06:00
|
|
|
auto& console = realm.intrinsics().console_object()->console();
|
|
|
|
|
console.output_debug_message(JS::Console::LogLevel::Warn,
|
2025-04-07 10:46:22 +00:00
|
|
|
TRY_OR_THROW_OOM(vm, String::formatted("focusReset behavior on NavigationEvent overridden (was: {}, now: {})", *m_focus_reset_behavior, *options.focus_reset)));
|
2023-08-23 18:40:42 -06:00
|
|
|
}
|
|
|
|
|
|
2023-08-24 16:15:54 -06:00
|
|
|
// 2. Set this's focus reset behavior to options["focusReset"].
|
|
|
|
|
m_focus_reset_behavior = options.focus_reset;
|
|
|
|
|
}
|
2023-08-23 18:40:42 -06:00
|
|
|
|
2023-08-24 16:15:54 -06:00
|
|
|
// 8. If options["scroll"] exists, then:
|
|
|
|
|
if (options.scroll.has_value()) {
|
|
|
|
|
// 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 != *options.scroll) {
|
2023-08-23 18:40:42 -06:00
|
|
|
auto& console = realm.intrinsics().console_object()->console();
|
|
|
|
|
console.output_debug_message(JS::Console::LogLevel::Warn,
|
2025-04-07 10:46:22 +00:00
|
|
|
TRY_OR_THROW_OOM(vm, String::formatted("scroll option on NavigationEvent overridden (was: {}, now: {})", *m_scroll_behavior, *options.scroll)));
|
2023-08-23 18:40:42 -06:00
|
|
|
}
|
|
|
|
|
|
2023-08-24 16:15:54 -06:00
|
|
|
// 2. Set this's scroll behavior to options["scroll"].
|
|
|
|
|
m_scroll_behavior = options.scroll;
|
2023-08-23 18:40:42 -06:00
|
|
|
}
|
|
|
|
|
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)
|
2025-08-07 19:31:52 -04:00
|
|
|
return WebIDL::InvalidStateError::create(realm(), "Cannot scroll NavigationEvent that is not committed"_utf16);
|
2023-08-23 18:40:42 -06:00
|
|
|
|
|
|
|
|
// 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.
|
2025-01-21 09:12:05 -05:00
|
|
|
auto& associated_document = as<HTML::Window>(relevant_global_object(*this)).associated_document();
|
2023-08-23 18:40:42 -06:00
|
|
|
if (!associated_document.is_fully_active())
|
2025-08-07 19:31:52 -04:00
|
|
|
return WebIDL::InvalidStateError::create(realm(), "Document is not fully active"_utf16);
|
2023-08-23 18:40:42 -06:00
|
|
|
|
|
|
|
|
// 2. If event's isTrusted attribute was initialized to false, then throw a "SecurityError" DOMException.
|
|
|
|
|
if (!this->is_trusted())
|
2025-08-07 19:31:52 -04:00
|
|
|
return WebIDL::SecurityError::create(realm(), "NavigateEvent is not trusted"_utf16);
|
2023-08-23 18:40:42 -06:00
|
|
|
|
|
|
|
|
// 3. If event's canceled flag is set, then throw an "InvalidStateError" DOMException.
|
|
|
|
|
if (this->cancelled())
|
2025-08-07 19:31:52 -04:00
|
|
|
return WebIDL::InvalidStateError::create(realm(), "NavigateEvent already cancelled"_utf16);
|
2023-08-23 18:40:42 -06:00
|
|
|
|
|
|
|
|
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.
|
2025-01-21 09:12:05 -05:00
|
|
|
auto& document = as<HTML::Window>(relevant_global_object(*this)).associated_document();
|
2023-08-23 18:40:42 -06:00
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-22 18:24:28 -06:00
|
|
|
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#potentially-process-scroll-behavior
|
|
|
|
|
void NavigateEvent::potentially_process_scroll_behavior()
|
|
|
|
|
{
|
|
|
|
|
// 1. Assert: event's interception state is "committed" or "scrolled".
|
2024-12-20 03:17:02 +04:00
|
|
|
VERIFY(m_interception_state == InterceptionState::Committed || m_interception_state == InterceptionState::Scrolled);
|
2023-09-22 18:24:28 -06:00
|
|
|
|
|
|
|
|
// 2. If event's interception state is "scrolled", then return.
|
|
|
|
|
if (m_interception_state == InterceptionState::Scrolled)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// 3. If event's scroll behavior is "manual", then return.
|
|
|
|
|
// NOTE: If it was left as null, then we treat that as "after-transition", and continue onward.
|
|
|
|
|
if (m_scroll_behavior == Bindings::NavigationScrollBehavior::Manual)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// 4. Process scroll behavior given event.
|
|
|
|
|
process_scroll_behavior();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#potentially-reset-the-focus
|
|
|
|
|
void NavigateEvent::potentially_reset_the_focus()
|
|
|
|
|
{
|
|
|
|
|
// 1. Assert: event's interception state is "committed" or "scrolled".
|
|
|
|
|
VERIFY(m_interception_state == InterceptionState::Committed || m_interception_state == InterceptionState::Scrolled);
|
|
|
|
|
|
|
|
|
|
// 2. Let navigation be event's relevant global object's navigation API.
|
2025-01-21 09:12:05 -05:00
|
|
|
auto& relevant_global_object = as<Window>(HTML::relevant_global_object(*this));
|
2023-09-22 18:24:28 -06:00
|
|
|
auto navigation = relevant_global_object.navigation();
|
|
|
|
|
|
|
|
|
|
// 3. Let focusChanged be navigation's focus changed during ongoing navigation.
|
|
|
|
|
auto focus_changed = navigation->focus_changed_during_ongoing_navigation();
|
|
|
|
|
|
|
|
|
|
// 4. Set navigation's focus changed during ongoing navigation to false.
|
|
|
|
|
navigation->set_focus_changed_during_ongoing_navigation(false);
|
|
|
|
|
|
|
|
|
|
// 5. If focusChanged is true, then return.
|
|
|
|
|
if (focus_changed)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// 6. If event's focus reset behavior is "manual", then return.
|
|
|
|
|
// NOTE: If it was left as null, then we treat that as "after-transition", and continue onward.
|
|
|
|
|
if (m_focus_reset_behavior == Bindings::NavigationFocusReset::Manual)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// 7. Let document be event's relevant global object's associated Document.
|
|
|
|
|
auto& document = relevant_global_object.associated_document();
|
|
|
|
|
|
|
|
|
|
// 8. FIXME: Let focusTarget be the autofocus delegate for document.
|
2024-11-15 04:01:23 +13:00
|
|
|
GC::Ptr<DOM::Node> focus_target = nullptr;
|
2023-09-22 18:24:28 -06:00
|
|
|
|
|
|
|
|
// 9. If focusTarget is null, then set focusTarget to document's body element.
|
|
|
|
|
if (focus_target == nullptr)
|
|
|
|
|
focus_target = document.body();
|
|
|
|
|
|
|
|
|
|
// 10. If focusTarget is null, then set focusTarget to document's document element.
|
|
|
|
|
if (focus_target == nullptr)
|
|
|
|
|
focus_target = document.document_element();
|
|
|
|
|
|
LibWeb: Map document element focus to the viewport
Apply the focusing steps' get-the-focusable-area mapping before
rejecting a non-focusable target. This preserves documentElement.focus()
by mapping the non-focusable document element to the Document viewport.
Also map rendered navigable containers with content navigables to their
active document, while leaving hidden containers unfocused. Preserve
Window focus events for child document viewports reached through iframe
focus, while still suppressing the top-level viewport surrogate events.
Treat rendered object elements as focusable through their default
non-null tabindex, even when they show fallback or image content instead
of a child navigable.
Keep the spec focus-chain common-tail handling intact for viewport
focus. The Document object is only our surrogate for the viewport, so
designate viewport focus from the new focus target without dispatching
Window focus/focusin events for that top-level surrogate.
Pass that viewport surrogate as the fallback target for fragment
scrolling and NavigateEvent focus reset, so unfocusable body or fragment
targets still clear stale element focus.
Cover documentElement.focus() in both the activeElement and focus-chain
tests, including a tabindex document element that remains focused as an
element. Cover object focus with and without a child navigable, hidden
object focus attempts, iframe focus events, hidden iframe focus, and
blurring a focused iframe after it becomes hidden. Also cover viewport
fallback for intercepted navigation focus reset and fragment scrolling
to an unfocusable target.
2026-05-20 15:52:18 +02:00
|
|
|
// 11. Run the focusing steps for focusTarget, with document's viewport as the fallback target.
|
|
|
|
|
run_focusing_steps(focus_target, &document);
|
2023-09-22 18:24:28 -06:00
|
|
|
|
|
|
|
|
// FIXME: 12. Move the sequential focus navigation starting point to focusTarget.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigateevent-finish
|
|
|
|
|
void NavigateEvent::finish(bool did_fulfill)
|
|
|
|
|
{
|
|
|
|
|
// 1. Assert: event's interception state is not "intercepted" or "finished".
|
|
|
|
|
VERIFY(m_interception_state != InterceptionState::Intercepted && m_interception_state != InterceptionState::Finished);
|
|
|
|
|
|
|
|
|
|
// 2. If event's interception state is "none", then return.
|
|
|
|
|
if (m_interception_state == InterceptionState::None)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// 3. Potentially reset the focus given event.
|
|
|
|
|
potentially_reset_the_focus();
|
|
|
|
|
|
|
|
|
|
// 4. If didFulfill is true, then potentially process scroll behavior given event.
|
|
|
|
|
if (did_fulfill)
|
|
|
|
|
potentially_process_scroll_behavior();
|
|
|
|
|
|
|
|
|
|
// 5. Set event's interception state to "finished".
|
|
|
|
|
m_interception_state = InterceptionState::Finished;
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-23 18:40:42 -06:00
|
|
|
}
|