Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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, but 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/docs/Web/API/HTMLFormElement/requestSubmit">
Expand Down
47 changes: 47 additions & 0 deletions src/clipboarditem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
type ClipboardItems = Record<string, string | Blob | PromiseLike<string | Blob>>
const records = new WeakMap<ClipboardItem, ClipboardItems>()
const presentationStyles = new WeakMap<ClipboardItem, PresentationStyle>()
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<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 formRequestSubmit from './form-requestsubmit.js'
import * as objectHasOwn from './object-hasown.js'
import * as promiseAllSettled from './promise-allsettled.js'
Expand Down Expand Up @@ -58,9 +60,11 @@ export const polyfills = {
abortSignalTimeout,
aggregateError,
arrayAt,
clipboardItem,
cryptoRandomUUID,
elementReplaceChildren,
eventAbortSignal,
navigatorClipboard,
formRequestSubmit,
objectHasOwn,
promiseAllSettled,
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'])
})
})
})