diff --git a/README.md b/README.md index eb7c24d..3d6ccfe 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -Angular Multi Select -==================== +Angular MultiSelect +== An AngularJS directive which creates a dropdown button with multiple or single selections. Allows you to customize your labels and use some HTML tags in the data. Fully configurable through element attributes and CSS. Doesn't require jQuery and works well with other Javascript libraries. diff --git a/angular-multi-select.css b/angular-multi-select.css index 7895301..9f4c564 100644 --- a/angular-multi-select.css +++ b/angular-multi-select.css @@ -74,7 +74,7 @@ border-top: 4px solid #333; border-right: 4px solid transparent; border-left: 4px solid transparent; - border-bottom: 0 dotted; + border-bottom: 0 dotted; } .multiSelect.multiSelectItem { @@ -110,11 +110,6 @@ label.multiSelect span { user-select: none; } -label.multiSelect span:hover{ - cursor: pointer; - color: #333; -} - /* hide the checkbox away */ .multiSelect .checkbox { position: absolute; @@ -138,6 +133,22 @@ label.multiSelect span:hover{ margin-left: 1px; } +/* 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 */ + /* content: ' \00AB'; */ +} + +label.multiSelect input:focus ~ span, +label.multiSelect span:hover { + color: #333; + cursor: pointer; + /* Enable this if you want some arrow pointer */ + /* content: ' \00AB'; */ +} + /* for checkboxes currently selected */ .multiSelect .checkboxSelected { color: #000; diff --git a/angular-multi-select.js b/angular-multi-select.js index fdb1bed..1197874 100644 --- a/angular-multi-select.js +++ b/angular-multi-select.js @@ -31,7 +31,7 @@ * -------------------------------------------------------------------------------- */ -angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', function ( $sce ) { +angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', '$filter', function ( $sce, $filter ) { return { restrict: 'AE', @@ -52,47 +52,93 @@ angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', fu maxLabels : '@', isDisabled : '=', directiveId : '@', - // JH DotComIt Added 5/8/2014 - onPopupopen: '&onPopupopen', - onPopupclose: '&onPopupclose' + helperElements : '@', + onOpen : '&', + onClose : '&', + onBlur : '&', + onFocus : '&' }, template: - '' + - '' + '
' + '
' + - 'Select:  ' + - ' ' + - ' ' + - '' + + 'Select:  ' + + ' ' + + ' ' + + '' + '
' + - '
' + + '
' + 'Filter: ' + ' ' + '
' + - '
' + + '
' + '
' + '
' + '
' + '
' + '  ' + '
' + '
' + '
' + '', - link: function ( $scope, element, attrs ) { + link: function ( $scope, element, attrs ) { $scope.selectedItems = []; $scope.backUp = []; - $scope.varButtonLabel = ''; + $scope.varButtonLabel = ''; + $scope.tabIndex = 0; + $scope.tabables = null; + $scope.currentButton = null; + + // Show or hide a helper element + $scope.displayHelper = function( elementString ) { + if ( typeof attrs.helperElements === 'undefined' ) { + return true; + } + switch( elementString.toUpperCase() ) { + case 'ALL': + if ( attrs.selectionMode && $scope.selectionMode.toUpperCase() === 'SINGLE' ) { + return false; + } + else { + if ( attrs.helperElements && $scope.helperElements.toUpperCase().indexOf( 'ALL' ) >= 0 ) { + return true; + } + } + break; + case 'NONE': + if ( attrs.selectionMode && $scope.selectionMode.toUpperCase() === 'SINGLE' ) { + return false; + } + else { + if ( attrs.helperElements && $scope.helperElements.toUpperCase().indexOf( 'NONE' ) >= 0 ) { + return true; + } + } + break; + case 'RESET': + if ( attrs.helperElements && $scope.helperElements.toUpperCase().indexOf( 'RESET' ) >= 0 ) { + return true; + } + break; + case 'FILTER': + if ( attrs.helperElements && $scope.helperElements.toUpperCase().indexOf( 'FILTER' ) >= 0 ) { + return true; + } + break; + default: + break; + } + } - // Checkbox is ticked + // 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 ]; @@ -107,8 +153,9 @@ angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', fu } $scope.toggleCheckboxes( e ); } - - $scope.refreshSelectedItems(); + + $scope.refreshSelectedItems(); + e.target.focus(); } // Refresh the button to display the selected items and push into output model if specified @@ -199,7 +246,7 @@ angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', fu return $sce.trustAsHtml( label ); } - // UI operations to show/hide checkboxes + // UI operations to show/hide checkboxes based on click event.. $scope.toggleCheckboxes = function( e ) { if ( e.target ) { @@ -238,20 +285,19 @@ angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', fu for( i=0; i < checkboxes.length; i++ ) { if ( i != multiSelectIndex ) { checkboxes[i].className = 'multiSelect checkboxLayer hide'; - // JH DotComIt 5/8/2014 Added method handler for closing the popup - $scope.onPopupclose(); } } if ( checkboxes[ multiSelectIndex ].className == 'multiSelect checkboxLayer hide' ) { + $scope.currentButton = multiSelectButtons[ multiSelectIndex ]; checkboxes[ multiSelectIndex ].className = 'multiSelect checkboxLayer show'; - // JH DotComIt 5/8/2014 Added method handler for opening the popup - $scope.onPopupopen(); + // https://github.com/isteven/angular-multi-select/pull/5 - On open callback + $scope.onOpen(); } else if ( checkboxes[ multiSelectIndex ].className == 'multiSelect checkboxLayer show' ) { checkboxes[ multiSelectIndex ].className = 'multiSelect checkboxLayer hide'; - // JH DotComIt 5/8/2014 Added method handler for closing the popup - $scope.onPopupclose(); + // https://github.com/isteven/angular-multi-select/pull/5 - On close callback + $scope.onClose(); } } } @@ -289,8 +335,8 @@ angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', fu } }); break; - case 'RESET': - $scope.inputModel = angular.copy( $scope.backUp ); + case 'RESET': + $scope.inputModel = angular.copy( $scope.backUp ); break; default: } @@ -299,6 +345,7 @@ angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', fu // 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 + ')' ); @@ -337,17 +384,16 @@ angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', fu }); if ( notThere === true ) { console.log( 'Multi-select error: property "' + missingLabel + '" is not available in the input model. (Name: ' + $scope.directiveId + ')' ); - } - - } + } + } /////////////////////// // Logic starts here /////////////////////// validate(); - $scope.refreshSelectedItems(); - + $scope.refreshSelectedItems(); + // Watch for changes in input model (allow dynamic input) $scope.$watch( 'inputModel' , function( oldVal, newVal ) { if ( $scope.inputModel !== 'undefined' ) { @@ -368,14 +414,12 @@ angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', fu var checkboxes = document.querySelectorAll( '.checkboxLayer' ); if ( e.target.className.indexOf( 'multiSelect' ) === -1 ) { for( i=0; i < checkboxes.length; i++ ) { - checkboxes[i].className = 'multiSelect checkboxLayer hide'; - // JH DotComIt 5/8/2014 Added method handler for closing the popup - $scope.onPopupclose(); + checkboxes[i].className = 'multiSelect checkboxLayer hide'; } e.stopPropagation(); } - }); - + }); + // For IE8, perhaps. Not sure if this is really executed. if ( !Array.prototype.indexOf ) { Array.prototype.indexOf = function(what, i) { diff --git a/angular-multi-select.min.js b/angular-multi-select.min.js index 14d1292..d2b7980 100644 --- a/angular-multi-select.min.js +++ b/angular-multi-select.min.js @@ -4,4 +4,4 @@ * http://github.com/isteven/angular-multi-select * Copyright (c) 2014 Ignatius Steven */ -angular.module("multi-select",["ng"]).directive("multiSelect",["$sce",function(a){return{restrict:"AE",replace:true,scope:{inputModel:"=",outputModel:"=",buttonLabel:"@",selectionMode:"@",itemLabel:"@",tickProperty:"@",disableProperty:"@",orientation:"@",maxLabels:"@",isDisabled:"=",directiveId:"@"},template:'
Select:  
Filter:  
  
',link:function(b,d,c){b.selectedItems=[];b.backUp=[];b.varButtonLabel="";b.syncItems=function(f,g){index=b.inputModel.indexOf(f);b.inputModel[index][b.tickProperty]=!b.inputModel[index][b.tickProperty];if(c.selectionMode&&b.selectionMode.toUpperCase()==="SINGLE"){b.inputModel[index][b.tickProperty]=true;for(i=0;ie){b.more=true}else{b.more=false}angular.forEach(b.selectedItems,function(g,f){if(typeof g!=="undefined"){if(ctr0?", ":"")+b.writeLabel(g,"buttonLabel")}ctr++}});if(b.more===true){b.varButtonLabel+=", ... (Total: "+b.selectedItems.length+")"}}b.varButtonLabel=a.trustAsHtml(b.varButtonLabel+'')};b.itemIsDisabled=function(e){if(e[b.disableProperty]===true){return true}else{if(b.isDisabled===true){return true}else{return false}}};b.writeLabel=function(h,g){var f="";var e=b[g].split(" ");angular.forEach(e,function(j,k){if(typeof j!=="undefined"){angular.forEach(h,function(l,m){if(m==j){f+=" "+l}})}});return a.trustAsHtml(f)};b.toggleCheckboxes=function(j){if(j.target){if(j.target.tagName.toUpperCase()!=="BUTTON"&&j.target.className.indexOf("multiSelectButton")<0){if(c.selectionMode&&b.selectionMode.toUpperCase()==="SINGLE"){if(j.target.tagName.toUpperCase()==="INPUT"){j=b.findUpTag(j.target,"div","checkboxLayer");j=j.previousSibling}}else{j=b.findUpTag(j.target,"button","multiSelectButton")}}else{j=j.target}}b.labelFilter="";var g=-1;var h=document.querySelectorAll(".checkboxLayer");var f=document.querySelectorAll(".multiSelectButton");for(i=0;i-1){for(i=0;i-1){return g}}}return null};b.select=function(f){var e=[];switch(f.toUpperCase()){case"ALL":angular.forEach(b.inputModel,function(h,g){if(typeof h!=="undefined"&&h[b.disableProperty]!==true){h[b.tickProperty]=true}});break;case"NONE":angular.forEach(b.inputModel,function(h,g){if(typeof h!=="undefined"&&h[b.disableProperty]!==true){h[b.tickProperty]=false}});break;case"RESET":b.inputModel=angular.copy(b.backUp);break;default:}b.refreshSelectedItems()};validate=function(){if(!("inputModel" in c)){console.log("Multi-select error: input-model is not defined! (ID: "+b.directiveId+")")}if(!("buttonLabel" in c)){console.log("Multi-select error: button-label is not defined! (ID: "+b.directiveId+")")}if(!("itemLabel" in c)){console.log("Multi-select error: item-label is not defined! (ID: "+b.directiveId+")")}if(!("tickProperty" in c)){console.log("Multi-select error: tick-property is not defined! (ID: "+b.directiveId+")")}};validateProperties=function(f,h){var g=false;var e="";angular.forEach(f,function(j,k){if(typeof j!=="undefined"){var l=true;angular.forEach(h,function(m,n){if(typeof m!=="undefined"&&l){if(!(j in m)){g=true;l=false;missingLabel=j}}})}});if(g===true){console.log('Multi-select error: property "'+missingLabel+'" is not available in the input model. (Name: '+b.directiveId+")")}};validate();b.refreshSelectedItems();b.$watch("inputModel",function(e,f){if(b.inputModel!=="undefined"){validateProperties(b.itemLabel.split(" "),b.inputModel);validateProperties(new Array(b.tickProperty),b.inputModel)}b.backUp=angular.copy(b.inputModel);b.refreshSelectedItems()});b.$watch("isDisabled",function(e){b.isDisabled=e});angular.element(document).bind("click",function(g){var f=document.querySelectorAll(".checkboxLayer");if(g.target.className.indexOf("multiSelect")===-1){for(i=0;i'+'"+'
'+'
'+"Select:  "+' '+' '+''+"
"+'
'+'Filter: '+' '+"
"+'
'+'
'+'
'+"
"+'
'+'  "+"
"+"
"+"
"+"",link:function(t,n,r){t.selectedItems=[];t.backUp=[];t.varButtonLabel="";t.tabIndex=0;t.tabables=null;t.currentButton=null;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.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;in){t.more=true}else{t.more=false}angular.forEach(t.selectedItems,function(e,r){if(typeof e!=="undefined"){if(ctr0?", ":"")+t.writeLabel(e,"buttonLabel")}ctr++}});if(t.more===true){t.varButtonLabel+=", ... (Total: "+t.selectedItems.length+")"}}t.varButtonLabel=e.trustAsHtml(t.varButtonLabel+'')};t.itemIsDisabled=function(e){if(e[t.disableProperty]===true){return true}else{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-1){for(i=0;i-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.inputModel!=="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("click",function(e){var t=document.querySelectorAll(".checkboxLayer");if(e.target.className.indexOf("multiSelect")===-1){for(i=0;i