Skip to content

Instantly share code, notes, and snippets.

@denieler
Created August 23, 2019 06:05
Show Gist options
  • Save denieler/f344b78112841a220d1d7b954582c4bb to your computer and use it in GitHub Desktop.
Save denieler/f344b78112841a220d1d7b954582c4bb to your computer and use it in GitHub Desktop.
Make html element to be pannable (drag & drop)
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
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