1 /*! ui-grid - v2.0.7-g7aeb576 - 2013-12-18
2 * Copyright (c) 2013 ; Licensed MIT */
6 var app = angular.module('ui.grid.body', []);
8 app.directive('uiGridBody', ['$log', 'GridUtil', function($log, GridUtil) {
12 templateUrl: 'ui-grid/ui-grid-body',
15 tableClass: '=uiGridTableClass'
17 link: function(scope, elm, attrs, uiGridCtrl) {
18 $log.debug('body postlink scope', scope.$id);
20 if (uiGridCtrl === undefined) {
21 $log.warn('[ui-grid-body] uiGridCtrl is undefined!');
24 if (uiGridCtrl && typeof(uiGridCtrl.columns) !== 'undefined' && uiGridCtrl.columns) {
25 scope.columns = uiGridCtrl.columns;
27 if (uiGridCtrl && typeof(uiGridCtrl.gridData) !== 'undefined' && uiGridCtrl.gridData) {
28 scope.gridData = uiGridCtrl.gridData;
38 var app = angular.module('ui.grid.header', ['ui.grid.util']);
40 app.directive('uiGridHeader', ['$log', '$templateCache', '$compile', 'GridUtil', function($log, $templateCache, $compile, GridUtil) {
43 // templateUrl: 'ui-grid/ui-grid-header',
48 tableClass: '=uiGridTableClass'
50 compile: function (elm, attrs) {
51 $log.debug('header compile');
53 // If the contents of the grid element are empty, use the default grid template
55 if (elm.html() === '' || /^\s*$/.test(elm.html())) {
56 tmpl = $templateCache.get('ui-grid/ui-grid-header');
59 var preLink = function (scope, elm, attrs) {
60 $log.debug('header prelink scope', scope.$id);
65 $compile(elm.contents())(scope);
68 var postLink = function(scope, elm, attrs, uiGridCtrl) {
69 $log.debug('header postlink scope', scope.$id);
71 if (uiGridCtrl === undefined) {
72 $log.warn('[ui-grid-header] uiGridCtrl is undefined!');
75 // Get the column defs from the parent grid controller
76 if (uiGridCtrl && typeof(uiGridCtrl.columns) !== 'undefined' && uiGridCtrl.columns) {
77 scope.columns = uiGridCtrl.columns;
82 // $compile(elm.contents())(scope);
85 // scope.$watch('columns', function(n, o) {
86 // $log.debug('columns change', n, o);
87 // var contents = elm.contents();
88 // $compile(contents)(scope);
106 * @name ui.grid.style.directive:uiGridStyle
111 * Allows us to interpolate expressions in `<style>` elements. Angular doesn't do this by default as it can/will/might? break in IE8.
114 <example module="app">
116 var app = angular.module('app', ['ui.grid']);
118 app.controller('MainCtrl', ['$scope', function ($scope) {
119 $scope.myStyle = '.blah { color: red }';
122 <file name="index.html">
123 <div ng-controller="MainCtrl">
124 <style ui-grid-style>{{ myStyle }}</style>
125 <span class="blah">I am red.</span>
131 var app = angular.module('ui.grid.style', []);
133 app.directive('uiGridStyle', ['$interpolate', '$sce', function($interpolate, $sce) {
137 link: function(scope, element) {
138 var interpolateFn = $interpolate(element.text(), true);
141 scope.$watch(interpolateFn, function(value) {
153 var app = angular.module('ui.grid', ['ui.grid.header', 'ui.grid.body', 'ui.grid.style', 'ui.virtual-repeat']);
157 * @name ui.grid.directive:uiGrid
160 * @param {array} uiGrid Array of rows to display in the grid
162 * @description Create a very basic grid.
165 <example module="app">
167 var app = angular.module('app', ['ui.grid']);
169 app.controller('MainCtrl', ['$scope', function ($scope) {
171 { name: 'Bob', title: 'CEO' },
172 { name: 'Frank', title: 'Lowly Developer' }
176 <file name="index.html">
177 <div ng-controller="MainCtrl">
178 <div ui-grid="data"></div>
183 app.directive('uiGrid',
196 function preLink(scope, elm, attrs) {
197 var options = scope.uiGrid;
199 // Create an ID for this grid
200 scope.gridId = GridUtil.newId();
202 // Get the grid dimensions from the element
204 // Initialize the grid
206 // Get the column definitions
207 // Put a watch on them
209 console.log('gridId', scope.gridId);
211 elm.on('$destroy', function() {
212 // Remove columnDefs watch
217 templateUrl: 'ui-grid/ui-grid',
221 compile: function () {
226 controller: function ($scope, $element, $attrs) {
237 // (part of the sf.virtualScroll module).
238 var mod = angular.module('ui.virtual-repeat', []);
240 var DONT_WORK_AS_VIEWPORTS = ['TABLE', 'TBODY', 'THEAD', 'TR', 'TFOOT'];
241 var DONT_WORK_AS_CONTENT = ['TABLE', 'TBODY', 'THEAD', 'TR', 'TFOOT'];
242 var DONT_SET_DISPLAY_BLOCK = ['TABLE', 'TBODY', 'THEAD', 'TR', 'TFOOT'];
244 // Utility to clip to range
245 function clip(value, min, max){
246 if (angular.isArray(value)) {
247 return angular.forEach(value, function(v) {
248 return clip(v, min, max);
252 return Math.max(min, Math.min(value, max));
255 mod.directive('uiVirtualRepeat', ['$log', '$rootElement', function($log, $rootElement){
257 // Turn the expression supplied to the directive:
261 // into `{ value: "a", collection: "b" }`
262 function parseRepeatExpression(expression) {
263 var match = expression.match(/^\s*([\$\w]+)\s+in\s+([\S\s]*)$/);
265 throw new Error("Expected uiVirtualRepeat in form of '_item_ in _collection_' but got '" + expression + "'.");
273 // Utility to filter out elements by tag name
274 function isTagNameInList(element, list) {
276 tag = element.tagName.toUpperCase();
278 for (t = 0; t < list.length; t++) {
279 if (list[t] === tag) {
287 // Utility to find the viewport/content elements given the start element:
288 function findViewportAndContent(startElement) {
289 /*jshint eqeqeq:false, curly:false */
290 var root = $rootElement[0];
293 // Somewhere between the grandparent and the root node
294 for (e = startElement.parent().parent()[0]; e !== root; e = e.parentNode) {
296 if (e.nodeType != 1) break;
297 // that isn't in the blacklist (tables etc.),
298 if (isTagNameInList(e, DONT_WORK_AS_VIEWPORTS)) continue;
299 // has a single child element (the content),
300 if (e.childElementCount != 1) continue;
301 // which is not in the blacklist
302 if (isTagNameInList(e.firstElementChild, DONT_WORK_AS_CONTENT)) continue;
304 for (n = e.firstChild; n; n = n.nextSibling) {
305 if (n.nodeType == 3 && /\S/g.test(n.textContent)) {
311 // That element should work as a viewport.
313 viewport: angular.element(e),
314 content: angular.element(e.firstElementChild)
319 throw new Error("No suitable viewport element");
322 // Apply explicit height and overflow styles to the viewport element.
324 // If the viewport has a max-height (inherited or otherwise), set max-height.
325 // Otherwise, set height from the current computed value or use
326 // window.innerHeight as a fallback
328 function setViewportCSS(viewport) {
329 var viewportCSS = {'overflow': 'auto'};
331 var style = window.getComputedStyle ?
332 window.getComputedStyle(viewport[0]) :
333 viewport[0].currentStyle;
335 var maxHeight = style && style.getPropertyValue('max-height');
336 var height = style && style.getPropertyValue('height');
338 if (maxHeight && maxHeight !== '0px') {
339 viewportCSS.maxHeight = maxHeight;
341 else if (height && height !== '0px') {
342 viewportCSS.height = height;
345 viewportCSS.height = window.innerHeight;
348 viewport.css(viewportCSS);
351 // Apply explicit styles to the content element to prevent pesky padding
352 // or borders messing with our calculations:
353 function setContentCSS(content) {
358 'box-sizing': 'border-box'
361 content.css(contentCSS);
364 // TODO: compute outerHeight (padding + border unless box-sizing is border)
365 function computeRowHeight(element) {
366 var style = window.getComputedStyle ?
367 window.getComputedStyle(element) :
368 element.currentStyle;
370 var maxHeight = style && style.getPropertyValue('max-height');
371 var height = style && style.getPropertyValue('height');
373 if (height && height !== '0px' && height !== 'auto') {
374 // $log.info('Row height is "%s" from css height', height);
376 else if (maxHeight && maxHeight !== '0px' && maxHeight !== 'none') {
378 // $log.info('Row height is "%s" from css max-height', height);
380 else if (element.clientHeight) {
381 height = element.clientHeight + 'px';
382 // $log.info('Row height is "%s" from client height', height);
385 //throw new Error("Unable to compute height of row");
389 angular.element(element).css('height', height);
391 return parseInt(height, 10);
394 // The compile gathers information about the declaration. There's not much
395 // else we could do in the compile step as we need a viewport parent that
396 // is exculsively ours - this is only available at link time.
397 function uiVirtualRepeatCompile(element, attr, linker) {
398 var ident = parseRepeatExpression(attr.uiVirtualRepeat);
402 // Set up the initial value for our watch expression (which is just the
403 // start and length of the active rows and the collection length) and
404 // adds a listener to handle child scopes based on the active rows.
405 function sfVirtualRepeatPostLink(scope, iterStartElement, attrs) {
408 scope.rendered = rendered;
412 var dom = findViewportAndContent(iterStartElement);
413 // The list structure is controlled by a few simple (visible) variables:
414 var state = 'ngModel' in attrs ? scope.$eval(attrs.ngModel) : {};
415 // - The index of the first active element
416 state.firstActive = 0;
417 // - The index of the first visible element
418 state.firstVisible = 0;
419 // - The number of elements visible in the viewport.
421 // - The number of active elements
423 // - The total number of elements
425 // - The point at which we add new elements
426 state.lowWater = state.lowWater || 10;
427 // - The point at which we remove old elements
428 state.highWater = state.highWater || 20;
429 // TODO: now watch the water marks
431 setContentCSS(dom.content);
432 setViewportCSS(dom.viewport);
433 // When the user scrolls, we move the `state.firstActive`
434 dom.viewport.bind('scroll', sfVirtualRepeatOnScroll);
436 // The watch on the collection is just a watch on the length of the
437 // collection. We don't care if the content changes.
438 scope.$watch(sfVirtualRepeatWatchExpression, sfVirtualRepeatListener, true);
440 // and that's the link done! All the action is in the handlers...
444 // Apply explicit styles to the item element
445 function setElementCSS(element) {
447 // no margin or it'll screw up the height calculations.
451 if (!isTagNameInList(element[0], DONT_SET_DISPLAY_BLOCK)) {
452 // display: block if it's safe to do so
453 elementCSS.display = 'block';
457 elementCSS.height = rowHeight + 'px';
460 element.css(elementCSS);
463 function makeNewScope (idx, collection, containerScope) {
464 var childScope = containerScope.$new();
465 childScope[ident.value] = collection[idx];
466 childScope.$index = idx;
467 childScope.$first = (idx === 0);
468 childScope.$last = (idx === (collection.length - 1));
469 childScope.$middle = !(childScope.$first || childScope.$last);
470 childScope.$watch(function updateChildScopeItem(){
471 childScope[ident.value] = collection[idx];
476 function addElements (start, end, collection, containerScope, insPoint) {
477 var frag = document.createDocumentFragment();
478 var newElements = [], element, idx, childScope;
480 for (idx = start; idx !== end; idx++) {
481 childScope = makeNewScope(idx, collection, containerScope);
482 element = linker(childScope, angular.noop);
483 setElementCSS(element);
484 newElements.push(element);
485 frag.appendChild(element[0]);
488 insPoint.after(frag);
492 function recomputeActive() {
493 // We want to set the start to the low water mark unless the current
494 // start is already between the low and high water marks.
495 var start = clip(state.firstActive, state.firstVisible - state.lowWater, state.firstVisible - state.highWater);
496 // Similarly for the end
497 var end = clip(state.firstActive + state.active,
498 state.firstVisible + state.visible + state.lowWater,
499 state.firstVisible + state.visible + state.highWater );
500 state.firstActive = Math.max(0, start);
501 state.active = Math.min(end, state.total) - state.firstActive;
504 function sfVirtualRepeatOnScroll(evt) {
509 // Enter the angular world for the state change to take effect.
510 scope.$apply(function() {
511 state.firstVisible = Math.floor(evt.target.scrollTop / rowHeight);
512 state.visible = Math.ceil(dom.viewport[0].clientHeight / rowHeight);
514 // $log.log('scroll to row %o', state.firstVisible);
515 sticky = evt.target.scrollTop + evt.target.clientHeight >= evt.target.scrollHeight;
518 // $log.log(' state is now %o', state);
519 // $log.log(' sticky = %o', sticky);
523 function sfVirtualRepeatWatchExpression(scope) {
524 var coll = scope.$eval(ident.collection);
526 if (coll.length !== state.total) {
527 state.total = coll.length;
532 start: state.firstActive,
533 active: state.active,
538 function destroyActiveElements (action, count) {
541 remover = Array.prototype[action];
543 for (ii = 0; ii < count; ii++) {
544 dead = remover.call(rendered);
545 dead.scope().$destroy();
550 // When the watch expression for the repeat changes, we may need to add
551 // and remove scopes and elements
552 function sfVirtualRepeatListener(newValue, oldValue, scope) {
553 var oldEnd = oldValue.start + oldValue.active,
554 collection = scope.$eval(ident.collection),
557 if (newValue === oldValue) {
558 // $log.info('initial listen');
559 newElements = addElements(newValue.start, oldEnd, collection, scope, iterStartElement);
560 rendered = newElements;
562 if (rendered.length) {
563 rowHeight = computeRowHeight(newElements[0][0]);
567 var newEnd = newValue.start + newValue.active;
568 var forward = newValue.start >= oldValue.start;
569 var delta = forward ? newValue.start - oldValue.start
570 : oldValue.start - newValue.start;
571 var endDelta = newEnd >= oldEnd ? newEnd - oldEnd : oldEnd - newEnd;
572 var contiguous = delta < (forward ? oldValue.active : newValue.active);
573 // $log.info('change by %o,%o rows %s', delta, endDelta, forward ? 'forward' : 'backward');
576 // $log.info('non-contiguous change');
577 destroyActiveElements('pop', rendered.length);
578 rendered = addElements(newValue.start, newEnd, collection, scope, iterStartElement);
582 // $log.info('need to remove from the top');
583 destroyActiveElements('shift', delta);
586 // $log.info('need to add at the top');
587 newElements = addElements(
590 collection, scope, iterStartElement);
591 rendered = newElements.concat(rendered);
594 if (newEnd < oldEnd) {
595 // $log.info('need to remove from the bottom');
596 destroyActiveElements('pop', oldEnd - newEnd);
599 var lastElement = rendered[rendered.length-1];
600 // $log.info('need to add to the bottom');
601 newElements = addElements(
604 collection, scope, lastElement);
606 rendered = rendered.concat(newElements);
610 if (!rowHeight && rendered.length) {
611 rowHeight = computeRowHeight(rendered[0][0]);
614 dom.content.css({'padding-top': newValue.start * rowHeight + 'px'});
617 dom.content.css({'height': newValue.len * rowHeight + 'px'});
620 dom.viewport[0].scrollTop = dom.viewport[0].clientHeight + dom.viewport[0].scrollHeight;
623 scope.rendered = rendered;
630 post: sfVirtualRepeatPostLink
636 transclude: 'element',
639 compile: uiVirtualRepeatCompile,
640 controller: ['$scope', function ($scope) {
641 this.visibleRows = 0;
642 this.visibleRows = this.visibleRows + 1;
652 var app = angular.module('ui.grid.util', []);
656 * @name ui.grid.util.service:GridUtil
658 * @description Grid utility functions
660 app.service('GridUtil', function () {
666 * @name readableColumnName
667 * @methodOf ui.grid.util.service:GridUtil
669 * @param {string} columnName Column name as a string
670 * @returns {string} Column name appropriately capitalized and split apart
673 <example module="app">
675 var app = angular.module('app', ['ui.grid.util']);
677 app.controller('MainCtrl', ['$scope', 'GridUtil', function ($scope, GridUtil) {
678 $scope.name = 'firstName';
679 $scope.columnName = function(name) {
680 return GridUtil.readableColumnName(name);
684 <file name="index.html">
685 <div ng-controller="MainCtrl">
686 <strong>Column name:</strong> <input ng-model="name" />
688 <strong>Output:</strong> <span ng-bind="columnName(name)"></span>
693 readableColumnName: function (columnName) {
694 // Convert underscores to spaces
695 if (typeof(columnName) === 'undefined' || columnName === undefined || columnName === null) { return columnName; }
697 if (typeof(columnName) !== 'string') {
698 columnName = String(columnName);
701 return columnName.replace(/_+/g, ' ')
702 // Replace a completely all-capsed word with a first-letter-capitalized version
703 .replace(/^[A-Z]+$/, function (match) {
704 return angular.lowercase(angular.uppercase(match.charAt(0)) + match.slice(1));
706 // Capitalize the first letter of words
707 .replace(/(\w+)/g, function (match) {
708 return angular.uppercase(match.charAt(0)) + match.slice(1);
710 // Put a space in between words that have partial capilizations (i.e. 'firstName' becomes 'First Name')
711 // .replace(/([A-Z]|[A-Z]\w+)([A-Z])/g, "$1 $2");
712 // .replace(/(\w+?|\w)([A-Z])/g, "$1 $2");
713 .replace(/(\w+?(?=[A-Z]))/g, '$1 ');
718 * @name getColumnsFromData
719 * @methodOf ui.grid.util.service:GridUtil
720 * @description Return a list of column names, given a data set
722 * @param {string} data Data array for grid
723 * @returns {Object} Column definitions with field accessor and column name
728 { firstName: 'Bob', lastName: 'Jones' },
729 { firstName: 'Frank', lastName: 'Smith' }
732 var columnDefs = GridUtil.getColumnsFromData(data);
746 getColumnsFromData: function (data) {
751 angular.forEach(item, function (prop, propName) {
754 name: s.readableColumnName(propName)
764 * @methodOf ui.grid.util.service:GridUtil
765 * @description Return a unique ID string
767 * @returns {string} Unique string
771 var id = GridUtil.newId();
777 var seedId = new Date().getTime();