dom: Implement ClipboardItem (#36336)

implement the `ClipboardItem` interface

Testing: covered by existing wpt tests

part of #36084

---------

Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
This commit is contained in:
Gae24 2025-04-07 01:47:57 +02:00 committed by GitHub
parent 33b00dbe40
commit d1243a1867
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 219 additions and 38 deletions

View file

@ -69,6 +69,7 @@ pub struct Preferences {
/// List of comma-separated backends to be used by wgpu. /// List of comma-separated backends to be used by wgpu.
pub dom_webgpu_wgpu_backend: String, pub dom_webgpu_wgpu_backend: String,
pub dom_abort_controller_enabled: bool, pub dom_abort_controller_enabled: bool,
pub dom_async_clipboard_enabled: bool,
pub dom_bluetooth_enabled: bool, pub dom_bluetooth_enabled: bool,
pub dom_bluetooth_testing_enabled: bool, pub dom_bluetooth_testing_enabled: bool,
pub dom_allow_scripts_to_close_windows: bool, pub dom_allow_scripts_to_close_windows: bool,
@ -246,6 +247,7 @@ impl Preferences {
devtools_server_port: 0, devtools_server_port: 0,
dom_abort_controller_enabled: false, dom_abort_controller_enabled: false,
dom_allow_scripts_to_close_windows: false, dom_allow_scripts_to_close_windows: false,
dom_async_clipboard_enabled: false,
dom_bluetooth_enabled: false, dom_bluetooth_enabled: false,
dom_bluetooth_testing_enabled: false, dom_bluetooth_testing_enabled: false,
dom_canvas_capture_enabled: false, dom_canvas_capture_enabled: false,

View file

@ -0,0 +1,181 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::ops::Deref;
use std::rc::Rc;
use std::str::FromStr;
use data_url::mime::Mime;
use dom_struct::dom_struct;
use js::rust::{HandleObject, MutableHandleValue};
use script_bindings::record::Record;
use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::ClipboardBinding::{
ClipboardItemMethods, ClipboardItemOptions, PresentationStyle,
};
use crate::dom::bindings::error::{Error, Fallible};
use crate::dom::bindings::frozenarray::CachedFrozenArray;
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto};
use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::str::DOMString;
use crate::dom::promise::Promise;
use crate::dom::window::Window;
use crate::script_runtime::{CanGc, JSContext};
/// <https://w3c.github.io/clipboard-apis/#web-custom-format>
const CUSTOM_FORMAT_PREFIX: &str = "web ";
/// <https://w3c.github.io/clipboard-apis/#representation>
#[derive(JSTraceable, MallocSizeOf)]
struct Representation {
#[no_trace]
#[ignore_malloc_size_of = "Extern type"]
mime_type: Mime,
is_custom: bool,
#[ignore_malloc_size_of = "Rc is hard"]
data: Rc<Promise>,
}
#[dom_struct]
pub(crate) struct ClipboardItem {
reflector_: Reflector,
representations: DomRefCell<Vec<Representation>>,
presentation_style: DomRefCell<PresentationStyle>,
#[ignore_malloc_size_of = "mozjs"]
frozen_types: CachedFrozenArray,
}
impl ClipboardItem {
fn new_inherited() -> ClipboardItem {
ClipboardItem {
reflector_: Reflector::new(),
representations: Default::default(),
presentation_style: Default::default(),
frozen_types: CachedFrozenArray::new(),
}
}
fn new(window: &Window, proto: Option<HandleObject>, can_gc: CanGc) -> DomRoot<ClipboardItem> {
reflect_dom_object_with_proto(
Box::new(ClipboardItem::new_inherited()),
window,
proto,
can_gc,
)
}
}
impl ClipboardItemMethods<crate::DomTypeHolder> for ClipboardItem {
/// <https://w3c.github.io/clipboard-apis/#dom-clipboarditem-clipboarditem>
fn Constructor(
global: &Window,
proto: Option<HandleObject>,
can_gc: CanGc,
items: Record<DOMString, Rc<Promise>>,
options: &ClipboardItemOptions,
) -> Fallible<DomRoot<ClipboardItem>> {
// Step 1 If items is empty, then throw a TypeError.
if items.is_empty() {
return Err(Error::Type(String::from("No item provided")));
}
// Step 2 If options is empty, then set options["presentationStyle"] = "unspecified".
// NOTE: This is done inside bindings
// Step 3 Set this's clipboard item to a new clipboard item.
let clipboard_item = ClipboardItem::new(global, proto, can_gc);
// Step 4 Set this's clipboard item's presentation style to options["presentationStyle"].
*clipboard_item.presentation_style.borrow_mut() = options.presentationStyle;
// Step 6 For each (key, value) in items:
for (key, value) in items.deref() {
// Step 6.2 Let isCustom be false.
// Step 6.3 If key starts with `"web "` prefix, then
// Step 6.3.1 Remove `"web "` prefix and assign the remaining string to key.
let (key, is_custom) = match key.strip_prefix(CUSTOM_FORMAT_PREFIX) {
None => (key.str(), false),
// Step 6.3.2 Set isCustom true
Some(stripped) => (stripped, true),
};
// Step 6.5 Let mimeType be the result of parsing a MIME type given key.
// Step 6.6 If mimeType is failure, then throw a TypeError.
let mime_type =
Mime::from_str(key).map_err(|_| Error::Type(String::from("Invalid mime type")))?;
// Step 6.7 If this's clipboard item's list of representations contains a representation
// whose MIME type is mimeType and whose [representation/isCustom] is isCustom, then throw a TypeError.
if clipboard_item
.representations
.borrow()
.iter()
.any(|representation| {
representation.mime_type == mime_type && representation.is_custom == is_custom
})
{
return Err(Error::Type(String::from("Tried to add a duplicate mime")));
}
// Step 6.1 Let representation be a new representation.
// Step 6.4 Set representations isCustom flag to isCustom.
// Step 6.8 Set representations MIME type to mimeType.
// Step 6.9 Set representations data to value.
let representation = Representation {
mime_type,
is_custom,
data: value.clone(),
};
// Step 6.10 Append representation to this's clipboard item's list of representations.
clipboard_item
.representations
.borrow_mut()
.push(representation);
}
// NOTE: The steps for creating a frozen array from the list of mimeType are done in the Types() method
Ok(clipboard_item)
}
/// <https://w3c.github.io/clipboard-apis/#dom-clipboarditem-presentationstyle>
fn PresentationStyle(&self) -> PresentationStyle {
*self.presentation_style.borrow()
}
/// <https://w3c.github.io/clipboard-apis/#dom-clipboarditem-types>
fn Types(&self, cx: JSContext, can_gc: CanGc, retval: MutableHandleValue) {
self.frozen_types.get_or_init(
|| {
// Step 5 Let types be a list of DOMString.
let mut types = Vec::new();
self.representations
.borrow()
.iter()
.for_each(|representation| {
// Step 6.11 Let mimeTypeString be the result of serializing a MIME type with mimeType.
let mime_type_string = representation.mime_type.to_string();
// Step 6.12 If isCustom is true, prefix mimeTypeString with `"web "`.
let mime_type_string = if representation.is_custom {
format!("{}{}", CUSTOM_FORMAT_PREFIX, mime_type_string)
} else {
mime_type_string
};
// Step 6.13 Add mimeTypeString to types.
types.push(DOMString::from(mime_type_string));
});
types
},
cx,
retval,
can_gc,
);
}
}

View file

@ -251,6 +251,7 @@ pub(crate) mod channelsplitternode;
pub(crate) mod characterdata; pub(crate) mod characterdata;
pub(crate) mod client; pub(crate) mod client;
pub(crate) mod clipboardevent; pub(crate) mod clipboardevent;
pub(crate) mod clipboarditem;
pub(crate) mod closeevent; pub(crate) mod closeevent;
pub(crate) mod comment; pub(crate) mod comment;
pub(crate) mod compositionevent; pub(crate) mod compositionevent;

View file

@ -87,6 +87,10 @@ DOMInterfaces = {
'canGc': ['Before', 'After', 'Remove', 'ReplaceWith'] 'canGc': ['Before', 'After', 'Remove', 'ReplaceWith']
}, },
'ClipboardItem': {
'canGc': ['Types']
},
'CountQueuingStrategy': { 'CountQueuingStrategy': {
'canGc': ['GetSize'], 'canGc': ['GetSize'],
}, },

View file

@ -0,0 +1,26 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// https://w3c.github.io/clipboard-apis/#clipboard-item-interface
typedef Promise<(DOMString or Blob)> ClipboardItemData;
[SecureContext, Exposed=Window, Pref="dom_async_clipboard_enabled"]
interface ClipboardItem {
[Throws] constructor(record<DOMString, ClipboardItemData> items,
optional ClipboardItemOptions options = {});
readonly attribute PresentationStyle presentationStyle;
readonly attribute /* FrozenArray<DOMString> */ any types;
// Promise<Blob> getType(DOMString type);
// static boolean supports(DOMString type);
};
enum PresentationStyle { "unspecified", "inline", "attachment" };
dictionary ClipboardItemOptions {
PresentationStyle presentationStyle = "unspecified";
};

View file

@ -548,6 +548,7 @@ pub(crate) fn parse_command_line_arguments(args: Vec<String>) -> ArgumentParsing
if opt_match.opt_present("enable-experimental-web-platform-features") { if opt_match.opt_present("enable-experimental-web-platform-features") {
vec![ vec![
"dom_async_clipboard_enabled",
"dom_fontface_enabled", "dom_fontface_enabled",
"dom_imagebitmap_enabled", "dom_imagebitmap_enabled",
"dom_intersection_observer_enabled", "dom_intersection_observer_enabled",

View file

@ -1,4 +1,5 @@
prefs: [ prefs: [
"dom_async_clipboard_enabled:true",
"dom_fontface_enabled:true", "dom_fontface_enabled:true",
"dom_imagebitmap_enabled:true", "dom_imagebitmap_enabled:true",
"dom_intersection_observer_enabled:true", "dom_intersection_observer_enabled:true",

View file

@ -5,18 +5,6 @@
[ClipboardItem() succeeds with empty options] [ClipboardItem() succeeds with empty options]
expected: FAIL expected: FAIL
[ClipboardItem({}) fails with empty dictionary input]
expected: FAIL
[ClipboardItem(Blob) fails]
expected: FAIL
[ClipboardItem() fails with null input]
expected: FAIL
[ClipboardItem() fails with no input]
expected: FAIL
[types() returns correct values] [types() returns correct values]
expected: FAIL expected: FAIL

View file

@ -1,28 +1,4 @@
[idlharness.https.window.html] [idlharness.https.window.html]
[ClipboardItem interface: existence and properties of interface object]
expected: FAIL
[ClipboardItem interface object length]
expected: FAIL
[ClipboardItem interface object name]
expected: FAIL
[ClipboardItem interface: existence and properties of interface prototype object]
expected: FAIL
[ClipboardItem interface: existence and properties of interface prototype object's "constructor" property]
expected: FAIL
[ClipboardItem interface: existence and properties of interface prototype object's @@unscopables property]
expected: FAIL
[ClipboardItem interface: attribute presentationStyle]
expected: FAIL
[ClipboardItem interface: attribute types]
expected: FAIL
[ClipboardItem interface: operation getType(DOMString)] [ClipboardItem interface: operation getType(DOMString)]
expected: FAIL expected: FAIL

View file

@ -13503,7 +13503,7 @@
] ]
], ],
"interfaces.https.html": [ "interfaces.https.html": [
"dce05a55fd33326768635c6b3cdb193d526fccdd", "cc6e99d8da7cd82edefd1c0b0fce984ae85462de",
[ [
null, null,
{} {}

View file

@ -1 +1 @@
prefs: ["dom_imagebitmap_enabled:true", "dom_offscreen_canvas_enabled:true", "dom_xpath_enabled:true", "dom_intersection_observer_enabled:true", "dom_resize_observer_enabled:true", "dom_notification_enabled:true", "dom_fontface_enabled:true", "dom_urlpattern_enabled:true", "dom_serviceworker_enabled:true"] prefs: ["dom_imagebitmap_enabled:true", "dom_offscreen_canvas_enabled:true", "dom_xpath_enabled:true", "dom_intersection_observer_enabled:true", "dom_resize_observer_enabled:true", "dom_notification_enabled:true", "dom_fontface_enabled:true", "dom_urlpattern_enabled:true", "dom_serviceworker_enabled:true", "dom_async_clipboard_enabled:true"]

View file

@ -39,6 +39,7 @@ test_interfaces([
"ChannelSplitterNode", "ChannelSplitterNode",
"CharacterData", "CharacterData",
"ClipboardEvent", "ClipboardEvent",
"ClipboardItem",
"CloseEvent", "CloseEvent",
"ConstantSourceNode", "ConstantSourceNode",
"CryptoKey", "CryptoKey",