The difference between offsetTop, scrollTop & clientTop (& why you should never use them)
The cursor position is accessed by clientX and clientY from the event argument, this is not dependent on scroll
At the top left corner, it is 0,0 and on the bottom right corner it is max, max
The position of the element can be accessed in terms of the scroll by offsetTop and offsetLeft, offset signifies that it respects padding and border width
clientTop and clientLeft don’t respect border-width but they do respect padding
Evidently offsetTop and offsetLeft are read-only property
scrollTop is the vertical scroll distance inside that element, scrollLeft is the same for horizontal scroll
pageXOffset is the horizontal scroll distance
pageYOffset is the vertical scroll distance
offsetTop is read-only, while scrollTop is read/write.
clientWidth = width + padding
clientHeight = height + padding
offsetWidth = width + padding + border
offsetHeight = height + padding + border
Let’s implement a drag system with the gained knowledge
As event.clientX and event.ClientY values are not dependent on scroll and element.offsetTop and offsetLeft are dependent on the scroll. It is required to convert the cursor position to scroll dependent variable or offset position to scroll independently.
Let’s try the latter
element.style.top = event.clientY -(parent.offsetTop- pageYOffset )element.style.left= event.clientX -(parent.offsetLeft- pageXOffset )
percentage positioning
element.style.top = (((event.clientY -(parent.offsetTop -pageYOffset ) )/parent.offsetHeight) * 100)+'%'element.style.left = you get the idea
The Alternative
offsetTop has a bug, making it’s value suddenly become 0, same with offset left
element.getBoundingClientRect provides a better alternative because it’s free from bugs (duh!) & it is scroll independent so less code
let rect = parent.getBoundingClientRect();element.style.top = event.clientY - rect.top + 'px'element.style.left = event.clientX - rect.left +'px'
percentage positioning
let rect = parent.getBoundingClientRect();element.style.top=((event.clientY - rect.top)/parent.offsetHeight) * 100+'%'element.style.left=((event.clientX-rect.left)/parent.offsetWidth) *100+'%'
One more Problem
The element being dragged will snap to the cursor position, it will look rough
We must keep the offset of the position cursor, but how?
cursor position relative to parent - position of element relative to the parentsubtract this offset position from new element positionON MOUSE DOWN ---------------------------------------let parentRect = event.target.parentElement.getBoundingClientRect();let childRect = event.target.getBoundingClientRect();
offsetX = (event.clientX - parentRect.left) - (childRect.left - parentRect.left)offsetY = (event.clientY - parentRect.top) - (childRect.top - parentRect.top)ON MOUSE MOVE ---------------------------------------
let parent = event.target.parentElementlet rect = parent.getBoundingClientRect();element.style.top = ((((event.clientY - rect.top)-this.offsetY )/parent.offsetHeight) * 100)+' % 'element.style.left = ((((event.clientX - rect.left ) -this.offsetX)/parent.offsetWidth) *100)+' % '
👋👋👋👋👋👋👋👋👋👋👋👋👋👋👋
Not yet, there is still one more problem. If element is scaled or any other transform is applied, the abrupt snap will come hunting
Solution
use change in the cursor position, it will also decrease code as we on longer to calculate offset between cursor and element
class draggable{constructor(element){
this.element = element
element.addEventListener('click',this.mouseClicked.bind(this))
element.addEventListener('mousemove',this.mouseMoving.bind(this))
element.addEventListener('mouseup',this.mouseRemoved.bind(this)) }setPreviousCursorPosition(event){
this.previousCursorPosition.x = event.clientX
this.previousCursorPosition.y = event.clientY
}mouseClicked(event){
this.setPreviousCursorPosition(event)
this.mouseDown = true
this.selectedOverlayIndex = event.target.dataset.index
}mouseMoving(event){if(!this.mouseDown) return
let parent = event.target.parentElementfunction toNumber(val){
return Number(val.replace('px'))}let dx = event.clientX - this.previousCursorPosition.x
let dy = event.clientY - this.previousCursorPosition.ythis.element.style.top = (toNum(element.style.top) + dy) + 'px'
this.element.style.left = (toNum(element.style.left) + dx) + 'px'}mouseRemoved(){ this.mouseDown = false}}
But the position of an element would not adapt if the container size changes. so we need to change px to %
solution
- We need to convert percentage value to pixels
y: parent.offsetWidth*(percentageX/100)x: parent.offsetHeight*(percentageY/100)
- Add dy and dx to the new position we got from above
- after adding dy and dx to corresponding values convert them to percentage with respect to the element’s parent height and width
y: ( afterAddingdy/parent.offsetHeight) * 100x: ( afterAddingdx/parent.offsetWidth) * 100
Whole & Final Code
class draggable{constructor(element){
this.element = element
element.addEventListener('click',this.mouseClicked.bind(this))
element.addEventListener('mousemove',this.mouseMoving.bind(this))
element.addEventListener('mouseup',this.mouseRemoved.bind(this))}setPreviousCursorPosition(event){
this.previousCursorPosition.x = event.clientX
this.previousCursorPosition.y = event.clientY
}mouseClicked(event){
this.setPreviousCursorPosition(event)
this.mouseDown = true
this.selectedOverlayIndex = event.target.dataset.index
}mouseMoving(event){if(!this.mouseDown) return
let parent = event.target.parentElementfunction toNumber(val){
return Number(val.replace('%'))
}let dx = event.clientX - this.previousCursorPosition.x
let dy = event.clientY - this.previousCursorPosition.yfunction percentageToPixel(x,y){
return {
x:parent.offsetWidth*(x/100),
y:parent.offsetHeight*(y/100)
}
}function pixelToPercentage(x,y){
return {
y:( y /parent.offsetHeight) * 100,
x:( x /parent.offsetWidth) * 100
}
}let pixelPosition = percentageToPixel(toNumber(element.style.left, toNumber(element.style.top))let top = pixelPosition.y + dy
let left = pixelPosition.x + dxlet percentagePosition = pixelToPercentage(left,top)this.element.style.top = percentagePosition.y+'%'
this.element.style.left = percentagePosition.x+'%'}mouseRemoved(){
this.mouseDown = false
}}