-
Notifications
You must be signed in to change notification settings - Fork 672
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(theme:context-menu): add context menu service (#191)
- Loading branch information
Showing
8 changed files
with
350 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
95 changes: 95 additions & 0 deletions
95
packages/theme/src/services/context-menu/context-menu.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { | ||
Injectable, | ||
ViewContainerRef, | ||
TemplateRef, | ||
ElementRef, | ||
} from '@angular/core'; | ||
import { | ||
Overlay, | ||
OverlayRef, | ||
ConnectionPositionPair, | ||
OverlayConfig, | ||
} from '@angular/cdk/overlay'; | ||
import { | ||
TemplatePortal, | ||
ComponentPortal, | ||
ComponentType, | ||
} from '@angular/cdk/portal'; | ||
|
||
export type ContextMenuType = TemplateRef<{}> | ComponentType<{}>; | ||
|
||
@Injectable({ | ||
providedIn: 'root', | ||
}) | ||
export class ContextMenuService { | ||
private ref: OverlayRef; | ||
private type: ContextMenuType; | ||
private containerRef: ViewContainerRef; | ||
|
||
constructor(private overlay: Overlay) {} | ||
|
||
private create(event: MouseEvent, options?: OverlayConfig) { | ||
const fakeElement = new ElementRef({ | ||
getBoundingClientRect: (): ClientRect => ({ | ||
bottom: event.clientY, | ||
height: 0, | ||
left: event.clientX, | ||
right: event.clientX, | ||
top: event.clientY, | ||
width: 0, | ||
}), | ||
}); | ||
const positions = [ | ||
new ConnectionPositionPair( | ||
{ originX: 'start', originY: 'bottom' }, | ||
{ overlayX: 'start', overlayY: 'top' }, | ||
), | ||
new ConnectionPositionPair( | ||
{ originX: 'start', originY: 'top' }, | ||
{ overlayX: 'start', overlayY: 'bottom' }, | ||
), | ||
]; | ||
const positionStrategy = this.overlay | ||
.position() | ||
.flexibleConnectedTo(fakeElement) | ||
.withPositions(positions); | ||
this.ref = this.overlay.create( | ||
Object.assign( | ||
{ | ||
positionStrategy, | ||
hasBackdrop: true, | ||
scrollStrategy: this.overlay.scrollStrategies.close(), | ||
}, | ||
options, | ||
), | ||
); | ||
if (this.type instanceof TemplateRef) { | ||
this.ref.attach(new TemplatePortal(this.type, this.containerRef)); | ||
} else { | ||
this.ref.attach(new ComponentPortal(this.type, this.containerRef)); | ||
} | ||
this.ref.backdropClick().subscribe(() => this.close()); | ||
} | ||
|
||
open( | ||
event: MouseEvent, | ||
ref: ContextMenuType, | ||
containerRef: ViewContainerRef, | ||
options?: OverlayConfig, | ||
): false { | ||
this.close(); | ||
this.type = ref; | ||
this.containerRef = containerRef; | ||
this.create(event, options); | ||
event.preventDefault(); | ||
event.stopPropagation(); | ||
return false; | ||
} | ||
|
||
close() { | ||
if (!this.ref) return; | ||
this.ref.detach(); | ||
this.ref.dispose(); | ||
this.ref = null; | ||
} | ||
} |
131 changes: 131 additions & 0 deletions
131
packages/theme/src/services/context-menu/context-menu.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
import { | ||
Component, | ||
ViewChild, | ||
TemplateRef, | ||
ViewContainerRef, | ||
} from '@angular/core'; | ||
import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||
import { ContextMenuService, AlainThemeModule } from '@delon/theme'; | ||
import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; | ||
import { OverlayConfig } from '@angular/cdk/overlay'; | ||
|
||
describe('theme: context-menu', () => { | ||
let fixture: ComponentFixture<TestComponent>; | ||
let context: TestComponent; | ||
let page: PageObject; | ||
|
||
beforeEach(() => { | ||
TestBed.configureTestingModule({ | ||
imports: [AlainThemeModule], | ||
declarations: [TestComponent, MenuComponent], | ||
}); | ||
TestBed.overrideModule(BrowserDynamicTestingModule, { | ||
set: { | ||
entryComponents: [MenuComponent], | ||
}, | ||
}); | ||
fixture = TestBed.createComponent(TestComponent); | ||
context = fixture.componentInstance; | ||
fixture.detectChanges(); | ||
page = new PageObject(); | ||
}); | ||
|
||
afterEach(() => { | ||
const els = document.querySelectorAll('.cdk-overlay-container') as any; | ||
els.forEach((el: HTMLElement) => (el.innerHTML = '')); | ||
}); | ||
|
||
it('should working', () => { | ||
page.checkFull('1'); | ||
}); | ||
|
||
it('should be close via click backdrop', () => { | ||
page.open(); | ||
const backdrops = document.querySelectorAll('.cdk-overlay-backdrop'); | ||
(backdrops[backdrops.length - 1] as HTMLElement).click(); | ||
page.check(null); | ||
}); | ||
|
||
it('should be open component', () => { | ||
context.isComponent = true; | ||
fixture.detectChanges(); | ||
page.open().check('component'); | ||
}); | ||
|
||
it('should be disabled backdrop', () => { | ||
context.options = { | ||
hasBackdrop: false, | ||
}; | ||
page.open(); | ||
const backdrops = document.querySelectorAll('.cdk-overlay-backdrop'); | ||
expect(backdrops.length).toBe(0); | ||
}); | ||
|
||
class PageObject { | ||
open(eventArgs?: any): this { | ||
const btn = document.querySelector('#btn') as HTMLDivElement; | ||
btn.dispatchEvent(new MouseEvent('contextmenu', eventArgs)); | ||
fixture.detectChanges(); | ||
return this; | ||
} | ||
checkFull(text: string): this { | ||
return this.open() | ||
.check(text) | ||
.close(); | ||
} | ||
check(text = '1'): this { | ||
const el = document.querySelector('#menu') as HTMLDivElement; | ||
if (text == null) { | ||
expect(el == null).toBe(true); | ||
} else { | ||
expect(el.textContent).toBe(text); | ||
} | ||
return this; | ||
} | ||
close(): this { | ||
context.srv.close(); | ||
return this; | ||
} | ||
} | ||
}); | ||
|
||
@Component({ | ||
template: ` | ||
<div id="btn" (contextmenu)="show($event)"></div> | ||
<ng-template #menu><div id="menu">{{text}}</div></ng-template> | ||
`, | ||
}) | ||
class TestComponent { | ||
@ViewChild('menu') | ||
menuRef: TemplateRef<any>; | ||
text = '1'; | ||
isComponent = false; | ||
options: OverlayConfig; | ||
|
||
constructor( | ||
private containerRef: ViewContainerRef, | ||
public srv: ContextMenuService, | ||
) {} | ||
|
||
show(e: MouseEvent) { | ||
if (this.options) { | ||
this.srv.open( | ||
e, | ||
this.isComponent ? MenuComponent : this.menuRef, | ||
this.containerRef, | ||
this.options, | ||
); | ||
} else { | ||
this.srv.open( | ||
e, | ||
this.isComponent ? MenuComponent : this.menuRef, | ||
this.containerRef, | ||
); | ||
} | ||
} | ||
} | ||
|
||
@Component({ | ||
template: `<div id="menu">component</div>`, | ||
}) | ||
class MenuComponent {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
--- | ||
title: | ||
zh-CN: 基础样例 | ||
en-US: Basic Usage | ||
order: 0 | ||
--- | ||
|
||
## zh-CN | ||
|
||
最简单的用法。 | ||
|
||
## en-US | ||
|
||
Simplest of usage. | ||
|
||
```ts | ||
import { | ||
Component, | ||
ViewChild, | ||
TemplateRef, | ||
ViewContainerRef, | ||
} from '@angular/core'; | ||
import { ContextMenuService } from '@delon/theme'; | ||
|
||
@Component({ | ||
selector: 'app-demo', | ||
template: ` | ||
<div (contextmenu)="show($event)" class="area"> | ||
Area | ||
</div> | ||
<ng-template #menu> | ||
<ul nz-menu> | ||
<li nz-menu-item>菜单项</li> | ||
<li nz-submenu> | ||
<span title>子菜单</span> | ||
<ul> | ||
<li nz-menu-item>子菜单项</li> | ||
</ul> | ||
</li> | ||
</ul> | ||
</ng-template> | ||
`, | ||
styles: [ | ||
` | ||
:host .area { | ||
height: 150px; | ||
line-height: 150px; | ||
text-align: center; | ||
border: 1px solid #ddd; | ||
border-radius: 4px; | ||
background: #f0f0f069; | ||
} | ||
` | ||
] | ||
}) | ||
export class DemoComponent { | ||
@ViewChild('menu') | ||
menuRef: TemplateRef<any>; | ||
|
||
constructor( | ||
private containerRef: ViewContainerRef, | ||
private srv: ContextMenuService, | ||
) {} | ||
|
||
show(e: MouseEvent) { | ||
this.srv.open(e, this.menuRef, this.containerRef); | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
--- | ||
order: 3 | ||
title: Context Menu Service | ||
type: Service | ||
--- | ||
|
||
Quickly build a context menu feature. | ||
|
||
## API | ||
|
||
### open | ||
|
||
Open a context menu. | ||
|
||
| Property | Description | Type | Default | | ||
| ---------------- | --------------------------- | ------------------ | ------- | | ||
| `[event]` | event of the target element | `MouseEvent` | - | | ||
| `[ref]` | menu container | `ContextMenuType` | - | | ||
| `[containerRef]` | area container | `ViewContainerRef` | - | | ||
| `[options]` | Additional [parameters](https://material.angular.io/cdk/overlay/api#OverlayConfig) | `OverlayConfig` | - | | ||
|
||
### close | ||
|
||
Close current context menu. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
--- | ||
order: 3 | ||
title: 右击菜单 | ||
type: Service | ||
--- | ||
|
||
快速构建右击菜单功能。 | ||
|
||
## API | ||
|
||
### open | ||
|
||
打开菜单。 | ||
|
||
| 参数 | 说明 | 类型 | 默认值 | | ||
|------------------|------------|--------------------|--------| | ||
| `[event]` | 鼠标事件 | `MouseEvent` | - | | ||
| `[ref]` | 菜单目标组件 | `ContextMenuType` | - | | ||
| `[containerRef]` | 容器对象 | `ViewContainerRef` | - | | ||
| `[options]` | 额外[参数](https://material.angular.io/cdk/overlay/api#OverlayConfig)定义 | `OverlayConfig` | - | | ||
|
||
### close | ||
|
||
关闭菜单。 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters