From f12f6d7297626bd15e5d48066f8c383c9b97090d Mon Sep 17 00:00:00 2001 From: Cleverson Nascimento Date: Tue, 13 Oct 2015 13:48:13 -0300 Subject: [PATCH 1/5] New contextMenuBuilder api to help users build the menu with ease. Several additions: * Issues: - #22 Adding a context menu class name to dropdown: Class ng-bootstrap-contextmenu is now appended to the holding div; * Pull Requests - #17 Added a event for opening: Added event for opening, after open and after closed; - #21 Allow icons (as HTML) in menu item: Not as html, but the new builder api handles the icon submission; - #25 Added support for item text promise: The text is now wrapped inside a $q.when function. --- README.md | 324 ++++++++++++++++++++++++++++++++++++++++++------- contextMenu.js | 256 +++++++++++++++++++++++++++++++------- 2 files changed, 493 insertions(+), 87 deletions(-) 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'; } }); }); From 17e77a17c4c9a04c743c433b9786cb1bb6def5c3 Mon Sep 17 00:00:00 2001 From: Cleverson Nascimento Date: Wed, 14 Oct 2015 14:44:56 -0300 Subject: [PATCH 2/5] Added capacity to create a context menu from a ul template. --- README.md | 43 +++++++++-- contextMenu.js | 204 ++++++++++++++++++++++++++++++++++++------------- 2 files changed, 188 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 5eab5e3..a47b89d 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,39 @@ AngularJS UI Bootstrap Module for adding context menus to elements. [Demo](http: Add a reference to `contextMenu.js`. In your app config add `ui.bootstrap.contextMenu` as a dependency module. -### View +There are two ways of setting up a context menu: by template or by options. + +### By Template + +To setup the context by a html template, you need to provide an `
        ` with the attribute `context-menu-template`.The value of this attribute is a string to identify the context-menu owning it. + +```html + +
        +``` + +You can use ngClick, ngDisabled and all ngRepeat scope functions. + +```js +$scope.selected = 'None'; +$scope.select = function(item){ + $scope.selected = $itemScope.item.name; +}; +$scope.remove = function(item){ + $scope.items.splice($scope.items.indexOf(item), 1); +}; +``` + +### By Menu Options + +To setup the context by menu options you need to provide an object model that defines the context. ```html
        @@ -19,7 +51,9 @@ Add a reference to `contextMenu.js`. In your app config add `ui.bootstrap.contex
        ``` -### Controller +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 following uses the `contextMenuBuilder` to provide the context definition. ```js $scope.selected = 'None'; @@ -40,11 +74,6 @@ builder.newMenuItem('Remove', function ($itemScope) { $scope.menuOptions = builder; ``` -## Menu Options - -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. - ### Menu Options as `Function` ```html diff --git a/contextMenu.js b/contextMenu.js index 0cd830c..f26746d 100644 --- a/contextMenu.js +++ b/contextMenu.js @@ -132,16 +132,74 @@ angular.module('ui.bootstrap.contextMenu', []) return new contextMenuBuilder(); }; }) -.directive('contextMenu', ["$parse", "$q", function ($parse, $q) { +.directive('contextMenu', ["$compile", "$rootElement", function ($compile, $rootElement) { + return { + restrict: 'A', + priority: 1001,//we must run our compile before ngRepeat for this to work + controller: function () { + var self = this; + //hold the template link function + self.fnTemplateLink = null; + //hold the options expression + self.optionsExpr = null; + //hold the model expresion + self.modelExpr = null; + //hold the event expressions + self.openingExpr = null; + self.openExpr = null; + self.closeExpr = null; + }, + compile: function ($element, $attrs) { + //get the template view related to our context and remove it + var $tmpl = $rootElement.find('[context-menu-template="' + $attrs.contextMenu + '"]').remove(); + + //if a template was provided + if ($tmpl.length > 0) { + if (!$tmpl.is("ul")) throw "context-menu-template must be a
          "; + } + else if (!$attrs.contextMenu) { + throw "context-menu needs a context-menu-template child or its options set"; + } + + //remove ourselves + $element.remove("context-menu"); + //add our run directive that will execute on each ngRepeat element + $element.attr("context-menu-run", $attrs.contextMenu); + + //return our link function + return function ($scope, $elem, $att, $controller) { + //fill our controller properties + $controller.optionsExpr = $attrs.contextMenu; + $controller.modelExpr = $attrs.model; + $controller.openingExpr = $attrs.opening; + $controller.openExpr = $attrs.open; + $controller.closeExpr = $attrs.close; + if ($tmpl.length > 0) { + $controller.fnTemplateLink = $compile($tmpl); + } + $scope.$on("$destroy", function () { + //clear references + $controller.optionsExpr = null; + $controller.modelExpr = null; + $controller.openingExpr = null; + $controller.openExpr = null; + $controller.fnTemplateLink = null; + $controller = null; + }); + } + } + } +}]) +.directive('contextMenuRun', ["$timeout", "$q", function ($timeout, $q) { if (!$) { var $ = angular.element; } - var renderMenuItem = function ($contextMenu, $scope, event, item, model, onClose) { + var renderMenuItem = function ($contextMenu, $scope, $event, item, model, $ctrl) { var itemdef = item; - //legacy: convert the array into a contextMenuItem mirror + //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 + enabled: item.length > 2 ? item[2] : true //defaults to true }; } //check the definition validity @@ -152,59 +210,83 @@ angular.module('ui.bootstrap.contextMenu', []) //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 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; + 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; + 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(); + $li.on('click', function (e) { + e.preventDefault(); $scope.$apply(function () { - $(event.currentTarget).removeClass('context'); + $($event.currentTarget).removeClass('context'); $contextMenu.remove(); - itemdef.click.call($scope, $scope, event, model); - $scope.$eval(onClose); + itemdef.click.call($scope, $scope, $event, model); + //call the close function + $timeout(function () { $scope.$eval($ctrl.closeExpr); }); }); }); } else { //disable and prevent propagation - $li.addClass('disabled').on('click', function ($event) { $event.preventDefault(); }); + $li.addClass('disabled').on('click', function (e) { e.preventDefault(); }); } return $li; }; - var renderContextMenu = function ($scope, event, options, model, onClose) { - var $target = $(event.currentTarget).addClass('context'); + var renderContextMenu = function ($scope, $event, options, model, $ctrl) { + var $target = $($event.currentTarget).addClass('context'); var $contextMenu = $('
          ').addClass('ng-bootstrap-contextmenu dropdown clearfix'); - var $ul = $('
            ') - .addClass('dropdown-menu') + var $ul = null; + //check if we will use a template or the options + if ($ctrl.fnTemplateLink) { + //link the scope to a clone template + $ctrl.fnTemplateLink($scope, function (clone) { + $ul = clone; + }); + $ul.find('li').on('click', function (e) { + e.preventDefault(); + $scope.$apply(function () { + $($event.currentTarget).removeClass('context'); + $contextMenu.remove(); + //call the close function + $timeout(function () { $scope.$eval($ctrl.closeExpr); }); + }); + }); + } else { + //create the ul and build the list + $ul = $('
              '); + angular.forEach(options, function (item, i) { + if (item === null) { + $ul.append($('
            • ').addClass('divider')); + } else { + $ul.append(renderMenuItem($contextMenu, $scope, $event, item, model, $ctrl)); + } + }); + } + //format the ul + $ul.addClass('dropdown-menu') .attr({ 'role': 'menu' }) .css({ display: 'block', position: 'absolute', - left: event.pageX + 'px', - top: event.pageY + 'px' + left: $event.pageX + 'px', + top: $event.pageY + 'px' }); - angular.forEach(options, function (item, i) { - if (item === null) { - $ul.append($('
            • ').addClass('divider')); - } else { - $ul.append(renderMenuItem($contextMenu, $scope, event, item, model, onClose)); - } - }); + //append to the div $contextMenu.append($ul); + //calculate height var height = Math.max( document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight, document.body.clientHeight, document.documentElement.clientHeight ); + //format the context menu div $contextMenu.css({ width: '100%', height: height + 'px', @@ -213,41 +295,59 @@ angular.module('ui.bootstrap.contextMenu', []) left: 0, zIndex: 9999 }); + //add to the page $(document).find('body').append($contextMenu); + //context menu control events $contextMenu.on("mousedown", function (e) { if ($(e.target).hasClass('dropdown')) { $target.removeClass('context'); $contextMenu.remove(); - $scope.$eval(onClose); + //call the close function + $timeout(function () { $scope.$eval($ctrl.closeExpr); }); } - }).on('contextmenu', function (ev) { - $(ev.currentTarget).removeClass('context'); - ev.preventDefault(); + }) + .on('contextmenu', function (e) { + $(e.currentTarget).removeClass('context'); + e.preventDefault(); $contextMenu.remove(); - $scope.$eval(onClose); + //call the close function + $timeout(function () { $scope.$eval($ctrl.closeExpr); }); }); }; - return function ($scope, element, attrs) { - element.on('contextmenu', function (event) { - event.stopPropagation(); - $scope.$apply(function () { - 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) { - var open = angular.isDefined(attrs.opening) ? $scope.$eval(attrs.opening) : true; - if (options.length === 0 || !open) { - return; + return { + restrict: 'A', + priority: 999,//to run after the item scope has been created + require: 'contextMenu', + link: function ($scope, $element, $attrs, $controller) { + //context menu event + $element.on('contextmenu', function ($event) { + //only our context must open + $event.stopPropagation(); + $scope.$apply(function () { + $event.preventDefault(); + //get the scope's options and model + var options = $scope.$eval($controller.optionsExpr); + var model = $scope.$eval($controller.modelExpr); + //work the options, if builder + if (angular.isFunction(options._toArray)) { + options = options._toArray(); } - renderContextMenu($scope, event, options, model, attrs.close); - $scope.$eval(attrs.open); - } else { - throw '"' + attrs.contextMenu + '" is not an array nor a contextMenuBuilder'; - } + //builder delivers an array + if (options instanceof Array) { + var open = $controller.openingExpr ? $scope.$eval($controller.openingExpr) : true; + //check if we will open or not + if (options.length === 0 || !open) { + return; + } + //render the menu + renderContextMenu($scope, $event, options, model, $controller); + //call the after open function + $timeout(function () { $scope.$eval($controller.openExpr); }); + } else { + throw '"' + $controller.optionsExpr + '" is not an array nor a contextMenuBuilder'; + } + }); }); - }); - }; + } + } }]); From b7afe2a2d79c14ea4e8477abd32cba96a8962410 Mon Sep 17 00:00:00 2001 From: Cleverson Nascimento Date: Wed, 14 Oct 2015 17:03:09 -0300 Subject: [PATCH 3/5] Fixed some not wanted behaviors --- README.md | 13 ++- contextMenu.js | 236 ++++++++++++++++++++++++++----------------------- 2 files changed, 135 insertions(+), 114 deletions(-) diff --git a/README.md b/README.md index a47b89d..98eca97 100644 --- a/README.md +++ b/README.md @@ -20,18 +20,23 @@ To setup the context by a html template, you need to provide an `
                ` with the
                Right Click: {{item.name}}
                ``` - -You can use ngClick, ngDisabled and all ngRepeat scope functions. +`ngDisabled` does not work with `a`, so if you want to use it you'll need to replace the `a` with `button` and style it accordingly. +Above we used lazy evaluation to prevent the click behaviour when disabled. +You can use ngClick and all ngRepeat scope functions. ```js $scope.selected = 'None'; +$scope.items = [ + { name: 'John', otherProperty: 'Foo' }, + { name: 'Joe', otherProperty: 'Bar' } +}; $scope.select = function(item){ $scope.selected = $itemScope.item.name; }; diff --git a/contextMenu.js b/contextMenu.js index f26746d..593f7b5 100644 --- a/contextMenu.js +++ b/contextMenu.js @@ -132,66 +132,34 @@ angular.module('ui.bootstrap.contextMenu', []) return new contextMenuBuilder(); }; }) -.directive('contextMenu', ["$compile", "$rootElement", function ($compile, $rootElement) { - return { - restrict: 'A', - priority: 1001,//we must run our compile before ngRepeat for this to work - controller: function () { - var self = this; - //hold the template link function - self.fnTemplateLink = null; - //hold the options expression - self.optionsExpr = null; - //hold the model expresion - self.modelExpr = null; - //hold the event expressions - self.openingExpr = null; - self.openExpr = null; - self.closeExpr = null; - }, - compile: function ($element, $attrs) { - //get the template view related to our context and remove it - var $tmpl = $rootElement.find('[context-menu-template="' + $attrs.contextMenu + '"]').remove(); - - //if a template was provided - if ($tmpl.length > 0) { - if (!$tmpl.is("ul")) throw "context-menu-template must be a
                  "; - } - else if (!$attrs.contextMenu) { - throw "context-menu needs a context-menu-template child or its options set"; - } - - //remove ourselves - $element.remove("context-menu"); - //add our run directive that will execute on each ngRepeat element - $element.attr("context-menu-run", $attrs.contextMenu); - - //return our link function - return function ($scope, $elem, $att, $controller) { - //fill our controller properties - $controller.optionsExpr = $attrs.contextMenu; - $controller.modelExpr = $attrs.model; - $controller.openingExpr = $attrs.opening; - $controller.openExpr = $attrs.open; - $controller.closeExpr = $attrs.close; - if ($tmpl.length > 0) { - $controller.fnTemplateLink = $compile($tmpl); - } - $scope.$on("$destroy", function () { - //clear references - $controller.optionsExpr = null; - $controller.modelExpr = null; - $controller.openingExpr = null; - $controller.openExpr = null; - $controller.fnTemplateLink = null; - $controller = null; - }); - } - } - } -}]) -.directive('contextMenuRun', ["$timeout", "$q", function ($timeout, $q) { +.service('_contextMenuWorker', ["$q", "$timeout", function ($q, $timeout) { + var self = this; if (!$) { var $ = angular.element; } + var callMenuItemClick = function (itemdef, $scope, $event, model) { + itemdef.click.call($scope, $scope, $event, model); + }; + var callMenuItemClose = function ($scope, $ctrl) { + $timeout(function () { + $scope.$eval($ctrl.closeExpr); + }); + }; + var callMenuItemOpen = function ($scope, $ctrl) { + $timeout(function () { + $scope.$eval($ctrl.openExpr); + }); + }; + var callMenuItemOpening = function ($scope, $ctrl) { + return $ctrl.openingExpr ? $scope.$eval($ctrl.openingExpr) : true; + }; + var liClickHandler = function ($scope, $e, $event, $contextMenu, $ctrl, model, itemdef, callClick) { + $e.preventDefault(); + $scope.$apply(function () { + $($event.currentTarget).removeClass('context'); + $contextMenu.remove(); + if (callClick) callMenuItemClick(itemdef, $scope, $event, model); + callMenuItemClose($scope, $ctrl); + }); + }; var renderMenuItem = function ($contextMenu, $scope, $event, item, model, $ctrl) { var itemdef = item; //LEGACY: convert the array into a contextMenuItem mirror @@ -222,21 +190,10 @@ angular.module('ui.bootstrap.contextMenu', []) 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 (e) { - e.preventDefault(); - $scope.$apply(function () { - $($event.currentTarget).removeClass('context'); - $contextMenu.remove(); - itemdef.click.call($scope, $scope, $event, model); - //call the close function - $timeout(function () { $scope.$eval($ctrl.closeExpr); }); - }); - }); - } else { - //disable and prevent propagation - $li.addClass('disabled').on('click', function (e) { e.preventDefault(); }); - } + $li.on('click', function (e) { + liClickHandler($scope, e, $event, $contextMenu, $ctrl, model, itemdef, enabled); + }); + if (!enabled) $li.addClass('disabled'); return $li; }; var renderContextMenu = function ($scope, $event, options, model, $ctrl) { @@ -250,13 +207,7 @@ angular.module('ui.bootstrap.contextMenu', []) $ul = clone; }); $ul.find('li').on('click', function (e) { - e.preventDefault(); - $scope.$apply(function () { - $($event.currentTarget).removeClass('context'); - $contextMenu.remove(); - //call the close function - $timeout(function () { $scope.$eval($ctrl.closeExpr); }); - }); + liClickHandler($scope, e, $event, $contextMenu, $ctrl, model, null, false); }); } else { //create the ul and build the list @@ -314,40 +265,105 @@ angular.module('ui.bootstrap.contextMenu', []) $timeout(function () { $scope.$eval($ctrl.closeExpr); }); }); }; + self.bindContextMenu = function ($scope, $element, $attrs, $controller) { + //context menu event + $element.on('contextmenu', function ($event) { + //only our context must open + $event.stopPropagation(); + $scope.$apply(function () { + $event.preventDefault(); + //get the scope's options and model + var options = $scope.$eval($controller.optionsExpr); + var model = $scope.$eval($controller.modelExpr); + //work the options, if builder + if (angular.isFunction(options._toArray)) { + options = options._toArray(); + } + //builder delivers an array + if (options instanceof Array) { + var open = callMenuItemOpening($scope, $controller); + //check if we will open or not + if (options.length === 0 || !open) { + return; + } + //render the menu + renderContextMenu($scope, $event, options, model, $controller); + callMenuItemOpen($scope, $controller); + } else { + throw '"' + $controller.optionsExpr + '" is not an array nor a contextMenuBuilder'; + } + }); + }); + }; +}]) +.directive('contextMenu', ["$compile", "$rootElement", "_contextMenuWorker", function ($compile, $rootElement, _contextMenuWorker) { + return { + restrict: 'A', + priority: 1001,//we must run our compile before ngRepeat for this to work + controller: function () { + var self = this; + //hold the template link function + self.fnTemplateLink = null; + //hold the options expression + self.optionsExpr = null; + //hold the model expresion + self.modelExpr = null; + //hold the event expressions + self.openingExpr = null; + self.openExpr = null; + self.closeExpr = null; + }, + compile: function ($element, $attrs) { + //get the template view related to our context and remove it + var $tmpl = $rootElement.find('[context-menu-template="' + $attrs.contextMenu + '"]').remove(); + var useTemplate = $tmpl.length > 0; + //if a template was provided + if (useTemplate) { + if (!$tmpl.is("ul")) throw "context-menu-template must be a
                    "; + } + else if (!$attrs.contextMenu) { + throw "context-menu needs a context-menu-template child or its options set"; + } + + //remove ourselves + $element.remove("context-menu"); + //add our run directive that will execute on each ngRepeat element + $element.attr("context-menu-run", ""); + + //return our link function + return function ($scope, $elem, $att, $controller) { + //fill our controller properties + $controller.optionsExpr = $attrs.contextMenu; + $controller.modelExpr = $attrs.model; + $controller.openingExpr = $attrs.opening; + $controller.openExpr = $attrs.open; + $controller.closeExpr = $attrs.close; + if (useTemplate) { + $controller.fnTemplateLink = $compile($tmpl); + } else { + //no template, bind now + _contextMenuWorker.bindContextMenu($scope, $elem, $att, $controller); + } + $scope.$on("$destroy", function () { + //clear references + $controller.optionsExpr = null; + $controller.modelExpr = null; + $controller.openingExpr = null; + $controller.openExpr = null; + $controller.fnTemplateLink = null; + $controller = null; + }); + } + } + } +}]) +.directive('contextMenuRun', ["$timeout", "$q", "_contextMenuWorker", function ($timeout, $q, _contextMenuWorker) { return { restrict: 'A', priority: 999,//to run after the item scope has been created require: 'contextMenu', link: function ($scope, $element, $attrs, $controller) { - //context menu event - $element.on('contextmenu', function ($event) { - //only our context must open - $event.stopPropagation(); - $scope.$apply(function () { - $event.preventDefault(); - //get the scope's options and model - var options = $scope.$eval($controller.optionsExpr); - var model = $scope.$eval($controller.modelExpr); - //work the options, if builder - if (angular.isFunction(options._toArray)) { - options = options._toArray(); - } - //builder delivers an array - if (options instanceof Array) { - var open = $controller.openingExpr ? $scope.$eval($controller.openingExpr) : true; - //check if we will open or not - if (options.length === 0 || !open) { - return; - } - //render the menu - renderContextMenu($scope, $event, options, model, $controller); - //call the after open function - $timeout(function () { $scope.$eval($controller.openExpr); }); - } else { - throw '"' + $controller.optionsExpr + '" is not an array nor a contextMenuBuilder'; - } - }); - }); + _contextMenuWorker.bindContextMenu($scope, $element, $attrs, $controller); } } }]); From fac3e97a46f4785ab396de9e4eaf32cb45901880 Mon Sep 17 00:00:00 2001 From: Cleverson Nascimento Date: Wed, 14 Oct 2015 17:24:34 -0300 Subject: [PATCH 4/5] Fixed attribute removal. --- contextMenu.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contextMenu.js b/contextMenu.js index 593f7b5..eaf9544 100644 --- a/contextMenu.js +++ b/contextMenu.js @@ -324,12 +324,10 @@ angular.module('ui.bootstrap.contextMenu', []) else if (!$attrs.contextMenu) { throw "context-menu needs a context-menu-template child or its options set"; } - //remove ourselves - $element.remove("context-menu"); + $element.removeAttr("context-menu"); //add our run directive that will execute on each ngRepeat element $element.attr("context-menu-run", ""); - //return our link function return function ($scope, $elem, $att, $controller) { //fill our controller properties From ee959ad88f51febbe9dbee348db19a5ef8f95788 Mon Sep 17 00:00:00 2001 From: Cleverson Nascimento Date: Sun, 17 Apr 2016 12:41:27 -0300 Subject: [PATCH 5/5] Fixed template usage. --- contextMenu.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contextMenu.js b/contextMenu.js index eaf9544..2939263 100644 --- a/contextMenu.js +++ b/contextMenu.js @@ -275,15 +275,16 @@ angular.module('ui.bootstrap.contextMenu', []) //get the scope's options and model var options = $scope.$eval($controller.optionsExpr); var model = $scope.$eval($controller.modelExpr); + var useTemplate = $controller.fnTemplateLink; //work the options, if builder - if (angular.isFunction(options._toArray)) { + if (!useTemplate && angular.isFunction(options._toArray)) { options = options._toArray(); } //builder delivers an array - if (options instanceof Array) { + if (useTemplate || options instanceof Array) { var open = callMenuItemOpening($scope, $controller); //check if we will open or not - if (options.length === 0 || !open) { + if (!useTemplate && (options.length === 0 || !open)) { return; } //render the menu