@usefy/use-copy-to-clipboard 0.0.7 → 0.0.10

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/README.md ADDED
@@ -0,0 +1,498 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/geon0529/usefy/master/assets/logo.png" alt="usefy logo" width="120" />
3
+ </p>
4
+
5
+ <h1 align="center">@usefy/use-copy-to-clipboard</h1>
6
+
7
+ <p align="center">
8
+ <strong>A robust React hook for copying text to clipboard with fallback support</strong>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/@usefy/use-copy-to-clipboard">
13
+ <img src="https://img.shields.io/npm/v/@usefy/use-copy-to-clipboard.svg?style=flat-square&color=007acc" alt="npm version" />
14
+ </a>
15
+ <a href="https://www.npmjs.com/package/@usefy/use-copy-to-clipboard">
16
+ <img src="https://img.shields.io/npm/dm/@usefy/use-copy-to-clipboard.svg?style=flat-square&color=007acc" alt="npm downloads" />
17
+ </a>
18
+ <a href="https://bundlephobia.com/package/@usefy/use-copy-to-clipboard">
19
+ <img src="https://img.shields.io/bundlephobia/minzip/@usefy/use-copy-to-clipboard?style=flat-square&color=007acc" alt="bundle size" />
20
+ </a>
21
+ <a href="https://github.com/geon0529/usefy/blob/master/LICENSE">
22
+ <img src="https://img.shields.io/npm/l/@usefy/use-copy-to-clipboard.svg?style=flat-square&color=007acc" alt="license" />
23
+ </a>
24
+ </p>
25
+
26
+ <p align="center">
27
+ <a href="#installation">Installation</a> •
28
+ <a href="#quick-start">Quick Start</a> •
29
+ <a href="#api-reference">API Reference</a> •
30
+ <a href="#examples">Examples</a> •
31
+ <a href="#license">License</a>
32
+ </p>
33
+
34
+ ---
35
+
36
+ ## Overview
37
+
38
+ `@usefy/use-copy-to-clipboard` provides a simple way to copy text to the clipboard using the modern Clipboard API with automatic fallback for older browsers. Features include auto-reset timeout, success/error callbacks, and copy state tracking.
39
+
40
+ **Part of the [@usefy](https://www.npmjs.com/org/usefy) ecosystem** — a collection of production-ready React hooks designed for modern applications.
41
+
42
+ ### Why use-copy-to-clipboard?
43
+
44
+ - **Zero Dependencies** — Pure React implementation with no external dependencies
45
+ - **TypeScript First** — Full type safety with exported interfaces
46
+ - **Modern + Fallback** — Uses Clipboard API with automatic `execCommand` fallback
47
+ - **Auto Reset** — Copied state automatically resets after configurable timeout
48
+ - **Callbacks** — `onSuccess` and `onError` callbacks for custom handling
49
+ - **Async/Await** — Returns promise with boolean success indicator
50
+ - **SSR Compatible** — Works seamlessly with Next.js, Remix, and other SSR frameworks
51
+ - **Stable References** — Memoized copy function for optimal performance
52
+ - **Well Tested** — Comprehensive test coverage with Vitest
53
+
54
+ ---
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ # npm
60
+ npm install @usefy/use-copy-to-clipboard
61
+
62
+ # yarn
63
+ yarn add @usefy/use-copy-to-clipboard
64
+
65
+ # pnpm
66
+ pnpm add @usefy/use-copy-to-clipboard
67
+ ```
68
+
69
+ ### Peer Dependencies
70
+
71
+ This package requires React 18 or 19:
72
+
73
+ ```json
74
+ {
75
+ "peerDependencies": {
76
+ "react": "^18.0.0 || ^19.0.0"
77
+ }
78
+ }
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Quick Start
84
+
85
+ ```tsx
86
+ import { useCopyToClipboard } from '@usefy/use-copy-to-clipboard';
87
+
88
+ function CopyButton() {
89
+ const [copiedText, copy] = useCopyToClipboard();
90
+
91
+ return (
92
+ <button onClick={() => copy('Hello World!')}>
93
+ {copiedText ? 'Copied!' : 'Copy'}
94
+ </button>
95
+ );
96
+ }
97
+ ```
98
+
99
+ ---
100
+
101
+ ## API Reference
102
+
103
+ ### `useCopyToClipboard(options?)`
104
+
105
+ A hook that provides clipboard copy functionality with state tracking.
106
+
107
+ #### Parameters
108
+
109
+ | Parameter | Type | Description |
110
+ |-----------|------|-------------|
111
+ | `options` | `UseCopyToClipboardOptions` | Configuration options |
112
+
113
+ #### Options
114
+
115
+ | Option | Type | Default | Description |
116
+ |--------|------|---------|-------------|
117
+ | `timeout` | `number` | `2000` | Time in ms before `copiedText` resets to null. Set to `0` to disable. |
118
+ | `onSuccess` | `(text: string) => void` | — | Callback called when copy succeeds |
119
+ | `onError` | `(error: Error) => void` | — | Callback called when copy fails |
120
+
121
+ #### Returns `[copiedText, copy]`
122
+
123
+ | Index | Type | Description |
124
+ |-------|------|-------------|
125
+ | `[0]` | `string \| null` | The last successfully copied text, or `null` |
126
+ | `[1]` | `(text: string) => Promise<boolean>` | Async function to copy text |
127
+
128
+ ---
129
+
130
+ ## Examples
131
+
132
+ ### Basic Copy Button
133
+
134
+ ```tsx
135
+ import { useCopyToClipboard } from '@usefy/use-copy-to-clipboard';
136
+
137
+ function CopyButton({ text }: { text: string }) {
138
+ const [copiedText, copy] = useCopyToClipboard();
139
+
140
+ return (
141
+ <button onClick={() => copy(text)}>
142
+ {copiedText === text ? 'Copied!' : 'Copy to Clipboard'}
143
+ </button>
144
+ );
145
+ }
146
+ ```
147
+
148
+ ### Copy with Visual Feedback
149
+
150
+ ```tsx
151
+ import { useCopyToClipboard } from '@usefy/use-copy-to-clipboard';
152
+
153
+ function CopyWithIcon({ text }: { text: string }) {
154
+ const [copiedText, copy] = useCopyToClipboard();
155
+ const isCopied = copiedText === text;
156
+
157
+ return (
158
+ <button
159
+ onClick={() => copy(text)}
160
+ className={isCopied ? 'copied' : ''}
161
+ >
162
+ {isCopied ? (
163
+ <CheckIcon className="icon" />
164
+ ) : (
165
+ <CopyIcon className="icon" />
166
+ )}
167
+ {isCopied ? 'Copied!' : 'Copy'}
168
+ </button>
169
+ );
170
+ }
171
+ ```
172
+
173
+ ### Code Block with Copy
174
+
175
+ ```tsx
176
+ import { useCopyToClipboard } from '@usefy/use-copy-to-clipboard';
177
+
178
+ function CodeBlock({ code, language }: { code: string; language: string }) {
179
+ const [copiedText, copy] = useCopyToClipboard();
180
+
181
+ return (
182
+ <div className="code-block">
183
+ <div className="code-header">
184
+ <span>{language}</span>
185
+ <button onClick={() => copy(code)}>
186
+ {copiedText === code ? 'Copied!' : 'Copy Code'}
187
+ </button>
188
+ </div>
189
+ <pre>
190
+ <code>{code}</code>
191
+ </pre>
192
+ </div>
193
+ );
194
+ }
195
+ ```
196
+
197
+ ### Custom Timeout
198
+
199
+ ```tsx
200
+ import { useCopyToClipboard } from '@usefy/use-copy-to-clipboard';
201
+
202
+ function LongFeedbackCopy() {
203
+ // Show "Copied!" for 5 seconds
204
+ const [copiedText, copy] = useCopyToClipboard({ timeout: 5000 });
205
+
206
+ return (
207
+ <button onClick={() => copy('Long feedback!')}>
208
+ {copiedText ? 'Copied!' : 'Copy'}
209
+ </button>
210
+ );
211
+ }
212
+ ```
213
+
214
+ ### Persistent Copied State
215
+
216
+ ```tsx
217
+ import { useCopyToClipboard } from '@usefy/use-copy-to-clipboard';
218
+
219
+ function PersistentCopy() {
220
+ // Never auto-reset the copied state
221
+ const [copiedText, copy] = useCopyToClipboard({ timeout: 0 });
222
+
223
+ return (
224
+ <div>
225
+ <button onClick={() => copy('Persistent!')}>
226
+ {copiedText ? 'Copied!' : 'Copy'}
227
+ </button>
228
+ {copiedText && <span>Copied text: {copiedText}</span>}
229
+ </div>
230
+ );
231
+ }
232
+ ```
233
+
234
+ ### With Callbacks
235
+
236
+ ```tsx
237
+ import { useCopyToClipboard } from '@usefy/use-copy-to-clipboard';
238
+ import { toast } from 'your-toast-library';
239
+
240
+ function CopyWithToast({ text }: { text: string }) {
241
+ const [, copy] = useCopyToClipboard({
242
+ onSuccess: (copiedText) => {
243
+ toast.success(`Copied: ${copiedText}`);
244
+ },
245
+ onError: (error) => {
246
+ toast.error(`Failed to copy: ${error.message}`);
247
+ },
248
+ });
249
+
250
+ return <button onClick={() => copy(text)}>Copy</button>;
251
+ }
252
+ ```
253
+
254
+ ### Async Handling
255
+
256
+ ```tsx
257
+ import { useCopyToClipboard } from '@usefy/use-copy-to-clipboard';
258
+
259
+ function AsyncCopy({ text }: { text: string }) {
260
+ const [, copy] = useCopyToClipboard();
261
+ const [status, setStatus] = useState<'idle' | 'success' | 'error'>('idle');
262
+
263
+ const handleCopy = async () => {
264
+ const success = await copy(text);
265
+ setStatus(success ? 'success' : 'error');
266
+ };
267
+
268
+ return (
269
+ <div>
270
+ <button onClick={handleCopy}>Copy</button>
271
+ {status === 'success' && <span className="success">Copied successfully!</span>}
272
+ {status === 'error' && <span className="error">Failed to copy</span>}
273
+ </div>
274
+ );
275
+ }
276
+ ```
277
+
278
+ ### Share URL Button
279
+
280
+ ```tsx
281
+ import { useCopyToClipboard } from '@usefy/use-copy-to-clipboard';
282
+
283
+ function ShareButton() {
284
+ const [copiedText, copy] = useCopyToClipboard();
285
+
286
+ const handleShare = () => {
287
+ copy(window.location.href);
288
+ };
289
+
290
+ return (
291
+ <button onClick={handleShare}>
292
+ {copiedText ? 'Link Copied!' : 'Share Link'}
293
+ </button>
294
+ );
295
+ }
296
+ ```
297
+
298
+ ### Copy Multiple Items
299
+
300
+ ```tsx
301
+ import { useCopyToClipboard } from '@usefy/use-copy-to-clipboard';
302
+
303
+ function CopyList({ items }: { items: string[] }) {
304
+ const [copiedText, copy] = useCopyToClipboard();
305
+
306
+ return (
307
+ <ul>
308
+ {items.map((item) => (
309
+ <li key={item}>
310
+ <span>{item}</span>
311
+ <button onClick={() => copy(item)}>
312
+ {copiedText === item ? 'Copied!' : 'Copy'}
313
+ </button>
314
+ </li>
315
+ ))}
316
+ </ul>
317
+ );
318
+ }
319
+ ```
320
+
321
+ ### API Key Display
322
+
323
+ ```tsx
324
+ import { useCopyToClipboard } from '@usefy/use-copy-to-clipboard';
325
+
326
+ function ApiKeyDisplay({ apiKey }: { apiKey: string }) {
327
+ const [copiedText, copy] = useCopyToClipboard();
328
+
329
+ const maskedKey = `${apiKey.slice(0, 4)}${'*'.repeat(20)}${apiKey.slice(-4)}`;
330
+
331
+ return (
332
+ <div className="api-key">
333
+ <code>{maskedKey}</code>
334
+ <button onClick={() => copy(apiKey)}>
335
+ {copiedText === apiKey ? 'Copied!' : 'Copy Key'}
336
+ </button>
337
+ </div>
338
+ );
339
+ }
340
+ ```
341
+
342
+ ---
343
+
344
+ ## TypeScript
345
+
346
+ This hook is written in TypeScript with exported types.
347
+
348
+ ```tsx
349
+ import {
350
+ useCopyToClipboard,
351
+ type UseCopyToClipboardOptions,
352
+ type UseCopyToClipboardReturn,
353
+ type CopyFn,
354
+ } from '@usefy/use-copy-to-clipboard';
355
+
356
+ // Return type
357
+ const [copiedText, copy]: UseCopyToClipboardReturn = useCopyToClipboard();
358
+
359
+ // copiedText: string | null
360
+ // copy: (text: string) => Promise<boolean>
361
+
362
+ // Options type
363
+ const options: UseCopyToClipboardOptions = {
364
+ timeout: 3000,
365
+ onSuccess: (text) => console.log('Copied:', text),
366
+ onError: (error) => console.error('Error:', error),
367
+ };
368
+ ```
369
+
370
+ ---
371
+
372
+ ## Browser Support
373
+
374
+ This hook uses the modern [Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API) when available, with automatic fallback to `document.execCommand('copy')` for older browsers.
375
+
376
+ | Browser | Clipboard API | Fallback |
377
+ |---------|---------------|----------|
378
+ | Chrome 66+ | Yes | - |
379
+ | Firefox 63+ | Yes | - |
380
+ | Safari 13.1+ | Yes | - |
381
+ | Edge 79+ | Yes | - |
382
+ | IE 11 | No | Yes |
383
+ | Older browsers | No | Yes |
384
+
385
+ ---
386
+
387
+ ## Testing
388
+
389
+ This package maintains comprehensive test coverage to ensure reliability and stability.
390
+
391
+ ### Test Coverage
392
+
393
+ | Category | Tests | Coverage |
394
+ |----------|-------|----------|
395
+ | Initialization | 3 | 100% |
396
+ | Copy Functionality | 5 | 100% |
397
+ | Copy Failure | 3 | 100% |
398
+ | Timeout/Auto-Reset | 4 | 100% |
399
+ | Callbacks | 4 | 100% |
400
+ | Function Stability | 3 | 100% |
401
+ | Cleanup | 2 | 100% |
402
+ | Multiple Instances | 2 | 100% |
403
+ | Fallback Behavior | 2 | 100% |
404
+ | Edge Cases | 4 | 100% |
405
+ | **Total** | **32** | **87.87%** |
406
+
407
+ ### Test Categories
408
+
409
+ <details>
410
+ <summary><strong>Copy Functionality Tests</strong></summary>
411
+
412
+ - Copy text successfully using Clipboard API
413
+ - Update copiedText after successful copy
414
+ - Return true on successful copy
415
+ - Handle empty string
416
+ - Handle special characters
417
+
418
+ </details>
419
+
420
+ <details>
421
+ <summary><strong>Timeout Tests</strong></summary>
422
+
423
+ - Reset copiedText after default timeout (2000ms)
424
+ - Reset after custom timeout
425
+ - Not reset when timeout is 0
426
+ - Reset timer on consecutive copies
427
+
428
+ </details>
429
+
430
+ <details>
431
+ <summary><strong>Fallback Tests</strong></summary>
432
+
433
+ - Use fallback when Clipboard API is not available
434
+ - Try fallback when Clipboard API throws error
435
+
436
+ </details>
437
+
438
+ ### Running Tests
439
+
440
+ ```bash
441
+ # Run all tests
442
+ pnpm test
443
+
444
+ # Run tests in watch mode
445
+ pnpm test:watch
446
+
447
+ # Run tests with coverage report
448
+ pnpm test --coverage
449
+ ```
450
+
451
+ ---
452
+
453
+ ## Related Packages
454
+
455
+ Explore other hooks in the **@usefy** collection:
456
+
457
+ | Package | Description |
458
+ |---------|-------------|
459
+ | [@usefy/use-local-storage](https://www.npmjs.com/package/@usefy/use-local-storage) | Persistent localStorage |
460
+ | [@usefy/use-session-storage](https://www.npmjs.com/package/@usefy/use-session-storage) | Session storage persistence |
461
+ | [@usefy/use-toggle](https://www.npmjs.com/package/@usefy/use-toggle) | Boolean state management |
462
+ | [@usefy/use-counter](https://www.npmjs.com/package/@usefy/use-counter) | Counter state management |
463
+ | [@usefy/use-debounce](https://www.npmjs.com/package/@usefy/use-debounce) | Value debouncing |
464
+ | [@usefy/use-click-any-where](https://www.npmjs.com/package/@usefy/use-click-any-where) | Global click detection |
465
+
466
+ ---
467
+
468
+ ## Contributing
469
+
470
+ We welcome contributions! Please see our [Contributing Guide](https://github.com/geon0529/usefy/blob/master/CONTRIBUTING.md) for details.
471
+
472
+ ```bash
473
+ # Clone the repository
474
+ git clone https://github.com/geon0529/usefy.git
475
+
476
+ # Install dependencies
477
+ pnpm install
478
+
479
+ # Run tests
480
+ pnpm test
481
+
482
+ # Build
483
+ pnpm build
484
+ ```
485
+
486
+ ---
487
+
488
+ ## License
489
+
490
+ MIT © [mirunamu](https://github.com/geon0529)
491
+
492
+ This package is part of the [usefy](https://github.com/geon0529/usefy) monorepo.
493
+
494
+ ---
495
+
496
+ <p align="center">
497
+ <sub>Built with care by the usefy team</sub>
498
+ </p>
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/useCopyToClipboard.ts"],"sourcesContent":["export {\r\n useCopyToClipboard,\r\n type UseCopyToClipboardOptions,\r\n type UseCopyToClipboardReturn,\r\n type CopyFn,\r\n} from \"./useCopyToClipboard\";\r\n","import { useCallback, useEffect, useRef, useState } from \"react\";\r\n\r\n/**\r\n * Options for useCopyToClipboard hook\r\n */\r\nexport interface UseCopyToClipboardOptions {\r\n /**\r\n * Time in milliseconds before the copied state resets to null.\r\n * Set to 0 to disable auto-reset.\r\n * @default 2000\r\n */\r\n timeout?: number;\r\n /**\r\n * Callback function called when copy succeeds\r\n * @param text - The text that was copied\r\n */\r\n onSuccess?: (text: string) => void;\r\n /**\r\n * Callback function called when copy fails\r\n * @param error - The error that occurred\r\n */\r\n onError?: (error: Error) => void;\r\n}\r\n\r\n/**\r\n * Type for the copy function\r\n */\r\nexport type CopyFn = (text: string) => Promise<boolean>;\r\n\r\n/**\r\n * Return type for useCopyToClipboard hook\r\n * Tuple format: [copiedText, copy]\r\n */\r\nexport type UseCopyToClipboardReturn = [\r\n copiedText: string | null,\r\n copy: CopyFn\r\n];\r\n\r\n/**\r\n * Fallback copy function for browsers that don't support the Clipboard API\r\n * @param text - Text to copy to clipboard\r\n * @returns Whether the copy was successful\r\n */\r\nfunction fallbackCopyToClipboard(text: string): boolean {\r\n // Check if we're in a browser environment\r\n if (typeof document === \"undefined\") {\r\n return false;\r\n }\r\n\r\n const textarea = document.createElement(\"textarea\");\r\n textarea.value = text;\r\n\r\n // Make the textarea invisible but still functional\r\n textarea.style.cssText =\r\n \"position:fixed;left:-9999px;top:-9999px;opacity:0;pointer-events:none\";\r\n textarea.setAttribute(\"readonly\", \"\");\r\n textarea.setAttribute(\"aria-hidden\", \"true\");\r\n\r\n document.body.appendChild(textarea);\r\n\r\n // Select the text\r\n textarea.focus();\r\n textarea.select();\r\n\r\n // For mobile devices\r\n textarea.setSelectionRange(0, text.length);\r\n\r\n let success = false;\r\n try {\r\n success = document.execCommand(\"copy\");\r\n } catch {\r\n success = false;\r\n }\r\n\r\n document.body.removeChild(textarea);\r\n return success;\r\n}\r\n\r\n/**\r\n * Copies text to clipboard using the Clipboard API with fallback support.\r\n * Returns the copied text (or null if not copied) and a copy function.\r\n *\r\n * @param options - Configuration options for the hook\r\n * @returns Tuple of [copiedText, copy]\r\n *\r\n * @example\r\n * ```tsx\r\n * function CopyButton() {\r\n * const [copiedText, copy] = useCopyToClipboard();\r\n *\r\n * return (\r\n * <button onClick={() => copy(\"Hello World\")}>\r\n * {copiedText ? \"Copied!\" : \"Copy\"}\r\n * </button>\r\n * );\r\n * }\r\n * ```\r\n *\r\n * @example\r\n * ```tsx\r\n * // With custom timeout\r\n * const [copiedText, copy] = useCopyToClipboard({ timeout: 3000 });\r\n * ```\r\n *\r\n * @example\r\n * ```tsx\r\n * // With callbacks\r\n * const [copiedText, copy] = useCopyToClipboard({\r\n * onSuccess: (text) => console.log(`Copied: ${text}`),\r\n * onError: (error) => console.error(`Failed to copy: ${error.message}`),\r\n * });\r\n * ```\r\n *\r\n * @example\r\n * ```tsx\r\n * // Disable auto-reset\r\n * const [copiedText, copy] = useCopyToClipboard({ timeout: 0 });\r\n * ```\r\n */\r\nexport function useCopyToClipboard(\r\n options: UseCopyToClipboardOptions = {}\r\n): UseCopyToClipboardReturn {\r\n const { timeout = 2000, onSuccess, onError } = options;\r\n\r\n const [copiedText, setCopiedText] = useState<string | null>(null);\r\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(\r\n undefined\r\n );\r\n\r\n // Store callbacks in refs to avoid dependency issues\r\n const onSuccessRef = useRef(onSuccess);\r\n const onErrorRef = useRef(onError);\r\n const timeoutValueRef = useRef(timeout);\r\n\r\n // Update refs when options change\r\n onSuccessRef.current = onSuccess;\r\n onErrorRef.current = onError;\r\n timeoutValueRef.current = timeout;\r\n\r\n // Cleanup timeout on unmount\r\n useEffect(() => {\r\n return () => {\r\n if (timeoutRef.current !== undefined) {\r\n clearTimeout(timeoutRef.current);\r\n }\r\n };\r\n }, []);\r\n\r\n const copy: CopyFn = useCallback(async (text: string): Promise<boolean> => {\r\n // Clear any existing timeout\r\n if (timeoutRef.current !== undefined) {\r\n clearTimeout(timeoutRef.current);\r\n timeoutRef.current = undefined;\r\n }\r\n\r\n // Check for SSR\r\n if (typeof window === \"undefined\") {\r\n const error = new Error(\"Clipboard is not available in this environment\");\r\n onErrorRef.current?.(error);\r\n return false;\r\n }\r\n\r\n try {\r\n // Try the modern Clipboard API first\r\n if (navigator.clipboard && typeof navigator.clipboard.writeText === \"function\") {\r\n await navigator.clipboard.writeText(text);\r\n } else {\r\n // Fall back to execCommand\r\n const success = fallbackCopyToClipboard(text);\r\n if (!success) {\r\n throw new Error(\"Failed to copy text using fallback method\");\r\n }\r\n }\r\n\r\n // Success\r\n setCopiedText(text);\r\n onSuccessRef.current?.(text);\r\n\r\n // Set timeout for auto-reset if enabled\r\n if (timeoutValueRef.current > 0) {\r\n timeoutRef.current = setTimeout(() => {\r\n setCopiedText(null);\r\n timeoutRef.current = undefined;\r\n }, timeoutValueRef.current);\r\n }\r\n\r\n return true;\r\n } catch (err) {\r\n // Try fallback if Clipboard API failed\r\n try {\r\n const success = fallbackCopyToClipboard(text);\r\n if (success) {\r\n setCopiedText(text);\r\n onSuccessRef.current?.(text);\r\n\r\n if (timeoutValueRef.current > 0) {\r\n timeoutRef.current = setTimeout(() => {\r\n setCopiedText(null);\r\n timeoutRef.current = undefined;\r\n }, timeoutValueRef.current);\r\n }\r\n\r\n return true;\r\n }\r\n } catch {\r\n // Fallback also failed\r\n }\r\n\r\n // Both methods failed\r\n const error =\r\n err instanceof Error ? err : new Error(\"Failed to copy text to clipboard\");\r\n setCopiedText(null);\r\n onErrorRef.current?.(error);\r\n return false;\r\n }\r\n }, []);\r\n\r\n return [copiedText, copy];\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAAyD;AA2CzD,SAAS,wBAAwB,MAAuB;AAEtD,MAAI,OAAO,aAAa,aAAa;AACnC,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,SAAS,cAAc,UAAU;AAClD,WAAS,QAAQ;AAGjB,WAAS,MAAM,UACb;AACF,WAAS,aAAa,YAAY,EAAE;AACpC,WAAS,aAAa,eAAe,MAAM;AAE3C,WAAS,KAAK,YAAY,QAAQ;AAGlC,WAAS,MAAM;AACf,WAAS,OAAO;AAGhB,WAAS,kBAAkB,GAAG,KAAK,MAAM;AAEzC,MAAI,UAAU;AACd,MAAI;AACF,cAAU,SAAS,YAAY,MAAM;AAAA,EACvC,QAAQ;AACN,cAAU;AAAA,EACZ;AAEA,WAAS,KAAK,YAAY,QAAQ;AAClC,SAAO;AACT;AA2CO,SAAS,mBACd,UAAqC,CAAC,GACZ;AAC1B,QAAM,EAAE,UAAU,KAAM,WAAW,QAAQ,IAAI;AAE/C,QAAM,CAAC,YAAY,aAAa,QAAI,uBAAwB,IAAI;AAChE,QAAM,iBAAa;AAAA,IACjB;AAAA,EACF;AAGA,QAAM,mBAAe,qBAAO,SAAS;AACrC,QAAM,iBAAa,qBAAO,OAAO;AACjC,QAAM,sBAAkB,qBAAO,OAAO;AAGtC,eAAa,UAAU;AACvB,aAAW,UAAU;AACrB,kBAAgB,UAAU;AAG1B,8BAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,WAAW,YAAY,QAAW;AACpC,qBAAa,WAAW,OAAO;AAAA,MACjC;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,WAAe,0BAAY,OAAO,SAAmC;AAEzE,QAAI,WAAW,YAAY,QAAW;AACpC,mBAAa,WAAW,OAAO;AAC/B,iBAAW,UAAU;AAAA,IACvB;AAGA,QAAI,OAAO,WAAW,aAAa;AACjC,YAAM,QAAQ,IAAI,MAAM,gDAAgD;AACxE,iBAAW,UAAU,KAAK;AAC1B,aAAO;AAAA,IACT;AAEA,QAAI;AAEF,UAAI,UAAU,aAAa,OAAO,UAAU,UAAU,cAAc,YAAY;AAC9E,cAAM,UAAU,UAAU,UAAU,IAAI;AAAA,MAC1C,OAAO;AAEL,cAAM,UAAU,wBAAwB,IAAI;AAC5C,YAAI,CAAC,SAAS;AACZ,gBAAM,IAAI,MAAM,2CAA2C;AAAA,QAC7D;AAAA,MACF;AAGA,oBAAc,IAAI;AAClB,mBAAa,UAAU,IAAI;AAG3B,UAAI,gBAAgB,UAAU,GAAG;AAC/B,mBAAW,UAAU,WAAW,MAAM;AACpC,wBAAc,IAAI;AAClB,qBAAW,UAAU;AAAA,QACvB,GAAG,gBAAgB,OAAO;AAAA,MAC5B;AAEA,aAAO;AAAA,IACT,SAAS,KAAK;AAEZ,UAAI;AACF,cAAM,UAAU,wBAAwB,IAAI;AAC5C,YAAI,SAAS;AACX,wBAAc,IAAI;AAClB,uBAAa,UAAU,IAAI;AAE3B,cAAI,gBAAgB,UAAU,GAAG;AAC/B,uBAAW,UAAU,WAAW,MAAM;AACpC,4BAAc,IAAI;AAClB,yBAAW,UAAU;AAAA,YACvB,GAAG,gBAAgB,OAAO;AAAA,UAC5B;AAEA,iBAAO;AAAA,QACT;AAAA,MACF,QAAQ;AAAA,MAER;AAGA,YAAM,QACJ,eAAe,QAAQ,MAAM,IAAI,MAAM,kCAAkC;AAC3E,oBAAc,IAAI;AAClB,iBAAW,UAAU,KAAK;AAC1B,aAAO;AAAA,IACT;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SAAO,CAAC,YAAY,IAAI;AAC1B;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/useCopyToClipboard.ts"],"sourcesContent":["export {\n useCopyToClipboard,\n type UseCopyToClipboardOptions,\n type UseCopyToClipboardReturn,\n type CopyFn,\n} from \"./useCopyToClipboard\";\n","import { useCallback, useEffect, useRef, useState } from \"react\";\n\n/**\n * Options for useCopyToClipboard hook\n */\nexport interface UseCopyToClipboardOptions {\n /**\n * Time in milliseconds before the copied state resets to null.\n * Set to 0 to disable auto-reset.\n * @default 2000\n */\n timeout?: number;\n /**\n * Callback function called when copy succeeds\n * @param text - The text that was copied\n */\n onSuccess?: (text: string) => void;\n /**\n * Callback function called when copy fails\n * @param error - The error that occurred\n */\n onError?: (error: Error) => void;\n}\n\n/**\n * Type for the copy function\n */\nexport type CopyFn = (text: string) => Promise<boolean>;\n\n/**\n * Return type for useCopyToClipboard hook\n * Tuple format: [copiedText, copy]\n */\nexport type UseCopyToClipboardReturn = [\n copiedText: string | null,\n copy: CopyFn\n];\n\n/**\n * Fallback copy function for browsers that don't support the Clipboard API\n * @param text - Text to copy to clipboard\n * @returns Whether the copy was successful\n */\nfunction fallbackCopyToClipboard(text: string): boolean {\n // Check if we're in a browser environment\n if (typeof document === \"undefined\") {\n return false;\n }\n\n const textarea = document.createElement(\"textarea\");\n textarea.value = text;\n\n // Make the textarea invisible but still functional\n textarea.style.cssText =\n \"position:fixed;left:-9999px;top:-9999px;opacity:0;pointer-events:none\";\n textarea.setAttribute(\"readonly\", \"\");\n textarea.setAttribute(\"aria-hidden\", \"true\");\n\n document.body.appendChild(textarea);\n\n // Select the text\n textarea.focus();\n textarea.select();\n\n // For mobile devices\n textarea.setSelectionRange(0, text.length);\n\n let success = false;\n try {\n success = document.execCommand(\"copy\");\n } catch {\n success = false;\n }\n\n document.body.removeChild(textarea);\n return success;\n}\n\n/**\n * Copies text to clipboard using the Clipboard API with fallback support.\n * Returns the copied text (or null if not copied) and a copy function.\n *\n * @param options - Configuration options for the hook\n * @returns Tuple of [copiedText, copy]\n *\n * @example\n * ```tsx\n * function CopyButton() {\n * const [copiedText, copy] = useCopyToClipboard();\n *\n * return (\n * <button onClick={() => copy(\"Hello World\")}>\n * {copiedText ? \"Copied!\" : \"Copy\"}\n * </button>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With custom timeout\n * const [copiedText, copy] = useCopyToClipboard({ timeout: 3000 });\n * ```\n *\n * @example\n * ```tsx\n * // With callbacks\n * const [copiedText, copy] = useCopyToClipboard({\n * onSuccess: (text) => console.log(`Copied: ${text}`),\n * onError: (error) => console.error(`Failed to copy: ${error.message}`),\n * });\n * ```\n *\n * @example\n * ```tsx\n * // Disable auto-reset\n * const [copiedText, copy] = useCopyToClipboard({ timeout: 0 });\n * ```\n */\nexport function useCopyToClipboard(\n options: UseCopyToClipboardOptions = {}\n): UseCopyToClipboardReturn {\n const { timeout = 2000, onSuccess, onError } = options;\n\n const [copiedText, setCopiedText] = useState<string | null>(null);\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(\n undefined\n );\n\n // Store callbacks in refs to avoid dependency issues\n const onSuccessRef = useRef(onSuccess);\n const onErrorRef = useRef(onError);\n const timeoutValueRef = useRef(timeout);\n\n // Update refs when options change\n onSuccessRef.current = onSuccess;\n onErrorRef.current = onError;\n timeoutValueRef.current = timeout;\n\n // Cleanup timeout on unmount\n useEffect(() => {\n return () => {\n if (timeoutRef.current !== undefined) {\n clearTimeout(timeoutRef.current);\n }\n };\n }, []);\n\n const copy: CopyFn = useCallback(async (text: string): Promise<boolean> => {\n // Clear any existing timeout\n if (timeoutRef.current !== undefined) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = undefined;\n }\n\n // Check for SSR\n if (typeof window === \"undefined\") {\n const error = new Error(\"Clipboard is not available in this environment\");\n onErrorRef.current?.(error);\n return false;\n }\n\n try {\n // Try the modern Clipboard API first\n if (navigator.clipboard && typeof navigator.clipboard.writeText === \"function\") {\n await navigator.clipboard.writeText(text);\n } else {\n // Fall back to execCommand\n const success = fallbackCopyToClipboard(text);\n if (!success) {\n throw new Error(\"Failed to copy text using fallback method\");\n }\n }\n\n // Success\n setCopiedText(text);\n onSuccessRef.current?.(text);\n\n // Set timeout for auto-reset if enabled\n if (timeoutValueRef.current > 0) {\n timeoutRef.current = setTimeout(() => {\n setCopiedText(null);\n timeoutRef.current = undefined;\n }, timeoutValueRef.current);\n }\n\n return true;\n } catch (err) {\n // Try fallback if Clipboard API failed\n try {\n const success = fallbackCopyToClipboard(text);\n if (success) {\n setCopiedText(text);\n onSuccessRef.current?.(text);\n\n if (timeoutValueRef.current > 0) {\n timeoutRef.current = setTimeout(() => {\n setCopiedText(null);\n timeoutRef.current = undefined;\n }, timeoutValueRef.current);\n }\n\n return true;\n }\n } catch {\n // Fallback also failed\n }\n\n // Both methods failed\n const error =\n err instanceof Error ? err : new Error(\"Failed to copy text to clipboard\");\n setCopiedText(null);\n onErrorRef.current?.(error);\n return false;\n }\n }, []);\n\n return [copiedText, copy];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAAyD;AA2CzD,SAAS,wBAAwB,MAAuB;AAEtD,MAAI,OAAO,aAAa,aAAa;AACnC,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,SAAS,cAAc,UAAU;AAClD,WAAS,QAAQ;AAGjB,WAAS,MAAM,UACb;AACF,WAAS,aAAa,YAAY,EAAE;AACpC,WAAS,aAAa,eAAe,MAAM;AAE3C,WAAS,KAAK,YAAY,QAAQ;AAGlC,WAAS,MAAM;AACf,WAAS,OAAO;AAGhB,WAAS,kBAAkB,GAAG,KAAK,MAAM;AAEzC,MAAI,UAAU;AACd,MAAI;AACF,cAAU,SAAS,YAAY,MAAM;AAAA,EACvC,QAAQ;AACN,cAAU;AAAA,EACZ;AAEA,WAAS,KAAK,YAAY,QAAQ;AAClC,SAAO;AACT;AA2CO,SAAS,mBACd,UAAqC,CAAC,GACZ;AAC1B,QAAM,EAAE,UAAU,KAAM,WAAW,QAAQ,IAAI;AAE/C,QAAM,CAAC,YAAY,aAAa,QAAI,uBAAwB,IAAI;AAChE,QAAM,iBAAa;AAAA,IACjB;AAAA,EACF;AAGA,QAAM,mBAAe,qBAAO,SAAS;AACrC,QAAM,iBAAa,qBAAO,OAAO;AACjC,QAAM,sBAAkB,qBAAO,OAAO;AAGtC,eAAa,UAAU;AACvB,aAAW,UAAU;AACrB,kBAAgB,UAAU;AAG1B,8BAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,WAAW,YAAY,QAAW;AACpC,qBAAa,WAAW,OAAO;AAAA,MACjC;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,WAAe,0BAAY,OAAO,SAAmC;AAEzE,QAAI,WAAW,YAAY,QAAW;AACpC,mBAAa,WAAW,OAAO;AAC/B,iBAAW,UAAU;AAAA,IACvB;AAGA,QAAI,OAAO,WAAW,aAAa;AACjC,YAAM,QAAQ,IAAI,MAAM,gDAAgD;AACxE,iBAAW,UAAU,KAAK;AAC1B,aAAO;AAAA,IACT;AAEA,QAAI;AAEF,UAAI,UAAU,aAAa,OAAO,UAAU,UAAU,cAAc,YAAY;AAC9E,cAAM,UAAU,UAAU,UAAU,IAAI;AAAA,MAC1C,OAAO;AAEL,cAAM,UAAU,wBAAwB,IAAI;AAC5C,YAAI,CAAC,SAAS;AACZ,gBAAM,IAAI,MAAM,2CAA2C;AAAA,QAC7D;AAAA,MACF;AAGA,oBAAc,IAAI;AAClB,mBAAa,UAAU,IAAI;AAG3B,UAAI,gBAAgB,UAAU,GAAG;AAC/B,mBAAW,UAAU,WAAW,MAAM;AACpC,wBAAc,IAAI;AAClB,qBAAW,UAAU;AAAA,QACvB,GAAG,gBAAgB,OAAO;AAAA,MAC5B;AAEA,aAAO;AAAA,IACT,SAAS,KAAK;AAEZ,UAAI;AACF,cAAM,UAAU,wBAAwB,IAAI;AAC5C,YAAI,SAAS;AACX,wBAAc,IAAI;AAClB,uBAAa,UAAU,IAAI;AAE3B,cAAI,gBAAgB,UAAU,GAAG;AAC/B,uBAAW,UAAU,WAAW,MAAM;AACpC,4BAAc,IAAI;AAClB,yBAAW,UAAU;AAAA,YACvB,GAAG,gBAAgB,OAAO;AAAA,UAC5B;AAEA,iBAAO;AAAA,QACT;AAAA,MACF,QAAQ;AAAA,MAER;AAGA,YAAM,QACJ,eAAe,QAAQ,MAAM,IAAI,MAAM,kCAAkC;AAC3E,oBAAc,IAAI;AAClB,iBAAW,UAAU,KAAK;AAC1B,aAAO;AAAA,IACT;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SAAO,CAAC,YAAY,IAAI;AAC1B;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/useCopyToClipboard.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\r\n\r\n/**\r\n * Options for useCopyToClipboard hook\r\n */\r\nexport interface UseCopyToClipboardOptions {\r\n /**\r\n * Time in milliseconds before the copied state resets to null.\r\n * Set to 0 to disable auto-reset.\r\n * @default 2000\r\n */\r\n timeout?: number;\r\n /**\r\n * Callback function called when copy succeeds\r\n * @param text - The text that was copied\r\n */\r\n onSuccess?: (text: string) => void;\r\n /**\r\n * Callback function called when copy fails\r\n * @param error - The error that occurred\r\n */\r\n onError?: (error: Error) => void;\r\n}\r\n\r\n/**\r\n * Type for the copy function\r\n */\r\nexport type CopyFn = (text: string) => Promise<boolean>;\r\n\r\n/**\r\n * Return type for useCopyToClipboard hook\r\n * Tuple format: [copiedText, copy]\r\n */\r\nexport type UseCopyToClipboardReturn = [\r\n copiedText: string | null,\r\n copy: CopyFn\r\n];\r\n\r\n/**\r\n * Fallback copy function for browsers that don't support the Clipboard API\r\n * @param text - Text to copy to clipboard\r\n * @returns Whether the copy was successful\r\n */\r\nfunction fallbackCopyToClipboard(text: string): boolean {\r\n // Check if we're in a browser environment\r\n if (typeof document === \"undefined\") {\r\n return false;\r\n }\r\n\r\n const textarea = document.createElement(\"textarea\");\r\n textarea.value = text;\r\n\r\n // Make the textarea invisible but still functional\r\n textarea.style.cssText =\r\n \"position:fixed;left:-9999px;top:-9999px;opacity:0;pointer-events:none\";\r\n textarea.setAttribute(\"readonly\", \"\");\r\n textarea.setAttribute(\"aria-hidden\", \"true\");\r\n\r\n document.body.appendChild(textarea);\r\n\r\n // Select the text\r\n textarea.focus();\r\n textarea.select();\r\n\r\n // For mobile devices\r\n textarea.setSelectionRange(0, text.length);\r\n\r\n let success = false;\r\n try {\r\n success = document.execCommand(\"copy\");\r\n } catch {\r\n success = false;\r\n }\r\n\r\n document.body.removeChild(textarea);\r\n return success;\r\n}\r\n\r\n/**\r\n * Copies text to clipboard using the Clipboard API with fallback support.\r\n * Returns the copied text (or null if not copied) and a copy function.\r\n *\r\n * @param options - Configuration options for the hook\r\n * @returns Tuple of [copiedText, copy]\r\n *\r\n * @example\r\n * ```tsx\r\n * function CopyButton() {\r\n * const [copiedText, copy] = useCopyToClipboard();\r\n *\r\n * return (\r\n * <button onClick={() => copy(\"Hello World\")}>\r\n * {copiedText ? \"Copied!\" : \"Copy\"}\r\n * </button>\r\n * );\r\n * }\r\n * ```\r\n *\r\n * @example\r\n * ```tsx\r\n * // With custom timeout\r\n * const [copiedText, copy] = useCopyToClipboard({ timeout: 3000 });\r\n * ```\r\n *\r\n * @example\r\n * ```tsx\r\n * // With callbacks\r\n * const [copiedText, copy] = useCopyToClipboard({\r\n * onSuccess: (text) => console.log(`Copied: ${text}`),\r\n * onError: (error) => console.error(`Failed to copy: ${error.message}`),\r\n * });\r\n * ```\r\n *\r\n * @example\r\n * ```tsx\r\n * // Disable auto-reset\r\n * const [copiedText, copy] = useCopyToClipboard({ timeout: 0 });\r\n * ```\r\n */\r\nexport function useCopyToClipboard(\r\n options: UseCopyToClipboardOptions = {}\r\n): UseCopyToClipboardReturn {\r\n const { timeout = 2000, onSuccess, onError } = options;\r\n\r\n const [copiedText, setCopiedText] = useState<string | null>(null);\r\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(\r\n undefined\r\n );\r\n\r\n // Store callbacks in refs to avoid dependency issues\r\n const onSuccessRef = useRef(onSuccess);\r\n const onErrorRef = useRef(onError);\r\n const timeoutValueRef = useRef(timeout);\r\n\r\n // Update refs when options change\r\n onSuccessRef.current = onSuccess;\r\n onErrorRef.current = onError;\r\n timeoutValueRef.current = timeout;\r\n\r\n // Cleanup timeout on unmount\r\n useEffect(() => {\r\n return () => {\r\n if (timeoutRef.current !== undefined) {\r\n clearTimeout(timeoutRef.current);\r\n }\r\n };\r\n }, []);\r\n\r\n const copy: CopyFn = useCallback(async (text: string): Promise<boolean> => {\r\n // Clear any existing timeout\r\n if (timeoutRef.current !== undefined) {\r\n clearTimeout(timeoutRef.current);\r\n timeoutRef.current = undefined;\r\n }\r\n\r\n // Check for SSR\r\n if (typeof window === \"undefined\") {\r\n const error = new Error(\"Clipboard is not available in this environment\");\r\n onErrorRef.current?.(error);\r\n return false;\r\n }\r\n\r\n try {\r\n // Try the modern Clipboard API first\r\n if (navigator.clipboard && typeof navigator.clipboard.writeText === \"function\") {\r\n await navigator.clipboard.writeText(text);\r\n } else {\r\n // Fall back to execCommand\r\n const success = fallbackCopyToClipboard(text);\r\n if (!success) {\r\n throw new Error(\"Failed to copy text using fallback method\");\r\n }\r\n }\r\n\r\n // Success\r\n setCopiedText(text);\r\n onSuccessRef.current?.(text);\r\n\r\n // Set timeout for auto-reset if enabled\r\n if (timeoutValueRef.current > 0) {\r\n timeoutRef.current = setTimeout(() => {\r\n setCopiedText(null);\r\n timeoutRef.current = undefined;\r\n }, timeoutValueRef.current);\r\n }\r\n\r\n return true;\r\n } catch (err) {\r\n // Try fallback if Clipboard API failed\r\n try {\r\n const success = fallbackCopyToClipboard(text);\r\n if (success) {\r\n setCopiedText(text);\r\n onSuccessRef.current?.(text);\r\n\r\n if (timeoutValueRef.current > 0) {\r\n timeoutRef.current = setTimeout(() => {\r\n setCopiedText(null);\r\n timeoutRef.current = undefined;\r\n }, timeoutValueRef.current);\r\n }\r\n\r\n return true;\r\n }\r\n } catch {\r\n // Fallback also failed\r\n }\r\n\r\n // Both methods failed\r\n const error =\r\n err instanceof Error ? err : new Error(\"Failed to copy text to clipboard\");\r\n setCopiedText(null);\r\n onErrorRef.current?.(error);\r\n return false;\r\n }\r\n }, []);\r\n\r\n return [copiedText, copy];\r\n}\r\n"],"mappings":";AAAA,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AA2CzD,SAAS,wBAAwB,MAAuB;AAEtD,MAAI,OAAO,aAAa,aAAa;AACnC,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,SAAS,cAAc,UAAU;AAClD,WAAS,QAAQ;AAGjB,WAAS,MAAM,UACb;AACF,WAAS,aAAa,YAAY,EAAE;AACpC,WAAS,aAAa,eAAe,MAAM;AAE3C,WAAS,KAAK,YAAY,QAAQ;AAGlC,WAAS,MAAM;AACf,WAAS,OAAO;AAGhB,WAAS,kBAAkB,GAAG,KAAK,MAAM;AAEzC,MAAI,UAAU;AACd,MAAI;AACF,cAAU,SAAS,YAAY,MAAM;AAAA,EACvC,QAAQ;AACN,cAAU;AAAA,EACZ;AAEA,WAAS,KAAK,YAAY,QAAQ;AAClC,SAAO;AACT;AA2CO,SAAS,mBACd,UAAqC,CAAC,GACZ;AAC1B,QAAM,EAAE,UAAU,KAAM,WAAW,QAAQ,IAAI;AAE/C,QAAM,CAAC,YAAY,aAAa,IAAI,SAAwB,IAAI;AAChE,QAAM,aAAa;AAAA,IACjB;AAAA,EACF;AAGA,QAAM,eAAe,OAAO,SAAS;AACrC,QAAM,aAAa,OAAO,OAAO;AACjC,QAAM,kBAAkB,OAAO,OAAO;AAGtC,eAAa,UAAU;AACvB,aAAW,UAAU;AACrB,kBAAgB,UAAU;AAG1B,YAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,WAAW,YAAY,QAAW;AACpC,qBAAa,WAAW,OAAO;AAAA,MACjC;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,OAAe,YAAY,OAAO,SAAmC;AAEzE,QAAI,WAAW,YAAY,QAAW;AACpC,mBAAa,WAAW,OAAO;AAC/B,iBAAW,UAAU;AAAA,IACvB;AAGA,QAAI,OAAO,WAAW,aAAa;AACjC,YAAM,QAAQ,IAAI,MAAM,gDAAgD;AACxE,iBAAW,UAAU,KAAK;AAC1B,aAAO;AAAA,IACT;AAEA,QAAI;AAEF,UAAI,UAAU,aAAa,OAAO,UAAU,UAAU,cAAc,YAAY;AAC9E,cAAM,UAAU,UAAU,UAAU,IAAI;AAAA,MAC1C,OAAO;AAEL,cAAM,UAAU,wBAAwB,IAAI;AAC5C,YAAI,CAAC,SAAS;AACZ,gBAAM,IAAI,MAAM,2CAA2C;AAAA,QAC7D;AAAA,MACF;AAGA,oBAAc,IAAI;AAClB,mBAAa,UAAU,IAAI;AAG3B,UAAI,gBAAgB,UAAU,GAAG;AAC/B,mBAAW,UAAU,WAAW,MAAM;AACpC,wBAAc,IAAI;AAClB,qBAAW,UAAU;AAAA,QACvB,GAAG,gBAAgB,OAAO;AAAA,MAC5B;AAEA,aAAO;AAAA,IACT,SAAS,KAAK;AAEZ,UAAI;AACF,cAAM,UAAU,wBAAwB,IAAI;AAC5C,YAAI,SAAS;AACX,wBAAc,IAAI;AAClB,uBAAa,UAAU,IAAI;AAE3B,cAAI,gBAAgB,UAAU,GAAG;AAC/B,uBAAW,UAAU,WAAW,MAAM;AACpC,4BAAc,IAAI;AAClB,yBAAW,UAAU;AAAA,YACvB,GAAG,gBAAgB,OAAO;AAAA,UAC5B;AAEA,iBAAO;AAAA,QACT;AAAA,MACF,QAAQ;AAAA,MAER;AAGA,YAAM,QACJ,eAAe,QAAQ,MAAM,IAAI,MAAM,kCAAkC;AAC3E,oBAAc,IAAI;AAClB,iBAAW,UAAU,KAAK;AAC1B,aAAO;AAAA,IACT;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SAAO,CAAC,YAAY,IAAI;AAC1B;","names":[]}
1
+ {"version":3,"sources":["../src/useCopyToClipboard.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\n/**\n * Options for useCopyToClipboard hook\n */\nexport interface UseCopyToClipboardOptions {\n /**\n * Time in milliseconds before the copied state resets to null.\n * Set to 0 to disable auto-reset.\n * @default 2000\n */\n timeout?: number;\n /**\n * Callback function called when copy succeeds\n * @param text - The text that was copied\n */\n onSuccess?: (text: string) => void;\n /**\n * Callback function called when copy fails\n * @param error - The error that occurred\n */\n onError?: (error: Error) => void;\n}\n\n/**\n * Type for the copy function\n */\nexport type CopyFn = (text: string) => Promise<boolean>;\n\n/**\n * Return type for useCopyToClipboard hook\n * Tuple format: [copiedText, copy]\n */\nexport type UseCopyToClipboardReturn = [\n copiedText: string | null,\n copy: CopyFn\n];\n\n/**\n * Fallback copy function for browsers that don't support the Clipboard API\n * @param text - Text to copy to clipboard\n * @returns Whether the copy was successful\n */\nfunction fallbackCopyToClipboard(text: string): boolean {\n // Check if we're in a browser environment\n if (typeof document === \"undefined\") {\n return false;\n }\n\n const textarea = document.createElement(\"textarea\");\n textarea.value = text;\n\n // Make the textarea invisible but still functional\n textarea.style.cssText =\n \"position:fixed;left:-9999px;top:-9999px;opacity:0;pointer-events:none\";\n textarea.setAttribute(\"readonly\", \"\");\n textarea.setAttribute(\"aria-hidden\", \"true\");\n\n document.body.appendChild(textarea);\n\n // Select the text\n textarea.focus();\n textarea.select();\n\n // For mobile devices\n textarea.setSelectionRange(0, text.length);\n\n let success = false;\n try {\n success = document.execCommand(\"copy\");\n } catch {\n success = false;\n }\n\n document.body.removeChild(textarea);\n return success;\n}\n\n/**\n * Copies text to clipboard using the Clipboard API with fallback support.\n * Returns the copied text (or null if not copied) and a copy function.\n *\n * @param options - Configuration options for the hook\n * @returns Tuple of [copiedText, copy]\n *\n * @example\n * ```tsx\n * function CopyButton() {\n * const [copiedText, copy] = useCopyToClipboard();\n *\n * return (\n * <button onClick={() => copy(\"Hello World\")}>\n * {copiedText ? \"Copied!\" : \"Copy\"}\n * </button>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With custom timeout\n * const [copiedText, copy] = useCopyToClipboard({ timeout: 3000 });\n * ```\n *\n * @example\n * ```tsx\n * // With callbacks\n * const [copiedText, copy] = useCopyToClipboard({\n * onSuccess: (text) => console.log(`Copied: ${text}`),\n * onError: (error) => console.error(`Failed to copy: ${error.message}`),\n * });\n * ```\n *\n * @example\n * ```tsx\n * // Disable auto-reset\n * const [copiedText, copy] = useCopyToClipboard({ timeout: 0 });\n * ```\n */\nexport function useCopyToClipboard(\n options: UseCopyToClipboardOptions = {}\n): UseCopyToClipboardReturn {\n const { timeout = 2000, onSuccess, onError } = options;\n\n const [copiedText, setCopiedText] = useState<string | null>(null);\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(\n undefined\n );\n\n // Store callbacks in refs to avoid dependency issues\n const onSuccessRef = useRef(onSuccess);\n const onErrorRef = useRef(onError);\n const timeoutValueRef = useRef(timeout);\n\n // Update refs when options change\n onSuccessRef.current = onSuccess;\n onErrorRef.current = onError;\n timeoutValueRef.current = timeout;\n\n // Cleanup timeout on unmount\n useEffect(() => {\n return () => {\n if (timeoutRef.current !== undefined) {\n clearTimeout(timeoutRef.current);\n }\n };\n }, []);\n\n const copy: CopyFn = useCallback(async (text: string): Promise<boolean> => {\n // Clear any existing timeout\n if (timeoutRef.current !== undefined) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = undefined;\n }\n\n // Check for SSR\n if (typeof window === \"undefined\") {\n const error = new Error(\"Clipboard is not available in this environment\");\n onErrorRef.current?.(error);\n return false;\n }\n\n try {\n // Try the modern Clipboard API first\n if (navigator.clipboard && typeof navigator.clipboard.writeText === \"function\") {\n await navigator.clipboard.writeText(text);\n } else {\n // Fall back to execCommand\n const success = fallbackCopyToClipboard(text);\n if (!success) {\n throw new Error(\"Failed to copy text using fallback method\");\n }\n }\n\n // Success\n setCopiedText(text);\n onSuccessRef.current?.(text);\n\n // Set timeout for auto-reset if enabled\n if (timeoutValueRef.current > 0) {\n timeoutRef.current = setTimeout(() => {\n setCopiedText(null);\n timeoutRef.current = undefined;\n }, timeoutValueRef.current);\n }\n\n return true;\n } catch (err) {\n // Try fallback if Clipboard API failed\n try {\n const success = fallbackCopyToClipboard(text);\n if (success) {\n setCopiedText(text);\n onSuccessRef.current?.(text);\n\n if (timeoutValueRef.current > 0) {\n timeoutRef.current = setTimeout(() => {\n setCopiedText(null);\n timeoutRef.current = undefined;\n }, timeoutValueRef.current);\n }\n\n return true;\n }\n } catch {\n // Fallback also failed\n }\n\n // Both methods failed\n const error =\n err instanceof Error ? err : new Error(\"Failed to copy text to clipboard\");\n setCopiedText(null);\n onErrorRef.current?.(error);\n return false;\n }\n }, []);\n\n return [copiedText, copy];\n}\n"],"mappings":";AAAA,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AA2CzD,SAAS,wBAAwB,MAAuB;AAEtD,MAAI,OAAO,aAAa,aAAa;AACnC,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,SAAS,cAAc,UAAU;AAClD,WAAS,QAAQ;AAGjB,WAAS,MAAM,UACb;AACF,WAAS,aAAa,YAAY,EAAE;AACpC,WAAS,aAAa,eAAe,MAAM;AAE3C,WAAS,KAAK,YAAY,QAAQ;AAGlC,WAAS,MAAM;AACf,WAAS,OAAO;AAGhB,WAAS,kBAAkB,GAAG,KAAK,MAAM;AAEzC,MAAI,UAAU;AACd,MAAI;AACF,cAAU,SAAS,YAAY,MAAM;AAAA,EACvC,QAAQ;AACN,cAAU;AAAA,EACZ;AAEA,WAAS,KAAK,YAAY,QAAQ;AAClC,SAAO;AACT;AA2CO,SAAS,mBACd,UAAqC,CAAC,GACZ;AAC1B,QAAM,EAAE,UAAU,KAAM,WAAW,QAAQ,IAAI;AAE/C,QAAM,CAAC,YAAY,aAAa,IAAI,SAAwB,IAAI;AAChE,QAAM,aAAa;AAAA,IACjB;AAAA,EACF;AAGA,QAAM,eAAe,OAAO,SAAS;AACrC,QAAM,aAAa,OAAO,OAAO;AACjC,QAAM,kBAAkB,OAAO,OAAO;AAGtC,eAAa,UAAU;AACvB,aAAW,UAAU;AACrB,kBAAgB,UAAU;AAG1B,YAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,WAAW,YAAY,QAAW;AACpC,qBAAa,WAAW,OAAO;AAAA,MACjC;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,OAAe,YAAY,OAAO,SAAmC;AAEzE,QAAI,WAAW,YAAY,QAAW;AACpC,mBAAa,WAAW,OAAO;AAC/B,iBAAW,UAAU;AAAA,IACvB;AAGA,QAAI,OAAO,WAAW,aAAa;AACjC,YAAM,QAAQ,IAAI,MAAM,gDAAgD;AACxE,iBAAW,UAAU,KAAK;AAC1B,aAAO;AAAA,IACT;AAEA,QAAI;AAEF,UAAI,UAAU,aAAa,OAAO,UAAU,UAAU,cAAc,YAAY;AAC9E,cAAM,UAAU,UAAU,UAAU,IAAI;AAAA,MAC1C,OAAO;AAEL,cAAM,UAAU,wBAAwB,IAAI;AAC5C,YAAI,CAAC,SAAS;AACZ,gBAAM,IAAI,MAAM,2CAA2C;AAAA,QAC7D;AAAA,MACF;AAGA,oBAAc,IAAI;AAClB,mBAAa,UAAU,IAAI;AAG3B,UAAI,gBAAgB,UAAU,GAAG;AAC/B,mBAAW,UAAU,WAAW,MAAM;AACpC,wBAAc,IAAI;AAClB,qBAAW,UAAU;AAAA,QACvB,GAAG,gBAAgB,OAAO;AAAA,MAC5B;AAEA,aAAO;AAAA,IACT,SAAS,KAAK;AAEZ,UAAI;AACF,cAAM,UAAU,wBAAwB,IAAI;AAC5C,YAAI,SAAS;AACX,wBAAc,IAAI;AAClB,uBAAa,UAAU,IAAI;AAE3B,cAAI,gBAAgB,UAAU,GAAG;AAC/B,uBAAW,UAAU,WAAW,MAAM;AACpC,4BAAc,IAAI;AAClB,yBAAW,UAAU;AAAA,YACvB,GAAG,gBAAgB,OAAO;AAAA,UAC5B;AAEA,iBAAO;AAAA,QACT;AAAA,MACF,QAAQ;AAAA,MAER;AAGA,YAAM,QACJ,eAAe,QAAQ,MAAM,IAAI,MAAM,kCAAkC;AAC3E,oBAAc,IAAI;AAClB,iBAAW,UAAU,KAAK;AAC1B,aAAO;AAAA,IACT;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SAAO,CAAC,YAAY,IAAI;AAC1B;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefy/use-copy-to-clipboard",
3
- "version": "0.0.7",
3
+ "version": "0.0.10",
4
4
  "description": "A React hook for copying text to clipboard with fallback support",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",