1 angular.module('ui.bootstrap.contextMenu', [])
3 .service('CustomService', function () {
7 initialize: function (item) {
8 console.log("got here", item);
13 .directive('contextMenu', ["$parse", "$q", "CustomService", "$sce", function ($parse, $q, custom, $sce) {
15 var contextMenus = [];
16 var $currentContextMenu = null;
17 var defaultItemText = "New Item";
19 var removeContextMenus = function (level) {
20 /// <summary>Remove context menu.</summary>
21 while (contextMenus.length && (!level || contextMenus.length > level)) {
22 contextMenus.pop().remove();
24 if (contextMenus.length == 0 && $currentContextMenu) {
25 $currentContextMenu.remove();
30 var processTextItem = function ($scope, item, text, event, model, $promises, nestedMenu, $) {
34 $a.css("padding-right", "8px");
35 $a.attr({ tabindex: '-1', href: '#' });
37 if (typeof item[0] === 'string') {
40 else if (typeof item[0] === "function") {
41 text = item[0].call($scope, $scope, event, model);
42 } else if (typeof item.text !== "undefined") {
46 var $promise = $q.when(text);
47 $promises.push($promise);
48 $promise.then(function (text) {
50 $a.css("cursor", "default");
51 $a.append($('<strong style="font-family:monospace;font-weight:bold;float:right;">></strong>'));
60 var processItem = function ($scope, event, model, item, $ul, $li, $promises, $q, $, level) {
61 /// <summary>Process individual item</summary>
63 // nestedMenu is either an Array or a Promise that will return that array.
64 var nestedMenu = angular.isArray(item[1]) || (item[1] && angular.isFunction(item[1].then))
65 ? item[1] : angular.isArray(item[2]) || (item[2] && angular.isFunction(item[2].then))
66 ? item[2] : angular.isArray(item[3]) || (item[3] && angular.isFunction(item[3].then))
69 // if html property is not defined, fallback to text, otherwise use default text
70 // if first item in the item array is a function then invoke .call()
71 // if first item is a string, then text should be the string.
73 var text = defaultItemText;
74 if (typeof item[0] === 'function' || typeof item[0] === 'string' || typeof item.text !== "undefined") {
75 text = processTextItem($scope, item, text, event, model, $promises, nestedMenu, $);
77 else if (typeof item.html !== "undefined") {
78 // leave styling open to dev
87 // if item is object, and has enabled prop invoke the prop
88 // els if fallback to item[2]
90 var isEnabled = function () {
91 if (typeof item.enabled !== "undefined") {
92 return item.enabled.call($scope, $scope, event, model, text);
93 } else if (typeof item[2] === "function") {
94 return item[2].call($scope, $scope, event, model, text);
100 registerEnabledEvents($scope, isEnabled(), item, $ul, $li, nestedMenu, model, text, event, $, level);
103 var handlePromises = function ($ul, level, event, $promises) {
105 /// calculate if drop down menu would go out of screen at left or bottom
106 /// calculation need to be done after element has been added (and all texts are set; thus thepromises)
107 /// to the DOM the get the actual height
110 $q.all($promises).then(function () {
111 var topCoordinate = event.pageY;
112 var menuHeight = angular.element($ul[0]).prop('offsetHeight');
113 var winHeight = event.view.innerHeight;
114 if (topCoordinate > menuHeight && winHeight - topCoordinate < menuHeight) {
115 topCoordinate = event.pageY - menuHeight;
116 } else if(winHeight <= menuHeight) {
117 // If it really can't fit, reset the height of the menu to one that will fit
118 angular.element($ul[0]).css({"height": winHeight - 5, "overflow-y": "scroll"});
119 // ...then set the topCoordinate height to 0 so the menu starts from the top
121 } else if(winHeight - topCoordinate < menuHeight) {
122 var reduceThreshold = 5;
123 if(topCoordinate < reduceThreshold) {
124 reduceThreshold = topCoordinate;
126 topCoordinate = winHeight - menuHeight - reduceThreshold;
129 var leftCoordinate = event.pageX;
130 var menuWidth = angular.element($ul[0]).prop('offsetWidth');
131 var winWidth = event.view.innerWidth;
132 var rightPadding = 5;
133 if (leftCoordinate > menuWidth && winWidth - leftCoordinate - rightPadding < menuWidth) {
134 leftCoordinate = winWidth - menuWidth - rightPadding;
135 } else if(winWidth - leftCoordinate < menuWidth) {
136 var reduceThreshold = 5;
137 if(leftCoordinate < reduceThreshold + rightPadding) {
138 reduceThreshold = leftCoordinate + rightPadding;
140 leftCoordinate = winWidth - menuWidth - reduceThreshold - rightPadding;
145 position: 'absolute',
146 left: leftCoordinate + 'px',
147 top: topCoordinate + 'px'
153 var registerEnabledEvents = function ($scope, enabled, item, $ul, $li, nestedMenu, model, text, event, $, level) {
154 /// <summary>If item is enabled, register various mouse events.</summary>
156 var openNestedMenu = function ($event) {
157 removeContextMenus(level + 1);
159 * The object here needs to be constructed and filled with data
160 * on an "as needed" basis. Copying the data from event directly
161 * or cloning the event results in unpredictable behavior.
164 pageX: event.pageX + $ul[0].offsetWidth - 1,
165 pageY: $ul[0].offsetTop + $li[0].offsetTop - 3,
166 view: event.view || window
170 * At this point, nestedMenu can only either be an Array or a promise.
171 * Regardless, passing them to when makes the implementation singular.
173 $q.when(nestedMenu).then(function(promisedNestedMenu) {
174 renderContextMenu($scope, ev, promisedNestedMenu, model, level + 1);
178 $li.on('click', function ($event) {
179 $event.preventDefault();
180 $scope.$apply(function () {
182 openNestedMenu($event);
184 $(event.currentTarget).removeClass('context');
185 removeContextMenus();
187 if (angular.isFunction(item[1])) {
188 item[1].call($scope, $scope, event, model, text)
190 item.click.call($scope, $scope, event, model, text);
196 $li.on('mouseover', function ($event) {
197 $scope.$apply(function () {
199 openNestedMenu($event);
204 $li.on('click', function ($event) {
205 $event.preventDefault();
207 $li.addClass('disabled');
213 var renderContextMenu = function ($scope, event, options, model, level, customClass) {
214 /// <summary>Render context menu recursively.</summary>
215 if (!level) { level = 0; }
216 if (!$) { var $ = angular.element; }
217 $(event.currentTarget).addClass('context');
218 var $contextMenu = $('<div>');
219 if ($currentContextMenu) {
220 $contextMenu = $currentContextMenu;
222 $currentContextMenu = $contextMenu;
223 $contextMenu.addClass('angular-bootstrap-contextmenu dropdown clearfix');
226 $contextMenu.addClass(customClass);
229 $ul.addClass('dropdown-menu');
230 $ul.attr({ 'role': 'menu' });
233 position: 'absolute',
234 left: event.pageX + 'px',
235 top: event.pageY + 'px',
241 angular.forEach(options, function (item) {
245 $li.addClass('divider');
246 } else if (typeof item[0] === "object") {
247 custom.initialize($li, item);
249 processItem($scope, event, model, item, $ul, $li, $promises, $q, $, level);
253 $contextMenu.append($ul);
254 var height = Math.max(
255 document.body.scrollHeight, document.documentElement.scrollHeight,
256 document.body.offsetHeight, document.documentElement.offsetHeight,
257 document.body.clientHeight, document.documentElement.clientHeight
261 height: height + 'px',
262 position: 'absolute',
266 //"max-height" : window.innerHeight - 3
268 var $container = $('<div>');
269 $container.addClass('angular-content');
270 $container.append($contextMenu);
271 //cdt-portlet angular-content
272 $(document).find('body').append($container);
274 handlePromises($ul, level, event, $promises);
276 $contextMenu.on("mousedown", function (e) {
277 if ($(e.target).hasClass('dropdown')) {
278 $(event.currentTarget).removeClass('context');
279 removeContextMenus();
281 }).on('contextmenu', function (event) {
282 $(event.currentTarget).removeClass('context');
283 event.preventDefault();
284 removeContextMenus(level);
287 $scope.$on("$destroy", function () {
288 removeContextMenus();
291 contextMenus.push($ul);
293 return function ($scope, element, attrs) {
294 var openMenuEvent = "contextmenu";
295 if(attrs.contextMenuOn && typeof(attrs.contextMenuOn) === "string"){
296 openMenuEvent = attrs.contextMenuOn;
298 element.on(openMenuEvent, function (event) {
299 event.stopPropagation();
300 $scope.$apply(function () {
301 event.preventDefault();
302 var options = $scope.$eval(attrs.contextMenu);
303 var customClass = attrs.contextMenuClass;
304 var model = $scope.$eval(attrs.model);
305 if (options instanceof Array) {
306 if (options.length === 0) { return; }
307 renderContextMenu($scope, event, options, model, undefined, customClass);
309 throw '"' + attrs.contextMenu + '" not an array';