summaryrefslogtreecommitdiffstats
path: root/angular-multi-select.js
diff options
context:
space:
mode:
Diffstat (limited to 'angular-multi-select.js')
-rw-r--r--angular-multi-select.js846
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: &nbsp;</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" />' +
- '&nbsp;<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 ]">&#10004;</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\' )"> &#10003;&nbsp; Select All</button> ' +
+ '<button type="button" ng-click="select( \'none\', $event );" class="helperButton" ng-if="!isDisabled && displayHelper( \'none\' )"> &#10799;&nbsp; Select None</button>&nbsp;' +
+ '<button type="button" ng-click="select( \'reset\', $event );" class="helperButton" ng-if="!isDisabled && displayHelper( \'reset\' )" style="float:right">&#8630;&nbsp; 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 )">&#10799;</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>&nbsp;&nbsp;' +
+ '<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">&nbsp;</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>' +
+ '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;' +
+
+ '<span class="tickMark" ng-if="item[ groupProperty ] !== true && item[ tickProperty ] === true">&#10004;</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 += '&nbsp;' + 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;