Skip to content

Commit f054df8

Browse files
committed
add navigator clipboard, ClipboardItem
1 parent 4235ca1 commit f054df8

7 files changed

+183
-1
lines changed

.eslintrc.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
},
1919
"globals": {
2020
"chai": false,
21-
"expect": false
21+
"expect": false,
22+
"globalThis": false
2223
},
2324
"env": {
2425
"mocha": true

docs/index.html

+31
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
tr[data-transpiled] ~ tr td[data-code] ~ [data-supported="false"] div,
117117
tr[data-transpiled] ~ tr td[data-code][data-supported="false"] div,
118118
caption p span.polyfilled,
119+
caption p span.buggy,
119120
caption p span.transpiled {
120121
background-color: #ddf4ff;
121122
color: #24292f;
@@ -153,6 +154,8 @@ <h1>GitHub Feature Support Table</h1>
153154
<span class="unsupported">!</span>Required feature, not available in this browser.
154155
</p><p>
155156
<span class="polyfilled">*</span>Not avaible in this browser, but polyfilled using this library.
157+
</p><p>
158+
<span class="buggy"><small></small></span>Required feature, buy polyfilled to smooth over bugs in this browser.
156159
</p><p>
157160
<span class="transpiled">**</span>Not available in this browser, but transpiled to a compatible syntax.
158161
</p>
@@ -523,6 +526,20 @@ <h1>GitHub Feature Support Table</h1>
523526
<td data-supported="true"><div>78+</div></td>
524527
<td data-supported="true"><div>16.0+</div></td>
525528
</tr>
529+
<tr>
530+
<th>
531+
<a href="https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem">
532+
<code>ClipboardItem</code>
533+
</a>
534+
</th>
535+
<td data-polyfill="clipboardItem"><div>*</div></td>
536+
<td data-supported="true" title="Buggy implementation"><div>66+ †</div></td>
537+
<td data-supported="true" title="Buggy implementation"><div>79+ †</div></td>
538+
<td data-supported="false"><div>*</div></td>
539+
<td data-supported="true"><div>13.1+</div></td>
540+
<td data-supported="true" title="Buggy implementation"><div>53+ †</div></td>
541+
<td data-supported="true" title="Buggy implementation"><div>9.0+ †</div></td>
542+
</tr>
526543
<tr>
527544
<th>
528545
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID">
@@ -565,6 +582,20 @@ <h1>GitHub Feature Support Table</h1>
565582
<td data-supported="true"><div>76+</div></td>
566583
<td data-supported="true"><div>15.0+</div></td>
567584
</tr>
585+
<tr>
586+
<th>
587+
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard">
588+
<code>navigator.clipboard</code>
589+
</a>
590+
</th>
591+
<td data-polyfill="navigatorClipboard"><div>*</div></td>
592+
<td data-supported="true"><div>86+</div></td>
593+
<td data-supported="true"><div>79+</div></td>
594+
<td data-supported="false"><div>*</div></td>
595+
<td data-supported="true"><div>13.1+</div></td>
596+
<td data-supported="true"><div>63+ †</div></td>
597+
<td data-supported="true"><div>12.0+ †</div></td>
598+
</tr>
568599
<tr>
569600
<th>
570601
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn">

src/clipboarditem.ts

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const records = new WeakMap<ClipboardItem, Record<string, ClipboardItemDataType | PromiseLike<ClipboardItemDataType>>>()
2+
const presentationStyles = new WeakMap<ClipboardItem, PresentationStyle>()
3+
export class ClipboardItem {
4+
constructor(
5+
items: Record<string, ClipboardItemDataType | PromiseLike<ClipboardItemDataType>>,
6+
options: ClipboardItemOptions | undefined = {}
7+
) {
8+
if (Object.keys(items).length === 0) throw new TypeError('Empty dictionary argument')
9+
records.set(this, items)
10+
presentationStyles.set(this, options.presentationStyle || 'unspecified')
11+
}
12+
13+
get presentationStyle(): PresentationStyle {
14+
return presentationStyles.get(this) || 'unspecified'
15+
}
16+
17+
get types() {
18+
return Object.freeze(Object.keys(records.get(this) || {}))
19+
}
20+
21+
async getType(type: string): Promise<Blob> {
22+
const record = records.get(this)
23+
if (record && type in record) {
24+
const item = await record[type]!
25+
if (typeof item === 'string') return new Blob([item], {type})
26+
return item
27+
}
28+
throw new DOMException("Failed to execute 'getType' on 'ClipboardItem': The type was not found", 'NotFoundError')
29+
}
30+
}
31+
32+
export function isSupported(): boolean {
33+
try {
34+
new globalThis.ClipboardItem({'text/plain': Promise.resolve('')})
35+
return true
36+
} catch {
37+
return false
38+
}
39+
}
40+
41+
export function isPolyfilled(): boolean {
42+
return globalThis.ClipboardItem === ClipboardItem
43+
}
44+
45+
export function apply(): void {
46+
if (!isSupported()) {
47+
globalThis.ClipboardItem = ClipboardItem
48+
}
49+
}

src/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import * as abortSignalAbort from './abortsignal-abort.js'
22
import * as abortSignalTimeout from './abortsignal-timeout.js'
33
import * as aggregateError from './aggregateerror.js'
44
import * as arrayAt from './arraylike-at.js'
5+
import * as clipboardItem from './clipboarditem.js'
56
import * as cryptoRandomUUID from './crypto-randomuuid.js'
67
import * as elementReplaceChildren from './element-replacechildren.js'
78
import * as eventAbortSignal from './event-abortsignal.js'
9+
import * as navigatorClipboard from './navigator-clipboard.js'
810
import * as objectHasOwn from './object-hasown.js'
911
import * as promiseAllSettled from './promise-allsettled.js'
1012
import * as promiseAny from './promise-any.js'
@@ -57,9 +59,11 @@ export const polyfills = {
5759
abortSignalTimeout,
5860
aggregateError,
5961
arrayAt,
62+
clipboardItem,
6063
cryptoRandomUUID,
6164
elementReplaceChildren,
6265
eventAbortSignal,
66+
navigatorClipboard,
6367
objectHasOwn,
6468
promiseAllSettled,
6569
promiseAny,

src/navigator-clipboard.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export async function clipboardWrite(data: ClipboardItems) {
2+
if (data.length === 0) return
3+
const item = data[0]
4+
const blob = await item.getType(item.types.includes('text/plain') ? 'text/plain' : item.types[0])
5+
return navigator.clipboard.writeText(typeof blob == 'string' ? blob : await blob.text())
6+
}
7+
8+
export async function clipboardRead() {
9+
const str = navigator.clipboard.readText()
10+
return [new ClipboardItem({'text/plain': str})]
11+
}
12+
13+
export function isSupported(): boolean {
14+
return typeof navigator.clipboard.read === 'function' && typeof navigator.clipboard.write === 'function'
15+
}
16+
17+
export function isPolyfilled(): boolean {
18+
return navigator.clipboard.write === clipboardWrite || navigator.clipboard.read === clipboardRead
19+
}
20+
21+
export function apply(): void {
22+
if (!isSupported()) {
23+
navigator.clipboard.write = clipboardWrite
24+
navigator.clipboard.read = clipboardRead
25+
}
26+
}

test/clipboarditem.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {ClipboardItem, apply, isSupported, isPolyfilled} from '../lib/clipboarditem.js'
2+
3+
describe('ClipboardItem', () => {
4+
it('has standard isSupported, isPolyfilled, apply API', () => {
5+
expect(isSupported).to.be.a('function')
6+
expect(isPolyfilled).to.be.a('function')
7+
expect(apply).to.be.a('function')
8+
expect(isSupported()).to.be.a('boolean')
9+
expect(isPolyfilled()).to.equal(false)
10+
})
11+
12+
it('takes a Promise type, that can resolve', async () => {
13+
const c = new ClipboardItem({'text/plain': Promise.resolve('hi')})
14+
expect(c.types).to.eql(['text/plain'])
15+
expect(await c.getType('text/plain')).to.be.instanceof(Blob)
16+
})
17+
})

test/navigator-clipboard.js

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {clipboardRead, clipboardWrite, apply, isPolyfilled, isSupported} from '../lib/navigator-clipboard.js'
2+
3+
describe('navigator clipboard', () => {
4+
it('has standard isSupported, isPolyfilled, apply API', () => {
5+
expect(isSupported).to.be.a('function')
6+
expect(isPolyfilled).to.be.a('function')
7+
expect(apply).to.be.a('function')
8+
expect(isSupported()).to.be.a('boolean')
9+
expect(isPolyfilled()).to.equal(false)
10+
})
11+
12+
describe('read', () => {
13+
it('read returns array of 1 clipboard entry with plaintext of readText value', async () => {
14+
navigator.clipboard.readText = () => Promise.resolve('foo')
15+
const arr = await clipboardRead()
16+
expect(arr).to.have.lengthOf(1)
17+
expect(arr[0]).to.be.an.instanceof(globalThis.ClipboardItem)
18+
expect(arr[0].types).to.eql(['text/plain'])
19+
expect(await arr[0].getType('text/plain')).to.eql('foo')
20+
})
21+
})
22+
23+
describe('write', () => {
24+
it('unpacks text/plain content to writeText', async () => {
25+
const calls = []
26+
navigator.clipboard.writeText = (...args) => calls.push(args)
27+
await clipboardWrite([
28+
new globalThis.ClipboardItem({
29+
'foo/bar': 'horrible',
30+
'text/plain': Promise.resolve('foo')
31+
})
32+
])
33+
expect(calls).to.have.lengthOf(1)
34+
expect(calls[0]).to.eql(['foo'])
35+
})
36+
37+
it('accepts multiple clipboard items, picking the first', async () => {
38+
const calls = []
39+
navigator.clipboard.writeText = (...args) => calls.push(args)
40+
await clipboardWrite([
41+
new globalThis.ClipboardItem({
42+
'foo/bar': 'horrible',
43+
'text/plain': Promise.resolve('multiple-pass')
44+
}),
45+
new globalThis.ClipboardItem({
46+
'foo/bar': 'multiple-fail',
47+
'text/plain': Promise.resolve('multiple-fail')
48+
})
49+
])
50+
expect(calls).to.have.lengthOf(1)
51+
expect(calls[0]).to.eql(['multiple-pass'])
52+
})
53+
})
54+
})

0 commit comments

Comments
 (0)