Zwischenablage aus dem Browser mit Vaadin

Keine Kommentare

Da der programmatische Zugriff auf die Zwischenablage des Betriebssystems aus dem Browser heraus nicht standardisiert, bzw. mit reinem JavaScript gar nicht möglich ist, weichen viele Webanwendungen auf den Umweg über ein Flash Movie aus. Dabei wird ein transparentes Flash Movie über einen Bereich gelegt und mit dem Text versehen, der aus dem Browser in die Zwischenablage kopiert werden soll. Klickt der Nutzer auf diesen Bereich, kann Flash den Inhalt in die Zwischenablage kopieren. Eine bekannte Bibliothek für diesen Zweck ist ZeroClipboard [1]. Dabei kommuniziert ein JavaScript Objekt mit dem Flash Movie und kümmert sich um die Erzeugung, die korrekte Positionierung und das Entfernen der DOM Elemente auf der Webseite.
Basiert die Webanwendung vollständig auf einem komponentenbasierten Framework wie Vaadin/gwt, ist auch die Einbindung von ZeroClipboard über diesen Weg wünschenswert. Dieser Eintrag beschäftigt sich mit der Integration von ZeroClipboard in ein Vaadin Widget. Das Widget kann im Vaadin Addon Verzeichnis heruntergeladen werden [2], der Sourcecode ist auf github.com verfügbar [3].
Die Grundkomponente besteht dabei aus dem com.vaadin.terminal.gwt.client.ui.VButton. Dieses Widget bietet bereits alles, was ich von einem Knopf erwarte. Ergänzt werden muss nur noch die korrekte Initialisierung und Kommunikation mit ZeroClipboard sowie die Kommunikation mit der Serverkomponente. Das Widget soll dann folgende Funktionen bieten:

  • Jede Position auf der Webseite in einer Vaadin Anwendung soll möglich sein
  • Button neu erzeugen und entfernen, bei laufender Anwendung
  • Der Knopf soll auch in Popups anwendbar sein (z.B. über das Vaadin addon PopupButton [4])
  • Mehrere Knöpfe gleichzeitig auf einer Seite darstellbar
  • Über einen Listener wird die Anwendung über den Erfolg der Kopieren Aktion informiert

Das serverseitige API bleibt eher schlicht:

  • Instanziierung wie ein gewöhnlicher Button
  • #setClipboardText(String t)
  • #addListener(ClipboardListener l)

Die Clientseite muss sich um die Anbindung an ZeroClipboard kümmern:

  • JSNI Aufrufe für die Instanziierung, Positionierung und Registrierung von Callbacks
  • für die korrekte Positionierung des Overlays:
    • positionieren des Flash Movies bei onMouseOver
    • verstecken des Flash Movies bei onMouseOut
  • entfernen der DOM Element wenn der Button von der Oberfläche entfernt wird

Clientseitig wird der ZeroClipboard Client in der #updateFromUIDL Methode erzeugt, da erst zu diesem Zeitpunkt alle JavaScript Objekte im DOM verfügbar sind. Über ein Flag wird die Instanziierung auf den ersten Aufruf beschränkt:

if (firstLoad) {
    this.buttonId = ID_PREFIX + paintableId; // create a unique Id for the DOM Element
    Element root = getElement();
    DOM.setElementAttribute(root, "id", buttonId); // add the id. this allows multiple clipboard buttons on the page
    glueCopy(GWT.getModuleBaseURL(), root, buttonId); // glue the js and flash to the component
 
    firstLoad = false;
}
 
// set the clipboard text to the flash movie
setClipboardText(clipboardText, buttonId);

Der JSNI [5] Aufruf #glueCopy verbindet das DOM Element des Buttons mit einem neuen DOM Element, dem ZeroClipboard Client. Das Client Element wird dabei direkt unter dem Body des html eingehängt. Das Einhängen unter den von Vaadin generierten DOM Elementen des Buttons hat zwar Vorteile bei der Objektverwaltung und dem Eventhandling, führt aber besonders in Verbindung mit dem PopupButton zur falschen Positionierung des Knopfes auf der Oberfläche. Vaadin kommt dann mit der Größenberechnung des Buttons durcheinander.

    private native void glueCopy(String baseUrl, Element buttonElement, String buttonId)
    /*-{
        var path = baseUrl + 'ZeroClipboard10.swf';
        $wnd.ZeroClipboard.setMoviePath(path);
 
        var clip = $wnd.ZeroClipboard.newClient(); // new instance
        clip.zIndex = 20001; // float just over a vaadin popup
        clip.glue(buttonElement); // magic
        clip.hide(); // initially hide the flash movie
 
        var widget = this;
 
        // notify me when the copy action is done:
        var completeListener = function copyComplete(client, text) { 
                widget.@de.codecentric.vaadin.gwt.client.VCopy2ClipboardButton::onComplete()(); 
        }
        clip.addEventListener('complete', completeListener);
 
        // notify me when the mouse leaves the flash movie:
        var mouseoutListener = function mouseoutL() { 
                widget.@de.codecentric.vaadin.gwt.client.VCopy2ClipboardButton::onMouseout()(); 
        }
        clip.addEventListener('mouseout', mouseoutListener);
 
        // store the object on the button
        $doc.getElementById(buttonId).clip = clip;
     }-*/;

Leider verhält sich Firefox bei den JSNI-Aufrufen anders als Chrome oder der IE.
Anstatt

var clip = new $wnd.ZeroClipboard.Client();

muss im ZeroClipboard.js noch die Methode newClient() hinzugefügt werden, die einfach eine neue Instanz des Clients liefert.
Auch die Listener müssen vor der Registrierung in Variablen erfasst werden. Diese rufen jeweils einen Callback des Widgets auf:
Der Listener für das Event „complete“ wird aufgerufen, wenn die Kopieraktion in die Zwischenablage erfolgreich war. Der Listener für „mouseout“ informiert das Widget wenn die Maus das Flash Movie wieder verlässt. Dieses wird dann außehalb des Anzeigebereichs verschoben. Das Widget selber reagiert auf das „mouseover“ Event und plaziert das Flash Movie wieder über dem Knopf.

Einbindung

Das Widget wird wie ein normaler Button eingebunden:

// Create a new instance
Copy2ClipboardButton button = new Copy2ClipboardButton("Copy");
// Set the text to copy
button.setClipboardText("copy this to the clipboard");
 
// The listener will trigger on successful copy action
button.addListener(new ClipboardListener() {
 
    @Override
    public void copiedToClipboard(ClipboardEvent event) {
        mainWindow.showNotification("Copied to the clipboard");
    }
});

Copy2ClipboardButton im Popup

Wird der neue Copy2ClipboardButton in einem Popup benutzt ergeben sich zwei Besonderheiten:
Da der Button serverseitig nicht entfernt sondern nur versteckt wird muss in der #onUnload Methode unterschieden werden, ob die ZeroClipboard Objekte behalten oder entfernt werden. Bei der Instanziierung der Serverkomponente muss in diesem Fall das retainClipElement Flag gesetzt werden.
Außerdem wird das Popup beim Klick auf das Flash Movie direkt geschlossen, da aus Vaadin Sicht ein Event außerhalb des Popup triggert. Um das Popup direkt wieder zu öffnen muss serverseitig ein ClipboardListener definiert werden, der dies übernimmt:

Copy2ClipboardButton copyButton = new Copy2ClipboardButton("copy", true); // set retainClipElement to true
final PopupButton popup = new PopupButton("show popup");
popup.addComponent(copyButton);
 
copyButton.addListener(new ClipboardListener() {
 
	@Override
	public void copiedToClipboard(ClipboardEvent event) {
	    popup.setPopupVisible(true);
	}
});

Referenzen
[1] ZeroClipboard bei github
[2] copy2clipboard im Vaadin Addon Verzeichnis
[3] copy2clipboard bei github
[4] Vaadin PopupButton addon
[5] JSNI Basics bei google

Henning Treu

Als Full-Stack-Consultant fühlt sich Henning in der modernen Frontendentwicklung mit JavaScript genauso zu Hause wie bei der Architektur und Umsetzung komplexer Backendsysteme. Dafür setzt er am liebsten moderne Open-Source-Technologien ein. Im Vordergrund stehen dabei immer elegante Lösungen für die Herausforderungen unserer Kunden.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.