1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-31 10:38:11 +00:00

LibWeb: Bring handling of anchor elements closer to spec

This commit moves the regular handling of links to the anchor elements'
activation behavior, and implements a few auxiliary algorithms as
defined by the HTML specification.

Note that certain things such as javascript links, fragments and opening
a new tab are still handled directly in EventHandler, but they have been
moved to handle_mouseup so that it behaves closer to how it would if it
was entirely up-to-spec.
This commit is contained in:
sin-ack 2022-03-15 14:37:58 +00:00 committed by Andreas Kling
parent 034c57f1f9
commit aaa954f900
8 changed files with 357 additions and 40 deletions

View file

@ -428,4 +428,118 @@ RefPtr<DOM::Node> BrowsingContext::currently_focused_area()
return candidate;
}
BrowsingContext* BrowsingContext::choose_a_browsing_context(StringView name, bool)
{
// The rules for choosing a browsing context, given a browsing context name
// name, a browsing context current, and a boolean noopener are as follows:
// 1. Let chosen be null.
BrowsingContext* chosen = nullptr;
// FIXME: 2. Let windowType be "existing or none".
// FIXME: 3. Let sandboxingFlagSet be current's active document's active
// sandboxing flag set.
// 4. If name is the empty string or an ASCII case-insensitive match for "_self", then set chosen to current.
if (name.is_empty() || name.equals_ignoring_case("_self"sv))
chosen = this;
// 5. Otherwise, if name is an ASCII case-insensitive match for "_parent",
// set chosen to current's parent browsing context, if any, and current
// otherwise.
if (name.equals_ignoring_case("_parent"sv)) {
if (auto* parent = this->parent())
chosen = parent;
else
chosen = this;
}
// 6. Otherwise, if name is an ASCII case-insensitive match for "_top", set
// chosen to current's top-level browsing context, if any, and current
// otherwise.
if (name.equals_ignoring_case("_top"sv)) {
chosen = &top_level_browsing_context();
}
// FIXME: 7. Otherwise, if name is not an ASCII case-insensitive match for
// "_blank", there exists a browsing context whose name is the same as name,
// current is familiar with that browsing context, and the user agent
// determines that the two browsing contexts are related enough that it is
// ok if they reach each other, set chosen to that browsing context. If
// there are multiple matching browsing contexts, the user agent should set
// chosen to one in some arbitrary consistent manner, such as the most
// recently opened, most recently focused, or more closely related.
if (!name.equals_ignoring_case("_blank"sv)) {
chosen = this;
} else {
// 8. Otherwise, a new browsing context is being requested, and what
// happens depends on the user agent's configuration and abilities — it
// is determined by the rules given for the first applicable option from
// the following list:
dbgln("FIXME: Create a new browsing context!");
// --> If current's active window does not have transient activation and
// the user agent has been configured to not show popups (i.e., the
// user agent has a "popup blocker" enabled)
//
// The user agent may inform the user that a popup has been blocked.
// --> If sandboxingFlagSet has the sandboxed auxiliary navigation
// browsing context flag set
//
// The user agent may report to a developer console that a popup has
// been blocked.
// --> If the user agent has been configured such that in this instance
// it will create a new browsing context
//
// 1. Set windowType to "new and unrestricted".
// 2. If current's top-level browsing context's active document's
// cross-origin opener policy's value is "same-origin" or
// "same-origin-plus-COEP", then:
// 2.1. Let currentDocument be current's active document.
// 2.2. If currentDocument's origin is not same origin with
// currentDocument's relevant settings object's top-level
// origin, then set noopener to true, name to "_blank", and
// windowType to "new with no opener".
// 3. If noopener is true, then set chosen to the result of creating
// a new top-level browsing context.
// 4. Otherwise:
// 4.1. Set chosen to the result of creating a new auxiliary
// browsing context with current.
// 4.2. If sandboxingFlagSet's sandboxed navigation browsing
// context flag is set, then current must be set as chosen's one
// permitted sandboxed navigator.
// 5. If sandboxingFlagSet's sandbox propagates to auxiliary
// browsing contexts flag is set, then all the flags that are set in
// sandboxingFlagSet must be set in chosen's popup sandboxing flag
// set.
// 6. If name is not an ASCII case-insensitive match for "_blank",
// then set chosen's name to name.
// --> If the user agent has been configured such that in this instance
// it will reuse current
//
// Set chosen to current.
// --> If the user agent has been configured such that in this instance
// it will not find a browsing context
//
// Do nothing.
}
// 9. Return chosen and windowType.
return chosen;
}
}

View file

@ -76,6 +76,8 @@ public:
BrowsingContext const& top_level_browsing_context() const { return const_cast<BrowsingContext*>(this)->top_level_browsing_context(); }
BrowsingContext* choose_a_browsing_context(StringView name, bool noopener);
HTML::BrowsingContextContainer* container() { return m_container; }
HTML::BrowsingContextContainer const* container() const { return m_container; }

View file

@ -11,6 +11,9 @@ namespace Web::HTML {
HTMLAnchorElement::HTMLAnchorElement(DOM::Document& document, DOM::QualifiedName qualified_name)
: HTMLElement(document, move(qualified_name))
{
activation_behavior = [this](auto const& event) {
run_activation_behavior(event);
};
}
HTMLAnchorElement::~HTMLAnchorElement() = default;
@ -33,4 +36,42 @@ void HTMLAnchorElement::set_hyperlink_element_utils_href(String href)
set_attribute(HTML::AttributeNames::href, move(href));
}
void HTMLAnchorElement::run_activation_behavior(Web::DOM::Event const&)
{
// The activation behavior of an a element element given an event event is:
// 1. If element has no href attribute, then return.
if (href().is_empty())
return;
// 2. Let hyperlinkSuffix be null.
Optional<String> hyperlink_suffix {};
// FIXME: 3. If event's target is an img with an ismap attribute
// specified, then:
// 3.1. Let x and y be 0.
//
// 3.2. If event's isTrusted attribute is initialized to true, then
// set x to the distance in CSS pixels from the left edge of the image
// to the location of the click, and set y to the distance in CSS
// pixels from the top edge of the image to the location of the click.
//
// 3.3. If x is negative, set x to 0.
//
// 3.4. If y is negative, set y to 0.
//
// 3.5. Set hyperlinkSuffix to the concatenation of U+003F (?), the
// value of x expressed as a base-ten integer using ASCII digits,
// U+002C (,), and the value of y expressed as a base-ten integer
// using ASCII digits.
// FIXME: 4. If element has a download attribute, or if the user has
// expressed a preference to download the hyperlink, then download the
// hyperlink created by element given hyperlinkSuffix.
// 5. Otherwise, follow the hyperlink created by element given
// hyperlinkSuffix.
follow_the_hyperlink(hyperlink_suffix);
}
}

View file

@ -21,19 +21,29 @@ public:
virtual ~HTMLAnchorElement() override;
String target() const { return attribute(HTML::AttributeNames::target); }
String download() const { return attribute(HTML::AttributeNames::download); }
virtual bool is_focusable() const override { return has_attribute(HTML::AttributeNames::href); }
virtual bool is_html_anchor_element() const override { return true; }
private:
void run_activation_behavior(Web::DOM::Event const&);
// ^DOM::Element
virtual void parse_attribute(FlyString const& name, String const& value) override;
// ^HTML::HTMLHyperlinkElementUtils
virtual DOM::Document const& hyperlink_element_utils_document() const override { return document(); }
virtual DOM::Document& hyperlink_element_utils_document() override { return document(); }
virtual String hyperlink_element_utils_href() const override;
virtual void set_hyperlink_element_utils_href(String) override;
virtual bool hyperlink_element_utils_is_html_anchor_element() const final { return true; }
virtual bool hyperlink_element_utils_is_connected() const final { return is_connected(); }
virtual String hyperlink_element_utils_target() const final { return target(); }
virtual void hyperlink_element_utils_queue_an_element_task(HTML::Task::Source source, Function<void()> steps) override
{
queue_an_element_task(source, move(steps));
}
};
}

View file

@ -26,9 +26,16 @@ private:
virtual void parse_attribute(FlyString const& name, String const& value) override;
// ^HTML::HTMLHyperlinkElementUtils
virtual DOM::Document const& hyperlink_element_utils_document() const override { return document(); }
virtual DOM::Document& hyperlink_element_utils_document() override { return document(); }
virtual String hyperlink_element_utils_href() const override;
virtual void set_hyperlink_element_utils_href(String) override;
virtual bool hyperlink_element_utils_is_html_anchor_element() const override { return false; }
virtual bool hyperlink_element_utils_is_connected() const override { return is_connected(); }
virtual String hyperlink_element_utils_target() const override { return ""; }
virtual void hyperlink_element_utils_queue_an_element_task(HTML::Task::Source source, Function<void()> steps) override
{
queue_an_element_task(source, move(steps));
}
};
}

View file

@ -7,6 +7,7 @@
#include <AK/URLParser.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/HTML/HTMLHyperlinkElementUtils.h>
#include <LibWeb/Loader/FrameLoader.h>
namespace Web::HTML {
@ -449,4 +450,125 @@ void HTMLHyperlinkElementUtils::update_href()
// To update href, set the element's href content attribute's value to the element's url, serialized.
}
bool HTMLHyperlinkElementUtils::cannot_navigate() const
{
// An element element cannot navigate if one of the following is true:
// 1. element's node document is not fully active
auto const& document = const_cast<HTMLHyperlinkElementUtils*>(this)->hyperlink_element_utils_document();
if (!document.is_fully_active())
return true;
// 2. element is not an a element and is not connected.
if (!hyperlink_element_utils_is_html_anchor_element() && !hyperlink_element_utils_is_connected())
return true;
return false;
}
// https://html.spec.whatwg.org/multipage/links.html#following-hyperlinks-2
void HTMLHyperlinkElementUtils::follow_the_hyperlink(Optional<String> hyperlink_suffix)
{
// To follow the hyperlink created by an element subject, given an optional hyperlinkSuffix (default null):
// 1. If subject cannot navigate, then return.
if (cannot_navigate())
return;
// FIXME: 2. Let replace be false.
// 3. Let source be subject's node document's browsing context.
auto* source = hyperlink_element_utils_document().browsing_context();
if (!source)
return;
// 4. Let targetAttributeValue be the empty string.
// 5. If subject is an a or area element, then set targetAttributeValue to
// the result of getting an element's target given subject.
String target_attribute_value = get_an_elements_target();
// 6. Let noopener be the result of getting an element's noopener with subject and targetAttributeValue.
bool noopener = get_an_elements_noopener(target_attribute_value);
// 7. Let target be the first return value of applying the rules for
// choosing a browsing context given targetAttributeValue, source, and
// noopener.
auto* target = source->choose_a_browsing_context(target_attribute_value, noopener);
// 8. If target is null, then return.
if (!target)
return;
// 9. Parse a URL given subject's href attribute, relative to subject's node
// document.
auto url = source->active_document()->parse_url(href());
// 10. If that is successful, let URL be the resulting URL string.
auto url_string = url.to_string();
// 11. Otherwise, if parsing the URL failed, the user agent may report the
// error to the user in a user-agent-specific manner, may queue an element
// task on the DOM manipulation task source given subject to navigate the
// target browsing context to an error page to report the error, or may
// ignore the error and do nothing. In any case, the user agent must then
// return.
// 12. If hyperlinkSuffix is non-null, then append it to URL.
if (hyperlink_suffix.has_value()) {
StringBuilder url_builder;
url_builder.append(url_string);
url_builder.append(*hyperlink_suffix);
url_string = url_builder.to_string();
}
// FIXME: 13. Let request be a new request whose URL is URL and whose
// referrer policy is the current state of subject's referrerpolicy content
// attribute.
// FIXME: 14. If subject's link types includes the noreferrer keyword, then
// set request's referrer to "no-referrer".
// 15. Queue an element task on the DOM manipulation task source given
// subject to navigate target to request with the source browsing context
// set to source.
// FIXME: "navigate" means implementing the navigation algorithm here:
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate
hyperlink_element_utils_queue_an_element_task(Task::Source::DOMManipulation, [url_string, target] {
target->loader().load(url_string, FrameLoader::Type::Navigation);
});
}
String HTMLHyperlinkElementUtils::get_an_elements_target() const
{
// To get an element's target, given an a, area, or form element element, run these steps:
// 1. If element has a target attribute, then return that attribute's value.
if (auto target = hyperlink_element_utils_target(); !target.is_empty())
return target;
// FIXME: 2. If element's node document contains a base element with a
// target attribute, then return the value of the target attribute of the
// first such base element.
// 3. Return the empty string.
return "";
}
bool HTMLHyperlinkElementUtils::get_an_elements_noopener(StringView target) const
{
// To get an element's noopener, given an a, area, or form element element and a string target:
// FIXME: 1. If element's link types include the noopener or noreferrer
// keyword, then return true.
// FIXME: 2. If element's link types do not include the opener keyword and
// target is an ASCII case-insensitive match for "_blank", then return true.
if (target.equals_ignoring_case("_blank"sv))
return true;
// 3. Return false.
return false;
}
}

View file

@ -8,6 +8,7 @@
#include <AK/URL.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/EventLoop/Task.h>
namespace Web::HTML {
@ -48,15 +49,23 @@ public:
void set_hash(String);
protected:
virtual DOM::Document const& hyperlink_element_utils_document() const = 0;
virtual DOM::Document& hyperlink_element_utils_document() = 0;
virtual String hyperlink_element_utils_href() const = 0;
virtual void set_hyperlink_element_utils_href(String) = 0;
virtual bool hyperlink_element_utils_is_html_anchor_element() const = 0;
virtual bool hyperlink_element_utils_is_connected() const = 0;
virtual String hyperlink_element_utils_target() const = 0;
virtual void hyperlink_element_utils_queue_an_element_task(HTML::Task::Source source, Function<void()> steps) = 0;
void set_the_url();
void follow_the_hyperlink(Optional<String> hyperlink_suffix);
private:
void reinitialize_url() const;
void update_href();
bool cannot_navigate() const;
String get_an_elements_target() const;
bool get_an_elements_noopener(StringView target) const;
Optional<AK::URL> m_url;
};

View file

@ -194,7 +194,46 @@ bool EventHandler::handle_mouseup(const Gfx::IntPoint& position, unsigned button
node->dispatch_event(UIEvents::MouseEvent::create(UIEvents::EventNames::mouseup, offset.x(), offset.y(), position.x(), position.y()));
handled_event = true;
if (node.ptr() == m_mousedown_target) {
bool should_dispatch_event = true;
// FIXME: This is ad-hoc and incorrect. The reason this exists is
// because we are missing browsing context navigation:
//
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate
//
// Additionally, we currently cannot spawn a new top-level
// browsing context for new tab operations, because the new
// top-level browsing context would be in another process. To
// fix this, there needs to be some way to be able to
// communicate with browsing contexts in remote WebContent
// processes, and then step 8 of this algorithm needs to be
// implemented in BrowsingContext::choose_a_browsing_context:
//
// https://html.spec.whatwg.org/multipage/browsers.html#the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name
if (RefPtr<HTML::HTMLAnchorElement> link = node->enclosing_link_element()) {
NonnullRefPtr document = *m_browsing_context.active_document();
auto href = link->href();
auto url = document->parse_url(href);
dbgln("Web::EventHandler: Clicking on a link to {}", url);
if (button == GUI::MouseButton::Primary) {
if (href.starts_with("javascript:")) {
document->run_javascript(href.substring_view(11, href.length() - 11));
} else if (!url.fragment().is_null() && url.equals(document->url(), AK::URL::ExcludeFragment::Yes)) {
m_browsing_context.scroll_to_anchor(url.fragment());
} else if (modifiers != 0) {
if (m_browsing_context.is_top_level()) {
if (auto* page = m_browsing_context.page())
page->client().page_did_click_link(url, link->target(), modifiers);
}
}
} else if (button == GUI::MouseButton::Middle) {
if (auto* page = m_browsing_context.page())
page->client().page_did_middle_click_link(url, link->target(), modifiers);
should_dispatch_event = false;
}
}
if (node.ptr() == m_mousedown_target && should_dispatch_event) {
node->dispatch_event(UIEvents::MouseEvent::create(UIEvents::EventNames::click, offset.x(), offset.y(), position.x(), position.y()));
}
}
@ -272,37 +311,10 @@ bool EventHandler::handle_mousedown(const Gfx::IntPoint& position, unsigned butt
return true;
}
if (RefPtr<HTML::HTMLAnchorElement> link = node->enclosing_link_element()) {
auto href = link->href();
auto url = document->parse_url(href);
dbgln("Web::EventHandler: Clicking on a link to {}", url);
if (button == GUI::MouseButton::Primary) {
if (href.starts_with("javascript:")) {
document->run_javascript(href.substring_view(11, href.length() - 11));
} else if (!url.fragment().is_null() && url.equals(document->url(), AK::URL::ExcludeFragment::Yes)) {
m_browsing_context.scroll_to_anchor(url.fragment());
} else {
document->set_active_element(link);
if (m_browsing_context.is_top_level()) {
if (auto* page = m_browsing_context.page())
page->client().page_did_click_link(url, link->target(), modifiers);
} else {
// FIXME: Handle different targets!
m_browsing_context.loader().load(url, FrameLoader::Type::Navigation);
}
}
} else if (button == GUI::MouseButton::Secondary) {
if (auto* page = m_browsing_context.page())
page->client().page_did_request_link_context_menu(m_browsing_context.to_top_level_position(position), url, link->target(), modifiers);
} else if (button == GUI::MouseButton::Middle) {
if (auto* page = m_browsing_context.page())
page->client().page_did_middle_click_link(url, link->target(), modifiers);
}
} else {
if (button == GUI::MouseButton::Primary) {
auto result = paint_root()->hit_test(position.to_type<float>(), Painting::HitTestType::TextCursor);
if (result.has_value() && result->dom_node()) {
if (button == GUI::MouseButton::Primary) {
if (auto result = paint_root()->hit_test(position.to_type<float>(), Painting::HitTestType::TextCursor); result.has_value()) {
auto paintable = result->paintable;
if (paintable->dom_node()) {
// See if we want to focus something.
bool did_focus_something = false;
for (auto candidate = node; candidate; candidate = candidate->parent()) {
@ -316,15 +328,15 @@ bool EventHandler::handle_mousedown(const Gfx::IntPoint& position, unsigned butt
// If we didn't focus anything, place the document text cursor at the mouse position.
// FIXME: This is all rather strange. Find a better solution.
if (!did_focus_something) {
m_browsing_context.set_cursor_position(DOM::Position(*result->dom_node(), result->index_in_node));
layout_root()->set_selection({ { result->paintable->layout_node(), result->index_in_node }, {} });
m_browsing_context.set_cursor_position(DOM::Position(*paintable->dom_node(), result->index_in_node));
layout_root()->set_selection({ { paintable->layout_node(), result->index_in_node }, {} });
m_in_mouse_selection = true;
}
}
} else if (button == GUI::MouseButton::Secondary) {
if (auto* page = m_browsing_context.page())
page->client().page_did_request_context_menu(m_browsing_context.to_top_level_position(position));
}
} else if (button == GUI::MouseButton::Secondary) {
if (auto* page = m_browsing_context.page())
page->client().page_did_request_context_menu(m_browsing_context.to_top_level_position(position));
}
return true;
}