diff --git a/Userland/Libraries/LibWeb/HTML/BrowsingContext.cpp b/Userland/Libraries/LibWeb/HTML/BrowsingContext.cpp
index 53aed5d5f2..80a7c0454a 100644
--- a/Userland/Libraries/LibWeb/HTML/BrowsingContext.cpp
+++ b/Userland/Libraries/LibWeb/HTML/BrowsingContext.cpp
@@ -428,4 +428,118 @@ RefPtr 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;
+}
+
}
diff --git a/Userland/Libraries/LibWeb/HTML/BrowsingContext.h b/Userland/Libraries/LibWeb/HTML/BrowsingContext.h
index 96980f8608..636893cf12 100644
--- a/Userland/Libraries/LibWeb/HTML/BrowsingContext.h
+++ b/Userland/Libraries/LibWeb/HTML/BrowsingContext.h
@@ -76,6 +76,8 @@ public:
BrowsingContext const& top_level_browsing_context() const { return const_cast(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; }
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLAnchorElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLAnchorElement.cpp
index 665b119027..3b2533fd13 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLAnchorElement.cpp
+++ b/Userland/Libraries/LibWeb/HTML/HTMLAnchorElement.cpp
@@ -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 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);
+}
+
}
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLAnchorElement.h b/Userland/Libraries/LibWeb/HTML/HTMLAnchorElement.h
index 8fc77c2ada..b55236dd83 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLAnchorElement.h
+++ b/Userland/Libraries/LibWeb/HTML/HTMLAnchorElement.h
@@ -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 steps) override
+ {
+ queue_an_element_task(source, move(steps));
+ }
};
}
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLAreaElement.h b/Userland/Libraries/LibWeb/HTML/HTMLAreaElement.h
index df7a662d81..fecfb33af8 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLAreaElement.h
+++ b/Userland/Libraries/LibWeb/HTML/HTMLAreaElement.h
@@ -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 steps) override
+ {
+ queue_an_element_task(source, move(steps));
+ }
};
}
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLHyperlinkElementUtils.cpp b/Userland/Libraries/LibWeb/HTML/HTMLHyperlinkElementUtils.cpp
index 25b80bb2f4..794cb51453 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLHyperlinkElementUtils.cpp
+++ b/Userland/Libraries/LibWeb/HTML/HTMLHyperlinkElementUtils.cpp
@@ -7,6 +7,7 @@
#include
#include
#include
+#include
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(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 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;
+}
+
}
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLHyperlinkElementUtils.h b/Userland/Libraries/LibWeb/HTML/HTMLHyperlinkElementUtils.h
index 75f2e5f49f..f1612a2f83 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLHyperlinkElementUtils.h
+++ b/Userland/Libraries/LibWeb/HTML/HTMLHyperlinkElementUtils.h
@@ -8,6 +8,7 @@
#include
#include
+#include
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 steps) = 0;
void set_the_url();
+ void follow_the_hyperlink(Optional 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 m_url;
};
diff --git a/Userland/Libraries/LibWeb/Page/EventHandler.cpp b/Userland/Libraries/LibWeb/Page/EventHandler.cpp
index b3da68ed98..a001e4a42a 100644
--- a/Userland/Libraries/LibWeb/Page/EventHandler.cpp
+++ b/Userland/Libraries/LibWeb/Page/EventHandler.cpp
@@ -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 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 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(), 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(), 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;
}