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">✓</span>
- <span ng-if="!list.displayColumns[col.name]"
+ <span ng-if="!grid.columnsProvider.visible[col.name]"
class="label label-warning">✗</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>
+++ /dev/null
-
-<!--
- Actions row.
- This sits above the grid and contains the column picker, etc.
--->
-
-<div class="eg-grid-row eg-grid-action-row">
-
- <div style="flex:1">
- <div class="eg-grid-primary-label">{{grid.mainLabel}}</div>
- </div>
-
- <!-- column picker -->
- <div class="btn-group column-picker">
-
- <!-- first page -->
- <button type="button" class="btn btn-default"
- ng-class="{disabled : grid.onFirstPage()}"
- ng-click="grid.offset = 0;grid.collect()"
- title="[% l('Start') %]">
- <span class="glyphicon glyphicon-fast-backward"></span>
- </button>
-
- <!-- previous page -->
- <button type="button" class="btn btn-default"
- ng-class="{disabled : grid.onFirstPage()}"
- ng-click="grid.decrementPage()"
- title="[% l('Previous Page') %]">
- <span class="glyphicon glyphicon-backward"></span>
- </button>
-
- <!-- next page -->
- <!-- todo: paging needs a total count value to be fully functional -->
- <button type="button" class="btn btn-default"
- ng-class="{disabled : !grid.hasNextPage()}"
- ng-click="grid.incrementPage()"
- title="[% l('Next Page') %]">
- <span class="glyphicon glyphicon-forward"></span>
- </button>
-
- <div class="btn-group">
- <button type="button" title="[% ('Select Row Count') %]"
- class="btn btn-default dropdown-toggle" data-toggle="dropdown">
- [% l('Rows [_1]', '{{grid.limit}}') %]
- <span class="caret"></span>
- </button>
- <ul class="dropdown-menu">
- <li ng-repeat="t in [5,10,25,50,100]">
- <a href='' ng-click='grid.offset=0;grid.limit=t;grid.collect()'>
- {{t}}
- </a>
- </li>
- </ul>
- </div>
-
- <div class="btn-group">
- <button type="button" title="[% ('Select Page') %]"
- class="btn btn-default dropdown-toggle" data-toggle="dropdown">
- [% l('Page [_1]', '{{grid.page()}}') %]
- <span class="caret"></span>
- </button>
- <ul class="dropdown-menu">
- <li>
- <div class="input-group">
- <input type="text" class="form-control"
- ng-model="pageFromUI"
- ng-click="$event.stopPropagation()"/>
- <span class="input-group-btn">
- <button class="btn btn-default" type="button"
- ng-click="grid.goToPage(pageFromUI);pageFromUI=''">
- [% l('Go To...') %]
- </button>
- </span>
- </div>
- </li>
- <li role="presentation" class="divider"></li>
- <li ng-repeat="t in [1,2,3,4,5,10,25,50,100]">
- <a href='' ng-click='grid.goToPage(t)'>{{t}}</a>
- </li>
- </ul>
- </div>
-
- <button type="button"
- class="btn btn-default dropdown-toggle"
- data-toggle="dropdown"><span class="caret"></span>
- </button>
- <ul class="dropdown-menu pull-right">
- <li><a href='' ng-click="grid.toggleConfDisplay()">
- <span class="glyphicon glyphicon-wrench"></span>
- [% l('Configure Columns') %]
- </a></li>
- <li><a href='' ng-click="grid.columnsProvider.showAllColumns()">
- <span class="glyphicon glyphicon-resize-full"></span>
- [% l('Show All Columns') %]
- </a></li>
- <li><a href='' ng-click="grid.columnsProvider.hideAllColumns()">
- <span class="glyphicon glyphicon-resize-small"></span>
- [% l('Hide All Columns') %]
- </a></li>
- <li role="presentation" class="divider"></li>
- <li ng-repeat="col in grid.columnsProvider.columns">
- <a href='' ng-click="grid.columnsProvider.visible[col.name] =
- !grid.columnsProvider.visible[col.name]">
- <span ng-if="grid.columnsProvider.visible[col.name]"
- class="label label-success">✓</span>
- <span ng-if="!grid.columnsProvider.visible[col.name]"
- class="label label-warning">✗</span>
- <span>{{col.label}}</span>
- </a>
- </li>
- </ul>
- </div>
-</div>
-
-<!-- Grid -->
-<div class="eg-grid" ng-class="{'eg-grid-as-conf' : grid.showGridConf}">
-
- <!-- import our eg-grid-field defs -->
- <div ng-transclude></div>
-
- <div class="eg-grid-row eg-grid-header-row">
- <div class="eg-grid-cell eg-grid-cell-stock" style="flex:{{grid.indexFlex}}">
- <div>[% l('#') %]</div>
- </div>
- <div class="eg-grid-cell eg-grid-cell-stock" style="flex:{{grid.selectorFlex}}">
- <div>
- <input type='checkbox' ng-click="grid.toggleSelectAllItems()"/>
- </div>
- </div>
- <div class="eg-grid-cell"
- eg-grid-column-drag-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>
-
--- /dev/null
+
+<!--
+ Actions row.
+ This sits above the grid and contains the column picker, etc.
+-->
+<div class="eg-grid-row eg-grid-action-row">
+ <div style="flex:1">
+ <div class="eg-grid-primary-label">{{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">✓</span>
+ <span ng-if="!list.displayColumns[col.name]"
+ class="label label-warning">✗</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>
+
[% 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 %]
-
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 : '@',
// 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
// 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
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();
+ }]
};
})
};
})
-/**
- * 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
}
}]);
-
+++ /dev/null
-angular.module('egGridMod',
- ['egCoreMod', 'egUiMod', 'ui.bootstrap'])
-
-.directive('egGrid', function() {
- return {
- restrict : 'AE',
- transclude : true,
- scope : {
-
- // IDL class hint (e.g. "aou")
- idlClass : '@',
-
- // points to a structure in the calling scope which defines
- // a PCRUD-compliant query.
- query : '=',
-
- // if true, grid columns are derived from all non-virtual
- // fields on the base idlClass
- autoFields : '@',
-
- // grid preferences will be stored / retrieved with this key
- persistKey : '@',
-
- // field whose value is unique and may be used for item
- // reference / lookup. This will usually be someting like
- // "id". This is not needed when using autoFields, since we
- // can determine the primary key directly from the IDL.
- idField : '@',
-
- // egList containting our tabular data is provided for us
- // and managed externally.
- dataProvider : '=',
-
- // if true, hide the sortPriority options in the
- // grid configuration UI. This is primarily used by
- // UIs where the data is ephemeral and can only be
- // single-display-column sorted.
- disableSortPriority : '@',
-
- // optional primary grid label
- mainLabel : '@'
- },
-
- // TODO: avoid hard-coded url
- templateUrl : '/eg/staff/parts/t_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;
- }
- }
-}]);
-
--- /dev/null
+
+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;
+ }
+ }
+}]);
+
+