web staff : deprecating old grid
authorBill Erickson <berick@esilibrary.com>
Wed, 2 Apr 2014 14:07:43 +0000 (10:07 -0400)
committerBill Erickson <berick@esilibrary.com>
Wed, 2 Apr 2014 14:07:43 +0000 (10:07 -0400)
Signed-off-by: Bill Erickson <berick@esilibrary.com>
Open-ILS/src/templates/staff/parts/t_autogrid.tt2
Open-ILS/src/templates/staff/parts/t_autogrid2.tt2 [deleted file]
Open-ILS/src/templates/staff/parts/t_autogrid_deprecated.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/test/index.tt2
Open-ILS/web/js/ui/default/staff/services/grid.js
Open-ILS/web/js/ui/default/staff/services/grid2.js [deleted file]
Open-ILS/web/js/ui/default/staff/services/grid_deprecated.js [new file with mode: 0644]

index db9b271..b9f9499 100644 (file)
   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">{{gridLabel}}</div>
+    <div class="eg-grid-primary-label">{{grid.mainLabel}}</div>
   </div>
-
+  
   <!-- column picker -->
   <div class="btn-group column-picker">
-    <button type="button" class="btn btn-default dropdown-toggle" 
+
+    <!-- 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="toggleGridConf()">
+      <li><a href='' ng-click="grid.toggleConfDisplay()">
         <span class="glyphicon glyphicon-wrench"></span>
         [% l('Configure Columns') %]
       </a></li>
-      <li><a href='' ng-click="list.showAllColumns()">
+      <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="list.hideAllColumns()">
+      <li><a href='' ng-click="grid.columnsProvider.hideAllColumns()">
         <span class="glyphicon glyphicon-resize-small"></span>
         [% l('Hide All Columns') %]
       </a></li>
       <li role="presentation" class="divider"></li>
-      <li ng-repeat="col in list.allColumns">
-        <a href='' ng-click="list.displayColumns[col.name] = 
-            !list.displayColumns[col.name]">
-            <span ng-if="list.displayColumns[col.name]" 
+      <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">&#x2713;</span>
-            <span ng-if="!list.displayColumns[col.name]" 
+            <span ng-if="!grid.columnsProvider.visible[col.name]" 
               class="label label-warning">&#x2717;</span>
             <span>{{col.label}}</span>
         </a>
   </div>
 </div>
 
-<style>
-  /* MOVE ME */
-  .eg-grid-drag-handle {
-    width:2px; 
-    user-select: none; 
-    cursor: move;
-  }
-  .eg-grid-drag-handle:hover {
-    /*border:1px dashed #888;*/
-    color: red;
-  }
-
-</style>
-
 <!-- Grid -->
-<div class="eg-grid"
-  ng-class="{'eg-grid-as-conf' : showGridConf, 'eg-grid-scroll' : isScroll}">
+<div class="eg-grid" ng-class="{'eg-grid-as-conf' : grid.showGridConf}">
 
-  <!-- import embedded eg-grid-field defs via no-op transclude -->
+  <!-- import our eg-grid-field defs -->
   <div ng-transclude></div>
 
-  <!-- ================== -->
-  <!-- Column headers row -->
   <div class="eg-grid-row eg-grid-header-row">
-    <div class="eg-grid-cell eg-grid-cell-stock" style="flex:{{indexFlex}}">
-      <div eg-drag-source eg-drag-dest column="+index">[% l('#') %]</div>
+    <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:{{selectorFlex}}">
-      <div eg-drag-source eg-drag-dest column="+selector">
-        <input type='checkbox' ng-click="list.toggleSelectAll()"/>
+    <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-drag-dest column="{{column.name}}"
-        ng-repeat="column in list.allColumns"
-        style="flex:{{column.flex}};order:{{list.columnPosition(column.name)}}"
-        ng-show="list.displayColumns[column.name]">
-        <a eg-drag-source column="{{column.name}}" 
-          href='' ng-click="sortOn(column.name)">{{column.label}}</a>
+        eg-grid-column-drag-source 
+        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]">
+        <a column="{{col.name}}" href='' 
+          ng-click="grid.quickSort(col.name)">{{col.label}}</a>
     </div>
   </div>
 
-  <!-- ============================= -->
   <!-- Inline grid configuration row -->
-  <div class="eg-grid-row eg-grid-conf-row" ng-show="showGridConf">
+  <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:{{indexFlex + selectorFlex}}">
+        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="column in list.allColumns"
-      style="flex:{{column.flex}};order:{{list.columnPosition(column.name)}}"
-      ng-show="list.displayColumns[column.name]">
+      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="modifyColumnFlex(column,1)">
+          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="modifyColumnFlex(column,-1)">
+          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="column.sortPriority
-          title="[% l('Sort Priority') %]" style='width:2.3em'/>
+        <input type='number' ng-model="col.sort
+          title="[% l('Sort Priority / Direction') %]" style='width:2.3em'/>
       </div>
     </div>
   </div>
 
-  <!-- ================= -->
-  <!-- grid content rows -->
-  <!--
-  <div class="eg-grid-content-body" infinite-scroll="fetchMoreData()">
-  -->
-  <div class="eg-grid-content-body" ng-scroll-viewport style="height:600px;">
-        <!--
-        ng-repeat="item in list.items"
-        -->
-
-    <div ng-show="list.items.length == 0" 
+  <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-show="list.items.length > 0"
-        ng-scroll="item in gridDataManager"
-        ng-class="{'eg-grid-row-selected' : itemIsSelected(item)}">
-      <div class="eg-grid-cell eg-grid-cell-stock" style="flex:{{indexFlex}}"
-        ng-click="handleRowClick($event, item)">
-        {{$index + gridDataManager.offset}}
+        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:{{selectorFlex}}">
+      <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="list.selected[list.indexValue(item)]"/>
+            ng-model="grid.selected[grid.indexValue(item)]"/>
         </div>
       </div>
       <div class="eg-grid-cell"
-          ng-click="handleRowClick($event, item)"
-          ng-repeat="column in list.allColumns"
-          style="flex:{{column.flex}};order:{{list.columnPosition(column.name)}}"
-          ng-show="list.displayColumns[column.name]">
-        {{fieldValue(item, column.name) | egGridvalueFilter:column}}
+          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>
 
diff --git a/Open-ILS/src/templates/staff/parts/t_autogrid2.tt2 b/Open-ILS/src/templates/staff/parts/t_autogrid2.tt2
deleted file mode 100644 (file)
index b9f9499..0000000
+++ /dev/null
@@ -1,207 +0,0 @@
-
-<!-- 
-  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 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">&#x2713;</span>
-            <span ng-if="!grid.columnsProvider.visible[col.name]" 
-              class="label label-warning">&#x2717;</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-source 
-        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]">
-        <a column="{{col.name}}" href='' 
-          ng-click="grid.quickSort(col.name)">{{col.label}}</a>
-    </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"
-          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>
-
diff --git a/Open-ILS/src/templates/staff/parts/t_autogrid_deprecated.tt2 b/Open-ILS/src/templates/staff/parts/t_autogrid_deprecated.tt2
new file mode 100644 (file)
index 0000000..db9b271
--- /dev/null
@@ -0,0 +1,158 @@
+
+<!-- 
+  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">{{gridLabel}}</div>
+  </div>
+
+  <!-- column picker -->
+  <div class="btn-group column-picker">
+    <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="toggleGridConf()">
+        <span class="glyphicon glyphicon-wrench"></span>
+        [% l('Configure Columns') %]
+      </a></li>
+      <li><a href='' ng-click="list.showAllColumns()">
+        <span class="glyphicon glyphicon-resize-full"></span>
+        [% l('Show All Columns') %]
+      </a></li>
+      <li><a href='' ng-click="list.hideAllColumns()">
+        <span class="glyphicon glyphicon-resize-small"></span>
+        [% l('Hide All Columns') %]
+      </a></li>
+      <li role="presentation" class="divider"></li>
+      <li ng-repeat="col in list.allColumns">
+        <a href='' ng-click="list.displayColumns[col.name] = 
+            !list.displayColumns[col.name]">
+            <span ng-if="list.displayColumns[col.name]" 
+              class="label label-success">&#x2713;</span>
+            <span ng-if="!list.displayColumns[col.name]" 
+              class="label label-warning">&#x2717;</span>
+            <span>{{col.label}}</span>
+        </a>
+      </li>
+    </ul>
+  </div>
+</div>
+
+<style>
+  /* MOVE ME */
+  .eg-grid-drag-handle {
+    width:2px; 
+    user-select: none; 
+    cursor: move;
+  }
+  .eg-grid-drag-handle:hover {
+    /*border:1px dashed #888;*/
+    color: red;
+  }
+
+</style>
+
+<!-- Grid -->
+<div class="eg-grid"
+  ng-class="{'eg-grid-as-conf' : showGridConf, 'eg-grid-scroll' : isScroll}">
+
+  <!-- import embedded eg-grid-field defs via no-op transclude -->
+  <div ng-transclude></div>
+
+  <!-- ================== -->
+  <!-- Column headers row -->
+  <div class="eg-grid-row eg-grid-header-row">
+    <div class="eg-grid-cell eg-grid-cell-stock" style="flex:{{indexFlex}}">
+      <div eg-drag-source eg-drag-dest column="+index">[% l('#') %]</div>
+    </div>
+    <div class="eg-grid-cell eg-grid-cell-stock" style="flex:{{selectorFlex}}">
+      <div eg-drag-source eg-drag-dest column="+selector">
+        <input type='checkbox' ng-click="list.toggleSelectAll()"/>
+      </div>
+    </div>
+    <div class="eg-grid-cell"
+        eg-drag-dest column="{{column.name}}"
+        ng-repeat="column in list.allColumns"
+        style="flex:{{column.flex}};order:{{list.columnPosition(column.name)}}"
+        ng-show="list.displayColumns[column.name]">
+        <a eg-drag-source column="{{column.name}}" 
+          href='' ng-click="sortOn(column.name)">{{column.label}}</a>
+    </div>
+  </div>
+
+  <!-- ============================= -->
+  <!-- Inline grid configuration row -->
+  <div class="eg-grid-row eg-grid-conf-row" ng-show="showGridConf">
+    <div class="eg-grid-cell eg-grid-cell-conf-header" 
+        style="flex:{{indexFlex + 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="column in list.allColumns"
+      style="flex:{{column.flex}};order:{{list.columnPosition(column.name)}}"
+      ng-show="list.displayColumns[column.name]">
+      <div class="eg-grid-conf-cell-entry">
+        <a href="" title="[% l('Make column wider') %]"
+          ng-click="modifyColumnFlex(column,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="modifyColumnFlex(column,-1)">
+          <span class="glyphicon glyphicon-fast-backward"></span>
+        </a>
+      </div>
+      <div class="eg-grid-conf-cell-entry">
+        <input type='number' ng-model="column.sortPriority" 
+          title="[% l('Sort Priority') %]" style='width:2.3em'/>
+      </div>
+    </div>
+  </div>
+
+  <!-- ================= -->
+  <!-- grid content rows -->
+  <!--
+  <div class="eg-grid-content-body" infinite-scroll="fetchMoreData()">
+  -->
+  <div class="eg-grid-content-body" ng-scroll-viewport style="height:600px;">
+        <!--
+        ng-repeat="item in list.items"
+        -->
+
+    <div ng-show="list.items.length == 0" 
+      class="alert alert-info">[% l('No Items To Display') %]</div>
+
+    <div class="eg-grid-row" 
+        id="eg-grid-row-{{$index + 1}}"
+        ng-show="list.items.length > 0"
+        ng-scroll="item in gridDataManager"
+        ng-class="{'eg-grid-row-selected' : itemIsSelected(item)}">
+      <div class="eg-grid-cell eg-grid-cell-stock" style="flex:{{indexFlex}}"
+        ng-click="handleRowClick($event, item)">
+        {{$index + gridDataManager.offset}}
+      </div>
+      <div class="eg-grid-cell eg-grid-cell-stock" style="flex:{{selectorFlex}}">
+        <!-- ng-click=handleRowClick here has unintended 
+             consequences and is unnecessary, avoid it -->
+        <div>
+          <input type='checkbox'  
+            ng-model="list.selected[list.indexValue(item)]"/>
+        </div>
+      </div>
+      <div class="eg-grid-cell"
+          ng-click="handleRowClick($event, item)"
+          ng-repeat="column in list.allColumns"
+          style="flex:{{column.flex}};order:{{list.columnPosition(column.name)}}"
+          ng-show="list.displayColumns[column.name]">
+        {{fieldValue(item, column.name) | egGridvalueFilter:column}}
+      </div>
+    </div>
+  </div>
+</div>
+
index 7ebbcbd..0f360bd 100644 (file)
@@ -8,7 +8,7 @@
 [% BLOCK APP_JS %]
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/list.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
-<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid2.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/test/app.js"></script>
 [% END %]
 
index c96733f..81eb00e 100644 (file)
@@ -1,12 +1,12 @@
-
 angular.module('egGridMod', 
-    ['egCoreMod', 'egListMod', 'egUiMod', 'ui.bootstrap', 'ui.scroll.jqlite', 'ui.scroll'])
+    ['egCoreMod', 'egUiMod', 'ui.bootstrap'])
 
-.directive('egGrid', function($window) {
+.directive('egGrid', function() {
     return {
         restrict : 'AE',
         transclude : true,
         scope : {
+
             // IDL class hint (e.g. "aou")
             idlClass : '@',
 
@@ -21,10 +21,6 @@ angular.module('egGridMod',
             // grid preferences will be stored / retrieved with this key
             persistKey : '@',
 
-            // if true, use the scroll CSS to force a vertical height 
-            // and scroll bar
-            isScroll : '@',
-
             // 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
@@ -33,7 +29,7 @@ angular.module('egGridMod',
 
             // egList containting our tabular data is provided for us
             // and managed externally.
-            egList : '=',
+            dataProvider : '=',
             
             // if true, hide the sortPriority options in the
             // grid configuration UI.  This is primarily used by
@@ -45,319 +41,327 @@ angular.module('egGridMod',
             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.fetchData();
+            scope.grid.collect();
         },
 
-        templateUrl : '/eg/staff/parts/t_autogrid', // TODO: avoid abs url
+        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.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
+                });
 
-        controller : // TODO: reqs list
-            function($scope, $timeout, $location, egIDL, egAuth, egNet, egList, egGridData) { 
-            var self = this;
+                if ($scope.autoFields) {
+                    grid.indexField = egIDL.classes[grid.idlClass].pkey;
+                    if (!grid.mainLabel)
+                        grid.mainLabel = egIDL.classes[grid.idlClass].label;
+                    grid.columnsProvider.compileAutoColumns();
+                }
 
-            // setup function. called at the end of the controller
-            this.init = function() {
-                self.limit = 10; 
-                self.offset = 0;
+                if (!grid.dataProvider) {
+                    grid.selfManagedData = true;
+                    grid.dataProvider = egGridFlatDataProvider.instance({
+                        idlClass : grid.idlClass,
+                        columnsProvider : grid.columnsProvider,
+                        query : $scope.query
+                    });
+                }
 
-                $scope.indexFlex = 1;
-                $scope.selectorFlex = 1;
-                $scope.gridLabel = $scope.mainLabel;
+                grid.compileSort();
+                $scope.grid = grid;
+            }
 
-                if (!$scope.query) {
-                    console.error("egGrid requires a query");
-                    return;
-                }
+            grid.onContextMenu = function($event) {
+                var col = angular.element($event.target).attr('column');
+            }
 
-                if (!$scope.idlClass) {
-                    console.error("egGrid requires an idlClass");
-                    return;
-                }
+            grid.page = function() {
+                return (grid.offset / grid.limit) + 1;
+            }
 
-                if ($scope.egList) {
-                    $scope.list = $scope.egList;
-                } else {
-                    self.selfManaged = true;
-                    $scope.list = egList.create();
+            grid.goToPage = function(page) {
+                page = Number(page);
+                if (angular.isNumber(page) && page > 0) {
+                    grid.offset = (page - 1) * grid.limit;
+                    grid.collect();
                 }
+            }
 
-                if ($scope.autoFields)
-                    self.compileAutoFields();
+            grid.onFirstPage = function() {
+                return grid.offset == 0;
+            }
 
-                $scope.list.indexField = $scope.idField;
+            grid.hasNextPage = function() {
+                // we have less data than requested, there must
+                // not be any more pages
+                if (grid.count() < grid.limit) return false;
 
-                $scope.gridDataManager = egGridData.create({
-                    idlClass : $scope.idlClass,
-                    query : $scope.query,
-                    list : $scope.list,
-                    // TODO: eg_grid_offset assumes one grid per page.
-                    offset : parseInt($location.search().eg_grid_offset)
-                });
-            }
+                // 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;
 
-            // clicking on a column header performs a quick, single-column
-            // sort.  Sorts on the same column toggle between ascending
-            // and descending sort.
-            $scope.sortOn = function(col_name) {
-                var sort = $scope.gridDataManager.sort;
-                if (sort && sort.length &&
-                    sort[0] == col_name) {
-                    var blob = {};
-                    blob[col_name] = 'desc';
-                    sort = [blob];
-                } else {
-                    sort = [col_name];
-                }
-                $scope.gridDataManager.applySort(sort);
+                // we have a full page of data, but is there more?
+                return grid.totalCount > (grid.offset + grid.count());
             }
 
-            $scope.modifyColumnFlex = function(column, val) {
-                column.flex += val;
-                // prevent flex:0;  use hiding instead
-                if (column.flex < 1)
-                    column.flex = 1;
+            grid.incrementPage = function() {
+                grid.offset += grid.limit;
+                grid.collect();
             }
 
-            $scope.toggleGridConf = function() {
-                if ($scope.showGridConf) {
-                    $scope.showGridConf = false;
-                    $scope.gridDataManager.applySort();
+            grid.decrementPage = function() {
+                if (grid.offset < grid.limit) {
+                    grid.offset = 0;
                 } else {
-                    $scope.showGridConf = true;
+                    grid.offset -= grid.limit;
                 }
+                grid.collect();
             }
 
-            /**
-             * Adds a column from an eg-grid-field or directly from 
-             * an IDL field via compileAutoFields.
-             */
-            this.addColumn = function(fieldSpec) {
-
-                var field = {
-                    name  : fieldSpec.name,
-                    label : fieldSpec.label,
-                    path  : fieldSpec.path,
-                    flex  : fieldSpec.flex,
-                    datatype : fieldSpec.datatype,
-                    display  : (fieldSpec.display !== false)
-                };
-
-                if (!field.name) field.name = field.path;
-                if (!field.path) field.path = field.name;
-
-                field = self.absorbField(field);
-                $scope.list.addColumn(field);
+            // number of items loaded for the current page of results
+            grid.count = function() {
+                return grid.items.length;
             }
 
-            $scope.fieldValue = function(item, key) {
-                if ($scope.egList) 
-                    return $scope.list.fieldValue(item, key);
-                // if we are managing the data, then our data is flat
-                return item ? item[key] : '';
+            // 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; 
             }
 
-            /**
-             * Caller wants to display all fields for the selected IDL class
-             * Find the fields and, when a field is a link, fetch the label
-             * from the "selector" field as well.
-             */
-            this.compileAutoFields = function() {
-                if ($scope.list.allColumns.length) return;
-                var idlClass = egIDL.classes[$scope.idlClass];
-
-                $scope.idField = $scope.idField || idlClass.pkey;
+            // selects one row after deselecting all of the others
+            grid.selectOneItem = function(index) {
+                grid.selected = {};
+                grid.selected[index] = true;
+            }
 
-                if (!$scope.gridLabel) {
-                    $scope.gridLabel = idlClass.label;
+            // 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;
                 }
-
-                angular.forEach(
-                    idlClass.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 grid.
-                            // flattener will take care of the fleshing.
-                            if (field['class']) {
-                                var selectorField = egIDL.classes[field['class']].fields
-                                    .filter(function(f) { return Boolean(f.selector) })[0];
-                                if (selectorField) {
-                                    field.path = field.name + '.' + selectorField.selector;
-                                }
-                            }
-                        }
-                        self.addColumn(field);
-                    }
-                );
             }
 
-            // given a base class and a dotpath, find the IDL field
-            this.getIDLFieldFromPath = function(idlClass, path) {
-                var class_obj = egIDL.classes[idlClass];
-                var path_parts = path.split(/\./);
-
-                // note: use of for() is intentional 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;
-                        }
-                    }
+            grid.selectAllItems = function() {
+                angular.forEach(grid.items, function(item) {
+                    grid.selected[grid.indexValue(item)] = true
+                });
+            }
 
-                    // 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);
-                        }
-                    }
+            // 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();
                 }
+            }
 
-                return idl_field;
+            // 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;
             }
 
-            /**
-             * Looks for the matching IDL field to extract the label
-             * and datattype as needed.
-             * Creates a local copy of the field for our internal
-             * machinations.
-             */
-            this.absorbField = function(field) {
-
-                // start by cloning the field so we can flesh it out.
-                // note: aungular.copy won't work, because 'field' may
-                // be a $scope object.
-                var new_field = {
-                    name : field.name,
-                    label : field.label,
-                    path : field.path,
-                    flex : Number(field.flex) || 2,
-                    display : (field.display === false) ? false : true
-                };
-
-                // lookup the matching IDL field
-                var idl_field = field.datatype ? field : 
-                    self.getIDLFieldFromPath($scope.idlClass, field.path);
-
-                // No matching IDL field.  Caller has gone commando.
-                // Nothing left to do.
-                if (!idl_field) return new_field;
-
-                new_field.datatype = idl_field.datatype;
-
-                if (field.label) {
-                    // caller-provided label
-                    new_field.label = field.label;
-                } else {
-                    if (idl_field.label) {
-                        new_field.label = idl_field.label;
-                    } else {
-                        new_field.label = new_field.name;
-                    }
+            // 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;
+            }
 
-                return new_field;
+            grid.modifyColumnFlex = function(column, val) {
+                column.flex += val;
+                // prevent flex:0;  use hiding instead
+                if (column.flex < 1)
+                    column.flex = 1;
             }
 
-            $scope.handleRowClick = function($event, item) {
-                var index = $scope.list.indexValue(item);
+            // 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 ($scope.list.toggleOneSelection(index)) 
-                        self.lastSelectedRowIndex = index;
+                    if (grid.toggleSelectOneItem(index)) 
+                        grid.lastSelectedItemIndex = index;
 
                 } else if ($event.shiftKey) { 
                     // shift-click
-                    if (!self.lastSelectedRowIndex || 
-                            index == self.lastSelectedRowIndex) {
+                    if (!grid.lastSelectedItemIndex || 
+                            index == grid.lastSelectedItemIndex) {
                         // no source row, just do a simple select
-                        $scope.list.selectOne(index);
-                        self.lastSelectedRowIndex = index;
+                        grid.selectOneItem(index);
+                        grid.lastSelectedItemIndex = index;
                         return;
                     }
 
                     var selecting = false;
                     var ascending = 
-                        $scope.list.comesBefore(self.lastSelectedRowIndex, item);
+                        grid.comesBefore(grid.lastSelectedItemIndex, item);
                     var startPos = 
-                        $scope.list.indexOf(self.lastSelectedRowIndex);
+                        grid.indexOf(grid.lastSelectedItemIndex);
 
                     // update to new last-selected
-                    self.lastSelectedRowIndex = index;
+                    grid.lastSelectedItemIndex = index;
 
                     // select each row between the last selected and 
                     // currently selected items
                     while (true) {
                         startPos += ascending ? 1 : -1;
-                        var curItem = $scope.list.items[startPos];
+                        var curItem = grid.items[startPos];
                         if (!curItem) break;
-                        var curIdx = $scope.list.indexValue(curItem);
-                        $scope.list.selected[curIdx] = true;
+                        var curIdx = grid.indexValue(curItem);
+                        grid.selected[curIdx] = true;
                         if (curIdx == index) break; // all done
                     }
                         
                 } else {
-                    $scope.list.selectOne(index);
-                    self.lastSelectedRowIndex = index;
+                    grid.selectOneItem(index);
+                    grid.lastSelectedItemIndex = index;
                 }
             }
 
-            $scope.itemIsSelected = function(item) {
-                return $scope.list.selected[
-                    $scope.list.indexValue(item)
-                ];
+            // 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;
+                    });
+                }
             }
 
-            this.onColumnDrag = function(col) {
-                // track which column we're dragging
-                self.dragColumn = col;
+            // 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();
             }
 
-            // if the target column does not match the source column,
-            // increase the size of the source column.
-            this.onColumnDragOver = function(target) {
-                if (angular.isUndefined(target)) return;
-                if (target == self.dragColumn) return;
-                if (self.dragColumn == '+index') {
-                    $scope.indexFlex += 1;
-                } else if (self.dragColumn == '+selector') {                             
-                    $scope.selectorFlex += 1;        
+            grid.toggleConfDisplay = function() {
+                if (grid.showGridConf) {
+                    grid.showGridConf = false;
+                    grid.compileSort();
+                    grid.collect();
                 } else {
-                    var column = $scope.list.findColumn(self.dragColumn);
-                    $scope.modifyColumnFlex(column, 1);
+                    grid.showGridConf = true;
                 }
-                $scope.$apply(); // needed
             }
 
-            /*
-            $scope.fetchMoreData = function() {
-                console.log('fetchMoreData');
-                self.offset += self.limit;
-                $scope.fetchData();
+            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(); 
             }
-            */
 
-            this.init();
-        }
+            // 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();
+        }]
     };
 })
 
@@ -384,147 +388,251 @@ angular.module('egGridMod',
     };
 })
 
-/**
- * Factory service for egGridDataManager instances, which are
- * responsible for collecting flattened grid data.
- */
-.factory('egGridData', ['egNet','egAuth',
+.factory('egGridColumnsProvider', ['egIDL', function(egIDL) {
 
-    function(egNet, egAuth) {
+    function ColumnsProvider(args) {
+        var cols = this;
+        cols.columns = [];
+        cols.visible = {};
+        cols.idlClass = args.idlClass;
 
-        // per-grid data manager class
-        function EgGridDataManager(args) {
-            var self = this;
+        cols.showAllColumns = function() {
+            angular.forEach(cols.columns, function(column) {
+                cols.visible[column.name] = true;
+            });
+        }
 
-            this.idlClass = args.idlClass;
-            this.query = args.query;
-            this.list = args.list;
-            this.offset = args.offset || 0;
-            this.version = 1;
+        cols.hideAllColumns = function() {
+            cols.visible = {};
+        }
 
-            this.reset = function() {
-                self.list.resetPageData();
-                self.version++;
+        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) {
 
-            this.revision = function() {
-                return self.version;
+            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;
             }
+        },
 
-            this.get = function(index, count, success) {
-                index -= 1; // make it zero-based
+        // 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;
+                    }
+                }
 
-                if (self.offset) { index += self.offset }
-                
-                if (index < 0) return success([]);
+                // unless we're at the end of the list, this field should
+                // link to another class.
 
-                console.log('index = ' + index + ' : count = ' + count);
+                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);
+                    }
+                }
+            }
 
-                // we may already have the data
-                var slice = self.list.items.slice(index, index + count);
-                if (slice.length && slice[0]) return success(slice);
+            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(self.list.allColumns, function(field) {
-                    if (self.list.displayColumns[field.name])
-                        queryFields[field.name] = field.path || field.name;
+                angular.forEach(gridData.columnsProvider.columns, function(col) {
+                    if (gridData.columnsProvider.visible[col.name]) 
+                        queryFields[col.name] = col.path;
                 });
 
-                var respIndex = index;
                 egNet.request(
                     'open-ils.fielder',
                     'open-ils.fielder.flattened_search',
-                    egAuth.token(), self.idlClass, queryFields,
-                    self.query,
-                    {   sort : self.sort,
+                    egAuth.token(), gridData.idlClass, queryFields,
+                    gridData.query,
+                    {   sort : gridData.sort,
                         limit : count,
                         offset : index
                     }
                 ).then( 
-                    function() { // oncomplete
-                        success(self.list.items.slice(index, index + count)); 
-                    }, 
-                    null, // onerror
-                    function(item) { // onmessage
-                        self.list.items[respIndex++] = item;
-                    }
+                    null, null, 
+                    function(item) { onresponse(item) }
                 );
             }
 
-            this.applySort = function(sortBlob) {
-                self.offset = 0;
-
-                if (sortBlob) {
-                    self.sort = sortBlob;
-
-                } else {
-                    // no sort provided, compile the sort blob
-                    // from the column sort priority values
-
-                    var sortList = self.list.allColumns.filter(
-                        function(col) { return Number(col.sortPriority) != 0 }
-                    ).sort( 
-                        function(a, b) { 
-                            if (Math.abs(a.sortPriority) < Math.abs(b.sortPriority))
-                                return -1;
-                            return 1;
-                        }
-                    );
-
-                    self.sort = sortList.map(function(col) {
-                        var blob = {};
-                        blob[col.name] = col.sortPriority < 0 ? 'desc' : 'asc';
-                        return blob;
-                    });
-                }
-
-                self.reset();
+            gridData.itemFieldValue = function(item, column) {
+                // all of our data is flattened
+                return item[column.name];
             }
         }
 
         return {
-            create : function(args) {
-                return new EgGridDataManager(args);
+            instance : function(args) {
+                return new FlatDataProvider(args);
             }
         };
     }
 ])
 
-/** Simplified dnd directives for grid column controls.
- *  Extract these out if the can be made generic enough
- */
-
-.directive('egDragSource', function() {
+.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) {
-                var col = angular.element(e.target).attr('column');
-                egGridCtrl.onColumnDrag(col);
+                egGridCtrl.dragColumn = attrs.column;
+                angular.element(e.target).addClass('eg-grid-col-drag');
+            });
+
+            element.bind('dragend', function(e) {
+                console.log('dragend');
+                angular.element(e.target).removeClass('eg-grid-col-drag');
             });
         }
     };
 })
 
-.directive('egDragDest', function() {
+.directive('egGridColumnDragDest', function() {
     return {
         restrict : 'A',
         require : '^egGrid',
         link : function(scope, element, attrs, egGridCtrl) {
-            element.bind('dragover', function(e) {
+
+            element.bind('dragover', function(e) { // required for drop
+                e.stopPropagation();
+                e.preventDefault();
+                e.dataTransfer.dropEffect = 'copy';
+            });
+
+            element.bind('dragenter', function(e) {
+                e.stopPropagation();
+                e.preventDefault();
+                angular.element(e.target).addClass('eg-grid-col-hover');
+            });
+
+            element.bind('dragleave', function(e) {
                 e.stopPropagation();
                 e.preventDefault();
-                //e.dataTransfer.dropEffect = 'copy';
-                var col = angular.element(e.target).attr('column');
-                console.log('dragover ' + col);
-                egGridCtrl.onColumnDragOver(col);
+                angular.element(e.target).removeClass('eg-grid-col-hover');
+            });
+
+            element.bind('drop', function(e) {
+                e.stopPropagation();
+                e.preventDefault();
+                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
@@ -547,4 +655,3 @@ angular.module('egGridMod',
     }                                                                          
 }]);
 
-
diff --git a/Open-ILS/web/js/ui/default/staff/services/grid2.js b/Open-ILS/web/js/ui/default/staff/services/grid2.js
deleted file mode 100644 (file)
index f72278c..0000000
+++ /dev/null
@@ -1,657 +0,0 @@
-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_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.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.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.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.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();
-            }
-
-            grid.toggleConfDisplay = function() {
-                if (grid.showGridConf) {
-                    grid.showGridConf = false;
-                    grid.compileSort();
-                    grid.collect();
-                } else {
-                    grid.showGridConf = true;
-                }
-            }
-
-            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(); 
-            }
-
-            // 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;
-                angular.element(e.target).addClass('eg-grid-col-drag');
-            });
-
-            element.bind('dragend', function(e) {
-                console.log('dragend');
-                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 = 'copy';
-            });
-
-            element.bind('dragenter', function(e) {
-                e.stopPropagation();
-                e.preventDefault();
-                angular.element(e.target).addClass('eg-grid-col-hover');
-            });
-
-            element.bind('dragleave', function(e) {
-                e.stopPropagation();
-                e.preventDefault();
-                angular.element(e.target).removeClass('eg-grid-col-hover');
-            });
-
-            element.bind('drop', function(e) {
-                e.stopPropagation();
-                e.preventDefault();
-                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, 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;                                                  
-        }                                                                      
-    }                                                                          
-}]);
-
diff --git a/Open-ILS/web/js/ui/default/staff/services/grid_deprecated.js b/Open-ILS/web/js/ui/default/staff/services/grid_deprecated.js
new file mode 100644 (file)
index 0000000..c96733f
--- /dev/null
@@ -0,0 +1,550 @@
+
+angular.module('egGridMod', 
+    ['egCoreMod', 'egListMod', 'egUiMod', 'ui.bootstrap', 'ui.scroll.jqlite', 'ui.scroll'])
+
+.directive('egGrid', function($window) {
+    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 : '@',
+
+            // if true, use the scroll CSS to force a vertical height 
+            // and scroll bar
+            isScroll : '@',
+
+            // 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.
+            egList : '=',
+            
+            // 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 : '@'
+        },
+
+        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.fetchData();
+        },
+
+        templateUrl : '/eg/staff/parts/t_autogrid', // TODO: avoid abs url
+
+        controller : // TODO: reqs list
+            function($scope, $timeout, $location, egIDL, egAuth, egNet, egList, egGridData) { 
+            var self = this;
+
+            // setup function. called at the end of the controller
+            this.init = function() {
+                self.limit = 10; 
+                self.offset = 0;
+
+                $scope.indexFlex = 1;
+                $scope.selectorFlex = 1;
+                $scope.gridLabel = $scope.mainLabel;
+
+                if (!$scope.query) {
+                    console.error("egGrid requires a query");
+                    return;
+                }
+
+                if (!$scope.idlClass) {
+                    console.error("egGrid requires an idlClass");
+                    return;
+                }
+
+                if ($scope.egList) {
+                    $scope.list = $scope.egList;
+                } else {
+                    self.selfManaged = true;
+                    $scope.list = egList.create();
+                }
+
+                if ($scope.autoFields)
+                    self.compileAutoFields();
+
+                $scope.list.indexField = $scope.idField;
+
+                $scope.gridDataManager = egGridData.create({
+                    idlClass : $scope.idlClass,
+                    query : $scope.query,
+                    list : $scope.list,
+                    // TODO: eg_grid_offset assumes one grid per page.
+                    offset : parseInt($location.search().eg_grid_offset)
+                });
+            }
+
+            // clicking on a column header performs a quick, single-column
+            // sort.  Sorts on the same column toggle between ascending
+            // and descending sort.
+            $scope.sortOn = function(col_name) {
+                var sort = $scope.gridDataManager.sort;
+                if (sort && sort.length &&
+                    sort[0] == col_name) {
+                    var blob = {};
+                    blob[col_name] = 'desc';
+                    sort = [blob];
+                } else {
+                    sort = [col_name];
+                }
+                $scope.gridDataManager.applySort(sort);
+            }
+
+            $scope.modifyColumnFlex = function(column, val) {
+                column.flex += val;
+                // prevent flex:0;  use hiding instead
+                if (column.flex < 1)
+                    column.flex = 1;
+            }
+
+            $scope.toggleGridConf = function() {
+                if ($scope.showGridConf) {
+                    $scope.showGridConf = false;
+                    $scope.gridDataManager.applySort();
+                } else {
+                    $scope.showGridConf = true;
+                }
+            }
+
+            /**
+             * Adds a column from an eg-grid-field or directly from 
+             * an IDL field via compileAutoFields.
+             */
+            this.addColumn = function(fieldSpec) {
+
+                var field = {
+                    name  : fieldSpec.name,
+                    label : fieldSpec.label,
+                    path  : fieldSpec.path,
+                    flex  : fieldSpec.flex,
+                    datatype : fieldSpec.datatype,
+                    display  : (fieldSpec.display !== false)
+                };
+
+                if (!field.name) field.name = field.path;
+                if (!field.path) field.path = field.name;
+
+                field = self.absorbField(field);
+                $scope.list.addColumn(field);
+            }
+
+            $scope.fieldValue = function(item, key) {
+                if ($scope.egList) 
+                    return $scope.list.fieldValue(item, key);
+                // if we are managing the data, then our data is flat
+                return item ? item[key] : '';
+            }
+
+            /**
+             * Caller wants to display all fields for the selected IDL class
+             * Find the fields and, when a field is a link, fetch the label
+             * from the "selector" field as well.
+             */
+            this.compileAutoFields = function() {
+                if ($scope.list.allColumns.length) return;
+                var idlClass = egIDL.classes[$scope.idlClass];
+
+                $scope.idField = $scope.idField || idlClass.pkey;
+
+                if (!$scope.gridLabel) {
+                    $scope.gridLabel = idlClass.label;
+                }
+
+                angular.forEach(
+                    idlClass.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 grid.
+                            // flattener will take care of the fleshing.
+                            if (field['class']) {
+                                var selectorField = egIDL.classes[field['class']].fields
+                                    .filter(function(f) { return Boolean(f.selector) })[0];
+                                if (selectorField) {
+                                    field.path = field.name + '.' + selectorField.selector;
+                                }
+                            }
+                        }
+                        self.addColumn(field);
+                    }
+                );
+            }
+
+            // given a base class and a dotpath, find the IDL field
+            this.getIDLFieldFromPath = function(idlClass, path) {
+                var class_obj = egIDL.classes[idlClass];
+                var path_parts = path.split(/\./);
+
+                // note: use of for() is intentional 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;
+            }
+
+            /**
+             * Looks for the matching IDL field to extract the label
+             * and datattype as needed.
+             * Creates a local copy of the field for our internal
+             * machinations.
+             */
+            this.absorbField = function(field) {
+
+                // start by cloning the field so we can flesh it out.
+                // note: aungular.copy won't work, because 'field' may
+                // be a $scope object.
+                var new_field = {
+                    name : field.name,
+                    label : field.label,
+                    path : field.path,
+                    flex : Number(field.flex) || 2,
+                    display : (field.display === false) ? false : true
+                };
+
+                // lookup the matching IDL field
+                var idl_field = field.datatype ? field : 
+                    self.getIDLFieldFromPath($scope.idlClass, field.path);
+
+                // No matching IDL field.  Caller has gone commando.
+                // Nothing left to do.
+                if (!idl_field) return new_field;
+
+                new_field.datatype = idl_field.datatype;
+
+                if (field.label) {
+                    // caller-provided label
+                    new_field.label = field.label;
+                } else {
+                    if (idl_field.label) {
+                        new_field.label = idl_field.label;
+                    } else {
+                        new_field.label = new_field.name;
+                    }
+                }
+
+                return new_field;
+            }
+
+            $scope.handleRowClick = function($event, item) {
+                var index = $scope.list.indexValue(item);
+
+                if ($event.ctrlKey || $event.metaKey /* mac command */) {
+                    // control-click
+                    if ($scope.list.toggleOneSelection(index)) 
+                        self.lastSelectedRowIndex = index;
+
+                } else if ($event.shiftKey) { 
+                    // shift-click
+                    if (!self.lastSelectedRowIndex || 
+                            index == self.lastSelectedRowIndex) {
+                        // no source row, just do a simple select
+                        $scope.list.selectOne(index);
+                        self.lastSelectedRowIndex = index;
+                        return;
+                    }
+
+                    var selecting = false;
+                    var ascending = 
+                        $scope.list.comesBefore(self.lastSelectedRowIndex, item);
+                    var startPos = 
+                        $scope.list.indexOf(self.lastSelectedRowIndex);
+
+                    // update to new last-selected
+                    self.lastSelectedRowIndex = index;
+
+                    // select each row between the last selected and 
+                    // currently selected items
+                    while (true) {
+                        startPos += ascending ? 1 : -1;
+                        var curItem = $scope.list.items[startPos];
+                        if (!curItem) break;
+                        var curIdx = $scope.list.indexValue(curItem);
+                        $scope.list.selected[curIdx] = true;
+                        if (curIdx == index) break; // all done
+                    }
+                        
+                } else {
+                    $scope.list.selectOne(index);
+                    self.lastSelectedRowIndex = index;
+                }
+            }
+
+            $scope.itemIsSelected = function(item) {
+                return $scope.list.selected[
+                    $scope.list.indexValue(item)
+                ];
+            }
+
+            this.onColumnDrag = function(col) {
+                // track which column we're dragging
+                self.dragColumn = col;
+            }
+
+            // if the target column does not match the source column,
+            // increase the size of the source column.
+            this.onColumnDragOver = function(target) {
+                if (angular.isUndefined(target)) return;
+                if (target == self.dragColumn) return;
+                if (self.dragColumn == '+index') {
+                    $scope.indexFlex += 1;
+                } else if (self.dragColumn == '+selector') {                             
+                    $scope.selectorFlex += 1;        
+                } else {
+                    var column = $scope.list.findColumn(self.dragColumn);
+                    $scope.modifyColumnFlex(column, 1);
+                }
+                $scope.$apply(); // needed
+            }
+
+            /*
+            $scope.fetchMoreData = function() {
+                console.log('fetchMoreData');
+                self.offset += self.limit;
+                $scope.fetchData();
+            }
+            */
+
+            this.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 service for egGridDataManager instances, which are
+ * responsible for collecting flattened grid data.
+ */
+.factory('egGridData', ['egNet','egAuth',
+
+    function(egNet, egAuth) {
+
+        // per-grid data manager class
+        function EgGridDataManager(args) {
+            var self = this;
+
+            this.idlClass = args.idlClass;
+            this.query = args.query;
+            this.list = args.list;
+            this.offset = args.offset || 0;
+            this.version = 1;
+
+            this.reset = function() {
+                self.list.resetPageData();
+                self.version++;
+            }
+
+            this.revision = function() {
+                return self.version;
+            }
+
+            this.get = function(index, count, success) {
+                index -= 1; // make it zero-based
+
+                if (self.offset) { index += self.offset }
+                
+                if (index < 0) return success([]);
+
+                console.log('index = ' + index + ' : count = ' + count);
+
+                // we may already have the data
+                var slice = self.list.items.slice(index, index + count);
+                if (slice.length && slice[0]) return success(slice);
+
+                var queryFields = {}
+                angular.forEach(self.list.allColumns, function(field) {
+                    if (self.list.displayColumns[field.name])
+                        queryFields[field.name] = field.path || field.name;
+                });
+
+                var respIndex = index;
+                egNet.request(
+                    'open-ils.fielder',
+                    'open-ils.fielder.flattened_search',
+                    egAuth.token(), self.idlClass, queryFields,
+                    self.query,
+                    {   sort : self.sort,
+                        limit : count,
+                        offset : index
+                    }
+                ).then( 
+                    function() { // oncomplete
+                        success(self.list.items.slice(index, index + count)); 
+                    }, 
+                    null, // onerror
+                    function(item) { // onmessage
+                        self.list.items[respIndex++] = item;
+                    }
+                );
+            }
+
+            this.applySort = function(sortBlob) {
+                self.offset = 0;
+
+                if (sortBlob) {
+                    self.sort = sortBlob;
+
+                } else {
+                    // no sort provided, compile the sort blob
+                    // from the column sort priority values
+
+                    var sortList = self.list.allColumns.filter(
+                        function(col) { return Number(col.sortPriority) != 0 }
+                    ).sort( 
+                        function(a, b) { 
+                            if (Math.abs(a.sortPriority) < Math.abs(b.sortPriority))
+                                return -1;
+                            return 1;
+                        }
+                    );
+
+                    self.sort = sortList.map(function(col) {
+                        var blob = {};
+                        blob[col.name] = col.sortPriority < 0 ? 'desc' : 'asc';
+                        return blob;
+                    });
+                }
+
+                self.reset();
+            }
+        }
+
+        return {
+            create : function(args) {
+                return new EgGridDataManager(args);
+            }
+        };
+    }
+])
+
+/** Simplified dnd directives for grid column controls.
+ *  Extract these out if the can be made generic enough
+ */
+
+.directive('egDragSource', function() {
+    return {
+        restrict : 'A',
+        require : '^egGrid',
+        link : function(scope, element, attrs, egGridCtrl) {
+            angular.element(element).attr('draggable', 'true');
+            element.bind('dragstart', function(e) {
+                var col = angular.element(e.target).attr('column');
+                egGridCtrl.onColumnDrag(col);
+            });
+        }
+    };
+})
+
+.directive('egDragDest', function() {
+    return {
+        restrict : 'A',
+        require : '^egGrid',
+        link : function(scope, element, attrs, egGridCtrl) {
+            element.bind('dragover', function(e) {
+                e.stopPropagation();
+                e.preventDefault();
+                //e.dataTransfer.dropEffect = 'copy';
+                var col = angular.element(e.target).attr('column');
+                console.log('dragover ' + col);
+                egGridCtrl.onColumnDragOver(col);
+            });
+        }
+    };
+})
+
+/**
+ * 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;                                                  
+        }                                                                      
+    }                                                                          
+}]);
+
+