mirror of
https://github.com/lexmount/moli.git
synced 2026-11-02 00:06:25 +00:00
fix(dnd): enforce drag data store modes and event lifetime
This commit is contained in:
@@ -1888,10 +1888,13 @@ async fn coordinate_drag_event_completes_through_pending_layout_dispatch() {
|
||||
<script>
|
||||
window.__dragPending = [];
|
||||
document.getElementById('target').addEventListener('dragenter', event => {
|
||||
window.__savedDragTransfer = event.dataTransfer;
|
||||
window.__dragPending.push([
|
||||
event.type,
|
||||
event instanceof DragEvent,
|
||||
event.dataTransfer.getData('text/plain')
|
||||
event.dataTransfer.getData('text/plain'),
|
||||
event.dataTransfer.types.join(','),
|
||||
event.dataTransfer.items[0].kind
|
||||
].join('|'));
|
||||
});
|
||||
</script>
|
||||
@@ -1933,7 +1936,12 @@ async fn coordinate_drag_event_completes_through_pending_layout_dispatch() {
|
||||
assert_eq!(messages, vec![json!({ "id": 4107, "result": {} })]);
|
||||
assert_eq!(
|
||||
evaluate_string(&mut ctx, "JSON.stringify(window.__dragPending)").await,
|
||||
r#"["dragenter|true|drag-pending"]"#
|
||||
r#"["dragenter|true||text/plain|string"]"#
|
||||
);
|
||||
assert_eq!(
|
||||
evaluate_string(&mut ctx, "JSON.stringify([__savedDragTransfer.items.length,__savedDragTransfer.types.length,__savedDragTransfer.getData('text/plain')])").await,
|
||||
r#"[0,0,""]"#,
|
||||
"the saved event view must be disabled when the protocol command completes"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -332,9 +332,10 @@ pub(crate) use self::file_api::{
|
||||
DirectoryReaderCallbackTaskEffect, FileEntryFileCallbackTask, FileEntryFileCallbackTaskEffect,
|
||||
};
|
||||
pub(crate) use self::file_api::{
|
||||
allowed_drag_drop_effect, apply_drag_modifier_drop_effect, build_clipboard_data_transfer,
|
||||
build_data_transfer_object, clipboard_data_transfer_contents, disable_clipboard_data_transfer,
|
||||
initialize_native_drag_data_transfer, prepare_drag_drop_effect, set_drag_drop_effect,
|
||||
DragDataStore, allowed_drag_drop_effect, apply_drag_modifier_drop_effect,
|
||||
build_clipboard_data_transfer, clipboard_data_transfer_contents,
|
||||
disable_clipboard_data_transfer, initialize_native_drag_data_transfer,
|
||||
prepare_drag_drop_effect, set_drag_drop_effect,
|
||||
};
|
||||
pub(crate) use self::file_api::{
|
||||
build_file_list_object, build_file_object, file_list_files_from_object,
|
||||
|
||||
@@ -207,7 +207,7 @@ struct WorkerTemplateMethodsDeclaration {
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::DataTransfer, enumerable)]
|
||||
#[webapi(interface = web_api_interfaces::DataTransfer, enumerable, receiver)]
|
||||
struct DataTransferTemplateMethodsDeclaration {
|
||||
#[webapi(
|
||||
method = "getData",
|
||||
@@ -232,7 +232,7 @@ struct DataTransferTemplateMethodsDeclaration {
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::DataTransferItem, enumerable)]
|
||||
#[webapi(interface = web_api_interfaces::DataTransferItem, enumerable, receiver)]
|
||||
struct DataTransferItemTemplateMethodsDeclaration {
|
||||
#[webapi(
|
||||
method = "getAsFile",
|
||||
@@ -257,7 +257,7 @@ struct DataTransferItemTemplateMethodsDeclaration {
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::DataTransferItemList, enumerable)]
|
||||
#[webapi(interface = web_api_interfaces::DataTransferItemList, enumerable, receiver)]
|
||||
struct DataTransferItemListTemplateMethodsDeclaration {
|
||||
#[webapi(
|
||||
intrinsic_data_property = v8::Intrinsic::ArrayProtoValues,
|
||||
|
||||
@@ -12,10 +12,10 @@ mod install;
|
||||
|
||||
pub(super) use data_transfer::data_transfer_constructor_callback;
|
||||
pub(crate) use data_transfer::{
|
||||
allowed_drag_drop_effect, apply_drag_modifier_drop_effect, build_clipboard_data_transfer,
|
||||
build_data_transfer_object, clipboard_data_transfer_contents, disable_clipboard_data_transfer,
|
||||
initialize_native_drag_data_transfer, is_branded_data_transfer_object,
|
||||
prepare_drag_drop_effect, set_drag_drop_effect,
|
||||
DragDataStore, allowed_drag_drop_effect, apply_drag_modifier_drop_effect,
|
||||
build_clipboard_data_transfer, clipboard_data_transfer_contents,
|
||||
disable_clipboard_data_transfer, initialize_native_drag_data_transfer,
|
||||
is_branded_data_transfer_object, prepare_drag_drop_effect, set_drag_drop_effect,
|
||||
};
|
||||
pub(crate) use data_transfer::{
|
||||
data_transfer_clear_data_callback, data_transfer_get_data_callback,
|
||||
|
||||
@@ -14,10 +14,12 @@ use moli_file_api::data_transfer::{
|
||||
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
|
||||
|
||||
mod clipboard;
|
||||
mod drag_store;
|
||||
pub(crate) use clipboard::{
|
||||
build_clipboard_data_transfer, clipboard_data_transfer_contents,
|
||||
disable_clipboard_data_transfer,
|
||||
};
|
||||
pub(crate) use drag_store::DragDataStore;
|
||||
|
||||
const DATA_TRANSFER_FILES_SLOT: &str = "__lmDataTransferFiles";
|
||||
const DATA_TRANSFER_ITEMS_SLOT: &str = "__lmDataTransferItems";
|
||||
@@ -26,14 +28,18 @@ const DATA_TRANSFER_DROP_EFFECT_SLOT: &str = "__lmDataTransferDropEffect";
|
||||
const DATA_TRANSFER_EFFECT_ALLOWED_SLOT: &str = "__lmDataTransferEffectAllowed";
|
||||
const DATA_TRANSFER_MODE_SLOT: &str = "__lmDataTransferMode";
|
||||
const DATA_TRANSFER_CLIPBOARD_SLOT: &str = "__lmDataTransferClipboard";
|
||||
const DATA_TRANSFER_STORE_SLOT: &str = "__lmDataTransferStore";
|
||||
const DATA_TRANSFER_ITEM_LIST_ARRAY_SLOT: &str = "__lmDataTransferItemArray";
|
||||
const DATA_TRANSFER_ITEM_LIST_OWNER_SLOT: &str = "__lmDataTransferOwner";
|
||||
const DATA_TRANSFER_ITEM_LIST_VIEWS_SLOT: &str = "__lmDataTransferItemViews";
|
||||
const DATA_TRANSFER_ITEM_LIST_INDEXED_LENGTH_SLOT: &str = "__lmDataTransferItemListIndexedLength";
|
||||
const DATA_TRANSFER_ITEM_KIND_SLOT: &str = "__lmDataTransferItemKind";
|
||||
const DATA_TRANSFER_ITEM_TYPE_SLOT: &str = "__lmDataTransferItemType";
|
||||
const DATA_TRANSFER_ITEM_FILE_SLOT: &str = "__lmDataTransferItemFile";
|
||||
const DATA_TRANSFER_ITEM_ENTRY_SLOT: &str = "__lmDataTransferItemEntry";
|
||||
const DATA_TRANSFER_ITEM_STRING_SLOT: &str = "__lmDataTransferItemString";
|
||||
const DATA_TRANSFER_ITEM_SOURCE_SLOT: &str = "__lmDataTransferItemSource";
|
||||
const DATA_TRANSFER_ITEM_OWNER_SLOT: &str = "__lmDataTransferItemOwner";
|
||||
const FILE_SYSTEM_ENTRY_FILESYSTEM_SLOT: &str = "__lmFileSystemEntryFilesystem";
|
||||
const FILE_SYSTEM_ENTRY_FULL_PATH_SLOT: &str = "__lmFileSystemEntryFullPath";
|
||||
const FILE_SYSTEM_ENTRY_IS_DIRECTORY_SLOT: &str = "__lmFileSystemEntryIsDirectory";
|
||||
@@ -99,10 +105,18 @@ struct DataTransferShellDeclaration {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
enum DataTransferMode {
|
||||
ReadWrite,
|
||||
ReadOnly,
|
||||
Disabled,
|
||||
ReadWrite = 0,
|
||||
ReadOnly = 1,
|
||||
Disabled = 2,
|
||||
Protected = 3,
|
||||
}
|
||||
|
||||
impl DataTransferMode {
|
||||
fn can_read(self) -> bool {
|
||||
matches!(self, Self::ReadWrite | Self::ReadOnly)
|
||||
}
|
||||
}
|
||||
|
||||
fn data_transfer_mode<'s>(
|
||||
@@ -112,6 +126,7 @@ fn data_transfer_mode<'s>(
|
||||
match private_number_property(scope, object, DATA_TRANSFER_MODE_SLOT) {
|
||||
Some(1.0) => DataTransferMode::ReadOnly,
|
||||
Some(2.0) => DataTransferMode::Disabled,
|
||||
Some(3.0) => DataTransferMode::Protected,
|
||||
_ => DataTransferMode::ReadWrite,
|
||||
}
|
||||
}
|
||||
@@ -125,7 +140,7 @@ fn item_list_is_writable<'s>(
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::DataTransfer)]
|
||||
#[webapi(interface = web_api_interfaces::DataTransfer, receiver)]
|
||||
struct DataTransferPrototypeAccessorsDeclaration {
|
||||
#[webapi(accessor_property, getter = data_transfer_files_getter, enumerable)]
|
||||
files: (),
|
||||
@@ -159,6 +174,8 @@ struct DataTransferItemListObjectDeclaration<'s> {
|
||||
items: Vec<v8::Local<'s, v8::Value>>,
|
||||
#[webapi(slot = DATA_TRANSFER_ITEM_LIST_OWNER_SLOT)]
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
#[webapi(slot = DATA_TRANSFER_ITEM_LIST_VIEWS_SLOT)]
|
||||
views: v8::Local<'s, v8::Map>,
|
||||
#[webapi(
|
||||
slot = DATA_TRANSFER_ITEM_LIST_INDEXED_LENGTH_SLOT,
|
||||
constructor_default = 0.0
|
||||
@@ -167,7 +184,7 @@ struct DataTransferItemListObjectDeclaration<'s> {
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::DataTransferItemList)]
|
||||
#[webapi(interface = web_api_interfaces::DataTransferItemList, receiver)]
|
||||
struct DataTransferItemListPrototypeAccessorsDeclaration {
|
||||
#[webapi(accessor_property, getter = data_transfer_item_list_length_getter, enumerable)]
|
||||
length: (),
|
||||
@@ -208,8 +225,19 @@ struct DataTransferStringItemObjectDeclaration<'item_type, 'data> {
|
||||
data: &'data str,
|
||||
}
|
||||
|
||||
// Item payloads belong to the store; each item list publishes its own stable
|
||||
// views whose access follows the associated DataTransfer's lifetime and mode.
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(interface = web_api_interfaces::DataTransferItem, require_prototype)]
|
||||
struct DataTransferItemViewDeclaration<'s> {
|
||||
#[webapi(slot = DATA_TRANSFER_ITEM_SOURCE_SLOT)]
|
||||
source: v8::Local<'s, v8::Object>,
|
||||
#[webapi(slot = DATA_TRANSFER_ITEM_OWNER_SLOT)]
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::DataTransferItem)]
|
||||
#[webapi(interface = web_api_interfaces::DataTransferItem, receiver)]
|
||||
struct DataTransferItemPrototypeAccessorsDeclaration {
|
||||
#[webapi(accessor_property, getter = data_transfer_item_kind_getter, enumerable)]
|
||||
kind: (),
|
||||
@@ -542,6 +570,63 @@ fn item_list_owner<'s>(
|
||||
get_private_object(scope, item_list, DATA_TRANSFER_ITEM_LIST_OWNER_SLOT)
|
||||
}
|
||||
|
||||
fn item_views<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
item_list: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Map>> {
|
||||
get_private_value(scope, item_list, DATA_TRANSFER_ITEM_LIST_VIEWS_SLOT)
|
||||
.and_then(|value| v8::Local::<v8::Map>::try_from(value).ok())
|
||||
}
|
||||
|
||||
fn data_transfer_item_view<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
item_list: v8::Local<'s, v8::Object>,
|
||||
source: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
let views = item_views(scope, item_list)?;
|
||||
if let Some(value) = views.get(scope, source.into())
|
||||
&& let Ok(view) = v8::Local::<v8::Object>::try_from(value)
|
||||
{
|
||||
return Some(view);
|
||||
}
|
||||
let owner = item_list_owner(scope, item_list)?;
|
||||
let view = DataTransferItemViewDeclaration::new(source, owner)
|
||||
.bind(scope)
|
||||
.ok()?;
|
||||
views.set(scope, source.into(), view.into())?;
|
||||
Some(view)
|
||||
}
|
||||
|
||||
fn item_data_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
item: v8::Local<'s, v8::Object>,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
get_private_object(scope, item, DATA_TRANSFER_ITEM_SOURCE_SLOT).unwrap_or(item)
|
||||
}
|
||||
|
||||
fn item_is_associated<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
item: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
if let Some(owner) = get_private_object(scope, item, DATA_TRANSFER_ITEM_OWNER_SLOT) {
|
||||
data_transfer_mode(scope, owner) != DataTransferMode::Disabled
|
||||
&& get_private_object(scope, item, DATA_TRANSFER_ITEM_SOURCE_SLOT).is_some()
|
||||
} else {
|
||||
// Canonical records never escape the store; removed records have empty metadata.
|
||||
private_string_property(scope, item, DATA_TRANSFER_ITEM_KIND_SLOT)
|
||||
.is_some_and(|kind| !kind.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn item_is_readable<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
item: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
item_is_associated(scope, item)
|
||||
&& get_private_object(scope, item, DATA_TRANSFER_ITEM_OWNER_SLOT)
|
||||
.is_none_or(|owner| data_transfer_mode(scope, owner).can_read())
|
||||
}
|
||||
|
||||
// Every write to the canonical item array runs inside DataTransferItemStore::mutate. The
|
||||
// outcome controls the single observer notification and, consequently, the cached `types`
|
||||
// identity required by Web IDL's CachedAttribute semantics.
|
||||
@@ -581,6 +666,21 @@ impl<'s> DataTransferItemStore<'s> {
|
||||
DataTransferItemListMutation::Changed(value) => (value, true),
|
||||
};
|
||||
if changed {
|
||||
// Native event views share the backing item array. Array replacements
|
||||
// must also reach the session before another event opens its view.
|
||||
if let Some(owner) = item_list_owner(scope, self.item_list)
|
||||
&& let Some(backing) = get_private_object(scope, owner, DATA_TRANSFER_STORE_SLOT)
|
||||
&& let Some(backing_items) = Self::for_owner(scope, backing)
|
||||
&& let Some(array) = item_list_array(scope, self.item_list)
|
||||
{
|
||||
set_private_value(
|
||||
scope,
|
||||
backing_items.item_list,
|
||||
DATA_TRANSFER_ITEM_LIST_ARRAY_SLOT,
|
||||
array.into(),
|
||||
);
|
||||
data_transfer_item_list_did_change(scope, backing_items.item_list);
|
||||
}
|
||||
data_transfer_item_list_did_change(scope, self.item_list);
|
||||
}
|
||||
Some(value)
|
||||
@@ -623,14 +723,16 @@ impl<'s> DataTransferItemStore<'s> {
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
item: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
self.mutate(scope, |scope, item_array| {
|
||||
if append_item(scope, item_array, item).is_some() {
|
||||
DataTransferItemListMutation::Changed(Some(item))
|
||||
} else {
|
||||
DataTransferItemListMutation::Unchanged(None)
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
let added = self
|
||||
.mutate(scope, |scope, item_array| {
|
||||
if append_item(scope, item_array, item).is_some() {
|
||||
DataTransferItemListMutation::Changed(Some(item))
|
||||
} else {
|
||||
DataTransferItemListMutation::Unchanged(None)
|
||||
}
|
||||
})
|
||||
.flatten()?;
|
||||
data_transfer_item_view(scope, self.item_list, added)
|
||||
}
|
||||
|
||||
fn set_string_data(self, scope: &mut v8::PinScope<'s, '_>, mime_type: &str, data: &str) {
|
||||
@@ -751,35 +853,46 @@ pub(super) fn item_kind<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
item: v8::Local<'s, v8::Object>,
|
||||
) -> Option<String> {
|
||||
private_string_property(scope, item, DATA_TRANSFER_ITEM_KIND_SLOT)
|
||||
if !item_is_associated(scope, item) {
|
||||
return Some(String::new());
|
||||
}
|
||||
let source = item_data_object(scope, item);
|
||||
private_string_property(scope, source, DATA_TRANSFER_ITEM_KIND_SLOT)
|
||||
}
|
||||
|
||||
fn item_type<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
item: v8::Local<'s, v8::Object>,
|
||||
) -> Option<String> {
|
||||
private_string_property(scope, item, DATA_TRANSFER_ITEM_TYPE_SLOT)
|
||||
if !item_is_associated(scope, item) {
|
||||
return Some(String::new());
|
||||
}
|
||||
let source = item_data_object(scope, item);
|
||||
private_string_property(scope, source, DATA_TRANSFER_ITEM_TYPE_SLOT)
|
||||
}
|
||||
|
||||
pub(super) fn item_string_value<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
item: v8::Local<'s, v8::Object>,
|
||||
) -> Option<String> {
|
||||
private_string_property(scope, item, DATA_TRANSFER_ITEM_STRING_SLOT)
|
||||
let source = item_data_object(scope, item);
|
||||
private_string_property(scope, source, DATA_TRANSFER_ITEM_STRING_SLOT)
|
||||
}
|
||||
|
||||
fn item_file_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
item: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
get_private_object(scope, item, DATA_TRANSFER_ITEM_FILE_SLOT)
|
||||
let source = item_data_object(scope, item);
|
||||
get_private_object(scope, source, DATA_TRANSFER_ITEM_FILE_SLOT)
|
||||
}
|
||||
|
||||
fn item_entry_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
item: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
get_private_object(scope, item, DATA_TRANSFER_ITEM_ENTRY_SLOT)
|
||||
let source = item_data_object(scope, item);
|
||||
get_private_object(scope, source, DATA_TRANSFER_ITEM_ENTRY_SLOT)
|
||||
}
|
||||
|
||||
fn item_summary<'s>(
|
||||
@@ -882,7 +995,8 @@ fn build_data_transfer_item_list_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
DataTransferItemListObjectDeclaration::new(owner)
|
||||
let views = v8::Map::new(scope);
|
||||
DataTransferItemListObjectDeclaration::new(owner, views)
|
||||
.bind(scope)
|
||||
.ok()
|
||||
}
|
||||
@@ -1078,6 +1192,39 @@ fn data_transfer_item_list_did_change<'s>(
|
||||
DATA_TRANSFER_TYPES_SLOT,
|
||||
v8::undefined(scope).into(),
|
||||
);
|
||||
let mode = data_transfer_mode(scope, owner);
|
||||
// Removed items and retired event views must not retain payload records in
|
||||
// the identity cache. Disassociation never clears the shared store itself.
|
||||
if let Some(views) = item_views(scope, item_list) {
|
||||
let entries = views.as_array(scope);
|
||||
let null = v8::null(scope);
|
||||
for index in (0..entries.length()).step_by(2) {
|
||||
let Some(key) = entries.get_index(scope, index) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(source) = v8::Local::<v8::Object>::try_from(key) else {
|
||||
continue;
|
||||
};
|
||||
if mode != DataTransferMode::Disabled && item_is_associated(scope, source) {
|
||||
continue;
|
||||
}
|
||||
if let Some(value) = entries.get_index(scope, index + 1)
|
||||
&& let Ok(view) = v8::Local::<v8::Object>::try_from(value)
|
||||
{
|
||||
set_private_value(scope, view, DATA_TRANSFER_ITEM_SOURCE_SLOT, null.into());
|
||||
}
|
||||
let _ = views.delete(scope, key);
|
||||
}
|
||||
}
|
||||
if mode == DataTransferMode::Disabled {
|
||||
let empty = v8::Array::new(scope, 0);
|
||||
set_private_value(
|
||||
scope,
|
||||
item_list,
|
||||
DATA_TRANSFER_ITEM_LIST_ARRAY_SLOT,
|
||||
empty.into(),
|
||||
);
|
||||
}
|
||||
let Some(item_array) = item_list_array(scope, item_list) else {
|
||||
return;
|
||||
};
|
||||
@@ -1099,8 +1246,11 @@ fn data_transfer_item_list_did_change<'s>(
|
||||
let Ok(item) = v8::Local::<v8::Object>::try_from(value) else {
|
||||
continue;
|
||||
};
|
||||
let _ = item_list.set_index(scope, index, item.into());
|
||||
if item_kind(scope, item).as_deref() == Some("file")
|
||||
if let Some(view) = data_transfer_item_view(scope, item_list, item) {
|
||||
let _ = item_list.set_index(scope, index, view.into());
|
||||
}
|
||||
if mode.can_read()
|
||||
&& item_kind(scope, item).as_deref() == Some("file")
|
||||
&& let Some(file) = item_file_object(scope, item)
|
||||
{
|
||||
files.push(file);
|
||||
@@ -1198,8 +1348,7 @@ fn data_transfer_drop_effect_setter<'s>(
|
||||
return;
|
||||
}
|
||||
};
|
||||
if data_transfer_mode(scope, args.this()) != DataTransferMode::Disabled
|
||||
&& !private_bool_property(scope, args.this(), DATA_TRANSFER_CLIPBOARD_SLOT).unwrap_or(false)
|
||||
if !private_bool_property(scope, args.this(), DATA_TRANSFER_CLIPBOARD_SLOT).unwrap_or(false)
|
||||
&& valid_drop_effect(&value)
|
||||
{
|
||||
set_private_string(scope, args.this(), DATA_TRANSFER_DROP_EFFECT_SLOT, &value);
|
||||
@@ -1251,7 +1400,7 @@ fn data_transfer_item_kind_getter<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let value = private_string_property(scope, args.this(), DATA_TRANSFER_ITEM_KIND_SLOT)
|
||||
let value = item_kind(scope, args.this())
|
||||
.and_then(|value| v8_string(scope, &value).map(Into::into))
|
||||
.unwrap_or_else(|| v8str(scope, "").into());
|
||||
rv.set(value);
|
||||
@@ -1262,7 +1411,7 @@ fn data_transfer_item_type_getter<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let value = private_string_property(scope, args.this(), DATA_TRANSFER_ITEM_TYPE_SLOT)
|
||||
let value = item_type(scope, args.this())
|
||||
.and_then(|value| v8_string(scope, &value).map(Into::into))
|
||||
.unwrap_or_else(|| v8str(scope, "").into());
|
||||
rv.set(value);
|
||||
@@ -1340,6 +1489,10 @@ pub(crate) fn data_transfer_get_data_callback<'s>(
|
||||
let Some(parsed) = webidl::parse_args::<DataTransferGetDataArgs>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
if !data_transfer_mode(scope, args.this()).can_read() {
|
||||
rv.set(v8str(scope, "").into());
|
||||
return;
|
||||
}
|
||||
let normalized_type = normalize_drag_data_type(&parsed.format);
|
||||
let Some(item_list) = get_private_object(scope, args.this(), DATA_TRANSFER_ITEMS_SLOT) else {
|
||||
rv.set(v8str(scope, "").into());
|
||||
@@ -1532,7 +1685,9 @@ pub(crate) fn data_transfer_item_get_as_file_callback<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
if let Some(file) = item_file_object(scope, args.this()) {
|
||||
if item_is_readable(scope, args.this())
|
||||
&& let Some(file) = item_file_object(scope, args.this())
|
||||
{
|
||||
rv.set(file.into());
|
||||
} else {
|
||||
rv.set(v8::null(scope).into());
|
||||
@@ -1544,6 +1699,10 @@ pub(crate) fn data_transfer_item_webkit_get_as_entry_callback<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
if !item_is_readable(scope, args.this()) {
|
||||
rv.set(v8::null(scope).into());
|
||||
return;
|
||||
}
|
||||
if let Some(entry) = item_entry_object(scope, args.this()) {
|
||||
rv.set(entry.into());
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
use super::*;
|
||||
|
||||
/// The session's backing transfer is never exposed to script. Native events
|
||||
/// publish separate DataTransfer/item views with event-local access rights.
|
||||
pub(crate) struct DragDataStore {
|
||||
backing: v8::Global<v8::Object>,
|
||||
}
|
||||
|
||||
impl DragDataStore {
|
||||
pub(crate) fn new(scope: &mut v8::PinScope<'_, '_>, data: &RendererDragData) -> Option<Self> {
|
||||
let backing = build_data_transfer_object(scope, data)?;
|
||||
Some(Self {
|
||||
backing: v8::Global::new(scope, backing),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn backing_transfer<'s>(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
v8::Local::new(scope, &self.backing)
|
||||
}
|
||||
|
||||
pub(crate) fn open_event<'s>(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
event_name: &str,
|
||||
) -> Option<DragDataTransferEvent<'s>> {
|
||||
let backing = self.backing_transfer(scope);
|
||||
let transfer = DataTransferShellDeclaration::default().bind(scope).ok()?;
|
||||
let items = initialize_data_transfer_object(scope, transfer)?;
|
||||
let backing_items = DataTransferItemStore::for_owner(scope, backing)?;
|
||||
let array = item_list_array(scope, backing_items.item_list)?;
|
||||
set_private_value(
|
||||
scope,
|
||||
items,
|
||||
DATA_TRANSFER_ITEM_LIST_ARRAY_SLOT,
|
||||
array.into(),
|
||||
);
|
||||
set_private_value(scope, transfer, DATA_TRANSFER_STORE_SLOT, backing.into());
|
||||
let mode = match event_name {
|
||||
"dragstart" => DataTransferMode::ReadWrite,
|
||||
"drop" => DataTransferMode::ReadOnly,
|
||||
_ => DataTransferMode::Protected,
|
||||
};
|
||||
set_private_number(
|
||||
scope,
|
||||
transfer,
|
||||
DATA_TRANSFER_MODE_SLOT,
|
||||
f64::from(mode as u8),
|
||||
);
|
||||
for slot in [
|
||||
DATA_TRANSFER_DROP_EFFECT_SLOT,
|
||||
DATA_TRANSFER_EFFECT_ALLOWED_SLOT,
|
||||
] {
|
||||
if let Some(value) = get_private_value(scope, backing, slot) {
|
||||
set_private_value(scope, transfer, slot, value);
|
||||
}
|
||||
}
|
||||
data_transfer_item_list_did_change(scope, items);
|
||||
Some(DragDataTransferEvent { backing, transfer })
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DragDataTransferEvent<'s> {
|
||||
backing: v8::Local<'s, v8::Object>,
|
||||
transfer: v8::Local<'s, v8::Object>,
|
||||
}
|
||||
|
||||
impl<'s> DragDataTransferEvent<'s> {
|
||||
pub(crate) fn object(&self) -> v8::Local<'s, v8::Object> {
|
||||
self.transfer
|
||||
}
|
||||
|
||||
/// Preserve the negotiated effects before invalidating every live view.
|
||||
/// Payloads stay with the backing store for later events and UA defaults.
|
||||
pub(crate) fn finish(self, scope: &mut v8::PinScope<'s, '_>) {
|
||||
for slot in [
|
||||
DATA_TRANSFER_DROP_EFFECT_SLOT,
|
||||
DATA_TRANSFER_EFFECT_ALLOWED_SLOT,
|
||||
] {
|
||||
if let Some(value) = get_private_value(scope, self.transfer, slot) {
|
||||
set_private_value(scope, self.backing, slot, value);
|
||||
}
|
||||
}
|
||||
set_private_number(
|
||||
scope,
|
||||
self.transfer,
|
||||
DATA_TRANSFER_MODE_SLOT,
|
||||
f64::from(DataTransferMode::Disabled as u8),
|
||||
);
|
||||
let null = v8::null(scope);
|
||||
set_private_value(scope, self.transfer, DATA_TRANSFER_STORE_SLOT, null.into());
|
||||
if let Some(items) = DataTransferItemStore::for_owner(scope, self.transfer) {
|
||||
data_transfer_item_list_did_change(scope, items.item_list);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ use crate::{
|
||||
};
|
||||
use moli_webidl_callback::WebIdlCallbackFunction;
|
||||
|
||||
use super::data_transfer::{item_kind, item_string_value};
|
||||
use super::data_transfer::{item_is_readable, item_kind, item_string_value};
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "DataTransferItem.getAsString")]
|
||||
@@ -112,7 +112,9 @@ pub(crate) fn data_transfer_item_get_as_string_callback<'s>(
|
||||
let Some(callback) = parsed.callback else {
|
||||
return;
|
||||
};
|
||||
if item_kind(scope, args.this()).as_deref() != Some("string") {
|
||||
if !item_is_readable(scope, args.this())
|
||||
|| item_kind(scope, args.this()).as_deref() != Some("string")
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(value) = item_string_value(scope, args.this()) else {
|
||||
|
||||
@@ -285,8 +285,8 @@ pub(crate) use events::construct_focus_event;
|
||||
pub(crate) use events::{
|
||||
NodePublicEventDispatchOutcome, TextEditInputType, TouchEventPoint,
|
||||
construct_activation_pointer_event, construct_clipboard_event, construct_command_event,
|
||||
construct_drag_event, construct_drag_event_with_related_target, construct_form_data_event,
|
||||
construct_input_event, construct_interest_event, construct_keyboard_event,
|
||||
construct_drag_event_with_related_target, construct_form_data_event, construct_input_event,
|
||||
construct_interest_event, construct_keyboard_event,
|
||||
construct_mouse_event_with_detail_and_modifiers, construct_mouse_event_with_modifiers,
|
||||
construct_mouse_event_with_related_target_and_modifiers, construct_pointer_event,
|
||||
construct_pointer_event_with_modifiers, construct_pointer_event_with_related_target,
|
||||
|
||||
@@ -12,12 +12,12 @@ pub(super) use self::constructors::construct_click_event;
|
||||
pub(crate) use self::constructors::construct_focus_event;
|
||||
pub(crate) use self::constructors::{
|
||||
TextEditInputType, TouchEventPoint, construct_activation_pointer_event,
|
||||
construct_clipboard_event, construct_command_event, construct_drag_event,
|
||||
construct_drag_event_with_related_target, construct_form_data_event, construct_input_event,
|
||||
construct_interest_event, construct_keyboard_event,
|
||||
construct_mouse_event_with_detail_and_modifiers, construct_mouse_event_with_modifiers,
|
||||
construct_mouse_event_with_related_target_and_modifiers, construct_pointer_event,
|
||||
construct_pointer_event_with_modifiers, construct_pointer_event_with_related_target,
|
||||
construct_clipboard_event, construct_command_event, construct_drag_event_with_related_target,
|
||||
construct_form_data_event, construct_input_event, construct_interest_event,
|
||||
construct_keyboard_event, construct_mouse_event_with_detail_and_modifiers,
|
||||
construct_mouse_event_with_modifiers, construct_mouse_event_with_related_target_and_modifiers,
|
||||
construct_pointer_event, construct_pointer_event_with_modifiers,
|
||||
construct_pointer_event_with_related_target,
|
||||
construct_pointer_event_with_related_target_and_modifiers, construct_simple_event,
|
||||
construct_simple_event_for_target, construct_submit_event, construct_toggle_event,
|
||||
construct_touch_event, construct_touch_event_with_points, construct_wheel_event,
|
||||
|
||||
@@ -698,27 +698,6 @@ pub(crate) fn construct_pointer_event_with_related_target_and_modifiers<'s>(
|
||||
construct_event(scope, "PointerEvent", event_type, init)
|
||||
}
|
||||
|
||||
pub(crate) fn construct_drag_event<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
event_type: &str,
|
||||
x: f64,
|
||||
y: f64,
|
||||
buttons: i32,
|
||||
data_transfer: v8::Local<'s, v8::Value>,
|
||||
modifiers: u8,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
construct_drag_event_with_related_target(
|
||||
scope,
|
||||
event_type,
|
||||
x,
|
||||
y,
|
||||
buttons,
|
||||
data_transfer,
|
||||
modifiers,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn construct_drag_event_with_related_target<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
event_type: &str,
|
||||
|
||||
@@ -1185,7 +1185,7 @@ pub(super) struct ActiveTouchPoint {
|
||||
}
|
||||
|
||||
pub(super) struct ActiveDragSession {
|
||||
pub data_transfer: v8::Global<v8::Object>,
|
||||
pub data_store: crate::context_bootstrap::DragDataStore,
|
||||
pub drop_allowed: bool,
|
||||
native: Option<input_drag::NativeDragSource>,
|
||||
target: Option<crate::native_bridge::element::InputHit>,
|
||||
|
||||
@@ -22,10 +22,10 @@ use crate::native_bridge::element::{
|
||||
activate_handle_after_pointer_release, activate_handle_via_click,
|
||||
activate_handle_via_click_with_detail_and_modifiers, cache_input_files_from_selected_files,
|
||||
captured_frame_input_surface_hit_test, construct_activation_pointer_event,
|
||||
construct_drag_event, construct_keyboard_event,
|
||||
construct_mouse_event_with_detail_and_modifiers, construct_mouse_event_with_modifiers,
|
||||
construct_mouse_event_with_related_target_and_modifiers, construct_pointer_event,
|
||||
construct_pointer_event_with_modifiers, construct_pointer_event_with_related_target,
|
||||
construct_keyboard_event, construct_mouse_event_with_detail_and_modifiers,
|
||||
construct_mouse_event_with_modifiers, construct_mouse_event_with_related_target_and_modifiers,
|
||||
construct_pointer_event, construct_pointer_event_with_modifiers,
|
||||
construct_pointer_event_with_related_target,
|
||||
construct_pointer_event_with_related_target_and_modifiers, construct_simple_event,
|
||||
construct_touch_event, construct_touch_event_with_points, construct_wheel_event,
|
||||
contenteditable_editing_host, dispatch_public_event, drag_start_frame_input_surface_hit_test,
|
||||
@@ -1220,26 +1220,24 @@ impl ScriptVm {
|
||||
directories: Vec::new(),
|
||||
drag_operations_mask: 1,
|
||||
};
|
||||
if let Some(data_transfer) =
|
||||
crate::context_bootstrap::build_data_transfer_object(scope, &empty_drag_data)
|
||||
if let Some(data_store) =
|
||||
crate::context_bootstrap::DragDataStore::new(scope, &empty_drag_data)
|
||||
{
|
||||
crate::context_bootstrap::initialize_native_drag_data_transfer(
|
||||
scope,
|
||||
data_transfer,
|
||||
);
|
||||
if let Some(event) = construct_drag_event(
|
||||
let backing = data_store.backing_transfer(scope);
|
||||
crate::context_bootstrap::initialize_native_drag_data_transfer(scope, backing);
|
||||
if super::input_drag::fire_drag_event(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
drag_start_handle,
|
||||
"dragstart",
|
||||
f64::from(drag_position.x),
|
||||
f64::from(drag_position.y),
|
||||
drag_position,
|
||||
buttons,
|
||||
data_transfer.into(),
|
||||
modifiers,
|
||||
) && dispatch_public_event(scope, runtime_ptr, drag_start_handle, event)
|
||||
.allows_default()
|
||||
{
|
||||
&data_store,
|
||||
None,
|
||||
) {
|
||||
*active_drag_session = Some(ActiveDragSession {
|
||||
data_transfer: v8::Global::new(scope, data_transfer),
|
||||
data_store,
|
||||
drop_allowed: false,
|
||||
native: Some(super::input_drag::NativeDragSource {
|
||||
handle: drag_start_handle,
|
||||
@@ -2009,16 +2007,19 @@ impl ScriptVm {
|
||||
data: RendererDragData,
|
||||
modifiers: u8,
|
||||
) -> Result<RendererInputDispatchOutcome> {
|
||||
let Some(handle) = observable_input_hit_test(
|
||||
let position = moli_layout::LayoutPoint::new(x as f32, y as f32);
|
||||
let Some(hit) = observable_input_hit_test(
|
||||
&self._context_host.borrow(),
|
||||
self.document_runtime.document_handle(),
|
||||
moli_layout::LayoutPoint::new(x as f32, y as f32),
|
||||
position,
|
||||
)?
|
||||
.map(|hit| hit.handle) else {
|
||||
else {
|
||||
return Ok(input_dispatch_outcome(false));
|
||||
};
|
||||
let handle = hit.handle;
|
||||
|
||||
let incoming_drag_data_empty = data.items.is_empty() && data.files.is_empty();
|
||||
let incoming_drag_data_empty =
|
||||
data.items.is_empty() && data.files.is_empty() && data.directories.is_empty();
|
||||
let replace_active_drag_data_transfer = event_name == "dragenter"
|
||||
&& (!incoming_drag_data_empty || self.active_drag_session.is_none());
|
||||
let clear_active_drag_data_transfer = event_name == "drop";
|
||||
@@ -2030,48 +2031,38 @@ impl ScriptVm {
|
||||
// remains exclusively borrowed, so the field pointer stays valid for the
|
||||
// duration of this callback and is not aliased elsewhere.
|
||||
let active_drag_session = unsafe { &mut *active_drag_session };
|
||||
let data_transfer =
|
||||
if replace_active_drag_data_transfer || active_drag_session.is_none() {
|
||||
let Some(data_transfer) =
|
||||
crate::context_bootstrap::build_data_transfer_object(scope, &data)
|
||||
else {
|
||||
return Ok(input_dispatch_outcome(false));
|
||||
};
|
||||
*active_drag_session = Some(ActiveDragSession {
|
||||
data_transfer: v8::Global::new(scope, data_transfer),
|
||||
drop_allowed: false,
|
||||
native: None,
|
||||
target: None,
|
||||
});
|
||||
data_transfer
|
||||
} else {
|
||||
v8::Local::new(
|
||||
scope,
|
||||
&active_drag_session
|
||||
.as_ref()
|
||||
.expect("active drag session should exist when reusing DataTransfer")
|
||||
.data_transfer,
|
||||
)
|
||||
if replace_active_drag_data_transfer || active_drag_session.is_none() {
|
||||
let Some(data_store) = crate::context_bootstrap::DragDataStore::new(scope, &data)
|
||||
else {
|
||||
return Ok(input_dispatch_outcome(false));
|
||||
};
|
||||
*active_drag_session = Some(ActiveDragSession {
|
||||
data_store,
|
||||
drop_allowed: false,
|
||||
native: None,
|
||||
target: None,
|
||||
});
|
||||
}
|
||||
let session = active_drag_session
|
||||
.as_ref()
|
||||
.expect("active drag session exists");
|
||||
let data_transfer = session.data_store.backing_transfer(scope);
|
||||
crate::context_bootstrap::apply_drag_modifier_drop_effect(
|
||||
scope,
|
||||
data_transfer,
|
||||
modifiers,
|
||||
);
|
||||
let data_transfer_value: v8::Local<'_, v8::Value> = data_transfer.into();
|
||||
let allows_default = if let Some(event) = construct_drag_event(
|
||||
let allows_default = super::input_drag::fire_drag_event(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
handle,
|
||||
event_name,
|
||||
x,
|
||||
y,
|
||||
hit.root_to_frame.map_point(position),
|
||||
buttons,
|
||||
data_transfer_value,
|
||||
modifiers,
|
||||
) {
|
||||
dispatch_public_event(scope, runtime_ptr, handle, event).allows_default()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
&session.data_store,
|
||||
None,
|
||||
);
|
||||
if event_name == "dragover"
|
||||
&& let Some(session) = active_drag_session.as_mut()
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ use super::input_dispatch::{
|
||||
};
|
||||
use super::{ActiveDragSession, ScriptVm, input_dispatch_outcome};
|
||||
use crate::context_bootstrap::{
|
||||
allowed_drag_drop_effect, prepare_drag_drop_effect, set_drag_drop_effect,
|
||||
DragDataStore, allowed_drag_drop_effect, prepare_drag_drop_effect, set_drag_drop_effect,
|
||||
};
|
||||
use crate::document_runtime::DomHandle;
|
||||
use crate::native_bridge::JsContextHost;
|
||||
@@ -77,27 +77,67 @@ fn dispatch_drag(
|
||||
position: LayoutPoint,
|
||||
buttons: i32,
|
||||
modifiers: u8,
|
||||
transfer: &v8::Global<v8::Object>,
|
||||
store: &DragDataStore,
|
||||
related: Option<DomHandle>,
|
||||
) -> bool {
|
||||
let transfer = v8::Local::new(scope, transfer);
|
||||
let point = target.root_to_frame.map_point(position);
|
||||
fire_drag_event(
|
||||
scope,
|
||||
runtime,
|
||||
target.handle,
|
||||
event_name,
|
||||
point,
|
||||
buttons,
|
||||
modifiers,
|
||||
store,
|
||||
related,
|
||||
)
|
||||
}
|
||||
|
||||
/// A native drag event owns one temporary view of the session's data store.
|
||||
/// Construct it in the target Document's realm, then retire it after dispatch.
|
||||
pub(super) fn fire_drag_event(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
runtime: *mut JsContextHost,
|
||||
target: DomHandle,
|
||||
event_name: &str,
|
||||
position: LayoutPoint,
|
||||
buttons: i32,
|
||||
modifiers: u8,
|
||||
store: &DragDataStore,
|
||||
related: Option<DomHandle>,
|
||||
) -> bool {
|
||||
let host = unsafe { &mut *runtime };
|
||||
let Some(context) = host
|
||||
.owner_dispatch_scope_for_node(target)
|
||||
.and_then(|target| {
|
||||
let owner = host.current_window_execution_context_owner(target)?;
|
||||
host.window_execution_context(scope, owner, target)
|
||||
.map(|(_, context)| context)
|
||||
})
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let Some(view) = store.open_event(scope, event_name) else {
|
||||
return false;
|
||||
};
|
||||
let related = related
|
||||
.and_then(|handle| node_wrapper_from_handle(scope, handle))
|
||||
.map(Into::into);
|
||||
let point = target.root_to_frame.map_point(position);
|
||||
construct_drag_event_with_related_target(
|
||||
let allows_default = construct_drag_event_with_related_target(
|
||||
scope,
|
||||
event_name,
|
||||
f64::from(point.x),
|
||||
f64::from(point.y),
|
||||
f64::from(position.x),
|
||||
f64::from(position.y),
|
||||
buttons,
|
||||
transfer.into(),
|
||||
view.object().into(),
|
||||
modifiers,
|
||||
related,
|
||||
)
|
||||
.is_some_and(|event| {
|
||||
dispatch_public_event(scope, runtime, target.handle, event).allows_default()
|
||||
})
|
||||
.is_some_and(|event| dispatch_public_event(scope, runtime, target, event).allows_default());
|
||||
view.finish(scope);
|
||||
allows_default
|
||||
}
|
||||
|
||||
fn end_native_drag(
|
||||
@@ -112,7 +152,7 @@ fn end_native_drag(
|
||||
let Some(source) = session.native else {
|
||||
return;
|
||||
};
|
||||
let transfer = v8::Local::new(scope, &session.data_transfer);
|
||||
let transfer = session.data_store.backing_transfer(scope);
|
||||
let mut effect = if allow_drop && session.drop_allowed {
|
||||
allowed_drag_drop_effect(scope, transfer)
|
||||
} else {
|
||||
@@ -129,7 +169,7 @@ fn end_native_drag(
|
||||
position,
|
||||
buttons,
|
||||
modifiers,
|
||||
&session.data_transfer,
|
||||
&session.data_store,
|
||||
None,
|
||||
);
|
||||
if allows_default && !perform_drop_default_action(scope, runtime, target.handle, transfer) {
|
||||
@@ -147,7 +187,7 @@ fn end_native_drag(
|
||||
position,
|
||||
buttons,
|
||||
modifiers,
|
||||
&session.data_transfer,
|
||||
&session.data_store,
|
||||
None,
|
||||
);
|
||||
}
|
||||
@@ -163,7 +203,7 @@ fn end_native_drag(
|
||||
position,
|
||||
0,
|
||||
modifiers,
|
||||
&session.data_transfer,
|
||||
&session.data_store,
|
||||
None,
|
||||
);
|
||||
}
|
||||
@@ -224,7 +264,7 @@ impl ScriptVm {
|
||||
);
|
||||
finished = true;
|
||||
} else if event_name == "mousemove" {
|
||||
let transfer = v8::Local::new(scope, &session.data_transfer);
|
||||
let transfer = session.data_store.backing_transfer(scope);
|
||||
set_drag_drop_effect(scope, transfer, "none");
|
||||
if !dispatch_drag(
|
||||
scope,
|
||||
@@ -237,7 +277,7 @@ impl ScriptVm {
|
||||
position,
|
||||
buttons,
|
||||
modifiers,
|
||||
&session.data_transfer,
|
||||
&session.data_store,
|
||||
None,
|
||||
) {
|
||||
end_native_drag(scope, runtime, &session, position, 0, modifiers, false);
|
||||
@@ -257,7 +297,7 @@ impl ScriptVm {
|
||||
position,
|
||||
buttons,
|
||||
modifiers,
|
||||
&session.data_transfer,
|
||||
&session.data_store,
|
||||
previous.map(|h| h.handle),
|
||||
);
|
||||
}
|
||||
@@ -271,7 +311,7 @@ impl ScriptVm {
|
||||
position,
|
||||
buttons,
|
||||
modifiers,
|
||||
&session.data_transfer,
|
||||
&session.data_store,
|
||||
hit.map(|h| h.handle),
|
||||
);
|
||||
}
|
||||
@@ -287,7 +327,7 @@ impl ScriptVm {
|
||||
position,
|
||||
buttons,
|
||||
modifiers,
|
||||
&session.data_transfer,
|
||||
&session.data_store,
|
||||
None,
|
||||
) && allowed_drag_drop_effect(scope, transfer) != "none"
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
globalThis.__installDragDataModeProbe = function(mode, capture, modifiers) {
|
||||
document.body.innerHTML = '<style>body{margin:0;user-select:none}#source{position:absolute;left:40px;top:40px;width:140px;height:140px}#a{position:absolute;left:300px;top:40px;width:140px;height:140px}#b{position:absolute;left:500px;top:40px;width:140px;height:140px}</style><div id="source" draggable="true">source</div><div id="a">A</div><div id="b">B</div>';
|
||||
const source = document.getElementById('source');
|
||||
const file = new File(['private-file-contents'], 'private.txt', {type:'text/plain'});
|
||||
const rows = [], transfers = [], callbacks = [], microtasks = [], trace = [];
|
||||
const expectedTypes = ['text/plain', 'text/html', 'application/test', 'Files'];
|
||||
const checks = [];
|
||||
const check = (name, actual, expected) => checks.push({name, actual, expected, pass:JSON.stringify(actual)===JSON.stringify(expected)});
|
||||
const exceptionName = action => { try { action(); return 'none'; } catch (error) { return error.name; } };
|
||||
let starts = 0, drops = 0;
|
||||
function retain(dt, event) {
|
||||
const row = {dt, event, items:dt.items, files:dt.files, types:dt.types, string:dt.items[0], file:dt.items[3]};
|
||||
transfers.push(row);
|
||||
return row;
|
||||
}
|
||||
function disabled(row, label) {
|
||||
const prefix = label + ':' + row.event + ':' + transfers.indexOf(row);
|
||||
const dt = row.dt;
|
||||
check(prefix+':empty getData', dt.getData('text/plain'), '');
|
||||
check(prefix+':empty types', Array.from(dt.types), []);
|
||||
check(prefix+':new frozen types snapshot', dt.types !== row.types && Object.isFrozen(dt.types), true);
|
||||
check(prefix+':saved types unchanged', Array.from(row.types), expectedTypes);
|
||||
check(prefix+':items same object', dt.items === row.items, true);
|
||||
check(prefix+':items empty', row.items.length, 0);
|
||||
check(prefix+':indexed item absent', row.items[0] === undefined, true);
|
||||
check(prefix+':files same object', dt.files === row.files, true);
|
||||
check(prefix+':saved files empty', row.files.length, 0);
|
||||
if (row.string) {
|
||||
check(prefix+':saved string metadata disabled', [row.string.kind,row.string.type], ['','']);
|
||||
row.string.getAsString(value => callbacks.push({phase:'disabled',value}));
|
||||
}
|
||||
if (row.file) {
|
||||
check(prefix+':saved file metadata disabled', [row.file.kind,row.file.type], ['','']);
|
||||
check(prefix+':saved file unavailable', row.file.getAsFile(), null);
|
||||
check(prefix+':saved entry unavailable', row.file.webkitGetAsEntry(), null);
|
||||
}
|
||||
check(prefix+':add blocked', row.items.add('no','application/blocked'), null);
|
||||
check(prefix+':remove disabled throws', exceptionName(()=>row.items.remove(999)), 'InvalidStateError');
|
||||
const allowed = dt.effectAllowed;
|
||||
dt.effectAllowed = 'none';
|
||||
check(prefix+':effectAllowed immutable', dt.effectAllowed, allowed);
|
||||
dt.setData('text/plain','escaped');
|
||||
dt.clearData();
|
||||
row.items.clear();
|
||||
check(prefix+':store cannot be repopulated', [dt.getData('text/plain'),row.items.length,dt.types.length], ['',0,0]);
|
||||
}
|
||||
for (const type of ['dragstart','drag','dragenter','dragover','dragleave','drop','dragend']) {
|
||||
document.addEventListener(type, event => {
|
||||
const dt = event.dataTransfer;
|
||||
const prefix = type+':'+rows.length;
|
||||
check(prefix+':fresh event DataTransfer', transfers.every(row=>row.dt!==dt), true);
|
||||
for (const row of transfers) disabled(row, prefix+':previous');
|
||||
if (type==='dragstart') {
|
||||
starts++;
|
||||
dt.setData('text/plain','payload');
|
||||
dt.setData('text/html','<b>payload</b>');
|
||||
dt.items.add('extra','application/test');
|
||||
const added = dt.items.add(file);
|
||||
check(prefix+':added file is indexed view', added===dt.items[3], true);
|
||||
dt.effectAllowed='all';
|
||||
}
|
||||
const row = retain(dt,type);
|
||||
check(prefix+':same items object', dt.items===dt.items, true);
|
||||
check(prefix+':same files object', dt.files===dt.files, true);
|
||||
check(prefix+':stable frozen types', dt.types===row.types && Object.isFrozen(row.types), true);
|
||||
check(prefix+':metadata enumerable', Array.from(dt.types), expectedTypes);
|
||||
check(prefix+':indexed metadata', Array.from(dt.items, item=>[item.kind,item.type]), [['string','text/plain'],['string','text/html'],['string','application/test'],['file','text/plain']]);
|
||||
const readable = type==='dragstart' || type==='drop';
|
||||
check(prefix+':payload read mode', dt.getData('text/plain'), readable?'payload':'');
|
||||
check(prefix+':file list read mode', dt.files.length, readable?1:0);
|
||||
if (row.file) {
|
||||
check(prefix+':file item read mode', readable?row.file.getAsFile()===file:row.file.getAsFile()===null, true);
|
||||
check(prefix+':entry read mode', !!row.file.webkitGetAsEntry(), readable);
|
||||
}
|
||||
if (row.string) {
|
||||
row.string.getAsString(value=>callbacks.push({phase:type,value}));
|
||||
check(prefix+':callback conversion before mode', exceptionName(()=>row.string.getAsString({})), 'TypeError');
|
||||
}
|
||||
let conversions=0;
|
||||
check(prefix+':format conversion still runs', dt.getData({toString(){conversions++;return 'text/plain'}}), readable?'payload':'');
|
||||
check(prefix+':format conversion count', conversions, 1);
|
||||
const marker={};
|
||||
let caught;
|
||||
try { dt.getData({toString(){throw marker;}}); } catch (error) {caught=error;}
|
||||
check(prefix+':format conversion exception propagates', caught===marker, true);
|
||||
Promise.resolve().then(()=>microtasks.push({phase:type,data:dt.getData('text/plain'),items:dt.items.length}));
|
||||
if (type!=='dragstart') {
|
||||
const before=Array.from(dt.types), allowed=dt.effectAllowed;
|
||||
dt.setData('text/plain','tampered');
|
||||
dt.clearData('text/html');
|
||||
check(prefix+':string add blocked', dt.items.add('no','application/blocked'), null);
|
||||
check(prefix+':file add blocked', dt.items.add(file), null);
|
||||
check(prefix+':remove readonly/protected throws', exceptionName(()=>dt.items.remove(999)), 'InvalidStateError');
|
||||
dt.items.clear();
|
||||
dt.effectAllowed='none';
|
||||
check(prefix+':readonly/protected mutations ignored', Array.from(dt.types), before);
|
||||
check(prefix+':effectAllowed cannot change', dt.effectAllowed, allowed);
|
||||
check(prefix+':data remains unmodified', dt.getData('text/plain'), readable?'payload':'');
|
||||
}
|
||||
if (type==='dragenter' || type==='dragover') {
|
||||
dt.dropEffect='copy';
|
||||
check(prefix+':dropEffect writable', dt.dropEffect, 'copy');
|
||||
if (mode==='drop') event.preventDefault();
|
||||
}
|
||||
if (type==='drop') {drops++;event.preventDefault();}
|
||||
if (type==='dragstart' && mode==='cancel') event.preventDefault();
|
||||
rows.push({entryAvailable:!!row.file?.webkitGetAsEntry(),type,data:dt.getData('text/plain'),items:dt.items.length,files:dt.files.length,types:Array.from(dt.types),dropEffect:dt.dropEffect,effectAllowed:dt.effectAllowed});
|
||||
trace.push(type);
|
||||
});
|
||||
}
|
||||
if (capture) source.addEventListener('pointerdown', event=>source.setPointerCapture(event.pointerId));
|
||||
return {
|
||||
afterDispatch(label) {for(const row of transfers) disabled(row,label);},
|
||||
finish() {
|
||||
for(const row of transfers) disabled(row,'finish');
|
||||
check('one native dragstart', starts, 1);
|
||||
check('drop iff accepted', drops, mode==='drop'?1:0);
|
||||
check('only readable events schedule string callbacks', callbacks.filter(row=>!['dragstart','drop'].includes(row.phase)), []);
|
||||
check('captured callback payload survives disassociation', callbacks.filter(row=>['dragstart','drop'].includes(row.phase)).map(row=>row.value), mode==='drop'?['payload','payload']:['payload']);
|
||||
return {mode,capture,modifiers,checks,rows,callbacks,microtasks,trace,passed:checks.filter(c=>c.pass).length,total:checks.length,complete:checks.every(c=>c.pass)};
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,234 @@
|
||||
use super::*;
|
||||
|
||||
fn mode_input(vm: &mut ScriptVm, pointer_type: &str, input: (&str, f64, i32, u8)) {
|
||||
let (event, x, buttons, modifiers) = input;
|
||||
vm.dispatch_mouse_event_at_point_with_pointer_and_modifiers(
|
||||
x,
|
||||
80.0,
|
||||
event,
|
||||
if event == "mousemove" { -1 } else { 0 },
|
||||
Some(buttons),
|
||||
i32::from(event != "mousemove"),
|
||||
0.0,
|
||||
0.0,
|
||||
crate::runtime::RendererPointerEventProperties {
|
||||
pointer_id: 17,
|
||||
pointer_type: pointer_type.to_owned(),
|
||||
pressure: if buttons == 0 { 0.0 } else { 0.5 },
|
||||
..Default::default()
|
||||
},
|
||||
modifiers,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn native_drag_data_modes_invalidate_views_and_preserve_queued_strings() {
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
|
||||
for pointer in ["mouse", "pen"] {
|
||||
for capture in [false, true] {
|
||||
for mode in ["cancel", "drop", "reject", "escape"] {
|
||||
for modifiers in [0, 8, 15] {
|
||||
let mut vm = new_storage_page_task_executor_test_vm_with_loader(
|
||||
"https://drag-data-modes.test/",
|
||||
&loader,
|
||||
);
|
||||
vm.eval("if (!document.documentElement) document.appendChild(document.createElement('html')); if (!document.body) document.documentElement.appendChild(document.createElement('body')); 'ready'").unwrap();
|
||||
vm.eval(&format!("{}; globalThis.__dragResults=__installDragDataModeProbe('{mode}',{capture},{modifiers}); 'ready'", include_str!("drag_data_modes.js"))).unwrap();
|
||||
vm.publish_layout_for_test().unwrap();
|
||||
for input in [
|
||||
("mousemove", 80.0, 0, modifiers),
|
||||
("mousedown", 80.0, 1, modifiers),
|
||||
("mousemove", 100.0, 1, modifiers),
|
||||
] {
|
||||
mode_input(&mut vm, pointer, input);
|
||||
}
|
||||
if mode == "escape" {
|
||||
vm.dispatch_key_event("keydown", "Escape", "Escape", "", 0, false, false)
|
||||
.unwrap();
|
||||
} else {
|
||||
for x in [320.0, 520.0] {
|
||||
mode_input(&mut vm, pointer, ("mousemove", x, 1, modifiers ^ 8));
|
||||
}
|
||||
}
|
||||
vm.eval("__dragResults.afterDispatch('before-release'); 'ready'")
|
||||
.unwrap();
|
||||
mode_input(
|
||||
&mut vm,
|
||||
pointer,
|
||||
(
|
||||
"mouseup",
|
||||
if mode == "escape" { 100.0 } else { 520.0 },
|
||||
0,
|
||||
modifiers ^ 8,
|
||||
),
|
||||
);
|
||||
vm.eval("__dragResults.afterDispatch('after-release'); 'ready'")
|
||||
.unwrap();
|
||||
let mut tasks = 0;
|
||||
while vm
|
||||
.run_one_user_interaction_executor_turn(&loader)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
tasks += 1;
|
||||
assert!(
|
||||
tasks <= 2,
|
||||
"protected/disabled items must not schedule callbacks"
|
||||
);
|
||||
}
|
||||
assert_eq!(tasks, if mode == "drop" { 2 } else { 1 });
|
||||
assert_eq!(vm.eval("globalThis.__dragFinished=__dragResults.finish(); __dragFinished.complete").unwrap(), "true", "{pointer}, {mode}, capture {capture}, modifiers {modifiers}: {}", vm.eval("JSON.stringify(__dragFinished.checks.filter(c=>!c.pass))").unwrap());
|
||||
assert_eq!(vm.eval("__dragFinished.microtasks.length===__dragFinished.rows.length && __dragFinished.microtasks.every(row=>row.items===4 && row.data===(['dragstart','drop'].includes(row.phase)?'payload':''))").unwrap(), "true", "listener cleanup must run while the event view is still associated");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constructed_transfer_remains_writable_after_synthetic_event_dispatch() {
|
||||
let mut vm = new_storage_test_vm("https://synthetic-transfer.test/");
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"
|
||||
globalThis.dt=new DataTransfer(); const item=dt.items.add('before','text/plain');
|
||||
document.dispatchEvent(new DragEvent('drop',{dataTransfer:dt}));
|
||||
dt.setData('text/plain','after');
|
||||
JSON.stringify([dt.getData('text/plain'),dt.items[0]===item,item.kind,dt.items.length])
|
||||
"#
|
||||
)
|
||||
.unwrap(),
|
||||
r#"["after",true,"string",1]"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_receiver_validation_precedes_argument_conversion() {
|
||||
let mut vm = new_storage_test_vm("https://transfer-receiver.test/");
|
||||
assert_eq!(vm.eval(r#"
|
||||
const dt=new DataTransfer(); dt.setData('text/plain','value');
|
||||
const classes=[DataTransfer,DataTransferItemList,DataTransferItem];
|
||||
const objects=[dt,dt.items,dt.items[0]];
|
||||
let conversions=0; const format={toString(){conversions++; return 'text/plain';}};
|
||||
const failures=[];
|
||||
for(let i=0;i<classes.length;i++) {
|
||||
const proto=classes[i].prototype;
|
||||
for(const receiver of [{},Object.create(objects[i]),new Proxy(objects[i],{})]) {
|
||||
const method=i===0?()=>proto.getData.call(receiver,format):i===1?()=>proto.remove.call(receiver,{valueOf(){conversions++;return 0;}}):()=>proto.getAsString.call(receiver,{});
|
||||
try {method(); failures.push(false);} catch(e) {failures.push(e instanceof TypeError);}
|
||||
}
|
||||
}
|
||||
JSON.stringify([failures.every(Boolean),conversions])
|
||||
"#).unwrap(), "[true,0]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_drag_views_hide_payload_until_drop_and_keep_text_default_action() {
|
||||
use crate::runtime::{RendererDragData, RendererDragDataItem};
|
||||
let mut vm = new_rendered_test_vm(
|
||||
"https://external-drag.test/",
|
||||
"<html><body style='margin:0'><input id='target' style='width:200px;height:100px'></body></html>",
|
||||
);
|
||||
vm.eval(r#"
|
||||
globalThis.transfers=[]; globalThis.observations=[];
|
||||
for (const type of ['dragenter','dragover','drop']) target.addEventListener(type,event=>{
|
||||
const dt=event.dataTransfer;
|
||||
observations.push([type,dt.getData('text/plain'),dt.items.length,Array.from(dt.types)]);
|
||||
dt.setData('text/plain','tampered'); dt.clearData();
|
||||
observations.push([dt.getData('text/plain'),transfers.every(old=>old.items.length===0 && old.getData('text/plain')==='')]);
|
||||
transfers.push(dt);
|
||||
}); 'ready'
|
||||
"#).unwrap();
|
||||
let data = RendererDragData {
|
||||
items: vec![RendererDragDataItem {
|
||||
mime_type: "text/plain".to_owned(),
|
||||
data: "external".to_owned(),
|
||||
title: None,
|
||||
base_url: None,
|
||||
}],
|
||||
files: vec![],
|
||||
directories: vec![],
|
||||
drag_operations_mask: 1,
|
||||
};
|
||||
for event in ["dragenter", "dragover", "drop"] {
|
||||
vm.dispatch_drag_event_at_point(10.0, 10.0, event, data.clone(), 0)
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(vm.eval("JSON.stringify([observations,target.value,transfers.every(dt=>dt.items.length===0 && dt.types.length===0 && dt.getData('text/plain')==='')])").unwrap(), r#"[[["dragenter","",1,["text/plain"]],["",true],["dragover","",1,["text/plain"]],["",true],["drop","external",1,["text/plain"]],["external",true]],"external",true]"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_file_drop_uses_backing_store_after_event_view_is_disabled() {
|
||||
use crate::runtime::{RendererDragData, RendererDraggedFile};
|
||||
let mut vm = new_rendered_test_vm(
|
||||
"https://external-file-drag.test/",
|
||||
"<html><body style='margin:0'><input id='target' type='file' style='width:200px;height:100px'></body></html>",
|
||||
);
|
||||
vm.eval(r#"
|
||||
globalThis.saved=null;globalThis.payload=null;globalThis.events=[];
|
||||
target.addEventListener('drop',event=>{
|
||||
saved=event.dataTransfer; payload=saved.items[0];
|
||||
events.push([saved.files.length,payload.getAsFile()===saved.files[0],payload.getAsFile().name]);
|
||||
saved.items.clear(); saved.setData('text/plain','tampered');
|
||||
});
|
||||
target.addEventListener('input',()=>events.push(['input',saved.files.length,saved.items.length]));
|
||||
'ready'
|
||||
"#).unwrap();
|
||||
let data = RendererDragData {
|
||||
items: vec![],
|
||||
files: vec![RendererDraggedFile {
|
||||
bytes: b"contents".to_vec(),
|
||||
mime_type: "text/plain".to_owned(),
|
||||
name: "upload.txt".to_owned(),
|
||||
last_modified: 7.0,
|
||||
}],
|
||||
directories: vec![],
|
||||
drag_operations_mask: 1,
|
||||
};
|
||||
vm.dispatch_drag_event_at_point(10.0, 10.0, "drop", data, 0)
|
||||
.unwrap();
|
||||
assert_eq!(vm.eval("JSON.stringify([events,target.files[0].name,target.files[0].size,saved.files.length,payload.kind,payload.getAsFile()])").unwrap(),r#"[[[1,true,"upload.txt"],["input",0,0]],"upload.txt",8,0,"",null]"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_drag_event_views_use_each_target_documents_realm() {
|
||||
for origin in ["parent", "child"] {
|
||||
let mut vm = new_rendered_test_vm(
|
||||
"https://drag-realms.test/",
|
||||
"<html><body style='margin:0'><div id='outside' style='position:absolute;left:40px;top:40px;width:120px;height:120px'></div><iframe id='frame' style='position:absolute;left:300px;top:40px;width:160px;height:160px;border:0'></iframe></body></html>",
|
||||
);
|
||||
vm.eval(&format!(r#"
|
||||
globalThis.child=frame.contentWindow;
|
||||
child.document.body.innerHTML='<div id="inside" style="position:absolute;left:0;top:0;width:160px;height:160px"></div>';
|
||||
const source={source};source.draggable=true;
|
||||
globalThis.rows=[];globalThis.saved=[];
|
||||
for(const [win,name] of [[window,'parent'],[child,'child']]) {{
|
||||
for(const type of ['dragstart','dragenter','dragover','drop','dragend']) win.document.addEventListener(type,event=>{{
|
||||
const dt=event.dataTransfer;
|
||||
if(type==='dragstart') {{dt.setData('text/plain','realm-payload');dt.effectAllowed='copy';}}
|
||||
rows.push([name,type,event instanceof win.DragEvent,dt instanceof win.DataTransfer,dt.items instanceof win.DataTransferItemList,dt.items[0] instanceof win.DataTransferItem,event.view===win,saved.every(old=>old.items.length===0 && old.types.length===0),dt.getData('text/plain')]);
|
||||
saved.push(dt);
|
||||
if(type==='dragenter'||type==='dragover') {{dt.dropEffect='copy';event.preventDefault();}}
|
||||
if(type==='drop') event.preventDefault();
|
||||
}});
|
||||
}} 'ready'
|
||||
"#,source=if origin=="parent" {"outside"} else {"child.document.getElementById('inside')"})).unwrap();
|
||||
vm.publish_layout_for_test().unwrap();
|
||||
let (start, end) = if origin == "parent" {
|
||||
(80.0, 320.0)
|
||||
} else {
|
||||
(320.0, 80.0)
|
||||
};
|
||||
for input in [
|
||||
("mousemove", start, 0, 0),
|
||||
("mousedown", start, 1, 0),
|
||||
("mousemove", start + 20.0, 1, 0),
|
||||
("mousemove", end, 1, 0),
|
||||
("mouseup", end, 0, 0),
|
||||
] {
|
||||
mode_input(&mut vm, "mouse", input);
|
||||
}
|
||||
assert_eq!(vm.eval("rows.every(row=>row.slice(2,8).every(Boolean) && row[8]===(['dragstart','drop'].includes(row[1])?'realm-payload':'')) && rows.filter(row=>row[1]==='drop').length===1 && saved.every(dt=>dt.items.length===0)").unwrap(),"true","{origin}: {}",vm.eval("JSON.stringify(rows)").unwrap());
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ mod document_domain_setter;
|
||||
mod document_open_navigation_abort;
|
||||
mod document_open_origin;
|
||||
mod dom_event_worlds;
|
||||
mod drag_data_modes;
|
||||
mod drag_init;
|
||||
mod drag_lifecycle;
|
||||
mod error_event_init;
|
||||
|
||||
@@ -93,7 +93,7 @@ fn native_event_producers_bypass_author_constructors_dictionaries_and_indexed_se
|
||||
0,
|
||||
),
|
||||
element::construct_pointer_event(scope, "pointerdown", 11.0, 12.0, 0, 1, &pointer),
|
||||
element::construct_drag_event(
|
||||
element::construct_drag_event_with_related_target(
|
||||
scope,
|
||||
"dragstart",
|
||||
11.0,
|
||||
@@ -101,6 +101,7 @@ fn native_event_producers_bypass_author_constructors_dictionaries_and_indexed_se
|
||||
1,
|
||||
transfer.into(),
|
||||
0,
|
||||
None,
|
||||
),
|
||||
element::construct_wheel_event(scope, "wheel", 11.0, 12.0, 4.0, 5.0, 0, 1, 0),
|
||||
element::construct_touch_event_with_points(
|
||||
|
||||
@@ -503,6 +503,9 @@ fn data_transfer_declared_slots_ignore_prototype_spoofing() {
|
||||
entry.__lmFileSystemEntryIsFile = false;
|
||||
entry.__lmFileSystemEntryIsDirectory = true;
|
||||
|
||||
const receiverError = action => {
|
||||
try { action(); return "no error"; } catch (error) { return error.name; }
|
||||
};
|
||||
return JSON.stringify({
|
||||
ownNamesBefore,
|
||||
descriptors,
|
||||
@@ -521,14 +524,14 @@ fn data_transfer_declared_slots_ignore_prototype_spoofing() {
|
||||
entry.isDirectory
|
||||
].join('|'),
|
||||
fake: [
|
||||
DataTransfer.prototype.getData.call(fakeDataTransfer, 'text/plain'),
|
||||
fakeDataTransfer.types === undefined ? 'undefined' : fakeDataTransfer.types.join(','),
|
||||
fakeDataTransfer.dropEffect,
|
||||
fakeDataTransfer.effectAllowed,
|
||||
fakeList.length,
|
||||
fakeItem.kind,
|
||||
fakeItem.type,
|
||||
fakeItem.getAsFile(),
|
||||
receiverError(() => DataTransfer.prototype.getData.call(fakeDataTransfer, 'text/plain')),
|
||||
receiverError(() => fakeDataTransfer.types === undefined ? 'undefined' : fakeDataTransfer.types.join(',')),
|
||||
receiverError(() => fakeDataTransfer.dropEffect),
|
||||
receiverError(() => fakeDataTransfer.effectAllowed),
|
||||
receiverError(() => fakeList.length),
|
||||
receiverError(() => fakeItem.kind),
|
||||
receiverError(() => fakeItem.type),
|
||||
receiverError(() => fakeItem.getAsFile()),
|
||||
entryNameGetter.call(fakeEntry),
|
||||
entryPathGetter.call(fakeEntry)
|
||||
].map(value => value === null ? 'null' : String(value)).join('|')
|
||||
@@ -540,7 +543,7 @@ fn data_transfer_declared_slots_ignore_prototype_spoofing() {
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"{"ownNamesBefore":{"dataTransfer":[],"itemList":[],"item":[],"entry":[]},"descriptors":{"dataTransfer":["files:function:get files:0:undefined:none:none:true:true","items:function:get items:0:undefined:none:none:true:true","types:function:get types:0:undefined:none:none:true:true","dropEffect:function:get dropEffect:0:function:set dropEffect:1:true:true","effectAllowed:function:get effectAllowed:0:function:set effectAllowed:1:true:true"],"itemList":["length:function:get length:0:undefined:none:none:true:true"],"item":["kind:function:get kind:0:undefined:none:none:true:true","type:function:get type:0:undefined:none:none:true:true"],"entry":["filesystem:function:get filesystem:0:undefined:none:none:true:true","fullPath:function:get fullPath:0:undefined:none:none:true:true","isDirectory:function:get isDirectory:0:undefined:none:none:true:true","isFile:function:get isFile:0:undefined:none:none:true:true","name:function:get name:0:undefined:none:none:true:true"]},"real":"alpha|text/plain,Files|none|none|2|file|text/plain|note.txt|note.txt|/note.txt|true|false","fake":"|undefined|none|uninitialized|0|||null||"}"#
|
||||
r#"{"ownNamesBefore":{"dataTransfer":[],"itemList":[],"item":[],"entry":[]},"descriptors":{"dataTransfer":["files:function:get files:0:undefined:none:none:true:true","items:function:get items:0:undefined:none:none:true:true","types:function:get types:0:undefined:none:none:true:true","dropEffect:function:get dropEffect:0:function:set dropEffect:1:true:true","effectAllowed:function:get effectAllowed:0:function:set effectAllowed:1:true:true"],"itemList":["length:function:get length:0:undefined:none:none:true:true"],"item":["kind:function:get kind:0:undefined:none:none:true:true","type:function:get type:0:undefined:none:none:true:true"],"entry":["filesystem:function:get filesystem:0:undefined:none:none:true:true","fullPath:function:get fullPath:0:undefined:none:none:true:true","isDirectory:function:get isDirectory:0:undefined:none:none:true:true","isFile:function:get isFile:0:undefined:none:none:true:true","name:function:get name:0:undefined:none:none:true:true"]},"real":"alpha|text/plain,Files|none|none|2|file|text/plain|note.txt|note.txt|/note.txt|true|false","fake":"TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError||"}"#
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,11 +47,14 @@ const dropped = new Promise(function (resolve) {
|
||||
|
||||
promise_test(async function () {
|
||||
await dropped;
|
||||
assert_equals(firstDataTransfer.items.length, 0, "saved dragenter view is disabled");
|
||||
assert_equals(firstDataTransfer.types.length, 0, "cancel does not reactivate the old view");
|
||||
assert_equals(firstDataTransfer.getData("text"), "", "old payload stays inaccessible");
|
||||
assert_array_equals(
|
||||
events,
|
||||
[
|
||||
"dragenter:initial:true",
|
||||
"dragover:mutated-before-cancel:true",
|
||||
"dragenter::true",
|
||||
"dragover::false",
|
||||
"drop:fresh-after-cancel:false",
|
||||
],
|
||||
"dragCancel should clear the active DataTransfer without dispatching a DOM event",
|
||||
|
||||
@@ -71,16 +71,18 @@ const dropped = new Promise(function (resolve) {
|
||||
|
||||
promise_test(async function () {
|
||||
await dropped;
|
||||
assert_equals(firstDataTransfer.items.length, 0, "saved event item list is disabled");
|
||||
assert_equals(firstDataTransfer.getData("text"), "", "saved event cannot read the store");
|
||||
assert_array_equals(
|
||||
events,
|
||||
[
|
||||
"dragenter:true:initial-text:copy:copyMove",
|
||||
"dragover-before:true:initial-text",
|
||||
"dragover-after:mutated-in-over:move",
|
||||
"drop:true:mutated-in-over:move:hello world",
|
||||
"input:hello mutated-in-over:21:21",
|
||||
"dragenter:true::copy:copyMove",
|
||||
"dragover-before:false:",
|
||||
"dragover-after::move",
|
||||
"drop:false:initial-text:move:hello world",
|
||||
"input:hello initial-text:18:18",
|
||||
],
|
||||
"drag lifecycle should reuse live DataTransfer and run text drop default action",
|
||||
"drag lifecycle should use temporary protected views and preserve data for the text drop default action",
|
||||
);
|
||||
}, "Input.dispatchDragEvent drives live DataTransfer reuse and text input default drop");
|
||||
}, "Input.dispatchDragEvent protects event views and preserves text input default drop");
|
||||
</script>
|
||||
|
||||
@@ -6,90 +6,90 @@
|
||||
<script src="/wpt/resources/moli-wpt-adapter.js"></script>
|
||||
<script>
|
||||
const dropzone = document.getElementById("dropzone");
|
||||
let dropped = null;
|
||||
let resolveDrop = null;
|
||||
|
||||
const dropPromise = new Promise(function (resolve) {
|
||||
let resolveDrop, rejectDrop;
|
||||
const dropPromise = new Promise((resolve, reject) => {
|
||||
resolveDrop = resolve;
|
||||
rejectDrop = reject;
|
||||
});
|
||||
|
||||
function item_string(item) {
|
||||
return new Promise(function (resolve) {
|
||||
item.getAsString(resolve);
|
||||
});
|
||||
return new Promise(resolve => item.getAsString(resolve));
|
||||
}
|
||||
|
||||
function entry_file(entry) {
|
||||
return new Promise(function (resolve) {
|
||||
entry.file(resolve);
|
||||
});
|
||||
return new Promise(resolve => entry.file(resolve));
|
||||
}
|
||||
|
||||
function read_entries(reader) {
|
||||
return new Promise(function (resolve) {
|
||||
reader.readEntries(resolve);
|
||||
});
|
||||
return new Promise(resolve => reader.readEntries(resolve));
|
||||
}
|
||||
|
||||
dropzone.addEventListener("dragover", function (event) {
|
||||
dropzone.addEventListener("dragover", event => event.preventDefault());
|
||||
dropzone.addEventListener("drop", event => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const dropped = event.dataTransfer;
|
||||
assert_equals(dropped.items.length, 4, "mixed payload exposes all item entries during drop");
|
||||
assert_array_equals(Array.from(dropped.types), ["text/plain", "text/html", "Files"],
|
||||
"types include string MIME types followed by Files");
|
||||
assert_equals(dropped.files.length, 1, "only real file payloads appear in files");
|
||||
const file = dropped.files[0];
|
||||
assert_equals(file.name, "report.txt", "FileList file name");
|
||||
assert_equals(file.type, "text/plain", "FileList file type");
|
||||
assert_equals(file.lastModified, 21, "FileList file lastModified");
|
||||
|
||||
const [plain, html, fileItem, directoryItem] = Array.from(dropped.items);
|
||||
assert_equals(plain.kind, "string", "plain item kind");
|
||||
assert_equals(plain.type, "text/plain", "plain item type");
|
||||
assert_equals(plain.getAsFile(), null, "string item does not expose a File");
|
||||
assert_equals(plain.webkitGetAsEntry(), null, "string item does not expose an entry");
|
||||
assert_equals(html.kind, "string", "HTML item kind");
|
||||
assert_equals(html.type, "text/html", "HTML item type");
|
||||
assert_equals(fileItem.kind, "file", "file item kind");
|
||||
assert_equals(fileItem.type, "text/plain", "file item type");
|
||||
assert_equals(fileItem.getAsFile(), file, "file item shares the FileList wrapper");
|
||||
const fileEntry = fileItem.webkitGetAsEntry();
|
||||
assert_true(fileEntry instanceof FileSystemFileEntry, "file item exposes a file entry");
|
||||
assert_equals(fileEntry.fullPath, "/report.txt", "file entry fullPath");
|
||||
assert_equals(directoryItem.kind, "file", "directory item kind");
|
||||
assert_equals(directoryItem.type, "", "directory item has empty MIME type");
|
||||
assert_equals(directoryItem.getAsFile(), null, "directory item does not expose a File");
|
||||
const directoryEntry = directoryItem.webkitGetAsEntry();
|
||||
assert_true(directoryEntry instanceof FileSystemDirectoryEntry,
|
||||
"directory item exposes a directory entry");
|
||||
assert_equals(directoryEntry.fullPath, "/bundle", "directory entry fullPath");
|
||||
|
||||
// Capture eligible callbacks and durable File/Entry objects during dispatch.
|
||||
resolveDrop({dropped, file, fileEntry, directoryEntry,
|
||||
items: [plain, html, fileItem, directoryItem],
|
||||
plainText: item_string(plain), htmlText: item_string(html)});
|
||||
} catch (error) {
|
||||
rejectDrop(error);
|
||||
}
|
||||
});
|
||||
|
||||
dropzone.addEventListener("drop", function (event) {
|
||||
event.preventDefault();
|
||||
dropped = event.dataTransfer;
|
||||
resolveDrop();
|
||||
});
|
||||
|
||||
promise_test(async function () {
|
||||
await dropPromise;
|
||||
|
||||
assert_equals(dropped.items.length, 4, "mixed payload exposes all DataTransferItem entries");
|
||||
assert_array_equals(
|
||||
Array.from(dropped.types),
|
||||
["text/plain", "text/html", "Files"],
|
||||
"types include string MIME types followed by Files",
|
||||
);
|
||||
assert_equals(dropped.files.length, 1, "only real file payloads appear in DataTransfer.files");
|
||||
assert_equals(dropped.files[0].name, "report.txt", "FileList file name");
|
||||
assert_equals(dropped.files[0].type, "text/plain", "FileList file type");
|
||||
assert_equals(dropped.files[0].lastModified, 21, "FileList file lastModified");
|
||||
assert_equals(await dropped.files[0].text(), "file payload", "FileList file bytes");
|
||||
|
||||
const plain = dropped.items[0];
|
||||
assert_equals(plain.kind, "string", "plain item kind");
|
||||
assert_equals(plain.type, "text/plain", "plain item type");
|
||||
assert_equals(await item_string(plain), "plain payload", "plain string payload");
|
||||
assert_equals(plain.getAsFile(), null, "string item does not expose a File");
|
||||
assert_equals(plain.webkitGetAsEntry(), null, "string item does not expose an entry");
|
||||
|
||||
const html = dropped.items[1];
|
||||
assert_equals(html.kind, "string", "HTML item kind");
|
||||
assert_equals(html.type, "text/html", "HTML item type");
|
||||
assert_equals(await item_string(html), "<b>html payload</b>", "HTML string payload");
|
||||
|
||||
const fileItem = dropped.items[2];
|
||||
assert_equals(fileItem.kind, "file", "file item kind");
|
||||
assert_equals(fileItem.type, "text/plain", "file item type");
|
||||
assert_equals(fileItem.getAsFile(), dropped.files[0], "file item shares the FileList wrapper");
|
||||
const fileEntry = fileItem.webkitGetAsEntry();
|
||||
assert_true(fileEntry instanceof FileSystemFileEntry, "file item exposes a file entry");
|
||||
assert_equals(fileEntry.fullPath, "/report.txt", "file entry fullPath");
|
||||
const entryBackedFile = await entry_file(fileEntry);
|
||||
promise_test(async () => {
|
||||
const payload = await dropPromise;
|
||||
const [plain, html] = await Promise.all([payload.plainText, payload.htmlText]);
|
||||
assert_equals(plain, "plain payload", "captured plain string survives disassociation");
|
||||
assert_equals(html, "<b>html payload</b>", "captured HTML string survives disassociation");
|
||||
assert_equals(await payload.file.text(), "file payload", "captured File preserves bytes");
|
||||
const entryBackedFile = await entry_file(payload.fileEntry);
|
||||
assert_equals(await entryBackedFile.text(), "file payload", "file entry materializes the same bytes");
|
||||
|
||||
const directoryItem = dropped.items[3];
|
||||
assert_equals(directoryItem.kind, "file", "directory item kind");
|
||||
assert_equals(directoryItem.type, "", "directory item has empty MIME type");
|
||||
assert_equals(directoryItem.getAsFile(), null, "directory item does not expose a File");
|
||||
const directoryEntry = directoryItem.webkitGetAsEntry();
|
||||
assert_true(directoryEntry instanceof FileSystemDirectoryEntry, "directory item exposes a directory entry");
|
||||
assert_equals(directoryEntry.fullPath, "/bundle", "directory entry fullPath");
|
||||
const children = await read_entries(directoryEntry.createReader());
|
||||
const children = await read_entries(payload.directoryEntry.createReader());
|
||||
assert_equals(children.length, 1, "directory entry exposes its child file");
|
||||
assert_equals(children[0].name, "inside.txt", "directory child file name");
|
||||
assert_equals(children[0].fullPath, "/bundle/inside.txt", "directory child fullPath");
|
||||
const childFile = await entry_file(children[0]);
|
||||
assert_equals(await childFile.text(), "inside payload", "directory child file bytes");
|
||||
}, "Input.dispatchDragEvent preserves mixed string, file, and directory payload surfaces");
|
||||
|
||||
assert_equals(payload.dropped.items.length, 0, "saved item list is disabled after drop");
|
||||
assert_equals(payload.dropped.files.length, 0, "saved FileList becomes empty");
|
||||
assert_equals(payload.dropped.types.length, 0, "saved transfer has no current types");
|
||||
assert_equals(payload.dropped.getData("text/plain"), "", "saved transfer cannot read data");
|
||||
for (const item of payload.items) {
|
||||
assert_equals(item.kind, "", "saved item metadata is disabled");
|
||||
assert_equals(item.type, "", "saved item type is disabled");
|
||||
assert_equals(item.getAsFile(), null, "saved item cannot reveal a File");
|
||||
assert_equals(item.webkitGetAsEntry(), null, "saved item cannot reveal an Entry");
|
||||
}
|
||||
}, "Input.dispatchDragEvent preserves captured mixed payloads and retires event views");
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user