@@ -28,6 +28,9 @@ type MapProjectionDeps = {
2828 ttlSeconds : number | null ;
2929 permanent : boolean ;
3030 } ) => boolean ;
31+ onDeleteTacticalWaypoint ?: ( payload : {
32+ waypointId : string ;
33+ } ) => boolean ;
3134} ;
3235
3336export function createMapProjection ( deps : MapProjectionDeps ) {
@@ -186,6 +189,37 @@ export function createMapProjection(deps: MapProjectionDeps) {
186189 } ;
187190 }
188191
192+ function bindFloatingMenuInteractions ( menu : HTMLElement ) {
193+ menu . addEventListener ( 'mousedown' , ( e ) => {
194+ e . stopPropagation ( ) ;
195+ } ) ;
196+ menu . addEventListener ( 'click' , ( e ) => {
197+ e . stopPropagation ( ) ;
198+ } ) ;
199+ menu . addEventListener ( 'wheel' , ( e ) => {
200+ e . stopPropagation ( ) ;
201+ } ) ;
202+ menu . addEventListener ( 'contextmenu' , ( e ) => {
203+ e . preventDefault ( ) ;
204+ e . stopPropagation ( ) ;
205+ } , true ) ;
206+ }
207+
208+ function positionFloatingMenu ( menu : HTMLElement , event : MouseEvent ) {
209+ const margin = 12 ;
210+ const menuRect = menu . getBoundingClientRect ( ) ;
211+ let left = event . clientX + 14 ;
212+ let top = event . clientY + 12 ;
213+
214+ const maxLeft = Math . max ( margin , window . innerWidth - menuRect . width - margin ) ;
215+ const maxTop = Math . max ( margin , window . innerHeight - menuRect . height - margin ) ;
216+ left = Math . max ( margin , Math . min ( maxLeft , left ) ) ;
217+ top = Math . max ( margin , Math . min ( maxTop , top ) ) ;
218+
219+ menu . style . left = `${ Math . round ( left ) } px` ;
220+ menu . style . top = `${ Math . round ( top ) } px` ;
221+ }
222+
189223 function openTacticalMenuAtPointer (
190224 map : any ,
191225 event : MouseEvent ,
@@ -254,19 +288,7 @@ export function createMapProjection(deps: MapProjectionDeps) {
254288 syncPreviewFromSelection ( ) ;
255289 } ) ;
256290
257- menu . addEventListener ( 'mousedown' , ( e ) => {
258- e . stopPropagation ( ) ;
259- } ) ;
260- menu . addEventListener ( 'click' , ( e ) => {
261- e . stopPropagation ( ) ;
262- } ) ;
263- menu . addEventListener ( 'wheel' , ( e ) => {
264- e . stopPropagation ( ) ;
265- } ) ;
266- menu . addEventListener ( 'contextmenu' , ( e ) => {
267- e . preventDefault ( ) ;
268- e . stopPropagation ( ) ;
269- } , true ) ;
291+ bindFloatingMenuInteractions ( menu ) ;
270292
271293 ttlSelect . addEventListener ( 'change' , ( ) => {
272294 customRow . style . display = ttlSelect . value === 'custom' ? 'flex' : 'none' ;
@@ -303,19 +325,75 @@ export function createMapProjection(deps: MapProjectionDeps) {
303325
304326 document . body . appendChild ( menu ) ;
305327 tacticalMenuEl = menu ;
328+ positionFloatingMenu ( menu , event ) ;
306329
307- const margin = 12 ;
308- const menuRect = menu . getBoundingClientRect ( ) ;
309- let left = event . clientX + 14 ;
310- let top = event . clientY + 12 ;
330+ tacticalMenuOutsideClickHandler = ( e : MouseEvent ) => {
331+ const target = e . target ;
332+ if ( tacticalMenuEl && target instanceof Node && tacticalMenuEl . contains ( target ) ) {
333+ return ;
334+ }
335+ closeTacticalMenu ( ) ;
336+ } ;
337+ tacticalMenuEscHandler = ( e : KeyboardEvent ) => {
338+ if ( e . key === 'Escape' ) {
339+ closeTacticalMenu ( ) ;
340+ }
341+ } ;
311342
312- const maxLeft = Math . max ( margin , window . innerWidth - menuRect . width - margin ) ;
313- const maxTop = Math . max ( margin , window . innerHeight - menuRect . height - margin ) ;
314- left = Math . max ( margin , Math . min ( maxLeft , left ) ) ;
315- top = Math . max ( margin , Math . min ( maxTop , top ) ) ;
343+ setTimeout ( ( ) => {
344+ if ( tacticalMenuOutsideClickHandler ) {
345+ document . addEventListener ( 'mousedown' , tacticalMenuOutsideClickHandler , true ) ;
346+ }
347+ if ( tacticalMenuEscHandler ) {
348+ document . addEventListener ( 'keydown' , tacticalMenuEscHandler , true ) ;
349+ }
350+ } , 0 ) ;
316351
317- menu . style . left = `${ Math . round ( left ) } px` ;
318- menu . style . top = `${ Math . round ( top ) } px` ;
352+ return true ;
353+ }
354+
355+ function openWaypointDeleteMenuAtPointer ( event : MouseEvent , waypointId : string , waypointLabel : string | null ) {
356+ closeTacticalMenu ( ) ;
357+
358+ const menu = document . createElement ( 'div' ) ;
359+ menu . className = 'nodemc-tactical-menu' ;
360+ menu . innerHTML = `
361+ <div class="nmc-tactical-title">删除战术标点</div>
362+ <label class="nmc-tactical-row">
363+ <span>目标标点</span>
364+ <input class="nmc-tactical-delete-label" type="text" readonly value="${ escapeHtml ( waypointLabel || waypointId ) } " />
365+ </label>
366+ <div class="nmc-tactical-row">
367+ <span>确认删除这个 waypoint 吗?</span>
368+ </div>
369+ <div class="nmc-tactical-actions">
370+ <button type="button" class="nmc-tactical-confirm">确认删除</button>
371+ <button type="button" class="nmc-tactical-cancel">取消</button>
372+ </div>
373+ ` ;
374+
375+ const confirmBtn = menu . querySelector ( '.nmc-tactical-confirm' ) as HTMLButtonElement | null ;
376+ const cancelBtn = menu . querySelector ( '.nmc-tactical-cancel' ) as HTMLButtonElement | null ;
377+ if ( ! confirmBtn || ! cancelBtn ) {
378+ return false ;
379+ }
380+
381+ bindFloatingMenuInteractions ( menu ) ;
382+
383+ cancelBtn . addEventListener ( 'click' , ( ) => {
384+ closeTacticalMenu ( ) ;
385+ } ) ;
386+
387+ confirmBtn . addEventListener ( 'click' , ( ) => {
388+ if ( typeof deps . onDeleteTacticalWaypoint === 'function' ) {
389+ deps . onDeleteTacticalWaypoint ( { waypointId } ) ;
390+ }
391+ closeTacticalMenu ( ) ;
392+ } ) ;
393+
394+ document . body . appendChild ( menu ) ;
395+ tacticalMenuEl = menu ;
396+ positionFloatingMenu ( menu , event ) ;
319397
320398 tacticalMenuOutsideClickHandler = ( e : MouseEvent ) => {
321399 const target = e . target ;
@@ -358,6 +436,28 @@ export function createMapProjection(deps: MapProjectionDeps) {
358436 return openTacticalMenuAtPointer ( map , event , pos ) ;
359437 }
360438
439+ function maybeHandleWaypointDelete ( event : MouseEvent , waypointId : string , waypointLabel : string | null ) {
440+ if ( ! shouldBlockMapLeftRightClick ( ) ) return false ;
441+ if ( ! shouldEnableTacticalMapMarking ( ) ) return false ;
442+ if ( event . button !== 2 ) return false ;
443+ if ( typeof deps . onDeleteTacticalWaypoint !== 'function' ) return false ;
444+
445+ const id = String ( waypointId || '' ) . trim ( ) ;
446+ if ( ! id ) return false ;
447+
448+ return openWaypointDeleteMenuAtPointer ( event , id , waypointLabel ) ;
449+ }
450+
451+ function findWaypointTargetInfo ( target : EventTarget | null ) {
452+ if ( ! ( target instanceof Element ) ) return null ;
453+ const anchor = target . closest ( '[data-nodemc-waypoint-id]' ) ;
454+ if ( ! ( anchor instanceof HTMLElement ) ) return null ;
455+ const waypointId = String ( anchor . dataset . nodemcWaypointId || '' ) . trim ( ) ;
456+ if ( ! waypointId ) return null ;
457+ const waypointLabel = String ( anchor . dataset . nodemcWaypointLabel || '' ) . trim ( ) || null ;
458+ return { waypointId, waypointLabel } ;
459+ }
460+
361461 function shouldBlockMapLeftRightClick ( ) {
362462 return Boolean ( CONFIG . BLOCK_MAP_LEFT_RIGHT_CLICK ) ;
363463 }
@@ -381,6 +481,15 @@ export function createMapProjection(deps: MapProjectionDeps) {
381481 function onGuardedMouseEvent ( event : Event ) {
382482 const mouseEvent = event as MouseEvent ;
383483 if ( ! shouldInterceptMouseEvent ( mouseEvent ) ) return ;
484+ const waypointTarget = findWaypointTargetInfo ( mouseEvent . target ) ;
485+ if ( waypointTarget && maybeHandleWaypointDelete ( mouseEvent , waypointTarget . waypointId , waypointTarget . waypointLabel ) ) {
486+ mouseEvent . preventDefault ( ) ;
487+ mouseEvent . stopPropagation ( ) ;
488+ if ( typeof mouseEvent . stopImmediatePropagation === 'function' ) {
489+ mouseEvent . stopImmediatePropagation ( ) ;
490+ }
491+ return ;
492+ }
384493 maybeHandleTacticalMarkPlacement ( mouseEvent ) ;
385494 mouseEvent . preventDefault ( ) ;
386495 mouseEvent . stopPropagation ( ) ;
@@ -1008,7 +1117,9 @@ export function createMapProjection(deps: MapProjectionDeps) {
10081117 ? `<span class="n-waypoint-icon" style="position:absolute;left:0;top:0;transform:translate(-50%,-50%);background:${ color } ;width:${ visual . iconSize } px;height:${ visual . iconSize } px;display:inline-block;border-radius:50%;line-height:${ visual . iconSize } px;text-align:center;font-size:${ Math . max ( 10 , Math . round ( visual . iconSize * 0.7 ) ) } px;z-index:2;">📍</span>`
10091118 : '' ;
10101119
1011- return `<div class="nodemc-waypoint-anchor" style="position:relative;display:inline-block;white-space:nowrap;">${ textHtml } ${ iconHtml } </div>` ;
1120+ const safeWaypointId = escapeHtml ( String ( waypoint && ( waypoint . id || waypoint . waypointId ) || '' ) ) ;
1121+ const safeWaypointLabel = escapeHtml ( safeName ) ;
1122+ return `<div class="nodemc-waypoint-anchor" data-nodemc-waypoint-id="${ safeWaypointId } " data-nodemc-waypoint-label="${ safeWaypointLabel } " style="position:relative;display:inline-block;white-space:nowrap;">${ textHtml } ${ iconHtml } </div>` ;
10121123 }
10131124
10141125 function upsertWaypoint ( map : any , waypointId : string , payload : any ) {
@@ -1045,7 +1156,7 @@ export function createMapProjection(deps: MapProjectionDeps) {
10451156 const marker = leafletRef . marker ( latLng , {
10461157 icon : leafletRef . divIcon ( { className : '' , html, iconSize : [ 0 , 0 ] , iconAnchor : [ 0 , 0 ] } ) ,
10471158 zIndexOffset,
1048- interactive : false ,
1159+ interactive : true ,
10491160 keyboard : false ,
10501161 } ) ;
10511162
@@ -1300,6 +1411,7 @@ export function createMapProjection(deps: MapProjectionDeps) {
13001411
13011412 nextWaypointIds . add ( String ( wpId ) ) ;
13021413 upsertWaypoint ( map , String ( wpId ) , {
1414+ id : String ( wpId ) ,
13031415 x : resolvedPos . x ,
13041416 z : resolvedPos . z ,
13051417 label : ( data as any ) . label || ( data as any ) . name || ( data as any ) . title || String ( wpId ) ,
0 commit comments