<!DOCTYPE html>
<html>

  <head>
    <script data-require="jquery@*" data-semver="2.2.0" src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
    <link data-require="jquery-ui@*" data-semver="1.11.2" rel="stylesheet" href="//code.jquery.com/ui/1.11.2/themes/smoothness/jquery-ui.css" />
    <link data-require="bootstrap-css@3.3.6" data-semver="3.3.6" rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.css" />
<script data-require="bootstrap.js@3.3.6" data-semver="3.3.6" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
    <script data-require="angularjs@1.5.7" data-semver="1.5.7" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>
    
    <link rel="stylesheet" href="style.css" />
    <script src="contextMenu.js"></script>
    <script src="contextController.js"></script>
    <script src="script.js"></script>
  </head>

  <body>
    <h2>ContextMenu</h2>
    <div class="container" ng-app="flightapp" ng-controller="contextController as cc">
      <div context-menu="cc.contextMenuOptions(cc.data[0])" style="width:100px; height:100px;">
        <p>This is an angular div that can be right-clicked on. It will load the submenu once the context is opened</p>
      </div>
    </div>
  </body>

</html>
// Code goes here

/* Styles go here */

body > div,
body > div > div {
    padding: 10px;
}
angular.module('ui.bootstrap.contextMenu', [])

.service('CustomService', function () {
    "use strict";

    return {
        initialize: function (item) {
            console.log("got here", item);
        }
    }

})
.directive('contextMenu', ["$parse", "$q", "CustomService", "$sce", function ($parse, $q, custom, $sce) {

    var contextMenus = [];
    var $currentContextMenu = null;
    var defaultItemText = "New Item";

    var removeContextMenus = function (level) {
        /// <summary>Remove context menu.</summary>
        while (contextMenus.length && (!level || contextMenus.length > level)) {
            contextMenus.pop().remove();
        }
        if (contextMenus.length == 0 && $currentContextMenu) {
            $currentContextMenu.remove();
        }
    };


    var processTextItem = function ($scope, item, text, event, model, $promises, nestedMenu, $) {
        "use strict";

        var $a = $('<a>');
        $a.css("padding-right", "8px");
        $a.attr({ tabindex: '-1', href: '#' });

        if (typeof item[0] === 'string') {
            text = item[0];
        }
        else if (typeof item[0] === "function") {
            text = item[0].call($scope, $scope, event, model);
        } else if (typeof item.text !== "undefined") {
            text = item.text;
        }

        var $promise = $q.when(text);
        $promises.push($promise);
        $promise.then(function (text) {
            if (nestedMenu) {
                $a.css("cursor", "default");
                $a.append($('<strong style="font-family:monospace;font-weight:bold;float:right;">&gt;</strong>'));
            }
            $a.append(text);
        });

        return $a;

    };

    var processItem = function ($scope, event, model, item, $ul, $li, $promises, $q, $, level) {
        /// <summary>Process individual item</summary>
        "use strict";
        // nestedMenu is either an Array or a Promise that will return that array.
        var nestedMenu = angular.isArray(item[1]) || (item[1] && angular.isFunction(item[1].then))
          ? item[1] : angular.isArray(item[2]) || (item[2] && angular.isFunction(item[2].then))
          ? item[2] : angular.isArray(item[3]) || (item[3] && angular.isFunction(item[3].then))
          ? item[3] : null;

        // if html property is not defined, fallback to text, otherwise use default text
        // if first item in the item array is a function then invoke .call()
        // if first item is a string, then text should be the string.

        var text = defaultItemText;
        var field = '';
        if (typeof item[0] === 'function' || typeof item[0] === 'string' || typeof item.text !== "undefined") {
            text = processTextItem($scope, item, text, event, model, $promises, nestedMenu, $);
            field = text;
        }
        else if (typeof item.html !== "undefined") {
            // leave styling open to dev
            text = item.html
            field = item.field;
        }

        $li.append(text);




        // if item is object, and has enabled prop invoke the prop
        // els if fallback to item[2]

        var isEnabled = function () {
            if (typeof item.enabled !== "undefined") {
                return item.enabled.call($scope, $scope, event, model, text, field);
            } else if (typeof item[2] === "function") {
                return item[2].call($scope, $scope, event, model, text, field);
            } else {
                return true;
            }
        };

        registerEnabledEvents($scope, isEnabled(), item, $ul, $li, nestedMenu, model, text,field, event, $, level);
    };

    var handlePromises = function ($ul, level, event, $promises) {
        /// <summary>
        /// calculate if drop down menu would go out of screen at left or bottom
        /// calculation need to be done after element has been added (and all texts are set; thus thepromises)
        /// to the DOM the get the actual height
        /// </summary>
        "use strict";
        $q.all($promises).then(function () {
            var topCoordinate = event.pageY;
            var menuHeight = angular.element($ul[0]).prop('offsetHeight');
            var winHeight = event.view.innerHeight;
            if (topCoordinate > menuHeight && winHeight - topCoordinate < menuHeight) {
                topCoordinate = event.pageY - menuHeight;
            } else if(winHeight <= menuHeight) {
                // If it really can't fit, reset the height of the menu to one that will fit
                angular.element($ul[0]).css({"height": winHeight - 5, "overflow-y": "scroll"});
                // ...then set the topCoordinate height to 0 so the menu starts from the top
                topCoordinate = 0;
            } else if(winHeight - topCoordinate < menuHeight) {
                var reduceThreshold = 5;
                if(topCoordinate < reduceThreshold) {
                    reduceThreshold = topCoordinate;
                }
                topCoordinate = winHeight - menuHeight - reduceThreshold;
            }

            var leftCoordinate = event.pageX;
            var menuWidth = angular.element($ul[0]).prop('offsetWidth');
            var winWidth = event.view.innerWidth;
            var rightPadding = 5;
            if (leftCoordinate > menuWidth && winWidth - leftCoordinate - rightPadding < menuWidth) {
                leftCoordinate = winWidth - menuWidth - rightPadding;
            } else if(winWidth - leftCoordinate < menuWidth) {
                var reduceThreshold = 5;
                if(leftCoordinate < reduceThreshold + rightPadding) {
                    reduceThreshold = leftCoordinate + rightPadding;
                }
                leftCoordinate = winWidth - menuWidth - reduceThreshold - rightPadding;
            }

            $ul.css({
                display: 'block',
                position: 'absolute',
                left: leftCoordinate + 'px',
                top: topCoordinate + 'px'
            });
        });

    };

    var registerEnabledEvents = function ($scope, enabled, item, $ul, $li, nestedMenu, model, text, field, event, $, level) {
        /// <summary>If item is enabled, register various mouse events.</summary>
        if (enabled) {
            var openNestedMenu = function ($event) {
                removeContextMenus(level + 1);
                /*
                 * The object here needs to be constructed and filled with data
                 * on an "as needed" basis. Copying the data from event directly
                 * or cloning the event results in unpredictable behavior.
                 */
                var ev = {
                    pageX: event.pageX + $ul[0].offsetWidth - 1,
                    pageY: $ul[0].offsetTop + $li[0].offsetTop - 3,
                    view: event.view || window
                };

                /*
                 * At this point, nestedMenu can only either be an Array or a promise.
                 * Regardless, passing them to when makes the implementation singular.
                 */
                $q.when(nestedMenu).then(function(promisedNestedMenu) {
                    renderContextMenu($scope, ev, promisedNestedMenu, model, level + 1);
                });
            };

            $li.on('click', function ($event) {
                $event.preventDefault();
                $scope.$apply(function () {
                    if (nestedMenu) {
                        openNestedMenu($event);
                    } else {
                        $(event.currentTarget).removeClass('context');
                        removeContextMenus();

                        if (angular.isFunction(item[1])) {
                            item[1].call($scope, $scope, event, model, text, field)
                        } else {
                            item.click.call($scope, $scope, event, model, text, field);
                        }
                    }
                });
            });

            $li.on('mouseover', function ($event) {
                $scope.$apply(function () {
                    if (nestedMenu) {
                        openNestedMenu($event);
                    }
                });
            });
           
        } else {
            $li.on('click', function ($event) {
                $event.preventDefault();
            });
            $li.addClass('disabled');
        }
        //custom!
        $ul.on('keydown keypress', function ($event) {
            console.log('keydown');
            $scope.$apply(function () {
                $event.action();
                if (nestedMenu) {
                    openNestedMenu($event);
                }
            });
        });
    };


    var renderContextMenu = function ($scope, event, options, model, level, customClass) {
        /// <summary>Render context menu recursively.</summary>
        if (!level) { level = 0; }
        if (!$) { var $ = angular.element; }
        $(event.currentTarget).addClass('context');
        var $contextMenu = $('<div>');
        if ($currentContextMenu) {
            $contextMenu = $currentContextMenu;
        } else {
            $currentContextMenu = $contextMenu;
            $contextMenu.addClass('angular-bootstrap-contextmenu dropdown clearfix');
        }
        if (customClass) {
            $contextMenu.addClass(customClass);
        }
       
        var $ul = $('<ul>');
        $ul.addClass('dropdown-menu');
        $ul.attr({ 'role': 'menu' });
        $ul.css({
            display: 'block',
            position: 'absolute',
            left: event.pageX + 'px',
            top: event.pageY + 'px',
            "z-index": 10000
        });

        var $promises = [];

        angular.forEach(options, function (item) {

            var $li = $('<li>');
            if (item === null) {
                $li.addClass('divider');
            } else if (typeof item[0] === "object") {
                custom.initialize($li, item);
            } else {
                processItem($scope, event, model, item, $ul, $li, $promises, $q, $, level);
            }
            $ul.append($li);
        });
        $contextMenu.append($ul);
        var height = Math.max(
            document.body.scrollHeight, document.documentElement.scrollHeight,
            document.body.offsetHeight, document.documentElement.offsetHeight,
            document.body.clientHeight, document.documentElement.clientHeight
        );
        $contextMenu.css({
            width: '100%',
            height: height + 'px',
            position: 'absolute',
            top: 0,
            left: 0,
            zIndex: 9999,
            "max-height" : window.innerHeight - 3,
        });
        $(document).find('body').append($contextMenu);

        handlePromises($ul, level, event, $promises);

        $contextMenu.on("mousedown", function (e) {
            if ($(e.target).hasClass('dropdown')) {
                $(event.currentTarget).removeClass('context');
                removeContextMenus();
            }
        }).on('contextmenu', function (event) {
            $(event.currentTarget).removeClass('context');
            event.preventDefault();
            removeContextMenus(level);
        })
            //custom
            .on('keypress', function (event) {
            console.log("keydown event");
        });

        $scope.$on("$destroy", function () {
            removeContextMenus();
        });

        contextMenus.push($ul);
    };

    function isTouchDevice() {
      return 'ontouchstart' in window        // works on most browsers
          || navigator.maxTouchPoints;       // works on IE10/11 and Surface
    };

    return function ($scope, element, attrs) {
        var openMenuEvent = "contextmenu";
        if(attrs.contextMenuOn && typeof(attrs.contextMenuOn) === "string"){
            openMenuEvent = attrs.contextMenuOn;
        }
        element.on(openMenuEvent, function (event) {
            event.stopPropagation();
            event.preventDefault();
            
            // Don't show context menu if on touch device and element is draggable
            if(isTouchDevice() && element.attr('draggable') === 'true') {
              return false;
            }

            $scope.$apply(function () {
                var options = $scope.$eval(attrs.contextMenu);
                var customClass = attrs.contextMenuClass;
                var model = $scope.$eval(attrs.model);
                if (options instanceof Array) {
                    if (options.length === 0) { return; }
                    renderContextMenu($scope, event, options, model, undefined, customClass);
                } else {
                    throw '"' + attrs.contextMenu + '" not an array';
                }
            });
        });
    };
}]);
var app = angular.module("flightapp", ['ui.bootstrap.contextMenu']);

var contextController = function ($scope,$q) {
    var self = this;

    this.columns = [{ field: 'aircraft' }, { field: 'type' }, { field: 'registration' }, ]
    this.data = [
        { aircraft: 'B737', type: 'Boeing', registration: 'PH1234' },
        { aircraft: 'B747', type: 'Boeing', registration: 'PH1254' },
        { aircraft: 'B757', type: 'Boeing', registration: 'PH1734' },
    ];

    this.getStatusItemsAsync = function (item) {
        var array = [['Scheduled', function ($itemScope, $event, item) {
            console.log("Scheduled, item:", item);
        }],
               ['Delayed', function ($itemScope, $event, item) {
                   console.log("Delayed, item:", item);
               }],
               ['BLALBA', function ($itemScope, $event, item) {
                   console.log("BLALBA, item:", item);
               }]];
        return array;
    };

    this.getStatusItems = function (item) {
        return $q(function (resolve, reject) {
            setTimeout(function () {
                resolve(self.getStatusItemsAsync(item));
            }, 1000);
        });
    }


    //in scope so template can reach it.
    this.contextMenuOptions = function (item) {
        var array = [['Add Flight', function ($itemScope, $event, item) {
            console.log("Add Flight, item:", item);
        }],
               ['Edit Flight', function ($itemScope, $event, item) {
                   console.log("Edit flight, item:", item);
               }],
               ['Delete Flight', function ($itemScope, $event, item) {
                   console.log("Delete, item:", item);
               }],
               ['Status', self.getStatusItems(item)]
        ];
        return array;
    };
};

angular.module('flightapp').controller('contextController', ['$scope','$q', contextController]);