Implement multi-select-list module

This commit is contained in:
Joe Fiorini 2015-03-24 17:06:55 -04:00
parent 4129282401
commit 9fd0184131
9 changed files with 361 additions and 53 deletions

View File

@ -40,8 +40,8 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
// hdr: <lookup dialog header>
//
// Inject into a custom element using options.id: <element id attribute value>
// Control breadcrumb creation with options.breadCrumbs: <true | false>
//
// Control breadcrumb creation with options.breadCrumbs: <true | false>
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 += "<div class=\"list-table-container\"";
html += (list.awCustomScroll) ? " aw-custom-scroll " : "";
html += ">\n";
html += "<table id=\"" + list.name + "_table\" ";
html += "class=\"table table-condensed";
html += (list['class']) ? " " + list['class'] : "";
html += (options.mode !== 'summary' && options.mode !== 'edit' && (options.mode === 'lookup' || options.id)) ?
' table-hover-inverse' : '';
html += (list.hover) ? ' table-hover' : '';
html += (options.mode === 'summary') ? ' table-summary' : '';
html += "\" ";
html += ">\n";
function buildTable() {
var extraClasses = list['class'];
var multiSelect = list.multiSelect ? 'multi-select-list' : null;
if (options.mode !== 'summary' && options.mode !== 'edit' && (options.mode === 'lookup' || options.id)) {
extraClasses += ' table-hover-inverse';
}
if (list.hover) {
extraClasses += ' table-hover';
}
if (options.mode === 'summary') {
extraClasses += ' table-summary';
}
return $('<table>')
.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 += "<tbody>\n";
html += "<tr ng-class=\"" + list.iterator;
html += (options.mode === 'lookup' || options.mode === 'select') ? ".success_class" : ".active_class";
html += "\" ";
html += "id=\"{{ " + list.iterator + ".id }}\" ";
html += "class=\"" + list.iterator + "_class\" ";
html += "ng-repeat=\"" + list.iterator + " in " + list.name;
html += (list.orderBy) ? " | orderBy:'" + list.orderBy + "'" : "";
html += (list.filterBy) ? " | filter: " + list.filterBy : "";
html += "\">\n";
if (list.index) {
html += "<td class=\"index-column hidden-xs\">{{ $index + ((" + list.iterator + "_page - 1) * " + list.iterator + "_page_size) + 1 }}.</td>\n";
innerTable += "<tbody>\n";
innerTable += "<tr ng-class=\"[" + list.iterator;
innerTable += (options.mode === 'lookup' || options.mode === 'select') ? ".success_class" : ".active_class";
if (list.multiSelect) {
innerTable += ", " + list.iterator + ".isSelected ? 'is-selected-row' : ''";
}
innerTable += "]\" ";
innerTable += "id=\"{{ " + list.iterator + ".id }}\" ";
innerTable += "class=\"" + list.iterator + "_class\" ";
innerTable += "ng-repeat=\"" + list.iterator + " in " + list.name;
innerTable += (list.orderBy) ? " | orderBy:'" + list.orderBy + "'" : "";
innerTable += (list.filterBy) ? " | filter: " + list.filterBy : "";
innerTable += "\">\n";
if (list.index) {
innerTable += "<td class=\"index-column hidden-xs\">{{ $index + ((" + list.iterator + "_page - 1) * " + list.iterator + "_page_size) + 1 }}.</td>\n";
}
if (list.multiSelect) {
innerTable += '<td class="col-xs-1 select-column"><select-list-item item=\"' + list.iterator + '\"></select-list-item></td>';
}
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 += "<td><input type=\"radio\" ng-model=\"" + list.iterator + ".checked\" name=\"check_{{" +
innerTable += "<td><input type=\"radio\" ng-model=\"" + list.iterator + ".checked\" name=\"check_{{" +
list.iterator + ".id }}\" ng-click=\"toggle_" + list.iterator + "(" + list.iterator + ".id, true)\" ng-value=\"1\" " +
"ng-false-value=\"0\" id=\"check_{{" + list.iterator + ".id}}\" /></td>";
}
else { // its assumed that options.input_type = checkbox
html += "<td><input type=\"checkbox\" ng-model=\"" + list.iterator + ".checked\" name=\"check_{{" +
innerTable += "<td><input type=\"checkbox\" ng-model=\"" + list.iterator + ".checked\" name=\"check_{{" +
list.iterator + ".id }}\" ng-click=\"toggle_" + list.iterator + "(" + list.iterator + ".id, true)\" ng-true-value=\"1\" " +
"ng-false-value=\"0\" id=\"check_{{" + list.iterator + ".id}}\" /></td>";
}
@ -353,12 +406,12 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
// Row level actions
html += "<td class=\"actions\">";
innerTable += "<td class=\"actions\">";
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 += "<a id=\"";
html += (fAction.id) ? fAction.id : field_action + "-action";
html += "\" ";
html += (fAction.href) ? "href=\"" + fAction.href + "\" " : "";
html += (fAction.ngHref) ? "ng-href=\"" + fAction.ngHref + "\" " : "";
html += (field_action === 'cancel') ? "class=\"cancel red-txt\" " : "";
html += (fAction.awPopOver) ? "aw-pop-over=\"" + fAction.awPopOver + "\" " : "";
html += (fAction.dataPlacement) ? Attr(fAction, 'dataPlacement') : "";
html += (fAction.dataTitle) ? Attr(fAction, 'dataTitle') : "";
innerTable += "<a id=\"";
innerTable += (fAction.id) ? fAction.id : field_action + "-action";
innerTable += "\" ";
innerTable += (fAction.href) ? "href=\"" + fAction.href + "\" " : "";
innerTable += (fAction.ngHref) ? "ng-href=\"" + fAction.ngHref + "\" " : "";
innerTable += (field_action === 'cancel') ? "class=\"cancel red-txt\" " : "";
innerTable += (fAction.awPopOver) ? "aw-pop-over=\"" + fAction.awPopOver + "\" " : "";
innerTable += (fAction.dataPlacement) ? Attr(fAction, 'dataPlacement') : "";
innerTable += (fAction.dataTitle) ? Attr(fAction, 'dataTitle') : "";
for (itm in fAction) {
if (itm !== 'ngHref' && itm !== 'href' && itm !== 'label' && itm !== 'icon' && itm !== 'class' &&
itm !== 'iconClass' && itm !== "dataPlacement" && itm !== "awPopOver" &&
itm !== "dataTitle") {
html += Attr(fAction, itm);
innerTable += Attr(fAction, itm);
}
}
html += ">";
innerTable += ">";
if (fAction.iconClass) {
html += "<i class=\"" + fAction.iconClass + "\"></i>";
innerTable += "<i class=\"" + fAction.iconClass + "\"></i>";
} else {
html += SelectIcon({
innerTable += SelectIcon({
action: field_action
});
}
//html += (fAction.label) ? "<span class=\"list-action-label\"> " + list.fieldActions[field_action].label +
// "</span>" : "";
html += "</a>";
innerTable += "</a>";
}
}
}
html += "</td>\n";
innerTable += "</td>\n";
}
html += "</tr>\n";
innerTable += "</tr>\n";
// Message for when a collection is empty
html += "<tr class=\"loading-info\" ng-show=\"" + list.iterator + "Loading == false && " + list.name + ".length == 0\">\n";
html += "<td colspan=\"" + cnt + "\"><div class=\"loading-info\">No records matched your search.</div></td>\n";
html += "</tr>\n";
innerTable += "<tr class=\"loading-info\" ng-show=\"" + list.iterator + "Loading == false && " + list.name + ".length == 0\">\n";
innerTable += "<td colspan=\"" + cnt + "\"><div class=\"loading-info\">No records matched your search.</div></td>\n";
innerTable += "</tr>\n";
// Message for loading
html += "<tr class=\"loading-info\" ng-show=\"" + list.iterator + "Loading == true\">\n";
html += "<td colspan=\"" + cnt + "\"><div class=\"loading-info\">Loading...</div></td>\n";
html += "</tr>\n";
innerTable += "<tr class=\"loading-info\" ng-show=\"" + list.iterator + "Loading == true\">\n";
innerTable += "<td colspan=\"" + cnt + "\"><div class=\"loading-info\">Loading...</div></td>\n";
innerTable += "</tr>\n";
// End List
html += "</tbody>\n";
html += "</table>\n";
innerTable += "</tbody>\n";
table.html(innerTable);
html += table.prop('outerHTML');
html += "</div><!-- table container -->\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 $('<th>')
.addClass('col-xs-1 select-column')
.append(
$('<select-all>')
.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 += "<th class=\"col-lg-1 col-md-1 col-sm-2 hidden-xs\">#</th>\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)) {

View File

@ -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);

View File

@ -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);

View File

@ -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();
};
}];

View File

@ -0,0 +1,11 @@
import controller from './multi-select-list.controller';
export default
[ function() {
return {
restrict: 'A',
scope: {
},
controller: controller
};
}];

View File

@ -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);
};
}
};
}];

View File

@ -0,0 +1,18 @@
<label>
<input
type="checkbox"
ng-disabled="itemsLength === 0"
ng-model="isSelected"
ng-change="doSelectAll()">
{{label}}
</label>
<button
ng-click="selectAllExtended()"
ng-if="!isSelectionExtended && showExtendedMessage">
{{selectExtendedLabel}}
</button>
<button
ng-click="deselectAllExtended()"
ng-if="isSelectionExtended">
{{deselectExtendedLabel}}
</button>

View File

@ -0,0 +1,33 @@
export default
[ function() {
return {
restrict: 'E',
scope: {
item: '=item'
},
require: '^multiSelectList',
template: '<input type="checkbox" ng-model="isSelected">',
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);
});
}
};
}];

View File

@ -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;
}
}