diff options
Diffstat (limited to 'angular-multi-select.js')
-rw-r--r-- | angular-multi-select.js | 846 |
1 files changed, 630 insertions, 216 deletions
diff --git a/angular-multi-select.js b/angular-multi-select.js index 33d5c60..003c3ce 100644 --- a/angular-multi-select.js +++ b/angular-multi-select.js @@ -3,7 +3,7 @@ * Creates a dropdown-like button with checkboxes. * * Created: Tue, 14 Jan 2014 - 5:18:02 PM - * + * * Released under the MIT License * * -------------------------------------------------------------------------------- @@ -31,7 +31,7 @@ * -------------------------------------------------------------------------------- */ -angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', '$filter', function ( $sce, $filter ) { +angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', '$timeout', function ( $sce, $timeout ) { return { restrict: 'AE', @@ -40,61 +40,171 @@ angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', '$ true, scope: - { + { + // models inputModel : '=', outputModel : '=', + + // settings based on attribute buttonLabel : '@', - selectionMode : '@', + defaultLabel : '@', + directiveId : '@', + helperElements : '@', + isDisabled : '=', itemLabel : '@', + maxLabels : '@', + orientation : '@', + selectionMode : '@', + + // settings based on input model property tickProperty : '@', disableProperty : '@', - orientation : '@', - maxLabels : '@', - isDisabled : '=', - directiveId : '@', - helperElements : '@', - onOpen : '&', - onClose : '&', - onBlur : '&', - onFocus : '&', + groupProperty : '@', + + // callbacks + onClose : '&', + onItemClick : '&', + onOpen : '&' }, template: - '<span class="multiSelect inlineBlock" >' + - '<button type="button" class="multiSelect button multiSelectButton" ng-click="toggleCheckboxes( $event ); refreshSelectedItems();" ng-bind-html="varButtonLabel" ng-focus="onFocus()" ng-blur="onBlur()">' + - '</button>' + - '<div class="multiSelect checkboxLayer hide">' + - '<div class="multiSelect line" ng-show="displayHelper( \'all\' ) || displayHelper( \'none\' ) || displayHelper( \'reset\' )">' + - '<span ng-if="!isDisabled && ( displayHelper( \'all\' ) || displayHelper( \'none\' ) || displayHelper( \'reset\' ))">Select: </span>' + - '<button type="button" ng-click="select( \'all\' )" class="multiSelect helperButton" ng-if="!isDisabled && displayHelper( \'all\' )">All</button> ' + - '<button type="button" ng-click="select( \'none\' )" class="multiSelect helperButton" ng-if="!isDisabled && displayHelper( \'none\' )">None</button> ' + - '<button type="button" ng-click="select( \'reset\' )" class="multiSelect helperButton" ng-if="!isDisabled && displayHelper( \'reset\' )">Reset</button>' + - '</div>' + - '<div class="multiSelect line" ng-show="displayHelper( \'filter\' )">' + - 'Filter: <input class="multiSelect" type="text" ng-model="labelFilter" />' + - ' <button type="button" class="multiSelect helperButton" ng-click="labelFilter=\'\'">Clear</button>' + - '</div>' + - '<div ng-repeat="item in (filteredModel = (inputModel | filter:labelFilter ))" ng-class="orientation" class="multiSelect multiSelectItem">' + - '<div class="multiSelect acol">' + - '<div class="multiSelect" ng-show="item[ tickProperty ]">✔</div>' + + '<span class="multiSelect inlineBlock">' + + '<button type="button" class="button multiSelectButton" ng-click="toggleCheckboxes( $event ); refreshSelectedItems(); refreshButton();" ng-bind-html="varButtonLabel">' + + '</button>' + + '<div class="checkboxLayer">' + + '<form>' + + '<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" ng-click="select( \'all\', $event );" class="helperButton" ng-if="!isDisabled && displayHelper( \'all\' )"> ✓ Select All</button> ' + + '<button type="button" ng-click="select( \'none\', $event );" class="helperButton" ng-if="!isDisabled && displayHelper( \'none\' )"> ⨯ Select None</button> ' + + '<button type="button" ng-click="select( \'reset\', $event );" class="helperButton" ng-if="!isDisabled && displayHelper( \'reset\' )" style="float:right">↶ Reset</button>' + + '</div>' + + '<div class="line" style="position:relative" ng-if="displayHelper( \'filter\' )">' + + '<input placeholder="Search..." type="text" ng-click="select( \'filter\', $event )" ng-model="inputLabel.labelFilter" ng-change="updateFilter();$scope.getFormElements();" class="inputFilter" />' + + '<button type="button" class="clearButton" ng-click="inputLabel.labelFilter=\'\';updateFilter();prepareGrouping();prepareIndex();select( \'clear\', $event )">⨯</button> ' + + '</div>' + '</div>' + - '<div class="multiSelect acol">' + - '<label class="multiSelect" ng-class="{checkboxSelected:item[ tickProperty ]}">' + - '<input class="multiSelect checkbox" type="checkbox" ng-disabled="itemIsDisabled( item )" ng-checked="item[ tickProperty ]" ng-click="syncItems( item, $event )"/>' + - '<span class="multiSelect" ng-class="{disabled:itemIsDisabled( item )}" ng-bind-html="writeLabel( item, \'itemLabel\' )"></span>' + - '</label> ' + + '<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>' + + '<form>' + '</div>' + '</span>', link: function ( $scope, element, attrs ) { - - $scope.selectedItems = []; + $scope.backUp = []; $scope.varButtonLabel = ''; - $scope.currentButton = null; $scope.scrolled = false; + $scope.spacingProperty = ''; + $scope.indexProperty = ''; + $scope.checkBoxLayer = ''; + $scope.orientationH = false; + $scope.orientationV = true; + $scope.filteredModel = []; + $scope.inputLabel = { labelFilter: '' }; + $scope.selectedItems = []; + $scope.formElements = []; + $scope.tabIndex = 0; + prevTabIndex = 0; + helperItems = []; + helperItemsLength = 0; + + // 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 ); + } + + $scope.updateFilter = function() + { + // we check by looping from end of array + $scope.filteredModel = []; + var i = 0; + + if ( typeof $scope.inputModel === 'undefined' ) { + return []; + } + + for( i = $scope.inputModel.length - 1; i >= 0; i-- ) { + + // if it's group end + if ( typeof $scope.inputModel[ i ][ $scope.groupProperty ] !== 'undefined' && $scope.inputModel[ i ][ $scope.groupProperty ] === false ) { + $scope.filteredModel.push( $scope.inputModel[ i ] ); + } + + // if it's data + var gotData = false; + if ( typeof $scope.inputModel[ i ][ $scope.groupProperty ] === 'undefined' ) { + + for (var key in $scope.inputModel[ i ] ) { + // if filter string is in one of object property + 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 ][ $scope.groupProperty ] !== 'undefined' && $scope.inputModel[ 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.inputModel[ i ] ); + } + } + } + + $scope.filteredModel.reverse(); + $timeout( function() { + $scope.getFormElements(); + },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() { + $scope.formElements = []; + for ( var i = 0; i < element[ 0 ].getElementsByTagName( 'FORM' )[ 0 ].elements.length ; i++ ) { + $scope.formElements.push( element[ 0 ].getElementsByTagName( 'FORM' )[ 0 ].elements[ 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 ) { @@ -134,60 +244,211 @@ angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', '$ 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; + // call this function when an item is clicked + $scope.syncItems = function( item, e, ng_repeat_index ) { + + e.preventDefault(); + e.stopPropagation(); + + // if it's globaly disabled, then don't do anything + if ( typeof attrs.disableProperty !== 'undefined' && item[ $scope.disableProperty ] === true ) { + return false; + } + + // don't change disabled items + if ( typeof attrs.isDisabled !== 'undefined' && $scope.isDisabled === true ) { + return false; + } + + // we don't care about end of group markers + if ( typeof item[ $scope.groupProperty ] !== 'undefined' && item[ $scope.groupProperty ] === false ) { + return false; + } + + index = $scope.filteredModel.indexOf( item ); + + // process items if the start of group marker is clicked ( only for multiple selection! ) + // 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 ) { + + if ( attrs.selectionMode && $scope.selectionMode.toUpperCase() === 'SINGLE' ) { + return false; + } + + var i,j,k; + var startIndex = 0; + var endIndex = $scope.filteredModel.length - 1; + var tempArr = []; + var nestLevel = 0; + + for( i = index ; i < $scope.filteredModel.length ; i++) { + + if ( nestLevel === 0 && i > index ) + { + break; } - } - $scope.toggleCheckboxes( e ); + + // if group start + 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.inputModel[ 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.inputModel[ 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.inputModel[ 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.inputModel[ inputModelIndex ][ $scope.tickProperty ] = true; + } + } + } + } + } + } + + // if data + else { + tempArr.push( $scope.filteredModel[ i ] ); + } + } } + + // single item click + else { + $scope.filteredModel[ index ][ $scope.tickProperty ] = !$scope.filteredModel[ index ][ $scope.tickProperty ]; + + // we refresh input model as well + inputModelIndex = $scope.filteredModel[ index ][ $scope.indexProperty ]; + $scope.inputModel[ inputModelIndex ][ $scope.tickProperty ] = $scope.filteredModel[ index ][ $scope.tickProperty ]; + + + // If it's single selection mode + if ( attrs.selectionMode && $scope.selectionMode.toUpperCase() === 'SINGLE' ) { + $scope.filteredModel[ index ][ $scope.tickProperty ] = true; + for( i=0 ; i < $scope.filteredModel.length ; i++) { + if ( i !== index ) { + $scope.filteredModel[ i ][ $scope.tickProperty ] = false; + } + } + $scope.toggleCheckboxes( e ); + } + } + + $scope.onItemClick( { data: item } ); + + // We update the index here + prevTabIndex = $scope.tabIndex; + $scope.tabIndex = ng_repeat_index + helperItemsLength; - $scope.refreshSelectedItems(); + // Set focus on the hidden checkbox e.target.focus(); - } - // Refresh the button to display the selected items and push into output model if specified - $scope.refreshSelectedItems = function() { + // set & remove CSS style + $scope.removeFocusStyle( prevTabIndex ); + $scope.setFocusStyle( $scope.tabIndex ); + } - $scope.varButtonLabel = ''; + // update $scope.selectedItems + // this variable is used in $scope.outputModel and to refresh the button label + $scope.refreshSelectedItems = function() { $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 ); + if ( typeof value !== 'undefined' ) { + if ( typeof value[ $scope.groupProperty ] === 'undefined' ) { + if ( value[ $scope.tickProperty ] === true ) { + $scope.selectedItems.push( value ); + } } } - }); - - // Push into output model + }); + } + + // refresh output model as well + $scope.refreshOutputModel = function() { if ( typeof attrs.outputModel !== 'undefined' ) { $scope.outputModel = angular.copy( $scope.selectedItems ); - } + angular.forEach( $scope.outputModel, function( value, key ) { + // remove the index number and spacing number from output model + delete value[ $scope.indexProperty ]; + delete value[ $scope.spacingProperty ]; + }) + } + } + + // refresh button label + $scope.refreshButton = function() { - // Write label... + $scope.varButtonLabel = ''; + ctr = 0; + + // refresh button label... if ( $scope.selectedItems.length === 0 ) { - $scope.varButtonLabel = 'None selected'; + // https://github.com/isteven/angular-multi-select/pull/19 + $scope.varButtonLabel = ( typeof $scope.defaultLabel !== 'undefined' ) ? $scope.defaultLabel : 'None selected'; } else { var tempMaxLabels = $scope.selectedItems.length; - if ( typeof $scope.maxLabels !== 'undefined' && $scope.maxLabels !== '' && $scope.maxLabels !== "0" ) { + if ( typeof $scope.maxLabels !== 'undefined' && $scope.maxLabels !== '' ) { tempMaxLabels = $scope.maxLabels; } - // If max amount of labels displayed.. + // if max amount of labels displayed.. if ( $scope.selectedItems.length > tempMaxLabels ) { $scope.more = true; } @@ -198,28 +459,32 @@ angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', '$ angular.forEach( $scope.selectedItems, function( value, key ) { if ( typeof value !== 'undefined' ) { if ( ctr < tempMaxLabels ) { - $scope.varButtonLabel += ( $scope.varButtonLabel.length > 0 ? ', ' : '') + $scope.writeLabel( value, 'buttonLabel' ); + $scope.varButtonLabel += ( $scope.varButtonLabel.length > 0 ? '</div>, <div class="buttonLabel">' : '<div class="buttonLabel">') + $scope.writeLabel( value, 'buttonLabel' ); } ctr++; } }); if ( $scope.more === true ) { - $scope.varButtonLabel += ', ... (Total: ' + $scope.selectedItems.length + ')'; - }$scope + // https://github.com/isteven/angular-multi-select/pull/16 + if (tempMaxLabels > 0) { + $scope.varButtonLabel += ', ... '; + } + $scope.varButtonLabel += '(Total: ' + $scope.selectedItems.length + ')'; + } } - $scope.varButtonLabel = $sce.trustAsHtml( $scope.varButtonLabel + '<span class="multiSelect caret"></span>' ); + $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 ( item[ $scope.disableProperty ] === true ) { + if ( typeof attrs.disableProperty !== 'undefined' && item[ $scope.disableProperty ] === true ) { return true; } - else { $scope - if ( $scope.isDisabled === true ) { + else { + if ( $scope.isDisabled === true ) { return true; } else { @@ -237,26 +502,50 @@ angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', '$ if ( typeof value2 !== 'undefined' ) { angular.forEach( item, function( value1, key1 ) { if ( key1 == value2 ) { - label += ' ' + value1; + label += ' ' + value1; } }); } }); + if ( type.toUpperCase() === 'BUTTONLABEL' ) { + return label; + } return $sce.trustAsHtml( label ); } // UI operations to show/hide checkboxes based on click event.. - $scope.toggleCheckboxes = function( e ) { + $scope.toggleCheckboxes = function( e ) { + + // Just to make sure.. had a bug where key events were recorded twice + angular.element( document ).unbind( 'click', $scope.externalClickListener ); + angular.element( window ).unbind( 'keypress', $scope.keyboardListener ); + + // clear filter + $scope.inputLabel.labelFilter = ''; + $scope.updateFilter(); + + // close if ESC key is pressed. + if ( e.keyCode === 27 ) { + angular.element( $scope.checkBoxLayer ).removeClass( 'show' ); + angular.element( clickedEl ).removeClass( 'buttonClicked' ); + angular.element( document ).unbind( 'click', $scope.externalClickListener ); + angular.element( window ).unbind( 'keypress', $scope.keyboardListener ); + + // clear the focused element; + $scope.removeFocusStyle( $scope.tabIndex ); - // Determine what element is clicked (has to be button). + // close callback + $scope.onClose( { data: element } ); + return true; + } + + // Our button accept HTML tags. Depending on browser, the click event might be intercepted by those tags instead of the button. + // Since we want the button to handle click event, we need traverse up to find the button element. 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; - } + e = $scope.findUpTag( e.target, 'div', 'checkboxLayer' ); + e = e.previousSibling; } else { e = $scope.findUpTag( e.target, 'button', 'multiSelectButton' ); @@ -265,52 +554,86 @@ angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', '$ 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'; + } + clickedEl = e; + $scope.checkBoxLayer = clickedEl.nextSibling; + + // 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( $scope.checkBoxLayer ).hasClass( 'show' )) { + angular.element( $scope.checkBoxLayer ).removeClass( 'show' ); + angular.element( clickedEl ).removeClass( 'buttonClicked' ); + angular.element( document ).unbind( 'click', $scope.externalClickListener ); + angular.element( window ).unbind( 'keypress', $scope.keyboardListener ); + + // clear the focused element; + $scope.removeFocusStyle( $scope.tabIndex ); + + // close callback + $scope.onClose( { data: element } ); + } + // open + else + { + helperItems = []; + helperItemsLength = 0; + + angular.element( $scope.checkBoxLayer ).addClass( 'show' ); + angular.element( clickedEl ).addClass( 'buttonClicked' ); + angular.element( document ).bind( 'click', $scope.externalClickListener ); + angular.element( window ).bind( 'keypress', $scope.keyboardListener ); + + // open callback + $scope.onOpen( { data: element } ); + + // 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 ( i = 0; i < helperContainer.getElementsByTagName( 'BUTTON' ).length ; i++ ) { + helperItems[ i ] = helperContainer.getElementsByTagName( 'BUTTON' )[ i ]; } - } - - // 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(); + helperItemsLength = helperItems.length + helperContainer.getElementsByTagName( 'INPUT' ).length; } - - // 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(); + + // 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 { + $scope.formElements[ $scope.tabIndex ].focus(); + } + } } + + // handle clicks outside the button / multi select layer + $scope.externalClickListener = function( e ) { + targetsArr = element.find( e.target.tagName ); + for (var i = 0; i < targetsArr.length; i++) { + if ( e.target == targetsArr[i] ) { + return; + } + } - // Traverse up to find the button tag + angular.element( $scope.checkBoxLayer.previousSibling ).removeClass( 'buttonClicked' ); + angular.element( $scope.checkBoxLayer ).removeClass( 'show' ); + angular.element( document ).unbind( 'click', $scope.externalClickListener ); + angular.element( document ).unbind( 'click', $scope.keyboardListener ); + // close callback + $scope.onClose( { data: element } ); + } + + // 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' ) { @@ -322,138 +645,229 @@ angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', '$ return null; } - // Select All / None / Reset - $scope.select = function( type ) { - var temp = []; + // select All / select None / reset buttons + $scope.select = function( type, e ) { + + helperIndex = helperItems.indexOf( e.target ); + $scope.tabIndex = helperIndex; + switch( type.toUpperCase() ) { case 'ALL': - angular.forEach( $scope.inputModel, function( value, key ) { - if ( typeof value !== 'undefined' && value[ $scope.disableProperty ] !== true ) { - value[ $scope.tickProperty ] = true; + 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; + } } - }); + }); break; case 'NONE': - angular.forEach( $scope.inputModel, function( value, key ) { + angular.forEach( $scope.filteredModel, function( value, key ) { if ( typeof value !== 'undefined' && value[ $scope.disableProperty ] !== true ) { - value[ $scope.tickProperty ] = false; + if ( typeof value[ $scope.groupProperty ] === 'undefined' ) { + value[ $scope.tickProperty ] = false; + } + } + }); + break; + case 'RESET': + angular.forEach( $scope.filteredModel, function( value, key ) { + if ( typeof value[ $scope.groupProperty ] === 'undefined' && typeof value !== 'undefined' && value[ $scope.disableProperty ] !== true ) { + temp = value[ $scope.indexProperty ]; + value[ $scope.tickProperty ] = $scope.backUp[ temp ][ $scope.tickProperty ]; } - }); - break; - case 'RESET': - $scope.inputModel = angular.copy( $scope.backUp ); + }); + break; + case 'CLEAR': + $scope.tabIndex = $scope.tabIndex + 1; + break; + case 'FILTER': + $scope.tabIndex = helperItems.length - 1; 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 + ')' ); + // just to create a random variable name + genRandomString = function( length ) { + var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + var temp = ''; + for( var i=0; i < length; i++ ) { + temp += possible.charAt( Math.floor( Math.random() * possible.length )); } + return temp; + } - 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 + ')' ); - } + // 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; + } + }); + } - if ( !( 'tickProperty' in attrs )) { - console.log( 'Multi-select error: tick-property is not defined! (ID: ' + $scope.directiveId + ')' ); - } + // prepare original index + $scope.prepareIndex = function() { + ctr = 0; + angular.forEach( $scope.filteredModel, function( value, key ) { + value[ $scope.indexProperty ] = ctr; + ctr++; + }); } - // 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; - } - } - }); + // 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 ) { + $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 > $scope.formElements.length - 1 ) { + $scope.tabIndex = 0; + prevTabIndex = $scope.formElements.length - 1; + } + while ( $scope.formElements[ $scope.tabIndex ].disabled === true ) { + $scope.tabIndex++; + if ( $scope.tabIndex > $scope.formElements.length - 1 ) { + $scope.tabIndex = 0; + } } - }); - if ( notThere === true ) { - console.log( 'Multi-select error: property "' + missingLabel + '" is not available in the input model. (Name: ' + $scope.directiveId + ')' ); - } - } + } + + // 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 = $scope.formElements.length - 1; + prevTabIndex = 0; + } + while ( $scope.formElements[ $scope.tabIndex ].disabled === true ) { + $scope.tabIndex--; + if ( $scope.tabIndex < 0 ) { + $scope.tabIndex = $scope.formElements.length - 1; + } + } + } + + if ( isNavigationKey === true ) { + + e.preventDefault(); + e.stopPropagation(); + + // set focus on the checkbox + $scope.formElements[ $scope.tabIndex ].focus(); + + // css styling + 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( $scope.formElements.length - 1 ); + } + } - /////////////////////// - // Logic starts here - /////////////////////// + isNavigationKey = false; + } + + // set (add) CSS style on selected row + $scope.setFocusStyle = function( tabIndex ) { + angular.element( $scope.formElements[ tabIndex ] ).parent().parent().parent().addClass( 'multiSelectFocus' ); + } + + // remove CSS style on selected row + $scope.removeFocusStyle = function( tabIndex ) { + angular.element( $scope.formElements[ tabIndex ] ).parent().parent().parent().removeClass( 'multiSelectFocus' ); + } - validate(); - $scope.refreshSelectedItems(); + /////////////////////////////////////////////////////// + // + // Logic starts here, initiated by watch 1 & watch 2. + // + /////////////////////////////////////////////////////// + + 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; + } + } - // Watch for changes in input model - // Updates multi-select when user select/deselect a single checkbox programatically + // 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( oldVal, newVal ) { - if ( $scope.newVal !== 'undefined' ) { - validateProperties( $scope.itemLabel.split( ' ' ), $scope.inputModel ); - validateProperties( new Array( $scope.tickProperty ), $scope.inputModel ); - } - $scope.refreshSelectedItems(); + $scope.$watch( 'inputModel' , function( newVal ) { + if ( newVal ) { + $scope.refreshSelectedItems(); + $scope.refreshOutputModel(); + $scope.refreshButton(); + } }, 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 ); + // 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( 'inputModel' , function( newVal ) { + if ( newVal ) { + $scope.backUp = angular.copy( $scope.inputModel ); + $scope.updateFilter(); + $scope.prepareGrouping(); + $scope.prepareIndex(); + $scope.refreshSelectedItems(); + $scope.refreshOutputModel(); + $scope.refreshButton(); } - $scope.backUp = angular.copy( $scope.inputModel ); - $scope.refreshSelectedItems(); }); - // Watch for changes in directive state (disabled or enabled) + // 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. + // 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; }); }); - + + // also for touch enabled devices 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. + // for IE8, perhaps. Not sure if this is really executed. if ( !Array.prototype.indexOf ) { Array.prototype.indexOf = function(what, i) { i = i || 0; |