71 | `,
72 | })
73 | export class ContextMenuComponent implements AfterContentInit {
74 | @Input() public useBootstrap4: boolean = false;
75 | @Output() public close: EventEmitter = new EventEmitter();
76 | @ContentChildren(ContextMenuItemDirective) public menuItems: QueryList;
77 | @ViewChild('menu') public menuElement: ElementRef;
78 | public visibleMenuItems: ContextMenuItemDirective[] = [];
79 |
80 | public links: ILinkConfig[] = [];
81 | public isShown: boolean = false;
82 | public isOpening: boolean = false;
83 | public item: any;
84 | private mouseLocation: MouseLocation = { left: '0px', top: '0px' };
85 | constructor(
86 | private _contextMenuService: ContextMenuService,
87 | private changeDetector: ChangeDetectorRef,
88 | private elementRef: ElementRef,
89 | @Optional()
90 | @Inject(CONTEXT_MENU_OPTIONS) private options: IContextMenuOptions
91 | ) {
92 | if (options) {
93 | this.useBootstrap4 = options.useBootstrap4;
94 | }
95 | _contextMenuService.show.subscribe(menuEvent => this.onMenuEvent(menuEvent));
96 | }
97 |
98 | stopEvent($event: MouseEvent) {
99 | $event.stopPropagation()
100 | }
101 |
102 | get locationCss(): any {
103 | return {
104 | 'position': 'fixed',
105 | 'display': this.isShown ? 'block' : 'none',
106 | left: this.mouseLocation.left,
107 | marginLeft: this.mouseLocation.marginLeft,
108 | marginTop: this.mouseLocation.marginTop,
109 | top: this.mouseLocation.top,
110 | };
111 | }
112 |
113 | @HostListener('document:click')
114 | @HostListener('document:contextmenu')
115 | public clickedOutside(): void {
116 | if (!this.isOpening) {
117 | this.hideMenu();
118 | }
119 | }
120 |
121 | public ngAfterContentInit(): void {
122 | this.menuItems.forEach(menuItem => {
123 | menuItem.execute.subscribe(() => this.hideMenu());
124 | });
125 | }
126 |
127 | public isMenuItemEnabled(menuItem: ContextMenuItemDirective): boolean {
128 | return this.evaluateIfFunction(menuItem.enabled);
129 | }
130 |
131 | public isMenuItemVisible(menuItem: ContextMenuItemDirective): boolean {
132 | return this.evaluateIfFunction(menuItem.visible);
133 | }
134 |
135 | public evaluateIfFunction(value: any): any {
136 | if (value instanceof Function) {
137 | return value(this.item);
138 | }
139 | return value;
140 | }
141 |
142 | public isDisabled(link: ILinkConfig): boolean {
143 | return link.enabled && !link.enabled(this.item);
144 | }
145 |
146 | public execute(link: ILinkConfig, $event?: MouseEvent): void {
147 | if (this.isDisabled(link)) {
148 | return;
149 | }
150 | this.hideMenu();
151 | link.click(this.item, $event);
152 | }
153 |
154 | public onMenuEvent(menuEvent: IContextMenuClickEvent): void {
155 | let { actions, contextMenu, event, item } = menuEvent;
156 | if (contextMenu && contextMenu !== this) {
157 | this.hideMenu();
158 | return;
159 | }
160 | this.isOpening = true;
161 | setTimeout(() => this.isOpening = false, 400);
162 | if (actions) {
163 | if (console && console.warn) {
164 | console.warn(`actions configuration object is deprecated and will be removed in version 1.x.
165 | See https://github.com/isaacplmann/angular2-contextmenu for the new declarative syntax.`);
166 | }
167 | }
168 | if (actions && actions.length > 0) {
169 | // Imperative context menu
170 | this.setVisibleMenuItems();
171 | this.showMenu();
172 | } else if (this.menuItems) {
173 | // Declarative context menu
174 | setTimeout(() => {
175 | this.setVisibleMenuItems();
176 | if (this.visibleMenuItems.length > 0) {
177 | this.showMenu();
178 | } else {
179 | this.hideMenu();
180 | }
181 | setTimeout(() => {
182 | const menuWidth = this.menuElement ? this.menuElement.nativeElement.clientWidth : 100;
183 | const menuHeight = this.menuElement ? this.menuElement.nativeElement.clientHeight : 100;
184 | const bodyWidth = event.view.document.body.clientWidth;
185 | const bodyHeight = event.view.document.body.clientHeight;
186 | const distanceFromRight = bodyWidth - (event.clientX + menuWidth);
187 | const distanceFromBottom = bodyHeight - (event.clientY + menuHeight);
188 | let isMenuOutsideBody: boolean = false;
189 | if (distanceFromRight < 0 && event.clientX > bodyWidth / 2) {
190 | this.mouseLocation.marginLeft = '-' + menuWidth + 'px';
191 | isMenuOutsideBody = true;
192 | }
193 | if (distanceFromBottom < 0 && event.clientY > bodyHeight / 2) {
194 | this.mouseLocation.marginTop = '-' + menuHeight + 'px';
195 | isMenuOutsideBody = true;
196 | }
197 | if (isMenuOutsideBody) {
198 | this.showMenu();
199 | }
200 | });
201 | });
202 | } else {
203 | this.hideMenu();
204 | }
205 | this.links = actions;
206 | this.item = item;
207 | let adjustX = 0;
208 | let adjustY = 0;
209 | const offsetParent: HTMLElement = this.elementRef.nativeElement.offsetParent;
210 | if (offsetParent && offsetParent.tagName !== 'BODY') {
211 | const position = event.view.getComputedStyle(offsetParent).position;
212 | if (position !== 'absolute' && position !== 'fixed') {
213 | const { left, top } = offsetParent.getBoundingClientRect();
214 | adjustX = -left;
215 | adjustY = -top;
216 | }
217 | }
218 | this.mouseLocation = {
219 | left: event.clientX + adjustX + 'px',
220 | top: event.clientY + adjustY + 'px',
221 | };
222 | }
223 |
224 | public setVisibleMenuItems(): void {
225 | this.visibleMenuItems = this.menuItems.filter(menuItem => this.isMenuItemVisible(menuItem));
226 | }
227 |
228 | public showMenu(): void {
229 | this.isShown = true;
230 | this.changeDetector.markForCheck();
231 | }
232 |
233 | @HostListener('window:scroll')
234 | @HostListener('document:keydown', ['$event'])
235 | public hideMenu(event?: KeyboardEvent): void {
236 | if (event && (event.keyCode && event.keyCode !== 27 || event.key && event.key !== 'Escape')) {
237 | return;
238 | }
239 | if (this.isShown === true) {
240 | this.close.emit({});
241 | }
242 | this.isShown = false;
243 | this.changeDetector.markForCheck();
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Deprecated - use [ngx-contextmenu](https://github.com/isaacplmann/ngx-contextmenu) instead
2 |
3 | # angular2-contextmenu
4 |
5 | **This library is being moved to [ngx-contextmenu](https://github.com/isaacplmann/ngx-contextmenu). With the name change comes support for Angular 4 and removal of the old imperative syntax.**
6 |
7 | A context menu built with Angular 2 inspired by [ui.bootstrap.contextMenu](https://github.com/Templarian/ui.bootstrap.contextMenu). Bootstrap classes are included in the markup, but there is no explicit dependency on Bootstrap. [Demo](http://plnkr.co/edit/wpJXpEh4zNZ4uCxTURx2?p=preview)
8 |
9 | ## Installation
10 |
11 | - `npm install angular2-contextmenu`
12 | - import ContextMenuModule into your app module
13 |
14 | ## Usage
15 |
16 | ### Declarative vs. Imperative
17 |
18 | With version 0.2.0, there is a new declarative syntax that allows for quite a bit more flexibility and keeps html out of configuration objects.
19 | The older syntax is deprecated and will be removed in version 1.x. (I have no timeline on when I'll release 1.x, but wanted to give everyone advance warning.)
20 |
21 | ### Template
22 |
23 | ```html
24 |
25 |
Right Click: {{item?.name}}
26 |
27 |
28 |
29 | Say hi!
30 |
31 |
32 |
33 | Bye, {{item?.name}}
34 |
35 |
36 | Input something:
37 |
38 |
39 | ```
40 |
41 | ### Component Code
42 |
43 | ```js
44 | @Component({
45 | ...
46 | })
47 | export class MyContextMenuClass {
48 | public items = [
49 | { name: 'John', otherProperty: 'Foo' },
50 | { name: 'Joe', otherProperty: 'Bar' }
51 | ];
52 | @ViewChild(ContextMenuComponent) public basicMenu: ContextMenuComponent;
53 | }
54 | ```
55 |
56 | ## Context Menu Items
57 |
58 | - Each context menu item is a `` element with the `contextMenuItem` attribute directive applied.
59 | - If the `item` object is used in the context menu item template, the `let-item` attribute must be applied to the `` element.
60 | ** Note: ** Make sure to use the `item?.property` syntax in the template rather than `item.property` as the item will be initially `undefined`.
61 | - Every context menu item emits `execute` events. The `$event` object is of the form `{ event: MouseEvent, item: any }` where `event` is the mouse click event
62 | that triggered the execution and `item` is the current item.
63 | - The `divider` input parameter is optional. Items default to normal menu items. If `divider` is `true`, all the other inputs are ignored.
64 | - The `passive` input parameter is optional. If `passive` is `true`, the menu item will not emit execute events or close
65 | the context menu when clicked.
66 | - The `enabled` input parameter is optional. Items are enabled by default.
67 | This can be a boolean value or a function definition that takes an item and returns a boolean.
68 | - The `visible` input parameter is optional. Items are visible by default. This property enables you to show certain context menu items based on what the data item is.
69 | This can be a boolean value or a function definition that takes an item and returns a boolean.
70 | - Within the template, you have access to any components and variables available in the outer context.
71 |
72 | ```html
73 |
74 |
75 | Say hi, {{item?.name}}!
76 | With access to the outside context: {{ outsideValue }}
77 |
78 |
79 | ```
80 | ```js
81 | public outsideValue = "something";
82 | public isMenuItemType1(item: any): boolean {
83 | return item.type === 'type1';
84 | }
85 | ```
86 |
87 | ## Binding `this` for `visible` and `enabled` functions
88 |
89 | If you need access to properties in your component from within the `enabled` or `visible` functions, you'll need to pass in a version of the function with `this` bound to your component.
90 |
91 | ```html
92 |
93 | ```
94 | ```js
95 | public outsideValue = "something";
96 | public isMenuItemOutsideValueBound = this.isMenuItemOutsideValue.bind(this);
97 | public isMenuItemOutsideValue(item: any): boolean {
98 | return item.type === this.outsideValue;
99 | }
100 | ```
101 |
102 | ## Multiple Context Menus
103 | You can use multiple context menus in the same component if you would like.
104 |
105 | ```html
106 |
107 |
{{item?.name}}
108 |
109 |
110 | ...
111 |
112 |
113 |
114 |
{{item?.name}}
115 |
116 |
117 | ...
118 |
119 | ```
120 |
121 | ```js
122 | @ViewChild('basicMenu') public basicMenu: ContextMenuComponent;
123 | @ViewChild('otherMenu') public otherMenu: ContextMenuComponent;
124 | ```
125 |
126 | ## Context Menu In a Different Component
127 | If your `` component is in a different component from your list, you'll need to wire up the context menu event yourself.
128 |
129 | ```html
130 |
131 |
Right Click: {{item.name}}
132 |
133 | ```
134 |
135 | ```js
136 | import { ContextMenuService } from 'angular2-contextmenu';
137 |
138 | @Component({
139 | ...
140 | })
141 | export class MyContextMenuClass {
142 | public items = [
143 | { name: 'John', otherProperty: 'Foo' },
144 | { name: 'Joe', otherProperty: 'Bar' }
145 | ];
146 |
147 | // Optional
148 | @Input() contextMenu: ContextMenuComponent;
149 |
150 | constructor(private contextMenuService: ContextMenuService) {}
151 |
152 | public onContextMenu($event: MouseEvent, item: any): void {
153 | this.contextMenuService.show.next({
154 | // Optional - if unspecified, all context menu components will open
155 | contextMenu: this.contextMenu,
156 | event: $event,
157 | item: item,
158 | });
159 | $event.preventDefault();
160 | $event.stopPropagation();
161 | }
162 | }
163 | ```
164 |
165 | ## Triggering the Context Menu with a Different Event
166 | The context menu can be triggered at any point using the method above. For instance, to trigger the context menu
167 | with a left click instead of a right click, use this html:
168 |
169 | ```html
170 |
171 |
Left Click: {{item.name}}
172 |
173 | ```
174 |
175 | This could be `(keydown)`, `(mouseover)`, or `(myCustomEvent)` as well.
176 |
177 | ## Custom Styles
178 |
179 | The html that is generated for the context menu looks like this:
180 |
181 | ```html
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 | ```
191 |
192 | You can key off of the `angular2-contextmenu` class to create your own styles. Note that the `ul.dropdown-menu` will have inline styles applied for `position`, `display`, `left` and `top` so that it will be positioned at the cursor when you right-click.
193 |
194 | ```css
195 | .angular2-contextmenu .dropdown-menu {
196 | border: solid 1px chartreuse;
197 | background-color: darkgreen;
198 | padding: 0;
199 | }
200 | .angular2-contextmenu li {
201 | display: block;
202 | border-top: solid 1px chartreuse;
203 | text-transform: uppercase;
204 | text-align: center;
205 | }
206 | .angular2-contextmenu li:first-child {
207 | border-top:none;
208 | }
209 | .angular2-contextmenu a {
210 | color:chartreuse;
211 | display: block;
212 | padding: 0.5em 1em;
213 | }
214 | .angular2-contextmenu a:hover {
215 | color:darkgreen;
216 | background-color:chartreuse;
217 | }
218 | ```
219 |
220 | ## Bootstrap 4
221 |
222 | If you're using Bootstrap 4, you can specify a `useBootstrap4` property in the `forRoot` function of the `ContextMenuModule` in order to get the appropriate class names. Like this:
223 |
224 | ```js
225 | @NgModule({
226 | import: [
227 | ContextMenuModule.forRoot({
228 | useBootstrap4: true,
229 | }),
230 | ],
231 | })
232 | export class AppModule {}
233 | ```
234 |
235 | **Or, if you want to repeat yourself,** you can add a `useBootstrap4` attribute to each `context-menu` component. Like this:
236 |
237 | ```html
238 |
239 | ```
240 |
241 | ## Close event emitter
242 |
243 | There is a `(close)` output EventEmitter that you can subscribe to for notifications when the context menu closes (either by clicking outside or choosing a menu item).
244 |
245 | ```html
246 |
247 | ```
248 |
249 | ## CSS Transforms
250 |
251 | The context menu will correctly position itself as long as the `` element does not have a parent element that has a complex transform applied to it. Complex in this case means anything besides a simple 2d translation. So rotate, skew, stretch, scale, z-axis translation will all cause the context menu to appear in unexpected places. The common scenario of rendering an element with `transform: translate3d(0px 0px 0px)` in order to trigger the browser's GPU works just fine.
252 |
253 | ## Deprecated syntax
254 |
255 | This alternate, deprecated syntax will continue working until version 1.x.
256 |
257 | ### Template
258 |
259 | ```html
260 |