const container = document.querySelector('.container');
const draggables = document.querySelectorAll('.draggable');
draggables.forEach(elem => {
makeDraggableResizable(elem);
elem.addEventListener('mousedown', () => {
const maxZ = Math.max(...[...draggables].map(elem => parseInt(getComputedStyle(elem)['z-index']) || 0));
elem.style['z-index'] = maxZ + 1;
});
});
function makeDraggableResizable(draggable){
const move = (x, y) => {
x = state.fromX + (x - state.startX);
y = state.fromY + (y - state.startY);
// don't allow moving outside the container
if (x < 0) x = 0;
else if (x + draggable.offsetWidth > container.offsetWidth) x = container.offsetWidth - draggable.offsetWidth;
if (y < 0) y = 0;
else if (y + draggable.offsetHeight > container.offsetHeight) y = container.offsetHeight - draggable.offsetHeight;
draggable.style.left = x + 'px';
draggable.style.top = y + 'px';
};
const resize = (x, y) => {
x = state.fromWidth + (x - state.startX);
y = state.fromHeight + (y - state.startY);
// don't allow moving outside the container
if (state.fromX + x > container.offsetWidth) x = container.offsetWidth - state.fromX;
if (state.fromY + y > container.offsetHeight ) y = container.offsetHeight - state.fromY;
draggable.style.width = x + 'px';
draggable.style.height = y + 'px';
};
const listen = (op = 'add') =>
Object.entries(listeners).slice(1)
.forEach(([name, listener]) => document[op + 'EventListener'](name, listener));
const state = new Proxy({}, {
set(state, prop, val){
const out = Reflect.set(...arguments);
const ops = {
startY: () => {
listen();
const style = getComputedStyle(draggable);
[state.fromX, state.fromY] = [parseInt(style.left), parseInt(style.top)];
[state.fromWidth, state.fromHeight] = [parseInt(style.width), parseInt(style.height)];
},
dragY: () => state.action(state.dragX, state.dragY),
stopY: () => listen('remove') + state.action(state.stopX, state.stopY),
};
// use a resolved Promise to postpone the move as a microtask so
// the order of state mutation isn't important
ops[prop] && Promise.resolve().then(ops[prop]);
return out;
}
});
const listeners = {
mousedown: e => Object.assign(state, {startX: e.pageX, startY: e.pageY}),
// here we first provide dragY to check that the order of props is not important
mousemove: e => Object.assign(state, {dragY: e.pageY, dragX: e.pageX}),
mouseup: e => Object.assign(state, {stopX: e.pageX, stopY: e.pageY}),
};
for(const [name, action] of Object.entries({move, resize})){
draggable.querySelector(`.${name}`).addEventListener('mousedown', e => {
state.action = action;
listeners.mousedown(e);
});
}
}
html,body{
height:100%;
margin:0;
padding:0;
}
*{
box-sizing: border-box;
}
.draggable{
position: absolute;
padding:45px 15px 15px 15px;
border-radius:4px;
background:#ddd;
user-select: none;
left: 15px;
top: 15px;
min-width:200px;
}
.draggable>.move{
line-height: 30px;
padding: 0 15px;
background:#bbb;
border-bottom: 1px solid #777;
cursor:move;
position:absolute;
left:0;
top:0;
height:30px;
width:100%;
border-radius: 4px 4px 0;
}
.draggable>.resize{
cursor:nw-resize;
position:absolute;
right:0;
bottom:0;
height:16px;
width:16px;
border-radius: 0 0 4px 0;
background: linear-gradient(to left top, #777 50%, transparent 50%)
}
.container{
left:15px;
top:15px;
background: #111;
border-radius:4px;
width:calc(100% - 30px);
height:calc(100% - 30px);
position: relative;
}
<div class="container">
<div class="draggable"><div class="move">draggable handle</div>Non-draggable content<div class="resize"></div></div>
<div class="draggable" style="left:230px;"><div class="move">draggable handle</div>Non-draggable content<div class="resize"></div></div>
</div>