UNPKG

8.62 kBPlain TextView Raw
1import {DT, suppressDTWarnings} from "./DT";
2
3// Debug log strings should be short, since they are copmiled into the production build.
4// TODO: Compile debug logging code out of production builds?
5var debugLog: (s: string) => void = function(s: string) {};
6var showWarnings = true;
7// Workaround for:
8// - IE9 (can't bind console functions directly), and
9// - Edge Issue #14495220 (referencing `console` without F12 Developer Tools can cause an exception)
10var warnOrLog = function() {
11 (console.warn || console.log).apply(console, arguments);
12};
13var warn = warnOrLog.bind("[clipboard-polyfill]");
14
15var TEXT_PLAIN = "text/plain";
16
17declare global {
18 interface Navigator {
19 clipboard: {
20 writeText?: (s: string) => Promise<void>;
21 readText?: () => Promise<string>;
22 };
23 }
24}
25
26export default class ClipboardPolyfill {
27 public static readonly DT = DT;
28
29 public static setDebugLog(f: (s: string) => void): void {
30 debugLog = f;
31 }
32
33 public static suppressWarnings() {
34 showWarnings = false;
35 suppressDTWarnings();
36 }
37
38 public static write(data: DT): Promise<void> {
39 if (showWarnings && !data.getData(TEXT_PLAIN)) {
40 warn("clipboard.write() was called without a "+
41 "`text/plain` data type. On some platforms, this may result in an "+
42 "empty clipboard. Call clipboard.suppressWarnings() "+
43 "to suppress this warning.");
44 }
45
46 return (new Promise((resolve, reject) => {
47 // Internet Explorer
48 if (seemToBeInIE()) {
49 if (writeIE(data)) {
50 resolve();
51 } else {
52 reject(new Error("Copying failed, possibly because the user rejected it."));
53 }
54 return;
55 }
56
57 if (execCopy(data)) {
58 debugLog("regular execCopy worked");
59 resolve();
60 return;
61 }
62
63 // Success detection on Edge is not possible, due to bugs in all 4
64 // detection mechanisms we could try to use. Assume success.
65 if (navigator.userAgent.indexOf("Edge") > -1) {
66 debugLog("UA \"Edge\" => assuming success");
67 resolve();
68 return;
69 }
70
71 // Fallback 1 for desktop Safari.
72 if (copyUsingTempSelection(document.body, data)) {
73 debugLog("copyUsingTempSelection worked");
74 resolve();
75 return;
76 }
77
78 // Fallback 2 for desktop Safari.
79 if (copyUsingTempElem(data)) {
80 debugLog("copyUsingTempElem worked");
81 resolve();
82 return;
83 }
84
85 // Fallback for iOS Safari.
86 var text = data.getData(TEXT_PLAIN);
87 if (text !== undefined && copyTextUsingDOM(text)) {
88 debugLog("copyTextUsingDOM worked");
89 resolve();
90 return;
91 }
92
93 reject(new Error("Copy command failed."));
94 })) as Promise<void>;
95 }
96
97 public static writeText(s: string): Promise<void> {
98 if (navigator.clipboard && navigator.clipboard.writeText) {
99 return navigator.clipboard.writeText(s);
100 }
101 var dt = new DT();
102 dt.setData(TEXT_PLAIN, s);
103 return this.write(dt);
104 }
105
106 public static read(): Promise<DT> {
107 return (new Promise((resolve, reject) => {
108 // TODO: Attempt to use navigator.clipboard.read() directly.
109 // Requires DT -> DataTransfer conversion.
110 this.readText().then(
111 (s: string) => resolve(DTFromText(s)),
112 reject
113 );
114 })) as Promise<DT>;
115 }
116
117 public static readText(): Promise<string> {
118 if (navigator.clipboard && navigator.clipboard.readText) {
119 return navigator.clipboard.readText();
120 }
121 if (seemToBeInIE()) {
122 return readIE();
123 }
124 return (new Promise((resolve, reject) => {
125 reject("Read is not supported in your browser.");
126 })) as Promise<string>;
127 }
128}
129
130/******** Implementations ********/
131
132class FallbackTracker {
133 public success: boolean = false;
134}
135
136function copyListener(tracker: FallbackTracker, data: DT, e: ClipboardEvent): void {
137 debugLog("listener called");
138 tracker.success = true;
139 data.forEach((value: string, key: string) => {
140 e.clipboardData.setData(key, value);
141 if (key === TEXT_PLAIN && e.clipboardData.getData(key) != value) {
142 debugLog("setting text/plain failed");
143 tracker.success = false;
144 }
145 });
146 e.preventDefault();
147}
148
149function execCopy(data: DT): boolean {
150 var tracker = new FallbackTracker();
151 var listener = copyListener.bind(this, tracker, data);
152
153 document.addEventListener("copy", listener);
154 try {
155 // We ignore the return value, since FallbackTracker tells us whether the
156 // listener was called. It seems that checking the return value here gives
157 // us no extra information in any browser.
158 document.execCommand("copy");
159 } finally {
160 document.removeEventListener("copy", listener);
161 }
162 return tracker.success;
163}
164
165// Temporarily select a DOM element, so that `execCommand()` is not rejected.
166function copyUsingTempSelection(e: HTMLElement, data: DT): boolean {
167 selectionSet(e);
168 var success = execCopy(data);
169 selectionClear();
170 return success;
171}
172
173// Create a temporary DOM element to select, so that `execCommand()` is not
174// rejected.
175function copyUsingTempElem(data: DT): boolean {
176 var tempElem = document.createElement("div");
177 // Setting an individual property does not support `!important`, so we set the
178 // whole style instead of just the `-webkit-user-select` property.
179 tempElem.setAttribute("style", "-webkit-user-select: text !important");
180 // Place some text in the elem so that Safari has something to select.
181 tempElem.textContent = "temporary element";
182 document.body.appendChild(tempElem);
183
184 var success = copyUsingTempSelection(tempElem, data);
185
186 document.body.removeChild(tempElem);
187 return success;
188}
189
190// Uses shadow DOM.
191function copyTextUsingDOM(str: string): boolean {
192 debugLog("copyTextUsingDOM");
193
194 var tempElem = document.createElement("div");
195 // Setting an individual property does not support `!important`, so we set the
196 // whole style instead of just the `-webkit-user-select` property.
197 tempElem.setAttribute("style", "-webkit-user-select: text !important");
198 // Use shadow DOM if available.
199 var spanParent: Node = tempElem;
200 if (tempElem.attachShadow) {
201 debugLog("Using shadow DOM.");
202 spanParent = tempElem.attachShadow({mode: "open"});
203 }
204
205 var span = document.createElement("span");
206 span.innerText = str;
207
208 spanParent.appendChild(span);
209 document.body.appendChild(tempElem);
210 selectionSet(span);
211
212 var result = document.execCommand("copy");
213
214 selectionClear();
215 document.body.removeChild(tempElem);
216
217 return result;
218}
219
220/******** Selection ********/
221
222function selectionSet(elem: Element): void {
223 var sel = document.getSelection();
224 var range = document.createRange();
225 range.selectNodeContents(elem);
226 sel.removeAllRanges();
227 sel.addRange(range);
228}
229
230function selectionClear(): void {
231 var sel = document.getSelection();
232 sel.removeAllRanges();
233}
234
235/******** Convenience ********/
236
237function DTFromText(s: string): DT {
238 var dt = new DT();
239 dt.setData(TEXT_PLAIN, s);
240 return dt;
241}
242
243/******** Internet Explorer ********/
244
245interface IEWindow extends Window {
246 clipboardData: {
247 setData: (key: string, value: string) => boolean;
248 // Always results in a string: https://msdn.microsoft.com/en-us/library/ms536436(v=vs.85).aspx
249 getData: (key: string) => string;
250 }
251}
252
253function seemToBeInIE(): boolean {
254 return typeof ClipboardEvent === "undefined" &&
255 typeof (window as IEWindow).clipboardData !== "undefined" &&
256 typeof (window as IEWindow).clipboardData.setData !== "undefined";
257}
258
259function writeIE(data: DT): boolean {
260 // IE supports text or URL, but not HTML: https://msdn.microsoft.com/en-us/library/ms536744(v=vs.85).aspx
261 // TODO: Write URLs to `text/uri-list`? https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types
262 var text = data.getData(TEXT_PLAIN);
263 if (text !== undefined) {
264 return (window as IEWindow).clipboardData.setData("Text", text);
265 }
266
267 throw ("No `text/plain` value was specified.");
268}
269
270// Returns "" if the read failed, e.g. because the user rejected the permission.
271function readIE(): Promise<string> {
272 return (new Promise((resolve, reject) => {
273 var text = (window as IEWindow).clipboardData.getData("Text");
274 if (text === "") {
275 reject(new Error("Empty clipboard or could not read plain text from clipboard"));
276 } else {
277 resolve(text);
278 }
279 })) as Promise<string>;
280}
281
282/******** Expose `clipboard` on the global object in browser. ********/
283
284// TODO: Figure out how to expose ClipboardPolyfill as self.clipboard using
285// WebPack?
286declare var module: any;
287module.exports = ClipboardPolyfill;
288
\No newline at end of file