Created
August 23, 2019 06:05
-
-
Save denieler/f344b78112841a220d1d7b954582c4bb to your computer and use it in GitHub Desktop.
Make html element to be pannable (drag & drop)
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 Sister from 'sister' | |
var dragStart, | |
dragMove, | |
dragEnd, | |
stopDrop | |
/** | |
* @param {HTMLElement} targetElement | |
*/ | |
function Pan (targetElement) { | |
var pan = this; | |
targetElement.draggable = true; | |
return { | |
eventEmitter: pan.bind(targetElement), | |
unmount: pan.unmount | |
}; | |
} | |
Pan.prototype.bind = function (targetElement) { | |
var eventEmitter = new Sister(), | |
pan = this, | |
handle, | |
positionTracker, | |
position, | |
dragStartSubjectDisplay, | |
dragStartSubjectOpacity, | |
firstMove; | |
dragStart = function (e) { | |
positionTracker = pan.positionTracker(e); | |
dragStartSubjectDisplay = targetElement.style.display; | |
dragStartSubjectOpacity = targetElement.style.opacity; | |
handle = pan.makeHandle(targetElement); | |
targetElement.style.opacity = 0; | |
firstMove = true; | |
// Doesn't work without this in Firefox. | |
e.dataTransfer.setData('text/plain', 'node'); | |
}; | |
dragMove = function (e) { | |
// For mobile. | |
e.preventDefault(); | |
position = positionTracker.update(e); | |
if (firstMove) { | |
// Manipulating (hiding) the targetElement on dragStart is | |
// causing instant dragEnd. | |
targetElement.parentNode.insertBefore(handle, targetElement); | |
targetElement.style.display = 'none'; | |
firstMove = false; | |
eventEmitter.trigger('start', { | |
type: 'start', | |
offsetX: position.offsetX, | |
offsetY: position.offsetY, | |
target: targetElement, | |
handle: handle | |
}); | |
} | |
if (!position.isChange || position.isOutside) { | |
return; | |
} | |
eventEmitter.trigger('move', { | |
type: 'move', | |
offsetX: position.offsetX, | |
offsetY: position.offsetY, | |
target: targetElement, | |
handle: handle | |
}); | |
}; | |
dragEnd = function (e) { | |
// For mobile. | |
e.preventDefault(); | |
targetElement.style.display = dragStartSubjectDisplay; | |
targetElement.style.opacity = dragStartSubjectOpacity; | |
handle.parentNode.removeChild(handle); | |
eventEmitter.trigger('end', { | |
type: 'end', | |
offsetX: position.offsetX, | |
offsetY: position.offsetY, | |
target: targetElement, | |
handle: handle | |
}); | |
}; | |
stopDrop = function (e) { | |
e.stopPropagation(); | |
e.preventDefault(); | |
}; | |
targetElement.addEventListener('dragstart', dragStart, false); | |
//targetElement.addEventListener('drag', dragMove, false); | |
targetElement.addEventListener('dragend', dragEnd, false); | |
targetElement.addEventListener('touchstart', dragStart, false); | |
targetElement.addEventListener('touchmove', dragMove, false); | |
targetElement.addEventListener('touchend', dragEnd, false); | |
// @see http://stackoverflow.com/a/902352/368691 | |
document.body.addEventListener('dragover', dragMove, false); | |
document.body.addEventListener('drop', stopDrop, false); | |
return eventEmitter; | |
}; | |
Pan.prototype.unmount = function (targetElement) { | |
if (targetElement) { | |
targetElement.removeEventListener('dragstart', dragStart, false); | |
targetElement.removeEventListener('dragend', dragEnd, false); | |
targetElement.removeEventListener('touchstart', dragStart, false); | |
targetElement.removeEventListener('touchmove', dragMove, false); | |
targetElement.removeEventListener('touchend', dragEnd, false); | |
} | |
// @see http://stackoverflow.com/a/902352/368691 | |
document.body.removeEventListener('dragover', dragMove, false); | |
document.body.removeEventListener('drop', stopDrop, false); | |
} | |
/** | |
* Get the mouse cursor position or the first touch position. | |
* | |
* @param {Object} event | |
* @return {Object} | |
*/ | |
Pan.prototype.getEventPosition = function (event) { | |
var source = event.touches ? event.touches[0] : event; | |
return { | |
x: source.pageX, | |
y: source.pageY | |
}; | |
}; | |
/** | |
* Position tracker receives the first event and uses to produce an offset calculator. | |
*/ | |
Pan.prototype.positionTracker = function (startEvent) { | |
var pan = this, | |
pt = {}, | |
eventPosition = pan.getEventPosition(startEvent), | |
lastPageX = eventPosition.x, | |
lastPageY = eventPosition.y; | |
pt.dragStartX = eventPosition.x; | |
pt.dragStartY = eventPosition.y; | |
return { | |
update: function (event) { | |
var eventPosition = pan.getEventPosition(event); | |
pt.pageX = eventPosition.x; | |
pt.pageY = eventPosition.y; | |
pt.offsetX = eventPosition.x - pt.dragStartX; | |
pt.offsetY = eventPosition.y - pt.dragStartY; | |
// Is the cursor outside the screen? | |
pt.isOutside = pt.pageX == document.documentElement.scrollLeft && pt.pageY == document.documentElement.scrollTop; | |
// Did the cursor position change? | |
pt.isChange = lastPageX != eventPosition.x || lastPageY != eventPosition.y; | |
lastPageX = eventPosition.x; | |
lastPageY = eventPosition.y; | |
return pt; | |
} | |
}; | |
}; | |
/** | |
* @param {HTMLElement} targetElement | |
* @return {HTMLElement} | |
*/ | |
Pan.prototype.makeHandle = function (targetElement) { | |
var handle = this.makeClone(targetElement); | |
return handle; | |
}; | |
/** | |
* Clone node. | |
* | |
* @param {HTMLElement} node | |
* @return {HTMLElement} | |
*/ | |
Pan.prototype.makeClone = function (node) { | |
var clone; | |
clone = node.cloneNode(true); | |
return clone; | |
}; | |
export default Pan |
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 React, { Component } from 'react' | |
import PropTypes from 'prop-types' | |
import Pan from './pan.js' | |
class Pannable extends Component { | |
constructor () { | |
super() | |
this.pan = null | |
} | |
componentDidMount () { | |
this.pan = new Pan(this.pannableElement) | |
this.pan.currentPosX = 0 | |
this.pan.currentPosY = 0 | |
this.pan.eventEmitter.on('start', e => { | |
if (!this.pan.target) { | |
this.pan.target = e.target | |
} | |
}) | |
this.pan.eventEmitter.on('move', e => { | |
const offsetX = this.pan.currentPosX + e.offsetX | |
const offsetY = this.pan.currentPosY + e.offsetY | |
e.handle.style.transform = `translate(${offsetX}px, ${offsetY}px)` | |
}) | |
this.pan.eventEmitter.on('end', e => { | |
this.pan.currentPosX += e.offsetX | |
this.pan.currentPosY += e.offsetY | |
this.pan.target.style.transform = `translate(${this.pan.currentPosX}px, ${this.pan.currentPosY}px)` | |
}) | |
} | |
componentWillReceiveProps (nextProps) { | |
if (nextProps.cancelPan && this.pan && this.pan.target) { | |
this.pan.currentPosX = 0 | |
this.pan.currentPosY = 0 | |
this.pan.target.style.transform = `translate(${this.pan.currentPosX}px, ${this.pan.currentPosY}px)` | |
} | |
} | |
componentWillUnmount () { | |
this.pan.unmount(this.pannableElement) | |
} | |
render () { | |
return ( | |
<div | |
ref={(node) => { this.pannableElement = node }} | |
className='lm-pannable' | |
> | |
{this.props.children} | |
</div> | |
) | |
} | |
} | |
Pannable.propTypes = { | |
children: PropTypes.any, | |
cancelPan: PropTypes.bool | |
} | |
export default Pannable |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment