/* * Angular JS Multi Select * Creates a dropdown-like button with checkboxes. * * Project started on: Tue, 14 Jan 2014 - 5:18:02 PM * Current version: 4.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 isDisabled : '=', // callbacks onClear : '&', onClose : '&', onSearchChange : '&', onItemClick : '&', onOpen : '&', onReset : '&', onSelectAll : '&', onSelectNone : '&', // i18n translation : '=' }, /* * The rest are attributes. They don't need to be parsed / binded, so we can safely access them by value. * - buttonLabel, directiveId, helperElements, itemLabel, maxLabels, orientation, selectionMode, minSearchLength, * tickProperty, disableProperty, groupProperty, searchProperty, maxHeight, outputProperties */ templateUrl: 'isteven-multi-select.htm', 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.helperStatus = { all : true, none : true, reset : true, filter : true }; 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.inputModel === 'undefined' ) { return false; } for( i = $scope.inputModel.length - 1; i >= 0; i-- ) { // if it's group end, we push it to filteredModel[]; if ( typeof $scope.inputModel[ i ][ attrs.groupProperty ] !== 'undefined' && $scope.inputModel[ i ][ attrs.groupProperty ] === false ) { $scope.filteredModel.push( $scope.inputModel[ i ] ); } // if it's data var gotData = false; if ( typeof $scope.inputModel[ i ][ attrs.groupProperty ] === 'undefined' ) { // If we set the search-key attribute, we use this loop. if ( typeof attrs.searchProperty !== 'undefined' && attrs.searchProperty !== '' ) { for (var key in $scope.inputModel[ i ] ) { if ( typeof $scope.inputModel[ i ][ key ] !== 'boolean' && String( $scope.inputModel[ i ][ key ] ).toUpperCase().indexOf( $scope.inputLabel.labelFilter.toUpperCase() ) >= 0 && attrs.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.inputModel[ i ] ) { if ( typeof $scope.inputModel[ i ][ key ] !== 'boolean' && String( $scope.inputModel[ i ][ key ] ).toUpperCase().indexOf( $scope.inputLabel.labelFilter.toUpperCase() ) >= 0 ) { gotData = true; break; } } } if ( gotData === true ) { // push $scope.filteredModel.push( $scope.inputModel[ i ] ); } } // if it's group start if ( typeof $scope.inputModel[ i ][ attrs.groupProperty ] !== 'undefined' && $scope.inputModel[ i ][ attrs.groupProperty ] === true ) { if ( typeof $scope.filteredModel[ $scope.filteredModel.length - 1 ][ attrs.groupProperty ] !== 'undefined' && $scope.filteredModel[ $scope.filteredModel.length - 1 ][ attrs.groupProperty ] === false ) { $scope.filteredModel.pop(); } else { $scope.filteredModel.push( $scope.inputModel[ 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[ attrs.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. We need this for our keyboard navigation. // This function will be called everytime the filter is updated. // Depending on the size of filtered mode, might not good for performance, but oh well.. $scope.getFormElements = function() { formElements = []; var selectButtons = [], inputField = [], checkboxes = [], clearButton = []; // If available, then get select all, select none, and reset buttons if ( $scope.helperStatus.all || $scope.helperStatus.none || $scope.helperStatus.reset ) { selectButtons = element.children().children().next().children().children()[ 0 ].getElementsByTagName( 'button' ); // If available, then get the search box and the clear button if ( $scope.helperStatus.filter ) { // Get helper - search and clear button. inputField = element.children().children().next().children().children().next()[ 0 ].getElementsByTagName( 'input' ); clearButton = element.children().children().next().children().children().next()[ 0 ].getElementsByTagName( 'button' ); } } else { if ( $scope.helperStatus.filter ) { // Get helper - search and clear button. inputField = element.children().children().next().children().children()[ 0 ].getElementsByTagName( 'input' ); clearButton = element.children().children().next().children().children()[ 0 ].getElementsByTagName( 'button' ); } } // Get checkboxes if ( !$scope.helperStatus.all && !$scope.helperStatus.none && !$scope.helperStatus.reset && !$scope.helperStatus.filter ) { checkboxes = element.children().children().next()[ 0 ].getElementsByTagName( 'input' ); } else { 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 attrs.groupProperty (be it true or false) $scope.isGroupMarker = function( item , type ) { if ( typeof item[ attrs.groupProperty ] !== 'undefined' && item[ attrs.groupProperty ] === type ) return true; return false; } $scope.removeGroupEndMarker = function( item ) { if ( typeof item[ attrs.groupProperty ] !== 'undefined' && item[ attrs.groupProperty ] === false ) return false; return true; } // 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[ attrs.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[ attrs.groupProperty ] !== 'undefined' && item[ attrs.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[ attrs.groupProperty ] !== 'undefined' && item[ attrs.groupProperty ] === true ) { // this is only for multiple selection, so if selection mode is single, do nothing if ( typeof attrs.selectionMode !== 'undefined' && attrs.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 ][ attrs.groupProperty ] !== 'undefined' && $scope.filteredModel[ i ][ attrs.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 ][ attrs.groupProperty ] !== 'undefined' && $scope.filteredModel[ i ][ attrs.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 ][ attrs.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.inputModel[ inputModelIndex ][ $scope.tickProperty ] = false; } else if ( $scope.filteredModel[ j ][ attrs.disableProperty ] !== true ) { $scope.filteredModel[ j ][ $scope.tickProperty ] = false; // we refresh input model as well inputModelIndex = $scope.filteredModel[ j ][ $scope.indexProperty ]; $scope.inputModel[ inputModelIndex ][ $scope.tickProperty ] = false; } } } } else { for ( j = startIndex; j <= endIndex ; j++ ) { if ( typeof $scope.filteredModel[ j ][ attrs.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.inputModel[ inputModelIndex ][ $scope.tickProperty ] = true; } else if ( $scope.filteredModel[ j ][ attrs.disableProperty ] !== true ) { $scope.filteredModel[ j ][ $scope.tickProperty ] = true; // we refresh input model as well inputModelIndex = $scope.filteredModel[ j ][ $scope.indexProperty ]; $scope.inputModel[ 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 ( typeof attrs.selectionMode !== 'undefined' && attrs.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.inputModel.length ; i++) { $scope.inputModel[ i ][ $scope.tickProperty ] = false; } // then set the clicked item to true $scope.filteredModel[ index ][ $scope.tickProperty ] = true; } // 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.inputModel[ 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 ); if ( typeof attrs.selectionMode !== 'undefined' && attrs.selectionMode.toUpperCase() === 'SINGLE' ) { // on single selection mode, we then hide the checkbox layer $scope.toggleCheckboxes( e ); } } // update $scope.outputModel $scope.refreshOutputModel = function() { $scope.outputModel = []; var outputProps = [], tempObj = {}; // v4.0.0 if ( typeof attrs.outputProperties !== 'undefined' ) { outputProps = attrs.outputProperties.split(' '); angular.forEach( $scope.inputModel, function( value, key ) { if ( typeof value !== 'undefined' && typeof value[ attrs.groupProperty ] === 'undefined' && value[ $scope.tickProperty ] === true ) { tempObj = {}; angular.forEach( value, function( value1, key1 ) { if ( outputProps.indexOf( key1 ) > -1 ) { tempObj[ key1 ] = value1; } }); var index = $scope.outputModel.push( tempObj ); delete $scope.outputModel[ index - 1 ][ $scope.indexProperty ]; delete $scope.outputModel[ index - 1 ][ $scope.spacingProperty ]; } }); } else { angular.forEach( $scope.inputModel, function( value, key ) { if ( typeof value !== 'undefined' && typeof value[ attrs.groupProperty ] === 'undefined' && value[ $scope.tickProperty ] === true ) { var temp = angular.copy( value ); var index = $scope.outputModel.push( temp ); delete $scope.outputModel[ index - 1 ][ $scope.indexProperty ]; delete $scope.outputModel[ index - 1 ][ $scope.spacingProperty ]; } }); } } // refresh button label $scope.refreshButton = function() { $scope.varButtonLabel = ''; var ctr = 0; // refresh button label... if ( $scope.outputModel.length === 0 ) { // https://github.com/isteven/angular-multi-select/pull/19 $scope.varButtonLabel = $scope.lang.nothingSelected; } else { var tempMaxLabels = $scope.outputModel.length; if ( typeof attrs.maxLabels !== 'undefined' && attrs.maxLabels !== '' ) { tempMaxLabels = attrs.maxLabels; } // if max amount of labels displayed.. if ( $scope.outputModel.length > tempMaxLabels ) { $scope.more = true; } else { $scope.more = false; } angular.forEach( $scope.inputModel, function( value, key ) { if ( typeof value !== 'undefined' && value[ attrs.tickProperty ] === true ) { if ( ctr < tempMaxLabels ) { $scope.varButtonLabel += ( $scope.varButtonLabel.length > 0 ? ',