@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ngockhoi96 contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# @ngockhoi96/ctc
|
|
2
|
+
|
|
3
|
+
Modular, tree-shakeable browser utilities library. Zero dependencies, framework-agnostic.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@ngockhoi96/ctc)
|
|
6
|
+
[](https://bundlephobia.com/package/@ngockhoi96/ctc)
|
|
7
|
+
[](https://github.com/anIcedAntFA/cttc/actions/workflows/ci.yml)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
|
|
10
|
+
> All functions are SSR-safe -- safe to import in Next.js, Nuxt, or any server-side environment without crashing.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# pnpm
|
|
16
|
+
pnpm add @ngockhoi96/ctc
|
|
17
|
+
|
|
18
|
+
# npm
|
|
19
|
+
npm install @ngockhoi96/ctc
|
|
20
|
+
|
|
21
|
+
# yarn
|
|
22
|
+
yarn add @ngockhoi96/ctc
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { copyToClipboard, isClipboardSupported } from '@ngockhoi96/ctc/clipboard'
|
|
29
|
+
|
|
30
|
+
button.addEventListener('click', async () => {
|
|
31
|
+
if (isClipboardSupported()) {
|
|
32
|
+
const success = await copyToClipboard('Hello, world!')
|
|
33
|
+
console.log(success ? 'Copied!' : 'Copy failed')
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## API Reference
|
|
39
|
+
|
|
40
|
+
### `copyToClipboard(text, options?)`
|
|
41
|
+
|
|
42
|
+
Copy text to the clipboard using the modern Clipboard API.
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
function copyToClipboard(text: string, options?: ClipboardOptions): Promise<boolean>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Returns:** `true` on success, `false` on any failure (never throws).
|
|
49
|
+
|
|
50
|
+
**Browser support:** Requires HTTPS (secure context) and a user gesture (click, keydown, etc.). Chrome 66+, Firefox 63+, Safari 13.1+.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { copyToClipboard } from '@ngockhoi96/ctc/clipboard'
|
|
54
|
+
|
|
55
|
+
button.addEventListener('click', async () => {
|
|
56
|
+
const success = await copyToClipboard('Hello, world!')
|
|
57
|
+
if (!success) {
|
|
58
|
+
showError('Copy failed -- check HTTPS and permissions')
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Remarks:**
|
|
64
|
+
|
|
65
|
+
- Must be called synchronously within a user gesture handler. Programmatic calls from timers or microtasks will be rejected by the browser with `CLIPBOARD_PERMISSION_DENIED`.
|
|
66
|
+
- Returns `false` on HTTP pages with `INSECURE_CONTEXT` error code. Use `copyToClipboardLegacy()` for HTTP environments.
|
|
67
|
+
- On Safari, calling any async operation before `writeText()` in the same microtask may break the user activation window. Keep the call synchronous within the click handler.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### `readFromClipboard(options?)`
|
|
72
|
+
|
|
73
|
+
Read text from the clipboard using the modern Clipboard API.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
function readFromClipboard(options?: ClipboardOptions): Promise<string | null>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Returns:** The clipboard text string on success, `null` on failure (never throws).
|
|
80
|
+
|
|
81
|
+
**Browser support:** Requires HTTPS (secure context) and a user gesture. May trigger a permission prompt on first call. Chrome 66+, Firefox 63+, Safari 13.1+.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { readFromClipboard } from '@ngockhoi96/ctc/clipboard'
|
|
85
|
+
|
|
86
|
+
pasteButton.addEventListener('click', async () => {
|
|
87
|
+
const text = await readFromClipboard()
|
|
88
|
+
if (text !== null) {
|
|
89
|
+
input.value = text
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Remarks:**
|
|
95
|
+
|
|
96
|
+
- Chrome prompts for `clipboard-read` permission on the first call. Firefox and Safari show a system-level paste prompt.
|
|
97
|
+
- If the clipboard contains only non-text content (e.g., an image), the read operation fails with `CLIPBOARD_READ_FAILED`.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
### `isClipboardSupported()`
|
|
102
|
+
|
|
103
|
+
Check if the Clipboard API is available and usable for write operations in the current context.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
function isClipboardSupported(): boolean
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Returns:** `true` if clipboard write operations are supported. Returns `false` in SSR environments, on HTTP pages, or when the Clipboard API is absent.
|
|
110
|
+
|
|
111
|
+
**Browser support:** Synchronous check, no permissions needed. Chrome 66+, Firefox 63+, Safari 13.1+. Returns `false` on HTTP and in SSR.
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { isClipboardSupported, copyToClipboardLegacy, copyToClipboard } from '@ngockhoi96/ctc/clipboard'
|
|
115
|
+
|
|
116
|
+
if (isClipboardSupported()) {
|
|
117
|
+
await copyToClipboard(text)
|
|
118
|
+
} else {
|
|
119
|
+
copyToClipboardLegacy(text)
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Remarks:**
|
|
124
|
+
|
|
125
|
+
- Permission state is not checked. A `true` result does not guarantee the user has granted clipboard access. Permission denial is surfaced at call time via `CLIPBOARD_PERMISSION_DENIED`.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### `isClipboardReadSupported()`
|
|
130
|
+
|
|
131
|
+
Check if clipboard read operations are available and usable in the current context.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
function isClipboardReadSupported(): boolean
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Returns:** `true` if clipboard read operations are supported. Returns `false` in SSR environments, on HTTP pages, or when the read API is absent.
|
|
138
|
+
|
|
139
|
+
**Browser support:** Synchronous check, no permissions needed. Chrome 66+, Firefox 63+, Safari 13.1+. Returns `false` on HTTP and in SSR.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { isClipboardReadSupported, readFromClipboard } from '@ngockhoi96/ctc/clipboard'
|
|
143
|
+
|
|
144
|
+
if (isClipboardReadSupported()) {
|
|
145
|
+
const text = await readFromClipboard()
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Remarks:**
|
|
150
|
+
|
|
151
|
+
- Firefox does not support the Permissions API `clipboard-read` query. This function uses synchronous feature detection only.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
### `copyToClipboardLegacy(text, options?)`
|
|
156
|
+
|
|
157
|
+
Copy text to the clipboard using the deprecated `document.execCommand('copy')` API.
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
function copyToClipboardLegacy(text: string, options?: ClipboardOptions): boolean
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Returns:** `true` on success, `false` on failure (synchronous, never throws).
|
|
164
|
+
|
|
165
|
+
**Browser support:** Works on HTTP pages via deprecated `execCommand`. Supported in all desktop browsers. Not reliable on iOS Safari. Use only when the modern Clipboard API is unavailable.
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { isClipboardSupported, copyToClipboard, copyToClipboardLegacy } from '@ngockhoi96/ctc/clipboard'
|
|
169
|
+
|
|
170
|
+
async function copyText(text: string): Promise<boolean> {
|
|
171
|
+
if (isClipboardSupported()) {
|
|
172
|
+
return copyToClipboard(text)
|
|
173
|
+
}
|
|
174
|
+
return copyToClipboardLegacy(text)
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Remarks:**
|
|
179
|
+
|
|
180
|
+
- Uses the deprecated `document.execCommand('copy')` API, which is synchronous and text-only.
|
|
181
|
+
- No secure context requirement. This function exists for environments where `copyToClipboard()` is unavailable due to HTTP.
|
|
182
|
+
- Temporarily creates and removes a textarea element in the DOM to perform the copy operation.
|
|
183
|
+
- `execCommand` copy is not reliably supported on iOS Safari. This function may return `false` on iOS Safari without a usable fallback.
|
|
184
|
+
|
|
185
|
+
## Error Handling
|
|
186
|
+
|
|
187
|
+
All clipboard functions accept an optional `onError` callback that receives a structured `BrowserUtilsError` object. Functions never throw -- they return `false` or `null` on failure.
|
|
188
|
+
|
|
189
|
+
### `BrowserUtilsError`
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
interface BrowserUtilsError {
|
|
193
|
+
code: ErrorCode
|
|
194
|
+
message: string
|
|
195
|
+
cause?: unknown
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
type ErrorCode =
|
|
199
|
+
| 'CLIPBOARD_NOT_SUPPORTED'
|
|
200
|
+
| 'CLIPBOARD_PERMISSION_DENIED'
|
|
201
|
+
| 'CLIPBOARD_WRITE_FAILED'
|
|
202
|
+
| 'CLIPBOARD_READ_FAILED'
|
|
203
|
+
| 'INSECURE_CONTEXT'
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### `ClipboardOptions`
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
interface ClipboardOptions {
|
|
210
|
+
onError?: (error: BrowserUtilsError) => void
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Error Codes
|
|
215
|
+
|
|
216
|
+
| Code | Description |
|
|
217
|
+
|------|-------------|
|
|
218
|
+
| `CLIPBOARD_NOT_SUPPORTED` | Clipboard API is unavailable (SSR environment, very old browser, or `navigator.clipboard` missing) |
|
|
219
|
+
| `INSECURE_CONTEXT` | Page is served over HTTP (not HTTPS). The Clipboard API requires a secure context. Use `copyToClipboardLegacy()` instead |
|
|
220
|
+
| `CLIPBOARD_PERMISSION_DENIED` | User denied clipboard permission, or the call was made outside a user gesture |
|
|
221
|
+
| `CLIPBOARD_WRITE_FAILED` | Write operation failed for an unexpected reason |
|
|
222
|
+
| `CLIPBOARD_READ_FAILED` | Read operation failed for an unexpected reason |
|
|
223
|
+
|
|
224
|
+
### Example: Using `onError`
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { copyToClipboard } from '@ngockhoi96/ctc/clipboard'
|
|
228
|
+
|
|
229
|
+
await copyToClipboard('text', {
|
|
230
|
+
onError: (err) => {
|
|
231
|
+
switch (err.code) {
|
|
232
|
+
case 'CLIPBOARD_PERMISSION_DENIED':
|
|
233
|
+
showPermissionPrompt()
|
|
234
|
+
break
|
|
235
|
+
case 'INSECURE_CONTEXT':
|
|
236
|
+
// Fall back to legacy method on HTTP pages
|
|
237
|
+
copyToClipboardLegacy(text)
|
|
238
|
+
break
|
|
239
|
+
default:
|
|
240
|
+
console.error('Clipboard error:', err.message)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Without `onError`, expected failures (`CLIPBOARD_NOT_SUPPORTED`, `INSECURE_CONTEXT`, `CLIPBOARD_PERMISSION_DENIED`) are logged with `console.warn`. Unexpected failures (`CLIPBOARD_WRITE_FAILED`, `CLIPBOARD_READ_FAILED`) are logged with `console.error`.
|
|
247
|
+
|
|
248
|
+
## Browser Support
|
|
249
|
+
|
|
250
|
+
| Function | Chrome | Firefox | Safari | HTTP Support |
|
|
251
|
+
|----------|--------|---------|--------|--------------|
|
|
252
|
+
| `copyToClipboard` | 66+ | 63+ | 13.1+ | No (use legacy) |
|
|
253
|
+
| `readFromClipboard` | 66+ | 63+ | 13.1+ | No |
|
|
254
|
+
| `isClipboardSupported` | 66+ | 63+ | 13.1+ | Returns `false` |
|
|
255
|
+
| `isClipboardReadSupported` | 66+ | 63+ | 13.1+ | Returns `false` |
|
|
256
|
+
| `copyToClipboardLegacy` | All | All | Partial | Yes |
|
|
257
|
+
|
|
258
|
+
All functions require ES2020+ (>95% global browser support).
|
|
259
|
+
|
|
260
|
+
All functions are SSR-safe and will return `false` or `null` when called in a server-side environment (Node.js, Deno, Bun) without crashing.
|
|
261
|
+
|
|
262
|
+
## Contributing
|
|
263
|
+
|
|
264
|
+
Contributions are welcome! Please open an [issue](https://github.com/anIcedAntFA/cttc/issues) for bug reports or feature requests.
|
|
265
|
+
|
|
266
|
+
### Development
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
# Install dependencies
|
|
270
|
+
pnpm install
|
|
271
|
+
|
|
272
|
+
# Run checks before committing
|
|
273
|
+
pnpm lint && pnpm test && pnpm build
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Commit Format
|
|
277
|
+
|
|
278
|
+
This project uses [conventional commits](https://www.conventionalcommits.org/) enforced by commitlint:
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
feat(clipboard): add new utility function
|
|
282
|
+
fix(clipboard): handle edge case in Safari
|
|
283
|
+
chore: update dependencies
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Versioning
|
|
287
|
+
|
|
288
|
+
This library follows [semantic versioning](https://semver.org/). While pre-1.0 (`0.x.y`), minor version bumps may include breaking changes. Pin to a specific minor version if stability is critical:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
pnpm add @ngockhoi96/ctc@~0.1.0
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Publishing
|
|
295
|
+
|
|
296
|
+
Releases are automated via [changesets](https://github.com/changesets/changesets). Merging a changeset to `main` triggers a Version PR. Merging the Version PR publishes to npm automatically.
|
|
297
|
+
|
|
298
|
+
Maintainers: an `NPM_TOKEN` secret must be configured in GitHub repository settings for automated npm publishing.
|
|
299
|
+
|
|
300
|
+
## License
|
|
301
|
+
|
|
302
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_clipboard = require("../clipboard-Bs9DV14p.cjs");
|
|
3
|
+
exports.copyToClipboard = require_clipboard.copyToClipboard;
|
|
4
|
+
exports.copyToClipboardLegacy = require_clipboard.copyToClipboardLegacy;
|
|
5
|
+
exports.isClipboardReadSupported = require_clipboard.isClipboardReadSupported;
|
|
6
|
+
exports.isClipboardSupported = require_clipboard.isClipboardSupported;
|
|
7
|
+
exports.readFromClipboard = require_clipboard.readFromClipboard;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as copyToClipboard, c as ErrorCode, i as isClipboardSupported, l as OnErrorCallback, n as copyToClipboardLegacy, o as ClipboardOptions, r as isClipboardReadSupported, s as BrowserUtilsError, t as readFromClipboard } from "../index-CHCHjqWe.cjs";
|
|
2
|
+
export { BrowserUtilsError, ClipboardOptions, ErrorCode, OnErrorCallback, copyToClipboard, copyToClipboardLegacy, isClipboardReadSupported, isClipboardSupported, readFromClipboard };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as copyToClipboard, c as ErrorCode, i as isClipboardSupported, l as OnErrorCallback, n as copyToClipboardLegacy, o as ClipboardOptions, r as isClipboardReadSupported, s as BrowserUtilsError, t as readFromClipboard } from "../index-B7FFRy7e.mjs";
|
|
2
|
+
export { BrowserUtilsError, ClipboardOptions, ErrorCode, OnErrorCallback, copyToClipboard, copyToClipboardLegacy, isClipboardReadSupported, isClipboardSupported, readFromClipboard };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as copyToClipboard, i as isClipboardSupported, n as copyToClipboardLegacy, r as isClipboardReadSupported, t as readFromClipboard } from "../clipboard-OTP55cvN.mjs";
|
|
2
|
+
export { copyToClipboard, copyToClipboardLegacy, isClipboardReadSupported, isClipboardSupported, readFromClipboard };
|
|
@@ -0,0 +1,338 @@
|
|
|
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
|
+
Object.defineProperty(exports, "copyToClipboard", {
|
|
308
|
+
enumerable: true,
|
|
309
|
+
get: function() {
|
|
310
|
+
return copyToClipboard;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
Object.defineProperty(exports, "copyToClipboardLegacy", {
|
|
314
|
+
enumerable: true,
|
|
315
|
+
get: function() {
|
|
316
|
+
return copyToClipboardLegacy;
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
Object.defineProperty(exports, "isClipboardReadSupported", {
|
|
320
|
+
enumerable: true,
|
|
321
|
+
get: function() {
|
|
322
|
+
return isClipboardReadSupported;
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
Object.defineProperty(exports, "isClipboardSupported", {
|
|
326
|
+
enumerable: true,
|
|
327
|
+
get: function() {
|
|
328
|
+
return isClipboardSupported;
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
Object.defineProperty(exports, "readFromClipboard", {
|
|
332
|
+
enumerable: true,
|
|
333
|
+
get: function() {
|
|
334
|
+
return readFromClipboard;
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
//# sourceMappingURL=clipboard-Bs9DV14p.cjs.map
|