From 09c85dd81d2971115167194b1c918775f6e295ec Mon Sep 17 00:00:00 2001 From: araza Date: Sun, 2 Apr 2017 12:39:29 -0500 Subject: [PATCH 1/6] add data provider and pager --- ajax-data-provider.js | 81 ++- data-provider.js | 1141 +++++++++++++++++++++++++++++++++++++++++ grid.js | 62 --- index.html | 23 +- pager.css | 41 ++ pager.js | 146 ++++++ 6 files changed, 1398 insertions(+), 96 deletions(-) create mode 100644 data-provider.js delete mode 100644 grid.js create mode 100644 pager.css create mode 100644 pager.js diff --git a/ajax-data-provider.js b/ajax-data-provider.js index 8c4063a..a1aa3b9 100644 --- a/ajax-data-provider.js +++ b/ajax-data-provider.js @@ -6,15 +6,20 @@ */ function AjaxDataProvider() { // private - var perPage = 10; + var pageSize = 10; + var page = 0; + var totalCount = 0; + var data = []; var url = "http://192.168.60.167:3002/api.php"; - var timer = null; - var req = null; // ajax timer + var timeout = null; + var req = null; + var refreshHints = {}; // events var onDataLoading = new Slick.Event(); var onDataLoaded = new Slick.Event(); + var onPagingInfoChanged = new Slick.Event(); function init() {} @@ -28,6 +33,36 @@ return true; } + function setPagingOptions(args) { + if (args.pageSize !== undefined) { + pageSize = args.pageSize; + page = pageSize ? Math.min(page, Math.max(0, Math.ceil(totalCount / pageSize) - 1)) : 0; + } + + if (args.pageNum !== undefined) { + page = Math.min(args.pageNum, Math.max(0, Math.ceil(totalCount / pageSize) - 1)); + } + + onPagingInfoChanged.notify(getPagingInfo(), null, self); + + refresh(); + } + + function getPagingInfo() { + var totalPages = pageSize ? Math.max(1, Math.ceil(totalCount / pageSize)) : 1; + return { + pageSize: pageSize, + pageNum: page, + totalRows: totalCount, + totalPages: totalPages, + dataProvider: this + }; + } + + function setRefreshHints(hints) { + refreshHints = hints; + } + function clear() { for (var key in data) { delete data[key]; @@ -35,52 +70,52 @@ } function getData() { + prepareData(); return data; } - function prepareData(from, to) { + function prepareData() { if (req) { req.abort(); for (var i = req.fromPage; i <= req.toPage; i++) { - delete data[i * perPage]; + delete data[i * pageSize]; } } - if (from < 0) { - from = 0; - } + var from = page; + var to = page * pageSize; if (data.length > 0) { to = Math.min(to, data.length - 1); } - var fromPage = Math.floor(from / perPage); - var toPage = Math.floor(to / perPage); + var fromPage = Math.floor(from / pageSize); + var toPage = Math.floor(to / pageSize); - while (data[fromPage * perPage] !== undefined && fromPage < toPage) + while (data[fromPage * pageSize] !== undefined && fromPage < toPage) fromPage++; - while (data[toPage * perPage] !== undefined && fromPage < toPage) + while (data[toPage * pageSize] !== undefined && fromPage < toPage) toPage--; - if (fromPage > toPage || ((fromPage == toPage) && data[fromPage * perPage] !== undefined)) { + if (fromPage > toPage || ((fromPage == toPage) && data[fromPage * pageSize] !== undefined)) { // TODO: look-ahead onDataLoaded.notify({from: from, to: to}); return; } - var recStart = (fromPage * perPage); - var recCount = (((toPage - fromPage) * perPage) + perPage); + var recStart = (fromPage * pageSize); + var recCount = (((toPage - fromPage) * pageSize) + pageSize); url += "?page=" + recStart + "&count=" + recCount; - if (timer !== null) { - clearTimeout(timer); + if (timeout !== null) { + clearTimeout(timeout); } - timer = setTimeout(function () { + timeout = setTimeout(function () { for (var i = fromPage; i <= toPage; i++) { - data[i * perPage] = null; // null indicates a 'timered but not available yet' + data[i * pageSize] = null; // null indicates a 'timered but not available yet' } onDataLoading.notify({from: from, to: to}); @@ -143,13 +178,15 @@ "isDataLoaded": isDataLoaded, "prepareData": prepareData, "refresh": refresh, + "getPagingInfo": getPagingInfo, + "setRefreshHints": setRefreshHints, // events "onDataLoading": onDataLoading, - "onDataLoaded": onDataLoaded + "onDataLoaded": onDataLoaded, + "onPagingInfoChanged": onPagingInfoChanged }; } - // Slick.RemoteDataProvider - $.extend(true, window, { Flow: { AjaxDataProvider: AjaxDataProvider }}); + $.extend(true, window, { Slick: { AjaxDataProvider: AjaxDataProvider }}); })(jQuery); diff --git a/data-provider.js b/data-provider.js new file mode 100644 index 0000000..ab21b78 --- /dev/null +++ b/data-provider.js @@ -0,0 +1,1141 @@ +(function ($) { + $.extend(true, window, { + Slick: { + DataProvider: DataProvider, + 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 DataProvider(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 "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, 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 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 k = 0, rl = rows.length; k < rl; k++) { + r = rows[k]; + 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 j = 0; j < groups.length; j++) { + group = groups[j]; + 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; + 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]; + + // 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 + if ((groupingInfos.length && (eitherIsNonData = (item.__nonDataRow) || (r.__nonDataRow)) && + item.__group !== r.__group || + item.__group && !item.equals(r)) || + (eitherIsNonData && (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, dataView: self}, null, self); + } + if (diff.length > 0) { + onRowsChanged.notify({rows: diff, dataView: self}, null, self); + } + } + + /*** + * Wires the grid and the DataProvider 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); + } + }); + + 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 !== "" && !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); diff --git a/grid.js b/grid.js deleted file mode 100644 index 6e21475..0000000 --- a/grid.js +++ /dev/null @@ -1,62 +0,0 @@ -function Grid(opts) { - this.el = opts.el; - if (this.el !== null && typeof this.el !== 'object') { - throw new Error("Grid Error: invalid element provided, element does not exist in the DOM."); - } - - this.opts = opts; - this.columns = opts.columns; - this.dataProvider = opts.dataProvider; - - this.init(); -} - -Grid.prototype.init = function() { -}; - -Grid.prototype.render = function() { - var gridPane = document.createElement('div'); - gridPane.className = 'grid-pane'; - gridPane.setAttribute('style', 'height: 500px'); - - var gridViewport = document.createElement('div'); - gridViewport.className = 'grid-viewport'; - gridViewport.setAttribute('style', 'height: 500px; overflow-y: scroll; overflow-x: hidden;'); - - var gridContent = document.createElement('div'); - gridContent.className = 'grid-content'; - gridContent.setAttribute('style', 'position: relative; height: 1000px;'); - - var gridHeader = document.createElement('div'); - gridHeader.className = 'grid-header'; - - var data = this.dataProvider.getData(); - var columns = this.columns; - - for (var i = 0, dataCount = data.length; i < dataCount; i++) { - var row = document.createElement('div'); - row.className = 'grid-row'; - row.setAttribute('style', 'position: absolute; top: ' + i * this.opts.rowHeight + 'px;'); - var rowData = data[i]; - - if (rowData === null || typeof rowData !== 'object') { - continue; - } - - for (var j = 0, columnCount = columns.length; j < columnCount; j++) { - var cell = document.createElement('div'); - cell.innerHTML = rowData[columns[j].id]; - cell.className = 'grid-cell ' + 'grid-cell-' + columns[j].id; - row.appendChild(cell); - } - - gridContent.appendChild(row); - } - - gridViewport.appendChild(gridContent); - gridPane.appendChild(gridViewport); - - this.el.addClass('grid') - .append(gridHeader) - .append(gridPane); -}; diff --git a/index.html b/index.html index 892cd2a..a4a39a9 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,7 @@ + - - - -
-
-
-
- - - - - - - - - - - - - - - - diff --git a/package.json b/package.json index 8773fd2..d89a081 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,24 @@ "dependencies": { "bootstrap": "^3.3.7", "jqGrid": "^5.1.1", + "jquery": "^3.2.1", "slickgrid": "^2.3.3" + }, + "devDependencies": { + "babel-core": "^6.24.0", + "babel-eslint": "^7.2.1", + "babel-preset-es2015": "^6.24.0", + "browser-sync": "^2.18.8", + "eslint-config-airbnb-base": "^11.1.3", + "eslint-config-import": "^0.13.0", + "gulp": "^3.9.1", + "gulp-better-rollup": "^1.1.1", + "gulp-eslint": "^3.0.1", + "gulp-plumber": "^1.1.0", + "gulp-sass": "^3.1.0", + "jquery": "^3.2.1", + "rollup": "^0.41.6", + "rollup-plugin-commonjs": "^8.0.2", + "rollup-plugin-node-resolve": "^3.0.0" } } diff --git a/pager.js b/pager.js deleted file mode 100644 index 84b9777..0000000 --- a/pager.js +++ /dev/null @@ -1,146 +0,0 @@ -(function ($) { - function Pager(dataProvider, grid, $container) { - var $status; - - function init() { - dataProvider.onPagingInfoChanged.subscribe(function (e, pagingInfo) { - updatePager(pagingInfo); - }); - - constructPagerUI(); - updatePager(dataProvider.getPagingInfo()); - } - - function getNavState() { - var cannotLeaveEditMode = !Slick.GlobalEditorLock.commitCurrentEdit(); - var pagingInfo = dataProvider.getPagingInfo(); - var lastPage = pagingInfo.totalPages - 1; - - return { - canGotoFirst: !cannotLeaveEditMode && pagingInfo.pageSize !== 0 && pagingInfo.pageNum > 0, - canGotoLast: !cannotLeaveEditMode && pagingInfo.pageSize !== 0 && pagingInfo.pageNum != lastPage, - canGotoPrev: !cannotLeaveEditMode && pagingInfo.pageSize !== 0 && pagingInfo.pageNum > 0, - canGotoNext: !cannotLeaveEditMode && pagingInfo.pageSize !== 0 && pagingInfo.pageNum < lastPage, - pagingInfo: pagingInfo - }; - } - - function setPageSize(n) { - dataProvider.setRefreshHints({ - isFilterUnchanged: true - }); - dataProvider.setPagingOptions({pageSize: n}); - } - - function gotoFirst() { - if (getNavState().canGotoFirst) { - dataProvider.setPagingOptions({pageNum: 0}); - } - } - - function gotoLast() { - var state = getNavState(); - if (state.canGotoLast) { - dataProvider.setPagingOptions({pageNum: state.pagingInfo.totalPages - 1}); - } - } - - function gotoPrev() { - var state = getNavState(); - if (state.canGotoPrev) { - dataProvider.setPagingOptions({pageNum: state.pagingInfo.pageNum - 1}); - } - } - - function gotoNext() { - var state = getNavState(); - if (state.canGotoNext) { - dataProvider.setPagingOptions({pageNum: state.pagingInfo.pageNum + 1}); - } - } - - function constructPagerUI() { - $container.empty(); - - var $nav = $("").appendTo($container); - var $settings = $("").appendTo($container); - $status = $("").appendTo($container); - - $settings - .append(""); - - $settings.find("a[data]").click(function (e) { - var pagesize = $(e.target).attr("data"); - if (pagesize !== undefined) { - if (pagesize === -1) { - var vp = grid.getViewport(); - setPageSize(vp.bottom - vp.top); - } else { - setPageSize(parseInt(pagesize)); - } - } - }); - - var icon_prefix = ""; - - $(icon_prefix + "ui-icon-lightbulb" + icon_suffix) - .click(function () { - $(".slick-pager-settings-expanded").toggle(); - }) - .appendTo($settings); - - $(icon_prefix + "ui-icon-seek-first" + icon_suffix) - .click(gotoFirst) - .appendTo($nav); - - $(icon_prefix + "ui-icon-seek-prev" + icon_suffix) - .click(gotoPrev) - .appendTo($nav); - - $(icon_prefix + "ui-icon-seek-next" + icon_suffix) - .click(gotoNext) - .appendTo($nav); - - $(icon_prefix + "ui-icon-seek-end" + icon_suffix) - .click(gotoLast) - .appendTo($nav); - - $container.find(".ui-icon-container") - .hover(function () { - $(this).toggleClass("ui-state-hover"); - }); - - $container.children().wrapAll("
"); - } - - - function updatePager(pagingInfo) { - var state = getNavState(); - - $container.find(".slick-pager-nav span").removeClass("ui-state-disabled"); - if (!state.canGotoFirst) { - $container.find(".ui-icon-seek-first").addClass("ui-state-disabled"); - } - if (!state.canGotoLast) { - $container.find(".ui-icon-seek-end").addClass("ui-state-disabled"); - } - if (!state.canGotoNext) { - $container.find(".ui-icon-seek-next").addClass("ui-state-disabled"); - } - if (!state.canGotoPrev) { - $container.find(".ui-icon-seek-prev").addClass("ui-state-disabled"); - } - - if (pagingInfo.pageSize === 0) { - $status.text("Showing all " + pagingInfo.totalRows + " rows"); - } else { - $status.text("Showing page " + (pagingInfo.pageNum + 1) + " of " + pagingInfo.totalPages); - } - } - - init(); - } - - $.extend(true, window, { Slick:{ Pager: Pager }}); -})(jQuery); diff --git a/api.php b/src/api.php similarity index 100% rename from api.php rename to src/api.php diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..7034717 --- /dev/null +++ b/src/index.html @@ -0,0 +1,54 @@ + + + + Listview Example + + + + + + + + + + +
+
+
+
+ + + + + + + + + + + + + diff --git a/src/js/ajax-data-provider.js b/src/js/ajax-data-provider.js new file mode 100644 index 0000000..bdaf716 --- /dev/null +++ b/src/js/ajax-data-provider.js @@ -0,0 +1,158 @@ +/* global Slick, $ */ + +/** + * A sample AJAX data store implementation. + * Right now, it's hooked up to load Hackernews stories, but can + * easily be extended to support any JSONP-compatible backend that accepts paging parameters. + */ +function AjaxDataProvider() { + // private + var pageSize = 10; + var page = 0; + var totalCount = 0; + var data = []; + var url = 'http://192.168.60.167:3002/api.php'; + var timeout; + var request; + var onDataLoaded = new Slick.Event(); + var onDataLoading = new Slick.Event(); + var onPaginationUpdated = new Slick.Event(); + var refreshHints = {}; + + function init() {} + + function setRefreshHints(hints) { + refreshHints = hints; + } + + function getRefreshHints() { + return refreshHints; + } + + function isDataLoaded(from, to) { + return data.slice(from, to) + .filter(i => i).length > 0; + } + + function clearData() { + data.splice(page, (page * pageSize)); + } + + function onError() { + throw new Error('Could not load page: ' + [page, page * pageSize].join('-')); + } + + function onSuccess(resp) { + var from = page * pageSize; + var to = from; + if (resp.data.length > 0) { + to = from + resp.data.length; + data.concat(resp.data); + } + + request = null; + onDataLoaded.notify({ from: from, to: to }); + } + + function prepareData() { + var from = page; + var to = page * pageSize; + + if (request) { + request.abort(); + data.splice(from, to); + } + + if (data.length > 0) { + to = Math.min(to, data.length - 1); + } + + url += '?page=' + page + '&per_page=' + pageSize; + + if (timeout !== null) { + clearTimeout(timeout); + } + + timeout = setTimeout(function requestTimeout() { + data.splice(from, to); + + onDataLoading.notify({ from: from, to: to }); + + request = $.ajax({ + url: url, + type: 'get', + dataType: 'json', + cache: true, + success: onSuccess, + error: onError + }); + }, 50); + } + + function getData() { + prepareData(); + return data; + } + + function refresh(from, to) { + clearData(); + getData(from, to); + } + + function getPagination() { + var totalPages = page * pageSize; + return { + page: page, + pageSize: pageSize, + totalCount: totalCount, + totalPages: totalPages + }; + } + + function setPaginationOptions(args) { + var defaultPageSize = Math.max(0, Math.ceil(totalCount / pageSize) - 1); + + if (args.pageSize !== undefined) { + pageSize = args.pageSize; + page = pageSize ? Math.min(page, defaultPageSize) : 0; + } + + if (args.page !== undefined) { + page = Math.min(args.page, defaultPageSize); + } + + onPaginationUpdated.notify(getPagination(), null, self); + + refresh(); + } + + function setPageSize(value) { + pageSize = value; + } + + function getPageSize() { + return pageSize; + } + + init(); + + return { + getData: getData, + isDataLoaded: isDataLoaded, + prepareData: prepareData, + refresh: refresh, + setPageSize: setPageSize, + getPageSize: getPageSize, + getPagination: getPagination, + setPaginationOptions: setPaginationOptions, + setRefreshHints: setRefreshHints, + getRefreshHints: getRefreshHints, + + onDataLoaded: onDataLoaded, + onDataLoading: onDataLoading, + onPaginationUpdated: onPaginationUpdated + }; +} + +export default AjaxDataProvider; + diff --git a/src/js/core.js b/src/js/core.js new file mode 100644 index 0000000..d818748 --- /dev/null +++ b/src/js/core.js @@ -0,0 +1,462 @@ +/* eslint-disable */ + +/*** + * A global singleton editor lock. + * @class GlobalEditorLock + * @static + */ +export const GlobalEditorLock = new EditorLock(); + +export const keyCode = { + BACKSPACE: 8, + DELETE: 46, + DOWN: 40, + END: 35, + ENTER: 13, + ESCAPE: 27, + HOME: 36, + INSERT: 45, + LEFT: 37, + PAGE_DOWN: 34, + PAGE_UP: 33, + RIGHT: 39, + TAB: 9, + UP: 38 +}; + +/*** + * An event object for passing data to event handlers and letting them control propagation. + *

This is pretty much identical to how W3C and jQuery implement events.

+ * @class EventData + * @constructor + */ +export function EventData() { + var isPropagationStopped = false; + var isImmediatePropagationStopped = false; + + /*** + * Stops event from propagating up the DOM tree. + * @method stopPropagation + */ + this.stopPropagation = function () { + isPropagationStopped = true; + }; + + /*** + * Returns whether stopPropagation was called on this event object. + * @method isPropagationStopped + * @return {Boolean} + */ + this.isPropagationStopped = function () { + return isPropagationStopped; + }; + + /*** + * Prevents the rest of the handlers from being executed. + * @method stopImmediatePropagation + */ + this.stopImmediatePropagation = function () { + isImmediatePropagationStopped = true; + }; + + /*** + * Returns whether stopImmediatePropagation was called on this event object.\ + * @method isImmediatePropagationStopped + * @return {Boolean} + */ + this.isImmediatePropagationStopped = function () { + return isImmediatePropagationStopped; + }; +} + +/*** + * A simple publisher-subscriber implementation. + * @class Event + * @constructor + */ +export function Event() { + var handlers = []; + + /*** + * Adds an event handler to be called when the event is fired. + *

Event handler will receive two arguments - an EventData and the data + * object the event was fired with.

+ * @method subscribe + * @param fn {Function} Event handler. + */ + this.subscribe = function (fn) { + handlers.push(fn); + }; + + /*** + * Removes an event handler added with subscribe(fn). + * @method unsubscribe + * @param fn {Function} Event handler to be removed. + */ + this.unsubscribe = function (fn) { + for (var i = handlers.length - 1; i >= 0; i--) { + if (handlers[i] === fn) { + handlers.splice(i, 1); + } + } + }; + + /*** + * Fires an event notifying all subscribers. + * @method notify + * @param args {Object} Additional data object to be passed to all handlers. + * @param e {EventData} + * Optional. + * An EventData object to be passed to all handlers. + * For DOM events, an existing W3C/jQuery event object can be passed in. + * @param scope {Object} + * Optional. + * The scope ("this") within which the handler will be executed. + * If not specified, the scope will be set to the Event instance. + */ + this.notify = function (args, e, scope) { + e = e || new EventData(); + scope = scope || this; + + var returnValue; + for (var i = 0; i < handlers.length && !(e.isPropagationStopped() || e.isImmediatePropagationStopped()); i++) { + returnValue = handlers[i].call(scope, e, args); + } + + return returnValue; + }; +} + +export function EventHandler() { + var handlers = []; + + this.subscribe = function (event, handler) { + handlers.push({ + event: event, + handler: handler + }); + event.subscribe(handler); + + return this; // allow chaining + }; + + this.unsubscribe = function (event, handler) { + var i = handlers.length; + while (i--) { + if (handlers[i].event === event && + handlers[i].handler === handler) { + handlers.splice(i, 1); + event.unsubscribe(handler); + return; + } + } + + return this; // allow chaining + }; + + this.unsubscribeAll = function () { + var i = handlers.length; + while (i--) { + handlers[i].event.unsubscribe(handlers[i].handler); + } + handlers = []; + + return this; // allow chaining + } +} + +/*** + * A structure containing a range of cells. + * @class Range + * @constructor + * @param fromRow {Integer} Starting row. + * @param fromCell {Integer} Starting cell. + * @param toRow {Integer} Optional. Ending row. Defaults to fromRow. + * @param toCell {Integer} Optional. Ending cell. Defaults to fromCell. + */ +export function Range(fromRow, fromCell, toRow, toCell) { + if (toRow === undefined && toCell === undefined) { + toRow = fromRow; + toCell = fromCell; + } + + /*** + * @property fromRow + * @type {Integer} + */ + this.fromRow = Math.min(fromRow, toRow); + + /*** + * @property fromCell + * @type {Integer} + */ + this.fromCell = Math.min(fromCell, toCell); + + /*** + * @property toRow + * @type {Integer} + */ + this.toRow = Math.max(fromRow, toRow); + + /*** + * @property toCell + * @type {Integer} + */ + this.toCell = Math.max(fromCell, toCell); + + /*** + * Returns whether a range represents a single row. + * @method isSingleRow + * @return {Boolean} + */ + this.isSingleRow = function () { + return this.fromRow == this.toRow; + }; + + /*** + * Returns whether a range represents a single cell. + * @method isSingleCell + * @return {Boolean} + */ + this.isSingleCell = function () { + return this.fromRow == this.toRow && this.fromCell == this.toCell; + }; + + /*** + * Returns whether a range contains a given cell. + * @method contains + * @param row {Integer} + * @param cell {Integer} + * @return {Boolean} + */ + this.contains = function (row, cell) { + return row >= this.fromRow && row <= this.toRow && + cell >= this.fromCell && cell <= this.toCell; + }; + + /*** + * Returns a readable representation of a range. + * @method toString + * @return {String} + */ + this.toString = function () { + if (this.isSingleCell()) { + return "(" + this.fromRow + ":" + this.fromCell + ")"; + } + else { + return "(" + this.fromRow + ":" + this.fromCell + " - " + this.toRow + ":" + this.toCell + ")"; + } + } +} + + +/*** + * A base class that all special / non-data rows (like Group and GroupTotals) derive from. + * @class NonDataItem + * @constructor + */ +export function NonDataItem() { + this.__nonDataRow = true; +} + + +/*** + * Information about a group of rows. + * @class Group + * @extends Slick.NonDataItem + * @constructor + */ +export function Group() { + this.__group = true; + + /** + * Grouping level, starting with 0. + * @property level + * @type {Number} + */ + this.level = 0; + + /*** + * Number of rows in the group. + * @property count + * @type {Integer} + */ + this.count = 0; + + /*** + * Grouping value. + * @property value + * @type {Object} + */ + this.value = null; + + /*** + * Formatted display value of the group. + * @property title + * @type {String} + */ + this.title = null; + + /*** + * Whether a group is collapsed. + * @property collapsed + * @type {Boolean} + */ + this.collapsed = false; + + /*** + * GroupTotals, if any. + * @property totals + * @type {GroupTotals} + */ + this.totals = null; + + /** + * Rows that are part of the group. + * @property rows + * @type {Array} + */ + this.rows = []; + + /** + * Sub-groups that are part of the group. + * @property groups + * @type {Array} + */ + this.groups = null; + + /** + * A unique key used to identify the group. This key can be used in calls to DataView + * collapseGroup() or expandGroup(). + * @property groupingKey + * @type {Object} + */ + this.groupingKey = null; +} + +Group.prototype = new NonDataItem(); + +/*** + * Compares two Group instances. + * @method equals + * @return {Boolean} + * @param group {Group} Group instance to compare to. + */ +Group.prototype.equals = function (group) { + return this.value === group.value && + this.count === group.count && + this.collapsed === group.collapsed && + this.title === group.title; +}; + +/*** + * Information about group totals. + * An instance of GroupTotals will be created for each totals row and passed to the aggregators + * so that they can store arbitrary data in it. That data can later be accessed by group totals + * formatters during the display. + * @class GroupTotals + * @extends Slick.NonDataItem + * @constructor + */ +export function GroupTotals() { + this.__groupTotals = true; + + /*** + * Parent Group. + * @param group + * @type {Group} + */ + this.group = null; + + /*** + * Whether the totals have been fully initialized / calculated. + * Will be set to false for lazy-calculated group totals. + * @param initialized + * @type {Boolean} + */ + this.initialized = false; +} + +GroupTotals.prototype = new NonDataItem(); + +/*** + * A locking helper to track the active edit controller and ensure that only a single controller + * can be active at a time. This prevents a whole class of state and validation synchronization + * issues. An edit controller (such as SlickGrid) can query if an active edit is in progress + * and attempt a commit or cancel before proceeding. + * @class EditorLock + * @constructor + */ +export function EditorLock() { + var activeEditController = null; + + /*** + * Returns true if a specified edit controller is active (has the edit lock). + * If the parameter is not specified, returns true if any edit controller is active. + * @method isActive + * @param editController {EditController} + * @return {Boolean} + */ + this.isActive = function (editController) { + return (editController ? activeEditController === editController : activeEditController !== null); + }; + + /*** + * Sets the specified edit controller as the active edit controller (acquire edit lock). + * If another edit controller is already active, and exception will be thrown. + * @method activate + * @param editController {EditController} edit controller acquiring the lock + */ + this.activate = function (editController) { + if (editController === activeEditController) { // already activated? + return; + } + if (activeEditController !== null) { + throw "SlickGrid.EditorLock.activate: an editController is still active, can't activate another editController"; + } + if (!editController.commitCurrentEdit) { + throw "SlickGrid.EditorLock.activate: editController must implement .commitCurrentEdit()"; + } + if (!editController.cancelCurrentEdit) { + throw "SlickGrid.EditorLock.activate: editController must implement .cancelCurrentEdit()"; + } + activeEditController = editController; + }; + + /*** + * Unsets the specified edit controller as the active edit controller (release edit lock). + * If the specified edit controller is not the active one, an exception will be thrown. + * @method deactivate + * @param editController {EditController} edit controller releasing the lock + */ + this.deactivate = function (editController) { + if (activeEditController !== editController) { + throw "SlickGrid.EditorLock.deactivate: specified editController is not the currently active one"; + } + activeEditController = null; + }; + + /*** + * Attempts to commit the current edit by calling "commitCurrentEdit" method on the active edit + * controller and returns whether the commit attempt was successful (commit may fail due to validation + * errors, etc.). Edit controller's "commitCurrentEdit" must return true if the commit has succeeded + * and false otherwise. If no edit controller is active, returns true. + * @method commitCurrentEdit + * @return {Boolean} + */ + this.commitCurrentEdit = function () { + return (activeEditController ? activeEditController.commitCurrentEdit() : true); + }; + + /*** + * Attempts to cancel the current edit by calling "cancelCurrentEdit" method on the active edit + * controller and returns whether the edit was successfully cancelled. If no edit controller is + * active, returns true. + * @method cancelCurrentEdit + * @return {Boolean} + */ + this.cancelCurrentEdit = function cancelCurrentEdit() { + return (activeEditController ? activeEditController.cancelCurrentEdit() : true); + }; +} + diff --git a/src/js/data-provider.js b/src/js/data-provider.js new file mode 100644 index 0000000..c81952f --- /dev/null +++ b/src/js/data-provider.js @@ -0,0 +1,1133 @@ +/* eslint-disable */ +/*** + * 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 DataProvider(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 "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, 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 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 k = 0, rl = rows.length; k < rl; k++) { + r = rows[k]; + 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 j = 0; j < groups.length; j++) { + group = groups[j]; + 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; + 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]; + + // 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 + if ((groupingInfos.length && (eitherIsNonData = (item.__nonDataRow) || (r.__nonDataRow)) && + item.__group !== r.__group || + item.__group && !item.equals(r)) || + (eitherIsNonData && (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, dataView: self}, null, self); + } + if (diff.length > 0) { + onRowsChanged.notify({rows: diff, dataView: self}, null, self); + } + } + + /*** + * Wires the grid and the DataProvider 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); + } + }); + + 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 !== "" && !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_; + }; +} + +export const Aggregators = { + Avg: AvgAggregator, + Min: MinAggregator, + Max: MaxAggregator, + Sum: SumAggregator +}; + +export default DataProvider; + diff --git a/src/js/grid.js b/src/js/grid.js new file mode 100644 index 0000000..e960063 --- /dev/null +++ b/src/js/grid.js @@ -0,0 +1,3685 @@ +/* eslint-disable */ +/* global: Event, EventData */ + +// make sure required JavaScript modules are loaded +if (typeof jQuery === "undefined") { + throw "Grid requires jquery module to be loaded"; +} +if (!jQuery.fn.drag) { + throw "Grid requires jquery.event.drag module to be loaded"; +} +if (typeof Slick === "undefined") { + throw "slick.core.js not loaded"; +} + + +// shared across all grids on the page +var scrollbarDimensions; +var maxSupportedCssHeight; // browser's breaking point + +////////////////////////////////////////////////////////////////////////////////////////////// +// Grid class implementation (available as Slick.Grid) + +/** + * Creates a new instance of the grid. + * @class Grid + * @constructor + * @param {Node} container Container node to create the grid in. + * @param {Array,Object} data An array of objects for databinding. + * @param {Array} columns An array of column definitions. + * @param {Object} options Grid options. + **/ +function Grid(container, data, columns, options) { + // settings + var defaults = { + explicitInitialization: false, + rowHeight: 25, + defaultColumnWidth: 80, + enableAddRow: false, + leaveSpaceForNewRows: false, + editable: false, + autoEdit: true, + enableCellNavigation: true, + enableColumnReorder: true, + asyncEditorLoading: false, + asyncEditorLoadDelay: 100, + forceFitColumns: false, + enableAsyncPostRender: false, + asyncPostRenderDelay: 50, + enableAsyncPostRenderCleanup: false, + asyncPostRenderCleanupDelay: 40, + autoHeight: false, + editorLock: Slick.GlobalEditorLock, + showHeaderRow: false, + headerRowHeight: 25, + createFooterRow: false, + showFooterRow: false, + footerRowHeight: 25, + showTopPanel: false, + topPanelHeight: 25, + formatterFactory: null, + editorFactory: null, + cellFlashingCssClass: "flashing", + selectedCellCssClass: "selected", + multiSelect: true, + enableTextSelectionOnCells: false, + dataItemColumnValueExtractor: null, + fullWidthRows: false, + multiColumnSort: false, + defaultFormatter: defaultFormatter, + forceSyncScrolling: false, + addNewRowCssClass: "new-row", + preserveCopiedSelectionOnPaste: false + }; + + var columnDefaults = { + name: "", + resizable: true, + sortable: false, + minWidth: 30, + rerenderOnResize: false, + headerCssClass: null, + defaultSortAsc: true, + focusable: true, + selectable: true + }; + + // scroller + var th; // virtual height + var h; // real scrollable height + var ph; // page height + var n; // number of pages + var cj; // "jumpiness" coefficient + + var page = 0; // current page + var offset = 0; // current page offset + var vScrollDir = 1; + + // private + var initialized = false; + var $container; + var uid = "slickgrid_" + Math.round(1000000 * Math.random()); + var self = this; + var $focusSink, $focusSink2; + var $headerScroller; + var $headers; + var $headerRow, $headerRowScroller, $headerRowSpacer; + var $footerRow, $footerRowScroller, $footerRowSpacer; + var $topPanelScroller; + var $topPanel; + var $viewport; + var $canvas; + var $style; + var $boundAncestors; + var stylesheet, columnCssRulesL, columnCssRulesR; + var viewportH, viewportW; + var canvasWidth; + var viewportHasHScroll, viewportHasVScroll; + var headerColumnWidthDiff = 0, headerColumnHeightDiff = 0, // border+padding + cellWidthDiff = 0, cellHeightDiff = 0, jQueryNewWidthBehaviour = false; + var absoluteColumnMinWidth; + + var tabbingDirection = 1; + var activePosX; + var activeRow, activeCell; + var activeCellNode = null; + var currentEditor = null; + var serializedEditorValue; + var editController; + + var rowsCache = {}; + var renderedRows = 0; + var numVisibleRows; + var prevScrollTop = 0; + var scrollTop = 0; + var lastRenderedScrollTop = 0; + var lastRenderedScrollLeft = 0; + var prevScrollLeft = 0; + var scrollLeft = 0; + + var selectionModel; + var selectedRows = []; + + var plugins = []; + var cellCssClasses = {}; + + var columnsById = {}; + var sortColumns = []; + var columnPosLeft = []; + var columnPosRight = []; + + var pagingActive = false; + var pagingIsLastPage = false; + + // async call handles + var h_editorLoader = null; + var h_render = null; + var h_postrender = null; + var h_postrenderCleanup = null; + var postProcessedRows = {}; + var postProcessToRow = null; + var postProcessFromRow = null; + var postProcessedCleanupQueue = []; + var postProcessgroupId = 0; + + // perf counters + var counter_rows_rendered = 0; + var counter_rows_removed = 0; + + // These two variables work around a bug with inertial scrolling in Webkit/Blink on Mac. + // See http://crbug.com/312427. + var rowNodeFromLastMouseWheelEvent; // this node must not be deleted while inertial scrolling + var zombieRowNodeFromLastMouseWheelEvent; // node that was hidden instead of getting deleted + var zombieRowCacheFromLastMouseWheelEvent; // row cache for above node + var zombieRowPostProcessedFromLastMouseWheelEvent; // post processing references for above node + + // store css attributes if display:none is active in container or parent + var cssShow = { position: 'absolute', visibility: 'hidden', display: 'block' }; + var $hiddenParents; + var oldProps = []; + + ////////////////////////////////////////////////////////////////////////////////////////////// + // Initialization + + function init() { + $container = $(container); + if ($container.length < 1) { + throw new Error("Grid requires a valid container, " + container + " does not exist in the DOM."); + } + + cacheCssForHiddenInit(); + + // calculate these only once and share between grid instances + maxSupportedCssHeight = maxSupportedCssHeight || getMaxSupportedCssHeight(); + scrollbarDimensions = scrollbarDimensions || measureScrollbar(); + + options = $.extend({}, defaults, options); + validateAndEnforceOptions(); + columnDefaults.width = options.defaultColumnWidth; + + columnsById = {}; + for (var i = 0; i < columns.length; i++) { + var m = columns[i] = $.extend({}, columnDefaults, columns[i]); + columnsById[m.id] = i; + if (m.minWidth && m.width < m.minWidth) { + m.width = m.minWidth; + } + if (m.maxWidth && m.width > m.maxWidth) { + m.width = m.maxWidth; + } + } + + // validate loaded JavaScript modules against requested options + if (options.enableColumnReorder && !$.fn.sortable) { + throw new Error("Grid's 'enableColumnReorder = true' option requires jquery-ui.sortable module to be loaded"); + } + + editController = { + "commitCurrentEdit": commitCurrentEdit, + "cancelCurrentEdit": cancelCurrentEdit + }; + + $container + .empty() + .css("overflow", "hidden") + .css("outline", 0) + .addClass(uid) + .addClass("ui-widget"); + + // set up a positioning container if needed + if (!/relative|absolute|fixed/.test($container.css("position"))) { + $container.css("position", "relative"); + } + + $focusSink = $("

").appendTo($container); + + $headerScroller = $("
").appendTo($container); + $headers = $("
").appendTo($headerScroller); + $headers.width(getHeadersWidth()); + + $headerRowScroller = $("
").appendTo($container); + $headerRow = $("
").appendTo($headerRowScroller); + $headerRowSpacer = $("
") + .css("width", getCanvasWidth() + scrollbarDimensions.width + "px") + .appendTo($headerRowScroller); + + $topPanelScroller = $("
").appendTo($container); + $topPanel = $("
").appendTo($topPanelScroller); + + if (!options.showTopPanel) { + $topPanelScroller.hide(); + } + + if (!options.showHeaderRow) { + $headerRowScroller.hide(); + } + + $viewport = $("
").appendTo($container); + $viewport.css("overflow-y", options.autoHeight ? "hidden" : "auto"); + + $canvas = $("
").appendTo($viewport); + + if (options.createFooterRow) { + $footerRowScroller = $("
").appendTo($container); + $footerRow = $("
").appendTo($footerRowScroller); + $footerRowSpacer = $("
") + .css("width", getCanvasWidth() + scrollbarDimensions.width + "px") + .appendTo($footerRowScroller); + + if (!options.showFooterRow) { + $footerRowScroller.hide(); + } + } + + $focusSink2 = $focusSink.clone().appendTo($container); + + if (!options.explicitInitialization) { + finishInitialization(); + } + } + + function finishInitialization() { + if (!initialized) { + initialized = true; + + viewportW = parseFloat($.css($container[0], "width", true)); + + // header columns and cells may have different padding/border skewing width calculations (box-sizing, hello?) + // calculate the diff so we can set consistent sizes + measureCellPaddingAndBorder(); + + // for usability reasons, all text selection in Grid is disabled + // with the exception of input and textarea elements (selection must + // be enabled there so that editors work as expected); note that + // selection in grid cells (grid body) is already unavailable in + // all browsers except IE + disableSelection($headers); // disable all text selection in header (including input and textarea) + + if (!options.enableTextSelectionOnCells) { + // disable text selection in grid cells except in input and textarea elements + // (this is IE-specific, because selectstart event will only fire in IE) + $viewport.on("selectstart.ui", function (event) { + return $(event.target).is("input,textarea"); + }); + } + + updateColumnCaches(); + createColumnHeaders(); + setupColumnSort(); + createCssRules(); + resizeCanvas(); + bindAncestorScrollEvents(); + + $container + .on("resize.slickgrid", resizeCanvas); + $viewport + //.on("click", handleClick) + .on("scroll", handleScroll); + $headerScroller + .on("contextmenu", handleHeaderContextMenu) + .on("click", handleHeaderClick) + .on("mouseenter", ".slick-header-column", handleHeaderMouseEnter) + .on("mouseleave", ".slick-header-column", handleHeaderMouseLeave); + $headerRowScroller + .on("scroll", handleHeaderRowScroll); + + if (options.createFooterRow) { + $footerRowScroller + .on("scroll", handleFooterRowScroll); + } + + $focusSink.add($focusSink2) + .on("keydown", handleKeyDown); + $canvas + .on("keydown", handleKeyDown) + .on("click", handleClick) + .on("dblclick", handleDblClick) + .on("contextmenu", handleContextMenu) + .on("draginit", handleDragInit) + .on("dragstart", {distance: 3}, handleDragStart) + .on("drag", handleDrag) + .on("dragend", handleDragEnd) + .on("mouseenter", ".slick-cell", handleMouseEnter) + .on("mouseleave", ".slick-cell", handleMouseLeave); + + // Work around http://crbug.com/312427. + if (navigator.userAgent.toLowerCase().match(/webkit/) && + navigator.userAgent.toLowerCase().match(/macintosh/)) { + $canvas.on("mousewheel", handleMouseWheel); + } + restoreCssFromHiddenInit(); + } + } + + function cacheCssForHiddenInit() { + // handle display:none on container or container parents + $hiddenParents = $container.parents().addBack().not(':visible'); + $hiddenParents.each(function() { + var old = {}; + for ( var name in cssShow ) { + old[ name ] = this.style[ name ]; + this.style[ name ] = cssShow[ name ]; + } + oldProps.push(old); + }); + } + + function restoreCssFromHiddenInit() { + // finish handle display:none on container or container parents + // - put values back the way they were + $hiddenParents.each(function(i) { + var old = oldProps[i]; + for ( var name in cssShow ) { + this.style[ name ] = old[ name ]; + } + }); + } + + function registerPlugin(plugin) { + plugins.unshift(plugin); + plugin.init(self); + } + + function unregisterPlugin(plugin) { + for (var i = plugins.length; i >= 0; i--) { + if (plugins[i] === plugin) { + if (plugins[i].destroy) { + plugins[i].destroy(); + } + plugins.splice(i, 1); + break; + } + } + } + + function setSelectionModel(model) { + if (selectionModel) { + selectionModel.onSelectedRangesChanged.unsubscribe(handleSelectedRangesChanged); + if (selectionModel.destroy) { + selectionModel.destroy(); + } + } + + selectionModel = model; + if (selectionModel) { + selectionModel.init(self); + selectionModel.onSelectedRangesChanged.subscribe(handleSelectedRangesChanged); + } + } + + function getSelectionModel() { + return selectionModel; + } + + function getCanvasNode() { + return $canvas[0]; + } + + function measureScrollbar() { + var $c = $("
").appendTo("body"); + var dim = { + width: $c.width() - $c[0].clientWidth, + height: $c.height() - $c[0].clientHeight + }; + $c.remove(); + return dim; + } + + function getHeadersWidth() { + var headersWidth = 0; + for (var i = 0, ii = columns.length; i < ii; i++) { + var width = columns[i].width; + headersWidth += width; + } + headersWidth += scrollbarDimensions.width; + return Math.max(headersWidth, viewportW) + 1000; + } + + function getCanvasWidth() { + var availableWidth = viewportHasVScroll ? viewportW - scrollbarDimensions.width : viewportW; + var rowWidth = 0; + var i = columns.length; + while (i--) { + rowWidth += columns[i].width; + } + return options.fullWidthRows ? Math.max(rowWidth, availableWidth) : rowWidth; + } + + function updateCanvasWidth(forceColumnWidthsUpdate) { + var oldCanvasWidth = canvasWidth; + canvasWidth = getCanvasWidth(); + + if (canvasWidth != oldCanvasWidth) { + $canvas.width(canvasWidth); + $headerRow.width(canvasWidth); + if (options.createFooterRow) { $footerRow.width(canvasWidth); } + $headers.width(getHeadersWidth()); + viewportHasHScroll = (canvasWidth > viewportW - scrollbarDimensions.width); + } + + var w=canvasWidth + (viewportHasVScroll ? scrollbarDimensions.width : 0); + $headerRowSpacer.width(w); + if (options.createFooterRow) { $footerRowSpacer.width(w); } + + if (canvasWidth != oldCanvasWidth || forceColumnWidthsUpdate) { + applyColumnWidths(); + } + } + + function disableSelection($target) { + if ($target && $target.jquery) { + $target + .attr("unselectable", "on") + .css("MozUserSelect", "none") + .on("selectstart.ui", function () { + return false; + }); // from jquery:ui.core.js 1.7.2 + } + } + + function getMaxSupportedCssHeight() { + var supportedHeight = 1000000; + // FF reports the height back but still renders blank after ~6M px + var testUpTo = navigator.userAgent.toLowerCase().match(/firefox/) ? 6000000 : 1000000000; + var div = $("
").appendTo(document.body); + + while (true) { + var test = supportedHeight * 2; + div.css("height", test); + if (test > testUpTo || div.height() !== test) { + break; + } else { + supportedHeight = test; + } + } + + div.remove(); + return supportedHeight; + } + + // TODO: this is static. need to handle page mutation. + function bindAncestorScrollEvents() { + var elem = $canvas[0]; + while ((elem = elem.parentNode) != document.body && elem != null) { + // bind to scroll containers only + if (elem == $viewport[0] || elem.scrollWidth != elem.clientWidth || elem.scrollHeight != elem.clientHeight) { + var $elem = $(elem); + if (!$boundAncestors) { + $boundAncestors = $elem; + } else { + $boundAncestors = $boundAncestors.add($elem); + } + $elem.on("scroll." + uid, handleActiveCellPositionChange); + } + } + } + + function unbindAncestorScrollEvents() { + if (!$boundAncestors) { + return; + } + $boundAncestors.off("scroll." + uid); + $boundAncestors = null; + } + + function updateColumnHeader(columnId, title, toolTip) { + if (!initialized) { return; } + var idx = getColumnIndex(columnId); + if (idx == null) { + return; + } + + var columnDef = columns[idx]; + var $header = $headers.children().eq(idx); + if ($header) { + if (title !== undefined) { + columns[idx].name = title; + } + if (toolTip !== undefined) { + columns[idx].toolTip = toolTip; + } + + trigger(self.onBeforeHeaderCellDestroy, { + "node": $header[0], + "column": columnDef, + "grid": self + }); + + $header + .attr("title", toolTip || "") + .children().eq(0).html(title); + + trigger(self.onHeaderCellRendered, { + "node": $header[0], + "column": columnDef, + "grid": self + }); + } + } + + function getHeaderRow() { + return $headerRow[0]; + } + + function getFooterRow() { + return $footerRow[0]; + } + + function getHeaderRowColumn(columnId) { + var idx = getColumnIndex(columnId); + var $header = $headerRow.children().eq(idx); + return $header && $header[0]; + } + + function getFooterRowColumn(columnId) { + var idx = getColumnIndex(columnId); + var $footer = $footerRow.children().eq(idx); + return $footer && $footer[0]; + } + + function createColumnHeaders() { + function onMouseEnter() { + $(this).addClass("ui-state-hover"); + } + + function onMouseLeave() { + $(this).removeClass("ui-state-hover"); + } + + $headers.find(".slick-header-column") + .each(function() { + var columnDef = $(this).data("column"); + if (columnDef) { + trigger(self.onBeforeHeaderCellDestroy, { + "node": this, + "column": columnDef, + "grid": self + }); + } + }); + $headers.empty(); + $headers.width(getHeadersWidth()); + + $headerRow.find(".slick-headerrow-column") + .each(function() { + var columnDef = $(this).data("column"); + if (columnDef) { + trigger(self.onBeforeHeaderRowCellDestroy, { + "node": this, + "column": columnDef, + "grid": self + }); + } + }); + $headerRow.empty(); + + if (options.createFooterRow) { + $footerRow.find(".slick-footerrow-column") + .each(function() { + var columnDef = $(this).data("column"); + if (columnDef) { + trigger(self.onBeforeFooterRowCellDestroy, { + "node": this, + "column": columnDef + }); + } + }); + $footerRow.empty(); + } + + for (var i = 0; i < columns.length; i++) { + var m = columns[i]; + + var header = $("
") + .html("" + m.name + "") + .width(m.width - headerColumnWidthDiff) + .attr("id", "" + uid + m.id) + .attr("title", m.toolTip || "") + .data("column", m) + .addClass(m.headerCssClass || "") + .appendTo($headers); + + if (options.enableColumnReorder || m.sortable) { + header + .on('mouseenter', onMouseEnter) + .on('mouseleave', onMouseLeave); + } + + if (m.sortable) { + header.addClass("slick-header-sortable"); + header.append(""); + } + + trigger(self.onHeaderCellRendered, { + "node": header[0], + "column": m, + "grid": self + }); + + if (options.showHeaderRow) { + var headerRowCell = $("
") + .data("column", m) + .appendTo($headerRow); + + trigger(self.onHeaderRowCellRendered, { + "node": headerRowCell[0], + "column": m, + "grid": self + }); + } + if (options.createFooterRow && options.showFooterRow) { + var footerRowCell = $("
") + .data("column", m) + .appendTo($footerRow); + + trigger(self.onFooterRowCellRendered, { + "node": footerRowCell[0], + "column": m + }); + } + } + + setSortColumns(sortColumns); + setupColumnResize(); + if (options.enableColumnReorder) { + setupColumnReorder(); + } + } + + function setupColumnSort() { + $headers.click(function (e) { + // temporary workaround for a bug in jQuery 1.7.1 (http://bugs.jquery.com/ticket/11328) + e.metaKey = e.metaKey || e.ctrlKey; + + if ($(e.target).hasClass("slick-resizable-handle")) { + return; + } + + var $col = $(e.target).closest(".slick-header-column"); + if (!$col.length) { + return; + } + + var column = $col.data("column"); + if (column.sortable) { + if (!getEditorLock().commitCurrentEdit()) { + return; + } + + var sortOpts = null; + var i = 0; + for (; i < sortColumns.length; i++) { + if (sortColumns[i].columnId == column.id) { + sortOpts = sortColumns[i]; + sortOpts.sortAsc = !sortOpts.sortAsc; + break; + } + } + + if (e.metaKey && options.multiColumnSort) { + if (sortOpts) { + sortColumns.splice(i, 1); + } + } + else { + if ((!e.shiftKey && !e.metaKey) || !options.multiColumnSort) { + sortColumns = []; + } + + if (!sortOpts) { + sortOpts = { columnId: column.id, sortAsc: column.defaultSortAsc }; + sortColumns.push(sortOpts); + } else if (sortColumns.length == 0) { + sortColumns.push(sortOpts); + } + } + + setSortColumns(sortColumns); + + if (!options.multiColumnSort) { + trigger(self.onSort, { + multiColumnSort: false, + sortCol: column, + sortAsc: sortOpts.sortAsc, + grid: self}, e); + } else { + trigger(self.onSort, { + multiColumnSort: true, + sortCols: $.map(sortColumns, function(col) { + return {sortCol: columns[getColumnIndex(col.columnId)], sortAsc: col.sortAsc }; + }), + grid: self}, e); + } + } + }); + } + + function setupColumnReorder() { + $headers.filter(":ui-sortable").sortable("destroy"); + $headers.sortable({ + containment: "parent", + distance: 3, + axis: "x", + cursor: "default", + tolerance: "intersection", + helper: "clone", + placeholder: "slick-sortable-placeholder ui-state-default slick-header-column", + start: function (e, ui) { + ui.placeholder.width(ui.helper.outerWidth() - headerColumnWidthDiff); + $(ui.helper).addClass("slick-header-column-active"); + }, + beforeStop: function (e, ui) { + $(ui.helper).removeClass("slick-header-column-active"); + }, + stop: function (e) { + if (!getEditorLock().commitCurrentEdit()) { + $(this).sortable("cancel"); + return; + } + + var reorderedIds = $headers.sortable("toArray"); + var reorderedColumns = []; + for (var i = 0; i < reorderedIds.length; i++) { + reorderedColumns.push(columns[getColumnIndex(reorderedIds[i].replace(uid, ""))]); + } + setColumns(reorderedColumns); + + trigger(self.onColumnsReordered, {grid: self}); + e.stopPropagation(); + setupColumnResize(); + } + }); + } + + function setupColumnResize() { + var $col, j, c, pageX, columnElements, minPageX, maxPageX, firstResizable, lastResizable; + columnElements = $headers.children(); + columnElements.find(".slick-resizable-handle").remove(); + columnElements.each(function (i, e) { + if (columns[i].resizable) { + if (firstResizable === undefined) { + firstResizable = i; + } + lastResizable = i; + } + }); + if (firstResizable === undefined) { + return; + } + columnElements.each(function (i, e) { + if (i < firstResizable || (options.forceFitColumns && i >= lastResizable)) { + return; + } + $col = $(e); + $("
") + .appendTo(e) + .on("dragstart", function (e, dd) { + if (!getEditorLock().commitCurrentEdit()) { + return false; + } + pageX = e.pageX; + $(this).parent().addClass("slick-header-column-active"); + var shrinkLeewayOnRight = null, stretchLeewayOnRight = null; + // lock each column's width option to current width + columnElements.each(function (i, e) { + columns[i].previousWidth = $(e).outerWidth(); + }); + if (options.forceFitColumns) { + shrinkLeewayOnRight = 0; + stretchLeewayOnRight = 0; + // colums on right affect maxPageX/minPageX + for (j = i + 1; j < columnElements.length; j++) { + c = columns[j]; + if (c.resizable) { + if (stretchLeewayOnRight !== null) { + if (c.maxWidth) { + stretchLeewayOnRight += c.maxWidth - c.previousWidth; + } else { + stretchLeewayOnRight = null; + } + } + shrinkLeewayOnRight += c.previousWidth - Math.max(c.minWidth || 0, absoluteColumnMinWidth); + } + } + } + var shrinkLeewayOnLeft = 0, stretchLeewayOnLeft = 0; + for (j = 0; j <= i; j++) { + // columns on left only affect minPageX + c = columns[j]; + if (c.resizable) { + if (stretchLeewayOnLeft !== null) { + if (c.maxWidth) { + stretchLeewayOnLeft += c.maxWidth - c.previousWidth; + } else { + stretchLeewayOnLeft = null; + } + } + shrinkLeewayOnLeft += c.previousWidth - Math.max(c.minWidth || 0, absoluteColumnMinWidth); + } + } + if (shrinkLeewayOnRight === null) { + shrinkLeewayOnRight = 100000; + } + if (shrinkLeewayOnLeft === null) { + shrinkLeewayOnLeft = 100000; + } + if (stretchLeewayOnRight === null) { + stretchLeewayOnRight = 100000; + } + if (stretchLeewayOnLeft === null) { + stretchLeewayOnLeft = 100000; + } + maxPageX = pageX + Math.min(shrinkLeewayOnRight, stretchLeewayOnLeft); + minPageX = pageX - Math.min(shrinkLeewayOnLeft, stretchLeewayOnRight); + }) + .on("drag", function (e, dd) { + var actualMinWidth, d = Math.min(maxPageX, Math.max(minPageX, e.pageX)) - pageX, x; + if (d < 0) { // shrink column + x = d; + for (j = i; j >= 0; j--) { + c = columns[j]; + if (c.resizable) { + actualMinWidth = Math.max(c.minWidth || 0, absoluteColumnMinWidth); + if (x && c.previousWidth + x < actualMinWidth) { + x += c.previousWidth - actualMinWidth; + c.width = actualMinWidth; + } else { + c.width = c.previousWidth + x; + x = 0; + } + } + } + + if (options.forceFitColumns) { + x = -d; + for (j = i + 1; j < columnElements.length; j++) { + c = columns[j]; + if (c.resizable) { + if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) { + x -= c.maxWidth - c.previousWidth; + c.width = c.maxWidth; + } else { + c.width = c.previousWidth + x; + x = 0; + } + } + } + } + } else { // stretch column + x = d; + for (j = i; j >= 0; j--) { + c = columns[j]; + if (c.resizable) { + if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) { + x -= c.maxWidth - c.previousWidth; + c.width = c.maxWidth; + } else { + c.width = c.previousWidth + x; + x = 0; + } + } + } + + if (options.forceFitColumns) { + x = -d; + for (j = i + 1; j < columnElements.length; j++) { + c = columns[j]; + if (c.resizable) { + actualMinWidth = Math.max(c.minWidth || 0, absoluteColumnMinWidth); + if (x && c.previousWidth + x < actualMinWidth) { + x += c.previousWidth - actualMinWidth; + c.width = actualMinWidth; + } else { + c.width = c.previousWidth + x; + x = 0; + } + } + } + } + } + applyColumnHeaderWidths(); + if (options.syncColumnCellResize) { + applyColumnWidths(); + } + }) + .on("dragend", function (e, dd) { + var newWidth; + $(this).parent().removeClass("slick-header-column-active"); + for (j = 0; j < columnElements.length; j++) { + c = columns[j]; + newWidth = $(columnElements[j]).outerWidth(); + + if (c.previousWidth !== newWidth && c.rerenderOnResize) { + invalidateAllRows(); + } + } + updateCanvasWidth(true); + render(); + trigger(self.onColumnsResized, {grid: self}); + }); + }); + } + + function getVBoxDelta($el) { + var p = ["borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom"]; + var delta = 0; + $.each(p, function (n, val) { + delta += parseFloat($el.css(val)) || 0; + }); + return delta; + } + + function measureCellPaddingAndBorder() { + var el; + var h = ["borderLeftWidth", "borderRightWidth", "paddingLeft", "paddingRight"]; + var v = ["borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom"]; + + // jquery prior to version 1.8 handles .width setter/getter as a direct css write/read + // jquery 1.8 changed .width to read the true inner element width if box-sizing is set to border-box, and introduced a setter for .outerWidth + // so for equivalent functionality, prior to 1.8 use .width, and after use .outerWidth + var verArray = $.fn.jquery.split('.'); + jQueryNewWidthBehaviour = (verArray[0]==1 && verArray[1]>=8) || verArray[0] >=2; + + el = $("").appendTo($headers); + headerColumnWidthDiff = headerColumnHeightDiff = 0; + if (el.css("box-sizing") != "border-box" && el.css("-moz-box-sizing") != "border-box" && el.css("-webkit-box-sizing") != "border-box") { + $.each(h, function (n, val) { + headerColumnWidthDiff += parseFloat(el.css(val)) || 0; + }); + $.each(v, function (n, val) { + headerColumnHeightDiff += parseFloat(el.css(val)) || 0; + }); + } + el.remove(); + + var r = $("
").appendTo($canvas); + el = $("").appendTo(r); + cellWidthDiff = cellHeightDiff = 0; + if (el.css("box-sizing") != "border-box" && el.css("-moz-box-sizing") != "border-box" && el.css("-webkit-box-sizing") != "border-box") { + $.each(h, function (n, val) { + cellWidthDiff += parseFloat(el.css(val)) || 0; + }); + $.each(v, function (n, val) { + cellHeightDiff += parseFloat(el.css(val)) || 0; + }); + } + r.remove(); + + absoluteColumnMinWidth = Math.max(headerColumnWidthDiff, cellWidthDiff); + } + + function createCssRules() { + $style = $("