(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 ? 0 : (a.value > b.value ? 1 : -1) ); }, 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 new Error("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 new Error("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, dataView: self}; } 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 getFilteredItems(){ return filteredItems; } function getFilter(){ return filter; } 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 getRowByItem(item) { ensureRowsByIdCache(); return rowsById[item[idProperty]]; } function getRowById(id) { ensureRowsByIdCache(); return rowsById[id]; } function getItemById(id) { return items[idxById[id]]; } function mapItemsToRows(itemArray) { var rows = []; ensureRowsByIdCache(); for (var i = 0, l = itemArray.length; i < l; i++) { var row = rowsById[itemArray[i][idProperty]]; if (row != null) { rows[rows.length] = row; } } return rows; } 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 new Error("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 new Error("Invalid id"); } delete idxById[id]; items.splice(idx, 1); updateIdxById(idx); refresh(); } function sortedAddItem(item) { if(!sortComparer) { throw new Error("sortedAddItem() requires a sort comparer, use sort()"); } insertItem(sortedIndex(item), item); } function sortedUpdateItem(id, item) { if (idxById[id] === undefined || id !== item[idProperty]) { throw new Error("Invalid or non-matching id " + idxById[id]); } if(!sortComparer) { throw new Error("sortedUpdateItem() requires a sort comparer, use sort()"); } var oldItem = getItemById(id); if(sortComparer(oldItem, item) !== 0) { // item affects sorting -> must use sorted add deleteItem(id); sortedAddItem(item); } else { // update does not affect sorting -> regular update works fine updateItem(id, item); } } function sortedIndex(searchItem) { var low = 0, high = items.length; while (low < high) { var mid = low + high >>> 1; if (sortComparer(items[mid], searchItem) === -1) { low = mid + 1; } else { high = mid; } } return low; } 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].totals.initialized) { calculateTotals(group.groups[i].totals); } } } 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 filterPath1 = "{ continue _coreloop; }$1"; var filterPath2 = "{ _retval[_idx++] = $item$; continue _coreloop; }$1"; // make some allowances for minification - there's only so far we can go with RegEx var filterBody = filterInfo.body .replace(/return false\s*([;}]|\}|$)/gi, filterPath1) .replace(/return!1([;}]|\}|$)/gi, filterPath1) .replace(/return true\s*([;}]|\}|$)/gi, filterPath2) .replace(/return!0([;}]|\}|$)/gi, filterPath2) .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 filterPath1 = "{ continue _coreloop; }$1"; var filterPath2 = "{ _cache[_i] = true;_retval[_idx++] = $item$; continue _coreloop; }$1"; // make some allowances for minification - there's only so far we can go with RegEx var filterBody = filterInfo.body .replace(/return false\s*([;}]|\}|$)/gi, filterPath1) .replace(/return!1([;}]|\}|$)/gi, filterPath1) .replace(/return true\s*([;}]|\}|$)/gi, filterPath2) .replace(/return!0([;}]|\}|$)/gi, filterPath2) .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 = filteredItems; // if (pagesize) { // if (filteredItems.length <= pagenum * pagesize) { // if (filteredItems.length === 0) { // pagenum = 0; // } else { // pagenum = Math.floor((filteredItems.length - 1) / 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; if(_items.length > 0 && _items[0].dataSrc) { totalRows = _items[0].dataSrc.getTotalRows(); } 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, dataView: self}, null, self); } //if (diff.length > 0) { onRowsChanged.notify({rows: diff, dataView: self}, 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, "dataView": self }, 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); } else { grid.onCellCssStylesChanged.unsubscribe(styleChanged); self.onRowsChanged.unsubscribe(update); self.onRowCountChanged.unsubscribe(update); } }); 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, "getFilter": getFilter, "getFilteredItems": getFilteredItems, "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, "getRowByItem": getRowByItem, "getRowById": getRowById, "getItemById": getItemById, "getItemByIdx": getItemByIdx, "mapItemsToRows": mapItemsToRows, "mapRowsToIds": mapRowsToIds, "mapIdsToRows": mapIdsToRows, "setRefreshHints": setRefreshHints, "setFilterArgs": setFilterArgs, "refresh": refresh, "updateItem": updateItem, "insertItem": insertItem, "addItem": addItem, "deleteItem": deleteItem, "sortedAddItem": sortedAddItem, "sortedUpdateItem": sortedUpdateItem, "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 !== "" && !isNaN(val)) { 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 !== "" && !isNaN(val)) { 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 !== "" && !isNaN(val)) { 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 !== "" && !isNaN(val)) { 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);