slickgrid added to third-party
[myslice.git] / third-party / slickgrid-2.1 / slick.dataview.js
1 (function ($) {
2   $.extend(true, window, {
3     Slick: {
4       Data: {
5         DataView: DataView,
6         Aggregators: {
7           Avg: AvgAggregator,
8           Min: MinAggregator,
9           Max: MaxAggregator,
10           Sum: SumAggregator
11         }
12       }
13     }
14   });
15
16
17   /***
18    * A sample Model implementation.
19    * Provides a filtered view of the underlying data.
20    *
21    * Relies on the data item having an "id" property uniquely identifying it.
22    */
23   function DataView(options) {
24     var self = this;
25
26     var defaults = {
27       groupItemMetadataProvider: null,
28       inlineFilters: false
29     };
30
31
32     // private
33     var idProperty = "id";  // property holding a unique row id
34     var items = [];         // data by index
35     var rows = [];          // data by row
36     var idxById = {};       // indexes by id
37     var rowsById = null;    // rows by id; lazy-calculated
38     var filter = null;      // filter function
39     var updated = null;     // updated item ids
40     var suspend = false;    // suspends the recalculation
41     var sortAsc = true;
42     var fastSortField;
43     var sortComparer;
44     var refreshHints = {};
45     var prevRefreshHints = {};
46     var filterArgs;
47     var filteredItems = [];
48     var compiledFilter;
49     var compiledFilterWithCaching;
50     var filterCache = [];
51
52     // grouping
53     var groupingInfoDefaults = {
54       getter: null,
55       formatter: null,
56       comparer: function(a, b) { return a.value - b.value; },
57       predefinedValues: [],
58       aggregators: [],
59       aggregateEmpty: false,
60       aggregateCollapsed: false,
61       aggregateChildGroups: false,
62       collapsed: false,
63       displayTotalsRow: true
64     };
65     var groupingInfos = [];
66     var groups = [];
67     var toggledGroupsByLevel = [];
68     var groupingDelimiter = ':|:';
69
70     var pagesize = 0;
71     var pagenum = 0;
72     var totalRows = 0;
73
74     // events
75     var onRowCountChanged = new Slick.Event();
76     var onRowsChanged = new Slick.Event();
77     var onPagingInfoChanged = new Slick.Event();
78
79     options = $.extend(true, {}, defaults, options);
80
81
82     function beginUpdate() {
83       suspend = true;
84     }
85
86     function endUpdate() {
87       suspend = false;
88       refresh();
89     }
90
91     function setRefreshHints(hints) {
92       refreshHints = hints;
93     }
94
95     function setFilterArgs(args) {
96       filterArgs = args;
97     }
98
99     function updateIdxById(startingIndex) {
100       startingIndex = startingIndex || 0;
101       var id;
102       for (var i = startingIndex, l = items.length; i < l; i++) {
103         id = items[i][idProperty];
104         if (id === undefined) {
105           throw "Each data element must implement a unique 'id' property";
106         }
107         idxById[id] = i;
108       }
109     }
110
111     function ensureIdUniqueness() {
112       var id;
113       for (var i = 0, l = items.length; i < l; i++) {
114         id = items[i][idProperty];
115         if (id === undefined || idxById[id] !== i) {
116           throw "Each data element must implement a unique 'id' property";
117         }
118       }
119     }
120
121     function getItems() {
122       return items;
123     }
124
125     function setItems(data, objectIdProperty) {
126       if (objectIdProperty !== undefined) {
127         idProperty = objectIdProperty;
128       }
129       items = filteredItems = data;
130       idxById = {};
131       updateIdxById();
132       ensureIdUniqueness();
133       refresh();
134     }
135
136     function setPagingOptions(args) {
137       if (args.pageSize != undefined) {
138         pagesize = args.pageSize;
139         pagenum = pagesize ? Math.min(pagenum, Math.max(0, Math.ceil(totalRows / pagesize) - 1)) : 0;
140       }
141
142       if (args.pageNum != undefined) {
143         pagenum = Math.min(args.pageNum, Math.max(0, Math.ceil(totalRows / pagesize) - 1));
144       }
145
146       onPagingInfoChanged.notify(getPagingInfo(), null, self);
147
148       refresh();
149     }
150
151     function getPagingInfo() {
152       var totalPages = pagesize ? Math.max(1, Math.ceil(totalRows / pagesize)) : 1;
153       return {pageSize: pagesize, pageNum: pagenum, totalRows: totalRows, totalPages: totalPages};
154     }
155
156     function sort(comparer, ascending) {
157       sortAsc = ascending;
158       sortComparer = comparer;
159       fastSortField = null;
160       if (ascending === false) {
161         items.reverse();
162       }
163       items.sort(comparer);
164       if (ascending === false) {
165         items.reverse();
166       }
167       idxById = {};
168       updateIdxById();
169       refresh();
170     }
171
172     /***
173      * Provides a workaround for the extremely slow sorting in IE.
174      * Does a [lexicographic] sort on a give column by temporarily overriding Object.prototype.toString
175      * to return the value of that field and then doing a native Array.sort().
176      */
177     function fastSort(field, ascending) {
178       sortAsc = ascending;
179       fastSortField = field;
180       sortComparer = null;
181       var oldToString = Object.prototype.toString;
182       Object.prototype.toString = (typeof field == "function") ? field : function () {
183         return this[field]
184       };
185       // an extra reversal for descending sort keeps the sort stable
186       // (assuming a stable native sort implementation, which isn't true in some cases)
187       if (ascending === false) {
188         items.reverse();
189       }
190       items.sort();
191       Object.prototype.toString = oldToString;
192       if (ascending === false) {
193         items.reverse();
194       }
195       idxById = {};
196       updateIdxById();
197       refresh();
198     }
199
200     function reSort() {
201       if (sortComparer) {
202         sort(sortComparer, sortAsc);
203       } else if (fastSortField) {
204         fastSort(fastSortField, sortAsc);
205       }
206     }
207
208     function setFilter(filterFn) {
209       filter = filterFn;
210       if (options.inlineFilters) {
211         compiledFilter = compileFilter();
212         compiledFilterWithCaching = compileFilterWithCaching();
213       }
214       refresh();
215     }
216
217     function getGrouping() {
218       return groupingInfos;
219     }
220
221     function setGrouping(groupingInfo) {
222       if (!options.groupItemMetadataProvider) {
223         options.groupItemMetadataProvider = new Slick.Data.GroupItemMetadataProvider();
224       }
225
226       groups = [];
227       toggledGroupsByLevel = [];
228       groupingInfo = groupingInfo || [];
229       groupingInfos = (groupingInfo instanceof Array) ? groupingInfo : [groupingInfo];
230
231       for (var i = 0; i < groupingInfos.length; i++) {
232         var gi = groupingInfos[i] = $.extend(true, {}, groupingInfoDefaults, groupingInfos[i]);
233         gi.getterIsAFn = typeof gi.getter === "function";
234
235         // pre-compile accumulator loops
236         gi.compiledAccumulators = [];
237         var idx = gi.aggregators.length;
238         while (idx--) {
239           gi.compiledAccumulators[idx] = compileAccumulatorLoop(gi.aggregators[idx]);
240         }
241
242         toggledGroupsByLevel[i] = {};
243       }
244
245       refresh();
246     }
247
248     /**
249      * @deprecated Please use {@link setGrouping}.
250      */
251     function groupBy(valueGetter, valueFormatter, sortComparer) {
252       if (valueGetter == null) {
253         setGrouping([]);
254         return;
255       }
256
257       setGrouping({
258         getter: valueGetter,
259         formatter: valueFormatter,
260         comparer: sortComparer
261       });
262     }
263
264     /**
265      * @deprecated Please use {@link setGrouping}.
266      */
267     function setAggregators(groupAggregators, includeCollapsed) {
268       if (!groupingInfos.length) {
269         throw new Error("At least one grouping must be specified before calling setAggregators().");
270       }
271
272       groupingInfos[0].aggregators = groupAggregators;
273       groupingInfos[0].aggregateCollapsed = includeCollapsed;
274
275       setGrouping(groupingInfos);
276     }
277
278     function getItemByIdx(i) {
279       return items[i];
280     }
281
282     function getIdxById(id) {
283       return idxById[id];
284     }
285
286     function ensureRowsByIdCache() {
287       if (!rowsById) {
288         rowsById = {};
289         for (var i = 0, l = rows.length; i < l; i++) {
290           rowsById[rows[i][idProperty]] = i;
291         }
292       }
293     }
294
295     function getRowById(id) {
296       ensureRowsByIdCache();
297       return rowsById[id];
298     }
299
300     function getItemById(id) {
301       return items[idxById[id]];
302     }
303
304     function mapIdsToRows(idArray) {
305       var rows = [];
306       ensureRowsByIdCache();
307       for (var i = 0; i < idArray.length; i++) {
308         var row = rowsById[idArray[i]];
309         if (row != null) {
310           rows[rows.length] = row;
311         }
312       }
313       return rows;
314     }
315
316     function mapRowsToIds(rowArray) {
317       var ids = [];
318       for (var i = 0; i < rowArray.length; i++) {
319         if (rowArray[i] < rows.length) {
320           ids[ids.length] = rows[rowArray[i]][idProperty];
321         }
322       }
323       return ids;
324     }
325
326     function updateItem(id, item) {
327       if (idxById[id] === undefined || id !== item[idProperty]) {
328         throw "Invalid or non-matching id";
329       }
330       items[idxById[id]] = item;
331       if (!updated) {
332         updated = {};
333       }
334       updated[id] = true;
335       refresh();
336     }
337
338     function insertItem(insertBefore, item) {
339       items.splice(insertBefore, 0, item);
340       updateIdxById(insertBefore);
341       refresh();
342     }
343
344     function addItem(item) {
345       items.push(item);
346       updateIdxById(items.length - 1);
347       refresh();
348     }
349
350     function deleteItem(id) {
351       var idx = idxById[id];
352       if (idx === undefined) {
353         throw "Invalid id";
354       }
355       delete idxById[id];
356       items.splice(idx, 1);
357       updateIdxById(idx);
358       refresh();
359     }
360
361     function getLength() {
362       return rows.length;
363     }
364
365     function getItem(i) {
366       return rows[i];
367     }
368
369     function getItemMetadata(i) {
370       var item = rows[i];
371       if (item === undefined) {
372         return null;
373       }
374
375       // overrides for grouping rows
376       if (item.__group) {
377         return options.groupItemMetadataProvider.getGroupRowMetadata(item);
378       }
379
380       // overrides for totals rows
381       if (item.__groupTotals) {
382         return options.groupItemMetadataProvider.getTotalsRowMetadata(item);
383       }
384
385       return null;
386     }
387
388     function expandCollapseAllGroups(level, collapse) {
389       if (level == null) {
390         for (var i = 0; i < groupingInfos.length; i++) {
391           toggledGroupsByLevel[i] = {};
392           groupingInfos[i].collapsed = collapse;
393         }
394       } else {
395         toggledGroupsByLevel[level] = {};
396         groupingInfos[level].collapsed = collapse;
397       }
398       refresh();
399     }
400
401     /**
402      * @param level {Number} Optional level to collapse.  If not specified, applies to all levels.
403      */
404     function collapseAllGroups(level) {
405       expandCollapseAllGroups(level, true);
406     }
407
408     /**
409      * @param level {Number} Optional level to expand.  If not specified, applies to all levels.
410      */
411     function expandAllGroups(level) {
412       expandCollapseAllGroups(level, false);
413     }
414
415     function expandCollapseGroup(level, groupingKey, collapse) {
416       toggledGroupsByLevel[level][groupingKey] = groupingInfos[level].collapsed ^ collapse;
417       refresh();
418     }
419
420     /**
421      * @param varArgs Either a Slick.Group's "groupingKey" property, or a
422      *     variable argument list of grouping values denoting a unique path to the row.  For
423      *     example, calling collapseGroup('high', '10%') will collapse the '10%' subgroup of
424      *     the 'high' setGrouping.
425      */
426     function collapseGroup(varArgs) {
427       var args = Array.prototype.slice.call(arguments);
428       var arg0 = args[0];
429       if (args.length == 1 && arg0.indexOf(groupingDelimiter) != -1) {
430         expandCollapseGroup(arg0.split(groupingDelimiter).length - 1, arg0, true);
431       } else {
432         expandCollapseGroup(args.length - 1, args.join(groupingDelimiter), true);
433       }
434     }
435
436     /**
437      * @param varArgs Either a Slick.Group's "groupingKey" property, or a
438      *     variable argument list of grouping values denoting a unique path to the row.  For
439      *     example, calling expandGroup('high', '10%') will expand the '10%' subgroup of
440      *     the 'high' setGrouping.
441      */
442     function expandGroup(varArgs) {
443       var args = Array.prototype.slice.call(arguments);
444       var arg0 = args[0];
445       if (args.length == 1 && arg0.indexOf(groupingDelimiter) != -1) {
446         expandCollapseGroup(arg0.split(groupingDelimiter).length - 1, arg0, false);
447       } else {
448         expandCollapseGroup(args.length - 1, args.join(groupingDelimiter), false);
449       }
450     }
451
452     function getGroups() {
453       return groups;
454     }
455
456     function extractGroups(rows, parentGroup) {
457       var group;
458       var val;
459       var groups = [];
460       var groupsByVal = [];
461       var r;
462       var level = parentGroup ? parentGroup.level + 1 : 0;
463       var gi = groupingInfos[level];
464
465       for (var i = 0, l = gi.predefinedValues.length; i < l; i++) {
466         val = gi.predefinedValues[i];
467         group = groupsByVal[val];
468         if (!group) {
469           group = new Slick.Group();
470           group.value = val;
471           group.level = level;
472           group.groupingKey = (parentGroup ? parentGroup.groupingKey + groupingDelimiter : '') + val;
473           groups[groups.length] = group;
474           groupsByVal[val] = group;
475         }
476       }
477
478       for (var i = 0, l = rows.length; i < l; i++) {
479         r = rows[i];
480         val = gi.getterIsAFn ? gi.getter(r) : r[gi.getter];
481         group = groupsByVal[val];
482         if (!group) {
483           group = new Slick.Group();
484           group.value = val;
485           group.level = level;
486           group.groupingKey = (parentGroup ? parentGroup.groupingKey + groupingDelimiter : '') + val;
487           groups[groups.length] = group;
488           groupsByVal[val] = group;
489         }
490
491         group.rows[group.count++] = r;
492       }
493
494       if (level < groupingInfos.length - 1) {
495         for (var i = 0; i < groups.length; i++) {
496           group = groups[i];
497           group.groups = extractGroups(group.rows, group);
498         }
499       }      
500
501       groups.sort(groupingInfos[level].comparer);
502
503       return groups;
504     }
505
506     // TODO:  lazy totals calculation
507     function calculateGroupTotals(group) {
508       // TODO:  try moving iterating over groups into compiled accumulator
509       var gi = groupingInfos[group.level];
510       var isLeafLevel = (group.level == groupingInfos.length);
511       var totals = new Slick.GroupTotals();
512       var agg, idx = gi.aggregators.length;
513       while (idx--) {
514         agg = gi.aggregators[idx];
515         agg.init();
516         gi.compiledAccumulators[idx].call(agg,
517             (!isLeafLevel && gi.aggregateChildGroups) ? group.groups : group.rows);
518         agg.storeResult(totals);
519       }
520       totals.group = group;
521       group.totals = totals;
522     }
523
524     function calculateTotals(groups, level) {
525       level = level || 0;
526       var gi = groupingInfos[level];
527       var idx = groups.length, g;
528       while (idx--) {
529         g = groups[idx];
530
531         if (g.collapsed && !gi.aggregateCollapsed) {
532           continue;
533         }
534
535         // Do a depth-first aggregation so that parent setGrouping aggregators can access subgroup totals.
536         if (g.groups) {
537           calculateTotals(g.groups, level + 1);
538         }
539
540         if (gi.aggregators.length && (
541             gi.aggregateEmpty || g.rows.length || (g.groups && g.groups.length))) {
542           calculateGroupTotals(g);
543         }
544       }
545     }
546
547     function finalizeGroups(groups, level) {
548       level = level || 0;
549       var gi = groupingInfos[level];
550       var groupCollapsed = gi.collapsed;
551       var toggledGroups = toggledGroupsByLevel[level];
552       var idx = groups.length, g;
553       while (idx--) {
554         g = groups[idx];
555         g.collapsed = groupCollapsed ^ toggledGroups[g.groupingKey];
556         g.title = gi.formatter ? gi.formatter(g) : g.value;
557
558         if (g.groups) {
559           finalizeGroups(g.groups, level + 1);
560           // Let the non-leaf setGrouping rows get garbage-collected.
561           // They may have been used by aggregates that go over all of the descendants,
562           // but at this point they are no longer needed.
563           g.rows = [];
564         }
565       }
566     }
567
568     function flattenGroupedRows(groups, level) {
569       level = level || 0;
570       var gi = groupingInfos[level];
571       var groupedRows = [], rows, gl = 0, g;
572       for (var i = 0, l = groups.length; i < l; i++) {
573         g = groups[i];
574         groupedRows[gl++] = g;
575
576         if (!g.collapsed) {
577           rows = g.groups ? flattenGroupedRows(g.groups, level + 1) : g.rows;
578           for (var j = 0, jj = rows.length; j < jj; j++) {
579             groupedRows[gl++] = rows[j];
580           }
581         }
582
583         if (g.totals && gi.displayTotalsRow && (!g.collapsed || gi.aggregateCollapsed)) {
584           groupedRows[gl++] = g.totals;
585         }
586       }
587       return groupedRows;
588     }
589
590     function getFunctionInfo(fn) {
591       var fnRegex = /^function[^(]*\(([^)]*)\)\s*{([\s\S]*)}$/;
592       var matches = fn.toString().match(fnRegex);
593       return {
594         params: matches[1].split(","),
595         body: matches[2]
596       };
597     }
598
599     function compileAccumulatorLoop(aggregator) {
600       var accumulatorInfo = getFunctionInfo(aggregator.accumulate);
601       var fn = new Function(
602           "_items",
603           "for (var " + accumulatorInfo.params[0] + ", _i=0, _il=_items.length; _i<_il; _i++) {" +
604               accumulatorInfo.params[0] + " = _items[_i]; " +
605               accumulatorInfo.body +
606           "}"
607       );
608       fn.displayName = fn.name = "compiledAccumulatorLoop";
609       return fn;
610     }
611
612     function compileFilter() {
613       var filterInfo = getFunctionInfo(filter);
614
615       var filterBody = filterInfo.body
616           .replace(/return false\s*([;}]|$)/gi, "{ continue _coreloop; }$1")
617           .replace(/return true\s*([;}]|$)/gi, "{ _retval[_idx++] = $item$; continue _coreloop; }$1")
618           .replace(/return ([^;}]+?)\s*([;}]|$)/gi,
619           "{ if ($1) { _retval[_idx++] = $item$; }; continue _coreloop; }$2");
620
621       // This preserves the function template code after JS compression,
622       // so that replace() commands still work as expected.
623       var tpl = [
624         //"function(_items, _args) { ",
625         "var _retval = [], _idx = 0; ",
626         "var $item$, $args$ = _args; ",
627         "_coreloop: ",
628         "for (var _i = 0, _il = _items.length; _i < _il; _i++) { ",
629         "$item$ = _items[_i]; ",
630         "$filter$; ",
631         "} ",
632         "return _retval; "
633         //"}"
634       ].join("");
635       tpl = tpl.replace(/\$filter\$/gi, filterBody);
636       tpl = tpl.replace(/\$item\$/gi, filterInfo.params[0]);
637       tpl = tpl.replace(/\$args\$/gi, filterInfo.params[1]);
638
639       var fn = new Function("_items,_args", tpl);
640       fn.displayName = fn.name = "compiledFilter";
641       return fn;
642     }
643
644     function compileFilterWithCaching() {
645       var filterInfo = getFunctionInfo(filter);
646
647       var filterBody = filterInfo.body
648           .replace(/return false\s*([;}]|$)/gi, "{ continue _coreloop; }$1")
649           .replace(/return true\s*([;}]|$)/gi, "{ _cache[_i] = true;_retval[_idx++] = $item$; continue _coreloop; }$1")
650           .replace(/return ([^;}]+?)\s*([;}]|$)/gi,
651           "{ if ((_cache[_i] = $1)) { _retval[_idx++] = $item$; }; continue _coreloop; }$2");
652
653       // This preserves the function template code after JS compression,
654       // so that replace() commands still work as expected.
655       var tpl = [
656         //"function(_items, _args, _cache) { ",
657         "var _retval = [], _idx = 0; ",
658         "var $item$, $args$ = _args; ",
659         "_coreloop: ",
660         "for (var _i = 0, _il = _items.length; _i < _il; _i++) { ",
661         "$item$ = _items[_i]; ",
662         "if (_cache[_i]) { ",
663         "_retval[_idx++] = $item$; ",
664         "continue _coreloop; ",
665         "} ",
666         "$filter$; ",
667         "} ",
668         "return _retval; "
669         //"}"
670       ].join("");
671       tpl = tpl.replace(/\$filter\$/gi, filterBody);
672       tpl = tpl.replace(/\$item\$/gi, filterInfo.params[0]);
673       tpl = tpl.replace(/\$args\$/gi, filterInfo.params[1]);
674
675       var fn = new Function("_items,_args,_cache", tpl);
676       fn.displayName = fn.name = "compiledFilterWithCaching";
677       return fn;
678     }
679
680     function uncompiledFilter(items, args) {
681       var retval = [], idx = 0;
682
683       for (var i = 0, ii = items.length; i < ii; i++) {
684         if (filter(items[i], args)) {
685           retval[idx++] = items[i];
686         }
687       }
688
689       return retval;
690     }
691
692     function uncompiledFilterWithCaching(items, args, cache) {
693       var retval = [], idx = 0, item;
694
695       for (var i = 0, ii = items.length; i < ii; i++) {
696         item = items[i];
697         if (cache[i]) {
698           retval[idx++] = item;
699         } else if (filter(item, args)) {
700           retval[idx++] = item;
701           cache[i] = true;
702         }
703       }
704
705       return retval;
706     }
707
708     function getFilteredAndPagedItems(items) {
709       if (filter) {
710         var batchFilter = options.inlineFilters ? compiledFilter : uncompiledFilter;
711         var batchFilterWithCaching = options.inlineFilters ? compiledFilterWithCaching : uncompiledFilterWithCaching;
712
713         if (refreshHints.isFilterNarrowing) {
714           filteredItems = batchFilter(filteredItems, filterArgs);
715         } else if (refreshHints.isFilterExpanding) {
716           filteredItems = batchFilterWithCaching(items, filterArgs, filterCache);
717         } else if (!refreshHints.isFilterUnchanged) {
718           filteredItems = batchFilter(items, filterArgs);
719         }
720       } else {
721         // special case:  if not filtering and not paging, the resulting
722         // rows collection needs to be a copy so that changes due to sort
723         // can be caught
724         filteredItems = pagesize ? items : items.concat();
725       }
726
727       // get the current page
728       var paged;
729       if (pagesize) {
730         if (filteredItems.length < pagenum * pagesize) {
731           pagenum = Math.floor(filteredItems.length / pagesize);
732         }
733         paged = filteredItems.slice(pagesize * pagenum, pagesize * pagenum + pagesize);
734       } else {
735         paged = filteredItems;
736       }
737
738       return {totalRows: filteredItems.length, rows: paged};
739     }
740
741     function getRowDiffs(rows, newRows) {
742       var item, r, eitherIsNonData, diff = [];
743       var from = 0, to = newRows.length;
744
745       if (refreshHints && refreshHints.ignoreDiffsBefore) {
746         from = Math.max(0,
747             Math.min(newRows.length, refreshHints.ignoreDiffsBefore));
748       }
749
750       if (refreshHints && refreshHints.ignoreDiffsAfter) {
751         to = Math.min(newRows.length,
752             Math.max(0, refreshHints.ignoreDiffsAfter));
753       }
754
755       for (var i = from, rl = rows.length; i < to; i++) {
756         if (i >= rl) {
757           diff[diff.length] = i;
758         } else {
759           item = newRows[i];
760           r = rows[i];
761
762           if ((groupingInfos.length && (eitherIsNonData = (item.__nonDataRow) || (r.__nonDataRow)) &&
763               item.__group !== r.__group ||
764               item.__group && !item.equals(r))
765               || (eitherIsNonData &&
766               // no good way to compare totals since they are arbitrary DTOs
767               // deep object comparison is pretty expensive
768               // always considering them 'dirty' seems easier for the time being
769               (item.__groupTotals || r.__groupTotals))
770               || item[idProperty] != r[idProperty]
771               || (updated && updated[item[idProperty]])
772               ) {
773             diff[diff.length] = i;
774           }
775         }
776       }
777       return diff;
778     }
779
780     function recalc(_items) {
781       rowsById = null;
782
783       if (refreshHints.isFilterNarrowing != prevRefreshHints.isFilterNarrowing ||
784           refreshHints.isFilterExpanding != prevRefreshHints.isFilterExpanding) {
785         filterCache = [];
786       }
787
788       var filteredItems = getFilteredAndPagedItems(_items);
789       totalRows = filteredItems.totalRows;
790       var newRows = filteredItems.rows;
791
792       groups = [];
793       if (groupingInfos.length) {
794         groups = extractGroups(newRows);
795         if (groups.length) {
796           calculateTotals(groups);
797           finalizeGroups(groups);
798           newRows = flattenGroupedRows(groups);
799         }
800       }
801
802       var diff = getRowDiffs(rows, newRows);
803
804       rows = newRows;
805
806       return diff;
807     }
808
809     function refresh() {
810       if (suspend) {
811         return;
812       }
813
814       var countBefore = rows.length;
815       var totalRowsBefore = totalRows;
816
817       var diff = recalc(items, filter); // pass as direct refs to avoid closure perf hit
818
819       // if the current page is no longer valid, go to last page and recalc
820       // we suffer a performance penalty here, but the main loop (recalc) remains highly optimized
821       if (pagesize && totalRows < pagenum * pagesize) {
822         pagenum = Math.max(0, Math.ceil(totalRows / pagesize) - 1);
823         diff = recalc(items, filter);
824       }
825
826       updated = null;
827       prevRefreshHints = refreshHints;
828       refreshHints = {};
829
830       if (totalRowsBefore != totalRows) {
831         onPagingInfoChanged.notify(getPagingInfo(), null, self);
832       }
833       if (countBefore != rows.length) {
834         onRowCountChanged.notify({previous: countBefore, current: rows.length}, null, self);
835       }
836       if (diff.length > 0) {
837         onRowsChanged.notify({rows: diff}, null, self);
838       }
839     }
840
841     function syncGridSelection(grid, preserveHidden) {
842       var self = this;
843       var selectedRowIds = self.mapRowsToIds(grid.getSelectedRows());;
844       var inHandler;
845
846       function update() {
847         if (selectedRowIds.length > 0) {
848           inHandler = true;
849           var selectedRows = self.mapIdsToRows(selectedRowIds);
850           if (!preserveHidden) {
851             selectedRowIds = self.mapRowsToIds(selectedRows);
852           }
853           grid.setSelectedRows(selectedRows);
854           inHandler = false;
855         }
856       }
857
858       grid.onSelectedRowsChanged.subscribe(function(e, args) {
859         if (inHandler) { return; }
860         selectedRowIds = self.mapRowsToIds(grid.getSelectedRows());
861       });
862
863       this.onRowsChanged.subscribe(update);
864
865       this.onRowCountChanged.subscribe(update);
866     }
867
868     function syncGridCellCssStyles(grid, key) {
869       var hashById;
870       var inHandler;
871
872       // since this method can be called after the cell styles have been set,
873       // get the existing ones right away
874       storeCellCssStyles(grid.getCellCssStyles(key));
875
876       function storeCellCssStyles(hash) {
877         hashById = {};
878         for (var row in hash) {
879           var id = rows[row][idProperty];
880           hashById[id] = hash[row];
881         }
882       }
883
884       function update() {
885         if (hashById) {
886           inHandler = true;
887           ensureRowsByIdCache();
888           var newHash = {};
889           for (var id in hashById) {
890             var row = rowsById[id];
891             if (row != undefined) {
892               newHash[row] = hashById[id];
893             }
894           }
895           grid.setCellCssStyles(key, newHash);
896           inHandler = false;
897         }
898       }
899
900       grid.onCellCssStylesChanged.subscribe(function(e, args) {
901         if (inHandler) { return; }
902         if (key != args.key) { return; }
903         if (args.hash) {
904           storeCellCssStyles(args.hash);
905         }
906       });
907
908       this.onRowsChanged.subscribe(update);
909
910       this.onRowCountChanged.subscribe(update);
911     }
912
913     $.extend(this, {
914       // methods
915       "beginUpdate": beginUpdate,
916       "endUpdate": endUpdate,
917       "setPagingOptions": setPagingOptions,
918       "getPagingInfo": getPagingInfo,
919       "getItems": getItems,
920       "setItems": setItems,
921       "setFilter": setFilter,
922       "sort": sort,
923       "fastSort": fastSort,
924       "reSort": reSort,
925       "setGrouping": setGrouping,
926       "getGrouping": getGrouping,
927       "groupBy": groupBy,
928       "setAggregators": setAggregators,
929       "collapseAllGroups": collapseAllGroups,
930       "expandAllGroups": expandAllGroups,
931       "collapseGroup": collapseGroup,
932       "expandGroup": expandGroup,
933       "getGroups": getGroups,
934       "getIdxById": getIdxById,
935       "getRowById": getRowById,
936       "getItemById": getItemById,
937       "getItemByIdx": getItemByIdx,
938       "mapRowsToIds": mapRowsToIds,
939       "mapIdsToRows": mapIdsToRows,
940       "setRefreshHints": setRefreshHints,
941       "setFilterArgs": setFilterArgs,
942       "refresh": refresh,
943       "updateItem": updateItem,
944       "insertItem": insertItem,
945       "addItem": addItem,
946       "deleteItem": deleteItem,
947       "syncGridSelection": syncGridSelection,
948       "syncGridCellCssStyles": syncGridCellCssStyles,
949
950       // data provider methods
951       "getLength": getLength,
952       "getItem": getItem,
953       "getItemMetadata": getItemMetadata,
954
955       // events
956       "onRowCountChanged": onRowCountChanged,
957       "onRowsChanged": onRowsChanged,
958       "onPagingInfoChanged": onPagingInfoChanged
959     });
960   }
961
962   function AvgAggregator(field) {
963     this.field_ = field;
964
965     this.init = function () {
966       this.count_ = 0;
967       this.nonNullCount_ = 0;
968       this.sum_ = 0;
969     };
970
971     this.accumulate = function (item) {
972       var val = item[this.field_];
973       this.count_++;
974       if (val != null && val !== "" && val !== NaN) {
975         this.nonNullCount_++;
976         this.sum_ += parseFloat(val);
977       }
978     };
979
980     this.storeResult = function (groupTotals) {
981       if (!groupTotals.avg) {
982         groupTotals.avg = {};
983       }
984       if (this.nonNullCount_ != 0) {
985         groupTotals.avg[this.field_] = this.sum_ / this.nonNullCount_;
986       }
987     };
988   }
989
990   function MinAggregator(field) {
991     this.field_ = field;
992
993     this.init = function () {
994       this.min_ = null;
995     };
996
997     this.accumulate = function (item) {
998       var val = item[this.field_];
999       if (val != null && val !== "" && val !== NaN) {
1000         if (this.min_ == null || val < this.min_) {
1001           this.min_ = val;
1002         }
1003       }
1004     };
1005
1006     this.storeResult = function (groupTotals) {
1007       if (!groupTotals.min) {
1008         groupTotals.min = {};
1009       }
1010       groupTotals.min[this.field_] = this.min_;
1011     }
1012   }
1013
1014   function MaxAggregator(field) {
1015     this.field_ = field;
1016
1017     this.init = function () {
1018       this.max_ = null;
1019     };
1020
1021     this.accumulate = function (item) {
1022       var val = item[this.field_];
1023       if (val != null && val !== "" && val !== NaN) {
1024         if (this.max_ == null || val > this.max_) {
1025           this.max_ = val;
1026         }
1027       }
1028     };
1029
1030     this.storeResult = function (groupTotals) {
1031       if (!groupTotals.max) {
1032         groupTotals.max = {};
1033       }
1034       groupTotals.max[this.field_] = this.max_;
1035     }
1036   }
1037
1038   function SumAggregator(field) {
1039     this.field_ = field;
1040
1041     this.init = function () {
1042       this.sum_ = null;
1043     };
1044
1045     this.accumulate = function (item) {
1046       var val = item[this.field_];
1047       if (val != null && val !== "" && val !== NaN) {
1048         this.sum_ += parseFloat(val);
1049       }
1050     };
1051
1052     this.storeResult = function (groupTotals) {
1053       if (!groupTotals.sum) {
1054         groupTotals.sum = {};
1055       }
1056       groupTotals.sum[this.field_] = this.sum_;
1057     }
1058   }
1059
1060   // TODO:  add more built-in aggregators
1061   // TODO:  merge common aggregators in one to prevent needles iterating
1062
1063 })(jQuery);