UNPKG

8.06 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 async 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 // Internet Explorer
47 if (seemToBeInIE()) {
48 if (writeIE(data)) {
49 return;
50 } else {
51 throw "Copying failed, possibly because the user rejected it.";
52 }
53 }
54
55 if (execCopy(data)) {
56 debugLog("regular execCopy worked");
57 return;
58 }
59
60 // Success detection on Edge is not possible, due to bugs in all 4
61 // detection mechanisms we could try to use. Assume success.
62 if (navigator.userAgent.indexOf("Edge") > -1) {
63 debugLog("UA \"Edge\" => assuming success");
64 return;
65 }
66
67 // Fallback 1 for desktop Safari.
68 if (copyUsingTempSelection(document.body, data)) {
69 debugLog("copyUsingTempSelection worked");
70 return;
71 }
72
73 // Fallback 2 for desktop Safari.
74 if (copyUsingTempElem(data)) {
75 debugLog("copyUsingTempElem worked");
76 return;
77 }
78
79 // Fallback for iOS Safari.
80 var text = data.getData(TEXT_PLAIN);
81 if (text !== undefined && copyTextUsingDOM(text)) {
82 debugLog("copyTextUsingDOM worked");
83 return;
84 }
85
86 throw "Copy command failed.";
87 }
88
89 public static async writeText(s: string): Promise<void> {
90 if (navigator.clipboard && navigator.clipboard.writeText) {
91 debugLog("Using `navigator.clipboard.writeText()`.");
92 return navigator.clipboard.writeText(s);
93 }
94 return this.write(DTFromText(s));
95 }
96
97 public static async read(): Promise<DT> {
98 return DTFromText(await this.readText());
99 }
100
101 public static async readText(): Promise<string> {
102 if (navigator.clipboard && navigator.clipboard.readText) {
103 debugLog("Using `navigator.clipboard.readText()`.");
104 return navigator.clipboard.readText();
105 }
106 if (seemToBeInIE()) {
107 debugLog("Reading text using IE strategy.");
108 return readIE();
109 }
110 throw "Read is not supported in your browser.";
111 }
112}
113
114/******** Implementations ********/
115
116class FallbackTracker {
117 public success: boolean = false;
118}
119
120function copyListener(tracker: FallbackTracker, data: DT, e: ClipboardEvent): void {
121 debugLog("listener called");
122 tracker.success = true;
123 data.forEach((value: string, key: string) => {
124 e.clipboardData.setData(key, value);
125 if (key === TEXT_PLAIN && e.clipboardData.getData(key) != value) {
126 debugLog("setting text/plain failed");
127 tracker.success = false;
128 }
129 });
130 e.preventDefault();
131}
132
133function execCopy(data: DT): boolean {
134 var tracker = new FallbackTracker();
135 var listener = copyListener.bind(this, tracker, data);
136
137 document.addEventListener("copy", listener);
138 try {
139 // We ignore the return value, since FallbackTracker tells us whether the
140 // listener was called. It seems that checking the return value here gives
141 // us no extra information in any browser.
142 document.execCommand("copy");
143 } finally {
144 document.removeEventListener("copy", listener);
145 }
146 return tracker.success;
147}
148
149// Temporarily select a DOM element, so that `execCommand()` is not rejected.
150function copyUsingTempSelection(e: HTMLElement, data: DT): boolean {
151 selectionSet(e);
152 var success = execCopy(data);
153 selectionClear();
154 return success;
155}
156
157// Create a temporary DOM element to select, so that `execCommand()` is not
158// rejected.
159function copyUsingTempElem(data: DT): boolean {
160 var tempElem = document.createElement("div");
161 // Setting an individual property does not support `!important`, so we set the
162 // whole style instead of just the `-webkit-user-select` property.
163 tempElem.setAttribute("style", "-webkit-user-select: text !important");
164 // Place some text in the elem so that Safari has something to select.
165 tempElem.textContent = "temporary element";
166 document.body.appendChild(tempElem);
167
168 var success = copyUsingTempSelection(tempElem, data);
169
170 document.body.removeChild(tempElem);
171 return success;
172}
173
174// Uses shadow DOM.
175function copyTextUsingDOM(str: string): boolean {
176 debugLog("copyTextUsingDOM");
177
178 var tempElem = document.createElement("div");
179 // Setting an individual property does not support `!important`, so we set the
180 // whole style instead of just the `-webkit-user-select` property.
181 tempElem.setAttribute("style", "-webkit-user-select: text !important");
182 // Use shadow DOM if available.
183 var spanParent: Node = tempElem;
184 if (tempElem.attachShadow) {
185 debugLog("Using shadow DOM.");
186 spanParent = tempElem.attachShadow({mode: "open"});
187 }
188
189 var span = document.createElement("span");
190 span.innerText = str;
191
192 spanParent.appendChild(span);
193 document.body.appendChild(tempElem);
194 selectionSet(span);
195
196 var result = document.execCommand("copy");
197
198 selectionClear();
199 document.body.removeChild(tempElem);
200
201 return result;
202}
203
204/******** Selection ********/
205
206function selectionSet(elem: Element): void {
207 var sel = document.getSelection();
208 var range = document.createRange();
209 range.selectNodeContents(elem);
210 sel.removeAllRanges();
211 sel.addRange(range);
212}
213
214function selectionClear(): void {
215 var sel = document.getSelection();
216 sel.removeAllRanges();
217}
218
219/******** Convenience ********/
220
221function DTFromText(s: string): DT {
222 var dt = new DT();
223 dt.setData(TEXT_PLAIN, s);
224 return dt;
225}
226
227/******** Internet Explorer ********/
228
229interface IEWindow extends Window {
230 clipboardData: {
231 setData: (key: string, value: string) => boolean;
232 // Always results in a string: https://msdn.microsoft.com/en-us/library/ms536436(v=vs.85).aspx
233 getData: (key: string) => string;
234 }
235}
236
237function seemToBeInIE(): boolean {
238 return typeof ClipboardEvent === "undefined" &&
239 typeof (window as IEWindow).clipboardData !== "undefined" &&
240 typeof (window as IEWindow).clipboardData.setData !== "undefined";
241}
242
243function writeIE(data: DT): boolean {
244 // IE supports text or URL, but not HTML: https://msdn.microsoft.com/en-us/library/ms536744(v=vs.85).aspx
245 // TODO: Write URLs to `text/uri-list`? https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types
246 var text = data.getData(TEXT_PLAIN);
247 if (text !== undefined) {
248 return (window as IEWindow).clipboardData.setData("Text", text);
249 }
250
251 throw ("No `text/plain` value was specified.");
252}
253
254// Returns "" if the read failed, e.g. because the user rejected the permission.
255async function readIE(): Promise<string> {
256 var text = (window as IEWindow).clipboardData.getData("Text");
257 if (text === "") {
258 throw "Empty clipboard or could not read plain text from clipboard";
259 }
260 return text;
261}
262
263/******** Expose `clipboard` on the global object in browser. ********/
264
265// TODO: Figure out how to expose ClipboardPolyfill as self.clipboard using
266// WebPack?
267declare var module: any;
268module.exports = ClipboardPolyfill;