613 lines
18 KiB
JavaScript
613 lines
18 KiB
JavaScript
(function($) {
|
|
$.extend(true, window, {
|
|
Slick: {
|
|
Data: {
|
|
DataView: DataView,
|
|
Aggregators: {
|
|
Avg: AvgAggregator,
|
|
Min: MinAggregator,
|
|
Max: MaxAggregator
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
/***
|
|
* 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
|
|
};
|
|
|
|
|
|
// 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;
|
|
|
|
// grouping
|
|
var groupingGetter;
|
|
var groupingGetterIsAFn;
|
|
var groupingFormatter;
|
|
var groupingComparer;
|
|
var groups = [];
|
|
var collapsedGroups = {};
|
|
var aggregators;
|
|
var aggregateCollapsed = false;
|
|
|
|
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(hints) {
|
|
suspend = false;
|
|
refresh(hints);
|
|
}
|
|
|
|
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 = data;
|
|
idxById = {};
|
|
updateIdxById();
|
|
ensureIdUniqueness();
|
|
refresh();
|
|
}
|
|
|
|
function setPagingOptions(args) {
|
|
if (args.pageSize != undefined)
|
|
pagesize = args.pageSize;
|
|
|
|
if (args.pageNum != undefined)
|
|
pagenum = Math.min(args.pageNum, Math.ceil(totalRows / pagesize));
|
|
|
|
onPagingInfoChanged.notify(getPagingInfo(), null, self);
|
|
|
|
refresh();
|
|
}
|
|
|
|
function getPagingInfo() {
|
|
return {pageSize:pagesize, pageNum:pagenum, totalRows:totalRows};
|
|
}
|
|
|
|
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;
|
|
refresh();
|
|
}
|
|
|
|
function groupBy(valueGetter, valueFormatter, sortComparer) {
|
|
if (!options.groupItemMetadataProvider) {
|
|
options.groupItemMetadataProvider = new Slick.Data.GroupItemMetadataProvider();
|
|
}
|
|
|
|
groupingGetter = valueGetter;
|
|
groupingGetterIsAFn = typeof groupingGetter === "function";
|
|
groupingFormatter = valueFormatter;
|
|
groupingComparer = sortComparer;
|
|
collapsedGroups = {};
|
|
groups = [];
|
|
refresh();
|
|
}
|
|
|
|
function setAggregators(groupAggregators, includeCollapsed) {
|
|
aggregators = groupAggregators;
|
|
aggregateCollapsed = includeCollapsed !== undefined ? includeCollapsed : aggregateCollapsed;
|
|
refresh();
|
|
}
|
|
|
|
function getItemByIdx(i) {
|
|
return items[i];
|
|
}
|
|
|
|
function getIdxById(id) {
|
|
return idxById[id];
|
|
}
|
|
|
|
// calculate the lookup table on first call
|
|
function getRowById(id) {
|
|
if (!rowsById) {
|
|
rowsById = {};
|
|
for (var i = 0, l = rows.length; i < l; ++i) {
|
|
rowsById[rows[i][idProperty]] = i;
|
|
}
|
|
}
|
|
|
|
return rowsById[id];
|
|
}
|
|
|
|
function getItemById(id) {
|
|
return items[idxById[id]];
|
|
}
|
|
|
|
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) {
|
|
return rows[i];
|
|
}
|
|
|
|
function getItemMetadata(i) {
|
|
var item = rows[i];
|
|
if (item === undefined) {
|
|
return null;
|
|
}
|
|
|
|
// overrides for group rows
|
|
if (item.__group) {
|
|
return options.groupItemMetadataProvider.getGroupRowMetadata(item);
|
|
}
|
|
|
|
// overrides for totals rows
|
|
if (item.__groupTotals) {
|
|
return options.groupItemMetadataProvider.getTotalsRowMetadata(item);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function collapseGroup(groupingValue) {
|
|
collapsedGroups[groupingValue] = true;
|
|
refresh();
|
|
}
|
|
|
|
function expandGroup(groupingValue) {
|
|
delete collapsedGroups[groupingValue];
|
|
refresh();
|
|
}
|
|
|
|
function getGroups() {
|
|
return groups;
|
|
}
|
|
|
|
function extractGroups(rows) {
|
|
var group;
|
|
var val;
|
|
var groups = [];
|
|
var groupsByVal = {};
|
|
var r;
|
|
|
|
for (var i = 0, l = rows.length; i < l; i++) {
|
|
r = rows[i];
|
|
val = (groupingGetterIsAFn) ? groupingGetter(r) : r[groupingGetter];
|
|
group = groupsByVal[val];
|
|
if (!group) {
|
|
group = new Slick.Group();
|
|
group.count = 0;
|
|
group.value = val;
|
|
group.rows = [];
|
|
groups[groups.length] = group;
|
|
groupsByVal[val] = group;
|
|
}
|
|
|
|
group.rows[group.count++] = r;
|
|
}
|
|
|
|
return groups;
|
|
}
|
|
|
|
// TODO: lazy totals calculation
|
|
function calculateGroupTotals(group) {
|
|
var r, idx;
|
|
|
|
if (group.collapsed && !aggregateCollapsed) {
|
|
return;
|
|
}
|
|
|
|
idx = aggregators.length;
|
|
while (idx--) {
|
|
aggregators[idx].init();
|
|
}
|
|
|
|
for (var j = 0, jj = group.rows.length; j < jj; j++) {
|
|
r = group.rows[j];
|
|
idx = aggregators.length;
|
|
while (idx--) {
|
|
aggregators[idx].accumulate(r);
|
|
}
|
|
}
|
|
|
|
var t = new Slick.GroupTotals();
|
|
idx = aggregators.length;
|
|
while (idx--) {
|
|
aggregators[idx].storeResult(t);
|
|
}
|
|
t.group = group;
|
|
group.totals = t;
|
|
}
|
|
|
|
function calculateTotals(groups) {
|
|
var idx = groups.length;
|
|
while (idx--) {
|
|
calculateGroupTotals(groups[idx]);
|
|
}
|
|
}
|
|
|
|
function finalizeGroups(groups) {
|
|
var idx = groups.length, g;
|
|
while (idx--) {
|
|
g = groups[idx];
|
|
g.collapsed = (g.value in collapsedGroups);
|
|
g.title = groupingFormatter ? groupingFormatter(g) : g.value;
|
|
}
|
|
}
|
|
|
|
function flattenGroupedRows(groups) {
|
|
var groupedRows = [], gl = 0, idx, t, g, r;
|
|
for (var i = 0, l = groups.length; i < l; i++) {
|
|
g = groups[i];
|
|
groupedRows[gl++] = g;
|
|
|
|
if (!g.collapsed) {
|
|
for (var j = 0, jj = g.rows.length; j < jj; j++) {
|
|
groupedRows[gl++] = g.rows[j];
|
|
}
|
|
}
|
|
|
|
if (g.totals && (!g.collapsed || aggregateCollapsed)) {
|
|
groupedRows[gl++] = g.totals;
|
|
}
|
|
}
|
|
return groupedRows;
|
|
}
|
|
|
|
function getFilteredAndPagedItems(items, filter) {
|
|
var pageStartRow = pagesize * pagenum;
|
|
var pageEndRow = pageStartRow + pagesize;
|
|
var itemIdx = 0, rowIdx = 0, item;
|
|
var newRows = [];
|
|
|
|
// filter the data and get the current page if paging
|
|
if (filter) {
|
|
for (var i = 0, il = items.length; i < il; ++i) {
|
|
item = items[i];
|
|
|
|
if (!filter || filter(item)) {
|
|
if (!pagesize || (itemIdx >= pageStartRow && itemIdx < pageEndRow)) {
|
|
newRows[rowIdx] = item;
|
|
rowIdx++;
|
|
}
|
|
itemIdx++;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
newRows = pagesize ? items.slice(pageStartRow, pageEndRow) : items.concat();
|
|
itemIdx = items.length;
|
|
}
|
|
|
|
return {totalRows:itemIdx, rows:newRows};
|
|
}
|
|
|
|
function getRowDiffs(rows, newRows) {
|
|
var item, r, eitherIsNonData, diff = [];
|
|
for (var i = 0, rl = rows.length, nrl = newRows.length; i < nrl; i++) {
|
|
if (i >= rl) {
|
|
diff[diff.length] = i;
|
|
}
|
|
else {
|
|
item = newRows[i];
|
|
r = rows[i];
|
|
|
|
if ((groupingGetter && (eitherIsNonData = (item.__nonDataRow) || (r.__nonDataRow)) &&
|
|
item.__group !== r.__group ||
|
|
item.__updated ||
|
|
item.__group && !item.equals(r))
|
|
|| (aggregators && 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, _rows, _filter) {
|
|
rowsById = null;
|
|
|
|
var newRows = [];
|
|
|
|
var filteredItems = getFilteredAndPagedItems(_items, _filter);
|
|
totalRows = filteredItems.totalRows;
|
|
newRows = filteredItems.rows;
|
|
|
|
groups = [];
|
|
if (groupingGetter != null) {
|
|
groups = extractGroups(newRows);
|
|
if (groups.length) {
|
|
finalizeGroups(groups);
|
|
if (aggregators) {
|
|
calculateTotals(groups);
|
|
}
|
|
groups.sort(groupingComparer);
|
|
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, rows, 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.floor(totalRows / pagesize);
|
|
diff = recalc(items, rows, filter);
|
|
}
|
|
|
|
updated = null;
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
return {
|
|
// methods
|
|
"beginUpdate": beginUpdate,
|
|
"endUpdate": endUpdate,
|
|
"setPagingOptions": setPagingOptions,
|
|
"getPagingInfo": getPagingInfo,
|
|
"getItems": getItems,
|
|
"setItems": setItems,
|
|
"setFilter": setFilter,
|
|
"sort": sort,
|
|
"fastSort": fastSort,
|
|
"reSort": reSort,
|
|
"groupBy": groupBy,
|
|
"setAggregators": setAggregators,
|
|
"collapseGroup": collapseGroup,
|
|
"expandGroup": expandGroup,
|
|
"getGroups": getGroups,
|
|
"getIdxById": getIdxById,
|
|
"getRowById": getRowById,
|
|
"getItemById": getItemById,
|
|
"getItemByIdx": getItemByIdx,
|
|
"refresh": refresh,
|
|
"updateItem": updateItem,
|
|
"insertItem": insertItem,
|
|
"addItem": addItem,
|
|
"deleteItem": deleteItem,
|
|
|
|
// data provider methods
|
|
"getLength": getLength,
|
|
"getItem": getItem,
|
|
"getItemMetadata": getItemMetadata,
|
|
|
|
// events
|
|
"onRowCountChanged": onRowCountChanged,
|
|
"onRowsChanged": onRowsChanged,
|
|
"onPagingInfoChanged": onPagingInfoChanged
|
|
};
|
|
}
|
|
|
|
|
|
|
|
|
|
function AvgAggregator(field) {
|
|
var count;
|
|
var nonNullCount;
|
|
var sum;
|
|
|
|
this.init = function() {
|
|
count = 0;
|
|
nonNullCount = 0;
|
|
sum = 0;
|
|
};
|
|
|
|
this.accumulate = function(item) {
|
|
var val = item[field];
|
|
count++;
|
|
if (val != null && val != NaN) {
|
|
nonNullCount++;
|
|
sum += 1 * val;
|
|
}
|
|
};
|
|
|
|
this.storeResult = function(groupTotals) {
|
|
if (!groupTotals.avg) {
|
|
groupTotals.avg = {};
|
|
}
|
|
if (nonNullCount != 0) {
|
|
groupTotals.avg[field] = sum / nonNullCount;
|
|
}
|
|
};
|
|
}
|
|
|
|
function MinAggregator(field) {
|
|
var min;
|
|
|
|
this.init = function() {
|
|
min = null;
|
|
};
|
|
|
|
this.accumulate = function(item) {
|
|
var val = item[field];
|
|
if (val != null && val != NaN) {
|
|
if (min == null ||val < min) {
|
|
min = val;
|
|
}
|
|
}
|
|
};
|
|
|
|
this.storeResult = function(groupTotals) {
|
|
if (!groupTotals.min) {
|
|
groupTotals.min = {};
|
|
}
|
|
groupTotals.min[field] = min;
|
|
}
|
|
}
|
|
|
|
function MaxAggregator(field) {
|
|
var max;
|
|
|
|
this.init = function() {
|
|
max = null;
|
|
};
|
|
|
|
this.accumulate = function(item) {
|
|
var val = item[field];
|
|
if (val != null && val != NaN) {
|
|
if (max == null ||val > max) {
|
|
max = val;
|
|
}
|
|
}
|
|
};
|
|
|
|
this.storeResult = function(groupTotals) {
|
|
if (!groupTotals.max) {
|
|
groupTotals.max = {};
|
|
}
|
|
groupTotals.max[field] = max;
|
|
}
|
|
}
|
|
|
|
// TODO: add more built-in aggregators
|
|
// TODO: merge common aggregators in one to prevent needles iterating
|
|
|
|
})(jQuery); |