Skip to content

Instantly share code, notes, and snippets.

@Sphinxxxx
Created October 11, 2017 05:00
Show Gist options
  • Save Sphinxxxx/f474197f53d7792516d997b695e30021 to your computer and use it in GitHub Desktop.
Save Sphinxxxx/f474197f53d7792516d997b695e30021 to your computer and use it in GitHub Desktop.
Draggable elements boilerplate
<script>
//window.onerror = function(msg, url, line) { alert('Error: '+msg+'\nURL: '+url+'\nLine: '+line); };
</script>
<div id="vanilla">
<h2>Simple HTML element dragging</h2>
<div class="container">
<div class="not-a-box">Can't touch this</div>
<div class="box">Drag me!</div>
<div class="box">Drag me!</div>
</div>
</div>
<div id="vue">
<h2>SVG dragging bound (sort of) to Vue.js state</h2>
<svg width="400" height="400">
<g class="point" v-for="p in svg.points">
<drag-point :p="p"></drag-point>
</g>
<g class="line" v-for="(l, i) in svg.lines">
<line :x1="l.start.coord[0]" :y1="l.start.coord[1]"
:x2="l .end.coord[0]" :y2="l .end.coord[1]" />
<drag-point class="start" :p="l.start"></drag-point>
<drag-point class="end" :p="l.end" ></drag-point>
</g>
</svg>
<p>
<button id="add-point" @click="addPoint">Add point</button>
<button id="add-line" @click="addLine">Add line</button>
<br>
<label>Size of selected point: <input id="size" type="range" min="1" max="50" v-model="svg.pointRadius" /></label>
</p>
<pre>_svgState = {{ svg }}</pre>
</div>
//Standalone dragging library
function dragAny(selector, options) {
options = options || {};
const container = options.container || document.body,
callback = options.callback;
let dragged, dragOffset;
//Element.closest polyfill:
//https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
//https://github.com/Financial-Times/polyfill-service/issues/1279
const ep = Element.prototype;
if (!ep.matches)
ep.matches = ep.msMatchesSelector || ep.webkitMatchesSelector;
if (!ep.closest)
ep.closest = function(s) {
var node = this;
do {
if(node.matches(s)) return node;
node = (node.tagName === 'svg') ? node.parentNode : node.parentElement;
} while(node);
return null;
};
function getMousePos(e, elm) {
let x = e.clientX,
y = e.clientY;
if(elm) {
const bounds = elm.getBoundingClientRect();
x -= bounds.left;
y -= bounds.top;
//An SVG circle is positioned by its center (cx/cy), not its top-left corner:
if(elm.nodeName === 'circle') {
x -= bounds.width/2;
y -= bounds.height/2;
}
}
return [x, y];
}
function onDown(e) {
dragged = e.target.closest(selector);
if(dragged) {
e.preventDefault();
dragOffset = getMousePos(e, dragged);
//console.log('drag', dragOffset);
}
}
function onMove(e) {
e.preventDefault();
const pos = getMousePos(e, container);
const x = Math.round(pos[0] - dragOffset[0]),
y = Math.round(pos[1] - dragOffset[1]);
//console.log('dragging', x, y);
callback(dragged, [x, y]);
}
container.addEventListener('mousedown', function(e) {
if(e.buttons === 1) { onDown(e); }
});
container.addEventListener('touchstart', function(e) {
onDown(tweakTouch(e));
});
window.addEventListener('mousemove', function(e) {
if(!dragged) { return; }
if(e.buttons === 1) { onMove(e); }
else { dragged = null; }
});
window.addEventListener('touchmove', function(e) {
if(dragged) { onMove(tweakTouch(e)); }
});
function tweakTouch(e) {
var touch = e.targetTouches[0];
touch.preventDefault = e.preventDefault.bind(e);
return touch;
}
}
/* #vanilla */
(function initVanilla() {
dragAny('.box', {
container: document.querySelector('#vanilla .container'),
callback: (box, pos) => {
box.style.left = pos[0] + 'px';
box.style.top = pos[1] + 'px';
},
});
})();
/* #vue */
(function initVue() {
//Global state model. Can be changed from within Vue or from the outside.
const _svgState = {
pointRadius: 16,
points: [],
lines: [],
selectedPoint: null,
};
function createPoint(x, y, r) {
return {
coord: [
x || Math.round(Math.random()*400),
y || Math.round(Math.random()*400)
],
radius: r || _svgState.pointRadius || 10,
};
}
function addPoint() {
_svgState.points.push(createPoint());
}
function addLine() {
_svgState.lines.push({ start: createPoint(), end: createPoint() });
}
addPoint();
addPoint();
addLine();
addLine();
//Needs to be part of the reactive state, or else drag-point's classObj() won't update correctly..
// let _selectedPoint;
Vue.component('drag-point', {
props: ['p'],
template: '<circle data-draggable :class="classObj" @dragging="onDragging" :cx="p.coord[0]" :cy="p.coord[1]" :r="p.radius" />',
computed: {
classObj() {
return {
selected: (this.p === _svgState.selectedPoint),
};
},
},
methods: {
onDragging(e) {
this.p.coord = e.detail.pos;
_svgState.selectedPoint = this.p;
},
}
})
new Vue({
el: '#vue',
data: {
foo: 'bar',
svg: _svgState,
},
methods: {
addPoint() { addPoint(); },
addLine() { addLine(); },
}
});
//Vue replaces the original <svg> element, so we must wait until now to enable dragging:
dragAny('[data-draggable]', {
container: document.querySelector('#vue svg'),
callback: (dragged, pos) => {
//Doesn't look like this binding is two-way,
//so we must dispatch a custom event which is handled by the point's Vue component...
// dragged.setAttribute('cx', pos[0]);
// dragged.setAttribute('cy', pos[1]);
var event = document.createEvent('CustomEvent');
event.initCustomEvent('dragging', true, false, { pos } );
//var event = new CustomEvent('dragging', { detail: { pos } });
dragged.dispatchEvent(event);
},
});
//Example of changing the state from the outside. Vue will notice and update the SVG:
document.querySelector('#vue #size').addEventListener('input', (e) => {
if(!_svgState.selectedPoint) { return; }
_svgState.selectedPoint.radius = e.target.value;
});
})();
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.4/vue.min.js"></script>
body {
display: flex;
flex-flow: row nowrap;
justify-content: space-around;
font-family: Georgia, sans-serif;
h2 {
font-size: 1.2em;
text-align: center;
}
}
#vanilla .container {
position: relative;
width: 400px;
height: 400px;
border: 1px solid gray;
.box, .not-a-box {
position: absolute;
width: 100px;
height: 60px;
&:nth-child(2) {
top: 200px;
left: 150px;
}
&:nth-child(3) {
top: 100px;
left: 250px;
}
}
.box {
background: dodgerblue;
cursor: move;
}
.not-a-box {
background: tomato;
}
}
#vue {
svg {
background: white;
border: 1px solid gray;
.line {
line {
stroke: royalblue;
stroke-width: 3;
//stroke-dasharray: 6;
pointer-events: none;
}
}
circle[data-draggable] {
stroke: limegreen;
stroke-width: 3;
stroke-dasharray: 6;
fill: rgba(yellow, .4);
cursor: move;
&.selected {
stroke: tomato;
stroke-width: 4;
}
}
}
input {
vertical-align: middle;
}
pre {
background: #f8f8f8;
border: 1px solid gainsboro;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment