2 $.extend(true, window, {
18 * A sample Model implementation.
19 * Provides a filtered view of the underlying data.
21 * Relies on the data item having an "id" property uniquely identifying it.
23 function DataView(options) {
27 groupItemMetadataProvider: null,
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
44 var refreshHints = {};
45 var prevRefreshHints = {};
47 var filteredItems = [];
49 var compiledFilterWithCaching;
53 var groupingInfoDefaults = {
56 comparer: function(a, b) { return a.value - b.value; },
59 aggregateEmpty: false,
60 aggregateCollapsed: false,
61 aggregateChildGroups: false,
63 displayTotalsRow: true
65 var groupingInfos = [];
67 var toggledGroupsByLevel = [];
68 var groupingDelimiter = ':|:';
75 var onRowCountChanged = new Slick.Event();
76 var onRowsChanged = new Slick.Event();
77 var onPagingInfoChanged = new Slick.Event();
79 options = $.extend(true, {}, defaults, options);
82 function beginUpdate() {
86 function endUpdate() {
91 function setRefreshHints(hints) {
95 function setFilterArgs(args) {
99 function updateIdxById(startingIndex) {
100 startingIndex = startingIndex || 0;
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";
111 function ensureIdUniqueness() {
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";
121 function getItems() {
125 function setItems(data, objectIdProperty) {
126 if (objectIdProperty !== undefined) {
127 idProperty = objectIdProperty;
129 items = filteredItems = data;
132 ensureIdUniqueness();
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;
142 if (args.pageNum != undefined) {
143 pagenum = Math.min(args.pageNum, Math.max(0, Math.ceil(totalRows / pagesize) - 1));
146 onPagingInfoChanged.notify(getPagingInfo(), null, self);
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};
156 function sort(comparer, ascending) {
158 sortComparer = comparer;
159 fastSortField = null;
160 if (ascending === false) {
163 items.sort(comparer);
164 if (ascending === false) {
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().
177 function fastSort(field, ascending) {
179 fastSortField = field;
181 var oldToString = Object.prototype.toString;
182 Object.prototype.toString = (typeof field == "function") ? field : function () {
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) {
191 Object.prototype.toString = oldToString;
192 if (ascending === false) {
202 sort(sortComparer, sortAsc);
203 } else if (fastSortField) {
204 fastSort(fastSortField, sortAsc);
208 function setFilter(filterFn) {
210 if (options.inlineFilters) {
211 compiledFilter = compileFilter();
212 compiledFilterWithCaching = compileFilterWithCaching();
217 function getGrouping() {
218 return groupingInfos;
221 function setGrouping(groupingInfo) {
222 if (!options.groupItemMetadataProvider) {
223 options.groupItemMetadataProvider = new Slick.Data.GroupItemMetadataProvider();
227 toggledGroupsByLevel = [];
228 groupingInfo = groupingInfo || [];
229 groupingInfos = (groupingInfo instanceof Array) ? groupingInfo : [groupingInfo];
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";
235 // pre-compile accumulator loops
236 gi.compiledAccumulators = [];
237 var idx = gi.aggregators.length;
239 gi.compiledAccumulators[idx] = compileAccumulatorLoop(gi.aggregators[idx]);
242 toggledGroupsByLevel[i] = {};
249 * @deprecated Please use {@link setGrouping}.
251 function groupBy(valueGetter, valueFormatter, sortComparer) {
252 if (valueGetter == null) {
259 formatter: valueFormatter,
260 comparer: sortComparer
265 * @deprecated Please use {@link setGrouping}.
267 function setAggregators(groupAggregators, includeCollapsed) {
268 if (!groupingInfos.length) {
269 throw new Error("At least one grouping must be specified before calling setAggregators().");
272 groupingInfos[0].aggregators = groupAggregators;
273 groupingInfos[0].aggregateCollapsed = includeCollapsed;
275 setGrouping(groupingInfos);
278 function getItemByIdx(i) {
282 function getIdxById(id) {
286 function ensureRowsByIdCache() {
289 for (var i = 0, l = rows.length; i < l; i++) {
290 rowsById[rows[i][idProperty]] = i;
295 function getRowById(id) {
296 ensureRowsByIdCache();
300 function getItemById(id) {
301 return items[idxById[id]];
304 function mapIdsToRows(idArray) {
306 ensureRowsByIdCache();
307 for (var i = 0; i < idArray.length; i++) {
308 var row = rowsById[idArray[i]];
310 rows[rows.length] = row;
316 function mapRowsToIds(rowArray) {
318 for (var i = 0; i < rowArray.length; i++) {
319 if (rowArray[i] < rows.length) {
320 ids[ids.length] = rows[rowArray[i]][idProperty];
326 function updateItem(id, item) {
327 if (idxById[id] === undefined || id !== item[idProperty]) {
328 throw "Invalid or non-matching id";
330 items[idxById[id]] = item;
338 function insertItem(insertBefore, item) {
339 items.splice(insertBefore, 0, item);
340 updateIdxById(insertBefore);
344 function addItem(item) {
346 updateIdxById(items.length - 1);
350 function deleteItem(id) {
351 var idx = idxById[id];
352 if (idx === undefined) {
356 items.splice(idx, 1);
361 function getLength() {
365 function getItem(i) {
369 function getItemMetadata(i) {
371 if (item === undefined) {
375 // overrides for grouping rows
377 return options.groupItemMetadataProvider.getGroupRowMetadata(item);
380 // overrides for totals rows
381 if (item.__groupTotals) {
382 return options.groupItemMetadataProvider.getTotalsRowMetadata(item);
388 function expandCollapseAllGroups(level, collapse) {
390 for (var i = 0; i < groupingInfos.length; i++) {
391 toggledGroupsByLevel[i] = {};
392 groupingInfos[i].collapsed = collapse;
395 toggledGroupsByLevel[level] = {};
396 groupingInfos[level].collapsed = collapse;
402 * @param level {Number} Optional level to collapse. If not specified, applies to all levels.
404 function collapseAllGroups(level) {
405 expandCollapseAllGroups(level, true);
409 * @param level {Number} Optional level to expand. If not specified, applies to all levels.
411 function expandAllGroups(level) {
412 expandCollapseAllGroups(level, false);
415 function expandCollapseGroup(level, groupingKey, collapse) {
416 toggledGroupsByLevel[level][groupingKey] = groupingInfos[level].collapsed ^ collapse;
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.
426 function collapseGroup(varArgs) {
427 var args = Array.prototype.slice.call(arguments);
429 if (args.length == 1 && arg0.indexOf(groupingDelimiter) != -1) {
430 expandCollapseGroup(arg0.split(groupingDelimiter).length - 1, arg0, true);
432 expandCollapseGroup(args.length - 1, args.join(groupingDelimiter), true);
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.
442 function expandGroup(varArgs) {
443 var args = Array.prototype.slice.call(arguments);
445 if (args.length == 1 && arg0.indexOf(groupingDelimiter) != -1) {
446 expandCollapseGroup(arg0.split(groupingDelimiter).length - 1, arg0, false);
448 expandCollapseGroup(args.length - 1, args.join(groupingDelimiter), false);
452 function getGroups() {
456 function extractGroups(rows, parentGroup) {
460 var groupsByVal = [];
462 var level = parentGroup ? parentGroup.level + 1 : 0;
463 var gi = groupingInfos[level];
465 for (var i = 0, l = gi.predefinedValues.length; i < l; i++) {
466 val = gi.predefinedValues[i];
467 group = groupsByVal[val];
469 group = new Slick.Group();
472 group.groupingKey = (parentGroup ? parentGroup.groupingKey + groupingDelimiter : '') + val;
473 groups[groups.length] = group;
474 groupsByVal[val] = group;
478 for (var i = 0, l = rows.length; i < l; i++) {
480 val = gi.getterIsAFn ? gi.getter(r) : r[gi.getter];
481 group = groupsByVal[val];
483 group = new Slick.Group();
486 group.groupingKey = (parentGroup ? parentGroup.groupingKey + groupingDelimiter : '') + val;
487 groups[groups.length] = group;
488 groupsByVal[val] = group;
491 group.rows[group.count++] = r;
494 if (level < groupingInfos.length - 1) {
495 for (var i = 0; i < groups.length; i++) {
497 group.groups = extractGroups(group.rows, group);
501 groups.sort(groupingInfos[level].comparer);
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;
514 agg = gi.aggregators[idx];
516 gi.compiledAccumulators[idx].call(agg,
517 (!isLeafLevel && gi.aggregateChildGroups) ? group.groups : group.rows);
518 agg.storeResult(totals);
520 totals.group = group;
521 group.totals = totals;
524 function calculateTotals(groups, level) {
526 var gi = groupingInfos[level];
527 var idx = groups.length, g;
531 if (g.collapsed && !gi.aggregateCollapsed) {
535 // Do a depth-first aggregation so that parent setGrouping aggregators can access subgroup totals.
537 calculateTotals(g.groups, level + 1);
540 if (gi.aggregators.length && (
541 gi.aggregateEmpty || g.rows.length || (g.groups && g.groups.length))) {
542 calculateGroupTotals(g);
547 function finalizeGroups(groups, level) {
549 var gi = groupingInfos[level];
550 var groupCollapsed = gi.collapsed;
551 var toggledGroups = toggledGroupsByLevel[level];
552 var idx = groups.length, g;
555 g.collapsed = groupCollapsed ^ toggledGroups[g.groupingKey];
556 g.title = gi.formatter ? gi.formatter(g) : g.value;
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.
568 function flattenGroupedRows(groups, level) {
570 var gi = groupingInfos[level];
571 var groupedRows = [], rows, gl = 0, g;
572 for (var i = 0, l = groups.length; i < l; i++) {
574 groupedRows[gl++] = g;
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];
583 if (g.totals && gi.displayTotalsRow && (!g.collapsed || gi.aggregateCollapsed)) {
584 groupedRows[gl++] = g.totals;
590 function getFunctionInfo(fn) {
591 var fnRegex = /^function[^(]*\(([^)]*)\)\s*{([\s\S]*)}$/;
592 var matches = fn.toString().match(fnRegex);
594 params: matches[1].split(","),
599 function compileAccumulatorLoop(aggregator) {
600 var accumulatorInfo = getFunctionInfo(aggregator.accumulate);
601 var fn = new Function(
603 "for (var " + accumulatorInfo.params[0] + ", _i=0, _il=_items.length; _i<_il; _i++) {" +
604 accumulatorInfo.params[0] + " = _items[_i]; " +
605 accumulatorInfo.body +
608 fn.displayName = fn.name = "compiledAccumulatorLoop";
612 function compileFilter() {
613 var filterInfo = getFunctionInfo(filter);
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");
621 // This preserves the function template code after JS compression,
622 // so that replace() commands still work as expected.
624 //"function(_items, _args) { ",
625 "var _retval = [], _idx = 0; ",
626 "var $item$, $args$ = _args; ",
628 "for (var _i = 0, _il = _items.length; _i < _il; _i++) { ",
629 "$item$ = _items[_i]; ",
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]);
639 var fn = new Function("_items,_args", tpl);
640 fn.displayName = fn.name = "compiledFilter";
644 function compileFilterWithCaching() {
645 var filterInfo = getFunctionInfo(filter);
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");
653 // This preserves the function template code after JS compression,
654 // so that replace() commands still work as expected.
656 //"function(_items, _args, _cache) { ",
657 "var _retval = [], _idx = 0; ",
658 "var $item$, $args$ = _args; ",
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; ",
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]);
675 var fn = new Function("_items,_args,_cache", tpl);
676 fn.displayName = fn.name = "compiledFilterWithCaching";
680 function uncompiledFilter(items, args) {
681 var retval = [], idx = 0;
683 for (var i = 0, ii = items.length; i < ii; i++) {
684 if (filter(items[i], args)) {
685 retval[idx++] = items[i];
692 function uncompiledFilterWithCaching(items, args, cache) {
693 var retval = [], idx = 0, item;
695 for (var i = 0, ii = items.length; i < ii; i++) {
698 retval[idx++] = item;
699 } else if (filter(item, args)) {
700 retval[idx++] = item;
708 function getFilteredAndPagedItems(items) {
710 var batchFilter = options.inlineFilters ? compiledFilter : uncompiledFilter;
711 var batchFilterWithCaching = options.inlineFilters ? compiledFilterWithCaching : uncompiledFilterWithCaching;
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);
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
724 filteredItems = pagesize ? items : items.concat();
727 // get the current page
730 if (filteredItems.length < pagenum * pagesize) {
731 pagenum = Math.floor(filteredItems.length / pagesize);
733 paged = filteredItems.slice(pagesize * pagenum, pagesize * pagenum + pagesize);
735 paged = filteredItems;
738 return {totalRows: filteredItems.length, rows: paged};
741 function getRowDiffs(rows, newRows) {
742 var item, r, eitherIsNonData, diff = [];
743 var from = 0, to = newRows.length;
745 if (refreshHints && refreshHints.ignoreDiffsBefore) {
747 Math.min(newRows.length, refreshHints.ignoreDiffsBefore));
750 if (refreshHints && refreshHints.ignoreDiffsAfter) {
751 to = Math.min(newRows.length,
752 Math.max(0, refreshHints.ignoreDiffsAfter));
755 for (var i = from, rl = rows.length; i < to; i++) {
757 diff[diff.length] = i;
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]])
773 diff[diff.length] = i;
780 function recalc(_items) {
783 if (refreshHints.isFilterNarrowing != prevRefreshHints.isFilterNarrowing ||
784 refreshHints.isFilterExpanding != prevRefreshHints.isFilterExpanding) {
788 var filteredItems = getFilteredAndPagedItems(_items);
789 totalRows = filteredItems.totalRows;
790 var newRows = filteredItems.rows;
793 if (groupingInfos.length) {
794 groups = extractGroups(newRows);
796 calculateTotals(groups);
797 finalizeGroups(groups);
798 newRows = flattenGroupedRows(groups);
802 var diff = getRowDiffs(rows, newRows);
814 var countBefore = rows.length;
815 var totalRowsBefore = totalRows;
817 var diff = recalc(items, filter); // pass as direct refs to avoid closure perf hit
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);
827 prevRefreshHints = refreshHints;
830 if (totalRowsBefore != totalRows) {
831 onPagingInfoChanged.notify(getPagingInfo(), null, self);
833 if (countBefore != rows.length) {
834 onRowCountChanged.notify({previous: countBefore, current: rows.length}, null, self);
836 if (diff.length > 0) {
837 onRowsChanged.notify({rows: diff}, null, self);
841 function syncGridSelection(grid, preserveHidden) {
843 var selectedRowIds = self.mapRowsToIds(grid.getSelectedRows());;
847 if (selectedRowIds.length > 0) {
849 var selectedRows = self.mapIdsToRows(selectedRowIds);
850 if (!preserveHidden) {
851 selectedRowIds = self.mapRowsToIds(selectedRows);
853 grid.setSelectedRows(selectedRows);
858 grid.onSelectedRowsChanged.subscribe(function(e, args) {
859 if (inHandler) { return; }
860 selectedRowIds = self.mapRowsToIds(grid.getSelectedRows());
863 this.onRowsChanged.subscribe(update);
865 this.onRowCountChanged.subscribe(update);
868 function syncGridCellCssStyles(grid, key) {
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));
876 function storeCellCssStyles(hash) {
878 for (var row in hash) {
879 var id = rows[row][idProperty];
880 hashById[id] = hash[row];
887 ensureRowsByIdCache();
889 for (var id in hashById) {
890 var row = rowsById[id];
891 if (row != undefined) {
892 newHash[row] = hashById[id];
895 grid.setCellCssStyles(key, newHash);
900 grid.onCellCssStylesChanged.subscribe(function(e, args) {
901 if (inHandler) { return; }
902 if (key != args.key) { return; }
904 storeCellCssStyles(args.hash);
908 this.onRowsChanged.subscribe(update);
910 this.onRowCountChanged.subscribe(update);
915 "beginUpdate": beginUpdate,
916 "endUpdate": endUpdate,
917 "setPagingOptions": setPagingOptions,
918 "getPagingInfo": getPagingInfo,
919 "getItems": getItems,
920 "setItems": setItems,
921 "setFilter": setFilter,
923 "fastSort": fastSort,
925 "setGrouping": setGrouping,
926 "getGrouping": getGrouping,
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,
943 "updateItem": updateItem,
944 "insertItem": insertItem,
946 "deleteItem": deleteItem,
947 "syncGridSelection": syncGridSelection,
948 "syncGridCellCssStyles": syncGridCellCssStyles,
950 // data provider methods
951 "getLength": getLength,
953 "getItemMetadata": getItemMetadata,
956 "onRowCountChanged": onRowCountChanged,
957 "onRowsChanged": onRowsChanged,
958 "onPagingInfoChanged": onPagingInfoChanged
962 function AvgAggregator(field) {
965 this.init = function () {
967 this.nonNullCount_ = 0;
971 this.accumulate = function (item) {
972 var val = item[this.field_];
974 if (val != null && val !== "" && val !== NaN) {
975 this.nonNullCount_++;
976 this.sum_ += parseFloat(val);
980 this.storeResult = function (groupTotals) {
981 if (!groupTotals.avg) {
982 groupTotals.avg = {};
984 if (this.nonNullCount_ != 0) {
985 groupTotals.avg[this.field_] = this.sum_ / this.nonNullCount_;
990 function MinAggregator(field) {
993 this.init = function () {
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_) {
1006 this.storeResult = function (groupTotals) {
1007 if (!groupTotals.min) {
1008 groupTotals.min = {};
1010 groupTotals.min[this.field_] = this.min_;
1014 function MaxAggregator(field) {
1015 this.field_ = field;
1017 this.init = function () {
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_) {
1030 this.storeResult = function (groupTotals) {
1031 if (!groupTotals.max) {
1032 groupTotals.max = {};
1034 groupTotals.max[this.field_] = this.max_;
1038 function SumAggregator(field) {
1039 this.field_ = field;
1041 this.init = function () {
1045 this.accumulate = function (item) {
1046 var val = item[this.field_];
1047 if (val != null && val !== "" && val !== NaN) {
1048 this.sum_ += parseFloat(val);
1052 this.storeResult = function (groupTotals) {
1053 if (!groupTotals.sum) {
1054 groupTotals.sum = {};
1056 groupTotals.sum[this.field_] = this.sum_;
1060 // TODO: add more built-in aggregators
1061 // TODO: merge common aggregators in one to prevent needles iterating