@usefy/use-copy-to-clipboard 0.21.1 → 0.23.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/dist/index.js +50 -40
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +50 -40
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -60,34 +60,26 @@ function useCopyToClipboard(options = {}) {
|
|
|
60
60
|
onSuccessRef.current = onSuccess;
|
|
61
61
|
onErrorRef.current = onError;
|
|
62
62
|
timeoutValueRef.current = timeout;
|
|
63
|
-
(0, import_react.
|
|
64
|
-
|
|
65
|
-
if (timeoutRef.current !== void 0) {
|
|
66
|
-
clearTimeout(timeoutRef.current);
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
}, []);
|
|
70
|
-
const copy = (0, import_react.useCallback)(async (text) => {
|
|
63
|
+
const mountedRef = (0, import_react.useRef)(true);
|
|
64
|
+
const clearResetTimer = (0, import_react.useCallback)(() => {
|
|
71
65
|
if (timeoutRef.current !== void 0) {
|
|
72
66
|
clearTimeout(timeoutRef.current);
|
|
73
67
|
timeoutRef.current = void 0;
|
|
74
68
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
throw new Error("Failed to copy text using fallback method");
|
|
87
|
-
}
|
|
88
|
-
}
|
|
69
|
+
}, []);
|
|
70
|
+
(0, import_react.useEffect)(() => {
|
|
71
|
+
mountedRef.current = true;
|
|
72
|
+
return () => {
|
|
73
|
+
mountedRef.current = false;
|
|
74
|
+
clearResetTimer();
|
|
75
|
+
};
|
|
76
|
+
}, [clearResetTimer]);
|
|
77
|
+
const commitSuccess = (0, import_react.useCallback)(
|
|
78
|
+
(text) => {
|
|
79
|
+
if (!mountedRef.current) return true;
|
|
89
80
|
setCopiedText(text);
|
|
90
81
|
onSuccessRef.current?.(text);
|
|
82
|
+
clearResetTimer();
|
|
91
83
|
if (timeoutValueRef.current > 0) {
|
|
92
84
|
timeoutRef.current = setTimeout(() => {
|
|
93
85
|
setCopiedText(null);
|
|
@@ -95,28 +87,46 @@ function useCopyToClipboard(options = {}) {
|
|
|
95
87
|
}, timeoutValueRef.current);
|
|
96
88
|
}
|
|
97
89
|
return true;
|
|
98
|
-
}
|
|
90
|
+
},
|
|
91
|
+
[clearResetTimer]
|
|
92
|
+
);
|
|
93
|
+
const copy = (0, import_react.useCallback)(
|
|
94
|
+
async (text) => {
|
|
95
|
+
clearResetTimer();
|
|
96
|
+
if (typeof window === "undefined") {
|
|
97
|
+
const error = new Error(
|
|
98
|
+
"Clipboard is not available in this environment"
|
|
99
|
+
);
|
|
100
|
+
onErrorRef.current?.(error);
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
99
103
|
try {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
setCopiedText(null);
|
|
107
|
-
timeoutRef.current = void 0;
|
|
108
|
-
}, timeoutValueRef.current);
|
|
104
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
105
|
+
await navigator.clipboard.writeText(text);
|
|
106
|
+
} else {
|
|
107
|
+
const success = fallbackCopyToClipboard(text);
|
|
108
|
+
if (!success) {
|
|
109
|
+
throw new Error("Failed to copy text using fallback method");
|
|
109
110
|
}
|
|
110
|
-
return true;
|
|
111
111
|
}
|
|
112
|
-
|
|
112
|
+
return commitSuccess(text);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
try {
|
|
115
|
+
const success = fallbackCopyToClipboard(text);
|
|
116
|
+
if (success) {
|
|
117
|
+
return commitSuccess(text);
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
if (!mountedRef.current) return false;
|
|
122
|
+
const error = err instanceof Error ? err : new Error("Failed to copy text to clipboard");
|
|
123
|
+
setCopiedText(null);
|
|
124
|
+
onErrorRef.current?.(error);
|
|
125
|
+
return false;
|
|
113
126
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
}, []);
|
|
127
|
+
},
|
|
128
|
+
[clearResetTimer, commitSuccess]
|
|
129
|
+
);
|
|
120
130
|
return [copiedText, copy];
|
|
121
131
|
}
|
|
122
132
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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 //
|
|
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 // Mounted flag — re-armed on (re-)mount so StrictMode's mount→unmount→mount\n // double-invoke does not leave it stuck at `false`. Guards post-await state\n // updates and timer scheduling that would otherwise run after unmount.\n const mountedRef = useRef(true);\n\n // Clear the pending auto-reset timer, if any.\n const clearResetTimer = useCallback(() => {\n if (timeoutRef.current !== undefined) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = undefined;\n }\n }, []);\n\n // Cleanup timeout on unmount + maintain the mounted flag.\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n clearResetTimer();\n };\n }, [clearResetTimer]);\n\n // Mark the copy as succeeded: update state, fire onSuccess, and (re)schedule\n // the auto-reset. Skips all state work once unmounted so a copy() resolving\n // after unmount never touches state or leaks a timer the cleanup can't clear.\n const commitSuccess = useCallback(\n (text: string): boolean => {\n if (!mountedRef.current) return true;\n\n setCopiedText(text);\n onSuccessRef.current?.(text);\n\n // Supersede any in-flight reset timer (e.g. from an overlapping copy)\n // right at the point of scheduling, so the latest copy always wins.\n clearResetTimer();\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 [clearResetTimer]\n );\n\n const copy: CopyFn = useCallback(\n async (text: string): Promise<boolean> => {\n // Clear any existing timeout\n clearResetTimer();\n\n // Check for SSR\n if (typeof window === \"undefined\") {\n const error = new Error(\n \"Clipboard is not available in this environment\"\n );\n onErrorRef.current?.(error);\n return false;\n }\n\n try {\n // Try the modern Clipboard API first\n if (\n navigator.clipboard &&\n typeof navigator.clipboard.writeText === \"function\"\n ) {\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 return commitSuccess(text);\n } catch (err) {\n // Try fallback if Clipboard API failed\n try {\n const success = fallbackCopyToClipboard(text);\n if (success) {\n return commitSuccess(text);\n }\n } catch {\n // Fallback also failed\n }\n\n // Both methods failed\n if (!mountedRef.current) return false;\n\n const error =\n err instanceof Error\n ? err\n : new Error(\"Failed to copy text to clipboard\");\n setCopiedText(null);\n onErrorRef.current?.(error);\n return false;\n }\n },\n [clearResetTimer, commitSuccess]\n );\n\n return [copiedText, copy];\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -34,34 +34,26 @@ function useCopyToClipboard(options = {}) {
|
|
|
34
34
|
onSuccessRef.current = onSuccess;
|
|
35
35
|
onErrorRef.current = onError;
|
|
36
36
|
timeoutValueRef.current = timeout;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (timeoutRef.current !== void 0) {
|
|
40
|
-
clearTimeout(timeoutRef.current);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
}, []);
|
|
44
|
-
const copy = useCallback(async (text) => {
|
|
37
|
+
const mountedRef = useRef(true);
|
|
38
|
+
const clearResetTimer = useCallback(() => {
|
|
45
39
|
if (timeoutRef.current !== void 0) {
|
|
46
40
|
clearTimeout(timeoutRef.current);
|
|
47
41
|
timeoutRef.current = void 0;
|
|
48
42
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
throw new Error("Failed to copy text using fallback method");
|
|
61
|
-
}
|
|
62
|
-
}
|
|
43
|
+
}, []);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
mountedRef.current = true;
|
|
46
|
+
return () => {
|
|
47
|
+
mountedRef.current = false;
|
|
48
|
+
clearResetTimer();
|
|
49
|
+
};
|
|
50
|
+
}, [clearResetTimer]);
|
|
51
|
+
const commitSuccess = useCallback(
|
|
52
|
+
(text) => {
|
|
53
|
+
if (!mountedRef.current) return true;
|
|
63
54
|
setCopiedText(text);
|
|
64
55
|
onSuccessRef.current?.(text);
|
|
56
|
+
clearResetTimer();
|
|
65
57
|
if (timeoutValueRef.current > 0) {
|
|
66
58
|
timeoutRef.current = setTimeout(() => {
|
|
67
59
|
setCopiedText(null);
|
|
@@ -69,28 +61,46 @@ function useCopyToClipboard(options = {}) {
|
|
|
69
61
|
}, timeoutValueRef.current);
|
|
70
62
|
}
|
|
71
63
|
return true;
|
|
72
|
-
}
|
|
64
|
+
},
|
|
65
|
+
[clearResetTimer]
|
|
66
|
+
);
|
|
67
|
+
const copy = useCallback(
|
|
68
|
+
async (text) => {
|
|
69
|
+
clearResetTimer();
|
|
70
|
+
if (typeof window === "undefined") {
|
|
71
|
+
const error = new Error(
|
|
72
|
+
"Clipboard is not available in this environment"
|
|
73
|
+
);
|
|
74
|
+
onErrorRef.current?.(error);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
73
77
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
setCopiedText(null);
|
|
81
|
-
timeoutRef.current = void 0;
|
|
82
|
-
}, timeoutValueRef.current);
|
|
78
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
79
|
+
await navigator.clipboard.writeText(text);
|
|
80
|
+
} else {
|
|
81
|
+
const success = fallbackCopyToClipboard(text);
|
|
82
|
+
if (!success) {
|
|
83
|
+
throw new Error("Failed to copy text using fallback method");
|
|
83
84
|
}
|
|
84
|
-
return true;
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
return commitSuccess(text);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
try {
|
|
89
|
+
const success = fallbackCopyToClipboard(text);
|
|
90
|
+
if (success) {
|
|
91
|
+
return commitSuccess(text);
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
if (!mountedRef.current) return false;
|
|
96
|
+
const error = err instanceof Error ? err : new Error("Failed to copy text to clipboard");
|
|
97
|
+
setCopiedText(null);
|
|
98
|
+
onErrorRef.current?.(error);
|
|
99
|
+
return false;
|
|
87
100
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
}, []);
|
|
101
|
+
},
|
|
102
|
+
[clearResetTimer, commitSuccess]
|
|
103
|
+
);
|
|
94
104
|
return [copiedText, copy];
|
|
95
105
|
}
|
|
96
106
|
export {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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 //
|
|
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 // Mounted flag — re-armed on (re-)mount so StrictMode's mount→unmount→mount\n // double-invoke does not leave it stuck at `false`. Guards post-await state\n // updates and timer scheduling that would otherwise run after unmount.\n const mountedRef = useRef(true);\n\n // Clear the pending auto-reset timer, if any.\n const clearResetTimer = useCallback(() => {\n if (timeoutRef.current !== undefined) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = undefined;\n }\n }, []);\n\n // Cleanup timeout on unmount + maintain the mounted flag.\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n clearResetTimer();\n };\n }, [clearResetTimer]);\n\n // Mark the copy as succeeded: update state, fire onSuccess, and (re)schedule\n // the auto-reset. Skips all state work once unmounted so a copy() resolving\n // after unmount never touches state or leaks a timer the cleanup can't clear.\n const commitSuccess = useCallback(\n (text: string): boolean => {\n if (!mountedRef.current) return true;\n\n setCopiedText(text);\n onSuccessRef.current?.(text);\n\n // Supersede any in-flight reset timer (e.g. from an overlapping copy)\n // right at the point of scheduling, so the latest copy always wins.\n clearResetTimer();\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 [clearResetTimer]\n );\n\n const copy: CopyFn = useCallback(\n async (text: string): Promise<boolean> => {\n // Clear any existing timeout\n clearResetTimer();\n\n // Check for SSR\n if (typeof window === \"undefined\") {\n const error = new Error(\n \"Clipboard is not available in this environment\"\n );\n onErrorRef.current?.(error);\n return false;\n }\n\n try {\n // Try the modern Clipboard API first\n if (\n navigator.clipboard &&\n typeof navigator.clipboard.writeText === \"function\"\n ) {\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 return commitSuccess(text);\n } catch (err) {\n // Try fallback if Clipboard API failed\n try {\n const success = fallbackCopyToClipboard(text);\n if (success) {\n return commitSuccess(text);\n }\n } catch {\n // Fallback also failed\n }\n\n // Both methods failed\n if (!mountedRef.current) return false;\n\n const error =\n err instanceof Error\n ? err\n : new Error(\"Failed to copy text to clipboard\");\n setCopiedText(null);\n onErrorRef.current?.(error);\n return false;\n }\n },\n [clearResetTimer, commitSuccess]\n );\n\n return [copiedText, copy];\n}\n"],"mappings":"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","names":[]}
|