Более безопасный и разблокированный доступ к буферу обмена для текста и изображений.
Традиционным способом получения доступа к системному буферу обмена было использование document.execCommand()
для взаимодействия с буфером обмена. Несмотря на широкую поддержку, этот метод вырезания и вставки стоил дорого: доступ к буферу обмена был синхронным и мог только читать и записывать в DOM.
Это нормально для небольших фрагментов текста, но во многих случаях блокировать страницу для передачи из буфера обмена нежелательно. Прежде чем можно будет безопасно вставить контент, может потребоваться трудоемкая очистка или декодирование изображений. Браузеру может потребоваться загрузить или встроить связанные ресурсы из вставленного документа. Это заблокирует страницу во время ожидания на диске или в сети. Представьте себе добавление разрешений, требующих, чтобы браузер блокировал страницу при запросе доступа к буферу обмена. В то же время разрешения, установленные для document.execCommand()
для взаимодействия с буфером обмена, определены слабо и различаются в зависимости от браузера.
API Async Clipboard решает эти проблемы, предоставляя четко определенную модель разрешений, которая не блокирует страницу. API Async Clipboard ограничен обработкой текста и изображений в большинстве браузеров, но поддержка различается. Обязательно внимательно изучите обзор совместимости браузеров для каждого из следующих разделов.
Копировать: запись данных в буфер обмена.
записьТекст()
Чтобы скопировать текст в буфер обмена, вызовите writeText()
. Поскольку этот API является асинхронным, функция writeText()
возвращает обещание, которое разрешается или отклоняется в зависимости от того, успешно ли скопирован переданный текст:
async function copyPageUrl() {
try {
await navigator.clipboard.writeText(location.href);
console.log('Page URL copied to clipboard');
} catch (err) {
console.error('Failed to copy: ', err);
}
}
писать()
На самом деле writeText()
— это всего лишь удобный метод для общего метода write()
, который также позволяет копировать изображения в буфер обмена. Как и writeText()
, он асинхронен и возвращает обещание.
Чтобы записать изображение в буфер обмена, вам нужно изображение в виде blob
. Один из способов сделать это — запросить изображение с сервера с помощью fetch()
и затем вызвать blob()
в ответ.
Запрос изображения с сервера может быть нежелательным или невозможным по ряду причин. К счастью, вы также можете нарисовать изображение на холсте и вызвать метод toBlob()
холста.
Затем передайте массив объектов ClipboardItem
в качестве параметра методу write()
. В настоящее время вы можете передавать только одно изображение за раз, но мы надеемся добавить поддержку нескольких изображений в будущем. ClipboardItem
принимает объект с MIME-типом изображения в качестве ключа и большой двоичный объект в качестве значения. Для объектов blob, полученных с помощью fetch()
или canvas.toBlob()
, свойство blob.type
автоматически содержит правильный тип MIME для изображения.
try {
const imgURL = '/images/generic/file.png';
const data = await fetch(imgURL);
const blob = await data.blob();
await navigator.clipboard.write([
new ClipboardItem({
// The key is determined dynamically based on the blob's type.
[blob.type]: blob
})
]);
console.log('Image copied.');
} catch (err) {
console.error(err.name, err.message);
}
Альтернативно вы можете написать обещание для объекта ClipboardItem
. Для этого шаблона вам необходимо заранее знать MIME-тип данных.
try {
const imgURL = '/images/generic/file.png';
await navigator.clipboard.write([
new ClipboardItem({
// Set the key beforehand and write a promise as the value.
'image/png': fetch(imgURL).then(response => response.blob()),
})
]);
console.log('Image copied.');
} catch (err) {
console.error(err.name, err.message);
}
Событие копирования
В случае, когда пользователь инициирует копирование в буфер обмена и не вызывает preventDefault()
, событие copy
включает свойство clipboardData
с элементами, уже имеющими правильный формат. Если вы хотите реализовать свою собственную логику, вам нужно вызвать preventDefault()
чтобы предотвратить поведение по умолчанию в пользу вашей собственной реализации. В этом случае clipboardData
будет пустым. Рассмотрим страницу с текстом и изображением. Когда пользователь выбирает все и запускает копирование в буфер обмена, ваше специальное решение должно отбрасывать текст и копировать только изображение. Этого можно добиться, как показано в примере кода ниже. В этом примере не рассматривается, как вернуться к более ранним API, если API буфера обмена не поддерживается.
<!-- The image we want on the clipboard. -->
<img src="kitten.webp" alt="Cute kitten.">
<!-- Some text we're not interested in. -->
<p>Lorem ipsum</p>
document.addEventListener("copy", async (e) => {
// Prevent the default behavior.
e.preventDefault();
try {
// Prepare an array for the clipboard items.
let clipboardItems = [];
// Assume `blob` is the blob representation of `kitten.webp`.
clipboardItems.push(
new ClipboardItem({
[blob.type]: blob,
})
);
await navigator.clipboard.write(clipboardItems);
console.log("Image copied, text ignored.");
} catch (err) {
console.error(err.name, err.message);
}
});
Для события copy
:
Для ClipboardItem
:
Вставить: чтение данных из буфера обмена.
читатьТекст()
Чтобы прочитать текст из буфера обмена, вызовите navigator.clipboard.readText()
и дождитесь разрешения возвращенного обещания:
async function getClipboardContents() {
try {
const text = await navigator.clipboard.readText();
console.log('Pasted content: ', text);
} catch (err) {
console.error('Failed to read clipboard contents: ', err);
}
}
читать()
Метод navigator.clipboard.read()
также является асинхронным и возвращает обещание. Чтобы прочитать изображение из буфера обмена, получите список объектов ClipboardItem
, а затем выполните итерацию по ним.
Каждый ClipboardItem
может содержать свое содержимое разных типов, поэтому вам придется перебирать список типов, снова используя цикл for...of
. Для каждого типа вызовите метод getType()
с текущим типом в качестве аргумента, чтобы получить соответствующий большой двоичный объект. Как и раньше, этот код не привязан к изображениям и в будущем будет работать с другими типами файлов.
async function getClipboardContents() {
try {
const clipboardItems = await navigator.clipboard.read();
for (const clipboardItem of clipboardItems) {
for (const type of clipboardItem.types) {
const blob = await clipboardItem.getType(type);
console.log(URL.createObjectURL(blob));
}
}
} catch (err) {
console.error(err.name, err.message);
}
}
Работа с вставленными файлами
Пользователям будет полезно иметь возможность использовать сочетания клавиш буфера обмена, такие как ctrl + c и ctrl + v . Chromium размещает в буфере обмена файлы, доступные только для чтения, как показано ниже. Это срабатывает, когда пользователь нажимает ярлык вставки операционной системы по умолчанию или когда пользователь нажимает «Редактировать», а затем «Вставить» в строке меню браузера. Никакого дополнительного сантехнического кода не требуется.
document.addEventListener("paste", async e => {
e.preventDefault();
if (!e.clipboardData.files.length) {
return;
}
const file = e.clipboardData.files[0];
// Read the file's contents, assuming it's a text file.
// There is no way to write back to it.
console.log(await file.text());
});
Событие вставки
Как отмечалось ранее, планируется ввести события для работы с API буфера обмена, но пока вы можете использовать существующее событие paste
. Он прекрасно работает с новыми асинхронными методами чтения текста из буфера обмена. Как и в случае с событием copy
, не забудьте вызвать preventDefault()
.
document.addEventListener('paste', async (e) => {
e.preventDefault();
const text = await navigator.clipboard.readText();
console.log('Pasted text: ', text);
});
Обработка нескольких типов MIME
Большинство реализаций помещают в буфер обмена несколько форматов данных для одной операции вырезания или копирования. Для этого есть две причины: как разработчик приложения вы не можете знать возможности приложения, в которое пользователь хочет скопировать текст или изображения, а многие приложения поддерживают вставку структурированных данных в виде обычного текста. Обычно это предоставляется пользователям с помощью пункта меню «Правка» с таким именем, как «Вставить и сопоставить стиль» или «Вставить без форматирования» .
В следующем примере показано, как это сделать. В этом примере используется fetch()
для получения данных изображения, но они также могут быть получены из <canvas>
или API доступа к файловой системе .
async function copy() {
const image = await fetch('kitten.png').then(response => response.blob());
const text = new Blob(['Cute sleeping kitten'], {type: 'text/plain'});
const item = new ClipboardItem({
'text/plain': text,
'image/png': image
});
await navigator.clipboard.write([item]);
}
Безопасность и разрешения
Доступ к буферу обмена всегда представлял проблему безопасности для браузеров. Без надлежащих разрешений страница может незаметно скопировать всевозможный вредоносный контент в буфер обмена пользователя, что приведет к катастрофическим результатам при вставке. Представьте себе веб-страницу, которая автоматически копирует rm -rf /
или изображение декомпрессионной бомбы в буфер обмена.
Предоставление веб-страницам беспрепятственного доступа к буферу обмена для чтения еще более проблематично. Пользователи регулярно копируют конфиденциальную информацию, такую как пароли и личные данные, в буфер обмена, который затем может быть прочитан на любой странице без ведома пользователя.
Как и многие новые API, API буфера обмена поддерживается только для страниц, обслуживаемых по протоколу HTTPS. Чтобы предотвратить злоупотребления, доступ к буферу обмена разрешен только тогда, когда страница является активной вкладкой. Страницы на активных вкладках могут записывать в буфер обмена без запроса разрешения, но чтение из буфера обмена всегда требует разрешения.
В Permissions API добавлены разрешения на копирование и вставку. Разрешение clipboard-write
автоматически предоставляется страницам, когда они являются активной вкладкой. Необходимо запросить разрешение clipboard-read
, что можно сделать, попытавшись прочитать данные из буфера обмена. Код ниже показывает последнее:
const queryOpts = { name: 'clipboard-read', allowWithoutGesture: false };
const permissionStatus = await navigator.permissions.query(queryOpts);
// Will be 'granted', 'denied' or 'prompt':
console.log(permissionStatus.state);
// Listen for changes to the permission state
permissionStatus.onchange = () => {
console.log(permissionStatus.state);
};
Вы также можете контролировать, требуется ли жест пользователя для вызова вырезания или вставки, используя allowWithoutGesture
. Значение по умолчанию для этого значения зависит от браузера, поэтому его следует всегда включать.
Вот тут-то асинхронный характер API буфера обмена действительно пригодится: попытка чтения или записи данных буфера обмена автоматически запрашивает у пользователя разрешение, если оно еще не было предоставлено. Поскольку API основан на обещаниях, он полностью прозрачен, и отказ пользователя в разрешении на буфер обмена приводит к отклонению обещания, чтобы страница могла отреагировать соответствующим образом.
Поскольку браузеры разрешают доступ к буферу обмена только тогда, когда страница является активной вкладкой, вы обнаружите, что некоторые из приведенных здесь примеров не запускаются, если вставить их непосредственно в консоль браузера, поскольку активными вкладками являются сами инструменты разработчика. Есть хитрость: отложите доступ к буферу обмена с помощью setTimeout()
, а затем быстро щелкните внутри страницы, чтобы сфокусировать ее до вызова функций:
setTimeout(async () => {
const text = await navigator.clipboard.readText();
console.log(text);
}, 2000);
Интеграция политики разрешений
Чтобы использовать API в iframe, вам необходимо включить его с помощью Permissions Policy , которая определяет механизм, позволяющий выборочно включать и отключать различные функции браузера и API. Конкретно, вам необходимо передать один или оба clipboard-read
или clipboard-write
, в зависимости от потребностей вашего приложения.
<iframe
src="index.html"
allow="clipboard-read; clipboard-write"
>
</iframe>
Обнаружение функций
Чтобы использовать API Async Clipboard при поддержке всех браузеров, проверьте navigator.clipboard
и вернитесь к более ранним методам. Например, вот как можно реализовать вставку для включения других браузеров.
document.addEventListener('paste', async (e) => {
e.preventDefault();
let text;
if (navigator.clipboard) {
text = await navigator.clipboard.readText();
}
else {
text = e.clipboardData.getData('text/plain');
}
console.log('Got pasted text: ', text);
});
Это еще не вся история. До API Async Clipboard в веб-браузерах существовало множество различных реализаций копирования и вставки. В большинстве браузеров собственное копирование и вставку браузера можно запустить с помощью document.execCommand('copy')
и document.execCommand('paste')
. Если копируемый текст представляет собой строку, отсутствующую в DOM, его необходимо вставить в DOM и выбрать:
button.addEventListener('click', (e) => {
const input = document.createElement('input');
input.style.display = 'none';
document.body.appendChild(input);
input.value = text;
input.focus();
input.select();
const result = document.execCommand('copy');
if (result === 'unsuccessful') {
console.error('Failed to copy text.');
}
input.remove();
});
Демо
Вы можете поиграть с API Async Clipboard в демонстрациях ниже. На Glitch вы можете сделать ремикс текстовой демонстрации или демонстрации изображения, чтобы поэкспериментировать с ними.
Первый пример демонстрирует перемещение текста в буфер обмена и за его пределы.
Чтобы попробовать API с изображениями, используйте эту демонстрацию. Напомним, что поддерживаются только PNG и только в нескольких браузерах .
Ссылки по теме
Благодарности
API асинхронного буфера обмена был реализован Дарвином Хуангом и Гэри Качмарчиком . Дарвин также предоставил демо-версию. Спасибо Кьярику и еще раз Гэри Качмарчику за рецензирование некоторых частей этой статьи.
Героическое изображение Маркуса Винклера на Unsplash .