Bezpieczniejszy, nieblokowany dostęp do schowka w przypadku tekstu i obrazów
Tradycyjnym sposobem uzyskiwania dostępu do schowka systemowego było używanie interfejsu document.execCommand()
do interakcji ze schowkiem. Chociaż ta metoda wycinania i wklejania jest powszechnie obsługiwana, ma pewną wadę: dostęp do schowka jest synchroniczny i umożliwia tylko odczyt i zapis w DOM.
W przypadku krótkich fragmentów tekstu jest to w porządku, ale w wielu przypadkach blokowanie strony na potrzeby przenoszenia do schowka pogarsza komfort użytkowania. Przed bezpiecznym wklejeniem treści może być konieczne czasochłonne czyszczenie lub dekodowanie obrazu. Przeglądarka może potrzebować wczytać lub wstawić powiązane zasoby z wklejonego dokumentu. Spowodowałoby to zablokowanie strony podczas oczekiwania na dysk lub sieć. Wyobraź sobie, że dodajesz do tego uprawnienia, które wymagają, aby przeglądarka blokowała stronę podczas żądania dostępu do schowka. Jednocześnie uprawnienia dotyczące document.execCommand()
w zakresie interakcji ze schowkiem są słabo zdefiniowane i różnią się w zależności od przeglądarki.
Asynchroniczny interfejs Clipboard API rozwiązuje te problemy, zapewniając dobrze zdefiniowany model uprawnień, który nie blokuje strony. Interfejs Async Clipboard API w większości przeglądarek obsługuje tylko tekst i obrazy, ale zakres obsługi może się różnić. Zapoznaj się dokładnie z omówieniem zgodności przeglądarek w każdej z tych sekcji.
Kopiowanie: zapisywanie danych w schowku
writeText()
Aby skopiować tekst do schowka, wywołaj funkcję writeText()
. Ten interfejs API jest asynchroniczny, więc funkcja writeText()
zwraca obietnicę, która jest spełniana lub odrzucana w zależności od tego, czy przekazany tekst został skopiowany:
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);
}
}
write()
W rzeczywistości writeText()
to tylko wygodna metoda ogólnej metody write()
, która umożliwia też kopiowanie obrazów do schowka. Podobnie jak writeText()
, jest asynchroniczna i zwraca obiekt Promise.
Aby zapisać obraz w schowku, musisz mieć go w formacie blob
. Możesz to zrobić, wysyłając żądanie obrazu z serwera za pomocą fetch()
, a następnie wywołując blob()
w odpowiedzi.
Żądanie obrazu z serwera może być niepożądane lub niemożliwe z różnych powodów. Na szczęście możesz też narysować obraz na obszarze roboczym i wywołać metodę toBlob()
tego obszaru.
Następnie przekaż tablicę obiektów ClipboardItem
jako parametr do metody write()
. Obecnie możesz przesyłać tylko 1 obraz naraz, ale w przyszłości planujemy dodać obsługę wielu obrazów. ClipboardItem
przyjmuje obiekt, w którym kluczem jest typ MIME obrazu, a wartością jest obiekt blob. W przypadku obiektów blob uzyskanych z fetch()
lub canvas.toBlob()
właściwość blob.type
automatycznie zawiera prawidłowy typ MIME obrazu.
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);
}
Możesz też zapisać obietnicę w obiekcie ClipboardItem
.
W przypadku tego wzorca musisz wcześniej znać typ MIME danych.
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);
}
Kopiowanie wydarzenia
Jeśli użytkownik zainicjuje kopiowanie do schowka i nie wywoła funkcji preventDefault()
, copy
zdarzenie zawiera właściwość clipboardData
z elementami w odpowiednim formacie.
Jeśli chcesz wdrożyć własną logikę, musisz wywołać funkcję preventDefault()
, aby zapobiec domyślnemu działaniu na rzecz własnego wdrożenia.
W takim przypadku pole clipboardData
będzie puste.
Załóżmy, że na stronie znajduje się tekst i obraz. Gdy użytkownik zaznaczy wszystko i skopiuje zawartość do schowka, Twoje niestandardowe rozwiązanie powinno odrzucić tekst i skopiować tylko obraz. Możesz to zrobić w sposób pokazany w przykładowym kodzie poniżej.
W tym przykładzie nie pokazujemy, jak wrócić do wcześniejszych interfejsów API, gdy interfejs Clipboard API nie jest obsługiwany.
<!-- 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);
}
});
W przypadku wydarzenia copy
:
W przypadku ClipboardItem
:
Wklejanie: odczytywanie danych ze schowka
readText()
Aby odczytać tekst ze schowka, wywołaj funkcję navigator.clipboard.readText()
i poczekaj, aż zwrócona obietnica zostanie rozwiązana:
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);
}
}
read()
Metoda navigator.clipboard.read()
jest też asynchroniczna i zwraca obietnicę. Aby odczytać obraz ze schowka, pobierz listę
ClipboardItem
obiektów, a następnie je przejrzyj.
Każdy element ClipboardItem
może zawierać różne typy, więc musisz przejść przez listę typów, ponownie używając pętli for...of
. W przypadku każdego typu wywołaj metodę getType()
, przekazując bieżący typ jako argument, aby uzyskać odpowiedni obiekt blob. Podobnie jak wcześniej, ten kod nie jest powiązany z obrazami i będzie działać z innymi typami plików w przyszłości.
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);
}
}
Praca z wklejonymi plikami
Użytkownicy mogą używać skrótów klawiszowych schowka, takich jak Ctrl+C i Ctrl+V. Chromium udostępnia tylko do odczytu pliki w schowku w sposób opisany poniżej. Jest uruchamiana, gdy użytkownik użyje domyślnego skrótu klawiszowego w systemie operacyjnym lub kliknie Edytuj, a następnie Wklej na pasku menu przeglądarki. Nie jest wymagany żaden dodatkowy kod instalacji wodno-kanalizacyjnej.
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());
});
Zdarzenie wklejania
Jak wspomnieliśmy wcześniej, planujemy wprowadzić zdarzenia, które będą działać z interfejsem Clipboard API, ale na razie możesz używać istniejącego zdarzenia paste
. Dobrze współpracuje z nowymi metodami asynchronicznego odczytywania tekstu ze schowka. Podobnie jak w przypadku zdarzenia copy
, nie zapomnij wywołać funkcji preventDefault()
.
document.addEventListener('paste', async (e) => {
e.preventDefault();
const text = await navigator.clipboard.readText();
console.log('Pasted text: ', text);
});
Obsługa wielu typów MIME
Większość implementacji umieszcza w schowku wiele formatów danych w ramach jednej operacji wycinania lub kopiowania. Są 2 powody, dla których tak się dzieje: jako deweloper aplikacji nie masz możliwości poznania funkcji aplikacji, do której użytkownik chce skopiować tekst lub obrazy, a wiele aplikacji obsługuje wklejanie danych strukturalnych jako zwykłego tekstu. Zazwyczaj jest to prezentowane użytkownikom w menu Edytuj z nazwą taką jak Wklej i dopasuj styl lub Wklej bez formatowania.
Poniższy przykład pokazuje, jak to zrobić. W tym przykładzie do uzyskania danych obrazu użyto elementu fetch()
, ale mogą one też pochodzić z elementu <canvas>
lub interfejsu File System Access 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]);
}
Zabezpieczenia i uprawnienia
Dostęp do schowka zawsze stanowił problem związany z bezpieczeństwem przeglądarek. Bez odpowiednich uprawnień strona może po cichu skopiować do schowka użytkownika różnego rodzaju złośliwe treści, które po wklejeniu mogą mieć katastrofalne skutki.
Wyobraź sobie stronę internetową, która po cichu kopiuje do schowka rm -rf /
lub obraz bomby dekompresyjnej.

Nieograniczony dostęp do schowka dla stron internetowych jest jeszcze bardziej problematyczny. Użytkownicy często kopiują do schowka informacje poufne, takie jak hasła i dane osobowe, które mogą być odczytywane przez dowolną stronę bez wiedzy użytkownika.
Podobnie jak w przypadku wielu nowych interfejsów API, interfejs Clipboard API jest obsługiwany tylko w przypadku stron udostępnianych przez HTTPS. Aby zapobiec nadużyciom, dostęp do schowka jest dozwolony tylko wtedy, gdy strona jest aktywną kartą. Strony w aktywnych kartach mogą zapisywać dane w schowku bez konieczności proszenia o uprawnienia, ale odczytywanie danych ze schowka zawsze wymaga uprawnień.
Uprawnienia do kopiowania i wklejania zostały dodane do interfejsu Permissions API.
Uprawnienie clipboard-write
jest przyznawane automatycznie stronom, gdy są one aktywną kartą. Musisz poprosić o uprawnienie clipboard-read
. Możesz to zrobić, próbując odczytać dane ze schowka. Poniższy kod pokazuje drugą z tych opcji:
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);
};
Możesz też określić, czy do wywołania wycinania lub wklejania wymagany jest gest użytkownika, korzystając z opcji allowWithoutGesture
. Wartość domyślna tego parametru różni się w zależności od przeglądarki, dlatego zawsze należy go uwzględniać.
W tym miejscu przydaje się asynchroniczny charakter interfejsu Clipboard API: próba odczytania lub zapisania danych w schowku automatycznie wyświetla użytkownikowi prośbę o zezwolenie, jeśli nie zostało ono jeszcze przyznane. Ponieważ interfejs API jest oparty na obietnicach, jest to całkowicie przejrzyste, a odmowa przez użytkownika uprawnień do schowka powoduje odrzucenie obietnicy, dzięki czemu strona może odpowiednio zareagować.
Przeglądarki zezwalają na dostęp do schowka tylko wtedy, gdy strona jest aktywną kartą. Dlatego niektóre z przykładów nie działają, jeśli zostaną wklejone bezpośrednio do konsoli przeglądarki, ponieważ narzędzia deweloperskie są aktywną kartą. Jest na to sposób: odłóż dostęp do schowka za pomocą setTimeout()
, a następnie szybko kliknij stronę, aby ją uaktywnić, zanim zostaną wywołane funkcje:
setTimeout(async () => {
const text = await navigator.clipboard.readText();
console.log(text);
}, 2000);
Integracja zasad dotyczących uprawnień
Aby używać interfejsu API w ramkach iframe, musisz włączyć go za pomocą zasad dotyczących uprawnień, które określają mechanizm umożliwiający selektywne włączanie i wyłączanie różnych funkcji przeglądarki i interfejsów API. W zależności od potrzeb aplikacji musisz przekazać wartość pola clipboard-read
lub clipboard-write
albo obie te wartości.
<iframe
src="index.html"
allow="clipboard-read; clipboard-write"
>
</iframe>
Wykrywanie cech
Aby korzystać z interfejsu Async Clipboard API i obsługiwać wszystkie przeglądarki, przetestuj navigator.clipboard
i wróć do wcześniejszych metod. Poniżej znajdziesz przykład implementacji wklejania w innych przeglądarkach.
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);
});
To nie wszystko. Przed wprowadzeniem asynchronicznego interfejsu Clipboard API w różnych przeglądarkach internetowych stosowano różne implementacje kopiowania i wklejania. W większości przeglądarek kopiowanie i wklejanie można wywołać za pomocą skrótów klawiszowych document.execCommand('copy')
i document.execCommand('paste')
. Jeśli tekst do skopiowania jest ciągiem znaków, którego nie ma w DOM, należy go wstrzyknąć do DOM i zaznaczyć:
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();
});
Przykłady
Możesz wypróbować interfejs Async Clipboard API w poniższych wersjach demonstracyjnych. Pierwszy przykład pokazuje przenoszenie tekstu do schowka i z niego.
Aby wypróbować interfejs API na obrazach, skorzystaj z tej wersji demonstracyjnej. Pamiętaj, że obsługiwane są tylko pliki PNG i tylko w kilku przeglądarkach.
Powiązane artykuły
Podziękowania
Asynchroniczny interfejs Clipboard API został wdrożony przez Darwina Huanga i Gary’ego Kačmarčíka. Darwin również udostępnił wersję demonstracyjną. Dziękujemy Kyarik i Gary’emu Kačmarčíkowi za sprawdzenie części tego artykułu.
Baner powitalny: Markus Winkler, Unsplash.