/*
* Angular JS Multi Select
* Creates a dropdown-like button with checkboxes.
*
* Created: Tue, 14 Jan 2014 - 5:18:02 PM
*
* 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.
* --------------------------------------------------------------------------------
*/
angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', '$filter', function ( $sce, $filter ) {
return {
restrict:
'AE',
replace:
true,
scope:
{
inputModel : '=',
outputModel : '=',
buttonLabel : '@',
selectionMode : '@',
itemLabel : '@',
tickProperty : '@',
disableProperty : '@',
orientation : '@',
maxLabels : '@',
isDisabled : '=',
directiveId : '@',
helperElements : '@',
onOpen : '&',
onClose : '&',
onBlur : '&',
onFocus : '&',
},
template:
'' +
'' +
'
' +
'
' +
'Select: ' +
' ' +
' ' +
'' +
'
' +
'
' +
'Filter: ' +
' ' +
'
' +
'
' +
'
' +
'
✔
' +
'
' +
'
' +
' ' +
'
' +
'
' +
'
' +
'',
link: function ( $scope, element, attrs ) {
$scope.selectedItems = [];
$scope.backUp = [];
$scope.varButtonLabel = '';
$scope.currentButton = null;
$scope.scrolled = false;
// Show or hide a helper element
$scope.displayHelper = function( elementString ) {
if ( typeof attrs.helperElements === 'undefined' ) {
return true;
}
switch( elementString.toUpperCase() ) {
case 'ALL':
if ( attrs.selectionMode && $scope.selectionMode.toUpperCase() === 'SINGLE' ) {
return false;
}
else {
if ( attrs.helperElements && $scope.helperElements.toUpperCase().indexOf( 'ALL' ) >= 0 ) {
return true;
}
}
break;
case 'NONE':
if ( attrs.selectionMode && $scope.selectionMode.toUpperCase() === 'SINGLE' ) {
return false;
}
else {
if ( attrs.helperElements && $scope.helperElements.toUpperCase().indexOf( 'NONE' ) >= 0 ) {
return true;
}
}
break;
case 'RESET':
if ( attrs.helperElements && $scope.helperElements.toUpperCase().indexOf( 'RESET' ) >= 0 ) {
return true;
}
break;
case 'FILTER':
if ( attrs.helperElements && $scope.helperElements.toUpperCase().indexOf( 'FILTER' ) >= 0 ) {
return true;
}
break;
default:
break;
}$scope
}
// Call this function when a checkbox is ticked...
$scope.syncItems = function( item, e ) {
index = $scope.inputModel.indexOf( item );
$scope.inputModel[ index ][ $scope.tickProperty ] = !$scope.inputModel[ index ][ $scope.tickProperty ];
// If it's single selection mode
if ( attrs.selectionMode && $scope.selectionMode.toUpperCase() === 'SINGLE' ) {
$scope.inputModel[ index ][ $scope.tickProperty ] = true;
for( i=0; i<$scope.inputModel.length;i++) {
if ( i !== index ) {
$scope.inputModel[ i ][ $scope.tickProperty ] = false;
}
}
$scope.toggleCheckboxes( e );
}
$scope.refreshSelectedItems();
e.target.focus();
}
// Refresh the button to display the selected items and push into output model if specified
$scope.refreshSelectedItems = function() {
$scope.varButtonLabel = '';
$scope.selectedItems = [];
ctr = 0;
angular.forEach( $scope.inputModel, function( value, key ) {
if ( typeof value !== 'undefined' ) {
if ( value[ $scope.tickProperty ] === true || value[ $scope.tickProperty ] === 'true' ) {
$scope.selectedItems.push( value );
}
}
});
// Push into output model
if ( typeof attrs.outputModel !== 'undefined' ) {
$scope.outputModel = angular.copy( $scope.selectedItems );
}
// Write label...
if ( $scope.selectedItems.length === 0 ) {
$scope.varButtonLabel = 'None selected';
}
else {
var tempMaxLabels = $scope.selectedItems.length;
if ( typeof $scope.maxLabels !== 'undefined' && $scope.maxLabels !== '' ) {
tempMaxLabels = $scope.maxLabels;
}
// If max amount of labels displayed..
if ( $scope.selectedItems.length > tempMaxLabels ) {
$scope.more = true;
}
else {
$scope.more = false;
}
angular.forEach( $scope.selectedItems, function( value, key ) {
if ( typeof value !== 'undefined' ) {
if ( ctr < tempMaxLabels ) {
$scope.varButtonLabel += ( $scope.varButtonLabel.length > 0 ? ', ' : '') + $scope.writeLabel( value, 'buttonLabel' );
}
ctr++;
}
});
if ( $scope.more === true ) {
if (tempMaxLabels > 0) {
$scope.varButtonLabel += ', ... ';
}
$scope.varButtonLabel += '(Total: ' + $scope.selectedItems.length + ')';
}
}
$scope.varButtonLabel = $sce.trustAsHtml( $scope.varButtonLabel + '' );
}
// 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 ( item[ $scope.disableProperty ] === true ) {
return true;
}
else { $scope
if ( $scope.isDisabled === true ) {
return true;
}
else {
return false;
}
}
}
// A simple function to parse the item label settings
$scope.writeLabel = function( item, type ) {
var label = '';
var temp = $scope[ type ].split( ' ' );
angular.forEach( temp, function( value2, key2 ) {
if ( typeof value2 !== 'undefined' ) {
angular.forEach( item, function( value1, key1 ) {
if ( key1 == value2 ) {
label += ' ' + value1;
}
});
}
});
return $sce.trustAsHtml( label );
}
// UI operations to show/hide checkboxes based on click event..
$scope.toggleCheckboxes = function( e ) {
// Determine what element is clicked (has to be button).
if ( e.target ) {
if ( e.target.tagName.toUpperCase() !== 'BUTTON' && e.target.className.indexOf( 'multiSelectButton' ) < 0 ) {
if ( attrs.selectionMode && $scope.selectionMode.toUpperCase() === 'SINGLE' ) {
if ( e.target.tagName.toUpperCase() === 'INPUT' )
{
e = $scope.findUpTag( e.target, 'div', 'checkboxLayer' );
e = e.previousSibling;
}
}
else {
e = $scope.findUpTag( e.target, 'button', 'multiSelectButton' );
}
}
else {
e = e.target;
}
}
$scope.labelFilter = '';
// Search all the multi-select instances based on the class names
var multiSelectIndex = -1;
var checkboxes = document.querySelectorAll( '.checkboxLayer' );
var multiSelectButtons = document.querySelectorAll( '.multiSelectButton' );
// Mark which instance is clicked
for( i=0; i < multiSelectButtons.length; i++ ) {
if ( e === multiSelectButtons[ i ] ) {
multiSelectIndex = i;
break;
}
}
// Apply the hide css to all multi-select instances except the clicked one
if ( multiSelectIndex > -1 ) {
for( i=0; i < checkboxes.length; i++ ) {
if ( i != multiSelectIndex ) {
checkboxes[i].className = 'multiSelect checkboxLayer hide';
}
}
// If it's already hidden, show it
if ( checkboxes[ multiSelectIndex ].className == 'multiSelect checkboxLayer hide' ) {
$scope.currentButton = multiSelectButtons[ multiSelectIndex ];
checkboxes[ multiSelectIndex ].className = 'multiSelect checkboxLayer show';
// https://github.com/isteven/angular-multi-select/pull/5 - On open callback
$scope.onOpen();
}
// If it's already displayed, hide it
else if ( checkboxes[ multiSelectIndex ].className == 'multiSelect checkboxLayer show' ) {
checkboxes[ multiSelectIndex ].className = 'multiSelect checkboxLayer hide';
// https://github.com/isteven/angular-multi-select/pull/5 - On close callback
$scope.onClose();
}
}
}
// Traverse up to find the button tag
// http://stackoverflow.com/questions/7332179/how-to-recursively-search-all-parentnodes
$scope.findUpTag = function ( el, tag, className ) {
while ( el.parentNode ) {
el = el.parentNode;
if ( typeof el.tagName !== 'undefined' ) {
if ( el.tagName.toUpperCase() === tag.toUpperCase() && el.className.indexOf( className ) > -1 ) {
return el;
}
}
}
return null;
}
// Select All / None / Reset
$scope.select = function( type ) {
var temp = [];
switch( type.toUpperCase() ) {
case 'ALL':
angular.forEach( $scope.inputModel, function( value, key ) {
if ( typeof value !== 'undefined' && value[ $scope.disableProperty ] !== true ) {
value[ $scope.tickProperty ] = true;
}
});
break;
case 'NONE':
angular.forEach( $scope.inputModel, function( value, key ) {
if ( typeof value !== 'undefined' && value[ $scope.disableProperty ] !== true ) {
value[ $scope.tickProperty ] = false;
}
});
break;
case 'RESET':
$scope.inputModel = angular.copy( $scope.backUp );
break;
default:
}
$scope.refreshSelectedItems();
}
// Generic validation for required attributes
// Might give false positives so just ignore if everything's alright.
validate = function() {
if ( !( 'inputModel' in attrs )) {
console.log( 'Multi-select error: input-model is not defined! (ID: ' + $scope.directiveId + ')' );
}
if ( !( 'buttonLabel' in attrs )) {
console.log( 'Multi-select error: button-label is not defined! (ID: ' + $scope.directiveId + ')' );
}
if ( !( 'itemLabel' in attrs )) {
console.log( 'Multi-select error: item-label is not defined! (ID: ' + $scope.directiveId + ')' );
}
if ( !( 'tickProperty' in attrs )) {
console.log( 'Multi-select error: tick-property is not defined! (ID: ' + $scope.directiveId + ')' );
}
}
// Validate whether the properties specified in the directive attributes are present in the input model
validateProperties = function( arrProperties, arrObject ) {
var notThere = false;
var missingProperty = '';
angular.forEach( arrProperties, function( value1, key1 ) {
if ( typeof value1 !== 'undefined' ) {
var keepGoing = true;
angular.forEach( arrObject, function( value2, key2 ) {
if ( typeof value2 !== 'undefined' && keepGoing ) {
if (!( value1 in value2 )) {
notThere = true;
keepGoing = false;
missingLabel = value1;
}
}
});
}
});
if ( notThere === true ) {
console.log( 'Multi-select error: property "' + missingLabel + '" is not available in the input model. (Name: ' + $scope.directiveId + ')' );
}
}
///////////////////////
// Logic starts here
///////////////////////
validate();
$scope.refreshSelectedItems();
// Watch for changes in input model
// Updates multi-select when user select/deselect a single checkbox programatically
// https://github.com/isteven/angular-multi-select/issues/8
$scope.$watch( 'inputModel' , function( oldVal, newVal ) {
if ( $scope.newVal !== 'undefined' ) {
validateProperties( $scope.itemLabel.split( ' ' ), $scope.inputModel );
validateProperties( new Array( $scope.tickProperty ), $scope.inputModel );
}
$scope.refreshSelectedItems();
}, true);
// Watch for changes in input model
// This on updates the multi-select when a user load a whole new input-model. We also update the $scope.backUp variable
$scope.$watch( 'inputModel' , function( oldVal, newVal ) {
if ( $scope.newVal !== 'undefined' ) {
validateProperties( $scope.itemLabel.split( ' ' ), $scope.inputModel );
validateProperties( new Array( $scope.tickProperty ), $scope.inputModel );
}
$scope.backUp = angular.copy( $scope.inputModel );
$scope.refreshSelectedItems();
});
// 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 ).bind( 'touchstart', function( e ) {
$scope.$apply( function() {
$scope.scrolled = false;
});
});
angular.element( document ).bind( 'touchmove', function( e ) {
$scope.$apply( function() {
$scope.scrolled = true;
});
});
// Monitor for click or touches outside the button element to hide the checkboxes
angular.element( document ).bind( 'click touchend' , function( e ) {
if ( e.type === 'click' || e.type === 'touchend' && $scope.scrolled === false ) {
var checkboxes = document.querySelectorAll( '.checkboxLayer' );
if ( e.target.className.indexOf === undefined || e.target.className.indexOf( 'multiSelect' )) {
for( i=0; i < checkboxes.length; i++ ) {
checkboxes[i].className = 'multiSelect checkboxLayer hide';
}
e.stopPropagation();
}
}
});
// For IE8, perhaps. Not sure if this is really executed.
if ( !Array.prototype.indexOf ) {
Array.prototype.indexOf = function(what, i) {
i = i || 0;
var L = this.length;
while (i < L) {
if(this[i] === what) return i;
++i;
}
return -1;
};
}
}
}
}]);