From 5b59116026dee8aa8ed5fa2f1a722d54e92c09e7 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Sun, 6 Apr 2014 14:03:53 -0400 Subject: [PATCH] web staff : side-porting grid Signed-off-by: Bill Erickson --- Open-ILS/src/templates/staff/css/style.css.tt2 | 179 +++++ Open-ILS/src/templates/staff/parts/t_autogrid.tt2 | 220 ++++++ Open-ILS/web/js/ui/default/staff/services/grid.js | 779 ++++++++++++++++++++++ 3 files changed, 1178 insertions(+) create mode 100644 Open-ILS/src/templates/staff/parts/t_autogrid.tt2 create mode 100644 Open-ILS/web/js/ui/default/staff/services/grid.js diff --git a/Open-ILS/src/templates/staff/css/style.css.tt2 b/Open-ILS/src/templates/staff/css/style.css.tt2 index 0a48f53429..c524fa0423 100644 --- a/Open-ILS/src/templates/staff/css/style.css.tt2 +++ b/Open-ILS/src/templates/staff/css/style.css.tt2 @@ -86,3 +86,182 @@ table.list tr.selected td { .pad-horiz {padding : 0px 10px 0px 10px; } .pad-vert {padding : 20px 0px 10px 0px;} + +/* ---------------------------------------------------------------------- + * Grid + * ---------------------------------------------------------------------- */ + +.eg-grid-primary-label { + font-weight: bold; + font-size: 120%; +} + +/* odd/even row styling */ +.eg-grid-content-body > div:nth-child(odd):not(.eg-grid-row-selected) { + background-color: rgb(248, 248, 248); +} + +.eg-grid-row { + width: 100%; + display: flex; + border: 1px solid #ccc; +} + +.eg-grid-row:not(.eg-grid-header-row):not(.eg-grid-conf-row) { + height: 1.8em; +} + +.eg-grid-action-row { + border: none; + /* margin should not have to be this large; something's up */ + margin-bottom: 12px; +} + +.eg-grid-header-row { + font-weight: bold; +} + +.eg-grid-header-row > .eg-grid-cell { + border-right: 1px solid #CCC; + text-align: center; + + /* vertically align header cell text by treating + each header cell as a vertical flex container */ + display:flex; + flex-direction:column; + justify-content:flex-end; +} + +.eg-grid-cell { + /* avoid text flowing into adjacent cells */ + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +/* in config display, make cells more obvious */ +.eg-grid-as-conf .eg-grid-row { + border: 1px solid #777; +} +.eg-grid-as-conf .eg-grid-cell { + border-right: 1px solid #777; +} + +/* stock columns need fixed-width controls */ +.eg-grid-cell-stock { + flex: 1; + text-align: center; +} + +/* the conf header must be twice the stock flex */ +.eg-grid-cell-conf-header { + flex: 2; + font-weight: bold; +} + +.eg-grid-row-selected { + color: rgb(51, 51, 51); + background-color: rgb(201, 221, 225); + border-bottom: 1px solid #888; +} + +/* Improve ::selection styling by only allowing selection on text + * content cells within the main body of the grid. Otherwise, the browser + * styles row background and text (all dark blue?) when shift-click or + * click-drag is used. + */ +.eg-grid-content-body .eg-grid-row { + user-select:none; + -moz-user-select: none; + -webkit-user-select: none; +} +.eg-grid-content-body .eg-grid-cell-content { + user-select:text; + -moz-user-select: text; + -webkit-user-select: text; +} +.eg-grid-cell-content::-moz-selection { + color: rgb(51, 51, 51); + background: rgb(201, 221, 225); + border-bottom: 1px solid #888; +} +.eg-grid-cell-content::selection { + color: rgb(51, 51, 51); + background: rgb(201, 221, 225); + border-bottom: 1px solid #888; +} + +.eg-grid-conf-cell-entry { + width:98%; + text-align:center; + padding: 3px; +} + +.eg-grid-conf-cell-entry:not(:first-child) { + border-top:1px solid #ccc; +} + +.eg-grid-conf-row { + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.eg-grid-conf-row:first-child { + /* alignment fix; account for one missing border */ + padding-right: 1px; +} + +.eg-grid-col-drag, .eg-grid-col-drag:active { + /* similar to label-primary, sans padding */ + background-color: rgb(66, 139, 202); + color: #fff; +} + +.eg-grid-col-hover { + /* similar to label-success, sans padding */ + background-color: rgb(92, 184, 92); + color: #fff; +} + +.eg-grid-column-drag-handle { + width: 1px; + margin-left: 2px; + height: 100%; +} +.eg-grid-column-drag-handle:hover { + cursor: col-resize; + width: 3px; + border: 1px dashed rgb(66, 139, 202); +} + +.eg-grid-column-drag-handle-west { + cursor: w-resize; +} +.eg-grid-column-drag-handle-east { + cursor: e-resize; +} + + +/* hack to make the header columns line up with the content columns + when the scroll bar is visible along the right side of the content + columns. TODO: if this varies enough by browser, we'll need to + calculate the width instead. */ +/* +.eg-grid-scroll > .eg-grid-header-row, +.eg-grid-scroll > .eg-grid-conf-row { + padding-right: 15px; +} +.eg-grid-scroll > .eg-grid-content-body { + overflow-y:scroll; + height: 600px; +} +*/ + + +/* ---------------------------------------------------------------------- + * /Grid + * ---------------------------------------------------------------------- */ + +[%# +vim: ft=css +%] diff --git a/Open-ILS/src/templates/staff/parts/t_autogrid.tt2 b/Open-ILS/src/templates/staff/parts/t_autogrid.tt2 new file mode 100644 index 0000000000..f5e4868a5a --- /dev/null +++ b/Open-ILS/src/templates/staff/parts/t_autogrid.tt2 @@ -0,0 +1,220 @@ + + + +
+ +
+
{{grid.mainLabel}}
+
+ + +
+ + + + + + + + + + + +
+ + +
+ +
+ + +
+ + + +
+
+ + +
+ + +
+ +
+
+
[% l('#') %]
+
+
+
+ +
+
+
+ +
+ +
 
+
+
+
+ + +
+
+
[% l('Expand') %]
+
[% l('Shrink') %]
+
[% l('Sort') %]
+
+
+
+ + + +
+
+ + + +
+
+ +
+
+
+ +
+
[% l('No Items To Display') %]
+ +
+
+ {{$index + grid.offset + 1}} +
+
+ +
+ +
+
+
+ {{grid.dataProvider.itemFieldValue(item, col) | egGridValueFilter:col}} +
+
+
+ + +
+ diff --git a/Open-ILS/web/js/ui/default/staff/services/grid.js b/Open-ILS/web/js/ui/default/staff/services/grid.js new file mode 100644 index 0000000000..c9ef91b7e2 --- /dev/null +++ b/Open-ILS/web/js/ui/default/staff/services/grid.js @@ -0,0 +1,779 @@ +angular.module('egGridMod', + ['egCoreMod', 'egUiMod', 'ui.bootstrap']) + +.directive('egGrid', function() { + return { + restrict : 'AE', + transclude : true, + scope : { + + // IDL class hint (e.g. "aou") + idlClass : '@', + + // points to a structure in the calling scope which defines + // a PCRUD-compliant query. + query : '=', + + // if true, grid columns are derived from all non-virtual + // fields on the base idlClass + autoFields : '@', + + // grid preferences will be stored / retrieved with this key + persistKey : '@', + + // field whose value is unique and may be used for item + // reference / lookup. This will usually be someting like + // "id". This is not needed when using autoFields, since we + // can determine the primary key directly from the IDL. + idField : '@', + + // egList containting our tabular data is provided for us + // and managed externally. + dataProvider : '=', + + // if true, hide the sortPriority options in the + // grid configuration UI. This is primarily used by + // UIs where the data is ephemeral and can only be + // single-display-column sorted. + disableSortPriority : '@', + + // optional primary grid label + mainLabel : '@' + }, + + // TODO: avoid hard-coded url + templateUrl : '/eg/staff/parts/t_autogrid', + + link : function(scope, element, attrs) { + // link() is called after page compilation, which means our + // eg-grid-field's have been parsed and loaded. Now it's + // safe to perform our initial page load. + scope.grid.collect(); + }, + + controller : [ + '$scope','egIDL','egAuth','egNet', + 'egGridFlatDataProvider','egGridColumnsProvider', + '$filter','$window', + function($scope, egIDL, egAuth, egNet, + egGridFlatDataProvider, egGridColumnsProvider, + $filter, $window) { + + var grid = this; + + grid.init = function() { + grid.offset = 0; + grid.limit = 25; + grid.items = []; + grid.selected = {}; // idField-based + grid.totalCount = -1; + grid.dataProvider = $scope.dataProvider; + grid.idlClass = $scope.idlClass; + grid.mainLabel = $scope.mainLabel; + grid.indexField = $scope.idField; + grid.showGridConf = false; + + // default flex values for the index and selector columns + grid.indexFlex = 1; + grid.selectorFlex = 1; + + grid.columnsProvider = egGridColumnsProvider.instance({ + idlClass : grid.idlClass + }); + + if ($scope.autoFields) { + grid.indexField = egIDL.classes[grid.idlClass].pkey; + if (!grid.mainLabel) + grid.mainLabel = egIDL.classes[grid.idlClass].label; + grid.columnsProvider.compileAutoColumns(); + } + + if (!grid.dataProvider) { + grid.selfManagedData = true; + grid.dataProvider = egGridFlatDataProvider.instance({ + idlClass : grid.idlClass, + columnsProvider : grid.columnsProvider, + query : $scope.query + }); + } + + grid.compileSort(); + $scope.grid = grid; + } + + grid.onContextMenu = function($event) { + var col = angular.element($event.target).attr('column'); + } + + grid.page = function() { + return (grid.offset / grid.limit) + 1; + } + + grid.goToPage = function(page) { + page = Number(page); + if (angular.isNumber(page) && page > 0) { + grid.offset = (page - 1) * grid.limit; + grid.collect(); + } + } + + grid.onFirstPage = function() { + return grid.offset == 0; + } + + grid.hasNextPage = function() { + // we have less data than requested, there must + // not be any more pages + if (grid.count() < grid.limit) return false; + + // if the total count is not known, assume that a full + // page of data implies more pages are available. + if (grid.totalCount == -1) return true; + + // we have a full page of data, but is there more? + return grid.totalCount > (grid.offset + grid.count()); + } + + grid.incrementPage = function() { + grid.offset += grid.limit; + grid.collect(); + } + + grid.decrementPage = function() { + if (grid.offset < grid.limit) { + grid.offset = 0; + } else { + grid.offset -= grid.limit; + } + grid.collect(); + } + + // number of items loaded for the current page of results + grid.count = function() { + return grid.items.length; + } + + // returns the unique identifier value for the provided item + grid.indexValue = function(item) { + if (angular.isObject(item)) { + if (item !== null) { + if (grid.indexFieldAsFunction) + return item[grid.indexField](); + return item[grid.indexField]; + } + } + // passed a non-object; assume it's an index + return item; + } + + // selects one row after deselecting all of the others + grid.selectOneItem = function(index) { + grid.selected = {}; + grid.selected[index] = true; + } + + // selects or deselects an item, without affecting the others. + // returns true if the item is selected; false if de-selected. + grid.toggleSelectOneItem = function(index) { + if (grid.selected[index]) { + delete grid.selected[index]; + return false; + } else { + return grid.selected[index] = true; + } + } + + grid.selectAllItems = function() { + angular.forEach(grid.items, function(item) { + grid.selected[grid.indexValue(item)] = true + }); + } + + // if all are selected, deselect all, otherwise select all + grid.toggleSelectAllItems = function() { + if (Object.keys(grid.selected).length == grid.items.length) { + grid.selected = {}; + } else { + grid.selectAllItems(); + } + } + + // returns true if item1 appears in the list before item2; + // false otherwise. this is slightly more efficient that + // finding the position of each then comparing them. + // item1 / item2 may be an item or an item index + grid.itemComesBefore = function(itemOrIndex1, itemOrIndex2) { + var idx1 = grid.indexValue(itemOrIndex1); + var idx2 = grid.indexValue(itemOrIndex2); + + // use for() for early exit + for (var i = 0; i < grid.items.length; i++) { + var idx = grid.indexValue(grid.items[i]); + if (idx == idx1) return true; + if (idx == idx2) return false; + } + return false; + } + + // 0-based position of item in the current data set + grid.indexOf = function(item) { + var idx = grid.indexValue(item); + for (var i = 0; i < grid.items.length; i++) { + if (grid.indexValue(grid.items[i]) == idx) + return i; + } + return -1; + } + + grid.modifyColumnFlex = function(column, val) { + column.flex += val; + // prevent flex:0; use hiding instead + if (column.flex < 1) + column.flex = 1; + } + + // handles click, control-click, and shift-click + grid.handleRowClick = function($event, item) { + var index = grid.indexValue(item); + + if ($event.ctrlKey || $event.metaKey /* mac command */) { + // control-click + if (grid.toggleSelectOneItem(index)) + grid.lastSelectedItemIndex = index; + + } else if ($event.shiftKey) { + // shift-click + if (!grid.lastSelectedItemIndex || + index == grid.lastSelectedItemIndex) { + // no source row, just do a simple select + grid.selectOneItem(index); + grid.lastSelectedItemIndex = index; + return; + } + + var selecting = false; + var ascending = + grid.itemComesBefore(grid.lastSelectedItemIndex, item); + var startPos = + grid.indexOf(grid.lastSelectedItemIndex); + + // update to new last-selected + grid.lastSelectedItemIndex = index; + + // select each row between the last selected and + // currently selected items + while (true) { + startPos += ascending ? 1 : -1; + var curItem = grid.items[startPos]; + if (!curItem) break; + var curIdx = grid.indexValue(curItem); + grid.selected[curIdx] = true; + if (curIdx == index) break; // all done + } + + } else { + grid.selectOneItem(index); + grid.lastSelectedItemIndex = index; + } + } + + // Builds a sort expression from column sort priorities. + // called on page load and any time the priorities are modified. + grid.compileSort = function() { + var sortList = grid.columnsProvider.columns.filter( + function(col) { return Number(col.sort) != 0 } + ).sort( + function(a, b) { + if (Math.abs(a.sort) < Math.abs(b.sort)) + return -1; + return 1; + } + ); + + if (sortList.length) { + grid.dataProvider.sort = sortList.map(function(col) { + var blob = {}; + blob[col.name] = col.sort < 0 ? 'desc' : 'asc'; + return blob; + }); + } + } + + // builds a sort expression using a single column, + // toggling between ascending and descending sort. + grid.quickSort = function(col_name) { + var sort = grid.dataProvider.sort; + if (sort && sort.length && + sort[0] == col_name) { + var blob = {}; + blob[col_name] = 'desc'; + grid.dataProvider.sort = [blob]; + } else { + grid.dataProvider.sort = [col_name]; + } + + grid.collect(); + } + + // show / hide the grid configuration row + grid.toggleConfDisplay = function() { + if (grid.showGridConf) { + grid.showGridConf = false; + grid.compileSort(); + grid.collect(); + } else { + grid.showGridConf = true; + } + } + + // called when a dragged column is dropped onto itself + // or any other column + grid.onColumnDrop = function(target) { + if (angular.isUndefined(target)) return; + if (target == grid.dragColumn) return; + var srcIdx, targetIdx, srcCol; + angular.forEach(grid.columnsProvider.columns, + function(col, idx) { + if (col.name == grid.dragColumn) { + srcIdx = idx; + srcCol = col; + } else if (col.name == target) { + targetIdx = idx; + } + } + ); + + if (srcIdx < targetIdx) targetIdx--; + + // move src column from old location to new location in + // the columns array, then force a page refresh + grid.columnsProvider.columns.splice(srcIdx, 1); + grid.columnsProvider.columns.splice(targetIdx, 0, srcCol); + $scope.$apply(); + } + + // prepares a string for inclusion within a CSV document + // by escaping commas and quotes and removing newlines. + grid.csvDatum = function(str) { + str = ''+str; + if (!str) return ''; + str = str.replace(/\n/g, ''); + if (str.match(/\,/) || str.match(/"/)) { + str = str.replace(/"/g, '""'); + str = '"' + str + '"'; + } + return str; + } + + // sets the download file name and inserts the current CSV + // into a Blob URL for browser download. + grid.generateCSVExportURL = function() { + + // let the file name describe the grid + grid.csvExportFileName = + (grid.mainLabel || grid.persistKey || 'eg_grid_data') + .replace(/\s+/g, '_') + '_' + grid.page(); + + // toss the CSV into a Blob and update the export URL + var csv = grid.generateCSV(); + var blob = new Blob([csv], {type : 'text/plain'}); + grid.csvExportURL = + ($window.URL || $window.webkitURL).createObjectURL(blob); + } + + // generates CSV for the currently visible grid contents + grid.generateCSV = function() { + var csvStr = ''; + var colCount = grid.columnsProvider.columns.length; + + // columns + angular.forEach(grid.columnsProvider.columns, + function(col, idx) { + csvStr += grid.csvDatum(col.name); + if (idx < colCount -1) csvStr += ','; + } + ); + + csvStr += "\n"; + + // items + angular.forEach(grid.items, function(item) { + angular.forEach(grid.columnsProvider.columns, + function(col, idx) { + // bare value + var val = grid.dataProvider.itemFieldValue(item, col); + // filtered value (dates, etc.) + val = $filter('egGridValueFilter')(val, col); + csvStr += grid.csvDatum(val); + if (idx < colCount -1) csvStr += ','; + } + ); + csvStr += "\n"; + }); + + return csvStr; + } + + // asks the dataProvider for a page of data + grid.collect = function() { + grid.items = []; + grid.selectedItems = {}; + grid.dataProvider.get( + grid.offset, + grid.limit, + function(item) { + if (item) grid.items.push(item) + } + ); + } + + grid.init(); + }] + }; +}) + +/** + * eg-grid-field : used for collecting custom field data from the templates. + * This directive does not direct display, it just passes data up to the + * parent grid. + */ +.directive('egGridField', function() { + return { + require : '^egGrid', + restrict : 'AE', + transclude : true, + scope : { + name : '@', // required; unique name + path : '@', // optional; flesh path + label : '@', // optional; display label + flex : '@', // optoinal; default flex width + }, + template : '
', // NOOP template + link : function(scope, element, attrs, egGridCtrl) { + egGridCtrl.addColumn(scope); + } + }; +}) + +.factory('egGridColumnsProvider', ['egIDL', function(egIDL) { + + function ColumnsProvider(args) { + var cols = this; + cols.columns = []; + cols.visible = {}; + cols.idlClass = args.idlClass; + + cols.showAllColumns = function() { + angular.forEach(cols.columns, function(column) { + cols.visible[column.name] = true; + }); + } + + cols.hideAllColumns = function() { + cols.visible = {}; + } + + cols.indexOf = function(name) { + for (var i = 0; i < cols.columns.length; i++) { + if (cols.columns[i].name == name) + return i; + } + return -1; + } + + cols.findColumn = function(name) { + return cols.columns[cols.indexOf(name)]; + } + + cols.compileAutoColumns = function() { + + var idl_class = egIDL.classes[cols.idlClass]; + + angular.forEach( + idl_class.fields.sort( + function(a, b) { return a.name < b.name ? -1 : 1 }), + function(field) { + if (field.virtual) return; + if (field.datatype == 'link' || field.datatype == 'org_unit') { + // if the field is a link and the linked class has a + // "selector" field specified, use the selector field + // as the display field for the columns. + // flattener will take care of the fleshing. + if (field['class']) { + var selector_field = egIDL.classes[field['class']].fields + .filter(function(f) { return Boolean(f.selector) })[0]; + if (selector_field) { + field.path = field.name + '.' + selector_field.selector; + } + } + } + cols.add(field, true); + } + ); + } + + // Add a column to the columns collection. + // Columns may come from a slim eg-columns-field or + // directly from the IDL. + cols.add = function(colSpec, fromIDL) { + + var column = { + name : colSpec.name, + label : colSpec.label, + path : colSpec.path, + flex : Number(colSpec.flex) || 2, + sort : Number(colSpec.sort) || 0, + datatype : colSpec.datatype, + }; + + if (!column.name) column.name = column.path; + if (!column.path) column.path = column.name; + + if (colSpec.display !== false) + cols.visible[column.name] = true; + + cols.columns.push(column); + + if (fromIDL) return; + + // lookup the matching IDL field + var idl_field = cols.idlFieldFromPath(column.path); + + if (!idl_field) return; // ad-hoc field + + column.datatype = idl_field.datatype; + + if (!column.label) { + column.label = idl_field.label || column.name; + } + }, + + // finds the IDL field from the dotpath, using the columns + // idlClass as the base. + cols.idlFieldFromPath = function(dotpath) { + var class_obj = egIDL.classes[cols.idlClass]; + var path_parts = dotpath.split(/\./); + + // for() == early exit + var idl_field; + for (var path_idx in path_parts) { + var part = path_parts[path_idx]; + + // find the field object matching the path component + for (var field_idx in class_obj.fields) { + if (class_obj.fields[field_idx].name == part) { + idl_field = class_obj.fields[field_idx]; + break; + } + } + + // unless we're at the end of the list, this field should + // link to another class. + + if (idl_field && idl_field['class'] && ( + idl_field.datatype == 'link' || + idl_field.datatype == 'org_unit')) { + class_obj = egIDL.classes[idl_field['class']]; + } else { + if (path_idx < (path_parts.length - 1)) { + // we ran out of classes to hop through before + // we ran out of path components + console.error("egGrid: invalid IDL path: " + path); + } + } + } + + return idl_field; + } + } + + return { + instance : function(args) { return new ColumnsProvider(args) } + } +}]) + +// Factory service for egGridDataManager instances, which are +// responsible for collecting flattened grid data. +.factory('egGridFlatDataProvider', + ['egNet','egAuth', + function(egNet, egAuth) { + + function FlatDataProvider(args) { + var gridData = this; + + gridData.idlClass = args.idlClass; + gridData.query = args.query; + gridData.columnsProvider = args.columnsProvider; + gridData.sort = []; + + gridData.get = function(index, count, onresponse) { + + // fetch data for all currently visible columns + var queryFields = {} + angular.forEach(gridData.columnsProvider.columns, function(col) { + if (gridData.columnsProvider.visible[col.name]) + queryFields[col.name] = col.path; + }); + + egNet.request( + 'open-ils.fielder', + 'open-ils.fielder.flattened_search', + egAuth.token(), gridData.idlClass, queryFields, + gridData.query, + { sort : gridData.sort, + limit : count, + offset : index + } + ).then( + null, null, + function(item) { onresponse(item) } + ); + } + + gridData.itemFieldValue = function(item, column) { + // all of our data is flattened + return item[column.name]; + } + } + + return { + instance : function(args) { + return new FlatDataProvider(args); + } + }; + } +]) + +.directive('egGridColumnDragSource', function() { + return { + restrict : 'A', + require : '^egGrid', + link : function(scope, element, attrs, egGridCtrl) { + angular.element(element).attr('draggable', 'true'); + + element.bind('dragstart', function(e) { + egGridCtrl.dragColumn = attrs.column; + egGridCtrl.dragType = attrs.dragType || 'move'; // or resize + egGridCtrl.colResizeDir = 0; + angular.element(e.target).addClass('eg-grid-col-drag'); + }); + + element.bind('dragend', function(e) { + if (egGridCtrl.dragType == 'move') + angular.element(e.target).removeClass('eg-grid-col-drag'); + }); + } + }; +}) + +.directive('egGridColumnDragDest', function() { + return { + restrict : 'A', + require : '^egGrid', + link : function(scope, element, attrs, egGridCtrl) { + + element.bind('dragover', function(e) { // required for drop + e.stopPropagation(); + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + + if (egGridCtrl.colResizeDir == 0) return; // move + + var cols = egGridCtrl.columnsProvider; + var srcCol = egGridCtrl.dragColumn; + var srcColIdx = cols.indexOf(srcCol); + + if (egGridCtrl.colResizeDir == -1) { + if (cols.indexOf(attrs.column) <= srcColIdx) { + egGridCtrl.modifyColumnFlex( + egGridCtrl.columnsProvider.findColumn( + egGridCtrl.dragColumn), -1); + if (cols.columns[srcColIdx+1]) { + // source column shrinks by one, column to the + // right grows by one. + egGridCtrl.modifyColumnFlex( + cols.columns[srcColIdx+1], 1); + } + scope.$apply(); + } + } else { + if (cols.indexOf(attrs.column) > srcColIdx) { + egGridCtrl.modifyColumnFlex( + egGridCtrl.columnsProvider.findColumn( + egGridCtrl.dragColumn), 1); + if (cols.columns[srcColIdx+1]) { + // source column grows by one, column to the + // right grows by one. + egGridCtrl.modifyColumnFlex( + cols.columns[srcColIdx+1], -1); + } + + scope.$apply(); + } + } + }); + + element.bind('dragenter', function(e) { + e.stopPropagation(); + e.preventDefault(); + if (egGridCtrl.dragType == 'move') { + angular.element(e.target).addClass('eg-grid-col-hover'); + } else { + // resize grips are on the right side of each column. + // dragenter will either occur on the source column + // (dragging left) or the column to the right. + if (egGridCtrl.colResizeDir == 0) { + if (egGridCtrl.dragColumn == attrs.column) { + egGridCtrl.colResizeDir = -1; // west + } else { + egGridCtrl.colResizeDir = 1; // east + } + } + } + }); + + element.bind('dragleave', function(e) { + e.stopPropagation(); + e.preventDefault(); + if (egGridCtrl.dragType == 'move') { + angular.element(e.target).removeClass('eg-grid-col-hover'); + } + }); + + element.bind('drop', function(e) { + e.stopPropagation(); + e.preventDefault(); + egGridCtrl.colResizeDir = 0; + if (egGridCtrl.dragType == 'move') { + angular.element(e.target).removeClass('eg-grid-col-hover'); + egGridCtrl.onColumnDrop(attrs.column); // move the column + } + }); + } + }; +}) + + + +/** + * Translates bare IDL object values into display values. + * 1. Passes dates through the angular date filter + * 2. Translates bools to Booleans so the browser can display translated + * value. (Though we could manually translate instead..) + * Others likely to follow... + */ +.filter('egGridValueFilter', ['$filter', function($filter) { + return function(value, column) { + switch(column.datatype) { + case 'bool': + // Browser will translate true/false for us + return Boolean(value == 't'); + case 'timestamp': + // canned angular date filter FTW + return $filter('date')(value); + default: + return value; + } + } +}]); + -- 2.11.0