diff options
Diffstat (limited to 'debian/missing-sources/clipboard-polyfill-be05dad.ts')
-rw-r--r-- | debian/missing-sources/clipboard-polyfill-be05dad.ts | 270 |
1 files changed, 270 insertions, 0 deletions
diff --git a/debian/missing-sources/clipboard-polyfill-be05dad.ts b/debian/missing-sources/clipboard-polyfill-be05dad.ts new file mode 100644 index 0000000..602cadb --- /dev/null +++ b/debian/missing-sources/clipboard-polyfill-be05dad.ts @@ -0,0 +1,270 @@ +import {Promise} from "es6-promise"; +import {DT, suppressDTWarnings} from "./DT"; + +// Debug log strings should be short, since they are copmiled into the production build. +// TODO: Compile debug logging code out of production builds? +var debugLog: (s: string) => void = function(s: string) {}; +var showWarnings = true; +var warn = (console.warn || console.log).bind(console, "[clipboard-polyfill]"); + +var TEXT_PLAIN = "text/plain"; + +export default class ClipboardPolyfill { + public static readonly DT = DT; + + public static setDebugLog(f: (s: string) => void): void { + debugLog = f; + } + + public static suppressWarnings() { + showWarnings = false; + suppressDTWarnings(); + } + + public static write(data: DT): Promise<void> { + if (showWarnings && !data.getData(TEXT_PLAIN)) { + warn("clipboard.write() was called without a "+ + "`text/plain` data type. On some platforms, this may result in an "+ + "empty clipboard. Call clipboard.suppressWarnings() "+ + "to suppress this warning."); + } + + return new Promise<void>((resolve, reject) => { + // Internet Explorer + if (seemToBeInIE()) { + if (writeIE(data)) { + resolve() + } else { + reject(new Error("Copying failed, possibly because the user rejected it.")); + } + return; + } + + var tracker = execCopy(data); + if (tracker.success) { + debugLog("regular execCopy worked"); + 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 (navigator.userAgent.indexOf("Edge") > -1) { + debugLog("UA \"Edge\" => assuming success"); + resolve(); + return; + } + + // Fallback 1 for desktop Safari. + tracker = copyUsingTempSelection(document.body, data); + if (tracker.success) { + debugLog("copyUsingTempSelection worked"); + resolve(); + return; + } + + // Fallback 2 for desktop Safari. + tracker = copyUsingTempElem(data); + if (tracker.success) { + debugLog("copyUsingTempElem worked"); + resolve(); + return; + } + + // Fallback for iOS Safari. + var text = data.getData(TEXT_PLAIN); + if (text !== undefined && copyTextUsingDOM(text)) { + debugLog("copyTextUsingDOM worked"); + resolve(); + return; + } + + reject(new Error("Copy command failed.")); + }); + } + + public static writeText(s: string): Promise<void> { + var dt = new DT(); + dt.setData(TEXT_PLAIN, s); + return this.write(dt); + } + + public static read(): Promise<DT> { + return new Promise((resolve, reject) => { + if (seemToBeInIE()) { + readIE().then( + (s: string) => resolve(DTFromText(s)), + reject + ); + return; + } + // TODO: Attempt to read using async clipboard API. + reject("Read is not supported in your browser.") + }); + } + + public static readText(): Promise<string> { + if (seemToBeInIE()) { + return readIE(); + } + return new Promise((resolve, reject) => { + // TODO: Attempt to read using async clipboard API. + reject("Read is not supported in your browser.") + }); + } +} + +/******** Implementations ********/ + +class FallbackTracker { + public success: boolean = false; +} + +function copyListener(tracker: FallbackTracker, data: DT, e: ClipboardEvent): void { + debugLog("listener called"); + tracker.success = true; + data.forEach((value: string, key: string) => { + e.clipboardData.setData(key, value); + if (key === TEXT_PLAIN && e.clipboardData.getData(key) != value) { + debugLog("setting text/plain failed"); + tracker.success = false; + } + }); + e.preventDefault(); +} + +function execCopy(data: DT): FallbackTracker { + var tracker = new FallbackTracker(); + var listener = copyListener.bind(this, tracker, data); + + document.addEventListener("copy", listener); + try { + // We ignore the return value, since FallbackTracker tells us whether the + // listener was called. It seems that checking the return value here gives + // us no extra information in any browser. + document.execCommand("copy"); + } finally { + document.removeEventListener("copy", listener); + } + return tracker; +} + +// Create a temporary DOM element to select, so that `execCommand()` is not +// rejected. +function copyUsingTempSelection(e: HTMLElement, data: DT): FallbackTracker { + selectionSet(e); + var tracker = execCopy(data); + selectionClear(); + return tracker; +} + +// Create a temporary DOM element to select, so that `execCommand()` is not +// rejected. +function 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 = copyUsingTempSelection(tempElem, data); + + document.body.removeChild(tempElem); + return tracker; +} + +// Uses shadow DOM. +function copyTextUsingDOM(str: string): boolean { + debugLog("copyTextUsingDOM"); + + var tempElem = document.createElement("div"); + // Use shadow DOM if available. + var spanParent: Node = tempElem; + if (tempElem.attachShadow) { + debugLog("Using shadow DOM."); + spanParent = tempElem.attachShadow({mode: "open"}); + } + + var span = document.createElement("span"); + span.innerText = str; + // span.style.whiteSpace = "pre-wrap"; // TODO: Use `innerText` above instead? + + spanParent.appendChild(span); + document.body.appendChild(tempElem); + selectionSet(span); + + var result = document.execCommand("copy"); + + selectionClear(); + document.body.removeChild(tempElem); + + return result; +} + +/******** Selection ********/ + +function selectionSet(elem: Element): void { + var sel = document.getSelection(); + var range = document.createRange(); + range.selectNodeContents(elem); + sel.removeAllRanges(); + sel.addRange(range); +} + +function selectionClear(): void { + var sel = document.getSelection(); + sel.removeAllRanges(); +} + +/******** Convenience ********/ + +function DTFromText(s: string): DT { + var dt = new DT(); + dt.setData(TEXT_PLAIN, s); + return dt; +} + +/******** Internet Explorer ********/ + +interface IEWindow extends Window { + clipboardData: { + setData: (key: string, value: string) => boolean; + // Always results in a string: https://msdn.microsoft.com/en-us/library/ms536436(v=vs.85).aspx + getData: (key: string) => string; + } +} + +function seemToBeInIE(): boolean { + return typeof ClipboardEvent === "undefined" && + typeof (window as IEWindow).clipboardData !== "undefined" && + typeof (window as IEWindow).clipboardData.setData !== "undefined"; +} + +function 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."); +} + +// Returns "" if the read failed, e.g. because the user rejected the permission. +function readIE(): Promise<string> { + return new Promise((resolve, reject) => { + var text = (window as IEWindow).clipboardData.getData("Text"); + if (text === "") { + reject(new Error("Empty clipboard or could not read plain text from clipboard")); + } else { + resolve(text); + } + }) +} + +/******** Expose `clipboard` on the global object in browser. ********/ + +// TODO: Figure out how to expose ClipboardPolyfill as self.clipboard using +// WebPack? +declare var module: any; +module.exports = ClipboardPolyfill; |