Created
May 15, 2017 12:49
-
-
Save Radiergummi/aba408acd53573f570a8e44d914b210d to your computer and use it in GitHub Desktop.
DripDrop rewrite ES6 + eventEmitter
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
// node - The dom node to set as a drag handle | |
// options | |
// image - Can take on one of the following possible values: | |
// false - (Default) No image | |
// true - The default generated drag image | |
// string - The path to an image | |
// imageObject - If this is an Image object, that will be used | |
// start(setData, e) - Called when dragging starts. Use setData to set the data for each type. | |
// setData(type, data) - Sets data for a particular type. | |
// NOTE: In an attempt mitigate type lowercasing weirdness, capitals will be converted to dash-lowercase *and* lowercase without dashes | |
// IE NOTE: IE is a piece of shit and doesn't allow any 'type' other than "text" and "url" - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/329509/ | |
// move(pointerPosition, e) | |
// end(e) | |
import EventEmitter from 'event-keeper' | |
class Drag extends EventEmitter { | |
/** | |
* @param {HTMLElement} node dom node to set as a drag handle | |
* @param {object} options configuration options object | |
* @param {bool|string|Image} options.image can take on one of the following possible values: | |
* - false (default): No image | |
* - true: The default generated drag image | |
* - string: The path to an image | |
* - Image: If this is an instance of Image, | |
* that will be used | |
*/ | |
constructor(node, options = {}) { | |
this._node = node | |
this._options = options | |
this._callbacks = {} | |
let recentMousePos | |
this._callbacks.start = event => { | |
if (this._options.image !== undefined) { | |
let image | |
switch (this._options.image) { | |
case true: | |
image = new window.Image | |
break | |
case false: | |
image = undefined | |
break | |
case string: | |
image = new window.Image(this._options.image) | |
break | |
default: | |
image = this._options.image | |
break | |
} | |
// set the drag image | |
if (image) { | |
event.dataTransfer.setDragImage(image, image.width || 0, image.height || 0) | |
} | |
} | |
this.emit('start', event.DataTransfer) | |
// TODO | |
let dataTransfer = event.dataTransfer, | |
effectAllowed = this._options.start((type, string) => { | |
dataTransfer.setData(type, string) | |
let mappedType = this._mapFromCamelCase(type) | |
if (mappedType !== type) { | |
dataTransfer.setData(mappedType, string) | |
} | |
}, event) | |
if (effectAllowed) { | |
event.dataTransfer.effectAllowed = effectAllowed | |
} | |
} | |
this._callbacks.over = event => { | |
if ( | |
recentMousePos === undefined || | |
event.pageX !== recentMousePos.x || | |
event.pageY !== recentMousePos.y | |
) { | |
recentMousePos = { | |
x: event.pageX, | |
y: event.pageY | |
} | |
this._options.move(event) | |
} | |
} | |
this._callbacks.end = () => { | |
document.removeEventListener('dragover', this._dragInfo.docOver, true) | |
this._node.removeEventListener('dragend', this._dragInfo.dragendHandler) | |
} | |
// set the draggable DOM attribute | |
this._node.setAttribute('draggable', 'true') | |
// attach all listeners | |
this._node.addEventListener('dragstart', this._callbacks.start) | |
document.addEventListener('dragover', this._callbacks.over, true) | |
this._node.addEventListener('dragend', this._callbacks.end, true) | |
} | |
/** | |
* maps a string from camelCase | |
* | |
* @param {string} str camelCase string | |
* @returns {string} mapped string | |
*/ | |
_mapFromCamelCase(str) { | |
return string.replace(/([A-Z])/g, (match, submatch) => '-' + submatch.toLowerCase()) | |
} | |
} | |
export default Drag |
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
import Drag from './drag' | |
import Drop from './drop' | |
/** | |
* The DripDrop class wraps all functionality | |
*/ | |
class DripDrop { | |
/** | |
* returns an opaque clone of the passed dom node ready to be moved with moveToMouse | |
* | |
* @param {HTMLElement} domNode node to clone | |
* @param {number} [zIndex] defaults to 1000 | |
* | |
* @returns {HTMLElement} cloned node | |
* @static | |
*/ | |
static createGhost(domNode, zIndex = 1000) { | |
const ghost = domNode.cloneNode(true) | |
ghost.style.position = 'absolute' | |
ghost.style.top = '-100px' | |
ghost.style.width = `${domNode.clientWidth}px` | |
ghost.style.opacity = '.6' | |
// makes this 'invisible' to mouse events so it doesn't | |
// block mouse events while you're dragging it around | |
ghost.style.pointerEvents = 'none' | |
ghost.style['z-index'] = zIndex | |
return ghost | |
} | |
/** | |
* preserves default events for drag operations | |
* | |
* @static | |
*/ | |
static preserveDefaultEvents() { | |
document.removeEventListener('dragenter', docEnterHandler, true) | |
document.removeEventListener('dragover', docOverHandler, true) | |
} | |
/** | |
* moves an absolutely positioned element to the position by x and y | |
* | |
* @param {HTMLElement} node node to move | |
* @param {number} x x coordinate | |
* @param {number} y y coordinate | |
* @static | |
*/ | |
static moveAbsoluteNode (node, x, y) { | |
node.style.left = `${x}px` | |
node.style.top = `${y}px` | |
} | |
/** | |
* getter for the Drop class | |
* | |
* @returns {Drop} Drop constructor | |
* @static | |
*/ | |
static get Drop() { | |
return Drop | |
} | |
/** | |
* getter for the Drag class | |
* | |
* @returns {Drag} Drag constructor | |
* @static | |
*/ | |
static get Drag() { | |
return Drag | |
} | |
} | |
export default DripDrop | |
/** | |
* I don't really know what this does yet... | |
*/ | |
// get rid of the need to do this for other drag events | |
var docEnterHandler, docOverHandler; | |
document.addEventListener('dragenter',docEnterHandler=function(e){ | |
e.preventDefault() | |
}, true) | |
document.addEventListener('dragover',docOverHandler=function(e){ | |
e.preventDefault() | |
}, true) | |
//document.addEventListener('dragstart',docOverHandler=function(e){ | |
// e.preventDefault() | |
//}) | |
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
/** | |
* Callback called when a drag action enters the node | |
* | |
* @callback dragEnterCallback | |
* @param {Array} types data types available on drop | |
* @param {Event} event original drag event | |
*/ | |
/** | |
* Callback called with the dragging pointer moves over the node | |
* IMPORTANT: | |
* 'data' will contain the correct keys, but will *not* actually | |
* contain any data. Blame the stupid html5 drag and drop api. | |
* | |
* @callback dragMoveCallback | |
* @param {Array} types data types available on drop | |
* @param {Event} event original drag event | |
*/ | |
/** | |
* Callback called when the dragging pointer crosses in over | |
* a child-boundary of a descendant node that is also a drop zone | |
* | |
* @callback dragInCallback | |
* @param {Array} types data types available on drop | |
* @param {Event} event original drag event | |
*/ | |
/** | |
* Callback called when the dragging pointer crosses out over | |
* a child-boundary of a descendant node that is also a drop zone | |
* | |
* @callback dragOutCallback | |
* @param {Array} types data types available on drop | |
* @param {Event} event original drag event | |
*/ | |
/** | |
* Callback called when the dragging pointer releases above the node | |
* | |
* @callback dragOutCallback | |
* @param {object} data object where each key is a data type, where if that type contains dashes, the type | |
* will be available as is *and* with dash-lowercase converted to camel case. | |
* The value is either: | |
* For the 'files' type, the value is a list of files, each with a set of properties | |
* described here: https://developer.mozilla.org/en-US/docs/Web/API/File. | |
* In addition, the files have the methods: | |
* - getText(errback): Returns the text of the file in a call to the the errback | |
* - getBuffer(errback): Returns a Buffer of the file contents in a call to the the errback | |
* For any other type, the value is a string of data in a format depending on the type | |
* @param {Event} event original drag event | |
*/ | |
import EventEmitter from 'event-keeper' | |
/** | |
* Drop class | |
* | |
* @extends EventEmitter | |
* | |
* @property {object} _options original options passed to the constructor | |
* @property {object} _callbacks callbacks for the individual events | |
* @property {HTMLElement} _node node to listen on | |
*/ | |
class Drop extends EventEmitter { | |
/** | |
* @param {HTMLElement} node node to set up as a drop zone | |
* @param {object} [options] configuration options | |
* @param {Array} [options.allow] list of types to allow the event handlers be called for. | |
* if this is passed and the current drag operation doesn't | |
* have an allowed type, the handlers will not be called. | |
* if this isn't passed, all types are allowed | |
* @param {dragEnterCallback} [options.enter] callback called when a drag action enters the node | |
* @param {dragMoveCallback} [options.move] callback called with the dragging pointer moves over the node | |
* @param {dragLeaveCallback} [options.leave] callback called with the dragging pointer moves out of the node | |
* @param {dragInCallback} [options.in] callback called when the dragging pointer crosses in over | |
* a child-boundary of a descendant node that is also a drop zone | |
* @param {dragOutCallback} [options.out] callback called when the dragging pointer crosses out over | |
* a child-boundary of a descendant node that is also a drop zone | |
* @param {DragDropCallback} [options.drop] callback called when the dragging pointer releases above the node | |
* | |
*/ | |
constructor(node, options = {}) { | |
// construct the eventEmitter | |
super() | |
// register instance properties | |
this._options = options | |
this._callbacks = {} | |
this._node = node | |
if (this._options.enter) { | |
this.on('enter', options.enter) | |
} | |
if (this._options.move) { | |
this.on('move', options.move) | |
} | |
if (this._options.leave) { | |
this.on('leave', options.leave) | |
} | |
if (this._options.in) { | |
this.on('in', options.in) | |
} | |
if (this._options.out) { | |
this.on('out', options.out) | |
} | |
if (this._options.drop) { | |
this.on('drop', options.drop) | |
} | |
this._allowedTypes = ( | |
this._options.allow | |
? this._options.allow | |
: [ '*' ] | |
) | |
let currentTypes, | |
recentMousePos, | |
stopPropCalled, | |
dragCounter = 0, | |
dropEffect = 'copy' | |
this._callbacks.enter = event => { | |
dragCounter++ | |
// browsers stupidly emit dragenter whenever crossing over a child boundary... | |
if (dragCounter === 1) { | |
let data = this._buildDataObject(event.dataTransfer) | |
currentTypes = Object.keys(data) | |
if (this._isAllowedType(currentTypes)) { | |
this.emit('enter', currentTypes, event) | |
} | |
} | |
this.emit('in', currentTypes, event) | |
} | |
this._callbacks.over = event => { | |
let originalStopProp = event.stopPropagation | |
event.stopPropagation = () => stopPropCalled = true | |
if ( | |
recentMousePos === undefined || | |
event.pageX !== recentMousePos.x || | |
event.pageY !== recentMousePos.y | |
) { | |
recentMousePos = { | |
x: event.pageX, | |
y: event.pageY | |
} | |
if (this._isAllowedType(currentTypes)) { | |
stopPropCalled = false | |
dropEffect = options.move(curTypes, event) | |
} | |
} | |
if (dropEffect) { | |
event.dataTransfer.dropEffect = dropEffect | |
} | |
if (stopPropCalled) { | |
originalStopProp.call(event) | |
} | |
} | |
this._callbacks.leave = event => { | |
dragCounter-- | |
// browsers stupidly emits dragleave whenever crossing over a child boundary... | |
if (dragCounter === 0) { | |
if (this._isAllowedType(currentTypes)) { | |
this.emit('leave', currentTypes, event) | |
} | |
} | |
this.emit('out', currentTypes, event) | |
} | |
this._callbacks.drop = event => { | |
event.preventDefault() | |
if (this._isAllowedType(currentTypes)) { | |
let data = this._buildDataObject(event.dataTransfer) | |
this.emit('drop', data, event) | |
} | |
// reset | |
dragCounter = 0 | |
} | |
// attach the callbacks | |
this._node.addEventListener('dragenter', this._callbacks.enter) | |
this._node.addEventListener('dragover', this._callbacks.over) | |
this._node.addEventListener('dragleave', this._callbacks.leave) | |
this._node.addEventListener('drop', this._callbacks.drop) | |
} | |
/** | |
* Destroys the instance and removes all event listeners | |
*/ | |
destroy() { | |
if (this._callbacks.enter) { | |
this._node.removeEventListener('dragstart', this._callbacks.start) | |
} | |
if (this._callbacks.move) { | |
document.removeEventListener('dragover', this._callbacks.over) | |
} | |
if (this._callbacks.leave) { | |
this._node.removeEventListener('dragend', this._callbacks.leave) | |
} | |
if (this._callbacks.drop) { | |
this._node.removeEventListener('drop', this._callbacks.drop) | |
} | |
} | |
/** | |
* reads a text file using a given method | |
* | |
* @param {File|Blob} file file to read | |
* @param {method} string method to use. can be one of | |
* - readAsText | |
* - readAsArrayBuffer | |
* - readAsBinaryString | |
* - readAsDataURL | |
* @param {function} callback callback to run once the file has been read | |
* @private | |
*/ | |
_readTextFile(file, method, callback) { | |
const reader = new window.FileReader() | |
reader.onloadend = event => { | |
if (event.target.readyState === window.FileReader.DONE) { | |
return callback(undefined, reader.result) | |
} | |
} | |
reader.onerror = error => callback(error) | |
reader[ method ](file) | |
} | |
/** | |
* creates a data object from a dataTransfer object | |
* | |
* @param {DataTransfer} dataTransfer the original dataTransfer object from an event | |
* @returns {object} parsed data object | |
*/ | |
_buildDataObject(dataTransfer) { | |
const data = {} | |
if (dataTransfer.files.length > 0) { | |
data.files = dataTransfer.files | |
} | |
dataTransfer.types.forEach(type => { | |
if (type === 'Files') { | |
data.files = Array.from(data.files) | |
data.files.forEach(file => { | |
file.getText = callback => this._readTextFile(file, 'readAsText', callback) | |
file.getBuffer = callback => this._readTextFile(file, 'readAsArrayBuffer', callback) | |
}) | |
} else { | |
this._attachGetter(data, dataTransfer, type) | |
let mappedType = this._mapToCamelCase(type) | |
if (mappedType !== type) { | |
this._attachGetter(data, dataTransfer, mappedType) | |
} | |
} | |
}) | |
return data | |
} | |
/** | |
* proxies properties from an object to a dataTransfer instance | |
* | |
* @param {object} target object to bind the getter to | |
* @param {DataTransfer} dataTransfer dataTransfer object to retrieve a value from | |
* @param {string} property property name for the getter | |
* @returns {object} modified target object | |
*/ | |
_attachGetter(target, dataTransfer, property) { | |
return Object.defineProperty(target, property, { | |
enumerable: true, | |
get: () => dataTransfer.getData(property) | |
}) | |
} | |
/** | |
* maps a string to camelCase | |
* | |
* @param {string} str string to map to camelCase | |
* @returns {string} camelCase mapped string | |
*/ | |
_mapToCamelCase(str) { | |
return str.replace(/(-[a-z])/g, (match, submatch) => submatch[ 1 ].toUpperCase()) | |
} | |
/** | |
* checks whether the mouse cursor is over a certain element | |
* | |
* @param {number} x current x coordinate of the cursor | |
* @param {number} y current y coordinate of the cursor | |
* @param {HTMLElement} element element to check | |
* @returns {boolean} whether the mouse cursor is over the element | |
*/ | |
_pointerIsOver(x, y, element) { | |
const boundaries = elment.getBoundingClientRect() | |
return ( | |
y <= boundaries.top && | |
y <= boundaries.bottom && | |
x <= boundaries.left && | |
x <= boundaries.right | |
) | |
} | |
/** | |
* checks whether the current list of types is allowed | |
* | |
* @param {Array} types types to check for | |
* @returns {boolean} whether there is an allowed type | |
* @private | |
*/ | |
_isAllowedType(types) { | |
if (this._allowedTypes.indexOf('*') > -1) { | |
return true | |
} | |
for (let n = 0; n < this._allowedTypes.length; n++) { | |
if (types.indexOf(this._allowedTypes[ n ]) > -1) { | |
return true | |
} | |
} | |
return false | |
} | |
} | |
export default Drop |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment