Skip to content

Instantly share code, notes, and snippets.

@Radiergummi
Created May 15, 2017 12:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Radiergummi/aba408acd53573f570a8e44d914b210d to your computer and use it in GitHub Desktop.
Save Radiergummi/aba408acd53573f570a8e44d914b210d to your computer and use it in GitHub Desktop.
DripDrop rewrite ES6 + eventEmitter
// 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
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()
//})
/**
* 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