Prefer JavaScript clipboard API if available

Modern versions of Chrome support a draft clipboard API from
JavaScript that allows copying without use of a Flash movie.
If the API appears to be available in the browser prefer it
over the Flash movie.

Leave the clippy.swf support in as ancient browsers are still
going to be around for many years and will continue to require
the Flash movie to put text on the clipboard.

Change-Id: I1984801c8fa9c398ea4edf762f700e4a50e1da61
This commit is contained in:
Shawn Pearce 2015-07-20 16:23:02 -07:00 committed by David Pursehouse
parent 2b1373f54d
commit 4894685e88
10 changed files with 316 additions and 10 deletions

View File

@ -7,6 +7,7 @@ gwt_module(
resources = [
SRC + 'clippy/client/clippy.css',
SRC + 'clippy/client/clippy.swf',
SRC + 'clippy/client/CopyableLabelText.properties',
],
provided_deps = ['//lib/gwt:user'],
deps = [
@ -84,6 +85,7 @@ gwt_module(
name = 'UserAgent',
srcs = glob([SRC + 'user/client/*.java']),
gwt_xml = SRC + 'user/User.gwt.xml',
resources = [SRC + 'user/client/tooltip.css'],
provided_deps = ['//lib/gwt:user'],
visibility = ['PUBLIC'],
)

View File

@ -18,5 +18,6 @@ import com.google.gwt.resources.client.CssResource;
public interface ClippyCss extends CssResource {
String label();
String control();
String copier();
String swf();
}

View File

@ -16,6 +16,7 @@ package com.google.gwtexpui.clippy.client;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ClickEvent;
@ -24,9 +25,12 @@ import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.http.client.URL;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HasText;
@ -35,6 +39,7 @@ import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwtexpui.safehtml.client.SafeHtml;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import com.google.gwtexpui.user.client.Tooltip;
import com.google.gwtexpui.user.client.UserAgent;
/**
@ -71,6 +76,7 @@ public class CopyableLabel extends Composite implements HasText {
private int visibleLen;
private Label textLabel;
private TextBox textBox;
private Button copier;
private Element swf;
public CopyableLabel() {
@ -111,7 +117,32 @@ public class CopyableLabel extends Composite implements HasText {
});
content.add(textLabel);
}
embedMovie();
if (UserAgent.hasJavaScriptClipboard()) {
copier = new Button("📋"); // CLIPBOARD
copier.setStyleName(ClippyResources.I.css().copier());
Tooltip.addStyle(copier);
Tooltip.setLabel(copier, CopyableLabelText.I.tooltip());
copier.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
copy();
}
});
copier.addMouseOutHandler(new MouseOutHandler() {
@Override
public void onMouseOut(MouseOutEvent event) {
Tooltip.setLabel(copier, CopyableLabelText.I.tooltip());
}
});
FlowPanel p = new FlowPanel();
p.getElement().getStyle().setDisplay(Display.INLINE_BLOCK);
p.add(copier);
content.add(p);
} else {
embedMovie();
}
}
/**
@ -127,12 +158,13 @@ public class CopyableLabel extends Composite implements HasText {
}
private void embedMovie() {
if (flashEnabled && !text.isEmpty() && UserAgent.Flash.isInstalled()) {
if (copier == null && flashEnabled && !text.isEmpty()
&& UserAgent.Flash.isInstalled()) {
final String flashVars = "text=" + URL.encodeQueryString(getText());
final SafeHtmlBuilder h = new SafeHtmlBuilder();
h.openElement("div");
h.setStyleName(ClippyResources.I.css().control());
h.setStyleName(ClippyResources.I.css().swf());
h.openElement("object");
h.setWidth(SWF_WIDTH);
@ -236,4 +268,35 @@ public class CopyableLabel extends Composite implements HasText {
}
textLabel.setVisible(true);
}
private void copy() {
TextBox t = new TextBox();
try {
t.setText(getText());
content.add(t);
t.selectAll();
boolean ok = execCommand("copy");
Tooltip.setLabel(copier, ok
? CopyableLabelText.I.copied()
: CopyableLabelText.I.failed());
if (!ok) {
// Disable JavaScript clipboard and try flash movie in another instance.
UserAgent.disableJavaScriptClipboard();
}
} finally {
t.removeFromParent();
}
}
private static boolean execCommand(String command) {
try {
return nativeExec(command);
} catch (Exception e) {
return false;
}
}
private static native boolean nativeExec(String c)
/*-{ return !! $doc.execCommand(c) }-*/;
}

View File

@ -0,0 +1,26 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gwtexpui.clippy.client;
import com.google.gwt.core.client.GWT;
import com.google.gwt.i18n.client.Constants;
interface CopyableLabelText extends Constants {
static final CopyableLabelText I = GWT.create(CopyableLabelText.class);
String tooltip();
String copied();
String failed();
}

View File

@ -0,0 +1,3 @@
tooltip = Copy to clipboard
copied = Copied
failed = Failed !

View File

@ -16,10 +16,24 @@
.label {
vertical-align: top;
}
.control {
.swf, .copier {
margin-left: 5px;
display: inline-block !important;
height: 14px;
width: 14px;
}
.swf {
display: inline-block !important;
overflow: hidden;
}
.copier {
display: inline-block;
font-size: 12px;
vertical-align: top;
padding: 0;
border: 0;
background-color: inherit;
cursor: pointer;
}
.copier:focus {
outline: none;
}

View File

@ -0,0 +1,80 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gwtexpui.user.client;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.user.client.ui.UIObject;
/** Displays custom tooltip message below an element. */
public class Tooltip {
interface Resources extends ClientBundle {
static final Resources I = GWT.create(Resources.class);
@Source("tooltip.css")
Css css();
}
interface Css extends CssResource {
String tooltip();
}
static {
Resources.I.css().ensureInjected();
}
/**
* Add required supporting style to enable custom tooltip rendering.
*
* @param o widget whose element should display a tooltip on hover.
*/
public static void addStyle(UIObject o) {
addStyle(o.getElement());
}
/**
* Add required supporting style to enable custom tooltip rendering.
*
* @param e element that should display a tooltip on hover.
*/
public static void addStyle(Element e) {
e.addClassName(Resources.I.css().tooltip());
}
/**
* Set the text displayed on hover.
*
* @param o widget whose hover text is being set.
* @param text message to display on hover.
*/
public static void setLabel(UIObject o, String text) {
setLabel(o.getElement(), text);
}
/**
* Set the text displayed on hover.
*
* @param e element whose hover text is being set.
* @param text message to display on hover.
*/
public static void setLabel(Element e, String text) {
e.setAttribute("aria-label", text != null ? text : "");
}
private Tooltip() {
}
}

View File

@ -27,6 +27,65 @@ import com.google.gwt.user.client.Window;
* trivial compared to the time developers lose building their application.
*/
public class UserAgent {
private static boolean jsClip = guessJavaScriptClipboard();
public static boolean hasJavaScriptClipboard() {
return jsClip;
}
public static void disableJavaScriptClipboard() {
jsClip = false;
}
private static native boolean nativeHasCopy()
/*-{ return $doc['queryCommandSupported'] && $doc.queryCommandSupported('copy') }-*/;
private static boolean guessJavaScriptClipboard() {
String ua = Window.Navigator.getUserAgent();
int chrome = major(ua, "Chrome/");
if (chrome > 0) {
return 42 <= chrome;
}
int ff = major(ua, "Firefox/");
if (ff > 0) {
return 41 <= ff;
}
int opera = major(ua, "OPR/");
if (opera > 0) {
return 29 <= opera;
}
int msie = major(ua, "MSIE ");
if (msie > 0) {
return 9 <= msie;
}
if (nativeHasCopy()) {
// Firefox 39.0 lies and says it supports copy, then fails.
// So we try this after the browser specific test above.
return true;
}
// Safari is not planning to support document.execCommand('copy').
// Assume the browser does not have the feature.
return false;
}
private static int major(String ua, String product) {
int entry = ua.indexOf(product);
if (entry >= 0) {
String s = ua.substring(entry + product.length());
String p = s.split("[ /;,.)]", 2)[0];
try {
return Integer.parseInt(p);
} catch (NumberFormatException nan) {
}
}
return -1;
}
public static class Flash {
private static boolean checked;
private static boolean installed;

View File

@ -0,0 +1,54 @@
/* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tooltip {
position: relative;
}
.tooltip:hover:before {
position: absolute;
z-index: 51;
border: solid;
border-color: #333 transparent;
border-width: 0 4px 4px 4px;
pointer-events: none;
content: "";
top: auto;
right: 50%;
bottom: -5px;
margin-right: -5px;
}
.tooltip:hover:after {
position: absolute;
z-index: 50;
font: normal normal 11px/1.5 Helvetica, arial, sans-serif;
text-align: center;
white-space: pre;
pointer-events: none;
background: rgba(0,0,0,.7);
color: #fff;
border-radius: 3px;
padding: 5px;
content: attr(aria-label);
top: 100%;
right: 50%;
margin-top: 5px;
-webkit-transform: translateX(50%);
-ms-transform: translateX(50%);
transform: translateX(50%)
}

View File

@ -37,6 +37,7 @@ import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwtexpui.user.client.UserAgent;
import java.util.ArrayList;
import java.util.Arrays;
@ -135,16 +136,19 @@ public class MyPreferencesScreen extends SettingsScreen {
legacycidInChangeTable = new CheckBox(Util.C.showLegacycidInChangeTable());
muteCommonPathPrefixes = new CheckBox(Util.C.muteCommonPathPrefixes());
final Grid formGrid = new Grid(11, 2);
boolean flashClippy = !UserAgent.hasJavaScriptClipboard() && UserAgent.Flash.isInstalled();
final Grid formGrid = new Grid(10 + (flashClippy ? 1 : 0), 2);
int row = 0;
formGrid.setText(row, labelIdx, "");
formGrid.setWidget(row, fieldIdx, showSiteHeader);
row++;
formGrid.setText(row, labelIdx, "");
formGrid.setWidget(row, fieldIdx, useFlashClipboard);
row++;
if (flashClippy) {
formGrid.setText(row, labelIdx, "");
formGrid.setWidget(row, fieldIdx, useFlashClipboard);
row++;
}
formGrid.setText(row, labelIdx, "");
formGrid.setWidget(row, fieldIdx, copySelfOnEmails);