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

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

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

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.parentElement
let 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)+' % '

👋👋👋👋👋👋👋👋👋👋👋👋👋👋👋

Solution

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.parentElement
function toNumber(val){
return Number(val.replace('px'))
}let dx = event.clientX - this.previousCursorPosition.x
let dy = event.clientY - this.previousCursorPosition.y
this.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

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.parentElement
function toNumber(val){
return Number(val.replace('%'))
}
let dx = event.clientX - this.previousCursorPosition.x
let dy = event.clientY - this.previousCursorPosition.y
function 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 + dx
let percentagePosition = pixelToPercentage(left,top)this.element.style.top = percentagePosition.y+'%'
this.element.style.left = percentagePosition.x+'%'
}mouseRemoved(){
this.mouseDown = false
}
}

More resources

https://www.programmersought.com/article/5562254337/

Finding Magic.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store