@mirawision/copily
Version:
A comprehensive clipboard manipulation library for TypeScript, providing functionalities for copying/pasting text, HTML, JSON, images, files, and smart content detection.
246 lines (245 loc) • 7.62 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.requestClipboardPermission = exports.isSecureContext = exports.removeTemporaryElement = exports.createTemporaryElement = exports.imageToBlob = exports.isJSON = exports.isOTP = exports.isEmail = exports.isURL = exports.sanitizeHTML = exports.getClipboardFormats = exports.supportsFormat = exports.supportsClipboardAPI = void 0;
const types_1 = require("./types");
/**
* Check if the Clipboard API is supported in the current environment.
*
* @returns True if `navigator.clipboard` is available.
*/
function supportsClipboardAPI() {
return typeof navigator !== 'undefined' &&
'clipboard' in navigator &&
typeof navigator.clipboard === 'object';
}
exports.supportsClipboardAPI = supportsClipboardAPI;
/**
* Check if the clipboard supports a specific MIME format.
*
* Note: The Clipboard API does not expose a formal capability query for
* formats. This function validates against a curated list of common types.
*
* @param format MIME type (e.g. `text/html`).
* @returns True when the format is recognized.
*/
function supportsFormat(format) {
if (!supportsClipboardAPI()) {
return false;
}
// Basic format validation
const validFormats = [
'text/plain',
'text/html',
'text/uri-list',
'image/png',
'image/jpeg',
'image/gif',
'image/webp',
'application/json'
];
return validFormats.includes(format);
}
exports.supportsFormat = supportsFormat;
/**
* Get available MIME formats currently present on the clipboard.
*
* @returns Resolves to an array of MIME types; returns an empty array when
* formats cannot be read.
* @throws {ClipboardUnsupportedError} If the Clipboard API is unavailable.
*/
async function getClipboardFormats() {
if (!supportsClipboardAPI()) {
throw new types_1.ClipboardUnsupportedError();
}
try {
const items = await navigator.clipboard.read();
return items.map(item => Array.from(item.types)).flat();
}
catch (error) {
// If we can't read formats, return empty array
return [];
}
}
exports.getClipboardFormats = getClipboardFormats;
/**
* Validate and sanitize an HTML string.
*
* Removes script/iframe tags and common inline event handlers. For strict
* sanitization in production, consider a dedicated sanitizer like DOMPurify.
*
* @param html HTML string.
* @returns Sanitized HTML string.
*/
function sanitizeHTML(html) {
// Basic HTML sanitization - in production, use a proper sanitizer like DOMPurify
return html
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
.replace(/javascript:/gi, '')
.replace(/on\w+\s*=/gi, '');
}
exports.sanitizeHTML = sanitizeHTML;
/**
* Detect if a string is a URL.
*
* Accepts http(s), ftp, mailto and tel protocols.
*
* @param text String to test.
* @returns True when the input is a URL.
*/
function isURL(text) {
try {
const url = new URL(text);
return ['http:', 'https:', 'ftp:', 'mailto:', 'tel:'].includes(url.protocol);
}
catch {
return false;
}
}
exports.isURL = isURL;
/**
* Detect if a string is an email address.
*
* @param text String to test.
* @returns True when the input matches a simple email regex.
*/
function isEmail(text) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(text.trim());
}
exports.isEmail = isEmail;
/**
* Detect if a string looks like a One-Time Passcode (OTP).
*
* Matches 4-8 digit codes or 6-character alphanumerics.
*
* @param text String to test.
* @returns True when the input matches OTP patterns.
*/
function isOTP(text) {
const trimmed = text.trim();
// OTP patterns: 4-8 digits, or 6 alphanumeric characters
const otpRegex = /^(\d{4,8}|[A-Za-z0-9]{6})$/;
return otpRegex.test(trimmed);
}
exports.isOTP = isOTP;
/**
* Detect if a string is valid JSON.
*
* @param text String to test.
* @returns True when `JSON.parse` succeeds.
*/
function isJSON(text) {
try {
JSON.parse(text);
return true;
}
catch {
return false;
}
}
exports.isJSON = isJSON;
/**
* Convert an image to a Blob.
*
* For a URL string, the image is fetched. For an `HTMLImageElement`, a canvas
* conversion is performed.
*
* @param image URL string or `HTMLImageElement`.
* @returns Resolves to a Blob of the image data.
*/
async function imageToBlob(image) {
if (typeof image === 'string') {
// Fetch image from URL
const response = await fetch(image);
if (!response.ok) {
throw new Error('Failed to fetch image');
}
return await response.blob();
}
else {
// Convert canvas to blob
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Failed to get canvas context');
}
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
ctx.drawImage(image, 0, 0);
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
}
else {
reject(new Error('Failed to convert image to blob'));
}
});
});
}
}
exports.imageToBlob = imageToBlob;
/**
* Create a hidden textarea for legacy copy operations.
*
* @returns The created textarea element.
*/
function createTemporaryElement() {
const textArea = document.createElement('textarea');
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
return textArea;
}
exports.createTemporaryElement = createTemporaryElement;
/**
* Remove a temporary element from the DOM.
*
* @param element The element to remove.
*/
function removeTemporaryElement(element) {
if (element.parentNode) {
element.parentNode.removeChild(element);
}
}
exports.removeTemporaryElement = removeTemporaryElement;
/**
* Check if the current page context is secure (HTTPS or otherwise).
*
* @returns True when in a secure context.
*/
function isSecureContext() {
return typeof window !== 'undefined' &&
(window.isSecureContext || location.protocol === 'https:');
}
exports.isSecureContext = isSecureContext;
/**
* Request clipboard permission by attempting a read.
*
* @returns Resolves true on success.
* @throws {ClipboardUnsupportedError} If the Clipboard API is unavailable.
* @throws {ClipboardPermissionError} If the operation is not permitted.
*/
async function requestClipboardPermission() {
if (!supportsClipboardAPI()) {
throw new types_1.ClipboardUnsupportedError();
}
if (!isSecureContext()) {
throw new types_1.ClipboardPermissionError('Clipboard API requires secure context (HTTPS)');
}
try {
// Try to read clipboard to trigger permission request
await navigator.clipboard.readText();
return true;
}
catch (error) {
if (error instanceof types_1.ClipboardUnsupportedError || error instanceof types_1.ClipboardPermissionError) {
throw error;
}
throw new types_1.ClipboardPermissionError();
}
}
exports.requestClipboardPermission = requestClipboardPermission;