Quick mess to make a div rotatable by dragging handle.
A Pen by Andreas Borgen on CodePen.
Quick mess to make a div rotatable by dragging handle.
A Pen by Andreas Borgen on CodePen.
<script> | |
//WinPhone stuff.. | |
Array.from = Array.from || function(list) { return Array.prototype.slice.call(list); }; | |
//window.onerror = function(msg, url, line) { alert('Error: '+msg+'\nURL: '+url+'\nLine: '+line); }; | |
</script> | |
<div class="box outer drag-move"> | |
<button onclick="console.log('click')">Still clickable</button> | |
<textarea>Still focusable</textarea> | |
<div class="handle drag-rotate"></div> | |
<div class="handle drag-resize"></div> | |
<div class="box inner"> | |
<ul contenteditable="true"> | |
<li>Some editable (and selectable) paragraphs of text. Sed ut perspiciatis unde omnis iste natus</li> | |
<li>Error sit voluptatem accusantium doloremque laudantium, totam rem aperiam</li> | |
</ul> | |
<textarea>More editable text...</textarea> | |
<div class="handle drag-rotate"></div> | |
<div class="handle drag-resize"></div> | |
<div class="handle drag-move"></div> | |
<div class="box inner drag-move"> | |
<div class="handle drag-rotate"></div> | |
<div class="handle drag-resize"></div> | |
</div> | |
</div> | |
<div class="box inner2 drag-move"> | |
</div> | |
</div> | |
<div id="debug"></div> |
function makeDraggable(box, handles) { | |
"use strict"; | |
//console.clear(); | |
box.style.position = 'absolute'; | |
box.style.transformOrigin = 'center'; | |
//Needed (along with offsetWidth/Height) to make the resizing calculations work: | |
box.style.boxSizing = 'border-box'; | |
handles = handles || {}; | |
const handleMove = handles.move || box, | |
handleRotate = handles.rotate, | |
handleResize = handles.resize; | |
handleMove.addEventListener('mousedown', dragStartMove); | |
handleMove.addEventListener('touchstart', dragStartMove); | |
if(handleRotate) { | |
handleRotate.addEventListener('mousedown', dragStartRotate); | |
handleRotate.addEventListener('touchstart', dragStartRotate); | |
} | |
if(handleResize) { | |
handleResize.addEventListener('mousedown', dragStartResize); | |
handleResize.addEventListener('touchstart', dragStartResize); | |
} | |
document.addEventListener('mousemove', dragMove); | |
document.addEventListener('touchmove', dragMove); | |
document.addEventListener('mouseup', dragEnd); | |
document.addEventListener('touchend', dragEnd); | |
//If the box itself isn't draggable, we must still stop event bubbling, | |
//or else interaction with the box (e.g. input elements) will drag a draggable parent: | |
if(handleMove !== box) { | |
box.addEventListener('mousedown', e => e.stopPropagation()); | |
} | |
let mode, start; | |
function dragStartMove(e) { | |
//console.log('drag start move'); | |
mode = 'move'; | |
dragStart(e); | |
} | |
function dragStartRotate(e) { | |
//console.log('drag start rotate'); | |
mode = 'rotate'; | |
dragStart(e); | |
} | |
function dragStartResize(e) { | |
//console.log('drag start resize'); | |
mode = 'resize'; | |
dragStart(e); | |
} | |
function dragStart(e) { | |
//Leave box elements clickable since we don't know if this is a drag operation just yet: | |
// e.preventDefault(); | |
//Only drag the topmost box (in case of nested boxes) | |
e.stopPropagation(); | |
const clientRect = box.getBoundingClientRect(), | |
clientCenter = { | |
x: clientRect.left + clientRect.width/2, | |
y: clientRect.top + clientRect.height/2 | |
}, | |
rotSelf = (box.dataset.dragRotation ? Number(box.dataset.dragRotation) : 0); | |
//Traverse the DOM to find the total rotation on the box: | |
let rotParent = 0, elm = box.parentElement; | |
while(elm) { | |
if(elm.dataset.dragRotation) { rotParent += Number(elm.dataset.dragRotation) || 0; } | |
elm = elm.parentElement; | |
} | |
start = { | |
//Coordinates within the viewport. | |
//Useful to have absolute coordinates for rotated boxes nested inside other rotatec boxes: | |
client: { | |
center: clientCenter, | |
rotation: { | |
self: rotSelf, | |
accumParent: rotParent, | |
accumSelf: rotParent + rotSelf | |
}, | |
//Top left corner, relative to the viewport: | |
topLeft: rotatePoint({ | |
x: clientCenter.x - box.clientWidth/2, | |
y: clientCenter.y - box.clientHeight/2 | |
}, rotParent + rotSelf, clientCenter), | |
mousePos: { x: e.clientX, y: e.clientY }, | |
}, | |
//The top/left coordinates used for absolute positioning, | |
//i.e. the bounding rect *before* any transforms like a previous rotation: | |
//https://stackoverflow.com/questions/27745438/how-to-compute-getboundingclientrect-without-considering-transforms | |
local: { | |
top: box.offsetTop, | |
left: box.offsetLeft, | |
//We need the entire size, including borders: | |
//https://stackoverflow.com/questions/30379924/understanding-offsetwidth-and-clientwidth-when-css-border-box-is-active | |
// width: box.clientWidth, | |
// height: box.clientHeight, | |
width: box.offsetWidth, | |
height: box.offsetHeight, | |
}, | |
}; | |
//debugPos(start.client.topLeft); | |
} | |
function dragMove(e) { | |
if(!mode) { return; } | |
e.preventDefault(); | |
e.stopPropagation(); | |
const css = box.style, | |
mousePoint = { | |
x: e.clientX, | |
y: e.clientY | |
}; | |
if (mode === 'rotate') { | |
const boxPivot = start.client.center, | |
startRads = start.memoRads || (start.memoRads = calcRads(start.client.mousePos, boxPivot) - Number(box.dataset.dragRotation || 0)), | |
currRads = calcRads(mousePoint, boxPivot), | |
rotation = currRads - startRads; | |
box.dataset.dragRotation = rotation; | |
css.transform = "rotate(" + (rotation) + "rad)"; | |
} | |
else if (mode === 'move') { | |
let mouseDiff = { | |
x: mousePoint.x - start.client.mousePos.x, | |
y: mousePoint.y - start.client.mousePos.y | |
}; | |
var parentRotation = start.client.rotation.accumParent; | |
if(parentRotation) { | |
//If this is a box inside another rotated box, we must rotate the movement the other way: | |
mouseDiff = rotatePoint(mouseDiff, -parentRotation); | |
} | |
css.top = start.local.top + mouseDiff.y + "px"; | |
css.left = start.local.left + mouseDiff.x + "px"; | |
} | |
else if (mode === 'resize') { | |
const topLeft = start.client.topLeft, | |
rotation = start.client.rotation; | |
const localMousePoint = rotatePoint(mousePoint, -rotation.accumSelf, topLeft), | |
localMouseW = localMousePoint.x - topLeft.x, | |
localMouseH = localMousePoint.y - topLeft.y, | |
sizeOffset = start.memoSize || (start.memoSize = { width: start.local.width - localMouseW, height: start.local.height - localMouseH }), | |
newW = Math.max(0, localMouseW + sizeOffset.width), | |
newH = Math.max(0, localMouseH + sizeOffset.height); | |
css.width = newW + 'px'; | |
css.height = newH + 'px'; | |
//When we change the size, we also shift the box' pivot point (transform-origin: center;), | |
//so the top-left corner will move if there is a rotation on this box: | |
const newCenter = { | |
x: (newW - start.local.width)/2, | |
y: (newH - start.local.height)/2, | |
}, | |
targetCenter = rotatePoint(newCenter, rotation.self); | |
css.top = start.local.top + (targetCenter.y-newCenter.y) + "px"; | |
css.left = start.local.left + (targetCenter.x-newCenter.x) + "px"; | |
} | |
//_stats.update(); | |
} | |
function dragEnd(e) { | |
mode = ''; | |
} | |
/* Geometry utils */ | |
function calcRads(point, center) { | |
let radians = Math.PI/2 + Math.atan((center.x - point.x) / (point.y - center.y)); | |
if ((point.y - center.y) < 0) { radians += Math.PI; } | |
return radians; | |
} | |
//Rotating a point about another point (2D) | |
//https://stackoverflow.com/a/32376643/1869660 | |
function rotatePoint(point, radians, center) { | |
center = center || { x: 0, y: 0 }; | |
const dx = point.x - center.x, dy = point.y - center.y, | |
sin = Math.sin(radians), cos = Math.cos(radians); | |
return { | |
x: (cos * dx) - (sin * dy) + center.x, | |
y: (sin * dx) + (cos * dy) + center.y | |
}; | |
} | |
} | |
Array.from(document.querySelectorAll('.box')).forEach(box => { | |
const handles = Array.from(box.querySelectorAll('.handle')).filter(handle => handle.parentElement === box); | |
makeDraggable(box, { | |
rotate: handles[0], | |
resize: handles[1], | |
move: handles[2], | |
}); | |
}); | |
function debugPos(pos) { | |
var css = document.querySelector('#debug').style; | |
css.top = pos.y + 'px'; | |
css.left = pos.x + 'px'; | |
} | |
/* | |
//https://cdn.rawgit.com/mrdoob/stats.js/r17/build/stats.min.js | |
var _stats = new Stats(); | |
_stats.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom | |
document.body.appendChild( _stats.dom ); | |
*/ |
body { | |
background: gainsboro; | |
#debug { | |
position: fixed; | |
width: 2px; | |
height: 2px; | |
background: red; | |
} | |
} | |
.box { | |
top: 60px; | |
left: 60px; | |
&.outer { | |
width: 600px; | |
height: 400px; | |
border: 2px dashed skyblue; | |
background: aliceblue; | |
} | |
&.inner { | |
width: 250px; | |
height: 180px; | |
border: 2px solid green; | |
background: rgba(lime, .5); | |
& ul { | |
list-style: none; | |
margin: 0; | |
padding: 0; | |
li { | |
text-indent: 1em; | |
margin-bottom: .5em; | |
} | |
} | |
} | |
&.inner2 { | |
top: 200px; | |
left: 500px; | |
width: 200px; | |
height: 150px; | |
border: 2px solid salmon; | |
background: rgba(salmon, .5); | |
} | |
} | |
.drag-move { | |
cursor: move; | |
} | |
.drag-rotate { | |
cursor: grab; //IE/Edge fallback | |
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='31' height='31' viewBox='0 0 31 31' %3E %3Cpath d='M5.5,15.5 a10,10 0 1,1 20,0 h3.5 l-5,5 -5,-5 h3.5 a5,5 0 1,0 -14,0 h3.5 l-5,5 -5,-5 z' stroke='black' stroke-width='1' fill='white' transform='rotate(45 15 15)' /%3E %3C/svg%3E") 15 15, grab; | |
} | |
.drag-resize { | |
cursor: nw-resize; | |
} | |
//Reset style for nested non-draggable boxes | |
:not(drag-move) { | |
cursor: auto; | |
} | |
.handle { | |
position: absolute; | |
box-sizing: border-box; | |
background: rgba(white, .5); | |
&::after { | |
box-sizing: border-box; | |
} | |
&.drag-move { | |
top:0; left:0; | |
width: 25px; | |
height: 25px; | |
margin: -15px; | |
overflow: hidden; | |
transform: rotate(45deg); | |
&::after { | |
content: ''; | |
position: absolute; | |
display: block; | |
top:-6px; left:-6px; bottom:-6px; right:-6px; | |
border: 5px solid currentColor; | |
transform: rotate(45deg); | |
} | |
} | |
&.drag-rotate { | |
top:0; right:0; | |
width: 30px; | |
height: 30px; | |
margin: -15px; | |
border: 2px dashed; | |
border-color: currentColor currentColor transparent transparent; | |
border-radius: 100%; | |
} | |
&.drag-resize { | |
bottom:0; right:0; | |
width: 25px; | |
height: 25px; | |
margin: -10px; | |
border: 2px dashed; | |
border-color: transparent currentColor currentColor transparent; | |
} | |
} |