UNPKG

tiny-essentials

Version:

Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.

421 lines (385 loc) 14.4 kB
'use strict'; /** * Utility class to handle clipboard operations for text and blob data. * Supports modern Clipboard API, custom platform, and legacy execCommand fallback. */ class TinyClipboard { /** * Indicates whether the legacy `document.execCommand()` API is available. * Used as a fallback for clipboard operations when modern APIs are not supported. * * @type {boolean} */ #existExecCommand = false; /** * Indicates whether the modern Clipboard API (`navigator.clipboard`) is available. * * @type {boolean} */ #existNavigator = false; /** * Function used to copy plain text to the clipboard. * Can be overridden using `setCopyText()`. * * @type {((text: string) => Promise<void>) | null} */ #copyText = null; /** * Function used to copy a Blob (binary data) to the clipboard. * Can be overridden using `setCopyBlob()`. * * @type {((blob: Blob) => Promise<void>) | null} */ #copyBlob = null; /** * Constructs a new TinyClipboard instance. * Automatically detects and configures available clipboard APIs. */ constructor() { // Whether the Clipboard API is available. if (typeof navigator.clipboard !== 'undefined' && navigator.clipboard !== null) { this.#existNavigator = true; this.#copyText = (text) => navigator.clipboard.writeText(text); this.#copyBlob = (blob) => navigator.clipboard.write([ new ClipboardItem({ [blob.type]: blob, }), ]); } /** * @type {boolean} * Whether the legacy execCommand API is available. */ this.#existExecCommand = typeof document.execCommand !== 'undefined' && document.execCommand !== null; } /** * Override the default text copy behavior. * This allows you to provide your own clipboard implementation or * integrate with external systems like Capacitor or Electron. * * @param {(text: string) => Promise<void>} callback - The function to use for copying text. * @throws {TypeError} If the callback is not a function. */ setCopyText(callback) { if (typeof callback !== 'function') throw new TypeError('setCopyText expected a function that returns Promise<void>.'); this.#copyText = callback; } /** * Override the default blob copy behavior. * This allows you to provide a custom clipboard handling method for blob data. * * @param {(blob: Blob) => Promise<void>} callback - The function to use for copying blob data. * @throws {TypeError} If the callback is not a function. */ setCopyBlob(callback) { if (typeof callback !== 'function') throw new TypeError('setCopyBlob expected a function that returns Promise<void>.'); this.#copyBlob = callback; } /** * Copy a plain text string to the clipboard. * Uses modern or legacy fallback. * * @param {string} text - The text string to be copied. * @returns {Promise<void>} A promise resolving when the text is copied or boolean for legacy. */ copyText(text) { if (typeof text !== 'string') throw new TypeError('copyText expected a string.'); // Clipboard API if (this.#copyText) return this.#copyText(text); // Classic API else if (this.#existExecCommand) { const host = document.body; const copyInput = document.createElement('input'); copyInput.style.position = 'fixed'; copyInput.style.opacity = '0'; copyInput.value = text; host.append(copyInput); copyInput.select(); copyInput.setSelectionRange(0, 99999); document.execCommand('Copy'); copyInput.remove(); return new Promise((resolve) => resolve(undefined)); } throw new Error('Clipboard API not found!'); } /** * Copy a Blob (binary data) to the clipboard. * * @param {Blob} blob - The blob object to copy. * @returns {Promise<void>} A promise that resolves when the blob is copied or null on fallback. */ copyBlob(blob) { if (!(blob instanceof Blob)) throw new TypeError('copyBlob expected a Blob instance.'); return new Promise((resolve, reject) => { if (this.#copyBlob) { return this.#copyBlob(blob).then(resolve).catch(reject); } throw new Error('Clipboard API not found!'); }); } /** * Internal: Handle getting blob data from a clipboard item. * * @private * @param {string} type - The MIME type to fetch. * @param {ClipboardItem} clipboardItem - Clipboard item instance. * @returns {Promise<Blob>} A promise that resolves with the Blob. */ _handleBlob(type, clipboardItem) { return clipboardItem.getType(type); } /** * Internal: Handle getting plain text from a clipboard item. * * @private * @param {string} type - The MIME type (should be 'text/plain'). * @param {ClipboardItem} clipboardItem - Clipboard item instance. * @returns {Promise<string>} A promise that resolves with the text content. */ _handleText(type, clipboardItem) { return this._handleBlob(type, clipboardItem).then((blob) => blob.text()); } /** * Read clipboard data based on filters like type, mime, index. * * @param {number|null} [index=0] - Item index or null for all. * @param {'text'|'custom'|null} [type=null] - Data type to filter. * @param {string|null} [mimeFormat=null] - MIME type or prefix. * @param {boolean} [fixValue=false] - If true, exact match on MIME type. * @returns {Promise<Blob|string|Array<Blob|string>|null>} A promise resolving with matching data. */ _readData(index = 0, type = null, mimeFormat = null, fixValue = false) { return new Promise((resolve, reject) => { this._read(index) .then((items) => { if (!items) return resolve(null); /** @type {Array<Blob|string>} */ const finalResult = []; // Complete task let continueLoop = true; /** * @param {string} mimeType * @param {ClipboardItem} item */ const completeTask = async (mimeType, item) => { if (!continueLoop) return; // Custom if ( (type === null || type === 'custom') && typeof mimeFormat === 'string' && ((!fixValue && mimeType.startsWith(mimeFormat)) || (fixValue && mimeType === mimeFormat)) ) { continueLoop = false; const result = await this._handleBlob(mimeType, item); if (result) finalResult.push(result); } // Text else if ((type === null || type === 'text') && mimeType === 'text/plain') { continueLoop = false; const result = await this._handleText(mimeType, item); if (result) finalResult.push(result); } // Blob else if (type === null) { continueLoop = false; const result = await this._handleBlob(mimeType, item); if (result) finalResult.push(result); } }; /** @type {Promise<void>[]} */ const promises = []; /** * Read Item * @param {ClipboardItem | ClipboardItems} item */ const readItem = (item) => { if (!(item instanceof ClipboardItem)) throw new Error('Expected ClipboardItem when reading data.'); for (const tIndex in item.types) promises.push(completeTask(item.types[tIndex], item)); }; // Specific Item if ( typeof index === 'number' && !Number.isNaN(index) && Number.isFinite(index) && index > -1 ) { readItem(items); Promise.all(promises) .then(() => { if (finalResult[0]) resolve(finalResult[0]); else resolve(null); }) .catch(reject); } // All else if (Array.isArray(items)) { for (const tIndex in items) readItem(items[tIndex]); Promise.all(promises) .then(() => resolve(finalResult)) .catch(reject); } }) // Fail .catch(reject); }); } /** * Read plain text from the clipboard (single item by index). * * @param {number} [index=0] - The index of the clipboard item to read. * @returns {Promise<string|null>} A promise that resolves to the clipboard text or null. */ async readText(index = 0) { const value = await this._readData(index, 'text'); if (typeof value !== 'string') throw new Error('Failed to read text: expected string result.'); return value; } /** * Read custom clipboard data based on MIME type from a specific index. * * @param {string|null} [mimeFormat=null] - MIME prefix to match (e.g., "image/"). * @param {boolean} [fixValue=false] - If true, matches exact MIME instead of prefix. * @param {number} [index=0] - Clipboard item index. * @returns {Promise<Blob|null>} A promise resolving with a blob or null. */ async readCustom(mimeFormat = null, fixValue = false, index = 0) { const value = await this._readData(index, 'custom', mimeFormat, fixValue); if (!(value instanceof Blob)) throw new Error('Failed to read custom data: expected Blob.'); return value; } /** * Read all available plain text entries from the clipboard. * * @returns {Promise<string[]>} A promise resolving to an array of strings or null. */ async readAllTexts() { const values = await this._readData(null, 'text'); if (!Array.isArray(values)) throw new Error('Expected array of strings when reading all texts.'); if (!values.every((value) => typeof value === 'string')) throw new Error('Some values returned were not strings.'); return values; } /** * Read all clipboard data matching a specific custom MIME type. * * @param {string|null} [mimeFormat=null] - MIME prefix or exact type. * @param {boolean} [fixValue=false] - Match prefix or exact MIME. * @returns {Promise<Blob[]>} A promise resolving with array of Blobs or null. */ async readAllCustom(mimeFormat = null, fixValue = false) { const values = await this._readData(null, 'custom', mimeFormat, fixValue); if (!Array.isArray(values)) throw new Error('Expected array of blobs when reading all custom items.'); if (!values.every((value) => value instanceof Blob)) throw new Error('Some values returned were not Blob instances.'); return values; } /** * Read all clipboard data as Blob or text depending on type. * * @param {'text'|'custom'|null} [type=null] - The type of data to retrieve. * @param {string|null} [mimeFormat=null] - The MIME type or prefix to match. * @returns {Promise<Array<Blob|string>>} A promise resolving with matching data array. */ async readAllData(type = null, mimeFormat = null) { const value = await this._readData(null, type, mimeFormat); if (!Array.isArray(value)) throw new Error('Expected array result when reading all data.'); return value; } /** * Read clipboard data at a specific index or all if null. * * @param {number|null} index - Index of the item to retrieve or null to get all. * @returns {Promise<ClipboardItem|ClipboardItems|null>} A promise resolving with a clipboard item or array of items. */ _read(index) { return new Promise((resolve, reject) => { if (!this.#existNavigator) reject(new Error('Clipboard API not found!')); navigator.clipboard .read() .then((items) => { // Index is number if (typeof index === 'number') { if (Number.isNaN(index) || !Number.isFinite(index) || index < 0) throw new Error(`Invalid index value: ${index}`); if (items[index]) resolve(items[index]); // Not found else resolve(null); } // Get All resolve(items); }) .catch(reject); }); } /** * Read clipboard data at a specific index. * * @param {number} index - Index of the item to retrieve * @returns {Promise<ClipboardItem|null>} A promise resolving with a clipboard item. */ async readIndex(index) { const value = await this._read(index); if (value !== null && !(value instanceof ClipboardItem)) throw new Error(`Value at index ${index} is not a ClipboardItem.`); return value; } /** * Read all clipboard content without any filters. * * @returns {Promise<ClipboardItems>} A promise resolving with all clipboard items. */ async readAll() { const value = await this._read(null); if (!Array.isArray(value)) throw new Error('Expected array result from clipboard read.'); for (const item of value) { if (!(item instanceof ClipboardItem)) throw new Error('Invalid item type found in clipboard result.'); } return value; } /** * Returns whether the legacy `document.execCommand()` API is available. * This can be used to determine if a fallback clipboard method is usable. * * @returns {boolean} True if `document.execCommand` is available. */ isExecCommandAvailable() { return this.#existExecCommand; } /** * Returns whether the modern Clipboard API (`navigator.clipboard`) is available. * Useful to know if full clipboard features can be accessed. * * @returns {boolean} True if `navigator.clipboard` is available. */ isNavigatorClipboardAvailable() { return this.#existNavigator; } /** * Returns the function used to copy plain text to the clipboard. * This function may be built-in or set manually via `setCopyText`. * * @returns {((text: string) => Promise<void>) | null} The current text copy function or null if unavailable. */ getCopyTextFunc() { return this.#copyText; } /** * Returns the function used to copy Blob (binary data) to the clipboard. * This function may be built-in or set manually via `setCopyBlob`. * * @returns {((blob: Blob) => Promise<void>) | null} The current blob copy function or null if unavailable. */ getCopyBlobFunc() { return this.#copyBlob; } } module.exports = TinyClipboard;