# jg-clipboard

> Copy and read text, HTML, and images on the browser clipboard, with support detection and coded errors that say why the browser refused.

- **Version:** `0.2.0`
- **Runtimes:** Browser
- **Install:** `npm install jg-clipboard`
- **On npm:** [jg-clipboard](https://www.npmjs.com/package/jg-clipboard)
- **Source:** [packages/jg-clipboard](https://github.com/jesusgraterol/jg-packages/tree/main/packages/jg-clipboard)
- **Recipes:** [jg-clipboard recipes](https://jg-packages.jesusgraterol.dev/packages/jg-clipboard/recipes/index.md)

## Usage

Copy from a click or key press handler. Each copy calls the clipboard before its first `await`, so the browser still sees the gesture:

```ts
import { copyText, ERRORS } from 'jg-clipboard';
import { hasErrorCode } from 'jg-exception';

copyButton.addEventListener('click', async () => {
  try {
    await copyText('npm install jg-clipboard');
    showToast('Copied');
  } catch (error) {
    if (hasErrorCode(error, ERRORS.CLIPBOARD_PERMISSION_DENIED)) {
      showToast('Allow clipboard access to copy');
    }
  }
});
```

Content that loads after the click can be a promise. It goes through a `ClipboardItem` that waits for it, which Safari needs:

```ts
shareButton.addEventListener('click', () => {
  void copyText(fetch('/share-link').then((response) => response.text()));
});
```

Copy HTML with its plain text, and images. A PNG is copied as it is, and other types the browser can decode, such as JPEG or WebP, are redrawn as PNG, the only image type browsers accept on the clipboard:

```ts
import { copyHtml, copyImage } from 'jg-clipboard';

await copyHtml('<strong>Order #42</strong> shipped', { text: 'Order #42 shipped' });
await copyImage(fetch('/chart.jpg').then((response) => response.blob()));
```

Read from a paste button. `readClipboard` reads the text, HTML, and image with one call, so the person pasting sees one prompt at most. An image the browser can't read comes back as `null` when text or HTML was copied with it, so the text still pastes:

```ts
import { readClipboard, readText } from 'jg-clipboard';

pasteButton.addEventListener('click', async () => {
  const { text, html, image } = await readClipboard(); // each null when the clipboard lacks it
  input.value = text ?? (await readText());
});
```

Show only what the browser supports. `getClipboardSupport` never throws, and reports everything as `false` during server rendering:

```ts
import { getClipboardSupport } from 'jg-clipboard';

const support = getClipboardSupport(); // { isSecureContext, copyText, copyHtml, copyImage, readText, read }
copyImageButton.hidden = !support.copyImage;
```

### Errors

Every failure is an `Exception` with one of these codes, and the browser's error as its `cause`:

| Code                          | When                                                                                                                                               |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CLIPBOARD_UNSUPPORTED`       | The page isn't a secure context, such as HTTPS or localhost, or the browser lacks the API                                                          |
| `CLIPBOARD_NOT_FOCUSED`       | The browser refused because the page doesn't have focus                                                                                            |
| `CLIPBOARD_NO_USER_GESTURE`   | The browser refused because the call didn't follow a click or a key press                                                                          |
| `CLIPBOARD_PERMISSION_DENIED` | The browser refused for any other reason, such as a denied or dismissed permission; `data.permission` holds the Permissions API's state, or `null` |
| `CLIPBOARD_INVALID_IMAGE`     | The image can't be decoded as PNG: an image to copy, or one `readClipboard` read without text or HTML beside it                                    |
| `CLIPBOARD_WRITE_FAILED`      | A copy failed otherwise, such as content whose promise rejected                                                                                    |
| `CLIPBOARD_READ_FAILED`       | A read failed otherwise                                                                                                                            |

### Browser limits

- The clipboard needs a secure context: HTTPS, or `localhost` while developing.
- Copy and read from a click or key press handler. Safari refuses calls that don't follow one directly, and Chromium needs the page to have focus.
- Reading asks first. Safari and Firefox show a paste prompt each time, and Chromium asks once for the clipboard-read permission.
- Images go on the clipboard as PNG only. Converting another type needs `createImageBitmap`, so it works in every current browser, but not in very old ones.
- Without `ClipboardItem`, a promised text loads before the copy starts, and Safari may then refuse it.

## <a id="api-reference"></a>jg-clipboard API reference

Every export of `jg-clipboard@0.2.0`, 10 in all, with its signature, its errors, and an example. It's generated from the package's TSDoc.

- Functions: [`copyHtml`](#api-copyHtml), [`copyImage`](#api-copyImage), [`copyText`](#api-copyText), [`getClipboardSupport`](#api-getClipboardSupport), [`readClipboard`](#api-readClipboard), [`readText`](#api-readText)
- Constants: [`ERRORS`](#api-ERRORS)
- Types: [`IClipboardContent`](#api-IClipboardContent), [`IClipboardSupport`](#api-IClipboardSupport), [`ICopyHtmlOptions`](#api-ICopyHtmlOptions)

### <a id="api-functions"></a>Functions

#### <a id="api-copyHtml"></a>`copyHtml`

*Function*

Copies HTML to the clipboard, with plain text for places that can't paste HTML: `options.text`, or else the HTML's text content. Call it from a click or key press handler; like copyText, it calls the clipboard before its first await, and either value can be a promise.

```ts
copyHtml(
  html: string | Promise<string>,
  options?: ICopyHtmlOptions,
): Promise<void>
```

**Parameters**

- `html` `string | Promise<string>`: The HTML, or a promise of it.
- `options?` `ICopyHtmlOptions`: The plain text that goes with it. Defaults to `{}`.

**Returns** `Promise<void>`: A promise that resolves once the HTML is on the clipboard.

**Throws**

- `CLIPBOARD_UNSUPPORTED`: The clipboard can't copy HTML here.
- `CLIPBOARD_NOT_FOCUSED`: The page must have focus to use the clipboard.
- `CLIPBOARD_NO_USER_GESTURE`: The browser only allows this right after a click or a key press.
- `CLIPBOARD_PERMISSION_DENIED`: The permission to use the clipboard was denied.
- `CLIPBOARD_WRITE_FAILED`: The content couldn't be copied to the clipboard.

**Example**

```ts
import { copyHtml } from 'jg-clipboard';

button.addEventListener('click', () => {
  void copyHtml('<strong>Order #42</strong> shipped', { text: 'Order #42 shipped' });
});
```

#### <a id="api-copyImage"></a>`copyImage`

*Function*

Copies an image to the clipboard as PNG, the only image type browsers accept there. A PNG is written as it is, and other types the browser can decode, such as JPEG or WebP, are redrawn as PNG. Call it from a click or key press handler; it calls the clipboard before its first await, and the image can be a promise, such as a fetched Blob.

```ts
copyImage(image: Blob | Promise<Blob>): Promise<void>
```

**Parameters**

- `image` `Blob | Promise<Blob>`: The image, or a promise of it.

**Returns** `Promise<void>`: A promise that resolves once the image is on the clipboard.

**Throws**

- `CLIPBOARD_UNSUPPORTED`: The clipboard can't copy images here.
- `CLIPBOARD_INVALID_IMAGE`: The image couldn't be decoded, so it can't be copied as PNG.
- `CLIPBOARD_NOT_FOCUSED`: The page must have focus to use the clipboard.
- `CLIPBOARD_NO_USER_GESTURE`: The browser only allows this right after a click or a key press.
- `CLIPBOARD_PERMISSION_DENIED`: The permission to use the clipboard was denied.
- `CLIPBOARD_WRITE_FAILED`: The content couldn't be copied to the clipboard.

**Example**

```ts
import { copyImage } from 'jg-clipboard';

button.addEventListener('click', () => {
  void copyImage(fetch('/chart.png').then((response) => response.blob()));
});
```

#### <a id="api-copyText"></a>`copyText`

*Function*

Copies text to the clipboard. Call it from a click or key press handler. It calls the clipboard before its first await, so the browser still sees that gesture. Text that loads after the click, such as from a request, can be a promise: it goes through a ClipboardItem that waits for it, which Safari needs. Browsers without ClipboardItem wait for the promise first, and may then refuse the copy.

```ts
copyText(text: string | Promise<string>): Promise<void>
```

**Parameters**

- `text` `string | Promise<string>`: The text, or a promise of it.

**Returns** `Promise<void>`: A promise that resolves once the text is on the clipboard.

**Throws**

- `CLIPBOARD_UNSUPPORTED`: The clipboard can't copy text here.
- `CLIPBOARD_NOT_FOCUSED`: The page must have focus to use the clipboard.
- `CLIPBOARD_NO_USER_GESTURE`: The browser only allows this right after a click or a key press.
- `CLIPBOARD_PERMISSION_DENIED`: The permission to use the clipboard was denied.
- `CLIPBOARD_WRITE_FAILED`: The content couldn't be copied to the clipboard.

**Example 1**

```ts
import { copyText } from 'jg-clipboard';

button.addEventListener('click', () => {
  void copyText('npm install jg-clipboard');
});
```

**Example 2**

```ts
import { copyText } from 'jg-clipboard';

// the text loads after the click
button.addEventListener('click', () => {
  void copyText(fetch('/share-link').then((response) => response.text()));
});
```

#### <a id="api-getClipboardSupport"></a>`getClipboardSupport`

*Function*

Tells what the browser lets the page do with the clipboard, from feature detection and `ClipboardItem.supports` where the browser has it. It never throws, and it's safe to call during server rendering, where everything is false. A supported feature can still be refused, such as without a click or a permission.

```ts
getClipboardSupport(): IClipboardSupport
```

**Returns** `IClipboardSupport`: Whether the page is a secure context, and whether each function can run.

**Example**

```ts
import { getClipboardSupport } from 'jg-clipboard';

const support = getClipboardSupport();
if (support.copyImage) {
  showCopyImageButton();
}
```

#### <a id="api-readClipboard"></a>`readClipboard`

*Function*

Reads the clipboard's text, HTML, and image with one `read()` call, so the person pasting sees one prompt at most. Call it from a click or key press handler, which Safari requires. An image the browser can't read never hides the text or HTML copied with it, such as the picture of spreadsheet cells that comes with their text: the image is then null.

```ts
readClipboard(): Promise<IClipboardContent>
```

**Returns** `Promise<IClipboardContent>`: A promise that resolves to each kind of content, with null for the kinds the clipboard doesn't hold or the image it can't read. The image is PNG.

**Throws**

- `CLIPBOARD_UNSUPPORTED`: The clipboard can't read here.
- `CLIPBOARD_NOT_FOCUSED`: The page must have focus to use the clipboard.
- `CLIPBOARD_NO_USER_GESTURE`: The browser only allows this right after a click or a key press.
- `CLIPBOARD_PERMISSION_DENIED`: The permission to use the clipboard was denied.
- `CLIPBOARD_INVALID_IMAGE`: The clipboard holds only an image, which couldn't be decoded as PNG.
- `CLIPBOARD_READ_FAILED`: The clipboard couldn't be read, or holds only an image it couldn't read.

**Example**

```ts
import { readClipboard } from 'jg-clipboard';

pasteButton.addEventListener('click', async () => {
  const { html, text, image } = await readClipboard();
  if (image) {
    preview.src = URL.createObjectURL(image);
  }
});
```

#### <a id="api-readText"></a>`readText`

*Function*

Reads the text on the clipboard. Browsers ask before a page reads it: Safari and Firefox show a paste prompt, and Chromium asks for the clipboard-read permission once. Call it from a click or key press handler, which Safari requires.

```ts
readText(): Promise<string>
```

**Returns** `Promise<string>`: A promise that resolves to the text, or `''` when the clipboard holds none.

**Throws**

- `CLIPBOARD_UNSUPPORTED`: The clipboard can't read text here.
- `CLIPBOARD_NOT_FOCUSED`: The page must have focus to use the clipboard.
- `CLIPBOARD_NO_USER_GESTURE`: The browser only allows this right after a click or a key press.
- `CLIPBOARD_PERMISSION_DENIED`: The permission to use the clipboard was denied.
- `CLIPBOARD_READ_FAILED`: The clipboard couldn't be read.

**Example**

```ts
import { readText } from 'jg-clipboard';

pasteButton.addEventListener('click', async () => {
  input.value = await readText();
});
```

### <a id="api-constants"></a>Constants

#### <a id="api-ERRORS"></a>`ERRORS`

*Constant*

the codes for every error the package throws

```ts
const ERRORS: { CLIPBOARD_UNSUPPORTED: "CLIPBOARD_UNSUPPORTED"; CLIPBOARD_PERMISSION_DENIED: "CLIPBOARD_PERMISSION_DENIED"; CLIPBOARD_NO_USER_GESTURE: "CLIPBOARD_NO_USER_GESTURE"; CLIPBOARD_NOT_FOCUSED: "CLIPBOARD_NOT_FOCUSED"; CLIPBOARD_INVALID_IMAGE: "CLIPBOARD_INVALID_IMAGE"; CLIPBOARD_WRITE_FAILED: "CLIPBOARD_WRITE_FAILED"; CLIPBOARD_READ_FAILED: "CLIPBOARD_READ_FAILED" }
```

### <a id="api-types"></a>Types

#### <a id="api-IClipboardContent"></a>`IClipboardContent`

*Type*

what readClipboard found, with null for each kind of content the clipboard doesn't hold

```ts
type IClipboardContent = {
    text: string | null;
    html: string | null;
    image: Blob | null;
};
```

**Members**

- `text: string | null` (property)
- `html: string | null` (property)
- `image: Blob | null` (property): the image, as PNG

#### <a id="api-IClipboardSupport"></a>`IClipboardSupport`

*Type*

what the browser lets the page do with the clipboard, each from feature detection

```ts
type IClipboardSupport = {
    isSecureContext: boolean;
    copyText: boolean;
    copyHtml: boolean;
    copyImage: boolean;
    readText: boolean;
    read: boolean;
};
```

**Members**

- `isSecureContext: boolean` (property): whether the page is in a secure context, such as HTTPS or localhost, which the clipboard needs
- `copyText: boolean` (property): whether copyText can run
- `copyHtml: boolean` (property): whether copyHtml can run
- `copyImage: boolean` (property): whether copyImage can run
- `readText: boolean` (property): whether readText can run
- `read: boolean` (property): whether readClipboard can run

#### <a id="api-ICopyHtmlOptions"></a>`ICopyHtmlOptions`

*Type*

what copyHtml accepts besides the HTML

```ts
type ICopyHtmlOptions = {
    text?: string | Promise<string>;
};
```

**Members**

- `text: string | Promise<string>` (property): the plain text pasted where HTML can't go. Defaults to the HTML's text content
