0 directories, 7 files

util

Home / testing / ai / tinai / util
/**
 * Asynchronous, clipboard, and timer utilities for TinAI.
 */

import { extractFormattedContent } from './format-utils.js';

/**
 * Creates a debounced function that delays invoking func until after wait milliseconds.
 * Provides a .cancel() method to abort any scheduled execution.
 * @param {Function} func - Function to debounce.
 * @param {number} [wait=250] - Milliseconds to delay.
 * @returns {Function & { cancel: Function }} Debounced wrapper function with cancel method.
 */
export function debounce(func, wait = 250) {
	let timeout = null;
	const executedFunction = function(...args) {
		const later = () => {
			timeout = null;
			func.apply(this, args);
		};
		if (timeout) {
			clearTimeout(timeout);
			timeout = null;
		}
		timeout = setTimeout(later, wait);
	};
	executedFunction.cancel = function() {
		if (timeout) {
			clearTimeout(timeout);
			timeout = null;
		}
	};
	return executedFunction;
}

/**
 * Copies plain text or rich HTML/images to the clipboard with modern clipboard API and fallback.
 * Supports strings, HTMLElements, or objects with text and html fields.
 * @param {string|HTMLElement|{ text: string, html?: string }} payload - Content or element to copy.
 * @param {HTMLElement|null} [feedbackButton=null] - Optional button to show visual feedback on.
 * @returns {Promise<boolean>} Whether copy succeeded.
 */
export async function copyToClipboard(payload, feedbackButton = null) {
	const applyFeedback = () => {
		if (!feedbackButton) return;
		const originalText = feedbackButton.textContent;
		feedbackButton.textContent = 'Copied!';
		feedbackButton.disabled = true;
		setTimeout(() => {
			feedbackButton.textContent = originalText;
			feedbackButton.disabled = false;
		}, 1500);
	};

	let plainText = '';
	let htmlText = '';

	if (payload instanceof HTMLElement || (payload && typeof payload === 'object' && payload.nodeType === 1)) {
		const extracted = await extractFormattedContent(payload);
		plainText = extracted.text;
		htmlText = extracted.html;
	} else if (payload && typeof payload === 'object' && (payload.text !== undefined || payload.html !== undefined)) {
		plainText = payload.text || '';
		htmlText = payload.html || '';
	} else {
		plainText = typeof payload === 'string' ? payload : String(payload || '');
	}

	if (navigator.clipboard && typeof navigator.clipboard.write === 'function' && typeof ClipboardItem !== 'undefined') {
		try {
			const clipboardItems = {};
			if (plainText) {
				clipboardItems['text/plain'] = new Blob([plainText], { type: 'text/plain' });
			}
			if (htmlText) {
				clipboardItems['text/html'] = new Blob([htmlText], { type: 'text/html' });
			}
			if (Object.keys(clipboardItems).length > 0) {
				await navigator.clipboard.write([new ClipboardItem(clipboardItems)]);
				applyFeedback();
				return true;
			}
		} catch (err) {
			console.warn('navigator.clipboard.write failed, falling back to writeText:', err);
		}
	}

	if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
		try {
			await navigator.clipboard.writeText(plainText);
			applyFeedback();
			return true;
		} catch (err) {
			console.warn('navigator.clipboard.writeText failed, falling back to legacy execCommand:', err);
		}
	}

	try {
		const textarea = document.createElement('textarea');
		textarea.value = plainText;
		textarea.setAttribute('readonly', '');
		textarea.style.position = 'fixed';
		textarea.style.left = '-9999px';
		textarea.style.top = '0';
		document.body.appendChild(textarea);
		textarea.select();
		const successful = document['execCommand']('copy');
		document.body.removeChild(textarea);
		if (successful) {
			applyFeedback();
		}
		return !!successful;
	} catch (err) {
		console.error('All clipboard copy methods failed:', err);
		return false;
	}
}
🌐
async-utils.js ×
Type: Web, text/x-java
3.69 Kilobytes
Last Modified 2026-09-05 04:01:53
⬇ Download File