Skip to content

Instantly share code, notes, and snippets.

@hunghg255
Forked from alexreardon/drag-event-polyfill.js
Created December 14, 2023 12:45
Show Gist options
  • Save hunghg255/78acd549612cac23d061a7e8acb80319 to your computer and use it in GitHub Desktop.
Save hunghg255/78acd549612cac23d061a7e8acb80319 to your computer and use it in GitHub Desktop.
DragEvent polyfill for jsdom
// 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