From f0e96f66437748dbd61283e91838b8085a14e287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A1=E8=89=B2?= Date: Mon, 1 Oct 2018 20:12:09 +0800 Subject: [PATCH] feat(theme:context-menu): add context menu service (#191) --- packages/theme/ng-package.json | 4 +- packages/theme/public_api.ts | 3 +- .../context-menu/context-menu.service.ts | 95 +++++++++++++ .../context-menu/context-menu.spec.ts | 131 ++++++++++++++++++ .../src/services/context-menu/demo/simple.md | 69 +++++++++ .../src/services/context-menu/index.en-US.md | 24 ++++ .../src/services/context-menu/index.zh-CN.md | 24 ++++ packages/theme/src/theme.module.ts | 3 +- 8 files changed, 350 insertions(+), 3 deletions(-) create mode 100644 packages/theme/src/services/context-menu/context-menu.service.ts create mode 100644 packages/theme/src/services/context-menu/context-menu.spec.ts create mode 100644 packages/theme/src/services/context-menu/demo/simple.md create mode 100644 packages/theme/src/services/context-menu/index.en-US.md create mode 100644 packages/theme/src/services/context-menu/index.zh-CN.md diff --git a/packages/theme/ng-package.json b/packages/theme/ng-package.json index faade8a81..36eb78528 100644 --- a/packages/theme/ng-package.json +++ b/packages/theme/ng-package.json @@ -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" } } diff --git a/packages/theme/public_api.ts b/packages/theme/public_api.ts index 65ba05ac9..58d974ad2 100644 --- a/packages/theme/public_api.ts +++ b/packages/theme/public_api.ts @@ -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'; diff --git a/packages/theme/src/services/context-menu/context-menu.service.ts b/packages/theme/src/services/context-menu/context-menu.service.ts new file mode 100644 index 000000000..e11ed1a80 --- /dev/null +++ b/packages/theme/src/services/context-menu/context-menu.service.ts @@ -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; + } +} diff --git a/packages/theme/src/services/context-menu/context-menu.spec.ts b/packages/theme/src/services/context-menu/context-menu.spec.ts new file mode 100644 index 000000000..def6978c3 --- /dev/null +++ b/packages/theme/src/services/context-menu/context-menu.spec.ts @@ -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; + 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: ` +
+ + `, +}) +class TestComponent { + @ViewChild('menu') + menuRef: TemplateRef; + 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: ``, +}) +class MenuComponent {} diff --git a/packages/theme/src/services/context-menu/demo/simple.md b/packages/theme/src/services/context-menu/demo/simple.md new file mode 100644 index 000000000..e985de602 --- /dev/null +++ b/packages/theme/src/services/context-menu/demo/simple.md @@ -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: ` +
+ Area +
+ +
    +
  • 菜单项
  • +
  • + 子菜单 +
      +
    • 子菜单项
    • +
    +
  • +
+
+ `, + 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; + + constructor( + private containerRef: ViewContainerRef, + private srv: ContextMenuService, + ) {} + + show(e: MouseEvent) { + this.srv.open(e, this.menuRef, this.containerRef); + } +} +``` diff --git a/packages/theme/src/services/context-menu/index.en-US.md b/packages/theme/src/services/context-menu/index.en-US.md new file mode 100644 index 000000000..8b17a9b17 --- /dev/null +++ b/packages/theme/src/services/context-menu/index.en-US.md @@ -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. diff --git a/packages/theme/src/services/context-menu/index.zh-CN.md b/packages/theme/src/services/context-menu/index.zh-CN.md new file mode 100644 index 000000000..b7bd4b6f2 --- /dev/null +++ b/packages/theme/src/services/context-menu/index.zh-CN.md @@ -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 + +关闭菜单。 diff --git a/packages/theme/src/theme.module.ts b/packages/theme/src/theme.module.ts index 4ec51b7b1..1fa84ea59 100644 --- a/packages/theme/src/theme.module.ts +++ b/packages/theme/src/theme.module.ts @@ -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'; @@ -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], })