UNPKG

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