11import { App , Modal } from "obsidian" ;
2+ import { clamp } from "@radix-ui/number" ;
23
34function getClientPoint ( e : PointerEvent | TouchEvent ) {
45 if ( e . type . startsWith ( "touch" ) ) {
@@ -26,13 +27,13 @@ function isPointOnText(e: PointerEvent | TouchEvent, doc: Document): boolean {
2627 let offset : number | null = null ;
2728
2829 // Chromium/Firefox-ish
29- const caretPos = ( doc as any ) . caretPositionFromPoint ?.( pt . x , pt . y ) ;
30+ const caretPos = doc . caretPositionFromPoint ?.( pt . x , pt . y ) ;
3031 if ( caretPos ?. offsetNode ) {
3132 offsetNode = caretPos . offsetNode ;
3233 offset = typeof caretPos . offset === "number" ? caretPos . offset : null ;
3334 } else {
3435 // Safari/WebKit
35- const caretRange = ( doc as any ) . caretRangeFromPoint ?.( pt . x , pt . y ) ;
36+ const caretRange = doc . caretRangeFromPoint ?.( pt . x , pt . y ) ;
3637 if ( caretRange ?. startContainer ) {
3738 offsetNode = caretRange . startContainer ;
3839 offset = typeof caretRange . startOffset === "number" ? caretRange . startOffset : null ;
@@ -67,14 +68,13 @@ export class FloatingModal extends Modal {
6768 private dragging = false ;
6869 private offsetX = 0 ;
6970 private offsetY = 0 ;
70- private pointerDownHandler : ( e : PointerEvent | TouchEvent ) => void ;
71- private pointerMoveHandler : ( e : PointerEvent | TouchEvent ) => void ;
72- private pointerUpHandler : ( ) => void ;
71+ private readonly pointerDownHandler : ( e : PointerEvent | TouchEvent ) => void ;
72+ private readonly pointerMoveHandler : ( e : PointerEvent | TouchEvent ) => void ;
73+ private readonly pointerUpHandler : ( ) => void ;
7374
7475 private disableKeyCapture = true ; // new flag: when true, let keystrokes pass through to workspace
75- private previousActive : HTMLElement | null = null ; // stores element focused before opening
76- private escListener : ( e : KeyboardEvent ) => void ;
77- private modalKeydownStopHandler : ( e : KeyboardEvent ) => void ; // store handler so we can remove it
76+ private previousActive ?: HTMLElement = null ; // stores element focused before opening
77+ private readonly modalKeydownStopHandler : ( e : KeyboardEvent ) => void ; // store handler so we can remove it
7878 private ownerWindow : Window = window ;
7979 private ownerDocument : Document = document ;
8080
@@ -84,24 +84,30 @@ export class FloatingModal extends Modal {
8484 this . pointerDownHandler = this . handlePointerDown . bind ( this ) ;
8585 this . pointerMoveHandler = this . handlePointerMove . bind ( this ) ;
8686 this . pointerUpHandler = this . handlePointerUp . bind ( this ) ;
87- this . escListener = this . handleEscKey . bind ( this ) ;
8887 this . modalKeydownStopHandler = ( ev : KeyboardEvent ) => ev . stopPropagation ( ) ;
89- }
9088
91- private handlePointerDown ( e : PointerEvent | TouchEvent ) : void {
92- // Get the target element
93- const target = e . target as HTMLElement ;
89+ this . setDimBackground ( false ) ;
90+ }
9491
95- // Ignore if clicking on interactive elements
96- if (
92+ protected shouldNotStartDrag ( target : HTMLElement , event : PointerEvent | TouchEvent ) : boolean {
93+ return Boolean (
9794 target instanceof HTMLInputElement ||
9895 target instanceof HTMLTextAreaElement ||
9996 target instanceof HTMLSelectElement ||
10097 target instanceof HTMLButtonElement ||
101- isPointOnText ( e , this . ownerDocument ) ||
98+ isPointOnText ( event , this . ownerDocument ) ||
10299 target . closest ( ".clickable-icon" ) ||
103- target . closest ( ".modal-close-button" ) // ensure close button never starts drag
104- ) {
100+ // ensure close button never starts drag
101+ target . closest ( ".modal-close-button" )
102+ )
103+ }
104+
105+ private handlePointerDown ( e : PointerEvent | TouchEvent ) : void {
106+ // Get the target element
107+ const target = e . target as HTMLElement ;
108+
109+ // Ignore if clicking on interactive elements
110+ if ( this . shouldNotStartDrag ( target , e ) ) {
105111 return ;
106112 }
107113
@@ -149,25 +155,18 @@ export class FloatingModal extends Modal {
149155 e . preventDefault ( ) ;
150156 if ( e . type === "touchmove" ) e . stopPropagation ( ) ;
151157
152- let clientX , clientY ;
158+ const { clientX, clientY } = ( e . type === "touchmove" ) ?
159+ ( e as TouchEvent ) . touches [ 0 ] : e as PointerEvent ;
153160
154- if ( e . type === "touchmove" ) {
155- const touch = ( e as TouchEvent ) . touches [ 0 ] ;
156- clientX = touch . clientX ;
157- clientY = touch . clientY ;
158- } else {
159- const pointerEvent = e as PointerEvent ;
160- clientX = pointerEvent . clientX ;
161- clientY = pointerEvent . clientY ;
162- }
163-
164- const x = clientX - this . offsetX ;
165- const y = clientY - this . offsetY ;
161+ // Prevent moving the modal offscreen
162+ const { width, height } = modalEl . getBoundingClientRect ( ) ;
163+ const margin = 8 ;
164+ const x = clamp ( clientX - this . offsetX , [ margin , this . ownerWindow . innerWidth - width - margin ] ) ;
165+ const y = clamp ( clientY - this . offsetY , [ margin , this . ownerWindow . innerHeight - height - margin ] ) ;
166166
167167 // Position the modal element
168168 modalEl . style . left = `${ x } px` ;
169169 modalEl . style . top = `${ y } px` ;
170- modalEl . style . transform = "none" ; // Remove centering transform
171170 }
172171
173172 private handlePointerUp ( ) : void {
@@ -180,13 +179,6 @@ export class FloatingModal extends Modal {
180179 this . ownerDocument . removeEventListener ( "touchcancel" , this . pointerUpHandler ) ;
181180 }
182181
183- private handleEscKey ( e : KeyboardEvent ) {
184- if ( e . key === "Escape" ) {
185- e . stopPropagation ( ) ;
186- this . close ( ) ;
187- }
188- }
189-
190182 open ( ) : void {
191183 super . open ( ) ;
192184 this . ownerDocument = this . modalEl . ownerDocument ?? document ;
@@ -195,20 +187,17 @@ export class FloatingModal extends Modal {
195187 if ( this . disableKeyCapture ) {
196188 this . previousActive = this . ownerDocument . activeElement as HTMLElement | null ;
197189 try {
198- // @ts -ignore pop modal's key scope so keys are not intercepted
190+ // Release modal scope so focus and key handling can return to the workspace.
191+ // @ts -ignore
199192 this . app . keymap . popScope ( this . scope ) ;
200193 } catch { }
201- try {
202- // @ts -ignore prevent automatic selection / focus restoration
203- this . shouldRestoreSelection = false ;
204- } catch { }
194+ // prevent automatic selection / focus restoration
195+ this . shouldRestoreSelection = false ;
205196 }
206197 setTimeout ( ( ) => {
207- //@ts -ignore
208- const { containerEl, modalEl, bgEl, headerEl } = this ;
209- containerEl . style . pointerEvents = "none" ;
210- if ( bgEl ) bgEl . style . display = "none" ;
211- if ( headerEl ) headerEl . style . pointerEvents = "none" ;
198+ // @ts -ignore
199+ const { containerEl, modalEl, bgEl } = this ;
200+ containerEl . addClass ( "mod-excalidraw-draggable" )
212201
213202 // Set initial position and make modal draggable
214203 if ( modalEl ) {
@@ -224,9 +213,6 @@ export class FloatingModal extends Modal {
224213 modalEl . style . left = `${ centerX } px` ;
225214 modalEl . style . top = `${ centerY } px` ;
226215 modalEl . style . transform = "none" ;
227- const modalStyle = this . ownerWindow . getComputedStyle ( modalEl ) ;
228- modalEl . style . borderBottomLeftRadius = modalStyle . borderTopLeftRadius ;
229- modalEl . style . borderBottomRightRadius = modalStyle . borderTopRightRadius ;
230216
231217 // Add event listeners for both pointer and touch events
232218 modalEl . addEventListener ( "pointerdown" , this . pointerDownHandler as ( e : PointerEvent ) => void ) ;
@@ -237,15 +223,19 @@ export class FloatingModal extends Modal {
237223 if ( this . disableKeyCapture ) {
238224 // Prevent the modal from stealing focus
239225 modalEl . setAttr ( "tabindex" , "-1" ) ;
226+
227+ // In order to propagate keypresses, modal container and backdrop
228+ // need to ignore (and thus propagate) any pointerEvents.
229+ containerEl . style . pointerEvents = "none" ;
230+ if ( bgEl ) bgEl . style . pointerEvents = "none" ;
231+
240232 // Refocus previous element (if still in DOM)
241233 if ( this . previousActive ?. isConnected ) {
242234 this . previousActive . focus ( { preventScroll : true } ) ;
243235 }
244236 // Stop key events originating inside the modal from bubbling back
245237 modalEl . addEventListener ( "keydown" , this . modalKeydownStopHandler , { capture : true } ) ;
246238 }
247- // Add ESC listener (capture to run before underlying workspace)
248- this . ownerDocument . addEventListener ( "keydown" , this . escListener , { capture : true } ) ;
249239
250240 // NEW: re-enable pointer events on the close button so it is tappable on mobile
251241 const closeBtn = containerEl . querySelector ( ".modal-close-button" ) ;
@@ -271,7 +261,6 @@ export class FloatingModal extends Modal {
271261 }
272262 // Remove any remaining document event listeners
273263 this . handlePointerUp ( ) ;
274- this . ownerDocument . removeEventListener ( "keydown" , this . escListener , { capture : true } ) ;
275264
276265 super . close ( ) ;
277266 }
0 commit comments