script: Allow opening links in a new WebView (#35017)

This changes starts tracking the keyboard modifier state in the
`Constellation` and forwards it with every input event. The state
is used to modify the target of link click so when the
platform-dependent alternate action key is enabled, the target is
overriden to "_blank".

In addition, specification step numbers and text is updated.

Signed-off-by: webbeef <me@webbeef.org>
Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
webbeef 2025-03-23 03:59:19 -07:00 committed by GitHub
parent 8b8b447ef0
commit 90161c1c91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 114 additions and 25 deletions

View file

@ -36,7 +36,7 @@ use html5ever::{LocalName, Namespace, QualName, local_name, namespace_url, ns};
use hyper_serde::Serde;
use ipc_channel::ipc;
use js::rust::{HandleObject, HandleValue};
use keyboard_types::{Code, Key, KeyState};
use keyboard_types::{Code, Key, KeyState, Modifiers};
use metrics::{InteractiveFlag, InteractiveWindow, ProgressiveWebMetrics};
use mime::{self, Mime};
use net_traits::CookieSource::NonHTTP;
@ -531,6 +531,9 @@ pub(crate) struct Document {
/// The lifetime of an intersection observer is specified at
/// <https://github.com/w3c/IntersectionObserver/issues/525>.
intersection_observers: DomRefCell<Vec<Dom<IntersectionObserver>>>,
/// The active keyboard modifiers for the WebView. This is updated when receiving any input event.
#[no_trace]
active_keyboard_modifiers: Cell<Modifiers>,
}
#[allow(non_snake_case)]
@ -3868,6 +3871,7 @@ impl Document {
inherited_insecure_requests_policy: Cell::new(inherited_insecure_requests_policy),
intersection_observer_task_queued: Cell::new(false),
intersection_observers: Default::default(),
active_keyboard_modifiers: Cell::new(Modifiers::empty()),
}
}
@ -3888,6 +3892,26 @@ impl Document {
.unwrap_or(InsecureRequestsPolicy::DoNotUpgrade)
}
/// Update the active keyboard modifiers for this [`Document`] while handling events.
pub(crate) fn update_active_keyboard_modifiers(&self, modifiers: Modifiers) {
self.active_keyboard_modifiers.set(modifiers);
}
pub(crate) fn alternate_action_keyboard_modifier_active(&self) -> bool {
#[cfg(target_os = "macos")]
{
self.active_keyboard_modifiers
.get()
.contains(Modifiers::META)
}
#[cfg(not(target_os = "macos"))]
{
self.active_keyboard_modifiers
.get()
.contains(Modifiers::CONTROL)
}
}
/// Note a pending compositor event, to be processed at the next `update_the_rendering` task.
pub(crate) fn note_pending_input_event(&self, event: ConstellationInputEvent) {
let mut pending_compositor_events = self.pending_input_events.borrow_mut();

View file

@ -342,30 +342,45 @@ pub(crate) fn follow_hyperlink(
relations: LinkRelations,
hyperlink_suffix: Option<String>,
) {
// Step 1. If subject cannot navigate, then return.
// Step 1: If subject cannot navigate, then return.
if subject.cannot_navigate() {
return;
}
// Step 2, done in Step 7.
// Step 2: Let targetAttributeValue be the empty string.
// This is done below.
// Step 3: If subject is an a or area element, then set targetAttributeValue to the
// result of getting an element's target given subject.
//
// Also allow the user to open links in a new WebView by pressing either the meta or
// control key (depending on the platform).
let document = subject.owner_document();
let window = document.window();
// Step 3: source browsing context.
let source = document.browsing_context().unwrap();
// Step 4-5: target attribute.
let target_attribute_value =
if subject.is::<HTMLAreaElement>() || subject.is::<HTMLAnchorElement>() {
get_element_target(subject)
if document.alternate_action_keyboard_modifier_active() {
Some("_blank".into())
} else {
get_element_target(subject)
}
} else {
None
};
// Step 6.
// Step 4: Let urlRecord be the result of encoding-parsing a URL given subject's href
// attribute value, relative to subject's node document.
// Step 5: If urlRecord is failure, then return.
// TODO: Implement this.
// Step 6: Let noopener be the result of getting an element's noopener with subject,
// urlRecord, and targetAttributeValue.
let noopener = relations.get_element_noopener(target_attribute_value.as_ref());
// Step 7.
// Step 7: Let targetNavigable be the first return value of applying the rules for
// choosing a navigable given targetAttributeValue, subject's node navigable, and
// noopener.
let window = document.window();
let source = document.browsing_context().unwrap();
let (maybe_chosen, history_handling) = match target_attribute_value {
Some(name) => {
let (maybe_chosen, new) = source.choose_browsing_context(name, noopener);
@ -379,7 +394,7 @@ pub(crate) fn follow_hyperlink(
None => (Some(window.window_proxy()), NavigationHistoryBehavior::Push),
};
// Step 8.
// Step 8: If targetNavigable is null, then return.
let chosen = match maybe_chosen {
Some(proxy) => proxy,
None => return,
@ -387,17 +402,13 @@ pub(crate) fn follow_hyperlink(
if let Some(target_document) = chosen.document() {
let target_window = target_document.window();
// Step 9, dis-owning target's opener, if necessary
// will have been done as part of Step 7 above
// in choose_browsing_context/create_auxiliary_browsing_context.
// Step 10, 11. TODO: if parsing the URL failed, navigate to error page.
// Step 9: Let urlString be the result of applying the URL serializer to urlRecord.
// TODO: Implement this.
let attribute = subject.get_attribute(&ns!(), &local_name!("href")).unwrap();
let mut href = attribute.Value();
// Step 11: append a hyperlink suffix.
// https://www.w3.org/Bugs/Public/show_bug.cgi?id=28925
// Step 10: If hyperlinkSuffix is non-null, then append it to urlString.
if let Some(suffix) = hyperlink_suffix {
href.push_str(&suffix);
}
@ -405,17 +416,20 @@ pub(crate) fn follow_hyperlink(
return;
};
// Step 12.
// Step 11: Let referrerPolicy be the current state of subject's referrerpolicy content attribute.
let referrer_policy = referrer_policy_for_element(subject);
// Step 13
// Step 12: If subject's link types includes the noreferrer keyword, then set
// referrerPolicy to "no-referrer".
let referrer = if relations.contains(LinkRelations::NO_REFERRER) {
Referrer::NoReferrer
} else {
target_window.as_global_scope().get_referrer()
};
// Step 14
// Step 13: Navigate targetNavigable to urlString using subject's node document,
// with referrerPolicy set to referrerPolicy, userInvolvement set to
// userInvolvement, and sourceElement set to subject.
let pipeline_id = target_window.as_global_scope().pipeline_id();
let secure = target_window.as_global_scope().is_secure_context();
let load_data = LoadData::new(

View file

@ -1069,6 +1069,8 @@ impl ScriptThread {
let window = document.window();
let _realm = enter_realm(document.window());
for event in document.take_pending_input_events().into_iter() {
document.update_active_keyboard_modifiers(event.active_keyboard_modifiers);
match event.event {
InputEvent::MouseButton(mouse_button_event) => {
document.handle_mouse_button_event(