From 9fd0184131493da23a4d4656478c116a9667bcc2 Mon Sep 17 00:00:00 2001 From: Joe Fiorini Date: Tue, 24 Mar 2015 17:06:55 -0400 Subject: [PATCH] Implement multi-select-list module --- .../list-generator/list-generator.factory.js | 174 ++++++++++++------ .../static/js/shared/list-generator/main.js | 3 +- .../js/shared/multi-select-list/main.js | 9 + .../multi-select-list.controller.js | 85 +++++++++ .../multi-select-list.directive.js | 11 ++ .../multi-select-list/select-all.directive.js | 62 +++++++ .../multi-select-list/select-all.partial.html | 18 ++ .../select-list-item.directive.js | 33 ++++ awx/ui/static/less/lists.less | 19 ++ 9 files changed, 361 insertions(+), 53 deletions(-) create mode 100644 awx/ui/static/js/shared/multi-select-list/main.js create mode 100644 awx/ui/static/js/shared/multi-select-list/multi-select-list.controller.js create mode 100644 awx/ui/static/js/shared/multi-select-list/multi-select-list.directive.js create mode 100644 awx/ui/static/js/shared/multi-select-list/select-all.directive.js create mode 100644 awx/ui/static/js/shared/multi-select-list/select-all.partial.html create mode 100644 awx/ui/static/js/shared/multi-select-list/select-list-item.directive.js diff --git a/awx/ui/static/js/shared/list-generator/list-generator.factory.js b/awx/ui/static/js/shared/list-generator/list-generator.factory.js index d0a5514d24..da225ecbef 100644 --- a/awx/ui/static/js/shared/list-generator/list-generator.factory.js +++ b/awx/ui/static/js/shared/list-generator/list-generator.factory.js @@ -40,8 +40,8 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate // hdr: // // Inject into a custom element using options.id: - // Control breadcrumb creation with options.breadCrumbs: // + // Control breadcrumb creation with options.breadCrumbs: var element; if (options.id) { @@ -115,6 +115,31 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate } }; + if (list.multiSelect) { + var cleanupSelectionChanged = + this.scope.$on('multiSelectList.selectionChanged', function(e, selection) { + this.scope.selectedItems = selection.selectedItems; + + // Track the selected state of each item + // for changing the row class based on the + // selection + // + selection.selectedItems.forEach(function(item) { + item.isSelected = true; + }); + + selection.deselectedItems.forEach(function(item) { + item.isSelected = false; + }); + + }.bind(this)); + + this.scope.$on('$destroy', function() { + cleanupSelectionChanged(); + }); + + } + $compile(element)(this.scope); // Reset the scope to prevent displaying old data from our last visit to this list @@ -294,34 +319,62 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate html += "
') + .attr('id', list.name + '_table') + .addClass('table table-condensed') + .addClass(extraClasses) + .attr('multi-select-list', multiSelect); + + } + + var table = buildTable(); + var innerTable = ''; if (!options.skipTableHead) { - html += this.buildHeader(options); + innerTable += this.buildHeader(options); } // table body - html += "\n"; - html += "\n"; - if (list.index) { - html += "{{ $index + ((" + list.iterator + "_page - 1) * " + list.iterator + "_page_size) + 1 }}.\n"; + innerTable += "\n"; + innerTable += "\n"; + + if (list.index) { + innerTable += "{{ $index + ((" + list.iterator + "_page - 1) * " + list.iterator + "_page_size) + 1 }}.\n"; + } + + if (list.multiSelect) { + innerTable += ''; + } + cnt = 2; base = (list.base) ? list.base : list.name; base = base.replace(/^\//, ''); @@ -329,7 +382,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate cnt++; if ((list.fields[fld].searchOnly === undefined || list.fields[fld].searchOnly === false) && !(options.mode === 'lookup' && list.fields[fld].excludeModal === true)) { - html += Column({ + innerTable += Column({ list: list, fld: fld, options: options, @@ -340,12 +393,12 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate if (options.mode === 'select' || options.mode === 'lookup') { if(options.input_type==="radio"){ //added by JT so that lookup forms can be either radio inputs or check box inputs - html += ""; } else { // its assumed that options.input_type = checkbox - html += ""; } @@ -353,12 +406,12 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate // Row level actions - html += ""; + innerTable += ""; for (field_action in list.fieldActions) { if (field_action !== 'columnClass') { if (list.fieldActions[field_action].type && list.fieldActions[field_action].type === 'DropDown') { - html += DropDown({ + innerTable += DropDown({ list: list, fld: field_action, options: options, @@ -368,53 +421,56 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate }); } else { fAction = list.fieldActions[field_action]; - html += ""; + innerTable += ""; } else { - html += SelectIcon({ + innerTable += SelectIcon({ action: field_action }); } //html += (fAction.label) ? " " + list.fieldActions[field_action].label + // "" : ""; - html += ""; + innerTable += ""; } } } - html += "\n"; + innerTable += "\n"; } - html += "\n"; + innerTable += "\n"; // Message for when a collection is empty - html += "\n"; - html += "
No records matched your search.
\n"; - html += "\n"; + innerTable += "\n"; + innerTable += "
No records matched your search.
\n"; + innerTable += "\n"; // Message for loading - html += "\n"; - html += "
Loading...
\n"; - html += "\n"; + innerTable += "\n"; + innerTable += "
Loading...
\n"; + innerTable += "\n"; // End List - html += "\n"; - html += "\n"; + innerTable += "\n"; + + table.html(innerTable); + html += table.prop('outerHTML'); + html += "
\n"; if (options.mode === 'select' && (options.selectButton === undefined || options.selectButton)) { @@ -447,6 +503,15 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate var list = this.list, fld, html; + function buildSelectAll() { + return $('') + .addClass('col-xs-1 select-column') + .append( + $('') + .attr('selections-empty', 'selectedItems.length === 0') + .attr('items-length', list.name + '.length')); + } + if (options === undefined) { options = this.options; } @@ -456,6 +521,11 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate if (list.index) { html += "#\n"; } + + if (list.multiSelect) { + html += buildSelectAll().prop('outerHTML'); + } + for (fld in list.fields) { if ((list.fields[fld].searchOnly === undefined || list.fields[fld].searchOnly === false) && !(options.mode === 'lookup' && list.fields[fld].excludeModal === true)) { diff --git a/awx/ui/static/js/shared/list-generator/main.js b/awx/ui/static/js/shared/list-generator/main.js index 1af299d520..85b21247eb 100644 --- a/awx/ui/static/js/shared/list-generator/main.js +++ b/awx/ui/static/js/shared/list-generator/main.js @@ -1,8 +1,9 @@ import generateList from './list-generator.factory'; import toolbarButton from './toolbar-button.directive'; import generatorHelpers from 'tower/shared/generator-helpers'; +import multiSelectList from 'tower/shared/multi-select-list/main'; export default - angular.module('listGenerator', [generatorHelpers.name]) + angular.module('listGenerator', [generatorHelpers.name, multiSelectList.name]) .factory('generateList', generateList) .directive('toolbarButton', toolbarButton); diff --git a/awx/ui/static/js/shared/multi-select-list/main.js b/awx/ui/static/js/shared/multi-select-list/main.js new file mode 100644 index 0000000000..41dbe0d572 --- /dev/null +++ b/awx/ui/static/js/shared/multi-select-list/main.js @@ -0,0 +1,9 @@ +import multiSelect from './multi-select-list.directive'; +import selectAll from './select-all.directive'; +import selectListItem from './select-list-item.directive'; + +export default + angular.module('multiSelectList', []) + .directive('multiSelectList', multiSelect) + .directive('selectAll', selectAll) + .directive('selectListItem', selectListItem); diff --git a/awx/ui/static/js/shared/multi-select-list/multi-select-list.controller.js b/awx/ui/static/js/shared/multi-select-list/multi-select-list.controller.js new file mode 100644 index 0000000000..f7a8c52849 --- /dev/null +++ b/awx/ui/static/js/shared/multi-select-list/multi-select-list.controller.js @@ -0,0 +1,85 @@ +export default ['$scope', + function ($scope) { + $scope.items = []; + $scope.selection = { + isExtended: false, + selectedItems: [], + deselectedItems: [] + }; + + Object.defineProperty($scope.selection, + 'length', + { get: function() { + return this.items.length; + } + }); + + function rebuildSelections() { + var _items = _($scope.items).chain(); + + $scope.selection.selectedItems = + _items.filter(function(item) { + return item.isSelected; + }).pluck('value').value(); + + $scope.selection.deselectedItems = + _items.pluck('value').difference($scope.selection.selectedItems) + .value(); + + $scope.$emit('multiSelectList.selectionChanged', $scope.selection); + } + + this.registerItem = function(item) { + var decoratedItem = this.decorateItem(item); + $scope.items = $scope.items.concat(decoratedItem); + return decoratedItem; + }; + + this.deregisterItem = function(leavingItem) { + $scope.items = $scope.items.filter(function(item) { + return leavingItem !== item; + }); + rebuildSelections(); + }; + + this.decorateItem = function(item) { + return { + isSelected: false, + value: item + }; + }; + + this.selectAll = function() { + $scope.items.forEach(function(item) { + item.isSelected = true; + }); + }; + + this.deselectAll = function() { + $scope.items.forEach(function(item) { + item.isSelected = false; + }); + $scope.selection.isExtended = false; + rebuildSelections(); + }; + + + this.deselectAllExtended = function(extendedLength) { + $scope.selection.isExtended = false; + }; + + this.selectAllExtended = function(extendedLength) { + $scope.selection.isExtended = true; + }; + + this.selectItem = function(item) { + item.isSelected = true; + rebuildSelections(); + }; + + this.deselectItem = function(item) { + item.isSelected = false; + rebuildSelections(); + }; + + }]; diff --git a/awx/ui/static/js/shared/multi-select-list/multi-select-list.directive.js b/awx/ui/static/js/shared/multi-select-list/multi-select-list.directive.js new file mode 100644 index 0000000000..0f80b99433 --- /dev/null +++ b/awx/ui/static/js/shared/multi-select-list/multi-select-list.directive.js @@ -0,0 +1,11 @@ +import controller from './multi-select-list.controller'; + +export default + [ function() { + return { + restrict: 'A', + scope: { + }, + controller: controller + }; + }]; diff --git a/awx/ui/static/js/shared/multi-select-list/select-all.directive.js b/awx/ui/static/js/shared/multi-select-list/select-all.directive.js new file mode 100644 index 0000000000..2c7fa8fc1b --- /dev/null +++ b/awx/ui/static/js/shared/multi-select-list/select-all.directive.js @@ -0,0 +1,62 @@ +// TODO: Extract to its own helper +// Example: +// template('shared/multi-select-list/select-all') +// // => +// '/static/js/shared/multi-select-list/select-all.html +// +function template(base) { + return '/static/js/' + base + '.partial.html'; +} + +export default + [ function() { + return { + require: '^multiSelectList', + restrict: 'E', + scope: { + label: '@', + itemsLength: '=', + extendedItemsLength: '=', + isSelectionExtended: '=', + isSelectionEmpty: '=selectionsEmpty' + }, + templateUrl: template('shared/multi-select-list/select-all'), + link: function(scope, element, attrs, controller) { + + scope.label = scope.label || 'All'; + scope.selectExtendedLabel = scope.extendedLabel || 'Select all ' + scope.extendedItemsLength + ' items'; + scope.deselectExtendedLabel = scope.deselectExtendedLabel || 'Deselect extra items'; + + scope.doSelectAll = function(e) { + if (scope.isSelected) { + controller.selectAll(); + + if (scope.supportsExtendedItems) { + scope.showExtendedMessage = scope.itemsLength !== scope.extendedItemsLength; + } + } else { + controller.deselectAll(); + } + }; + + scope.$watch('extendedItemsLength', function(value) { + scope.supportsExtendedItems = _.isNumber(value); + }); + + scope.$watch('isSelectionEmpty', function(value) { + if (value) { + scope.isSelected = false; + } + }); + + scope.selectAllExtended = function() { + controller.selectAllExtended(scope.extendedItemsLength); + }; + + scope.deselectAllExtended = function() { + controller.deselectAllExtended(scope.extendedItemsLength); + }; + + } + }; + }]; diff --git a/awx/ui/static/js/shared/multi-select-list/select-all.partial.html b/awx/ui/static/js/shared/multi-select-list/select-all.partial.html new file mode 100644 index 0000000000..9b5e62a3f9 --- /dev/null +++ b/awx/ui/static/js/shared/multi-select-list/select-all.partial.html @@ -0,0 +1,18 @@ + + + diff --git a/awx/ui/static/js/shared/multi-select-list/select-list-item.directive.js b/awx/ui/static/js/shared/multi-select-list/select-list-item.directive.js new file mode 100644 index 0000000000..ecba972cc9 --- /dev/null +++ b/awx/ui/static/js/shared/multi-select-list/select-list-item.directive.js @@ -0,0 +1,33 @@ +export default + [ function() { + return { + restrict: 'E', + scope: { + item: '=item' + }, + require: '^multiSelectList', + template: '', + link: function(scope, element, attrs, multiSelectList) { + + scope.isSelected = false; + scope.decoratedItem = multiSelectList.registerItem(scope.item); + + scope.$watch('isSelected', function(value) { + if (value === true) { + multiSelectList.selectItem(scope.decoratedItem); + } else if (value === false) { + multiSelectList.deselectItem(scope.decoratedItem); + } + }); + + scope.$watch('decoratedItem.isSelected', function(value) { + scope.isSelected = value; + }); + + scope.$on('$destroy', function() { + multiSelectList.deregisterItem(scope.decoratedItem); + }); + + } + }; + }]; diff --git a/awx/ui/static/less/lists.less b/awx/ui/static/less/lists.less index afa2514006..cfd0615ba8 100644 --- a/awx/ui/static/less/lists.less +++ b/awx/ui/static/less/lists.less @@ -14,3 +14,22 @@ font-weight: normal; } } + +.is-selected-row, .is-selected-row td { + background-color: #E4F1FF !important; +} + +.select-column { + text-align: center; +} + +th.select-column { + label { + // overrides the default style of + // display: inline-block, which allowed + // margins to push down the label, + // thus breaking the vertical-alignment + // of the cell + display: inline; + } +}