Clipboard API на практике: чтение/запись буфера обмена браузера и политики безопасности
Clipboard API: Современный стандарт взаимодействия с буфером обмена
Операции с буфером обмена в браузере прошли путь от document.execCommand до Async Clipboard API:
| Аспект | execCommand('copy') | Async Clipboard API |
|---|---|---|
| Стандарт | Устаревший | Рекомендован W3C |
| Асинхронность | Нет (синхронная блокировка) | Да (на основе Promise) |
| Разрешения | Неявные (жест пользователя) | Явная Permissions API |
| Форматированный текст | Не поддерживается | Поддерживается (ClipboardItem) |
| Пользовательские форматы | Не поддерживается | Поддерживается (веб-пользовательские форматы) |
| Чтение/запись изображений | Не поддерживается | Поддерживается |
| Надёжность | Зависит от выделения | Прямой доступ к буферу обмена |
Подробный разбор основного API
Интерфейсы Clipboard API
interface Clipboard extends EventTarget {
read(): Promise<ClipboardItem[]>;
readText(): Promise<string>;
write(data: ClipboardItem[]): Promise<void>;
writeText(text: string): Promise<void>;
}
interface ClipboardItem {
readonly types: string[];
getType(type: string): Promise<Blob>;
}
Чтение/запись текста
async function copyText(text: string) {
try {
await navigator.clipboard.writeText(text);
showToast('Скопировано в буфер обмена');
} catch (err) {
console.error('Ошибка копирования:', err);
fallbackCopyText(text);
}
}
async function pasteText(): Promise<string> {
try {
const text = await navigator.clipboard.readText();
return text;
} catch (err) {
console.error('Ошибка чтения:', err);
return '';
}
}
Запись форматированного текста и мультиформатная запись
Мультиформатный ClipboardItem
async function copyRichText(html: string, plainText: string) {
const clipboardItem = new ClipboardItem({
'text/html': new Blob([html], { type: 'text/html' }),
'text/plain': new Blob([plainText], { type: 'text/plain' }),
});
await navigator.clipboard.write([clipboardItem]);
}
copyRichText(
'<strong>Важно</strong>: Отправить до <em>пятницы</em>',
'Важно: Отправить до пятницы'
);
Копирование изображения
async function copyImage(pngBlob: Blob) {
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': pngBlob })
]);
}
async function copyCanvas(canvas: HTMLCanvasElement) {
const blob = await new Promise<Blob>((resolve) => {
canvas.toBlob((b) => resolve(b!), 'image/png');
});
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': blob })
]);
}
Вставка изображения
document.addEventListener('paste', async (e) => {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.type.startsWith('image/')) {
const blob = item.getAsFile()!;
const url = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = url;
document.body.appendChild(img);
}
}
});
Пользовательские MIME-типы (веб-пользовательские форматы)
async function copyCustomData(data: object) {
const json = JSON.stringify(data);
await navigator.clipboard.write([
new ClipboardItem({
'web application/json': new Blob([json], { type: 'application/json' }),
'text/plain': new Blob([json], { type: 'text/plain' }),
})
]);
}
async function pasteCustomData(): Promise<object | null> {
const items = await navigator.clipboard.read();
for (const item of items) {
if (item.types.includes('web application/json')) {
const blob = await item.getType('web application/json');
const text = await blob.text();
return JSON.parse(text);
}
}
return null;
}
Префикс
web— требование безопасности для пользовательских форматов, предотвращающее чтение страницами нативных форматов буфера обмена.
Разрешения и политики безопасности
Permissions API
async function checkClipboardPermission(): Promise<boolean> {
try {
const result = await navigator.permissions.query({
name: 'clipboard-read' as PermissionName
});
return result.state === 'granted';
} catch {
return false;
}
}
Ограничения безопасности
| Операция | Требование | Примечания |
|---|---|---|
writeText |
Активация пользователя | Нужен жест клика/клавиши |
write |
Активация пользователя | То же, что выше |
readText |
Активация пользователя + разрешение | Запрос авторизации пользователя |
read |
Активация пользователя + разрешение | Запрос авторизации пользователя |
| Окно в фокусе | Документ должен быть в фокусе | Вызовы в нефокусированном окне отклоняются |
Проверка активации пользователя
button.addEventListener('click', async () => {
// ✅ Внутри события клика — активация пользователя присутствует
await navigator.clipboard.writeText('скопировано!');
});
setTimeout(async () => {
await navigator.clipboard.writeText('ошибка'); // ❌ DOMException
}, 1000);
Резервный вариант: execCommand
function fallbackCopyText(text: string): boolean {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
const success = document.execCommand('copy');
document.body.removeChild(textarea);
return success;
} catch {
document.body.removeChild(textarea);
return false;
}
}
async function safeCopy(text: string) {
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {}
}
return fallbackCopyText(text);
}
Глубокая обработка события вставки
Перехват и преобразование содержимого вставки
editor.addEventListener('paste', (e) => {
e.preventDefault();
const clipboardData = e.clipboardData!;
const html = clipboardData.getData('text/html');
if (html) {
const sanitized = sanitizeHTML(html);
insertHTML(sanitized);
return;
}
const text = clipboardData.getData('text/plain');
if (text) {
const linked = text.replace(
/https?:\/\/\S+/g,
(url) => `<a href="${url}">${url}</a>`
);
insertHTML(linked);
}
});
Определение типа данных буфера обмена
editor.addEventListener('paste', (e) => {
const data = e.clipboardData!;
console.log('Типы:', [...data.types]);
const priorityOrder = [
'text/html',
'application/json',
'text/plain',
'image/png',
];
for (const type of priorityOrder) {
if (data.types.includes(type)) {
const content = data.getData(type);
console.log(`Вставка ${type}:`, content.slice(0, 100));
break;
}
}
});
Практика: Компонент кнопки копирования
class CopyButton {
private timeoutId: number | null = null;
constructor(private button: HTMLButtonElement, private getText: () => string) {
button.addEventListener('click', () => this.handleCopy());
}
private async handleCopy() {
const text = this.getText();
const success = await safeCopy(text);
if (success) {
this.showFeedback('Скопировано!');
} else {
this.showFeedback('Ошибка');
}
}
private showFeedback(message: string) {
const original = this.button.textContent;
this.button.textContent = message;
this.button.disabled = true;
if (this.timeoutId) clearTimeout(this.timeoutId);
this.timeoutId = window.setTimeout(() => {
this.button.textContent = original;
this.button.disabled = false;
}, 2000);
}
}
Совместимость браузеров
| API | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
writeText |
66+ | 63+ | 13.1+ | 79+ |
readText |
66+ | 63+ | 13.1+ | 79+ |
write (ClipboardItem) |
76+ | 87+ | 13.1+ | 79+ |
read |
76+ | 87+ | 13.1+ | 79+ |
| Web Custom Formats | 104+ | Нет | Нет | 104+ |
| Буфер обмена изображений | 76+ | 87+ | 13.1+ | 79+ |
Лучшие практики
- Всегда предоставляйте резервный вариант: Используйте
execCommand, если Clipboard API недоступна - Вызывайте внутри активации пользователя: Копирование должно инициироваться в обработчиках клика/клавиши
- Всегда показывайте обратную связь: Визуальная обратная связь как при успехе, так и при неудаче
- Читайте с осторожностью:
read()затрагивает конфиденциальность — запрашивайте только при необходимости - Очищайте содержимое вставки: HTML-вставка должна фильтроваться от рисков XSS
Итоги
Async Clipboard API — это стандартный подход для современных операций с буфером обмена в браузере, поддерживающий чтение/запись текста, форматированного текста, изображений и пользовательских форматов. Надёжнее, безопаснее и функциональнее, чем execCommand, но требует внимания к активации пользователя и моделям разрешений.
Используйте Clipboard Viewer для отладки содержимого буфера обмена, Замена текста для пакетной обработки вставки и Изменение регистра для нормализации текста из буфера обмена.
Попробуйте эти локальные браузерные инструменты — регистрация не требуется →