Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
},
"globals": {
"chai": false,
"expect": false
"expect": false,
"globalThis": false
},
"env": {
"mocha": true
Expand Down
31 changes: 31 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -153,6 +154,8 @@ <h1>GitHub Feature Support Table</h1>
<span class="unsupported">!</span>Required feature, not available in this browser.
</p><p>
<span class="polyfilled">*</span>Not avaible in this browser, but polyfilled using this library.
</p><p>
<span class="buggy"><small>†</small></span>Required feature, buy polyfilled to smooth over bugs in this browser.
</p><p>
<span class="transpiled">**</span>Not available in this browser, but transpiled to a compatible syntax.
</p>
Expand Down Expand Up @@ -523,6 +526,20 @@ <h1>GitHub Feature Support Table</h1>
<td data-supported="true"><div>78+</div></td>
<td data-supported="true"><div>16.0+</div></td>
</tr>
<tr>
<th>
<a href="https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem">
<code>ClipboardItem</code>
</a>
</th>
<td data-polyfill="clipboardItem"><div>*</div></td>
<td data-supported="true" title="Buggy implementation"><div>66+ †</div></td>
<td data-supported="true" title="Buggy implementation"><div>79+ †</div></td>
<td data-supported="false"><div>*</div></td>
<td data-supported="true"><div>13.1+</div></td>
<td data-supported="true" title="Buggy implementation"><div>53+ †</div></td>
<td data-supported="true" title="Buggy implementation"><div>9.0+ †</div></td>
</tr>
<tr>
<th>
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID">
Expand Down Expand Up @@ -565,6 +582,20 @@ <h1>GitHub Feature Support Table</h1>
<td data-supported="true"><div>76+</div></td>
<td data-supported="true"><div>15.0+</div></td>
</tr>
<tr>
<th>
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard">
<code>navigator.clipboard</code>
</a>
</th>
<td data-polyfill="navigatorClipboard"><div>*</div></td>
<td data-supported="true"><div>86+</div></td>
<td data-supported="true"><div>79+</div></td>
<td data-supported="false"><div>*</div></td>
<td data-supported="true"><div>13.1+</div></td>
<td data-supported="true"><div>63+ †</div></td>
<td data-supported="true"><div>12.0+ †</div></td>
</tr>
<tr>
<th>
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn">
Expand Down
49 changes: 49 additions & 0 deletions src/clipboarditem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const records = new WeakMap<ClipboardItem, Record<string, ClipboardItemDataType | PromiseLike<ClipboardItemDataType>>>()
const presentationStyles = new WeakMap<ClipboardItem, PresentationStyle>()
export class ClipboardItem {
constructor(
items: Record<string, ClipboardItemDataType | PromiseLike<ClipboardItemDataType>>,
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<Blob> {
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
}
}
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 objectHasOwn from './object-hasown.js'
import * as promiseAllSettled from './promise-allsettled.js'
import * as promiseAny from './promise-any.js'
Expand Down Expand Up @@ -57,9 +59,11 @@ export const polyfills = {
abortSignalTimeout,
aggregateError,
arrayAt,
clipboardItem,
cryptoRandomUUID,
elementReplaceChildren,
eventAbortSignal,
navigatorClipboard,
objectHasOwn,
promiseAllSettled,
promiseAny,
Expand Down
26 changes: 26 additions & 0 deletions src/navigator-clipboard.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
17 changes: 17 additions & 0 deletions test/clipboarditem.js
Original file line number Diff line number Diff line change
@@ -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)
})
})
54 changes: 54 additions & 0 deletions test/navigator-clipboard.js
Original file line number Diff line number Diff line change
@@ -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'])
})
})
})