Copier des données dans le clipboard en JavaScript

Comment copier des données quelconques dans le clipboard en JavaScript dans le contexte d'une page Web ? Ce problème est assez simple à régler. Il suffit d'ajouter un gestionnaire de l'événement copy à l'objet document.
Le problème se complique s'il est exigé que ce gestionnaire d'événement soit anonyme et par ailleurs qu'il soit strictement éphémère, c'est-à-dire qu'il se retire de l'objet document une fois que l'événement copy est survenu, comme ce serait le cas si l'utilisateur cliquait sur un bouton "Copier" à usage ponctuel. En effet, en mode strict, il n'est plus possible d'utiliser arguments.callee pour faire référence à une fonction anonyme dans le corps de cette dernière. Par conséquent, impossible de faire référence à la fonction dans un appel à removeEventListener ().
Mise à jour du 26/12/2018 : Il est désormais possible d'utiliser l'option "once" lors de l'appel à addEventListener () (ex: addEventListener ("click", function (e) {}, { once: true })) pour ajouter un gestionnaire d'événement éphémère. Plus simple !

La solution

Après quelques rebondissements liés à des enjeux de sécurité, il semble que le moyen de copier des données dans le clipboard depuis un programme JavaScript soit enfin stabilisé. Cela passe par une commande copy, à exécuter via document.execCommand ()
Quant au moyen de permettre à une fonction anonyme de se retirer d'un objet auquel elle est associée en tant que gestionnaire d'événement, il faut recourir à une subtilité de la gestion des événements qui consiste à associer non pas une fonction anonyme, mais un objet anonyme doté d'une fonction handleEvent () à l'objet dont il s'agit de gérer un événement.

Le code JavaScript

En JavaScript, la solution se traduit par le code suivant :
function onClick (e) {
	document.designMode = "on";
	document.execCommand ("copy");
	document.designMode = "off";
}
document.getElementById ("tagButton").addEventListener ("click", onClick);
document.addEventListener (
	"copy",
	{
		handleEvent:function (e) {
			e.preventDefault ();
			// Utiliser l'objet window et non e pour accéder à clipboardData dans IE
			e.clipboardData.setData ("text/plain", "Hello, world!");
			document.removeEventListener ("copy", this, false);
			alert ("Donnés copiées et gestionnaire retiré !");
		},
		false
	}
);
tagButton serait par exemple un bouton dans la page Web.

L'exemple

Cliquez ici pour accéder à une page de test minimaliste. Vous pourrez visualiser le code et le récupérer pour travailler avec.

La logique

La possibilité d'accéder au clipboard est de ces romans-feuilletons que les éditeurs de navigateurs Web savent nous offrir, ponctué de rebondissements au gré notamment de la découverte de failles de sécurité qu'il faut combler en urgence. Partant, il est assez difficile de se retrouver dans la pléthore d'informations qu'une recherche sur le Web fait remonter, c'est-à-dire de trouver la dernière information en date.
D'après le commentaires de ses développeurs, il semble que la situation se soit stabilisée à partir de Firefox 22. De nos jours, il existe une API Clipboard dont l'excellent site Can I Use ? rapporte le degré d'adoption par les différents navigateurs. Cette API est notamment implémentée par Firefox, comme en atteste sa documentation.
On trouve une bonne exposition du code permettant de copier des données dans le clipboard sur le site Stack Overflow.
Le procédé est un peu particulier, car c'est le gestionnaire de l'événement généré par une copie dans la clipboard qui modifie les données qui y sont copiées :
document.addEventListener (
	"copy",
	function (e) {
		e.clipboardData.setData ("text/plain", "Hello, world!");	// Dans IE, utiliser window, et non e : window.clipboardData.setData ("Text", "Hello, world!")
		e.preventDefault ();
	},
	false
);
L'appel à preventDefault () est indispensable. Pour rappel, preventDefault () permet d'éviter que la gestion par défaut de l'événement ne survienne. En l'occurence, cela permet d'éviter l'action du gestionnaire d'événement soit tout simplement ignorée.
La question devient :
  • Comment générer un événement "copy" ?
  • Comment retirer le gestionnaire d'événement "copy" une fois qu'il a été utilisé ?
Comment générer un événement "copy" ? Il est impossible d'y parvenir par dispatchEvent (). L'instruction suivante ne fonctionne pas :
e.target.dispatchEvent (new ClipboardEvent ("copy", {dataType:"text/plain", data:"Hello, world!"}));
A la place, il faut utiliser execCommand (). Toutefois, comme détaillé dans la note accompagnant la version 22 de Firefox pointée plus tôt : "With the 'cut' or 'copy' command as argument, Document.execCommand() now works, but only within the context of user-initiated or privileged code." La documentation de la fonction préciser que pour utiliser execCommand (), le document doit être en mode Design. Autrement dit :
document.designMode = "on";
document.execCommand ("copy");
document.designMode = "off";
Comment retirer le gestionnaire de l'événement ? La difficulté est qu'un gestionnaire d'événement ne peut être retiré que par un appel à removeEventListener () qui fait référence à lui-même :
function handleClick (e) {
	alert ("Hello, world!");
}
tag.addEventListener ("click", handleClick), false);
tag.removeEventListener ("click", handleClick), false);
Cela pose un problème dans le cas (très général) où le gestionnaire est une fonction anonyme :
someTag.addEventListener ("click", function (e) { alert ("Hello, world!"); }, false);
Ne peut-on pas récupérer une référence sur le gestionnaire dans le gestionnaire, via this ? Non, pour deux raisons. Premièrement, une fonction appartient toujours à un objet (par défaut, l'objet window), si bien que dans le contexte d'une fonction, this référence cet objet et non l'objet Function correspondant à la fonction. Secondement, un gestionnaire d'événement est lié (bind) à l'élément HTML générateur de l'événement. Pour reprendre l'exemple précédent, si someTag est un DIV :
function handleClick (e) {
	alert (this.tagName); // Affiche "DIV"
}
La solution consiste à utiliser la propriété callee de la variable locale arguments de la fonction :
someTag.addEventListener (
	"click",
	function (e) {
		alert ("Hello, world!");
		someTag.removeEventListener ("click", arguments.callee, false);
	},
	false
);
Toutefois, comme évoqué dans sa documentation, arguments.callee ne figure pas dans la spécification du mode strict dans ECMAScript 2015.
Dans ces conditions, la solution consiste à exploiter un autre moyen d'associer un gestionnaire à un événément, évoqué dans la documentation de addEventListener (). Il s'agit d'associer un objet comportant une fonction réservée handleEvent () :
function SomeObject (tag) {
	tag.addEventListener ("click", this);
	this.handleEvent = function (e) {
		alert (this);	// Affiche "[object Object]"
	}
}
var o = new SomeObject (someTag);
Partant, il est possible d'enrichir le code de handleEvent () pour retirer l'objet, car ce dernier peut être référencé par this :
function SomeObject (tag) {
	this.tag = tag;
	this.tag.addEventListener ("click", this);
	this.handleEvent = function (e) {
		alert (this);	// Affiche "[object Object]"
		this.tag.removeEventListener ("click", this);
	}
}
var o = new SomeObject (someTag);
Il ne reste plus qu'à transformer la déclaration de l'objet pour en faire une déclaration anonyme :
tag.addEventListener (
	"click",
	{
		tag:tag,
		handleEvent:function (e) {
			alert (this);	// Affiche "[object Object]"
			this.tag.removeEventListener ("click", this, false);
		}
	}
);
Les deux techniques qui viennent d'être présentées peuvent alors être combinées pour que la gestion de l'événement "copy" soit assurée par un gestionnaire qui se retire lui-même. Par exemple, pour copier lorsqu'un bouton tagButton est cliqué, le programme suivant est exécuté une seule fois au chargement de la page :
function onClick (e) {
	document.designMode = "on";
	document.execCommand ("copy");
	document.designMode = "off";
}
document.getElementById ("tagButton").addEventListener ("click", onClick);
document.addEventListener (
	"copy",
	{
		handleEvent:function (e) {
			e.preventDefault ();
			// Utiliser l'objet window et non e pour accéder à clipboardData dans IE
			e.clipboardData.setData ("text/plain", "Hello, world!");
			document.removeEventListener ("copy", this, false);
			alert ("Donnés copiées et gestionnaire retiré !");
		},
		false
	}
);
Copier des données dans le clipboard en JavaScript