Merge "Re-designed and Angularized tables"

This commit is contained in:
Jenkins 2015-02-12 11:41:10 +00:00 committed by Gerrit Code Review
commit 8d49a2eeb2
10 changed files with 694 additions and 9 deletions

View File

@ -1,2 +1,3 @@
@import "help-panel/help-panel";
@import "wizard/wizard";
@import "table/table";

216
horizon/static/angular/table/table.js vendored Normal file
View File

@ -0,0 +1,216 @@
/* jshint globalstrict: true */
(function() {
'use strict';
/**
* @ngdoc overview
* @name hz.widget.table
* @description
*
* # hz.widget.table
*
* The `hz.widget.table` provides support for user interactions and checkbox
* selection in tables.
*
* Requires {@link https://github.com/lorenzofox3/Smart-Table `Smart-Table`}
* module and jQuery (for table drawer slide animation in IE9) to be installed.
*
* | Directives |
* |-------------------------------------------------------------------|
* | {@link hz.widget.table.directive:hzTable `hzTable`} |
* | {@link hz.widget.table.directive:hzSelectAll `hzSelectAll`} |
* | {@link hz.widget.table.directive:hzExpandDetail `hzExpandDetail`} |
*
*/
var app = angular.module('hz.widget.table', [ 'smart-table' ]);
/**
* @ngdoc directive
* @name hz.widget.table.directive:hzTable
* @element table st-table='rowCollection'
* @description
* The `hzTable` directive extends the Smart-Table module to provide
* support for saving the checkbox selection state of each row in the
* table. Also included is the `updateSelectCount` function which
* updates the checkbox selection count of the table.
*
* @restrict A
* @scope true
* @example
*
* ```
* <table st-table='rowCollection' hz-table>
* <thead>
* <tr>
* <th><input type='checkbox' hz-select-all='rowCollection'/></th>
* <th>Name</th>
* </tr>
* </thead>
* <tbody>
* <tr ng-repeat="row in rowCollection">
* <td>
* <input type='checkbox'
* ng-model='selected[row.id].checked'
* ng-change='updateSelectCount(row)'/>
* </td>
* <td>Foo</td>
* </tr>
* </tbody>
* </table>
* ```
*
*/
app.directive('hzTable', function() {
return {
restrict: 'A',
scope: true,
controller: function($scope) {
$scope.selected = {};
$scope.numSelected = 0;
$scope.updateSelectCount = function(row) {
if ($scope.selected.hasOwnProperty(row.id)) {
var checkedState = $scope.selected[row.id].checked;
if (checkedState) {
$scope.numSelected += 1;
} else {
$scope.numSelected -= 1;
}
}
};
this.select = function(row, checkedState) {
var oldCheckedState = $scope.selected.hasOwnProperty(row.id) ?
$scope.selected[row.id].checked :
false;
$scope.selected[row.id] = {
checked: checkedState,
item: row
};
if (checkedState && !oldCheckedState) {
$scope.numSelected += 1;
} else if (!checkedState && oldCheckedState) {
$scope.numSelected -= 1;
}
};
}
};
});
/**
* @ngdoc directive
* @name hz.widget.table.directive:hzSelectAll
* @element input type='checkbox'
* @description
* The `hzSelectAll` directive updates the checkbox selection state of
* every row in the table. Assign this as an attribute to a checkbox
* input element, passing in the row collection data.
*
* @restrict A
* @scope rows: '=hzSelectAll'
* @example
*
* ```
* <input type='checkbox' hz-select-all='rowCollection'/>
* ```
*
*/
app.directive('hzSelectAll', function() {
return {
restrict: 'A',
require: '^hzTable',
scope: {
rows: '=hzSelectAll'
},
link: function(scope, element, attrs, hzTableCtrl) {
element.on('click', function() {
scope.$apply(function() {
var checkedState = element.prop('checked');
angular.forEach(scope.rows, function(row) {
hzTableCtrl.select(row, checkedState);
});
});
});
}
};
});
/**
* @ngdoc directive
* @name hz.widget.table.directive:hzExpandDetail
* @element i class='fa fa-chevron-right'
* @param {number} duration The duration for drawer slide animation
* @description
* The `hzExpandDetail` directive toggles the detailed drawer of the row.
* The animation is implemented using jQuery's slideDown() and slideUp().
* Assign this as an attribute to an icon that should trigger the toggle,
* passing in the two class names of the icon. If no class names are
* specified, the default 'fa-chevron-right fa-chevron-down' is used. A
* duration for the slide animation can be specified as well (default: 400).
* The detail drawer row and cell also needs to be implemented and include
* the classes 'detail-row' and 'detail', respectively.
*
* @restrict A
* @scope icons: '@hzExpandDetail', duration: '@'
* @example
*
* ```
* <tr>
* <td>
* <i class='fa fa-chevron-right'
* hz-expand-detail='fa-chevron-right fa-chevron-down'
* duration='200'></i>
* </td>
* </tr>
* <tr class='detail-row'>
* <td class='detail'></td>
* </tr>
* ```
*
*/
app.directive('hzExpandDetail', function() {
return {
restrict: 'A',
scope: {
icons: '@hzExpandDetail',
duration: '@'
},
link: function(scope, element) {
element.on('click', function() {
var iconClasses = scope.icons || 'fa-chevron-right fa-chevron-down';
element.toggleClass(iconClasses);
var summaryRow = element.closest('tr');
var detailCell = summaryRow.next('tr').find('.detail');
var duration = scope.duration ? parseInt(scope.duration) : 400;
if (summaryRow.hasClass('expanded')) {
var options = {
duration: duration,
complete: function() {
// Hide the row after the slide animation finishes
summaryRow.toggleClass('expanded');
}
};
detailCell.find('.detail-expanded').slideUp(options);
} else {
summaryRow.toggleClass('expanded');
if (detailCell.find('.detail-expanded').length === 0) {
// Slide down animation doesn't work on table cells
// so a <div> wrapper needs to be added
detailCell.wrapInner('<div class="detail-expanded"></div>');
}
detailCell.find('.detail-expanded').slideDown(duration);
}
});
}
};
});
})();

View File

@ -0,0 +1,300 @@
$em-per-priority: floor($table-col-avg-width / $font-size-base) * 3;
.table-rsp {
border-collapse: separate;
border-spacing: 0 $table-gap-height;
width: 100%;
thead tr th, tfoot tr td {
background: none;
border: none;
padding: $table-padding;
}
tbody tr {
&[lr-drag-src] td:not(.expander) {
cursor: move;
}
&.lr-drop-target-before td {
border-top: $reorder-border !important;
}
&.lr-drop-target-after td {
border-bottom: $reorder-border !important;
}
td {
background-color: #ffffff;
border-top: $table-border;
border-bottom: $table-border;
padding: $table-padding;
position: relative;
white-space: nowrap;
z-index: 10;
&:first-child, &.action-col {
border-left: $table-border;
}
&:last-child, &.select-col {
border-right: $table-border;
}
}
}
.select-col {
max-width: $select-col-width;
text-align: center;
width: $select-col-width;
}
.action-col {
position: relative;
text-align: center;
vertical-align: top;
min-width: $batch-action-width;
width: $batch-action-width;
z-index: 20;
.btn {
width: 100%;
}
}
.reorder {
border-right: $table-border;
padding-right: 1.5em;
position: relative;
&:after {
content: '\f07d';
display: inline-block;
font-family: 'FontAwesome';
position: absolute;
right: 0;
text-align: left;
width: 1em;
}
}
.numeric {
text-align: right;
}
[st-sort] {
cursor: pointer;
&:after {
color: #d4d4d4;
content: '\f0dc';
font-family: 'FontAwesome';
margin-left: 0.5em;
opacity: 0;
}
&:not(.st-sort-ascent):hover:after, &:not(.st-sort-descent):hover:after {
opacity: 1;
}
}
.st-sort-ascent:after {
color: #000000;
content: '\f0dd';
font-family: 'FontAwesome';
margin-left: 0.5em;
opacity: 1;
}
.st-sort-descent:after {
color: #000000;
content: '\f0de';
font-family: 'FontAwesome';
margin-left: 0.5em;
opacity: 1;
}
&.modern {
border-spacing: 0;
tbody tr {
td {
border: none;
border-top: $table-border;
}
&:last-child td {
border-bottom: $table-border;
}
}
}
&.table-detail {
border-spacing: 0;
tbody {
tr td {
border-bottom: none;
}
tr:last-child:not(.spacer-row) td {
border-bottom: $table-border;
}
tr.expanded td {
border-bottom: $table-border;
&[rowspan='2'].action-col {
border-bottom: none;
}
}
tr.expanded:nth-last-child(-n+3) [rowspan='2'].action-col {
border-bottom: $table-border;
}
tr:nth-last-child(2):not(.expanded) td {
border-bottom: $table-border;
}
tr:nth-last-child(3).expanded + .detail-row + tr.spacer-row td {
border-top: none;
}
tr + .detail-row + tr.spacer-row td {
border-top: $table-border;
}
}
.detail-row td {
display: none;
padding: 0;
&.detail .detail-expanded {
border-top: none;
display: none;
padding: $detail-row-padding $batch-action-padding $detail-row-padding $table-padding;
white-space: normal;
}
}
.expanded + tr td {
border-top: none;
display: table-cell;
}
.expander {
cursor: pointer;
max-width: $expander-width;
width: $expander-width;
}
.spacer-row td {
background-color: inherit;
border: none;
height: $table-gap-height;
padding: 0;
position: relative;
z-index: 30;
}
&.table-striped {
tbody {
tr {
&:nth-child(2n+1) > td, &:nth-child(2n+1) + .detail-row > td {
background-color: $table-stripe-bgcolor;
}
&.spacer-row > td, &.spacer-row:nth-child(6n+3) + tr + tr.detail-row td,
&.detail-row:nth-child(4n+2) + tr:not(.spacer-row) td,
&.detail-row:nth-child(4n+2) + tr:not(.spacer-row) + tr.detail-row td {
background-color: transparent;
}
}
}
}
&.modern {
.expanded + tr td {
border-top: $table-border;
}
.expanded {
td:not(.action-col), td.action-col:not([rowspan='2']) {
border-bottom: none;
}
}
}
}
@media only all {
.rsp-p1, .rsp-p2,
.rsp-p3, .rsp-p4 {
display: none;
}
.rsp-alt-p1, .rsp-alt-p2,
.rsp-alt-p3, .rsp-alt-p4 {
display: inline-block;
}
}
@media (min-width: 0em) {
$content-width: $body-min-width - $sidebar-width - (2 * $content-body-padding);
$max-priority: floor($content-width / $font-size-base / $em-per-priority);
@for $i from 1 through $max-priority {
.rsp-p#{$i} {
display: table-cell;
}
.rsp-alt-p#{$i} {
display: none;
}
}
}
$p1-width: 5em + $em-per-priority * 1em;
@media (min-width: $p1-width) {
.rsp-p1 {
display: table-cell;
}
.rsp-alt-p1 {
display: none;
}
}
$p2-width: 5em + $em-per-priority * 2em;
@media (min-width: $p2-width) {
.rsp-p2 {
display: table-cell;
}
.rsp-alt-p2 {
display: none;
}
}
$p3-width: 5em + $em-per-priority * 3em;
@media (min-width: $p3-width) {
.rsp-p3 {
display: table-cell;
}
.rsp-alt-p3 {
display: none;
}
}
$p4-width: 5em + $em-per-priority * 4em;
@media (min-width: $p4-width) {
.rsp-p4 {
display: table-cell;
}
.rsp-alt-p4 {
display: none;
}
}
}

View File

@ -0,0 +1,144 @@
/* jshint browser: true, globalstrict: true */
'use strict';
describe('hz.widget.table module', function() {
it('should have been defined".', function () {
expect(angular.module('hz.widget.table')).toBeDefined();
});
});
describe('table directives', function () {
var $scope, $element;
beforeEach(module('smart-table'));
beforeEach(module('hz'));
beforeEach(module('hz.widgets'));
beforeEach(module('hz.widget.table'));
beforeEach(inject(function($injector) {
var $compile = $injector.get('$compile');
$scope = $injector.get('$rootScope').$new();
$scope.fakeData = [
{ id: '1', animal: 'cat' },
{ id: '2', animal: 'dog' },
{ id: '3', animal: 'fish' }
];
var markup =
'<table st-table="fakeData" hz-table>' +
'<thead><tr>' +
'<th><input type="checkbox" hz-select-all="fakeData"' +
' ng-checked="numSelected === fakeData.length"/></th>' +
'<th></th>' +
'<th>Animal</th>' +
'</tr></thead>' +
'<tbody>' +
'<tr ng-repeat-start="row in fakeData">' +
'<td><input type="checkbox" ng-model="selected[row.id].checked"' +
' ng-change="updateSelectCount(row)"/></td>' +
'<td><i class="fa fa-chevron-right" hz-expand-detail></i></td>' +
'<td>{{ row.animal }}</td>' +
'</tr>' +
'<tr class="detail-row" ng-repeat-end>' +
'<td class="detail" colspan="3"></td>' +
'</tr>' +
'</tbody>' +
'</table>';
$element = angular.element(markup);
$compile($element)($scope);
$scope.$digest();
}));
describe('hzTable directive', function() {
it('should have 3 summary rows', function() {
expect($element.find('tbody tr[ng-repeat-start]').length).toBe(3);
});
it('should have 3 detail rows', function() {
expect($element.find('tbody tr.detail-row').length).toBe(3);
});
it('should have each checkbox initially unchecked', function() {
var checkboxes = $element.find('tbody tr[ng-repeat-start] input[type="checkbox"]');
angular.forEach(checkboxes, function(checkbox) {
expect(checkbox.checked).toBe(false);
});
});
it('should have numSelected === 1 when first checkbox is clicked', function() {
var firstInput = $element.find('tbody input[type="checkbox"]').first();
var ngModelCtrl = firstInput.controller('ngModel');
ngModelCtrl.$setViewValue(true);
$scope.$digest();
expect($element.scope().numSelected).toBe(1);
});
it('should have numSelected === 3 and select-all checkbox checked when all rows selected', function() {
var checkboxes = $element.find('tbody input[type="checkbox"]');
angular.forEach(checkboxes, function(checkbox) {
checkbox.checked = true;
var ngModelCtrl = angular.element(checkbox).controller('ngModel');
ngModelCtrl.$setViewValue(true);
});
$scope.$digest();
expect($element.scope().numSelected).toBe(3);
expect($element.find('thead input[hz-select-all]')[0].checked).toBe(true);
});
});
describe('hzSelectAll directive', function() {
it('should select all checkboxes if select-all checkbox checked', function() {
var selectAllCb = $element.find('input[hz-select-all]').first();
selectAllCb[0].checked = true;
selectAllCb.triggerHandler('click');
$scope.$digest();
expect($element.scope().numSelected).toBe(3);
var checkboxes = $element.find('tbody input[type="checkbox"]');
angular.forEach(checkboxes, function(checkbox) {
expect(checkbox.checked).toBe(true);
});
});
});
describe('hzExpandDetail directive', function() {
it('should have summary row with class "expanded" when expanded', function() {
var expandIcon = $element.find('i.fa').first();
expandIcon.click();
var summaryRow = expandIcon.closest('tr');
expect(summaryRow.hasClass('expanded')).toBe(true);
});
it('should have summary row without class "expanded" when not expanded', function(done) {
var expandIcon = $element.find('i.fa').first();
// Click twice to mock expand and collapse
expandIcon.click();
expandIcon.click();
// Wait for the slide down animation to complete before test
setTimeout(function() {
var summaryRow = expandIcon.closest('tr');
expect(summaryRow.hasClass('expanded')).toBe(false);
done();
}, 2000);
});
});
});

View File

@ -3,7 +3,8 @@
angular.module('hz.widgets', [
'hz.widget.help-panel',
'hz.widget.wizard'
'hz.widget.wizard',
'hz.widget.table'
])
.constant('basePath', '/static/angular/');

View File

@ -14,6 +14,7 @@
<script src='{{ STATIC_URL }}horizon/js/angular/controllers/namespace-controller.js'></script>
<script src='{{ STATIC_URL }}horizon/js/angular/controllers/dummy.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/angular/directives/forms.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/angular/horizon.conf.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/angular/services/horizon.utils.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/angular/controllers/metadata-widget-controller.js'></script>
@ -24,6 +25,7 @@
<script src='{{ STATIC_URL }}angular/widget.module.js'></script>
<script src='{{ STATIC_URL }}angular/help-panel/help-panel.js'></script>
<script src='{{ STATIC_URL }}angular/wizard/wizard.js'></script>
<script src='{{ STATIC_URL }}angular/table/table.js'></script>
<script src='{{ STATIC_URL }}horizon/lib/jquery/jquery.quicksearch.js' type='text/javascript' charset="utf-8"></script>
<script src="{{ STATIC_URL }}horizon/lib/jquery/jquery.tablesorter.js" type="text/javascript" charset="utf-8"></script>

View File

@ -10,10 +10,13 @@
<script type="text/javascript" src="{{ STATIC_URL }}horizon/lib/jasmine/boot.js"></script>
<script type='text/javascript' src='{{ STATIC_URL }}horizon/lib/jquery/jquery.js'></script>
<script type="text/javascript" src="{{ STATIC_URL }}horizon/lib/angular/angular.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}horizon/lib/angular/angular-mocks.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}horizon/lib/angular/angular-cookies.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}horizon/lib/angular/angular-bootstrap.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}horizon/lib/smart-table/smart-table.js"></script>
<script type='text/javascript' charset='utf-8'>
/* Load angular modules extensions list before we include angular/horizon.js */

View File

@ -28,6 +28,7 @@ class ServicesTests(test.JasmineTests):
'angular/widget.module.js',
'angular/help-panel/help-panel.js',
'angular/wizard/wizard.js',
'angular/table/table.js',
]
specs = [
'horizon/tests/jasmine/utilsSpec.js',
@ -35,6 +36,7 @@ class ServicesTests(test.JasmineTests):
'horizon/js/angular/services/hz.api.service.spec.js',
'angular/help-panel/help-panel.spec.js',
'angular/wizard/wizard.spec.js',
'angular/table/table.spec.js',
]
externalTemplates = [
'angular/help-panel/help-panel.html',

View File

@ -13,10 +13,12 @@ $gray-light: #cccccc;
/* Horizon Custom Variables */
$sidebar-background-color: #f9f9f9;
$sidebar-width: 220px;
$border-color: #dddddd;
$table-bg-odd: #f9f9f9;
$body-min-width: 900px !default;
$sidebar-background-color: #f9f9f9 !default;
$sidebar-width: 220px !default;
$border-color: #dddddd !default;
$table-bg-odd: #f9f9f9 !default;
$content-body-padding: 15px !default;
/* Workflows */
@ -69,7 +71,7 @@ $overview_chart_height: 81px;
/* Accordion Navigation */
$accordionBorderColor: #e5e5e5;
$accordionBorderColor: #e5e5e5 !default;
/* Help panel */
@ -109,3 +111,17 @@ $WizardSidePadding: 35px !default;
$WizardStatusIndicatorSize: 24px !default;
$WizardBtnGap: 5px !default;
$WizardToolbarBtnHeight: 28px !default;
/* Responsive Table */
$batch-action-width: 10em !default;
$batch-action-padding: $batch-action-width + 1em !default;
$detail-row-padding: 1em !default;
$expander-width: 1.5em !default;
$reorder-border: 2px solid #1f83c6 !default;
$select-col-width: 2.5em !default;
$table-col-avg-width: 150px !default;
$table-border-color: #cccccc !default;
$table-border: 1px solid $table-border-color !default;
$table-gap-height: 0.5em !default;
$table-padding: 0.5em !default;
$table-stripe-bgcolor: #f9f9f9 !default;

View File

@ -60,7 +60,7 @@ body {
background-position: -200px top;
}
color: $text-color;
min-width: 900px;
min-width: $body-min-width;
}
small {
@ -1019,8 +1019,8 @@ tr.terminated {
}
#content_body {
padding-left: $sidebar-width + 15px;
padding-right: 15px;
padding-left: $sidebar-width + $content-body-padding;
padding-right: $content-body-padding;
}
.tab_wrapper {