@mirawision/copily
Version:
A comprehensive clipboard manipulation library for TypeScript, providing functionalities for copying/pasting text, HTML, JSON, images, files, and smart content detection.
248 lines (247 loc) • 9.21 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.listenCopy = exports.listenPaste = exports.interceptPaste = exports.interceptCopy = exports.pasteSmart = void 0;
const utils_1 = require("./utils");
const core_1 = require("./core");
/**
* Intelligently detect clipboard content and return structured results.
*
* Aggregates multiple clipboard reads (text, HTML, image, file) and classifies
* the content into typed results like `otp`, `url`, `email`, `json`, `html`,
* `text`, `image`, and `file`.
*
* @returns Resolves to an array of detected clipboard items.
* @example
* const items = await pasteSmart();
*/
async function pasteSmart() {
const results = [];
try {
// Try to get text content
const text = await (0, core_1.pasteText)();
if (text.trim()) {
// Check for OTP
if ((0, utils_1.isOTP)(text)) {
results.push({ type: 'otp', value: text.trim() });
}
// Check for URL
if ((0, utils_1.isURL)(text)) {
results.push({ type: 'url', value: text.trim() });
}
// Check for email
if ((0, utils_1.isEmail)(text)) {
results.push({ type: 'email', value: text.trim() });
}
// Check for JSON
if ((0, utils_1.isJSON)(text)) {
try {
const jsonValue = JSON.parse(text);
results.push({ type: 'json', value: jsonValue });
}
catch {
// Ignore JSON parsing errors
}
}
// Always include as text if not already categorized
if (results.length === 0 || results.every(r => r.type !== 'text')) {
results.push({ type: 'text', value: text });
}
}
// Try to get HTML content
const html = await (0, core_1.pasteHTML)();
if (html && html !== text) {
results.push({ type: 'html', value: html });
}
// Try to get image
const image = await (0, core_1.pasteImage)();
if (image) {
results.push({ type: 'image', blob: image });
}
// Try to get file
const file = await (0, core_1.pasteFile)();
if (file) {
results.push({ type: 'file', file });
}
}
catch (error) {
// Return empty array on error
return [];
}
return results;
}
exports.pasteSmart = pasteSmart;
/**
* Intercept native `copy` events and optionally modify clipboard data.
*
* Use the handler return value to prevent the default or override text/HTML
* placed on the clipboard.
*
* @param handler Function invoked on each `copy` event.
* @returns Cleanup function to remove the listener.
* @example
* const off = interceptCopy(({ selection }) => ({ overrideText: selection.trim() }));
*/
function interceptCopy(handler) {
const originalHandler = (event) => {
const selection = window.getSelection()?.toString() || '';
const target = event.target;
const result = handler({
event,
selection,
target
});
if (result.prevent) {
event.preventDefault();
return;
}
if (result.overrideText || result.overrideHTML) {
event.preventDefault();
if (result.overrideText) {
event.clipboardData?.setData('text/plain', result.overrideText);
}
if (result.overrideHTML) {
event.clipboardData?.setData('text/html', result.overrideHTML);
}
}
};
document.addEventListener('copy', originalHandler);
// Return cleanup function
return () => {
document.removeEventListener('copy', originalHandler);
};
}
exports.interceptCopy = interceptCopy;
/**
* Intercept native `paste` events and optionally modify inserted content.
*
* The handler can prevent the default paste, or override with custom
* plain text or HTML content. Text overrides are inserted into inputs,
* textareas, and contenteditable elements; HTML overrides are inserted for
* contenteditable contexts when possible.
*
* @param handler Function invoked on each `paste` event.
* @returns Cleanup function to remove the listener.
* @example
* const off = interceptPaste(() => ({ overrideText: 'plain text only' }));
*/
function interceptPaste(handler) {
const originalHandler = (event) => {
const target = event.target;
const plainText = event.clipboardData?.getData('text/plain') || '';
const html = event.clipboardData?.getData('text/html') || '';
const result = handler({
event,
plainText,
html: html || undefined,
target
});
if (result.prevent) {
event.preventDefault();
return;
}
if (result.overrideText || result.overrideHTML) {
event.preventDefault();
const stripTags = (value) => value.replace(/<[^>]*>/g, '');
// If input/textarea, insert text override (or textified HTML)
if (target && (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) {
const input = target;
const insertion = result.overrideText ?? (result.overrideHTML ? stripTags(result.overrideHTML) : '');
const start = input.selectionStart ?? input.value.length;
const end = input.selectionEnd ?? start;
input.setRangeText(insertion, start, end, 'end');
input.dispatchEvent(new Event('input', { bubbles: true }));
return;
}
// For contenteditable/others, prefer HTML insertion then text
if (result.overrideHTML) {
try {
// execCommand path (still broadly supported)
if (typeof document.execCommand === 'function') {
document.execCommand('insertHTML', false, result.overrideHTML);
return;
}
}
catch { }
const selection = window.getSelection();
const range = selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
if (range) {
range.deleteContents();
const temp = document.createElement('div');
temp.innerHTML = result.overrideHTML;
const frag = document.createDocumentFragment();
while (temp.firstChild)
frag.appendChild(temp.firstChild);
range.insertNode(frag);
}
return;
}
if (result.overrideText) {
try {
if (typeof document.execCommand === 'function') {
document.execCommand('insertText', false, result.overrideText);
return;
}
}
catch { }
const selection = window.getSelection();
const range = selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
if (range) {
range.deleteContents();
range.insertNode(document.createTextNode(result.overrideText));
}
}
}
};
document.addEventListener('paste', originalHandler);
return () => {
document.removeEventListener('paste', originalHandler);
};
}
exports.interceptPaste = interceptPaste;
/**
* Listen to `paste` events and emit smart-detected clipboard data.
*
* Internally calls {@link pasteSmart} on paste and passes the results to the
* provided callback.
*
* @param callback Receives the array of detected clipboard results.
* @returns Cleanup function to remove the listener.
*/
function listenPaste(callback) {
const handler = async (_event) => {
try {
const results = await pasteSmart();
callback(results);
}
catch (error) {
// Call callback with empty array on error
callback([]);
}
};
document.addEventListener('paste', handler);
// Return unsubscribe function
return () => {
document.removeEventListener('paste', handler);
};
}
exports.listenPaste = listenPaste;
/**
* Listen to `copy` events and emit selection details.
*
* Provides the raw event, the current text selection, and the event target.
*
* @param callback Receives `{ event, selection, target }` on each copy.
* @returns Cleanup function to remove the listener.
*/
function listenCopy(callback) {
const handler = (event) => {
const selection = window.getSelection()?.toString() || '';
const target = event.target ?? null;
callback({ event, selection, target });
};
document.addEventListener('copy', handler);
return () => {
document.removeEventListener('copy', handler);
};
}
exports.listenCopy = listenCopy;