Skip to content

Commit

Permalink
feat(theme:context-menu): add context menu service (#191)
Browse files Browse the repository at this point in the history
  • Loading branch information
cipchk authored Oct 1, 2018
1 parent cfd0497 commit f0e96f6
Show file tree
Hide file tree
Showing 8 changed files with 350 additions and 3 deletions.
4 changes: 3 additions & 1 deletion packages/theme/ng-package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
"flatModuleFile": "theme",
"entryFile": "public_api.ts",
"umdModuleIds": {
"date-fns": "DateFns",
"@angular/cdk/overlay": "ng.cdk.overlay",
"@angular/cdk/portal": "ng.cdk.portal",
"ng-zorro-antd": "ngZorro.antd",
"date-fns": "DateFns",
"@delon/acl": "delon.acl"
}
}
Expand Down
3 changes: 2 additions & 1 deletion packages/theme/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ export { WINDOW } from './src/win_tokens';
export { preloaderFinished } from './src/services/preloader/preloader';
export * from './src/services/menu/interface';
export * from './src/services/menu/menu.service';
export { ScrollService } from './src/services/scroll/scroll.service';
export * from './src/services/context-menu/context-menu.service';
export * from './src/services/scroll/scroll.service';
export * from './src/services/settings/interface';
export * from './src/services/settings/settings.service';
export * from './src/services/responsive/responsive.config';
Expand Down
95 changes: 95 additions & 0 deletions packages/theme/src/services/context-menu/context-menu.service.ts
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 packages/theme/src/services/context-menu/context-menu.spec.ts
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 {}
69 changes: 69 additions & 0 deletions packages/theme/src/services/context-menu/demo/simple.md
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);
}
}
```
24 changes: 24 additions & 0 deletions packages/theme/src/services/context-menu/index.en-US.md
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.
24 changes: 24 additions & 0 deletions packages/theme/src/services/context-menu/index.zh-CN.md
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

关闭菜单。
3 changes: 2 additions & 1 deletion packages/theme/src/theme.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { OverlayModule } from '@angular/cdk/overlay';

import { WINDOW } from './win_tokens';

Expand All @@ -25,7 +26,7 @@ const PIPES = [DatePipe, CNCurrencyPipe, KeysPipe, YNPipe];
// endregion

@NgModule({
imports: [CommonModule, RouterModule],
imports: [CommonModule, RouterModule, OverlayModule],
declarations: [...COMPONENTS, ...PIPES],
exports: [...COMPONENTS, ...PIPES, DelonLocaleModule],
})
Expand Down

0 comments on commit f0e96f6

Please sign in to comment.