Abschnitt 19 · Lektion 4
Zwischenablage und Teilen
Diese Lektion hat keine Aufgabe, und der Grund dafür ist derselbe, aus dem es die Funktion überhaupt gibt.
Kopieren braucht einen echten Klick
<p><code id="befehl">npm run dev</code></p>
<p><button id="kopieren" type="button">Kopieren</button></p>
<p id="rueckmeldung"></p> body {
font-family: system-ui, sans-serif;
max-width: 40rem;
margin: 0;
padding: 1rem;
line-height: 1.6;
color: #1c1917;
}
p {
margin: 0 0 0.75rem;
}
button,
input,
select,
textarea {
font: inherit;
}
button {
padding: 0.4rem 0.8rem;
border: 1px solid #d6d3d1;
border-radius: 6px;
background: #fafaf9;
cursor: pointer;
} const knopf = document.getElementById("kopieren");
const befehl = document.getElementById("befehl");
const rueckmeldung = document.getElementById("rueckmeldung");
// Erst prüfen, ob es die API überhaupt gibt. In dieser
// Seite gibt es sie meistens schon, abgelehnt wird erst
// das Schreiben. Warum, steht unten im Artikel.
if (!navigator.clipboard) {
rueckmeldung.textContent = "Die Zwischenablage steht hier nicht zur Verfügung.";
knopf.disabled = true;
}
knopf.addEventListener("click", async () => {
try {
// writeText gibt ein Promise zurück.
await navigator.clipboard.writeText(befehl.textContent);
// Ohne Rückmeldung weiß niemand, ob etwas passiert ist.
rueckmeldung.textContent = "Kopiert.";
setTimeout(() => {
rueckmeldung.textContent = "";
}, 2000);
} catch (fehler) {
rueckmeldung.textContent = "Kopieren hat nicht geklappt. Bitte von Hand markieren.";
console.warn("Kopieren abgelehnt:", fehler.name);
}
}); navigator.clipboard.writeText(text) schreibt Text in die Zwischenablage und gibt ein Promise zurück.
An zwei Bedingungen hängt es:
Ein sicherer Kontext. Die Seite muss über https geladen sein, localhost zählt auch. Über http gibt es die API schlicht nicht. Ob du in einem sicheren Kontext sitzt, beantwortet die Eingabezeile der Console mit window.isSecureContext. Hier steht dort true, und trotzdem wird das Kopieren abgelehnt. Warum, steht am Ende dieser Lektion.
Eine echte Nutzergeste. Der Aufruf muss aus einem Klick oder Tastendruck heraus kommen, den ein Mensch ausgelöst hat. Ein Skript, das beim Laden in die Zwischenablage schreibt, wird abgewiesen.
Beides ist Absicht. Die Zwischenablage gehört dem Nutzer, und was dort liegt, ist oft genau das, was er gerade woanders einfügen wollte. Eine Seite, die sie ungefragt überschreibt, wäre ein Ärgernis, und eine, die sie ungefragt liest, wäre schlimmer.
Die Rückmeldung gehört dazu
Kopieren ist unsichtbar. Nichts blinkt, nichts bewegt sich, und der Nutzer hat keine Möglichkeit zu erkennen, ob es geklappt hat, außer es woanders auszuprobieren.
Deshalb gehört zu jedem Kopierknopf eine kurze Bestätigung, und zwar direkt daneben. Zwei Sekunden „Kopiert” reichen.
Und dazu gehört ein catch. Wird der Zugriff abgelehnt, muss dort etwas Brauchbares stehen: ein Hinweis, den Text von Hand zu markieren. Ein Knopf, der stumm nichts tut, ist schlimmer als gar keiner.
Lesen ist noch strenger
navigator.clipboard.readText() gibt es auch, und dafür verlangt der Browser zusätzlich eine ausdrückliche Erlaubnis. In den meisten Fällen erscheint eine Nachfrage, in manchen Browsern geht es gar nicht.
Rechne also nie damit, dass Lesen funktioniert. Ein Einfügefeld, in das der Nutzer selbst einfügt, ist der bessere Weg, und der funktioniert überall.
Teilen auf Mobilgeräten
<p><button id="teilen" type="button">Teilen</button></p>
<p id="rueckmeldung"></p> body {
font-family: system-ui, sans-serif;
max-width: 40rem;
margin: 0;
padding: 1rem;
line-height: 1.6;
color: #1c1917;
}
p {
margin: 0 0 0.75rem;
}
button,
input,
select,
textarea {
font: inherit;
}
button {
padding: 0.4rem 0.8rem;
border: 1px solid #d6d3d1;
border-radius: 6px;
background: #fafaf9;
cursor: pointer;
} const knopf = document.getElementById("teilen");
const rueckmeldung = document.getElementById("rueckmeldung");
const daten = {
title: "Programmieren mit Mario",
text: "Der interaktive JavaScript-Kurs",
url: "https://mitmario.dev/lernen/javascript",
};
console.log("Gibt es navigator.share?", typeof navigator.share);
console.log("Gibt es navigator.clipboard?", typeof navigator.clipboard);
knopf.addEventListener("click", async () => {
// Der eingebaute Teilen-Dialog gibt es fast nur auf
// Mobilgeräten. Deshalb immer erst fragen.
if (navigator.share && navigator.canShare && navigator.canShare(daten)) {
try {
await navigator.share(daten);
rueckmeldung.textContent = "Geteilt.";
} catch (fehler) {
// Ein Abbruch durch den Nutzer ist kein Fehler.
if (fehler.name !== "AbortError") {
rueckmeldung.textContent = "Teilen hat nicht geklappt.";
}
}
return;
}
// Rückfallweg: die Adresse in die Zwischenablage. Mit
// try/catch, denn auch writeText kann abgelehnt werden, und
// dann muss der dritte Weg noch greifen.
if (navigator.clipboard) {
try {
await navigator.clipboard.writeText(daten.url);
rueckmeldung.textContent = "Adresse kopiert.";
return;
} catch (fehler) {
console.warn("Kopieren abgelehnt:", fehler.name);
}
}
// Und wenn auch das nicht geht, zeig sie wenigstens an.
rueckmeldung.textContent = daten.url;
}); navigator.share(daten) öffnet den eingebauten Teilen-Dialog des Betriebssystems, mit allen installierten Apps darin. Auf einem Telefon ist das genau das, was der Nutzer erwartet.
Auf dem Schreibtischrechner gibt es die Funktion oft nicht. Deshalb steht davor immer eine Prüfung, und dahinter ein Rückfallweg: die Adresse in die Zwischenablage, und wenn auch das scheitert, sichtbar auf der Seite.
Im zweiten Beispiel läufst du die ganze Kette ab, und zwar ohne Telefon. typeof navigator.share antwortet in der Eingabezeile mit undefined, der erste Weg fällt also weg. Der zweite wird abgelehnt, das steht gleich als Kopieren abgelehnt: NotAllowedError in der Console. Und weil es einen dritten gibt, steht am Ende die Adresse als Text auf der Seite, statt dass der Knopf stumm bleibt. Ohne das try/catch um den zweiten Weg wäre hier Schluss: leere Seite, roter Eintrag in der Console, und der Nutzer weiß nicht, ob er zu langsam geklickt hat.
navigator.canShare(daten) beantwortet zusätzlich die Frage, ob genau diese Daten geteilt werden können. Das ist wichtig, sobald Dateien dabei sind.
Eine Eigenheit, die man kennen muss: Bricht der Nutzer den Dialog ab, wird das Promise abgelehnt, mit einem Fehler namens AbortError. Das ist kein Fehler, sondern eine Entscheidung, und dafür gehört keine Fehlermeldung auf die Seite.
Der alte Weg
<p><input id="feld" value="https://mitmario.dev/lernen"></p>
<p><button id="alt" type="button">Auf dem alten Weg kopieren</button></p>
<p id="rueckmeldung"></p> body {
font-family: system-ui, sans-serif;
max-width: 40rem;
margin: 0;
padding: 1rem;
line-height: 1.6;
color: #1c1917;
}
p {
margin: 0 0 0.75rem;
}
button,
input,
select,
textarea {
font: inherit;
}
button {
padding: 0.4rem 0.8rem;
border: 1px solid #d6d3d1;
border-radius: 6px;
background: #fafaf9;
cursor: pointer;
}
input,
select,
textarea {
padding: 0.35rem 0.5rem;
border: 1px solid #d6d3d1;
border-radius: 6px;
} const feld = document.getElementById("feld");
const rueckmeldung = document.getElementById("rueckmeldung");
document.getElementById("alt").addEventListener("click", () => {
// Der Weg vor der Clipboard-API: Text markieren und
// den Browser den Kopierbefehl ausführen lassen.
feld.select();
feld.setSelectionRange(0, feld.value.length);
// execCommand ist offiziell veraltet. Es arbeitet
// synchron, gibt nur true oder false zurück und kann
// nur das, was gerade markiert ist.
const geklappt = document.execCommand("copy");
rueckmeldung.textContent = geklappt ? "Kopiert, auf die alte Art." : "Auch das ging nicht.";
console.log("execCommand meldet:", geklappt);
}); Vor der Clipboard-API gab es document.execCommand("copy"). Der Ablauf war: einen Text in ein Eingabefeld schreiben, markieren, den Befehl ausführen, das Feld wieder verstecken.
Es ist offiziell veraltet und trotzdem noch überall zu finden. Es arbeitet synchron, gibt nur true oder false zurück und kann ausschließlich das kopieren, was gerade markiert ist.
Als Rückfallweg für sehr alte Umgebungen taugt es noch. Für alles Neue nimmst du die Clipboard-API.
Und jetzt kommt die Stelle, an der du mir nicht glauben wirst. Drück die Knöpfe im ersten und im dritten Beispiel nacheinander. Der erste, mit der modernen API, meldet „Kopieren hat nicht geklappt”. Der dritte, mit dem veralteten execCommand, meldet „Kopiert, auf die alte Art”, und er hat wirklich kopiert: Füg es irgendwo ein, der Text ist da.
Das ist kein Argument für den alten Weg, sondern eine Eigenheit dieser Umgebung. Der alte Weg hat nie um Erlaubnis gefragt, er hat einfach kopiert, was gerade markiert war, und genau deshalb ist er abgeschafft worden. Die moderne API fragt, und hier bekommt sie ein Nein. Auf deiner eigenen Seite ist es umgekehrt: Dort arbeitet die moderne API, und execCommand ist das, was es laut Standard ist, nämlich Altlast.
Merk dir daran zwei Sachen. Erstens: „funktioniert hier” und „ist der richtige Weg” sind zwei verschiedene Fragen. Zweitens: Wenn eine API in einer Umgebung blockiert ist, liegt das fast nie an deinem Code, und du findest es auch nicht durch Umbauen heraus. Du findest es heraus, indem du die Fehlermeldung liest.
Warum diese Lektion keine Aufgabe hat
Die Seite läuft hier in einem abgeschotteten Bereich ohne eigene Herkunft. Vier Zeilen in der Eingabezeile der Console zeigen dir genau das, und in dieser Reihenfolge werden sie zu einer Diagnose:
typeof navigator.clipboard antwortet object. Die API ist also da, es fehlt nicht an ihr.
window.isSecureContext antwortet true. Am sicheren Kontext liegt es auch nicht.
location.origin antwortet null. Das ist die Antwort, auf die es ankommt. Jede andere Seite im Netz hat hier etwas wie https://mitmario.dev, und daran hängen die meisten Erlaubnisse im Browser. Dieser Bereich hat keine Herkunft, also hat er auch nichts, worauf eine Erlaubnis lauten könnte.
navigator.permissions.query({ name: "clipboard-write" }).then((e) => console.log(e.state)) antwortet denied. Nicht prompt, also nicht „der Nutzer wird gefragt”, sondern von vornherein abgelehnt.
Im ersten Beispiel gibt das catch nur fehler.name aus, also Kopieren abgelehnt: NotAllowedError. Schreib dort einmal fehler.message dazu, und du liest den Grund im Klartext: Failed to execute 'writeText' on 'Clipboard': The Clipboard API has been blocked because of a permissions policy applied to the current document. Eine Richtlinie, kein Bedienfehler. Genau dafür ist die zweite Hälfte einer Fehlermeldung da, und genau sie wird am häufigsten weggelassen.
Dazu kommt: Selbst wenn es ginge, zählt ein Klick, den eine Prüfung auslöst, nicht als Nutzergeste. Eine Aufgabe wäre also nicht abnehmbar.
Man könnte eine Attrappe bauen, die so tut, als würde sie kopieren. Genau davon lernst du aber nichts: Der ganze Inhalt dieser Lektion sind die Bedingungen, unter denen es funktioniert, und eine Attrappe hat keine davon.
Probier es stattdessen auf deiner eigenen Seite aus, und zwar über den kleinen lokalen Server aus Lektion 20.5, nicht per Doppelklick. Erst dann hat deine Seite eine Herkunft, auf die eine Erlaubnis lauten kann (http://localhost:…), und der Kopierknopf aus dem ersten Beispiel funktioniert. Ein Doppelklick auf die Datei gibt dir zwar einen sicheren Kontext, aber keine Herkunft, und damit ist die Lage dieselbe wie hier.
Zum Mitnehmen
Kopieren geht nur aus einem echten Klick heraus. Ein Skript, das beim Laden in die Zwischenablage schreibt, wird vom Browser gestoppt, und das ist gut so.
Jetzt du
Basis Konto, kostenlosIm Editor änderst du die Beispiele dieser Lektion und lässt sie gleich laufen. So merkst du am schnellsten, ob es sitzt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
Was in dieser Lektion steckt
-
Artikel mit 3 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.