Test
26 | 27 | 28 | 29 |30 | 31 | 32 | 33 |
34 | 35 | 36 | 37 |
38 | 39 | 40 | 41 |
42 | 43 | 44 | 45 |
46 | 47 | 48 | 49 |
50 | 51 | 52 | 53 |
54 | 55 | 56 | 57 |
58 | 59 | 60 | 61 |
62 | 63 | 64 | 65 |
66 | 67 | 68 | 69 |
70 | 71 |
(https://garron.net/)",
6 | "license": "MIT",
7 | "bugs": {
8 | "url": "https://github.com/lgarron/clipboard-polyfill/issues"
9 | },
10 | "type": "module",
11 | "main": "./dist/es6/clipboard-polyfill.es6.js",
12 | "module": "./dist/es6/clipboard-polyfill.es6.js",
13 | "types": "./dist/types/entries/es6/clipboard-polyfill.es6.d.ts",
14 | "exports": {
15 | ".": {
16 | "types": "./dist/types/entries/es6/clipboard-polyfill.es6.d.ts",
17 | "import": "./dist/es6/clipboard-polyfill.es6.js",
18 | "default": "./dist/es6/clipboard-polyfill.es6.js"
19 | },
20 | "./overwrite-globals": {
21 | "types": "./dist/types/entries/es5/overwrite-globals.d.ts",
22 | "import": "./dist/es5/overwrite-globals/clipboard-polyfill.overwrite-globals.es5.js",
23 | "default": "./dist/es5/overwrite-globals/clipboard-polyfill.overwrite-globals.es5.js"
24 | }
25 | },
26 | "repository": {
27 | "type": "git",
28 | "url": "https://github.com/lgarron/clipboard-polyfill"
29 | },
30 | "keywords": [
31 | "clipboard",
32 | "HTML5",
33 | "copy",
34 | "copying",
35 | "cut",
36 | "paste",
37 | "execCommand",
38 | "setData",
39 | "getData",
40 | "polyfill"
41 | ],
42 | "files": [
43 | "/dist",
44 | "/overwrite-globals",
45 | "README.md"
46 | ],
47 | "devDependencies": {
48 | "@biomejs/biome": "^2.0.0-beta.3",
49 | "@cubing/deploy": "^0.2.2",
50 | "@types/bun": "^1.1.0",
51 | "barely-a-dev-server": "^0.7.1",
52 | "esbuild": "^0.25.0",
53 | "typescript": "^5.4.5"
54 | },
55 | "scripts": {
56 | "prepublishOnly": "make prepublishOnly"
57 | },
58 | "@cubing/deploy": {
59 | "$schema": "./node_modules/@cubing/deploy/config-schema.json",
60 | "https://garron.net/code/clipboard-polyfill": {
61 | "fromLocalDir": "./dist/demo/"
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/script/build-demo.ts:
--------------------------------------------------------------------------------
1 | import { barelyServe } from "barely-a-dev-server";
2 |
3 | await barelyServe({
4 | dev: false,
5 | entryRoot: "./src/demo",
6 | outDir: "./dist/demo",
7 | esbuildOptions: {
8 | minify: false,
9 | target: "es5",
10 | splitting: false,
11 | },
12 | });
13 |
--------------------------------------------------------------------------------
/script/build.ts:
--------------------------------------------------------------------------------
1 | import { build } from "esbuild";
2 |
3 | await build({
4 | entryPoints: [
5 | "./src/clipboard-polyfill/entries/es6/clipboard-polyfill.es6.ts",
6 | ],
7 | format: "esm",
8 | target: "es6",
9 | bundle: true,
10 | sourcemap: true,
11 | outdir: "./dist/es6/",
12 | });
13 |
14 | async function buildES5(src, entriestem) {
15 | await build({
16 | entryPoints: [src],
17 | target: "es5",
18 | bundle: true,
19 | sourcemap: true,
20 | banner: { js: '"use strict";' },
21 | outfile: `${entriestem}.es5.js`,
22 | });
23 | }
24 |
25 | await buildES5(
26 | "src/clipboard-polyfill/entries/es5/window-var.ts",
27 | "dist/es5/window-var/clipboard-polyfill.window-var",
28 | );
29 | await buildES5(
30 | "src/clipboard-polyfill/entries/es5/window-var.promise.ts",
31 | "dist/es5/window-var/clipboard-polyfill.window-var.promise",
32 | );
33 | await buildES5(
34 | "src/clipboard-polyfill/entries/es5/overwrite-globals.ts",
35 | "dist/es5/overwrite-globals/clipboard-polyfill.overwrite-globals",
36 | );
37 | await buildES5(
38 | "src/clipboard-polyfill/entries/es5/overwrite-globals.promise.ts",
39 | "dist/es5/overwrite-globals/clipboard-polyfill.overwrite-globals.promise",
40 | );
41 |
--------------------------------------------------------------------------------
/script/dev.ts:
--------------------------------------------------------------------------------
1 | import { barelyServe } from "barely-a-dev-server";
2 |
3 | await barelyServe({
4 | entryRoot: "./src/demo",
5 | outDir: "./.temp/dev",
6 | esbuildOptions: {
7 | target: "es5",
8 | splitting: false,
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/script/mock-test.bash:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euo pipefail
4 |
5 | function runMockText {
6 | BASENAME="${1}"
7 | npx esbuild \
8 | --format=esm --target=es2020 \
9 | --bundle --external:node:assert \
10 | --supported:top-level-await=true \
11 | --outfile="./dist/mock-test/${BASENAME}.js" \
12 | "./src/mock-test/${BASENAME}.ts"
13 |
14 | node "./dist/mock-test/${BASENAME}.js"
15 | }
16 |
17 | runMockText modern-writeText
18 | runMockText missing-Promise
19 |
--------------------------------------------------------------------------------
/script/test-bun.ts:
--------------------------------------------------------------------------------
1 | import { basename } from "node:path";
2 | import { Glob, spawn } from "bun";
3 |
4 | const glob = new Glob("**/*.test.ts");
5 |
6 | console.log("Running bun test files individually");
7 |
8 | // Scans the current working directory and each of its sub-directories recursively
9 | for await (const file of glob.scan(".")) {
10 | if (basename(file) === "bun-test-cannot-run-all-tests.test.ts") {
11 | continue;
12 | }
13 | console.log(`Running: bun test "${file}"`);
14 |
15 | if ((await spawn(["bun", "test", file], {}).exited) !== 0) {
16 | throw new Error(`Bun test failed for file: ${file}`);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/ClipboardItem/ClipboardItemPolyfill.ts:
--------------------------------------------------------------------------------
1 | import { promiseConstructor } from "../builtins/builtin-globals";
2 | import { stringToBlob } from "./convert";
3 | import type {
4 | ClipboardItemConstructor,
5 | ClipboardItemDataType,
6 | ClipboardItemInterface,
7 | ClipboardItemOptions,
8 | } from "./spec";
9 |
10 | function ClipboardItemPolyfillImpl(
11 | // TODO: The spec specifies values as `ClipboardItemData`, but
12 | // implementations (e.g. Chrome 83) seem to assume `ClipboardItemDataType`
13 | // values. https://github.com/w3c/clipboard-apis/pull/126
14 | items: { [type: string]: ClipboardItemDataType },
15 | options?: ClipboardItemOptions,
16 | ): ClipboardItemInterface {
17 | var types = Object.keys(items);
18 | var _items: { [type: string]: Blob } = {};
19 |
20 | for (var type in items) {
21 | var item = items[type];
22 | if (typeof item === "string") {
23 | _items[type] = stringToBlob(type, item);
24 | } else {
25 | _items[type] = item;
26 | }
27 | }
28 | // The explicit default for `presentationStyle` is "unspecified":
29 | // https://www.w3.org/TR/clipboard-apis/#clipboard-interface
30 | var presentationStyle = options?.presentationStyle ?? "unspecified";
31 |
32 | function getType(type: string): Promise {
33 | return promiseConstructor.resolve(_items[type]);
34 | }
35 | return {
36 | types: types,
37 | presentationStyle: presentationStyle,
38 | getType: getType,
39 | };
40 | }
41 |
42 | export var ClipboardItemPolyfill: ClipboardItemConstructor =
43 | ClipboardItemPolyfillImpl as any as ClipboardItemConstructor;
44 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/ClipboardItem/check.ts:
--------------------------------------------------------------------------------
1 | import type { ClipboardItemInterface } from "./spec";
2 |
3 | export function hasItemWithType(
4 | clipboardItems: ClipboardItemInterface[],
5 | typeName: string,
6 | ): boolean {
7 | for (var i = 0; i < clipboardItems.length; i++) {
8 | var item = clipboardItems[i];
9 | if (item.types.indexOf(typeName) !== -1) {
10 | return true;
11 | }
12 | }
13 | return false;
14 | }
15 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/ClipboardItem/convert.ts:
--------------------------------------------------------------------------------
1 | import {
2 | originalWindowClipboardItem,
3 | promiseConstructor,
4 | } from "../builtins/builtin-globals";
5 | import { promiseRecordMap } from "../promise/promise-compat";
6 | import { ClipboardItemPolyfill } from "./ClipboardItemPolyfill";
7 | import { TEXT_PLAIN } from "./data-types";
8 | import type { ClipboardItemInterface, ClipboardItemOptions } from "./spec";
9 |
10 | export function stringToBlob(type: string, str: string): Blob {
11 | return new Blob([str], {
12 | type,
13 | });
14 | }
15 |
16 | export function blobToString(blob: Blob): Promise {
17 | return new promiseConstructor((resolve, reject) => {
18 | var fileReader = new FileReader();
19 | fileReader.addEventListener("load", () => {
20 | var result = fileReader.result;
21 | if (typeof result === "string") {
22 | resolve(result);
23 | } else {
24 | reject("could not convert blob to string");
25 | }
26 | });
27 | fileReader.readAsText(blob);
28 | });
29 | }
30 |
31 | export function clipboardItemToGlobalClipboardItem(
32 | clipboardItem: ClipboardItemInterface,
33 | ): Promise {
34 | // Note that we use `Blob` instead of `ClipboardItemDataType`. This is because
35 | // Chrome 83 can only accept `Blob` (not `string`). The return value of
36 | // `getType()` is already `Blob` per the spec, so this is simple for us.
37 | return promiseRecordMap(clipboardItem.types, (type: string) => {
38 | return clipboardItem.getType(type);
39 | }).then((items: Record) => {
40 | return new promiseConstructor((resolve, reject) => {
41 | var options: ClipboardItemOptions = {};
42 | if (clipboardItem.presentationStyle) {
43 | options.presentationStyle = clipboardItem.presentationStyle;
44 | }
45 | if (originalWindowClipboardItem) {
46 | resolve(new originalWindowClipboardItem(items, options));
47 | } else {
48 | reject("window.ClipboardItem is not defined");
49 | }
50 | });
51 | });
52 | }
53 |
54 | export function textToClipboardItem(text: string): ClipboardItemInterface {
55 | var items: { [type: string]: Blob } = {};
56 | items[TEXT_PLAIN] = stringToBlob(text, TEXT_PLAIN);
57 | return new ClipboardItemPolyfill(items);
58 | }
59 |
60 | export function getTypeAsString(
61 | clipboardItem: ClipboardItemInterface,
62 | type: string,
63 | ): Promise {
64 | return clipboardItem.getType(type).then((text: Blob) => {
65 | return blobToString(text);
66 | });
67 | }
68 |
69 | export interface StringItem {
70 | [type: string]: string;
71 | }
72 |
73 | export function toStringItem(
74 | data: ClipboardItemInterface,
75 | ): Promise {
76 | return promiseRecordMap(data.types, (type: string) =>
77 | getTypeAsString(data, type),
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/ClipboardItem/data-types.ts:
--------------------------------------------------------------------------------
1 | export var TEXT_PLAIN = "text/plain";
2 | export var TEXT_HTML = "text/html";
3 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/ClipboardItem/spec.ts:
--------------------------------------------------------------------------------
1 | // This should be a `.d.ts` file, but we need to make it `.ts` (or Rollup won't include it in the output).
2 |
3 | // This file is a representation of the Clipboard Interface from the async
4 | // clipboard API spec. We match the order and spacing of the spec as much as
5 | // possible, for easy comparison.
6 | // https://www.w3.org/TR/clipboard-apis/#clipboard-interface
7 |
8 | // The spec specifies some non-optional fields, but initial browser implementations of the async clipboard API
9 | // may not have them. We don't rely on their existence in this library, and we
10 | // mark them as optional with [optional here, non-optional in spec].
11 |
12 | export type ClipboardItems = ClipboardItemInterface[];
13 |
14 | export interface ClipboardWithoutEventTarget {
15 | read(): Promise;
16 | readText(): Promise;
17 | write(data: ClipboardItems): Promise;
18 | writeText(data: string): Promise;
19 | }
20 |
21 | export interface ClipboardEventTarget
22 | extends EventTarget,
23 | ClipboardWithoutEventTarget {}
24 |
25 | export type ClipboardItemDataType = string | Blob;
26 | export type ClipboardItemData = Promise;
27 |
28 | export type ClipboardItemDelayedCallback = () => ClipboardItemDelayedCallback;
29 |
30 | // We can't specify the constructor (or static methods) inside the main
31 | // interface definition, so we specify it separately. See
32 | // https://www.typescriptlang.org/docs/handbook/interfaces.html#difference-between-the-static-and-instance-sides-of-classes
33 | export interface ClipboardItemConstructor {
34 | // Note: some implementations (e.g. Chrome 83) only acceps Blob item values,
35 | // and throw an exception if you try to pass any strings.
36 | new (
37 | // TODO: The spec specifies values as `ClipboardItemData`, but
38 | // implementations (e.g. Chrome 83) seem to assume `ClipboardItemDataType`
39 | // values. https://github.com/w3c/clipboard-apis/pull/126
40 | items: { [type: string]: ClipboardItemDataType },
41 | options?: ClipboardItemOptions,
42 | ): ClipboardItemInterface;
43 |
44 | createDelayed?(
45 | // [optional here, non-optional in spec]
46 | items: { [type: string]: () => ClipboardItemDelayedCallback },
47 | options?: ClipboardItemOptions,
48 | ): ClipboardItemInterface;
49 | }
50 |
51 | // We name this `ClipboardItemInterface` instead of `ClipboardItem` because we
52 | // implement our polyfill from the library as `ClipboardItem`.
53 | export interface ClipboardItemInterface {
54 | // Safari 13.1 implements `presentationStyle`:
55 | // https://webkit.org/blog/10855/async-clipboard-api/
56 | readonly presentationStyle?: PresentationStyle; // [optional here, non-optional in spec]
57 | readonly lastModified?: number; // [optional here, non-optional in spec]
58 | readonly delayed?: boolean; // [optional here, non-optional in spec]
59 |
60 | readonly types: ReadonlyArray;
61 |
62 | getType(type: string): Promise;
63 | }
64 |
65 | export type PresentationStyle = "unspecified" | "inline" | "attachment";
66 |
67 | export interface ClipboardItemOptions {
68 | presentationStyle?: PresentationStyle;
69 | }
70 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/builtins/builtin-globals.ts:
--------------------------------------------------------------------------------
1 | // We cache the references so that callers can do the following without causing infinite recursion/bugs:
2 | //
3 | // import * as clipboard from "clipboard-polyfill";
4 | // navigator.clipboard = clipboard;
5 | //
6 | // import { ClipboardItem } from "clipboard-polyfill";
7 | // window.ClipboardItem = clipboard;
8 | //
9 | // Note that per the spec:
10 | //
11 | // - is *not* possible to overwrite `navigator.clipboard`. https://www.w3.org/TR/clipboard-apis/#navigator-interface
12 | // - it *may* be possible to overwrite `window.ClipboardItem`.
13 | //
14 | // Chrome 83 and Safari 13.1 match this. We save the original
15 | // `navigator.clipboard` anyhow, because 1) it doesn't cost more code (in fact,
16 | // it probably saves code), and 2) just in case an unknown/future implementation
17 | // allows overwriting `navigator.clipboard` like this.
18 |
19 | import type {
20 | ClipboardEventTarget,
21 | ClipboardItemConstructor,
22 | ClipboardItems,
23 | } from "../ClipboardItem/spec";
24 | import type { PromiseConstructor } from "../promise/es6-promise";
25 | import { getPromiseConstructor } from "./promise-constructor";
26 | import { originalWindow } from "./window-globalThis";
27 |
28 | var originalNavigator =
29 | typeof navigator === "undefined" ? undefined : navigator;
30 | var originalNavigatorClipboard: ClipboardEventTarget | undefined =
31 | originalNavigator?.clipboard as any;
32 | export var originalNavigatorClipboardRead:
33 | | (() => Promise)
34 | | undefined = originalNavigatorClipboard?.read?.bind(
35 | originalNavigatorClipboard,
36 | );
37 | export var originalNavigatorClipboardReadText:
38 | | (() => Promise)
39 | | undefined = originalNavigatorClipboard?.readText?.bind(
40 | originalNavigatorClipboard,
41 | );
42 | export var originalNavigatorClipboardWrite:
43 | | ((data: ClipboardItems) => Promise)
44 | | undefined = originalNavigatorClipboard?.write?.bind(
45 | originalNavigatorClipboard,
46 | );
47 | export var originalNavigatorClipboardWriteText:
48 | | ((data: string) => Promise)
49 | | undefined = originalNavigatorClipboard?.writeText?.bind(
50 | originalNavigatorClipboard,
51 | );
52 |
53 | // The spec specifies that this goes on `window`, not e.g. `globalThis`. It's not (currently) available in workers.
54 | export var originalWindowClipboardItem: ClipboardItemConstructor | undefined =
55 | originalWindow?.ClipboardItem;
56 |
57 | export var promiseConstructor: PromiseConstructor = getPromiseConstructor();
58 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/builtins/promise-constructor.ts:
--------------------------------------------------------------------------------
1 | import type { PromiseConstructor } from "../promise/es6-promise";
2 | import { originalGlobalThis, originalWindow } from "./window-globalThis";
3 |
4 | var promiseConstructorImpl: PromiseConstructor | undefined =
5 | (originalWindow as { Promise?: PromiseConstructor } | undefined)?.Promise ??
6 | originalGlobalThis?.Promise;
7 |
8 | // This must be called *before* `builtin-globals.ts` is imported, or it has no effect.
9 | export function setPromiseConstructor(
10 | newPromiseConstructorImpl: PromiseConstructor,
11 | ) {
12 | promiseConstructorImpl = newPromiseConstructorImpl;
13 | }
14 |
15 | export function getPromiseConstructor(): PromiseConstructor {
16 | if (!promiseConstructorImpl) {
17 | throw new Error(
18 | "No `Promise` implementation available for `clipboard-polyfill`. Consider using: https://github.com/lgarron/clipboard-polyfill#flat-file-version-with-promise-included",
19 | );
20 | }
21 | return promiseConstructorImpl;
22 | }
23 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/builtins/window-globalThis.ts:
--------------------------------------------------------------------------------
1 | export var originalWindow = typeof window === "undefined" ? undefined : window;
2 | export var originalGlobalThis =
3 | typeof globalThis === "undefined" ? undefined : globalThis;
4 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/debug.ts:
--------------------------------------------------------------------------------
1 | /******** Debug Logging ********/
2 |
3 | // tslint:disable-next-line: no-empty
4 | var debugLogImpl = (_s: string) => {};
5 |
6 | export function debugLog(s: string) {
7 | debugLogImpl(s);
8 | }
9 |
10 | export function setDebugLog(logFn: (s: string) => void) {
11 | debugLogImpl = logFn;
12 | }
13 |
14 | /******** Warnings ********/
15 |
16 | var showWarnings = true;
17 |
18 | export function suppressWarnings() {
19 | showWarnings = false;
20 | }
21 |
22 | export function shouldShowWarnings(): boolean {
23 | return showWarnings;
24 | }
25 |
26 | // Workaround for:
27 | // - IE9 (can't bind console functions directly), and
28 | // - Edge Issue #14495220 (referencing `console` without F12 Developer Tools can cause an exception)
29 | function warnOrLog() {
30 | // biome-ignore lint/style/noArguments: Intentional old-fashioned code.
31 | (console.warn || console.log).apply(console, arguments);
32 | }
33 |
34 | export var warn = warnOrLog.bind("[clipboard-polyfill]");
35 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/entries/es5/overwrite-globals.promise.ts:
--------------------------------------------------------------------------------
1 | // Set the Promise polyfill before globals.
2 | import "../../promise/set-promise-polyfill-if-needed";
3 | // Import `./globals` that the globals are cached before this runs.
4 | import "../../builtins/builtin-globals";
5 |
6 | import { PromisePolyfillConstructor } from "../../promise/polyfill";
7 |
8 | import "./overwrite-globals";
9 |
10 | (window as any).Promise = PromisePolyfillConstructor;
11 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/entries/es5/overwrite-globals.ts:
--------------------------------------------------------------------------------
1 | import { ClipboardItemPolyfill } from "../../ClipboardItem/ClipboardItemPolyfill";
2 | import { read, write } from "../../implementations/blob";
3 | import { readText, writeText } from "../../implementations/text";
4 |
5 | // Create the `navigator.clipboard` object if it doesn't exist.
6 | if (!navigator.clipboard) {
7 | (navigator as any).clipboard = {};
8 | }
9 |
10 | // Set/replace the implementations.
11 | navigator.clipboard.read = read as any;
12 | navigator.clipboard.readText = readText;
13 | navigator.clipboard.write = write;
14 | navigator.clipboard.writeText = writeText;
15 |
16 | // @ts-ignore
17 | window.ClipboardItem = ClipboardItemPolyfill;
18 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/entries/es5/window-var.promise.ts:
--------------------------------------------------------------------------------
1 | // Set the Promise polyfill before globals.
2 | import "../../promise/set-promise-polyfill-if-needed";
3 |
4 | import type { PromiseConstructor } from "../../promise/es6-promise";
5 | import { PromisePolyfillConstructor } from "../../promise/polyfill";
6 |
7 | import "./window-var";
8 |
9 | declare global {
10 | var PromisePolyfill: PromiseConstructor;
11 | }
12 |
13 | window.PromisePolyfill = PromisePolyfillConstructor;
14 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/entries/es5/window-var.ts:
--------------------------------------------------------------------------------
1 | import { ClipboardItemPolyfill } from "../../ClipboardItem/ClipboardItemPolyfill";
2 | import type {
3 | ClipboardItemConstructor,
4 | ClipboardWithoutEventTarget,
5 | } from "../../ClipboardItem/spec";
6 | import { setDebugLog, suppressWarnings } from "../../debug";
7 | import { read, write } from "../../implementations/blob";
8 | import { readText, writeText } from "../../implementations/text";
9 |
10 | declare global {
11 | var clipboard: ClipboardWithoutEventTarget & {
12 | ClipboardItem: ClipboardItemConstructor;
13 | setDebugLog: typeof setDebugLog;
14 | suppressWarnings: typeof suppressWarnings;
15 | };
16 | }
17 |
18 | window.clipboard = {
19 | read: read,
20 | readText: readText,
21 | write: write,
22 | writeText: writeText,
23 | ClipboardItem: ClipboardItemPolyfill,
24 | setDebugLog: setDebugLog,
25 | suppressWarnings: suppressWarnings,
26 | };
27 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/entries/es6/clipboard-polyfill.es6.ts:
--------------------------------------------------------------------------------
1 | export { ClipboardItemPolyfill as ClipboardItem } from "../../ClipboardItem/ClipboardItemPolyfill";
2 | export type {
3 | ClipboardItemConstructor,
4 | ClipboardItemData,
5 | ClipboardItemDataType,
6 | ClipboardItemDelayedCallback,
7 | ClipboardItemInterface,
8 | ClipboardItemOptions,
9 | ClipboardItems,
10 | PresentationStyle,
11 | } from "../../ClipboardItem/spec";
12 | export { setDebugLog, suppressWarnings } from "../../debug";
13 | export { read, write } from "../../implementations/blob";
14 | export { readText, writeText } from "../../implementations/text";
15 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/implementations/blob.ts:
--------------------------------------------------------------------------------
1 | import {
2 | originalNavigatorClipboardRead,
3 | originalNavigatorClipboardWrite,
4 | originalWindowClipboardItem,
5 | promiseConstructor,
6 | } from "../builtins/builtin-globals";
7 | import { hasItemWithType } from "../ClipboardItem/check";
8 | import {
9 | clipboardItemToGlobalClipboardItem,
10 | type StringItem,
11 | textToClipboardItem,
12 | toStringItem,
13 | } from "../ClipboardItem/convert";
14 | import { TEXT_HTML, TEXT_PLAIN } from "../ClipboardItem/data-types";
15 | import type {
16 | ClipboardItemInterface,
17 | ClipboardItems,
18 | } from "../ClipboardItem/spec";
19 | import { debugLog, shouldShowWarnings } from "../debug";
20 | import {
21 | falsePromise,
22 | rejectThrownErrors,
23 | truePromiseFn,
24 | voidPromise,
25 | } from "../promise/promise-compat";
26 | import { readText } from "./text";
27 | import { writeFallback } from "./write-fallback";
28 |
29 | export function write(data: ClipboardItemInterface[]): Promise {
30 | // Use the browser implementation if it exists.
31 | // TODO: detect `text/html`.
32 | return rejectThrownErrors((): Promise => {
33 | if (originalNavigatorClipboardWrite && originalWindowClipboardItem) {
34 | // TODO: This reference is a workaround for TypeScript inference.
35 | var originalNavigatorClipboardWriteCached =
36 | originalNavigatorClipboardWrite;
37 | debugLog("Using `navigator.clipboard.write()`.");
38 | return promiseConstructor
39 | .all(data.map(clipboardItemToGlobalClipboardItem))
40 | .then(
41 | (
42 | globalClipboardItems: ClipboardItemInterface[],
43 | ): Promise => {
44 | return originalNavigatorClipboardWriteCached(globalClipboardItems)
45 | .then(truePromiseFn)
46 | .catch((e: Error): Promise => {
47 | // Chrome 83 will throw a DOMException or NotAllowedError because it doesn't support e.g. `text/html`.
48 | // We want to fall back to the other strategies in a situation like this.
49 | // See https://github.com/w3c/clipboard-apis/issues/128 and https://github.com/w3c/clipboard-apis/issues/67
50 | if (
51 | !hasItemWithType(data, TEXT_PLAIN) &&
52 | !hasItemWithType(data, TEXT_HTML)
53 | ) {
54 | throw e;
55 | }
56 | return falsePromise;
57 | });
58 | },
59 | );
60 | }
61 | return falsePromise;
62 | }).then((success: boolean) => {
63 | if (success) {
64 | return voidPromise;
65 | }
66 |
67 | var hasTextPlain = hasItemWithType(data, TEXT_PLAIN);
68 | if (shouldShowWarnings() && !hasTextPlain) {
69 | debugLog(
70 | "clipboard.write() was called without a " +
71 | "`text/plain` data type. On some platforms, this may result in an " +
72 | "empty clipboard. Call suppressWarnings() " +
73 | "to suppress this warning.",
74 | );
75 | }
76 |
77 | return toStringItem(data[0]).then((stringItem: StringItem) => {
78 | if (!writeFallback(stringItem)) {
79 | throw new Error("write() failed");
80 | }
81 | });
82 | });
83 | }
84 |
85 | export function read(): Promise {
86 | return rejectThrownErrors(() => {
87 | // Use the browser implementation if it exists.
88 | if (originalNavigatorClipboardRead) {
89 | debugLog("Using `navigator.clipboard.read()`.");
90 | return originalNavigatorClipboardRead();
91 | }
92 |
93 | // Fallback to reading text only.
94 | return readText().then((text: string) => {
95 | return [textToClipboardItem(text)];
96 | });
97 | });
98 | }
99 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/implementations/text.blank-document.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "bun:test";
2 |
3 | (globalThis as any).document = {};
4 |
5 | test("writeText(…) failure in an unsupported browser", async () => {
6 | const { writeText } = await import("./text");
7 |
8 | expect(async () => writeText("hello")).toThrowError(
9 | "document.addEventListener is not a function.",
10 | );
11 | });
12 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/implementations/text.blank-environment.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, mock, test } from "bun:test";
2 | import { setDebugLog } from "../debug";
3 |
4 | const consoleLogMock = mock(console.log);
5 | setDebugLog(consoleLogMock);
6 |
7 | test("writeText(…) failure in a blank environmnent", async () => {
8 | const { writeText } = await import("./text");
9 |
10 | expect(async () => writeText("hello")).toThrowError(ReferenceError);
11 | expect(consoleLogMock).toHaveBeenCalledTimes(0);
12 | });
13 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/implementations/text.execCommand-fallback.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "bun:test";
2 | import {
3 | createDebugLogConsoleMock,
4 | createDocumentMock,
5 | } from "../../test/mocks";
6 |
7 | const debugLogConsoleMock = createDebugLogConsoleMock();
8 | const { documentMock, eventMock } = createDocumentMock();
9 |
10 | test("writeText(…) execCommand fallback in a modern browser", async () => {
11 | const { writeText } = await import("./text");
12 |
13 | expect(() => writeText("hello execCommand fallback")).not.toThrow();
14 |
15 | expect(debugLogConsoleMock.mock.calls).toEqual([
16 | ["listener called"],
17 | ["regular execCopy worked"],
18 | ]);
19 |
20 | expect(documentMock.execCommand.mock.calls).toEqual([["copy"]]);
21 |
22 | expect(documentMock.addEventListener).toHaveBeenCalledTimes(1);
23 | expect(documentMock.removeEventListener).toHaveBeenCalledTimes(1);
24 |
25 | expect(eventMock.preventDefault).toHaveBeenCalledTimes(1);
26 | expect(eventMock.clipboardData.setData.mock.calls).toEqual([
27 | ["text/plain", "hello execCommand fallback"],
28 | ]);
29 |
30 | expect(eventMock.clipboardData.getData.mock.calls).toEqual([["text/plain"]]);
31 | });
32 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/implementations/text.modern-browser-async-api-disabled.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "bun:test";
2 | import {
3 | createDebugLogConsoleMock,
4 | createDocumentMock,
5 | createWriteTextMock,
6 | } from "../../test/mocks";
7 |
8 | const debugLogConsoleMock = createDebugLogConsoleMock();
9 | const writeTextMock = createWriteTextMock(async () => {
10 | throw new Error("writeText(…) is disabled");
11 | });
12 | const { documentMock, eventMock } = createDocumentMock();
13 |
14 | // Regression test for https://github.com/lgarron/clipboard-polyfill/issues/167
15 | test("writeText(…) success in a modern browser with the async API disabled", async () => {
16 | const { writeText } = await import("./text");
17 |
18 | expect(() =>
19 | writeText("hello modern browser with async API disabled"),
20 | ).not.toThrow();
21 |
22 | expect(debugLogConsoleMock.mock.calls).toEqual([
23 | ["Using `navigator.clipboard.writeText()`."],
24 | ["listener called"],
25 | ["regular execCopy worked"],
26 | ]);
27 |
28 | expect(writeTextMock.mock.calls).toEqual([
29 | ["hello modern browser with async API disabled"],
30 | ]);
31 |
32 | expect(documentMock.execCommand.mock.calls).toEqual([["copy"]]);
33 |
34 | expect(documentMock.addEventListener).toHaveBeenCalledTimes(1);
35 | expect(documentMock.removeEventListener).toHaveBeenCalledTimes(1);
36 |
37 | expect(eventMock.preventDefault).toHaveBeenCalledTimes(1);
38 | expect(eventMock.clipboardData.setData.mock.calls).toEqual([
39 | ["text/plain", "hello modern browser with async API disabled"],
40 | ]);
41 |
42 | expect(eventMock.clipboardData.getData.mock.calls).toEqual([["text/plain"]]);
43 | });
44 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/implementations/text.modern-browser.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "bun:test";
2 | import {
3 | createDebugLogConsoleMock,
4 | createDocumentMock,
5 | createWriteTextMock,
6 | } from "../../test/mocks";
7 |
8 | const debugLogConsoleMock = createDebugLogConsoleMock();
9 | const writeTextMock = createWriteTextMock();
10 | const { documentMock } = createDocumentMock();
11 |
12 | test("writeText(…) success in a modern browser", async () => {
13 | const { writeText } = await import("./text");
14 |
15 | expect(() => writeText("hello modern browser")).not.toThrow();
16 |
17 | expect(debugLogConsoleMock.mock.calls).toEqual([
18 | ["Using `navigator.clipboard.writeText()`."],
19 | ]);
20 |
21 | expect(writeTextMock.mock.calls).toEqual([["hello modern browser"]]);
22 |
23 | expect(documentMock.execCommand).toHaveBeenCalledTimes(0);
24 | expect(documentMock.addEventListener).toHaveBeenCalledTimes(0);
25 | expect(documentMock.removeEventListener).toHaveBeenCalledTimes(0);
26 | });
27 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/implementations/text.ts:
--------------------------------------------------------------------------------
1 | import {
2 | originalNavigatorClipboardReadText,
3 | originalNavigatorClipboardWriteText,
4 | promiseConstructor,
5 | } from "../builtins/builtin-globals";
6 | import type { StringItem } from "../ClipboardItem/convert";
7 | import { TEXT_PLAIN } from "../ClipboardItem/data-types";
8 | import { debugLog } from "../debug";
9 | import { rejectThrownErrors } from "../promise/promise-compat";
10 | import { readTextIE, seemToBeInIE } from "../strategies/internet-explorer";
11 | import { writeFallback } from "./write-fallback";
12 |
13 | function stringToStringItem(s: string): StringItem {
14 | var stringItem: StringItem = {};
15 | stringItem[TEXT_PLAIN] = s;
16 | return stringItem;
17 | }
18 |
19 | export function writeText(s: string): Promise {
20 | // Use the browser implementation if it exists.
21 | if (originalNavigatorClipboardWriteText) {
22 | debugLog("Using `navigator.clipboard.writeText()`.");
23 | return originalNavigatorClipboardWriteText(s).catch(() =>
24 | writeTextStringFallbackPromise(s),
25 | );
26 | }
27 | return writeTextStringFallbackPromise(s);
28 | }
29 |
30 | function writeTextStringFallbackPromise(s: string): Promise {
31 | return rejectThrownErrors(() =>
32 | promiseConstructor.resolve(writeTextStringFallback(s)),
33 | );
34 | }
35 |
36 | function writeTextStringFallback(s: string): void {
37 | if (!writeFallback(stringToStringItem(s))) {
38 | throw new Error("writeText() failed");
39 | }
40 | }
41 |
42 | export function readText(): Promise {
43 | return rejectThrownErrors(() => {
44 | // Use the browser implementation if it exists.
45 | if (originalNavigatorClipboardReadText) {
46 | debugLog("Using `navigator.clipboard.readText()`.");
47 | return originalNavigatorClipboardReadText();
48 | }
49 |
50 | // Fallback for IE.
51 | if (seemToBeInIE()) {
52 | var result = readTextIE();
53 | return promiseConstructor.resolve(result);
54 | }
55 |
56 | throw new Error("Read is not supported in your browser.");
57 | });
58 | }
59 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/implementations/write-fallback.ts:
--------------------------------------------------------------------------------
1 | import type { StringItem } from "../ClipboardItem/convert";
2 | import { TEXT_PLAIN } from "../ClipboardItem/data-types";
3 | import { debugLog } from "../debug";
4 | import {
5 | copyTextUsingDOM,
6 | copyUsingTempElem,
7 | copyUsingTempSelection,
8 | execCopy,
9 | } from "../strategies/dom";
10 | import { seemToBeInIE, writeTextIE } from "../strategies/internet-explorer";
11 |
12 | // Note: the fallback order is carefully tuned for compatibility. It might seem
13 | // safe to move some of them around, but do not do so without testing all browsers.
14 | export function writeFallback(stringItem: StringItem): boolean {
15 | var hasTextPlain = TEXT_PLAIN in stringItem;
16 |
17 | // Internet Explorer
18 | if (seemToBeInIE()) {
19 | if (!hasTextPlain) {
20 | throw new Error("No `text/plain` value was specified.");
21 | }
22 | if (writeTextIE(stringItem[TEXT_PLAIN])) {
23 | return true;
24 | }
25 | throw new Error("Copying failed, possibly because the user rejected it.");
26 | }
27 |
28 | if (execCopy(stringItem)) {
29 | debugLog("regular execCopy worked");
30 | return true;
31 | }
32 |
33 | // Success detection on Edge is not possible, due to bugs in all 4
34 | // detection mechanisms we could try to use. Assume success.
35 | if (navigator.userAgent.indexOf("Edge") > -1) {
36 | debugLog('UA "Edge" => assuming success');
37 | return true;
38 | }
39 |
40 | // Fallback 1 for desktop Safari.
41 | if (copyUsingTempSelection(document.body, stringItem)) {
42 | debugLog("copyUsingTempSelection worked");
43 | return true;
44 | }
45 |
46 | // Fallback 2 for desktop Safari.
47 | if (copyUsingTempElem(stringItem)) {
48 | debugLog("copyUsingTempElem worked");
49 | return true;
50 | }
51 |
52 | // Fallback for iOS Safari.
53 | if (copyTextUsingDOM(stringItem[TEXT_PLAIN])) {
54 | debugLog("copyTextUsingDOM worked");
55 | return true;
56 | }
57 |
58 | return false;
59 | }
60 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/promise/es6-promise.d.ts:
--------------------------------------------------------------------------------
1 | // TypeScript's ES6 definition for the `Promise` constructor, without the globally available variable.
2 | // Note that ES2015 is the same as ES6: https://262.ecma-international.org/6.0/
3 |
4 | export interface PromiseConstructor {
5 | /**
6 | * A reference to the prototype.
7 | */
8 | readonly prototype: Promise;
9 |
10 | /**
11 | * Creates a new Promise.
12 | * @param executor A callback used to initialize the promise. This callback is passed two arguments:
13 | * a resolve callback used to resolve the promise with a value or the result of another promise,
14 | * and a reject callback used to reject the promise with a provided reason or error.
15 | */
16 | new (
17 | executor: (
18 | resolve: (value: T | PromiseLike) => void,
19 | reject: (reason?: any) => void,
20 | ) => void,
21 | ): Promise;
22 |
23 | /**
24 | * Creates a Promise that is resolved with an array of results when all of the provided Promises
25 | * resolve, or rejected when any Promise is rejected.
26 | * @param values An array of Promises.
27 | * @returns A new Promise.
28 | */
29 | all(
30 | values: T,
31 | ): Promise<{
32 | -readonly [P in keyof T]: Awaited;
33 | }>;
34 |
35 | // see: lib.es2015.iterable.d.ts
36 | // all(values: Iterable>): Promise[]>;
37 |
38 | /**
39 | * Creates a Promise that is resolved or rejected when any of the provided Promises are resolved
40 | * or rejected.
41 | * @param values An array of Promises.
42 | * @returns A new Promise.
43 | */
44 | race(
45 | values: T,
46 | ): Promise>;
47 |
48 | // see: lib.es2015.iterable.d.ts
49 | // race(values: Iterable>): Promise>;
50 |
51 | /**
52 | * Creates a new rejected promise for the provided reason.
53 | * @param reason The reason the promise was rejected.
54 | * @returns A new rejected Promise.
55 | */
56 | reject(reason?: any): Promise;
57 |
58 | /**
59 | * Creates a new resolved promise.
60 | * @returns A resolved promise.
61 | */
62 | resolve(): Promise;
63 | /**
64 | * Creates a new resolved promise for the provided value.
65 | * @param value A promise.
66 | * @returns A promise whose internal state matches the provided promise.
67 | */
68 | resolve(value: T): Promise>;
69 | /**
70 | * Creates a new resolved promise for the provided value.
71 | * @param value A promise.
72 | * @returns A promise whose internal state matches the provided promise.
73 | */
74 | resolve(value: T | PromiseLike): Promise>;
75 | }
76 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/promise/polyfill.ts:
--------------------------------------------------------------------------------
1 | // biome-ignore-all lint/complexity/useArrowFunction: Vendored code.
2 |
3 | import type { PromiseConstructor } from "./es6-promise";
4 |
5 | /**
6 | * @this {PromisePolyfill}
7 | */
8 | function finallyConstructor(callback) {
9 | var thisConstructor = this.constructor;
10 | return this.then(
11 | function (value) {
12 | return thisConstructor.resolve(callback()).then(() => {
13 | return value;
14 | });
15 | },
16 | function (reason) {
17 | return thisConstructor.resolve(callback()).then(() => {
18 | return thisConstructor.reject(reason);
19 | });
20 | },
21 | );
22 | }
23 |
24 | function allSettled(arr) {
25 | // biome-ignore lint/complexity/noUselessThisAlias: Vendored code.
26 | var P = this;
27 | return new P(function (resolve, reject) {
28 | if (!(arr && typeof arr.length !== "undefined")) {
29 | return reject(
30 | new TypeError(
31 | typeof arr +
32 | " " +
33 | arr +
34 | " is not iterable(cannot read property Symbol(Symbol.iterator))",
35 | ),
36 | );
37 | }
38 | var args = Array.prototype.slice.call(arr);
39 | if (args.length === 0) return resolve([]);
40 | var remaining = args.length;
41 |
42 | function res(i, val) {
43 | if (val && (typeof val === "object" || typeof val === "function")) {
44 | var then = val.then;
45 | if (typeof then === "function") {
46 | then.call(
47 | val,
48 | function (val) {
49 | res(i, val);
50 | },
51 | function (e) {
52 | args[i] = { status: "rejected", reason: e };
53 | if (--remaining === 0) {
54 | resolve(args);
55 | }
56 | },
57 | );
58 | return;
59 | }
60 | }
61 | args[i] = { status: "fulfilled", value: val };
62 | if (--remaining === 0) {
63 | resolve(args);
64 | }
65 | }
66 |
67 | for (var i = 0; i < args.length; i++) {
68 | res(i, args[i]);
69 | }
70 | });
71 | }
72 |
73 | // Store setTimeout reference so promise-polyfill will be unaffected by
74 | // other code modifying setTimeout (like sinon.useFakeTimers())
75 | var setTimeoutFunc = setTimeout;
76 |
77 | function isArray(x) {
78 | return Boolean(x && typeof x.length !== "undefined");
79 | }
80 |
81 | function noop() {}
82 |
83 | // Polyfill for Function.prototype.bind
84 | function bind(fn, thisArg) {
85 | return function () {
86 | // biome-ignore lint/style/noArguments: Vendored code.
87 | fn.apply(thisArg, arguments);
88 | };
89 | }
90 |
91 | /**
92 | * @constructor
93 | * @param {Function} fn
94 | */
95 |
96 | export function PromisePolyfill(fn) {
97 | if (!(this instanceof PromisePolyfill))
98 | throw new TypeError("Promises must be constructed via new");
99 | if (typeof fn !== "function") throw new TypeError("not a function");
100 | /** @type {!number} */
101 | this._state = 0;
102 | /** @type {!boolean} */
103 | this._handled = false;
104 | /** @type {PromisePolyfill|undefined} */
105 | this._value = undefined;
106 | /** @type {!Array} */
107 | this._deferreds = [];
108 |
109 | doResolve(fn, this);
110 | }
111 |
112 | function handle(self, deferred) {
113 | while (self._state === 3) {
114 | self = self._value;
115 | }
116 | if (self._state === 0) {
117 | self._deferreds.push(deferred);
118 | return;
119 | }
120 | self._handled = true;
121 | PromisePolyfill._immediateFn(function () {
122 | var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
123 | if (cb === null) {
124 | (self._state === 1 ? resolve : reject)(deferred.promise, self._value);
125 | return;
126 | }
127 | // biome-ignore lint/suspicious/noImplicitAnyLet: Vendored code.
128 | var ret;
129 | try {
130 | ret = cb(self._value);
131 | } catch (e) {
132 | reject(deferred.promise, e);
133 | return;
134 | }
135 | resolve(deferred.promise, ret);
136 | });
137 | }
138 |
139 | function resolve(self, newValue) {
140 | try {
141 | // Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure
142 | if (newValue === self)
143 | throw new TypeError("A promise cannot be resolved with itself.");
144 | if (
145 | newValue &&
146 | (typeof newValue === "object" || typeof newValue === "function")
147 | ) {
148 | var then = newValue.then;
149 | if (newValue instanceof PromisePolyfill) {
150 | self._state = 3;
151 | self._value = newValue;
152 | finale(self);
153 | return;
154 | } else if (typeof then === "function") {
155 | doResolve(bind(then, newValue), self);
156 | return;
157 | }
158 | }
159 | self._state = 1;
160 | self._value = newValue;
161 | finale(self);
162 | } catch (e) {
163 | reject(self, e);
164 | }
165 | }
166 |
167 | function reject(self, newValue) {
168 | self._state = 2;
169 | self._value = newValue;
170 | finale(self);
171 | }
172 |
173 | function finale(self) {
174 | if (self._state === 2 && self._deferreds.length === 0) {
175 | PromisePolyfill._immediateFn(function () {
176 | if (!self._handled) {
177 | PromisePolyfill._unhandledRejectionFn(self._value);
178 | }
179 | });
180 | }
181 |
182 | for (var i = 0, len = self._deferreds.length; i < len; i++) {
183 | handle(self, self._deferreds[i]);
184 | }
185 | self._deferreds = null;
186 | }
187 |
188 | /**
189 | * @constructor
190 | */
191 | function Handler(onFulfilled, onRejected, promise) {
192 | this.onFulfilled = typeof onFulfilled === "function" ? onFulfilled : null;
193 | this.onRejected = typeof onRejected === "function" ? onRejected : null;
194 | this.promise = promise;
195 | }
196 |
197 | /**
198 | * Take a potentially misbehaving resolver function and make sure
199 | * onFulfilled and onRejected are only called once.
200 | *
201 | * Makes no guarantees about asynchrony.
202 | */
203 | function doResolve(fn, self) {
204 | var done = false;
205 | try {
206 | fn(
207 | function (value) {
208 | if (done) return;
209 | done = true;
210 | resolve(self, value);
211 | },
212 | function (reason) {
213 | if (done) return;
214 | done = true;
215 | reject(self, reason);
216 | },
217 | );
218 | } catch (ex) {
219 | if (done) return;
220 | done = true;
221 | reject(self, ex);
222 | }
223 | }
224 |
225 | PromisePolyfill.prototype["catch"] = function (onRejected) {
226 | return this.then(null, onRejected);
227 | };
228 |
229 | // biome-ignore lint/suspicious/noThenProperty: This is specifically implementing the `Promise` API.
230 | PromisePolyfill.prototype.then = function (onFulfilled, onRejected) {
231 | // @ts-ignore
232 | var prom = new this.constructor(noop);
233 |
234 | handle(this, new Handler(onFulfilled, onRejected, prom));
235 | return prom;
236 | };
237 |
238 | PromisePolyfill.prototype["finally"] = finallyConstructor;
239 |
240 | PromisePolyfill.all = function (arr) {
241 | return new PromisePolyfill(function (resolve, reject) {
242 | if (!isArray(arr)) {
243 | return reject(new TypeError("Promise.all accepts an array"));
244 | }
245 |
246 | var args = Array.prototype.slice.call(arr);
247 | if (args.length === 0) return resolve([]);
248 | var remaining = args.length;
249 |
250 | function res(i, val) {
251 | try {
252 | if (val && (typeof val === "object" || typeof val === "function")) {
253 | var then = val.then;
254 | if (typeof then === "function") {
255 | then.call(
256 | val,
257 | function (val) {
258 | res(i, val);
259 | },
260 | reject,
261 | );
262 | return;
263 | }
264 | }
265 | args[i] = val;
266 | if (--remaining === 0) {
267 | resolve(args);
268 | }
269 | } catch (ex) {
270 | reject(ex);
271 | }
272 | }
273 |
274 | for (var i = 0; i < args.length; i++) {
275 | res(i, args[i]);
276 | }
277 | });
278 | };
279 |
280 | PromisePolyfill.allSettled = allSettled;
281 |
282 | PromisePolyfill.resolve = function (value) {
283 | if (
284 | value &&
285 | typeof value === "object" &&
286 | value.constructor === PromisePolyfill
287 | ) {
288 | return value;
289 | }
290 |
291 | return new PromisePolyfill(function (resolve) {
292 | resolve(value);
293 | });
294 | };
295 |
296 | PromisePolyfill.reject = function (value) {
297 | return new PromisePolyfill(function (_resolve, reject) {
298 | reject(value);
299 | });
300 | };
301 |
302 | PromisePolyfill.race = function (arr) {
303 | return new PromisePolyfill(function (resolve, reject) {
304 | if (!isArray(arr)) {
305 | return reject(new TypeError("Promise.race accepts an array"));
306 | }
307 |
308 | for (var i = 0, len = arr.length; i < len; i++) {
309 | PromisePolyfill.resolve(arr[i]).then(resolve, reject);
310 | }
311 | });
312 | };
313 |
314 | // Use polyfill for setImmediate for performance gains
315 | PromisePolyfill._immediateFn =
316 | // @ts-ignore
317 | (typeof setImmediate === "function" &&
318 | function (fn) {
319 | // @ts-ignore
320 | setImmediate(fn);
321 | }) ||
322 | function (fn) {
323 | setTimeoutFunc(fn, 0);
324 | };
325 |
326 | PromisePolyfill._unhandledRejectionFn = function _unhandledRejectionFn(err) {
327 | if (typeof console !== "undefined" && console) {
328 | console.warn("Possible Unhandled Promise Rejection:", err); // eslint-disable-line no-console
329 | }
330 | };
331 |
332 | export var PromisePolyfillConstructor: PromiseConstructor =
333 | PromisePolyfill as any as PromiseConstructor;
334 |
335 | // Set the Promise polyfill before getting globals.
336 | import { setPromiseConstructor } from "../builtins/promise-constructor";
337 |
338 | setPromiseConstructor(PromisePolyfillConstructor);
339 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/promise/promise-compat.ts:
--------------------------------------------------------------------------------
1 | import { promiseConstructor } from "../builtins/builtin-globals";
2 |
3 | export function promiseRecordMap(
4 | keys: readonly string[],
5 | f: (key: string) => Promise,
6 | ): Promise> {
7 | var promiseList: Promise[] = [];
8 | for (var i = 0; i < keys.length; i++) {
9 | var key = keys[i];
10 | promiseList.push(f(key));
11 | }
12 | return promiseConstructor
13 | .all(promiseList)
14 | .then((vList: T[]): Record => {
15 | var dataOut: Record = {};
16 | for (var i = 0; i < keys.length; i++) {
17 | dataOut[keys[i]] = vList[i];
18 | }
19 | return dataOut;
20 | });
21 | }
22 |
23 | export var voidPromise: Promise = promiseConstructor.resolve();
24 | export var truePromiseFn: () => Promise = () =>
25 | promiseConstructor.resolve(true);
26 | export var falsePromise: Promise = promiseConstructor.resolve(false);
27 |
28 | export function rejectThrownErrors(executor: () => Promise): Promise {
29 | return new promiseConstructor((resolve, reject) => {
30 | try {
31 | resolve(executor());
32 | } catch (e) {
33 | reject(e);
34 | }
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/promise/set-promise-polyfill-if-needed.ts:
--------------------------------------------------------------------------------
1 | import { setPromiseConstructor } from "../builtins/promise-constructor";
2 | import { originalWindow } from "../builtins/window-globalThis";
3 | import { PromisePolyfillConstructor } from "./polyfill";
4 |
5 | originalWindow?.Promise || setPromiseConstructor(PromisePolyfillConstructor);
6 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/strategies/dom.ts:
--------------------------------------------------------------------------------
1 | import type { StringItem } from "../ClipboardItem/convert";
2 | import { TEXT_PLAIN } from "../ClipboardItem/data-types";
3 | import { debugLog } from "../debug";
4 |
5 | /******** Implementations ********/
6 |
7 | interface FallbackTracker {
8 | success: boolean;
9 | }
10 |
11 | function copyListener(
12 | tracker: FallbackTracker,
13 | data: StringItem,
14 | e: ClipboardEvent,
15 | ): void {
16 | debugLog("listener called");
17 | tracker.success = true;
18 | // tslint:disable-next-line: forin
19 | for (var type in data) {
20 | var value = data[type];
21 |
22 | // biome-ignore lint/style/noNonNullAssertion: We assume this field is present.
23 | var clipboardData = e.clipboardData!;
24 | clipboardData.setData(type, value);
25 | if (type === TEXT_PLAIN && clipboardData.getData(type) !== value) {
26 | debugLog("setting text/plain failed");
27 | tracker.success = false;
28 | }
29 | }
30 | e.preventDefault();
31 | }
32 |
33 | export function execCopy(data: StringItem): boolean {
34 | var tracker: FallbackTracker = { success: false };
35 | var listener = copyListener.bind(this, tracker, data);
36 |
37 | document.addEventListener("copy", listener);
38 | try {
39 | // We ignore the return value, since FallbackTracker tells us whether the
40 | // listener was called. It seems that checking the return value here gives
41 | // us no extra information in any browser.
42 | document.execCommand("copy");
43 | } finally {
44 | document.removeEventListener("copy", listener);
45 | }
46 | return tracker.success;
47 | }
48 |
49 | // Temporarily select a DOM element, so that `execCommand()` is not rejected.
50 | export function copyUsingTempSelection(
51 | e: HTMLElement,
52 | data: StringItem,
53 | ): boolean {
54 | selectionSet(e);
55 | var success = execCopy(data);
56 | selectionClear();
57 | return success;
58 | }
59 |
60 | // Create a temporary DOM element to select, so that `execCommand()` is not
61 | // rejected.
62 | export function copyUsingTempElem(data: StringItem): boolean {
63 | var tempElem = document.createElement("div");
64 | // Setting an individual property does not support `!important`, so we set the
65 | // whole style instead of just the `-webkit-user-select` property.
66 | tempElem.setAttribute("style", "-webkit-user-select: text !important");
67 | // Place some text in the elem so that Safari has something to select.
68 | tempElem.textContent = "temporary element";
69 | document.body.appendChild(tempElem);
70 |
71 | var success = copyUsingTempSelection(tempElem, data);
72 |
73 | document.body.removeChild(tempElem);
74 | return success;
75 | }
76 |
77 | // Uses shadow DOM.
78 | export function copyTextUsingDOM(str: string): boolean {
79 | debugLog("copyTextUsingDOM");
80 |
81 | var tempElem = document.createElement("div");
82 | // Setting an individual property does not support `!important`, so we set the
83 | // whole style instead of just the `-webkit-user-select` property.
84 | tempElem.setAttribute("style", "-webkit-user-select: text !important");
85 | // Use shadow DOM if available.
86 | var spanParent: Node = tempElem;
87 | if (tempElem.attachShadow) {
88 | debugLog("Using shadow DOM.");
89 | spanParent = tempElem.attachShadow({ mode: "open" });
90 | }
91 |
92 | var span = document.createElement("span");
93 | span.innerText = str;
94 |
95 | spanParent.appendChild(span);
96 | document.body.appendChild(tempElem);
97 | selectionSet(span);
98 |
99 | var result = document.execCommand("copy");
100 |
101 | selectionClear();
102 | document.body.removeChild(tempElem);
103 |
104 | return result;
105 | }
106 |
107 | /******** Selection ********/
108 |
109 | function selectionSet(elem: Element): void {
110 | var sel = document.getSelection();
111 | if (sel) {
112 | var range = document.createRange();
113 | range.selectNodeContents(elem);
114 | sel.removeAllRanges();
115 | sel.addRange(range);
116 | }
117 | }
118 |
119 | function selectionClear(): void {
120 | var sel = document.getSelection();
121 | if (sel) {
122 | sel.removeAllRanges();
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/clipboard-polyfill/strategies/internet-explorer.ts:
--------------------------------------------------------------------------------
1 | import { originalWindow } from "../builtins/window-globalThis";
2 | import { debugLog } from "../debug";
3 |
4 | interface IEWindow extends Window {
5 | clipboardData?: {
6 | setData: (key: string, value: string) => boolean;
7 | // Always results in a string: https://msdn.microsoft.com/en-us/library/ms536436(v=vs.85).aspx
8 | getData: (key: string) => string;
9 | };
10 | }
11 |
12 | var ieWindow = originalWindow as IEWindow;
13 |
14 | export function seemToBeInIE(): boolean {
15 | return (
16 | typeof ClipboardEvent === "undefined" &&
17 | typeof ieWindow?.clipboardData !== "undefined" &&
18 | typeof ieWindow?.clipboardData.setData !== "undefined"
19 | );
20 | }
21 |
22 | export function writeTextIE(text: string): boolean {
23 | if (!ieWindow.clipboardData) {
24 | return false;
25 | }
26 | // IE supports text or URL, but not HTML: https://msdn.microsoft.com/en-us/library/ms536744(v=vs.85).aspx
27 | // TODO: Write URLs to `text/uri-list`? https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types
28 | var success = ieWindow.clipboardData.setData("Text", text);
29 | if (success) {
30 | debugLog("writeTextIE worked");
31 | }
32 | return success;
33 | }
34 |
35 | // Returns "" if the read failed, e.g. because the user rejected the permission.
36 | export function readTextIE(): string {
37 | if (!ieWindow.clipboardData) {
38 | throw new Error("Cannot read IE clipboard Data ");
39 | }
40 | var text = ieWindow.clipboardData.getData("Text");
41 | if (text === "") {
42 | throw new Error(
43 | "Empty clipboard or could not read plain text from clipboard",
44 | );
45 | }
46 | return text;
47 | }
48 |
--------------------------------------------------------------------------------
/src/demo/clipboard-polyfill-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/demo/demo.ts:
--------------------------------------------------------------------------------
1 | import "../clipboard-polyfill/entries/es5/window-var.promise";
2 |
--------------------------------------------------------------------------------
/src/demo/index.css:
--------------------------------------------------------------------------------
1 | html {
2 | font-family: -apple-system, Roboto, Ubuntu, Tahoma, sans-serif;
3 | font-size: 1.25rem;
4 | padding: 2em;
5 | display: grid;
6 | justify-content: center;
7 | }
8 |
9 | body {
10 | width: 100%;
11 | max-width: 40em;
12 | margin: 0;
13 | }
14 |
15 | @media (prefers-color-scheme: dark) {
16 | html {
17 | background: #000d;
18 | color: #eee;
19 | }
20 |
21 | a {
22 | color: #669df5;
23 | }
24 |
25 | a:visited {
26 | color: #af73d5;
27 | }
28 | }
29 |
30 | header {
31 | text-align: center;
32 | }
33 |
34 | td {
35 | padding: 0.5em;
36 | vertical-align: top;
37 | }
38 |
39 | table td:first-child {
40 | text-align: right;
41 | }
42 |
--------------------------------------------------------------------------------
/src/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | clipboard-polyfill
7 |
8 |
9 |
10 |
11 |
53 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | clipboard-polyfill
73 |
74 | Demo/test page
75 |
76 |
77 |
78 |
79 |
80 |
81 | Plain text
82 |
83 |
84 |
85 |
86 |
87 |
88 |
93 |
94 |
95 |
96 |
97 | Markup
98 |
99 |
100 |
101 |
102 |
103 |
104 |
114 |
115 |
116 |
117 |
118 | DOM node
119 |
120 |
121 |
122 |
123 | This
125 | will be copied.
126 |
127 |
128 |
129 |
141 |
142 |
143 |
144 |
145 | Image (PNG)
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
162 |
163 |
164 |
165 |
166 | Paste text
167 |
168 |
169 |
170 |
171 |
172 |
173 |
178 |
179 |
180 |
181 |
182 | Paste area
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
--------------------------------------------------------------------------------
/src/demo/readme-examples/main.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/demo/readme-examples/main.ts:
--------------------------------------------------------------------------------
1 | import * as clipboard from "../../clipboard-polyfill/entries/es6/clipboard-polyfill.es6";
2 |
3 | function handler() {
4 | clipboard.writeText("This text is plain.").then(
5 | () => {
6 | console.log("success!");
7 | },
8 | () => {
9 | console.log("error!");
10 | },
11 | );
12 | }
13 |
14 | window.addEventListener("DOMContentLoaded", () => {
15 | var button = document.body.appendChild(document.createElement("button"));
16 | button.textContent = "Copy";
17 | button.addEventListener("click", handler);
18 | });
19 |
--------------------------------------------------------------------------------
/src/mock-test/missing-Promise.ts:
--------------------------------------------------------------------------------
1 | const importPromise = import(
2 | "../clipboard-polyfill/entries/es6/clipboard-polyfill.es6"
3 | );
4 |
5 | const globalPromise = globalThis.Promise;
6 | // @ts-ignore: We're deleting something that's not normally meant to be deleted.
7 | delete globalThis.Promise;
8 |
9 | let caughtError: Error | undefined;
10 | try {
11 | await importPromise;
12 | } catch (e) {
13 | caughtError = e;
14 | }
15 |
16 | globalThis.Promise = globalPromise;
17 |
18 | import { equal } from "node:assert";
19 |
20 | // TODO: Use `match` once `node` v19.4 is available from Homebrew: https://nodejs.org/api/assert.html#assertmatchstring-regexp-messageg
21 | equal(
22 | // biome-ignore lint/style/noNonNullAssertion: Error must be caught.
23 | !!caughtError!.message.match(/No `Promise` implementation available/),
24 | true,
25 | );
26 |
--------------------------------------------------------------------------------
/src/mock-test/modern-writeText.ts:
--------------------------------------------------------------------------------
1 | import { equal } from "node:assert";
2 | import type { ClipboardWithoutEventTarget } from "../clipboard-polyfill/ClipboardItem/spec";
3 |
4 | const mockStringClipboard = new (class MockStringClipboard {
5 | value: string = "";
6 | setText(s: string) {
7 | this.value = s;
8 | }
9 | getText(): string {
10 | return this.value;
11 | }
12 | })();
13 |
14 | function unimplemented(): any {
15 | throw new Error("unimplemented");
16 | }
17 |
18 | globalThis.navigator ??= {} as any;
19 | (globalThis.navigator as any).clipboard ??= {
20 | writeText: async (s) => mockStringClipboard.setText(s),
21 | readText: async (): Promise => mockStringClipboard.getText(),
22 | read: unimplemented,
23 | write: unimplemented,
24 | } satisfies ClipboardWithoutEventTarget;
25 |
26 | // This needs to happen after the mocks are set up.
27 | const { readText, writeText } = await import(
28 | "../clipboard-polyfill/entries/es6/clipboard-polyfill.es6"
29 | );
30 |
31 | mockStringClipboard.setText("hello world");
32 | equal("hello world", await readText());
33 | await writeText("new text");
34 | equal("new text", await readText());
35 |
--------------------------------------------------------------------------------
/src/test/bun-test-cannot-run-all-tests.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "bun:test";
2 |
3 | test("`bun test` must be run one file at a time", () => {
4 | console.error(
5 | "\n\n\n\n⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️\n\n\n\n[clipboard-polyfill] Each test file requires a fresh global environment before importing library code. Run `make test-bun` to run test files one at a time instead.\n\n\n\n⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️\n\n\n\n",
6 | );
7 | expect(true).toBe(false);
8 | });
9 |
--------------------------------------------------------------------------------
/src/test/mocks.ts:
--------------------------------------------------------------------------------
1 | import { type Mock, mock } from "bun:test";
2 | import { setDebugLog } from "../clipboard-polyfill/debug";
3 |
4 | const emptyFunction = () => {};
5 | const asyncEmptyFunction = async () => {};
6 |
7 | export function createDebugLogConsoleMock(): Mock {
8 | const consoleLogMock = mock(console.log);
9 | setDebugLog(consoleLogMock);
10 | return consoleLogMock;
11 | }
12 |
13 | interface DocumentMock {
14 | addEventListener: Mock;
15 | removeEventListener: Mock;
16 | execCommand: Mock;
17 | }
18 |
19 | function assertEventNameOrCommandIsCopy(eventName: string) {
20 | if (eventName !== "copy") {
21 | throw new Error("Unexpected event name or command.");
22 | }
23 | }
24 |
25 | // TODO: Full return type.
26 | export function createDocumentMock(): {
27 | documentMock: DocumentMock;
28 | eventMock: {
29 | clipboardData: { setData: Mock; getData: Mock };
30 | preventDefault: Mock;
31 | };
32 | } {
33 | const listeners: Set = new Set(); // TODO
34 |
35 | // TODO: mock `DataTransfer`
36 | let textPlain: string | undefined;
37 | const eventMock = {
38 | clipboardData: {
39 | setData: mock((type: string, value: string) => {
40 | if (type !== "text/plain") {
41 | throw new Error("Unexpected type");
42 | }
43 | textPlain = value;
44 | }),
45 | getData: mock((type: string) => {
46 | if (type !== "text/plain") {
47 | throw new Error("Unexpected type");
48 | }
49 | return textPlain;
50 | }),
51 | },
52 | preventDefault: mock(emptyFunction), // TODO: Expose this to test that it gets called exactly once.
53 | };
54 |
55 | const documentMock = {
56 | addEventListener: mock((eventName, listener) => {
57 | assertEventNameOrCommandIsCopy(eventName);
58 | listeners.add(listener);
59 | }),
60 | removeEventListener: mock((eventName, listener) => {
61 | assertEventNameOrCommandIsCopy(eventName);
62 | listeners.delete(listener);
63 | }),
64 | execCommand: mock((command) => {
65 | assertEventNameOrCommandIsCopy(command);
66 | for (const listener of listeners) {
67 | listener(eventMock);
68 | }
69 | }),
70 | };
71 | (globalThis as any).document = documentMock;
72 | return { documentMock, eventMock };
73 | }
74 |
75 | export function createWriteTextMock(
76 | executor: (s: string) => Promise = asyncEmptyFunction,
77 | ): Mock {
78 | // biome-ignore lint/suspicious/noAssignInExpressions: DRY pattern.
79 | const navigatorMock = ((globalThis as any).navigator ??= {});
80 | // biome-ignore lint/suspicious/noAssignInExpressions: DRY pattern.
81 | const clipboardMock = (navigatorMock.clipboard ??= {});
82 | const writeTextMock = mock(executor);
83 | clipboardMock.writeText = writeTextMock;
84 | return writeTextMock;
85 | }
86 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["es5", "dom"],
4 | "strictNullChecks": true,
5 | "declaration": true,
6 | "emitDeclarationOnly": true,
7 | "outDir": "dist/types",
8 | "skipLibCheck": true
9 | },
10 | "include": ["./src/clipboard-polyfill/entries"]
11 | }
12 |
--------------------------------------------------------------------------------