From ca478ab1e1c2149e7573e0b35070efea883237c5 Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Mon, 29 Jan 2024 15:08:43 +0330 Subject: [PATCH] feat(butil): add Clipboard APIs to Butil #6745 (#6747) --- src/Butil/Bit.Butil/BitButil.cs | 1 + src/Butil/Bit.Butil/Publics/Clipboard.cs | 58 +++++++++++++ .../Publics/Clipboard/ClipboardFormats.cs | 6 ++ .../Publics/Clipboard/ClipboardItem.cs | 8 ++ src/Butil/Bit.Butil/Scripts/clipboard.ts | 41 +++++++++ src/Butil/Bit.Butil/Scripts/crypto.ts | 18 ++-- src/Butil/Bit.Butil/Scripts/utils.ts | 11 +++ .../Pages/ClipboardPage.razor | 85 +++++++++++++++++++ .../Bit.Butil.Demo.Core/Shared/Header.razor | 3 +- 9 files changed, 219 insertions(+), 12 deletions(-) create mode 100644 src/Butil/Bit.Butil/Publics/Clipboard.cs create mode 100644 src/Butil/Bit.Butil/Publics/Clipboard/ClipboardFormats.cs create mode 100644 src/Butil/Bit.Butil/Publics/Clipboard/ClipboardItem.cs create mode 100644 src/Butil/Bit.Butil/Scripts/clipboard.ts create mode 100644 src/Butil/Bit.Butil/Scripts/utils.ts create mode 100644 src/Butil/Demo/Bit.Butil.Demo.Core/Pages/ClipboardPage.razor diff --git a/src/Butil/Bit.Butil/BitButil.cs b/src/Butil/Bit.Butil/BitButil.cs index 9abe1f0b4b..735263a3c5 100644 --- a/src/Butil/Bit.Butil/BitButil.cs +++ b/src/Butil/Bit.Butil/BitButil.cs @@ -18,6 +18,7 @@ public static IServiceCollection AddBitButilServices(this IServiceCollection ser services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); return services; } diff --git a/src/Butil/Bit.Butil/Publics/Clipboard.cs b/src/Butil/Bit.Butil/Publics/Clipboard.cs new file mode 100644 index 0000000000..50235a9afb --- /dev/null +++ b/src/Butil/Bit.Butil/Publics/Clipboard.cs @@ -0,0 +1,58 @@ +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Microsoft.JSInterop; +using static System.Net.Mime.MediaTypeNames; + +namespace Bit.Butil; + +public class Clipboard(IJSRuntime js) +{ + /// + /// Requests text from the system clipboard, returning a Promise that + /// is fulfilled with a string containing the clipboard's text once it's available. + ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText + ///
+ public async ValueTask ReadText() + => await js.InvokeAsync("BitButil.clipboard.readText"); + + /// + /// Writes text to the system clipboard, returning a Promise that is + /// resolved once the text is fully copied into the clipboard. + ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText + ///
+ public async ValueTask WriteText(string text) + { + if (text is not null) + { + await js.InvokeVoidAsync("BitButil.clipboard.writeText", text); + } + } + + /// + /// Requests arbitrary data (such as images) from the clipboard, returning a Promise that + /// resolves with an array of ClipboardItem objects containing the clipboard's contents. + ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/read + ///
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ClipboardItem))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ClipboardFormats))] + public async ValueTask Read(ClipboardFormats? formats = null) + => await (formats is null ? js.InvokeAsync("BitButil.clipboard.read") + : js.InvokeAsync("BitButil.clipboard.read", formats)); + + /// + /// Writes arbitrary data to the system clipboard, returning a Promise + /// that resolves when the operation completes. + ///
+ /// https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/write + ///
+ public async ValueTask Write(ClipboardItem[] items) + { + if (items is not null) + { + await js.InvokeVoidAsync("BitButil.clipboard.write", (object)items); + } + } +} diff --git a/src/Butil/Bit.Butil/Publics/Clipboard/ClipboardFormats.cs b/src/Butil/Bit.Butil/Publics/Clipboard/ClipboardFormats.cs new file mode 100644 index 0000000000..8b881706bb --- /dev/null +++ b/src/Butil/Bit.Butil/Publics/Clipboard/ClipboardFormats.cs @@ -0,0 +1,6 @@ +namespace Bit.Butil; + +public class ClipboardFormats +{ + public string[] Unsanitized { get; set; } = []; +} diff --git a/src/Butil/Bit.Butil/Publics/Clipboard/ClipboardItem.cs b/src/Butil/Bit.Butil/Publics/Clipboard/ClipboardItem.cs new file mode 100644 index 0000000000..3e1a29b109 --- /dev/null +++ b/src/Butil/Bit.Butil/Publics/Clipboard/ClipboardItem.cs @@ -0,0 +1,8 @@ +namespace Bit.Butil; + +public class ClipboardItem +{ + public string MimeType { get; set; } = default!; + + public byte[] Data { get; set; } = default!; +} diff --git a/src/Butil/Bit.Butil/Scripts/clipboard.ts b/src/Butil/Bit.Butil/Scripts/clipboard.ts new file mode 100644 index 0000000000..f64941ae3e --- /dev/null +++ b/src/Butil/Bit.Butil/Scripts/clipboard.ts @@ -0,0 +1,41 @@ +var BitButil = BitButil || {}; + +(function (butil: any) { + butil.clipboard = { + readText, + writeText, + read, + write + }; + + async function readText() { + return await window.navigator.clipboard.readText(); + } + + async function writeText(text: string) { + return await window.navigator.clipboard.writeText(text); + } + + async function read(formats) { + const clipboardItems = await (navigator.clipboard as any).read(formats); + const result = []; + for (const item of clipboardItems) { + for (const mimeType of item.types) { + const blob = await item.getType(mimeType); + const buffer = await blob.arrayBuffer(); + result.push({ mimeType: mimeType, data: new Uint8Array(buffer) }); + } + } + return result; + } + + async function write(items) { + const clipboardItems = []; + for (const item of items) { + const type = item.mimeType; + const blob = new Blob([butil.utils.arrayToBuffer(item.data)], { type }); + clipboardItems.push(new ClipboardItem({ [type]: blob })); + } + await navigator.clipboard.write(clipboardItems); + } +}(BitButil)); \ No newline at end of file diff --git a/src/Butil/Bit.Butil/Scripts/crypto.ts b/src/Butil/Bit.Butil/Scripts/crypto.ts index e3899590b9..a6828f2811 100644 --- a/src/Butil/Bit.Butil/Scripts/crypto.ts +++ b/src/Butil/Bit.Butil/Scripts/crypto.ts @@ -19,7 +19,7 @@ var BitButil = BitButil || {}; async function endecryptRsaOaep(algorithm, key, data, keyHash, func) { const cryptoAlgorithm = { name: algorithm.name, - label: arrayToBuffer(algorithm.label) + label: butil.utils.arrayToBuffer(algorithm.label) } const keyAlgorithm = { name: "RSA-OAEP", hash: keyHash ?? "SHA-256" }; @@ -30,7 +30,7 @@ var BitButil = BitButil || {}; async function endecryptAesCtr(algorithm, key, data, func) { const cryptoAlgorithm = { name: algorithm.name, - counter: arrayToBuffer(algorithm.counter), + counter: butil.utils.arrayToBuffer(algorithm.counter), length: algorithm.length } @@ -42,7 +42,7 @@ var BitButil = BitButil || {}; async function endecryptAesCbc(algorithm, key, data, func) { const cryptoAlgorithm = { name: algorithm.name, - iv: arrayToBuffer(algorithm.iv), + iv: butil.utils.arrayToBuffer(algorithm.iv), } const keyAlgorithm = { name: "AES-CBC" }; @@ -53,8 +53,8 @@ var BitButil = BitButil || {}; async function endecryptAesGcm(algorithm, key, data, func) { const cryptoAlgorithm = { name: algorithm.name, - iv: arrayToBuffer(algorithm.iv), - additionalData: arrayToBuffer(algorithm.additionalData), + iv: butil.utils.arrayToBuffer(algorithm.iv), + additionalData: butil.utils.arrayToBuffer(algorithm.additionalData), tagLength: algorithm.tagLength, } @@ -64,14 +64,10 @@ var BitButil = BitButil || {}; } async function endecrypt(cryptoAlgorithm, key, data, keyAlgorithm, func) { - const cryptoKey = await crypto.subtle.importKey("raw", arrayToBuffer(key), keyAlgorithm, false, ["encrypt", "decrypt"]); + const cryptoKey = await crypto.subtle.importKey("raw", butil.utils.arrayToBuffer(key), keyAlgorithm, false, ["encrypt", "decrypt"]); - const encryptedBuffer = await window.crypto.subtle[func](cryptoAlgorithm, cryptoKey, arrayToBuffer(data)); + const encryptedBuffer = await window.crypto.subtle[func](cryptoAlgorithm, cryptoKey, butil.utils.arrayToBuffer(data)); return new Uint8Array(encryptedBuffer); } - - function arrayToBuffer(array: Uint8Array): ArrayBuffer { - return array?.buffer.slice(array.byteOffset, array.byteLength + array.byteOffset) - } }(BitButil)); \ No newline at end of file diff --git a/src/Butil/Bit.Butil/Scripts/utils.ts b/src/Butil/Bit.Butil/Scripts/utils.ts new file mode 100644 index 0000000000..26f38ab794 --- /dev/null +++ b/src/Butil/Bit.Butil/Scripts/utils.ts @@ -0,0 +1,11 @@ +var BitButil = BitButil || {}; + +(function (butil: any) { + butil.utils = { + arrayToBuffer + }; + + function arrayToBuffer(array: Uint8Array) { + return array?.buffer.slice(array.byteOffset, array.byteLength + array.byteOffset) + } +}(BitButil)); \ No newline at end of file diff --git a/src/Butil/Demo/Bit.Butil.Demo.Core/Pages/ClipboardPage.razor b/src/Butil/Demo/Bit.Butil.Demo.Core/Pages/ClipboardPage.razor new file mode 100644 index 0000000000..9722d112b4 --- /dev/null +++ b/src/Butil/Demo/Bit.Butil.Demo.Core/Pages/ClipboardPage.razor @@ -0,0 +1,85 @@ +@page "/clipboard" +@inject Bit.Butil.Console console +@inject Bit.Butil.Clipboard clipboard + +Clipboard Samples + +

Clipboard

+ +
+@@inject Bit.Butil.Clipboard clipboard
+
+@@code {
+    ...
+    await clipboard.WriteText("new clipboard text");
+    ...
+}
+
+ +
+
+ +

Open the DevTools console and start clicking on buttons

+ +
+
+ + + +
+
+
+ + +
+
+ + +
+
+
+ + + +
+
+
+ + +
+
+ + +@code { + private string newClipText; + private string newText; + + private async Task ReadText() + { + var text = await clipboard.ReadText(); + await console.Log("Clipboard.ReadText =", $"\"{text}\""); + } + + private async Task WriteText() + { + await clipboard.WriteText(newClipText ?? string.Empty); + await console.Log("Clipboard.WriteText =", $"\"{newClipText}\""); + } + + private async Task Read() + { + var items = await clipboard.Read(); + foreach (var item in items) + { + await console.Log("Clipboard.Read=", $"\"{item.MimeType}\",", System.Text.Encoding.UTF8.GetString(item.Data)); + } + } + + public async Task Write() + { + var data = System.Text.Encoding.UTF8.GetBytes(newText); + var item = new ClipboardItem() { MimeType = "text/plain", Data = data }; + await clipboard.Write([item]); + await console.Log("Clipboard.Write=", $"\"{item.MimeType}\",", System.Text.Encoding.UTF8.GetString(item.Data)); + } +} \ No newline at end of file diff --git a/src/Butil/Demo/Bit.Butil.Demo.Core/Shared/Header.razor b/src/Butil/Demo/Bit.Butil.Demo.Core/Shared/Header.razor index d09c247b29..fbe825bd5d 100644 --- a/src/Butil/Demo/Bit.Butil.Demo.Core/Shared/Header.razor +++ b/src/Butil/Demo/Bit.Butil.Demo.Core/Shared/Header.razor @@ -11,6 +11,7 @@ Location | Screen | Cookie | - Crypto + Crypto | + Clipboard
\ No newline at end of file