The driving force behind my current Javascript experiment was a need to modernize the UI for the Pivot Table. Clearly the old good JPivot-like UI doesn't match modern users expectations: context menus, drag & drop items, etc. So, once my Javascript cellset fulfilled basic functionality (drill up/down and add/remove hierarchies) I've started my work to rejuvenate the UI into a more stylish one.
Bootstrap looked like the obvious choice for styling, and the AngularJS team has a project providing Angular directives for Bootstrap components. So, I made no further research (as I've said in a previous post, I'm pretty new to this Javascript thing, and have no strong opinions on it).
Table Styling: A Piece of Cake
Never underestimate a CSS style sheet, just:
- include Bootstrap's stylesheet
- remove previous lame styling CSSs
- add a handful of classes to the
<table>
element:table, table-striped, table-bordered
table-hover, table-condensed
- include expand/collapse icons (courtesy of Glyphicons through
Bootstrap)
and voilá, a modern looking CellSet with row hover highlight for free.
Context Menu
Wouldn't be nice if you could just right-click on a hierarchy element to remove it from the hierachy, remove the whole hierarchy, or add a new one to that axis? That's the first step to get rid of the hierarchy panel in the old JSF version, and there are a couple of things to do to make that happen.
Styling
Bootstrap's toolbox saves our day again: just use its dropdowns to get something like this:
Dynamic Behaviour
I'll start with the context for the menu. I modified the CellSet jQuery component to add a function olapGetMetadataAt(element)
to the DOM element it's binded. This function provides, for each DOM element within the table, metadata information for the represented olap
object. The returned object will have the following properties:
axisOrdinal {number}
- for hierarchies and hierarchy members, the ordinal for the axis containing it (0 for columns, 1 for rows)
hierarchy {Object}
- hierarchy metadata for hierarchies and hierarchy members
member {Object}
- member metadata for hierarchy members
isLeaf {Boolean}
- indicates if that member is a leaf in its hierarchy
With this function in place, we can add a ng-click
directive to the HTML element containing the cell set, and use the event information to get the metadata for the clicked element. So we modify the div
containing the cellset like this:
<div ng-click="showContextMenu($event)" olap-cellset="query"/>
And then the implementation for the click handler in the Angular controller sets a scope property (currentMember
) with the clicked on metadata:
$scope.showContextMenu = function(event) { var cellSetElem = event.currentTarget var metadata = cellSetElem.olapGetMetadataAt( angular.element(event.target) ); $scope.currentMember = metadata; };
The HTML for the menu can now be rendered using standard ng-repeat
directives based on this property:
<ul aria-labelledby="dropdownMenu" class="dropdown-menu" context-menu-on="currentMember" role="menu"> <li> <a ng-click="...">Remove <b>{{context.hierarchy.caption}}</b> hierarchy</a></li> <li class="dropdown-submenu"><a href="#">Add hierarchy</a> <ul class="dropdown-menu"> <li ng-repeat="hierarchy in $parent.hierarchies"> <a ng-click="..." href="#">{{hierarchy.caption}}</a> </li> </ul> </li> </ul>
ng-click
directives are used to implement menu actions delegating to controller functions, the Remove menu option handler I've omitted in line 3 of the previous code snippet looks like this:
$parent.removeHierarchy(context.axisOrdinal,context.hierarchy.caption)
And, finally, the custom context-menu-on
directive shows/hides the context menu based on the value of the watched context property (passed as the value for the directive attribute)
.directive('contextMenuOn', function () { return { scope: {context: '=contextMenuOn'}, link: function (scope, iElement, iAttrs) { function hideMenu() { scope.$apply(function (scope) { scope.context = null }); $('html').unbind('click', hideMenu); } scope.$watch('context', function (newValue, oldValue) { if (newValue && !oldValue) { // The menu is closed if the menu is alreadyOpen (oldValue truthy) scope.context = newValue; iElement.css('position', 'absolute'); iElement.css('left', window.event.clientX - 5); iElement.css('top', window.event.clientY - 5); iElement.show(); $('html').click(hideMenu); iElement.click(function (event) { event.stopPropagation(); }); if ( window.event.stopPropagation ) { window.event.stopPropagation(); } else { window.event.cancelBubble = true; } } else { scope.context = null; iElement.hide(); } }); } }; });
No comments :
Post a Comment