中心垫菜单,中心垫菜单覆盖到按钮
Center Mat-Menu, Center MatMenu Overlay to Button
Per Material Issue 9631 将 mat-menu
居中到按钮如下。
the issue is that supporting it deviates from the spec and may end up looking weird.
我需要这个功能...因为编写我自己的 CDK 覆盖比覆盖 mat-menu
组件更耗时...我对重新创建 mat-menu
,只需要将菜单居中...我对任何其他库的实现也不感兴趣,我想使用 Material mat-menu
,所以我的问题如下。
问题:
利用 angular directive
,如何覆盖 MatMenuTrigger
的私有变量和方法以将 CDK 覆盖层居中放置在按钮上?
我选择了一个指令,因为 MatMenuTrigger
是一个 Directive
,复制 Material 源使用的逻辑是有意义的。
到目前为止,这是我想出的方法,我不确定像这样进入 class 本身是否是 "acceptable",或者这样做是否有任何负面影响就这样。
I am open to constructive discussion on this approach for any recommendations or improvements.
Stackblitz
https://stackblitz.com/edit/angular-jpgjdc-nqmyht?embed=1&file=app/center-matmenu.directive.ts
本质上,我将 matMenuTrigger
与按钮分离,方法是将它放在 div
上包裹 mat-menu
... 这样我就可以通过指令以编程方式打开菜单而不是按钮。
- 我还将
menuTrigger
分配给 div
上的 templateRef
并将其作为输入传递给我的 center-mat-menu
选择器
<button mat-button [center-mat-menu]="menuTrigger">Menu</button>
<div #menuTrigger="matMenuTrigger" [matMenuTriggerFor]="menu">
<mat-menu #menu="matMenu">
从那里我通过 @HostListener
创建一个监听器
@HostListener('click', ['$event'])
然后复制 MatMenuTrigger
指令的逻辑来操纵位置、初始化菜单并在单击时触发打开动画。
这基本上是 menu-trigger.ts
源代码中 openMenu()
方法的复制,只是我在菜单初始化之后和调用 this.menuTrigger.menu['_startAnimation']();
之前操作左侧和顶部样式打开菜单。
I store the dimension of the source button into variables and
use that information to calculate the center point of the button, I then use that with the
width of the initialized menu to calculate left
@Directive({
selector: '[center-mat-menu]'
})
export class CenterMatmenuDirective {
overlayRef: OverlayRef;
overlayConf: OverlayConfig;
dropDown: HTMLElement;
overlayPositionBox: HTMLElement;
menu: MatMenuPanel;
button: HTMLElement;
buttonWidth: number;
buttonLeft: number;
buttonBottom: number;
@Input('center-mat-menu') private menuTrigger: MatMenuTrigger;
constructor(private _menuButton: ElementRef, private _renderer: Renderer2) { }
@HostListener('click', ['$event'])
onclick(e) {
this._setVariables();
//menu not opened by keyboard down arrow, have to set this so MatMenuTrigger knows the menu was opened with a mouse click
this.menuTrigger['_openedBy'] = e.button === 0 ? 'mouse' : null;
this._overrideMatMenu();
this.dropDown = this.overlayRef.overlayElement.children[0].children[0] as HTMLElement;
this.overlayPositionBox = this.overlayRef.hostElement;
setTimeout(() => {
this._styleDropDown(this.dropDown);
this._setOverlayPosition(this.dropDown, this.overlayPositionBox);
this._openMenu();
})
}
private _setVariables() {
const config = this.menuTrigger['_getOverlayConfig']();
this.menuTrigger['_overlayRef'] = this.menuTrigger['_overlay'].create(config);
this.overlayRef = this.menuTrigger['_overlayRef'];
this.overlayConf = this.overlayRef.getConfig();
this.overlayRef.keydownEvents().subscribe();
this.menu = this.menuTrigger.menu;
this._setButtonVars();
}
private _setButtonVars() {
this.button = this._menuButton.nativeElement;
this.buttonWidth = this.button.getBoundingClientRect().width;
this.buttonLeft = this.button.getBoundingClientRect().left;
this.buttonBottom = this.button.getBoundingClientRect().bottom;
}
private _overrideMatMenu() {
let strat = this.overlayConf.positionStrategy as FlexibleConnectedPositionStrategy;
this.menuTrigger['_setPosition'](strat);
strat.positionChanges.subscribe(() => {
this._setButtonVars();
this._setOverlayPosition(this.dropDown, this.overlayPositionBox);
})
this.overlayConf.hasBackdrop = this.menu.hasBackdrop == null ?
!this.menuTrigger.triggersSubmenu() : this.menu.hasBackdrop;
this.overlayRef.attach(this.menuTrigger['_getPortal']());
if (this.menu.lazyContent) {
this.menu.lazyContent.attach()
}
this.menuTrigger['_closeSubscription'] = this.menuTrigger['_menuClosingActions']().subscribe(() => {
this.menuTrigger.closeMenu();
});
this.menuTrigger['_initMenu']();
}
private _styleDropDown(dropDown: HTMLElement) {
this._renderer.setStyle(this._renderer.parentNode(dropDown), 'transform-origin', 'center top 0px');
}
private _setOverlayPosition(dropDown: HTMLElement, overlayPositionBox: HTMLElement) {
let dropDownleft = ((this.buttonWidth / 2 + this.buttonLeft) - dropDown.offsetWidth / 2);
this._renderer.setStyle(overlayPositionBox, 'top', this.buttonBottom + 5 + 'px');
this._renderer.setStyle(overlayPositionBox, 'left', dropDownleft + 'px');
this._renderer.setStyle(overlayPositionBox, 'height', '100%');
}
private _openMenu() {
this.menuTrigger.menu['_startAnimation']();
}
}
Per Material Issue 9631 将 mat-menu
居中到按钮如下。
the issue is that supporting it deviates from the spec and may end up looking weird.
我需要这个功能...因为编写我自己的 CDK 覆盖比覆盖 mat-menu
组件更耗时...我对重新创建 mat-menu
,只需要将菜单居中...我对任何其他库的实现也不感兴趣,我想使用 Material mat-menu
,所以我的问题如下。
问题:
利用 angular directive
,如何覆盖 MatMenuTrigger
的私有变量和方法以将 CDK 覆盖层居中放置在按钮上?
我选择了一个指令,因为 MatMenuTrigger
是一个 Directive
,复制 Material 源使用的逻辑是有意义的。
到目前为止,这是我想出的方法,我不确定像这样进入 class 本身是否是 "acceptable",或者这样做是否有任何负面影响就这样。
I am open to constructive discussion on this approach for any recommendations or improvements.
Stackblitz
https://stackblitz.com/edit/angular-jpgjdc-nqmyht?embed=1&file=app/center-matmenu.directive.ts
本质上,我将 matMenuTrigger
与按钮分离,方法是将它放在 div
上包裹 mat-menu
... 这样我就可以通过指令以编程方式打开菜单而不是按钮。
- 我还将
menuTrigger
分配给div
上的templateRef
并将其作为输入传递给我的center-mat-menu
选择器
<button mat-button [center-mat-menu]="menuTrigger">Menu</button> <div #menuTrigger="matMenuTrigger" [matMenuTriggerFor]="menu"> <mat-menu #menu="matMenu">
从那里我通过 @HostListener
@HostListener('click', ['$event'])
然后复制 MatMenuTrigger
指令的逻辑来操纵位置、初始化菜单并在单击时触发打开动画。
这基本上是 menu-trigger.ts
源代码中 openMenu()
方法的复制,只是我在菜单初始化之后和调用 this.menuTrigger.menu['_startAnimation']();
之前操作左侧和顶部样式打开菜单。
I store the dimension of the source button into variables and use that information to calculate the center point of the button, I then use that with the width of the initialized menu to calculate
left
@Directive({
selector: '[center-mat-menu]'
})
export class CenterMatmenuDirective {
overlayRef: OverlayRef;
overlayConf: OverlayConfig;
dropDown: HTMLElement;
overlayPositionBox: HTMLElement;
menu: MatMenuPanel;
button: HTMLElement;
buttonWidth: number;
buttonLeft: number;
buttonBottom: number;
@Input('center-mat-menu') private menuTrigger: MatMenuTrigger;
constructor(private _menuButton: ElementRef, private _renderer: Renderer2) { }
@HostListener('click', ['$event'])
onclick(e) {
this._setVariables();
//menu not opened by keyboard down arrow, have to set this so MatMenuTrigger knows the menu was opened with a mouse click
this.menuTrigger['_openedBy'] = e.button === 0 ? 'mouse' : null;
this._overrideMatMenu();
this.dropDown = this.overlayRef.overlayElement.children[0].children[0] as HTMLElement;
this.overlayPositionBox = this.overlayRef.hostElement;
setTimeout(() => {
this._styleDropDown(this.dropDown);
this._setOverlayPosition(this.dropDown, this.overlayPositionBox);
this._openMenu();
})
}
private _setVariables() {
const config = this.menuTrigger['_getOverlayConfig']();
this.menuTrigger['_overlayRef'] = this.menuTrigger['_overlay'].create(config);
this.overlayRef = this.menuTrigger['_overlayRef'];
this.overlayConf = this.overlayRef.getConfig();
this.overlayRef.keydownEvents().subscribe();
this.menu = this.menuTrigger.menu;
this._setButtonVars();
}
private _setButtonVars() {
this.button = this._menuButton.nativeElement;
this.buttonWidth = this.button.getBoundingClientRect().width;
this.buttonLeft = this.button.getBoundingClientRect().left;
this.buttonBottom = this.button.getBoundingClientRect().bottom;
}
private _overrideMatMenu() {
let strat = this.overlayConf.positionStrategy as FlexibleConnectedPositionStrategy;
this.menuTrigger['_setPosition'](strat);
strat.positionChanges.subscribe(() => {
this._setButtonVars();
this._setOverlayPosition(this.dropDown, this.overlayPositionBox);
})
this.overlayConf.hasBackdrop = this.menu.hasBackdrop == null ?
!this.menuTrigger.triggersSubmenu() : this.menu.hasBackdrop;
this.overlayRef.attach(this.menuTrigger['_getPortal']());
if (this.menu.lazyContent) {
this.menu.lazyContent.attach()
}
this.menuTrigger['_closeSubscription'] = this.menuTrigger['_menuClosingActions']().subscribe(() => {
this.menuTrigger.closeMenu();
});
this.menuTrigger['_initMenu']();
}
private _styleDropDown(dropDown: HTMLElement) {
this._renderer.setStyle(this._renderer.parentNode(dropDown), 'transform-origin', 'center top 0px');
}
private _setOverlayPosition(dropDown: HTMLElement, overlayPositionBox: HTMLElement) {
let dropDownleft = ((this.buttonWidth / 2 + this.buttonLeft) - dropDown.offsetWidth / 2);
this._renderer.setStyle(overlayPositionBox, 'top', this.buttonBottom + 5 + 'px');
this._renderer.setStyle(overlayPositionBox, 'left', dropDownleft + 'px');
this._renderer.setStyle(overlayPositionBox, 'height', '100%');
}
private _openMenu() {
this.menuTrigger.menu['_startAnimation']();
}
}