Skip to content

Commit

Permalink
feat(butil): add Clipboard APIs to Butil #6745 (#6747)
Browse files Browse the repository at this point in the history
  • Loading branch information
msynk authored Jan 29, 2024
1 parent 93050ba commit ca478ab
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 12 deletions.
1 change: 1 addition & 0 deletions src/Butil/Bit.Butil/BitButil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public static IServiceCollection AddBitButilServices(this IServiceCollection ser
services.AddTransient<Screen>();
services.AddTransient<Cookie>();
services.AddTransient<Crypto>();
services.AddTransient<Clipboard>();

return services;
}
Expand Down
58 changes: 58 additions & 0 deletions src/Butil/Bit.Butil/Publics/Clipboard.cs
Original file line number Diff line number Diff line change
@@ -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)
{
/// <summary>
/// Requests text from the system clipboard, returning a Promise that
/// is fulfilled with a string containing the clipboard's text once it's available.
/// <br/>
/// <see href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText">https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText</see>
/// </summary>
public async ValueTask<string> ReadText()
=> await js.InvokeAsync<string>("BitButil.clipboard.readText");

/// <summary>
/// Writes text to the system clipboard, returning a Promise that is
/// resolved once the text is fully copied into the clipboard.
/// <br/>
/// <see href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText">https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText</see>
/// </summary>
public async ValueTask WriteText(string text)
{
if (text is not null)
{
await js.InvokeVoidAsync("BitButil.clipboard.writeText", text);
}
}

/// <summary>
/// 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.
/// <br/>
/// <see href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/read">https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/read</see>
/// </summary>
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ClipboardItem))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ClipboardFormats))]
public async ValueTask<ClipboardItem[]> Read(ClipboardFormats? formats = null)
=> await (formats is null ? js.InvokeAsync<ClipboardItem[]>("BitButil.clipboard.read")
: js.InvokeAsync<ClipboardItem[]>("BitButil.clipboard.read", formats));

/// <summary>
/// Writes arbitrary data to the system clipboard, returning a Promise
/// that resolves when the operation completes.
/// <br/>
/// <see href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/write">https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/write</see>
/// </summary>
public async ValueTask Write(ClipboardItem[] items)
{
if (items is not null)
{
await js.InvokeVoidAsync("BitButil.clipboard.write", (object)items);
}
}
}
6 changes: 6 additions & 0 deletions src/Butil/Bit.Butil/Publics/Clipboard/ClipboardFormats.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Bit.Butil;

public class ClipboardFormats
{
public string[] Unsanitized { get; set; } = [];
}
8 changes: 8 additions & 0 deletions src/Butil/Bit.Butil/Publics/Clipboard/ClipboardItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Bit.Butil;

public class ClipboardItem
{
public string MimeType { get; set; } = default!;

public byte[] Data { get; set; } = default!;
}
41 changes: 41 additions & 0 deletions src/Butil/Bit.Butil/Scripts/clipboard.ts
Original file line number Diff line number Diff line change
@@ -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));
18 changes: 7 additions & 11 deletions src/Butil/Bit.Butil/Scripts/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" };
Expand All @@ -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
}

Expand All @@ -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" };
Expand All @@ -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,
}

Expand All @@ -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));
11 changes: 11 additions & 0 deletions src/Butil/Bit.Butil/Scripts/utils.ts
Original file line number Diff line number Diff line change
@@ -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));
85 changes: 85 additions & 0 deletions src/Butil/Demo/Bit.Butil.Demo.Core/Pages/ClipboardPage.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
@page "/clipboard"
@inject Bit.Butil.Console console
@inject Bit.Butil.Clipboard clipboard

<PageTitle>Clipboard Samples</PageTitle>

<h1>Clipboard</h1>

<pre style="font-family:Consolas">
@@inject Bit.Butil.Clipboard clipboard

@@code {
...
await clipboard.WriteText("new clipboard text");
...
}
</pre>

<br />
<hr />

<h3>Open the DevTools console and start clicking on buttons</h3>

<hr />
<br />

<button @onclick="ReadText">Read text</button>

<br />
<hr />
<br />

<input @bind=@newClipText />
<br />
<br />
<button @onclick="WriteText">Write text</button>

<br />
<hr />
<br />

<button @onclick="Read">Read</button>

<br />
<hr />
<br />

<input @bind=@newText />
<br />
<br />
<button @onclick="Write">Write</button>

@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));
}
}
3 changes: 2 additions & 1 deletion src/Butil/Demo/Bit.Butil.Demo.Core/Shared/Header.razor
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<a href="/location">Location</a> |
<a href="/screen">Screen</a> |
<a href="/cookie">Cookie</a> |
<a href="/crypto">Crypto</a>
<a href="/crypto">Crypto</a> |
<a href="/clipboard">Clipboard</a>
</div>
<hr />

0 comments on commit ca478ab

Please sign in to comment.