Update thanks the TheWildHealer's response in this SO, we can play with [cdkDragConstrainPosition]
and use cdkDrag
our .html it's equal
<div #container class="container">
<div class="circle" [ngStyle]="styleCircle"></div>
<div #indicator
class="indicator"
cdkDrag (mousedown)="initDrag($event)"
[cdkDragConstrainPosition]="computeDragRenderPos"
data="indicator"
></div>
</div>
And we need also a ViewChild to get the "indicator" and the "container". See that it's necesary use the mouseDown to get the position relative where we are "click"
initDrag(event){
const rect=this.indicator.nativeElement.getBoundingClientRect()
this.incr={x:event.clientX-rect.x-rect.width/2,
y:event.clientY-rect.y-rect.height/2}
}
computeDragRenderPos = (pos, dragRef=null) => {
const rect=this.container.nativeElement.getBoundingClientRect()
const origin={x:this.origin.x+rect.x+this.incr.x,
y:this.origin.y+rect.y+this.incr.y}
const angle = Math.atan2(
pos.y - origin.y,
pos.x - origin.x,
);
return {
x: origin.x + this.radius * Math.cos(angle),
y: origin.y + this.radius * Math.sin(angle),
};
};
You can see in this stackblitz
Old answer
You can not do it using "drag", a "drag" allow move the element free, so you need "play" with mousemove mousedown and mouseup. The idea is using [ngStyle]
to change the position of an "indicator" changing the properties "left" and "top"
Imagine you has an .html like
<div #container class="container">
<div class="circle" [ngStyle]="styleCircle"></div>
<div #indicator class="indicator" (mousedown)="drag()" [ngStyle]="style"></div>
</div>
See that I use two reference variables, one for "container" and another one for "indicator" -the div with class "circle is only to show the circle-
See also that "container" should has position:relative and "indicator" has position:absolute
.container{
position:relative
}
.indicator {
width:3rem;
height: 3rem;
position: absolute;
cursor: move;
background-color:lightsteelblue;
}
Using ViewChild, in ngAfterViewInit we calculate the position of the "container" and the size of indicator (really the size divided by 2)
@ViewChild('indicator',{static:true}) indicator:ElementRef
@ViewChild('container',{static:true}) container:ElementRef
ngAfterViewInit()
{
const { width, height } = this.indicator.nativeElement.getBoundingClientRect()
const { x, y } = this.container.nativeElement.getBoundingClientRect()
this.pos={x:x,y:y}
this.size={width:width/2,height:height/2}
this.origin={x:this.radius,y:this.radius}
})
}
We are going to calculate the style given an x and a y using Math.atan2, Math.cos and Math.sin. As the position is relative to the top-left of the "item", you less the size.with and size.height (really size.with and size.height is the size divided by 2)
calculateStyle(posx:number,posy:number)
{
const angle=Math.atan2(posy,posx);
return {left:this.origin.x-this.size.width+this.radius*Math.cos(angle)+'px',
top:this.origin.y-this.size.height+this.radius*Math.sin(angle)+'px'
}
}
The last is subscribe to event mousemove (while we don't mouseup) when "click" the indicator
drag(){
this.onDrag=true;
fromEvent(this.document,'mousemove').pipe(
takeWhile(()=>this.onDrag)
).subscribe((event:any)=>{
this.style=this.calculateStyle(
event.pageX-this.pos.x-this.origin.x,
event.pageY-this.pos.y-this.origin.y)
})
fromEvent(this.document,'mouseup').pipe(take(1)).subscribe(_=>this.onDrag=false)
}
See that you pass to the function calculateStyle the values taking account the "pos" of container, and the "origin" calculate in the AfterViewInit
You can see in this stackblitz