diff --git a/moli-renderer-v8/src/context_bootstrap/event_legacy.rs b/moli-renderer-v8/src/context_bootstrap/event_legacy.rs index 5ed7e90392..20d5745057 100644 --- a/moli-renderer-v8/src/context_bootstrap/event_legacy.rs +++ b/moli-renderer-v8/src/context_bootstrap/event_legacy.rs @@ -124,33 +124,6 @@ struct InitMouseEventArgs<'s> { related_target: Option>, } -#[derive(webidl::WebIdlArgs)] -#[webidl(prefix = "KeyboardEvent.initKeyboardEvent")] -struct InitKeyboardEventArgs<'s> { - #[webidl( - required, - converter = "raw", - missing_message = "Failed to execute 'initKeyboardEvent': 1 argument required." - )] - event_type: webidl::DomString16, - #[webidl(default = false)] - bubbles: bool, - #[webidl(default = false)] - cancelable: bool, - #[webidl(index = 3, converter = "raw")] - view: Option>, - #[webidl(default = "", index = 4)] - key: String, - #[webidl(default = 0, index = 5)] - location: i32, - #[webidl(default = "", index = 6)] - _modifiers_list: String, - #[webidl(default = false, index = 7)] - repeat: bool, - #[webidl(default = "", index = 8)] - _locale: String, -} - #[derive(webidl::WebIdlArgs)] #[webidl(prefix = "TextEvent.initTextEvent")] struct InitTextEventArgs<'s> { @@ -189,13 +162,6 @@ struct InitCompositionEventArgs<'s> { data: String, } -#[derive(webidl::WebIdlArgs)] -#[webidl(prefix = "KeyboardEvent.getModifierState")] -struct KeyboardEventGetModifierStateArgs { - #[webidl(required)] - key_arg: String, -} - #[derive(WebApiObject)] #[webapi(plain, data_properties, enumerable)] struct LegacyUiEventInitDeclaration<'scope> { @@ -243,28 +209,6 @@ struct LegacyMouseEventTailInitDeclaration<'scope> { related_target: v8::Local<'scope, v8::Value>, } -#[derive(WebApiObject)] -#[webapi(plain, data_properties, enumerable)] -struct LegacyKeyboardEventInitDeclaration<'scope> { - view: v8::Local<'scope, v8::Value>, - #[webapi(constructor_default = 0)] - detail: i32, - key: v8::Local<'scope, v8::String>, - code: v8::Local<'scope, v8::String>, - location: i32, - repeat: bool, - #[webapi(constructor_default = false)] - is_composing: bool, - #[webapi(constructor_default = false)] - ctrl_key: bool, - #[webapi(constructor_default = false)] - shift_key: bool, - #[webapi(constructor_default = false)] - alt_key: bool, - #[webapi(constructor_default = false)] - meta_key: bool, -} - #[derive(WebApiObject)] #[webapi(plain, data_properties, enumerable)] struct LegacyCustomEventInitDeclaration<'scope> { @@ -420,32 +364,6 @@ pub(super) fn mouse_event_init_callback<'s>( .expect("legacy MouseEvent tail init declaration should initialize"); } -pub(super) fn keyboard_event_init_callback<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - _rv: v8::ReturnValue<'_, v8::Value>, -) { - let event = super::events::event_backing(scope, args.this()); - let Some(parsed) = webidl::parse_args::(scope, &args) else { - return; - }; - let view = legacy_event_view_or_global(scope, parsed.view); - if !reinitialize_event_object( - scope, - event, - &parsed.event_type.0, - parsed.bubbles, - parsed.cancelable, - ) { - return; - } - let key = v8_string(scope, &parsed.key).unwrap_or_else(|| v8str(scope, "")); - let code = v8str(scope, ""); - LegacyKeyboardEventInitDeclaration::new(view, key, code, parsed.location, parsed.repeat) - .initialize(scope, event) - .expect("legacy KeyboardEvent init declaration should initialize"); -} - pub(super) fn composition_event_init_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -523,38 +441,3 @@ pub(super) fn storage_event_init_callback<'s>( parsed.storage_area.map(|value| value.0.into()), ); } - -pub(super) fn keyboard_event_get_modifier_state_callback<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - let Some(parsed) = webidl::parse_args::(scope, &args) else { - return; - }; - let event = super::events::event_backing(scope, args.this()); - let key_name = match parsed.key_arg.as_str() { - "Alt" | "AltGraph" => "altKey", - "Control" => "ctrlKey", - "Shift" => "shiftKey", - "Meta" => "metaKey", - "Accel" => { - let ctrl = event - .get(scope, v8str(scope, "ctrlKey").into()) - .is_some_and(|value| value.boolean_value(scope)); - let meta = event - .get(scope, v8str(scope, "metaKey").into()) - .is_some_and(|value| value.boolean_value(scope)); - rv.set(v8::Boolean::new(scope, ctrl || meta).into()); - return; - } - _ => { - rv.set(v8::Boolean::new(scope, false).into()); - return; - } - }; - let value = event - .get(scope, v8str(scope, key_name).into()) - .is_some_and(|value| value.boolean_value(scope)); - rv.set(v8::Boolean::new(scope, value).into()); -} diff --git a/moli-renderer-v8/src/context_bootstrap/event_template.rs b/moli-renderer-v8/src/context_bootstrap/event_template.rs index 653379171e..13d8d5e1e9 100644 --- a/moli-renderer-v8/src/context_bootstrap/event_template.rs +++ b/moli-renderer-v8/src/context_bootstrap/event_template.rs @@ -2,7 +2,6 @@ use super::{ event_document::{document_create_event_callback, document_has_focus_callback}, event_legacy::{ composition_event_init_callback, custom_event_init_callback, event_init_event_callback, - keyboard_event_get_modifier_state_callback, keyboard_event_init_callback, mouse_event_init_callback, storage_event_init_callback, text_event_init_callback, ui_event_init_callback, }, @@ -13,16 +12,18 @@ use super::{ event_cancelable_getter_function, event_composed_getter_function, event_composed_path_callback, event_current_target_getter_function, event_default_prevented_getter_function, event_event_phase_getter_function, + event_get_modifier_state_callback, event_platform_attribute_getter, event_prevent_default_callback, event_return_value_getter_function, event_return_value_setter_function, event_src_element_getter_function, event_stop_immediate_propagation_callback, event_stop_propagation_callback, event_target_getter_function, event_time_stamp_getter_function, event_type_getter_function, event_value_attribute_getter, focus_event_related_target_getter_function, - form_data_event_form_data_getter_function, message_event_init_callback, - mouse_event_related_target_getter_function, pointer_event_get_predicted_events_callback, - submit_event_agent_invoked_getter_function, submit_event_respond_with_callback, - submit_event_submitter_getter_function, track_event_track_getter_function, - ui_event_pseudo_target_getter_function, ui_event_which_getter_function, + form_data_event_form_data_getter_function, keyboard_event_init_callback, + message_event_init_callback, mouse_event_related_target_getter_function, + pointer_event_get_predicted_events_callback, submit_event_agent_invoked_getter_function, + submit_event_respond_with_callback, submit_event_submitter_getter_function, + track_event_track_getter_function, ui_event_pseudo_target_getter_function, + ui_event_which_getter_function, }, selection_surface::document_get_selection_callback, specs::ConstructorSpec, @@ -166,7 +167,7 @@ struct FormDataEventTemplateAccessorsDeclaration { #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::UIEvent, enumerable, receiver)] struct UiEventTemplateMethodsDeclaration { - #[webapi(accessor_property = "view", getter = event_value_attribute_getter, data = crate::util::v8str(scope, "view"))] + #[webapi(accessor_property = "view", getter = event_platform_attribute_getter, data = crate::util::v8str(scope, "view"))] view: (), #[webapi(accessor_property = "detail", getter = event_value_attribute_getter, data = crate::util::v8str(scope, "detail"))] detail: (), @@ -301,8 +302,8 @@ struct KeyboardEventTemplateMethodsDeclaration { #[webapi( method = "getModifierState", - length = 0, - callback = keyboard_event_get_modifier_state_callback + length = 1, + callback = event_get_modifier_state_callback )] get_modifier_state: (), } diff --git a/moli-renderer-v8/src/context_bootstrap/events.rs b/moli-renderer-v8/src/context_bootstrap/events.rs index d0df2f19b7..d09f6f0ee9 100644 --- a/moli-renderer-v8/src/context_bootstrap/events.rs +++ b/moli-renderer-v8/src/context_bootstrap/events.rs @@ -9,8 +9,10 @@ mod kind; mod message; pub(crate) use message::construct_original_message_event; mod methods; +mod modifiers; mod subclasses; mod submit; +mod ui; mod value; mod wrappers; @@ -499,7 +501,12 @@ pub(crate) use wrappers::{ }; pub(in crate::context_bootstrap) use message::message_event_init_callback; -pub(in crate::context_bootstrap) use wrappers::event_value_attribute_getter; +pub(in crate::context_bootstrap) use wrappers::{ + event_platform_attribute_getter, event_value_attribute_getter, +}; + +pub(in crate::context_bootstrap) use modifiers::event_get_modifier_state_callback; +pub(in crate::context_bootstrap) use subclasses::keyboard_event_init_callback; pub(crate) use subclasses::construct_original_error_event; diff --git a/moli-renderer-v8/src/context_bootstrap/events/modifiers.rs b/moli-renderer-v8/src/context_bootstrap/events/modifiers.rs new file mode 100644 index 0000000000..ec5505d6cd --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/events/modifiers.rs @@ -0,0 +1,153 @@ +use super::*; +use crate::webidl; + +const EVENT_MODIFIERS_SLOT: &str = "__moliEventModifiers"; +const MODIFIER_KEYS: [&str; 14] = [ + "Alt", + "Control", + "Meta", + "AltGraph", + "CapsLock", + "Fn", + "FnLock", + "Hyper", + "NumLock", + "ScrollLock", + "Super", + "Symbol", + "SymbolLock", + "Shift", +]; + +/// EventModifierInit members, in Web IDL dictionary order. +#[derive(Default, webidl::WebIdlDictionary)] +#[webidl(prefix = "EventModifierInit")] +pub(super) struct EventModifierInitMembers { + #[webidl(default = false)] + alt_key: bool, + #[webidl(default = false)] + ctrl_key: bool, + #[webidl(default = false)] + meta_key: bool, + #[webidl(default = false)] + modifier_alt_graph: bool, + #[webidl(default = false)] + modifier_caps_lock: bool, + #[webidl(default = false)] + modifier_fn: bool, + #[webidl(default = false)] + modifier_fn_lock: bool, + #[webidl(default = false)] + modifier_hyper: bool, + #[webidl(default = false)] + modifier_num_lock: bool, + #[webidl(default = false)] + modifier_scroll_lock: bool, + #[webidl(default = false)] + modifier_super: bool, + #[webidl(default = false)] + modifier_symbol: bool, + #[webidl(default = false)] + modifier_symbol_lock: bool, + #[webidl(default = false)] + shift_key: bool, +} + +#[derive(WebApiObject)] +#[webapi(plain, data_properties, enumerable)] +struct EventModifierProperties { + alt_key: bool, + ctrl_key: bool, + meta_key: bool, + shift_key: bool, +} + +impl EventModifierInitMembers { + pub(super) fn initialize<'s>( + &self, + scope: &mut v8::PinScope<'s, '_>, + event: v8::Local<'s, v8::Object>, + ) { + let event = event_backing(scope, event); + let values = [ + self.alt_key, + self.ctrl_key, + self.meta_key, + self.modifier_alt_graph, + self.modifier_caps_lock, + self.modifier_fn, + self.modifier_fn_lock, + self.modifier_hyper, + self.modifier_num_lock, + self.modifier_scroll_lock, + self.modifier_super, + self.modifier_symbol, + self.modifier_symbol_lock, + self.shift_key, + ]; + let bits = values + .into_iter() + .enumerate() + .fold(0, |bits, (index, active)| { + bits | (u32::from(active) << index) + }); + set_private_value( + scope, + event, + EVENT_MODIFIERS_SLOT, + v8::Integer::new_from_unsigned(scope, bits).into(), + ); + EventModifierProperties::new(self.alt_key, self.ctrl_key, self.meta_key, self.shift_key) + .initialize(scope, event) + .expect("event modifier properties should initialize"); + } +} + +pub(in crate::context_bootstrap) fn initialize_legacy_event_modifiers<'s>( + scope: &mut v8::PinScope<'s, '_>, + event: v8::Local<'s, v8::Object>, + ctrl_key: bool, + alt_key: bool, + shift_key: bool, + meta_key: bool, +) { + EventModifierInitMembers { + ctrl_key, + alt_key, + shift_key, + meta_key, + ..Default::default() + } + .initialize(scope, event); +} + +#[derive(webidl::WebIdlArgs)] +#[webidl(prefix = "getModifierState")] +struct GetModifierStateArgs { + #[webidl(required)] + key_arg: String, +} + +pub(in crate::context_bootstrap) fn event_get_modifier_state_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + // Retain the legacy accelerator alias for the native platform. + let key = match parsed.key_arg.as_str() { + "Accel" if cfg!(target_os = "macos") => "Meta", + "Accel" => "Control", + key => key, + }; + let bits = event_private_value(scope, args.this(), EVENT_MODIFIERS_SLOT) + .and_then(|value| value.uint32_value(scope)) + .unwrap_or_default(); + let active = MODIFIER_KEYS + .iter() + .position(|name| *name == key) + .is_some_and(|index| bits & (1 << index) != 0); + rv.set(v8::Boolean::new(scope, active).into()); +} diff --git a/moli-renderer-v8/src/context_bootstrap/events/subclasses.rs b/moli-renderer-v8/src/context_bootstrap/events/subclasses.rs index 1838a9f11e..456d87202c 100644 --- a/moli-renderer-v8/src/context_bootstrap/events/subclasses.rs +++ b/moli-renderer-v8/src/context_bootstrap/events/subclasses.rs @@ -26,3 +26,4 @@ pub(in crate::context_bootstrap) use pointer::{ pub(in crate::context_bootstrap::events) use data::initialize_navigate_event_methods; pub(crate) use error::construct_original_error_event; +pub(in crate::context_bootstrap) use keyboard::keyboard_event_init_callback; diff --git a/moli-renderer-v8/src/context_bootstrap/events/subclasses/constructor.rs b/moli-renderer-v8/src/context_bootstrap/events/subclasses/constructor.rs index 62701aeaf1..4a173f2731 100644 --- a/moli-renderer-v8/src/context_bootstrap/events/subclasses/constructor.rs +++ b/moli-renderer-v8/src/context_bootstrap/events/subclasses/constructor.rs @@ -51,6 +51,14 @@ fn event_subclass_constructor_callback<'s>( } else { None }; + let keyboard_event_init = if kind == EventSubclassKind::KeyboardEvent { + let Some(init) = keyboard::parse_keyboard_event_init(scope, &args) else { + return; + }; + Some(init) + } else { + None + }; let security_policy_init = if kind == EventSubclassKind::SecurityPolicyViolationEvent { let Some(init) = security_policy::parse_security_policy_violation_event_init(scope, &args) else { @@ -80,6 +88,11 @@ fn event_subclass_constructor_callback<'s>( .as_ref() .map(security_policy::SecurityPolicyViolationEventInit::event_flags) .or_else(|| error_init.as_ref().map(error::ErrorEventInit::event_flags)) + .or_else(|| { + keyboard_event_init + .as_ref() + .map(keyboard::KeyboardEventInit::event_flags) + }) .or_else(|| { storage_event_init .as_ref() @@ -141,9 +154,11 @@ fn event_subclass_constructor_callback<'s>( } } EventSubclassKind::KeyboardEvent => { - if !keyboard::initialize_keyboard_event(scope, event, init) { - return; - } + keyboard::initialize_keyboard_event( + scope, + event, + keyboard_event_init.expect("KeyboardEvent init should be parsed"), + ); } EventSubclassKind::InputEvent => data::initialize_input_event(scope, event, init), EventSubclassKind::WheelEvent => { diff --git a/moli-renderer-v8/src/context_bootstrap/events/subclasses/keyboard.rs b/moli-renderer-v8/src/context_bootstrap/events/subclasses/keyboard.rs index 50ea026123..f143d0a618 100644 --- a/moli-renderer-v8/src/context_bootstrap/events/subclasses/keyboard.rs +++ b/moli-renderer-v8/src/context_bootstrap/events/subclasses/keyboard.rs @@ -1,56 +1,179 @@ +use super::super::ui::{UiEventInit, initialize_legacy_ui_event}; use super::*; +use crate::webidl; use moli_webapi_declare::WebApiObject; +/// Own KeyboardEventInit members, including the legacy code members. +#[derive(webidl::WebIdlDictionary)] +#[webidl(prefix = "KeyboardEventInit")] +struct KeyboardEventInitMembers { + #[webidl(default = 0)] + char_code: u32, + #[webidl(default = webidl::DomString16(Vec::new()), converter = "raw")] + code: webidl::DomString16, + #[webidl(default = false)] + is_composing: bool, + #[webidl(default = webidl::DomString16(Vec::new()), converter = "raw")] + key: webidl::DomString16, + #[webidl(default = 0)] + key_code: u32, + #[webidl(default = 0)] + location: u32, + #[webidl(default = false)] + repeat: bool, +} + +impl Default for KeyboardEventInitMembers { + fn default() -> Self { + Self { + char_code: 0, + code: webidl::DomString16(Vec::new()), + is_composing: false, + key: webidl::DomString16(Vec::new()), + key_code: 0, + location: 0, + repeat: false, + } + } +} + +#[derive(Default)] +pub(super) struct KeyboardEventInit<'s> { + ui: UiEventInit<'s>, + modifiers: super::super::modifiers::EventModifierInitMembers, + keyboard: KeyboardEventInitMembers, +} + +impl KeyboardEventInit<'_> { + pub(super) fn event_flags(&self) -> (bool, bool, bool) { + self.ui.event_flags() + } +} + +pub(super) fn parse_keyboard_event_init<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: &v8::FunctionCallbackArguments<'s>, +) -> Option> { + let parsed = webidl::dictionary_arg(args, 1, webidl::Context::argument("KeyboardEvent", 2)) + .and_then(|object| { + let Some(object) = object else { + return Ok(KeyboardEventInit::default()); + }; + // Parse the inheritance chain base-first. Each group's members are + // read once in lexical order, before initializing any event state. + Ok(KeyboardEventInit { + ui: webidl::parse_dictionary_object(scope, object)?, + modifiers: webidl::parse_dictionary_object(scope, object)?, + keyboard: webidl::parse_dictionary_object(scope, object)?, + }) + }); + match parsed { + Ok(init) => Some(init), + Err(error) => { + webidl::throw_error(scope, &error); + None + } + } +} + #[derive(WebApiObject)] #[webapi(plain, data_properties, enumerable)] struct KeyboardEventInitDeclaration<'scope> { - view: v8::Local<'scope, v8::Value>, - detail: f64, key: v8::Local<'scope, v8::String>, code: v8::Local<'scope, v8::String>, - location: f64, - char_code: f64, - key_code: f64, - which: f64, + location: u32, + char_code: u32, + key_code: u32, repeat: bool, is_composing: bool, - ctrl_key: bool, - shift_key: bool, - alt_key: bool, - meta_key: bool, } -pub(in crate::context_bootstrap::events::subclasses) fn initialize_keyboard_event<'s>( +pub(super) fn initialize_keyboard_event<'s>( scope: &mut v8::PinScope<'s, '_>, event: v8::Local<'s, v8::Object>, - init: Option>, -) -> bool { - let Ok(view) = init_window_view_property(scope, init, "KeyboardEvent") else { - return false; - }; - let detail = init_number_property(scope, init, "detail", 0.0); - let key = init_string_property(scope, init, "key", ""); - let code = init_string_property(scope, init, "code", ""); - let key = v8_string(scope, &key).expect("KeyboardEvent key"); - let code = v8_string(scope, &code).expect("KeyboardEvent code"); - let location = init_number_property(scope, init, "location", 0.0); + parsed: KeyboardEventInit<'s>, +) { + parsed.ui.initialize(scope, event); + let key = v8_string_from_utf16_units(scope, &parsed.keyboard.key.0).expect("KeyboardEvent key"); + let code = + v8_string_from_utf16_units(scope, &parsed.keyboard.code.0).expect("KeyboardEvent code"); + parsed.modifiers.initialize(scope, event); KeyboardEventInitDeclaration::new( - view, - detail, key, code, - location, - init_number_property(scope, init, "charCode", 0.0), - init_number_property(scope, init, "keyCode", 0.0), - init_number_property(scope, init, "which", 0.0), - init_bool_property(scope, init, "repeat", false), - init_bool_property(scope, init, "isComposing", false), - init_bool_property(scope, init, "ctrlKey", false), - init_bool_property(scope, init, "shiftKey", false), - init_bool_property(scope, init, "altKey", false), - init_bool_property(scope, init, "metaKey", false), + parsed.keyboard.location, + parsed.keyboard.char_code, + parsed.keyboard.key_code, + parsed.keyboard.repeat, + parsed.keyboard.is_composing, ) .initialize(scope, event) .expect("KeyboardEvent init declaration should initialize"); - true +} + +#[derive(webidl::WebIdlArgs)] +#[webidl(prefix = "KeyboardEvent.initKeyboardEvent")] +struct InitKeyboardEventArgs<'s> { + #[webidl(required, converter = "raw")] + event_type: webidl::DomString16, + #[webidl(default = false)] + bubbles: bool, + #[webidl(default = false)] + cancelable: bool, + #[webidl(nullable, interface = crate::web_api_interfaces::Window, brand_check = crate::context_bootstrap::is_window_receiver)] + view: Option>, + #[webidl(default = webidl::DomString16(Vec::new()), converter = "raw")] + key: webidl::DomString16, + #[webidl(default = 0)] + location: u32, + #[webidl(default = false)] + ctrl_key: bool, + #[webidl(default = false)] + alt_key: bool, + #[webidl(default = false)] + shift_key: bool, + #[webidl(default = false)] + meta_key: bool, +} + +#[derive(WebApiObject)] +#[webapi(plain, data_properties, enumerable)] +struct LegacyKeyboardEventInitDeclaration<'scope> { + key: v8::Local<'scope, v8::String>, + location: u32, + #[webapi(constructor_default = false)] + repeat: bool, +} + +pub(in crate::context_bootstrap) fn keyboard_event_init_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + let event = event_backing(scope, args.this()); + if !reinitialize_event_object( + scope, + event, + &parsed.event_type.0, + parsed.bubbles, + parsed.cancelable, + ) { + return; + } + initialize_legacy_ui_event(scope, event, parsed.view, 0); + let key = v8_string_from_utf16_units(scope, &parsed.key.0).expect("KeyboardEvent key"); + super::super::modifiers::initialize_legacy_event_modifiers( + scope, + event, + parsed.ctrl_key, + parsed.alt_key, + parsed.shift_key, + parsed.meta_key, + ); + LegacyKeyboardEventInitDeclaration::new(key, parsed.location) + .initialize(scope, event) + .expect("legacy KeyboardEvent init declaration should initialize"); } diff --git a/moli-renderer-v8/src/context_bootstrap/events/ui.rs b/moli-renderer-v8/src/context_bootstrap/events/ui.rs new file mode 100644 index 0000000000..2c20f71fb3 --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/events/ui.rs @@ -0,0 +1,87 @@ +use crate::webidl; +use moli_webapi_declare::WebApiObject; + +fn nullable_window_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + window: Option>, +) -> v8::Local<'s, v8::Value> { + window + .map(Into::into) + .unwrap_or_else(|| v8::null(scope).into()) +} + +/// Own UIEventInit members, including legacy which, in dictionary order. +#[derive(Default, webidl::WebIdlDictionary)] +#[webidl(prefix = "UIEventInit")] +struct UiEventInitMembers<'s> { + #[webidl(default = 0)] + detail: i32, + #[webidl(nullable, interface = crate::web_api_interfaces::Window, brand_check = crate::context_bootstrap::is_window_receiver)] + view: Option>, + #[webidl(default = 0)] + which: u32, +} + +#[derive(Default)] +pub(super) struct UiEventInit<'s> { + flags: (bool, bool, bool), + members: UiEventInitMembers<'s>, +} + +impl<'s> webidl::WebIdlDictionary<'s> for UiEventInit<'s> { + fn parse_dictionary( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + ) -> Result { + // Read inherited members before own members. Derived dictionaries + // delegate here so each observable getter runs once, base-first. + Ok(Self { + flags: super::init::parse_event_init(scope, Some(object))?, + members: webidl::parse_dictionary_object(scope, object)?, + }) + } +} + +impl<'s> UiEventInit<'s> { + pub(super) fn event_flags(&self) -> (bool, bool, bool) { + self.flags + } + + pub(super) fn initialize( + self, + scope: &mut v8::PinScope<'s, '_>, + event: v8::Local<'s, v8::Object>, + ) { + let view = nullable_window_value(scope, self.members.view); + UiEventState::new(view, self.members.detail, self.members.which) + .initialize(scope, event) + .expect("UIEvent state should initialize"); + } +} + +#[derive(WebApiObject)] +#[webapi(plain, data_properties, enumerable)] +struct UiEventState<'s> { + view: v8::Local<'s, v8::Value>, + detail: i32, + which: u32, +} + +#[derive(WebApiObject)] +#[webapi(plain, data_properties, enumerable)] +struct LegacyUiEventState<'s> { + view: v8::Local<'s, v8::Value>, + detail: i32, +} + +pub(in crate::context_bootstrap) fn initialize_legacy_ui_event<'s>( + scope: &mut v8::PinScope<'s, '_>, + event: v8::Local<'s, v8::Object>, + view: Option>, + detail: i32, +) { + let view = nullable_window_value(scope, view); + LegacyUiEventState::new(view, detail) + .initialize(scope, event) + .expect("legacy UIEvent state should initialize"); +} diff --git a/moli-renderer-v8/src/context_bootstrap/events/wrappers.rs b/moli-renderer-v8/src/context_bootstrap/events/wrappers.rs index 3e57f6ea89..917d2caebc 100644 --- a/moli-renderer-v8/src/context_bootstrap/events/wrappers.rs +++ b/moli-renderer-v8/src/context_bootstrap/events/wrappers.rs @@ -248,6 +248,14 @@ pub(in crate::context_bootstrap) fn event_value_attribute_getter<'s>( event_attribute_getter::<0>(scope, args, rv); } +pub(in crate::context_bootstrap) fn event_platform_attribute_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, +) { + event_attribute_getter::<2>(scope, args, rv); +} + pub(super) fn event_attribute_in_wrapper<'s>( scope: &mut v8::PinScope<'s, '_>, wrapper: v8::Local<'s, v8::Object>, diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/keyboard_event_init.js b/moli-renderer-v8/src/script_vm/tests/browser_api/keyboard_event_init.js new file mode 100644 index 0000000000..2a007b1960 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/keyboard_event_init.js @@ -0,0 +1,216 @@ +(() => { + const rows = [], observed = {}; + const check = (name, run) => { + try { + if (run() !== true) throw new Error('assertion failed'); + rows.push({name, passed: true}); + } catch (error) { + rows.push({name, passed: false, error: String(error), stack: error.stack}); + } + }; + const typeError = (run, realm = globalThis) => { + try { run(); } catch (error) { return error instanceof realm.TypeError; } + return false; + }; + const throws = (run, sentinel) => { + try { run(); } catch (error) { return error === sentinel; } + return false; + }; + const other = document.getElementById('child').contentWindow; + const legacy = (key, location = 0, view = null) => { + const event = new KeyboardEvent('before'); + event.initKeyboardEvent('after', false, false, view, key, location); + return event; + }; + const snapshot = event => JSON.stringify([ + event.type, event.bubbles, event.cancelable, event.composed, event.defaultPrevented, + event.detail, event.key, event.code, event.location, event.which, event.charCode, + event.keyCode, event.repeat, event.isComposing, event.ctrlKey, event.altKey, + event.shiftKey, event.metaKey, event.getModifierState('AltGraph') + ]); + const texts = ['', '\ud800', '\udc00', '\ud83d\ude00', 'a\ud800b\udc00\u0000\ud83d\ude00']; + for (let i = 0; i < texts.length; i++) { + const text = texts[i]; + for (const field of ['key', 'code']) { + check(`constructor ${field} UTF-16 ${i}`, () => new KeyboardEvent('x', {[field]: text})[field] === text); + } + check(`legacy key UTF-16 ${i}`, () => legacy(text).key === text); + } + for (const [value, expected] of [[undefined, ''], [null, 'null'], [23, '23'], [true, 'true']]) { + for (const field of ['key', 'code']) { + check(`constructor ${field} ${String(value)}`, () => new KeyboardEvent('x', {[field]: value})[field] === expected); + } + check(`legacy key ${String(value)}`, () => legacy(value).key === expected); + } + for (const field of ['key', 'code']) { + check(`constructor ${field} string hint once`, () => { + let count = 0; + const value = {[Symbol.toPrimitive](hint) { if (hint !== 'string') throw new Error(hint); count++; return texts[4]; }}; + return new KeyboardEvent('x', {[field]: value})[field] === texts[4] && count === 1; + }); + check(`constructor ${field} Symbol rejects`, () => typeError(() => new KeyboardEvent('x', {[field]: Symbol()}))); + } + check('legacy key string hint once', () => { + let count = 0; + const value = {[Symbol.toPrimitive](hint) { if (hint !== 'string') throw new Error(hint); count++; return texts[4]; }}; + return legacy(value).key === texts[4] && count === 1; + }); + check('legacy Symbol key rejects', () => typeError(() => legacy(Symbol()))); + check('constructor default payload', () => { + const e = new KeyboardEvent('x'); + return e.view === null && e.detail === 0 && e.key === '' && e.code === '' && e.location === 0 && + e.charCode === 0 && e.keyCode === 0 && e.which === 0 && !e.repeat && !e.isComposing && !e.isTrusted; + }); + for (const value of [undefined, null]) { + check(`nullish dictionary ${String(value)}`, () => new KeyboardEvent('x', value).view === null); + } + for (const value of [false, 1, '', Symbol(), 1n]) { + check(`invalid dictionary ${typeof value}`, () => typeError(() => new KeyboardEvent('x', value))); + } + for (const [field, value, expected] of [ + ['detail', 4.9, 4], ['detail', -4.9, -4], ['detail', 2147483648, -2147483648], + ['location', -1, 4294967295], ['location', 4294967297, 1], ['location', 1.9, 1], + ['charCode', -1, 4294967295], ['keyCode', -1, 4294967295], ['which', -1, 4294967295], + ]) { + check(`constructor ${field} integer ${value}`, () => new KeyboardEvent('x', {[field]: value})[field] === expected); + } + for (const field of ['detail', 'location', 'charCode', 'keyCode', 'which']) { + for (const value of [NaN, Infinity, -Infinity]) { + check(`constructor ${field} nonfinite ${value}`, () => new KeyboardEvent('x', {[field]: value})[field] === 0); + } + for (const value of [1n, Symbol()]) { + check(`constructor ${field} rejects ${typeof value}`, () => typeError(() => new KeyboardEvent('x', {[field]: value}))); + } + } + for (const [value, expected] of [[-1, 4294967295], [4294967297, 1], [1.9, 1], [NaN, 0], [Infinity, 0]]) { + check(`legacy location integer ${value}`, () => legacy('', value).location === expected); + } + check('legacy BigInt location rejects', () => typeError(() => legacy('', 1n))); + const modifierMembers = ['altKey', 'ctrlKey', 'metaKey', 'modifierAltGraph', 'modifierCapsLock', + 'modifierFn', 'modifierFnLock', 'modifierHyper', 'modifierNumLock', 'modifierScrollLock', + 'modifierSuper', 'modifierSymbol', 'modifierSymbolLock', 'shiftKey']; + const order = ['bubbles', 'cancelable', 'composed', 'detail', 'view', 'which', ...modifierMembers, + 'charCode', 'code', 'isComposing', 'key', 'keyCode', 'location', 'repeat']; + check('complete inherited dictionary order', () => { + const seen = []; + new KeyboardEvent('x', new Proxy({}, {get(_target, name) { seen.push(name); }})); + observed.dictionaryReads = seen; + return seen.join() === order.join(); + }); + for (const stop of order) { + check(`dictionary getter exception stops at ${stop}`, () => { + const sentinel = {}; + const seen = []; + const init = new Proxy({}, {get(_target, name) { seen.push(name); if (name === stop) throw sentinel; }}); + return throws(() => new KeyboardEvent('x', init), sentinel) && seen.join() === order.slice(0, order.indexOf(stop) + 1).join(); + }); + } + for (const field of ['detail', 'which', 'code', 'key', 'charCode', 'keyCode', 'location']) { + check(`constructor ${field} conversion exception stops`, () => { + const sentinel = {}, seen = []; + const init = new Proxy({}, {get(_target, name) { + seen.push(name); + if (name === field) return {[Symbol.toPrimitive]() { throw sentinel; }}; + }}); + return throws(() => new KeyboardEvent('x', init), sentinel) && seen.join() === order.slice(0, order.indexOf(field) + 1).join(); + }); + } + check('constructor type converts before dictionary', () => { + const seen = []; + const type = {toString() {seen.push('type'); return texts[4];}}; + const e = new KeyboardEvent(type, new Proxy({}, {get(_target, name) {seen.push(name);}})); + return e.type === texts[4] && seen.join() === ['type', ...order].join(); + }); + check('constructor inherits dictionary members', () => { + const e = new KeyboardEvent('x', Object.create({key: texts[4], code: texts[3], location: -1, view: other, ctrlKey: true})); + return e.key === texts[4] && e.code === texts[3] && e.location === 4294967295 && e.view === other && e.getModifierState('Control'); + }); + const views = [['window', window], ['child', other], ['null', null], ['undefined', undefined]]; + for (const [name, view] of views) { + const expected = view === undefined ? null : view; + check(`constructor accepts ${name} view`, () => new KeyboardEvent('x', {view}).view === expected); + check(`legacy accepts ${name} view`, () => legacy('', 0, view).view === expected); + } + let traps = 0; + const handler = {get() {traps++; throw new Error('get trap');}, getPrototypeOf() {traps++; throw new Error('prototype trap');}}; + const revoked = Proxy.revocable(window, handler); revoked.revoke(); + for (const [name, view] of [['ordinary', {}], ['prototype', Object.create(window)], ['proxy', new Proxy(window, handler)], ['revoked', revoked.proxy], ['number', 1]]) { + check(`constructor rejects ${name} view before later conversion`, () => { + let converted = false; + const result = typeError(() => new KeyboardEvent('x', {view, code: {toString() {converted = true; return '';}}})); + return result && !converted && traps === 0; + }); + check(`legacy rejects ${name} view before key conversion`, () => { + let converted = false; + const result = typeError(() => legacy({toString() {converted = true; return '';}}, 0, view)); + return result && !converted && traps === 0; + }); + } + check('legacy method descriptor', () => { + const d = Object.getOwnPropertyDescriptor(KeyboardEvent.prototype, 'initKeyboardEvent'); + return d.value.name === 'initKeyboardEvent' && d.value.length === 1 && d.enumerable && d.configurable && d.writable; + }); + check('legacy missing required type', () => typeError(() => new KeyboardEvent('x').initKeyboardEvent())); + check('legacy modifier booleans do not invoke author conversion', () => { + const e = new KeyboardEvent('x', {modifierAltGraph: true, modifierCapsLock: true}); + const truthy = {[Symbol.toPrimitive]() {throw new Error('boolean conversion');}}; + e.initKeyboardEvent('y', false, false, null, '', 0, truthy, false, truthy, false); + return e.ctrlKey && !e.altKey && e.shiftKey && !e.metaKey && e.getModifierState('Control') && + e.getModifierState('Shift') && !e.getModifierState('AltGraph') && !e.getModifierState('CapsLock'); + }); + check('legacy payload defaults preserve unrelated code and composing state', () => { + const e = new KeyboardEvent('before', {code:'KeyB', isComposing:true, repeat:true, composed:true, key:'old', view:window, detail:7, keyCode:8, charCode:9, which:10}); + const result = e.initKeyboardEvent('after'); + return result === undefined && e.type === 'after' && e.view === null && e.detail === 0 && e.key === '' && + e.code === 'KeyB' && e.isComposing && !e.repeat && e.location === 0 && e.keyCode === 8 && e.charCode === 9 && e.which === 10 && e.composed; + }); + check('legacy arguments convert left to right before mutation', () => { + const e = new KeyboardEvent('before', {key:'old', location:3}); + const old = snapshot(e), seen = [], sentinel = {}; + const key = {toString() {seen.push('key'); return 'new';}}; + const location = {valueOf() {seen.push('location'); throw sentinel;}}; + return throws(() => e.initKeyboardEvent({toString() {seen.push('type'); return 'new';}}, true, true, null, key, location), sentinel) && + seen.join() === 'type,key,location' && snapshot(e) === old; + }); + check('legacy ignored extra arguments are not converted', () => { + const e = new KeyboardEvent('before'); + e.initKeyboardEvent('after', false, false, null, '', 0, false, false, false, false, {[Symbol.toPrimitive]() {throw new Error('extra conversion');}}); + return e.type === 'after'; + }); + check('legacy dispatch guard follows conversions and preserves state', () => { + const e = new KeyboardEvent('before', {key:'old', ctrlKey:true, code:'KeyB', composed:true}); + const before = snapshot(e), target = new EventTarget(); + let converted = false, ignored = false, propagated = false; + target.addEventListener('before', () => { + e.initKeyboardEvent('after', false, false, null, {toString() {converted = true; return 'new';}}, 1, false, true); + ignored = snapshot(e) === before; + propagated = typeError(() => e.initKeyboardEvent('after', false, false, null, Symbol())); + }); + return target.dispatchEvent(e) && converted && ignored && propagated && snapshot(e) === before; + }); + const real = new KeyboardEvent('before'); + const revokedEvent = Proxy.revocable(real, handler); revokedEvent.revoke(); + for (const [name, receiver] of [['ordinary', {}], ['prototype', Object.create(KeyboardEvent.prototype)], ['inherit-real', Object.create(real)], ['proxy', new Proxy(real, handler)], ['revoked', revokedEvent.proxy], ['other-interface', new MouseEvent('x')]]) { + check(`legacy rejects ${name} receiver before conversion`, () => { + let converted = false; + const type = {toString() {converted = true; return 'x';}}; + return typeError(() => KeyboardEvent.prototype.initKeyboardEvent.call(receiver, type)) && !converted && traps === 0; + }); + } + check('cross-realm genuine receiver and Window view', () => { + const e = new other.KeyboardEvent('before'); + KeyboardEvent.prototype.initKeyboardEvent.call(e, 'after', true, false, window, texts[4], -1, true); + return e.type === 'after' && e.key === texts[4] && e.view === window && e.location === 4294967295 && e.ctrlKey && e instanceof other.KeyboardEvent; + }); + check('cross-realm method TypeError uses callee realm', () => typeError(() => other.KeyboardEvent.prototype.initKeyboardEvent.call({}, 'x'), other)); + check('cross-realm constructor TypeError uses callee realm', () => typeError(() => new other.KeyboardEvent('x', {view:{}}), other)); + check('cross-realm argument TypeError uses callee realm', () => typeError(() => other.KeyboardEvent.prototype.initKeyboardEvent.call(real, 'x', false, false, null, Symbol()), other)); + check('genuine receiver remains valid after prototype replacement', () => { + const e = new KeyboardEvent('before'); + Object.setPrototypeOf(e, null); + KeyboardEvent.prototype.initKeyboardEvent.call(e, 'after'); + return Object.getOwnPropertyDescriptor(Event.prototype, 'type').get.call(e) === 'after'; + }); + globalThis.__keyboardEventResults = {rows, observed, total: rows.length, passed: rows.filter(row => row.passed).length}; + return rows.every(row => row.passed); +})() diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/keyboard_event_init.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/keyboard_event_init.rs new file mode 100644 index 0000000000..712a2b07eb --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/keyboard_event_init.rs @@ -0,0 +1,16 @@ +use super::*; + +#[test] +fn keyboard_event_initializers_preserve_dom_strings_and_convert_complete_dictionaries() { + let mut vm = new_storage_html_test_vm("https://keyboard-event-init-webidl.test/"); + vm.eval("document.body.innerHTML = '';") + .unwrap(); + let result = vm.eval(include_str!("keyboard_event_init.js")).unwrap(); + assert_eq!( + result, + "true", + "{}", + vm.eval("JSON.stringify(__keyboardEventResults.rows.filter(row=>!row.passed))") + .unwrap() + ); +} diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs index 91a315bc2e..b7ada18391 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs @@ -72,6 +72,7 @@ mod document_domain_lifetime; mod document_domain_setter; mod error_event_init; +mod keyboard_event_init; mod message_event_init; mod window_current_event_private;