diff --git a/README.md b/README.md index e40bbc2..fad7815 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ `develop` -![Circle CI Badge] -(https://circleci.com/gh/neilff/angular-menu-aim/tree/develop.png?circle-token=:circle-token) + +![Circle CI Badge](https://circleci.com/gh/neilff/angular-menu-aim/tree/develop.png?circle-token=:circle-token) `master` -![Circle CI Badge] -(https://circleci.com/gh/neilff/angular-menu-aim/tree/master.png?circle-token=:circle-token) + +![Circle CI Badge](https://circleci.com/gh/neilff/angular-menu-aim/tree/master.png?circle-token=:circle-token) # angular-menu-aim @@ -13,21 +13,20 @@ # Installation ``` +npm install angular-menu-aim +... bower install angular-menu-aim ``` -1. Include `build/flyout-tpls.min.js` -2. Link `src/flyout.css` (or copy it into your your own CSS) -3. Include `neilff.flyout-tpls` into your Angular dependencies +1. Include `build/neilff-flyout.js` +2. Include `build/neilff-flyout.css` 4. Use the provided HTML structure # Usage ``` - - + + {{ item.name }} diff --git a/build/flyout-tpls.min.js b/build/flyout-tpls.min.js deleted file mode 100644 index 4d3e8f9..0000000 --- a/build/flyout-tpls.min.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";angular.module("template/flyout/flyout.html",[]).run(["$templateCache",function(t){t.put("template/flyout/flyout.html",'
')}]),angular.module("template/flyout/flyout-item.html",[]).run(["$templateCache",function(t){t.put("template/flyout/flyout-item.html",'
  • ')}]),angular.module("template/flyout/flyout-popover.html",[]).run(["$templateCache",function(t){t.put("template/flyout/flyout-popover.html",'
    ')}]),angular.module("template/flyout/flyout-link.html",[]).run(["$templateCache",function(t){t.put("template/flyout/flyout-link.html",'')}]),angular.module("neilff.flyout-tpls",["ng","template/flyout/flyout.html","template/flyout/flyout-item.html","template/flyout/flyout-popover.html","template/flyout/flyout-link.html"]).factory("flyoutPopover",[function(){return function(t,e,o,n){var l=n[0];angular.extend(t,{getActiveRow:l.getActiveRow,getSelector:l.getSelector})}}]).factory("flyoutLink",[function(){return function(t,e,o,n){var l=n[0];angular.extend(t,{mouseenterRow:l.mouseenterRow,mouseleaveRow:l.mouseleaveRow,clickRow:l.clickRow})}}]).factory("menuAim",[function(){function t(t,e,o){function l(t,e){return(e.y-t.y)/(e.x-t.x)}var u=null;if(!t)return 0;var i=e.offset(),r={x:i.left,y:i.top-o.tolerance},a={x:i.left+e.outerWidth(),y:r.y},c={x:i.left,y:i.top+e.outerHeight()+o.tolerance},f={x:i.left+e.outerWidth(),y:c.y},m=n[n.length-1],s=n[0];if(!m)return 0;if(s||(s=m),s.xf.x||s.yf.y)return 0;if(u&&m.x===u.x&&m.y===u.y)return 0;var y=a,v=f;"left"===o.submenuDirection?(y=c,v=r):"below"===o.submenuDirection?(y=f,v=c):"above"===o.submenuDirection&&(y=r,v=a);var p=l(m,y),d=l(m,v),g=l(s,y),h=l(s,v);return g>p&&d>h?(u=m,o.delay):(u=null,0)}function e(t){n.push({x:t.pageX,y:t.pageY}),n.length>l&&n.shift()}function o(){return n}var n=[],l=3;return{activationDelay:t,setMouseLocs:e,getMouseLocs:o}}]).directive("flyoutItem",[function(){return{restrict:"E",transclude:!0,replace:!0,scope:!0,templateUrl:"template/flyout/flyout-item.html"}}]).directive("flyoutLink",["flyoutLink",function(t){return{restrict:"E",transclude:!0,replace:!0,require:["^flyoutNavigation"],scope:!0,templateUrl:"template/flyout/flyout-link.html",link:t}}]).directive("flyoutPopover",["flyoutPopover",function(t){return{restrict:"E",transclude:!0,replace:!0,require:["^flyoutNavigation"],scope:!0,templateUrl:"template/flyout/flyout-popover.html",link:t}}]).directive("flyoutNavigation",[function(){return{restrict:"E",scope:{visible:"=",tolerance:"=",delay:"=",direction:"=",enter:"=",exit:"=",activate:"=",deactivate:"=",exitmenu:"=exitmenu",selector:"@"},transclude:!0,replace:!0,templateUrl:"template/flyout/flyout.html",controllerAs:"flyoutCtrl",controller:"FlyoutController"}}]).controller("FlyoutController",["$element","$scope","menuAim",function(t,e,o){function n(){M=!0,$.find("."+e.selector).css("min-height",$.outerHeight())}function l(){A&&clearTimeout(A),L.exitmenu(this)&&(b.activeRow&&L.deactivate(b.activeRow),b.activeRow=null)}function u(t){A&&clearTimeout(A),L.enter(t),m(t)}function i(t){L.exit(t)}function r(t){f(t)}function a(){e.visible=!1}function c(){e.visible=!0}function f(t){t!==b.activeRow&&(b.activeRow&&L.deactivate(b.activeRow),L.activate(t),b.activeRow=t)}function m(t){var e=o.activationDelay(b.activeRow,C,L);e?A=setTimeout(function(){m(t)},e):f(t)}function s(){return b.activeRow}function y(){return e.selector}function v(){return e.delay||1e3}function p(){return e.tolerance||500}function d(){return e.direction||"right"}function g(){return e.enter||function(){M||n()}}function h(){return e.exit||angular.noop}function w(){return e.activate||function(){M||n()}}function x(){return e.exitmenu||function(){return a(),!0}}function R(){return e.deactivate||angular.noop}function k(){return e.visible||!1}e.selector=e.selector||"popover";var b=this,$=t,C=$.find(".flyout-dropdown");!function(){document.addEventListener("mousemove",o.setMouseLocs),angular.extend(b,{mouseleaveMenu:l,mouseenterRow:u,mouseleaveRow:i,clickRow:r,closeMenu:a,openMenu:c,getActiveRow:s,getSelector:y,isVisible:k})}();var A=null,L={submenuDirection:d(),tolerance:p(),delay:v(),enter:g(),exit:h(),activate:w(),deactivate:R(),exitmenu:x()},M=!1}]); \ No newline at end of file diff --git a/build/flyout.min.js b/build/flyout.min.js deleted file mode 100644 index ec61ccb..0000000 --- a/build/flyout.min.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";angular.module("neilff.flyout",["ng"]).factory("flyoutPopover",[function(){return function(e,t,n,o){var r=o[0];angular.extend(e,{getActiveRow:r.getActiveRow,getSelector:r.getSelector})}}]).factory("flyoutLink",[function(){return function(e,t,n,o){var r=o[0];angular.extend(e,{mouseenterRow:r.mouseenterRow,mouseleaveRow:r.mouseleaveRow,clickRow:r.clickRow})}}]).factory("menuAim",[function(){function e(e,t,n){function r(e,t){return(t.y-e.y)/(t.x-e.x)}var i=null;if(!e)return 0;var u=t.offset(),c={x:u.left,y:u.top-n.tolerance},l={x:u.left+t.outerWidth(),y:c.y},a={x:u.left,y:u.top+t.outerHeight()+n.tolerance},f={x:u.left+t.outerWidth(),y:a.y},s=o[o.length-1],v=o[0];if(!s)return 0;if(v||(v=s),v.xf.x||v.yf.y)return 0;if(i&&s.x===i.x&&s.y===i.y)return 0;var y=l,m=f;"left"===n.submenuDirection?(y=a,m=c):"below"===n.submenuDirection?(y=f,m=a):"above"===n.submenuDirection&&(y=c,m=l);var p=r(s,y),d=r(s,m),g=r(v,y),x=r(v,m);return g>p&&d>x?(i=s,n.delay):(i=null,0)}function t(e){o.push({x:e.pageX,y:e.pageY}),o.length>r&&o.shift()}function n(){return o}var o=[],r=3;return{activationDelay:e,setMouseLocs:t,getMouseLocs:n}}]).directive("flyoutItem",[function(){return{restrict:"E",transclude:!0,replace:!0,scope:!0,templateUrl:"template/flyout/flyout-item.html"}}]).directive("flyoutLink",["flyoutLink",function(e){return{restrict:"E",transclude:!0,replace:!0,require:["^flyoutNavigation"],scope:!0,templateUrl:"template/flyout/flyout-link.html",link:e}}]).directive("flyoutPopover",["flyoutPopover",function(e){return{restrict:"E",transclude:!0,replace:!0,require:["^flyoutNavigation"],scope:!0,templateUrl:"template/flyout/flyout-popover.html",link:e}}]).directive("flyoutNavigation",[function(){return{restrict:"E",scope:{visible:"=",tolerance:"=",delay:"=",direction:"=",enter:"=",exit:"=",activate:"=",deactivate:"=",exitmenu:"=exitmenu",selector:"@"},transclude:!0,replace:!0,templateUrl:"template/flyout/flyout.html",controllerAs:"flyoutCtrl",controller:"FlyoutController"}}]).controller("FlyoutController",["$element","$scope","menuAim",function(e,t,n){function o(){E=!0,L.find("."+t.selector).css("min-height",L.outerHeight())}function r(){D&&clearTimeout(D),M.exitmenu(this)&&(k.activeRow&&M.deactivate(k.activeRow),k.activeRow=null)}function i(e){D&&clearTimeout(D),M.enter(e),s(e)}function u(e){M.exit(e)}function c(e){f(e)}function l(){t.visible=!1}function a(){t.visible=!0}function f(e){e!==k.activeRow&&(k.activeRow&&M.deactivate(k.activeRow),M.activate(e),k.activeRow=e)}function s(e){var t=n.activationDelay(k.activeRow,A,M);t?D=setTimeout(function(){s(e)},t):f(e)}function v(){return k.activeRow}function y(){return t.selector}function m(){return t.delay||1e3}function p(){return t.tolerance||500}function d(){return t.direction||"right"}function g(){return t.enter||function(){E||o()}}function x(){return t.exit||angular.noop}function w(){return t.activate||function(){E||o()}}function R(){return t.exitmenu||function(){return l(),!0}}function h(){return t.deactivate||angular.noop}function b(){return t.visible||!1}t.selector=t.selector||"popover";var k=this,L=e,A=L.find(".flyout-dropdown");!function(){document.addEventListener("mousemove",n.setMouseLocs),angular.extend(k,{mouseleaveMenu:r,mouseenterRow:i,mouseleaveRow:u,clickRow:c,closeMenu:l,openMenu:a,getActiveRow:v,getSelector:y,isVisible:b})}();var D=null,M={submenuDirection:d(),tolerance:p(),delay:m(),enter:g(),exit:x(),activate:w(),deactivate:h(),exitmenu:R()},E=!1}]); \ No newline at end of file diff --git a/build/neilff-flyout.css b/build/neilff-flyout.css new file mode 100644 index 0000000..50d8fcf --- /dev/null +++ b/build/neilff-flyout.css @@ -0,0 +1,59 @@ +.flyout-navigation { + padding: 0; + margin: 0; + background-color: smoke; + border: 1px solid black; + width: 100%; + max-width: 275px; + position: absolute; + top: 64px; + left: -100%; + transition: all 300ms; + border-radius: 0; + box-shadow: none; +} + +.flyout-navigation.reveal { + left: 0; +} + +.flyout-navigation .flyout-dropdown { + width: 100%; +} + +.flyout-navigation .flyout-dropdown a { + color: black; + display: inline-block; + width: 100%; + padding: 15px; +} + +.flyout-navigation .flyout-dropdown a:hover { + background: #efefef; + text-decoration: none; +} + +.flyout-navigation .flyout-list-item { + font-size: 18px; + text-transform: uppercase; +} + +.flyout-navigation .popover { + border: 1px solid black; + margin: 0; + padding: 15px; + border-radius: 0; + box-shadow: none; + position: absolute; + top: 0; + left: 0; + transition: left 300ms; + width: 100%; + max-width: 275px; +} + +.flyout-navigation .popover.reveal { + display: block; + top: -1px; + left: 273px; +} diff --git a/build/neilff-flyout.js b/build/neilff-flyout.js new file mode 100644 index 0000000..f73e7bc --- /dev/null +++ b/build/neilff-flyout.js @@ -0,0 +1,1117 @@ +'use strict'; + +/** + * This directive will render the desktop flyout menu. It uses the + * jquery-menu-aim plugin, and converts it to a more friendlier Angular + * implementation. Refer to the original plugin here: + * + * https://github.com/kamens/jQuery-menu-aim + */ +angular + .module('template/flyout/flyout.html', []) + .run(['$templateCache', function($templateCache) { + $templateCache.put('template/flyout/flyout.html', + '
    ' + + '
      ' + + '
    ' + + '
    ' + ); + }]); + +angular + .module('template/flyout/flyout-item.html', []) + .run(['$templateCache', function($templateCache) { + $templateCache.put('template/flyout/flyout-item.html', + '
  • ' + ); + }]); + +angular + .module('template/flyout/flyout-popover.html', []) + .run(['$templateCache', function($templateCache) { + $templateCache.put('template/flyout/flyout-popover.html', + '
    ' + + '
    ' + ); + }]); + +angular + .module('template/flyout/flyout-link.html', []) + .run(['$templateCache', function($templateCache) { + $templateCache.put('template/flyout/flyout-link.html', + '' + ); + }]); + +angular + .module('neilff.flyout-tpls', [ + 'ng', + 'template/flyout/flyout.html', + 'template/flyout/flyout-item.html', + 'template/flyout/flyout-popover.html', + 'template/flyout/flyout-link.html' + ]) + .factory('flyoutPopover', [function() { + return function flyoutPopover(scope, elem, attrs, ctrls) { + var flyoutCtrl = ctrls[0]; + + angular.extend(scope, { + getActiveRow: flyoutCtrl.getActiveRow, + getSelector: flyoutCtrl.getSelector + }); + }; + }]) + .factory('flyoutLink', [function() { + return function flyoutLink(scope, elem, attrs, ctrls) { + var flyoutCtrl = ctrls[0]; + + angular.extend(scope, { + mouseenterRow: flyoutCtrl.mouseenterRow, + mouseleaveRow: flyoutCtrl.mouseleaveRow, + clickRow: flyoutCtrl.clickRow + }); + }; + }]) + .factory('menuAim', [function() { + var mouseLocs = []; + var MOUSE_LOCS_TRACKED = 3; // number of past mouse locations to track + + /** + * Calculate activation delay based on mouse position + * + * @method activationDelay + * @param {Integer} activeRow The currently active row + * @param {Object} $menu The $element for the menu + * @param {Object} options Options for the activation delay + * @return {Integer} Activation delay in MS + */ + function activationDelay(activeRow, $menu, options) { + var lastDelayLoc = null; + + if (!activeRow) { + // If there is no other submenu row already active, then + // go ahead and activate immediately. + return 0; + } + + var offset = $menu.offset(); + + var upperLeft = { + x: offset.left, + y: offset.top - options.tolerance + }; + + var upperRight = { + x: offset.left + $menu.outerWidth(), + y: upperLeft.y + }; + + var lowerLeft = { + x: offset.left, + y: offset.top + $menu.outerHeight() + options.tolerance + }; + + var lowerRight = { + x: offset.left + $menu.outerWidth(), + y: lowerLeft.y + }; + + var loc = mouseLocs[mouseLocs.length - 1]; + var prevLoc = mouseLocs[0]; + + if (!loc) { + return 0; + } + + if (!prevLoc) { + prevLoc = loc; + } + + if (prevLoc.x < offset.left || prevLoc.x > lowerRight.x || + prevLoc.y < offset.top || prevLoc.y > lowerRight.y) { + // If the previous mouse location was outside of the entire + // menu's bounds, immediately activate. + return 0; + } + + if (lastDelayLoc && + loc.x === lastDelayLoc.x && loc.y === lastDelayLoc.y) { + // If the mouse hasn't moved since the last time we checked + // for activation status, immediately activate. + return 0; + } + + // Detect if the user is moving towards the currently activated + // submenu. + // + // If the mouse is heading relatively clearly towards + // the submenu's content, we should wait and give the user more + // time before activating a new row. If the mouse is heading + // elsewhere, we can immediately activate a new row. + // + // We detect this by calculating the slope formed between the + // current mouse location and the upper/lower right points of + // the menu. We do the same for the previous mouse location. + // If the current mouse location's slopes are + // increasing/decreasing appropriately compared to the + // previous's, we know the user is moving toward the submenu. + // + // Note that since the y-axis increases as the cursor moves + // down the screen, we are looking for the slope between the + // cursor and the upper right corner to decrease over time, not + // increase (somewhat counterintuitively). + function slope(a, b) { + return (b.y - a.y) / (b.x - a.x); + } + + var decreasingCorner = upperRight, + increasingCorner = lowerRight; + + // Our expectations for decreasing or increasing slope values + // depends on which direction the submenu opens relative to the + // main menu. By default, if the menu opens on the right, we + // expect the slope between the cursor and the upper right + // corner to decrease over time, as explained above. If the + // submenu opens in a different direction, we change our slope + // expectations. + if (options.submenuDirection === 'left') { + decreasingCorner = lowerLeft; + increasingCorner = upperLeft; + } else if (options.submenuDirection === 'below') { + decreasingCorner = lowerRight; + increasingCorner = lowerLeft; + } else if (options.submenuDirection === 'above') { + decreasingCorner = upperLeft; + increasingCorner = upperRight; + } + + var decreasingSlope = slope(loc, decreasingCorner), + increasingSlope = slope(loc, increasingCorner), + prevDecreasingSlope = slope(prevLoc, decreasingCorner), + prevIncreasingSlope = slope(prevLoc, increasingCorner); + + if (decreasingSlope < prevDecreasingSlope && + increasingSlope > prevIncreasingSlope) { + // Mouse is moving from previous location towards the + // currently activated submenu. Delay before activating a + // new menu row, because user may be moving into submenu. + lastDelayLoc = loc; + return options.delay; + } + + lastDelayLoc = null; + return 0; + } + + /** + * Track the last locations of the mouse + * + * @method setMouseLocs + * @param {Object} e Event object + * @return {undefined} undefined + */ + function setMouseLocs(e) { + mouseLocs.push({x: e.pageX, y: e.pageY}); + + if (mouseLocs.length > MOUSE_LOCS_TRACKED) { + mouseLocs.shift(); + } + } + + function getMouseLocs() { + return mouseLocs; + } + + return { + activationDelay: activationDelay, + setMouseLocs: setMouseLocs, + getMouseLocs: getMouseLocs + }; + }]) + .directive('flyoutItem', [function() { + return { + restrict: 'E', + transclude: true, + replace: true, + scope: true, + templateUrl: 'template/flyout/flyout-item.html' + }; + }]) + .directive('flyoutLink', ['flyoutLink', function(flyoutLink) { + return { + restrict: 'E', + transclude: true, + replace: true, + require: ['^flyoutNavigation'], + scope: true, + templateUrl: 'template/flyout/flyout-link.html', + link: flyoutLink + }; + }]) + .directive('flyoutPopover', ['flyoutPopover', function(flyoutPopover) { + return { + restrict: 'E', + transclude: true, + replace: true, + require: ['^flyoutNavigation'], + scope: true, + templateUrl: 'template/flyout/flyout-popover.html', + link: flyoutPopover + }; + }]) + .directive('flyoutNavigation', [function() { + return { + restrict: 'E', + scope: { + visible: '=', + tolerance: '=', + delay: '=', + direction: '=', + enter: '=', + exit: '=', + activate: '=', + deactivate: '=', + exitmenu: '=exitmenu', // Angular treats camel case specially + selector: '@' + }, + transclude: true, + replace: true, + templateUrl: 'template/flyout/flyout.html', + controllerAs: 'flyoutCtrl', + controller: 'FlyoutController' + }; + }]) + .controller('FlyoutController', ['$element', '$scope', 'menuAim', + function( + $element, + $scope, + menuAim + ) { + + $scope.selector = $scope.selector || 'popover'; + + var vm = this; + var elem = $element; + var $menu = elem.find('.flyout-dropdown'); + + (function init() { + /** + * Attach document event handlers + */ + document.addEventListener('mousemove', menuAim.setMouseLocs); + + /** + * Attach vm methods and properties + */ + angular.extend(vm, { + mouseleaveMenu: mouseleaveMenu, + mouseenterRow: mouseenterRow, + mouseleaveRow: mouseleaveRow, + clickRow: clickRow, + closeMenu: closeMenu, + openMenu: openMenu, + getActiveRow: getActiveRow, + getSelector: getSelector, + isVisible: isVisible + }); + })(); + + var timeoutId = null; + var options = { + submenuDirection: getDirection(), + tolerance: getTolerance(), + delay: getDelay(), + enter: getEnter(), + exit: getExit(), + activate: getActivate(), + deactivate: getDeactivate(), + exitmenu: getExitMenu() + }; + + var initHeight = false; + + /** + * Set the popover menu height on the first hover / click + * + * @method setPopoverHeight + * @return {undefined} undefined + */ + function setPopoverHeight() { + initHeight = true; + elem.find('.' + $scope.selector).css('min-height', elem.outerHeight()); + } + + /** + * Hide menu when user completely exits the menu + * + * @method mouseleaveMenu + * @return {undefined} undefined + */ + function mouseleaveMenu() { + if (timeoutId) { + clearTimeout(timeoutId); + } + + if (options.exitmenu(this)) { + if (vm.activeRow) { + options.deactivate(vm.activeRow); + } + + vm.activeRow = null; + } + } + + /** + * Trigger a possible row activation when user enters + * a new row. If this is first time entering a row, + * then set the height of the menu. + * + * @method mouseenterRow + * @param {Integer} row Row index to activate + * @return {undefined} undefined + */ + function mouseenterRow(row) { + if (timeoutId) { + clearTimeout(timeoutId); + } + + options.enter(row); + possiblyActivate(row); + } + + /** + * Trigger when user exits a row. + * + * @method mouseleaveRow + * @param {Integer} row Row index to activate + * @return {undefined} undefined + */ + function mouseleaveRow(row) { + options.exit(row); + } + + /** + * Immediately activate a row if user clicks on it. + * If this is first time entering a row, then set the + * height of the menu. + * + * @method clickRow + * @param {Integer} row Row to activate + * @return {undefined} undefined + */ + function clickRow(row) { + activate(row); + } + + /** + * Close the menu + * + * @method closeMenu + * @return {undefined} undefined + */ + function closeMenu() { + $scope.visible = false; + } + + /** + * Open the menu + * + * @method openMenu + * @return {undefined} undefined + */ + function openMenu() { + $scope.visible = true; + } + + /** + * Active the provided menu row + * + * @method activate + * @param {Integer} row The row index + * @return {undefined} undefined + */ + function activate(row) { + if (row === vm.activeRow) { + return; + } + + if (vm.activeRow) { + options.deactivate(vm.activeRow); + } + + options.activate(row); + vm.activeRow = row; + } + + /** + * Possibly activate a row + * + * @method possiblyActivate + * @param {Integer} row Row index to activate + * @return {undefined} undefined + */ + function possiblyActivate(row) { + var delay = menuAim.activationDelay(vm.activeRow, $menu, options); + + if (delay) { + timeoutId = setTimeout(function() { + possiblyActivate(row); + }, delay); + } else { + activate(row); + } + } + + /** + * Returns the currently active row + * + * @return {Integer} The currently active row + */ + function getActiveRow() { + return vm.activeRow; + } + + /** + * Gets the name of the selector used for the popover + * + * @return {String} Selector name + */ + function getSelector() { + return $scope.selector; + } + + /** + * Gets the delay when user appears to be entering submenu + * + * @return {Integer} Millisecond delay when user appears to be entering submenu + */ + function getDelay() { + return $scope.delay || 1000; + } + + /** + * Gets the tolerance forgivey when entering submenu + * + * @return {Integer} Amount of forgivey when entering submenu (bigger = more) + */ + function getTolerance() { + return $scope.tolerance || 500; + } + + /** + * Gets the direction for the submenu + * + * @return {String} Direction of submenu (one of: right, left, below, above) + */ + function getDirection() { + return $scope.direction || 'right'; + } + + /** + * Gets callback function to call when a row is entered + * + * @return {Function} Callback function to call when row is entered + */ + function getEnter() { + return $scope.enter || function() { + if (!initHeight) { + setPopoverHeight(); + } + }; + } + + /** + * Gets callback function to call when a row is exited + * + * @return {Function} Callback function to call when row is exited + */ + function getExit() { + return $scope.exit || angular.noop; + } + + /** + * Gets callback function to call when a row is clicked + * + * @return {Function} Callback function to call when row is clicked + */ + function getActivate() { + return $scope.activate || function() { + if (!initHeight) { + setPopoverHeight(); + } + }; + } + + /** + * Gets callback function to call when the menu is exited + * + * @return {Function} Callback function to call when menu is exited + */ + function getExitMenu() { + return $scope.exitmenu || function() { + closeMenu(); + + return true; + }; + } + + /** + * Gets callback function to call when a row is deactivated + * + * @return {Function} Callback function to call when row is deactivated + */ + function getDeactivate() { + return $scope.deactivate || angular.noop; + } + + /** + * Returns whether the menu is currently visible or not + * + * @return {Boolean} Is the menu visible + */ + function isVisible() { + return $scope.visible || false; + } + }]); + +'use strict'; + +/** + * This directive will render the desktop flyout menu. It uses the + * jquery-menu-aim plugin, and converts it to a more friendlier Angular + * implementation. Refer to the original plugin here: + * + * https://github.com/kamens/jQuery-menu-aim + */ + +angular + .module('neilff.flyout', ['ng']) + .factory('flyoutPopover', [function() { + return function flyoutPopover(scope, elem, attrs, ctrls) { + var flyoutCtrl = ctrls[0]; + + angular.extend(scope, { + getActiveRow: flyoutCtrl.getActiveRow, + getSelector: flyoutCtrl.getSelector + }); + }; + }]) + .factory('flyoutLink', [function() { + return function flyoutLink(scope, elem, attrs, ctrls) { + var flyoutCtrl = ctrls[0]; + + angular.extend(scope, { + mouseenterRow: flyoutCtrl.mouseenterRow, + mouseleaveRow: flyoutCtrl.mouseleaveRow, + clickRow: flyoutCtrl.clickRow + }); + }; + }]) + .factory('menuAim', [function() { + var mouseLocs = []; + var MOUSE_LOCS_TRACKED = 3; // number of past mouse locations to track + + /** + * Calculate activation delay based on mouse position + * + * @method activationDelay + * @param {Integer} activeRow The currently active row + * @param {Object} $menu The $element for the menu + * @param {Object} options Options for the activation delay + * @return {Integer} Activation delay in MS + */ + function activationDelay(activeRow, $menu, options) { + var lastDelayLoc = null; + + if (!activeRow) { + // If there is no other submenu row already active, then + // go ahead and activate immediately. + return 0; + } + + var offset = $menu.offset(); + + var upperLeft = { + x: offset.left, + y: offset.top - options.tolerance + }; + + var upperRight = { + x: offset.left + $menu.outerWidth(), + y: upperLeft.y + }; + + var lowerLeft = { + x: offset.left, + y: offset.top + $menu.outerHeight() + options.tolerance + }; + + var lowerRight = { + x: offset.left + $menu.outerWidth(), + y: lowerLeft.y + }; + + var loc = mouseLocs[mouseLocs.length - 1]; + var prevLoc = mouseLocs[0]; + + if (!loc) { + return 0; + } + + if (!prevLoc) { + prevLoc = loc; + } + + if (prevLoc.x < offset.left || prevLoc.x > lowerRight.x || + prevLoc.y < offset.top || prevLoc.y > lowerRight.y) { + // If the previous mouse location was outside of the entire + // menu's bounds, immediately activate. + return 0; + } + + if (lastDelayLoc && + loc.x === lastDelayLoc.x && loc.y === lastDelayLoc.y) { + // If the mouse hasn't moved since the last time we checked + // for activation status, immediately activate. + return 0; + } + + // Detect if the user is moving towards the currently activated + // submenu. + // + // If the mouse is heading relatively clearly towards + // the submenu's content, we should wait and give the user more + // time before activating a new row. If the mouse is heading + // elsewhere, we can immediately activate a new row. + // + // We detect this by calculating the slope formed between the + // current mouse location and the upper/lower right points of + // the menu. We do the same for the previous mouse location. + // If the current mouse location's slopes are + // increasing/decreasing appropriately compared to the + // previous's, we know the user is moving toward the submenu. + // + // Note that since the y-axis increases as the cursor moves + // down the screen, we are looking for the slope between the + // cursor and the upper right corner to decrease over time, not + // increase (somewhat counterintuitively). + function slope(a, b) { + return (b.y - a.y) / (b.x - a.x); + } + + var decreasingCorner = upperRight, + increasingCorner = lowerRight; + + // Our expectations for decreasing or increasing slope values + // depends on which direction the submenu opens relative to the + // main menu. By default, if the menu opens on the right, we + // expect the slope between the cursor and the upper right + // corner to decrease over time, as explained above. If the + // submenu opens in a different direction, we change our slope + // expectations. + if (options.submenuDirection === 'left') { + decreasingCorner = lowerLeft; + increasingCorner = upperLeft; + } else if (options.submenuDirection === 'below') { + decreasingCorner = lowerRight; + increasingCorner = lowerLeft; + } else if (options.submenuDirection === 'above') { + decreasingCorner = upperLeft; + increasingCorner = upperRight; + } + + var decreasingSlope = slope(loc, decreasingCorner), + increasingSlope = slope(loc, increasingCorner), + prevDecreasingSlope = slope(prevLoc, decreasingCorner), + prevIncreasingSlope = slope(prevLoc, increasingCorner); + + if (decreasingSlope < prevDecreasingSlope && + increasingSlope > prevIncreasingSlope) { + // Mouse is moving from previous location towards the + // currently activated submenu. Delay before activating a + // new menu row, because user may be moving into submenu. + lastDelayLoc = loc; + return options.delay; + } + + lastDelayLoc = null; + return 0; + } + + /** + * Track the last locations of the mouse + * + * @method setMouseLocs + * @param {Object} e Event object + * @return {undefined} undefined + */ + function setMouseLocs(e) { + mouseLocs.push({x: e.pageX, y: e.pageY}); + + if (mouseLocs.length > MOUSE_LOCS_TRACKED) { + mouseLocs.shift(); + } + } + + function getMouseLocs() { + return mouseLocs; + } + + return { + activationDelay: activationDelay, + setMouseLocs: setMouseLocs, + getMouseLocs: getMouseLocs + }; + }]) + .directive('flyoutItem', [function() { + return { + restrict: 'E', + transclude: true, + replace: true, + scope: true, + templateUrl: 'template/flyout/flyout-item.html' + }; + }]) + .directive('flyoutLink', ['flyoutLink', function(flyoutLink) { + return { + restrict: 'E', + transclude: true, + replace: true, + require: ['^flyoutNavigation'], + scope: true, + templateUrl: 'template/flyout/flyout-link.html', + link: flyoutLink + }; + }]) + .directive('flyoutPopover', ['flyoutPopover', function(flyoutPopover) { + return { + restrict: 'E', + transclude: true, + replace: true, + require: ['^flyoutNavigation'], + scope: true, + templateUrl: 'template/flyout/flyout-popover.html', + link: flyoutPopover + }; + }]) + .directive('flyoutNavigation', [function() { + return { + restrict: 'E', + scope: { + visible: '=', + tolerance: '=', + delay: '=', + direction: '=', + enter: '=', + exit: '=', + activate: '=', + deactivate: '=', + exitmenu: '=exitmenu', // Angular treats camel case specially + selector: '@' + }, + transclude: true, + replace: true, + templateUrl: 'template/flyout/flyout.html', + controllerAs: 'flyoutCtrl', + controller: 'FlyoutController' + }; + }]) + .controller('FlyoutController', ['$element', '$scope', 'menuAim', + function( + $element, + $scope, + menuAim + ) { + + $scope.selector = $scope.selector || 'popover'; + + var vm = this; + var elem = $element; + var $menu = elem.find('.flyout-dropdown'); + + (function init() { + /** + * Attach document event handlers + */ + document.addEventListener('mousemove', menuAim.setMouseLocs); + + /** + * Attach vm methods and properties + */ + angular.extend(vm, { + mouseleaveMenu: mouseleaveMenu, + mouseenterRow: mouseenterRow, + mouseleaveRow: mouseleaveRow, + clickRow: clickRow, + closeMenu: closeMenu, + openMenu: openMenu, + getActiveRow: getActiveRow, + getSelector: getSelector, + isVisible: isVisible + }); + })(); + + var timeoutId = null; + var options = { + submenuDirection: getDirection(), + tolerance: getTolerance(), + delay: getDelay(), + enter: getEnter(), + exit: getExit(), + activate: getActivate(), + deactivate: getDeactivate(), + exitmenu: getExitMenu() + }; + + var initHeight = false; + + /** + * Set the popover menu height on the first hover / click + * + * @method setPopoverHeight + * @return {undefined} undefined + */ + function setPopoverHeight() { + initHeight = true; + elem.find('.' + $scope.selector).css('min-height', elem.outerHeight()); + } + + /** + * Hide menu when user completely exits the menu + * + * @method mouseleaveMenu + * @return {undefined} undefined + */ + function mouseleaveMenu() { + if (timeoutId) { + clearTimeout(timeoutId); + } + + if (options.exitmenu(this)) { + if (vm.activeRow) { + options.deactivate(vm.activeRow); + } + + vm.activeRow = null; + } + } + + /** + * Trigger a possible row activation when user enters + * a new row. If this is first time entering a row, + * then set the height of the menu. + * + * @method mouseenterRow + * @param {Integer} row Row index to activate + * @return {undefined} undefined + */ + function mouseenterRow(row) { + if (timeoutId) { + clearTimeout(timeoutId); + } + + options.enter(row); + possiblyActivate(row); + } + + /** + * Trigger when user exits a row. + * + * @method mouseleaveRow + * @param {Integer} row Row index to activate + * @return {undefined} undefined + */ + function mouseleaveRow(row) { + options.exit(row); + } + + /** + * Immediately activate a row if user clicks on it. + * If this is first time entering a row, then set the + * height of the menu. + * + * @method clickRow + * @param {Integer} row Row to activate + * @return {undefined} undefined + */ + function clickRow(row) { + activate(row); + } + + /** + * Close the menu + * + * @method closeMenu + * @return {undefined} undefined + */ + function closeMenu() { + $scope.visible = false; + } + + /** + * Open the menu + * + * @method openMenu + * @return {undefined} undefined + */ + function openMenu() { + $scope.visible = true; + } + + /** + * Active the provided menu row + * + * @method activate + * @param {Integer} row The row index + * @return {undefined} undefined + */ + function activate(row) { + if (row === vm.activeRow) { + return; + } + + if (vm.activeRow) { + options.deactivate(vm.activeRow); + } + + options.activate(row); + vm.activeRow = row; + } + + /** + * Possibly activate a row + * + * @method possiblyActivate + * @param {Integer} row Row index to activate + * @return {undefined} undefined + */ + function possiblyActivate(row) { + var delay = menuAim.activationDelay(vm.activeRow, $menu, options); + + if (delay) { + timeoutId = setTimeout(function() { + possiblyActivate(row); + }, delay); + } else { + activate(row); + } + } + + /** + * Returns the currently active row + * + * @return {Integer} The currently active row + */ + function getActiveRow() { + return vm.activeRow; + } + + /** + * Gets the name of the selector used for the popover + * + * @return {String} Selector name + */ + function getSelector() { + return $scope.selector; + } + + /** + * Gets the delay when user appears to be entering submenu + * + * @return {Integer} Millisecond delay when user appears to be entering submenu + */ + function getDelay() { + return $scope.delay || 1000; + } + + /** + * Gets the tolerance forgivey when entering submenu + * + * @return {Integer} Amount of forgivey when entering submenu (bigger = more) + */ + function getTolerance() { + return $scope.tolerance || 500; + } + + /** + * Gets the direction for the submenu + * + * @return {String} Direction of submenu (one of: right, left, below, above) + */ + function getDirection() { + return $scope.direction || 'right'; + } + + /** + * Gets callback function to call when a row is entered + * + * @return {Function} Callback function to call when row is entered + */ + function getEnter() { + return $scope.enter || function() { + if (!initHeight) { + setPopoverHeight(); + } + }; + } + + /** + * Gets callback function to call when a row is exited + * + * @return {Function} Callback function to call when row is exited + */ + function getExit() { + return $scope.exit || angular.noop; + } + + /** + * Gets callback function to call when a row is clicked + * + * @return {Function} Callback function to call when row is clicked + */ + function getActivate() { + return $scope.activate || function() { + if (!initHeight) { + setPopoverHeight(); + } + }; + } + + /** + * Gets callback function to call when the menu is exited + * + * @return {Function} Callback function to call when menu is exited + */ + function getExitMenu() { + return $scope.exitmenu || function() { + closeMenu(); + + return true; + }; + } + + /** + * Gets callback function to call when a row is deactivated + * + * @return {Function} Callback function to call when row is deactivated + */ + function getDeactivate() { + return $scope.deactivate || angular.noop; + } + + /** + * Returns whether the menu is currently visible or not + * + * @return {Boolean} Is the menu visible + */ + function isVisible() { + return $scope.visible || false; + } + }]); diff --git a/build/neilff-flyout.min.js b/build/neilff-flyout.min.js new file mode 100644 index 0000000..97dc4d2 --- /dev/null +++ b/build/neilff-flyout.min.js @@ -0,0 +1 @@ +"use strict";angular.module("template/flyout/flyout.html",[]).run(["$templateCache",function(e){e.put("template/flyout/flyout.html",'
    ')}]),angular.module("template/flyout/flyout-item.html",[]).run(["$templateCache",function(e){e.put("template/flyout/flyout-item.html",'
  • ')}]),angular.module("template/flyout/flyout-popover.html",[]).run(["$templateCache",function(e){e.put("template/flyout/flyout-popover.html",'
    ')}]),angular.module("template/flyout/flyout-link.html",[]).run(["$templateCache",function(e){e.put("template/flyout/flyout-link.html",'')}]),angular.module("neilff.flyout-tpls",["ng","template/flyout/flyout.html","template/flyout/flyout-item.html","template/flyout/flyout-popover.html","template/flyout/flyout-link.html"]).factory("flyoutPopover",[function(){return function(e,t,n,o){var u=o[0];angular.extend(e,{getActiveRow:u.getActiveRow,getSelector:u.getSelector})}}]).factory("flyoutLink",[function(){return function(e,t,n,o){var u=o[0];angular.extend(e,{mouseenterRow:u.mouseenterRow,mouseleaveRow:u.mouseleaveRow,clickRow:u.clickRow})}}]).factory("menuAim",[function(){function e(e,t,n){function u(e,t){return(t.y-e.y)/(t.x-e.x)}var l=null;if(!e)return 0;var i=t.offset(),r={x:i.left,y:i.top-n.tolerance},c={x:i.left+t.outerWidth(),y:r.y},a={x:i.left,y:i.top+t.outerHeight()+n.tolerance},f={x:i.left+t.outerWidth(),y:a.y},s=o[o.length-1],m=o[0];if(!s)return 0;if(m||(m=s),m.xf.x||m.yf.y)return 0;if(l&&s.x===l.x&&s.y===l.y)return 0;var v=c,y=f;"left"===n.submenuDirection?(v=a,y=r):"below"===n.submenuDirection?(v=f,y=a):"above"===n.submenuDirection&&(v=r,y=c);var p=u(s,v),d=u(s,y),g=u(m,v),x=u(m,y);return px?(l=s,n.delay):(l=null,0)}function t(e){o.push({x:e.pageX,y:e.pageY}),o.length>u&&o.shift()}function n(){return o}var o=[],u=3;return{activationDelay:e,setMouseLocs:t,getMouseLocs:n}}]).directive("flyoutItem",[function(){return{restrict:"E",transclude:!0,replace:!0,scope:!0,templateUrl:"template/flyout/flyout-item.html"}}]).directive("flyoutLink",["flyoutLink",function(e){return{restrict:"E",transclude:!0,replace:!0,require:["^flyoutNavigation"],scope:!0,templateUrl:"template/flyout/flyout-link.html",link:e}}]).directive("flyoutPopover",["flyoutPopover",function(e){return{restrict:"E",transclude:!0,replace:!0,require:["^flyoutNavigation"],scope:!0,templateUrl:"template/flyout/flyout-popover.html",link:e}}]).directive("flyoutNavigation",[function(){return{restrict:"E",scope:{visible:"=",tolerance:"=",delay:"=",direction:"=",enter:"=",exit:"=",activate:"=",deactivate:"=",exitmenu:"=exitmenu",selector:"@"},transclude:!0,replace:!0,templateUrl:"template/flyout/flyout.html",controllerAs:"flyoutCtrl",controller:"FlyoutController"}}]).controller("FlyoutController",["$element","$scope","menuAim",function(e,t,n){function o(){D=!0,L.find("."+t.selector).css("min-height",L.outerHeight())}function u(){M&&clearTimeout(M),C.exitmenu(this)&&(k.activeRow&&C.deactivate(k.activeRow),k.activeRow=null)}function l(e){M&&clearTimeout(M),C.enter(e),s(e)}function i(e){C.exit(e)}function r(e){f(e)}function c(){t.visible=!1}function a(){t.visible=!0}function f(e){e!==k.activeRow&&(k.activeRow&&C.deactivate(k.activeRow),C.activate(e),k.activeRow=e)}function s(e){var t=n.activationDelay(k.activeRow,A,C);t?M=setTimeout(function(){s(e)},t):f(e)}function m(){return k.activeRow}function v(){return t.selector}function y(){return t.delay||1e3}function p(){return t.tolerance||500}function d(){return t.direction||"right"}function g(){return t.enter||function(){D||o()}}function x(){return t.exit||angular.noop}function h(){return t.activate||function(){D||o()}}function w(){return t.exitmenu||function(){return c(),!0}}function R(){return t.deactivate||angular.noop}function b(){return t.visible||!1}t.selector=t.selector||"popover";var k=this,L=e,A=L.find(".flyout-dropdown");!function(){document.addEventListener("mousemove",n.setMouseLocs),angular.extend(k,{mouseleaveMenu:u,mouseenterRow:l,mouseleaveRow:i,clickRow:r,closeMenu:c,openMenu:a,getActiveRow:m,getSelector:v,isVisible:b})}();var M=null,C={submenuDirection:d(),tolerance:p(),delay:y(),enter:g(),exit:x(),activate:h(),deactivate:R(),exitmenu:w()},D=!1}]),angular.module("neilff.flyout",["ng"]).factory("flyoutPopover",[function(){return function(e,t,n,o){var u=o[0];angular.extend(e,{getActiveRow:u.getActiveRow,getSelector:u.getSelector})}}]).factory("flyoutLink",[function(){return function(e,t,n,o){var u=o[0];angular.extend(e,{mouseenterRow:u.mouseenterRow,mouseleaveRow:u.mouseleaveRow,clickRow:u.clickRow})}}]).factory("menuAim",[function(){function e(e,t,n){function u(e,t){return(t.y-e.y)/(t.x-e.x)}var l=null;if(!e)return 0;var i=t.offset(),r={x:i.left,y:i.top-n.tolerance},c={x:i.left+t.outerWidth(),y:r.y},a={x:i.left,y:i.top+t.outerHeight()+n.tolerance},f={x:i.left+t.outerWidth(),y:a.y},s=o[o.length-1],m=o[0];if(!s)return 0;if(m||(m=s),m.xf.x||m.yf.y)return 0;if(l&&s.x===l.x&&s.y===l.y)return 0;var v=c,y=f;"left"===n.submenuDirection?(v=a,y=r):"below"===n.submenuDirection?(v=f,y=a):"above"===n.submenuDirection&&(v=r,y=c);var p=u(s,v),d=u(s,y),g=u(m,v),x=u(m,y);return px?(l=s,n.delay):(l=null,0)}function t(e){o.push({x:e.pageX,y:e.pageY}),o.length>u&&o.shift()}function n(){return o}var o=[],u=3;return{activationDelay:e,setMouseLocs:t,getMouseLocs:n}}]).directive("flyoutItem",[function(){return{restrict:"E",transclude:!0,replace:!0,scope:!0,templateUrl:"template/flyout/flyout-item.html"}}]).directive("flyoutLink",["flyoutLink",function(e){return{restrict:"E",transclude:!0,replace:!0,require:["^flyoutNavigation"],scope:!0,templateUrl:"template/flyout/flyout-link.html",link:e}}]).directive("flyoutPopover",["flyoutPopover",function(e){return{restrict:"E",transclude:!0,replace:!0,require:["^flyoutNavigation"],scope:!0,templateUrl:"template/flyout/flyout-popover.html",link:e}}]).directive("flyoutNavigation",[function(){return{restrict:"E",scope:{visible:"=",tolerance:"=",delay:"=",direction:"=",enter:"=",exit:"=",activate:"=",deactivate:"=",exitmenu:"=exitmenu",selector:"@"},transclude:!0,replace:!0,templateUrl:"template/flyout/flyout.html",controllerAs:"flyoutCtrl",controller:"FlyoutController"}}]).controller("FlyoutController",["$element","$scope","menuAim",function(e,t,n){function o(){D=!0,L.find("."+t.selector).css("min-height",L.outerHeight())}function u(){M&&clearTimeout(M),C.exitmenu(this)&&(k.activeRow&&C.deactivate(k.activeRow),k.activeRow=null)}function l(e){M&&clearTimeout(M),C.enter(e),s(e)}function i(e){C.exit(e)}function r(e){f(e)}function c(){t.visible=!1}function a(){t.visible=!0}function f(e){e!==k.activeRow&&(k.activeRow&&C.deactivate(k.activeRow),C.activate(e),k.activeRow=e)}function s(e){var t=n.activationDelay(k.activeRow,A,C);t?M=setTimeout(function(){s(e)},t):f(e)}function m(){return k.activeRow}function v(){return t.selector}function y(){return t.delay||1e3}function p(){return t.tolerance||500}function d(){return t.direction||"right"}function g(){return t.enter||function(){D||o()}}function x(){return t.exit||angular.noop}function h(){return t.activate||function(){D||o()}}function w(){return t.exitmenu||function(){return c(),!0}}function R(){return t.deactivate||angular.noop}function b(){return t.visible||!1}t.selector=t.selector||"popover";var k=this,L=e,A=L.find(".flyout-dropdown");!function(){document.addEventListener("mousemove",n.setMouseLocs),angular.extend(k,{mouseleaveMenu:u,mouseenterRow:l,mouseleaveRow:i,clickRow:r,closeMenu:c,openMenu:a,getActiveRow:m,getSelector:v,isVisible:b})}();var M=null,C={submenuDirection:d(),tolerance:p(),delay:y(),enter:g(),exit:x(),activate:h(),deactivate:R(),exitmenu:w()},D=!1}]); \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index 3df543e..9f4f2d6 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,12 +1,7 @@ var gulp = require('gulp-help')(require('gulp')); -var eslint = require('gulp-eslint'); -var karma = require('gulp-karma'); -var connect = require('gulp-connect'); -var uglify = require('gulp-uglify'); -var ngAnnotate = require('gulp-ng-annotate'); -var rename = require('gulp-rename'); gulp.task('connect', false, function() { + var connect = require('gulp-connect'); connect.server({ root: './', livereload: true @@ -29,6 +24,7 @@ gulp.task('watch', false, function () { }); gulp.task('lint', false, function () { + var eslint = require('gulp-eslint'); return gulp.src(['src/*.js']) .pipe(eslint()) .pipe(eslint.format()) @@ -36,6 +32,8 @@ gulp.task('lint', false, function () { }); gulp.task('test', 'Runs karma and lints code.', ['lint'], function() { + var karma = require('gulp-karma'); + var testFiles = [ 'bower_components/jquery/dist/jquery.js', 'bower_components/angular/angular.js', @@ -54,13 +52,26 @@ gulp.task('test', 'Runs karma and lints code.', ['lint'], function() { }); }); -gulp.task('build', 'Builds project (concat, ngmin, uglify).', function () { - return gulp.src('src/*.js') +gulp.task('css', function () { + var rename = require('gulp-rename'); + + return gulp.src('src/flyout.css') + .pipe(rename('neilff-flyout.css')) + .pipe(gulp.dest('build')); +}); + +gulp.task('build', 'Builds project (concat, ngmin, uglify).', ['css'], function () { + var uglify = require('gulp-uglify'); + var ngAnnotate = require('gulp-ng-annotate'); + var rename = require('gulp-rename'); + var concat = require('gulp-concat'); + + return gulp.src(['src/flyout-tpls.js', 'src/flyout.js']) .pipe(ngAnnotate()) + .pipe(concat('neilff-flyout.js')) + .pipe(gulp.dest('build')) .pipe(uglify()) - .pipe(rename({ - extname: '.min.js' - })) + .pipe(rename({ extname: '.min.js' })) .pipe(gulp.dest('build')); }); diff --git a/package.json b/package.json index 92f8ba1..97a7f48 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,13 @@ "name": "angular-menu-aim", "version": "1.1.0", "description": "jQuery Menu aim adopted for Angular", - "main": "component.js", + "main": "build/neilff-flyout.js", + "files": [ + "build" + ], "scripts": { - "test": "node_modules/gulp/bin/gulp.js test", - "postinstall": "node_modules/bower/bin/bower install" + "prepublish": "gulp build", + "test": "gulp test" }, "author": { "name": "Neil Fenton" @@ -15,6 +18,7 @@ "bower": "^1.4.1", "chai": "^2.3.0", "gulp": "^3.8.11", + "gulp-concat": "^2.6.1", "gulp-connect": "^2.2.0", "gulp-eslint": "^0.12.0", "gulp-help": "^1.3.4",