jg-clipboard 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/API.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # <a id="api-reference"></a>jg-clipboard API reference
2
2
 
3
- Every export of `jg-clipboard@0.1.0`, 10 in all, with its signature, its errors, and an example. It's generated from the package's TSDoc.
3
+ 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.
4
4
 
5
5
  - Functions: [`copyHtml`](#api-copyHtml), [`copyImage`](#api-copyImage), [`copyText`](#api-copyText), [`getClipboardSupport`](#api-getClipboardSupport), [`readClipboard`](#api-readClipboard), [`readText`](#api-readText)
6
6
  - Constants: [`ERRORS`](#api-ERRORS)
@@ -153,13 +153,13 @@ if (support.copyImage) {
153
153
 
154
154
  *Function*
155
155
 
156
- 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.
156
+ 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.
157
157
 
158
158
  ```ts
159
159
  readClipboard(): Promise<IClipboardContent>
160
160
  ```
161
161
 
162
- **Returns** `Promise<IClipboardContent>`: A promise that resolves to each kind of content, with null for the kinds the clipboard doesn't hold. The image is PNG.
162
+ **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.
163
163
 
164
164
  **Throws**
165
165
 
@@ -167,8 +167,8 @@ readClipboard(): Promise<IClipboardContent>
167
167
  - `CLIPBOARD_NOT_FOCUSED`: The page must have focus to use the clipboard.
168
168
  - `CLIPBOARD_NO_USER_GESTURE`: The browser only allows this right after a click or a key press.
169
169
  - `CLIPBOARD_PERMISSION_DENIED`: The permission to use the clipboard was denied.
170
- - `CLIPBOARD_INVALID_IMAGE`: The image couldn't be decoded, so it can't be copied as PNG.
171
- - `CLIPBOARD_READ_FAILED`: The clipboard couldn't be read.
170
+ - `CLIPBOARD_INVALID_IMAGE`: The clipboard holds only an image, which couldn't be decoded as PNG.
171
+ - `CLIPBOARD_READ_FAILED`: The clipboard couldn't be read, or holds only an image it couldn't read.
172
172
 
173
173
  **Example**
174
174
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Changed
6
+
7
+ - `readClipboard` returns the text and HTML when the clipboard's image can't be read, with `image: null`, instead of failing as a whole. It still throws `CLIPBOARD_INVALID_IMAGE`, or `CLIPBOARD_READ_FAILED`, when the image is all the clipboard holds.
8
+
9
+ ## 0.1.1
10
+
11
+ ### Added
12
+
13
+ - `RECIPES.md`, patterns for apps that use the package: a copy button that explains a refusal, and a paste button for text or an image. Each block is typechecked against the release.
14
+
3
15
  ## 0.1.0
4
16
 
5
17
  The first release.
package/README.md CHANGED
@@ -45,7 +45,7 @@ await copyHtml('<strong>Order #42</strong> shipped', { text: 'Order #42 shipped'
45
45
  await copyImage(fetch('/chart.jpg').then((response) => response.blob()));
46
46
  ```
47
47
 
48
- Read from a paste button. `readClipboard` reads the text, HTML, and image with one call, so the person pasting sees one prompt at most:
48
+ 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:
49
49
 
50
50
  ```ts
51
51
  import { readClipboard, readText } from 'jg-clipboard';
@@ -75,7 +75,7 @@ Every failure is an `Exception` with one of these codes, and the browser's error
75
75
  | `CLIPBOARD_NOT_FOCUSED` | The browser refused because the page doesn't have focus |
76
76
  | `CLIPBOARD_NO_USER_GESTURE` | The browser refused because the call didn't follow a click or a key press |
77
77
  | `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` |
78
- | `CLIPBOARD_INVALID_IMAGE` | The image can't be decoded, so it can't be copied as PNG |
78
+ | `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 |
79
79
  | `CLIPBOARD_WRITE_FAILED` | A copy failed otherwise, such as content whose promise rejected |
80
80
  | `CLIPBOARD_READ_FAILED` | A read failed otherwise |
81
81
 
@@ -91,6 +91,8 @@ Every failure is an `Exception` with one of these codes, and the browser's error
91
91
 
92
92
  The full API reference ships in the package as `API.md`, generated from its TSDoc for the installed version, and is at https://jg-packages.jesusgraterol.dev/packages/jg-clipboard/.
93
93
 
94
+ Recipes for common jobs ship as `RECIPES.md`, with every block typechecked against the release, and are at https://jg-packages.jesusgraterol.dev/packages/jg-clipboard/recipes/.
95
+
94
96
  ## License
95
97
 
96
98
  [MIT](https://github.com/jesusgraterol/jg-packages/blob/main/packages/jg-clipboard/LICENSE)
package/RECIPES.md ADDED
@@ -0,0 +1,107 @@
1
+ # jg-clipboard recipes
2
+
3
+ Patterns for copy and paste buttons that work across browsers, and say what to do when the browser refuses.
4
+
5
+ ## A copy button that explains a refusal
6
+
7
+ Use this for a button that copies a code snippet, a share link, or a key. Browsers refuse a copy for several reasons, and jg-clipboard names each one with a code, so the message can tell the person what to do next, and how to get the text another way:
8
+
9
+ ```ts
10
+ import { copyText, ERRORS } from 'jg-clipboard';
11
+ import { hasErrorCode } from 'jg-exception';
12
+
13
+ // the element whose text the button copies
14
+ declare const snippet: HTMLElement;
15
+
16
+ // a status line, such as an element with role="status", so screen readers announce the outcome
17
+ declare const copyStatus: HTMLElement;
18
+
19
+ // says what to do after a copy failed, so the text can still be copied by hand
20
+ const describeCopyFailure = (error: unknown): string => {
21
+ if (hasErrorCode(error, ERRORS.CLIPBOARD_PERMISSION_DENIED)) {
22
+ return 'Your browser blocked copying. Select the text and copy it manually.';
23
+ }
24
+ if (hasErrorCode(error, ERRORS.CLIPBOARD_NO_USER_GESTURE)) {
25
+ return 'Your browser only copies right after a click. Click Copy again.';
26
+ }
27
+ if (hasErrorCode(error, ERRORS.CLIPBOARD_NOT_FOCUSED)) {
28
+ return 'Copying needs this tab to be in focus. Click the page, then click Copy again.';
29
+ }
30
+ if (hasErrorCode(error, ERRORS.CLIPBOARD_UNSUPPORTED)) {
31
+ return "This browser can't copy from this page. Select the text and copy it manually.";
32
+ }
33
+ return 'Copying failed. Select the text and copy it manually.';
34
+ };
35
+
36
+ button.addEventListener('click', async () => {
37
+ // cleared first, so an earlier outcome never shows during this attempt
38
+ copyStatus.textContent = '';
39
+ try {
40
+ // nothing awaits before copyText, so the browser still sees the click
41
+ await copyText(snippet.textContent ?? '');
42
+ copyStatus.textContent = 'Copied';
43
+ } catch (error) {
44
+ copyStatus.textContent = describeCopyFailure(error);
45
+ }
46
+ });
47
+ ```
48
+
49
+ `CLIPBOARD_WRITE_FAILED`, and anything unexpected, gets the general message. A `CLIPBOARD_PERMISSION_DENIED` error's `data.permission` holds the Permissions API's state, or `null` where the browser can't say; `'denied'` means the site's settings block the clipboard, which no second click fixes. To hide a button that can never work, check `getClipboardSupport().copyText` first, and keep the messages, because a supported copy can still be refused.
50
+
51
+ ## A paste button for text or an image
52
+
53
+ Use this for a paste button in a composer that takes a screenshot or text. `readClipboard` reads every kind of content with one call, so the person sees one paste prompt at most, and returns an image as PNG, whatever type it was copied as. A browser that reads only text gets `readText` instead:
54
+
55
+ ```ts
56
+ import { ERRORS, getClipboardSupport, readClipboard, readText } from 'jg-clipboard';
57
+ import { hasErrorCode } from 'jg-exception';
58
+
59
+ // a status line that says why a paste failed
60
+ declare const pasteStatus: HTMLElement;
61
+
62
+ const support = getClipboardSupport();
63
+
64
+ // a browser that can't read the clipboard gets no button, and the keyboard still pastes there
65
+ pasteButton.hidden = !support.read && !support.readText;
66
+
67
+ // shows a pasted image, and releases the previous one, so repeated pastes don't keep it in memory
68
+ const showImage = (image: Blob): void => {
69
+ if (preview.src.startsWith('blob:')) {
70
+ URL.revokeObjectURL(preview.src);
71
+ }
72
+ preview.src = URL.createObjectURL(image);
73
+ };
74
+
75
+ const describePasteFailure = (error: unknown): string => {
76
+ if (hasErrorCode(error, ERRORS.CLIPBOARD_PERMISSION_DENIED)) {
77
+ return 'Your browser blocked pasting. Paste with Ctrl+V, or Cmd+V on a Mac, instead.';
78
+ }
79
+ if (hasErrorCode(error, ERRORS.CLIPBOARD_INVALID_IMAGE)) {
80
+ return "The copied image can't be read. Save it, then attach the file instead.";
81
+ }
82
+ return 'Pasting failed. Paste with Ctrl+V, or Cmd+V on a Mac, instead.';
83
+ };
84
+
85
+ pasteButton.addEventListener('click', async () => {
86
+ pasteStatus.textContent = '';
87
+ try {
88
+ // nothing awaits before the read, which Safari needs
89
+ if (!support.read) {
90
+ input.value = await readText();
91
+ return;
92
+ }
93
+ const { text, image } = await readClipboard();
94
+ if (image !== null) {
95
+ showImage(image);
96
+ } else if (text !== null) {
97
+ input.value = text;
98
+ } else {
99
+ pasteStatus.textContent = 'The clipboard holds no text or image to paste.';
100
+ }
101
+ } catch (error) {
102
+ pasteStatus.textContent = describePasteFailure(error);
103
+ }
104
+ });
105
+ ```
106
+
107
+ Safari and Firefox show a paste prompt on every read, and Chromium asks for the clipboard-read permission once, so read only when the person clicks. Which content wins when the clipboard holds both depends on the app: copied spreadsheet cells can come with a picture of the cells, so a field for tables would check `text` first. `CLIPBOARD_INVALID_IMAGE` means the clipboard held only an image the browser can't decode. When text or HTML came with it, `readClipboard` returns them with `image: null` instead, so the paste falls through to the text.
@@ -20,9 +20,11 @@ import { type IClipboardContent } from '../shared/index.js';
20
20
  export declare const readText: () => Promise<string>;
21
21
  /**
22
22
  * Reads the clipboard's text, HTML, and image with one `read()` call, so the person pasting sees
23
- * one prompt at most. Call it from a click or key press handler, which Safari requires.
23
+ * one prompt at most. Call it from a click or key press handler, which Safari requires. An image
24
+ * the browser can't read never hides the text or HTML copied with it, such as the picture of
25
+ * spreadsheet cells that comes with their text: the image is then null.
24
26
  * @returns A promise that resolves to each kind of content, with null for the kinds the clipboard
25
- * doesn't hold. The image is PNG.
27
+ * doesn't hold or the image it can't read. The image is PNG.
26
28
  * @example
27
29
  * import { readClipboard } from 'jg-clipboard';
28
30
  *
@@ -37,7 +39,7 @@ export declare const readText: () => Promise<string>;
37
39
  * - CLIPBOARD_NOT_FOCUSED: The page must have focus to use the clipboard.
38
40
  * - CLIPBOARD_NO_USER_GESTURE: The browser only allows this right after a click or a key press.
39
41
  * - CLIPBOARD_PERMISSION_DENIED: The permission to use the clipboard was denied.
40
- * - CLIPBOARD_INVALID_IMAGE: The image couldn't be decoded, so it can't be copied as PNG.
41
- * - CLIPBOARD_READ_FAILED: The clipboard couldn't be read.
42
+ * - CLIPBOARD_INVALID_IMAGE: The clipboard holds only an image, which couldn't be decoded as PNG.
43
+ * - CLIPBOARD_READ_FAILED: The clipboard couldn't be read, or holds only an image it couldn't read.
42
44
  */
43
45
  export declare const readClipboard: () => Promise<IClipboardContent>;
package/dist/read/read.js CHANGED
@@ -55,9 +55,11 @@ const readImage = async (items) => {
55
55
  };
56
56
  /**
57
57
  * Reads the clipboard's text, HTML, and image with one `read()` call, so the person pasting sees
58
- * one prompt at most. Call it from a click or key press handler, which Safari requires.
58
+ * one prompt at most. Call it from a click or key press handler, which Safari requires. An image
59
+ * the browser can't read never hides the text or HTML copied with it, such as the picture of
60
+ * spreadsheet cells that comes with their text: the image is then null.
59
61
  * @returns A promise that resolves to each kind of content, with null for the kinds the clipboard
60
- * doesn't hold. The image is PNG.
62
+ * doesn't hold or the image it can't read. The image is PNG.
61
63
  * @example
62
64
  * import { readClipboard } from 'jg-clipboard';
63
65
  *
@@ -72,8 +74,8 @@ const readImage = async (items) => {
72
74
  * - CLIPBOARD_NOT_FOCUSED: The page must have focus to use the clipboard.
73
75
  * - CLIPBOARD_NO_USER_GESTURE: The browser only allows this right after a click or a key press.
74
76
  * - CLIPBOARD_PERMISSION_DENIED: The permission to use the clipboard was denied.
75
- * - CLIPBOARD_INVALID_IMAGE: The image couldn't be decoded, so it can't be copied as PNG.
76
- * - CLIPBOARD_READ_FAILED: The clipboard couldn't be read.
77
+ * - CLIPBOARD_INVALID_IMAGE: The clipboard holds only an image, which couldn't be decoded as PNG.
78
+ * - CLIPBOARD_READ_FAILED: The clipboard couldn't be read, or holds only an image it couldn't read.
77
79
  */
78
80
  export const readClipboard = async () => {
79
81
  const clipboard = getSecureClipboard();
@@ -92,9 +94,16 @@ export const readClipboard = async () => {
92
94
  const [text, html, image] = await Promise.all([
93
95
  readType(items, 'text/plain'),
94
96
  readType(items, 'text/html'),
95
- readImage(items),
97
+ // a failed image is kept as its error, so the text copied with it can still be pasted
98
+ readImage(items).then((png) => ({ png }), (failure) => ({ failure })),
96
99
  ]);
97
- return { text, html, image };
100
+ if (!('failure' in image)) {
101
+ return { text, html, image: image.png };
102
+ }
103
+ if (!text && !html) {
104
+ throw image.failure;
105
+ }
106
+ return { text, html, image: null };
98
107
  }
99
108
  catch (error) {
100
109
  throw isException(error)
@@ -1 +1 @@
1
- {"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/read/read.ts"],"names":[],"mappings":"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","sourcesContent":["import { Exception, isException } from 'jg-exception';\n\nimport { toPng } from '../copy/image.js';\nimport {\n ERRORS,\n getSecureClipboard,\n toClipboardException,\n unsupported,\n type IClipboardContent,\n} from '../shared/index.js';\n\n/**\n * Reads the text on the clipboard. Browsers ask before a page reads it: Safari and Firefox show a\n * paste prompt, and Chromium asks for the clipboard-read permission once. Call it from a click or\n * key press handler, which Safari requires.\n * @returns A promise that resolves to the text, or `''` when the clipboard holds none.\n * @example\n * import { readText } from 'jg-clipboard';\n *\n * pasteButton.addEventListener('click', async () => {\n * input.value = await readText();\n * });\n * @throws\n * - CLIPBOARD_UNSUPPORTED: The clipboard can't read text here.\n * - CLIPBOARD_NOT_FOCUSED: The page must have focus to use the clipboard.\n * - CLIPBOARD_NO_USER_GESTURE: The browser only allows this right after a click or a key press.\n * - CLIPBOARD_PERMISSION_DENIED: The permission to use the clipboard was denied.\n * - CLIPBOARD_READ_FAILED: The clipboard couldn't be read.\n */\nexport const readText = async (): Promise<string> => {\n const clipboard = getSecureClipboard();\n const read = clipboard?.readText;\n if (typeof read !== 'function') {\n throw unsupported('read text');\n }\n try {\n return await read.call(clipboard);\n } catch (error) {\n throw await toClipboardException(error, 'read');\n }\n};\n\n/**\n * Reads one type from the first item that has it.\n * @param items The clipboard's items.\n * @param type The MIME type.\n * @returns A promise that resolves to the item's text, or null when no item has the type.\n */\nconst readType = async (items: ClipboardItems, type: string): Promise<string | null> => {\n const item = items.find(({ types }) => types.includes(type));\n return item === undefined ? null : (await item.getType(type)).text();\n};\n\n/**\n * Reads the first image, preferring PNG, as PNG.\n * @param items The clipboard's items.\n * @returns A promise that resolves to the PNG, or null when no item holds an image.\n */\nconst readImage = async (items: ClipboardItems): Promise<Blob | null> => {\n const types = items.flatMap((item) => item.types.map((type) => ({ item, type })));\n const image =\n types.find(({ type }) => type === 'image/png') ??\n types.find(({ type }) => type.startsWith('image/'));\n return image === undefined ? null : toPng(await image.item.getType(image.type));\n};\n\n/**\n * Reads the clipboard's text, HTML, and image with one `read()` call, so the person pasting sees\n * one prompt at most. Call it from a click or key press handler, which Safari requires.\n * @returns A promise that resolves to each kind of content, with null for the kinds the clipboard\n * doesn't hold. The image is PNG.\n * @example\n * import { readClipboard } from 'jg-clipboard';\n *\n * pasteButton.addEventListener('click', async () => {\n * const { html, text, image } = await readClipboard();\n * if (image) {\n * preview.src = URL.createObjectURL(image);\n * }\n * });\n * @throws\n * - CLIPBOARD_UNSUPPORTED: The clipboard can't read here.\n * - CLIPBOARD_NOT_FOCUSED: The page must have focus to use the clipboard.\n * - CLIPBOARD_NO_USER_GESTURE: The browser only allows this right after a click or a key press.\n * - CLIPBOARD_PERMISSION_DENIED: The permission to use the clipboard was denied.\n * - CLIPBOARD_INVALID_IMAGE: The image couldn't be decoded, so it can't be copied as PNG.\n * - CLIPBOARD_READ_FAILED: The clipboard couldn't be read.\n */\nexport const readClipboard = async (): Promise<IClipboardContent> => {\n const clipboard = getSecureClipboard();\n const read = clipboard?.read;\n if (typeof read !== 'function') {\n throw unsupported('read');\n }\n let items: ClipboardItems;\n try {\n items = await read.call(clipboard);\n } catch (error) {\n throw await toClipboardException(error, 'read');\n }\n try {\n const [text, html, image] = await Promise.all([\n readType(items, 'text/plain'),\n readType(items, 'text/html'),\n readImage(items),\n ]);\n return { text, html, image };\n } catch (error) {\n throw isException(error)\n ? error\n : new Exception(\"The clipboard's content couldn't be read.\", {\n code: ERRORS.CLIPBOARD_READ_FAILED,\n cause: error,\n });\n }\n};\n"]}
1
+ {"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/read/read.ts"],"names":[],"mappings":"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","sourcesContent":["import { Exception, isException } from 'jg-exception';\n\nimport { toPng } from '../copy/image.js';\nimport {\n ERRORS,\n getSecureClipboard,\n toClipboardException,\n unsupported,\n type IClipboardContent,\n} from '../shared/index.js';\n\n/**\n * Reads the text on the clipboard. Browsers ask before a page reads it: Safari and Firefox show a\n * paste prompt, and Chromium asks for the clipboard-read permission once. Call it from a click or\n * key press handler, which Safari requires.\n * @returns A promise that resolves to the text, or `''` when the clipboard holds none.\n * @example\n * import { readText } from 'jg-clipboard';\n *\n * pasteButton.addEventListener('click', async () => {\n * input.value = await readText();\n * });\n * @throws\n * - CLIPBOARD_UNSUPPORTED: The clipboard can't read text here.\n * - CLIPBOARD_NOT_FOCUSED: The page must have focus to use the clipboard.\n * - CLIPBOARD_NO_USER_GESTURE: The browser only allows this right after a click or a key press.\n * - CLIPBOARD_PERMISSION_DENIED: The permission to use the clipboard was denied.\n * - CLIPBOARD_READ_FAILED: The clipboard couldn't be read.\n */\nexport const readText = async (): Promise<string> => {\n const clipboard = getSecureClipboard();\n const read = clipboard?.readText;\n if (typeof read !== 'function') {\n throw unsupported('read text');\n }\n try {\n return await read.call(clipboard);\n } catch (error) {\n throw await toClipboardException(error, 'read');\n }\n};\n\n/**\n * Reads one type from the first item that has it.\n * @param items The clipboard's items.\n * @param type The MIME type.\n * @returns A promise that resolves to the item's text, or null when no item has the type.\n */\nconst readType = async (items: ClipboardItems, type: string): Promise<string | null> => {\n const item = items.find(({ types }) => types.includes(type));\n return item === undefined ? null : (await item.getType(type)).text();\n};\n\n/**\n * Reads the first image, preferring PNG, as PNG.\n * @param items The clipboard's items.\n * @returns A promise that resolves to the PNG, or null when no item holds an image.\n */\nconst readImage = async (items: ClipboardItems): Promise<Blob | null> => {\n const types = items.flatMap((item) => item.types.map((type) => ({ item, type })));\n const image =\n types.find(({ type }) => type === 'image/png') ??\n types.find(({ type }) => type.startsWith('image/'));\n return image === undefined ? null : toPng(await image.item.getType(image.type));\n};\n\n/**\n * Reads the clipboard's text, HTML, and image with one `read()` call, so the person pasting sees\n * one prompt at most. Call it from a click or key press handler, which Safari requires. An image\n * the browser can't read never hides the text or HTML copied with it, such as the picture of\n * spreadsheet cells that comes with their text: the image is then null.\n * @returns A promise that resolves to each kind of content, with null for the kinds the clipboard\n * doesn't hold or the image it can't read. The image is PNG.\n * @example\n * import { readClipboard } from 'jg-clipboard';\n *\n * pasteButton.addEventListener('click', async () => {\n * const { html, text, image } = await readClipboard();\n * if (image) {\n * preview.src = URL.createObjectURL(image);\n * }\n * });\n * @throws\n * - CLIPBOARD_UNSUPPORTED: The clipboard can't read here.\n * - CLIPBOARD_NOT_FOCUSED: The page must have focus to use the clipboard.\n * - CLIPBOARD_NO_USER_GESTURE: The browser only allows this right after a click or a key press.\n * - CLIPBOARD_PERMISSION_DENIED: The permission to use the clipboard was denied.\n * - CLIPBOARD_INVALID_IMAGE: The clipboard holds only an image, which couldn't be decoded as PNG.\n * - CLIPBOARD_READ_FAILED: The clipboard couldn't be read, or holds only an image it couldn't read.\n */\nexport const readClipboard = async (): Promise<IClipboardContent> => {\n const clipboard = getSecureClipboard();\n const read = clipboard?.read;\n if (typeof read !== 'function') {\n throw unsupported('read');\n }\n let items: ClipboardItems;\n try {\n items = await read.call(clipboard);\n } catch (error) {\n throw await toClipboardException(error, 'read');\n }\n try {\n const [text, html, image] = await Promise.all([\n readType(items, 'text/plain'),\n readType(items, 'text/html'),\n // a failed image is kept as its error, so the text copied with it can still be pasted\n readImage(items).then(\n (png) => ({ png }),\n (failure: unknown) => ({ failure }),\n ),\n ]);\n if (!('failure' in image)) {\n return { text, html, image: image.png };\n }\n if (!text && !html) {\n throw image.failure;\n }\n return { text, html, image: null };\n } catch (error) {\n throw isException(error)\n ? error\n : new Exception(\"The clipboard's content couldn't be read.\", {\n code: ERRORS.CLIPBOARD_READ_FAILED,\n cause: error,\n });\n }\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jg-clipboard",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Copy and read text, HTML, and images on the browser clipboard, with support detection and coded errors that say why the browser refused.",
5
5
  "author": "Jesus Graterol",
6
6
  "license": "MIT",
@@ -16,7 +16,8 @@
16
16
  "files": [
17
17
  "dist",
18
18
  "CHANGELOG.md",
19
- "API.md"
19
+ "API.md",
20
+ "RECIPES.md"
20
21
  ],
21
22
  "sideEffects": false,
22
23
  "engines": {
@@ -42,7 +43,7 @@
42
43
  "clipboard-api"
43
44
  ],
44
45
  "dependencies": {
45
- "jg-exception": "^1.1.0"
46
+ "jg-exception": "^1.2.0"
46
47
  },
47
48
  "devDependencies": {
48
49
  "@jg-packages/eslint-config": "0.0.0",