Odblokowuję dostęp do schowka

Bezpieczniejszy, nieblokowany dostęp do schowka dla tekstu i obrazów

Tradycyjny sposób uzyskiwania dostępu do schowka systemowego był uzyskiwany przez metodę document.execCommand() na potrzeby interakcji ze schowek. Ta metoda wycinania i wklejania była szeroko obsługiwana, ale wiązała się z kosztami: dostęp do schowka był synchroniczny i można było tylko odczytywać oraz zapisywać dane w DOM.

Jest to w porządku w przypadku krótkich fragmentów tekstu, ale w wielu przypadkach blokowanie strony na potrzeby przenoszenia tekstu ze schowka nie jest wygodne. Zanim będzie można bezpiecznie wkleić treści, może być konieczne ich ręczne oczyszczanie lub dekodowanie obrazu. Przeglądarka może potrzebować zasobów z wklejonego dokumentu do załadowania lub wstawienia w tekście. Spowoduje to zablokowanie strony podczas oczekiwania na dysk lub sieć. Wyobraź sobie, że dodasz uprawnienia, które wymagają zablokowania strony podczas żądania dostępu do schowka. Jednocześnie uprawnienia dotyczące interakcji z obsługą schowka są luźno zdefiniowane i różnią się w zależności od przeglądarki.

Async Clipboard API rozwiązuje te problemy, oferując dobrze zdefiniowany model uprawnień, który nie blokuje strony. Interfejs Async Clipboard API jest ograniczony do obsługi tekstu i obrazów w większości przeglądarek, ale obsługa może się różnić. W poszczególnych sekcjach dokładnie zapoznaj się z informacjami o zgodnności z przeglądarkami.

Kopiowanie: zapisywanie danych na schowku

writeText()

Aby skopiować tekst do schowka, naciśnij writeText(). Ponieważ ten interfejs API jest asynchroniczny, funkcja writeText() zwraca obietnicę, która jest akceptowana 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);
 
}
}

Obsługa przeglądarek

  • Chrome: 66.
  • Edge: 79.
  • Firefox: 63.
  • Safari: 13.1

Źródło

zapis()

W rzeczywistości writeText() to tylko wygodna metoda dla ogólnej metody write(), która umożliwia również kopiowanie obrazów do schowka. Podobnie jak writeText(), jest asynchroniczna i zwraca obietnicę.

Aby zapisać obraz w schowku, musisz mieć obraz w formacie blob. Aby to zrobić, możesz na przykład wysłać żądanie obrazu do serwera za pomocą funkcji fetch(), a następnie wywołać metodę blob() w odpowiedzi.

Wysyłanie prośby o dostęp do obrazu z serwera może być niepożądane lub możliwe z różnych powodów. Na szczęście możesz też narysować obraz na płótnie i wywołać metodę płótna toBlob().

Następnie jako parametr metody write() prześlij tablicę obiektów ClipboardItem. Obecnie można przesłać tylko 1 obraz naraz, ale mamy nadzieję, że w przyszłości będziemy mogli dodać obsługę większej liczby obrazów. ClipboardItem przyjmuje obiekt z typem MIME obrazu jako klucz i blobem jako wartością. 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ż napisać obietnicę do obiektu 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);
}

Obsługa przeglądarek

  • Chrome: 76.
  • Krawędź: 79.
  • Firefox: 127.
  • Safari: 13.1

Źródło

Zdarzenie kopiowania

Jeśli użytkownik zainicjuje kopiowanie ze schowka, ale nie wywoła preventDefault(), zdarzenie copy będzie zawierać właściwość clipboardData z elementami w odpowiednim formacie. Jeśli chcesz zastosować własną logikę, musisz wywołać funkcję preventDefault(), aby zastąpić domyślne działanie własnym. W tym przypadku clipboardData będzie pusty. Załóżmy, że mamy stronę z tekstem i obrazem. Gdy użytkownik zaznaczy wszystko i zainicjuje kopię do schowka, niestandardowe rozwiązanie powinno odrzucić tekst i skopiować tylko obraz. Aby to zrobić, użyj przykładowego kodu poniżej. W tym przykładzie nie omawiamy sposobu korzystania z starszych 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:

Obsługa przeglądarek

  • Chrome: 1.
  • Krawędź: 12.
  • Firefox: 22.
  • Safari: 3.

Źródło

Do witryn ClipboardItem:

Obsługa przeglądarek

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 127.
  • Safari: 13.1.

Źródło

Wklej: odczytywanie danych ze schowka

readText()

Aby odczytać tekst ze schowka, zadzwoń pod numer 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);
 
}
}

Obsługa przeglądarek

  • Chrome: 66.
  • Edge: 79.
  • Firefox: 125.
  • Safari: 13.1.

Źródło

read()

Metoda navigator.clipboard.read() jest też asynchroniczna i zwraca obietnicę. Aby odczytać obraz ze schowka, pobierz listę obiektów ClipboardItem, a następnie przejdź przez nią.

Każdy element ClipboardItem może zawierać dane o różnych typach, 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() z bieżącym typem jako argumentem, 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);
 
}
}

Obsługa przeglądarek

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 127.
  • Safari: 13.1.

Źródło

Praca z wklejonymi plikami

Użytkownicy mogą korzystać ze skrótów klawiszowych w schowku, takich jak Ctrl + CCtrl + V. Chromium udostępnia pliki tylko do odczytu w schowku, jak opisano poniżej. Jest to wywoływane, gdy użytkownik kliknie domyślny skrót do wklejania w systemie operacyjnym lub gdy kliknie Edytuj, a potem Wklej na pasku menu przeglądarki. Nie musisz pisać dodatkowego kodu.

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());
});

Obsługa przeglądarek

  • Chrome:
  • Krawędź: 12.
  • Firefox: 3.6.
  • Safari: 4.

Źródło

Zdarzenie wklejania

Jak już wspomnieliśmy, 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 asynchronicznymi metodami odczytu tekstu z Schowka. Tak 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 przeglądarek

  • Chrome: 1.
  • Krawędź: 12.
  • Firefox: 22.
  • Safari: 3.

Źródło

Obsługa wielu typów MIME

Większość implementacji umieszcza wiele formatów danych na schowku w ramach pojedynczej operacji wycinania lub kopiowania. Dzieje się tak z 2 powodów: jako deweloper aplikacji nie ma możliwości poznania funkcji aplikacji, do której użytkownik chce skopiować tekst lub obrazy. Wiele aplikacji obsługuje wklejanie uporządkowanych danych jako zwykły tekst. Zwykle użytkownicy widzą opcję menu Edytuj o nazwie takiej jak Wklej i dopasuj styl lub Wklej bez formatowania.

Poniżej znajdziesz przykład, jak to zrobić. W tym przykładzie dane obrazu są pobierane za pomocą interfejsu fetch(), ale mogą też pochodzić z interfejsu <canvas> lub File System 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 bezpieczeństwa w przypadku przeglądarek. Bez odpowiednich uprawnień strona może w sposób cichy kopiować wszelkiego rodzaju złośliwe treści do schowka użytkownika, co może spowodować katastrofalne skutki po wklejeniu. Wyobraź sobie stronę internetową, która po cichu kopiuje rm -rf / lub obraz z bombą dekompresyjną do schowka.

Prośba przeglądarki o przyznanie uprawnień dostępu do schowka.
Prośba o uprawnienia do interfejsu Clipboard API.

Zapewnianie stronom internetowym nieograniczonego dostępu do odczytu zawartości schowka jest jeszcze trudniejsze. Użytkownicy często kopiują na pulpit informacje poufne, takie jak hasła i dane osobowe, które mogą być odczytane 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 aktywna jest karta z tą stroną. Strony na aktywnych kartach mogą zapisywać dane na schowku bez prośby o uprawnienia, ale odczyt 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, co możesz zrobić, próbując odczytać dane ze schowka. Poniżej przedstawiono kod, który pokazuje tę drugą opcję:

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 wycinania lub wklejania wymagany jest gest użytkownika, korzystając z opcji allowWithoutGesture. Domyślna wartość tego parametru zależy od przeglądarki, dlatego zawsze należy go uwzględniać.

W tym miejscu asynchroniczny charakter interfejsu Clipboard API okazuje się bardzo przydatny: próba odczytu lub zapisu danych w schowku systemowym automatycznie wyświetla użytkownikowi prośbę o udzielenie uprawnień, jeśli nie zostały one jeszcze przyznane. Ponieważ interfejs API opiera się na obietnicach, jest to całkowicie przejrzyste, a użytkownik odmówiający przyznania dostępu do schowka powoduje odrzucenie obietnic, dzięki czemu strona może odpowiednio zareagować.

Ponieważ przeglądarki zezwalają na dostęp do schowka tylko wtedy, gdy strona jest aktywną kartą, niektóre z podanych tu przykładów nie będą działać, jeśli wklei się je bezpośrednio do konsoli przeglądarki, ponieważ narzędzia dla programistów są aktywną kartą. Jest na to sposób: odłóż dostęp do schowka za pomocą setTimeout(), a potem szybko kliknij stronę, aby ją zaznaczyć, zanim funkcje zostaną wywołane:

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 go włączyć za pomocą zasad dotyczących uprawnień, które definiują mechanizm umożliwiający selektywne włączanie i wyłączanie różnych funkcji przeglądarki oraz interfejsów API. W zależności od potrzeb aplikacji musisz przekazać clipboard-read lub clipboard-write (lub oba te parametry).

<iframe
   
src="index.html"
   
allow="clipboard-read; clipboard-write"
>
</iframe>

Wykrywanie cech

Aby korzystać z interfejsu Async Clipboard API i jednocześnie obsługiwać wszystkie przeglądarki, przetestuj metodę navigator.clipboard i wróć do wcześniejszych metod. Oto przykład implementacji wklejania, która obejmuje inne przeglądarki.

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 interfejsu Async Clipboard API dostępne były różne sposoby kopiowania i wklejania w przeglądarkach. Większość przeglądarek umożliwia kopiowanie i wklejanie za pomocą skrótów document.execCommand('copy') i document.execCommand('paste'). Jeśli tekst, który ma zostać skopiowany, jest ciągiem znaków, który nie występuje w DOM, musi zostać wstrzyknięty do DOM i wybrany:

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();
});

Prezentacje

W poniżej zamieszczonych prezentacjach możesz zapoznać się z interfejsem Async Clipboard API. Na Glitch możesz zremiksować demo tekstu lub demo obrazu, aby eksperymentować z tymi materiałami.

Pierwszy przykład pokazuje przenoszenie tekstu do schowka i z niego.

Aby wypróbować interfejs API na przykładzie obrazów, skorzystaj z tej wersji demonstracyjnej. Pamiętaj, że obsługiwane są tylko pliki PNG i tylko w kilku przeglądarkach.

Podziękowania

Asynchroniczny interfejs API schowka został wdrożony przez Darwina Huanga i Gary'ego Kačmarčíka. Darwin przekazał też wersję demonstracyjną. Dziękujemy Kyarik i ponownie Gary’emu Kačmarčíkowi za sprawdzenie części tego artykułu.

Baner powitalny autorstwa Markus Winkler na Unsplash.