1 | import {DT, suppressDTWarnings} from "./DT";
|
2 |
|
3 |
|
4 |
|
5 | var debugLog: (s: string) => void = function(s: string) {};
|
6 | var showWarnings = true;
|
7 |
|
8 |
|
9 |
|
10 | var warnOrLog = function() {
|
11 | (console.warn || console.log).apply(console, arguments);
|
12 | };
|
13 | var warn = warnOrLog.bind("[clipboard-polyfill]");
|
14 |
|
15 | var 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 |
|
26 | export 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 |
|
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 |
|
64 |
|
65 | if (navigator.userAgent.indexOf("Edge") > -1) {
|
66 | debugLog("UA \"Edge\" => assuming success");
|
67 | resolve();
|
68 | return;
|
69 | }
|
70 |
|
71 |
|
72 | if (copyUsingTempSelection(document.body, data)) {
|
73 | debugLog("copyUsingTempSelection worked");
|
74 | resolve();
|
75 | return;
|
76 | }
|
77 |
|
78 |
|
79 | if (copyUsingTempElem(data)) {
|
80 | debugLog("copyUsingTempElem worked");
|
81 | resolve();
|
82 | return;
|
83 | }
|
84 |
|
85 |
|
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 |
|
109 |
|
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 |
|
132 | class FallbackTracker {
|
133 | public success: boolean = false;
|
134 | }
|
135 |
|
136 | function 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 |
|
149 | function 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.
|
166 | function 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.
|
175 | function 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.
|
191 | function 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 |
|
222 | function 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 |
|
230 | function selectionClear(): void {
|
231 | var sel = document.getSelection();
|
232 | sel.removeAllRanges();
|
233 | }
|
234 |
|
235 | /******** Convenience ********/
|
236 |
|
237 | function DTFromText(s: string): DT {
|
238 | var dt = new DT();
|
239 | dt.setData(TEXT_PLAIN, s);
|
240 | return dt;
|
241 | }
|
242 |
|
243 | /******** Internet Explorer ********/
|
244 |
|
245 | interface IEWindow extends Window {
|
246 | clipboardData: {
|
247 | setData: (key: string, value: string) => boolean;
|
248 |
|
249 | getData: (key: string) => string;
|
250 | }
|
251 | }
|
252 |
|
253 | function seemToBeInIE(): boolean {
|
254 | return typeof ClipboardEvent === "undefined" &&
|
255 | typeof (window as IEWindow).clipboardData !== "undefined" &&
|
256 | typeof (window as IEWindow).clipboardData.setData !== "undefined";
|
257 | }
|
258 |
|
259 | function writeIE(data: DT): boolean {
|
260 |
|
261 |
|
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 |
|
271 | function 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?
|
286 | declare var module: any;
|
287 | module.exports = ClipboardPolyfill;
|
288 |
|
\ | No newline at end of file |