-
-
Save hunghg255/78acd549612cac23d061a7e8acb80319 to your computer and use it in GitHub Desktop.
DragEvent polyfill for jsdom
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This file polyfills DragEvent for jsdom | |
// https://github.com/jsdom/jsdom/issues/2913 | |
// This file is in JS rather than TS, as our jsdom setup files currently need to be in JS | |
// Good news: DragEvents are almost the same as MouseEvents | |
(() => { | |
if (typeof window === 'undefined') { | |
return; | |
} | |
// Polyfill not needed | |
if (typeof window.DragEvent !== 'undefined') { | |
return; | |
} | |
// Let's create what we need for DragEvent's! | |
if (window.DataTransferItemList) { | |
throw new Error(`Unexpected global found: "DataTransferItemList"`); | |
} | |
if (window.DataTransfer) { | |
throw new Error(`Unexpected global found: "DataTransfer"`); | |
} | |
// Using this so we can quickly look up an items | |
// data without needing to go through the public async API | |
// to get item values | |
const fastItemValueLookup = Symbol('item-value'); | |
/** @type DataTransferItemList | |
* | |
* Cheating an making `DataTransferItemList` extend an `Array` so we can get: | |
* - `list.length` for free | |
* - indexed lookup (`list[0]`) for free | |
* - makes other operations such as clearing, finding, adding, removing easy as well | |
*/ | |
class DataTransferItemList extends Array { | |
/** | |
* @param {(string | File)} stringValueOrFile | |
* @param {string=} stringMimeType | |
* @return {(DataTransferItem | null)} | |
*/ | |
add(stringValueOrFile, stringMimeType) { | |
if (stringValueOrFile instanceof File) { | |
/** @type DataTransferItem */ | |
const item = { | |
kind: 'file', | |
// The type of file being dragged (eg "image/jpeg") | |
type: stringValueOrFile.type, | |
getAsFile: () => { | |
return stringValueOrFile; | |
}, | |
getAsString: (/* callback */) => { | |
// callback will never be resolved for files | |
}, | |
webkitGetAsEntry() { | |
throw new Error('webkitGetAsEntry() not implemented'); | |
}, | |
// This allows us to lookup items synchronously with `dataTransfer.getData()` | |
[fastItemValueLookup]: stringValueOrFile, | |
}; | |
this.push(item); | |
return item; | |
} | |
if (typeof stringValueOrFile === 'string') { | |
// Throws if adding data to a type that already has data | |
// https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItemList/add#notsupportederror | |
const exists = this.some( | |
item => item.kind === 'string' && item.type === stringMimeType, | |
); | |
if (exists) { | |
throw new DOMException('NotSupportedError'); | |
} | |
/** @type DataTransferItem */ | |
const item = { | |
kind: 'string', | |
type: stringMimeType, | |
getAsFile: () => { | |
// this will be `null` for non-files | |
return null; | |
}, | |
getAsString: callback => { | |
setTimeout(() => { | |
callback(stringValueOrFile); | |
}); | |
}, | |
webkitGetAsEntry() { | |
throw new Error('webkitGetAsEntry() not implemented'); | |
}, | |
// This allows us to lookup items synchronously with `dataTransfer.getData()` | |
[fastItemValueLookup]: stringValueOrFile, | |
}; | |
this.push(item); | |
return item; | |
} | |
throw new Error( | |
'Unexpected arguments. Expected: .add(file: File) or .add(data: string, type: string)', | |
); | |
} | |
/** Removes all items | |
* @return {void} | |
*/ | |
clear() { | |
this.length = 0; | |
} | |
/** Removes an item at a given index | |
* @param {number} index | |
* @return {void} | |
*/ | |
remove(index) { | |
this.splice(index, 1); | |
} | |
} | |
window.DataTransferItemList = DataTransferItemList; | |
/** | |
* Get the full media type, adjusting for shorthand values. | |
*/ | |
function getDataFormat(format) { | |
if (format === 'text') { | |
return 'text/plain'; | |
} | |
if (format === 'url') { | |
return 'text/uri-list'; | |
} | |
return format; | |
} | |
/** @type DataTransfer */ | |
class DataTransfer { | |
constructor() { | |
// From spec: | |
// > Set the dropEffect and effectAllowed to "none". | |
this.dropEffect = 'none'; | |
this.effectAllowed = 'none'; | |
// DataTransferItemList() is usually a hidden constructor | |
this.items = new DataTransferItemList(); | |
} | |
/** | |
* Get unique types of `.items` | |
* @return {string[]} | |
* https://html.spec.whatwg.org/multipage/dnd.html#concept-datatransfer-types | |
*/ | |
get types() { | |
const all = this.items.map(item => { | |
if (item.kind === 'string') { | |
return item.type; | |
} | |
return 'Files'; | |
}); | |
// it is possible to have multiple 'Files' entries | |
// so we need to strip them out | |
const unique = Array.from(new Set(all)); | |
// sorting for consistency | |
return unique.sort(); | |
} | |
// TODO: is this the right data structure? | |
/** | |
* Get files being dragged | |
* @return {FileList} | |
*/ | |
get files() { | |
return this.items | |
.filter(item => item.kind === 'file') | |
.reduce((acc, item, index) => { | |
const file = item.getAsFile(); | |
acc[index] = file; | |
return acc; | |
}, {}); | |
} | |
/** Clears string items. Note: cannot be used to clear files | |
* | |
* @param {string=} format | |
* @return {void} | |
* @see https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-cleardata | |
*/ | |
clearData(format) { | |
if (format) { | |
const actualFormat = getDataFormat(format); | |
const index = this.items.findIndex(item => { | |
// Note: can never clear files with `clearData` | |
return item.type === actualFormat; | |
}); | |
if (index !== -1) { | |
this.items.remove(index); | |
} | |
return; | |
} | |
// According to the spec, `.clearData()` does not remove files. | |
// However, in Chrome it does remove files... | |
// Looping backwards so that we can safely remove | |
// items without messing up indexes | |
for (let i = this.items.length - 1; i >= 0; i--) { | |
const item = this.items[i]; | |
if (item.kind === 'string') { | |
this.items.remove(i); | |
} | |
} | |
} | |
/** This function is only used to get the value of string items | |
* | |
* @param {string} format | |
* @return {string} | |
* @see https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-getdata | |
*/ | |
getData(format) { | |
const actualFormat = getDataFormat(format); | |
const match = this.items.find( | |
item => item.kind === 'string' && item.type === actualFormat, | |
); | |
if (match) { | |
return match[fastItemValueLookup]; | |
} | |
return ''; | |
} | |
/** This function is only used to set string items | |
* | |
* @param {string} format | |
* @param {string} data | |
* @return {void} | |
* @see https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-setdata | |
*/ | |
setData(format, data) { | |
this.items.add(data, getDataFormat(format)); | |
} | |
// eslint-disable-next-line class-methods-use-this | |
setDragImage() { | |
// Stub | |
} | |
} | |
window.DataTransfer = DataTransfer; | |
class DragEvent extends MouseEvent { | |
constructor(type, eventInitDict = {}) { | |
super(type, eventInitDict); | |
// MouseEvent in jsdom doesn't implement the standard pageX and pageY properties | |
this.pageX = eventInitDict.pageX ?? 0; | |
this.pageY = eventInitDict.pageY ?? 0; | |
// TODO: 'implement modes' (eg protected mode) | |
this.dataTransfer = new DataTransfer(); | |
} | |
} | |
window.DragEvent = DragEvent; | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment