clipboard-polyfill
Version:
A polyfill for the asynchronous clipboard API
245 lines (204 loc) • 8.63 kB
text/typescript
//"use strict";
import {Promise} from "es6-promise";
import {DataTypes} from "./DataTypes"
import DT from "./DT";
interface IEWindowClipbardData {
setData: (key: string, value: string) => boolean;
getData: (key: string) => string | undefined;
}
interface IEWindow extends Window {
clipboardData: IEWindowClipbardData
}
export default class ClipboardPolyfill {
private static DEBUG: boolean = false;
private static misingPlainTextWarning = true;
public static DT = DT;
// TODO: Compile debug logging code out of release builds?
public static enableDebugLogging() {
this.DEBUG = true;
}
private static suppressMissingPlainTextWarning() {
this.misingPlainTextWarning = false;
}
protected static copyListener(tracker: FallbackTracker, data: DT, e: ClipboardEvent): void {
if (this.DEBUG) (console.info || console.log).call(console, "listener called");
tracker.listenerCalled = true;
data.forEach((value: string, key: string) => {
e.clipboardData.setData(key, value);
if (key === DataTypes.TEXT_PLAIN && e.clipboardData.getData(key) != value) {
if (this.DEBUG) (console.info || console.log).call(console, "Setting text/plain failed.");
tracker.listenerSetPlainTextFailed = true;
}
});
e.preventDefault();
}
protected static execCopy(data: DT): FallbackTracker {
var tracker = new FallbackTracker();
var listener = this.copyListener.bind(this, tracker, data);
document.addEventListener("copy", listener);
try {
tracker.execCommandReturnedTrue = document.execCommand("copy");
} finally {
document.removeEventListener("copy", listener);
}
return tracker;
}
// Create a temporary DOM element to select, so that `execCommand()` is not
// rejected.
private static copyUsingTempSelection(e: HTMLElement, data: DT): FallbackTracker {
Selection.select(e);
var tracker = this.execCopy(data);
Selection.clear();
return tracker;
}
// Create a temporary DOM element to select, so that `execCommand()` is not
// rejected.
private static copyUsingTempElem(data: DT): FallbackTracker {
var tempElem = document.createElement("div");
// Place some text in the elem so that Safari has something to select.
tempElem.textContent = "temporary element";
document.body.appendChild(tempElem);
var tracker = this.copyUsingTempSelection(tempElem, data);
document.body.removeChild(tempElem);
return tracker;
}
// Uses shadow DOM.
private static copyTextUsingDOM(str: string): boolean {
if (this.DEBUG) (console.info || console.log).call(console, "attempting to copy text using DOM");
var tempElem = document.createElement("div");
var shadowRoot = tempElem.attachShadow({mode: "open"});
document.body.appendChild(tempElem);
var span = document.createElement("span");
span.textContent = str;
span.style.whiteSpace = "pre-wrap"; // TODO: Use `innerText` above instead?
shadowRoot.appendChild(span);
Selection.select(span);
var result = document.execCommand("copy");
// Selection.clear();
document.body.removeChild(tempElem);
return result;
}
public static writeIE(data: DT): boolean {
// IE supports text or URL, but not HTML: https://msdn.microsoft.com/en-us/library/ms536744(v=vs.85).aspx
// TODO: Write URLs to `text/uri-list`? https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types
var text = data.getData("text/plain");
if (text !== undefined) {
return (window as IEWindow).clipboardData.setData("Text", text);
}
throw ("No `text/plain` value was specified.");
}
public static write(data: DT): Promise<void> {
if (this.misingPlainTextWarning && !data.getData(DataTypes.TEXT_PLAIN)) {
(console.warn || console.log).call(console,
"[clipboard.js] clipboard.write() was called without a "+
"`text/plain` data type. On some platforms, this may result in an "+
"empty clipboard. Call clipboard.suppressMissingPlainTextWarning() "+
"to suppress this warning.");
}
// TODO: Allow fallback graph other than a single line.
return new Promise<void>((resolve, reject) => {
// Internet Explorer
if (typeof ClipboardEvent === "undefined" &&
typeof (window as IEWindow).clipboardData !== "undefined" &&
typeof (window as IEWindow).clipboardData.setData !== "undefined") {
if (this.writeIE(data)) {
resolve()
} else {
reject(new Error("Copying failed, possibly because the user rejected it."));
}
return;
}
var tracker = this.execCopy(data);
if (tracker.listenerCalled && !tracker.listenerSetPlainTextFailed) {
if (this.DEBUG) (console.info || console.log).call(console, "Regular copy command succeeded.");
resolve();
return;
}
// Success detection on Edge is not possible, due to bugs in all 4
// detection mechanisms we could try to use. Assume success.
if (tracker.listenerCalled && navigator.userAgent.indexOf("Edge") > -1) {
if (this.DEBUG) (console.info || console.log).call(console, "User agent contains \"Edge\". Blindly assuming success.");
resolve();
return;
}
// Fallback 1 for desktop Safari.
tracker = this.copyUsingTempSelection(document.body, data);
if (tracker.listenerCalled && !tracker.listenerSetPlainTextFailed) {
if (this.DEBUG) (console.info || console.log).call(console, "Copied using temporary document.body selection.");
resolve();
return;
}
// Fallback 2 for desktop Safari.
tracker = this.copyUsingTempElem(data);
if (tracker.listenerCalled && !tracker.listenerSetPlainTextFailed) {
if (this.DEBUG) (console.info || console.log).call(console, "Copied using selection of temporary element added to DOM.");
resolve();
return;
}
// Fallback for iOS Safari.
var text = data.getData(DataTypes.TEXT_PLAIN);
if (text !== undefined) {
if (this.DEBUG) (console.info || console.log).call(console, "Copied text using DOM.");
resolve();
return;
}
reject(new Error("Copy command failed."));
});
}
static writeText(s: string): Promise<void> {
var dt = new DT();
dt.setData(DataTypes.TEXT_PLAIN, s);
return this.write(dt);
}
static read(): Promise<DT> {
return new Promise((resolve, reject) => reject("Cannot read in any modern browsers. IE11 pasting is not implemented yet."));
}
static readText(): Promise<string> {
return new Promise((resolve, reject) => reject("Cannot read in any modern browsers. IE11 pasting is not implemented yet."));
}
// Legacy v1 API.
static copy(obj: string|{[key:string]:string}|HTMLElement): Promise<void> {
(console.warn || console.log).call(console, "[clipboard.js] The clipboard.copy() API is deprecated and may be removed in a future version. Please switch to clipboard.write() or clipboard.writeText().");
return new Promise((resolve, reject) => {
var data: DT;
if (typeof obj === "string") {
data = DT.fromText(obj);
} else if (obj instanceof HTMLElement) {
data = DT.fromElement(obj);
} else if (obj instanceof Object) {
data = DT.fromObject(obj);
} else {
reject("Invalid data type. Must be string, DOM node, or an object mapping MIME types to strings.");
return;
}
this.write(data);
});
}
// Legacy v1 API.
static paste(): Promise<string> {
(console.warn || console.log).call(console, "[clipboard.js] The clipboard.paste() API is deprecated and may be removed in a future version. Please switch to clipboard.read() or clipboard.readText().");
return new Promise((resolve, reject) => reject("Cannot read in any modern browsers. IE11 pasting is not implemented yet."));
}
}
class Selection {
static select(elem: Element): void {
var sel = document.getSelection();
var range = document.createRange();
range.selectNodeContents(elem);
sel.removeAllRanges();
sel.addRange(range);
}
static clear(): void {
var sel = document.getSelection();
sel.removeAllRanges();
}
}
class FallbackTracker {
public execCommandReturnedTrue: boolean = false;
public listenerCalled: boolean = false;
public listenerSetPlainTextFailed: boolean = false;
}
// TODO: Figure out how to expose ClipboardPolyfill as self.clipboard using
// WebPack?
declare var module: any;
module.exports = ClipboardPolyfill;