1
1
import { BindingEventService } from '@slickgrid-universal/binding' ;
2
2
import type { BasePubSubService } from '@slickgrid-universal/event-pub-sub' ;
3
- import { classNameToList , createDomElement , emptyElement , isDefined } from '@slickgrid-universal/utils' ;
3
+ import {
4
+ calculateAvailableSpace ,
5
+ classNameToList ,
6
+ createDomElement ,
7
+ emptyElement ,
8
+ getOffset ,
9
+ getOffsetRelativeToParent ,
10
+ isDefined ,
11
+ } from '@slickgrid-universal/utils' ;
4
12
5
13
import type {
6
14
CellMenu ,
@@ -9,16 +17,18 @@ import type {
9
17
DOMMouseOrTouchEvent ,
10
18
GridMenu ,
11
19
GridMenuItem ,
20
+ GridMenuOption ,
12
21
GridOption ,
13
22
HeaderButton ,
14
23
HeaderButtonItem ,
15
24
HeaderMenu ,
25
+ HeaderMenuOption ,
16
26
MenuCommandItem ,
17
27
MenuOptionItem ,
18
28
} from '../interfaces/index.js' ;
29
+ import { type SlickEventData , SlickEventHandler , type SlickGrid } from '../core/index.js' ;
19
30
import type { ExtensionUtility } from '../extensions/extensionUtility.js' ;
20
31
import type { SharedService } from '../services/shared.service.js' ;
21
- import { SlickEventHandler , type SlickGrid } from '../core/index.js' ;
22
32
23
33
export type MenuType = 'command' | 'option' ;
24
34
export type ExtendableItemTypes = HeaderButtonItem | MenuCommandItem | MenuOptionItem | 'divider' ;
@@ -36,6 +46,7 @@ export class MenuBaseClass<M extends CellMenu | ContextMenu | GridMenu | HeaderM
36
46
protected _menuCssPrefix = '' ;
37
47
protected _menuPluginCssPrefix = '' ;
38
48
protected _optionTitleElm ?: HTMLSpanElement ;
49
+ pluginName = '' ;
39
50
40
51
/** Constructor of the SlickGrid 3rd party plugin, it can optionally receive options */
41
52
constructor (
@@ -351,4 +362,184 @@ export class MenuBaseClass<M extends CellMenu | ContextMenu | GridMenu | HeaderM
351
362
}
352
363
return commandLiElm ;
353
364
}
365
+
366
+ /**
367
+ * Reposition any of the menu plugins (CellMenu, ContextMenu, GridMenu, HeaderMenu) to where the user clicked,
368
+ * it will calculate the best position depending on available space in the viewport and the menu type.
369
+ */
370
+ repositionMenu (
371
+ e : DOMMouseOrTouchEvent < HTMLElement > | SlickEventData ,
372
+ menuElm : HTMLElement ,
373
+ buttonElm ?: HTMLButtonElement ,
374
+ addonOptions ?: GridMenu | CellMenu | ContextMenu | HeaderMenu
375
+ ) : void {
376
+ const targetElm = e . target as HTMLDivElement ; // get header button createElement
377
+ const targetEvent : MouseEvent | Touch = ( e as TouchEvent ) ?. touches ?. [ 0 ] ?? e ;
378
+ const isSubMenu = menuElm . classList . contains ( 'slick-submenu' ) ;
379
+ const rowHeight = this . gridOptions . rowHeight || 0 ;
380
+ const parentElm = isSubMenu
381
+ ? ( ( e . target as HTMLElement ) ! . closest ( '.slick-menu-item' ) as HTMLDivElement )
382
+ : this . pluginName === 'CellMenu' || this . pluginName === 'ContextMenu'
383
+ ? ( e . target ! . closest ( '.slick-cell' ) as HTMLDivElement )
384
+ : ( targetEvent . target as HTMLElement ) ;
385
+
386
+ if ( menuElm && parentElm ) {
387
+ // for Cell/Context Menus we should move to (0,0) coordinates before calculating height/width
388
+ // since it could end up being cropped width values when element is outside browser viewport.
389
+ if ( this . pluginName === 'CellMenu' || this . pluginName === 'ContextMenu' ) {
390
+ menuElm . style . top = `0px` ;
391
+ menuElm . style . left = `0px` ;
392
+ }
393
+
394
+ const containerElm : HTMLElement = this . sharedService . gridContainerElement . classList . contains ( 'slickgrid-container' )
395
+ ? this . sharedService . gridContainerElement
396
+ : ( this . sharedService . gridContainerElement . querySelector ( '.slickgrid-container' ) ?? this . sharedService . gridContainerElement ) ;
397
+ const relativePos = getOffsetRelativeToParent ( containerElm , targetElm ) ;
398
+ const menuWidth = menuElm . offsetWidth ;
399
+ const parentOffset = getOffset ( parentElm ) ;
400
+ let menuOffsetLeft = 0 ;
401
+ let menuOffsetTop = 0 ;
402
+ let dropOffset = 0 ;
403
+ let sideOffset = 0 ;
404
+ let availableSpaceBottom = 0 ;
405
+ let availableSpaceTop = 0 ;
406
+ const { bottom : parentSpaceBottom , top : parentSpaceTop } = calculateAvailableSpace ( parentElm ) ;
407
+
408
+ if ( this . pluginName === 'GridMenu' && buttonElm ) {
409
+ if ( ! isSubMenu ) {
410
+ const buttonComptStyle = getComputedStyle ( buttonElm ) ;
411
+ const buttonWidth = parseInt ( buttonComptStyle ?. width ?? ( addonOptions as GridMenuOption ) ?. menuWidth , 10 ) ;
412
+ const contentMinWidth = ( addonOptions as GridMenuOption ) ?. contentMinWidth ?? 0 ;
413
+ const currentMenuWidth = ( contentMinWidth > menuWidth ? contentMinWidth : menuWidth ) || 0 ;
414
+ if ( contentMinWidth > 0 ) {
415
+ menuElm . style . minWidth = `${ contentMinWidth } px` ;
416
+ }
417
+ const menuIconOffset = getOffset ( buttonElm ) ; // get button offset position
418
+ const nextPositionLeft = menuIconOffset . right ;
419
+ menuOffsetTop = menuIconOffset . top + buttonElm ! . offsetHeight ; // top position has to include button height so the menu is placed just below it
420
+ menuOffsetLeft =
421
+ ( addonOptions as GridMenuOption ) ?. dropSide === 'right' ? nextPositionLeft - buttonWidth : nextPositionLeft - currentMenuWidth ;
422
+ }
423
+ } else if ( this . pluginName === 'CellMenu' || this . pluginName === 'ContextMenu' ) {
424
+ menuOffsetLeft = parentElm && this . pluginName === 'CellMenu' ? parentOffset . left : targetEvent . pageX ;
425
+ menuOffsetTop = parentElm && this . pluginName === 'CellMenu' ? parentOffset . top : targetEvent . pageY ;
426
+ dropOffset = Number ( ( addonOptions as CellMenu | ContextMenu ) ?. autoAdjustDropOffset || 0 ) ;
427
+ sideOffset = Number ( ( addonOptions as CellMenu | ContextMenu ) ?. autoAlignSideOffset || 0 ) ;
428
+ } else {
429
+ menuOffsetLeft = isSubMenu ? parentOffset . left : ( relativePos ?. left ?? 0 ) ;
430
+ menuOffsetTop = isSubMenu
431
+ ? parentOffset . top
432
+ : ( relativePos ?. top ?? 0 ) + ( ( addonOptions as HeaderMenuOption ) ?. menuOffsetTop ?? 0 ) + targetElm . clientHeight ;
433
+ }
434
+
435
+ if ( ( this . pluginName === 'ContextMenu' || this . pluginName === 'GridMenu' ) && isSubMenu ) {
436
+ menuOffsetLeft = parentOffset . left ;
437
+ menuOffsetTop = parentOffset . top ;
438
+ }
439
+
440
+ // for sub-menus only, auto-adjust drop position (up/down)
441
+ // we first need to see what position the drop will be located (defaults to bottom)
442
+ // since we reposition menu below slick cell, we need to take it in consideration and do our calculation from that element
443
+ const menuHeight = menuElm ?. clientHeight || 0 ;
444
+ if ( ( this . pluginName === 'GridMenu' || this . pluginName === 'HeaderMenu' ) && isSubMenu ) {
445
+ availableSpaceBottom = parentSpaceBottom ;
446
+ availableSpaceTop = parentSpaceTop ;
447
+ } else if (
448
+ this . pluginName === 'CellMenu' ||
449
+ this . pluginName === 'ContextMenu' ||
450
+ ( addonOptions as CellMenu | ContextMenu ) ?. autoAdjustDrop ||
451
+ ( addonOptions as CellMenu | ContextMenu ) ?. dropDirection
452
+ ) {
453
+ availableSpaceBottom = parentSpaceBottom + dropOffset - rowHeight ;
454
+ availableSpaceTop = parentSpaceTop - dropOffset + rowHeight ;
455
+ }
456
+ const dropPosition = availableSpaceBottom < menuHeight && availableSpaceTop > availableSpaceBottom ? 'top' : 'bottom' ;
457
+ if ( dropPosition === 'top' || ( addonOptions as CellMenu | ContextMenu ) ?. dropDirection === 'top' ) {
458
+ menuElm . classList . remove ( 'dropdown' ) ;
459
+ menuElm . classList . add ( 'dropup' ) ;
460
+ if ( isSubMenu ) {
461
+ menuOffsetTop -= menuHeight - dropOffset - parentElm . clientHeight ;
462
+ } else {
463
+ menuOffsetTop -= menuHeight - dropOffset ;
464
+ }
465
+ } else {
466
+ menuElm . classList . remove ( 'dropup' ) ;
467
+ menuElm . classList . add ( 'dropdown' ) ;
468
+ if ( this . pluginName === 'CellMenu' || this . pluginName === 'ContextMenu' ) {
469
+ menuOffsetTop = menuOffsetTop + dropOffset ;
470
+ if ( this . pluginName === 'CellMenu' ) {
471
+ if ( isSubMenu ) {
472
+ menuOffsetTop += dropOffset ;
473
+ } else {
474
+ menuOffsetTop += rowHeight + dropOffset ;
475
+ }
476
+ }
477
+ }
478
+ }
479
+
480
+ // when auto-align is set, it will calculate whether it has enough space in the viewport to show the drop menu on the right (default)
481
+ // if there isn't enough space on the right, it will automatically align the drop menu to the left
482
+ // to simulate an align left, we actually need to know the width of the drop menu
483
+ if (
484
+ ( addonOptions as HeaderMenu ) ?. autoAlign ||
485
+ ( addonOptions as CellMenu | ContextMenu ) ?. autoAlignSide ||
486
+ ( addonOptions as CellMenu | ContextMenu ) ?. dropSide === 'left'
487
+ ) {
488
+ let subMenuPosCalc = menuOffsetLeft + Number ( menuWidth ) ; // calculate coordinate at caller element far right
489
+ if ( isSubMenu ) {
490
+ subMenuPosCalc += parentElm . clientWidth ;
491
+ }
492
+ const gridPos = this . grid . getGridPosition ( ) ;
493
+ const browserWidth = document . documentElement . clientWidth ;
494
+ const dropSide = subMenuPosCalc >= gridPos . width || subMenuPosCalc >= browserWidth ? 'left' : 'right' ;
495
+
496
+ let needHeaderMenuOffsetLeftRecalc = false ;
497
+ if ( dropSide === 'left' || ( ! isSubMenu && ( addonOptions as CellMenu | ContextMenu ) ?. dropSide === 'left' ) ) {
498
+ menuElm . classList . remove ( 'dropright' ) ;
499
+ menuElm . classList . add ( 'dropleft' ) ;
500
+ if ( this . pluginName === 'HeaderMenu' ) {
501
+ if ( isSubMenu ) {
502
+ menuOffsetLeft -= menuWidth ;
503
+ } else {
504
+ needHeaderMenuOffsetLeftRecalc = true ;
505
+ }
506
+ } else if ( this . pluginName === 'CellMenu' && ! isSubMenu ) {
507
+ const parentCellWidth = parentElm . offsetWidth || 0 ;
508
+ menuOffsetLeft -= Number ( menuWidth ) - parentCellWidth - sideOffset ;
509
+ } else if ( this . pluginName !== 'GridMenu' || ( this . pluginName === 'GridMenu' && isSubMenu ) ) {
510
+ menuOffsetLeft -= Number ( menuWidth ) - sideOffset ;
511
+ }
512
+ } else {
513
+ menuElm . classList . remove ( 'dropleft' ) ;
514
+ menuElm . classList . add ( 'dropright' ) ;
515
+ if ( isSubMenu ) {
516
+ menuOffsetLeft += sideOffset + parentElm . offsetWidth ;
517
+ } else {
518
+ if ( this . pluginName === 'HeaderMenu' ) {
519
+ needHeaderMenuOffsetLeftRecalc = true ;
520
+ } else {
521
+ menuOffsetLeft += sideOffset ;
522
+ }
523
+ }
524
+ }
525
+
526
+ if ( needHeaderMenuOffsetLeftRecalc ) {
527
+ menuOffsetLeft = relativePos ?. left ?? 0 ;
528
+ if ( ( addonOptions as HeaderMenu ) ?. autoAlign && gridPos ?. width && menuOffsetLeft + ( menuElm . clientWidth ?? 0 ) >= gridPos . width ) {
529
+ menuOffsetLeft =
530
+ menuOffsetLeft + targetElm . clientWidth - menuElm . clientWidth + ( ( addonOptions as HeaderMenuOption ) ?. autoAlignOffset || 0 ) ;
531
+ }
532
+ }
533
+ }
534
+
535
+ // ready to reposition the menu
536
+ menuElm . style . top = `${ menuOffsetTop } px` ;
537
+ menuElm . style . left = `${ menuOffsetLeft } px` ;
538
+
539
+ if ( this . pluginName === 'GridMenu' ) {
540
+ menuElm . style . opacity = '1' ;
541
+ menuElm . style . display = 'block' ;
542
+ }
543
+ }
544
+ }
354
545
}
0 commit comments