diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..c13c5f6 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..6c43956 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,17 @@ +{ + + "env": { + "browser": true, + "node": true, + "mocha": true + }, + + "extends": "airbnb-base/legacy", + + "parser": "babel-eslint", + + "rules": { + "func-names": "off" + } + +} diff --git a/.gitignore b/.gitignore index 3c3629e..f06235c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +dist diff --git a/ajax-data-provider.js b/ajax-data-provider.js deleted file mode 100644 index 8c4063a..0000000 --- a/ajax-data-provider.js +++ /dev/null @@ -1,155 +0,0 @@ -(function ($) { - /*** - * 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 perPage = 10; - var data = []; - var url = "http://192.168.60.167:3002/api.php"; - var timer = null; - var req = null; // ajax timer - - // events - var onDataLoading = new Slick.Event(); - var onDataLoaded = new Slick.Event(); - - function init() {} - - function isDataLoaded(from, to) { - for (var i = from; i <= to; i++) { - if (data[i] === undefined || data[i] === null) { - return false; - } - } - - return true; - } - - function clear() { - for (var key in data) { - delete data[key]; - } - } - - function getData() { - return data; - } - - function prepareData(from, to) { - if (req) { - req.abort(); - for (var i = req.fromPage; i <= req.toPage; i++) { - delete data[i * perPage]; - } - } - - if (from < 0) { - from = 0; - } - - if (data.length > 0) { - to = Math.min(to, data.length - 1); - } - - var fromPage = Math.floor(from / perPage); - var toPage = Math.floor(to / perPage); - - while (data[fromPage * perPage] !== undefined && fromPage < toPage) - fromPage++; - - while (data[toPage * perPage] !== undefined && fromPage < toPage) - toPage--; - - if (fromPage > toPage || ((fromPage == toPage) && data[fromPage * perPage] !== undefined)) { - // TODO: look-ahead - onDataLoaded.notify({from: from, to: to}); - return; - } - - var recStart = (fromPage * perPage); - var recCount = (((toPage - fromPage) * perPage) + perPage); - - url += "?page=" + recStart + "&count=" + recCount; - - if (timer !== null) { - clearTimeout(timer); - } - - timer = setTimeout(function () { - for (var i = fromPage; i <= toPage; i++) { - data[i * perPage] = null; // null indicates a 'timered but not available yet' - } - - onDataLoading.notify({from: from, to: to}); - - req = $.ajax({ - url: url, - type: 'get', - dataType: 'json', - cache: true, - success: function (resp, textStatus, xOptions) { - onSuccess(resp, recStart); - }, - error: function () { - onError(fromPage, toPage); - } - }); - - req.fromPage = fromPage; - req.toPage = toPage; - }, 50); - } - - function refresh(from, to) { - for (var i = from; i <= to; i++) { - delete data[i]; - } - - getData(from, to); - } - - function onError(fromPage, toPage) { - console.log("Error loading pages " + fromPage + " to " + toPage); - } - - function onSuccess(resp, start) { - var end = start; - if (resp.data.length > 0) { - end = start + resp.data.length; - data.length += Math.min(parseInt(resp.data.length, 10),1000); // limitation of the API - - for (var i = 0; i < resp.data.length; i++) { - var item = resp.data[i]; - data[start + i] = item; - } - } - - req = null; - - onDataLoaded.notify({from: start, to: end}); - } - - init(); - - return { - // properties - "getData": getData, - - // methods - "clear": clear, - "isDataLoaded": isDataLoaded, - "prepareData": prepareData, - "refresh": refresh, - - // events - "onDataLoading": onDataLoading, - "onDataLoaded": onDataLoaded - }; - } - - // Slick.RemoteDataProvider - $.extend(true, window, { Flow: { AjaxDataProvider: AjaxDataProvider }}); -})(jQuery); diff --git a/api.php b/examples/api.php similarity index 100% rename from api.php rename to examples/api.php diff --git a/examples/index.html b/examples/index.html new file mode 100644 index 0000000..564e38f --- /dev/null +++ b/examples/index.html @@ -0,0 +1,138 @@ + + +
+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/emitter.js b/src/js/emitter.js
new file mode 100644
index 0000000..b5e887b
--- /dev/null
+++ b/src/js/emitter.js
@@ -0,0 +1,122 @@
+/**
+ * A minimal event emitter implemention in ES6, api is fairly to similar to
+ * node api check node emitter documention for information.
+ *
+ * @example
+ * ```
+ * var emitter = new Emitter();
+ * emitter.on('foo', () => {
+ * console.log('foo triggered');
+ * });
+ * emitter.emit('foo');
+ * ```
+ */
+
+class Emitter {
+ constructor() {
+ this.events = {};
+ }
+
+ /**
+ * Register an `event`.
+ *
+ * @param {String} eventName
+ * @param {Function} fn
+ * @return {Emitter}
+ */
+
+ on(eventName, fn) {
+ if (typeof this.events[eventName] === 'undefined') {
+ this.events[eventName] = [];
+ }
+
+ this.events[eventName].push(fn);
+ return this;
+ }
+
+ /**
+ * Remove an `event`.
+ *
+ * @param {String} eventName
+ * @param {Function} fn
+ * @return {Emitter}
+ */
+
+ off(eventName, fn) {
+ if (typeof eventName === 'undefined') {
+ this.events = {};
+ }
+
+ if (typeof fn === 'undefined') {
+ this.events[eventName] = [];
+ }
+
+ if (typeof fn === 'function') {
+ let i;
+ let events = this.events[eventName];
+
+ for (i = 0; i < events.length; i += 1) {
+ if (events[i] === fn) {
+ events.splice(i, 1);
+ }
+ }
+ }
+
+ return this;
+ }
+
+ /**
+ * Register an `event` which is fired only once and removed.
+ *
+ * @param {Strint} eventName
+ * @param {Function} fn
+ * @return {Emitter}
+ */
+
+ once(eventName, fn) {
+ let self = this;
+ let cb = (data) => {
+ self.off(eventName, cb);
+ fn.call(this, data);
+ };
+
+ this.on(eventName, cb);
+ return this;
+ }
+
+ /**
+ * Emit `event` with given arguments.
+ *
+ * @param {String} eventName
+ * @param {Mixed} args
+ * @return {Emitter}
+ */
+
+ emit(eventName, data) {
+ var i;
+ var events = this.events[eventName];
+
+ if (typeof events === 'undefined') {
+ return this;
+ }
+
+ for (i = 0; i < events.length; i += 1) {
+ events[i].call(this, data);
+ }
+
+ return this;
+ }
+
+ /**
+ * An alias to `emit`.
+ *
+ * @see `emit`
+ */
+
+ trigger(eventName, data) {
+ return this.emit(eventName, data);
+ }
+}
+
+export default Emitter;
+
diff --git a/src/js/event-data.js b/src/js/event-data.js
new file mode 100644
index 0000000..6055042
--- /dev/null
+++ b/src/js/event-data.js
@@ -0,0 +1,47 @@
+/**
+ * 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 + */ +function EventData() { + this.isPropagationStopped = false; + this.isImmediatePropagationStopped = false; +} + +/** + * Stops event from propagating up the DOM tree. + * @method stopPropagation + */ +EventData.prototype.stopPropagation = function () { + this.isPropagationStopped = true; +}; + +/** + * Returns whether stopPropagation was called on EventData.prototype.event object. + * @method isPropagationStopped + * @return {Boolean} + */ +EventData.prototype.isPropagationStopped = function () { + return this.isPropagationStopped; +}; + +/** + * Prevents the rest of the handlers from being executed. + * @method stopImmediatePropagation + */ +EventData.prototype.stopImmediatePropagation = function () { + this.isImmediatePropagationStopped = true; +}; + +/** + * Returns whether stopImmediatePropagation was called on EventData.prototype.event object.\ + * @method isImmediatePropagationStopped + * @return {Boolean} + */ +EventData.prototype.isImmediatePropagationStopped = function () { + return this.isImmediatePropagationStopped; +}; + +export default EventData; + diff --git a/src/js/event-handler.js b/src/js/event-handler.js new file mode 100644 index 0000000..4fde69c --- /dev/null +++ b/src/js/event-handler.js @@ -0,0 +1,41 @@ +/* eslint-disable */ +function EventHandler() { + this.handlers = []; +} + +EventHandler.prototype.on = function (event, handler) { + this.handlers.push({ + event: event, + handler: handler + }); + event.subscribe(handler); + + return this; +}; + +EventHandler.prototype.off = function (event, handler) { + var i = this.handlers.length; + while (i--) { + if (this.handlers[i].event === event && + this.handlers[i].handler === handler) { + this.handlers.splice(i, 1); + event.off(handler); + return; + } + } + + return this; +}; + +EventHandler.prototype.offAll = function () { + var i = this.handlers.length; + while (i--) { + this.handlers[i].event.off(this.handlers[i].handler); + } + this.handlers = []; + + return this; +}; + +export default EventHandler; + diff --git a/src/js/grid.js b/src/js/grid.js new file mode 100644 index 0000000..becbb5e --- /dev/null +++ b/src/js/grid.js @@ -0,0 +1,48 @@ +/* global $ */ +import Emitter from './emitter'; + +class Grid extends Emitter { + constructor(options) { + super(); + + this.el = options.el; + if (typeof this.el === 'undefined') { + throw new Error('Grid container must be valid dom element.'); + } + + this.columns = options.columns; + this.provider = options.dataProvider; + + this.render(); + } + + render() { + const { el, columns, provider } = this; + const data = provider.getData(); + const thead = $(''); + const dataLength = data.length; + + for (let i = 0; i < columns.length; i += 1) { + + let column = columns[i]; + const tr = $('