summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoristeven <isteven@server.fake>2014-07-07 11:48:00 +0800
committeristeven <isteven@server.fake>2014-07-07 11:48:00 +0800
commit3bd2d53705490549f41b903c5ab30d0065b7d929 (patch)
treedf26a172766dffaf02ee86cdf04b867dfe68cf99
parent3550dfe977a924183643e7a03b401096ae34f26d (diff)
downloadangular-multi-select-3bd2d53705490549f41b903c5ab30d0065b7d929.zip
angular-multi-select-3bd2d53705490549f41b903c5ab30d0065b7d929.tar.gz
angular-multi-select-3bd2d53705490549f41b903c5ab30d0065b7d929.tar.bz2
v2.0.0
-rw-r--r--CHANGELOG.md25
-rw-r--r--LICENSE.txt (renamed from LICENSE)0
-rw-r--r--README.md10
-rw-r--r--angular-multi-select.css316
-rw-r--r--angular-multi-select.js846
-rw-r--r--angular-multi-select.min.js6
-rw-r--r--bower.json7
-rw-r--r--screenshot.pngbin22237 -> 33984 bytes
8 files changed, 881 insertions, 329 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..1efac78
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,25 @@
+### v2.0.0
+Added / Updated
+- Unlimited nested grouping. Group headers are clickable to select / deselect all items under the group. Group headers are filter aware, means it will only affect filtered result.
+- Helper buttons are now filter aware as well (For example, if you filter something and click 'Select All', the directive will tick all of the filtered result only. Same goes with 'Select None' and 'Reset' )
+- Supports arrow key navigation (up, down, left, right, and spacebar).
+- New CSS styling
+- default-label attribute. You can define your default text on the button when nothing is selected.
+- on-item-click attribute. This is a callback which will be triggered when a user click an item.
+Removed / Deprecated:
+- on-focus attribute is deprecated.
+- on-blur attribute is deprecated. Use on-close instead, as it will be triggered when user close a directive by clicking outside the directive.
+- IE8 will no longer be supported.
+
+### v1.2.0
+Added / Updated:
+- Default label on the dropdown button is now configurable using attribute "default-label"="..." (Issue #19)
+- Attribute "max-labels" can now be 0. If set to 0, the dropdown button will only display "(Total: X)" (Issue #16)
+
+### v1.1.0
+Added / Updated:
+- Added event callbacks
+- Helper elements are now configurable (Issue #5)
+
+### v1.0.0
+First release
diff --git a/LICENSE b/LICENSE.txt
index 8439bd9..8439bd9 100644
--- a/LICENSE
+++ b/LICENSE.txt
diff --git a/README.md b/README.md
index 3d6ccfe..0caaefb 100644
--- a/README.md
+++ b/README.md
@@ -10,11 +10,15 @@ Demo & How To
--
Go to http://isteven.github.io/angular-multi-select
+Changes
+--
+See changelog.md
+
Note
--
-I'm a Github newbie so pardon me if you experience weird commit / forking sequence =)
-<br />I'm all ears for new development ideas. Just open an issue in <a href="https://github.com/isteven/angular-multi-select">https://github.com/isteven/angular-multi-select</a>.
-<br />Kindly let me know if you use this directive in your projects -just open a new issue in Github- it's a huge motivation for me. Thanks!
+<br />If you have any problem, just open an issue in https://github.com/isteven/angular-multi-select.
+<br />Pardon the some incorrect HTML highlighting. Apparently highlight.js breaks when it sees on-open="function()" etc.
+<br />If you like / use this directive in your awesome projects, star this repo. It's a huge motivation for me. Thanks!
Licence
--
diff --git a/angular-multi-select.css b/angular-multi-select.css
index 1e69a31..4c7eb5e 100644
--- a/angular-multi-select.css
+++ b/angular-multi-select.css
@@ -1,171 +1,289 @@
-.multiSelect {
+/*
+ * Don't modify things marked with ! - unless you know what you're doing
+ */
+
+/* ! vertical layout */
+.multiSelect .vertical {
+ float: none;
+}
+
+/* ! horizontal layout */
+.multiSelect .horizontal:not(.multiSelectGroup) {
+ float: left;
+}
+
+/* ! create a "column" */
+.multiSelect .acol {
+ display: inline-block;
+ min-width: 12px;
+}
+
+/* ! create a "row" */
+.multiSelect .line {
+ height: 26px;
+ padding-bottom: 8px;
}
-.multiSelect.inlineBlock {
+/* ! */
+.multiSelect .inlineBlock {
display: inline-block;
}
-/* button */
+/*
+:focus { outline:none; }
+::-moz-focus-inner {border:0;}
+*/
+
+/* the multiselect button */
.multiSelect .button {
display: block;
position: relative;
- margin:0 auto;
- text-align: center;
+ text-align: center;
cursor: pointer;
- border: 1px solid #c6c6c6;
- padding: 5px 8px 6px;
+ border: 1px solid #c6c6c6;
+ padding: 1px 8px 1px 8px;
font-size: 14px;
- line-height: 1.4;
- border-radius: 2px;
- color: #333;
- -webkit-user-select: none;
+ min-height : 38px !important;
+ border-radius: 4px;
+ color: #555;
+ -webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
white-space:normal;
background-color: #fff;
- background-image: linear-gradient(#fff, #e4e4e4);
-
+ background-image: linear-gradient(#fff, #f7f7f7);
}
-/* button on mouse hover */
-.multiSelect .button:hover {
- background-image: linear-gradient(#fff, #eee);
+/* button: hover */
+.multiSelect .button:hover {
+ background-image: linear-gradient(#fff, #e9e9e9);
}
-/* helper buttons style (select all, none, reset, clear filter); */
-.multiSelect .helperButton {
+/* button: clicked */
+.multiSelect .buttonClicked {
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15) inset, 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+/* labels on the button */
+.multiSelect .buttonLabel {
display: inline-block;
- text-align: center;
- vertical-align: top;
- cursor: pointer;
- border: 1px solid #ccc;
- padding: 2px 7px;
- font-size: 14px;
- line-height: 1;
- border-radius: 2px;
- color: #666;
+ padding: 5px 0px 5px 0px;
}
-.multiSelect .button > div {
- float: left;
+/* downward pointing arrow */
+.multiSelect .caret {
+ display: inline-block;
+ width: 0;
+ height: 0;
+ margin: 0px 0px 1px 12px !important;
+ vertical-align: middle;
+ border-top: 4px solid #333;
+ border-right: 4px solid transparent;
+ border-left: 4px solid transparent;
+ border-bottom: 0 dotted;
}
-/* The checkboxes container */
+/* the main checkboxes and helper layer */
.multiSelect .checkboxLayer {
background-color: #fff;
position: absolute;
z-index: 999;
- border: 1px solid #cccccc;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
- box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
- background-clip: padding-box;
- padding: 14px 0px 7px 14px;
- min-width:270px;
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+ min-width:278px;
+ display: none !important;
+ margin-right: 30px;
}
-/* Downward pointing arrow */
-.multiSelect .caret {
- display: inline-block;
- width: 0;
- height: 0;
- margin-left: 15px !important;
- vertical-align: middle;
- border-top: 4px solid #333;
- border-right: 4px solid transparent;
- border-left: 4px solid transparent;
- border-bottom: 0 dotted;
+/* container of helper elements */
+.multiSelect .helperContainer {
+ border-bottom: 1px solid #ddd;
+ padding: 8px 8px 0px 8px;
}
-.multiSelect.multiSelectItem {
- display: block;
- min-height: 30px;
- color: #777;
+/* helper buttons (select all, none, reset); */
+.multiSelect .helperButton {
+ display: inline;
+ text-align: center;
+ cursor: pointer;
+ border: 1px solid #ccc;
+ height: 26px;
+ font-size: 13px;
+ border-radius: 2px;
+ color: #666;
+ background-color: #f1f1f1;
+ line-height: 1.6;
}
-.multiSelect .vertical {
- float: none;
+/* clear button */
+.multiSelect .clearButton {
+ position: absolute;
+ display: inline;
+ text-align: center;
+ cursor: pointer;
+ border: 1px solid #ccc;
+ height: 22px;
+ width: 22px;
+ font-size: 13px;
+ border-radius: 2px;
+ color: #666;
+ background-color: #f1f1f1;
+ line-height: 1.4;
+ right : 2px;
+ top: 2px;
}
-.multiSelect .horizontal {
- float: left;
+/* filter */
+.multiSelect .inputFilter {
+ border-radius: 2px;
+ border: 1px solid #ccc;
+ height: 26px;
+ font-size: 14px;
+ width:100%;
+ padding-left:7px;
+ -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
+ -moz-box-sizing: border-box; /* Firefox, other Gecko */
+ box-sizing: border-box; /* Opera/IE 8+ */
+ color: #888;
}
-.multiSelect .acol {
- display: table-cell;
- line-height: 1.4;
- min-width: 14px;
+/* helper elements on hover & focus */
+.multiSelect .clearButton:hover,
+.multiSelect .helperButton:hover {
+ border: 1px solid #ccc;
+ color: #999;
+ background-color: #f4f4f4;
+}
+
+.multiSelect .clearButton:focus,
+.multiSelect .helperButton:focus,
+.multiSelect .inputFilter:focus {
+ border: 1px solid #66AFE9 !important;
+ box-shadow: inset 0 0px 1px rgba(0,0,0,.035), 0 0 5px rgba(82,168,236,.7) !important;
}
-label.multiSelect span {
- white-space:nowrap;
- margin: 0;
- padding: 0;
+/* container of multi select items */
+.multiSelect .checkBoxContainer {
display: inline-block;
- -webkit-user-select: none;
+ padding: 8px;
+
+ /* Enable this to set a height limit
+ max-height:200px;
+ overflow-y: scroll;
+ */
+
+}
+
+/* ! to show / hide the checkbox layer above */
+.multiSelect .show {
+ display: block !important;
+}
+
+/* item labels */
+.multiSelect .multiSelectItem {
+ display: block;
+ padding: 3px;
+ color: #444;
+ white-space: nowrap;
+ -webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
- user-select: none;
+ user-select: none;
+ border: 1px solid transparent;
+ position: relative;
+ min-width:278px;
}
-/* hide the checkbox away */
-.multiSelect .checkbox {
- position: absolute;
- left: -9999px;
- cursor: pointer;
+/* Item labels: selected - Enable this if you want to apply styling on selected items */
+.multiSelect .multiSelectItem:not(.multiSelectGroup).selected
+{
+ background-image: linear-gradient( #e9e9e9, #f1f1f1 );
+ color: #555;
+ cursor: pointer;
+ border-top: 1px solid #e4e4e4;
+ border-left: 1px solid #e4e4e4;
+ border-right: 1px solid #d9d9d9;
}
-.multiSelect .show {
- display: inline-block;
+/* item labels focus on mouse hover */
+.multiSelect .multiSelectItem:hover,
+.multiSelect .multiSelectGroup:hover {
+ background-image: linear-gradient( #c1c1c1, #999 ) !important;
+ color: #fff !important;
+ cursor: pointer;
+ border: 1px solid #ccc !important;
}
-.multiSelect .hide {
- display: none;
+/* item labels focus using keyboard */
+.multiSelect .multiSelectFocus {
+ background-image: linear-gradient( #c1c1c1, #999 ) !important;
+ color: #fff !important;
+ cursor: pointer;
+ border: 1px solid #ccc !important;
}
-.multiSelect .line {
- margin-bottom: 15px;
+/* change mouse pointer into the pointing finger */
+.multiSelect .multiSelectItem span:hover,
+.multiSelect .multiSelectGroup span:hover
+{
+ cursor: pointer;
}
-.multiSelect .helperButton + .helperButton {
- margin-left: 1px;
+/* ! group labels */
+.multiSelect .multiSelectGroup {
+ display: block;
+ clear: both;
}
-/* On mouse over and focus */
-label.multiSelect input:focus ~ span::after,
-label.multiSelect span:focus::after,
-label.multiSelect span:hover::after {
- /* Enable this if you want some arrow pointer on focus */
- /* content: ' \00AB'; */
+/* right-align the tick mark (&#10004;) */
+.multiSelect .tickMark {
+ display:inline-block;
+ position: absolute;
+ right: 10px;
+ top: 7px;
+ font-size: 10px;
}
-label.multiSelect input:focus ~ span,
-label.multiSelect span:hover {
- color: #333;
- cursor: pointer;
- /* Enable this if you want some arrow pointer on focus */
- /* content: ' \00AB'; */
+/* hide the original HTML checkbox away */
+.multiSelect .checkbox {
+ color: #ddd !important;
+ position: absolute;
+ left: -9999px;
+ cursor: pointer;
+}
+
+/* Some visual aids on item labels
+.multiSelect label input:focus ~ span::after,
+.multiSelect label span:focus::after,
+.multiSelect label span:hover::after {
+ /* Enable this if you want some arrow pointer on focus
+ content: ' \00AB';
}
+*/
-/* for checkboxes currently selected */
-.multiSelect .checkboxSelected {
- color: #000;
- text-shadow: 1px 0px #eee;
+/* Another visual aid - highlight during TAB / SHIFT+TAB keyboard operations
+.multiSelect label input:focus ~ span {
+ color: #ff0000;
+ text-shadow: 1px 0px #ccc;
+ /* Enable this if you want some arrow pointer on focus
+ content: ' \00AB';
}
+*/
/* checkboxes currently disabled */
-.multiSelect.disabled, .multiSelect.disabled:hover {
- color: #ccc;
- cursor: not-allowed;
+.multiSelect .disabled,
+.multiSelect .disabled:hover,
+.multiSelect .disabled label input:hover ~ span {
+ color: #c4c4c4 !important;
+ cursor: not-allowed !important;
}
-/*
- * If you use images in button / checkbox label, you might want to change the image style here.
- */
+/* If you use images in button / checkbox label, you might want to change the image style here. */
.multiSelect img {
vertical-align: middle;
margin-bottom:0px;
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;
diff --git a/angular-multi-select.min.js b/angular-multi-select.min.js
deleted file mode 100644
index 3acf268..0000000
--- a/angular-multi-select.min.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
- * Angular JS Multi Select
- * http://github.com/isteven/angular-multi-select
- * Copyright (c) 2014 Ignatius Steven
- */
-angular.module("multi-select",["ng"]).directive("multiSelect",["$sce","$filter",function(e,t){return{restrict:"AE",replace:true,scope:{inputModel:"=",outputModel:"=",buttonLabel:"@",selectionMode:"@",itemLabel:"@",tickProperty:"@",disableProperty:"@",orientation:"@",maxLabels:"@",isDisabled:"=",directiveId:"@",helperElements:"@",onOpen:"&",onClose:"&",onBlur:"&",onFocus:"&"},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>'+"</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>"+"</div>"+"</div>"+"</span>",link:function(t,n,r){t.selectedItems=[];t.backUp=[];t.varButtonLabel="";t.currentButton=null;t.scrolled=false;t.displayHelper=function(e){if(typeof r.helperElements==="undefined"){return true}switch(e.toUpperCase()){case"ALL":if(r.selectionMode&&t.selectionMode.toUpperCase()==="SINGLE"){return false}else{if(r.helperElements&&t.helperElements.toUpperCase().indexOf("ALL")>=0){return true}}break;case"NONE":if(r.selectionMode&&t.selectionMode.toUpperCase()==="SINGLE"){return false}else{if(r.helperElements&&t.helperElements.toUpperCase().indexOf("NONE")>=0){return true}}break;case"RESET":if(r.helperElements&&t.helperElements.toUpperCase().indexOf("RESET")>=0){return true}break;case"FILTER":if(r.helperElements&&t.helperElements.toUpperCase().indexOf("FILTER")>=0){return true}break;default:break}t};t.syncItems=function(e,n){index=t.inputModel.indexOf(e);t.inputModel[index][t.tickProperty]=!t.inputModel[index][t.tickProperty];if(r.selectionMode&&t.selectionMode.toUpperCase()==="SINGLE"){t.inputModel[index][t.tickProperty]=true;for(i=0;i<t.inputModel.length;i++){if(i!==index){t.inputModel[i][t.tickProperty]=false}}t.toggleCheckboxes(n)}t.refreshSelectedItems();n.target.focus()};t.refreshSelectedItems=function(){t.varButtonLabel="";t.selectedItems=[];ctr=0;angular.forEach(t.inputModel,function(e,n){if(typeof e!=="undefined"){if(e[t.tickProperty]===true||e[t.tickProperty]==="true"){t.selectedItems.push(e)}}});if(typeof r.outputModel!=="undefined"){t.outputModel=angular.copy(t.selectedItems)}if(t.selectedItems.length===0){t.varButtonLabel="None selected"}else{var n=t.selectedItems.length;if(typeof t.maxLabels!=="undefined"&&t.maxLabels!==""&&t.maxLabels!=="0"){n=t.maxLabels}if(t.selectedItems.length>n){t.more=true}else{t.more=false}angular.forEach(t.selectedItems,function(e,r){if(typeof e!=="undefined"){if(ctr<n){t.varButtonLabel+=(t.varButtonLabel.length>0?", ":"")+t.writeLabel(e,"buttonLabel")}ctr++}});if(t.more===true){t.varButtonLabel+=", ... (Total: "+t.selectedItems.length+")"}t}t.varButtonLabel=e.trustAsHtml(t.varButtonLabel+'<span class="multiSelect caret"></span>')};t.itemIsDisabled=function(e){if(e[t.disableProperty]===true){return true}else{t;if(t.isDisabled===true){return true}else{return false}}};t.writeLabel=function(n,r){var i="";var s=t[r].split(" ");angular.forEach(s,function(e,t){if(typeof e!=="undefined"){angular.forEach(n,function(t,n){if(n==e){i+=" "+t}})}});return e.trustAsHtml(i)};t.toggleCheckboxes=function(e){if(e.target){if(e.target.tagName.toUpperCase()!=="BUTTON"&&e.target.className.indexOf("multiSelectButton")<0){if(r.selectionMode&&t.selectionMode.toUpperCase()==="SINGLE"){if(e.target.tagName.toUpperCase()==="INPUT"){e=t.findUpTag(e.target,"div","checkboxLayer");e=e.previousSibling}}else{e=t.findUpTag(e.target,"button","multiSelectButton")}}else{e=e.target}}t.labelFilter="";var n=-1;var s=document.querySelectorAll(".checkboxLayer");var o=document.querySelectorAll(".multiSelectButton");for(i=0;i<o.length;i++){if(e===o[i]){n=i;break}}if(n>-1){for(i=0;i<s.length;i++){if(i!=n){s[i].className="multiSelect checkboxLayer hide"}}if(s[n].className=="multiSelect checkboxLayer hide"){t.currentButton=o[n];s[n].className="multiSelect checkboxLayer show";t.onOpen()}else if(s[n].className=="multiSelect checkboxLayer show"){s[n].className="multiSelect checkboxLayer hide";t.onClose()}}};t.findUpTag=function(e,t,n){while(e.parentNode){e=e.parentNode;if(typeof e.tagName!=="undefined"){if(e.tagName.toUpperCase()===t.toUpperCase()&&e.className.indexOf(n)>-1){return e}}}return null};t.select=function(e){var n=[];switch(e.toUpperCase()){case"ALL":angular.forEach(t.inputModel,function(e,n){if(typeof e!=="undefined"&&e[t.disableProperty]!==true){e[t.tickProperty]=true}});break;case"NONE":angular.forEach(t.inputModel,function(e,n){if(typeof e!=="undefined"&&e[t.disableProperty]!==true){e[t.tickProperty]=false}});break;case"RESET":t.inputModel=angular.copy(t.backUp);break;default:}t.refreshSelectedItems()};validate=function(){if(!("inputModel"in r)){console.log("Multi-select error: input-model is not defined! (ID: "+t.directiveId+")")}if(!("buttonLabel"in r)){console.log("Multi-select error: button-label is not defined! (ID: "+t.directiveId+")")}if(!("itemLabel"in r)){console.log("Multi-select error: item-label is not defined! (ID: "+t.directiveId+")")}if(!("tickProperty"in r)){console.log("Multi-select error: tick-property is not defined! (ID: "+t.directiveId+")")}};validateProperties=function(e,n){var r=false;var i="";angular.forEach(e,function(e,t){if(typeof e!=="undefined"){var i=true;angular.forEach(n,function(t,n){if(typeof t!=="undefined"&&i){if(!(e in t)){r=true;i=false;missingLabel=e}}})}});if(r===true){console.log('Multi-select error: property "'+missingLabel+'" is not available in the input model. (Name: '+t.directiveId+")")}};validate();t.refreshSelectedItems();t.$watch("inputModel",function(e,n){if(t.newVal!=="undefined"){validateProperties(t.itemLabel.split(" "),t.inputModel);validateProperties(new Array(t.tickProperty),t.inputModel)}t.refreshSelectedItems()},true);t.$watch("inputModel",function(e,n){if(t.newVal!=="undefined"){validateProperties(t.itemLabel.split(" "),t.inputModel);validateProperties(new Array(t.tickProperty),t.inputModel)}t.backUp=angular.copy(t.inputModel);t.refreshSelectedItems()});t.$watch("isDisabled",function(e){t.isDisabled=e});angular.element(document).bind("touchstart",function(e){t.$apply(function(){t.scrolled=false})});angular.element(document).bind("touchmove",function(e){t.$apply(function(){t.scrolled=true})});angular.element(document).bind("click touchend",function(e){if(e.type==="click"||e.type==="touchend"&&t.scrolled===false){var n=document.querySelectorAll(".checkboxLayer");if(e.target.className.indexOf("multiSelect")===-1){for(i=0;i<n.length;i++){n[i].className="multiSelect checkboxLayer hide"}e.stopPropagation()}}});if(!Array.prototype.indexOf){Array.prototype.indexOf=function(e,t){t=t||0;var n=this.length;while(t<n){if(this[t]===e)return t;++t}return-1}}}}}])
diff --git a/bower.json b/bower.json
index 460a0a3..54b284e 100644
--- a/bower.json
+++ b/bower.json
@@ -1,11 +1,8 @@
{
"name" : "isteven-angular-multiselect",
- "version" : "v1.1.0",
+ "version" : "v2.0.0",
"main" : [
- "angular-multi-select.js",
- "angular-multi-select.min.js",
- "angular-multi-select.css",
- "LICENSE"
+ "angular-multi-select.js"
],
"ignore" : [
".git",
diff --git a/screenshot.png b/screenshot.png
index 6103fe3..dce2d82 100644
--- a/screenshot.png
+++ b/screenshot.png
Binary files differ