I am trying to build a site for my client, which is not dissimilar to Miro (or other whiteboard apps).
The concept is that individual elements are laid out using absolute positioning but the admin can drag them in the viewport and set width and height properties for each element.
I cannot get any method to work that will allow dragging below the original window height (even though the entire document has elements with heights set greater than the window height).
I have tried 3 methods:
const draggingClass = 'dragging';
class draggable {
constructor(element, node = [ document ] ) {
this.node = node;
this.el = element;
if ( this.el ) {
this.mouseDown = this.mouseDown.bind(this);
this.mouseUp = this.mouseUp.bind(this);
this.mouseMove = this.mouseMove.bind(this);
}
}
setHandle( handle ) {
this.handle = handle;
return this;
}
setCallback( callback ) {
this.callback = callback;
return this;
}
mouseDown( event ) {
if ( this.callback?.start ) this.callback.start( event, this.el )
else this.el.classList.add(draggingClass)
for ( const node of this.node ) {
node.addEventListener('mouseup', this.mouseUp);
node.addEventListener('mousemove', this.mouseMove);
node.addEventListener('mouseleave', this.mouseUp);
}
this.el.addEventListener('mouseleave', this.mouseUp);
}
mouseUp( event ) {
if ( this.callback?.end ) this.callback.end( event, this.el )
else this.el.classList.remove(draggingClass)
for ( const node of this.node ) {
node.removeEventListener('mouseup', this.mouseUp);
node.removeEventListener('mousemove', this.mouseMove);
node.removeEventListener('mouseleave', this.mouseUp);
}
this.el.removeEventListener('mouseleave', this.mouseUp);
}
mouseMove(event) {
if ( this.callback?.move ) {
this.callback.move( event, this.el );
} else {
const style = window.getComputedStyle(this.el);
const x = (parseFloat(style.getPropertyValue('left')) || 0) + event.movementX;
const y = (parseFloat(style.getPropertyValue('top')) || 0) + event.movementY;
const rect = this.el.getBoundingClientRect();
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
const viewWidth = window.innerWidth || document.documentElement.clientWidth;
const maxX = viewWidth - rect.width;
const maxY = viewHeight - rect.height;
const constrainedX = Math.max(0, Math.min(x, maxX));
const constrainedY = Math.max(0, Math.min(y, maxY));
this.el.style.position = 'absolute';
this.el.style.top = constrainedY + 'px';
this.el.style.left = constrainedX + 'px';
}
}
run() {
const handle = this.handle ?? this.el
handle.addEventListener('mousedown', this.mouseDown)
}
stop() {
const handle = this.handle ?? this.el
handle.removeEventListener('mousedown', this.mouseDown)
}
}
const boxes = document.querySelectorAll('.box');
const movable1 = new draggable(boxes[0]);
const movable2 = new draggable(boxes[1]);
movable1.run();
movable2.run();
body {
font-family: monospace;
}
.wrapper {
min-height: 400vh;
background-color: rgb(255 0 0 / 10%);
top: 10px;
right: 10px;
bottom: 10px;
left: 10px;
overflow-y: scroll;
}
.box {
position: absolute;
padding: 10px;
resize: both;
height: 150px;
width: 150px;
margin-bottom: 20px;
background: #f0f0f0;
border: 2px solid #555;
overflow: auto;
background-image: linear-gradient(-45deg, #ccc 10px, transparent 10px);
}
.second {
top: 50px;
left: 50px;
}
.third {
top: 100px;
left: 100px;
}
.heading {
background: red;
padding: 5px 10px;
}
.dragging {
opacity: 0.9;
border: 2px dashed #8181b9;
user-select: none;
z-index: 2;
cursor: move;
}
.actions {
display: flex;
flex-direction: column;
position: absolute;
right: 0;
z-index: 1;
}
<div class="outer">
<div class="wrapper">
<div class="box">
I am box 1
</div>
<div class="box second">
I am box 2
</div>
<div class="box second third dragging">
I am box 3
</div>
</div>
</div>
HTML is same as above, but using this CSS
html.no-scroll {
overflow-y: hidden;
}
.outer {
position: fixed;
top: 0;
bottom: 0;
width: 100%;
}
.wrapper {
height: 100%;
background-color: rgb(255 0 0 / 10%);
top: 10px;
right: 10px;
bottom: 10px;
left: 10px;
overflow-y: scroll;
}
.box {
position: relative; // I also added a JS toggle to switch position from relative to absolute
...
}
Similar HTML and CSS to the previous examples but here is some JS
let vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
const trigger = document.querySelector('.actions button');
trigger.addEventListener('click', () => {
let root = document.querySelector(':root');
let docHeight = document.documentElement.clientHeight;
let winHeight = window.innerHeight;
docHeight = 1200; // also tried `${1200}px`
winHeight = 1300; // also tried `${1300}px`
root.style.height = `${1400}px`;
console.log(docHeight, winHeight, root.style.height);
console.log(vh);
});
I also tried adding an element below the draggable wrapper. As well as setting height properties on the html and body. It seems nothing will let the user drag the boxes below the original window height. Unless I am missing some easy css fix.
The JSFiddles contain all of the other functionality (dragging boxes) which is working as is, but I have added a code snippet to the first example, which would be the easiest to figure out as I assume this could be pure CSS.
Funnily enough if you google this topic there seem to be a glut of answers about limiting the dragging to inside a specific container.
I would rather avoid a library if possible.
https://www.google.com/search?q=javascript+drag+item+beyond+window+height+site%3Astackoverflow.com
Any help much appreciated.
If I understand your problem correctly, it is just the way how this behavior is described in your code.
Please look at how I disabled constraints in your mouseMove(event)
method and replaced constrained...
with x/y, and now everything works:
// const constrainedX = Math.max(0, Math.min(x, maxX));
// const constrainedY = Math.max(0, Math.min(y, maxY));
this.el.style.position = 'absolute';
this.el.style.top = y + 'px';
this.el.style.left = x + 'px';
Please let me know if this helps.
const draggingClass = 'dragging';
class draggable {
constructor(element, node = [ document ] ) {
this.node = node;
this.el = element;
if ( this.el ) {
this.mouseDown = this.mouseDown.bind(this);
this.mouseUp = this.mouseUp.bind(this);
this.mouseMove = this.mouseMove.bind(this);
}
}
setHandle( handle ) {
this.handle = handle;
return this;
}
setCallback( callback ) {
this.callback = callback;
return this;
}
mouseDown( event ) {
if ( this.callback?.start ) this.callback.start( event, this.el )
else this.el.classList.add(draggingClass)
for ( const node of this.node ) {
node.addEventListener('mouseup', this.mouseUp);
node.addEventListener('mousemove', this.mouseMove);
node.addEventListener('mouseleave', this.mouseUp);
}
this.el.addEventListener('mouseleave', this.mouseUp);
}
mouseUp( event ) {
if ( this.callback?.end ) this.callback.end( event, this.el )
else this.el.classList.remove(draggingClass)
for ( const node of this.node ) {
node.removeEventListener('mouseup', this.mouseUp);
node.removeEventListener('mousemove', this.mouseMove);
node.removeEventListener('mouseleave', this.mouseUp);
}
this.el.removeEventListener('mouseleave', this.mouseUp);
}
mouseMove(event) {
if ( this.callback?.move ) {
this.callback.move( event, this.el );
} else {
const style = window.getComputedStyle(this.el);
const x = (parseFloat(style.getPropertyValue('left')) || 0) + event.movementX;
const y = (parseFloat(style.getPropertyValue('top')) || 0) + event.movementY;
const rect = this.el.getBoundingClientRect();
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
const viewWidth = window.innerWidth || document.documentElement.clientWidth;
const maxX = viewWidth - rect.width;
const maxY = viewHeight - rect.height;
// const constrainedX = Math.max(0, Math.min(x, maxX));
// const constrainedY = Math.max(0, Math.min(y, maxY));
this.el.style.position = 'absolute';
this.el.style.top = y + 'px';
this.el.style.left = x + 'px';
}
}
run() {
const handle = this.handle ?? this.el
handle.addEventListener('mousedown', this.mouseDown)
}
stop() {
const handle = this.handle ?? this.el
handle.removeEventListener('mousedown', this.mouseDown)
}
}
const boxes = document.querySelectorAll('.box');
const movable1 = new draggable(boxes[0]);
const movable2 = new draggable(boxes[1]);
movable1.run();
movable2.run();
body {
font-family: monospace;
}
.wrapper {
min-height: 400vh;
background-color: rgb(255 0 0 / 10%);
top: 10px;
right: 10px;
bottom: 10px;
left: 10px;
overflow-y: scroll;
}
.box {
position: absolute;
padding: 10px;
resize: both;
height: 150px;
width: 150px;
margin-bottom: 20px;
background: #f0f0f0;
border: 2px solid #555;
overflow: auto;
background-image: linear-gradient(-45deg, #ccc 10px, transparent 10px);
}
.second {
top: 50px;
left: 50px;
}
.third {
top: 100px;
left: 100px;
}
.heading {
background: red;
padding: 5px 10px;
}
.dragging {
opacity: 0.9;
border: 2px dashed #8181b9;
user-select: none;
z-index: 2;
cursor: move;
}
.actions {
display: flex;
flex-direction: column;
position: absolute;
right: 0;
z-index: 1;
}
<div class="outer">
<div class="wrapper">
<div class="box">
I am box 1
</div>
<div class="box second">
I am box 2
</div>
<div class="box second third dragging">
I am box 3
</div>
</div>
</div>