优化内容
- placeholder="提示语"
同input框一样有提示语 - output-properties="选中标签输出的属性"
对于程序我们通常只需要关注选中的标签对应的代码列表,而不是对象列表 不指定字段就是对象列表 - max-labels="最多显示的标签"
下拉框和选中以后的样式会随着内容无限撑大导致DOM样式会很难看 - 修复部分样式问题
- 页面初始化根据output-model 的内容自动勾选列表数据
示例代码
<div isteven-multi-select
input-model="reinsTreatyNos"
output-model="keywords.treatyNo"
button-label="value"
item-label="id value"
tick-property="ticked"
placeholder="请选择合约"
tick-property="ticked"
output-properties="id"
max-labels="3">
</div>
效果图
修改以后的源码
/*
* Angular JS Multi Select
* Creates a dropdown-like button with checkboxes.
*
* Project started on: Tue, 14 Jan 2014 - 5:18:02 PM
* Current version: 3.0.0
*
* Released under the MIT License
* --------------------------------------------------------------------------------
* The MIT License (MIT)
*
* Copyright (c) 2014 Ignatius Steven (https://github.com/isteven)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
* --------------------------------------------------------------------------------
*/
'use strict'
angular.module( 'isteven-multi-select', ['ng'] ).directive( 'istevenMultiSelect' , [ '$sce', '$timeout', '$templateCache', function ( $sce, $timeout, $templateCache ) {
return {
restrict:
'AE',
scope:
{
// models
inputModel : '=',
outputModel : '=',
// settings based on attribute
placeholder : '@',
buttonLabel : '@',
directiveId : '@',
helperElements : '@',
isDisabled : '=',
itemLabel : '@',
outputProperties : '@',
maxLabels : '@',
orientation : '@',
selectionMode : '@',
minSearchLength : '@', // 3.0.0 - OK
// settings based on input model property
tickProperty : '@',
disableProperty : '@',
groupProperty : '@',
searchProperty : '@', // 3.0.0 - OK
maxHeight : '@',
// callbacks
onClear : '&', // 3.0.0 - OK
onClose : '&',
onSearchChange : '&', // 3.0.0 - OK
onItemClick : '&',
onOpen : '&',
onReset : '&', // 3.0.0 - OK
onSelectAll : '&', // 3.0.0 - OK
onSelectNone : '&', // 3.0.0 - OK
// i18n
translation : '=' // 3.0.0 - OK
},
template:
'<span class="multiSelect inlineBlock" id={{directiveId}}>' +
'<button type="button" style="width: 100%;" ' +
'ng-click="toggleCheckboxes( $event ); refreshSelectedItems(); refreshButton(); prepareGrouping; prepareIndex();"' +
'ng-bind-html="varButtonLabel">' +
'</button>' +
'<div class="checkboxLayer">' +
'<div class="helperContainer" ng-if="displayHelper( \'filter\' ) || displayHelper( \'all\' ) || displayHelper( \'none\' ) || displayHelper( \'reset\' )">' +
'<div class="line" ng-if="displayHelper( \'all\' ) || displayHelper( \'none\' ) || displayHelper( \'reset\' )">' +
'<button type="button" class="helperButton"' +
'ng-if="!isDisabled && displayHelper( \'all\' )"' +
'ng-click="select( \'all\', $event );"' +
'ng-bind-html="lang.selectAll">' +
'</button>'+
'<button type="button" class="helperButton"' +
'ng-if="!isDisabled && displayHelper( \'none\' )"' +
'ng-click="select( \'none\', $event );"' +
'ng-bind-html="lang.selectNone">' +
'</button>'+
'<button type="button" class="helperButton reset"' +
'ng-if="!isDisabled && displayHelper( \'reset\' )"' +
'ng-click="select( \'reset\', $event );"' +
'ng-bind-html="lang.reset">'+
'</button>' +
'</div>' +
'<div class="line" style="position:relative" ng-if="displayHelper( \'filter\' )">'+
'<input placeholder="请输入搜索内容" type="text"' +
'ng-click="select( \'filter\', $event )" '+
'ng-model="inputLabel.labelFilter" '+
'ng-change="searchChanged()" class="inputFilter"'+
'/>'+
'<button type="button" class="clearButton" ng-click="clearClicked( $event )" >×</button> '+
'</div> '+
'</div> '+
'<div class="checkBoxContainer">'+
'<div '+
'ng-repeat="item in filteredModel | filter:removeGroupEndMarker" class="multiSelectItem"'+
'ng-class="{selected: item[ tickProperty ], horizontal: orientationH, vertical: orientationV, multiSelectGroup:item[ groupProperty ], disabled:itemIsDisabled( item )}"'+
'ng-click="syncItems( item, $event, $index );" '+
'ng-mouseleave="removeFocusStyle( tabIndex );"> '+
'<div class="acol" ng-if="item[ spacingProperty ] > 0" ng-repeat="i in numberToArray( item[ spacingProperty ] ) track by $index">'+
'</div> '+
'<div class="acol">'+
'<label>'+
'<input class="checkbox focusable" type="checkbox" '+
'ng-disabled="itemIsDisabled( item )" '+
'ng-checked="item[ tickProperty ]" '+
'ng-click="syncItems( item, $event, $index )" />'+
'<span '+
'ng-class="{disabled:itemIsDisabled( item )}" '+
'ng-bind-html="writeLabel( item, \'itemLabel\' )">'+
'</span>'+
'</label>'+
'</div>'+
'<span class="tickMark" ng-if="item[ groupProperty ] !== true && item[ tickProperty ] === true">✔</span>'+
'</div>'+
'</div>'+
'</div>'+
'</span>',
link: function ( $scope, element, attrs ) {
$scope.backUp = [];
$scope.varButtonLabel = '';
$scope.spacingProperty = '';
$scope.indexProperty = '';
$scope.orientationH = false;
$scope.orientationV = true;
$scope.filteredModel = [];
$scope.inputLabel = { labelFilter: '' };
$scope.tabIndex = 0;
$scope.lang = {};
$scope.localModel = [];
var
prevTabIndex = 0,
helperItems = [],
helperItemsLength = 0,
checkBoxLayer = '',
scrolled = false,
selectedItems = [],
formElements = [],
vMinSearchLength = 0,
clickedItem = null;
// v3.0.0
// clear button clicked
$scope.clearClicked = function( e ) {
$scope.inputLabel.labelFilter = '';
$scope.updateFilter();
$scope.select( 'clear', e );
}
// A little hack so that AngularJS ng-repeat can loop using start and end index like a normal loop
// http://stackoverflow.com/questions/16824853/way-to-ng-repeat-defined-number-of-times-instead-of-repeating-over-array
$scope.numberToArray = function( num ) {
return new Array( num );
}
// Call this function when user type on the filter field
$scope.searchChanged = function() {
if ( $scope.inputLabel.labelFilter.length < vMinSearchLength && $scope.inputLabel.labelFilter.length > 0 ) {
return false;
}
$scope.updateFilter();
}
$scope.updateFilter = function()
{
// we check by looping from end of input-model
$scope.filteredModel = [];
var i = 0;
if ( typeof $scope.localModel === 'undefined' ) {
return false;
}
for( i = $scope.localModel.length - 1; i >= 0; i-- ) {
// if it's group end, we push it to filteredModel[];
if ( typeof $scope.localModel[ i ][ $scope.groupProperty ] !== 'undefined' && $scope.localModel[ i ][ $scope.groupProperty ] === false ) {
$scope.filteredModel.push( $scope.localModel[ i ] );
}
// if it's data
var gotData = false;
if ( typeof $scope.localModel[ i ][ $scope.groupProperty ] === 'undefined' ) {
// If we set the search-key attribute, we use this loop.
if ( typeof attrs.searchProperty !== 'undefined' && $scope.searchProperty !== '' ) {
for (var key in $scope.localModel[ i ] ) {
if (
typeof $scope.localModel[ i ][ key ] !== 'boolean'
&& String( $scope.localModel[ i ][ key ] ).toUpperCase().indexOf( $scope.inputLabel.labelFilter.toUpperCase() ) >= 0
&& $scope.searchProperty.indexOf( key ) > -1
) {
gotData = true;
break;
}
}
}
// if there's no search-key attribute, we use this one. Much better on performance.
else {
for ( var key in $scope.localModel[ i ] ) {
if (
typeof $scope.localModel[ i ][ key ] !== 'boolean'
&& String( $scope.localModel[ i ][ key ] ).toUpperCase().indexOf( $scope.inputLabel.labelFilter.toUpperCase() ) >= 0
) {
gotData = true;
break;
}
}
}
if ( gotData === true ) {
// push
$scope.filteredModel.push( $scope.localModel[ i ] );
}
}
// if it's group start
if ( typeof $scope.localModel[ i ][ $scope.groupProperty ] !== 'undefined' && $scope.localModel[ i ][ $scope.groupProperty ] === true ) {
if ( typeof $scope.filteredModel[ $scope.filteredModel.length - 1 ][ $scope.groupProperty ] !== 'undefined'
&& $scope.filteredModel[ $scope.filteredModel.length - 1 ][ $scope.groupProperty ] === false ) {
$scope.filteredModel.pop();
}
else {
$scope.filteredModel.push( $scope.localModel[ i ] );
}
}
}
$scope.filteredModel.reverse();
$timeout( function() {
$scope.getFormElements();
// Callback: on filter change
if ( $scope.inputLabel.labelFilter.length > vMinSearchLength ) {
var filterObj = [];
angular.forEach( $scope.filteredModel, function( value, key ) {
if ( typeof value !== 'undefined' ) {
if ( typeof value[ $scope.groupProperty ] === 'undefined' ) {
var tempObj = angular.copy( value );
var index = filterObj.push( tempObj );
delete filterObj[ index - 1 ][ $scope.indexProperty ];
delete filterObj[ index - 1 ][ $scope.spacingProperty ];
}
}
});
$scope.onSearchChange({
data:
{
keyword: $scope.inputLabel.labelFilter,
result: filterObj
}
});
}
},0);
};
// List all the input elements.
// This function will be called everytime the filter is updated. Not good for performance, but oh well..
$scope.getFormElements = function() {
formElements = [];
// Get helper - select & reset buttons
var selectButtons = element.children().children().next().children().children()[ 0 ].getElementsByTagName( 'button' );
// Get helper - search
var inputField = element.children().children().next().children().children().next()[ 0 ].getElementsByTagName( 'input' );
// Get helper - clear button
var clearButton = element.children().children().next().children().children().next()[ 0 ].getElementsByTagName( 'button' );
// Get checkboxes
var checkboxes = element.children().children().next().children().next()[ 0 ].getElementsByTagName( 'input' );
// Push them into global array formElements[]
for ( var i = 0; i < selectButtons.length ; i++ ) { formElements.push( selectButtons[ i ] ); }
for ( var i = 0; i < inputField.length ; i++ ) { formElements.push( inputField[ i ] ); }
for ( var i = 0; i < clearButton.length ; i++ ) { formElements.push( clearButton[ i ] ); }
for ( var i = 0; i < checkboxes.length ; i++ ) { formElements.push( checkboxes[ i ] ); }
}
// check if an item has $scope.groupProperty (be it true or false)
$scope.isGroupMarker = function( item , type ) {
if ( typeof item[ $scope.groupProperty ] !== 'undefined' && item[ $scope.groupProperty ] === type ) return true;
return false;
}
$scope.removeGroupEndMarker = function( item ) {
if ( typeof item[ $scope.groupProperty ] !== 'undefined' && item[ $scope.groupProperty ] === false ) return false;
return true;
}
// Show or hide a helper element
$scope.displayHelper = function( elementString ) {
if ( attrs.selectionMode && $scope.selectionMode.toUpperCase() === 'SINGLE' ) {
switch( elementString.toUpperCase() ) {
case 'ALL':
return false;
break;
case 'NONE':
return false;
break;
case 'RESET':
if ( typeof attrs.helperElements === 'undefined' ) {
return true;
}
else if ( attrs.helperElements && $scope.helperElements.toUpperCase().indexOf( 'RESET' ) >= 0 ) {
return true;
}
break;
case 'FILTER':
if ( typeof attrs.helperElements === 'undefined' ) {
return true;
}
if ( attrs.helperElements && $scope.helperElements.toUpperCase().indexOf( 'FILTER' ) >= 0 ) {
return true;
}
break;
default:
break;
}
return false;
}
else {
if ( typeof attrs.helperElements === 'undefined' ) {
return true;
}
if ( attrs.helperElements && $scope.helperElements.toUpperCase().indexOf( elementString.toUpperCase() ) >= 0 ) {
return true;
}
return false;
}
}
// call this function when an item is clicked
$scope.syncItems = function( item, e, ng_repeat_index ) {
e.preventDefault();
e.stopPropagation();
// if the directive is globaly disabled, do nothing
if ( typeof attrs.disableProperty !== 'undefined' && item[ $scope.disableProperty ] === true ) {
return false;
}
// if item is disabled, do nothing
if ( typeof attrs.isDisabled !== 'undefined' && $scope.isDisabled === true ) {
return false;
}
// if end group marker is clicked, do nothing
if ( typeof item[ $scope.groupProperty ] !== 'undefined' && item[ $scope.groupProperty ] === false ) {
return false;
}
var index = $scope.filteredModel.indexOf( item );
// if the start of group marker is clicked ( only for multiple selection! )
// how it works:
// - if, in a group, there are items which are not selected, then they all will be selected
// - if, in a group, all items are selected, then they all will be de-selected
if ( typeof item[ $scope.groupProperty ] !== 'undefined' && item[ $scope.groupProperty ] === true ) {
// this is only for multiple selection, so if selection mode is single, do nothing
if ( attrs.selectionMode && $scope.selectionMode.toUpperCase() === 'SINGLE' ) {
return false;
}
var i,j,k;
var startIndex = 0;
var endIndex = $scope.filteredModel.length - 1;
var tempArr = [];
// nest level is to mark the depth of the group.
// when you get into a group (start group marker), nestLevel++
// when you exit a group (end group marker), nextLevel--
var nestLevel = 0;
// we loop throughout the filtered model (not whole model)
for( i = index ; i < $scope.filteredModel.length ; i++) {
// this break will be executed when we're done processing each group
if ( nestLevel === 0 && i > index )
{
break;
}
if ( typeof $scope.filteredModel[ i ][ $scope.groupProperty ] !== 'undefined' && $scope.filteredModel[ i ][ $scope.groupProperty ] === true ) {
// To cater multi level grouping
if ( tempArr.length === 0 ) {
startIndex = i + 1;
}
nestLevel = nestLevel + 1;
}
// if group end
else if ( typeof $scope.filteredModel[ i ][ $scope.groupProperty ] !== 'undefined' && $scope.filteredModel[ i ][ $scope.groupProperty ] === false ) {
nestLevel = nestLevel - 1;
// cek if all are ticked or not
if ( tempArr.length > 0 && nestLevel === 0 ) {
var allTicked = true;
endIndex = i;
for ( j = 0; j < tempArr.length ; j++ ) {
if ( typeof tempArr[ j ][ $scope.tickProperty ] !== 'undefined' && tempArr[ j ][ $scope.tickProperty ] === false ) {
allTicked = false;
break;
}
}
if ( allTicked === true ) {
for ( j = startIndex; j <= endIndex ; j++ ) {
if ( typeof $scope.filteredModel[ j ][ $scope.groupProperty ] === 'undefined' ) {
if ( typeof attrs.disableProperty === 'undefined' ) {
$scope.filteredModel[ j ][ $scope.tickProperty ] = false;
// we refresh input model as well
inputModelIndex = $scope.filteredModel[ j ][ $scope.indexProperty ];
$scope.localModel[ inputModelIndex ][ $scope.tickProperty ] = false;
}
else if ( $scope.filteredModel[ j ][ $scope.disableProperty ] !== true ) {
$scope.filteredModel[ j ][ $scope.tickProperty ] = false;
// we refresh input model as well
inputModelIndex = $scope.filteredModel[ j ][ $scope.indexProperty ];
$scope.localModel[ inputModelIndex ][ $scope.tickProperty ] = false;
}
}
}
}
else {
for ( j = startIndex; j <= endIndex ; j++ ) {
if ( typeof $scope.filteredModel[ j ][ $scope.groupProperty ] === 'undefined' ) {
if ( typeof attrs.disableProperty === 'undefined' ) {
$scope.filteredModel[ j ][ $scope.tickProperty ] = true;
// we refresh input model as well
inputModelIndex = $scope.filteredModel[ j ][ $scope.indexProperty ];
$scope.localModel[ inputModelIndex ][ $scope.tickProperty ] = true;
}
else if ( $scope.filteredModel[ j ][ $scope.disableProperty ] !== true ) {
$scope.filteredModel[ j ][ $scope.tickProperty ] = true;
// we refresh input model as well
inputModelIndex = $scope.filteredModel[ j ][ $scope.indexProperty ];
$scope.localModel[ inputModelIndex ][ $scope.tickProperty ] = true;
}
}
}
}
}
}
// if data
else {
tempArr.push( $scope.filteredModel[ i ] );
}
}
}
// if an item (not group marker) is clicked
else {
// If it's single selection mode
if ( attrs.selectionMode && $scope.selectionMode.toUpperCase() === 'SINGLE' ) {
// first, set everything to false
for( i=0 ; i < $scope.filteredModel.length ; i++) {
$scope.filteredModel[ i ][ $scope.tickProperty ] = false;
}
for( i=0 ; i < $scope.localModel.length ; i++) {
$scope.localModel[ i ][ $scope.tickProperty ] = false;
}
// then set the clicked item to true
$scope.filteredModel[ index ][ $scope.tickProperty ] = true;
// we then hide the checkbox layer
$scope.toggleCheckboxes( e );
}
// Multiple
else {
$scope.filteredModel[ index ][ $scope.tickProperty ] = !$scope.filteredModel[ index ][ $scope.tickProperty ];
}
// we refresh input model as well
var inputModelIndex = $scope.filteredModel[ index ][ $scope.indexProperty ];
$scope.localModel[ inputModelIndex ][ $scope.tickProperty ] = $scope.filteredModel[ index ][ $scope.tickProperty ];
}
// we execute the callback function here
clickedItem = angular.copy( item );
if ( clickedItem !== null ) {
$timeout( function() {
delete clickedItem[ $scope.indexProperty ];
delete clickedItem[ $scope.spacingProperty ];
$scope.onItemClick( { data: clickedItem } );
clickedItem = null;
}, 0 );
}
$scope.refreshOutputModel();
$scope.refreshButton();
// We update the index here
prevTabIndex = $scope.tabIndex;
$scope.tabIndex = ng_repeat_index + helperItemsLength;
// Set focus on the hidden checkbox
e.target.focus();
// set & remove CSS style
$scope.removeFocusStyle( prevTabIndex );
$scope.setFocusStyle( $scope.tabIndex );
}
// update $scope.outputModel
$scope.refreshOutputModel = function() {
$scope.outputModel = [];
$scope.outputModelView = [];
angular.forEach( $scope.localModel, function( value, key ) {
if ( typeof value !== 'undefined' ) {
if ( typeof value[ $scope.groupProperty ] === 'undefined' ) {
if ( value[ $scope.tickProperty ] === true ) {
// selectedItems.push( value );
var temp = angular.copy( value );
var indexV = $scope.outputModelView.push( temp);
var index ='';
if(undefined !=null!=$scope.outputProperties && null!=$scope.outputProperties && $scope.outputProperties.trim().length>0 ){
index = $scope.outputModel.push( temp[$scope.outputProperties] );
}else {
index = $scope.outputModel.push( temp);
}
delete $scope.outputModel[ index - 1 ][ $scope.indexProperty ];
delete $scope.outputModel[ index - 1 ][ $scope.spacingProperty ];
delete $scope.outputModelView[ indexV - 1 ][ $scope.indexProperty ];
delete $scope.outputModelView[ indexV - 1 ][ $scope.spacingProperty ];
}
}
}
});
if($scope.outputModel.length>0){
$scope.outputModel = $scope.outputModel.toString();
}else {
$scope.outputModel ='';
}
}
// refresh button label
$scope.refreshButton = function() {
$scope.varButtonLabel = '';
var ctr = 0;
// refresh button label...
if ( $scope.outputModelView.length === 0 ) {
// https://github.com/isteven/angular-multi-select/pull/19
$scope.varButtonLabel = $scope.lang.nothingSelected;
}
else {
var tempMaxLabels = $scope.outputModelView.length;
if ( typeof $scope.maxLabels !== 'undefined' && $scope.maxLabels !== '' ) {
tempMaxLabels = $scope.maxLabels;
}
// if max amount of labels displayed..
if ( $scope.outputModelView.length > tempMaxLabels ) {
$scope.more = true;
}
else {
$scope.more = false;
}
angular.forEach( $scope.outputModelView, function( value, key ) {
if ( typeof value !== 'undefined' ) {
if ( ctr < tempMaxLabels ) {
$scope.varButtonLabel += ( $scope.varButtonLabel.length > 0 ? '</div> <div class="buttonLabel">' : '<div class="buttonLabel">') + $scope.writeLabel( value, 'buttonLabel' );
}
ctr++;
}
});
if ( $scope.more === true ) {
// https://github.com/isteven/angular-multi-select/pull/16
if (tempMaxLabels > 0) {
$scope.varButtonLabel += ' ...';
}
$scope.varButtonLabel += '(' + $scope.outputModelView.length + ') 个';
}
}
$scope.varButtonLabel = $sce.trustAsHtml( $scope.varButtonLabel + ' <span class="caret"></span>' );
}
// Check if a checkbox is disabled or enabled. It will check the granular control (disableProperty) and global control (isDisabled)
// Take note that the granular control has higher priority.
$scope.itemIsDisabled = function( item ) {
if ( typeof attrs.disableProperty !== 'undefined' && item[ $scope.disableProperty ] === true ) {
return true;
}
else {
if ( $scope.isDisabled === true ) {
return true;
}
else {
return false;
}
}
}
// A simple function to parse the item label settings. Used on the buttons and checkbox labels.
$scope.writeLabel = function( item, type ) {
// type is either 'itemLabel' or 'buttonLabel'
var temp = $scope[ type ].split( ' ' );
var label = '';
angular.forEach( temp, function( value, key ) {
item[ value ] && ( label += ' ' + value.split( '.' ).reduce( function( prev, current ) {
return prev[ current ];
}, item ));
});
if ( type.toUpperCase() === 'BUTTONLABEL' ) {
return label;
}
return $sce.trustAsHtml( label );
}
// UI operations to show/hide checkboxes based on click event..
$scope.toggleCheckboxes = function( e ) {
// We grab the button
var clickedEl = element.children()[0];
// Just to make sure.. had a bug where key events were recorded twice
angular.element( document ).off( 'click', $scope.externalClickListener );
angular.element( document ).off( 'keydown', $scope.keyboardListener );
// clear filter
$scope.inputLabel.labelFilter = '';
$scope.updateFilter();
// The idea below was taken from another multi-select directive - https://github.com/amitava82/angular-multiselect
// His version is awesome if you need a more simple multi-select approach.
// close
if ( angular.element( checkBoxLayer ).hasClass( 'show' )) {
angular.element( checkBoxLayer ).removeClass( 'show' );
angular.element( clickedEl ).removeClass( 'buttonClicked' );
angular.element( document ).off( 'click', $scope.externalClickListener );
angular.element( document ).off( 'keydown', $scope.keyboardListener );
// clear the focused element;
$scope.removeFocusStyle( $scope.tabIndex );
// close callback
$timeout( function() {
$scope.onClose();
}, 0 );
// set focus on button again
element.children().children()[ 0 ].focus();
}
// open
else
{
helperItems = [];
helperItemsLength = 0;
angular.element( checkBoxLayer ).addClass( 'show' );
angular.element( clickedEl ).addClass( 'buttonClicked' );
// Attach change event listener on the input filter.
// We need this because ng-change is apparently not an event listener.
angular.element( document ).on( 'click', $scope.externalClickListener );
angular.element( document ).on( 'keydown', $scope.keyboardListener );
// to get the initial tab index, depending on how many helper elements we have.
// priority is to always focus it on the input filter
$scope.getFormElements();
$scope.tabIndex = 0;
var helperContainer = angular.element( element[ 0 ].querySelector( '.helperContainer' ) )[0];
if ( typeof helperContainer !== 'undefined' ) {
for ( var i = 0; i < helperContainer.getElementsByTagName( 'BUTTON' ).length ; i++ ) {
helperItems[ i ] = helperContainer.getElementsByTagName( 'BUTTON' )[ i ];
}
helperItemsLength = helperItems.length + helperContainer.getElementsByTagName( 'INPUT' ).length;
}
// focus on the filter element on open.
if ( element[ 0 ].querySelector( '.inputFilter' ) ) {
element[ 0 ].querySelector( '.inputFilter' ).focus();
$scope.tabIndex = $scope.tabIndex + helperItemsLength - 2;
}
// if there's no filter then just focus on the first checkbox item
else {
formElements[ $scope.tabIndex ].focus();
}
// open callback
$scope.onOpen();
}
}
// handle clicks outside the button / multi select layer
$scope.externalClickListener = function( e ) {
var targetsArr = element.find( e.target.tagName );
for (var i = 0; i < targetsArr.length; i++) {
if ( e.target == targetsArr[i] ) {
return;
}
}
angular.element( checkBoxLayer.previousSibling ).removeClass( 'buttonClicked' );
angular.element( checkBoxLayer ).removeClass( 'show' );
angular.element( document ).off( 'click', $scope.externalClickListener );
angular.element( document ).off( 'keydown', $scope.keyboardListener );
// close callback
$timeout( function() {
$scope.onClose();
}, 0 );
// set focus on button again
element.children().children()[ 0 ].focus();
}
// select All / select None / reset buttons
$scope.select = function( type, e ) {
var helperIndex = helperItems.indexOf( e.target );
$scope.tabIndex = helperIndex;
switch( type.toUpperCase() ) {
case 'ALL':
angular.forEach( $scope.filteredModel, function( value, key ) {
if ( typeof value !== 'undefined' && value[ $scope.disableProperty ] !== true ) {
if ( typeof value[ $scope.groupProperty ] === 'undefined' ) {
value[ $scope.tickProperty ] = true;
}
}
});
$scope.refreshOutputModel();
$scope.refreshButton();
$scope.onSelectAll();
break;
case 'NONE':
angular.forEach( $scope.filteredModel, function( value, key ) {
if ( typeof value !== 'undefined' && value[ $scope.disableProperty ] !== true ) {
if ( typeof value[ $scope.groupProperty ] === 'undefined' ) {
value[ $scope.tickProperty ] = false;
}
}
});
$scope.refreshOutputModel();
$scope.refreshButton();
$scope.onSelectNone();
break;
case 'RESET':
angular.forEach( $scope.filteredModel, function( value, key ) {
if ( typeof value[ $scope.groupProperty ] === 'undefined' && typeof value !== 'undefined' && value[ $scope.disableProperty ] !== true ) {
var temp = value[ $scope.indexProperty ];
value[ $scope.tickProperty ] = $scope.backUp[ temp ][ $scope.tickProperty ];
}
});
$scope.refreshOutputModel();
$scope.refreshButton();
$scope.onReset();
break;
case 'CLEAR':
$scope.tabIndex = $scope.tabIndex + 1;
$scope.onClear();
break;
case 'FILTER':
$scope.tabIndex = helperItems.length - 1;
break;
default:
}
}
// just to create a random variable name
function genRandomString( length ) {
var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
var temp = '';
for( var i=0; i < length; i++ ) {
temp += possible.charAt( Math.floor( Math.random() * possible.length ));
}
return temp;
}
// count leading spaces
$scope.prepareGrouping = function() {
var spacing = 0;
angular.forEach( $scope.filteredModel, function( value, key ) {
value[ $scope.spacingProperty ] = spacing;
if ( value[ $scope.groupProperty ] === true ) {
spacing+=2;
}
else if ( value[ $scope.groupProperty ] === false ) {
spacing-=2;
}
});
}
// prepare original index
$scope.prepareIndex = function() {
var ctr = 0;
angular.forEach( $scope.filteredModel, function( value, key ) {
value[ $scope.indexProperty ] = ctr;
ctr++;
});
}
// navigate using up and down arrow
$scope.keyboardListener = function( e ) {
var key = e.keyCode ? e.keyCode : e.which;
var isNavigationKey = false;
// ESC key (close)
if ( key === 27 ) {
e.preventDefault();
$scope.toggleCheckboxes( e );
}
// next element ( tab, down & right key )
else if ( key === 40 || key === 39 || ( !e.shiftKey && key == 9 ) ) {
isNavigationKey = true;
prevTabIndex = $scope.tabIndex;
$scope.tabIndex++;
if ( $scope.tabIndex > formElements.length - 1 ) {
$scope.tabIndex = 0;
prevTabIndex = formElements.length - 1;
}
while ( formElements[ $scope.tabIndex ].disabled === true ) {
$scope.tabIndex++;
if ( $scope.tabIndex > formElements.length - 1 ) {
$scope.tabIndex = 0;
}
}
}
// prev element ( shift+tab, up & left key )
else if ( key === 38 || key === 37 || ( e.shiftKey && key == 9 ) ) {
isNavigationKey = true;
prevTabIndex = $scope.tabIndex;
$scope.tabIndex--;
if ( $scope.tabIndex < 0 ) {
$scope.tabIndex = formElements.length - 1;
prevTabIndex = 0;
}
while ( formElements[ $scope.tabIndex ].disabled === true ) {
$scope.tabIndex--;
if ( $scope.tabIndex < 0 ) {
$scope.tabIndex = formElements.length - 1;
}
}
}
if ( isNavigationKey === true ) {
e.preventDefault();
// set focus on the checkbox
formElements[ $scope.tabIndex ].focus();
var actEl = document.activeElement;
if ( actEl.type.toUpperCase() === 'CHECKBOX' ) {
$scope.setFocusStyle( $scope.tabIndex );
$scope.removeFocusStyle( prevTabIndex );
}
else {
$scope.removeFocusStyle( prevTabIndex );
$scope.removeFocusStyle( helperItemsLength );
$scope.removeFocusStyle( formElements.length - 1 );
}
}
isNavigationKey = false;
}
// set (add) CSS style on selected row
$scope.setFocusStyle = function( tabIndex ) {
angular.element( formElements[ tabIndex ] ).parent().parent().parent().addClass( 'multiSelectFocus' );
}
// remove CSS style on selected row
$scope.removeFocusStyle = function( tabIndex ) {
angular.element( formElements[ tabIndex ] ).parent().parent().parent().removeClass( 'multiSelectFocus' );
}
/*****************************************************
*
* Initializations
*
*****************************************************/
// Unfortunately I need to add these grouping properties
var tempStr = genRandomString( 5 );
$scope.indexProperty = 'idx_' + tempStr;
$scope.spacingProperty = 'spc_' + tempStr;
// set orientation css
if ( typeof attrs.orientation !== 'undefined' ) {
if ( attrs.orientation.toUpperCase() === 'HORIZONTAL' ) {
$scope.orientationH = true;
$scope.orientationV = false;
}
else
{
$scope.orientationH = false;
$scope.orientationV = true;
}
}
// get elements required for DOM operation
checkBoxLayer = element.children().children().next()[0];
// set max-height property if provided
if ( typeof attrs.maxHeight !== 'undefined' ) {
var layer = element.children().children().children()[0];
angular.element( layer ).attr( "style", "height:" + $scope.maxHeight + "; overflow-y:scroll;" );
}
// icons.. I guess you can use <img> tag here if you want to.
var icon = {};
icon.selectAll = '✓' // a tick icon
icon.selectNone = '×' // x icon
icon.reset = '↶' // undo icon
// configurable button labels
if ( typeof attrs.translation !== 'undefined' ) {
$scope.lang.selectAll = $sce.trustAsHtml( icon.selectAll + ' ' + $scope.translation.selectAll );
$scope.lang.selectNone = $sce.trustAsHtml( icon.selectNone + ' ' + $scope.translation.selectNone );
$scope.lang.reset = $sce.trustAsHtml( icon.reset + ' ' + $scope.translation.reset );
$scope.lang.search = $scope.translation.search;
$scope.lang.nothingSelected = $sce.trustAsHtml( $scope.translation.nothingSelected );
}
else {
$scope.lang.selectAll = $sce.trustAsHtml( icon.selectAll + ' 全选' );
$scope.lang.selectNone = $sce.trustAsHtml( icon.selectNone + ' 清空' );
$scope.lang.reset = $sce.trustAsHtml( icon.reset + ' 重置' );
$scope.lang.search = 'Search...';
$scope.lang.nothingSelected = 'None Selected';
if(null!=$scope.placeholder ){
$scope.lang.search = $scope.placeholder;
$scope.lang.nothingSelected = "<span>"+$scope.placeholder+"</span>";
}
}
// min length of keyword to trigger the filter function
if ( typeof attrs.MinSearchLength !== 'undefined' && parseInt( attrs.MinSearchLength ) > 0 ) {
vMinSearchLength = Math.floor( parseInt( attrs.MinSearchLength ) );
}
/****************************************************
*
* Logic starts here, initiated by watch 1 & watch 2
*
****************************************************/
// watch1, for changes in input model property
// updates multi-select when user select/deselect a single checkbox programatically
// https://github.com/isteven/angular-multi-select/issues/8
$scope.$watch( 'inputModel' , function( newVal ) {
if ( newVal ) {
$scope.inputModel.forEach(function(data,key1){
$scope.outputModel.split(",").forEach(function(value,key){
if(data[$scope.outputProperties]===value){
data.ticked=true;
}
});
});
$scope.localModel = angular.copy( $scope.inputModel );
$scope.refreshOutputModel();
$scope.refreshButton();
}
}, true );
// watch2 for changes in input model as a whole
// this on updates the multi-select when a user load a whole new input-model. We also update the $scope.backUp variable
$scope.$watch( 'localModel' , function( newVal ) {
if ( newVal ) {
$scope.backUp = angular.copy( $scope.localModel );
$scope.updateFilter();
$scope.prepareGrouping();
$scope.prepareIndex();
$scope.refreshOutputModel();
$scope.refreshButton();
}
});
// watch for changes in directive state (disabled or enabled)
$scope.$watch( 'isDisabled' , function( newVal ) {
$scope.isDisabled = newVal;
});
// this is for touch enabled devices. We don't want to hide checkboxes on scroll.
angular.element( document ).on( 'touchstart', function( e ) {
$scope.$apply( function() {
scrolled = false;
});
});
// also for touch enabled devices
angular.element( document ).on( 'touchmove', function( e ) {
$scope.$apply( function() {
scrolled = true;
});
});
}
}
}]);
/*
* Don't modify things marked with ! - unless you know what you're doing
*/
/* ! vertical layout */
.multiSelect .vertical {
float: none;
}
/* ! horizontal layout */
.multiSelect .horizontal:not(.multiSelectGroup) {
float: left;
}
/* ! create a "row" */
.multiSelect .line {
padding: 2px 0px 4px 0px;
max-height: 30px;
overflow: hidden;
box-sizing: content-box;
}
/* ! create a "column" */
.multiSelect .acol {
display: inline-block;
min-width: 12px;
}
/* ! */
.multiSelect .inlineBlock {
display: inline-block;
}
/* the multiselect button */
.multiSelect > button {
display: inline-block;
position: relative;
text-align: center;
cursor: pointer;
border: 1px solid #c6c6c6;
padding: 1px 8px 1px 8px;
font-size: 14px;
min-height : 38px !important;
border-radius: 4px;
color: #555;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
white-space:normal;
background-color: #fff;
background-image: linear-gradient(#fff, #f7f7f7);
}
/* button: hover */
.multiSelect > button:hover {
background-image: linear-gradient(#fff, #e9e9e9);
}
/* button: clicked */
.multiSelect .buttonClicked {
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15) inset, 0 1px 2px rgba(0, 0, 0, 0.05);
}
/* labels on the button */
.multiSelect .buttonLabel {
padding: 5px;
background: #e9e9e9;
display: inline-block;
max-width: 100%;
transition: margin .3s cubic-bezier(.645,.045,.355,1);
border-radius: 5px;
margin: 3px;
white-space: normal;
text-overflow: ellipsis
}
/* downward pointing arrow */
.multiSelect .caret {
display: inline-block;
width: 0;
height: 0;
margin: 0px 0px 1px 12px !important;
vertical-align: middle;
border-top: 4px solid #333;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
border-bottom: 0 dotted;
}
/* the main checkboxes and helper layer */
.multiSelect .checkboxLayer {
background-color: #fff;
position: absolute;
z-index: 999;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
min-width:278px;
display: none !important;
height: 300px;
overflow-y: auto;
}
/* container of helper elements */
.multiSelect .helperContainer {
border-bottom: 1px solid #ddd;
padding: 8px 8px 0px 8px;
}
/* helper buttons (select all, none, reset); */
.multiSelect .helperButton {
display: inline;
text-align: center;
cursor: pointer;
border: 1px solid #ccc;
height: 26px;
font-size: 13px;
border-radius: 2px;
color: #666;
background-color: #f1f1f1;
line-height: 1.6;
margin: 0px 0px 8px 0px;
}
.multiSelect .helperButton.reset{
float: right;
}
.multiSelect .helperButton:not( .reset ) {
margin-right: 4px;
}
/* clear button */
.multiSelect .clearButton {
position: absolute;
display: inline;
text-align: center;
cursor: pointer;
border: 1px solid #ccc;
height: 22px;
width: 22px;
font-size: 13px;
border-radius: 2px;
color: #666;
background-color: #f1f1f1;
line-height: 1.4;
right : 2px;
top: 4px;
}
/* filter */
.multiSelect .inputFilter {
border-radius: 2px;
border: 1px solid #ccc;
height: 26px;
font-size: 14px;
width:100%;
padding-left:7px;
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
-moz-box-sizing: border-box; /* Firefox, other Gecko */
box-sizing: border-box; /* Opera/IE 8+ */
color: #888;
margin: 0px 0px 8px 0px;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
}
/* helper elements on hover & focus */
.multiSelect .clearButton:hover,
.multiSelect .helperButton:hover {
border: 1px solid #ccc;
color: #999;
background-color: #f4f4f4;
}
.multiSelect .clearButton:focus,
.multiSelect .helperButton:focus,
.multiSelect .inputFilter:focus {
border: 1px solid #66AFE9 !important;
outline: 0;
-webkit-box-shadow: box-shadow: inset 0 0 1px rgba(0,0,0,.065), 0 0 5px rgba(102, 175, 233, .6) !important;
box-shadow: inset 0 0 1px rgba(0,0,0,.065), 0 0 5px rgba(102, 175, 233, .6) !important;
}
/* container of multi select items */
.multiSelect .checkBoxContainer {
display: block;
padding: 8px;
overflow: hidden;
}
/* ! to show / hide the checkbox layer above */
.multiSelect .show {
display: block !important;
}
/* item labels */
.multiSelect .multiSelectItem {
display: block;
padding: 3px;
color: #444;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
border: 1px solid transparent;
position: relative;
min-width:278px;
min-height: 32px;
}
/* Styling on selected items */
.multiSelect .multiSelectItem:not(.multiSelectGroup).selected
{
background-image: linear-gradient( #e9e9e9, #f1f1f1 );
color: #555;
cursor: pointer;
border-top: 1px solid #e4e4e4;
border-left: 1px solid #e4e4e4;
border-right: 1px solid #d9d9d9;
}
.multiSelect .multiSelectItem .acol label {
display: inline-block;
padding-right: 30px;
margin: 0px;
font-weight: normal;
line-height: normal;
}
/* item labels focus on mouse hover */
.multiSelect .multiSelectItem:hover,
.multiSelect .multiSelectGroup:hover {
background-image: linear-gradient( #c1c1c1, #999 ) !important;
color: #fff !important;
cursor: pointer;
border: 1px solid #ccc !important;
}
/* item labels focus using keyboard */
.multiSelect .multiSelectFocus {
background-image: linear-gradient( #c1c1c1, #999 ) !important;
color: #fff !important;
cursor: pointer;
border: 1px solid #ccc !important;
}
/* change mouse pointer into the pointing finger */
.multiSelect .multiSelectItem span:hover,
.multiSelect .multiSelectGroup span:hover
{
cursor: pointer;
}
/* ! group labels */
.multiSelect .multiSelectGroup {
display: block;
clear: both;
}
/* right-align the tick mark (✔) */
.multiSelect .tickMark {
display:inline-block;
position: absolute;
right: 10px;
top: 7px;
font-size: 10px;
}
/* hide the original HTML checkbox away */
.multiSelect .checkbox {
color: #ddd !important;
position: absolute;
left: -9999px;
cursor: pointer;
}
/* checkboxes currently disabled */
.multiSelect .disabled,
.multiSelect .disabled:hover,
.multiSelect .disabled label input:hover ~ span {
color: #c4c4c4 !important;
cursor: not-allowed !important;
}
/* If you use images in button / checkbox label, you might want to change the image style here. */
.multiSelect img {
vertical-align: middle;
margin-bottom:0px;
max-height: 22px;
max-width:22px;
}
评论区