diff --git a/.gitignore b/.gitignore index 5e22086..bebc304 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ -.idea +.* node_modules bower_components coverage treecontrol.iml .DS_Store Chromium 49.0.2623 (Ubuntu) + diff --git a/Gruntfile.js b/Gruntfile.js index 4851758..7f17e8a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -34,6 +34,7 @@ module.exports = function(grunt) { 'demo/angular.1.3.12.js', 'demo/angular-mocks.1.3.12.js', 'angular-tree-control.js', + 'context-menu.js', 'test/**/*.js' ] } diff --git a/README.md b/README.md index 0ea4e53..ce390a0 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,13 @@ Copy the script and css into your project and add a script and link tag to your ```html + + + + + ``` @@ -113,6 +118,7 @@ Attributes of angular treecontrol - `expanded-nodes` : [Array[Node]] binding for the expanded nodes in the tree. Updating this value updates the nodes that are expanded in the tree. - `on-selection` : `(node, selected)` callback called whenever selecting a node in the tree. The callback expression can use the selected node (`node`) and a boolean which indicates if the node was selected or deselected (`selected`). - `on-node-toggle` : `(node, expanded)` callback called whenever a node expands or collapses in the tree. The callback expression can use the toggled node (`node`) and a boolean which indicates expansion or collapse (`expanded`). +- `on-right-click` : `(node)` callback called whenever a node is right-clicked. - `options` : different options to customize the tree control. - `multiSelection` : [Boolean] enable multiple nodes selection in the tree. - `nodeChildren` : the name of the property of each node that holds the node children. Defaults to 'children'. @@ -133,6 +139,7 @@ Attributes of angular treecontrol - `reverse-order` : whether or not to reverse the ordering of sibling nodes based on the value of `order-by` - `filter-expression` : value for ng-repeat to use for filtering the sibling nodes - `filter-comparator` : value for ng-repeat to use for comparing nodes with the filter expression +- `menu-id` : the id of an ul element which will be displayed after a right-click ### The tree labels diff --git a/angular-tree-control.js b/angular-tree-control.js index 3043027..c0c3212 100644 --- a/angular-tree-control.js +++ b/angular-tree-control.js @@ -80,11 +80,12 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex ensureDefault($scope.options, "allowDeselect", true); ensureDefault($scope.options, "isSelectable", defaultIsSelectable); } - - angular.module( 'treeControl', [] ) + + angular.module( 'treeControl', ['contextMenu'] ) .constant('treeConfig', { templateUrl: null }) + .directive( 'treecontrol', ['$compile', function( $compile ) { /** * @param cssClass - the css class @@ -100,9 +101,6 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex else return ""; } - - - return { restrict: 'EA', require: "treecontrol", @@ -114,6 +112,8 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex expandedNodes: "=?", onSelection: "&", onNodeToggle: "&", + onRightClick: "&", + menuId: "@", options: "=?", orderBy: "=?", reverseOrder: "@", @@ -121,11 +121,11 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex filterComparator: "=?" }, controller: ['$scope', '$templateCache', '$interpolate', 'treeConfig', function ($scope, $templateCache, $interpolate, treeConfig) { - + $scope.options = $scope.options || {}; - + ensureAllDefaultOptions($scope); - + $scope.selectedNodes = $scope.selectedNodes || []; $scope.expandedNodes = $scope.expandedNodes || []; $scope.expandedNodesMap = {}; @@ -249,6 +249,25 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex } }; + $scope.rightClickNodeLabel = function( targetNode, $event ) { + + // Is there a right click function?? + if($scope.onRightClick) { + + // Turn off the browser default context-menu + if($event) + $event.preventDefault(); + + // Are are we changing the 'selected' node (as well)? + if ($scope.selectedNode != targetNode) { + this.selectNodeLabel(targetNode); + } + + // Finally go do what they asked + $scope.onRightClick({node: targetNode}); + } + }; + $scope.selectedClass = function() { var isThisNodeSelected = isSelectedNode(this.node); var labelSelectionClass = classIfDefined($scope.options.injectClasses.labelSelected, false); @@ -266,6 +285,9 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex }; //tree template + var rcLabel = $scope.onRightClick ? ' tree-right-click="rightClickNodeLabel(node)"' : ''; + var ctxMenuId = $scope.menuId ? ' context-menu-id="'+ $scope.menuId+'"' : ''; + $scope.isReverse = function() { return !($scope.reverseOrder === 'false' || $scope.reverseOrder === 'False' || $scope.reverseOrder === '' || $scope.reverseOrder === false); }; @@ -273,7 +295,6 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex $scope.orderByFunc = function() { return $scope.orderBy; }; -// return "" + $scope.orderBy; var templateOptions = { orderBy: $scope.orderBy ? " | orderBy:orderByFunc():isReverse()" : '', @@ -298,7 +319,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex 'set-node-to-data>' + '' + '' + - '
' + + '
' + '' + '' + ''; @@ -306,6 +327,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex this.template = $compile($interpolate(template)({options: templateOptions})); }], + compile: function(element, attrs, childTranscludeFn) { return function ( scope, element, attrs, treemodelCntr ) { @@ -381,6 +403,18 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex } }; }]) + + .directive('treeRightClick', function($parse) { + return function(scope, element, attrs) { + var fn = $parse(attrs.treeRightClick); + element.bind('contextmenu', function(event) { + scope.$apply(function() { + fn(scope, {$event:event}); // go do our stuff + }); + }); + }; + }) + .directive("treeitem", function() { return { restrict: 'E', diff --git a/context-menu.js b/context-menu.js new file mode 100644 index 0000000..12b8ddb --- /dev/null +++ b/context-menu.js @@ -0,0 +1,173 @@ +(function ( angular ) { + 'use strict'; + + /* Figure out page (viewport) dimensions of current page, by + * putting an empty DIV in the bottom right, and checking its offset. + */ + function getPageDimensions() { + var bttmRight = document.createElement("div"); + bttmRight.setAttribute("style" , "visibility:hidden;position:fixed;bottom:0px;right:0px;"); + document.getElementsByTagName("body")[0].appendChild(bttmRight); + var pageWidth = bttmRight.offsetLeft; + var pageHeight = bttmRight.offsetTop; + bttmRight.parentNode.removeChild(bttmRight); + return { width:pageWidth, height:pageHeight }; + } + + angular.module( 'contextMenu', [] ) + + .directive('contextMenuId', ['$document', function($document) { + + return { + restrict : 'A', + scope : '@&', + compile: function compile(tElement, tAttrs, transclude) { + + return { + post: function postLink(scope, iElement, iAttrs, controller) { + + var ul = angular.element(document.querySelector('#' + iAttrs.contextMenuId)); + + ul.css({ 'display' : 'none'}); + + // right-click on context-menu will show the menu + iElement.bind('contextmenu', function showContextMenu(event) { + + // don't do the normal browser right-click context menu + event.preventDefault(); + + // Organise to show off the menu (in roughly the right place) + ul.css({ + visibility:"hidden", + position: "fixed", + display: "block", + left: event.clientX + 'px', + top: event.clientY + 'px' + }); + + var ulDim = { height: ul.prop("clientHeight"), + width: ul.prop("cientWidth") + }; + + var pgDim = getPageDimensions(); + + // will ctxMenu fit on screen (height-wise) ? + // TODO: figure out why we need the fudge-factor of 14 + var ulTop = event.clientY + ulDim.height <= pgDim.height - 14 + ? event.clientY + : pgDim.height - ulDim.height - 14; + + // will ctxMenu fit on screen (width-wise) ? + var ulLeft = event.clientX + ulDim.width <= pgDim.width - 2 + ? event.clientX + : pgDim.width - ulDim.width - 2; + + // Ok, now show it off in the right place + ul.css({ + visibility:"visible", + position: "fixed", + display: "block", + left: ulLeft + 'px', + top: ulTop + 'px' + }); + + // setup a one-time click event on the document to hide the dropdown-menu + $document.one('click', function hideContextMenu(event) { + ul.css({ + 'display' : 'none' + }); + }); + }); + } + }; + } + }; + }]) + + .directive('contextSubmenuId', ['$document', function($document) { + return { + restrict : 'A', + scope : '@&', + compile: function compile(tElement, tAttrs, transclude) { + return { + post: function postLink(scope, iElement, iAttrs, controller) { + + var ul = angular.element(document.querySelector('#' + iAttrs.contextSubmenuId)); + + ul.css({ 'display' : 'none'}); + + + iElement.bind('mouseover', function showSubContextMenu(event) { + // use CSS to move and show the sub dropdown-menu + if(ul.css("display") == 'none') { + + // Organise to show off the sub-menu (in roughly the right place) + ul.css({ + visibility:"hidden", + position: "fixed", + display: "block", + left: event.clientX + 'px', + top: event.clientY + 'px' + }); + + var ulDim = { height: ul.prop("clientHeight"), + width: ul.prop("clientWidth") + }; + + var pgDim = getPageDimensions(); + + + // Will ctxSubMenu fit (height-wise) ? + // TODO: figure out why we need the fudge-factor of 14 + var ulTop = event.clientY + ulDim.height <= pgDim.height - 14 + ? event.clientY + : pgDim.height - ulDim.height - 14; + + // Will ctxSubMenu fit (on the right of parent menu) ? + var ulLeft = + (event.target.offsetParent.offsetLeft + + event.target.clientWidth + ulDim.width < pgDim.width) + ? event.target.offsetParent.offsetLeft + + event.target.clientWidth + + : event.target.offsetParent.offsetLeft - ulDim.width; + + // OK, now show it off in the right place + ul.css({ + visibility:"visible", + position: "fixed", + display: "block", + left: ulLeft + 'px', + top: ulTop + 'px' + }); + + // Each uncle/aunt menu item needs a mouseover event to make the subContext menu disappear + angular.forEach(iElement[0].parentElement.parentElement.children, function(child, ndx) { + if(child !== iElement[0].parentElement) { + angular.element(child).one('mouseover', function(event) { + if(ul.css("display") == 'block') { + ul.css({ + 'display' : 'none' + }); + } + }); + } + }); + } + + // setup a one-time click event on the document to hide the sub dropdown-menu + $document.one('click', function hideContextMenu(event) { + if(ul.css("display") == 'block') { + ul.css({ + 'display' : 'none' + }); + } + }); + }); + } + }; + } + }; + }]); + +})( angular ); diff --git a/index.html b/index.html index e816839..0fc9fae 100644 --- a/index.html +++ b/index.html @@ -12,6 +12,7 @@ + @@ -1049,7 +1051,7 @@

Custom Branch / Leaf (options.isLeaf)

@@ -1068,7 +1070,8 @@

Sorting tree nodes (order-by, reserve-order)

-

The order-by and reverse-order properties allows controlling the sorting of the tree nodes. +

The order-by and reverse-order properties allows + controlling the sorting of the tree nodes. The value of those attributes is used with the ng-repeat orderBy filter - see more details at the Angular JS orderBy docs.

The sorting is done for each branch individually (sorting does not change the structure of the tree itself).

@@ -1232,15 +1235,130 @@

Custom Equality (options.equality)

+ +
+ +
+
+
+
+
EXAMPLE:
+ + +
+
+
+
+

The angular tree control provides right-click and context menu functionality: +

    +
  • + on-right-click + is evaluated as an angular expression (like ng-click value) when + a right-click event is detected. If context menu is enabled, then this will be + evaluated before the context menu is opened. +
  • +
  • + menu-id is the id for an <ul> + element which will be displayed after a right-click has been detected. +
  • +
+

+

The last right-click was on the {{lastRightClickNode}} node

+

The last food to be cooked was {{lastCookedFood}}

+

The last context menu action was {{lastAction}}

+
+
+
+ + +

+            
+ +

+            
+
+
+ + +
+ +
EXAMPLE:
+
External Template (options.templateUrl, treeConfig.templateUrl)
+