@ngockhoi96/ctc 0.1.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/LICENSE +21 -0
- package/README.md +302 -0
- package/dist/clipboard/index.cjs +7 -0
- package/dist/clipboard/index.d.cts +2 -0
- package/dist/clipboard/index.d.mts +2 -0
- package/dist/clipboard/index.mjs +2 -0
- package/dist/clipboard-Bs9DV14p.cjs +338 -0
- package/dist/clipboard-Bs9DV14p.cjs.map +1 -0
- package/dist/clipboard-OTP55cvN.mjs +309 -0
- package/dist/clipboard-OTP55cvN.mjs.map +1 -0
- package/dist/index-B7FFRy7e.d.mts +193 -0
- package/dist/index-CHCHjqWe.d.cts +193 -0
- package/dist/index.cjs +7 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +2 -0
- package/package.json +77 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clipboard-Bs9DV14p.cjs","names":[],"sources":["../src/lib/env.ts","../src/lib/errors.ts","../src/clipboard/copy.ts","../src/clipboard/detect.ts","../src/clipboard/fallback.ts","../src/clipboard/read.ts"],"sourcesContent":["/**\n * Check if code is running in a browser environment.\n *\n * @returns `true` if `navigator` and `window` are defined\n */\nexport function isBrowser(): boolean {\n\treturn typeof navigator !== 'undefined' && typeof window !== 'undefined'\n}\n\n/**\n * Check if the current context is secure (HTTPS or localhost).\n *\n * Clipboard API requires a secure context in modern browsers.\n *\n * @returns `true` if running in a browser with a secure context\n */\nexport function isSecureContext(): boolean {\n\treturn isBrowser() && window.isSecureContext === true\n}\n","import type { BrowserUtilsError, ErrorCode, OnErrorCallback } from './types.ts'\n\n/**\n * Error codes that represent expected, recoverable failures.\n *\n * Expected errors are logged with `console.warn`.\n * Unexpected errors are logged with `console.error`.\n *\n * @internal\n */\nconst EXPECTED_ERROR_CODES = new Set<ErrorCode>([\n\t'CLIPBOARD_NOT_SUPPORTED',\n\t'INSECURE_CONTEXT',\n\t'CLIPBOARD_PERMISSION_DENIED',\n])\n\n/**\n * Create a structured browser utils error.\n *\n * @param code - Error code identifying the failure type\n * @param message - Human-readable error description\n * @param cause - Original error that caused this failure\n * @returns A structured BrowserUtilsError object\n */\nexport function createError(\n\tcode: ErrorCode,\n\tmessage: string,\n\tcause?: unknown,\n): BrowserUtilsError {\n\treturn { code, message, cause }\n}\n\n/**\n * Invoke the onError callback if provided, otherwise log a warning or error.\n *\n * Expected failures (CLIPBOARD_NOT_SUPPORTED, INSECURE_CONTEXT,\n * CLIPBOARD_PERMISSION_DENIED) are logged with `console.warn`.\n * Unexpected failures (CLIPBOARD_WRITE_FAILED, CLIPBOARD_READ_FAILED) are\n * logged with `console.error` and include the original cause for debugging.\n *\n * @param error - The structured error to handle\n * @param onError - Optional callback for error reporting\n */\nexport function handleError(\n\terror: BrowserUtilsError,\n\tonError?: OnErrorCallback,\n): void {\n\tif (onError) {\n\t\ttry {\n\t\t\tonError(error)\n\t\t} catch {\n\t\t\t// Consumer callback errors must not escape — the no-throw contract\n\t\t\t// applies to the full call stack originating from clipboard functions.\n\t\t}\n\t\treturn\n\t}\n\n\tconst prefix = '[ngockhoi96]'\n\n\tif (EXPECTED_ERROR_CODES.has(error.code)) {\n\t\tconsole.warn(`${prefix} ${error.code}: ${error.message}`)\n\t} else {\n\t\tconsole.error(`${prefix} ${error.code}: ${error.message}`, error.cause)\n\t}\n}\n","import { isBrowser, isSecureContext } from '../lib/env.ts'\nimport { createError, handleError } from '../lib/errors.ts'\nimport type { ClipboardOptions } from './types.ts'\n\n/**\n * Copy text to the clipboard using the modern Clipboard API.\n *\n * Requires a secure context (HTTPS or localhost) and must be called from\n * within a user gesture handler (click, keydown, etc.).\n *\n * @param text - The text to copy to the clipboard\n * @param options - Optional configuration including `onError` callback\n * @returns `true` on success, `false` on any failure (never throws)\n *\n * @remarks\n * **User gesture requirement:** Must be called synchronously within a user\n * gesture handler. Programmatic calls from timers or microtasks will be\n * rejected by the browser with `CLIPBOARD_PERMISSION_DENIED`.\n *\n * **Secure context:** Returns `false` on HTTP pages with `INSECURE_CONTEXT`\n * error code. Use `copyToClipboardLegacy()` for HTTP environments.\n *\n * **Safari:** Calling any async operation before `writeText()` in the same\n * microtask may break Safari's user activation window. Keep the call\n * synchronous within the click handler.\n *\n * @example\n * ```ts\n * button.addEventListener('click', async () => {\n * const success = await copyToClipboard('Hello, world!')\n * if (!success) {\n * showError('Copy failed — check HTTPS and permissions')\n * }\n * })\n * ```\n */\nexport async function copyToClipboard(\n\ttext: string,\n\toptions?: ClipboardOptions,\n): Promise<boolean> {\n\tif (!isBrowser()) {\n\t\thandleError(\n\t\t\tcreateError('CLIPBOARD_NOT_SUPPORTED', 'Not in a browser environment'),\n\t\t\toptions?.onError,\n\t\t)\n\t\treturn false\n\t}\n\n\tif (!isSecureContext()) {\n\t\thandleError(\n\t\t\tcreateError(\n\t\t\t\t'INSECURE_CONTEXT',\n\t\t\t\t'Clipboard API requires a secure context (HTTPS)',\n\t\t\t),\n\t\t\toptions?.onError,\n\t\t)\n\t\treturn false\n\t}\n\n\tif (typeof navigator.clipboard?.writeText !== 'function') {\n\t\thandleError(\n\t\t\tcreateError('CLIPBOARD_NOT_SUPPORTED', 'Clipboard API not available'),\n\t\t\toptions?.onError,\n\t\t)\n\t\treturn false\n\t}\n\n\ttry {\n\t\tawait navigator.clipboard.writeText(text)\n\t\treturn true\n\t} catch (error) {\n\t\tconst isPermissionDenied =\n\t\t\terror instanceof DOMException && error.name === 'NotAllowedError'\n\n\t\tif (isPermissionDenied) {\n\t\t\thandleError(\n\t\t\t\tcreateError(\n\t\t\t\t\t'CLIPBOARD_PERMISSION_DENIED',\n\t\t\t\t\t'Clipboard write permission denied',\n\t\t\t\t\terror,\n\t\t\t\t),\n\t\t\t\toptions?.onError,\n\t\t\t)\n\t\t} else {\n\t\t\thandleError(\n\t\t\t\tcreateError(\n\t\t\t\t\t'CLIPBOARD_WRITE_FAILED',\n\t\t\t\t\t'Failed to write to clipboard',\n\t\t\t\t\terror,\n\t\t\t\t),\n\t\t\t\toptions?.onError,\n\t\t\t)\n\t\t}\n\t\treturn false\n\t}\n}\n","import { isBrowser, isSecureContext } from '../lib/env.ts'\n\n/**\n * Check if the Clipboard API is available and usable in the current context.\n *\n * Returns `true` only when `navigator.clipboard.writeText` exists AND the\n * page is running in a secure context (HTTPS or localhost). Returns `false`\n * in SSR environments, on HTTP pages, or when the Clipboard API is absent.\n *\n * @returns `true` if clipboard write operations are supported\n *\n * @remarks\n * Permission state is not checked — a `true` result does not guarantee the\n * user has granted clipboard access. Permission denial is surfaced at call\n * time via the `CLIPBOARD_PERMISSION_DENIED` error code on `copyToClipboard`.\n *\n * @example\n * ```ts\n * if (isClipboardSupported()) {\n * await copyToClipboard(text)\n * } else {\n * copyToClipboardLegacy(text)\n * }\n * ```\n */\nexport function isClipboardSupported(): boolean {\n\treturn (\n\t\tisBrowser() &&\n\t\tisSecureContext() &&\n\t\ttypeof navigator.clipboard?.writeText === 'function'\n\t)\n}\n\n/**\n * Check if clipboard read operations are available and usable in the current context.\n *\n * Returns `true` only when `navigator.clipboard.readText` exists AND the\n * page is running in a secure context (HTTPS or localhost). Returns `false`\n * in SSR environments, on HTTP pages, or when the read API is absent.\n *\n * @returns `true` if clipboard read operations are supported\n *\n * @remarks\n * Permission state is not checked — a `true` result does not guarantee the\n * user has granted clipboard read access. Permission denial is surfaced at\n * call time via the `CLIPBOARD_PERMISSION_DENIED` error code on `readFromClipboard`.\n *\n * Firefox does not support the Permissions API `clipboard-read` query. This\n * function uses synchronous feature detection only — no async permission\n * queries are performed.\n *\n * @example\n * ```ts\n * if (isClipboardReadSupported()) {\n * const text = await readFromClipboard()\n * }\n * ```\n */\nexport function isClipboardReadSupported(): boolean {\n\treturn (\n\t\tisBrowser() &&\n\t\tisSecureContext() &&\n\t\ttypeof navigator.clipboard?.readText === 'function'\n\t)\n}\n","import { isBrowser } from '../lib/env.ts'\nimport { createError, handleError } from '../lib/errors.ts'\nimport type { ClipboardOptions } from './types.ts'\n\n/**\n * Copy text to the clipboard using the legacy `document.execCommand` API.\n *\n * Use this function when the modern Clipboard API is unavailable — for\n * example, on HTTP pages (non-HTTPS) or in browsers that do not support\n * `navigator.clipboard`. For HTTPS pages, prefer `copyToClipboard()`.\n *\n * @param text - The text to copy to the clipboard\n * @param options - Optional configuration including `onError` callback\n * @returns `true` on success, `false` on any failure (never throws)\n *\n * @remarks\n * This function uses the deprecated `document.execCommand('copy')` API,\n * which is synchronous and text-only. It temporarily creates and removes a\n * textarea element in the DOM to perform the copy operation.\n *\n * **No secure context requirement:** This function works on HTTP pages.\n * This is by design — it exists for environments where `copyToClipboard()`\n * is unavailable due to missing secure context.\n *\n * **iOS Safari:** `execCommand` copy is not reliably supported on iOS.\n * This function may return `false` on iOS Safari without a usable fallback.\n *\n * @example\n * ```ts\n * if (isClipboardSupported()) {\n * await copyToClipboard(text)\n * } else {\n * const success = copyToClipboardLegacy(text)\n * if (!success) {\n * showManualCopyInstructions()\n * }\n * }\n * ```\n */\nexport function copyToClipboardLegacy(\n\ttext: string,\n\toptions?: ClipboardOptions,\n): boolean {\n\tif (!isBrowser()) {\n\t\thandleError(\n\t\t\tcreateError('CLIPBOARD_NOT_SUPPORTED', 'Not in a browser environment'),\n\t\t\toptions?.onError,\n\t\t)\n\t\treturn false\n\t}\n\n\tif (!document.body) {\n\t\thandleError(\n\t\t\tcreateError('CLIPBOARD_NOT_SUPPORTED', 'document.body is not available'),\n\t\t\toptions?.onError,\n\t\t)\n\t\treturn false\n\t}\n\n\tconst textarea = document.createElement('textarea')\n\n\t// Position off-screen but visible to the browser — required for text selection on iOS.\n\t// display:none or visibility:hidden prevents the browser from selecting the content.\n\ttextarea.style.position = 'fixed'\n\ttextarea.style.top = '0'\n\ttextarea.style.left = '0'\n\ttextarea.style.opacity = '0'\n\ttextarea.style.pointerEvents = 'none'\n\t// Prevent iOS Safari from auto-zooming when the textarea is focused\n\ttextarea.style.fontSize = '16px'\n\t// Prevent the mobile keyboard from appearing during the copy operation\n\ttextarea.readOnly = true\n\ttextarea.value = text\n\n\tdocument.body.appendChild(textarea)\n\n\ttry {\n\t\ttextarea.focus()\n\t\t// setSelectionRange is more reliable than .select() on mobile browsers (Pitfall 3)\n\t\ttextarea.setSelectionRange(0, text.length)\n\n\t\tconst success = document.execCommand('copy')\n\n\t\tif (!success) {\n\t\t\thandleError(\n\t\t\t\tcreateError(\n\t\t\t\t\t'CLIPBOARD_WRITE_FAILED',\n\t\t\t\t\t'execCommand copy returned false',\n\t\t\t\t),\n\t\t\t\toptions?.onError,\n\t\t\t)\n\t\t\treturn false\n\t\t}\n\n\t\treturn true\n\t} catch (error) {\n\t\thandleError(\n\t\t\tcreateError(\n\t\t\t\t'CLIPBOARD_WRITE_FAILED',\n\t\t\t\t'execCommand copy threw an error',\n\t\t\t\terror,\n\t\t\t),\n\t\t\toptions?.onError,\n\t\t)\n\t\treturn false\n\t} finally {\n\t\t// Guard with isConnected before removing — prevents NotFoundError if a\n\t\t// framework (e.g., React 18 Strict Mode) or MutationObserver removed the\n\t\t// node between appendChild and here. textarea.remove() does not throw on\n\t\t// already-detached nodes. (Pitfall 4)\n\t\tif (textarea.isConnected) {\n\t\t\ttextarea.remove()\n\t\t}\n\t}\n}\n","import { isBrowser, isSecureContext } from '../lib/env.ts'\nimport { createError, handleError } from '../lib/errors.ts'\nimport type { ClipboardOptions } from './types.ts'\n\n/**\n * Read text from the clipboard using the modern Clipboard API.\n *\n * Requires a secure context (HTTPS or localhost). The browser may prompt\n * the user for permission on the first call.\n *\n * @param options - Optional configuration including `onError` callback\n * @returns The clipboard text on success, `null` on any failure (never throws)\n *\n * @remarks\n * **Permission prompt:** Chrome prompts for `clipboard-read` permission on\n * the first call. Firefox and Safari show a system-level paste prompt.\n * Permission denial is reported via `CLIPBOARD_PERMISSION_DENIED`.\n *\n * **Non-text content:** If the clipboard contains only non-text content\n * (e.g., an image), the browser throws `NotFoundError` which is reported\n * as `CLIPBOARD_READ_FAILED`.\n *\n * @example\n * ```ts\n * button.addEventListener('click', async () => {\n * const text = await readFromClipboard()\n * if (text !== null) {\n * input.value = text\n * }\n * })\n * ```\n */\nexport async function readFromClipboard(\n\toptions?: ClipboardOptions,\n): Promise<string | null> {\n\tif (!isBrowser()) {\n\t\thandleError(\n\t\t\tcreateError('CLIPBOARD_NOT_SUPPORTED', 'Not in a browser environment'),\n\t\t\toptions?.onError,\n\t\t)\n\t\treturn null\n\t}\n\n\tif (!isSecureContext()) {\n\t\thandleError(\n\t\t\tcreateError(\n\t\t\t\t'INSECURE_CONTEXT',\n\t\t\t\t'Clipboard API requires a secure context (HTTPS)',\n\t\t\t),\n\t\t\toptions?.onError,\n\t\t)\n\t\treturn null\n\t}\n\n\tif (typeof navigator.clipboard?.readText !== 'function') {\n\t\thandleError(\n\t\t\tcreateError(\n\t\t\t\t'CLIPBOARD_NOT_SUPPORTED',\n\t\t\t\t'Clipboard read API not available',\n\t\t\t),\n\t\t\toptions?.onError,\n\t\t)\n\t\treturn null\n\t}\n\n\ttry {\n\t\treturn await navigator.clipboard.readText()\n\t} catch (error) {\n\t\tconst isPermissionDenied =\n\t\t\terror instanceof DOMException && error.name === 'NotAllowedError'\n\n\t\tif (isPermissionDenied) {\n\t\t\thandleError(\n\t\t\t\tcreateError(\n\t\t\t\t\t'CLIPBOARD_PERMISSION_DENIED',\n\t\t\t\t\t'Clipboard read permission denied',\n\t\t\t\t\terror,\n\t\t\t\t),\n\t\t\t\toptions?.onError,\n\t\t\t)\n\t\t} else {\n\t\t\thandleError(\n\t\t\t\tcreateError(\n\t\t\t\t\t'CLIPBOARD_READ_FAILED',\n\t\t\t\t\t'Failed to read from clipboard',\n\t\t\t\t\terror,\n\t\t\t\t),\n\t\t\t\toptions?.onError,\n\t\t\t)\n\t\t}\n\t\treturn null\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
//#region src/lib/env.ts
|
|
2
|
+
/**
|
|
3
|
+
* Check if code is running in a browser environment.
|
|
4
|
+
*
|
|
5
|
+
* @returns `true` if `navigator` and `window` are defined
|
|
6
|
+
*/
|
|
7
|
+
function isBrowser() {
|
|
8
|
+
return typeof navigator !== "undefined" && typeof window !== "undefined";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Check if the current context is secure (HTTPS or localhost).
|
|
12
|
+
*
|
|
13
|
+
* Clipboard API requires a secure context in modern browsers.
|
|
14
|
+
*
|
|
15
|
+
* @returns `true` if running in a browser with a secure context
|
|
16
|
+
*/
|
|
17
|
+
function isSecureContext() {
|
|
18
|
+
return isBrowser() && window.isSecureContext === true;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/lib/errors.ts
|
|
22
|
+
/**
|
|
23
|
+
* Error codes that represent expected, recoverable failures.
|
|
24
|
+
*
|
|
25
|
+
* Expected errors are logged with `console.warn`.
|
|
26
|
+
* Unexpected errors are logged with `console.error`.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
const EXPECTED_ERROR_CODES = new Set([
|
|
31
|
+
"CLIPBOARD_NOT_SUPPORTED",
|
|
32
|
+
"INSECURE_CONTEXT",
|
|
33
|
+
"CLIPBOARD_PERMISSION_DENIED"
|
|
34
|
+
]);
|
|
35
|
+
/**
|
|
36
|
+
* Create a structured browser utils error.
|
|
37
|
+
*
|
|
38
|
+
* @param code - Error code identifying the failure type
|
|
39
|
+
* @param message - Human-readable error description
|
|
40
|
+
* @param cause - Original error that caused this failure
|
|
41
|
+
* @returns A structured BrowserUtilsError object
|
|
42
|
+
*/
|
|
43
|
+
function createError(code, message, cause) {
|
|
44
|
+
return {
|
|
45
|
+
code,
|
|
46
|
+
message,
|
|
47
|
+
cause
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Invoke the onError callback if provided, otherwise log a warning or error.
|
|
52
|
+
*
|
|
53
|
+
* Expected failures (CLIPBOARD_NOT_SUPPORTED, INSECURE_CONTEXT,
|
|
54
|
+
* CLIPBOARD_PERMISSION_DENIED) are logged with `console.warn`.
|
|
55
|
+
* Unexpected failures (CLIPBOARD_WRITE_FAILED, CLIPBOARD_READ_FAILED) are
|
|
56
|
+
* logged with `console.error` and include the original cause for debugging.
|
|
57
|
+
*
|
|
58
|
+
* @param error - The structured error to handle
|
|
59
|
+
* @param onError - Optional callback for error reporting
|
|
60
|
+
*/
|
|
61
|
+
function handleError(error, onError) {
|
|
62
|
+
if (onError) {
|
|
63
|
+
try {
|
|
64
|
+
onError(error);
|
|
65
|
+
} catch {}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const prefix = "[ngockhoi96]";
|
|
69
|
+
if (EXPECTED_ERROR_CODES.has(error.code)) console.warn(`${prefix} ${error.code}: ${error.message}`);
|
|
70
|
+
else console.error(`${prefix} ${error.code}: ${error.message}`, error.cause);
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/clipboard/copy.ts
|
|
74
|
+
/**
|
|
75
|
+
* Copy text to the clipboard using the modern Clipboard API.
|
|
76
|
+
*
|
|
77
|
+
* Requires a secure context (HTTPS or localhost) and must be called from
|
|
78
|
+
* within a user gesture handler (click, keydown, etc.).
|
|
79
|
+
*
|
|
80
|
+
* @param text - The text to copy to the clipboard
|
|
81
|
+
* @param options - Optional configuration including `onError` callback
|
|
82
|
+
* @returns `true` on success, `false` on any failure (never throws)
|
|
83
|
+
*
|
|
84
|
+
* @remarks
|
|
85
|
+
* **User gesture requirement:** Must be called synchronously within a user
|
|
86
|
+
* gesture handler. Programmatic calls from timers or microtasks will be
|
|
87
|
+
* rejected by the browser with `CLIPBOARD_PERMISSION_DENIED`.
|
|
88
|
+
*
|
|
89
|
+
* **Secure context:** Returns `false` on HTTP pages with `INSECURE_CONTEXT`
|
|
90
|
+
* error code. Use `copyToClipboardLegacy()` for HTTP environments.
|
|
91
|
+
*
|
|
92
|
+
* **Safari:** Calling any async operation before `writeText()` in the same
|
|
93
|
+
* microtask may break Safari's user activation window. Keep the call
|
|
94
|
+
* synchronous within the click handler.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* button.addEventListener('click', async () => {
|
|
99
|
+
* const success = await copyToClipboard('Hello, world!')
|
|
100
|
+
* if (!success) {
|
|
101
|
+
* showError('Copy failed — check HTTPS and permissions')
|
|
102
|
+
* }
|
|
103
|
+
* })
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
async function copyToClipboard(text, options) {
|
|
107
|
+
if (!isBrowser()) {
|
|
108
|
+
handleError(createError("CLIPBOARD_NOT_SUPPORTED", "Not in a browser environment"), options?.onError);
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
if (!isSecureContext()) {
|
|
112
|
+
handleError(createError("INSECURE_CONTEXT", "Clipboard API requires a secure context (HTTPS)"), options?.onError);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (typeof navigator.clipboard?.writeText !== "function") {
|
|
116
|
+
handleError(createError("CLIPBOARD_NOT_SUPPORTED", "Clipboard API not available"), options?.onError);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
await navigator.clipboard.writeText(text);
|
|
121
|
+
return true;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error instanceof DOMException && error.name === "NotAllowedError") handleError(createError("CLIPBOARD_PERMISSION_DENIED", "Clipboard write permission denied", error), options?.onError);
|
|
124
|
+
else handleError(createError("CLIPBOARD_WRITE_FAILED", "Failed to write to clipboard", error), options?.onError);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/clipboard/detect.ts
|
|
130
|
+
/**
|
|
131
|
+
* Check if the Clipboard API is available and usable in the current context.
|
|
132
|
+
*
|
|
133
|
+
* Returns `true` only when `navigator.clipboard.writeText` exists AND the
|
|
134
|
+
* page is running in a secure context (HTTPS or localhost). Returns `false`
|
|
135
|
+
* in SSR environments, on HTTP pages, or when the Clipboard API is absent.
|
|
136
|
+
*
|
|
137
|
+
* @returns `true` if clipboard write operations are supported
|
|
138
|
+
*
|
|
139
|
+
* @remarks
|
|
140
|
+
* Permission state is not checked — a `true` result does not guarantee the
|
|
141
|
+
* user has granted clipboard access. Permission denial is surfaced at call
|
|
142
|
+
* time via the `CLIPBOARD_PERMISSION_DENIED` error code on `copyToClipboard`.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```ts
|
|
146
|
+
* if (isClipboardSupported()) {
|
|
147
|
+
* await copyToClipboard(text)
|
|
148
|
+
* } else {
|
|
149
|
+
* copyToClipboardLegacy(text)
|
|
150
|
+
* }
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
function isClipboardSupported() {
|
|
154
|
+
return isBrowser() && isSecureContext() && typeof navigator.clipboard?.writeText === "function";
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Check if clipboard read operations are available and usable in the current context.
|
|
158
|
+
*
|
|
159
|
+
* Returns `true` only when `navigator.clipboard.readText` exists AND the
|
|
160
|
+
* page is running in a secure context (HTTPS or localhost). Returns `false`
|
|
161
|
+
* in SSR environments, on HTTP pages, or when the read API is absent.
|
|
162
|
+
*
|
|
163
|
+
* @returns `true` if clipboard read operations are supported
|
|
164
|
+
*
|
|
165
|
+
* @remarks
|
|
166
|
+
* Permission state is not checked — a `true` result does not guarantee the
|
|
167
|
+
* user has granted clipboard read access. Permission denial is surfaced at
|
|
168
|
+
* call time via the `CLIPBOARD_PERMISSION_DENIED` error code on `readFromClipboard`.
|
|
169
|
+
*
|
|
170
|
+
* Firefox does not support the Permissions API `clipboard-read` query. This
|
|
171
|
+
* function uses synchronous feature detection only — no async permission
|
|
172
|
+
* queries are performed.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```ts
|
|
176
|
+
* if (isClipboardReadSupported()) {
|
|
177
|
+
* const text = await readFromClipboard()
|
|
178
|
+
* }
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
function isClipboardReadSupported() {
|
|
182
|
+
return isBrowser() && isSecureContext() && typeof navigator.clipboard?.readText === "function";
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/clipboard/fallback.ts
|
|
186
|
+
/**
|
|
187
|
+
* Copy text to the clipboard using the legacy `document.execCommand` API.
|
|
188
|
+
*
|
|
189
|
+
* Use this function when the modern Clipboard API is unavailable — for
|
|
190
|
+
* example, on HTTP pages (non-HTTPS) or in browsers that do not support
|
|
191
|
+
* `navigator.clipboard`. For HTTPS pages, prefer `copyToClipboard()`.
|
|
192
|
+
*
|
|
193
|
+
* @param text - The text to copy to the clipboard
|
|
194
|
+
* @param options - Optional configuration including `onError` callback
|
|
195
|
+
* @returns `true` on success, `false` on any failure (never throws)
|
|
196
|
+
*
|
|
197
|
+
* @remarks
|
|
198
|
+
* This function uses the deprecated `document.execCommand('copy')` API,
|
|
199
|
+
* which is synchronous and text-only. It temporarily creates and removes a
|
|
200
|
+
* textarea element in the DOM to perform the copy operation.
|
|
201
|
+
*
|
|
202
|
+
* **No secure context requirement:** This function works on HTTP pages.
|
|
203
|
+
* This is by design — it exists for environments where `copyToClipboard()`
|
|
204
|
+
* is unavailable due to missing secure context.
|
|
205
|
+
*
|
|
206
|
+
* **iOS Safari:** `execCommand` copy is not reliably supported on iOS.
|
|
207
|
+
* This function may return `false` on iOS Safari without a usable fallback.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```ts
|
|
211
|
+
* if (isClipboardSupported()) {
|
|
212
|
+
* await copyToClipboard(text)
|
|
213
|
+
* } else {
|
|
214
|
+
* const success = copyToClipboardLegacy(text)
|
|
215
|
+
* if (!success) {
|
|
216
|
+
* showManualCopyInstructions()
|
|
217
|
+
* }
|
|
218
|
+
* }
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
function copyToClipboardLegacy(text, options) {
|
|
222
|
+
if (!isBrowser()) {
|
|
223
|
+
handleError(createError("CLIPBOARD_NOT_SUPPORTED", "Not in a browser environment"), options?.onError);
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
if (!document.body) {
|
|
227
|
+
handleError(createError("CLIPBOARD_NOT_SUPPORTED", "document.body is not available"), options?.onError);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
const textarea = document.createElement("textarea");
|
|
231
|
+
textarea.style.position = "fixed";
|
|
232
|
+
textarea.style.top = "0";
|
|
233
|
+
textarea.style.left = "0";
|
|
234
|
+
textarea.style.opacity = "0";
|
|
235
|
+
textarea.style.pointerEvents = "none";
|
|
236
|
+
textarea.style.fontSize = "16px";
|
|
237
|
+
textarea.readOnly = true;
|
|
238
|
+
textarea.value = text;
|
|
239
|
+
document.body.appendChild(textarea);
|
|
240
|
+
try {
|
|
241
|
+
textarea.focus();
|
|
242
|
+
textarea.setSelectionRange(0, text.length);
|
|
243
|
+
if (!document.execCommand("copy")) {
|
|
244
|
+
handleError(createError("CLIPBOARD_WRITE_FAILED", "execCommand copy returned false"), options?.onError);
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
return true;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
handleError(createError("CLIPBOARD_WRITE_FAILED", "execCommand copy threw an error", error), options?.onError);
|
|
250
|
+
return false;
|
|
251
|
+
} finally {
|
|
252
|
+
if (textarea.isConnected) textarea.remove();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/clipboard/read.ts
|
|
257
|
+
/**
|
|
258
|
+
* Read text from the clipboard using the modern Clipboard API.
|
|
259
|
+
*
|
|
260
|
+
* Requires a secure context (HTTPS or localhost). The browser may prompt
|
|
261
|
+
* the user for permission on the first call.
|
|
262
|
+
*
|
|
263
|
+
* @param options - Optional configuration including `onError` callback
|
|
264
|
+
* @returns The clipboard text on success, `null` on any failure (never throws)
|
|
265
|
+
*
|
|
266
|
+
* @remarks
|
|
267
|
+
* **Permission prompt:** Chrome prompts for `clipboard-read` permission on
|
|
268
|
+
* the first call. Firefox and Safari show a system-level paste prompt.
|
|
269
|
+
* Permission denial is reported via `CLIPBOARD_PERMISSION_DENIED`.
|
|
270
|
+
*
|
|
271
|
+
* **Non-text content:** If the clipboard contains only non-text content
|
|
272
|
+
* (e.g., an image), the browser throws `NotFoundError` which is reported
|
|
273
|
+
* as `CLIPBOARD_READ_FAILED`.
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```ts
|
|
277
|
+
* button.addEventListener('click', async () => {
|
|
278
|
+
* const text = await readFromClipboard()
|
|
279
|
+
* if (text !== null) {
|
|
280
|
+
* input.value = text
|
|
281
|
+
* }
|
|
282
|
+
* })
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
async function readFromClipboard(options) {
|
|
286
|
+
if (!isBrowser()) {
|
|
287
|
+
handleError(createError("CLIPBOARD_NOT_SUPPORTED", "Not in a browser environment"), options?.onError);
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
if (!isSecureContext()) {
|
|
291
|
+
handleError(createError("INSECURE_CONTEXT", "Clipboard API requires a secure context (HTTPS)"), options?.onError);
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
if (typeof navigator.clipboard?.readText !== "function") {
|
|
295
|
+
handleError(createError("CLIPBOARD_NOT_SUPPORTED", "Clipboard read API not available"), options?.onError);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
return await navigator.clipboard.readText();
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (error instanceof DOMException && error.name === "NotAllowedError") handleError(createError("CLIPBOARD_PERMISSION_DENIED", "Clipboard read permission denied", error), options?.onError);
|
|
302
|
+
else handleError(createError("CLIPBOARD_READ_FAILED", "Failed to read from clipboard", error), options?.onError);
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
//#endregion
|
|
307
|
+
export { copyToClipboard as a, isClipboardSupported as i, copyToClipboardLegacy as n, isClipboardReadSupported as r, readFromClipboard as t };
|
|
308
|
+
|
|
309
|
+
//# sourceMappingURL=clipboard-OTP55cvN.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clipboard-OTP55cvN.mjs","names":[],"sources":["../src/lib/env.ts","../src/lib/errors.ts","../src/clipboard/copy.ts","../src/clipboard/detect.ts","../src/clipboard/fallback.ts","../src/clipboard/read.ts"],"sourcesContent":["/**\n * Check if code is running in a browser environment.\n *\n * @returns `true` if `navigator` and `window` are defined\n */\nexport function isBrowser(): boolean {\n\treturn typeof navigator !== 'undefined' && typeof window !== 'undefined'\n}\n\n/**\n * Check if the current context is secure (HTTPS or localhost).\n *\n * Clipboard API requires a secure context in modern browsers.\n *\n * @returns `true` if running in a browser with a secure context\n */\nexport function isSecureContext(): boolean {\n\treturn isBrowser() && window.isSecureContext === true\n}\n","import type { BrowserUtilsError, ErrorCode, OnErrorCallback } from './types.ts'\n\n/**\n * Error codes that represent expected, recoverable failures.\n *\n * Expected errors are logged with `console.warn`.\n * Unexpected errors are logged with `console.error`.\n *\n * @internal\n */\nconst EXPECTED_ERROR_CODES = new Set<ErrorCode>([\n\t'CLIPBOARD_NOT_SUPPORTED',\n\t'INSECURE_CONTEXT',\n\t'CLIPBOARD_PERMISSION_DENIED',\n])\n\n/**\n * Create a structured browser utils error.\n *\n * @param code - Error code identifying the failure type\n * @param message - Human-readable error description\n * @param cause - Original error that caused this failure\n * @returns A structured BrowserUtilsError object\n */\nexport function createError(\n\tcode: ErrorCode,\n\tmessage: string,\n\tcause?: unknown,\n): BrowserUtilsError {\n\treturn { code, message, cause }\n}\n\n/**\n * Invoke the onError callback if provided, otherwise log a warning or error.\n *\n * Expected failures (CLIPBOARD_NOT_SUPPORTED, INSECURE_CONTEXT,\n * CLIPBOARD_PERMISSION_DENIED) are logged with `console.warn`.\n * Unexpected failures (CLIPBOARD_WRITE_FAILED, CLIPBOARD_READ_FAILED) are\n * logged with `console.error` and include the original cause for debugging.\n *\n * @param error - The structured error to handle\n * @param onError - Optional callback for error reporting\n */\nexport function handleError(\n\terror: BrowserUtilsError,\n\tonError?: OnErrorCallback,\n): void {\n\tif (onError) {\n\t\ttry {\n\t\t\tonError(error)\n\t\t} catch {\n\t\t\t// Consumer callback errors must not escape — the no-throw contract\n\t\t\t// applies to the full call stack originating from clipboard functions.\n\t\t}\n\t\treturn\n\t}\n\n\tconst prefix = '[ngockhoi96]'\n\n\tif (EXPECTED_ERROR_CODES.has(error.code)) {\n\t\tconsole.warn(`${prefix} ${error.code}: ${error.message}`)\n\t} else {\n\t\tconsole.error(`${prefix} ${error.code}: ${error.message}`, error.cause)\n\t}\n}\n","import { isBrowser, isSecureContext } from '../lib/env.ts'\nimport { createError, handleError } from '../lib/errors.ts'\nimport type { ClipboardOptions } from './types.ts'\n\n/**\n * Copy text to the clipboard using the modern Clipboard API.\n *\n * Requires a secure context (HTTPS or localhost) and must be called from\n * within a user gesture handler (click, keydown, etc.).\n *\n * @param text - The text to copy to the clipboard\n * @param options - Optional configuration including `onError` callback\n * @returns `true` on success, `false` on any failure (never throws)\n *\n * @remarks\n * **User gesture requirement:** Must be called synchronously within a user\n * gesture handler. Programmatic calls from timers or microtasks will be\n * rejected by the browser with `CLIPBOARD_PERMISSION_DENIED`.\n *\n * **Secure context:** Returns `false` on HTTP pages with `INSECURE_CONTEXT`\n * error code. Use `copyToClipboardLegacy()` for HTTP environments.\n *\n * **Safari:** Calling any async operation before `writeText()` in the same\n * microtask may break Safari's user activation window. Keep the call\n * synchronous within the click handler.\n *\n * @example\n * ```ts\n * button.addEventListener('click', async () => {\n * const success = await copyToClipboard('Hello, world!')\n * if (!success) {\n * showError('Copy failed — check HTTPS and permissions')\n * }\n * })\n * ```\n */\nexport async function copyToClipboard(\n\ttext: string,\n\toptions?: ClipboardOptions,\n): Promise<boolean> {\n\tif (!isBrowser()) {\n\t\thandleError(\n\t\t\tcreateError('CLIPBOARD_NOT_SUPPORTED', 'Not in a browser environment'),\n\t\t\toptions?.onError,\n\t\t)\n\t\treturn false\n\t}\n\n\tif (!isSecureContext()) {\n\t\thandleError(\n\t\t\tcreateError(\n\t\t\t\t'INSECURE_CONTEXT',\n\t\t\t\t'Clipboard API requires a secure context (HTTPS)',\n\t\t\t),\n\t\t\toptions?.onError,\n\t\t)\n\t\treturn false\n\t}\n\n\tif (typeof navigator.clipboard?.writeText !== 'function') {\n\t\thandleError(\n\t\t\tcreateError('CLIPBOARD_NOT_SUPPORTED', 'Clipboard API not available'),\n\t\t\toptions?.onError,\n\t\t)\n\t\treturn false\n\t}\n\n\ttry {\n\t\tawait navigator.clipboard.writeText(text)\n\t\treturn true\n\t} catch (error) {\n\t\tconst isPermissionDenied =\n\t\t\terror instanceof DOMException && error.name === 'NotAllowedError'\n\n\t\tif (isPermissionDenied) {\n\t\t\thandleError(\n\t\t\t\tcreateError(\n\t\t\t\t\t'CLIPBOARD_PERMISSION_DENIED',\n\t\t\t\t\t'Clipboard write permission denied',\n\t\t\t\t\terror,\n\t\t\t\t),\n\t\t\t\toptions?.onError,\n\t\t\t)\n\t\t} else {\n\t\t\thandleError(\n\t\t\t\tcreateError(\n\t\t\t\t\t'CLIPBOARD_WRITE_FAILED',\n\t\t\t\t\t'Failed to write to clipboard',\n\t\t\t\t\terror,\n\t\t\t\t),\n\t\t\t\toptions?.onError,\n\t\t\t)\n\t\t}\n\t\treturn false\n\t}\n}\n","import { isBrowser, isSecureContext } from '../lib/env.ts'\n\n/**\n * Check if the Clipboard API is available and usable in the current context.\n *\n * Returns `true` only when `navigator.clipboard.writeText` exists AND the\n * page is running in a secure context (HTTPS or localhost). Returns `false`\n * in SSR environments, on HTTP pages, or when the Clipboard API is absent.\n *\n * @returns `true` if clipboard write operations are supported\n *\n * @remarks\n * Permission state is not checked — a `true` result does not guarantee the\n * user has granted clipboard access. Permission denial is surfaced at call\n * time via the `CLIPBOARD_PERMISSION_DENIED` error code on `copyToClipboard`.\n *\n * @example\n * ```ts\n * if (isClipboardSupported()) {\n * await copyToClipboard(text)\n * } else {\n * copyToClipboardLegacy(text)\n * }\n * ```\n */\nexport function isClipboardSupported(): boolean {\n\treturn (\n\t\tisBrowser() &&\n\t\tisSecureContext() &&\n\t\ttypeof navigator.clipboard?.writeText === 'function'\n\t)\n}\n\n/**\n * Check if clipboard read operations are available and usable in the current context.\n *\n * Returns `true` only when `navigator.clipboard.readText` exists AND the\n * page is running in a secure context (HTTPS or localhost). Returns `false`\n * in SSR environments, on HTTP pages, or when the read API is absent.\n *\n * @returns `true` if clipboard read operations are supported\n *\n * @remarks\n * Permission state is not checked — a `true` result does not guarantee the\n * user has granted clipboard read access. Permission denial is surfaced at\n * call time via the `CLIPBOARD_PERMISSION_DENIED` error code on `readFromClipboard`.\n *\n * Firefox does not support the Permissions API `clipboard-read` query. This\n * function uses synchronous feature detection only — no async permission\n * queries are performed.\n *\n * @example\n * ```ts\n * if (isClipboardReadSupported()) {\n * const text = await readFromClipboard()\n * }\n * ```\n */\nexport function isClipboardReadSupported(): boolean {\n\treturn (\n\t\tisBrowser() &&\n\t\tisSecureContext() &&\n\t\ttypeof navigator.clipboard?.readText === 'function'\n\t)\n}\n","import { isBrowser } from '../lib/env.ts'\nimport { createError, handleError } from '../lib/errors.ts'\nimport type { ClipboardOptions } from './types.ts'\n\n/**\n * Copy text to the clipboard using the legacy `document.execCommand` API.\n *\n * Use this function when the modern Clipboard API is unavailable — for\n * example, on HTTP pages (non-HTTPS) or in browsers that do not support\n * `navigator.clipboard`. For HTTPS pages, prefer `copyToClipboard()`.\n *\n * @param text - The text to copy to the clipboard\n * @param options - Optional configuration including `onError` callback\n * @returns `true` on success, `false` on any failure (never throws)\n *\n * @remarks\n * This function uses the deprecated `document.execCommand('copy')` API,\n * which is synchronous and text-only. It temporarily creates and removes a\n * textarea element in the DOM to perform the copy operation.\n *\n * **No secure context requirement:** This function works on HTTP pages.\n * This is by design — it exists for environments where `copyToClipboard()`\n * is unavailable due to missing secure context.\n *\n * **iOS Safari:** `execCommand` copy is not reliably supported on iOS.\n * This function may return `false` on iOS Safari without a usable fallback.\n *\n * @example\n * ```ts\n * if (isClipboardSupported()) {\n * await copyToClipboard(text)\n * } else {\n * const success = copyToClipboardLegacy(text)\n * if (!success) {\n * showManualCopyInstructions()\n * }\n * }\n * ```\n */\nexport function copyToClipboardLegacy(\n\ttext: string,\n\toptions?: ClipboardOptions,\n): boolean {\n\tif (!isBrowser()) {\n\t\thandleError(\n\t\t\tcreateError('CLIPBOARD_NOT_SUPPORTED', 'Not in a browser environment'),\n\t\t\toptions?.onError,\n\t\t)\n\t\treturn false\n\t}\n\n\tif (!document.body) {\n\t\thandleError(\n\t\t\tcreateError('CLIPBOARD_NOT_SUPPORTED', 'document.body is not available'),\n\t\t\toptions?.onError,\n\t\t)\n\t\treturn false\n\t}\n\n\tconst textarea = document.createElement('textarea')\n\n\t// Position off-screen but visible to the browser — required for text selection on iOS.\n\t// display:none or visibility:hidden prevents the browser from selecting the content.\n\ttextarea.style.position = 'fixed'\n\ttextarea.style.top = '0'\n\ttextarea.style.left = '0'\n\ttextarea.style.opacity = '0'\n\ttextarea.style.pointerEvents = 'none'\n\t// Prevent iOS Safari from auto-zooming when the textarea is focused\n\ttextarea.style.fontSize = '16px'\n\t// Prevent the mobile keyboard from appearing during the copy operation\n\ttextarea.readOnly = true\n\ttextarea.value = text\n\n\tdocument.body.appendChild(textarea)\n\n\ttry {\n\t\ttextarea.focus()\n\t\t// setSelectionRange is more reliable than .select() on mobile browsers (Pitfall 3)\n\t\ttextarea.setSelectionRange(0, text.length)\n\n\t\tconst success = document.execCommand('copy')\n\n\t\tif (!success) {\n\t\t\thandleError(\n\t\t\t\tcreateError(\n\t\t\t\t\t'CLIPBOARD_WRITE_FAILED',\n\t\t\t\t\t'execCommand copy returned false',\n\t\t\t\t),\n\t\t\t\toptions?.onError,\n\t\t\t)\n\t\t\treturn false\n\t\t}\n\n\t\treturn true\n\t} catch (error) {\n\t\thandleError(\n\t\t\tcreateError(\n\t\t\t\t'CLIPBOARD_WRITE_FAILED',\n\t\t\t\t'execCommand copy threw an error',\n\t\t\t\terror,\n\t\t\t),\n\t\t\toptions?.onError,\n\t\t)\n\t\treturn false\n\t} finally {\n\t\t// Guard with isConnected before removing — prevents NotFoundError if a\n\t\t// framework (e.g., React 18 Strict Mode) or MutationObserver removed the\n\t\t// node between appendChild and here. textarea.remove() does not throw on\n\t\t// already-detached nodes. (Pitfall 4)\n\t\tif (textarea.isConnected) {\n\t\t\ttextarea.remove()\n\t\t}\n\t}\n}\n","import { isBrowser, isSecureContext } from '../lib/env.ts'\nimport { createError, handleError } from '../lib/errors.ts'\nimport type { ClipboardOptions } from './types.ts'\n\n/**\n * Read text from the clipboard using the modern Clipboard API.\n *\n * Requires a secure context (HTTPS or localhost). The browser may prompt\n * the user for permission on the first call.\n *\n * @param options - Optional configuration including `onError` callback\n * @returns The clipboard text on success, `null` on any failure (never throws)\n *\n * @remarks\n * **Permission prompt:** Chrome prompts for `clipboard-read` permission on\n * the first call. Firefox and Safari show a system-level paste prompt.\n * Permission denial is reported via `CLIPBOARD_PERMISSION_DENIED`.\n *\n * **Non-text content:** If the clipboard contains only non-text content\n * (e.g., an image), the browser throws `NotFoundError` which is reported\n * as `CLIPBOARD_READ_FAILED`.\n *\n * @example\n * ```ts\n * button.addEventListener('click', async () => {\n * const text = await readFromClipboard()\n * if (text !== null) {\n * input.value = text\n * }\n * })\n * ```\n */\nexport async function readFromClipboard(\n\toptions?: ClipboardOptions,\n): Promise<string | null> {\n\tif (!isBrowser()) {\n\t\thandleError(\n\t\t\tcreateError('CLIPBOARD_NOT_SUPPORTED', 'Not in a browser environment'),\n\t\t\toptions?.onError,\n\t\t)\n\t\treturn null\n\t}\n\n\tif (!isSecureContext()) {\n\t\thandleError(\n\t\t\tcreateError(\n\t\t\t\t'INSECURE_CONTEXT',\n\t\t\t\t'Clipboard API requires a secure context (HTTPS)',\n\t\t\t),\n\t\t\toptions?.onError,\n\t\t)\n\t\treturn null\n\t}\n\n\tif (typeof navigator.clipboard?.readText !== 'function') {\n\t\thandleError(\n\t\t\tcreateError(\n\t\t\t\t'CLIPBOARD_NOT_SUPPORTED',\n\t\t\t\t'Clipboard read API not available',\n\t\t\t),\n\t\t\toptions?.onError,\n\t\t)\n\t\treturn null\n\t}\n\n\ttry {\n\t\treturn await navigator.clipboard.readText()\n\t} catch (error) {\n\t\tconst isPermissionDenied =\n\t\t\terror instanceof DOMException && error.name === 'NotAllowedError'\n\n\t\tif (isPermissionDenied) {\n\t\t\thandleError(\n\t\t\t\tcreateError(\n\t\t\t\t\t'CLIPBOARD_PERMISSION_DENIED',\n\t\t\t\t\t'Clipboard read permission denied',\n\t\t\t\t\terror,\n\t\t\t\t),\n\t\t\t\toptions?.onError,\n\t\t\t)\n\t\t} else {\n\t\t\thandleError(\n\t\t\t\tcreateError(\n\t\t\t\t\t'CLIPBOARD_READ_FAILED',\n\t\t\t\t\t'Failed to read from clipboard',\n\t\t\t\t\terror,\n\t\t\t\t),\n\t\t\t\toptions?.onError,\n\t\t\t)\n\t\t}\n\t\treturn null\n\t}\n}\n"],"mappings":";;;;;;AAKA,SAAgB,YAAqB;AACpC,QAAO,OAAO,cAAc,eAAe,OAAO,WAAW;;;;;;;;;AAU9D,SAAgB,kBAA2B;AAC1C,QAAO,WAAW,IAAI,OAAO,oBAAoB;;;;;;;;;;;;ACPlD,MAAM,uBAAuB,IAAI,IAAe;CAC/C;CACA;CACA;CACA,CAAC;;;;;;;;;AAUF,SAAgB,YACf,MACA,SACA,OACoB;AACpB,QAAO;EAAE;EAAM;EAAS;EAAO;;;;;;;;;;;;;AAchC,SAAgB,YACf,OACA,SACO;AACP,KAAI,SAAS;AACZ,MAAI;AACH,WAAQ,MAAM;UACP;AAIR;;CAGD,MAAM,SAAS;AAEf,KAAI,qBAAqB,IAAI,MAAM,KAAK,CACvC,SAAQ,KAAK,GAAG,OAAO,GAAG,MAAM,KAAK,IAAI,MAAM,UAAU;KAEzD,SAAQ,MAAM,GAAG,OAAO,GAAG,MAAM,KAAK,IAAI,MAAM,WAAW,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC1BzE,eAAsB,gBACrB,MACA,SACmB;AACnB,KAAI,CAAC,WAAW,EAAE;AACjB,cACC,YAAY,2BAA2B,+BAA+B,EACtE,SAAS,QACT;AACD,SAAO;;AAGR,KAAI,CAAC,iBAAiB,EAAE;AACvB,cACC,YACC,oBACA,kDACA,EACD,SAAS,QACT;AACD,SAAO;;AAGR,KAAI,OAAO,UAAU,WAAW,cAAc,YAAY;AACzD,cACC,YAAY,2BAA2B,8BAA8B,EACrE,SAAS,QACT;AACD,SAAO;;AAGR,KAAI;AACH,QAAM,UAAU,UAAU,UAAU,KAAK;AACzC,SAAO;UACC,OAAO;AAIf,MAFC,iBAAiB,gBAAgB,MAAM,SAAS,kBAGhD,aACC,YACC,+BACA,qCACA,MACA,EACD,SAAS,QACT;MAED,aACC,YACC,0BACA,gCACA,MACA,EACD,SAAS,QACT;AAEF,SAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACpET,SAAgB,uBAAgC;AAC/C,QACC,WAAW,IACX,iBAAiB,IACjB,OAAO,UAAU,WAAW,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6B5C,SAAgB,2BAAoC;AACnD,QACC,WAAW,IACX,iBAAiB,IACjB,OAAO,UAAU,WAAW,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACvB3C,SAAgB,sBACf,MACA,SACU;AACV,KAAI,CAAC,WAAW,EAAE;AACjB,cACC,YAAY,2BAA2B,+BAA+B,EACtE,SAAS,QACT;AACD,SAAO;;AAGR,KAAI,CAAC,SAAS,MAAM;AACnB,cACC,YAAY,2BAA2B,iCAAiC,EACxE,SAAS,QACT;AACD,SAAO;;CAGR,MAAM,WAAW,SAAS,cAAc,WAAW;AAInD,UAAS,MAAM,WAAW;AAC1B,UAAS,MAAM,MAAM;AACrB,UAAS,MAAM,OAAO;AACtB,UAAS,MAAM,UAAU;AACzB,UAAS,MAAM,gBAAgB;AAE/B,UAAS,MAAM,WAAW;AAE1B,UAAS,WAAW;AACpB,UAAS,QAAQ;AAEjB,UAAS,KAAK,YAAY,SAAS;AAEnC,KAAI;AACH,WAAS,OAAO;AAEhB,WAAS,kBAAkB,GAAG,KAAK,OAAO;AAI1C,MAAI,CAFY,SAAS,YAAY,OAAO,EAE9B;AACb,eACC,YACC,0BACA,kCACA,EACD,SAAS,QACT;AACD,UAAO;;AAGR,SAAO;UACC,OAAO;AACf,cACC,YACC,0BACA,mCACA,MACA,EACD,SAAS,QACT;AACD,SAAO;WACE;AAKT,MAAI,SAAS,YACZ,UAAS,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC/EpB,eAAsB,kBACrB,SACyB;AACzB,KAAI,CAAC,WAAW,EAAE;AACjB,cACC,YAAY,2BAA2B,+BAA+B,EACtE,SAAS,QACT;AACD,SAAO;;AAGR,KAAI,CAAC,iBAAiB,EAAE;AACvB,cACC,YACC,oBACA,kDACA,EACD,SAAS,QACT;AACD,SAAO;;AAGR,KAAI,OAAO,UAAU,WAAW,aAAa,YAAY;AACxD,cACC,YACC,2BACA,mCACA,EACD,SAAS,QACT;AACD,SAAO;;AAGR,KAAI;AACH,SAAO,MAAM,UAAU,UAAU,UAAU;UACnC,OAAO;AAIf,MAFC,iBAAiB,gBAAgB,MAAM,SAAS,kBAGhD,aACC,YACC,+BACA,oCACA,MACA,EACD,SAAS,QACT;MAED,aACC,YACC,yBACA,iCACA,MACA,EACD,SAAS,QACT;AAEF,SAAO"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
//#region src/lib/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Error codes for browser utility operations.
|
|
4
|
+
*/
|
|
5
|
+
type ErrorCode = "CLIPBOARD_NOT_SUPPORTED" | "CLIPBOARD_PERMISSION_DENIED" | "CLIPBOARD_WRITE_FAILED" | "CLIPBOARD_READ_FAILED" | "INSECURE_CONTEXT";
|
|
6
|
+
/**
|
|
7
|
+
* Structured error for browser utility operations.
|
|
8
|
+
*
|
|
9
|
+
* All clipboard functions accept an optional `onError` callback
|
|
10
|
+
* that receives this type with a specific error code.
|
|
11
|
+
*/
|
|
12
|
+
interface BrowserUtilsError {
|
|
13
|
+
code: ErrorCode;
|
|
14
|
+
message: string;
|
|
15
|
+
cause?: unknown;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Callback invoked when a browser utility operation fails.
|
|
19
|
+
*
|
|
20
|
+
* @param error - Structured error with code and message
|
|
21
|
+
*/
|
|
22
|
+
type OnErrorCallback = (error: BrowserUtilsError) => void;
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/clipboard/types.d.ts
|
|
25
|
+
/**
|
|
26
|
+
* Options for clipboard operations.
|
|
27
|
+
*/
|
|
28
|
+
interface ClipboardOptions {
|
|
29
|
+
/**
|
|
30
|
+
* Callback invoked when the clipboard operation fails.
|
|
31
|
+
* Receives a structured error with a specific error code.
|
|
32
|
+
*/
|
|
33
|
+
onError?: OnErrorCallback | undefined;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/clipboard/copy.d.ts
|
|
37
|
+
/**
|
|
38
|
+
* Copy text to the clipboard using the modern Clipboard API.
|
|
39
|
+
*
|
|
40
|
+
* Requires a secure context (HTTPS or localhost) and must be called from
|
|
41
|
+
* within a user gesture handler (click, keydown, etc.).
|
|
42
|
+
*
|
|
43
|
+
* @param text - The text to copy to the clipboard
|
|
44
|
+
* @param options - Optional configuration including `onError` callback
|
|
45
|
+
* @returns `true` on success, `false` on any failure (never throws)
|
|
46
|
+
*
|
|
47
|
+
* @remarks
|
|
48
|
+
* **User gesture requirement:** Must be called synchronously within a user
|
|
49
|
+
* gesture handler. Programmatic calls from timers or microtasks will be
|
|
50
|
+
* rejected by the browser with `CLIPBOARD_PERMISSION_DENIED`.
|
|
51
|
+
*
|
|
52
|
+
* **Secure context:** Returns `false` on HTTP pages with `INSECURE_CONTEXT`
|
|
53
|
+
* error code. Use `copyToClipboardLegacy()` for HTTP environments.
|
|
54
|
+
*
|
|
55
|
+
* **Safari:** Calling any async operation before `writeText()` in the same
|
|
56
|
+
* microtask may break Safari's user activation window. Keep the call
|
|
57
|
+
* synchronous within the click handler.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* button.addEventListener('click', async () => {
|
|
62
|
+
* const success = await copyToClipboard('Hello, world!')
|
|
63
|
+
* if (!success) {
|
|
64
|
+
* showError('Copy failed — check HTTPS and permissions')
|
|
65
|
+
* }
|
|
66
|
+
* })
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
declare function copyToClipboard(text: string, options?: ClipboardOptions): Promise<boolean>;
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/clipboard/detect.d.ts
|
|
72
|
+
/**
|
|
73
|
+
* Check if the Clipboard API is available and usable in the current context.
|
|
74
|
+
*
|
|
75
|
+
* Returns `true` only when `navigator.clipboard.writeText` exists AND the
|
|
76
|
+
* page is running in a secure context (HTTPS or localhost). Returns `false`
|
|
77
|
+
* in SSR environments, on HTTP pages, or when the Clipboard API is absent.
|
|
78
|
+
*
|
|
79
|
+
* @returns `true` if clipboard write operations are supported
|
|
80
|
+
*
|
|
81
|
+
* @remarks
|
|
82
|
+
* Permission state is not checked — a `true` result does not guarantee the
|
|
83
|
+
* user has granted clipboard access. Permission denial is surfaced at call
|
|
84
|
+
* time via the `CLIPBOARD_PERMISSION_DENIED` error code on `copyToClipboard`.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* if (isClipboardSupported()) {
|
|
89
|
+
* await copyToClipboard(text)
|
|
90
|
+
* } else {
|
|
91
|
+
* copyToClipboardLegacy(text)
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
declare function isClipboardSupported(): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Check if clipboard read operations are available and usable in the current context.
|
|
98
|
+
*
|
|
99
|
+
* Returns `true` only when `navigator.clipboard.readText` exists AND the
|
|
100
|
+
* page is running in a secure context (HTTPS or localhost). Returns `false`
|
|
101
|
+
* in SSR environments, on HTTP pages, or when the read API is absent.
|
|
102
|
+
*
|
|
103
|
+
* @returns `true` if clipboard read operations are supported
|
|
104
|
+
*
|
|
105
|
+
* @remarks
|
|
106
|
+
* Permission state is not checked — a `true` result does not guarantee the
|
|
107
|
+
* user has granted clipboard read access. Permission denial is surfaced at
|
|
108
|
+
* call time via the `CLIPBOARD_PERMISSION_DENIED` error code on `readFromClipboard`.
|
|
109
|
+
*
|
|
110
|
+
* Firefox does not support the Permissions API `clipboard-read` query. This
|
|
111
|
+
* function uses synchronous feature detection only — no async permission
|
|
112
|
+
* queries are performed.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* if (isClipboardReadSupported()) {
|
|
117
|
+
* const text = await readFromClipboard()
|
|
118
|
+
* }
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
declare function isClipboardReadSupported(): boolean;
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/clipboard/fallback.d.ts
|
|
124
|
+
/**
|
|
125
|
+
* Copy text to the clipboard using the legacy `document.execCommand` API.
|
|
126
|
+
*
|
|
127
|
+
* Use this function when the modern Clipboard API is unavailable — for
|
|
128
|
+
* example, on HTTP pages (non-HTTPS) or in browsers that do not support
|
|
129
|
+
* `navigator.clipboard`. For HTTPS pages, prefer `copyToClipboard()`.
|
|
130
|
+
*
|
|
131
|
+
* @param text - The text to copy to the clipboard
|
|
132
|
+
* @param options - Optional configuration including `onError` callback
|
|
133
|
+
* @returns `true` on success, `false` on any failure (never throws)
|
|
134
|
+
*
|
|
135
|
+
* @remarks
|
|
136
|
+
* This function uses the deprecated `document.execCommand('copy')` API,
|
|
137
|
+
* which is synchronous and text-only. It temporarily creates and removes a
|
|
138
|
+
* textarea element in the DOM to perform the copy operation.
|
|
139
|
+
*
|
|
140
|
+
* **No secure context requirement:** This function works on HTTP pages.
|
|
141
|
+
* This is by design — it exists for environments where `copyToClipboard()`
|
|
142
|
+
* is unavailable due to missing secure context.
|
|
143
|
+
*
|
|
144
|
+
* **iOS Safari:** `execCommand` copy is not reliably supported on iOS.
|
|
145
|
+
* This function may return `false` on iOS Safari without a usable fallback.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* if (isClipboardSupported()) {
|
|
150
|
+
* await copyToClipboard(text)
|
|
151
|
+
* } else {
|
|
152
|
+
* const success = copyToClipboardLegacy(text)
|
|
153
|
+
* if (!success) {
|
|
154
|
+
* showManualCopyInstructions()
|
|
155
|
+
* }
|
|
156
|
+
* }
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
declare function copyToClipboardLegacy(text: string, options?: ClipboardOptions): boolean;
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/clipboard/read.d.ts
|
|
162
|
+
/**
|
|
163
|
+
* Read text from the clipboard using the modern Clipboard API.
|
|
164
|
+
*
|
|
165
|
+
* Requires a secure context (HTTPS or localhost). The browser may prompt
|
|
166
|
+
* the user for permission on the first call.
|
|
167
|
+
*
|
|
168
|
+
* @param options - Optional configuration including `onError` callback
|
|
169
|
+
* @returns The clipboard text on success, `null` on any failure (never throws)
|
|
170
|
+
*
|
|
171
|
+
* @remarks
|
|
172
|
+
* **Permission prompt:** Chrome prompts for `clipboard-read` permission on
|
|
173
|
+
* the first call. Firefox and Safari show a system-level paste prompt.
|
|
174
|
+
* Permission denial is reported via `CLIPBOARD_PERMISSION_DENIED`.
|
|
175
|
+
*
|
|
176
|
+
* **Non-text content:** If the clipboard contains only non-text content
|
|
177
|
+
* (e.g., an image), the browser throws `NotFoundError` which is reported
|
|
178
|
+
* as `CLIPBOARD_READ_FAILED`.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```ts
|
|
182
|
+
* button.addEventListener('click', async () => {
|
|
183
|
+
* const text = await readFromClipboard()
|
|
184
|
+
* if (text !== null) {
|
|
185
|
+
* input.value = text
|
|
186
|
+
* }
|
|
187
|
+
* })
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
declare function readFromClipboard(options?: ClipboardOptions): Promise<string | null>;
|
|
191
|
+
//#endregion
|
|
192
|
+
export { copyToClipboard as a, ErrorCode as c, isClipboardSupported as i, OnErrorCallback as l, copyToClipboardLegacy as n, ClipboardOptions as o, isClipboardReadSupported as r, BrowserUtilsError as s, readFromClipboard as t };
|
|
193
|
+
//# sourceMappingURL=index-B7FFRy7e.d.mts.map
|