diff --git a/README.md b/README.md
index a41ab0b..5eab5e3 100644
--- a/README.md
+++ b/README.md
@@ -28,55 +28,256 @@ $scope.items = [
{ name: 'Joe', otherProperty: 'Bar' }
};
-$scope.menuOptions = [
- ['Select', function ($itemScope) {
- $scope.selected = $itemScope.item.name;
- }],
- null, // Dividier
- ['Remove', function ($itemScope) {
- $scope.items.splice($itemScope.$index, 1);
- }]
-];
+var builder = contextMenuBuilder();
+builder.newMenuItem('Select', function ($itemScope) {
+ $scope.selected = $itemScope.item.name;
+});
+builder.addSeparator();
+builder.newMenuItem('Remove', function ($itemScope) {
+ $scope.items.splice($itemScope.$index, 1);
+});
+
+$scope.menuOptions = builder;
```
## Menu Options
-Every menu option has an array with 2-3 indexs. Most items will use the `[String, Function]` format. If you need a dynamic item in your context menu you can also use the `[Function, Function]` format.
+A menu options model can be a `contextMenuBuilder`, an `Array`, or a `Function` returning one of those.
+An empty `contextMenuBuilder` or `Array` will not display a context menu.
-The third optional index is a function used to enable/disable the item. If the functtion returns true, the item is enabled (default). If no function is provided, the item will be enabled by default.
-
-```js
-$scope.menuOptions = [
- [function ($itemScope, $event) {
- return $itemScope.item.name;
- }, function ($itemScope, $event) {
- // Action
- }, function($itemScope, $event) {
- // Enable or Disable
- return true; // enabled = true, disabled = false
- }]
-];
-```
-
-The menuOptions can also be defined as a function returning an array. An empty array will not display a context menu.
+### Menu Options as `Function`
```html
Right Click: {{item.name}}
```
+Returning an `Array`:
```js
$scope.menuOptions = function (item) {
if (item.name == 'John') { return []; }
- return [
- [function ($itemScope) {
+ return [{
+ text: function ($itemScope) {
return $itemScope.item.name;
- }, function ($itemScope) {
+ },
+ click: function ($itemScope) {
// Action
- }]
- ];
+ }
+ }];
};
```
+Returning a `contextMenuBuilder`:
+```js
+$scope.menuOptions = function (item) {
+ var builder = contextMenuBuilder();
+ if (item.name != 'John') {
+ builder.newMenuItem(function ($itemScope) {
+ return $itemScope.item.name;
+ },
+ function ($itemScope) {
+ // Action
+ });
+ }
+ return builder;
+};
+```
+
+### Menu Options as `Array`
+
+Using an `Array` to build your options, every item is an object with the properties below.
+To add a separator, leave the item as `null`;
+
+```js
+[{
+ text: "item name",
+ icon: "icon class",
+ enabled: true,
+ click: function($itemScope, $event, $model){}
+},
+...
+]
+```
+
+The properties definitions are:
+
+Property | Type | Details
+---------|------|--------
+text | `String`, `Function`, `Promise` | The text property will define the text that will appear for the menu item. If `String`, the literal will be put in the item. If `Function`, the function will be called with params `$itemScope`, `$event`, `$model`. The result of it will be put in the item. If `Promise`, the resolve of the promise will be put in the item.
+icon (optional) | `String`, `Function` | The icon property is the class that will be appended to `` in the menu item. If this property is not present, no icon will be inserted. If `String`, the literal will be added as class. If `Function`, the function will be called with params `$itemScope`, `$event`, `$model`. The result of it will be added as class.
+enabled (optional) | `Boolean`, `Function` | The enabled property will define if the item will be clickable or disabled. Defaults to `true`. If `Boolean`, the item will ALWAYS be enabled (when true) or disabled (when false). If `Function`, the function will be called with params `$itemScope`, `$event`, `$model`. The `Boolean` result of it will determine if the item is clickable or not.
+click | `Function` | The click property is the action that will be called when the item is clicked. The function will be called with params `$itemScope`, `$event`, `$model`.
+
+### Menu Options as `contextMenuBuilder`
+
+Using a builder to construct your context menu is the recommended approach.
+
+#### `contextMenuBuilder`
+
+The `contextMenuBuilder` has the following methods:
+
+##### newMenuItem([text],[fnAction]);
+
+Create and add a new item to the context menu at the current position.
+
+Param | Type | Details
+------|------|--------
+text (optional) | `String`, `Function`, `Promise` | The text param will define the text that will appear for the menu item. If `String`, the literal will be put in the item. If `Function`, the function will be called with params `$itemScope`, `$event`, `$model`. The result of it will be put in the item. If `Promise`, the resolve of the promise will be put in the item.
+fnAction (optional) | `Function` | The fnAction param is the action that will be called when the item is clicked. The function will be called with params `$itemScope`, `$event`, `$model`.
+
+###### Returns
+
+`contextMenuItem` The return is an instance of a `contextMenuItem` containing functions to help setup the item.
+
+##### newMenuItemAt(index, [text],[fnAction]);
+
+Create and add a new item to the context menu at the given position.
+
+Param | Type | Details
+------|------|--------
+index | `Number` | The index to insert the new menu item at.
+text (optional) | `String`, `Function`, `Promise` | The `text` param will define the text that will appear for the menu item. If `String`, the literal will be put in the item. If `Function`, the function will be called with params `$itemScope`, `$event`, `$model`. The result of it will be put in the item. If `Promise`, the resolve of the promise will be put in the item.
+fnAction (optional) | `Function` | The fnAction param is the action that will be called when the item is clicked. The function will be called with params `$itemScope`, `$event`, `$model`.
+
+###### Returns
+
+`contextMenuItem` The return is an instance of a `contextMenuItem` containing functions to help setup the item.
+
+##### addSeparator();
+
+Add a separator to the context menu at the current position.
+
+##### addSeparatorAt(index);
+
+Add a separator to the context menu at the given position.
+
+Param | Type | Details
+------|------|--------
+index | `Number` | The index to insert the separator at.
+
+##### removeLast();
+
+Remove the last menu item.
+
+##### removeAt(index);
+
+Remove the menu item at the given position.
+
+Param | Type | Details
+------|------|--------
+index | `Number` | The index to remove the item from
+
+##### clear();
+
+Remove all menu items.
+
+#### `contextMenuItem`
+
+The `contextMenuItem` is an object that holds the whole item definition and contains various functions to help you set it up.
+It contains the followig properties and methods:
+
+Property | Type | Details
+---------|------|--------
+text | `String`, `Function`, `Promise` | The text property will define the text that will appear for the menu item. If `String`, the literal will be put in the item. If `Function`, the function will be called with params `$itemScope`, `$event`, `$model`. The result of it will be put in the item. If `Promise`, the resolve of the promise will be put in the item.
+icon | `String`, `Function` | The icon property is the class that will be appended to `` in the menu item. If this property is left undefined, no icon will be inserted. If `String`, the literal will be added as class. If `Function`, the function will be called with params `$itemScope`, `$event`, `$model`. The result of it will be added as class.
+enabled | `Boolean`, `Function` | The enabled property will define if the item will be clickable or disabled. Defaults to `true`. If `Boolean`, the item will ALWAYS be enabled (when true) or disabled (when false). If `Function`, the function will be called with params `$itemScope`, `$event`, `$model`. The `Boolean` result of it will determine if the item is clickable or not.
+click | `Function` | The click property is the action that will be called when the item is clicked. The function will be called with params `$itemScope`, `$event`, `$model`.
+
+##### setText(text)
+
+Set the text property of the menu item.
+
+Param | Type | Details
+------|------|--------
+text | `String`, `Function`, `Promise` | If `String`, the literal will be put in the item. If `Function`, the function will be called with params `$itemScope`, `$event`, `$model`. The result of it will be put in the item. If `Promise`, the resolve of the promise will be put in the item.
+
+###### Returns
+
+`contextMenuItem` Returns the self instance to enable chain calls.
+
+##### setTextFunction(fn)
+
+Wrapper for the `setText` function that accepts only function.
+
+Param | Type | Details
+------|------|--------
+fn | `Function` | The function will be called with params `$itemScope`, `$event`, `$model`. The result of it will be put in the item.
+
+###### Returns
+
+`contextMenuItem` Returns the self instance to enable chain calls.
+
+##### setTextPromise(promise)
+
+Wrapper for the `setText` function that accepts only promises.
+
+Param | Type | Details
+------|------|--------
+promise | `Promise` | The resolve of the promise will be put in the item.
+
+###### Returns
+
+`contextMenuItem` Returns the self instance to enable chain calls.
+
+##### setIcon(icon)
+
+Set the icon property of the menu item.
+
+Param | Type | Details
+------|------|--------
+icon | `String`, `Function` | If `String`, the literal will be added as class. If `Function`, the function will be called with params `$itemScope`, `$event`, `$model`. The result of it will be added as class.
+
+###### Returns
+
+`contextMenuItem` Returns the self instance to enable chain calls.
+
+##### setIconFunction(fn)
+
+Wrapper for the `setIcon` function that accepts only functions.
+
+Param | Type | Details
+------|------|--------
+icon | `Function` | The function will be called with params `$itemScope`, `$event`, `$model`. The result of it will be added as class.
+
+###### Returns
+
+`contextMenuItem` Returns the self instance to enable chain calls.
+
+##### setEnabled(enabled)
+
+Set the enabled property of the menu item.
+
+Param | Type | Details
+------|------|--------
+enabled | `Boolean`, `Function` | If `Boolean`, the item will ALWAYS be enabled (when true) or disabled (when false). If `Function`, the function will be called with params `$itemScope`, `$event`, `$model`. The `Boolean` result of it will determine if the item is clickable or not.
+
+###### Returns
+
+`contextMenuItem` Returns the self instance to enable chain calls.
+
+##### setEnabledFunction(fn)
+
+Wrapper for the `setEnabled` function that accepts only functions.
+
+Param | Type | Details
+------|------|--------
+enabled | `Function` | The function will be called with params `$itemScope`, `$event`, `$model`. The `Boolean` result of it will determine if the item is clickable or not.
+
+###### Returns
+
+`contextMenuItem` Returns the self instance to enable chain calls.
+
+##### setClick(fn)
+
+Set the click property of the menu item.
+
+Param | Type | Details
+------|------|--------
+click | `Function` | The function will be called with params `$itemScope`, `$event`, `$model`.
+
+###### Returns
+
+`contextMenuItem` Returns the self instance to enable chain calls.
+
## Model Attribute (optional)
In instances where a reference is not passed through the `$itemScope` (i.e. not using `ngRepeat`), there is a `model` attribute that can pass a value.
@@ -88,21 +289,62 @@ In instances where a reference is not passed through the `$itemScope` (i.e. not
The `model` is evaluated as an expression using `$scope.$eval` and passed as the third argument.
```js
-$scope.menuOptions = [
- [function ($itemScope, $event, model) {
- return $itemScope.item.name;
- }, function ($itemScope, $event, model) {
- // Action
- }, function($itemScope, $event, model) {
- // Enable or Disable
+var builder = contextMenuBuilder();
+builder.newMenuItem(function ($itemScope, $event, $model) {
+ return $itemScope.item.name;
+ },
+ function ($itemScope, $event, $model) {
+ // Action
+ })
+ .setEnabled(function($itemScope, $event, text, $model){
+ // Enable or Disable
return true; // enabled = true, disabled = false
- }]
-];
+ });
+$scope.menuOptions = builder;
+```
+
+## Context Menu Events
+
+The context menu supports these three events:
+
+Event | Details
+------|--------
+opening | This event happens before the context menu is open and it must return a `Boolean`. If the return is false, the context will not be shown.
+open | This event happens after the context menu is open. Its return is irrelevant.
+close | This event happens after the context menu is closed. Its return is irrelevant.
+
+### Adding handlers
+
+To handle any of these events, add a tag with the same name to the context menu tag.
+
+```html
+
+
Right Click: {{item.name}}
+
+```
+
+The expression on the events will be evaluated using `$scope.$eval`.
+
+```js
+$scope.willOpen = function(item) {
+ //Do something
+ return true; // true will show the context, false will not
+};
+
+$scope.onOpen = function(item) {
+ //Do something
+};
+
+$scope.onClose = function(item) {
+ //Do something
+};
```
## Style Overlay
-To give a light darker disabled tint while the menu is open add the style below.
+The `` holding the menu item list is decorated with the class `ng-bootstrap-contextmenu`.
+
+Also to give a light darker disabled tint while the menu is open add the style below.
```css
body > .dropdown {
diff --git a/contextMenu.js b/contextMenu.js
index 9ff195b..0cd830c 100644
--- a/contextMenu.js
+++ b/contextMenu.js
@@ -1,48 +1,203 @@
angular.module('ui.bootstrap.contextMenu', [])
+.service("contextMenuBuilder", function () {
+ //Meni Item class
+ function contextMenuItem(text, action) {
+ //hold self
+ var self = this;
-.directive('contextMenu', ["$parse", function ($parse) {
- var renderContextMenu = function ($scope, event, options, model) {
- if (!$) { var $ = angular.element; }
- $(event.currentTarget).addClass('context');
- var $contextMenu = $('
');
- $contextMenu.addClass('dropdown clearfix');
- var $ul = $('
');
- $ul.addClass('dropdown-menu');
- $ul.attr({ 'role': 'menu' });
- $ul.css({
- display: 'block',
- position: 'absolute',
- left: event.pageX + 'px',
- top: event.pageY + 'px'
- });
+ //menu item definitions
+ self.text = text;
+ self.icon = null;
+ self.enabled = true;
+ self.click = action;
+
+ //set the text for this item
+ self.setText = function (txt) {
+ if (!angular.isDefined(txt) || (!angular.isFunction(txt) && !(txt instanceof String) && !angular.isFunction(txt.then)))
+ throw 'The text should be a String, Function or Promise';
+ self.text = txt;
+ return self;
+ };
+
+ //set a text function to retrieve the text
+ self.setTextFunction = function (fn) {
+ if (!angular.isDefined(fn) || !angular.isFunction(fn))
+ throw 'The setTextFunction accepts only Functions';
+ return self.setText(fn);
+ };
+
+ //set a text promise to retrieve the text
+ self.setTextPromise = function (promise) {
+ if (!angular.isDefined(promise) || !angular.isFunction(promise.then))
+ throw 'The setTextPromise accepts only Promises';
+ return self.setText(promise);
+ };
+
+ //set the icon class to use on the menu item
+ self.setIcon = function (icon) {
+ if (!angular.isDefined(icon) || (!angular.isFunction(icon) && !(icon instanceof String)))
+ throw 'The icon should be a String or Function';
+ self.icon = icon;
+ return self;
+ };
+
+ //set a function to retrieve the icon class
+ self.setIconFunction = function (fn) {
+ if (!angular.isDefined(fn) || !angular.isFunction(fn))
+ throw 'The setIconFunction accepts only Functions';
+ return self.setIcon(fn);
+ }
+
+ //set this item enabled state
+ self.setEnabled = function (enabled) {
+ if (!angular.isDefined(enabled) || (!angular.isFunction(enabled) && !(enabled instanceof Boolean)))
+ throw 'The enabled should be a Boolean or Function';
+ self.enabled = enabled;
+ return self;
+ };
+
+ //set a function to retrieve the enabled state of this item
+ self.setEnabledFunction = function (fn) {
+ if (!angular.isDefined(fn) || !angular.isFunction(fn))
+ throw 'The setEnabledFunction accepts only Functions';
+ return self.setEnabled(fn);
+ };
+
+ //set the click action for this item
+ self.setClick = function (fn) {
+ if (!angular.isDefined(fn) || !angular.isFunction(fn))
+ throw 'The setClick accepts only Functions';
+ self.click = fn;
+ return self;
+ };
+ };
+
+ //Builder class
+ function contextMenuBuilder() {
+ //hold self
+ var self = this;
+ //menu item list
+ var lst = [];
+
+ //create and add a new menu item at the given position
+ //returns the menu item instance
+ self.newMenuItemAt = function (idx, text, fnAction) {
+ //instantiate new item
+ var item = new contextMenuItem(text, fnAction);
+ //add the internal list
+ lst.splice(idx, 0, item);
+ //return to build the rest
+ return item;
+ };
+
+ //create and add a new menu item
+ //returns the menu item instance
+ self.newMenuItem = function (text, fnAction) {
+ return self.newMenuItemAt(lst.length, text, fnAction);
+ };
+
+ //add a separator at the given position
+ self.addSeparatorAt = function (idx) {
+ lst.splice(idx, 0, null);
+ };
+
+ //add a separator to the current position
+ self.addSeparator = function () {
+ self.addSeparatorAt(lst.length);
+ };
+
+ //remove the menu item in the given position
+ self.removeAt = function (idx) {
+ lst.splice(idx, 1);
+ };
+
+ //remove last menu item
+ self.removeLast = function () {
+ self.removeAt(lst.length - 1);
+ };
+
+ //clear all items from the menu
+ self.clear = function () {
+ lst.splice(0, lst.length);
+ };
+
+ //return the array representation
+ self._toArray = function () {
+ return lst;
+ };
+ };
+
+ //return builder factory
+ return function () {
+ return new contextMenuBuilder();
+ };
+})
+.directive('contextMenu', ["$parse", "$q", function ($parse, $q) {
+ if (!$) { var $ = angular.element; }
+ var renderMenuItem = function ($contextMenu, $scope, event, item, model, onClose) {
+ var itemdef = item;
+ //legacy: convert the array into a contextMenuItem mirror
+ if (item instanceof Array) {
+ itemdef = {
+ text: item[0],
+ click: item[1],
+ enabled: item[2] || true //defaults to true
+ };
+ }
+ //check the definition validity
+ if (!itemdef.text) { throw 'A menu item needs a text'; }
+ else if (!itemdef.click) { throw 'A menu item needs a click function'; }
+ //setup the anchor
+ var $a = $('').attr({ tabindex: '-1', href: '#' });
+ //check for an icon
+ if (itemdef.icon) {
+ //get the icon, no promises here
+ var icon = angular.isFunction(itemdef.icon) ? itemdef.icon.call($scope, $scope, event, model) : itemdef.icon;
+ var $i = $('').addClass(icon);
+ $a.append($i).append(' ');//append space to separate the icon from the text
+ }
+ //if function, get the text, otherwise, the $q will take care of it
+ var text = angular.isFunction(itemdef.text) ? itemdef.text.call($scope, $scope, event, model) : itemdef.text;
+ //resolve the text
+ $q.when(text).then(function (txt) { $a.append(txt); });
+ //create the li and append the anchor
+ var $li = $('- ').append($a);
+ //check the enabled function
+ var enabled = angular.isFunction(itemdef.enabled) ? itemdef.enabled.call($scope, $scope, event, text, model) : itemdef.enabled;
+ if (enabled) {
+ $li.on('click', function ($event) {
+ $event.preventDefault();
+ $scope.$apply(function () {
+ $(event.currentTarget).removeClass('context');
+ $contextMenu.remove();
+ itemdef.click.call($scope, $scope, event, model);
+ $scope.$eval(onClose);
+ });
+ });
+ } else {
+ //disable and prevent propagation
+ $li.addClass('disabled').on('click', function ($event) { $event.preventDefault(); });
+ }
+ return $li;
+ };
+ var renderContextMenu = function ($scope, event, options, model, onClose) {
+ var $target = $(event.currentTarget).addClass('context');
+ var $contextMenu = $('
').addClass('ng-bootstrap-contextmenu dropdown clearfix');
+ var $ul = $('
')
+ .addClass('dropdown-menu')
+ .attr({ 'role': 'menu' })
+ .css({
+ display: 'block',
+ position: 'absolute',
+ left: event.pageX + 'px',
+ top: event.pageY + 'px'
+ });
angular.forEach(options, function (item, i) {
- var $li = $('- ');
if (item === null) {
- $li.addClass('divider');
+ $ul.append($('
- ').addClass('divider'));
} else {
- var $a = $('');
- $a.attr({ tabindex: '-1', href: '#' });
- var text = typeof item[0] == 'string' ? item[0] : item[0].call($scope, $scope, event, model);
- $a.text(text);
- $li.append($a);
- var enabled = angular.isDefined(item[2]) ? item[2].call($scope, $scope, event, text, model) : true;
- if (enabled) {
- $li.on('click', function ($event) {
- $event.preventDefault();
- $scope.$apply(function () {
- $(event.currentTarget).removeClass('context');
- $contextMenu.remove();
- item[1].call($scope, $scope, event, model);
- });
- });
- } else {
- $li.on('click', function ($event) {
- $event.preventDefault();
- });
- $li.addClass('disabled');
- }
+ $ul.append(renderMenuItem($contextMenu, $scope, event, item, model, onClose));
}
- $ul.append($li);
});
$contextMenu.append($ul);
var height = Math.max(
@@ -61,13 +216,15 @@ angular.module('ui.bootstrap.contextMenu', [])
$(document).find('body').append($contextMenu);
$contextMenu.on("mousedown", function (e) {
if ($(e.target).hasClass('dropdown')) {
- $(event.currentTarget).removeClass('context');
+ $target.removeClass('context');
$contextMenu.remove();
+ $scope.$eval(onClose);
}
- }).on('contextmenu', function (event) {
- $(event.currentTarget).removeClass('context');
- event.preventDefault();
+ }).on('contextmenu', function (ev) {
+ $(ev.currentTarget).removeClass('context');
+ ev.preventDefault();
$contextMenu.remove();
+ $scope.$eval(onClose);
});
};
return function ($scope, element, attrs) {
@@ -77,11 +234,18 @@ angular.module('ui.bootstrap.contextMenu', [])
event.preventDefault();
var options = $scope.$eval(attrs.contextMenu);
var model = $scope.$eval(attrs.model);
+ if (angular.isFunction(options._toArray)) {
+ options = options._toArray();
+ }
if (options instanceof Array) {
- if (options.length === 0) { return; }
- renderContextMenu($scope, event, options, model);
+ var open = angular.isDefined(attrs.opening) ? $scope.$eval(attrs.opening) : true;
+ if (options.length === 0 || !open) {
+ return;
+ }
+ renderContextMenu($scope, event, options, model, attrs.close);
+ $scope.$eval(attrs.open);
} else {
- throw '"' + attrs.contextMenu + '" not an array';
+ throw '"' + attrs.contextMenu + '" is not an array nor a contextMenuBuilder';
}
});
});