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; | |
| } | |
| } |