Sblocco dell'accesso agli appunti

Accesso più sicuro e senza blocchi agli appunti per testo e immagini

Il modo tradizionale per accedere agli appunti di sistema era tramite document.execCommand() per le interazioni con gli appunti. Sebbene ampiamente supportato, questo metodo di taglio e incolla aveva un costo: l'accesso agli appunti era sincrono e poteva solo leggere e scrivere nel DOM.

Va bene per piccoli frammenti di testo, ma in molti casi bloccare il trasferimento della pagina negli appunti è un'esperienza negativa. Potrebbe essere necessario un processo di sanificazione o decodifica delle immagini che richiede tempo prima che i contenuti possano essere incollati in modo sicuro. Il browser potrebbe dover caricare o incorporare risorse collegate da un documento incollato. In questo modo la pagina viene bloccata durante l'attesa del disco o della rete. Immagina di aggiungere le autorizzazioni al mix, richiedendo che il browser blocchi la pagina durante la richiesta di accesso agli appunti. Allo stesso tempo, le autorizzazioni implementate in document.execCommand() per l'interazione con gli appunti sono definite in modo generico e variano a seconda del browser.

L'API Async Clipboard risolve questi problemi fornendo un modello di autorizzazioni ben definito che non blocca la pagina. L'API Async Clipboard è limitata alla gestione di testo e immagini nella maggior parte dei browser, ma il supporto varia. Assicurati di studiare attentamente la panoramica della compatibilità del browser per ciascuna delle seguenti sezioni.

Copia: scrittura dei dati negli appunti

writeText()

Per copiare il testo negli appunti, chiama writeText(). Poiché questa API è asincrona, la funzione writeText() restituisce una promessa che viene risolta o rifiutata a seconda che il testo passato venga copiato correttamente:

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

Browser Support

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

Source

write()

In realtà, writeText() è solo un metodo pratico per il metodo generico write(), che consente anche di copiare le immagini negli appunti. Come writeText(), è asincrono e restituisce una promessa.

Per scrivere un'immagine negli appunti, devi avere l'immagine come blob. Un modo per farlo è richiedere l'immagine da un server utilizzando fetch(), quindi chiamare blob() nella risposta.

Richiedere un'immagine dal server potrebbe non essere auspicabile o possibile per una serie di motivi. Fortunatamente, puoi anche disegnare l'immagine su un canvas e chiamare il metodo toBlob() del canvas.

Successivamente, passa un array di oggetti ClipboardItem come parametro al metodo write(). Al momento puoi passare una sola immagine alla volta, ma speriamo di aggiungere il supporto per più immagini in futuro. ClipboardItem accetta un oggetto con il tipo MIME dell'immagine come chiave e il blob come valore. Per gli oggetti blob ottenuti da fetch() o canvas.toBlob(), la proprietà blob.type contiene automaticamente il tipo MIME corretto per un'immagine.

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

In alternativa, puoi scrivere una promessa all'oggetto ClipboardItem. Per questo pattern, devi conoscere in anticipo il tipo MIME dei dati.

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

Browser Support

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

Source

L'evento di copia

Nel caso in cui un utente avvii una copia negli appunti e non chiami preventDefault(), l'copyevento include una proprietà clipboardData con gli elementi già nel formato corretto. Se vuoi implementare una logica personalizzata, devi chiamare preventDefault() per impedire il comportamento predefinito a favore della tua implementazione. In questo caso, clipboardData sarà vuoto. Considera una pagina con testo e un'immagine. Quando l'utente seleziona tutto e avvia una copia negli appunti, la tua soluzione personalizzata deve ignorare il testo e copiare solo l'immagine. Puoi farlo come mostrato nell'esempio di codice riportato di seguito. Questo esempio non illustra come utilizzare le API precedenti quando l'API Clipboard non è supportata.

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

Per l'evento copy:

Browser Support

  • Chrome: 1.
  • Edge: 12.
  • Firefox: 22.
  • Safari: 3.

Source

Per ClipboardItem:

Browser Support

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

Source

Incolla: lettura dei dati dagli appunti

readText()

Per leggere il testo dagli appunti, chiama navigator.clipboard.readText() e attendi che la promessa restituita venga risolta:

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

Browser Support

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

Source

read()

Il metodo navigator.clipboard.read() è anche asincrono e restituisce una promessa. Per leggere un'immagine dagli appunti, ottieni un elenco di ClipboardItem oggetti, poi esegui l'iterazione.

Ogni ClipboardItem può contenere i propri contenuti in diversi tipi, quindi dovrai iterare l'elenco dei tipi, utilizzando di nuovo un ciclo for...of. Per ogni tipo, chiama il metodo getType() con il tipo corrente come argomento per ottenere il blob corrispondente. Come in precedenza, questo codice non è associato alle immagini e funzionerà con altri tipi di file futuri.

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

Browser Support

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

Source

Utilizzo dei file incollati

È utile per gli utenti poter utilizzare le scorciatoie da tastiera degli appunti, ad esempio Ctrl+C e Ctrl+V. Chromium espone i file di sola lettura negli appunti come descritto di seguito. Questo evento viene attivato quando l'utente preme la scorciatoia di incolla predefinita del sistema operativo o quando fa clic su Modifica e poi su Incolla nella barra dei menu del browser. Non è necessario alcun codice di programmazione aggiuntivo.

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

Browser Support

  • Chrome: 3.
  • Edge: 12.
  • Firefox: 3.6.
  • Safari: 4.

Source

L'evento di incolla

Come accennato in precedenza, sono previsti eventi da utilizzare con l'API Clipboard, ma per ora puoi utilizzare l'evento paste esistente. Funziona bene con i nuovi metodi asincroni per la lettura del testo negli appunti. Come per l'evento copy, non dimenticare di chiamare preventDefault().

document.addEventListener('paste', async (e) => {
  e.preventDefault();
  const text = await navigator.clipboard.readText();
  console.log('Pasted text: ', text);
});

Browser Support

  • Chrome: 1.
  • Edge: 12.
  • Firefox: 22.
  • Safari: 3.

Source

Gestione di più tipi MIME

La maggior parte delle implementazioni inserisce più formati di dati negli appunti per una singola operazione di taglio o copia. Ciò è dovuto a due motivi: in qualità di sviluppatore di app, non hai modo di conoscere le funzionalità dell'app in cui un utente vuole copiare testo o immagini e molte applicazioni supportano l'incollatura di dati strutturati come testo normale. In genere, agli utenti viene presentato un elemento di menu Modifica con un nome come Incolla e mantieni stile o Incolla senza formattazione.

Il seguente esempio mostra come farlo. Questo esempio utilizza fetch() per ottenere i dati delle immagini, ma potrebbero provenire anche da <canvas> o dall'API File System Access.

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

Sicurezza e autorizzazioni

L'accesso agli appunti ha sempre rappresentato un problema di sicurezza per i browser. Senza autorizzazioni appropriate, una pagina potrebbe copiare silenziosamente qualsiasi tipo di contenuti dannosi negli appunti di un utente, con risultati catastrofici quando vengono incollati. Immagina una pagina web che copia silenziosamente rm -rf / o un'immagine di una bomba di decompressione negli appunti.

Richiesta del browser che chiede all&#39;utente l&#39;autorizzazione di accesso agli appunti.
La richiesta di autorizzazione per l'API Clipboard.

Concedere alle pagine web l'accesso in lettura illimitato agli appunti è ancora più problematico. Gli utenti copiano regolarmente informazioni sensibili come password e dettagli personali negli appunti, che potrebbero poi essere letti da qualsiasi pagina senza che l'utente ne sia a conoscenza.

Come per molte nuove API, l'API Clipboard è supportata solo per le pagine pubblicate tramite HTTPS. Per contribuire a prevenire abusi, l'accesso agli appunti è consentito solo quando una pagina è la scheda attiva. Le pagine nelle schede attive possono scrivere negli appunti senza richiedere l'autorizzazione, ma la lettura dagli appunti richiede sempre l'autorizzazione.

Le autorizzazioni per copia e incolla sono state aggiunte all'API Permissions. L'autorizzazione clipboard-write viene concessa automaticamente alle pagine quando sono la scheda attiva. È necessario richiedere l'autorizzazione clipboard-read, che puoi ottenere provando a leggere i dati dagli appunti. Il codice riportato di seguito mostra quest'ultima opzione:

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

Puoi anche controllare se è necessario un gesto dell'utente per richiamare il taglio o l'incollatura utilizzando l'opzione allowWithoutGesture. Il valore predefinito per questo valore varia a seconda del browser, quindi devi sempre includerlo.

È qui che la natura asincrona dell'API Clipboard si rivela davvero utile: il tentativo di leggere o scrivere dati negli appunti richiede automaticamente all'utente l'autorizzazione, se non è già stata concessa. Poiché l'API è basata su promesse, questo è completamente trasparente e un utente che nega l'autorizzazione per gli appunti fa sì che la promessa venga rifiutata, in modo che la pagina possa rispondere in modo appropriato.

Poiché i browser consentono l'accesso agli appunti solo quando una pagina è la scheda attiva, alcuni degli esempi riportati qui non vengono eseguiti se incollati direttamente nella console del browser, poiché gli strumenti per gli sviluppatori stessi sono la scheda attiva. Esiste un trucco: rimanda l'accesso agli appunti utilizzando setTimeout(), quindi fai clic rapidamente all'interno della pagina per metterla a fuoco prima che vengano chiamate le funzioni:

setTimeout(async () => {
  const text = await navigator.clipboard.readText();
  console.log(text);
}, 2000);

Integrazione dei criteri relativi alle autorizzazioni

Per utilizzare l'API negli iframe, devi abilitarla con le norme relative alle autorizzazioni, che definiscono un meccanismo che consente di abilitare e disabilitare selettivamente varie funzionalità e API del browser. In pratica, devi superare uno o entrambi i test clipboard-read o clipboard-write, a seconda delle esigenze della tua app.

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

Rilevamento delle funzionalità

Per utilizzare l'API Async Clipboard supportando tutti i browser, esegui il test per navigator.clipboard e torna ai metodi precedenti. Ad esempio, ecco come potresti implementare l'incollatura per includere altri browser.

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

Ma non è tutta la storia. Prima dell'API Async Clipboard, esisteva un mix di diverse implementazioni di copia e incolla nei browser web. Nella maggior parte dei browser, la funzionalità di copia e incolla del browser può essere attivata utilizzando document.execCommand('copy') e document.execCommand('paste'). Se il testo da copiare è una stringa non presente nel DOM, deve essere inserito nel DOM e selezionato:

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

Demo

Puoi provare l'API Async Clipboard nelle demo riportate di seguito. Il primo esempio mostra come spostare il testo negli appunti e viceversa.

Per provare l'API con le immagini, utilizza questa demo. Ricorda che sono supportati solo i PNG e solo in alcuni browser.

Ringraziamenti

L'API Async Clipboard è stata implementata da Darwin Huang e Gary Kačmarčík. Darwin ha fornito anche la demo. Grazie a Kyarik e ancora a Gary Kačmarčík per aver esaminato alcune parti di questo articolo.

Immagine hero di Markus Winkler su Unsplash.