LibWeb: Implement the start of the Navigation API

This API is how JavaScript can manipulate the new Navigable concepts
directly. We are still missing most of the interesting algorithms on
Navigation that do the actual navigation steps, and call into the
currently WIP navigable AOs.
This commit is contained in:
Andrew Kaster 2023-08-23 10:57:12 -06:00 committed by Andrew Kaster
parent 51c2835044
commit 0c2f758067
Notes: sideshowbarker 2024-07-17 10:05:47 +09:00
9 changed files with 378 additions and 0 deletions

View file

@ -123,6 +123,7 @@ source_set("HTML") {
"MimeTypeArray.cpp",
"Navigable.cpp",
"NavigableContainer.cpp",
"Navigation.cpp",
"NavigationCurrentEntryChangeEvent.cpp",
"NavigationHistoryEntry.cpp",
"Navigator.cpp",

View file

@ -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/Navigation.idl",
"//Userland/Libraries/LibWeb/HTML/NavigationCurrentEntryChangeEvent.idl",
"//Userland/Libraries/LibWeb/HTML/NavigationHistoryEntry.idl",
"//Userland/Libraries/LibWeb/HTML/Navigator.idl",

View file

@ -350,6 +350,7 @@ set(SOURCES
HTML/MimeTypeArray.cpp
HTML/Navigable.cpp
HTML/NavigableContainer.cpp
HTML/Navigation.cpp
HTML/NavigationCurrentEntryChangeEvent.cpp
HTML/NavigationHistoryEntry.cpp
HTML/Navigator.cpp

View file

@ -414,6 +414,7 @@ class MimeType;
class MimeTypeArray;
class Navigable;
class NavigableContainer;
class Navigation;
class NavigationCurrentEntryChangeEvent;
class NavigationHistoryEntry;
class Navigator;

View file

@ -62,6 +62,9 @@ namespace Web::HTML::EventNames {
__ENUMERATE_HTML_EVENT(loadstart) \
__ENUMERATE_HTML_EVENT(message) \
__ENUMERATE_HTML_EVENT(messageerror) \
__ENUMERATE_HTML_EVENT(navigate) \
__ENUMERATE_HTML_EVENT(navigatesuccess) \
__ENUMERATE_HTML_EVENT(navigateerror) \
__ENUMERATE_HTML_EVENT(offline) \
__ENUMERATE_HTML_EVENT(online) \
__ENUMERATE_HTML_EVENT(open) \

View file

@ -0,0 +1,224 @@
/*
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Heap/Heap.h>
#include <LibJS/Runtime/Realm.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/NavigationPrototype.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/HTML/Navigation.h>
#include <LibWeb/HTML/NavigationCurrentEntryChangeEvent.h>
#include <LibWeb/HTML/NavigationHistoryEntry.h>
#include <LibWeb/HTML/Window.h>
namespace Web::HTML {
JS::NonnullGCPtr<Navigation> Navigation::create(JS::Realm& realm)
{
return realm.heap().allocate<Navigation>(realm, realm);
}
Navigation::Navigation(JS::Realm& realm)
: DOM::EventTarget(realm)
{
}
Navigation::~Navigation() = default;
void Navigation::initialize(JS::Realm& realm)
{
Base::initialize(realm);
set_prototype(&Bindings::ensure_web_prototype<Bindings::NavigationPrototype>(realm, "Navigation"));
}
void Navigation::visit_edges(JS::Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
for (auto& entry : m_entry_list)
visitor.visit(entry);
}
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-entries
Vector<JS::NonnullGCPtr<NavigationHistoryEntry>> Navigation::entries() const
{
// The entries() method steps are:
// 1. If this has entries and events disabled, then return the empty list.
if (has_entries_and_events_disabled())
return {};
// 2. Return this's entry list.
// NOTE: Recall that because of Web IDL's sequence type conversion rules,
// this will create a new JavaScript array object on each call.
// That is, navigation.entries() !== navigation.entries().
return m_entry_list;
}
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigation-current-entry
JS::GCPtr<NavigationHistoryEntry> Navigation::current_entry() const
{
// The current entry of a Navigation navigation is the result of running the following steps:
// 1. If navigation has entries and events disabled, then return null.
if (has_entries_and_events_disabled())
return nullptr;
// 2. Assert: navigation's current entry index is not 1.
VERIFY(m_current_entry_index != -1);
// 3. Return navigation's entry list[navigation's current entry index].
return m_entry_list[m_current_entry_index];
}
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-updatecurrententry
WebIDL::ExceptionOr<void> Navigation::update_current_entry(NavigationUpdateCurrentEntryOptions options)
{
// The updateCurrentEntry(options) method steps are:
// 1. Let current be the current entry of this.
auto current = current_entry();
// 2. If current is null, then throw an "InvalidStateError" DOMException.
if (current == nullptr)
return WebIDL::InvalidStateError::create(realm(), "Cannot update current NavigationHistoryEntry when there is no current entry"sv);
// 3. Let serializedState be StructuredSerializeForStorage(options["state"]), rethrowing any exceptions.
auto serialized_state = TRY(structured_serialize_for_storage(vm(), options.state));
// 4. Set current's session history entry's navigation API state to serializedState.
current->session_history_entry().navigation_api_state = serialized_state;
// 5. Fire an event named currententrychange at this using NavigationCurrentEntryChangeEvent,
// with its navigationType attribute initialized to null and its from initialized to current.
NavigationCurrentEntryChangeEventInit event_init = {};
event_init.navigation_type = {};
event_init.from = current;
dispatch_event(HTML::NavigationCurrentEntryChangeEvent::create(realm(), HTML::EventNames::currententrychange, event_init));
return {};
}
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-cangoback
bool Navigation::can_go_back() const
{
// The canGoBack getter steps are:
// 1. If this has entries and events disabled, then return false.
if (has_entries_and_events_disabled())
return false;
// 2. Assert: this's current entry index is not 1.
VERIFY(m_current_entry_index != -1);
// 3. If this's current entry index is 0, then return false.
// 4. Return true.
return (m_current_entry_index != 0);
}
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-cangoforward
bool Navigation::can_go_forward() const
{
// The canGoForward getter steps are:
// 1. If this has entries and events disabled, then return false.
if (has_entries_and_events_disabled())
return false;
// 2. Assert: this's current entry index is not 1.
VERIFY(m_current_entry_index != -1);
// 3. If this's current entry index is equal to this's entry list's size, then return false.
// 4. Return true.
return (m_current_entry_index != static_cast<i64>(m_entry_list.size()));
}
void Navigation::set_onnavigate(WebIDL::CallbackType* event_handler)
{
set_event_handler_attribute(HTML::EventNames::navigate, event_handler);
}
WebIDL::CallbackType* Navigation::onnavigate()
{
return event_handler_attribute(HTML::EventNames::navigate);
}
void Navigation::set_onnavigatesuccess(WebIDL::CallbackType* event_handler)
{
set_event_handler_attribute(HTML::EventNames::navigatesuccess, event_handler);
}
WebIDL::CallbackType* Navigation::onnavigatesuccess()
{
return event_handler_attribute(HTML::EventNames::navigatesuccess);
}
void Navigation::set_onnavigateerror(WebIDL::CallbackType* event_handler)
{
set_event_handler_attribute(HTML::EventNames::navigateerror, event_handler);
}
WebIDL::CallbackType* Navigation::onnavigateerror()
{
return event_handler_attribute(HTML::EventNames::navigateerror);
}
void Navigation::set_oncurrententrychange(WebIDL::CallbackType* event_handler)
{
set_event_handler_attribute(HTML::EventNames::currententrychange, event_handler);
}
WebIDL::CallbackType* Navigation::oncurrententrychange()
{
return event_handler_attribute(HTML::EventNames::currententrychange);
}
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#has-entries-and-events-disabled
bool Navigation::has_entries_and_events_disabled() const
{
// A Navigation navigation has entries and events disabled if the following steps return true:
// 1. Let document be navigation's relevant global object's associated Document.
auto const& document = verify_cast<HTML::Window>(relevant_global_object(*this)).associated_document();
// 2. If document is not fully active, then return true.
if (!document.is_fully_active())
return true;
// 3. If document's is initial about:blank is true, then return true.
if (document.is_initial_about_blank())
return true;
// 4. If document's origin is opaque, then return true.
if (document.origin().is_opaque())
return true;
// 5. Return false.
return false;
}
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#getting-the-navigation-api-entry-index
i64 Navigation::get_the_navigation_api_entry_index(SessionHistoryEntry const& she) const
{
// To get the navigation API entry index of a session history entry she within a Navigation navigation:
// 1. Let index be 0.
i64 index = 0;
// 2. For each nhe of navigation's entry list:
for (auto const& nhe : m_entry_list) {
// 1. If nhe's session history entry is equal to she, then return index.
if (&nhe->session_history_entry() == &she)
return index;
// 2. Increment index by 1.
++index;
}
// 3. Return 1.
return -1;
}
}

View file

@ -0,0 +1,90 @@
/*
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibJS/Runtime/Promise.h>
#include <LibWeb/Bindings/NavigationPrototype.h>
#include <LibWeb/DOM/EventTarget.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigationupdatecurrententryoptions
struct NavigationUpdateCurrentEntryOptions {
JS::Value state;
};
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigationoptions
struct NavigationOptions {
JS::Value info;
};
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigationnavigateoptions
struct NavigationNavigateOptions : public NavigationOptions {
JS::Value state;
Bindings::NavigationHistoryBehavior history = Bindings::NavigationHistoryBehavior::Auto;
};
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigationreloadoptions
struct NavigationReloadOptions : public NavigationOptions {
JS::Value state;
};
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigationresult
struct NavigationResult {
JS::NonnullGCPtr<JS::Promise> committed;
JS::NonnullGCPtr<JS::Promise> finished;
};
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigation-interface
class Navigation : public DOM::EventTarget {
WEB_PLATFORM_OBJECT(Navigation, DOM::EventTarget);
public:
[[nodiscard]] static JS::NonnullGCPtr<Navigation> create(JS::Realm&);
// IDL properties and methods
Vector<JS::NonnullGCPtr<NavigationHistoryEntry>> entries() const;
JS::GCPtr<NavigationHistoryEntry> current_entry() const;
WebIDL::ExceptionOr<void> update_current_entry(NavigationUpdateCurrentEntryOptions);
bool can_go_back() const;
bool can_go_forward() const;
// Event Handlers
void set_onnavigate(WebIDL::CallbackType*);
WebIDL::CallbackType* onnavigate();
void set_onnavigatesuccess(WebIDL::CallbackType*);
WebIDL::CallbackType* onnavigatesuccess();
void set_onnavigateerror(WebIDL::CallbackType*);
WebIDL::CallbackType* onnavigateerror();
void set_oncurrententrychange(WebIDL::CallbackType*);
WebIDL::CallbackType* oncurrententrychange();
// Abstract Operations
bool has_entries_and_events_disabled() const;
i64 get_the_navigation_api_entry_index(SessionHistoryEntry const&) const;
virtual ~Navigation() override;
private:
explicit Navigation(JS::Realm&);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Visitor&) override;
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigation-entry-list
// Each Navigation has an associated entry list, a list of NavigationHistoryEntry objects, initially empty.
Vector<JS::NonnullGCPtr<NavigationHistoryEntry>> m_entry_list;
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigation-current-entry-index
// Each Navigation has an associated current entry index, an integer, initially 1.
i64 m_current_entry_index { -1 };
};
}

View file

@ -0,0 +1,56 @@
#import <DOM/EventHandler.idl>
#import <DOM/EventTarget.idl>
#import <HTML/NavigationHistoryEntry.idl>
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigation-interface
[Exposed=Window]
interface Navigation : EventTarget {
sequence<NavigationHistoryEntry> entries();
readonly attribute NavigationHistoryEntry? currentEntry;
undefined updateCurrentEntry(NavigationUpdateCurrentEntryOptions options);
// FIXME: readonly attribute NavigationTransition? transition;
readonly attribute boolean canGoBack;
readonly attribute boolean canGoForward;
// TODO: Actually implement navigation algorithms
// NavigationResult navigate(USVString url, optional NavigationNavigateOptions options = {});
// NavigationResult reload(optional NavigationReloadOptions options = {});
// NavigationResult traverseTo(DOMString key, optional NavigationOptions options = {});
// NavigationResult back(optional NavigationOptions options = {});
// NavigationResult forward(optional NavigationOptions options = {});
attribute EventHandler onnavigate;
attribute EventHandler onnavigatesuccess;
attribute EventHandler onnavigateerror;
attribute EventHandler oncurrententrychange;
};
dictionary NavigationUpdateCurrentEntryOptions {
required any state;
};
dictionary NavigationOptions {
any info;
};
dictionary NavigationNavigateOptions : NavigationOptions {
any state;
NavigationHistoryBehavior history = "auto";
};
dictionary NavigationReloadOptions : NavigationOptions {
any state;
};
dictionary NavigationResult {
Promise<NavigationHistoryEntry> committed;
Promise<NavigationHistoryEntry> finished;
};
enum NavigationHistoryBehavior {
"auto",
"push",
"replace"
};

View file

@ -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/Navigation)
libweb_js_bindings(HTML/NavigationCurrentEntryChangeEvent)
libweb_js_bindings(HTML/NavigationHistoryEntry)
libweb_js_bindings(HTML/Navigator)