1127 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1127 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| (function ($) {
 | |
|   $.extend(true, window, {
 | |
|     Slick: {
 | |
|       Data: {
 | |
|         DataView: DataView,
 | |
|         Aggregators: {
 | |
|           Avg: AvgAggregator,
 | |
|           Min: MinAggregator,
 | |
|           Max: MaxAggregator,
 | |
|           Sum: SumAggregator
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   });
 | |
| 
 | |
| 
 | |
|   /***
 | |
|    * A sample Model implementation.
 | |
|    * Provides a filtered view of the underlying data.
 | |
|    *
 | |
|    * Relies on the data item having an "id" property uniquely identifying it.
 | |
|    */
 | |
|   function DataView(options) {
 | |
|     var self = this;
 | |
| 
 | |
|     var defaults = {
 | |
|       groupItemMetadataProvider: null,
 | |
|       inlineFilters: false
 | |
|     };
 | |
| 
 | |
| 
 | |
|     // private
 | |
|     var idProperty = "id";  // property holding a unique row id
 | |
|     var items = [];         // data by index
 | |
|     var rows = [];          // data by row
 | |
|     var idxById = {};       // indexes by id
 | |
|     var rowsById = null;    // rows by id; lazy-calculated
 | |
|     var filter = null;      // filter function
 | |
|     var updated = null;     // updated item ids
 | |
|     var suspend = false;    // suspends the recalculation
 | |
|     var sortAsc = true;
 | |
|     var fastSortField;
 | |
|     var sortComparer;
 | |
|     var refreshHints = {};
 | |
|     var prevRefreshHints = {};
 | |
|     var filterArgs;
 | |
|     var filteredItems = [];
 | |
|     var compiledFilter;
 | |
|     var compiledFilterWithCaching;
 | |
|     var filterCache = [];
 | |
| 
 | |
|     // grouping
 | |
|     var groupingInfoDefaults = {
 | |
|       getter: null,
 | |
|       formatter: null,
 | |
|       comparer: function(a, b) { return a.value - b.value; },
 | |
|       predefinedValues: [],
 | |
|       aggregators: [],
 | |
|       aggregateEmpty: false,
 | |
|       aggregateCollapsed: false,
 | |
|       aggregateChildGroups: false,
 | |
|       collapsed: false,
 | |
|       displayTotalsRow: true,
 | |
|       lazyTotalsCalculation: false
 | |
|     };
 | |
|     var groupingInfos = [];
 | |
|     var groups = [];
 | |
|     var toggledGroupsByLevel = [];
 | |
|     var groupingDelimiter = ':|:';
 | |
| 
 | |
|     var pagesize = 0;
 | |
|     var pagenum = 0;
 | |
|     var totalRows = 0;
 | |
| 
 | |
|     // events
 | |
|     var onRowCountChanged = new Slick.Event();
 | |
|     var onRowsChanged = new Slick.Event();
 | |
|     var onPagingInfoChanged = new Slick.Event();
 | |
| 
 | |
|     options = $.extend(true, {}, defaults, options);
 | |
| 
 | |
| 
 | |
|     function beginUpdate() {
 | |
|       suspend = true;
 | |
|     }
 | |
| 
 | |
|     function endUpdate() {
 | |
|       suspend = false;
 | |
|       refresh();
 | |
|     }
 | |
| 
 | |
|     function setRefreshHints(hints) {
 | |
|       refreshHints = hints;
 | |
|     }
 | |
| 
 | |
|     function setFilterArgs(args) {
 | |
|       filterArgs = args;
 | |
|     }
 | |
| 
 | |
|     function updateIdxById(startingIndex) {
 | |
|       startingIndex = startingIndex || 0;
 | |
|       var id;
 | |
|       for (var i = startingIndex, l = items.length; i < l; i++) {
 | |
|         id = items[i][idProperty];
 | |
|         if (id === undefined) {
 | |
|           throw "Each data element must implement a unique 'id' property";
 | |
|         }
 | |
|         idxById[id] = i;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function ensureIdUniqueness() {
 | |
|       var id;
 | |
|       for (var i = 0, l = items.length; i < l; i++) {
 | |
|         id = items[i][idProperty];
 | |
|         if (id === undefined || idxById[id] !== i) {
 | |
|           throw "Each data element must implement a unique 'id' property";
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function getItems() {
 | |
|       return items;
 | |
|     }
 | |
| 
 | |
|     function setItems(data, objectIdProperty) {
 | |
|       if (objectIdProperty !== undefined) {
 | |
|         idProperty = objectIdProperty;
 | |
|       }
 | |
|       items = filteredItems = data;
 | |
|       idxById = {};
 | |
|       updateIdxById();
 | |
|       ensureIdUniqueness();
 | |
|       refresh();
 | |
|     }
 | |
| 
 | |
|     function setPagingOptions(args) {
 | |
|       if (args.pageSize != undefined) {
 | |
|         pagesize = args.pageSize;
 | |
|         pagenum = pagesize ? Math.min(pagenum, Math.max(0, Math.ceil(totalRows / pagesize) - 1)) : 0;
 | |
|       }
 | |
| 
 | |
|       if (args.pageNum != undefined) {
 | |
|         pagenum = Math.min(args.pageNum, Math.max(0, Math.ceil(totalRows / pagesize) - 1));
 | |
|       }
 | |
| 
 | |
|       onPagingInfoChanged.notify(getPagingInfo(), null, self);
 | |
| 
 | |
|       refresh();
 | |
|     }
 | |
| 
 | |
|     function getPagingInfo() {
 | |
|       var totalPages = pagesize ? Math.max(1, Math.ceil(totalRows / pagesize)) : 1;
 | |
|       return {pageSize: pagesize, pageNum: pagenum, totalRows: totalRows, totalPages: totalPages};
 | |
|     }
 | |
| 
 | |
|     function sort(comparer, ascending) {
 | |
|       sortAsc = ascending;
 | |
|       sortComparer = comparer;
 | |
|       fastSortField = null;
 | |
|       if (ascending === false) {
 | |
|         items.reverse();
 | |
|       }
 | |
|       items.sort(comparer);
 | |
|       if (ascending === false) {
 | |
|         items.reverse();
 | |
|       }
 | |
|       idxById = {};
 | |
|       updateIdxById();
 | |
|       refresh();
 | |
|     }
 | |
| 
 | |
|     /***
 | |
|      * Provides a workaround for the extremely slow sorting in IE.
 | |
|      * Does a [lexicographic] sort on a give column by temporarily overriding Object.prototype.toString
 | |
|      * to return the value of that field and then doing a native Array.sort().
 | |
|      */
 | |
|     function fastSort(field, ascending) {
 | |
|       sortAsc = ascending;
 | |
|       fastSortField = field;
 | |
|       sortComparer = null;
 | |
|       var oldToString = Object.prototype.toString;
 | |
|       Object.prototype.toString = (typeof field == "function") ? field : function () {
 | |
|         return this[field]
 | |
|       };
 | |
|       // an extra reversal for descending sort keeps the sort stable
 | |
|       // (assuming a stable native sort implementation, which isn't true in some cases)
 | |
|       if (ascending === false) {
 | |
|         items.reverse();
 | |
|       }
 | |
|       items.sort();
 | |
|       Object.prototype.toString = oldToString;
 | |
|       if (ascending === false) {
 | |
|         items.reverse();
 | |
|       }
 | |
|       idxById = {};
 | |
|       updateIdxById();
 | |
|       refresh();
 | |
|     }
 | |
| 
 | |
|     function reSort() {
 | |
|       if (sortComparer) {
 | |
|         sort(sortComparer, sortAsc);
 | |
|       } else if (fastSortField) {
 | |
|         fastSort(fastSortField, sortAsc);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function setFilter(filterFn) {
 | |
|       filter = filterFn;
 | |
|       if (options.inlineFilters) {
 | |
|         compiledFilter = compileFilter();
 | |
|         compiledFilterWithCaching = compileFilterWithCaching();
 | |
|       }
 | |
|       refresh();
 | |
|     }
 | |
| 
 | |
|     function getGrouping() {
 | |
|       return groupingInfos;
 | |
|     }
 | |
| 
 | |
|     function setGrouping(groupingInfo) {
 | |
|       if (!options.groupItemMetadataProvider) {
 | |
|         options.groupItemMetadataProvider = new Slick.Data.GroupItemMetadataProvider();
 | |
|       }
 | |
| 
 | |
|       groups = [];
 | |
|       toggledGroupsByLevel = [];
 | |
|       groupingInfo = groupingInfo || [];
 | |
|       groupingInfos = (groupingInfo instanceof Array) ? groupingInfo : [groupingInfo];
 | |
| 
 | |
|       for (var i = 0; i < groupingInfos.length; i++) {
 | |
|         var gi = groupingInfos[i] = $.extend(true, {}, groupingInfoDefaults, groupingInfos[i]);
 | |
|         gi.getterIsAFn = typeof gi.getter === "function";
 | |
| 
 | |
|         // pre-compile accumulator loops
 | |
|         gi.compiledAccumulators = [];
 | |
|         var idx = gi.aggregators.length;
 | |
|         while (idx--) {
 | |
|           gi.compiledAccumulators[idx] = compileAccumulatorLoop(gi.aggregators[idx]);
 | |
|         }
 | |
| 
 | |
|         toggledGroupsByLevel[i] = {};
 | |
|       }
 | |
| 
 | |
|       refresh();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @deprecated Please use {@link setGrouping}.
 | |
|      */
 | |
|     function groupBy(valueGetter, valueFormatter, sortComparer) {
 | |
|       if (valueGetter == null) {
 | |
|         setGrouping([]);
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       setGrouping({
 | |
|         getter: valueGetter,
 | |
|         formatter: valueFormatter,
 | |
|         comparer: sortComparer
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @deprecated Please use {@link setGrouping}.
 | |
|      */
 | |
|     function setAggregators(groupAggregators, includeCollapsed) {
 | |
|       if (!groupingInfos.length) {
 | |
|         throw new Error("At least one grouping must be specified before calling setAggregators().");
 | |
|       }
 | |
| 
 | |
|       groupingInfos[0].aggregators = groupAggregators;
 | |
|       groupingInfos[0].aggregateCollapsed = includeCollapsed;
 | |
| 
 | |
|       setGrouping(groupingInfos);
 | |
|     }
 | |
| 
 | |
|     function getItemByIdx(i) {
 | |
|       return items[i];
 | |
|     }
 | |
| 
 | |
|     function getIdxById(id) {
 | |
|       return idxById[id];
 | |
|     }
 | |
| 
 | |
|     function ensureRowsByIdCache() {
 | |
|       if (!rowsById) {
 | |
|         rowsById = {};
 | |
|         for (var i = 0, l = rows.length; i < l; i++) {
 | |
|           rowsById[rows[i][idProperty]] = i;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function getRowById(id) {
 | |
|       ensureRowsByIdCache();
 | |
|       return rowsById[id];
 | |
|     }
 | |
| 
 | |
|     function getItemById(id) {
 | |
|       return items[idxById[id]];
 | |
|     }
 | |
| 
 | |
|     function mapIdsToRows(idArray) {
 | |
|       var rows = [];
 | |
|       ensureRowsByIdCache();
 | |
|       for (var i = 0, l = idArray.length; i < l; i++) {
 | |
|         var row = rowsById[idArray[i]];
 | |
|         if (row != null) {
 | |
|           rows[rows.length] = row;
 | |
|         }
 | |
|       }
 | |
|       return rows;
 | |
|     }
 | |
| 
 | |
|     function mapRowsToIds(rowArray) {
 | |
|       var ids = [];
 | |
|       for (var i = 0, l = rowArray.length; i < l; i++) {
 | |
|         if (rowArray[i] < rows.length) {
 | |
|           ids[ids.length] = rows[rowArray[i]][idProperty];
 | |
|         }
 | |
|       }
 | |
|       return ids;
 | |
|     }
 | |
| 
 | |
|     function updateItem(id, item) {
 | |
|       if (idxById[id] === undefined || id !== item[idProperty]) {
 | |
|         throw "Invalid or non-matching id";
 | |
|       }
 | |
|       items[idxById[id]] = item;
 | |
|       if (!updated) {
 | |
|         updated = {};
 | |
|       }
 | |
|       updated[id] = true;
 | |
|       refresh();
 | |
|     }
 | |
| 
 | |
|     function insertItem(insertBefore, item) {
 | |
|       items.splice(insertBefore, 0, item);
 | |
|       updateIdxById(insertBefore);
 | |
|       refresh();
 | |
|     }
 | |
| 
 | |
|     function addItem(item) {
 | |
|       items.push(item);
 | |
|       updateIdxById(items.length - 1);
 | |
|       refresh();
 | |
|     }
 | |
| 
 | |
|     function deleteItem(id) {
 | |
|       var idx = idxById[id];
 | |
|       if (idx === undefined) {
 | |
|         throw "Invalid id";
 | |
|       }
 | |
|       delete idxById[id];
 | |
|       items.splice(idx, 1);
 | |
|       updateIdxById(idx);
 | |
|       refresh();
 | |
|     }
 | |
| 
 | |
|     function getLength() {
 | |
|       return rows.length;
 | |
|     }
 | |
| 
 | |
|     function getItem(i) {
 | |
|       var item = rows[i];
 | |
| 
 | |
|       // if this is a group row, make sure totals are calculated and update the title
 | |
|       if (item && item.__group && item.totals && !item.totals.initialized) {
 | |
|         var gi = groupingInfos[item.level];
 | |
|         if (!gi.displayTotalsRow) {
 | |
|           calculateTotals(item.totals);
 | |
|           item.title = gi.formatter ? gi.formatter(item) : item.value;
 | |
|         }
 | |
|       }
 | |
|       // if this is a totals row, make sure it's calculated
 | |
|       else if (item && item.__groupTotals && !item.initialized) {
 | |
|         calculateTotals(item);
 | |
|       }
 | |
| 
 | |
|       return item;
 | |
|     }
 | |
| 
 | |
|     function getItemMetadata(i) {
 | |
|       var item = rows[i];
 | |
|       if (item === undefined) {
 | |
|         return null;
 | |
|       }
 | |
| 
 | |
|       // overrides for grouping rows
 | |
|       if (item.__group) {
 | |
|         return options.groupItemMetadataProvider.getGroupRowMetadata(item);
 | |
|       }
 | |
| 
 | |
|       // overrides for totals rows
 | |
|       if (item.__groupTotals) {
 | |
|         return options.groupItemMetadataProvider.getTotalsRowMetadata(item);
 | |
|       }
 | |
| 
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     function expandCollapseAllGroups(level, collapse) {
 | |
|       if (level == null) {
 | |
|         for (var i = 0; i < groupingInfos.length; i++) {
 | |
|           toggledGroupsByLevel[i] = {};
 | |
|           groupingInfos[i].collapsed = collapse;
 | |
|         }
 | |
|       } else {
 | |
|         toggledGroupsByLevel[level] = {};
 | |
|         groupingInfos[level].collapsed = collapse;
 | |
|       }
 | |
|       refresh();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param level {Number} Optional level to collapse.  If not specified, applies to all levels.
 | |
|      */
 | |
|     function collapseAllGroups(level) {
 | |
|       expandCollapseAllGroups(level, true);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param level {Number} Optional level to expand.  If not specified, applies to all levels.
 | |
|      */
 | |
|     function expandAllGroups(level) {
 | |
|       expandCollapseAllGroups(level, false);
 | |
|     }
 | |
| 
 | |
|     function expandCollapseGroup(level, groupingKey, collapse) {
 | |
|       toggledGroupsByLevel[level][groupingKey] = groupingInfos[level].collapsed ^ collapse;
 | |
|       refresh();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param varArgs Either a Slick.Group's "groupingKey" property, or a
 | |
|      *     variable argument list of grouping values denoting a unique path to the row.  For
 | |
|      *     example, calling collapseGroup('high', '10%') will collapse the '10%' subgroup of
 | |
|      *     the 'high' group.
 | |
|      */
 | |
|     function collapseGroup(varArgs) {
 | |
|       var args = Array.prototype.slice.call(arguments);
 | |
|       var arg0 = args[0];
 | |
|       if (args.length == 1 && arg0.indexOf(groupingDelimiter) != -1) {
 | |
|         expandCollapseGroup(arg0.split(groupingDelimiter).length - 1, arg0, true);
 | |
|       } else {
 | |
|         expandCollapseGroup(args.length - 1, args.join(groupingDelimiter), true);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param varArgs Either a Slick.Group's "groupingKey" property, or a
 | |
|      *     variable argument list of grouping values denoting a unique path to the row.  For
 | |
|      *     example, calling expandGroup('high', '10%') will expand the '10%' subgroup of
 | |
|      *     the 'high' group.
 | |
|      */
 | |
|     function expandGroup(varArgs) {
 | |
|       var args = Array.prototype.slice.call(arguments);
 | |
|       var arg0 = args[0];
 | |
|       if (args.length == 1 && arg0.indexOf(groupingDelimiter) != -1) {
 | |
|         expandCollapseGroup(arg0.split(groupingDelimiter).length - 1, arg0, false);
 | |
|       } else {
 | |
|         expandCollapseGroup(args.length - 1, args.join(groupingDelimiter), false);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function getGroups() {
 | |
|       return groups;
 | |
|     }
 | |
| 
 | |
|     function extractGroups(rows, parentGroup) {
 | |
|       var group;
 | |
|       var val;
 | |
|       var groups = [];
 | |
|       var groupsByVal = {};
 | |
|       var r;
 | |
|       var level = parentGroup ? parentGroup.level + 1 : 0;
 | |
|       var gi = groupingInfos[level];
 | |
| 
 | |
|       for (var i = 0, l = gi.predefinedValues.length; i < l; i++) {
 | |
|         val = gi.predefinedValues[i];
 | |
|         group = groupsByVal[val];
 | |
|         if (!group) {
 | |
|           group = new Slick.Group();
 | |
|           group.value = val;
 | |
|           group.level = level;
 | |
|           group.groupingKey = (parentGroup ? parentGroup.groupingKey + groupingDelimiter : '') + val;
 | |
|           groups[groups.length] = group;
 | |
|           groupsByVal[val] = group;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       for (var i = 0, l = rows.length; i < l; i++) {
 | |
|         r = rows[i];
 | |
|         val = gi.getterIsAFn ? gi.getter(r) : r[gi.getter];
 | |
|         group = groupsByVal[val];
 | |
|         if (!group) {
 | |
|           group = new Slick.Group();
 | |
|           group.value = val;
 | |
|           group.level = level;
 | |
|           group.groupingKey = (parentGroup ? parentGroup.groupingKey + groupingDelimiter : '') + val;
 | |
|           groups[groups.length] = group;
 | |
|           groupsByVal[val] = group;
 | |
|         }
 | |
| 
 | |
|         group.rows[group.count++] = r;
 | |
|       }
 | |
| 
 | |
|       if (level < groupingInfos.length - 1) {
 | |
|         for (var i = 0; i < groups.length; i++) {
 | |
|           group = groups[i];
 | |
|           group.groups = extractGroups(group.rows, group);
 | |
|         }
 | |
|       }      
 | |
| 
 | |
|       groups.sort(groupingInfos[level].comparer);
 | |
| 
 | |
|       return groups;
 | |
|     }
 | |
| 
 | |
|     function calculateTotals(totals) {
 | |
|       var group = totals.group;
 | |
|       var gi = groupingInfos[group.level];
 | |
|       var isLeafLevel = (group.level == groupingInfos.length);
 | |
|       var agg, idx = gi.aggregators.length;
 | |
| 
 | |
|       if (!isLeafLevel && gi.aggregateChildGroups) {
 | |
|         // make sure all the subgroups are calculated
 | |
|         var i = group.groups.length;
 | |
|         while (i--) {
 | |
|           if (!group.groups[i].initialized) {
 | |
|             calculateTotals(group.groups[i]);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       while (idx--) {
 | |
|         agg = gi.aggregators[idx];
 | |
|         agg.init();
 | |
|         if (!isLeafLevel && gi.aggregateChildGroups) {
 | |
|           gi.compiledAccumulators[idx].call(agg, group.groups);
 | |
|         } else {
 | |
|           gi.compiledAccumulators[idx].call(agg, group.rows);
 | |
|         }
 | |
|         agg.storeResult(totals);
 | |
|       }
 | |
|       totals.initialized = true;
 | |
|     }
 | |
| 
 | |
|     function addGroupTotals(group) {
 | |
|       var gi = groupingInfos[group.level];
 | |
|       var totals = new Slick.GroupTotals();
 | |
|       totals.group = group;
 | |
|       group.totals = totals;
 | |
|       if (!gi.lazyTotalsCalculation) {
 | |
|         calculateTotals(totals);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function addTotals(groups, level) {
 | |
|       level = level || 0;
 | |
|       var gi = groupingInfos[level];
 | |
|       var groupCollapsed = gi.collapsed;
 | |
|       var toggledGroups = toggledGroupsByLevel[level];      
 | |
|       var idx = groups.length, g;
 | |
|       while (idx--) {
 | |
|         g = groups[idx];
 | |
| 
 | |
|         if (g.collapsed && !gi.aggregateCollapsed) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         // Do a depth-first aggregation so that parent group aggregators can access subgroup totals.
 | |
|         if (g.groups) {
 | |
|           addTotals(g.groups, level + 1);
 | |
|         }
 | |
| 
 | |
|         if (gi.aggregators.length && (
 | |
|             gi.aggregateEmpty || g.rows.length || (g.groups && g.groups.length))) {
 | |
|           addGroupTotals(g);
 | |
|         }
 | |
| 
 | |
|         g.collapsed = groupCollapsed ^ toggledGroups[g.groupingKey];
 | |
|         g.title = gi.formatter ? gi.formatter(g) : g.value;
 | |
|       }
 | |
|     } 
 | |
| 
 | |
|     function flattenGroupedRows(groups, level) {
 | |
|       level = level || 0;
 | |
|       var gi = groupingInfos[level];
 | |
|       var groupedRows = [], rows, gl = 0, g;
 | |
|       for (var i = 0, l = groups.length; i < l; i++) {
 | |
|         g = groups[i];
 | |
|         groupedRows[gl++] = g;
 | |
| 
 | |
|         if (!g.collapsed) {
 | |
|           rows = g.groups ? flattenGroupedRows(g.groups, level + 1) : g.rows;
 | |
|           for (var j = 0, jj = rows.length; j < jj; j++) {
 | |
|             groupedRows[gl++] = rows[j];
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         if (g.totals && gi.displayTotalsRow && (!g.collapsed || gi.aggregateCollapsed)) {
 | |
|           groupedRows[gl++] = g.totals;
 | |
|         }
 | |
|       }
 | |
|       return groupedRows;
 | |
|     }
 | |
| 
 | |
|     function getFunctionInfo(fn) {
 | |
|       var fnRegex = /^function[^(]*\(([^)]*)\)\s*{([\s\S]*)}$/;
 | |
|       var matches = fn.toString().match(fnRegex);
 | |
|       return {
 | |
|         params: matches[1].split(","),
 | |
|         body: matches[2]
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     function compileAccumulatorLoop(aggregator) {
 | |
|       var accumulatorInfo = getFunctionInfo(aggregator.accumulate);
 | |
|       var fn = new Function(
 | |
|           "_items",
 | |
|           "for (var " + accumulatorInfo.params[0] + ", _i=0, _il=_items.length; _i<_il; _i++) {" +
 | |
|               accumulatorInfo.params[0] + " = _items[_i]; " +
 | |
|               accumulatorInfo.body +
 | |
|           "}"
 | |
|       );
 | |
|       fn.displayName = fn.name = "compiledAccumulatorLoop";
 | |
|       return fn;
 | |
|     }
 | |
| 
 | |
|     function compileFilter() {
 | |
|       var filterInfo = getFunctionInfo(filter);
 | |
| 
 | |
|       var filterBody = filterInfo.body
 | |
|           .replace(/return false\s*([;}]|$)/gi, "{ continue _coreloop; }$1")
 | |
|           .replace(/return true\s*([;}]|$)/gi, "{ _retval[_idx++] = $item$; continue _coreloop; }$1")
 | |
|           .replace(/return ([^;}]+?)\s*([;}]|$)/gi,
 | |
|           "{ if ($1) { _retval[_idx++] = $item$; }; continue _coreloop; }$2");
 | |
| 
 | |
|       // This preserves the function template code after JS compression,
 | |
|       // so that replace() commands still work as expected.
 | |
|       var tpl = [
 | |
|         //"function(_items, _args) { ",
 | |
|         "var _retval = [], _idx = 0; ",
 | |
|         "var $item$, $args$ = _args; ",
 | |
|         "_coreloop: ",
 | |
|         "for (var _i = 0, _il = _items.length; _i < _il; _i++) { ",
 | |
|         "$item$ = _items[_i]; ",
 | |
|         "$filter$; ",
 | |
|         "} ",
 | |
|         "return _retval; "
 | |
|         //"}"
 | |
|       ].join("");
 | |
|       tpl = tpl.replace(/\$filter\$/gi, filterBody);
 | |
|       tpl = tpl.replace(/\$item\$/gi, filterInfo.params[0]);
 | |
|       tpl = tpl.replace(/\$args\$/gi, filterInfo.params[1]);
 | |
| 
 | |
|       var fn = new Function("_items,_args", tpl);
 | |
|       fn.displayName = fn.name = "compiledFilter";
 | |
|       return fn;
 | |
|     }
 | |
| 
 | |
|     function compileFilterWithCaching() {
 | |
|       var filterInfo = getFunctionInfo(filter);
 | |
| 
 | |
|       var filterBody = filterInfo.body
 | |
|           .replace(/return false\s*([;}]|$)/gi, "{ continue _coreloop; }$1")
 | |
|           .replace(/return true\s*([;}]|$)/gi, "{ _cache[_i] = true;_retval[_idx++] = $item$; continue _coreloop; }$1")
 | |
|           .replace(/return ([^;}]+?)\s*([;}]|$)/gi,
 | |
|           "{ if ((_cache[_i] = $1)) { _retval[_idx++] = $item$; }; continue _coreloop; }$2");
 | |
| 
 | |
|       // This preserves the function template code after JS compression,
 | |
|       // so that replace() commands still work as expected.
 | |
|       var tpl = [
 | |
|         //"function(_items, _args, _cache) { ",
 | |
|         "var _retval = [], _idx = 0; ",
 | |
|         "var $item$, $args$ = _args; ",
 | |
|         "_coreloop: ",
 | |
|         "for (var _i = 0, _il = _items.length; _i < _il; _i++) { ",
 | |
|         "$item$ = _items[_i]; ",
 | |
|         "if (_cache[_i]) { ",
 | |
|         "_retval[_idx++] = $item$; ",
 | |
|         "continue _coreloop; ",
 | |
|         "} ",
 | |
|         "$filter$; ",
 | |
|         "} ",
 | |
|         "return _retval; "
 | |
|         //"}"
 | |
|       ].join("");
 | |
|       tpl = tpl.replace(/\$filter\$/gi, filterBody);
 | |
|       tpl = tpl.replace(/\$item\$/gi, filterInfo.params[0]);
 | |
|       tpl = tpl.replace(/\$args\$/gi, filterInfo.params[1]);
 | |
| 
 | |
|       var fn = new Function("_items,_args,_cache", tpl);
 | |
|       fn.displayName = fn.name = "compiledFilterWithCaching";
 | |
|       return fn;
 | |
|     }
 | |
| 
 | |
|     function uncompiledFilter(items, args) {
 | |
|       var retval = [], idx = 0;
 | |
| 
 | |
|       for (var i = 0, ii = items.length; i < ii; i++) {
 | |
|         if (filter(items[i], args)) {
 | |
|           retval[idx++] = items[i];
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       return retval;
 | |
|     }
 | |
| 
 | |
|     function uncompiledFilterWithCaching(items, args, cache) {
 | |
|       var retval = [], idx = 0, item;
 | |
| 
 | |
|       for (var i = 0, ii = items.length; i < ii; i++) {
 | |
|         item = items[i];
 | |
|         if (cache[i]) {
 | |
|           retval[idx++] = item;
 | |
|         } else if (filter(item, args)) {
 | |
|           retval[idx++] = item;
 | |
|           cache[i] = true;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       return retval;
 | |
|     }
 | |
| 
 | |
|     function getFilteredAndPagedItems(items) {
 | |
|       if (filter) {
 | |
|         var batchFilter = options.inlineFilters ? compiledFilter : uncompiledFilter;
 | |
|         var batchFilterWithCaching = options.inlineFilters ? compiledFilterWithCaching : uncompiledFilterWithCaching;
 | |
| 
 | |
|         if (refreshHints.isFilterNarrowing) {
 | |
|           filteredItems = batchFilter(filteredItems, filterArgs);
 | |
|         } else if (refreshHints.isFilterExpanding) {
 | |
|           filteredItems = batchFilterWithCaching(items, filterArgs, filterCache);
 | |
|         } else if (!refreshHints.isFilterUnchanged) {
 | |
|           filteredItems = batchFilter(items, filterArgs);
 | |
|         }
 | |
|       } else {
 | |
|         // special case:  if not filtering and not paging, the resulting
 | |
|         // rows collection needs to be a copy so that changes due to sort
 | |
|         // can be caught
 | |
|         filteredItems = pagesize ? items : items.concat();
 | |
|       }
 | |
| 
 | |
|       // get the current page
 | |
|       var paged;
 | |
|       if (pagesize) {
 | |
|         if (filteredItems.length < pagenum * pagesize) {
 | |
|           pagenum = Math.floor(filteredItems.length / pagesize);
 | |
|         }
 | |
|         paged = filteredItems.slice(pagesize * pagenum, pagesize * pagenum + pagesize);
 | |
|       } else {
 | |
|         paged = filteredItems;
 | |
|       }
 | |
| 
 | |
|       return {totalRows: filteredItems.length, rows: paged};
 | |
|     }
 | |
| 
 | |
|     function getRowDiffs(rows, newRows) {
 | |
|       var item, r, eitherIsNonData, diff = [];
 | |
|       var from = 0, to = newRows.length;
 | |
| 
 | |
|       if (refreshHints && refreshHints.ignoreDiffsBefore) {
 | |
|         from = Math.max(0,
 | |
|             Math.min(newRows.length, refreshHints.ignoreDiffsBefore));
 | |
|       }
 | |
| 
 | |
|       if (refreshHints && refreshHints.ignoreDiffsAfter) {
 | |
|         to = Math.min(newRows.length,
 | |
|             Math.max(0, refreshHints.ignoreDiffsAfter));
 | |
|       }
 | |
| 
 | |
|       for (var i = from, rl = rows.length; i < to; i++) {
 | |
|         if (i >= rl) {
 | |
|           diff[diff.length] = i;
 | |
|         } else {
 | |
|           item = newRows[i];
 | |
|           r = rows[i];
 | |
| 
 | |
|           if ((groupingInfos.length && (eitherIsNonData = (item.__nonDataRow) || (r.__nonDataRow)) &&
 | |
|               item.__group !== r.__group ||
 | |
|               item.__group && !item.equals(r))
 | |
|               || (eitherIsNonData &&
 | |
|               // no good way to compare totals since they are arbitrary DTOs
 | |
|               // deep object comparison is pretty expensive
 | |
|               // always considering them 'dirty' seems easier for the time being
 | |
|               (item.__groupTotals || r.__groupTotals))
 | |
|               || item[idProperty] != r[idProperty]
 | |
|               || (updated && updated[item[idProperty]])
 | |
|               ) {
 | |
|             diff[diff.length] = i;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       return diff;
 | |
|     }
 | |
| 
 | |
|     function recalc(_items) {
 | |
|       rowsById = null;
 | |
| 
 | |
|       if (refreshHints.isFilterNarrowing != prevRefreshHints.isFilterNarrowing ||
 | |
|           refreshHints.isFilterExpanding != prevRefreshHints.isFilterExpanding) {
 | |
|         filterCache = [];
 | |
|       }
 | |
| 
 | |
|       var filteredItems = getFilteredAndPagedItems(_items);
 | |
|       totalRows = filteredItems.totalRows;
 | |
|       var newRows = filteredItems.rows;
 | |
| 
 | |
|       groups = [];
 | |
|       if (groupingInfos.length) {
 | |
|         groups = extractGroups(newRows);
 | |
|         if (groups.length) {
 | |
|           addTotals(groups);
 | |
|           newRows = flattenGroupedRows(groups);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       var diff = getRowDiffs(rows, newRows);
 | |
| 
 | |
|       rows = newRows;
 | |
| 
 | |
|       return diff;
 | |
|     }
 | |
| 
 | |
|     function refresh() {
 | |
|       if (suspend) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       var countBefore = rows.length;
 | |
|       var totalRowsBefore = totalRows;
 | |
| 
 | |
|       var diff = recalc(items, filter); // pass as direct refs to avoid closure perf hit
 | |
| 
 | |
|       // if the current page is no longer valid, go to last page and recalc
 | |
|       // we suffer a performance penalty here, but the main loop (recalc) remains highly optimized
 | |
|       if (pagesize && totalRows < pagenum * pagesize) {
 | |
|         pagenum = Math.max(0, Math.ceil(totalRows / pagesize) - 1);
 | |
|         diff = recalc(items, filter);
 | |
|       }
 | |
| 
 | |
|       updated = null;
 | |
|       prevRefreshHints = refreshHints;
 | |
|       refreshHints = {};
 | |
| 
 | |
|       if (totalRowsBefore != totalRows) {
 | |
|         onPagingInfoChanged.notify(getPagingInfo(), null, self);
 | |
|       }
 | |
|       if (countBefore != rows.length) {
 | |
|         onRowCountChanged.notify({previous: countBefore, current: rows.length}, null, self);
 | |
|       }
 | |
|       if (diff.length > 0) {
 | |
|         onRowsChanged.notify({rows: diff}, null, self);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     /***
 | |
|      * Wires the grid and the DataView together to keep row selection tied to item ids.
 | |
|      * This is useful since, without it, the grid only knows about rows, so if the items
 | |
|      * move around, the same rows stay selected instead of the selection moving along
 | |
|      * with the items.
 | |
|      *
 | |
|      * NOTE:  This doesn't work with cell selection model.
 | |
|      *
 | |
|      * @param grid {Slick.Grid} The grid to sync selection with.
 | |
|      * @param preserveHidden {Boolean} Whether to keep selected items that go out of the
 | |
|      *     view due to them getting filtered out.
 | |
|      * @param preserveHiddenOnSelectionChange {Boolean} Whether to keep selected items
 | |
|      *     that are currently out of the view (see preserveHidden) as selected when selection
 | |
|      *     changes.
 | |
|      * @return {Slick.Event} An event that notifies when an internal list of selected row ids
 | |
|      *     changes.  This is useful since, in combination with the above two options, it allows
 | |
|      *     access to the full list selected row ids, and not just the ones visible to the grid.
 | |
|      * @method syncGridSelection
 | |
|      */
 | |
|     function syncGridSelection(grid, preserveHidden, preserveHiddenOnSelectionChange) {
 | |
|       var self = this;
 | |
|       var inHandler;
 | |
|       var selectedRowIds = self.mapRowsToIds(grid.getSelectedRows());
 | |
|       var onSelectedRowIdsChanged = new Slick.Event();
 | |
| 
 | |
|       function setSelectedRowIds(rowIds) {
 | |
|         if (selectedRowIds.join(",") == rowIds.join(",")) {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         selectedRowIds = rowIds;
 | |
| 
 | |
|         onSelectedRowIdsChanged.notify({
 | |
|           "grid": grid,
 | |
|           "ids": selectedRowIds
 | |
|         }, new Slick.EventData(), self);
 | |
|       }
 | |
| 
 | |
|       function update() {
 | |
|         if (selectedRowIds.length > 0) {
 | |
|           inHandler = true;
 | |
|           var selectedRows = self.mapIdsToRows(selectedRowIds);
 | |
|           if (!preserveHidden) {
 | |
|             setSelectedRowIds(self.mapRowsToIds(selectedRows));       
 | |
|           }
 | |
|           grid.setSelectedRows(selectedRows);
 | |
|           inHandler = false;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       grid.onSelectedRowsChanged.subscribe(function(e, args) {
 | |
|         if (inHandler) { return; }
 | |
|         var newSelectedRowIds = self.mapRowsToIds(grid.getSelectedRows());
 | |
|         if (!preserveHiddenOnSelectionChange || !grid.getOptions().multiSelect) {
 | |
|           setSelectedRowIds(newSelectedRowIds);
 | |
|         } else {
 | |
|           // keep the ones that are hidden
 | |
|           var existing = $.grep(selectedRowIds, function(id) { return self.getRowById(id) === undefined; });
 | |
|           // add the newly selected ones
 | |
|           setSelectedRowIds(existing.concat(newSelectedRowIds));
 | |
|         }
 | |
|       });
 | |
| 
 | |
|       this.onRowsChanged.subscribe(update);
 | |
| 
 | |
|       this.onRowCountChanged.subscribe(update);
 | |
| 
 | |
|       return onSelectedRowIdsChanged;
 | |
|     }
 | |
| 
 | |
|     function syncGridCellCssStyles(grid, key) {
 | |
|       var hashById;
 | |
|       var inHandler;
 | |
| 
 | |
|       // since this method can be called after the cell styles have been set,
 | |
|       // get the existing ones right away
 | |
|       storeCellCssStyles(grid.getCellCssStyles(key));
 | |
| 
 | |
|       function storeCellCssStyles(hash) {
 | |
|         hashById = {};
 | |
|         for (var row in hash) {
 | |
|           var id = rows[row][idProperty];
 | |
|           hashById[id] = hash[row];
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       function update() {
 | |
|         if (hashById) {
 | |
|           inHandler = true;
 | |
|           ensureRowsByIdCache();
 | |
|           var newHash = {};
 | |
|           for (var id in hashById) {
 | |
|             var row = rowsById[id];
 | |
|             if (row != undefined) {
 | |
|               newHash[row] = hashById[id];
 | |
|             }
 | |
|           }
 | |
|           grid.setCellCssStyles(key, newHash);
 | |
|           inHandler = false;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       grid.onCellCssStylesChanged.subscribe(function(e, args) {
 | |
|         if (inHandler) { return; }
 | |
|         if (key != args.key) { return; }
 | |
|         if (args.hash) {
 | |
|           storeCellCssStyles(args.hash);
 | |
|         }
 | |
|       });
 | |
| 
 | |
|       this.onRowsChanged.subscribe(update);
 | |
| 
 | |
|       this.onRowCountChanged.subscribe(update);
 | |
|     }
 | |
| 
 | |
|     $.extend(this, {
 | |
|       // methods
 | |
|       "beginUpdate": beginUpdate,
 | |
|       "endUpdate": endUpdate,
 | |
|       "setPagingOptions": setPagingOptions,
 | |
|       "getPagingInfo": getPagingInfo,
 | |
|       "getItems": getItems,
 | |
|       "setItems": setItems,
 | |
|       "setFilter": setFilter,
 | |
|       "sort": sort,
 | |
|       "fastSort": fastSort,
 | |
|       "reSort": reSort,
 | |
|       "setGrouping": setGrouping,
 | |
|       "getGrouping": getGrouping,
 | |
|       "groupBy": groupBy,
 | |
|       "setAggregators": setAggregators,
 | |
|       "collapseAllGroups": collapseAllGroups,
 | |
|       "expandAllGroups": expandAllGroups,
 | |
|       "collapseGroup": collapseGroup,
 | |
|       "expandGroup": expandGroup,
 | |
|       "getGroups": getGroups,
 | |
|       "getIdxById": getIdxById,
 | |
|       "getRowById": getRowById,
 | |
|       "getItemById": getItemById,
 | |
|       "getItemByIdx": getItemByIdx,
 | |
|       "mapRowsToIds": mapRowsToIds,
 | |
|       "mapIdsToRows": mapIdsToRows,
 | |
|       "setRefreshHints": setRefreshHints,
 | |
|       "setFilterArgs": setFilterArgs,
 | |
|       "refresh": refresh,
 | |
|       "updateItem": updateItem,
 | |
|       "insertItem": insertItem,
 | |
|       "addItem": addItem,
 | |
|       "deleteItem": deleteItem,
 | |
|       "syncGridSelection": syncGridSelection,
 | |
|       "syncGridCellCssStyles": syncGridCellCssStyles,
 | |
| 
 | |
|       // data provider methods
 | |
|       "getLength": getLength,
 | |
|       "getItem": getItem,
 | |
|       "getItemMetadata": getItemMetadata,
 | |
| 
 | |
|       // events
 | |
|       "onRowCountChanged": onRowCountChanged,
 | |
|       "onRowsChanged": onRowsChanged,
 | |
|       "onPagingInfoChanged": onPagingInfoChanged
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   function AvgAggregator(field) {
 | |
|     this.field_ = field;
 | |
| 
 | |
|     this.init = function () {
 | |
|       this.count_ = 0;
 | |
|       this.nonNullCount_ = 0;
 | |
|       this.sum_ = 0;
 | |
|     };
 | |
| 
 | |
|     this.accumulate = function (item) {
 | |
|       var val = item[this.field_];
 | |
|       this.count_++;
 | |
|       if (val != null && val !== "" && val !== NaN) {
 | |
|         this.nonNullCount_++;
 | |
|         this.sum_ += parseFloat(val);
 | |
|       }
 | |
|     };
 | |
| 
 | |
|     this.storeResult = function (groupTotals) {
 | |
|       if (!groupTotals.avg) {
 | |
|         groupTotals.avg = {};
 | |
|       }
 | |
|       if (this.nonNullCount_ != 0) {
 | |
|         groupTotals.avg[this.field_] = this.sum_ / this.nonNullCount_;
 | |
|       }
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   function MinAggregator(field) {
 | |
|     this.field_ = field;
 | |
| 
 | |
|     this.init = function () {
 | |
|       this.min_ = null;
 | |
|     };
 | |
| 
 | |
|     this.accumulate = function (item) {
 | |
|       var val = item[this.field_];
 | |
|       if (val != null && val !== "" && val !== NaN) {
 | |
|         if (this.min_ == null || val < this.min_) {
 | |
|           this.min_ = val;
 | |
|         }
 | |
|       }
 | |
|     };
 | |
| 
 | |
|     this.storeResult = function (groupTotals) {
 | |
|       if (!groupTotals.min) {
 | |
|         groupTotals.min = {};
 | |
|       }
 | |
|       groupTotals.min[this.field_] = this.min_;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function MaxAggregator(field) {
 | |
|     this.field_ = field;
 | |
| 
 | |
|     this.init = function () {
 | |
|       this.max_ = null;
 | |
|     };
 | |
| 
 | |
|     this.accumulate = function (item) {
 | |
|       var val = item[this.field_];
 | |
|       if (val != null && val !== "" && val !== NaN) {
 | |
|         if (this.max_ == null || val > this.max_) {
 | |
|           this.max_ = val;
 | |
|         }
 | |
|       }
 | |
|     };
 | |
| 
 | |
|     this.storeResult = function (groupTotals) {
 | |
|       if (!groupTotals.max) {
 | |
|         groupTotals.max = {};
 | |
|       }
 | |
|       groupTotals.max[this.field_] = this.max_;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function SumAggregator(field) {
 | |
|     this.field_ = field;
 | |
| 
 | |
|     this.init = function () {
 | |
|       this.sum_ = null;
 | |
|     };
 | |
| 
 | |
|     this.accumulate = function (item) {
 | |
|       var val = item[this.field_];
 | |
|       if (val != null && val !== "" && val !== NaN) {
 | |
|         this.sum_ += parseFloat(val);
 | |
|       }
 | |
|     };
 | |
| 
 | |
|     this.storeResult = function (groupTotals) {
 | |
|       if (!groupTotals.sum) {
 | |
|         groupTotals.sum = {};
 | |
|       }
 | |
|       groupTotals.sum[this.field_] = this.sum_;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // TODO:  add more built-in aggregators
 | |
|   // TODO:  merge common aggregators in one to prevent needles iterating
 | |
| 
 | |
| })(jQuery);
 |