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