interface GrabListener { onMotion ( dx , dy ) : void ; onGrabReleased ( ) : void ; } interface GrabOptions { takePointerLock : boolean ; useGrabbingCursor : boolean ; releaseOnMouseUp : boolean ; grabElement ? : HTMLElement ; } function containsElement ( sub_ , searchFor ) { let sub = sub_ ; while ( sub !== null ) { if ( sub === searchFor ) return true ; sub = sub . parentElement ; } return false ; } function setGlobalCursor ( cursor ) { if ( document . body . style . cursor !== cursor ) { document . body . focus ( ) ; document . body . style . cursor = cursor ; } } export class GrabManager { private grabListener : GrabListener | null = null ; private grabOptions : GrabOptions | null = null ; private currentGrabTarget : HTMLElement | null = null ; private usingPointerLock : boolean = false ; private lastX : number = - 1 ; private lastY : number = - 1 ; private grabButton : number = - 1 ; private _onMouseMove = ( e ) => { if ( this . grabListener === null ) return ; let dx , dy ; if ( e . movementX !== undefined ) { dx = e . movementX ; dy = e . movementY ; } else { dx = e . pageX - this . lastX ; dy = e . pageY - this . lastY ; this . lastX = e . pageX ; this . lastY = e . pageY ; } this . grabListener . onMotion ( dx , dy ) ; } ; private _onMouseDown = ( e ) => { const grabElement = this . grabOptions ! . grabElement ; if ( ! grabElement || ! containsElement ( e . target as HTMLElement , grabElement ) ) this . releaseGrab ( ) ; } ; private _onMouseUp = ( e ) => { if ( e . button === this . grabButton ) this . releaseGrab ( ) ; } ; private _onPointerLockChange = ( e ) => { if ( document . pointerLockElement === this . currentGrabTarget ) { this . usingPointerLock = true ; } else { this . releaseGrab ( ) ; } } ; private _onPointerLockError = ( e ) => { if ( this . grabOptions !== null && this . grabOptions . useGrabbingCursor ) setGlobalCursor ( 'grabbing' ) ; } ; public getGrabListenerOptions ( grabListener ) { if ( this . grabListener === grabListener ) return this . grabOptions ; else return null ; } public isGrabbed ( ) { return this . grabListener !== null ; } public takeGrab ( grabListener , e , grabOptions ) { e . preventDefault ( ) ; if ( this . grabListener !== null ) return ; this . grabListener = grabListener ; this . grabOptions = grabOptions ; this . lastX = e . pageX ; this . lastY = e . pageY ; this . grabButton = e . button ; const target = e . target as HTMLElement ; target . focus ( ) ; this . usingPointerLock = false ; if ( grabOptions . takePointerLock && target . requestPointerLock !== undefined ) { document . addEventListener ( 'pointerlockchange' , this . _onPointerLockChange ) ; document . addEventListener ( 'pointerlockerror' , this . _onPointerLockError ) ; target . requestPointerLock ( ) ; this . currentGrabTarget = target ; } document . addEventListener ( 'mousemove' , this . _onMouseMove ) ; if ( grabOptions . releaseOnMouseUp ) document . addEventListener ( 'mouseup' , this . _onMouseUp ) ; else document . addEventListener ( 'mousedown' , this . _onMouseDown , { capture : true } ) ; } public releaseGrab ( ) { document . removeEventListener ( 'mousemove' , this . _onMouseMove ) ; document . removeEventListener ( 'mouseup' , this . _onMouseUp ) ; document . removeEventListener ( 'mousedown' , this . _onMouseDown , { capture : true } ) ; if ( this . currentGrabTarget !== null ) { document . removeEventListener ( 'pointerlockchange' , this . _onPointerLockChange ) ; if ( document . exitPointerLock !== undefined ) document . exitPointerLock ( ) ; } if ( this . grabOptions ! . useGrabbingCursor && ! this . usingPointerLock ) setGlobalCursor ( '' ) ; const grabListener = this . grabListener ! ; this . grabListener = null ; grabListener . onGrabReleased ( ) ; this . grabOptions = null ; } } export const GlobalGrabManager = new GrabManager ( ) ;