diff --git a/.eslintrc.json b/.eslintrc.json
index 88d626d..65f8018 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -18,7 +18,8 @@
},
"globals": {
"chai": false,
- "expect": false
+ "expect": false,
+ "globalThis": false
},
"env": {
"mocha": true
diff --git a/docs/index.html b/docs/index.html
index fe525f8..3644e39 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -116,6 +116,7 @@
tr[data-transpiled] ~ tr td[data-code] ~ [data-supported="false"] div,
tr[data-transpiled] ~ tr td[data-code][data-supported="false"] div,
caption p span.polyfilled,
+ caption p span.buggy,
caption p span.transpiled {
background-color: #ddf4ff;
color: #24292f;
@@ -153,6 +154,8 @@
GitHub Feature Support Table
!Required feature, not available in this browser.
*Not avaible in this browser, but polyfilled using this library.
+
+ †Required feature, but polyfilled to smooth over bugs in this browser.
**Not available in this browser, but transpiled to a compatible syntax.
@@ -523,6 +526,20 @@ GitHub Feature Support Table
78+ |
16.0+ |
+
+
+
+ ClipboardItem
+
+ |
+ * |
+ 66+ † |
+ 79+ † |
+ * |
+ 13.1+ |
+ 53+ † |
+ 9.0+ † |
+
@@ -565,6 +582,20 @@ GitHub Feature Support Table
| 76+ |
15.0+ |
+
+
+
+ navigator.clipboard
+
+ |
+ * |
+ 86+ |
+ 79+ |
+ * |
+ 13.1+ |
+ 63+ † |
+ 12.0+ † |
+
diff --git a/src/clipboarditem.ts b/src/clipboarditem.ts
new file mode 100644
index 0000000..d478806
--- /dev/null
+++ b/src/clipboarditem.ts
@@ -0,0 +1,47 @@
+type ClipboardItems = Record>
+const records = new WeakMap()
+const presentationStyles = new WeakMap()
+export class ClipboardItem {
+ constructor(items: ClipboardItems, options: ClipboardItemOptions | undefined = {}) {
+ if (Object.keys(items).length === 0) throw new TypeError('Empty dictionary argument')
+ records.set(this, items)
+ presentationStyles.set(this, options.presentationStyle || 'unspecified')
+ }
+
+ get presentationStyle(): PresentationStyle {
+ return presentationStyles.get(this) || 'unspecified'
+ }
+
+ get types() {
+ return Object.freeze(Object.keys(records.get(this) || {}))
+ }
+
+ async getType(type: string): Promise {
+ const record = records.get(this)
+ if (record && type in record) {
+ const item = await record[type]!
+ if (typeof item === 'string') return new Blob([item], {type})
+ return item
+ }
+ throw new DOMException("Failed to execute 'getType' on 'ClipboardItem': The type was not found", 'NotFoundError')
+ }
+}
+
+export function isSupported(): boolean {
+ try {
+ new globalThis.ClipboardItem({'text/plain': Promise.resolve('')})
+ return true
+ } catch {
+ return false
+ }
+}
+
+export function isPolyfilled(): boolean {
+ return globalThis.ClipboardItem === ClipboardItem
+}
+
+export function apply(): void {
+ if (!isSupported()) {
+ globalThis.ClipboardItem = ClipboardItem
+ }
+}
diff --git a/src/index.ts b/src/index.ts
index 3632807..7076201 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -2,9 +2,11 @@ import * as abortSignalAbort from './abortsignal-abort.js'
import * as abortSignalTimeout from './abortsignal-timeout.js'
import * as aggregateError from './aggregateerror.js'
import * as arrayAt from './arraylike-at.js'
+import * as clipboardItem from './clipboarditem.js'
import * as cryptoRandomUUID from './crypto-randomuuid.js'
import * as elementReplaceChildren from './element-replacechildren.js'
import * as eventAbortSignal from './event-abortsignal.js'
+import * as navigatorClipboard from './navigator-clipboard.js'
import * as formRequestSubmit from './form-requestsubmit.js'
import * as objectHasOwn from './object-hasown.js'
import * as promiseAllSettled from './promise-allsettled.js'
@@ -58,9 +60,11 @@ export const polyfills = {
abortSignalTimeout,
aggregateError,
arrayAt,
+ clipboardItem,
cryptoRandomUUID,
elementReplaceChildren,
eventAbortSignal,
+ navigatorClipboard,
formRequestSubmit,
objectHasOwn,
promiseAllSettled,
diff --git a/src/navigator-clipboard.ts b/src/navigator-clipboard.ts
new file mode 100644
index 0000000..2be7e7d
--- /dev/null
+++ b/src/navigator-clipboard.ts
@@ -0,0 +1,26 @@
+export async function clipboardWrite(data: ClipboardItems) {
+ if (data.length === 0) return
+ const item = data[0]
+ const blob = await item.getType(item.types.includes('text/plain') ? 'text/plain' : item.types[0])
+ return navigator.clipboard.writeText(typeof blob == 'string' ? blob : await blob.text())
+}
+
+export async function clipboardRead() {
+ const str = navigator.clipboard.readText()
+ return [new ClipboardItem({'text/plain': str})]
+}
+
+export function isSupported(): boolean {
+ return typeof navigator.clipboard.read === 'function' && typeof navigator.clipboard.write === 'function'
+}
+
+export function isPolyfilled(): boolean {
+ return navigator.clipboard.write === clipboardWrite || navigator.clipboard.read === clipboardRead
+}
+
+export function apply(): void {
+ if (!isSupported()) {
+ navigator.clipboard.write = clipboardWrite
+ navigator.clipboard.read = clipboardRead
+ }
+}
diff --git a/test/clipboarditem.js b/test/clipboarditem.js
new file mode 100644
index 0000000..d12cb32
--- /dev/null
+++ b/test/clipboarditem.js
@@ -0,0 +1,17 @@
+import {ClipboardItem, apply, isSupported, isPolyfilled} from '../lib/clipboarditem.js'
+
+describe('ClipboardItem', () => {
+ it('has standard isSupported, isPolyfilled, apply API', () => {
+ expect(isSupported).to.be.a('function')
+ expect(isPolyfilled).to.be.a('function')
+ expect(apply).to.be.a('function')
+ expect(isSupported()).to.be.a('boolean')
+ expect(isPolyfilled()).to.equal(false)
+ })
+
+ it('takes a Promise type, that can resolve', async () => {
+ const c = new ClipboardItem({'text/plain': Promise.resolve('hi')})
+ expect(c.types).to.eql(['text/plain'])
+ expect(await c.getType('text/plain')).to.be.instanceof(Blob)
+ })
+})
diff --git a/test/navigator-clipboard.js b/test/navigator-clipboard.js
new file mode 100644
index 0000000..aebb525
--- /dev/null
+++ b/test/navigator-clipboard.js
@@ -0,0 +1,54 @@
+import {clipboardRead, clipboardWrite, apply, isPolyfilled, isSupported} from '../lib/navigator-clipboard.js'
+
+describe('navigator clipboard', () => {
+ it('has standard isSupported, isPolyfilled, apply API', () => {
+ expect(isSupported).to.be.a('function')
+ expect(isPolyfilled).to.be.a('function')
+ expect(apply).to.be.a('function')
+ expect(isSupported()).to.be.a('boolean')
+ expect(isPolyfilled()).to.equal(false)
+ })
+
+ describe('read', () => {
+ it('read returns array of 1 clipboard entry with plaintext of readText value', async () => {
+ navigator.clipboard.readText = () => Promise.resolve('foo')
+ const arr = await clipboardRead()
+ expect(arr).to.have.lengthOf(1)
+ expect(arr[0]).to.be.an.instanceof(globalThis.ClipboardItem)
+ expect(arr[0].types).to.eql(['text/plain'])
+ expect(await arr[0].getType('text/plain')).to.eql('foo')
+ })
+ })
+
+ describe('write', () => {
+ it('unpacks text/plain content to writeText', async () => {
+ const calls = []
+ navigator.clipboard.writeText = (...args) => calls.push(args)
+ await clipboardWrite([
+ new globalThis.ClipboardItem({
+ 'foo/bar': 'horrible',
+ 'text/plain': Promise.resolve('foo')
+ })
+ ])
+ expect(calls).to.have.lengthOf(1)
+ expect(calls[0]).to.eql(['foo'])
+ })
+
+ it('accepts multiple clipboard items, picking the first', async () => {
+ const calls = []
+ navigator.clipboard.writeText = (...args) => calls.push(args)
+ await clipboardWrite([
+ new globalThis.ClipboardItem({
+ 'foo/bar': 'horrible',
+ 'text/plain': Promise.resolve('multiple-pass')
+ }),
+ new globalThis.ClipboardItem({
+ 'foo/bar': 'multiple-fail',
+ 'text/plain': Promise.resolve('multiple-fail')
+ })
+ ])
+ expect(calls).to.have.lengthOf(1)
+ expect(calls[0]).to.eql(['multiple-pass'])
+ })
+ })
+})
|