UNPKG

42.1 kBJavaScriptView Raw
1(function ($) {
2 $.extend(true, window, {
3 Slick: {
4 Data: {
5 DataView: DataView,
6 Aggregators: {
7 Avg: AvgAggregator,
8 Min: MinAggregator,
9 Max: MaxAggregator,
10 Sum: SumAggregator,
11 Count: CountAggregator
12 }
13 }
14 }
15 });
16
17
18 /***
19 * A sample Model implementation.
20 * Provides a filtered view of the underlying data.
21 *
22 * Relies on the data item having an "id" property uniquely identifying it.
23 */
24 function DataView(options) {
25 var self = this;
26
27 var defaults = {
28 groupItemMetadataProvider: null,
29 inlineFilters: false
30 };
31
32
33 // private
34 var idProperty = "id"; // property holding a unique row id
35 var items = []; // data by index
36 var rows = []; // data by row
37 var idxById = {}; // indexes by id
38 var rowsById = null; // rows by id; lazy-calculated
39 var filter = null; // filter function
40 var updated = null; // updated item ids
41 var suspend = false; // suspends the recalculation
42 var sortAsc = true;
43 var fastSortField;
44 var sortComparer;
45 var refreshHints = {};
46 var prevRefreshHints = {};
47 var filterArgs;
48 var filteredItems = [];
49 var compiledFilter;
50 var compiledFilterWithCaching;
51 var filterCache = [];
52
53 // grouping
54 var groupingInfoDefaults = {
55 getter: null,
56 formatter: null,
57 comparer: function (a, b) {
58 return (a.value === b.value ? 0 :
59 (a.value > b.value ? 1 : -1)
60 );
61 },
62 predefinedValues: [],
63 aggregators: [],
64 aggregateEmpty: false,
65 aggregateCollapsed: false,
66 aggregateChildGroups: false,
67 collapsed: false,
68 displayTotalsRow: true,
69 lazyTotalsCalculation: false
70 };
71 var groupingInfos = [];
72 var groups = [];
73 var toggledGroupsByLevel = [];
74 var groupingDelimiter = ':|:';
75
76 var pagesize = 0;
77 var pagenum = 0;
78 var totalRows = 0;
79
80 // events
81 var onSetItemsCalled = new Slick.Event();
82 var onRowCountChanged = new Slick.Event();
83 var onRowsChanged = new Slick.Event();
84 var onRowsOrCountChanged = new Slick.Event();
85 var onBeforePagingInfoChanged = new Slick.Event();
86 var onPagingInfoChanged = new Slick.Event();
87 var onGroupExpanded = new Slick.Event();
88 var onGroupCollapsed = new Slick.Event();
89
90 options = $.extend(true, {}, defaults, options);
91
92
93 function beginUpdate() {
94 suspend = true;
95 }
96
97 function endUpdate() {
98 suspend = false;
99 refresh();
100 }
101
102 function setRefreshHints(hints) {
103 refreshHints = hints;
104 }
105
106 function setFilterArgs(args) {
107 filterArgs = args;
108 }
109
110 function updateIdxById(startingIndex) {
111 startingIndex = startingIndex || 0;
112 var id;
113 for (var i = startingIndex, l = items.length; i < l; i++) {
114 id = items[i][idProperty];
115 if (id === undefined) {
116 throw new Error("Each data element must implement a unique 'id' property");
117 }
118 idxById[id] = i;
119 }
120 }
121
122 function ensureIdUniqueness() {
123 var id;
124 for (var i = 0, l = items.length; i < l; i++) {
125 id = items[i][idProperty];
126 if (id === undefined || idxById[id] !== i) {
127 throw new Error("Each data element must implement a unique 'id' property");
128 }
129 }
130 }
131
132 function getItems() {
133 return items;
134 }
135
136 function getIdPropertyName() {
137 return idProperty;
138 }
139
140 function setItems(data, objectIdProperty) {
141 if (objectIdProperty !== undefined) {
142 idProperty = objectIdProperty;
143 }
144 items = filteredItems = data;
145 idxById = {};
146 updateIdxById();
147 ensureIdUniqueness();
148 refresh();
149 onSetItemsCalled.notify({ idProperty: objectIdProperty }, null, self);
150 }
151
152 function setPagingOptions(args) {
153 onBeforePagingInfoChanged.notify(getPagingInfo(), null, self);
154
155 if (args.pageSize != undefined) {
156 pagesize = args.pageSize;
157 pagenum = pagesize ? Math.min(pagenum, Math.max(0, Math.ceil(totalRows / pagesize) - 1)) : 0;
158 }
159
160 if (args.pageNum != undefined) {
161 pagenum = Math.min(args.pageNum, Math.max(0, Math.ceil(totalRows / pagesize) - 1));
162 }
163
164 onPagingInfoChanged.notify(getPagingInfo(), null, self);
165
166 refresh();
167 }
168
169 function getPagingInfo() {
170 var totalPages = pagesize ? Math.max(1, Math.ceil(totalRows / pagesize)) : 1;
171 return { pageSize: pagesize, pageNum: pagenum, totalRows: totalRows, totalPages: totalPages, dataView: self };
172 }
173
174 function sort(comparer, ascending) {
175 sortAsc = ascending;
176 sortComparer = comparer;
177 fastSortField = null;
178 if (ascending === false) {
179 items.reverse();
180 }
181 items.sort(comparer);
182 if (ascending === false) {
183 items.reverse();
184 }
185 idxById = {};
186 updateIdxById();
187 refresh();
188 }
189
190 /***
191 * Provides a workaround for the extremely slow sorting in IE.
192 * Does a [lexicographic] sort on a give column by temporarily overriding Object.prototype.toString
193 * to return the value of that field and then doing a native Array.sort().
194 */
195 function fastSort(field, ascending) {
196 sortAsc = ascending;
197 fastSortField = field;
198 sortComparer = null;
199 var oldToString = Object.prototype.toString;
200 Object.prototype.toString = (typeof field == "function") ? field : function () {
201 return this[field];
202 };
203 // an extra reversal for descending sort keeps the sort stable
204 // (assuming a stable native sort implementation, which isn't true in some cases)
205 if (ascending === false) {
206 items.reverse();
207 }
208 items.sort();
209 Object.prototype.toString = oldToString;
210 if (ascending === false) {
211 items.reverse();
212 }
213 idxById = {};
214 updateIdxById();
215 refresh();
216 }
217
218 function reSort() {
219 if (sortComparer) {
220 sort(sortComparer, sortAsc);
221 } else if (fastSortField) {
222 fastSort(fastSortField, sortAsc);
223 }
224 }
225
226 function getFilteredItems() {
227 return filteredItems;
228 }
229
230
231 function getFilter() {
232 return filter;
233 }
234
235 function setFilter(filterFn) {
236 filter = filterFn;
237 if (options.inlineFilters) {
238 compiledFilter = compileFilter();
239 compiledFilterWithCaching = compileFilterWithCaching();
240 }
241 refresh();
242 }
243
244 function getGrouping() {
245 return groupingInfos;
246 }
247
248 function setGrouping(groupingInfo) {
249 if (!options.groupItemMetadataProvider) {
250 options.groupItemMetadataProvider = new Slick.Data.GroupItemMetadataProvider();
251 }
252
253 groups = [];
254 toggledGroupsByLevel = [];
255 groupingInfo = groupingInfo || [];
256 groupingInfos = (groupingInfo instanceof Array) ? groupingInfo : [groupingInfo];
257
258 for (var i = 0; i < groupingInfos.length; i++) {
259 var gi = groupingInfos[i] = $.extend(true, {}, groupingInfoDefaults, groupingInfos[i]);
260 gi.getterIsAFn = typeof gi.getter === "function";
261
262 // pre-compile accumulator loops
263 gi.compiledAccumulators = [];
264 var idx = gi.aggregators.length;
265 while (idx--) {
266 gi.compiledAccumulators[idx] = compileAccumulatorLoop(gi.aggregators[idx]);
267 }
268
269 toggledGroupsByLevel[i] = {};
270 }
271
272 refresh();
273 }
274
275 /**
276 * @deprecated Please use {@link setGrouping}.
277 */
278 function groupBy(valueGetter, valueFormatter, sortComparer) {
279 if (valueGetter == null) {
280 setGrouping([]);
281 return;
282 }
283
284 setGrouping({
285 getter: valueGetter,
286 formatter: valueFormatter,
287 comparer: sortComparer
288 });
289 }
290
291 /**
292 * @deprecated Please use {@link setGrouping}.
293 */
294 function setAggregators(groupAggregators, includeCollapsed) {
295 if (!groupingInfos.length) {
296 throw new Error("At least one grouping must be specified before calling setAggregators().");
297 }
298
299 groupingInfos[0].aggregators = groupAggregators;
300 groupingInfos[0].aggregateCollapsed = includeCollapsed;
301
302 setGrouping(groupingInfos);
303 }
304
305 function getItemByIdx(i) {
306 return items[i];
307 }
308
309 function getIdxById(id) {
310 return idxById[id];
311 }
312
313 function ensureRowsByIdCache() {
314 if (!rowsById) {
315 rowsById = {};
316 for (var i = 0, l = rows.length; i < l; i++) {
317 rowsById[rows[i][idProperty]] = i;
318 }
319 }
320 }
321
322 function getRowByItem(item) {
323 ensureRowsByIdCache();
324 return rowsById[item[idProperty]];
325 }
326
327 function getRowById(id) {
328 ensureRowsByIdCache();
329 return rowsById[id];
330 }
331
332 function getItemById(id) {
333 return items[idxById[id]];
334 }
335
336 function mapItemsToRows(itemArray) {
337 var rows = [];
338 ensureRowsByIdCache();
339 for (var i = 0, l = itemArray.length; i < l; i++) {
340 var row = rowsById[itemArray[i][idProperty]];
341 if (row != null) {
342 rows[rows.length] = row;
343 }
344 }
345 return rows;
346 }
347
348 function mapIdsToRows(idArray) {
349 var rows = [];
350 ensureRowsByIdCache();
351 for (var i = 0, l = idArray.length; i < l; i++) {
352 var row = rowsById[idArray[i]];
353 if (row != null) {
354 rows[rows.length] = row;
355 }
356 }
357 return rows;
358 }
359
360 function mapRowsToIds(rowArray) {
361 var ids = [];
362 for (var i = 0, l = rowArray.length; i < l; i++) {
363 if (rowArray[i] < rows.length) {
364 ids[ids.length] = rows[rowArray[i]][idProperty];
365 }
366 }
367 return ids;
368 }
369
370 function updateItem(id, item) {
371 // see also https://github.com/mleibman/SlickGrid/issues/1082
372 if (idxById[id] === undefined) {
373 throw new Error("Invalid id");
374 }
375
376 // What if the specified item also has an updated idProperty?
377 // Then we'll have to update the index as well, and possibly the `updated` cache too.
378 if (id !== item[idProperty]) {
379 // make sure the new id is unique:
380 var newId = item[idProperty];
381 if (newId == null) {
382 throw new Error("Cannot update item to associate with a null id");
383 }
384 if (idxById[newId] !== undefined) {
385 throw new Error("Cannot update item to associate with a non-unique id");
386 }
387 idxById[newId] = idxById[id];
388 delete idxById[id];
389
390 // Also update the `updated` hashtable/markercache? Yes, `recalc()` inside `refresh()` needs that one!
391 if (updated && updated[id]) {
392 delete updated[id];
393 }
394
395 // Also update the row indexes? no need since the `refresh()`, further down, blows away the `rowsById[]` cache!
396
397 id = newId;
398 }
399 items[idxById[id]] = item;
400
401 // Also update the rows? no need since the `refresh()`, further down, blows away the `rows[]` cache and recalculates it via `recalc()`!
402
403 if (!updated) {
404 updated = {};
405 }
406 updated[id] = true;
407 refresh();
408 }
409
410 function insertItem(insertBefore, item) {
411 items.splice(insertBefore, 0, item);
412 updateIdxById(insertBefore);
413 refresh();
414 }
415
416 function addItem(item) {
417 items.push(item);
418 updateIdxById(items.length - 1);
419 refresh();
420 }
421
422 function deleteItem(id) {
423 var idx = idxById[id];
424 if (idx === undefined) {
425 throw new Error("Invalid id");
426 }
427 delete idxById[id];
428 items.splice(idx, 1);
429 updateIdxById(idx);
430 refresh();
431 }
432
433 function sortedAddItem(item) {
434 if (!sortComparer) {
435 throw new Error("sortedAddItem() requires a sort comparer, use sort()");
436 }
437 insertItem(sortedIndex(item), item);
438 }
439
440 function sortedUpdateItem(id, item) {
441 if (idxById[id] === undefined || id !== item[idProperty]) {
442 throw new Error("Invalid or non-matching id " + idxById[id]);
443 }
444 if (!sortComparer) {
445 throw new Error("sortedUpdateItem() requires a sort comparer, use sort()");
446 }
447 var oldItem = getItemById(id);
448 if (sortComparer(oldItem, item) !== 0) {
449 // item affects sorting -> must use sorted add
450 deleteItem(id);
451 sortedAddItem(item);
452 }
453 else { // update does not affect sorting -> regular update works fine
454 updateItem(id, item);
455 }
456 }
457
458 function sortedIndex(searchItem) {
459 var low = 0, high = items.length;
460
461 while (low < high) {
462 var mid = low + high >>> 1;
463 if (sortComparer(items[mid], searchItem) === -1) {
464 low = mid + 1;
465 }
466 else {
467 high = mid;
468 }
469 }
470 return low;
471 }
472
473 function getLength() {
474 return rows.length;
475 }
476
477 function getItem(i) {
478 var item = rows[i];
479
480 // if this is a group row, make sure totals are calculated and update the title
481 if (item && item.__group && item.totals && !item.totals.initialized) {
482 var gi = groupingInfos[item.level];
483 if (!gi.displayTotalsRow) {
484 calculateTotals(item.totals);
485 item.title = gi.formatter ? gi.formatter(item) : item.value;
486 }
487 }
488 // if this is a totals row, make sure it's calculated
489 else if (item && item.__groupTotals && !item.initialized) {
490 calculateTotals(item);
491 }
492
493 return item;
494 }
495
496 function getItemMetadata(i) {
497 var item = rows[i];
498 if (item === undefined) {
499 return null;
500 }
501
502 // overrides for grouping rows
503 if (item.__group) {
504 return options.groupItemMetadataProvider.getGroupRowMetadata(item);
505 }
506
507 // overrides for totals rows
508 if (item.__groupTotals) {
509 return options.groupItemMetadataProvider.getTotalsRowMetadata(item);
510 }
511
512 return null;
513 }
514
515 function expandCollapseAllGroups(level, collapse) {
516 if (level == null) {
517 for (var i = 0; i < groupingInfos.length; i++) {
518 toggledGroupsByLevel[i] = {};
519 groupingInfos[i].collapsed = collapse;
520
521 if (collapse === true) {
522 onGroupCollapsed.notify({ level: i, groupingKey: null });
523 } else {
524 onGroupExpanded.notify({ level: i, groupingKey: null });
525 }
526 }
527 } else {
528 toggledGroupsByLevel[level] = {};
529 groupingInfos[level].collapsed = collapse;
530
531 if (collapse === true) {
532 onGroupCollapsed.notify({ level: level, groupingKey: null });
533 } else {
534 onGroupExpanded.notify({ level: level, groupingKey: null });
535 }
536 }
537 refresh();
538 }
539
540 /**
541 * @param level {Number} Optional level to collapse. If not specified, applies to all levels.
542 */
543 function collapseAllGroups(level) {
544 expandCollapseAllGroups(level, true);
545 }
546
547 /**
548 * @param level {Number} Optional level to expand. If not specified, applies to all levels.
549 */
550 function expandAllGroups(level) {
551 expandCollapseAllGroups(level, false);
552 }
553
554 function expandCollapseGroup(level, groupingKey, collapse) {
555 toggledGroupsByLevel[level][groupingKey] = groupingInfos[level].collapsed ^ collapse;
556 refresh();
557 }
558
559 /**
560 * @param varArgs Either a Slick.Group's "groupingKey" property, or a
561 * variable argument list of grouping values denoting a unique path to the row. For
562 * example, calling collapseGroup('high', '10%') will collapse the '10%' subgroup of
563 * the 'high' group.
564 */
565 function collapseGroup(varArgs) {
566 var args = Array.prototype.slice.call(arguments);
567 var arg0 = args[0];
568 var groupingKey;
569 var level;
570
571 if (args.length === 1 && arg0.indexOf(groupingDelimiter) !== -1) {
572 groupingKey = arg0;
573 level = arg0.split(groupingDelimiter).length - 1;
574 } else {
575 groupingKey = args.join(groupingDelimiter);
576 level = args.length - 1;
577 }
578
579 expandCollapseGroup(level, groupingKey, true);
580 onGroupCollapsed.notify({ level: level, groupingKey: groupingKey });
581 }
582
583 /**
584 * @param varArgs Either a Slick.Group's "groupingKey" property, or a
585 * variable argument list of grouping values denoting a unique path to the row. For
586 * example, calling expandGroup('high', '10%') will expand the '10%' subgroup of
587 * the 'high' group.
588 */
589 function expandGroup(varArgs) {
590 var args = Array.prototype.slice.call(arguments);
591 var arg0 = args[0];
592 var groupingKey;
593 var level;
594
595 if (args.length === 1 && arg0.indexOf(groupingDelimiter) !== -1) {
596 level = arg0.split(groupingDelimiter).length - 1;
597 groupingKey = arg0;
598 } else {
599 level = args.length - 1;
600 groupingKey = args.join(groupingDelimiter);
601 }
602
603 expandCollapseGroup(level, groupingKey, false);
604 onGroupExpanded.notify({ level: level, groupingKey: groupingKey });
605 }
606
607 function getGroups() {
608 return groups;
609 }
610
611 function extractGroups(rows, parentGroup) {
612 var group;
613 var val;
614 var groups = [];
615 var groupsByVal = {};
616 var r;
617 var level = parentGroup ? parentGroup.level + 1 : 0;
618 var gi = groupingInfos[level];
619
620 for (var i = 0, l = gi.predefinedValues.length; i < l; i++) {
621 val = gi.predefinedValues[i];
622 group = groupsByVal[val];
623 if (!group) {
624 group = new Slick.Group();
625 group.value = val;
626 group.level = level;
627 group.groupingKey = (parentGroup ? parentGroup.groupingKey + groupingDelimiter : '') + val;
628 groups[groups.length] = group;
629 groupsByVal[val] = group;
630 }
631 }
632
633 for (var i = 0, l = rows.length; i < l; i++) {
634 r = rows[i];
635 val = gi.getterIsAFn ? gi.getter(r) : r[gi.getter];
636 group = groupsByVal[val];
637 if (!group) {
638 group = new Slick.Group();
639 group.value = val;
640 group.level = level;
641 group.groupingKey = (parentGroup ? parentGroup.groupingKey + groupingDelimiter : '') + val;
642 groups[groups.length] = group;
643 groupsByVal[val] = group;
644 }
645
646 group.rows[group.count++] = r;
647 }
648
649 if (level < groupingInfos.length - 1) {
650 for (var i = 0; i < groups.length; i++) {
651 group = groups[i];
652 group.groups = extractGroups(group.rows, group);
653 }
654 }
655
656 if(groups.length) {
657 addTotals(groups);
658 }
659
660 groups.sort(groupingInfos[level].comparer);
661
662 return groups;
663 }
664
665 function calculateTotals(totals) {
666 var group = totals.group;
667 var gi = groupingInfos[group.level];
668 var isLeafLevel = (group.level == groupingInfos.length);
669 var agg, idx = gi.aggregators.length;
670
671 if (!isLeafLevel && gi.aggregateChildGroups) {
672 // make sure all the subgroups are calculated
673 var i = group.groups.length;
674 while (i--) {
675 if (!group.groups[i].totals.initialized) {
676 calculateTotals(group.groups[i].totals);
677 }
678 }
679 }
680
681 while (idx--) {
682 agg = gi.aggregators[idx];
683 agg.init();
684 if (!isLeafLevel && gi.aggregateChildGroups) {
685 gi.compiledAccumulators[idx].call(agg, group.groups);
686 } else {
687 gi.compiledAccumulators[idx].call(agg, group.rows);
688 }
689 agg.storeResult(totals);
690 }
691 totals.initialized = true;
692 }
693
694 function addGroupTotals(group) {
695 var gi = groupingInfos[group.level];
696 var totals = new Slick.GroupTotals();
697 totals.group = group;
698 group.totals = totals;
699 if (!gi.lazyTotalsCalculation) {
700 calculateTotals(totals);
701 }
702 }
703
704 function addTotals(groups, level) {
705 level = level || 0;
706 var gi = groupingInfos[level];
707 var groupCollapsed = gi.collapsed;
708 var toggledGroups = toggledGroupsByLevel[level];
709 var idx = groups.length, g;
710 while (idx--) {
711 g = groups[idx];
712
713 if (g.collapsed && !gi.aggregateCollapsed) {
714 continue;
715 }
716
717 // Do a depth-first aggregation so that parent group aggregators can access subgroup totals.
718 if (g.groups) {
719 addTotals(g.groups, level + 1);
720 }
721
722 if (gi.aggregators.length && (
723 gi.aggregateEmpty || g.rows.length || (g.groups && g.groups.length))) {
724 addGroupTotals(g);
725 }
726
727 g.collapsed = groupCollapsed ^ toggledGroups[g.groupingKey];
728 g.title = gi.formatter ? gi.formatter(g) : g.value;
729 }
730 }
731
732 function flattenGroupedRows(groups, level) {
733 level = level || 0;
734 var gi = groupingInfos[level];
735 var groupedRows = [], rows, gl = 0, g;
736 for (var i = 0, l = groups.length; i < l; i++) {
737 g = groups[i];
738 groupedRows[gl++] = g;
739
740 if (!g.collapsed) {
741 rows = g.groups ? flattenGroupedRows(g.groups, level + 1) : g.rows;
742 for (var j = 0, jj = rows.length; j < jj; j++) {
743 groupedRows[gl++] = rows[j];
744 }
745 }
746
747 if (g.totals && gi.displayTotalsRow && (!g.collapsed || gi.aggregateCollapsed)) {
748 groupedRows[gl++] = g.totals;
749 }
750 }
751 return groupedRows;
752 }
753
754 function getFunctionInfo(fn) {
755 var fnStr = fn.toString();
756 var usingEs5 = fnStr.indexOf('function') >= 0; // with ES6, the word function is not present
757 var fnRegex = usingEs5 ? /^function[^(]*\(([^)]*)\)\s*{([\s\S]*)}$/ : /^[^(]*\(([^)]*)\)\s*{([\s\S]*)}$/;
758 var matches = fn.toString().match(fnRegex);
759 return {
760 params: matches[1].split(","),
761 body: matches[2]
762 };
763 }
764
765 function compileAccumulatorLoop(aggregator) {
766 if (aggregator.accumulate) {
767 var accumulatorInfo = getFunctionInfo(aggregator.accumulate);
768 var fn = new Function(
769 "_items",
770 "for (var " + accumulatorInfo.params[0] + ", _i=0, _il=_items.length; _i<_il; _i++) {" +
771 accumulatorInfo.params[0] + " = _items[_i]; " +
772 accumulatorInfo.body +
773 "}"
774 );
775 var fnName = "compiledAccumulatorLoop";
776 fn.displayName = fnName;
777 fn.name = setFunctionName(fn, fnName);
778 return fn;
779 } else {
780 return function noAccumulator() {
781 }
782 }
783 }
784
785 function compileFilter() {
786 var filterInfo = getFunctionInfo(filter);
787
788 var filterPath1 = "{ continue _coreloop; }$1";
789 var filterPath2 = "{ _retval[_idx++] = $item$; continue _coreloop; }$1";
790 // make some allowances for minification - there's only so far we can go with RegEx
791 var filterBody = filterInfo.body
792 .replace(/return false\s*([;}]|\}|$)/gi, filterPath1)
793 .replace(/return!1([;}]|\}|$)/gi, filterPath1)
794 .replace(/return true\s*([;}]|\}|$)/gi, filterPath2)
795 .replace(/return!0([;}]|\}|$)/gi, filterPath2)
796 .replace(/return ([^;}]+?)\s*([;}]|$)/gi,
797 "{ if ($1) { _retval[_idx++] = $item$; }; continue _coreloop; }$2");
798
799 // This preserves the function template code after JS compression,
800 // so that replace() commands still work as expected.
801 var tpl = [
802 //"function(_items, _args) { ",
803 "var _retval = [], _idx = 0; ",
804 "var $item$, $args$ = _args; ",
805 "_coreloop: ",
806 "for (var _i = 0, _il = _items.length; _i < _il; _i++) { ",
807 "$item$ = _items[_i]; ",
808 "$filter$; ",
809 "} ",
810 "return _retval; "
811 //"}"
812 ].join("");
813 tpl = tpl.replace(/\$filter\$/gi, filterBody);
814 tpl = tpl.replace(/\$item\$/gi, filterInfo.params[0]);
815 tpl = tpl.replace(/\$args\$/gi, filterInfo.params[1]);
816
817 var fn = new Function("_items,_args", tpl);
818 var fnName = "compiledFilter";
819 fn.displayName = fnName;
820 fn.name = setFunctionName(fn, fnName);
821 return fn;
822 }
823
824 function compileFilterWithCaching() {
825 var filterInfo = getFunctionInfo(filter);
826
827 var filterPath1 = "{ continue _coreloop; }$1";
828 var filterPath2 = "{ _cache[_i] = true;_retval[_idx++] = $item$; continue _coreloop; }$1";
829 // make some allowances for minification - there's only so far we can go with RegEx
830 var filterBody = filterInfo.body
831 .replace(/return false\s*([;}]|\}|$)/gi, filterPath1)
832 .replace(/return!1([;}]|\}|$)/gi, filterPath1)
833 .replace(/return true\s*([;}]|\}|$)/gi, filterPath2)
834 .replace(/return!0([;}]|\}|$)/gi, filterPath2)
835 .replace(/return ([^;}]+?)\s*([;}]|$)/gi,
836 "{ if ((_cache[_i] = $1)) { _retval[_idx++] = $item$; }; continue _coreloop; }$2");
837
838 // This preserves the function template code after JS compression,
839 // so that replace() commands still work as expected.
840 var tpl = [
841 //"function(_items, _args, _cache) { ",
842 "var _retval = [], _idx = 0; ",
843 "var $item$, $args$ = _args; ",
844 "_coreloop: ",
845 "for (var _i = 0, _il = _items.length; _i < _il; _i++) { ",
846 "$item$ = _items[_i]; ",
847 "if (_cache[_i]) { ",
848 "_retval[_idx++] = $item$; ",
849 "continue _coreloop; ",
850 "} ",
851 "$filter$; ",
852 "} ",
853 "return _retval; "
854 //"}"
855 ].join("");
856 tpl = tpl.replace(/\$filter\$/gi, filterBody);
857 tpl = tpl.replace(/\$item\$/gi, filterInfo.params[0]);
858 tpl = tpl.replace(/\$args\$/gi, filterInfo.params[1]);
859
860 var fn = new Function("_items,_args,_cache", tpl);
861 var fnName = "compiledFilterWithCaching";
862 fn.displayName = fnName;
863 fn.name = setFunctionName(fn, fnName);
864 return fn;
865 }
866
867 /**
868 * In ES5 we could set the function name on the fly but in ES6 this is forbidden and we need to set it through differently
869 * We can use Object.defineProperty and set it the property to writable, see MDN for reference
870 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
871 * @param {string} fn
872 * @param {string} fnName
873 */
874 function setFunctionName(fn, fnName) {
875 try {
876 Object.defineProperty(fn, 'name', {
877 writable: true,
878 value: fnName
879 });
880 } catch (err) {
881 fn.name = fnName;
882 }
883 }
884
885 function uncompiledFilter(items, args) {
886 var retval = [], idx = 0;
887
888 for (var i = 0, ii = items.length; i < ii; i++) {
889 if (filter(items[i], args)) {
890 retval[idx++] = items[i];
891 }
892 }
893
894 return retval;
895 }
896
897 function uncompiledFilterWithCaching(items, args, cache) {
898 var retval = [], idx = 0, item;
899
900 for (var i = 0, ii = items.length; i < ii; i++) {
901 item = items[i];
902 if (cache[i]) {
903 retval[idx++] = item;
904 } else if (filter(item, args)) {
905 retval[idx++] = item;
906 cache[i] = true;
907 }
908 }
909
910 return retval;
911 }
912
913 function getFilteredAndPagedItems(items) {
914 if (filter) {
915 var batchFilter = options.inlineFilters ? compiledFilter : uncompiledFilter;
916 var batchFilterWithCaching = options.inlineFilters ? compiledFilterWithCaching : uncompiledFilterWithCaching;
917
918 if (refreshHints.isFilterNarrowing) {
919 filteredItems = batchFilter(filteredItems, filterArgs);
920 } else if (refreshHints.isFilterExpanding) {
921 filteredItems = batchFilterWithCaching(items, filterArgs, filterCache);
922 } else if (!refreshHints.isFilterUnchanged) {
923 filteredItems = batchFilter(items, filterArgs);
924 }
925 } else {
926 // special case: if not filtering and not paging, the resulting
927 // rows collection needs to be a copy so that changes due to sort
928 // can be caught
929 filteredItems = pagesize ? items : items.concat();
930 }
931
932 // get the current page
933 var paged;
934 if (pagesize) {
935 if (filteredItems.length <= pagenum * pagesize) {
936 if (filteredItems.length === 0) {
937 pagenum = 0;
938 } else {
939 pagenum = Math.floor((filteredItems.length - 1) / pagesize);
940 }
941 }
942 paged = filteredItems.slice(pagesize * pagenum, pagesize * pagenum + pagesize);
943 } else {
944 paged = filteredItems;
945 }
946 return { totalRows: filteredItems.length, rows: paged };
947 }
948
949 function getRowDiffs(rows, newRows) {
950 var item, r, eitherIsNonData, diff = [];
951 var from = 0, to = Math.max(newRows.length, rows.length);
952
953 if (refreshHints && refreshHints.ignoreDiffsBefore) {
954 from = Math.max(0,
955 Math.min(newRows.length, refreshHints.ignoreDiffsBefore));
956 }
957
958 if (refreshHints && refreshHints.ignoreDiffsAfter) {
959 to = Math.min(newRows.length,
960 Math.max(0, refreshHints.ignoreDiffsAfter));
961 }
962
963 for (var i = from, rl = rows.length; i < to; i++) {
964 if (i >= rl) {
965 diff[diff.length] = i;
966 } else {
967 item = newRows[i];
968 r = rows[i];
969
970 if (!item || (groupingInfos.length && (eitherIsNonData = (item.__nonDataRow) || (r.__nonDataRow)) &&
971 item.__group !== r.__group ||
972 item.__group && !item.equals(r))
973 || (eitherIsNonData &&
974 // no good way to compare totals since they are arbitrary DTOs
975 // deep object comparison is pretty expensive
976 // always considering them 'dirty' seems easier for the time being
977 (item.__groupTotals || r.__groupTotals))
978 || item[idProperty] != r[idProperty]
979 || (updated && updated[item[idProperty]])
980 ) {
981 diff[diff.length] = i;
982 }
983 }
984 }
985 return diff;
986 }
987
988 function recalc(_items) {
989 rowsById = null;
990
991 if (refreshHints.isFilterNarrowing != prevRefreshHints.isFilterNarrowing ||
992 refreshHints.isFilterExpanding != prevRefreshHints.isFilterExpanding) {
993 filterCache = [];
994 }
995
996 var filteredItems = getFilteredAndPagedItems(_items);
997 totalRows = filteredItems.totalRows;
998 var newRows = filteredItems.rows;
999
1000 groups = [];
1001 if (groupingInfos.length) {
1002 groups = extractGroups(newRows);
1003 if (groups.length) {
1004 newRows = flattenGroupedRows(groups);
1005 }
1006 }
1007
1008 var diff = getRowDiffs(rows, newRows);
1009
1010 rows = newRows;
1011
1012 return diff;
1013 }
1014
1015 function refresh() {
1016 if (suspend) {
1017 return;
1018 }
1019
1020 var previousPagingInfo = $.extend(true, {}, getPagingInfo());
1021
1022 var countBefore = rows.length;
1023 var totalRowsBefore = totalRows;
1024
1025 var diff = recalc(items, filter); // pass as direct refs to avoid closure perf hit
1026
1027 // if the current page is no longer valid, go to last page and recalc
1028 // we suffer a performance penalty here, but the main loop (recalc) remains highly optimized
1029 if (pagesize && totalRows < pagenum * pagesize) {
1030 pagenum = Math.max(0, Math.ceil(totalRows / pagesize) - 1);
1031 diff = recalc(items, filter);
1032 }
1033
1034 updated = null;
1035 prevRefreshHints = refreshHints;
1036 refreshHints = {};
1037
1038 if (totalRowsBefore !== totalRows) {
1039 onBeforePagingInfoChanged.notify(previousPagingInfo, null, self); // use the previously saved paging info
1040 onPagingInfoChanged.notify(getPagingInfo(), null, self);
1041 }
1042 if (countBefore !== rows.length) {
1043 onRowCountChanged.notify({ previous: countBefore, current: rows.length, dataView: self, callingOnRowsChanged: (diff.length > 0) }, null, self);
1044 }
1045 if (diff.length > 0) {
1046 onRowsChanged.notify({ rows: diff, dataView: self, calledOnRowCountChanged: (countBefore !== rows.length) }, null, self);
1047 }
1048 if (countBefore !== rows.length || diff.length > 0) {
1049 onRowsOrCountChanged.notify({
1050 rowsDiff: diff, previousRowCount: countBefore, currentRowCount: rows.length,
1051 rowCountChanged: countBefore !== rows.length, rowsChanged: diff.length > 0, dataView: self
1052 }, null, self);
1053 }
1054 }
1055
1056 /***
1057 * Wires the grid and the DataView together to keep row selection tied to item ids.
1058 * This is useful since, without it, the grid only knows about rows, so if the items
1059 * move around, the same rows stay selected instead of the selection moving along
1060 * with the items.
1061 *
1062 * NOTE: This doesn't work with cell selection model.
1063 *
1064 * @param grid {Slick.Grid} The grid to sync selection with.
1065 * @param preserveHidden {Boolean} Whether to keep selected items that go out of the
1066 * view due to them getting filtered out.
1067 * @param preserveHiddenOnSelectionChange {Boolean} Whether to keep selected items
1068 * that are currently out of the view (see preserveHidden) as selected when selection
1069 * changes.
1070 * @return {Slick.Event} An event that notifies when an internal list of selected row ids
1071 * changes. This is useful since, in combination with the above two options, it allows
1072 * access to the full list selected row ids, and not just the ones visible to the grid.
1073 * @method syncGridSelection
1074 */
1075 function syncGridSelection(grid, preserveHidden, preserveHiddenOnSelectionChange) {
1076 var self = this;
1077 var inHandler;
1078 var selectedRowIds = self.mapRowsToIds(grid.getSelectedRows());
1079 var onSelectedRowIdsChanged = new Slick.Event();
1080
1081 function setSelectedRowIds(rowIds) {
1082 if (selectedRowIds.join(",") == rowIds.join(",")) {
1083 return;
1084 }
1085
1086 selectedRowIds = rowIds;
1087
1088 onSelectedRowIdsChanged.notify({
1089 "grid": grid,
1090 "ids": selectedRowIds,
1091 "dataView": self
1092 }, new Slick.EventData(), self);
1093 }
1094
1095 function update() {
1096 if (selectedRowIds.length > 0) {
1097 inHandler = true;
1098 var selectedRows = self.mapIdsToRows(selectedRowIds);
1099 if (!preserveHidden) {
1100 setSelectedRowIds(self.mapRowsToIds(selectedRows));
1101 }
1102 grid.setSelectedRows(selectedRows);
1103 inHandler = false;
1104 }
1105 }
1106
1107 grid.onSelectedRowsChanged.subscribe(function (e, args) {
1108 if (inHandler) { return; }
1109 var newSelectedRowIds = self.mapRowsToIds(grid.getSelectedRows());
1110 if (!preserveHiddenOnSelectionChange || !grid.getOptions().multiSelect) {
1111 setSelectedRowIds(newSelectedRowIds);
1112 } else {
1113 // keep the ones that are hidden
1114 var existing = $.grep(selectedRowIds, function (id) { return self.getRowById(id) === undefined; });
1115 // add the newly selected ones
1116 setSelectedRowIds(existing.concat(newSelectedRowIds));
1117 }
1118 });
1119
1120 this.onRowsOrCountChanged.subscribe(update);
1121
1122 return onSelectedRowIdsChanged;
1123 }
1124
1125 function syncGridCellCssStyles(grid, key) {
1126 var hashById;
1127 var inHandler;
1128
1129 // since this method can be called after the cell styles have been set,
1130 // get the existing ones right away
1131 storeCellCssStyles(grid.getCellCssStyles(key));
1132
1133 function storeCellCssStyles(hash) {
1134 hashById = {};
1135 for (var row in hash) {
1136 var id = rows[row][idProperty];
1137 hashById[id] = hash[row];
1138 }
1139 }
1140
1141 function update() {
1142 if (hashById) {
1143 inHandler = true;
1144 ensureRowsByIdCache();
1145 var newHash = {};
1146 for (var id in hashById) {
1147 var row = rowsById[id];
1148 if (row != undefined) {
1149 newHash[row] = hashById[id];
1150 }
1151 }
1152 grid.setCellCssStyles(key, newHash);
1153 inHandler = false;
1154 }
1155 }
1156
1157 grid.onCellCssStylesChanged.subscribe(function (e, args) {
1158 if (inHandler) { return; }
1159 if (key != args.key) { return; }
1160 if (args.hash) {
1161 storeCellCssStyles(args.hash);
1162 } else {
1163 grid.onCellCssStylesChanged.unsubscribe();
1164 self.onRowsOrCountChanged.unsubscribe(update);
1165 }
1166 });
1167
1168 this.onRowsOrCountChanged.subscribe(update);
1169 }
1170
1171 $.extend(this, {
1172 // methods
1173 "beginUpdate": beginUpdate,
1174 "endUpdate": endUpdate,
1175 "setPagingOptions": setPagingOptions,
1176 "getPagingInfo": getPagingInfo,
1177 "getIdPropertyName": getIdPropertyName,
1178 "getItems": getItems,
1179 "setItems": setItems,
1180 "setFilter": setFilter,
1181 "getFilter": getFilter,
1182 "getFilteredItems": getFilteredItems,
1183 "sort": sort,
1184 "fastSort": fastSort,
1185 "reSort": reSort,
1186 "setGrouping": setGrouping,
1187 "getGrouping": getGrouping,
1188 "groupBy": groupBy,
1189 "setAggregators": setAggregators,
1190 "collapseAllGroups": collapseAllGroups,
1191 "expandAllGroups": expandAllGroups,
1192 "collapseGroup": collapseGroup,
1193 "expandGroup": expandGroup,
1194 "getGroups": getGroups,
1195 "getIdxById": getIdxById,
1196 "getRowByItem": getRowByItem,
1197 "getRowById": getRowById,
1198 "getItemById": getItemById,
1199 "getItemByIdx": getItemByIdx,
1200 "mapItemsToRows": mapItemsToRows,
1201 "mapRowsToIds": mapRowsToIds,
1202 "mapIdsToRows": mapIdsToRows,
1203 "setRefreshHints": setRefreshHints,
1204 "setFilterArgs": setFilterArgs,
1205 "refresh": refresh,
1206 "updateItem": updateItem,
1207 "insertItem": insertItem,
1208 "addItem": addItem,
1209 "deleteItem": deleteItem,
1210 "sortedAddItem": sortedAddItem,
1211 "sortedUpdateItem": sortedUpdateItem,
1212 "syncGridSelection": syncGridSelection,
1213 "syncGridCellCssStyles": syncGridCellCssStyles,
1214
1215 // data provider methods
1216 "getLength": getLength,
1217 "getItem": getItem,
1218 "getItemMetadata": getItemMetadata,
1219
1220 // events
1221 "onSetItemsCalled": onSetItemsCalled,
1222 "onRowCountChanged": onRowCountChanged,
1223 "onRowsChanged": onRowsChanged,
1224 "onRowsOrCountChanged": onRowsOrCountChanged,
1225 "onBeforePagingInfoChanged": onBeforePagingInfoChanged,
1226 "onPagingInfoChanged": onPagingInfoChanged,
1227 "onGroupExpanded": onGroupExpanded,
1228 "onGroupCollapsed": onGroupCollapsed
1229 });
1230 }
1231
1232 function AvgAggregator(field) {
1233 this.field_ = field;
1234
1235 this.init = function () {
1236 this.count_ = 0;
1237 this.nonNullCount_ = 0;
1238 this.sum_ = 0;
1239 };
1240
1241 this.accumulate = function (item) {
1242 var val = item[this.field_];
1243 this.count_++;
1244 if (val != null && val !== "" && !isNaN(val)) {
1245 this.nonNullCount_++;
1246 this.sum_ += parseFloat(val);
1247 }
1248 };
1249
1250 this.storeResult = function (groupTotals) {
1251 if (!groupTotals.avg) {
1252 groupTotals.avg = {};
1253 }
1254 if (this.nonNullCount_ !== 0) {
1255 groupTotals.avg[this.field_] = this.sum_ / this.nonNullCount_;
1256 }
1257 };
1258 }
1259
1260 function MinAggregator(field) {
1261 this.field_ = field;
1262
1263 this.init = function () {
1264 this.min_ = null;
1265 };
1266
1267 this.accumulate = function (item) {
1268 var val = item[this.field_];
1269 if (val != null && val !== "" && !isNaN(val)) {
1270 if (this.min_ == null || val < this.min_) {
1271 this.min_ = val;
1272 }
1273 }
1274 };
1275
1276 this.storeResult = function (groupTotals) {
1277 if (!groupTotals.min) {
1278 groupTotals.min = {};
1279 }
1280 groupTotals.min[this.field_] = this.min_;
1281 };
1282 }
1283
1284 function MaxAggregator(field) {
1285 this.field_ = field;
1286
1287 this.init = function () {
1288 this.max_ = null;
1289 };
1290
1291 this.accumulate = function (item) {
1292 var val = item[this.field_];
1293 if (val != null && val !== "" && !isNaN(val)) {
1294 if (this.max_ == null || val > this.max_) {
1295 this.max_ = val;
1296 }
1297 }
1298 };
1299
1300 this.storeResult = function (groupTotals) {
1301 if (!groupTotals.max) {
1302 groupTotals.max = {};
1303 }
1304 groupTotals.max[this.field_] = this.max_;
1305 };
1306 }
1307
1308 function SumAggregator(field) {
1309 this.field_ = field;
1310
1311 this.init = function () {
1312 this.sum_ = null;
1313 };
1314
1315 this.accumulate = function (item) {
1316 var val = item[this.field_];
1317 if (val != null && val !== "" && !isNaN(val)) {
1318 this.sum_ += parseFloat(val);
1319 }
1320 };
1321
1322 this.storeResult = function (groupTotals) {
1323 if (!groupTotals.sum) {
1324 groupTotals.sum = {};
1325 }
1326 groupTotals.sum[this.field_] = this.sum_;
1327 };
1328 }
1329
1330 function CountAggregator(field) {
1331 this.field_ = field;
1332
1333 this.init = function () {
1334 };
1335
1336 this.storeResult = function (groupTotals) {
1337 if (!groupTotals.count) {
1338 groupTotals.count = {};
1339 }
1340 groupTotals.count[this.field_] = groupTotals.group.rows.length;
1341 };
1342 }
1343
1344 // TODO: add more built-in aggregators
1345 // TODO: merge common aggregators in one to prevent needles iterating
1346
1347})(jQuery);