--- /dev/null
+
+<!--
+ Actions row.
+ This sits above the grid and contains the column picker, etc.
+-->
+
+<div class="eg-grid-row eg-grid-action-row">
+
+ <div style="flex:1">
+ <div class="eg-grid-primary-label">{{grid.mainLabel}}</div>
+ </div>
+
+ <!-- column picker -->
+ <div class="btn-group column-picker">
+
+ <!-- first page -->
+ <button type="button" class="btn btn-default"
+ ng-class="{disabled : grid.onFirstPage()}"
+ ng-click="grid.offset = 0;grid.collect()"
+ title="[% l('Start') %]">
+ <span class="glyphicon glyphicon-fast-backward"></span>
+ </button>
+
+ <!-- previous page -->
+ <button type="button" class="btn btn-default"
+ ng-class="{disabled : grid.onFirstPage()}"
+ ng-click="grid.decrementPage()"
+ title="[% l('Previous Page') %]">
+ <span class="glyphicon glyphicon-backward"></span>
+ </button>
+
+ <!-- next page -->
+ <!-- todo: paging needs a total count value to be fully functional -->
+ <button type="button" class="btn btn-default"
+ ng-class="{disabled : !grid.hasNextPage()}"
+ ng-click="grid.incrementPage()"
+ title="[% l('Next Page') %]">
+ <span class="glyphicon glyphicon-forward"></span>
+ </button>
+
+ <div class="btn-group">
+ <button type="button" title="[% ('Select Row Count') %]"
+ class="btn btn-default dropdown-toggle" data-toggle="dropdown">
+ [% l('Rows [_1]', '{{grid.limit}}') %]
+ <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu">
+ <li ng-repeat="t in [5,10,25,50,100]">
+ <a href='' ng-click='grid.offset=0;grid.limit=t;grid.collect()'>
+ {{t}}
+ </a>
+ </li>
+ </ul>
+ </div>
+
+ <div class="btn-group">
+ <button type="button" title="[% ('Select Page') %]"
+ class="btn btn-default dropdown-toggle" data-toggle="dropdown">
+ [% l('Page [_1]', '{{grid.page()}}') %]
+ <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu">
+ <li>
+ <div class="input-group">
+ <input type="text" class="form-control"
+ ng-model="pageFromUI"
+ ng-click="$event.stopPropagation()"/>
+ <span class="input-group-btn">
+ <button class="btn btn-default" type="button"
+ ng-click="grid.goToPage(pageFromUI);pageFromUI=''">
+ [% l('Go To...') %]
+ </button>
+ </span>
+ </div>
+ </li>
+ <li role="presentation" class="divider"></li>
+ <li ng-repeat="t in [1,2,3,4,5,10,25,50,100]">
+ <a href='' ng-click='grid.goToPage(t)'>{{t}}</a>
+ </li>
+ </ul>
+ </div>
+
+ <button type="button"
+ class="btn btn-default dropdown-toggle"
+ data-toggle="dropdown"><span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu pull-right">
+ <li><a href='' ng-click="grid.toggleConfDisplay()">
+ <span class="glyphicon glyphicon-wrench"></span>
+ [% l('Configure Columns') %]
+ </a></li>
+ <li><a href='' ng-click="grid.columnsProvider.showAllColumns()">
+ <span class="glyphicon glyphicon-resize-full"></span>
+ [% l('Show All Columns') %]
+ </a></li>
+ <li><a href='' ng-click="grid.columnsProvider.hideAllColumns()">
+ <span class="glyphicon glyphicon-resize-small"></span>
+ [% l('Hide All Columns') %]
+ </a></li>
+ <li><a ng-click="grid.generateCSVExportURL()"
+ download="{{grid.csvExportFileName}}.csv" ng-href="{{grid.csvExportURL}}">
+ <span class="glyphicon glyphicon-download"></span>
+ [% l('Download CSV') %]
+ </a></li>
+ <li role="presentation" class="divider"></li>
+ <li ng-repeat="col in grid.columnsProvider.columns">
+ <a href='' ng-click="grid.columnsProvider.visible[col.name] =
+ !grid.columnsProvider.visible[col.name]">
+ <span ng-if="grid.columnsProvider.visible[col.name]"
+ class="label label-success">✓</span>
+ <span ng-if="!grid.columnsProvider.visible[col.name]"
+ class="label label-warning">✗</span>
+ <span>{{col.label}}</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+</div>
+
+<!-- Grid -->
+<div class="eg-grid" ng-class="{'eg-grid-as-conf' : grid.showGridConf}">
+
+ <!-- import our eg-grid-field defs -->
+ <div ng-transclude></div>
+
+ <div class="eg-grid-row eg-grid-header-row">
+ <div class="eg-grid-cell eg-grid-cell-stock" style="flex:{{grid.indexFlex}}">
+ <div>[% l('#') %]</div>
+ </div>
+ <div class="eg-grid-cell eg-grid-cell-stock" style="flex:{{grid.selectorFlex}}">
+ <div>
+ <input type='checkbox' ng-click="grid.toggleSelectAllItems()"/>
+ </div>
+ </div>
+ <div class="eg-grid-cell"
+ eg-grid-column-drag-dest
+ column="{{col.name}}"
+ eg-right-click="grid.onContextMenu($event)"
+ ng-repeat="col in grid.columnsProvider.columns"
+ style="flex:{{col.flex}}"
+ ng-show="grid.columnsProvider.visible[col.name]">
+
+ <div style="display:flex">
+ <div style="flex:1">
+ <a column="{{col.name}}" href=''
+ eg-grid-column-drag-source
+ ng-click="grid.quickSort(col.name)">{{col.label}}</a>
+ </div>
+ <div eg-grid-column-drag-source
+ drag-type="resize" column="{{col.name}}"
+ class="eg-grid-column-drag-handle"> </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Inline grid configuration row -->
+ <div class="eg-grid-row eg-grid-conf-row" ng-show="grid.showGridConf">
+ <div class="eg-grid-cell eg-grid-cell-conf-header"
+ style="flex:{{grid.indexFlex + grid.selectorFlex}}">
+ <div class="eg-grid-conf-cell-entry">[% l('Expand') %]</div>
+ <div class="eg-grid-conf-cell-entry">[% l('Shrink') %]</div>
+ <div class="eg-grid-conf-cell-entry">[% l('Sort') %]</div>
+ </div>
+ <div class="eg-grid-cell"
+ ng-repeat="col in grid.columnsProvider.columns"
+ style="flex:{{col.flex}}"
+ ng-show="grid.columnsProvider.visible[col.name]">
+ <div class="eg-grid-conf-cell-entry">
+ <a href="" title="[% l('Make column wider') %]"
+ ng-click="grid.modifyColumnFlex(col,1)">
+ <span class="glyphicon glyphicon-fast-forward"></span>
+ </a>
+ </div>
+ <div class="eg-grid-conf-cell-entry">
+ <a href="" title="[% l('Make column narrower') %]"
+ ng-click="grid.modifyColumnFlex(col,-1)">
+ <span class="glyphicon glyphicon-fast-backward"></span>
+ </a>
+ </div>
+ <div class="eg-grid-conf-cell-entry">
+ <input type='number' ng-model="col.sort"
+ title="[% l('Sort Priority / Direction') %]" style='width:2.3em'/>
+ </div>
+ </div>
+ </div>
+
+ <div class="eg-grid-content-body">
+ <div ng-show="grid.count() == 0"
+ class="alert alert-info">[% l('No Items To Display') %]</div>
+
+ <div class="eg-grid-row"
+ id="eg-grid-row-{{$index + 1}}"
+ ng-repeat="item in grid.items"
+ ng-show="grid.count() > 0"
+ ng-class="{'eg-grid-row-selected' : grid.selected[grid.indexValue(item)]}">
+ <div class="eg-grid-cell eg-grid-cell-stock" style="flex:{{grid.indexFlex}}"
+ ng-click="grid.handleRowClick($event, item)">
+ {{$index + grid.offset + 1}}
+ </div>
+ <div class="eg-grid-cell eg-grid-cell-stock" style="flex:{{grid.selectorFlex}}">
+ <!-- ng-click=handleRowClick here has unintended
+ consequences and is unnecessary, avoid it -->
+ <div>
+ <input type='checkbox'
+ ng-model="grid.selected[grid.indexValue(item)]"/>
+ </div>
+ </div>
+ <div class="eg-grid-cell eg-grid-cell-content"
+ ng-click="grid.handleRowClick($event, item)"
+ ng-repeat="col in grid.columnsProvider.columns"
+ style="flex:{{col.flex}}"
+ ng-show="grid.columnsProvider.visible[col.name]">
+ {{grid.dataProvider.itemFieldValue(item, col) | egGridValueFilter:col}}
+ </div>
+ </div>
+ </div>
+
+
+</div>
+
--- /dev/null
+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 : '<div></div>', // 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;
+ }
+ }
+}]);
+