diff --git a/KoGrid.css b/KoGrid.css index f022f859..57708508 100644 --- a/KoGrid.css +++ b/KoGrid.css @@ -1,4 +1,4 @@ - + /******** Grid Global ********/ .kglabel { display: block; @@ -186,16 +186,30 @@ position: absolute; border-bottom: 1px solid rgb(229, 229, 229); } +/* + For Grids with group totals we can use a simple css even selector since + we want grouped rows to also have a striped background color, + we'll want to move this css change to a binding when we add in an option for group totals vs group labels. +*/ +/* .kgRow.even { background-color: rgb(243, 243, 243); } .kgRow.odd { background-color: rgb(253, 253, 253); } +*/ +.kgRow:nth-child(even):not(.selected) { + background-color: #c4dcf2; +} .kgRow.selected { background-color: rgb(189, 208, 203); } +.kgCell.selected { + background-color: rgb(169, 178, 173); +} + /******** Cells ********/ .kgCell { @@ -285,25 +299,44 @@ input { line-height: 20px; white-space:nowrap; } -.kgAggArrowExpanded { +.kgAggArrowExpanded, .kgAggArrowCollapsed { + left: 0; + bottom: 0; + width: 20px; + height: 20px; + cursor: pointer; + margin-top: 10px; + margin-left: 5px; + margin-right: 5px; + float: right; + position: relative; +} +.icon- { position: absolute; + left: 7px; + bottom: 8px; +} +.kgAggArrowExpanded i.icon- { + background-position: -433px -96px; +/* position: absolute; left: 8px; bottom: 10px; width: 0; height: 0; border-style: solid; border-width: 0 0 9px 9px; - border-color: transparent transparent #000000 transparent; + border-color: transparent transparent #000000 transparent;*/ } -.kgAggArrowCollapsed { - position: absolute; +.kgAggArrowCollapsed i.icon- { + background-position: -408px -96px; +/* position: absolute; left: 8px; bottom: 10px; width: 0; height: 0; border-style: solid; border-width: 5px 0 5px 8.7px; - border-color: transparent transparent transparent #000000; + border-color: transparent transparent transparent #000000;*/ } .kgHeaderButton { diff --git a/README.md b/README.md index 597b36a2..8bddb8b7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ +#A koGrid Fork +This is a koGrid fork with several features and bugfixes: + + - Added grouped rows and aggregation + - Add a footer row which will show totals for each column + - Improve the naming of the group column + - Support for sorting grouped grids + - Create Expand/Collapse-all buttons + - Improve speed of grouped grid, esp when expanding and collapsing rows + - Add an amd module output to the build process + - Add cell selection and improve row selection + +To see the grid in action please see the [example page] (http://cwohlman.github.io/KoGrid/) + #koGrid : A Knockout DataGrid# __Contributors:__ diff --git a/build/KoGrid.debug.js b/build/KoGrid.debug.js index 32a16b38..3082f1e4 100644 --- a/build/KoGrid.debug.js +++ b/build/KoGrid.debug.js @@ -2,7 +2,6 @@ * koGrid JavaScript Library * Authors: https://github.com/ericmbarnard/koGrid/blob/master/README.md * License: MIT (http://www.opensource.org/licenses/mit-license.php) -* Compiled At: 01/11/2013 15:58:36 ***********************************************/ (function (window) { @@ -31,6 +30,9 @@ var SELECTED_PROP = '__kg_selected__', KG_DEPTH = '_kg_depth_', KG_HIDDEN = '_kg_hidden_', KG_COLUMN = '_kg_column_', + KG_SORTINDEX = '_kg_sortindex_', + CELLSELECTED_PROP = '__kg_cellsselected__', + KG_VALUE = '_kg_value_', TEMPLATE_REGEXP = /<.+>/; /*********************************************** @@ -45,6 +47,10 @@ window.kg.moveSelectionHandler = function(grid, evt) { var charCode = evt.which || evt.keyCode, // detect which direction for arrow keys to navigate the grid offset = (charCode === 38 ? -1 : (charCode === 40 ? 1 : null)); + if (charCode == 46) { + grid.selectionService.RemoveSelectedRows(); + return false; + } if (!offset) { return true; } @@ -198,12 +204,12 @@ $.extend(window.kg.utils, { /*********************************************** * FILE: ..\src\templates\gridTemplate.html ***********************************************/ -window.kg.defaultGridTemplate = function(){ return '
Drag a column header here and drop it to group by that column
  • x
Choose Columns:
Total Items: (Showing: )
Selected Items:
Page Size:
';}; +window.kg.defaultGridTemplate = function(){ return '
Drag a column header here and drop it to group by that column
  • x
Choose Columns:
';}; /*********************************************** * FILE: ..\src\templates\rowTemplate.html ***********************************************/ -window.kg.defaultRowTemplate = function(){ return '
';}; +window.kg.defaultRowTemplate = function(){ return '
';}; /*********************************************** * FILE: ..\src\templates\cellTemplate.html @@ -211,9 +217,11 @@ window.kg.defaultRowTemplate = function(){ return '
';}; /*********************************************** -* FILE: ..\src\templates\aggregateTemplate.html +* FILE: ..\src\templates\aggregateTemplate.js ***********************************************/ -window.kg.aggregateTemplate = function(){ return '
( Items)
';}; +window.kg.aggregateTemplate = function () { + return window.kg.defaultRowTemplate(); +}; /*********************************************** * FILE: ..\src\templates\headerRowTemplate.html @@ -225,6 +233,11 @@ window.kg.defaultHeaderRowTemplate = function(){ return '
';}; +/*********************************************** +* FILE: ..\src\templates\aggCellTemplate.html +***********************************************/ +window.kg.aggCellTemplate = function(){ return '
';}; + /*********************************************** * FILE: ..\src\bindingHandlers\ko-grid.js ***********************************************/ @@ -235,7 +248,7 @@ ko.bindingHandlers['koGrid'] = (function () { var elem = $(element); options.gridDim = new window.kg.Dimension({ outerHeight: ko.observable(elem.height()), outerWidth: ko.observable(elem.width()) }); var grid = new window.kg.Grid(options); - var gridElem = $(window.kg.defaultGridTemplate()); + var gridElem = $(options.gridTemplate || window.kg.defaultGridTemplate()); // if it is a string we can watch for data changes. otherwise you won't be able to update the grid data options.data.subscribe(function () { if (grid.$$selectionPhase) { @@ -315,11 +328,16 @@ ko.bindingHandlers['kgCell'] = (function () { return { 'init': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { bindingContext.$userViewModel = bindingContext.$parent.$userViewModel; + var $element = $(element); var compile = function (html) { - var cell = $(html); - ko.applyBindings(bindingContext, cell[0]); - $(element).html(cell); + //viewModel.$cellTemplate = $(html); + $element.html(html); + ko.applyBindings(bindingContext, $element.children()[0]); }; + // if (viewModel.$cellTemplate) { + // $element.html(viewModel.$cellTemplate); + // ko.applyBindings(bindingContext, $element.children()[0]); + // } if (viewModel.cellTemplate.then) { viewModel.cellTemplate.then(function(p) { compile(p); @@ -397,7 +415,7 @@ ko.bindingHandlers['mouseEvents'] = (function () { /*********************************************** * FILE: ..\src\classes\aggregate.js ***********************************************/ -window.kg.Aggregate = function (aggEntity, rowFactory) { +window.kg.Aggregate = function (aggEntity, config, rowFactory, selectionService) { var self = this; self.index = 0; self.offsetTop = ko.observable(0); @@ -415,37 +433,43 @@ window.kg.Aggregate = function (aggEntity, rowFactory) { self.aggLabelFilter = aggEntity.aggLabelFilter; self.toggleExpand = function() { var c = self.collapsed(); - self.collapsed(!c); + self._setExpand(!c); self.notifyChildren(); }; self.setExpand = function (state) { - self.collapsed(state); + self._setExpand(state); self.notifyChildren(); }; - self.notifyChildren = function() { + self._setExpand = function (state, child) { + if (!child) { + self.collapsed(state); + self.entity._kg_collapsed = self.collapsed(); + } + if (self.parent && self.entity[KG_HIDDEN]) state = true; $.each(self.aggChildren, function (i, child) { - child.entity[KG_HIDDEN] = self.collapsed(); - if (self.collapsed()) { - var c = self.collapsed(); - child.setExpand(c); - } + var c = !!state || child.collapsed(); + child.entity[KG_HIDDEN] = state; + child._setExpand(c, true); }); $.each(self.children, function (i, child) { - child[KG_HIDDEN] = self.collapsed(); + child[KG_HIDDEN] = !!state; }); + // var foundMyself = false; + // $.each(rowFactory.aggCache, function (i, agg) { + // if (foundMyself) { + // var offset = (30 * self.children.length); + // var c = self.collapsed(); + // agg.offsetTop(c ? agg.offsetTop() - offset : agg.offsetTop() + offset); + // } else { + // if (i == self.aggIndex) { + // foundMyself = true; + // } + // } + // }); + }; + self.notifyChildren = function() { rowFactory.rowCache = []; - var foundMyself = false; - $.each(rowFactory.aggCache, function (i, agg) { - if (foundMyself) { - var offset = (30 * self.children.length); - var c = self.collapsed(); - agg.offsetTop(c ? agg.offsetTop() - offset : agg.offsetTop() + offset); - } else { - if (i == self.aggIndex) { - foundMyself = true; - } - } - }); + rowFactory.renderedChange(); }; self.aggClass = ko.computed(function() { @@ -469,10 +493,56 @@ window.kg.Aggregate = function (aggEntity, rowFactory) { return self.children.length; } }); - self.selected = ko.observable(false); self.isEven = ko.observable(false); self.isOdd = ko.observable(false); - self.toggleSelected = function () { return true; }; + self.canSelectRows = config.canSelectRows; + self.selectedItems = config.selectedItems; + self.selectionService = selectionService; + + self.selected = ko.observable(false); + self.cellSelection = ko.observableArray(aggEntity[CELLSELECTED_PROP] || []); + self.continueSelection = function(event) { + self.selectionService.ChangeSelection(self, event); + }; + self.toggleSelected = function (row, event) { + if (!self.canSelectRows) { + return true; + } + var element = event.target || event; + //check and make sure its not the bubbling up of our checked 'click' event + if (element.type == "checkbox") { + self.selected(!self.selected()); + } + if (config.selectWithCheckboxOnly && element.type != "checkbox"){ + return true; + } else { + if (self.beforeSelectionChange(self, event)) { + self.continueSelection(event); + return self.afterSelectionChange(self, event); + } + } + return false; + }; + //selectify the entity + if (self.entity[SELECTED_PROP] === undefined) { + self.entity[SELECTED_PROP] = false; + } else { + // or else maintain the selection set by the entity. + self.selectionService.setSelection(self, self.entity[SELECTED_PROP]); + self.selectionService.updateCellSelection(self, self.entity[CELLSELECTED_PROP]); + } + self.beforeSelectionChange = config.beforeSelectionChangeCallback; + self.afterSelectionChange = config.afterSelectionChangeCallback; + self.propertyCache = {}; + self.getProperty = function (path) { + return self.propertyCache[path] || (self.propertyCache[path] = window.kg.utils.evalProperty(self.entity, path)); + }; + self.selectCell = function (column) { + var field = column.field; + var index = self.cellSelection().indexOf(field); + if (index == -1) self.selectionService.setCellSelection(self, column, true); + else self.selectionService.setCellSelection(self, column, false); + }; }; /*********************************************** @@ -484,13 +554,19 @@ window.kg.Column = function (config, grid) { delay = 500, clicks = 0, timer = null; + self.config = config; self.eventTaget = undefined; self.width = colDef.width; - self.groupIndex = ko.observable(0); + self.groupIndex = ko.observable(config.colDef.groupIndex || 0); self.isGroupedBy = ko.observable(false); + self.collapsed = ko.observable(); + self.aggClass = ko.computed(function() { + return self.collapsed() ? "kgAggArrowCollapsed" : "kgAggArrowExpanded"; + }); self.groupedByClass = ko.computed(function(){ return self.isGroupedBy() ? "kgGroupedByIcon": "kgGroupIcon";}); self.sortable = ko.observable(false); - self.resizable = ko.observable(false); + self.resizable = ko.observable(false); + self.selectable = ko.observable(false); self.minWidth = !colDef.minWidth ? 50 : colDef.minWidth; self.maxWidth = !colDef.maxWidth ? 9000 : colDef.maxWidth; self.headerRowHeight = config.headerRowHeight; @@ -504,6 +580,9 @@ window.kg.Column = function (config, grid) { self._visible = ko.observable(window.kg.utils.isNullOrUndefined(colDef.visible) || colDef.visible); self.visible = ko.computed({ read: function() { + if (grid && grid.config.showGroupedColumns === false && self.isGroupedBy()) { + return false; + } return self._visible(); }, write: function(val) { @@ -516,11 +595,17 @@ window.kg.Column = function (config, grid) { if (config.enableResize) { self.resizable(window.kg.utils.isNullOrUndefined(colDef.resizable) || colDef.resizable); } - self.sortDirection = ko.observable(undefined); + self.selectable(colDef.selectable); + self.sortDirection = ko.observable(colDef.sortDirection); + if (self.sortDirection()) { + // This line would prevent multiple columns being sorted simultaneously + // if (grid.lastSortedColumn()) grid.lastSortedColumn().sortDirection(""); + grid.lastSortedColumn = self; + } self.sortingAlgorithm = colDef.sortFn; self.headerClass = ko.observable(colDef.headerClass); self.headerCellTemplate = colDef.headerCellTemplate || window.kg.defaultHeaderCellTemplate(); - self.cellTemplate = colDef.cellTemplate || window.kg.defaultCellTemplate(); + self.cellTemplate = colDef.cellTemplate || grid.config.cellTemplate || window.kg.defaultCellTemplate(); if (colDef.cellTemplate && !TEMPLATE_REGEXP.test(colDef.cellTemplate)) { self.cellTemplate = window.kg.utils.getTemplatePromise(colDef.cellTemplate); } @@ -649,7 +734,7 @@ window.kg.EventProvider = function (grid) { } self.setDraggables(); } - grid.columns.subscribe(self.setDraggables); + grid.visibleColumns.subscribe(self.setDraggables); }; self.dragOver = function(evt) { evt.preventDefault(); @@ -686,7 +771,7 @@ window.kg.EventProvider = function (grid) { // Get the scope from the header container if(groupItem[0].className !='kgRemoveGroup'){ var groupItemScope = ko.dataFor(groupItem[0]); - if (groupItemScope) { + if (groupItemScope instanceof window.kg.Column) { // set draggable events if(!grid.config.jqueryUIDraggable){ groupItem.attr('draggable', 'true'); @@ -697,6 +782,7 @@ window.kg.EventProvider = function (grid) { } else { self.groupToMove = undefined; } + self.colToMove = undefined; }; self.onGroupDrop = function(event) { @@ -709,9 +795,9 @@ window.kg.EventProvider = function (grid) { if (groupContainer.context.className =='kgGroupPanel') { grid.configGroups.splice(self.groupToMove.index, 1); grid.configGroups.push(self.groupToMove.groupName); - } else { + } else if (groupContainer[0]) { groupScope = ko.dataFor(groupContainer[0]); - if (groupScope) { + if (groupScope instanceof window.kg.Column) { // If we have the same column, do nothing. if (self.groupToMove.index != groupScope.groupIndex()) { // Splice the columns @@ -729,7 +815,7 @@ window.kg.EventProvider = function (grid) { grid.groupBy(self.colToMove.col); } else { groupScope = ko.dataFor(groupContainer[0]); - if (groupScope) { + if (groupScope instanceof window.kg.Column) { // Splice the columns grid.removeGroup(groupScope.groupIndex()); } @@ -753,6 +839,7 @@ window.kg.EventProvider = function (grid) { // Save the column for later. self.colToMove = { header: headerContainer, col: headerScope }; } + self.groupToMove = undefined; return true; }; @@ -871,8 +958,11 @@ window.kg.RowFactory = function (grid) { self.parentCache = []; // Used for grouping and is cleared each time groups are calulated. self.dataChanged = true; self.parsedData = []; + grid.config.parsedDataCache = grid.config.parsedDataCache || ko.observableArray(); + self.parsedDataCache = grid.config.parsedDataCache; self.rowConfig = {}; self.selectionService = grid.selectionService; + self.aggregationProvider = new window.kg.AggregationProvider(grid); self.rowHeight = 30; self.numberOfAggregates = 0; self.groupedData = undefined; @@ -897,22 +987,91 @@ window.kg.RowFactory = function (grid) { row = new window.kg.Row(entity, self.rowConfig, self.selectionService); row.rowIndex(rowIndex + 1); //not a zero-based rowIndex row.offsetTop((self.rowHeight * rowIndex).toString() + 'px'); - row.selected(entity[SELECTED_PROP]); + // row.selected(entity[SELECTED_PROP]); // finally cache it for the next round self.rowCache[rowIndex] = row; } return row; }; + self.getChildCount = function (row) { + if (row.children && row.children.length) return row.children.length; + else if (row.aggChildren && row.aggChildren.length) { + var total = 0; + row.aggChildren.forEach(function (a) { + total += self.getChildCount(a); + }); + return total; + } + return 0; + }; + self.calcAggContent = function (row, column) { + if (column.field == 'Group') { + return row.label(); + } else if (column.field == row.entity.gField) { + return row.entity.gLabel; + } else if (column.groupIndex() && row.parent) { + if (row.parent) { + return ko.utils.unwrapObservable(row.parent.entity[column.field]); + } else { + return ''; + } + // } else if (column.field == "lineNum") { + // return self.getChildCount(row); + } else { + var def = column.config.colDef; + //TODO: add a switch for whether or not to aggregate at all. + if (def && (def.aggregator || def.agg)) { + var aggType = def.agg || def.aggregator || 'count'; + var aggParts = aggType.match(/^([^(]+)\(([^)]+)?\)/); + if (aggParts) { + aggType = aggParts[1]; + } + var aggregator = self.aggregationProvider[aggType]; + if (aggParts && typeof aggregator == "function") { + aggregator = aggregator(aggParts[2]); + } + if (!aggregator || typeof aggregator.grid != "function") return "#error"; + var aggregateValue = aggregator.grid(row, def); + return aggregateValue ? aggregateValue : ''; + } + console.log('No way to calc agg content'); + return ''; + } + }; + self.getAggKey = function (aggRow) { + var key = {}; + key[aggRow.entity.gField] = aggRow.entity.gLabel; + if (aggRow.parent) { + key = $.extend(key, self.getAggKey(aggRow.parent)); + } + return key; + }; + self.buildAggregateEntity = function (agg) { + var aggEntity = agg.entity; + grid.nonAggColumns().forEach(function (column) { + aggEntity[column.field] = ko.computed({ + read: function () { + if (!this.val) this.val = self.calcAggContent(agg, column); + return this.val; + }, + owner: {}, + deferEvaluation: true + }); + // if (result.field == column.field) result.setExpand + }); + agg.Key = aggEntity.Key = self.getAggKey(agg); + }; self.buildAggregateRow = function(aggEntity, rowIndex) { var agg = self.aggCache[aggEntity.aggIndex]; // first check to see if we've already built it if (!agg) { // build the row - agg = new window.kg.Aggregate(aggEntity, self); + agg = new window.kg.Aggregate(aggEntity, self.rowConfig, self, self.selectionService); self.aggCache[aggEntity.aggIndex] = agg; } agg.index = rowIndex + 1; //not a zero-based rowIndex agg.offsetTop((self.rowHeight * rowIndex).toString() + 'px'); + self.rowCache[rowIndex] = agg; return agg; }; self.UpdateViewableRange = function(newRange) { @@ -928,10 +1087,58 @@ window.kg.RowFactory = function (grid) { } self.dataChanged = true; self.rowCache = []; //if data source changes, kill this! + grid.selectedCells([]); + grid.selectedItems([]); if (grid.config.groups.length > 0) { + if (!grid.columns().filter(function (a) { + return a.field == 'Group'; + }).length) { + grid.columns.splice(0, 0, new window.kg.Column({ + colDef: { + field: 'Group', + displayName: grid.config.columnDefs + .filter(function (a) { + return a.groupIndex > 0; + }) + .map(function (a) { + return a.displayName; + }) + .join("-"), + width: 250, + index: 0, + sortable: true, + sortDirection: 'asc', + resizable: true + }, + sortCallback: grid.sortData, + resizeOnDataCallback: grid.resizeOnData, + enableResize: grid.config.enableColumnResize, + enableSort: grid.config.enableSorting, + index: 0, + }, grid)); + window.kg.domUtilityService.BuildStyles(grid); + + } self.getGrouping(grid.config.groups); } + + var aggRow = new window.kg.Aggregate( + { + isAggRow: true, + '_kg_hidden_': false, + children: [], + aggChildren: [] + }, // entity + self.rowConfig, + self, + self.selectionService + ); + self.buildAggregateEntity(aggRow); + aggRow.children = grid.filteredData(); + aggRow.entity.lineNum = aggRow.children.length; + grid.totalsRow(aggRow); self.UpdateViewableRange(self.renderedRange); + grid.selectedCells.notifySubscribers(grid.selectedCells()); }; self.renderedChange = function() { @@ -945,11 +1152,15 @@ window.kg.RowFactory = function (grid) { var dataArray = self.parsedData.filter(function(e) { return e[KG_HIDDEN] === false; }).slice(self.renderedRange.topRow, self.renderedRange.bottomRow); + var indexArray = self.parsedData.filter(function (a) { + return /*a[KG_HIDDEN] === false && */!a.isAggRow; + }); $.each(dataArray, function (indx, item) { var row; if (item.isAggRow) { row = self.buildAggregateRow(item, self.renderedRange.topRow + indx); } else { + item.lineNum = indexArray.indexOf(item) + 1; row = self.buildEntityRow(item, self.renderedRange.topRow + indx); } //add the row to our return array @@ -962,7 +1173,12 @@ window.kg.RowFactory = function (grid) { self.renderedChangeNoGroups = function() { var rowArr = []; var dataArr = grid.filteredData.slice(self.renderedRange.topRow, self.renderedRange.bottomRow); + var indexArray = ko.utils.unwrapObservable(grid.filteredData); + // .filter(function (a) { + // return a[KG_HIDDEN] === false && !a.isAggRow; + // }); $.each(dataArr, function (i, item) { + item.lineNum = indexArray.indexOf(item) + 1; var row = self.buildEntityRow(item, self.renderedRange.topRow + i); //add the row to our return array rowArr.push(row); @@ -975,20 +1191,30 @@ window.kg.RowFactory = function (grid) { if (g.values) { $.each(g.values, function (i, item) { // get the last parent in the array because that's where our children want to be - self.parentCache[self.parentCache.length - 1].children.push(item); + var parent = self.parentCache[self.parentCache.length - 1]; + parent.children.push(item); //add the row to our return array self.parsedData.push(item); + item[KG_HIDDEN] = !!parent.collapsed(); }); } else { + var props = []; for (var prop in g) { + if (g[prop] && typeof g[prop] == "object" && typeof g[prop][KG_SORTINDEX] != "undefined") { + props[g[prop][KG_SORTINDEX]] = prop; + } + } + for (var i = 0; i < props.length; i++) { + var prop = props[i]; + if (!prop) continue; // exclude the meta properties. - if (prop == KG_FIELD || prop == KG_DEPTH || prop == KG_COLUMN) { + if (prop == KG_FIELD || prop == KG_DEPTH || prop == KG_COLUMN || prop == KG_SORTINDEX) { continue; } else if (g.hasOwnProperty(prop)) { - //build the aggregate row - var agg = self.buildAggregateRow({ + //build the aggregate entity + var entity = { gField: g[KG_FIELD], - gLabel: prop, + gLabel: g[prop][KG_VALUE], gDepth: g[KG_DEPTH], isAggRow: true, '_kg_hidden_': false, @@ -996,21 +1222,53 @@ window.kg.RowFactory = function (grid) { aggChildren: [], aggIndex: self.numberOfAggregates, aggLabelFilter: g[KG_COLUMN].aggLabelFilter - }, 0); + }; + var parent = self.parentCache[g[KG_DEPTH] - 1]; + var key = self.getAggKey({ + entity: { + gField: g[KG_FIELD], + gLabel: g[prop][KG_VALUE], + }, + parent: parent + }); + var cachedIndex = -1; + self.parsedDataCache().forEach(function (a, i) { + var isMatch = a.gField == entity.gField; + for (var prop in key) { + if (ko.utils.unwrapObservable(a.Key[prop]) != ko.utils.unwrapObservable(key[prop])) isMatch = false; + } + if (isMatch) cachedIndex = i; + }); + if (cachedIndex != -1) { + var cachedEntity = self.parsedDataCache().splice(cachedIndex, 1)[0]; + entity[SELECTED_PROP] = cachedEntity[SELECTED_PROP]; + entity[CELLSELECTED_PROP] = cachedEntity[CELLSELECTED_PROP]; + entity._kg_collapsed = cachedEntity._kg_collapsed; + } + //build the aggregate row + var agg = self.buildAggregateRow(entity, 0); + if (self.parsedDataCache().indexOf(agg.entity) == -1) self.parsedDataCache().push(agg.entity); + else throw new Error("Stop"); + // If agg is the last grouping and hideChildren is enabled collapse the agg to hide it's children + if (g[KG_DEPTH] == self.maxDepth - 1 && self.hideChildren) agg.entity._kg_collapsed = true; self.numberOfAggregates++; //set the aggregate parent to the parent in the array that is one less deep. agg.parent = self.parentCache[agg.depth - 1]; // if we have a parent, set the parent to not be collapsed and append the current agg to its children if (agg.parent) { - agg.parent.collapsed(false); + agg.entity._kg_hidden_ = !!agg.parent.collapsed(); + agg.entity._kg_collapsed = agg.parent.collapsed() || agg.entity._kg_collapsed; agg.parent.aggChildren.push(agg); } + agg.collapsed(agg.entity._kg_collapsed); + agg.entity.Key = self.getAggKey(agg); // add the aggregate row to the parsed data. self.parsedData.push(agg.entity); // the current aggregate now the parent of the current depth self.parentCache[agg.depth] = agg; // dig deeper for more aggregates or children. self.parseGroupData(g[prop]); + self.buildAggregateEntity(agg); } } } @@ -1021,23 +1279,25 @@ window.kg.RowFactory = function (grid) { self.rowCache = []; self.numberOfAggregates = 0; self.groupedData = {}; + self.maxDepth = groups.length; // Here we set the onmousedown event handler to the header container. var data = grid.filteredData(); var maxDepth = groups.length; var cols = grid.columns(); - + self.hideChildren = !!ko.utils.unwrapObservable(grid.config.hideChildren); $.each(data, function (i, item) { - item[KG_HIDDEN] = true; + item[KG_HIDDEN] = self.hideChildren; var ptr = self.groupedData; $.each(groups, function(depth, group) { - if (!cols[depth].isAggCol && depth <= maxDepth) { + if (!cols[depth].isAggCol && (depth + (self.hideChildren ? 2 : 0)) <= maxDepth) { grid.columns.splice(item.gDepth, 0, new window.kg.Column({ colDef: { field: '', width: 25, sortable: false, resizable: false, - headerCellTemplate: '
' + headerCellTemplate: '
', + cellTemplate: window.kg.aggCellTemplate() }, isAggCol: true, index: item.gDepth, @@ -1049,7 +1309,8 @@ window.kg.RowFactory = function (grid) { var val = window.kg.utils.evalProperty(item, group); if (col.cellFilter) { val = col.cellFilter(val); - } + } + var childVal = val; val = val ? val.toString() : 'null'; if (!ptr[val]) { ptr[val] = {}; @@ -1062,9 +1323,16 @@ window.kg.RowFactory = function (grid) { } if (!ptr[KG_COLUMN]) { ptr[KG_COLUMN] = col; - } + } + if (!ptr[KG_SORTINDEX]) { + ptr[KG_SORTINDEX] = i; + } ptr = ptr[val]; + if (!ptr[KG_VALUE]) ptr[KG_VALUE] = childVal; }); + if (!ptr[KG_SORTINDEX]) { + ptr[KG_SORTINDEX] = i; + } if (!ptr.values) { ptr.values = []; } @@ -1096,6 +1364,7 @@ window.kg.Grid = function (options) { data: ko.observableArray([]), columnDefs: undefined, selectedItems: ko.observableArray([]), // array, if multi turned off will have only one item in array + selectedCells: ko.observableArray([]), displaySelectionCheckbox: true, //toggles whether row selection check boxes appear selectWithCheckboxOnly: false, useExternalSorting: false, @@ -1144,6 +1413,7 @@ window.kg.Grid = function (options) { self.$groupPanel = null; self.$topPanel = null; self.$headerContainer = null; + self.$footerContainer = null; self.$headerScroller = null; self.$headers = null; self.$viewport = null; @@ -1151,6 +1421,7 @@ window.kg.Grid = function (options) { self.rootDim = self.config.gridDim; self.sortInfo = ko.isObservable(self.config.sortInfo) ? self.config.sortInfo : ko.observable(self.config.sortInfo); self.sortedData = self.config.data; + self.totalsRow = ko.observable(); self.lateBindColumns = false; self.filteredData = ko.observableArray([]); self.lastSortedColumn = undefined; @@ -1222,11 +1493,16 @@ window.kg.Grid = function (options) { cellTemplate: '
' }); } + columnDefs.sort(function (a, b) {return a.index - b.index;}); if (columnDefs.length > 0) { + self.configGroups([]); + var configGroups = []; $.each(columnDefs, function (i, colDef) { + var index = i; var column = new window.kg.Column({ - colDef: colDef, - index: i, + colDef: colDef, + // This is likely causing our bug, we need to clean the index vield to ensure that all the indexes are valid. + index: index, headerRowHeight: self.config.headerRowHeight, sortCallback: self.sortData, resizeOnDataCallback: self.resizeOnData, @@ -1236,10 +1512,36 @@ window.kg.Grid = function (options) { cols.push(column); var indx = self.config.groups.indexOf(colDef.field); if (indx != -1) { - self.configGroups.splice(indx, 0, column); + indx = colDef.groupIndex ? colDef.groupIndex - 1 : indx; + configGroups.splice(indx, 0, column); + column.isGroupedBy(true); + } else if (colDef.groupIndex) { + //self.config.groups.splice(colDef.groupIndex - 1, 0, colDef.field); + configGroups.splice(colDef.groupIndex - 1, 0, column); + column.isGroupedBy(true); } }); + cols.sort(function (a, b) {return a.index - b.index;}); + var gindex = []; + $.each(cols, function (index, item) { + var idx = item.groupIndex(); + if (idx) { + while(gindex[idx]) { + idx++; + } + item.groupIndex(idx); + gindex[idx] = item; + } + }); + configGroups.sort(function (a, b) {return a.groupIndex() - b.groupIndex();}); + var groups = []; + $.each(configGroups, function (index, item) { + groups.push(item.field); + }); + self.config.groups = groups; self.columns(cols); + self.configGroups(configGroups); + self.fixGroupIndexes(); } }; self.configureColumnWidths = function() { @@ -1250,8 +1552,21 @@ window.kg.Grid = function (options) { asteriskNum = 0, totalWidth = 0; var columns = self.columns(); - $.each(cols, function (i, col) { - var isPercent = false, t = undefined; + var aggColOffset = self.columns().length - self.nonAggColumns().length; + $.each(columns, function(i, column) { + var col; + $.each(cols, function (index, c) { + if (c.field == column.field) { + col = c; + } + }); + col = col ? {width: col.width, index: i} : {width: column.width, index: i}; + // }); + // $.each(cols, function (i, col) { + if (column.visible === false) { + return; + } + var isPercent = false, t; //if width is not defined, set it to a single star if (window.kg.utils.isNullOrUndefined(col.width)) { col.width = "*"; @@ -1270,12 +1585,11 @@ window.kg.Grid = function (options) { return; } else if (t.indexOf("*") != -1) { asteriskNum += t.length; - col.index = i; - asterisksArray.push(col); + asterisksArray.push({width: col.width, index: i}); return; } else if (isPercent) { // If the width is a percentage, save it until the very last. - col.index = i; - percentArray.push(col); + + percentArray.push({width: col.width, index: i}); return; } else { // we can't parse the width so lets throw an error. throw "unable to parse column width, use percentage (\"10%\",\"20%\", etc...) or \"*\" to use remaining width of grid"; @@ -1292,9 +1606,16 @@ window.kg.Grid = function (options) { // calculate the weight of each asterisk rounded down var asteriskVal = Math.floor(remainingWidth / asteriskNum); // set the width of each column based on the number of stars - $.each(asterisksArray, function (i, col) { - var t = col.width.length; - columns[col.index].width = asteriskVal * t; + if (asteriskVal < 1) { + asteriskVal = 1; + } + $.each(asterisksArray, function (i, col) { + var t = col.width.length; + var column = columns[col.index]; + column.width = asteriskVal * t; + if (column.width < column.minWidth) { + column.width = column.minWidth; + } //check if we are on the last column if (col.index + 1 == numOfCols) { var offset = 2; //We're going to remove 2 px so we won't overlflow the viwport by default @@ -1303,7 +1624,7 @@ window.kg.Grid = function (options) { //compensate for scrollbar offset += window.kg.domUtilityService.ScrollW; } - columns[col.index].width -= offset; + column.width -= offset; } totalWidth += columns[col.index].width; }); @@ -1341,6 +1662,12 @@ window.kg.Grid = function (options) { self.config.groups = tempArr; self.rowFactory.filteredDataChanged(); }); + self.sortedData.subscribe(function () { + if (!self.isSorting) { + self.sortByDefault(); + } + }); + self.filteredData.subscribe(function () { if (self.$$selectionPhase) { return; @@ -1375,6 +1702,9 @@ window.kg.Grid = function (options) { if (self.$headerContainer) { self.$headerContainer.scrollLeft(scrollLeft); } + if (self.$footerContainer) { + self.$footerContainer.scrollLeft(scrollLeft); + } }; self.resizeOnData = function (col) { // we calculate the longest data. @@ -1396,12 +1726,21 @@ window.kg.Grid = function (options) { col.width = longest = Math.min(col.maxWidth, longest + 7); // + 7 px to make it look decent. window.kg.domUtilityService.BuildStyles(self); }; + self.sortByDefault = function () { + // console.log(self.sortedData().length); + var column = self.columns().filter(function (a) {return a.sortDirection && a.sortDirection()})[0]; + if (!column) return; + var direction = column.sortDirection(); + self.sortData(column, direction); + } self.sortData = function (col, direction) { // if external sorting is being used, do nothing. self.isSorting = true; + // if (col.field == "Group") col = self.configGroups()[0]; self.sortInfo({ column: col, - direction: direction + direction: direction, + grid: self }); self.clearSortingData(col); if(!self.config.useExternalSorting){ @@ -1412,6 +1751,14 @@ window.kg.Grid = function (options) { self.lastSortedColumn = col; self.isSorting = false; }; + self.toggleCollapse = function (data) { + var collapsed = !data.collapsed(); + data.collapsed(collapsed); + self.rowFactory.aggCache.forEach(function (a) {if (a.field == data.field) {a._setExpand(collapsed);}}); + + self.rowFactory.rowCache = []; + self.rowFactory.renderedChange(); + }; self.clearSortingData = function (col) { if (!col) { $.each(self.columns(), function (i, c) { @@ -1442,6 +1789,7 @@ window.kg.Grid = function (options) { self.jqueryUITheme = ko.observable(self.config.jqueryUITheme); self.footer = null; self.selectedItems = self.config.selectedItems; + self.selectedCells = self.config.selectedCells; self.multiSelect = self.config.multiSelect; self.footerVisible = window.kg.utils.isNullOrUndefined(self.config.displayFooter) ? self.config.footerVisible : self.config.displayFooter; self.config.footerRowHeight = self.footerVisible ? self.config.footerRowHeight : 0; @@ -1501,8 +1849,10 @@ window.kg.Grid = function (options) { var indx = self.configGroups().indexOf(col); if (indx == -1) { col.isGroupedBy(true); + col.visible(false); self.configGroups.push(col); col.groupIndex(self.configGroups().length); + self.sortByDefault(); } else { self.removeGroup(indx); } @@ -1512,15 +1862,26 @@ window.kg.Grid = function (options) { var col = self.columns().filter(function(item){ return item.groupIndex() == (index + 1); })[0]; - col.isGroupedBy(false); - col.groupIndex(0); - self.columns.splice(index, 1); - self.configGroups.splice(index, 1); - self.fixGroupIndexes(); - if (self.configGroups().length === 0) { - self.fixColumnIndexes(); + if (col) { + col.visible(true); + col.isGroupedBy(false); + col.groupIndex(0); + if (self.columns()[index].isAggCol) { + self.columns.splice(index, 1); + } + self.configGroups.splice(index, 1); + if (self.configGroups.length == 0) { + var groupCol = self.columns().filter(function (item) { + return item.field == "Group"; + })[0]; + if (groupCol) groupCol.visible(false); + } + self.fixGroupIndexes(); + if (self.configGroups().length === 0) { + self.fixColumnIndexes(); + } + window.kg.domUtilityService.BuildStyles(self); } - window.kg.domUtilityService.BuildStyles(self); }; self.fixGroupIndexes = function(){ $.each(self.configGroups(), function(i,item){ @@ -1613,6 +1974,7 @@ window.kg.Row = function (entity, config, selectionService) { self.selectionService = selectionService; self.selected = ko.observable(false); + self.cellSelection = ko.observableArray(entity[CELLSELECTED_PROP] || []); self.continueSelection = function(event) { self.selectionService.ChangeSelection(self, event); }; @@ -1636,11 +1998,12 @@ window.kg.Row = function (entity, config, selectionService) { return false; }; //selectify the entity - if (self.entity[SELECTED_PROP] === undefined) { + if (!self.entity[SELECTED_PROP] === undefined) { self.entity[SELECTED_PROP] = false; } else { // or else maintain the selection set by the entity. self.selectionService.setSelection(self, self.entity[SELECTED_PROP]); + self.selectionService.updateCellSelection(self, self.entity[CELLSELECTED_PROP]); } self.rowIndex = ko.observable(0); self.offsetTop = ko.observable("0px"); @@ -1663,7 +2026,13 @@ window.kg.Row = function (entity, config, selectionService) { self.getProperty = function (path) { return self.propertyCache[path] || (self.propertyCache[path] = window.kg.utils.evalProperty(self.entity, path)); }; -}; + self.selectCell = function (column) { + var field = column.field; + var index = self.cellSelection().indexOf(field); + if (index == -1) self.selectionService.setCellSelection(self, column, true); + else self.selectionService.setCellSelection(self, column, false); + }; +}; /*********************************************** * FILE: ..\src\classes\searchProvider.js @@ -1781,74 +2150,130 @@ window.kg.SelectionService = function (grid) { var self = this; self.multi = grid.config.multiSelect; self.selectedItems = grid.config.selectedItems; + self.selectedCells = grid.config.selectedCells; self.selectedIndex = grid.config.selectedIndex; self.lastClickedRow = undefined; self.ignoreSelectedItemChanges = false; // flag to prevent circular event loops keeping single-select var in sync self.rowFactory = {}; - self.Initialize = function (rowFactory) { + self.Initialize = function (rowFactory) { self.rowFactory = rowFactory; }; - - // function to manage the selection action of a data item (entity) - self.ChangeSelection = function (rowItem, evt) { - grid.$$selectionPhase = true; - if (evt && evt.shiftKey && self.multi) { - if (self.lastClickedRow) { - var thisIndx = grid.filteredData.indexOf(rowItem.entity); - var prevIndx = grid.filteredData.indexOf(self.lastClickedRow.entity); - if (thisIndx == prevIndx) { - return false; - } - prevIndx++; - if (thisIndx < prevIndx) { - thisIndx = thisIndx ^ prevIndx; - prevIndx = thisIndx ^ prevIndx; - thisIndx = thisIndx ^ prevIndx; - } - var rows = []; - for (; prevIndx <= thisIndx; prevIndx++) { - rows.push(self.rowFactory.rowCache[prevIndx]); - } - if (rows[rows.length - 1].beforeSelectionChange(rows, evt)) { - $.each(rows, function(i, ri) { - ri.selected(true); - ri.entity[SELECTED_PROP] = true; - if (self.selectedItems.indexOf(ri.entity) === -1) { - self.selectedItems.push(ri.entity); - } - }); - rows[rows.length - 1].afterSelectionChange(rows, evt); - } - self.lastClickedRow = rows[rows.length - 1]; - return true; - } - } else if (!self.multi) { - if (self.lastClickedRow && self.lastClickedRow != rowItem) { - self.setSelection(self.lastClickedRow, false); - } - self.setSelection(rowItem, grid.config.keepLastSelected ? true : !rowItem.selected()); - } else { - self.setSelection(rowItem, !rowItem.selected()); - } - self.lastClickedRow = rowItem; - grid.$$selectionPhase = false; + + // function to manage the selection action of a data item (entity) + self.ChangeSelection = function (rowItem, evt) { + grid.$$selectionPhase = true; + if (evt && !(evt.ctrlKey || evt.shiftKey) && self.multi) { + // clear selection + self.toggleSelectAll(false); + } + if (evt && evt.shiftKey && self.multi) { + if (self.lastClickedRow) { + var thisIndx = self.rowFactory.parsedData.indexOf(rowItem.entity); + var prevIndx = self.rowFactory.parsedData.indexOf(self.lastClickedRow.entity); + if (thisIndx == -1) thisIndx = grid.filteredData().indexOf(rowItem.entity); + if (prevIndx == -1) prevIndx = grid.filteredData().indexOf(self.lastClickedRow.entity); + + + if (thisIndx == prevIndx) { + grid.$$selectionPhase = false; + return false; + } + prevIndx++; + if (thisIndx < prevIndx) { + thisIndx = thisIndx ^ prevIndx; + prevIndx = thisIndx ^ prevIndx; + thisIndx = thisIndx ^ prevIndx; + } + var rows = []; + for (; prevIndx <= thisIndx; prevIndx++) { + var row = self.rowFactory.rowCache[prevIndx]; + if (!row) row = { + entity: self.rowFactory.parsedData[prevIndx] || grid.filteredData.peek()[prevIndx] + }; + rows.push(row); + } + if (rowItem.beforeSelectionChange(rows, evt)) { + $.each(rows, function(i, ri) { + if (ri.selected) ri.selected(true); + ri.entity[SELECTED_PROP] = true; + if (self.selectedItems().indexOf(ri.entity) === -1) { + self.selectedItems.peek().push(ri.entity); + } + }); + self.selectedItems.notifySubscribers(self.selectedItems()); + rows[rows.length - 1].afterSelectionChange(rows, evt); + } + self.lastClickedRow = rows[rows.length - 1]; + grid.$$selectionPhase = false; + return true; + } + } else if (!self.multi) { + if (self.lastClickedRow && self.lastClickedRow != rowItem) { + self.setSelection(self.lastClickedRow, false); + } + self.setSelection(rowItem, grid.config.keepLastSelected ? true : !rowItem.selected()); + } else { + self.setSelection(rowItem, !rowItem.selected()); + } + self.lastClickedRow = rowItem; + grid.$$selectionPhase = false; return true; }; + self.setCellSelection = function (rowItem, column, isSelected) { + var field = column.field; + if (isSelected) { + rowItem.cellSelection.push(field); + self.selectedCells.push({ + entity: rowItem.entity, + column: column, + field: field + }); + } else { + var index = rowItem.cellSelection().indexOf(field); + rowItem.cellSelection.splice(index, 1); + self.selectedCells(self.selectedCells().filter(function (a) { + return !(a.entity == rowItem.entity && a.field == field); + })); + } + rowItem.entity[CELLSELECTED_PROP] = rowItem.cellSelection(); + if (rowItem.cellSelection().length) self.setSelection(rowItem, true); + else self.setSelection(rowItem, false); + }; + + self.updateCellSelection = function (rowItem, cellSelection) { + if (cellSelection instanceof Array) { + var cellsToSelect = cellSelection.concat(); + cellSelection.length = 0; + cellsToSelect.forEach(function (a) { + var column = grid.columns.peek().filter(function (b) { + return a == b.field; + })[0]; + if (column) { + self.setCellSelection(rowItem, column, true); + } + }); + } + }; // just call this func and hand it the rowItem you want to select (or de-select) self.setSelection = function(rowItem, isSelected) { - rowItem.selected(isSelected) ; - rowItem.entity[SELECTED_PROP] = isSelected; + self.setSelectionQuiet(rowItem, isSelected); if (!isSelected) { var indx = self.selectedItems.indexOf(rowItem.entity); - self.selectedItems.splice(indx, 1); + if (indx != -1) self.selectedItems.splice(indx, 1); } else { if (self.selectedItems.indexOf(rowItem.entity) === -1) { self.selectedItems.push(rowItem.entity); } } }; + + self.setSelectionQuiet = function (rowItem, isSelected) { + if (ko.isObservable(rowItem.selected)) rowItem.selected(isSelected); + rowItem.entity[SELECTED_PROP] = isSelected; + if (!isSelected) rowItem.cellSelection([]); + }; // @return - boolean indicating if all items are selected or not // @val - boolean indicating whether to select all/de-select all @@ -1869,6 +2294,225 @@ window.kg.SelectionService = function (grid) { } }); }; + + self.getEntitySelection = function (items) { + if (!items) items = self.selectedItems(); + var result = []; + items.forEach(function (a) { + if (a.isAggRow) { + var children = a.children.length ? a.children : a.aggChildren; + result = result.concat(self.getEntitySelection(children)); + } else { + result.push(a); + } + }); + return result; + }; + + self.RemoveSelectedRows = function () { + var itemsToDelete = self.getEntitySelection(); + grid.sortedData(grid.sortedData().filter(function (a) { + return itemsToDelete.indexOf(a) == -1; + })); + }; +}; + +/*********************************************** +* FILE: ..\src\classes\aggregationProvider.js +***********************************************/ +window.kg.AggregationProvider = function (grid) { + var self = this; + function getGridCount(row, field, condition) { + var count = 0; + if (row.aggChildren && row.aggChildren.length) { + for (var i = row.aggChildren.length - 1; i >= 0; i--) { + count += getGridCount(row.aggChildren[i], field, condition); + } + } else if (row.children) { + var children = row.children; + if (condition === "") count = Number(children.length) || 0; + else if (condition === "true") { + for (var idx = children.length - 1; idx >= 0; idx--) { + var child = children[idx]; + var val = child[field.field]; + if (val === "true" || val === true) count++; + } + } + else count = "#NotImplemented"; + } else { + // this is a non agg row and we're counting it? + // count = 1; + count = 0; + } + return count; + } + + function getGridSum(row, field) { + if (!row) return; + + var result = 0; + if (row.aggChildren && row.aggChildren.length > 0) { + //TODO: implement koUnwrapper, or refrence ko. + var aggChildren = row.aggChildren; + for (var idx = aggChildren.length - 1; idx >= 0; idx--) { + var aggChild = aggChildren[idx]; + if (aggChild) { + result += getGridSum(aggChild, field); + } + } + } else if (row.children && row.children.length) { + var children = row.children; + for (var i = children.length - 1; i >= 0; i--) { + var child = children[i]; + if (child && child[field.field]) { + // TODO: add a field to entity to indicate grouping, use that to deturmine whether to sum or count the fields. + var val = ko.utils.unwrapObservable(child[field.field]); + if (Number(val)) result += Number(val); + //else if (val == "true") result += 1; + } + } + } + return result; + } + + function getGridMin(row, field, min) { + var result, + children, + getVal; + min = min || function (a, b) {return a > b ? b : a;}; + var getMin = function (a, b) {if (typeof a != "number") return b; if (typeof b != "number") return a; return min (a, b); }; + if (row.aggChildren && row.aggChildren.length > 0) { + children = row.aggChildren; + getVal = getGridMin; + + } else if (row.children && row.children.length) { + children = row.children; + getVal = function (row) {return row[field.field];}; + } else { + return; + } + for (var idx = children.length - 1; idx >= 0; idx--) { + var child = children[idx]; + var val = /*Number*/(getVal(child, field, getMin)); + result = getMin(result, val); + } + return result; + } + + function getGridAny(row, field) { + return getGridMin(row, field, function (a, b) {return a || b;}); + } + function getWeightedSum(row, field) { + if (!row) return; + + var result = 0; + if (row.aggChildren && row.aggChildren.length > 0) { + //TODO: implement koUnwrapper, or refrence ko. + var aggChildren = row.aggChildren; + for (var idx = aggChildren.length - 1; idx >= 0; idx--) { + var aggChild = aggChildren[idx]; + if (aggChild) { + result += getWeightedSum(aggChild, field); + } + } + } else if (row.children && row.children.length) { + var children = row.children; + for (var i = children.length - 1; i >= 0; i--) { + var child = children[i]; + if (child && child[field.field]) { + // TODO: add a field to entity to indicate grouping, use that to deturmine whether to sum or count the fields. + var val = child[field.field] * child[field.field + "_weight"]; + result += Number(val); + } + } + } + return result; + } + + function getFld(field, flexView) { + if (!flexView) return 1; + var column = flexView.flexFields().filter(function (a) {return a.field == field;})[0]; + if (column) { + return column.fld; + } + } + + self.sum = { + sql: function (field) { + return 'sum(' + field.fld + ')'; + }, + grid: function (row, field) { + return getGridSum(row, field); + } + }; + self.asis = { + sql: function (field) { + return field.fld; + }, + grid: function (row, field) { + return row[field.field]; + } + }; + self.count = { + sql: function (field) { + return 'sum(iif(' + field.fld + ',1,0))'; + }, + grid: function (row, field) { + return getGridSum(row, field); + } + }; + self.gridCount = { + grid: function (row, field) { + var text = getGridCount(row, field, ""); + return text; + } + }; + self.sibling = function (siblingField) { + return { + grid: function (row, field) { return getGridMin(row, {field: siblingField}, function (a, b) { return a > b ? a : b; }); } + }; + }; + self.average = { + sql: function (field) { + return 'avg(' + field.fld + ')'; + }, + grid: function (row, field) { + return getWeightedSum(row, field) / getGridSum(row, {field: field.field + "_weight"}); + } + }; + self.min = { + sql: function (field) { + return 'min(' + field.fld + ')'; + }, + grid: getGridMin + }; + self.max = { + sql: function (field) { + return 'max(' + field.fld + ')'; + }, + grid: function (row, field) { return getGridMin(row, field, function (a, b) { return a > b ? a : b; }); } + }; + self.weightedAvg = { + sql: function (field, flexView) { + var fld = field.fld; + var weight = getFld(field.weightedColumn, flexView); + return 'sum(' + fld + ' * ' + weight + ')' + ' / sum(' + weight + ')'; + }, + grid: function (row, field) { + return getWeightedSum(row, field) / getGridSum(row, {field: field.weightedColumn}); + } + }; + self.countDistinct = { + sql: function (field, flexView) { + return 'count(distinct ' + field.fld + ")"; + }, + grid: function (row, field) { + return getGridSum(row, field); + } + }; + self.any = { + grid: getGridAny + }; }; /*********************************************** @@ -1889,6 +2533,11 @@ window.kg.StyleProvider = function (grid) { }); grid.viewportStyle = ko.computed(function() { return { "width": grid.rootDim.outerWidth() + "px", "height": grid.viewportDimHeight() + "px" }; + }); + grid.rowFooterStyle = ko.computed(function () { + var result = $.extend({}, grid.headerStyle()); + result.bottom = kg.domUtilityService.ScrollW + 'px'; + return result; }); grid.footerStyle = ko.computed(function () { return { "width": grid.rootDim.outerWidth() + "px", "height": grid.config.footerRowHeight + "px" }; @@ -2000,8 +2649,8 @@ window.kg.sortService = { return numA - numB; }, sortAlpha: function(a, b) { - var strA = a.toLowerCase(), - strB = b.toLowerCase(); + var strA = ((a || '') + '').toLowerCase(), + strB = ((b || '') + '').toLowerCase(); return strA == strB ? 0 : (strA < strB ? -1 : 1); }, sortBool: function(a, b) { @@ -2064,7 +2713,7 @@ window.kg.sortService = { d = '0' + d; } dateA = y + m + d; - mtch = b.match(dateRE); + mtch = b.match(window.kg.sortService.dateRE); y = mtch[3]; d = mtch[2]; m = mtch[1]; @@ -2092,32 +2741,48 @@ window.kg.sortService = { // grab the metadata for the rest of the logic var col = sortInfo.column, direction = sortInfo.direction, - sortFn, - item; - //see if we already figured out what to use to sort the column - if (window.kg.sortService.colSortFnCache[col.field]) { - sortFn = window.kg.sortService.colSortFnCache[col.field]; - } else if (col.sortingAlgorithm != undefined) { - sortFn = col.sortingAlgorithm; - window.kg.sortService.colSortFnCache[col.field] = col.sortingAlgorithm; - } else { // try and guess what sort function to use - item = unwrappedData[0]; - if (!item) { - return; - } - sortFn = kg.sortService.guessSortFn(item[col.field]); - //cache it - if (sortFn) { - window.kg.sortService.colSortFnCache[col.field] = sortFn; - } else { - // we assign the alpha sort because anything that is null/undefined will never get passed to - // the actual sorting function. It will get caught in our null check and returned to be sorted - // down to the bottom - sortFn = window.kg.sortService.sortAlpha; + item, + cols; + + + if (col.field == "Group") cols = sortInfo.grid.configGroups(); + else cols = [col]; + + var sortInfos = cols.map(function (col) { + var sortFn; + //see if we already figured out what to use to sort the column + if (window.kg.sortService.colSortFnCache[col.field]) { + sortFn = window.kg.sortService.colSortFnCache[col.field]; + } else if (col.sortingAlgorithm != undefined) { + sortFn = col.sortingAlgorithm; + window.kg.sortService.colSortFnCache[col.field] = col.sortingAlgorithm; + } else { // try and guess what sort function to use + item = unwrappedData[0]; + if (!item) { + return; + } + var val = item[col.field]; + if (typeof sortInfo.grid.config.formatString == "function") val = sortInfo.grid.config.formatString(val, col); + sortFn = kg.sortService.guessSortFn(val); + //cache it + if (sortFn) { + window.kg.sortService.colSortFnCache[col.field] = sortFn; + } else { + // we assign the alpha sort because anything that is null/undefined will never get passed to + // the actual sorting function. It will get caught in our null check and returned to be sorted + // down to the bottom + sortFn = window.kg.sortService.sortAlpha; + } } - } - //now actually sort the data - unwrappedData.sort(function (itemA, itemB) { + return { + col: col, + direction: direction, + sortFn: sortFn + }; + }); + + var sortFn; + var outerSortFn = function (itemA, itemB) { var propA = window.kg.utils.evalProperty(itemA, col.field); var propB = window.kg.utils.evalProperty(itemB, col.field); // we want to force nulls and such to the bottom when we sort... which effectively is "greater than" @@ -2128,12 +2793,31 @@ window.kg.sortService = { } else if (!propB) { return -1; } + //allow the user to preprocess the data + if (typeof sortInfo.grid.config.formatString == "function") { + propA = sortInfo.grid.config.formatString(propA, col); + propB = sortInfo.grid.config.formatString(propB, col); + } //made it this far, we don't have to worry about null & undefined if (direction === ASC) { return sortFn(propA, propB); } else { return 0 - sortFn(propA, propB); } + }; + //now actually sort the data + unwrappedData.sort(function (itemA, itemB) { + var result = 0; + var i = 0; + while(!result && i < sortInfos.length) { + if (sortInfos[i]) { + col = sortInfos[i].col; + sortFn = sortInfos[i].sortFn; + result = outerSortFn(itemA, itemB); + } + i++; + } + return result; }); data(unwrappedData); return; @@ -2174,6 +2858,8 @@ window.kg.domUtilityService = { //Headers grid.$topPanel = grid.$root.find(".kgTopPanel"); grid.$groupPanel = grid.$root.find(".kgGroupPanel"); + grid.$footerContainer = grid.$root.find(".kgRowFooter"); + grid.$footerScroller = grid.$root.find(".kgRowFooterScroller"); grid.$headerContainer = grid.$topPanel.find(".kgHeaderContainer"); grid.$headerScroller = grid.$topPanel.find(".kgHeaderScroller"); grid.$headers = grid.$headerScroller.children(); diff --git a/build/build-order.txt b/build/build-order.txt index 39c5d881..226953cc 100644 --- a/build/build-order.txt +++ b/build/build-order.txt @@ -5,9 +5,10 @@ ..\src\templates\gridTemplate.html ..\src\templates\rowTemplate.html ..\src\templates\cellTemplate.html -..\src\templates\aggregateTemplate.html +..\src\templates\aggregateTemplate.js ..\src\templates\headerRowTemplate.html ..\src\templates\headerCellTemplate.html +..\src\templates\aggCellTemplate.html ..\src\bindingHandlers\ko-grid.js ..\src\bindingHandlers\kg-row.js ..\src\bindingHandlers\kg-cell.js @@ -24,6 +25,7 @@ ..\src\classes\row.js ..\src\classes\searchProvider.js ..\src\classes\selectionService.js +..\src\classes\aggregationProvider.js ..\src\classes\styleProvider.js ..\src\classes\sortService.js ..\src\classes\domUtilityService.js diff --git a/build/build.bat b/build/build.bat new file mode 100644 index 00000000..f715a341 --- /dev/null +++ b/build/build.bat @@ -0,0 +1,2 @@ +powershell .\build.ps1 +powershell .\buildamd.ps1 \ No newline at end of file diff --git a/build/build.ps1 b/build/build.ps1 index 3b05d060..fd73a831 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -15,7 +15,7 @@ Set-Content $TempFile "/***********************************************"; Add-Content $TempFile "* koGrid JavaScript Library"; Add-Content $TempFile "* Authors: https://github.com/ericmbarnard/koGrid/blob/master/README.md"; Add-Content $TempFile "* License: MIT (http://www.opensource.org/licenses/mit-license.php)"; -Add-Content $TempFile "* Compiled At: $compileTime"; +#Add-Content $TempFile "* Compiled At: $compileTime"; Add-Content $TempFile "***********************************************/`n" Add-Content $TempFile "(function (window) {"; Add-Content $TempFile "'use strict';"; @@ -39,4 +39,4 @@ Add-Content $TempFile "}(window));"; Get-Content $TempFile | Set-Content $OutputFile; Remove-Item $TempFile -Force; Copy-Item $OutputFile $FinalFile; -Write-Host "Build Succeeded!" +Write-Host "Build Succeeded!" \ No newline at end of file diff --git a/build/buildamd.ps1 b/build/buildamd.ps1 new file mode 100644 index 00000000..18b9fe44 --- /dev/null +++ b/build/buildamd.ps1 @@ -0,0 +1,44 @@ + +$CurrentDir = (Get-Location).Path; +$OutPutFile = $CurrentDir + "\koGrid.AMD.debug.js"; +$TempFile = $OutPutFile + ".temp"; +$FinalFile = "..\koGrid-2.1.1.AMD.debug.js"; +$BuildOrder = $CurrentDir + "\build-order.txt"; +$commentStart = ""; + +Write-Host "JSBuild Starting..."; +$files = Get-Content $BuildOrder; +$compileTime = Get-Date; + +Set-Content $TempFile "/***********************************************"; +Add-Content $TempFile "* koGrid JavaScript Library"; +Add-Content $TempFile "* Authors: https://github.com/ericmbarnard/koGrid/blob/master/README.md"; +Add-Content $TempFile "* License: MIT (http://www.opensource.org/licenses/mit-license.php)"; +#Add-Content $TempFile "* Compiled At: $compileTime"; +Add-Content $TempFile "***********************************************/`n" +Add-Content $TempFile "define(['jquery', 'knockout'], function (`$, ko) {"; +Add-Content $TempFile "(function (window) {"; +Add-Content $TempFile "'use strict';"; +Foreach ($file in $files){ + # Wrap each file output in a new line + Write-Host "Building... $file"; + Add-Content $TempFile "`n/***********************************************`n* FILE: $file`n***********************************************/"; + $fileContents = Get-Content $file | where {!$_.StartsWith("///")}; + if ($fileContents[0].StartsWith("', + iElems[0]) ; + return version > 4 ? version : undefined; + })() +}; + +$.extend(window.kg.utils, { + isIe6: (function() { + return window.kg.utils.ieVersion === 6; + })(), + isIe7: (function() { + return window.kg.utils.ieVersion === 7; + })(), + isIe: (function() { + return window.kg.utils.ieVersion !== undefined; + })() +}); + +/*********************************************** +* FILE: ..\src\templates\gridTemplate.html +***********************************************/ +window.kg.defaultGridTemplate = function(){ return '
Drag a column header here and drop it to group by that column
  • x
Choose Columns:
';}; + +/*********************************************** +* FILE: ..\src\templates\rowTemplate.html +***********************************************/ +window.kg.defaultRowTemplate = function(){ return '
';}; + +/*********************************************** +* FILE: ..\src\templates\cellTemplate.html +***********************************************/ +window.kg.defaultCellTemplate = function(){ return '
';}; + +/*********************************************** +* FILE: ..\src\templates\aggregateTemplate.js +***********************************************/ +window.kg.aggregateTemplate = function () { + return window.kg.defaultRowTemplate(); +}; + +/*********************************************** +* FILE: ..\src\templates\headerRowTemplate.html +***********************************************/ +window.kg.defaultHeaderRowTemplate = function(){ return '
';}; + +/*********************************************** +* FILE: ..\src\templates\headerCellTemplate.html +***********************************************/ +window.kg.defaultHeaderCellTemplate = function(){ return '
';}; + +/*********************************************** +* FILE: ..\src\templates\aggCellTemplate.html +***********************************************/ +window.kg.aggCellTemplate = function(){ return '
';}; + +/*********************************************** +* FILE: ..\src\bindingHandlers\ko-grid.js +***********************************************/ +ko.bindingHandlers['koGrid'] = (function () { + return { + 'init': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + var options = valueAccessor(); + var elem = $(element); + options.gridDim = new window.kg.Dimension({ outerHeight: ko.observable(elem.height()), outerWidth: ko.observable(elem.width()) }); + var grid = new window.kg.Grid(options); + var gridElem = $(options.gridTemplate || window.kg.defaultGridTemplate()); + // if it is a string we can watch for data changes. otherwise you won't be able to update the grid data + options.data.subscribe(function () { + if (grid.$$selectionPhase) { + return; + } + grid.searchProvider.evalFilter(); + grid.refreshDomSizes(); + }); + // if columndefs are observable watch for changes and rebuild columns. + if (ko.isObservable(options.columnDefs)) { + options.columnDefs.subscribe(function (newDefs) { + grid.columns([]); + grid.config.columnDefs = newDefs; + grid.buildColumns(); + grid.configureColumnWidths(); + }); + } + //set the right styling on the container + elem.addClass("koGrid").addClass(grid.gridId.toString()); + elem.append(gridElem); + grid.$userViewModel = bindingContext.$data; + ko.applyBindings(grid, gridElem[0]); + //walk the element's graph and the correct properties on the grid + window.kg.domUtilityService.AssignGridContainers(elem, grid); + grid.configureColumnWidths(); + grid.refreshDomSizes(); + //now use the manager to assign the event handlers + grid.eventProvider = new window.kg.EventProvider(grid); + //initialize plugins. + $.each(grid.config.plugins, function (i, p) { + if (typeof p.onGridInit === 'function') { + p.onGridInit(grid); + } + }); + window.kg.domUtilityService.BuildStyles(grid); + return { controlsDescendantBindings: true }; + } + }; +}()); + +/*********************************************** +* FILE: ..\src\bindingHandlers\kg-row.js +***********************************************/ +ko.bindingHandlers['kgRow'] = (function () { + return { + 'init': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + var row = valueAccessor(); + var grid = row.$grid = bindingContext.$parent; + var source; + if (row.isAggRow) { + source = window.kg.aggregateTemplate(); + } else { + source = grid.rowTemplate; + } + var compile = function(html) { + var rowElem = $(html); + row.$userViewModel = bindingContext.$parent.$userViewModel; + ko.applyBindings(row, rowElem[0]); + $(element).html(rowElem); + }; + if (source.then) { + source.then(function (p) { + compile(p); + }); + } else { + compile(source); + } + return { controlsDescendantBindings: true }; + } + }; +}()); + +/*********************************************** +* FILE: ..\src\bindingHandlers\kg-cell.js +***********************************************/ +ko.bindingHandlers['kgCell'] = (function () { + return { + 'init': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + bindingContext.$userViewModel = bindingContext.$parent.$userViewModel; + var $element = $(element); + var compile = function (html) { + //viewModel.$cellTemplate = $(html); + $element.html(html); + ko.applyBindings(bindingContext, $element.children()[0]); + }; + // if (viewModel.$cellTemplate) { + // $element.html(viewModel.$cellTemplate); + // ko.applyBindings(bindingContext, $element.children()[0]); + // } + if (viewModel.cellTemplate.then) { + viewModel.cellTemplate.then(function(p) { + compile(p); + }); + } else { + compile(viewModel.cellTemplate); + } + return { controlsDescendantBindings: true }; + } + }; +}()); + +/*********************************************** +* FILE: ..\src\bindingHandlers\kg-header-row.js +***********************************************/ +ko.bindingHandlers['kgHeaderRow'] = (function () { + return { + 'init': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + bindingContext.$userViewModel = bindingContext.$data.$userViewModel; + var compile = function(html) { + var headerRow = $(html); + ko.applyBindings(bindingContext, headerRow[0]); + $(element).html(headerRow); + }; + if (viewModel.headerRowTemplate.then) { + viewModel.headerRowTemplate.then(function (p) { + compile(p); + }); + } else { + compile(viewModel.headerRowTemplate); + } + return { controlsDescendantBindings: true }; + } + }; +}()); + +/*********************************************** +* FILE: ..\src\bindingHandlers\kg-header-cell.js +***********************************************/ +ko.bindingHandlers['kgHeaderCell'] = (function () { + return { + 'init': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + var newContext = bindingContext.extend({ $grid: bindingContext.$parent, $userViewModel: bindingContext.$parent.$userViewModel }); + var compile = function (html) { + var headerCell = $(html); + ko.applyBindings(newContext, headerCell[0]); + $(element).html(headerCell); + }; + if (viewModel.headerCellTemplate.then) { + viewModel.headerCellTemplate.then(function (p) { + compile(p); + }); + } else { + compile(viewModel.headerCellTemplate); + } + return { controlsDescendantBindings: true }; + } + }; +}()); + +/*********************************************** +* FILE: ..\src\bindingHandlers\kg-mouse-events.js +***********************************************/ +ko.bindingHandlers['mouseEvents'] = (function () { + return { + 'init': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + var eFuncs = valueAccessor(); + if (eFuncs.mouseDown) { + $(element).mousedown(eFuncs.mouseDown); + } + } + }; +}()); + +/*********************************************** +* FILE: ..\src\classes\aggregate.js +***********************************************/ +window.kg.Aggregate = function (aggEntity, config, rowFactory, selectionService) { + var self = this; + self.index = 0; + self.offsetTop = ko.observable(0); + self.entity = aggEntity; + self.label = ko.observable(aggEntity.gLabel); + self.field = aggEntity.gField; + self.depth = aggEntity.gDepth; + self.parent = aggEntity.parent; + self.children = aggEntity.children; + self.aggChildren = aggEntity.aggChildren; + self.aggIndex = aggEntity.aggIndex; + self.collapsed = ko.observable(true); + self.isAggRow = true; + self.offsetLeft = ko.observable((aggEntity.gDepth * 25).toString() + 'px'); + self.aggLabelFilter = aggEntity.aggLabelFilter; + self.toggleExpand = function() { + var c = self.collapsed(); + self._setExpand(!c); + self.notifyChildren(); + }; + self.setExpand = function (state) { + self._setExpand(state); + self.notifyChildren(); + }; + self._setExpand = function (state, child) { + if (!child) { + self.collapsed(state); + self.entity._kg_collapsed = self.collapsed(); + } + if (self.parent && self.entity[KG_HIDDEN]) state = true; + $.each(self.aggChildren, function (i, child) { + var c = !!state || child.collapsed(); + child.entity[KG_HIDDEN] = state; + child._setExpand(c, true); + }); + $.each(self.children, function (i, child) { + child[KG_HIDDEN] = !!state; + }); + // var foundMyself = false; + // $.each(rowFactory.aggCache, function (i, agg) { + // if (foundMyself) { + // var offset = (30 * self.children.length); + // var c = self.collapsed(); + // agg.offsetTop(c ? agg.offsetTop() - offset : agg.offsetTop() + offset); + // } else { + // if (i == self.aggIndex) { + // foundMyself = true; + // } + // } + // }); + }; + self.notifyChildren = function() { + rowFactory.rowCache = []; + + rowFactory.renderedChange(); + }; + self.aggClass = ko.computed(function() { + return self.collapsed() ? "kgAggArrowCollapsed" : "kgAggArrowExpanded"; + }); + self.totalChildren = ko.computed(function() { + if (self.aggChildren.length > 0) { + var i = 0; + var recurse = function (cur) { + if (cur.aggChildren.length > 0) { + $.each(cur.aggChildren, function (x, a) { + recurse(a); + }); + } else { + i += cur.children.length; + } + }; + recurse(self); + return i; + } else { + return self.children.length; + } + }); + self.isEven = ko.observable(false); + self.isOdd = ko.observable(false); + self.canSelectRows = config.canSelectRows; + self.selectedItems = config.selectedItems; + self.selectionService = selectionService; + + self.selected = ko.observable(false); + self.cellSelection = ko.observableArray(aggEntity[CELLSELECTED_PROP] || []); + self.continueSelection = function(event) { + self.selectionService.ChangeSelection(self, event); + }; + self.toggleSelected = function (row, event) { + if (!self.canSelectRows) { + return true; + } + var element = event.target || event; + //check and make sure its not the bubbling up of our checked 'click' event + if (element.type == "checkbox") { + self.selected(!self.selected()); + } + if (config.selectWithCheckboxOnly && element.type != "checkbox"){ + return true; + } else { + if (self.beforeSelectionChange(self, event)) { + self.continueSelection(event); + return self.afterSelectionChange(self, event); + } + } + return false; + }; + //selectify the entity + if (self.entity[SELECTED_PROP] === undefined) { + self.entity[SELECTED_PROP] = false; + } else { + // or else maintain the selection set by the entity. + self.selectionService.setSelection(self, self.entity[SELECTED_PROP]); + self.selectionService.updateCellSelection(self, self.entity[CELLSELECTED_PROP]); + } + self.beforeSelectionChange = config.beforeSelectionChangeCallback; + self.afterSelectionChange = config.afterSelectionChangeCallback; + self.propertyCache = {}; + self.getProperty = function (path) { + return self.propertyCache[path] || (self.propertyCache[path] = window.kg.utils.evalProperty(self.entity, path)); + }; + self.selectCell = function (column) { + var field = column.field; + var index = self.cellSelection().indexOf(field); + if (index == -1) self.selectionService.setCellSelection(self, column, true); + else self.selectionService.setCellSelection(self, column, false); + }; +}; + +/*********************************************** +* FILE: ..\src\classes\column.js +***********************************************/ +window.kg.Column = function (config, grid) { + var self = this, + colDef = config.colDef, + delay = 500, + clicks = 0, + timer = null; + self.config = config; + self.eventTaget = undefined; + self.width = colDef.width; + self.groupIndex = ko.observable(config.colDef.groupIndex || 0); + self.isGroupedBy = ko.observable(false); + self.collapsed = ko.observable(); + self.aggClass = ko.computed(function() { + return self.collapsed() ? "kgAggArrowCollapsed" : "kgAggArrowExpanded"; + }); + self.groupedByClass = ko.computed(function(){ return self.isGroupedBy() ? "kgGroupedByIcon": "kgGroupIcon";}); + self.sortable = ko.observable(false); + self.resizable = ko.observable(false); + self.selectable = ko.observable(false); + self.minWidth = !colDef.minWidth ? 50 : colDef.minWidth; + self.maxWidth = !colDef.maxWidth ? 9000 : colDef.maxWidth; + self.headerRowHeight = config.headerRowHeight; + self.displayName = ko.observable(colDef.displayName || colDef.field); + self.index = config.index; + self.isAggCol = config.isAggCol; + self.cellClass = ko.observable(colDef.cellClass || ""); + self.cellFilter = colDef.cellFilter || colDef.cellFormatter; + self.field = colDef.field; + self.aggLabelFilter = colDef.cellFilter || colDef.cellFormatter || colDef.aggLabelFilter || colDef.aggLabelFormatter; + self._visible = ko.observable(window.kg.utils.isNullOrUndefined(colDef.visible) || colDef.visible); + self.visible = ko.computed({ + read: function() { + if (grid && grid.config.showGroupedColumns === false && self.isGroupedBy()) { + return false; + } + return self._visible(); + }, + write: function(val) { + self.toggleVisible(val); + } + }); + if (config.enableSort) { + self.sortable(window.kg.utils.isNullOrUndefined(colDef.sortable) || colDef.sortable); + } + if (config.enableResize) { + self.resizable(window.kg.utils.isNullOrUndefined(colDef.resizable) || colDef.resizable); + } + self.selectable(colDef.selectable); + self.sortDirection = ko.observable(colDef.sortDirection); + if (self.sortDirection()) { + // This line would prevent multiple columns being sorted simultaneously + // if (grid.lastSortedColumn()) grid.lastSortedColumn().sortDirection(""); + grid.lastSortedColumn = self; + } + self.sortingAlgorithm = colDef.sortFn; + self.headerClass = ko.observable(colDef.headerClass); + self.headerCellTemplate = colDef.headerCellTemplate || window.kg.defaultHeaderCellTemplate(); + self.cellTemplate = colDef.cellTemplate || grid.config.cellTemplate || window.kg.defaultCellTemplate(); + if (colDef.cellTemplate && !TEMPLATE_REGEXP.test(colDef.cellTemplate)) { + self.cellTemplate = window.kg.utils.getTemplatePromise(colDef.cellTemplate); + } + if (colDef.headerCellTemplate && !TEMPLATE_REGEXP.test(colDef.headerCellTemplate)) { + self.headerCellTemplate = window.kg.utils.getTemplatePromise(colDef.headerCellTemplate); + } + self.getProperty = function (row) { + var ret; + if (self.cellFilter) { + ret = self.cellFilter(row.getProperty(self.field)); + } else { + ret = row.getProperty(self.field); + } + return ret; + }; + self.toggleVisible = function (val) { + var v; + if (window.kg.utils.isNullOrUndefined(val) || typeof val == "object") { + v = !self._visible(); + } else { + v = val; + } + self._visible(v); + window.kg.domUtilityService.BuildStyles(grid); + }; + + self.showSortButtonUp = ko.computed(function () { + return self.sortable ? self.sortDirection() === DESC : self.sortable; + }); + self.showSortButtonDown = ko.computed(function () { + return self.sortable ? self.sortDirection() === ASC : self.sortable; + }); + self.noSortVisible = ko.computed(function () { + return !self.sortDirection(); + }); + self.sort = function () { + if (!self.sortable()) { + return true; // column sorting is disabled, do nothing + } + var dir = self.sortDirection() === ASC ? DESC : ASC; + self.sortDirection(dir); + config.sortCallback(self, dir); + return false; + }; + self.gripClick = function (data, event) { + event.stopPropagation(); + clicks++; //count clicks + if (clicks === 1) { + timer = setTimeout(function () { + //Here you can add a single click action. + clicks = 0; //after action performed, reset counter + }, delay); + } else { + clearTimeout(timer); //prevent single-click action + config.resizeOnDataCallback(self); //perform double-click action + clicks = 0; //after action performed, reset counter + } + }; + self.gripOnMouseDown = function (event) { + event.stopPropagation(); + if (event.ctrlKey) { + self.toggleVisible(); + window.kg.domUtilityService.BuildStyles(grid); + grid.config.columnsChanged(grid.columns.peek()); + return true; + } + self.eventTaget = event.target.parentElement; + self.eventTaget.style.cursor = 'col-resize'; + self.startMousePosition = event.clientX; + self.origWidth = self.width; + $(document).mousemove(self.onMouseMove); + $(document).mouseup(self.gripOnMouseUp); + return false; + }; + self.onMouseMove = function (event) { + event.stopPropagation(); + var diff = event.clientX - self.startMousePosition; + var newWidth = diff + self.origWidth; + self.width = (newWidth < self.minWidth ? self.minWidth : (newWidth > self.maxWidth ? self.maxWidth : newWidth)); + window.kg.domUtilityService.BuildStyles(grid); + return false; + }; + self.gripOnMouseUp = function (event) { + event.stopPropagation(); + $(document).off('mousemove'); + $(document).off('mouseup'); + self.eventTaget.style.cursor = self.sortable() ? 'pointer' : 'default'; + self.eventTaget = undefined; + grid.config.columnsChanged(grid.columns.peek()); + return false; + }; +}; + +/*********************************************** +* FILE: ..\src\classes\dimension.js +***********************************************/ +window.kg.Dimension = function (options) { + this.outerHeight = null; + this.outerWidth = null; + $.extend(this, options); +}; + +/*********************************************** +* FILE: ..\src\classes\eventProvider.js +***********************************************/ +window.kg.EventProvider = function (grid) { + var self = this; + // The init method gets called during the ng-grid directive execution. + self.colToMove = undefined; + self.groupToMove = undefined; + self.assignEvents = function () { + // Here we set the onmousedown event handler to the header container. + if(grid.config.jqueryUIDraggable){ + grid.$groupPanel.droppable({ + addClasses: false, + drop: function(event) { + self.onGroupDrop(event); + } + }); + $(document).ready(self.setDraggables); + } else { + grid.$groupPanel.on('mousedown', self.onGroupMouseDown).on('dragover', self.dragOver).on('drop', self.onGroupDrop); + grid.$headerScroller.on('mousedown', self.onHeaderMouseDown).on('dragover', self.dragOver).on('drop', self.onHeaderDrop); + if (grid.config.enableRowReordering) { + grid.$viewport.on('mousedown', self.onRowMouseDown).on('dragover', self.dragOver).on('drop', self.onRowDrop); + } + self.setDraggables(); + } + grid.visibleColumns.subscribe(self.setDraggables); + }; + self.dragOver = function(evt) { + evt.preventDefault(); + }; + + //For JQueryUI + self.setDraggables = function(){ + if(!grid.config.jqueryUIDraggable){ + grid.$root.find('.kgHeaderSortColumn').attr('draggable', 'true'); + if (navigator.userAgent.indexOf("MSIE") != -1) + { + //call native IE dragDrop() to start dragging + grid.$root.find('.kgHeaderSortColumn').bind('selectstart', function () { this.dragDrop(); return false; }); + } + } else { + grid.$root.find('.kgHeaderSortColumn').draggable({ + helper: 'clone', + appendTo: 'body', + stack: 'div', + addClasses: false, + start: function(event){ + self.onHeaderMouseDown(event); + } + }).droppable({ + drop: function(event) { + self.onHeaderDrop(event); + } + }); + } + }; + + self.onGroupMouseDown = function(event) { + var groupItem = $(event.target); + // Get the scope from the header container + if(groupItem[0].className !='kgRemoveGroup'){ + var groupItemScope = ko.dataFor(groupItem[0]); + if (groupItemScope instanceof window.kg.Column) { + // set draggable events + if(!grid.config.jqueryUIDraggable){ + groupItem.attr('draggable', 'true'); + } + // Save the column for later. + self.groupToMove = { header: groupItem, groupName: groupItemScope, index: groupItemScope.groupIndex() - 1 }; + } + } else { + self.groupToMove = undefined; + } + self.colToMove = undefined; + }; + + self.onGroupDrop = function(event) { + // clear out the colToMove object + var groupContainer; + var groupScope; + if (self.groupToMove) { + // Get the closest header to where we dropped + groupContainer = $(event.target).closest('.kgGroupElement'); // Get the scope from the header. + if (groupContainer.context.className =='kgGroupPanel') { + grid.configGroups.splice(self.groupToMove.index, 1); + grid.configGroups.push(self.groupToMove.groupName); + } else if (groupContainer[0]) { + groupScope = ko.dataFor(groupContainer[0]); + if (groupScope instanceof window.kg.Column) { + // If we have the same column, do nothing. + if (self.groupToMove.index != groupScope.groupIndex()) { + // Splice the columns + grid.configGroups.splice(self.groupToMove.index, 1); + grid.configGroups.splice(groupScope.groupIndex(), 0, self.groupToMove.groupName); + } + } + } + self.groupToMove = undefined; + grid.fixGroupIndexes(); + } else { + if (grid.configGroups.indexOf(self.colToMove.col) == -1) { + groupContainer = $(event.target).closest('.kgGroupElement'); // Get the scope from the header. + if (groupContainer.context.className =='kgGroupPanel' || groupContainer.context.className =='kgGroupPanelDescription') { + grid.groupBy(self.colToMove.col); + } else { + groupScope = ko.dataFor(groupContainer[0]); + if (groupScope instanceof window.kg.Column) { + // Splice the columns + grid.removeGroup(groupScope.groupIndex()); + } + } + } + self.colToMove = undefined; + } + }; + + //Header functions + self.onHeaderMouseDown = function (event) { + // Get the closest header container from where we clicked. + var headerContainer = $(event.target).closest('.kgHeaderSortColumn'); + if (!headerContainer[0]) { + return true; + } + // Get the scope from the header container + + var headerScope = ko.dataFor(headerContainer[0]); + if (headerScope) { + // Save the column for later. + self.colToMove = { header: headerContainer, col: headerScope }; + } + self.groupToMove = undefined; + return true; + }; + + self.onHeaderDrop = function (event) { + if (!self.colToMove) { + return true; + } + // Get the closest header to where we dropped + var headerContainer = $(event.target).closest('.kgHeaderSortColumn'); + if (!headerContainer[0]) { + return true; + } + // Get the scope from the header. + var headerScope = ko.dataFor(headerContainer[0]); + if (headerScope) { + // If we have the same column, do nothing. + if (self.colToMove.col == headerScope) { + return true; + } + // Splice the columns + var cols = grid.columns.peek(); + cols.splice(self.colToMove.col.index, 1); + cols.splice(headerScope.index, 0, self.colToMove.col); + grid.fixColumnIndexes(); + grid.columns(cols); + // Finally, rebuild the CSS styles. + window.kg.domUtilityService.BuildStyles(grid); + // clear out the colToMove object + self.colToMove = undefined; + } + return true; + }; + + // Row functions + self.onRowMouseDown = function (event) { + // Get the closest row element from where we clicked. + var targetRow = $(event.target).closest('.kgRow'); + if (!targetRow[0]) { + return; + } + // Get the scope from the row element + var rowScope = ko.dataFor(targetRow[0]); + if (rowScope) { + // set draggable events + targetRow.attr('draggable', 'true'); + // Save the row for later. + window.kg.eventStorage.rowToMove = { targetRow: targetRow, scope: rowScope }; + } + }; + + self.onRowDrop = function (event) { + // Get the closest row to where we dropped + var targetRow = $(event.target).closest('.kgRow'); + // Get the scope from the row element. + var rowScope = ko.dataFor(targetRow[0]); + if (rowScope) { + // If we have the same Row, do nothing. + var prevRow = window.kg.eventStorage.rowToMove; + if (prevRow.scope == rowScope) { + return; + } + // Splice the Rows via the actual datasource + var sd = grid.sortedData(); + var i = sd.indexOf(prevRow.scope.entity); + var j = sd.indexOf(rowScope.entity); + grid.sortedData.splice(i, 1); + grid.sortedData.splice(j, 0, prevRow.scope.entity); + grid.searchProvider.evalFilter(); + // clear out the rowToMove object + window.kg.eventStorage.rowToMove = undefined; + // if there isn't an apply already in progress lets start one + } + }; + self.assignGridEventHandlers = function() { + grid.$viewport.scroll(function(e) { + var scrollLeft = e.target.scrollLeft, + scrollTop = e.target.scrollTop; + grid.adjustScrollLeft(scrollLeft); + grid.adjustScrollTop(scrollTop); + }); + grid.$viewport.off('keydown'); + grid.$viewport.on('keydown', function(e) { + return window.kg.moveSelectionHandler(grid, e); + }); + //Chrome and firefox both need a tab index so the grid can recieve focus. + //need to give the grid a tabindex if it doesn't already have one so + //we'll just give it a tab index of the corresponding gridcache index + //that way we'll get the same result every time it is run. + //configurable within the options. + if (grid.config.tabIndex === -1) { + grid.$viewport.attr('tabIndex', window.kg.numberOfGrids); + window.kg.numberOfGrids++; + } else { + grid.$viewport.attr('tabIndex', grid.config.tabIndex); + } + $(window).resize(function() { + window.kg.domUtilityService.UpdateGridLayout(grid); + if (grid.config.maintainColumnRatios) { + grid.configureColumnWidths(); + } + }); + }; + self.assignGridEventHandlers(); + // In this example we want to assign grid events. + self.assignEvents(); +}; + +/*********************************************** +* FILE: ..\src\classes\rowFactory.js +***********************************************/ +window.kg.RowFactory = function (grid) { + var self = this; + // we cache rows when they are built, and then blow the cache away when sorting + self.rowCache = []; + self.aggCache = {}; + self.parentCache = []; // Used for grouping and is cleared each time groups are calulated. + self.dataChanged = true; + self.parsedData = []; + grid.config.parsedDataCache = grid.config.parsedDataCache || ko.observableArray(); + self.parsedDataCache = grid.config.parsedDataCache; + self.rowConfig = {}; + self.selectionService = grid.selectionService; + self.aggregationProvider = new window.kg.AggregationProvider(grid); + self.rowHeight = 30; + self.numberOfAggregates = 0; + self.groupedData = undefined; + self.rowHeight = grid.config.rowHeight; + self.rowConfig = { + canSelectRows: grid.config.canSelectRows, + rowClasses: grid.config.rowClasses, + selectedItems: grid.config.selectedItems, + selectWithCheckboxOnly: grid.config.selectWithCheckboxOnly, + beforeSelectionChangeCallback: grid.config.beforeSelectionChange, + afterSelectionChangeCallback: grid.config.afterSelectionChange + }; + + self.renderedRange = new window.kg.Range(0, grid.minRowsToRender() + EXCESS_ROWS); + // Builds rows for each data item in the 'filteredData' + // @entity - the data item + // @rowIndex - the index of the row + self.buildEntityRow = function(entity, rowIndex) { + var row = self.rowCache[rowIndex]; // first check to see if we've already built it + if (!row) { + // build the row + row = new window.kg.Row(entity, self.rowConfig, self.selectionService); + row.rowIndex(rowIndex + 1); //not a zero-based rowIndex + row.offsetTop((self.rowHeight * rowIndex).toString() + 'px'); + // row.selected(entity[SELECTED_PROP]); + // finally cache it for the next round + self.rowCache[rowIndex] = row; + } + return row; + }; + self.getChildCount = function (row) { + if (row.children && row.children.length) return row.children.length; + else if (row.aggChildren && row.aggChildren.length) { + var total = 0; + row.aggChildren.forEach(function (a) { + total += self.getChildCount(a); + }); + return total; + } + return 0; + }; + self.calcAggContent = function (row, column) { + if (column.field == 'Group') { + return row.label(); + } else if (column.field == row.entity.gField) { + return row.entity.gLabel; + } else if (column.groupIndex() && row.parent) { + if (row.parent) { + return ko.utils.unwrapObservable(row.parent.entity[column.field]); + } else { + return ''; + } + // } else if (column.field == "lineNum") { + // return self.getChildCount(row); + } else { + var def = column.config.colDef; + //TODO: add a switch for whether or not to aggregate at all. + if (def && (def.aggregator || def.agg)) { + var aggType = def.agg || def.aggregator || 'count'; + var aggParts = aggType.match(/^([^(]+)\(([^)]+)?\)/); + if (aggParts) { + aggType = aggParts[1]; + } + var aggregator = self.aggregationProvider[aggType]; + if (aggParts && typeof aggregator == "function") { + aggregator = aggregator(aggParts[2]); + } + if (!aggregator || typeof aggregator.grid != "function") return "#error"; + var aggregateValue = aggregator.grid(row, def); + return aggregateValue ? aggregateValue : ''; + } + + console.log('No way to calc agg content'); + return ''; + } + }; + self.getAggKey = function (aggRow) { + var key = {}; + key[aggRow.entity.gField] = aggRow.entity.gLabel; + if (aggRow.parent) { + key = $.extend(key, self.getAggKey(aggRow.parent)); + } + return key; + }; + self.buildAggregateEntity = function (agg) { + var aggEntity = agg.entity; + grid.nonAggColumns().forEach(function (column) { + aggEntity[column.field] = ko.computed({ + read: function () { + if (!this.val) this.val = self.calcAggContent(agg, column); + return this.val; + }, + owner: {}, + deferEvaluation: true + }); + // if (result.field == column.field) result.setExpand + }); + agg.Key = aggEntity.Key = self.getAggKey(agg); + }; + self.buildAggregateRow = function(aggEntity, rowIndex) { + var agg = self.aggCache[aggEntity.aggIndex]; // first check to see if we've already built it + if (!agg) { + // build the row + agg = new window.kg.Aggregate(aggEntity, self.rowConfig, self, self.selectionService); + self.aggCache[aggEntity.aggIndex] = agg; + } + agg.index = rowIndex + 1; //not a zero-based rowIndex + agg.offsetTop((self.rowHeight * rowIndex).toString() + 'px'); + self.rowCache[rowIndex] = agg; + return agg; + }; + self.UpdateViewableRange = function(newRange) { + self.renderedRange = newRange; + self.renderedChange(); + }; + self.filteredDataChanged = function() { + // check for latebound autogenerated columns + if (grid.lateBoundColumns && grid.filteredData().length > 1) { + grid.config.columnDefs = undefined; + grid.buildColumns(); + grid.lateBoundColumns = false; + } + self.dataChanged = true; + self.rowCache = []; //if data source changes, kill this! + grid.selectedCells([]); + grid.selectedItems([]); + if (grid.config.groups.length > 0) { + if (!grid.columns().filter(function (a) { + return a.field == 'Group'; + }).length) { + grid.columns.splice(0, 0, new window.kg.Column({ + colDef: { + field: 'Group', + displayName: grid.config.columnDefs + .filter(function (a) { + return a.groupIndex > 0; + }) + .map(function (a) { + return a.displayName; + }) + .join("-"), + width: 250, + index: 0, + sortable: true, + sortDirection: 'asc', + resizable: true + }, + sortCallback: grid.sortData, + resizeOnDataCallback: grid.resizeOnData, + enableResize: grid.config.enableColumnResize, + enableSort: grid.config.enableSorting, + index: 0, + }, grid)); + window.kg.domUtilityService.BuildStyles(grid); + + } + self.getGrouping(grid.config.groups); + } + + var aggRow = new window.kg.Aggregate( + { + isAggRow: true, + '_kg_hidden_': false, + children: [], + aggChildren: [] + }, // entity + self.rowConfig, + self, + self.selectionService + ); + self.buildAggregateEntity(aggRow); + aggRow.children = grid.filteredData(); + aggRow.entity.lineNum = aggRow.children.length; + grid.totalsRow(aggRow); + self.UpdateViewableRange(self.renderedRange); + grid.selectedCells.notifySubscribers(grid.selectedCells()); + }; + + self.renderedChange = function() { + if (!self.groupedData || grid.config.groups.length < 1) { + self.renderedChangeNoGroups(); + grid.refreshDomSizes(); + return; + } + self.parentCache = []; + var rowArr = []; + var dataArray = self.parsedData.filter(function(e) { + return e[KG_HIDDEN] === false; + }).slice(self.renderedRange.topRow, self.renderedRange.bottomRow); + var indexArray = self.parsedData.filter(function (a) { + return /*a[KG_HIDDEN] === false && */!a.isAggRow; + }); + $.each(dataArray, function (indx, item) { + var row; + if (item.isAggRow) { + row = self.buildAggregateRow(item, self.renderedRange.topRow + indx); + } else { + item.lineNum = indexArray.indexOf(item) + 1; + row = self.buildEntityRow(item, self.renderedRange.topRow + indx); + } + //add the row to our return array + rowArr.push(row); + }); + grid.setRenderedRows(rowArr); + grid.refreshDomSizes(); + }; + + self.renderedChangeNoGroups = function() { + var rowArr = []; + var dataArr = grid.filteredData.slice(self.renderedRange.topRow, self.renderedRange.bottomRow); + var indexArray = ko.utils.unwrapObservable(grid.filteredData); + // .filter(function (a) { + // return a[KG_HIDDEN] === false && !a.isAggRow; + // }); + $.each(dataArr, function (i, item) { + item.lineNum = indexArray.indexOf(item) + 1; + var row = self.buildEntityRow(item, self.renderedRange.topRow + i); + //add the row to our return array + rowArr.push(row); + }); + grid.setRenderedRows(rowArr); + }; + + //magical recursion. it works. I swear it. I figured it out in the shower one day. + self.parseGroupData = function(g) { + if (g.values) { + $.each(g.values, function (i, item) { + // get the last parent in the array because that's where our children want to be + var parent = self.parentCache[self.parentCache.length - 1]; + parent.children.push(item); + //add the row to our return array + self.parsedData.push(item); + item[KG_HIDDEN] = !!parent.collapsed(); + }); + } else { + var props = []; + for (var prop in g) { + if (g[prop] && typeof g[prop] == "object" && typeof g[prop][KG_SORTINDEX] != "undefined") { + props[g[prop][KG_SORTINDEX]] = prop; + } + } + for (var i = 0; i < props.length; i++) { + var prop = props[i]; + if (!prop) continue; + // exclude the meta properties. + if (prop == KG_FIELD || prop == KG_DEPTH || prop == KG_COLUMN || prop == KG_SORTINDEX) { + continue; + } else if (g.hasOwnProperty(prop)) { + //build the aggregate entity + var entity = { + gField: g[KG_FIELD], + gLabel: g[prop][KG_VALUE], + gDepth: g[KG_DEPTH], + isAggRow: true, + '_kg_hidden_': false, + children: [], + aggChildren: [], + aggIndex: self.numberOfAggregates, + aggLabelFilter: g[KG_COLUMN].aggLabelFilter + }; + var parent = self.parentCache[g[KG_DEPTH] - 1]; + var key = self.getAggKey({ + entity: { + gField: g[KG_FIELD], + gLabel: g[prop][KG_VALUE], + }, + parent: parent + }); + var cachedIndex = -1; + self.parsedDataCache().forEach(function (a, i) { + var isMatch = a.gField == entity.gField; + for (var prop in key) { + if (ko.utils.unwrapObservable(a.Key[prop]) != ko.utils.unwrapObservable(key[prop])) isMatch = false; + } + if (isMatch) cachedIndex = i; + }); + if (cachedIndex != -1) { + var cachedEntity = self.parsedDataCache().splice(cachedIndex, 1)[0]; + entity[SELECTED_PROP] = cachedEntity[SELECTED_PROP]; + entity[CELLSELECTED_PROP] = cachedEntity[CELLSELECTED_PROP]; + entity._kg_collapsed = cachedEntity._kg_collapsed; + } + //build the aggregate row + var agg = self.buildAggregateRow(entity, 0); + if (self.parsedDataCache().indexOf(agg.entity) == -1) self.parsedDataCache().push(agg.entity); + else throw new Error("Stop"); + // If agg is the last grouping and hideChildren is enabled collapse the agg to hide it's children + if (g[KG_DEPTH] == self.maxDepth - 1 && self.hideChildren) agg.entity._kg_collapsed = true; + self.numberOfAggregates++; + //set the aggregate parent to the parent in the array that is one less deep. + agg.parent = self.parentCache[agg.depth - 1]; + // if we have a parent, set the parent to not be collapsed and append the current agg to its children + if (agg.parent) { + agg.entity._kg_hidden_ = !!agg.parent.collapsed(); + agg.entity._kg_collapsed = agg.parent.collapsed() || agg.entity._kg_collapsed; + agg.parent.aggChildren.push(agg); + } + agg.collapsed(agg.entity._kg_collapsed); + agg.entity.Key = self.getAggKey(agg); + // add the aggregate row to the parsed data. + self.parsedData.push(agg.entity); + // the current aggregate now the parent of the current depth + self.parentCache[agg.depth] = agg; + // dig deeper for more aggregates or children. + self.parseGroupData(g[prop]); + self.buildAggregateEntity(agg); + } + } + } + }; + //Shuffle the data into their respective groupings. + self.getGrouping = function(groups) { + self.aggCache = []; + self.rowCache = []; + self.numberOfAggregates = 0; + self.groupedData = {}; + self.maxDepth = groups.length; + // Here we set the onmousedown event handler to the header container. + var data = grid.filteredData(); + var maxDepth = groups.length; + var cols = grid.columns(); + self.hideChildren = !!ko.utils.unwrapObservable(grid.config.hideChildren); + $.each(data, function (i, item) { + item[KG_HIDDEN] = self.hideChildren; + var ptr = self.groupedData; + $.each(groups, function(depth, group) { + if (!cols[depth].isAggCol && (depth + (self.hideChildren ? 2 : 0)) <= maxDepth) { + grid.columns.splice(item.gDepth, 0, new window.kg.Column({ + colDef: { + field: '', + width: 25, + sortable: false, + resizable: false, + headerCellTemplate: '
', + cellTemplate: window.kg.aggCellTemplate() + }, + isAggCol: true, + index: item.gDepth, + headerRowHeight: grid.config.headerRowHeight + })); + window.kg.domUtilityService.BuildStyles(grid); + } + var col = cols.filter(function (c) { return c.field == group; })[0]; + var val = window.kg.utils.evalProperty(item, group); + if (col.cellFilter) { + val = col.cellFilter(val); + } + var childVal = val; + val = val ? val.toString() : 'null'; + if (!ptr[val]) { + ptr[val] = {}; + } + if (!ptr[KG_FIELD]) { + ptr[KG_FIELD] = group; + } + if (!ptr[KG_DEPTH]) { + ptr[KG_DEPTH] = depth; + } + if (!ptr[KG_COLUMN]) { + ptr[KG_COLUMN] = col; + } + if (!ptr[KG_SORTINDEX]) { + ptr[KG_SORTINDEX] = i; + } + ptr = ptr[val]; + if (!ptr[KG_VALUE]) ptr[KG_VALUE] = childVal; + }); + if (!ptr[KG_SORTINDEX]) { + ptr[KG_SORTINDEX] = i; + } + if (!ptr.values) { + ptr.values = []; + } + ptr.values.push(item); + }); + grid.fixColumnIndexes(); + self.parsedData.length = 0; + self.parseGroupData(self.groupedData); + }; + + if (grid.config.groups.length > 0 && grid.filteredData().length > 0) { + self.getGrouping(grid.config.groups); + } +}; + +/*********************************************** +* FILE: ..\src\classes\grid.js +***********************************************/ +window.kg.Grid = function (options) { + var defaults = { + rowHeight: 30, + columnWidth: 100, + headerRowHeight: 30, + footerRowHeight: 55, + footerVisible: true, + displayFooter: undefined, + canSelectRows: true, + selectAllState: ko.observable(false), + data: ko.observableArray([]), + columnDefs: undefined, + selectedItems: ko.observableArray([]), // array, if multi turned off will have only one item in array + selectedCells: ko.observableArray([]), + displaySelectionCheckbox: true, //toggles whether row selection check boxes appear + selectWithCheckboxOnly: false, + useExternalSorting: false, + sortInfo: ko.observable(undefined), // similar to filterInfo + multiSelect: true, + tabIndex: -1, + enableColumnResize: true, + enableSorting: true, + maintainColumnRatios: undefined, + beforeSelectionChange: function () { return true;}, + afterSelectionChange: function () { }, + columnsChanged: function() { }, + rowTemplate: undefined, + headerRowTemplate: undefined, + jqueryUITheme: false, + jqueryUIDraggable: false, + plugins: [], + keepLastSelected: true, + groups: [], + showGroupPanel: false, + enableRowReordering: false, + showColumnMenu: true, + showFilter: true, + disableTextSelection: true, + filterOptions: { + filterText: ko.observable(""), + useExternalFilter: false + }, + //Paging + enablePaging: false, + pagingOptions: { + pageSizes: ko.observableArray([250, 500, 1000]), //page Sizes + pageSize: ko.observable(250), //Size of Paging data + totalServerItems: ko.observable(0), //how many items are on the server (for paging) + currentPage: ko.observable(1) //what page they are currently on + } + }, + self = this; + + self.maxCanvasHt = ko.observable(0); + //self vars + self.config = $.extend(defaults, options); + self.config.columnDefs = ko.utils.unwrapObservable(options.columnDefs); + self.gridId = "ng" + window.kg.utils.newId(); + self.$root = null; //this is the root element that is passed in with the binding handler + self.$groupPanel = null; + self.$topPanel = null; + self.$headerContainer = null; + self.$footerContainer = null; + self.$headerScroller = null; + self.$headers = null; + self.$viewport = null; + self.$canvas = null; + self.rootDim = self.config.gridDim; + self.sortInfo = ko.isObservable(self.config.sortInfo) ? self.config.sortInfo : ko.observable(self.config.sortInfo); + self.sortedData = self.config.data; + self.totalsRow = ko.observable(); + self.lateBindColumns = false; + self.filteredData = ko.observableArray([]); + self.lastSortedColumn = undefined; + self.showFilter = self.config.showFilter; + self.filterText = self.config.filterOptions.filterText; + self.disableTextSelection = ko.observable(self.config.disableTextSelection); + self.calcMaxCanvasHeight = function() { + return (self.configGroups().length > 0) ? (self.rowFactory.parsedData.filter(function (e) { + return e[KG_HIDDEN] === false; + }).length * self.config.rowHeight) : (self.filteredData().length * self.config.rowHeight); + }; + self.elementDims = { + scrollW: 0, + scrollH: 0, + rowIndexCellW: 25, + rowSelectedCellW: 25, + rootMaxW: 0, + rootMaxH: 0 + }; + //self funcs + self.setRenderedRows = function (newRows) { + self.renderedRows(newRows); + self.refreshDomSizes(); + }; + self.minRowsToRender = function () { + var viewportH = self.viewportDimHeight() || 1; + return Math.floor(viewportH / self.config.rowHeight); + }; + self.refreshDomSizes = function () { + self.rootDim.outerWidth(self.elementDims.rootMaxW); + self.rootDim.outerHeight(self.elementDims.rootMaxH); + self.maxCanvasHt(self.calcMaxCanvasHeight()); + }; + self.buildColumnDefsFromData = function () { + var sd = self.sortedData(); + if (!self.config.columnDefs) { + self.config.columnDefs = []; + } + if (!sd || !sd[0]) { + self.lateBoundColumns = true; + return; + } + var item; + item = sd[0]; + + window.kg.utils.forIn(item, function (prop, propName) { + if (propName != SELECTED_PROP) { + self.config.columnDefs.push({ + field: propName + }); + } + }); + }; + self.buildColumns = function () { + var columnDefs = self.config.columnDefs, + cols = []; + + if (!columnDefs) { + self.buildColumnDefsFromData(); + columnDefs = self.config.columnDefs; + } + if (self.config.displaySelectionCheckbox && self.config.canSelectRows) { + columnDefs.splice(0, 0, { + field: '\u2714', + width: self.elementDims.rowSelectedCellW, + sortable: false, + resizable: false, + headerCellTemplate: '', + cellTemplate: '
' + }); + } + columnDefs.sort(function (a, b) {return a.index - b.index;}); + if (columnDefs.length > 0) { + self.configGroups([]); + var configGroups = []; + $.each(columnDefs, function (i, colDef) { + var index = i; + var column = new window.kg.Column({ + colDef: colDef, + // This is likely causing our bug, we need to clean the index vield to ensure that all the indexes are valid. + index: index, + headerRowHeight: self.config.headerRowHeight, + sortCallback: self.sortData, + resizeOnDataCallback: self.resizeOnData, + enableResize: self.config.enableColumnResize, + enableSort: self.config.enableSorting + }, self); + cols.push(column); + var indx = self.config.groups.indexOf(colDef.field); + if (indx != -1) { + indx = colDef.groupIndex ? colDef.groupIndex - 1 : indx; + configGroups.splice(indx, 0, column); + column.isGroupedBy(true); + } else if (colDef.groupIndex) { + //self.config.groups.splice(colDef.groupIndex - 1, 0, colDef.field); + configGroups.splice(colDef.groupIndex - 1, 0, column); + column.isGroupedBy(true); + } + }); + cols.sort(function (a, b) {return a.index - b.index;}); + var gindex = []; + $.each(cols, function (index, item) { + var idx = item.groupIndex(); + if (idx) { + while(gindex[idx]) { + idx++; + } + item.groupIndex(idx); + gindex[idx] = item; + } + }); + configGroups.sort(function (a, b) {return a.groupIndex() - b.groupIndex();}); + var groups = []; + $.each(configGroups, function (index, item) { + groups.push(item.field); + }); + self.config.groups = groups; + self.columns(cols); + self.configGroups(configGroups); + self.fixGroupIndexes(); + } + }; + self.configureColumnWidths = function() { + var cols = self.config.columnDefs; + var numOfCols = cols.length, + asterisksArray = [], + percentArray = [], + asteriskNum = 0, + totalWidth = 0; + var columns = self.columns(); + var aggColOffset = self.columns().length - self.nonAggColumns().length; + $.each(columns, function(i, column) { + var col; + $.each(cols, function (index, c) { + if (c.field == column.field) { + col = c; + } + }); + col = col ? {width: col.width, index: i} : {width: column.width, index: i}; + // }); + // $.each(cols, function (i, col) { + if (column.visible === false) { + return; + } + var isPercent = false, t; + //if width is not defined, set it to a single star + if (window.kg.utils.isNullOrUndefined(col.width)) { + col.width = "*"; + } else { // get column width + isPercent = isNaN(col.width) ? window.kg.utils.endsWith(col.width, "%") : false; + t = isPercent ? col.width : parseInt(col.width, 10); + } + // check if it is a number + if (isNaN(t)) { + t = col.width; + // figure out if the width is defined or if we need to calculate it + if (t == 'auto') { // set it for now until we have data and subscribe when it changes so we can set the width. + columns[i].width = columns[i].minWidth; + var temp = columns[i]; + $(document).ready(function() { self.resizeOnData(temp, true); }); + return; + } else if (t.indexOf("*") != -1) { + asteriskNum += t.length; + asterisksArray.push({width: col.width, index: i}); + return; + } else if (isPercent) { // If the width is a percentage, save it until the very last. + + percentArray.push({width: col.width, index: i}); + return; + } else { // we can't parse the width so lets throw an error. + throw "unable to parse column width, use percentage (\"10%\",\"20%\", etc...) or \"*\" to use remaining width of grid"; + } + } else { + totalWidth += columns[i].width = parseInt(col.width, 10); + } + }); + // check if we saved any asterisk columns for calculating later + if (asterisksArray.length > 0) { + self.config.maintainColumnRatios === false ? $.noop() : self.config.maintainColumnRatios = true; + // get the remaining width + var remainingWidth = self.rootDim.outerWidth() - totalWidth; + // calculate the weight of each asterisk rounded down + var asteriskVal = Math.floor(remainingWidth / asteriskNum); + // set the width of each column based on the number of stars + if (asteriskVal < 1) { + asteriskVal = 1; + } + $.each(asterisksArray, function (i, col) { + var t = col.width.length; + var column = columns[col.index]; + column.width = asteriskVal * t; + if (column.width < column.minWidth) { + column.width = column.minWidth; + } + //check if we are on the last column + if (col.index + 1 == numOfCols) { + var offset = 2; //We're going to remove 2 px so we won't overlflow the viwport by default + // are we overflowing? + if (self.maxCanvasHt() > self.viewportDimHeight()) { + //compensate for scrollbar + offset += window.kg.domUtilityService.ScrollW; + } + column.width -= offset; + } + totalWidth += columns[col.index].width; + }); + } + // Now we check if we saved any percentage columns for calculating last + if (percentArray.length > 0) { + // do the math + $.each(percentArray, function (i, col) { + var t = col.width; + columns[col.index].width = Math.floor(self.rootDim.outerWidth() * (parseInt(t.slice(0, -1), 10) / 100)); + }); + } + self.columns(columns); + window.kg.domUtilityService.BuildStyles(self); + }; + self.init = function () { + //factories and services + self.selectionService = new window.kg.SelectionService(self); + self.rowFactory = new window.kg.RowFactory(self); + self.selectionService.Initialize(self.rowFactory); + self.searchProvider = new window.kg.SearchProvider(self); + self.styleProvider = new window.kg.StyleProvider(self); + self.buildColumns(); + window.kg.sortService.columns = self.columns; + self.configGroups.subscribe(function (a) { + if (!a) { + return; + } + var tempArr = []; + $.each(a, function (i, item) { + if(item){ + tempArr.push(item.field || item); + } + }); + self.config.groups = tempArr; + self.rowFactory.filteredDataChanged(); + }); + self.sortedData.subscribe(function () { + if (!self.isSorting) { + self.sortByDefault(); + } + }); + + self.filteredData.subscribe(function () { + if (self.$$selectionPhase) { + return; + } + self.maxCanvasHt(self.calcMaxCanvasHeight()); + if (!self.isSorting) { + self.configureColumnWidths(); + } + }); + self.maxCanvasHt(self.calcMaxCanvasHeight()); + self.searchProvider.evalFilter(); + self.refreshDomSizes(); + }; + self.prevScrollTop = 0; + self.prevScrollIndex = 0; + self.adjustScrollTop = function (scrollTop, force) { + if (self.prevScrollTop === scrollTop && !force) { return; } + var rowIndex = Math.floor(scrollTop / self.config.rowHeight); + // Have we hit the threshold going down? + if (self.prevScrollTop < scrollTop && rowIndex < self.prevScrollIndex + SCROLL_THRESHOLD) { + return; + } + //Have we hit the threshold going up? + if (self.prevScrollTop > scrollTop && rowIndex > self.prevScrollIndex - SCROLL_THRESHOLD) { + return; + } + self.prevScrollTop = scrollTop; + self.rowFactory.UpdateViewableRange(new window.kg.Range(Math.max(0, rowIndex - EXCESS_ROWS), rowIndex + self.minRowsToRender() + EXCESS_ROWS)); + self.prevScrollIndex = rowIndex; + }; + self.adjustScrollLeft = function (scrollLeft) { + if (self.$headerContainer) { + self.$headerContainer.scrollLeft(scrollLeft); + } + if (self.$footerContainer) { + self.$footerContainer.scrollLeft(scrollLeft); + } + }; + self.resizeOnData = function (col) { + // we calculate the longest data. + var longest = col.minWidth; + var arr = window.kg.utils.getElementsByClassName('col' + col.index); + $.each(arr, function (index, elem) { + var i; + if (index === 0) { + var kgHeaderText = $(elem).find('.kgHeaderText'); + i = window.kg.utils.visualLength(kgHeaderText) + 10;// +10 some margin + } else { + var ngCellText = $(elem).find('.kgCellText'); + i = window.kg.utils.visualLength(ngCellText) + 10; // +10 some margin + } + if (i > longest) { + longest = i; + } + }); + col.width = longest = Math.min(col.maxWidth, longest + 7); // + 7 px to make it look decent. + window.kg.domUtilityService.BuildStyles(self); + }; + self.sortByDefault = function () { + // console.log(self.sortedData().length); + var column = self.columns().filter(function (a) {return a.sortDirection && a.sortDirection()})[0]; + if (!column) return; + var direction = column.sortDirection(); + self.sortData(column, direction); + } + self.sortData = function (col, direction) { + // if external sorting is being used, do nothing. + self.isSorting = true; + // if (col.field == "Group") col = self.configGroups()[0]; + self.sortInfo({ + column: col, + direction: direction, + grid: self + }); + self.clearSortingData(col); + if(!self.config.useExternalSorting){ + window.kg.sortService.Sort(self.sortInfo.peek(), self.sortedData); + } else { + self.config.sortInfo(self.sortInfo.peek()); + } + self.lastSortedColumn = col; + self.isSorting = false; + }; + self.toggleCollapse = function (data) { + var collapsed = !data.collapsed(); + data.collapsed(collapsed); + self.rowFactory.aggCache.forEach(function (a) {if (a.field == data.field) {a._setExpand(collapsed);}}); + + self.rowFactory.rowCache = []; + self.rowFactory.renderedChange(); + }; + self.clearSortingData = function (col) { + if (!col) { + $.each(self.columns(), function (i, c) { + c.sortDirection(""); + }); + } else if (self.lastSortedColumn && col != self.lastSortedColumn) { + self.lastSortedColumn.sortDirection(""); + } + }; + self.fixColumnIndexes = function () { + self.$$indexPhase = true; + //fix column indexes + var cols = self.columns.peek(); + $.each(cols, function (i, col) { + col.index = i; + }); + self.$$indexPhase = false; + }; + //self vars + self.elementsNeedMeasuring = true; + self.columns = ko.observableArray([]); + self.columns.subscribe(function(newCols) { + self.config.columnsChanged(newCols); + }); + self.renderedRows = ko.observableArray([]); + self.headerRow = null; + self.rowHeight = self.config.rowHeight; + self.jqueryUITheme = ko.observable(self.config.jqueryUITheme); + self.footer = null; + self.selectedItems = self.config.selectedItems; + self.selectedCells = self.config.selectedCells; + self.multiSelect = self.config.multiSelect; + self.footerVisible = window.kg.utils.isNullOrUndefined(self.config.displayFooter) ? self.config.footerVisible : self.config.displayFooter; + self.config.footerRowHeight = self.footerVisible ? self.config.footerRowHeight : 0; + self.showColumnMenu = self.config.showColumnMenu; + self.showMenu = ko.observable(false); + self.configGroups = ko.observableArray([]); + + //Paging + self.enablePaging = self.config.enablePaging; + self.pagingOptions = self.config.pagingOptions; + //Templates + self.rowTemplate = self.config.rowTemplate || window.kg.defaultRowTemplate(); + self.headerRowTemplate = self.config.headerRowTemplate || window.kg.defaultHeaderRowTemplate(); + if (self.config.rowTemplate && !TEMPLATE_REGEXP.test(self.config.rowTemplate)) { + self.rowTemplate = window.kg.utils.getTemplatePromise(self.config.rowTemplate); + } + if (self.config.headerRowTemplate && !TEMPLATE_REGEXP.test(self.config.headerRowTemplate)) { + self.headerRowTemplate = window.kg.utils.getTemplatePromise(self.config.headerRowTemplate); + } + //scope funcs + self.visibleColumns = ko.computed(function () { + var cols = self.columns(); + return cols.filter(function (col) { + var isVis = col.visible(); + return isVis; + }); + }); + self.nonAggColumns = ko.computed(function () { + return self.columns().filter(function (col) { + return !col.isAggCol; + }); + }); + self.toggleShowMenu = function () { + self.showMenu(!self.showMenu()); + }; + self.allSelected = self.config.selectAllState; + self.allSelected.subscribe(function (state) { + if (self.config.beforeSelectionChange(self.sortedData.peek(), this)) { + self.selectionService.toggleSelectAll(state); + self.config.afterSelectionChange(self.selectedItems.peek(), this); + } + }); + self.totalFilteredItemsLength = ko.computed(function () { + return self.filteredData().length; + }); + self.showGroupPanel = ko.computed(function(){ + return self.config.showGroupPanel; + }); + self.topPanelHeight = ko.observable(self.config.showGroupPanel === true ? (self.config.headerRowHeight * 2) : self.config.headerRowHeight); + self.viewportDimHeight = ko.computed(function () { + return Math.max(0, self.rootDim.outerHeight() - self.topPanelHeight() - self.config.footerRowHeight - 2); + }); + self.groupBy = function (col) { + if (self.sortedData().length < 1) { + return; + } + var indx = self.configGroups().indexOf(col); + if (indx == -1) { + col.isGroupedBy(true); + col.visible(false); + self.configGroups.push(col); + col.groupIndex(self.configGroups().length); + self.sortByDefault(); + } else { + self.removeGroup(indx); + } + window.kg.domUtilityService.BuildStyles(self); + }; + self.removeGroup = function(index) { + var col = self.columns().filter(function(item){ + return item.groupIndex() == (index + 1); + })[0]; + if (col) { + col.visible(true); + col.isGroupedBy(false); + col.groupIndex(0); + if (self.columns()[index].isAggCol) { + self.columns.splice(index, 1); + } + self.configGroups.splice(index, 1); + if (self.configGroups.length == 0) { + var groupCol = self.columns().filter(function (item) { + return item.field == "Group"; + })[0]; + if (groupCol) groupCol.visible(false); + } + self.fixGroupIndexes(); + if (self.configGroups().length === 0) { + self.fixColumnIndexes(); + } + window.kg.domUtilityService.BuildStyles(self); + } + }; + self.fixGroupIndexes = function(){ + $.each(self.configGroups(), function(i,item){ + item.groupIndex(i + 1); + }); + }; + self.totalRowWidth = function () { + var totalWidth = 0, + cols = self.visibleColumns(); + $.each(cols, function (i, col) { + totalWidth += col.width; + }); + return totalWidth; + }; + self.headerScrollerDim = function () { + var viewportH = self.viewportDimHeight(), + maxHeight = self.maxCanvasHt(), + vScrollBarIsOpen = (maxHeight > viewportH), + newDim = new window.kg.Dimension(); + + newDim.autoFitHeight = true; + newDim.outerWidth = self.totalRowWidth(); + if (vScrollBarIsOpen) { newDim.outerWidth += self.elementDims.scrollW; } + else if ((maxHeight - viewportH) <= self.elementDims.scrollH) { //if the horizontal scroll is open it forces the viewport to be smaller + newDim.outerWidth += self.elementDims.scrollW; + } + return newDim; + }; + //footer + self.jqueryUITheme = self.config.jqueryUITheme; + self.maxRows = ko.observable(Math.max(self.config.pagingOptions.totalServerItems() || self.sortedData().length, 1)); + self.maxRowsDisplay = ko.computed(function () { + return self.maxRows(); + }); + self.multiSelect = ko.observable((self.config.canSelectRows && self.config.multiSelect)); + self.selectedItemCount = ko.computed(function () { + return self.selectedItems().length; + }); + self.maxPages = ko.computed(function () { + self.maxRows(Math.max(self.config.pagingOptions.totalServerItems() || self.sortedData().length, 1)); + return Math.ceil(self.maxRows() / self.pagingOptions.pageSize()); + }); + self.pageForward = function () { + var page = self.config.pagingOptions.currentPage(); + self.config.pagingOptions.currentPage(Math.min(page + 1, self.maxPages())); + }; + self.pageBackward = function () { + var page = self.config.pagingOptions.currentPage(); + self.config.pagingOptions.currentPage(Math.max(page - 1, 1)); + }; + self.pageToFirst = function () { + self.config.pagingOptions.currentPage(1); + }; + self.pageToLast = function () { + var maxPages = self.maxPages(); + self.config.pagingOptions.currentPage(maxPages); + }; + self.cantPageForward = ko.computed(function () { + var curPage = self.config.pagingOptions.currentPage(); + var maxPages = self.maxPages(); + return !(curPage < maxPages); + }); + self.cantPageBackward = ko.computed(function () { + var curPage = self.config.pagingOptions.currentPage(); + return !(curPage > 1); + }); + //call init + self.init(); +}; + +/*********************************************** +* FILE: ..\src\classes\range.js +***********************************************/ +kg.Range = function (top, bottom) { + this.topRow = top; + this.bottomRow = bottom; +}; + +/*********************************************** +* FILE: ..\src\classes\row.js +***********************************************/ +window.kg.Row = function (entity, config, selectionService) { + var self = this; // constant for the selection property that we add to each data item + + self.canSelectRows = config.canSelectRows; + + self.rowClasses = config.rowClasses; + self.selectedItems = config.selectedItems; + self.entity = entity; + self.selectionService = selectionService; + + self.selected = ko.observable(false); + self.cellSelection = ko.observableArray(entity[CELLSELECTED_PROP] || []); + self.continueSelection = function(event) { + self.selectionService.ChangeSelection(self, event); + }; + self.toggleSelected = function (row, event) { + if (!self.canSelectRows) { + return true; + } + var element = event.target || event; + //check and make sure its not the bubbling up of our checked 'click' event + if (element.type == "checkbox") { + self.selected(!self.selected()); + } + if (config.selectWithCheckboxOnly && element.type != "checkbox"){ + return true; + } else { + if (self.beforeSelectionChange(self, event)) { + self.continueSelection(event); + return self.afterSelectionChange(self, event); + } + } + return false; + }; + //selectify the entity + if (!self.entity[SELECTED_PROP] === undefined) { + self.entity[SELECTED_PROP] = false; + } else { + // or else maintain the selection set by the entity. + self.selectionService.setSelection(self, self.entity[SELECTED_PROP]); + self.selectionService.updateCellSelection(self, self.entity[CELLSELECTED_PROP]); + } + self.rowIndex = ko.observable(0); + self.offsetTop = ko.observable("0px"); + self.rowDisplayIndex = 0; + self.isEven = ko.computed(function () { + if (self.rowIndex() % 2 === 0) { + return true; + } + return false; + }); + self.isOdd = ko.computed(function () { + if (self.rowIndex() % 2 !== 0) { + return true; + } + return false; + }); + self.beforeSelectionChange = config.beforeSelectionChangeCallback; + self.afterSelectionChange = config.afterSelectionChangeCallback; + self.propertyCache = {}; + self.getProperty = function (path) { + return self.propertyCache[path] || (self.propertyCache[path] = window.kg.utils.evalProperty(self.entity, path)); + }; + self.selectCell = function (column) { + var field = column.field; + var index = self.cellSelection().indexOf(field); + if (index == -1) self.selectionService.setCellSelection(self, column, true); + else self.selectionService.setCellSelection(self, column, false); + }; +}; + +/*********************************************** +* FILE: ..\src\classes\searchProvider.js +***********************************************/ +window.kg.SearchProvider = function (grid) { + var self = this, + searchConditions = [], + lastSearchStr; + self.extFilter = grid.config.filterOptions.useExternalFilter; + self.showFilter = grid.config.showFilter; + self.filterText = grid.config.filterOptions.filterText; + self.throttle = grid.config.filterOptions.filterThrottle; + self.fieldMap = {}; + self.evalFilter = function () { + if (searchConditions.length === 0) { + grid.filteredData(grid.sortedData.peek().filter(function(item) { + return !item._destroy; + })); + } else { + grid.filteredData(grid.sortedData.peek().filter(function(item) { + if (item._destroy) { + return false; + } + + for (var i = 0, len = searchConditions.length; i < len; i++) { + var condition = searchConditions[i]; + //Search entire row + if (!condition.column) { + for (var prop in item) { + if (item.hasOwnProperty(prop)) { + var pVal = ko.utils.unwrapObservable(item[prop]); + if (pVal && condition.regex.test(pVal.toString())) { + return true; + } + } + } + return false; + } + //Search by column. + var field = ko.utils.unwrapObservable(item[condition.column]) || ko.utils.unwrapObservable(item[self.fieldMap[condition.columnDisplay]]); + if (!field || !condition.regex.test(field.toString())) { + return false; + } + } + return true; + })); + } + grid.rowFactory.filteredDataChanged(); + }; + var getRegExp = function(str, modifiers) { + try { + return new RegExp(str, modifiers); + } catch(err) { + //Escape all RegExp metacharacters. + return new RegExp(str.replace(/(\^|\$|\(|\)|\<|\>|\[|\]|\{|\}|\\|\||\.|\*|\+|\?)/g, '\\$1')); + } + }; + var buildSearchConditions = function (a) { + //reset. + searchConditions = []; + var qStr; + if (!(qStr = $.trim(a))) { + return; + } + var columnFilters = qStr.split(";"); + $.each(columnFilters, function (i, filter) { + var args = filter.split(':'); + if (args.length > 1) { + var columnName = $.trim(args[0]); + var columnValue = $.trim(args[1]); + if (columnName && columnValue) { + searchConditions.push({ + column: columnName, + columnDisplay: columnName.replace(/\s+/g, '').toLowerCase(), + regex: getRegExp(columnValue, 'i') + }); + } + } else { + var val = $.trim(args[0]); + if (val) { + searchConditions.push({ + column: '', + regex: getRegExp(val, 'i') + }); + } + } + }); + }; + + var filterTextComputed = ko.computed(function () { + var a = self.filterText(); + if (!self.extFilter && a != lastSearchStr) { + //To prevent circular dependency when throttle is enabled. + lastSearchStr = a; + buildSearchConditions(a); + self.evalFilter(); + } + }); + if (typeof self.throttle === 'number') { + filterTextComputed.extend({ throttle: self.throttle }); + } + if (!self.extFilter) { + grid.columns.subscribe(function (a) { + $.each(a, function (i, col) { + self.fieldMap[col.displayName().toLowerCase().replace(/\s+/g, '')] = col.field; + }); + }); + } +}; + +/*********************************************** +* FILE: ..\src\classes\selectionService.js +***********************************************/ +window.kg.SelectionService = function (grid) { + var self = this; + self.multi = grid.config.multiSelect; + self.selectedItems = grid.config.selectedItems; + self.selectedCells = grid.config.selectedCells; + self.selectedIndex = grid.config.selectedIndex; + self.lastClickedRow = undefined; + self.ignoreSelectedItemChanges = false; // flag to prevent circular event loops keeping single-select var in sync + + self.rowFactory = {}; + self.Initialize = function (rowFactory) { + self.rowFactory = rowFactory; + }; + + // function to manage the selection action of a data item (entity) + self.ChangeSelection = function (rowItem, evt) { + grid.$$selectionPhase = true; + if (evt && !(evt.ctrlKey || evt.shiftKey) && self.multi) { + // clear selection + self.toggleSelectAll(false); + } + if (evt && evt.shiftKey && self.multi) { + if (self.lastClickedRow) { + var thisIndx = self.rowFactory.parsedData.indexOf(rowItem.entity); + var prevIndx = self.rowFactory.parsedData.indexOf(self.lastClickedRow.entity); + if (thisIndx == -1) thisIndx = grid.filteredData().indexOf(rowItem.entity); + if (prevIndx == -1) prevIndx = grid.filteredData().indexOf(self.lastClickedRow.entity); + + + if (thisIndx == prevIndx) { + grid.$$selectionPhase = false; + return false; + } + prevIndx++; + if (thisIndx < prevIndx) { + thisIndx = thisIndx ^ prevIndx; + prevIndx = thisIndx ^ prevIndx; + thisIndx = thisIndx ^ prevIndx; + } + var rows = []; + for (; prevIndx <= thisIndx; prevIndx++) { + var row = self.rowFactory.rowCache[prevIndx]; + if (!row) row = { + entity: self.rowFactory.parsedData[prevIndx] || grid.filteredData.peek()[prevIndx] + }; + rows.push(row); + } + if (rowItem.beforeSelectionChange(rows, evt)) { + $.each(rows, function(i, ri) { + if (ri.selected) ri.selected(true); + ri.entity[SELECTED_PROP] = true; + if (self.selectedItems().indexOf(ri.entity) === -1) { + self.selectedItems.peek().push(ri.entity); + } + }); + self.selectedItems.notifySubscribers(self.selectedItems()); + rows[rows.length - 1].afterSelectionChange(rows, evt); + } + self.lastClickedRow = rows[rows.length - 1]; + grid.$$selectionPhase = false; + return true; + } + } else if (!self.multi) { + if (self.lastClickedRow && self.lastClickedRow != rowItem) { + self.setSelection(self.lastClickedRow, false); + } + self.setSelection(rowItem, grid.config.keepLastSelected ? true : !rowItem.selected()); + } else { + self.setSelection(rowItem, !rowItem.selected()); + } + self.lastClickedRow = rowItem; + grid.$$selectionPhase = false; + return true; + }; + + self.setCellSelection = function (rowItem, column, isSelected) { + var field = column.field; + if (isSelected) { + rowItem.cellSelection.push(field); + self.selectedCells.push({ + entity: rowItem.entity, + column: column, + field: field + }); + } else { + var index = rowItem.cellSelection().indexOf(field); + rowItem.cellSelection.splice(index, 1); + self.selectedCells(self.selectedCells().filter(function (a) { + return !(a.entity == rowItem.entity && a.field == field); + })); + } + rowItem.entity[CELLSELECTED_PROP] = rowItem.cellSelection(); + if (rowItem.cellSelection().length) self.setSelection(rowItem, true); + else self.setSelection(rowItem, false); + }; + + self.updateCellSelection = function (rowItem, cellSelection) { + if (cellSelection instanceof Array) { + var cellsToSelect = cellSelection.concat(); + cellSelection.length = 0; + cellsToSelect.forEach(function (a) { + var column = grid.columns.peek().filter(function (b) { + return a == b.field; + })[0]; + if (column) { + self.setCellSelection(rowItem, column, true); + } + }); + } + }; + // just call this func and hand it the rowItem you want to select (or de-select) + self.setSelection = function(rowItem, isSelected) { + self.setSelectionQuiet(rowItem, isSelected); + if (!isSelected) { + var indx = self.selectedItems.indexOf(rowItem.entity); + if (indx != -1) self.selectedItems.splice(indx, 1); + } else { + if (self.selectedItems.indexOf(rowItem.entity) === -1) { + self.selectedItems.push(rowItem.entity); + } + } + }; + + self.setSelectionQuiet = function (rowItem, isSelected) { + if (ko.isObservable(rowItem.selected)) rowItem.selected(isSelected); + rowItem.entity[SELECTED_PROP] = isSelected; + if (!isSelected) rowItem.cellSelection([]); + }; + + // @return - boolean indicating if all items are selected or not + // @val - boolean indicating whether to select all/de-select all + self.toggleSelectAll = function (checkAll) { + var selectedlength = self.selectedItems().length; + if (selectedlength > 0) { + self.selectedItems.splice(0, selectedlength); + } + $.each(grid.filteredData(), function (i, item) { + item[SELECTED_PROP] = checkAll; + if (checkAll) { + self.selectedItems.push(item); + } + }); + $.each(self.rowFactory.rowCache, function (i, row) { + if (row && row.selected) { + row.selected(checkAll); + } + }); + }; + + self.getEntitySelection = function (items) { + if (!items) items = self.selectedItems(); + var result = []; + items.forEach(function (a) { + if (a.isAggRow) { + var children = a.children.length ? a.children : a.aggChildren; + result = result.concat(self.getEntitySelection(children)); + } else { + result.push(a); + } + }); + return result; + }; + + self.RemoveSelectedRows = function () { + var itemsToDelete = self.getEntitySelection(); + grid.sortedData(grid.sortedData().filter(function (a) { + return itemsToDelete.indexOf(a) == -1; + })); + }; +}; + +/*********************************************** +* FILE: ..\src\classes\aggregationProvider.js +***********************************************/ +window.kg.AggregationProvider = function (grid) { + var self = this; + function getGridCount(row, field, condition) { + var count = 0; + if (row.aggChildren && row.aggChildren.length) { + for (var i = row.aggChildren.length - 1; i >= 0; i--) { + count += getGridCount(row.aggChildren[i], field, condition); + } + } else if (row.children) { + var children = row.children; + if (condition === "") count = Number(children.length) || 0; + else if (condition === "true") { + for (var idx = children.length - 1; idx >= 0; idx--) { + var child = children[idx]; + var val = child[field.field]; + if (val === "true" || val === true) count++; + } + } + else count = "#NotImplemented"; + } else { + // this is a non agg row and we're counting it? + // count = 1; + count = 0; + } + return count; + } + + function getGridSum(row, field) { + if (!row) return; + + var result = 0; + if (row.aggChildren && row.aggChildren.length > 0) { + //TODO: implement koUnwrapper, or refrence ko. + var aggChildren = row.aggChildren; + for (var idx = aggChildren.length - 1; idx >= 0; idx--) { + var aggChild = aggChildren[idx]; + if (aggChild) { + result += getGridSum(aggChild, field); + } + } + } else if (row.children && row.children.length) { + var children = row.children; + for (var i = children.length - 1; i >= 0; i--) { + var child = children[i]; + if (child && child[field.field]) { + // TODO: add a field to entity to indicate grouping, use that to deturmine whether to sum or count the fields. + var val = ko.utils.unwrapObservable(child[field.field]); + if (Number(val)) result += Number(val); + //else if (val == "true") result += 1; + } + } + } + return result; + } + + function getGridMin(row, field, min) { + var result, + children, + getVal; + min = min || function (a, b) {return a > b ? b : a;}; + var getMin = function (a, b) {if (typeof a != "number") return b; if (typeof b != "number") return a; return min (a, b); }; + if (row.aggChildren && row.aggChildren.length > 0) { + children = row.aggChildren; + getVal = getGridMin; + + } else if (row.children && row.children.length) { + children = row.children; + getVal = function (row) {return row[field.field];}; + } else { + return; + } + for (var idx = children.length - 1; idx >= 0; idx--) { + var child = children[idx]; + var val = /*Number*/(getVal(child, field, getMin)); + result = getMin(result, val); + } + return result; + } + + function getGridAny(row, field) { + return getGridMin(row, field, function (a, b) {return a || b;}); + } + function getWeightedSum(row, field) { + if (!row) return; + + var result = 0; + if (row.aggChildren && row.aggChildren.length > 0) { + //TODO: implement koUnwrapper, or refrence ko. + var aggChildren = row.aggChildren; + for (var idx = aggChildren.length - 1; idx >= 0; idx--) { + var aggChild = aggChildren[idx]; + if (aggChild) { + result += getWeightedSum(aggChild, field); + } + } + } else if (row.children && row.children.length) { + var children = row.children; + for (var i = children.length - 1; i >= 0; i--) { + var child = children[i]; + if (child && child[field.field]) { + // TODO: add a field to entity to indicate grouping, use that to deturmine whether to sum or count the fields. + var val = child[field.field] * child[field.field + "_weight"]; + result += Number(val); + } + } + } + return result; + } + + function getFld(field, flexView) { + if (!flexView) return 1; + var column = flexView.flexFields().filter(function (a) {return a.field == field;})[0]; + if (column) { + return column.fld; + } + } + + self.sum = { + sql: function (field) { + return 'sum(' + field.fld + ')'; + }, + grid: function (row, field) { + return getGridSum(row, field); + } + }; + self.asis = { + sql: function (field) { + return field.fld; + }, + grid: function (row, field) { + return row[field.field]; + } + }; + self.count = { + sql: function (field) { + return 'sum(iif(' + field.fld + ',1,0))'; + }, + grid: function (row, field) { + return getGridSum(row, field); + } + }; + self.gridCount = { + grid: function (row, field) { + var text = getGridCount(row, field, ""); + return text; + } + }; + self.sibling = function (siblingField) { + return { + grid: function (row, field) { return getGridMin(row, {field: siblingField}, function (a, b) { return a > b ? a : b; }); } + }; + }; + self.average = { + sql: function (field) { + return 'avg(' + field.fld + ')'; + }, + grid: function (row, field) { + return getWeightedSum(row, field) / getGridSum(row, {field: field.field + "_weight"}); + } + }; + self.min = { + sql: function (field) { + return 'min(' + field.fld + ')'; + }, + grid: getGridMin + }; + self.max = { + sql: function (field) { + return 'max(' + field.fld + ')'; + }, + grid: function (row, field) { return getGridMin(row, field, function (a, b) { return a > b ? a : b; }); } + }; + self.weightedAvg = { + sql: function (field, flexView) { + var fld = field.fld; + var weight = getFld(field.weightedColumn, flexView); + return 'sum(' + fld + ' * ' + weight + ')' + ' / sum(' + weight + ')'; + }, + grid: function (row, field) { + return getWeightedSum(row, field) / getGridSum(row, {field: field.weightedColumn}); + } + }; + self.countDistinct = { + sql: function (field, flexView) { + return 'count(distinct ' + field.fld + ")"; + }, + grid: function (row, field) { + return getGridSum(row, field); + } + }; + self.any = { + grid: getGridAny + }; +}; + +/*********************************************** +* FILE: ..\src\classes\styleProvider.js +***********************************************/ +window.kg.StyleProvider = function (grid) { + grid.canvasStyle = ko.computed(function() { + return { "height": grid.maxCanvasHt().toString() + "px" }; + }); + grid.headerScrollerStyle = ko.computed(function() { + return { "height": grid.config.headerRowHeight + "px" }; + }); + grid.topPanelStyle = ko.computed(function() { + return { "width": grid.rootDim.outerWidth() + "px", "height": grid.topPanelHeight() + "px" }; + }); + grid.headerStyle = ko.computed(function() { + return { "width": Math.max(0, grid.rootDim.outerWidth() - window.kg.domUtilityService.ScrollW) + "px", "height": grid.config.headerRowHeight + "px" }; + }); + grid.viewportStyle = ko.computed(function() { + return { "width": grid.rootDim.outerWidth() + "px", "height": grid.viewportDimHeight() + "px" }; + }); + grid.rowFooterStyle = ko.computed(function () { + var result = $.extend({}, grid.headerStyle()); + result.bottom = kg.domUtilityService.ScrollW + 'px'; + return result; + }); + grid.footerStyle = ko.computed(function () { + return { "width": grid.rootDim.outerWidth() + "px", "height": grid.config.footerRowHeight + "px" }; + }); +}; + +/*********************************************** +* FILE: ..\src\classes\sortService.js +***********************************************/ +window.kg.sortService = { + colSortFnCache: {}, // cache of sorting functions. Once we create them, we don't want to keep re-doing it + dateRE: /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/, // nasty regex for date parsing + guessSortFn: function(item) { + var sortFn, // sorting function that is guessed + itemType, // the typeof item + dateParts, // for date parsing + month, // for date parsing + day; // for date parsing + + if (item === undefined || item === null || item === '') { + return null; + } + itemType = typeof(item); + //check for numbers and booleans + switch (itemType) { + case "number": + sortFn = window.kg.sortService.sortNumber; + break; + case "boolean": + sortFn = window.kg.sortService.sortBool; + break; + default: + sortFn = undefined; + break; + } + //if we found one, return it + if (sortFn) { + return sortFn; + } + //check if the item is a valid Date + if (Object.prototype.toString.call(item) === '[object Date]') { + return window.kg.sortService.sortDate; + } + // if we aren't left with a string, return a basic sorting function... + if (itemType !== "string") { + return window.kg.sortService.basicSort; + } + // now lets string check.. + //check if the item data is a valid number + if (item.match(/^-?[£$¤]?[\d,.]+%?$/)) { + return window.kg.sortService.sortNumberStr; + } + // check for a date: dd/mm/yyyy or dd/mm/yy + // can have / or . or - as separator + // can be mm/dd as well + dateParts = item.match(window.kg.sortService.dateRE); + if (dateParts) { + // looks like a date + month = parseInt(dateParts[1], 10); + day = parseInt(dateParts[2], 10); + if (month > 12) { + // definitely dd/mm + return window.kg.sortService.sortDDMMStr; + } else if (day > 12) { + return window.kg.sortService.sortMMDDStr; + } else { + // looks like a date, but we can't tell which, so assume that it's MM/DD + return window.kg.sortService.sortMMDDStr; + } + } + //finally just sort the normal string... + return window.kg.sortService.sortAlpha; + }, + basicSort: function(a, b) { + if (a == b) { + return 0; + } + if (a < b) { + return -1; + } + return 1; + }, + sortNumber: function(a, b) { + return a - b; + }, + sortNumberStr: function(a, b) { + var numA, + numB, + badA = false, + badB = false; + numA = parseFloat(a.replace(/[^0-9.-]/g, '')); + if (isNaN(numA)) { + badA = true; + } + numB = parseFloat(b.replace(/[^0-9.-]/g, '')); + if (isNaN(numB)) { + badB = true; + } + // we want bad ones to get pushed to the bottom... which effectively is "greater than" + if (badA && badB) { + return 0; + } + if (badA) { + return 1; + } + if (badB) { + return -1; + } + return numA - numB; + }, + sortAlpha: function(a, b) { + var strA = ((a || '') + '').toLowerCase(), + strB = ((b || '') + '').toLowerCase(); + return strA == strB ? 0 : (strA < strB ? -1 : 1); + }, + sortBool: function(a, b) { + if (a && b) { + return 0; + } + if (!a && !b) { + return 0; + } else { + return a ? 1 : -1; + } + }, + sortDate: function(a, b) { + var timeA = a.getTime(), + timeB = b.getTime(); + return timeA == timeB ? 0 : (timeA < timeB ? -1 : 1); + }, + sortDDMMStr: function(a, b) { + var dateA, dateB, mtch, m, d, y; + mtch = a.match(window.kg.sortService.dateRE); + y = mtch[3]; + m = mtch[2]; + d = mtch[1]; + if (m.length == 1) { + m = '0' + m; + } + if (d.length == 1) { + d = '0' + d; + } + dateA = y + m + d; + mtch = b.match(window.kg.sortService.dateRE); + y = mtch[3]; + m = mtch[2]; + d = mtch[1]; + if (m.length == 1) { + m = '0' + m; + } + if (d.length == 1) { + d = '0' + d; + } + dateB = y + m + d; + if (dateA == dateB) { + return 0; + } + if (dateA < dateB) { + return -1; + } + return 1; + }, + sortMMDDStr: function(a, b) { + var dateA, dateB, mtch, m, d, y; + mtch = a.match(window.kg.sortService.dateRE); + y = mtch[3]; + d = mtch[2]; + m = mtch[1]; + if (m.length == 1) { + m = '0' + m; + } + if (d.length == 1) { + d = '0' + d; + } + dateA = y + m + d; + mtch = b.match(window.kg.sortService.dateRE); + y = mtch[3]; + d = mtch[2]; + m = mtch[1]; + if (m.length == 1) { + m = '0' + m; + } + if (d.length == 1) { + d = '0' + d; + } + dateB = y + m + d; + if (dateA == dateB) { + return 0; + } + if (dateA < dateB) { + return -1; + } + return 1; + }, + sortData: function (data /*datasource*/, sortInfo) { + var unwrappedData = data(); + // first make sure we are even supposed to do work + if (!unwrappedData || !sortInfo) { + return; + } + // grab the metadata for the rest of the logic + var col = sortInfo.column, + direction = sortInfo.direction, + item, + cols; + + + if (col.field == "Group") cols = sortInfo.grid.configGroups(); + else cols = [col]; + + var sortInfos = cols.map(function (col) { + var sortFn; + //see if we already figured out what to use to sort the column + if (window.kg.sortService.colSortFnCache[col.field]) { + sortFn = window.kg.sortService.colSortFnCache[col.field]; + } else if (col.sortingAlgorithm != undefined) { + sortFn = col.sortingAlgorithm; + window.kg.sortService.colSortFnCache[col.field] = col.sortingAlgorithm; + } else { // try and guess what sort function to use + item = unwrappedData[0]; + if (!item) { + return; + } + var val = item[col.field]; + if (typeof sortInfo.grid.config.formatString == "function") val = sortInfo.grid.config.formatString(val, col); + sortFn = kg.sortService.guessSortFn(val); + //cache it + if (sortFn) { + window.kg.sortService.colSortFnCache[col.field] = sortFn; + } else { + // we assign the alpha sort because anything that is null/undefined will never get passed to + // the actual sorting function. It will get caught in our null check and returned to be sorted + // down to the bottom + sortFn = window.kg.sortService.sortAlpha; + } + } + return { + col: col, + direction: direction, + sortFn: sortFn + }; + }); + + var sortFn; + var outerSortFn = function (itemA, itemB) { + var propA = window.kg.utils.evalProperty(itemA, col.field); + var propB = window.kg.utils.evalProperty(itemB, col.field); + // we want to force nulls and such to the bottom when we sort... which effectively is "greater than" + if (!propB && !propA) { + return 0; + } else if (!propA) { + return 1; + } else if (!propB) { + return -1; + } + //allow the user to preprocess the data + if (typeof sortInfo.grid.config.formatString == "function") { + propA = sortInfo.grid.config.formatString(propA, col); + propB = sortInfo.grid.config.formatString(propB, col); + } + //made it this far, we don't have to worry about null & undefined + if (direction === ASC) { + return sortFn(propA, propB); + } else { + return 0 - sortFn(propA, propB); + } + }; + //now actually sort the data + unwrappedData.sort(function (itemA, itemB) { + var result = 0; + var i = 0; + while(!result && i < sortInfos.length) { + if (sortInfos[i]) { + col = sortInfos[i].col; + sortFn = sortInfos[i].sortFn; + result = outerSortFn(itemA, itemB); + } + i++; + } + return result; + }); + data(unwrappedData); + return; + }, + Sort: function (sortInfo, data) { + if (window.kg.sortService.isSorting) { + return; + } + window.kg.sortService.isSorting = true; + window.kg.sortService.sortData(data, sortInfo); + window.kg.sortService.isSorting = false; + } +}; + +/*********************************************** +* FILE: ..\src\classes\domUtilityService.js +***********************************************/ +var getWidths = function () { + var $testContainer = $('
'); + $testContainer.appendTo('body'); + // 1. Run all the following measurements on startup! + //measure Scroll Bars + $testContainer.height(100).width(100).css("position", "absolute").css("overflow", "scroll"); + $testContainer.append('
'); + window.kg.domUtilityService.ScrollH = ($testContainer.height() - $testContainer[0].clientHeight); + window.kg.domUtilityService.ScrollW = ($testContainer.width() - $testContainer[0].clientWidth); + $testContainer.empty(); + //clear styles + $testContainer.attr('style', ''); + //measure letter sizes using a pretty typical font size and fat font-family + $testContainer.append('M'); + window.kg.domUtilityService.LetterW = $testContainer.children().first().width(); + $testContainer.remove(); +}; +window.kg.domUtilityService = { + AssignGridContainers: function (rootEl, grid) { + grid.$root = $(rootEl); + //Headers + grid.$topPanel = grid.$root.find(".kgTopPanel"); + grid.$groupPanel = grid.$root.find(".kgGroupPanel"); + grid.$footerContainer = grid.$root.find(".kgRowFooter"); + grid.$footerScroller = grid.$root.find(".kgRowFooterScroller"); + grid.$headerContainer = grid.$topPanel.find(".kgHeaderContainer"); + grid.$headerScroller = grid.$topPanel.find(".kgHeaderScroller"); + grid.$headers = grid.$headerScroller.children(); + //Viewport + grid.$viewport = grid.$root.find(".kgViewport"); + //Canvas + grid.$canvas = grid.$viewport.find(".kgCanvas"); + //Footers + grid.$footerPanel = grid.$root.find(".ngFooterPanel"); + window.kg.domUtilityService.UpdateGridLayout(grid); + }, + UpdateGridLayout: function(grid) { + //catch this so we can return the viewer to their original scroll after the resize! + var scrollTop = grid.$viewport.scrollTop(); + grid.elementDims.rootMaxW = grid.$root.width(); + grid.elementDims.rootMaxH = grid.$root.height(); + //check to see if anything has changed + grid.refreshDomSizes(); + grid.adjustScrollTop(scrollTop, true); //ensure that the user stays scrolled where they were + }, + BuildStyles: function(grid) { + var rowHeight = grid.config.rowHeight, + $style = grid.$styleSheet, + gridId = grid.gridId, + css, + cols = grid.visibleColumns(), + sumWidth = 0; + + if (!$style) { + $style = $('#' + gridId); + if (!$style[0]) { + $style = $("