1/* 2 * Angular JS Multi Select 3 * Creates a dropdown-like button with checkboxes. 4 * 5 * Project started on: Tue, 14 Jan 2014 - 5:18:02 PM 6 * Current version: 2.0.2 7 * 8 * Released under the MIT License 9 * -------------------------------------------------------------------------------- 10 * The MIT License (MIT) 11 * 12 * Copyright (c) 2014 Ignatius Steven (https://github.com/isteven) 13 * 14 * Permission is hereby granted, free of charge, to any person obtaining a copy 15 * of this software and associated documentation files (the "Software"), to deal 16 * in the Software without restriction, including without limitation the rights 17 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 * copies of the Software, and to permit persons to whom the Software is 19 * furnished to do so, subject to the following conditions: 20 * 21 * The above copyright notice and this permission notice shall be included in all 22 * copies or substantial portions of the Software. 23 * 24 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30 * SOFTWARE. 31 * -------------------------------------------------------------------------------- 32 */ 33 34angular.module( 'multi-select', ['ng'] ).directive( 'multiSelect' , [ '$sce', '$timeout', function ( $sce, $timeout ) { 35 return { 36 restrict: 37 'AE', 38 39 replace: 40 true, 41 42 scope: 43 { 44 // models 45 inputModel : '=', 46 outputModel : '=', 47 48 // settings based on attribute 49 buttonLabel : '@', 50 defaultLabel : '@', 51 directiveId : '@', 52 helperElements : '@', 53 isDisabled : '=', 54 itemLabel : '@', 55 maxLabels : '@', 56 orientation : '@', 57 selectionMode : '@', 58 59 // settings based on input model property 60 tickProperty : '@', 61 disableProperty : '@', 62 groupProperty : '@', 63 maxHeight : '@', 64 65 // callbacks 66 onClose : '&', 67 onItemClick : '&', 68 onOpen : '&' 69 }, 70 71 template: 72 '<span class="multiSelect inlineBlock">' + 73 '<button type="button" class="button multiSelectButton" ng-class="{disabled: isDisabled}" ng-click="toggleCheckboxes( $event ); refreshSelectedItems(); refreshButton();" ng-bind-html="varButtonLabel">' + 74 '</button>' + 75 '<div class="checkboxLayer">' + 76 '<form>' + 77 '<div class="helperContainer" ng-if="displayHelper( \'filter\' ) || displayHelper( \'all\' ) || displayHelper( \'none\' ) || displayHelper( \'reset\' )">' + 78 '<div class="line" ng-if="displayHelper( \'all\' ) || displayHelper( \'none\' ) || displayHelper( \'reset\' )">' + 79 '<button type="button" ng-click="select( \'all\', $event );" class="helperButton" ng-if="!isDisabled && displayHelper( \'all\' )"> ✓ Select All</button> ' + 80 '<button type="button" ng-click="select( \'none\', $event );" class="helperButton" ng-if="!isDisabled && displayHelper( \'none\' )"> × Select None</button>' + 81 '<button type="button" ng-click="select( \'reset\', $event );" class="helperButton" ng-if="!isDisabled && displayHelper( \'reset\' )" style="float:right">↶ Reset</button>' + 82 '</div>' + 83 '<div class="line" style="position:relative" ng-if="displayHelper( \'filter\' )">' + 84 '<input placeholder="Search..." type="text" ng-click="select( \'filter\', $event )" ng-model="inputLabel.labelFilter" ng-change="updateFilter();$scope.getFormElements();" class="inputFilter" />' + 85 '<button type="button" class="clearButton" ng-click="inputLabel.labelFilter=\'\';updateFilter();prepareGrouping();prepareIndex();select( \'clear\', $event )">×</button> ' + 86 '</div>' + 87 '</div>' + 88 '<div class="checkBoxContainer" style="{{setHeight();}}">' + 89 '<div ng-repeat="item in filteredModel | filter:removeGroupEndMarker" class="multiSelectItem"' + 90 'ng-class="{selected: item[ tickProperty ], horizontal: orientationH, vertical: orientationV, multiSelectGroup:item[ groupProperty ], disabled:itemIsDisabled( item )}"' + 91 'ng-click="syncItems( item, $event, $index );"' + 92 'ng-mouseleave="removeFocusStyle( tabIndex );">' + 93 '<div class="acol" ng-if="item[ spacingProperty ] > 0" ng-repeat="i in numberToArray( item[ spacingProperty ] ) track by $index"> </div>' + 94 '<div class="acol">' + 95 '<label>' + 96 '<input class="checkbox focusable" type="checkbox" ng-disabled="itemIsDisabled( item )" ng-checked="item[ tickProperty ]" ng-click="syncItems( item, $event, $index )" />' + 97 '<span ng-class="{disabled:itemIsDisabled( item )}" ng-bind-html="writeLabel( item, \'itemLabel\' )"></span>' + 98 '</label>' + 99 '</div>' + 100 ' ' + 101 '<span class="tickMark" ng-if="item[ groupProperty ] !== true && item[ tickProperty ] === true">✔</span>' + 102 '</div>' + 103 '</div>' + 104 '</form>' + 105 '</div>' + 106 '</span>', 107
108 link: function ( $scope, element, attrs ) { 109 110 $scope.backUp = []; 111 $scope.varButtonLabel = ''; 112 $scope.scrolled = false; 113 $scope.spacingProperty = ''; 114 $scope.indexProperty = ''; 115 $scope.checkBoxLayer = ''; 116 $scope.orientationH = false; 117 $scope.orientationV = true; 118 $scope.filteredModel = []; 119 $scope.inputLabel = { labelFilter: '' }; 120 $scope.selectedItems = []; 121 $scope.formElements = []; 122 $scope.tabIndex = 0; 123 $scope.clickedItem = null; 124 prevTabIndex = 0; 125 helperItems = []; 126 helperItemsLength = 0; 127 128 // If user specify a height, call this function 129 $scope.setHeight = function() { 130 if ( typeof $scope.maxHeight !== 'undefined' ) { 131 return 'max-height: ' + $scope.maxHeight + '; overflow-y:scroll'; 132 } 133 } 134 135 // A little hack so that AngularJS ng-repeat can loop using start and end index like a normal loop 136 // http://stackoverflow.com/questions/16824853/way-to-ng-repeat-defined-number-of-times-instead-of-repeating-over-array 137 $scope.numberToArray = function( num ) { 138 return new Array( num ); 139 } 140 141 $scope.updateFilter = function() 142 { 143 // we check by looping from end of array 144 $scope.filteredModel = []; 145 var i = 0; 146 147 if ( typeof $scope.inputModel === 'undefined' ) { 148 return []; 149 } 150 151 for( i = $scope.inputModel.length - 1; i >= 0; i-- ) { 152 153 // if it's group end 154 if ( typeof $scope.inputModel[ i ][ $scope.groupProperty ] !== 'undefined' && $scope.inputModel[ i ][ $scope.groupProperty ] === false ) { 155 $scope.filteredModel.push( $scope.inputModel[ i ] ); 156 } 157 158 // if it's data 159 var gotData = false; 160 if ( typeof $scope.inputModel[ i ][ $scope.groupProperty ] === 'undefined' ) { 161 162 for (var key in $scope.inputModel[ i ] ) { 163 // if filter string is in one of object property 164 if ( typeof $scope.inputModel[ i ][ key ] !== 'boolean' && String( $scope.inputModel[ i ][ key ] ).toUpperCase().indexOf( $scope.inputLabel.labelFilter.toUpperCase() ) >= 0 ) { 165 gotData = true; 166 break; 167 } 168 } 169 if ( gotData === true ) { 170 // push 171 $scope.filteredModel.push( $scope.inputModel[ i ] ); 172 } 173 } 174 175 // if it's group start 176 if ( typeof $scope.inputModel[ i ][ $scope.groupProperty ] !== 'undefined' && $scope.inputModel[ i ][ $scope.groupProperty ] === true ) { 177 178 if ( typeof $scope.filteredModel[ $scope.filteredModel.length - 1 ][ $scope.groupProperty ] !== 'undefined' && $scope.filteredModel[ $scope.filteredModel.length - 1 ][ $scope.groupProperty ] === false ) { 179 $scope.filteredModel.pop(); 180 } 181 else { 182 $scope.filteredModel.push( $scope.inputModel[ i ] ); 183 } 184 } 185 } 186 187 $scope.filteredModel.reverse(); 188 $timeout( function() { 189 $scope.getFormElements(); 190 },0); 191 }; 192 193 // List all the input elements. 194 // This function will be called everytime the filter is updated. Not good for performance, but oh well.. 195 $scope.getFormElements = function() { 196 $scope.formElements = []; 197 for ( var i = 0; i < element[ 0 ].getElementsByTagName( 'FORM' )[ 0 ].elements.length ; i++ ) { 198 $scope.formElements.push( element[ 0 ].getElementsByTagName( 'FORM' )[ 0 ].elements[ i ] ); 199 } 200 } 201 202 // check if an item has $scope.groupProperty (be it true or false) 203 $scope.isGroupMarker = function( item , type ) { 204 if ( typeof item[ $scope.groupProperty ] !== 'undefined' && item[ $scope.groupProperty ] === type ) return true; 205 return false;
206 } 207 208 $scope.removeGroupEndMarker = function( item ) { 209 if ( typeof item[ $scope.groupProperty ] !== 'undefined' && item[ $scope.groupProperty ] === false ) return false; 210 return true; 211 } 212 213 214 // Show or hide a helper element 215 $scope.displayHelper = function( elementString ) { 216 217 if ( attrs.selectionMode && $scope.selectionMode.toUpperCase() === 'SINGLE' ) { 218 219 switch( elementString.toUpperCase() ) { 220 case 'ALL': 221 return false; 222 break; 223 case 'NONE': 224 return false; 225 break; 226 case 'RESET': 227 if ( typeof attrs.helperElements === 'undefined' ) { 228 return true; 229 } 230 else if ( attrs.helperElements && $scope.helperElements.toUpperCase().indexOf( 'RESET' ) >= 0 ) { 231 return true; 232 } 233 break; 234 case 'FILTER': 235 if ( typeof attrs.helperElements === 'undefined' ) { 236 return true; 237 } 238 if ( attrs.helperElements && $scope.helperElements.toUpperCase().indexOf( 'FILTER' ) >= 0 ) { 239 return true; 240 } 241 break; 242 default: 243 break; 244 } 245 246 return false; 247 } 248 249 else { 250 if ( typeof attrs.helperElements === 'undefined' ) { 251 return true; 252 } 253 if ( attrs.helperElements && $scope.helperElements.toUpperCase().indexOf( elementString.toUpperCase() ) >= 0 ) { 254 return true; 255 } 256 return false; 257 } 258 } 259 260 // call this function when an item is clicked 261 $scope.syncItems = function( item, e, ng_repeat_index ) { 262 263 e.preventDefault(); 264 e.stopPropagation(); 265 266 // if it's globaly disabled, then don't do anything 267 if ( typeof attrs.disableProperty !== 'undefined' && item[ $scope.disableProperty ] === true ) { 268 return false; 269 } 270 271 // don't change disabled items 272 if ( typeof attrs.isDisabled !== 'undefined' && $scope.isDisabled === true ) { 273 return false; 274 } 275 276 // we don't care about end of group markers 277 if ( typeof item[ $scope.groupProperty ] !== 'undefined' && item[ $scope.groupProperty ] === false ) { 278 return false; 279 } 280 281 index = $scope.filteredModel.indexOf( item ); 282 283 // process items if the start of group marker is clicked ( only for multiple selection! ) 284 // if, in a group, there are items which are not selected, then they all will be selected 285 // if, in a group, all items are selected, then they all will be de-selected 286 if ( typeof item[ $scope.groupProperty ] !== 'undefined' && item[ $scope.groupProperty ] === true ) { 287 288 if ( attrs.selectionMode && $scope.selectionMode.toUpperCase() === 'SINGLE' ) { 289 return false; 290 } 291 292 var i,j,k; 293 var startIndex = 0; 294 var endIndex = $scope.filteredModel.length - 1; 295 var tempArr = []; 296 var nestLevel = 0; 297 298 for( i = index ; i < $scope.filteredModel.length ; i++) { 299 300 if ( nestLevel === 0 && i > index ) 301 { 302 break; 303 } 304 305 // if group start 306 if ( typeof $scope.filteredModel[ i ][ $scope.groupProperty ] !== 'undefined' && $scope.filteredModel[ i ][ $scope.groupProperty ] === true ) { 307 308 // To cater multi level grouping 309 if ( tempArr.length === 0 ) { 310 startIndex = i + 1; 311 } 312 nestLevel = nestLevel + 1; 313 } 314 315 // if group end 316 else if ( typeof $scope.filteredModel[ i ][ $scope.groupProperty ] !== 'undefined' && $scope.filteredModel[ i ][ $scope.groupProperty ] === false ) { 317 318 nestLevel = nestLevel - 1; 319 320 // cek if all are ticked or not 321 if ( tempArr.length > 0 && nestLevel === 0 ) { 322
323 var allTicked = true; 324 325 endIndex = i; 326 327 for ( j = 0; j < tempArr.length ; j++ ) { 328 if ( typeof tempArr[ j ][ $scope.tickProperty ] !== 'undefined' && tempArr[ j ][ $scope.tickProperty ] === false ) { 329 allTicked = false; 330 break; 331 } 332 } 333 334 if ( allTicked === true ) { 335 for ( j = startIndex; j <= endIndex ; j++ ) { 336 if ( typeof $scope.filteredModel[ j ][ $scope.groupProperty ] === 'undefined' ) { 337 if ( typeof attrs.disableProperty === 'undefined' ) { 338 $scope.filteredModel[ j ][ $scope.tickProperty ] = false; 339 // we refresh input model as well 340 inputModelIndex = $scope.filteredModel[ j ][ $scope.indexProperty ]; 341 $scope.inputModel[ inputModelIndex ][ $scope.tickProperty ] = false; 342 } 343 else if ( $scope.filteredModel[ j ][ $scope.disableProperty ] !== true ) { 344 $scope.filteredModel[ j ][ $scope.tickProperty ] = false; 345 // we refresh input model as well 346 inputModelIndex = $scope.filteredModel[ j ][ $scope.indexProperty ]; 347 $scope.inputModel[ inputModelIndex ][ $scope.tickProperty ] = false; 348 } 349 } 350 } 351 } 352 353 else { 354 for ( j = startIndex; j <= endIndex ; j++ ) { 355 if ( typeof $scope.filteredModel[ j ][ $scope.groupProperty ] === 'undefined' ) { 356 if ( typeof attrs.disableProperty === 'undefined' ) { 357 $scope.filteredModel[ j ][ $scope.tickProperty ] = true; 358 // we refresh input model as well 359 inputModelIndex = $scope.filteredModel[ j ][ $scope.indexProperty ]; 360 $scope.inputModel[ inputModelIndex ][ $scope.tickProperty ] = true; 361 362 } 363 else if ( $scope.filteredModel[ j ][ $scope.disableProperty ] !== true ) { 364 $scope.filteredModel[ j ][ $scope.tickProperty ] = true; 365 // we refresh input model as well 366 inputModelIndex = $scope.filteredModel[ j ][ $scope.indexProperty ]; 367 $scope.inputModel[ inputModelIndex ][ $scope.tickProperty ] = true; 368 } 369 } 370 } 371 } 372 } 373 } 374 375 // if data 376 else { 377 tempArr.push( $scope.filteredModel[ i ] ); 378 } 379 } 380 } 381 382 // single item click 383 else { 384 385 // If it's single selection mode 386 if ( attrs.selectionMode && $scope.selectionMode.toUpperCase() === 'SINGLE' ) { 387 388 // first, set everything to false 389 for( i=0 ; i < $scope.filteredModel.length ; i++) { 390 $scope.filteredModel[ i ][ $scope.tickProperty ] = false;
391 } 392 for( i=0 ; i < $scope.inputModel.length ; i++) { 393 $scope.inputModel[ i ][ $scope.tickProperty ] = false; 394 } 395 396 // then set the clicked item to true 397 $scope.filteredModel[ index ][ $scope.tickProperty ] = true; 398 399 $scope.toggleCheckboxes( e ); 400 } 401 402 // Multiple 403 else { 404 $scope.filteredModel[ index ][ $scope.tickProperty ] = !$scope.filteredModel[ index ][ $scope.tickProperty ]; 405 } 406 407 // we refresh input model as well 408 inputModelIndex = $scope.filteredModel[ index ][ $scope.indexProperty ]; 409 $scope.inputModel[ inputModelIndex ][ $scope.tickProperty ] = $scope.filteredModel[ index ][ $scope.tickProperty ]; 410 } 411 412 $scope.clickedItem = angular.copy( item ); 413 414 // We update the index here 415 prevTabIndex = $scope.tabIndex; 416 $scope.tabIndex = ng_repeat_index + helperItemsLength; 417 418 // Set focus on the hidden checkbox 419 e.target.focus(); 420 421 // set & remove CSS style 422 $scope.removeFocusStyle( prevTabIndex ); 423 $scope.setFocusStyle( $scope.tabIndex ); 424 } 425 426 // update $scope.selectedItems 427 // this variable is used in $scope.outputModel and to refresh the button label 428 $scope.refreshSelectedItems = function() { 429 $scope.selectedItems = []; 430 angular.forEach( $scope.inputModel, function( value, key ) { 431 if ( typeof value !== 'undefined' ) { 432 if ( typeof value[ $scope.groupProperty ] === 'undefined' ) { 433 if ( value[ $scope.tickProperty ] === true ) { 434 $scope.selectedItems.push( value ); 435 } 436 } 437 } 438 }); 439 } 440 441 // refresh output model as well 442 $scope.refreshOutputModel = function() { 443 if ( typeof attrs.outputModel !== 'undefined' ) { 444 $scope.outputModel = angular.copy( $scope.selectedItems ); 445 angular.forEach( $scope.outputModel, function( value, key ) { 446 // remove the index number and spacing number from output model 447 delete value[ $scope.indexProperty ]; 448 delete value[ $scope.spacingProperty ]; 449 }); 450 } 451 } 452 453 // refresh button label 454 $scope.refreshButton = function() { 455 456 $scope.varButtonLabel = ''; 457 ctr = 0; 458 459 // refresh button label... 460 if ( $scope.selectedItems.length === 0 ) { 461 // https://github.com/isteven/angular-multi-select/pull/19 462 $scope.varButtonLabel = ( typeof $scope.defaultLabel !== 'undefined' ) ? $scope.defaultLabel : 'None selected'; 463 } 464 else { 465 var tempMaxLabels = $scope.selectedItems.length; 466 if ( typeof $scope.maxLabels !== 'undefined' && $scope.maxLabels !== '' ) { 467 tempMaxLabels = $scope.maxLabels; 468 } 469 470 // if max amount of labels displayed.. 471 if ( $scope.selectedItems.length > tempMaxLabels ) { 472 $scope.more = true; 473 } 474 else { 475 $scope.more = false; 476 } 477 478 angular.forEach( $scope.selectedItems, function( value, key ) { 479 if ( typeof value !== 'undefined' ) { 480 if ( ctr < tempMaxLabels ) { 481 $scope.varButtonLabel += ( $scope.varButtonLabel.length > 0 ? '</div>, <div class="buttonLabel">' : '<div class="buttonLabel">') + $scope.writeLabel( value, 'buttonLabel' ); 482 } 483 ctr++; 484 } 485 }); 486 487 if ( $scope.more === true ) { 488 // https://github.com/isteven/angular-multi-select/pull/16 489 if (tempMaxLabels > 0) { 490 $scope.varButtonLabel += ', ... '; 491 } 492 $scope.varButtonLabel += '(Total: ' + $scope.selectedItems.length + ')'; 493 } 494 } 495 $scope.varButtonLabel = $sce.trustAsHtml( $scope.varButtonLabel + '<span class="caret"></span>' ); 496 } 497 498 // Check if a checkbox is disabled or enabled. It will check the granular control (disableProperty) and global control (isDisabled) 499 // Take note that the granular control has higher priority. 500 $scope.itemIsDisabled = function( item ) { 501 502 if ( typeof attrs.disableProperty !== 'undefined' && item[ $scope.disableProperty ] === true ) { 503 return true; 504 } 505 else { 506 if ( $scope.isDisabled === true ) { 507 return true; 508 } 509 else { 510 return false;
511 } 512 } 513 514 } 515 516 // A simple function to parse the item label settings 517 $scope.writeLabel = function( item, type ) { 518 var label = ''; 519 var temp = $scope[ type ].split( ' ' ); 520 angular.forEach( temp, function( value2, key2 ) { 521 if ( typeof value2 !== 'undefined' ) { 522 angular.forEach( item, function( value1, key1 ) { 523 if ( key1 == value2 ) { 524 label += ' ' + value1; 525 } 526 }); 527 } 528 }); 529 if ( type.toUpperCase() === 'BUTTONLABEL' ) { 530 return label; 531 } 532 return $sce.trustAsHtml( label ); 533 } 534 535 // UI operations to show/hide checkboxes based on click event.. 536 $scope.toggleCheckboxes = function( e ) { 537 538 // We grab the checkboxLayer 539 $scope.checkBoxLayer = element.children()[1]; 540 541 // We grab the button 542 clickedEl = element.children()[0]; 543 544 // Just to make sure.. had a bug where key events were recorded twice 545 angular.element( document ).unbind( 'click', $scope.externalClickListener ); 546 angular.element( document ).unbind( 'keydown', $scope.keyboardListener ); 547 548 // clear filter 549 $scope.inputLabel.labelFilter = ''; 550 $scope.updateFilter(); 551 552 // close if ESC key is pressed. 553 if ( e.keyCode === 27 ) { 554 angular.element( $scope.checkBoxLayer ).removeClass( 'show' ); 555 angular.element( clickedEl ).removeClass( 'buttonClicked' ); 556 angular.element( document ).unbind( 'click', $scope.externalClickListener ); 557 angular.element( document ).unbind( 'keydown', $scope.keyboardListener ); 558 559 // clear the focused element; 560 $scope.removeFocusStyle( $scope.tabIndex ); 561 562 // close callback 563 $scope.onClose( { data: element } ); 564 return true; 565 } 566 567 // The idea below was taken from another multi-select directive - https://github.com/amitava82/angular-multiselect 568 // His version is awesome if you need a more simple multi-select approach. 569 570 // close 571 if ( angular.element( $scope.checkBoxLayer ).hasClass( 'show' )) { 572 angular.element( $scope.checkBoxLayer ).removeClass( 'show' ); 573 angular.element( clickedEl ).removeClass( 'buttonClicked' ); 574 angular.element( document ).unbind( 'click', $scope.externalClickListener ); 575 angular.element( document ).unbind( 'keydown', $scope.keyboardListener ); 576 577 // clear the focused element; 578 $scope.removeFocusStyle( $scope.tabIndex ); 579 580 // close callback 581 $scope.onClose( { data: element } ); 582 } 583 // open 584 else 585 { 586 helperItems = []; 587 helperItemsLength = 0; 588 589 angular.element( $scope.checkBoxLayer ).addClass( 'show' ); 590 angular.element( clickedEl ).addClass( 'buttonClicked' ); 591 angular.element( document ).bind( 'click', $scope.externalClickListener ); 592 angular.element( document ).bind( 'keydown', $scope.keyboardListener ); 593 594 // to get the initial tab index, depending on how many helper elements we have. 595 // priority is to always focus it on the input filter 596 $scope.getFormElements(); 597 $scope.tabIndex = 0; 598 599 var helperContainer = angular.element( element[ 0 ].querySelector( '.helperContainer' ) )[0]; 600 601 if ( typeof helperContainer !== 'undefined' ) { 602 for ( i = 0; i < helperContainer.getElementsByTagName( 'BUTTON' ).length ; i++ ) { 603 helperItems[ i ] = helperContainer.getElementsByTagName( 'BUTTON' )[ i ]; 604 } 605 helperItemsLength = helperItems.length + helperContainer.getElementsByTagName( 'INPUT' ).length; 606 } 607 608 // focus on the filter element on open. 609 if ( element[ 0 ].querySelector( '.inputFilter' ) ) { 610 element[ 0 ].querySelector( '.inputFilter' ).focus(); 611 $scope.tabIndex = $scope.tabIndex + helperItemsLength - 2; 612 } 613 // if there's no filter then just focus on the first checkbox item 614 else { 615 $scope.formElements[ $scope.tabIndex ].focus(); 616 } 617 618 // open callback 619 $scope.onOpen( { data: element } ); 620 } 621 } 622 623 // handle clicks outside the button / multi select layer 624 $scope.externalClickListener = function( e ) { 625 targetsArr = element.find( e.target.tagName ); 626 for (var i = 0; i < targetsArr.length; i++) { 627 if ( e.target == targetsArr[i] ) { 628 return; 629 } 630 } 631 632 angular.element( $scope.checkBoxLayer.previousSibling ).removeClass( 'buttonClicked' ); 633 angular.element( $scope.checkBoxLayer ).removeClass( 'show' ); 634 angular.element( document ).unbind( 'click', $scope.externalClickListener ); 635 angular.element( document ).unbind( 'keydown', $scope.keyboardListener ); 636 637 // close callback 638 $timeout( function() { 639 $scope.onClose( { data: element } ); 640 }, 0 ); 641 } 642 643 // traverse up to find the button tag 644 // http://stackoverflow.com/questions/7332179/how-to-recursively-search-all-parentnodes 645 $scope.findUpTag = function ( el, tag, className ) { 646 while ( el.parentNode ) { 647 el = el.parentNode; 648 if ( typeof el.tagName !== 'undefined' ) { 649 if ( el.tagName.toUpperCase() === tag.toUpperCase() && el.className.indexOf( className ) > -1 ) { 650 return el; 651 } 652 } 653 } 654 return null; 655 } 656 657 // select All / select None / reset buttons 658 $scope.select = function( type, e ) { 659 660 helperIndex = helperItems.indexOf( e.target ); 661 $scope.tabIndex = helperIndex; 662 663 switch( type.toUpperCase() ) { 664 case 'ALL': 665 angular.forEach( $scope.filteredModel, function( value, key ) { 666 if ( typeof value !== 'undefined' && value[ $scope.disableProperty ] !== true ) { 667 if ( typeof value[ $scope.groupProperty ] === 'undefined' ) { 668 value[ $scope.tickProperty ] = true; 669 } 670 } 671 }); 672 break; 673 case 'NONE': 674 angular.forEach( $scope.filteredModel, function( value, key ) { 675 if ( typeof value !== 'undefined' && value[ $scope.disableProperty ] !== true ) { 676 if ( typeof value[ $scope.groupProperty ] === 'undefined' ) { 677 value[ $scope.tickProperty ] = false;
678 } 679 } 680 }); 681 break; 682 case 'RESET': 683 angular.forEach( $scope.filteredModel, function( value, key ) { 684 if ( typeof value[ $scope.groupProperty ] === 'undefined' && typeof value !== 'undefined' && value[ $scope.disableProperty ] !== true ) { 685 temp = value[ $scope.indexProperty ]; 686 value[ $scope.tickProperty ] = $scope.backUp[ temp ][ $scope.tickProperty ]; 687 } 688 }); 689 break; 690 case 'CLEAR': 691 $scope.tabIndex = $scope.tabIndex + 1; 692 break; 693 case 'FILTER': 694 $scope.tabIndex = helperItems.length - 1; 695 break; 696 default: 697 } 698 } 699 700 // just to create a random variable name 701 genRandomString = function( length ) { 702 var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 703 var temp = ''; 704 for( var i=0; i < length; i++ ) { 705 temp += possible.charAt( Math.floor( Math.random() * possible.length )); 706 } 707 return temp; 708 } 709 710 // count leading spaces 711 $scope.prepareGrouping = function() { 712 var spacing = 0; 713 angular.forEach( $scope.filteredModel, function( value, key ) { 714 value[ $scope.spacingProperty ] = spacing; 715 if ( value[ $scope.groupProperty ] === true ) { 716 spacing+=2; 717 } 718 else if ( value[ $scope.groupProperty ] === false ) { 719 spacing-=2; 720 } 721 }); 722 } 723 724 // prepare original index 725 $scope.prepareIndex = function() { 726 ctr = 0; 727 angular.forEach( $scope.filteredModel, function( value, key ) { 728 value[ $scope.indexProperty ] = ctr; 729 ctr++; 730 }); 731 } 732 733 // navigate using up and down arrow 734 $scope.keyboardListener = function( e ) { 735 736 var key = e.keyCode ? e.keyCode : e.which; 737 var isNavigationKey = false; 738 739 // ESC key (close) 740 if ( key === 27 ) { 741 $scope.toggleCheckboxes( e ); 742 } 743 744 // next element ( tab, down & right key ) 745 else if ( key === 40 || key === 39 || ( !e.shiftKey && key == 9 ) ) { 746 isNavigationKey = true; 747 prevTabIndex = $scope.tabIndex; 748 $scope.tabIndex++; 749 if ( $scope.tabIndex > $scope.formElements.length - 1 ) { 750 $scope.tabIndex = 0; 751 prevTabIndex = $scope.formElements.length - 1; 752 } 753 while ( $scope.formElements[ $scope.tabIndex ].disabled === true ) { 754 $scope.tabIndex++; 755 if ( $scope.tabIndex > $scope.formElements.length - 1 ) { 756 $scope.tabIndex = 0; 757 } 758 } 759 } 760 761 // prev element ( shift+tab, up & left key ) 762 else if ( key === 38 || key === 37 || ( e.shiftKey && key == 9 ) ) { 763 isNavigationKey = true; 764 prevTabIndex = $scope.tabIndex; 765 $scope.tabIndex--; 766 if ( $scope.tabIndex < 0 ) { 767 $scope.tabIndex = $scope.formElements.length - 1; 768 prevTabIndex = 0; 769 } 770 while ( $scope.formElements[ $scope.tabIndex ].disabled === true ) { 771 $scope.tabIndex--; 772 if ( $scope.tabIndex < 0 ) { 773 $scope.tabIndex = $scope.formElements.length - 1; 774 } 775 } 776 } 777 778 if ( isNavigationKey === true ) { 779 780 e.preventDefault(); 781 e.stopPropagation(); 782 783 // set focus on the checkbox 784 $scope.formElements[ $scope.tabIndex ].focus(); 785 786 // css styling 787 var actEl = document.activeElement; 788 789 if ( actEl.type.toUpperCase() === 'CHECKBOX' ) { 790 $scope.setFocusStyle( $scope.tabIndex ); 791 $scope.removeFocusStyle( prevTabIndex ); 792 } 793 else { 794 $scope.removeFocusStyle( prevTabIndex ); 795 $scope.removeFocusStyle( helperItemsLength ); 796 $scope.removeFocusStyle( $scope.formElements.length - 1 ); 797 } 798 } 799 800 isNavigationKey = false;
801 } 802 803 // set (add) CSS style on selected row 804 $scope.setFocusStyle = function( tabIndex ) { 805 angular.element( $scope.formElements[ tabIndex ] ).parent().parent().parent().addClass( 'multiSelectFocus' ); 806 } 807 808 // remove CSS style on selected row 809 $scope.removeFocusStyle = function( tabIndex ) { 810 angular.element( $scope.formElements[ tabIndex ] ).parent().parent().parent().removeClass( 'multiSelectFocus' ); 811 } 812 813 /////////////////////////////////////////////////////// 814 // 815 // Logic starts here, initiated by watch 1 & watch 2. 816 // 817 /////////////////////////////////////////////////////// 818 819 var tempStr = genRandomString( 5 ); 820 $scope.indexProperty = 'idx_' + tempStr; 821 $scope.spacingProperty = 'spc_' + tempStr; 822 823 // set orientation css 824 if ( typeof attrs.orientation !== 'undefined' ) { 825 if ( attrs.orientation.toUpperCase() === 'HORIZONTAL' ) { 826 $scope.orientationH = true; 827 $scope.orientationV = false; 828 } 829 else { 830 $scope.orientationH = false; 831 $scope.orientationV = true; 832 } 833 } 834 835 // watch1, for changes in input model property 836 // updates multi-select when user select/deselect a single checkbox programatically 837 // https://github.com/isteven/angular-multi-select/issues/8 838 $scope.$watch( 'inputModel' , function( newVal ) { 839 if ( newVal ) { 840 $scope.refreshSelectedItems(); 841 $scope.refreshOutputModel(); 842 $scope.refreshButton(); 843 if ( $scope.clickedItem !== null ) { 844 $timeout( function() { 845 $scope.onItemClick( { data: $scope.clickedItem } ); 846 $scope.clickedItem = null; 847 }, 0 ); 848 } 849 } 850 }, true); 851 852 // watch2 for changes in input model as a whole 853 // this on updates the multi-select when a user load a whole new input-model. We also update the $scope.backUp variable 854 $scope.$watch( 'inputModel' , function( newVal ) { 855 if ( newVal ) { 856 $scope.backUp = angular.copy( $scope.inputModel ); 857 $scope.updateFilter(); 858 $scope.prepareGrouping(); 859 $scope.prepareIndex(); 860 $scope.refreshSelectedItems(); 861 $scope.refreshOutputModel(); 862 $scope.refreshButton(); 863 } 864 }); 865 866 // watch for changes in directive state (disabled or enabled) 867 $scope.$watch( 'isDisabled' , function( newVal ) { 868 $scope.isDisabled = newVal; 869 }); 870 871 // this is for touch enabled devices. We don't want to hide checkboxes on scroll. 872 angular.element( document ).bind( 'touchstart', function( e ) { 873 $scope.$apply( function() { 874 $scope.scrolled = false; 875 }); 876 }); 877 878 // also for touch enabled devices 879 angular.element( document ).bind( 'touchmove', function( e ) { 880 $scope.$apply( function() { 881 $scope.scrolled = true; 882 }); 883 }); 884 885 // for IE8, perhaps. Not sure if this is really executed. 886 if ( !Array.prototype.indexOf ) { 887 Array.prototype.indexOf = function(what, i) { 888 i = i || 0; 889 var L = this.length; 890 while (i < L) { 891 if(this[i] === what) return i; 892 ++i; 893 } 894 return -1; 895 }; 896 } 897 } 898 } 899}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.