--- /dev/null
+angular.module('egGridMod',
+ ['egCoreMod', '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_autogrid2',
+
+ 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',
+ function($scope, egIDL, egAuth, egNet,
+ egGridFlatDataProvider, egGridColumnsProvider) {
+
+ var grid = this;
+
+ grid.init = function() {
+ grid.offset = 0;
+ grid.limit = 25;
+ grid.items = [];
+ grid.selected = {}; // idField-based
+ 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;
+ 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.count = function() {
+ return grid.items.length;
+ }
+
+ 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.comesBefore = 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.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.comesBefore(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();
+ }
+
+ // 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) {
+ col.visible[column.name] = true;
+ });
+ }
+
+ cols.hideAllColumns = function() {
+ cols.visible = {};
+ }
+
+ 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);
+ }
+ };
+ }
+])
+
+/**
+ * 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, item) {
+ switch(item.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;
+ }
+ }
+}]);
+