mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-22 07:30:19 +00:00
LibWeb: Begin implementing the Clipboard API
https://w3c.github.io/clipboard-apis/ This implements enough for navigator.clipboard.writeText(String).
This commit is contained in:
parent
78d455e231
commit
4b94b0b561
Notes:
sideshowbarker
2024-07-17 00:59:43 +09:00
Author: https://github.com/trflynn89 Commit: https://github.com/SerenityOS/serenity/commit/4b94b0b561 Pull-request: https://github.com/SerenityOS/serenity/pull/21878
14 changed files with 303 additions and 0 deletions
|
@ -14,6 +14,7 @@ namespace IDL {
|
|||
|
||||
static constexpr Array libweb_interface_namespaces = {
|
||||
"CSS"sv,
|
||||
"Clipboard"sv,
|
||||
"Crypto"sv,
|
||||
"DOM"sv,
|
||||
"DOMParsing"sv,
|
||||
|
|
2
Tests/LibWeb/Text/expected/clipboard.txt
Normal file
2
Tests/LibWeb/Text/expected/clipboard.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Failure
|
||||
Success
|
26
Tests/LibWeb/Text/input/clipboard.html
Normal file
26
Tests/LibWeb/Text/input/clipboard.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
<button id="button"></button>
|
||||
<script src="include.js"></script>
|
||||
<script type="text/javascript">
|
||||
function writeText(onComplete) {
|
||||
navigator.clipboard.writeText("Well hello friends :^)").then(
|
||||
() => {
|
||||
println("Success");
|
||||
onComplete();
|
||||
},
|
||||
() => {
|
||||
println("Failure");
|
||||
onComplete();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
asyncTest((done) => {
|
||||
writeText(() => {
|
||||
const button = document.getElementById("button");
|
||||
internals.dispatchUserActivatedEvent(button, new Event("mousedown"));
|
||||
button.dispatchEvent(new Event("click"));
|
||||
|
||||
writeText(done);
|
||||
});
|
||||
});
|
||||
</script>
|
|
@ -20,6 +20,7 @@ set(SOURCES
|
|||
Bindings/MainThreadVM.cpp
|
||||
Bindings/OptionConstructor.cpp
|
||||
Bindings/PlatformObject.cpp
|
||||
Clipboard/Clipboard.cpp
|
||||
Crypto/Crypto.cpp
|
||||
Crypto/SubtleCrypto.cpp
|
||||
CSS/Angle.cpp
|
||||
|
|
198
Userland/Libraries/LibWeb/Clipboard/Clipboard.cpp
Normal file
198
Userland/Libraries/LibWeb/Clipboard/Clipboard.cpp
Normal file
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibJS/Runtime/Realm.h>
|
||||
#include <LibTextCodec/Decoder.h>
|
||||
#include <LibWeb/Bindings/HostDefined.h>
|
||||
#include <LibWeb/Clipboard/Clipboard.h>
|
||||
#include <LibWeb/FileAPI/Blob.h>
|
||||
#include <LibWeb/HTML/Scripting/Environments.h>
|
||||
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
#include <LibWeb/MimeSniff/MimeType.h>
|
||||
#include <LibWeb/Page/Page.h>
|
||||
#include <LibWeb/Platform/EventLoopPlugin.h>
|
||||
#include <LibWeb/WebIDL/Promise.h>
|
||||
|
||||
namespace Web::Clipboard {
|
||||
|
||||
WebIDL::ExceptionOr<JS::NonnullGCPtr<Clipboard>> Clipboard::construct_impl(JS::Realm& realm)
|
||||
{
|
||||
return realm.heap().allocate<Clipboard>(realm, realm);
|
||||
}
|
||||
|
||||
Clipboard::Clipboard(JS::Realm& realm)
|
||||
: DOM::EventTarget(realm)
|
||||
{
|
||||
}
|
||||
|
||||
Clipboard::~Clipboard() = default;
|
||||
|
||||
void Clipboard::initialize(JS::Realm& realm)
|
||||
{
|
||||
Base::initialize(realm);
|
||||
set_prototype(&Bindings::ensure_web_prototype<Bindings::ClipboardPrototype>(realm, "Clipboard"));
|
||||
}
|
||||
|
||||
// https://w3c.github.io/clipboard-apis/#os-specific-well-known-format
|
||||
static String os_specific_well_known_format(StringView mime_type_string)
|
||||
{
|
||||
// NOTE: Here we always takes the Linux case, and defer to the chrome layer to handle OS specific implementations.
|
||||
auto mime_type = MUST(MimeSniff::MimeType::parse(mime_type_string));
|
||||
|
||||
// 1. Let wellKnownFormat be an empty string.
|
||||
String well_known_format {};
|
||||
|
||||
// 2. If mimeType’s essence is "text/plain", then
|
||||
if (mime_type->essence() == "text/plain"sv) {
|
||||
// On Windows, follow the convention described below:
|
||||
// Assign CF_UNICODETEXT to wellKnownFormat.
|
||||
// On MacOS, follow the convention described below:
|
||||
// Assign NSPasteboardTypeString to wellKnownFormat.
|
||||
// On Linux, ChromeOS, and Android, follow the convention described below:
|
||||
// Assign "text/plain" to wellKnownFormat.
|
||||
well_known_format = "text/plain"_string;
|
||||
}
|
||||
// 3. Else, if mimeType’s essence is "text/html", then
|
||||
if (mime_type->essence() == "text/html"sv) {
|
||||
// On Windows, follow the convention described below:
|
||||
// Assign CF_HTML to wellKnownFormat.
|
||||
// On MacOS, follow the convention described below:
|
||||
// Assign NSHTMLPboardType to wellKnownFormat.
|
||||
// On Linux, ChromeOS, and Android, follow the convention described below:
|
||||
// Assign "text/html" to wellKnownFormat.
|
||||
well_known_format = "text/html"_string;
|
||||
}
|
||||
// 4. Else, if mimeType’s essence is "image/png", then
|
||||
if (mime_type->essence() == "image/png"sv) {
|
||||
// On Windows, follow the convention described below:
|
||||
// Assign "PNG" to wellKnownFormat.
|
||||
// On MacOS, follow the convention described below:
|
||||
// Assign NSPasteboardTypePNG to wellKnownFormat.
|
||||
// On Linux, ChromeOS, and Android, follow the convention described below:
|
||||
// Assign "image/png" to wellKnownFormat.
|
||||
well_known_format = "image/png"_string;
|
||||
}
|
||||
|
||||
// 5. Return wellKnownFormat.
|
||||
return well_known_format;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/clipboard-apis/#write-blobs-and-option-to-the-clipboard
|
||||
static void write_blobs_and_option_to_clipboard(JS::Realm& realm, ReadonlySpan<JS::NonnullGCPtr<FileAPI::Blob>> items, String presentation_style)
|
||||
{
|
||||
auto& window = verify_cast<HTML::Window>(realm.global_object());
|
||||
|
||||
// FIXME: 1. Let webCustomFormats be a sequence<Blob>.
|
||||
|
||||
// 2. For each item in items:
|
||||
for (auto const& item : items) {
|
||||
// 1. Let formatString be the result of running os specific well-known format given item’s type.
|
||||
auto format_string = os_specific_well_known_format(item->type());
|
||||
|
||||
// 2. If formatString is empty then follow the below steps:
|
||||
if (format_string.is_empty()) {
|
||||
// FIXME: 1. Let webCustomFormatString be the item’s type.
|
||||
// FIXME: 2. Let webCustomFormat be an empty type.
|
||||
// FIXME: 3. If webCustomFormatString starts with `"web "` prefix, then remove the `"web "` prefix and store the
|
||||
// FIXME: remaining string in webMimeTypeString.
|
||||
// FIXME: 4. Let webMimeType be the result of parsing a MIME type given webMimeTypeString.
|
||||
// FIXME: 5. If webMimeType is failure, then abort all steps.
|
||||
// FIXME: 6. Let webCustomFormat’s type's essence equal to webMimeType.
|
||||
// FIXME: 7. Set item’s type to webCustomFormat.
|
||||
// FIXME: 8. Append webCustomFormat to webCustomFormats.
|
||||
}
|
||||
|
||||
// 3. Let payload be the result of UTF-8 decoding item’s underlying byte sequence.
|
||||
auto decoder = TextCodec::decoder_for("UTF-8"sv);
|
||||
auto payload = MUST(TextCodec::convert_input_to_utf8_using_given_decoder_unless_there_is_a_byte_order_mark(*decoder, item->bytes()));
|
||||
|
||||
// 4. Insert payload and presentationStyle into the system clipboard using formatString as the native clipboard format.
|
||||
if (auto* page = window.page())
|
||||
page->client().page_did_insert_clipboard_entry(move(payload), move(presentation_style), move(format_string));
|
||||
}
|
||||
|
||||
// FIXME: 3. Write web custom formats given webCustomFormats.
|
||||
}
|
||||
|
||||
// https://w3c.github.io/clipboard-apis/#check-clipboard-write-permission
|
||||
static bool check_clipboard_write_permission(JS::Realm& realm)
|
||||
{
|
||||
// NOTE: The clipboard permission is undergoing a refactor because the clipboard-write permission was removed from
|
||||
// the Permissions spec. So this partially implements the proposed update:
|
||||
// https://pr-preview.s3.amazonaws.com/w3c/clipboard-apis/pull/164.html#write-permission
|
||||
|
||||
// 1. Let hasGesture be true if the relevant global object of this has transient activation, false otherwise.
|
||||
auto has_gesture = verify_cast<HTML::Window>(realm.global_object()).has_transient_activation();
|
||||
|
||||
// 2. If hasGesture then,
|
||||
if (has_gesture) {
|
||||
// FIXME: 1. Return true if the current script is running as a result of user interaction with a "cut" or "copy"
|
||||
// element created by the user agent or operating system.
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. Otherwise, return false.
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/clipboard-apis/#dom-clipboard-writetext
|
||||
JS::NonnullGCPtr<JS::Promise> Clipboard::write_text(String data)
|
||||
{
|
||||
// 1. Let realm be this's relevant realm.
|
||||
auto& realm = HTML::relevant_realm(*this);
|
||||
|
||||
// 2. Let p be a new promise in realm.
|
||||
auto promise = WebIDL::create_promise(realm);
|
||||
|
||||
// 3. Run the following steps in parallel:
|
||||
Platform::EventLoopPlugin::the().deferred_invoke([&realm, promise, data = move(data)]() mutable {
|
||||
// 1. Let r be the result of running check clipboard write permission.
|
||||
auto result = check_clipboard_write_permission(realm);
|
||||
|
||||
// 2. If r is false, then:
|
||||
if (!result) {
|
||||
// 1. Queue a global task on the permission task source, given realm’s global object, to reject p with
|
||||
// "NotAllowedError" DOMException in realm.
|
||||
queue_global_task(HTML::Task::Source::Permissions, realm.global_object(), [&realm, promise]() mutable {
|
||||
HTML::TemporaryExecutionContext execution_context { Bindings::host_defined_environment_settings_object(realm) };
|
||||
WebIDL::reject_promise(realm, promise, WebIDL::NotAllowedError::create(realm, "Clipboard writing is only allowed through user activation"_fly_string));
|
||||
});
|
||||
|
||||
// 2. Abort these steps.
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Queue a global task on the clipboard task source, given realm’s global object, to perform the below steps:
|
||||
queue_global_task(HTML::Task::Source::Clipboard, realm.global_object(), [&realm, promise, data = move(data)]() mutable {
|
||||
// 1. Let itemList be an empty sequence<Blob>.
|
||||
Vector<JS::NonnullGCPtr<FileAPI::Blob>> item_list;
|
||||
|
||||
// 2. Let textBlob be a new Blob created with: type attribute set to "text/plain;charset=utf-8", and its
|
||||
// underlying byte sequence set to the UTF-8 encoding of data.
|
||||
// Note: On Windows replace `\n` characters with `\r\n` in data before creating textBlob.
|
||||
auto text_blob = FileAPI::Blob::create(realm, MUST(ByteBuffer::copy(data.bytes())), "text/plain;charset=utf-8"_string);
|
||||
|
||||
// 3. Add textBlob to itemList.
|
||||
item_list.append(text_blob);
|
||||
|
||||
// 4. Let option be set to "unspecified".
|
||||
auto option = "unspecified"_string;
|
||||
|
||||
// 5. Write blobs and option to the clipboard with itemList and option.
|
||||
write_blobs_and_option_to_clipboard(realm, item_list, move(option));
|
||||
|
||||
// 6. Resolve p.
|
||||
HTML::TemporaryExecutionContext execution_context { Bindings::host_defined_environment_settings_object(realm) };
|
||||
WebIDL::resolve_promise(realm, promise, JS::js_undefined());
|
||||
});
|
||||
});
|
||||
|
||||
// 4. Return p.
|
||||
return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise->promise()) };
|
||||
}
|
||||
|
||||
}
|
33
Userland/Libraries/LibWeb/Clipboard/Clipboard.h
Normal file
33
Userland/Libraries/LibWeb/Clipboard/Clipboard.h
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/String.h>
|
||||
#include <LibJS/Forward.h>
|
||||
#include <LibJS/Heap/GCPtr.h>
|
||||
#include <LibWeb/DOM/EventTarget.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
#include <LibWeb/WebIDL/ExceptionOr.h>
|
||||
|
||||
namespace Web::Clipboard {
|
||||
|
||||
class Clipboard final : public DOM::EventTarget {
|
||||
WEB_PLATFORM_OBJECT(Clipboard, DOM::EventTarget);
|
||||
|
||||
public:
|
||||
static WebIDL::ExceptionOr<JS::NonnullGCPtr<Clipboard>> construct_impl(JS::Realm&);
|
||||
virtual ~Clipboard() override;
|
||||
|
||||
JS::NonnullGCPtr<JS::Promise> write_text(String);
|
||||
|
||||
private:
|
||||
Clipboard(JS::Realm&);
|
||||
|
||||
virtual void initialize(JS::Realm&) override;
|
||||
};
|
||||
|
||||
}
|
12
Userland/Libraries/LibWeb/Clipboard/Clipboard.idl
Normal file
12
Userland/Libraries/LibWeb/Clipboard/Clipboard.idl
Normal file
|
@ -0,0 +1,12 @@
|
|||
#import <DOM/EventTarget.idl>
|
||||
|
||||
// FIXME: typedef sequence<ClipboardItem> ClipboardItems;
|
||||
|
||||
// https://w3c.github.io/clipboard-apis/#clipboard
|
||||
[SecureContext, Exposed=Window]
|
||||
interface Clipboard : EventTarget {
|
||||
// FIXME: Promise<ClipboardItems> read();
|
||||
// FIXME: Promise<DOMString> readText();
|
||||
// FIXME: Promise<undefined> write(ClipboardItems data);
|
||||
Promise<undefined> writeText(DOMString data);
|
||||
};
|
|
@ -65,6 +65,10 @@ enum class ResponseType;
|
|||
enum class XMLHttpRequestResponseType;
|
||||
}
|
||||
|
||||
namespace Web::Clipboard {
|
||||
class Clipboard;
|
||||
}
|
||||
|
||||
namespace Web::Cookie {
|
||||
struct Cookie;
|
||||
struct ParsedCookie;
|
||||
|
|
|
@ -47,6 +47,12 @@ public:
|
|||
// https://html.spec.whatwg.org/multipage/canvas.html#canvas-blob-serialisation-task-source
|
||||
CanvasBlobSerializationTask,
|
||||
|
||||
// https://w3c.github.io/clipboard-apis/#clipboard-task-source
|
||||
Clipboard,
|
||||
|
||||
// https://w3c.github.io/permissions/#permissions-task-source
|
||||
Permissions,
|
||||
|
||||
// !!! IMPORTANT: Keep this field last!
|
||||
// This serves as the base value of all unique task sources.
|
||||
// Some elements, such as the HTMLMediaElement, must have a unique task source per instance.
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include <LibJS/Heap/Heap.h>
|
||||
#include <LibJS/Runtime/Realm.h>
|
||||
#include <LibWeb/Bindings/Intrinsics.h>
|
||||
#include <LibWeb/Clipboard/Clipboard.h>
|
||||
#include <LibWeb/HTML/Navigator.h>
|
||||
#include <LibWeb/HTML/Scripting/Environments.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
|
@ -57,6 +58,7 @@ void Navigator::visit_edges(Cell::Visitor& visitor)
|
|||
Base::visit_edges(visitor);
|
||||
visitor.visit(m_mime_type_array);
|
||||
visitor.visit(m_plugin_array);
|
||||
visitor.visit(m_clipboard);
|
||||
}
|
||||
|
||||
JS::NonnullGCPtr<MimeTypeArray> Navigator::mime_types()
|
||||
|
@ -73,4 +75,11 @@ JS::NonnullGCPtr<PluginArray> Navigator::plugins()
|
|||
return *m_plugin_array;
|
||||
}
|
||||
|
||||
JS::NonnullGCPtr<Clipboard::Clipboard> Navigator::clipboard()
|
||||
{
|
||||
if (!m_clipboard)
|
||||
m_clipboard = heap().allocate<Clipboard::Clipboard>(realm(), realm());
|
||||
return *m_clipboard;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ public:
|
|||
|
||||
[[nodiscard]] JS::NonnullGCPtr<MimeTypeArray> mime_types();
|
||||
[[nodiscard]] JS::NonnullGCPtr<PluginArray> plugins();
|
||||
[[nodiscard]] JS::NonnullGCPtr<Clipboard::Clipboard> clipboard();
|
||||
|
||||
virtual ~Navigator() override;
|
||||
|
||||
|
@ -58,6 +59,9 @@ private:
|
|||
|
||||
JS::GCPtr<PluginArray> m_plugin_array;
|
||||
JS::GCPtr<MimeTypeArray> m_mime_type_array;
|
||||
|
||||
// https://w3c.github.io/clipboard-apis/#dom-navigator-clipboard
|
||||
JS::GCPtr<Clipboard::Clipboard> m_clipboard;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#import <Clipboard/Clipboard.idl>
|
||||
#import <HTML/MimeTypeArray.idl>
|
||||
#import <HTML/NavigatorBeacon.idl>
|
||||
#import <HTML/NavigatorID.idl>
|
||||
|
@ -10,6 +11,9 @@
|
|||
[Exposed=Window]
|
||||
interface Navigator {
|
||||
// objects implementing this interface also implement the interfaces given below
|
||||
|
||||
// https://w3c.github.io/clipboard-apis/#navigator-interface
|
||||
[SecureContext, SameObject] readonly attribute Clipboard clipboard;
|
||||
};
|
||||
|
||||
// NOTE: As NavigatorContentUtils, NavigatorCookies, NavigatorPlugins, and NavigatorAutomationInformation
|
||||
|
|
|
@ -258,6 +258,8 @@ public:
|
|||
|
||||
virtual void page_did_change_theme_color(Gfx::Color) { }
|
||||
|
||||
virtual void page_did_insert_clipboard_entry([[maybe_unused]] String data, [[maybe_unused]] String presentation_style, [[maybe_unused]] String mime_type) { }
|
||||
|
||||
protected:
|
||||
virtual ~PageClient() = default;
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@ libweb_js_bindings(Animations/Animation)
|
|||
libweb_js_bindings(Animations/AnimationEffect)
|
||||
libweb_js_bindings(Animations/AnimationTimeline)
|
||||
libweb_js_bindings(Animations/DocumentTimeline)
|
||||
libweb_js_bindings(Clipboard/Clipboard)
|
||||
libweb_js_bindings(Crypto/Crypto)
|
||||
libweb_js_bindings(Crypto/SubtleCrypto)
|
||||
libweb_js_bindings(CSS/CSSConditionRule)
|
||||
|
|
Loading…
Reference in a new issue