A Pen by Andreas Borgen on CodePen.
Created
October 11, 2017 05:00
-
-
Save Sphinxxxx/f474197f53d7792516d997b695e30021 to your computer and use it in GitHub Desktop.
Draggable elements boilerplate
This file contains hidden or 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
<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> |
This file contains hidden or 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
//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; | |
}); | |
})(); |
This file contains hidden or 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
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.4/vue.min.js"></script> |
This file contains hidden or 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
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