fix(webidl): unify EventListener argument conversion

This commit is contained in:
ldm0
2026-09-24 05:17:04 +08:00
parent 72bc7cbef7
commit fdc44dbbac
8 changed files with 343 additions and 264 deletions
+20 -29
View File
@@ -8,7 +8,7 @@
//! registration back to the owning store.
use crate::context_bootstrap::context_host_ptr_from_global_bridge;
use crate::util::{throw_type_error, v8str};
use crate::webidl;
#[derive(Clone, Copy)]
enum AbortSignalOwner {
@@ -136,39 +136,30 @@ impl<'s> ResolvedAbortSignal<'s> {
}
}
/// Parses the `signal` member of `AddEventListenerOptions`.
///
/// The outer `Option` distinguishes abrupt conversion from an absent member;
/// the inner `Option` distinguishes no signal from a validated Window/worker
/// signal capability.
/// Converts the final AddEventListenerOptions member after capture/once/passive.
/// A dictionary uses Get, not HasProperty: Proxy traps and inherited getters
/// must run exactly once, and their original exceptions must escape unchanged.
pub(crate) fn event_listener_signal_from_options_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
) -> Option<Option<ResolvedAbortSignal<'s>>> {
if value.is_null_or_undefined() || !value.is_object() {
return Some(None);
}
) -> Result<Option<ResolvedAbortSignal<'s>>, webidl::WebIdlError> {
let Ok(options) = v8::Local::<v8::Object>::try_from(value) else {
return Some(None);
return Ok(None);
};
let context = webidl::Context::member("AddEventListenerOptions", "signal");
let Some(signal_value) = webidl::property_result(scope, options, "signal", context)? else {
return Ok(None);
};
let signal_key = v8str(scope, "signal");
let signal_value = options.get(scope, signal_key.into())?;
if signal_value.is_undefined() {
return Some(None);
return Ok(None);
}
let Ok(signal) = v8::Local::<v8::Object>::try_from(signal_value) else {
throw_type_error(
scope,
"Failed to execute 'addEventListener': options.signal must be an AbortSignal.",
);
return None;
};
let Some(signal) = ResolvedAbortSignal::resolve(scope, signal) else {
throw_type_error(
scope,
"Failed to execute 'addEventListener': options.signal must be an AbortSignal.",
);
return None;
};
Some(Some(signal))
let signal = v8::Local::<v8::Object>::try_from(signal_value)
.ok()
.and_then(|signal| ResolvedAbortSignal::resolve(scope, signal))
.ok_or_else(|| {
webidl::WebIdlError::custom_message(
"Failed to execute 'addEventListener': options.signal must be an AbortSignal.",
)
})?;
Ok(Some(signal))
}
+2 -31
View File
@@ -41,7 +41,7 @@ pub(crate) struct RemoveEventListenerArgs {
pub(crate) event_type: String,
#[webidl(required, converter = "callback_interface", nullable)]
pub(crate) listener: Option<webidl::WebIdlCallbackInterface>,
#[webidl(with = remove_event_listener_options)]
#[webidl(with = webidl::event_listener_options)]
pub(crate) options: webidl::EventListenerOptions,
}
@@ -59,35 +59,6 @@ fn add_event_listener_options<'s>(
// lexical order. Finish conversion even when the callback is null.
let value = args.get(index);
let options = webidl::add_event_listener_options_value(scope, value)?;
let signal = event_listener_signal_from_options_value(scope, value).ok_or_else(|| {
webidl::WebIdlError::pending_exception(webidl::Context::member(
"AddEventListenerOptions",
"signal",
))
})?;
let signal = event_listener_signal_from_options_value(scope, value)?;
Ok(AddEventListenerOptions { options, signal })
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "EventListenerOptions")]
struct RemoveEventListenerOptions {
#[webidl(default = false)]
capture: bool,
}
fn remove_event_listener_options<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
index: i32,
) -> Result<webidl::EventListenerOptions, webidl::WebIdlError> {
let value = args.get(index);
let capture = if let Ok(object) = v8::Local::<v8::Object>::try_from(value) {
webidl::parse_dictionary_object::<RemoveEventListenerOptions>(scope, object)?.capture
} else {
value.boolean_value(scope)
};
Ok(webidl::EventListenerOptions {
capture,
..webidl::EventListenerOptions::default()
})
}
@@ -0,0 +1,254 @@
use super::service_worker_drain::drain_service_worker_test_turn;
use super::*;
const OPTIONS_PROBE: &str = r#"function checkOptions(targets) {
const rows=[];let serial=0;
for(const [targetName,create] of targets) for(const method of ['addEventListener','removeEventListener'])
for(const callbackKind of ['function','null','primitive']) for(const fail of ['none','capture','once','passive','signal']) {
const [target,eventType,cleanup]=create(++serial);
const trace=[],marker={};let count=0,error=null;
const listener=()=>count++;
const callback=callbackKind==='function'?listener:callbackKind==='null'?null:42;
if(method==='removeEventListener')target.addEventListener(eventType,listener);
const type={toString(){trace.push('type');return eventType;}};
const options=new Proxy({}, {
has(_,name){trace.push('has:'+name);return true;},
get(_,name){trace.push(String(name));if(name===fail)throw marker;return name==='signal'?undefined:false;}
});
try{target[method](type,callback,options);}catch(e){error=e===marker?'marker':e.name;}
target.dispatchEvent(new Event(eventType));
target.removeEventListener(eventType,listener);
rows.push({target:targetName,method,callback:callbackKind,fail,trace,error,count});
if(cleanup)cleanup();
}
return rows;
}
"#;
const PORT_PROBE: &str = r#"async function checkPortOptions() {
const targets=[['MessagePort',()=>{const channel=new MessageChannel();return [channel.port1,'message',()=>{channel.port1.close();channel.port2.close();},()=>channel.port2.postMessage('probe')];}]];
const rows=[];let serial=0;
for(const [targetName,create] of targets) for(const method of ['addEventListener','removeEventListener'])
for(const callbackKind of ['function','null','primitive']) for(const fail of ['none','capture','once','passive','signal']) {
const [target,eventType,cleanup,send]=create(++serial);
const trace=[],marker={};let count=0,error=null;
const listener=()=>count++;
const callback=callbackKind==='function'?listener:callbackKind==='null'?null:42;
if(method==='removeEventListener')target.addEventListener(eventType,listener);
const type={toString(){trace.push('type');return eventType;}};
const options=new Proxy({}, {
has(_,name){trace.push('has:'+name);return true;},
get(_,name){trace.push(String(name));if(name===fail)throw marker;return name==='signal'?undefined:false;}
});
try{target[method](type,callback,options);}catch(e){error=e===marker?'marker':e.name;}
await new Promise(resolve=>{target.onmessage=resolve;target.start();send();});
target.removeEventListener(eventType,listener);
rows.push({target:targetName,method,callback:callbackKind,fail,trace,error,count});
if(cleanup)cleanup();
}
return rows;
}
"#;
const PAGE_PROBE: &str = r#"(() => {
const frame=document.body.appendChild(document.createElement('iframe'));
const rows=checkOptions([
['Window',n=>[window,'options-'+n]],
['child Window',n=>[frame.contentWindow,'options-'+n]],
['Document',n=>[document,'options-'+n]],
['Element',n=>[document.createElement('div'),'options-'+n]],
['EventTarget',n=>[new EventTarget(),'options-'+n]],
['FileReader',n=>[new FileReader(),'options-'+n]],
['AbortSignal',()=>[new AbortController().signal,'abort']]
]);frame.remove();return rows;
})()"#;
const WORKER_TARGETS: &str = r#"checkOptions([
['WorkerGlobalScope',n=>[self,'options-'+n]],
['EventTarget',n=>[new EventTarget(),'options-'+n]],
['FileReader',n=>[new FileReader(),'options-'+n]],
['AbortSignal',()=>[new AbortController().signal,'abort']]
])"#;
const SIGNAL_PROBE: &str = r#"(() => {
const frame=document.body.appendChild(document.createElement('iframe'));
const child=frame.contentWindow;
const channel=new MessageChannel();
const targets=[window,child,document,document.createElement('div'),new EventTarget(),new FileReader(),new AbortController().signal,channel.port1];
const localSignal=new AbortController().signal,childSignal=new child.AbortController().signal;
const revoked=Proxy.revocable(localSignal,{});revoked.revoke();
const signals=[['undefined',undefined],['null',null],['number',1],['object',{}],['forged',Object.create(AbortSignal.prototype)],['proxy',new Proxy(localSignal,{})],['revoked',revoked.proxy],['local',localSignal],['child',childSignal]];
const rows=[];
for(let index=0;index<targets.length;index++)for(const method of ['addEventListener','removeEventListener'])for(const [kind,signal]of signals){
const trace=[];let error=null,realm=null;
const options=new Proxy({}, {get(_,key){trace.push(String(key));return key==='signal'?signal:false;},has(){throw new Error('must not call HasProperty');}});
try{targets[index][method]('probe',null,options);}catch(e){error=e.name;realm=e instanceof (index===1?child:window).TypeError;}
rows.push({index,method,kind,trace,error,realm});
}
channel.port1.close();channel.port2.close();frame.remove();return rows;
})()"#;
fn assert_option_rows(value: &serde_json::Value, expected_rows: usize) {
let rows = value.as_array().expect("options probe must return rows");
assert_eq!(rows.len(), expected_rows);
for row in rows {
let add = row["method"] == "addEventListener";
let primitive = row["callback"] == "primitive";
let callable = row["callback"] == "function";
let fail = row["fail"].as_str().unwrap();
let members: &[&str] = if add {
&["capture", "once", "passive", "signal"]
} else {
&["capture"]
};
let mut trace = vec!["type"];
let mut error = None;
if primitive {
error = Some("TypeError");
} else {
for member in members {
trace.push(member);
if *member == fail {
error = Some("marker");
break;
}
}
}
let count = if add {
usize::from(callable && error.is_none())
} else {
usize::from(!callable || error.is_some())
};
assert_eq!(row["trace"], serde_json::json!(trace), "{row}");
assert_eq!(row["error"], serde_json::json!(error), "{row}");
assert_eq!(row["count"], serde_json::json!(count), "{row}");
}
}
#[test]
fn event_listener_options_convert_in_order_and_stop_before_mutation() {
let mut vm = new_parsed_test_vm(
"https://event-listener-options.test/",
"<!doctype html><html><body></body></html>",
);
let value = vm
.eval(&format!("{OPTIONS_PROBE}\nJSON.stringify({PAGE_PROBE})"))
.unwrap();
assert_option_rows(&serde_json::from_str(&value).unwrap(), 210);
}
#[test]
fn event_listener_options_validate_signal_even_for_null_callbacks() {
let mut vm = new_parsed_test_vm(
"https://event-listener-signal-conversion.test/",
"<!doctype html><html><body></body></html>",
);
let value = vm.eval(&format!("JSON.stringify({SIGNAL_PROBE})")).unwrap();
let rows: serde_json::Value = serde_json::from_str(&value).unwrap();
let rows = rows.as_array().unwrap();
assert_eq!(rows.len(), 144);
for row in rows {
let add = row["method"] == "addEventListener";
let valid = matches!(
row["kind"].as_str().unwrap(),
"undefined" | "local" | "child"
);
let expected = if add && !valid {
serde_json::json!("TypeError")
} else {
serde_json::Value::Null
};
assert_eq!(row["error"], expected, "{row}");
assert_eq!(
row["realm"],
if expected.is_null() {
serde_json::Value::Null
} else {
serde_json::json!(true)
},
"{row}"
);
assert_eq!(
row["trace"],
if add {
serde_json::json!(["capture", "once", "passive", "signal"])
} else {
serde_json::json!(["capture"])
},
"{row}"
);
}
}
#[tokio::test]
async fn worker_event_listener_options_share_conversion_and_exception_semantics() {
for shared in [false, true] {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
let (mut vm, browser_context_runtime) =
new_service_worker_page_test_vm_with_loader_and_browser_context_runtime(
"https://worker-listener-options.test/",
&loader,
);
let worker_source = if shared {
format!(
"{OPTIONS_PROBE}\n{PORT_PROBE}\nonconnect=async e=>e.ports[0].postMessage({{options:{WORKER_TARGETS},ports:await checkPortOptions()}});"
)
} else {
format!(
"{OPTIONS_PROBE}\n{PORT_PROBE}\n(async()=>postMessage({{options:{WORKER_TARGETS},ports:await checkPortOptions()}}))();"
)
};
let worker_source = serde_json::to_string(&worker_source).unwrap();
let constructor = if shared { "SharedWorker" } else { "Worker" };
let port = if shared { "worker.port" } else { "worker" };
let cleanup = if shared {
"port.close()"
} else {
"worker.terminate()"
};
vm.eval(&format!(r#"
const url=URL.createObjectURL(new Blob([{worker_source}],{{type:'text/javascript'}}));
const worker=new {constructor}(url),port={port};
port.onmessage=e=>{{globalThis.optionsResult=e.data;{cleanup};URL.revokeObjectURL(url);}};
worker.onerror=e=>{{globalThis.optionsResult=String(e.message);}};
"#)).unwrap();
tokio::time::timeout(std::time::Duration::from_secs(10), async {
while vm.eval("globalThis.optionsResult !== undefined").unwrap() != "true" {
browser_context_runtime.drain_shared_worker_service_lane();
drain_service_worker_test_turn(&mut vm, &browser_context_runtime, &loader).await;
}
})
.await
.expect("worker options probe should settle");
let value = vm.eval("JSON.stringify(globalThis.optionsResult)").unwrap();
let value: serde_json::Value = serde_json::from_str(&value).unwrap();
assert_option_rows(&value["options"], 120);
assert_option_rows(&value["ports"], 30);
}
}
#[tokio::test]
async fn message_port_options_errors_preserve_actual_message_listeners() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
let (mut vm, browser_context_runtime) =
new_service_worker_page_test_vm_with_loader_and_browser_context_runtime(
"https://message-port-options.test/",
&loader,
);
vm.eval(&format!("{PORT_PROBE}\ncheckPortOptions().then(value=>globalThis.portOptionsResult=value,error=>globalThis.portOptionsResult=String(error));")).unwrap();
tokio::time::timeout(std::time::Duration::from_secs(10), async {
while vm
.eval("globalThis.portOptionsResult !== undefined")
.unwrap()
!= "true"
{
drain_service_worker_test_turn(&mut vm, &browser_context_runtime, &loader).await;
}
})
.await
.expect("message port options probe should settle");
let value = vm
.eval("JSON.stringify(globalThis.portOptionsResult)")
.unwrap();
assert_option_rows(&serde_json::from_str(&value).unwrap(), 30);
}
@@ -16,6 +16,7 @@ mod crypto_subtle_x25519;
mod date_locale;
mod details;
mod event_handlers;
mod event_listener_options;
mod events_selection_storage;
mod gamepad;
mod ice_candidate;
+26 -133
View File
@@ -39,7 +39,7 @@ use super::{
},
script_provenance::CompiledStringProvenance,
util::{
callback_arg_string, context_host_from_global_bridge, context_host_ptr_from_global_bridge,
context_host_from_global_bridge, context_host_ptr_from_global_bridge,
context_host_ptr_from_window_object, define_non_enumerable_static_bool_property,
get_private_value, object_bool_property, object_number_property,
script_base_url_from_continuation_data, script_base_url_from_host_defined_options,
@@ -47,6 +47,9 @@ use super::{
},
webidl,
};
use crate::event_listener_args::{
AddEventListenerArgs, RemoveEventListenerArgs, parse_listener_args,
};
use crate::web_api_interfaces;
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
@@ -142,124 +145,6 @@ struct IdleDeadlinePrototypeDeclaration {
time_remaining: (),
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "EventTarget.addEventListener")]
struct WindowAddEventListenerArgs<'s> {
#[webidl(with = window_add_event_listener_call)]
call: webidl::ParseOutcome<WindowAddEventListenerCall<'s>>,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "EventTarget.removeEventListener")]
struct WindowRemoveEventListenerArgs<'s> {
#[webidl(with = window_remove_event_listener_call)]
call: webidl::ParseOutcome<WindowRemoveEventListenerCall<'s>>,
}
struct WindowAddEventListenerCall<'s> {
event_type: String,
callback: v8::Local<'s, v8::Object>,
callback_relevant_context: v8::Local<'s, v8::Context>,
incumbent_context: v8::Local<'s, v8::Context>,
options: webidl::EventListenerOptions,
signal: Option<v8::Local<'s, v8::Object>>,
}
struct WindowRemoveEventListenerCall<'s> {
event_type: String,
callback: v8::Local<'s, v8::Object>,
options: webidl::EventListenerOptions,
}
fn window_add_event_listener_call<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
_index: i32,
) -> Result<webidl::ParseOutcome<WindowAddEventListenerCall<'s>>, webidl::WebIdlError> {
let Some(event_type) = callback_arg_string(scope, args, 0) else {
return Ok(webidl::ParseOutcome::Skip);
};
let options = webidl::event_listener_options(scope, args, 2, true);
let listener_arg = args.get(1);
let current_context = scope.get_current_context();
let callback = if let Ok(function) = v8::Local::<v8::Function>::try_from(listener_arg) {
let callback = v8::Local::<v8::Object>::from(function);
let callback_relevant_context = callback
.get_creation_context(scope)
.unwrap_or(current_context);
Some((callback, callback_relevant_context))
} else if listener_arg.is_object() && !listener_arg.is_null_or_undefined() {
let Ok(object) = v8::Local::<v8::Object>::try_from(listener_arg) else {
return Ok(webidl::ParseOutcome::Skip);
};
let callback_relevant_context = object
.get_creation_context(scope)
.unwrap_or(current_context);
Some((object, callback_relevant_context))
} else {
None
};
let Some((callback, callback_relevant_context)) = callback else {
return Ok(webidl::ParseOutcome::Skip);
};
let incumbent_context = scope.get_incumbent_context().unwrap_or(current_context);
Ok(webidl::ParseOutcome::Parsed(WindowAddEventListenerCall {
event_type,
callback,
callback_relevant_context,
incumbent_context,
signal: signal_from_options_value(scope, args.get(2)),
options,
}))
}
fn window_remove_event_listener_call<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
_index: i32,
) -> Result<webidl::ParseOutcome<WindowRemoveEventListenerCall<'s>>, webidl::WebIdlError> {
let Some(event_type) = callback_arg_string(scope, args, 0) else {
return Ok(webidl::ParseOutcome::Skip);
};
let options = webidl::event_listener_options(scope, args, 2, true);
let listener_arg = args.get(1);
let callback = if let Ok(function) = v8::Local::<v8::Function>::try_from(listener_arg) {
Some(v8::Local::<v8::Object>::from(function))
} else if listener_arg.is_object() && !listener_arg.is_null_or_undefined() {
v8::Local::<v8::Object>::try_from(listener_arg).ok()
} else {
None
};
let Some(callback) = callback else {
return Ok(webidl::ParseOutcome::Skip);
};
Ok(webidl::ParseOutcome::Parsed(
WindowRemoveEventListenerCall {
event_type,
callback,
options,
},
))
}
fn signal_from_options_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
) -> Option<v8::Local<'s, v8::Object>> {
let Ok(options) = v8::Local::<v8::Object>::try_from(value) else {
return None;
};
options
.get(scope, v8str(scope, "signal").into())
.and_then(|value| {
if value.is_null_or_undefined() {
None
} else {
v8::Local::<v8::Object>::try_from(value).ok()
}
})
}
pub(super) fn event_target_add_event_listener_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -273,16 +158,17 @@ pub(super) fn event_target_add_event_listener_callback<'s>(
return;
};
let host = unsafe { &mut *host_ptr };
let parsed = webidl::parse_args::<WindowAddEventListenerArgs>(scope, &args);
let Some(parsed) = parsed else {
let parsed = parse_listener_args::<AddEventListenerArgs>(scope, &args, "addEventListener");
let Some(call) = parsed else {
return;
};
let webidl::ParseOutcome::Parsed(call) = parsed.call else {
let Some(listener) = call.listener else {
return;
};
let capture = call.options.capture;
let once = call.options.once;
let signal = call.signal;
let options = call.options.options;
let capture = options.capture;
let once = options.once;
let signal = call.options.signal.map(|signal| signal.value());
if let Some(signal) = signal
&& host.abort_signal_aborted(scope, signal)
{
@@ -303,17 +189,20 @@ pub(super) fn event_target_add_event_listener_callback<'s>(
throw_type_error(scope, "Illegal invocation");
return;
};
let passive = call
.options
let passive = options
.passive
.unwrap_or_else(|| default_passive_value(host, target, &call.event_type));
let callback = v8::Local::<v8::Object>::try_from(listener.value(scope))
.expect("converted EventListener must remain an object");
let callback_relevant_context = listener.relevant_context(scope);
let incumbent_context = listener.incumbent_context(scope);
let Some(callback_id) = host.register_target_event_listener(
scope,
target,
&call.event_type,
call.callback,
call.callback_relevant_context,
call.incumbent_context,
callback,
callback_relevant_context,
incumbent_context,
capture,
once,
passive,
@@ -391,10 +280,12 @@ pub(super) fn event_target_remove_event_listener_callback<'s>(
return;
};
let host = unsafe { &mut *host_ptr };
let Some(parsed) = webidl::parse_args::<WindowRemoveEventListenerArgs>(scope, &args) else {
let Some(call) =
parse_listener_args::<RemoveEventListenerArgs>(scope, &args, "removeEventListener")
else {
return;
};
let webidl::ParseOutcome::Parsed(call) = parsed.call else {
let Some(listener) = call.listener else {
return;
};
let capture = call.options.capture;
@@ -411,7 +302,9 @@ pub(super) fn event_target_remove_event_listener_callback<'s>(
throw_type_error(scope, "Illegal invocation");
return;
};
host.remove_registered_event_listener(scope, target, &call.event_type, call.callback, capture);
let callback = v8::Local::<v8::Object>::try_from(listener.value(scope))
.expect("converted EventListener must remain an object");
host.remove_registered_event_listener(scope, target, &call.event_type, callback, capture);
}
pub(super) fn event_target_dispatch_event_callback<'s>(
+27 -65
View File
@@ -1,4 +1,4 @@
use crate::types::EventListenerOptionsMembers;
use crate::types::{AddEventListenerOptionsMembers, EventListenerOptionsMembers};
use crate::{
Context, DomString, EventListenerOptions, UnrestrictedDouble, WebIdlError,
legacy_optional_member, parse_dictionary_object,
@@ -170,69 +170,6 @@ pub fn optional_number_property<'s>(
.map(Into::into)
}
/// Parses the third argument shape used by event listener registration.
///
/// Boolean values use the legacy capture-only path. Object values are parsed as
/// `AddEventListenerOptions`. When `observe_passive` is true, the `passive`
/// member is read even if the resulting value is not otherwise needed, matching
/// sites that observe getter side effects.
pub fn event_listener_options<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
index: i32,
observe_passive: bool,
) -> EventListenerOptions {
if args.length() <= index {
return EventListenerOptions::default();
}
event_listener_options_value(scope, args.get(index), observe_passive)
}
pub fn event_listener_options_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
observe_passive: bool,
) -> EventListenerOptions {
if is_nullish(value) {
return EventListenerOptions::default();
}
if !value.is_object() || value.is_boolean() {
return EventListenerOptions {
capture: value.boolean_value(scope),
once: false,
passive: None,
};
}
let Ok(object) = v8::Local::<v8::Object>::try_from(value) else {
return EventListenerOptions::default();
};
if observe_passive {
let _ = property(scope, object, "passive");
}
parse_dictionary_object::<EventListenerOptionsMembers>(scope, object)
.map(|parsed| EventListenerOptions {
capture: parsed.capture,
once: parsed.once,
passive: parsed.passive,
})
.unwrap_or_default()
}
pub fn event_listener_once_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
) -> bool {
event_listener_options_value(scope, value, false).once
}
pub fn event_listener_once_option<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
index: i32,
) -> bool {
event_listener_options(scope, args, index, false).once
}
/// Converts `(AddEventListenerOptions or boolean)` through `passive`.
///
/// The caller must convert the platform-specific `signal` member next, before
@@ -247,10 +184,35 @@ pub fn add_event_listener_options_value<'s>(
..EventListenerOptions::default()
});
};
let parsed = parse_dictionary_object::<EventListenerOptionsMembers>(scope, object)?;
let parsed = parse_dictionary_object::<AddEventListenerOptionsMembers>(scope, object)?;
Ok(EventListenerOptions {
capture: parsed.capture,
once: parsed.once,
passive: parsed.passive,
})
}
/// Converts removeEventListener's `(EventListenerOptions or boolean)` argument.
/// Only `capture` belongs to this dictionary; no registration-only getters run.
pub fn event_listener_options<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
index: i32,
) -> Result<EventListenerOptions, WebIdlError> {
event_listener_options_value(scope, args.get(index))
}
pub fn event_listener_options_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
) -> Result<EventListenerOptions, WebIdlError> {
let capture = if let Ok(object) = v8::Local::<v8::Object>::try_from(value) {
parse_dictionary_object::<EventListenerOptionsMembers>(scope, object)?.capture
} else {
value.boolean_value(scope)
};
Ok(EventListenerOptions {
capture,
..EventListenerOptions::default()
})
}
+5 -5
View File
@@ -49,11 +49,11 @@ pub use convert::{
};
pub use error::{Context, WebIdlError, WebIdlErrorKind};
pub use helpers::{
add_event_listener_options_value, dictionary_arg, dictionary_value, event_listener_once_option,
event_listener_once_value, event_listener_options, event_listener_options_value, is_nullish,
optional_number_property, optional_object_arg, optional_string_property, property,
property_non_nullish, property_non_undefined, property_result, symbol_property_result,
throw_dom_exception, throw_error, throw_index_size_error, throw_type_error, v8_string,
add_event_listener_options_value, dictionary_arg, dictionary_value, event_listener_options,
event_listener_options_value, is_nullish, optional_number_property, optional_object_arg,
optional_string_property, property, property_non_nullish, property_non_undefined,
property_result, symbol_property_result, throw_dom_exception, throw_error,
throw_index_size_error, throw_type_error, v8_string,
};
pub use moli_webidl_callback::{
PreparedWebIdlCallbackFunction, PreparedWebIdlCallbackInterface, WebIdlCallbackFunction,
+8 -1
View File
@@ -143,10 +143,17 @@ pub struct EventListenerOptions {
}
#[derive(WebIdlDictionary)]
#[webidl(prefix = "AddEventListenerOptions")]
#[webidl(prefix = "EventListenerOptions")]
pub(crate) struct EventListenerOptionsMembers {
#[webidl(default = false)]
pub(crate) capture: bool,
}
#[derive(WebIdlDictionary)]
#[webidl(prefix = "AddEventListenerOptions")]
pub(crate) struct AddEventListenerOptionsMembers {
#[webidl(default = false)]
pub(crate) capture: bool,
#[webidl(default = false)]
pub(crate) once: bool,
pub(crate) passive: Option<bool>,