isteven-multi-select.js 59 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112
  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: 4.0.0
  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. 'use strict'
  34. angular.module( 'isteven-multi-select', ['ng'] ).directive( 'istevenMultiSelect' , [ '$sce', '$timeout', '$templateCache', function ( $sce, $timeout, $templateCache ) {
  35. return {
  36. restrict:
  37. 'AE',
  38. scope:
  39. {
  40. // models
  41. inputModel : '=',
  42. outputModel : '=',
  43. // settings based on attribute
  44. isDisabled : '=',
  45. // callbacks
  46. onClear : '&',
  47. onClose : '&',
  48. onSearchChange : '&',
  49. onItemClick : '&',
  50. onOpen : '&',
  51. onReset : '&',
  52. onSelectAll : '&',
  53. onSelectNone : '&',
  54. // i18n
  55. translation : '='
  56. },
  57. /*
  58. * The rest are attributes. They don't need to be parsed / binded, so we can safely access them by value.
  59. * - buttonLabel, directiveId, helperElements, itemLabel, maxLabels, orientation, selectionMode, minSearchLength,
  60. * tickProperty, disableProperty, groupProperty, searchProperty, maxHeight, outputProperties
  61. */
  62. templateUrl:
  63. 'isteven-multi-select.htm',
  64. link: function ( $scope, element, attrs ) {
  65. $scope.backUp = [];
  66. $scope.varButtonLabel = '';
  67. $scope.spacingProperty = '';
  68. $scope.indexProperty = '';
  69. $scope.orientationH = false;
  70. $scope.orientationV = true;
  71. $scope.filteredModel = [];
  72. $scope.inputLabel = { labelFilter: '' };
  73. $scope.tabIndex = 0;
  74. $scope.lang = {};
  75. $scope.helperStatus = {
  76. all : true,
  77. none : true,
  78. reset : true,
  79. filter : true
  80. };
  81. var
  82. prevTabIndex = 0,
  83. helperItems = [],
  84. helperItemsLength = 0,
  85. checkBoxLayer = '',
  86. scrolled = false,
  87. selectedItems = [],
  88. formElements = [],
  89. vMinSearchLength = 0,
  90. clickedItem = null
  91. // v3.0.0
  92. // clear button clicked
  93. $scope.clearClicked = function( e ) {
  94. $scope.inputLabel.labelFilter = '';
  95. $scope.updateFilter();
  96. $scope.select( 'clear', e );
  97. }
  98. // A little hack so that AngularJS ng-repeat can loop using start and end index like a normal loop
  99. // http://stackoverflow.com/questions/16824853/way-to-ng-repeat-defined-number-of-times-instead-of-repeating-over-array
  100. $scope.numberToArray = function( num ) {
  101. return new Array( num );
  102. }
  103. // Call this function when user type on the filter field
  104. $scope.searchChanged = function() {
  105. if ( $scope.inputLabel.labelFilter.length < vMinSearchLength && $scope.inputLabel.labelFilter.length > 0 ) {
  106. return false;
  107. }
  108. $scope.updateFilter();
  109. }
  110. $scope.updateFilter = function()
  111. {
  112. // we check by looping from end of input-model
  113. $scope.filteredModel = [];
  114. var i = 0;
  115. if ( typeof $scope.inputModel === 'undefined' ) {
  116. return false;
  117. }
  118. for( i = $scope.inputModel.length - 1; i >= 0; i-- ) {
  119. // if it's group end, we push it to filteredModel[];
  120. if ( typeof $scope.inputModel[ i ][ attrs.groupProperty ] !== 'undefined' && $scope.inputModel[ i ][ attrs.groupProperty ] === false ) {
  121. $scope.filteredModel.push( $scope.inputModel[ i ] );
  122. }
  123. // if it's data
  124. var gotData = false;
  125. if ( typeof $scope.inputModel[ i ][ attrs.groupProperty ] === 'undefined' ) {
  126. // If we set the search-key attribute, we use this loop.
  127. if ( typeof attrs.searchProperty !== 'undefined' && attrs.searchProperty !== '' ) {
  128. for (var key in $scope.inputModel[ i ] ) {
  129. if (
  130. typeof $scope.inputModel[ i ][ key ] !== 'boolean'
  131. && String( $scope.inputModel[ i ][ key ] ).toUpperCase().indexOf( $scope.inputLabel.labelFilter.toUpperCase() ) >= 0
  132. && attrs.searchProperty.indexOf( key ) > -1
  133. ) {
  134. gotData = true;
  135. break;
  136. }
  137. }
  138. }
  139. // if there's no search-key attribute, we use this one. Much better on performance.
  140. else {
  141. for ( var key in $scope.inputModel[ i ] ) {
  142. if (
  143. typeof $scope.inputModel[ i ][ key ] !== 'boolean'
  144. && String( $scope.inputModel[ i ][ key ] ).toUpperCase().indexOf( $scope.inputLabel.labelFilter.toUpperCase() ) >= 0
  145. ) {
  146. gotData = true;
  147. break;
  148. }
  149. }
  150. }
  151. if ( gotData === true ) {
  152. // push
  153. $scope.filteredModel.push( $scope.inputModel[ i ] );
  154. }
  155. }
  156. // if it's group start
  157. if ( typeof $scope.inputModel[ i ][ attrs.groupProperty ] !== 'undefined' && $scope.inputModel[ i ][ attrs.groupProperty ] === true ) {
  158. if ( typeof $scope.filteredModel[ $scope.filteredModel.length - 1 ][ attrs.groupProperty ] !== 'undefined'
  159. && $scope.filteredModel[ $scope.filteredModel.length - 1 ][ attrs.groupProperty ] === false ) {
  160. $scope.filteredModel.pop();
  161. }
  162. else {
  163. $scope.filteredModel.push( $scope.inputModel[ i ] );
  164. }
  165. }
  166. }
  167. $scope.filteredModel.reverse();
  168. $timeout( function() {
  169. $scope.getFormElements();
  170. // Callback: on filter change
  171. if ( $scope.inputLabel.labelFilter.length > vMinSearchLength ) {
  172. var filterObj = [];
  173. angular.forEach( $scope.filteredModel, function( value, key ) {
  174. if ( typeof value !== 'undefined' ) {
  175. if ( typeof value[ attrs.groupProperty ] === 'undefined' ) {
  176. var tempObj = angular.copy( value );
  177. var index = filterObj.push( tempObj );
  178. delete filterObj[ index - 1 ][ $scope.indexProperty ];
  179. delete filterObj[ index - 1 ][ $scope.spacingProperty ];
  180. }
  181. }
  182. });
  183. $scope.onSearchChange({
  184. data:
  185. {
  186. keyword: $scope.inputLabel.labelFilter,
  187. result: filterObj
  188. }
  189. });
  190. }
  191. },0);
  192. };
  193. // List all the input elements. We need this for our keyboard navigation.
  194. // This function will be called everytime the filter is updated.
  195. // Depending on the size of filtered mode, might not good for performance, but oh well..
  196. $scope.getFormElements = function() {
  197. formElements = [];
  198. var
  199. selectButtons = [],
  200. inputField = [],
  201. checkboxes = [],
  202. clearButton = [];
  203. // If available, then get select all, select none, and reset buttons
  204. if ( $scope.helperStatus.all || $scope.helperStatus.none || $scope.helperStatus.reset ) {
  205. selectButtons = element.children().children().next().children().children()[ 0 ].getElementsByTagName( 'button' );
  206. // If available, then get the search box and the clear button
  207. if ( $scope.helperStatus.filter ) {
  208. // Get helper - search and clear button.
  209. inputField = element.children().children().next().children().children().next()[ 0 ].getElementsByTagName( 'input' );
  210. clearButton = element.children().children().next().children().children().next()[ 0 ].getElementsByTagName( 'button' );
  211. }
  212. }
  213. else {
  214. if ( $scope.helperStatus.filter ) {
  215. // Get helper - search and clear button.
  216. inputField = element.children().children().next().children().children()[ 0 ].getElementsByTagName( 'input' );
  217. clearButton = element.children().children().next().children().children()[ 0 ].getElementsByTagName( 'button' );
  218. }
  219. }
  220. // Get checkboxes
  221. if ( !$scope.helperStatus.all && !$scope.helperStatus.none && !$scope.helperStatus.reset && !$scope.helperStatus.filter ) {
  222. checkboxes = element.children().children().next()[ 0 ].getElementsByTagName( 'input' );
  223. }
  224. else {
  225. checkboxes = element.children().children().next().children().next()[ 0 ].getElementsByTagName( 'input' );
  226. }
  227. // Push them into global array formElements[]
  228. for ( var i = 0; i < selectButtons.length ; i++ ) { formElements.push( selectButtons[ i ] ); }
  229. for ( var i = 0; i < inputField.length ; i++ ) { formElements.push( inputField[ i ] ); }
  230. for ( var i = 0; i < clearButton.length ; i++ ) { formElements.push( clearButton[ i ] ); }
  231. for ( var i = 0; i < checkboxes.length ; i++ ) { formElements.push( checkboxes[ i ] ); }
  232. }
  233. // check if an item has attrs.groupProperty (be it true or false)
  234. $scope.isGroupMarker = function( item , type ) {
  235. if ( typeof item[ attrs.groupProperty ] !== 'undefined' && item[ attrs.groupProperty ] === type ) return true;
  236. return false;
  237. }
  238. $scope.removeGroupEndMarker = function( item ) {
  239. if ( typeof item[ attrs.groupProperty ] !== 'undefined' && item[ attrs.groupProperty ] === false ) return false;
  240. return true;
  241. }
  242. // call this function when an item is clicked
  243. $scope.syncItems = function( item, e, ng_repeat_index ) {
  244. e.preventDefault();
  245. e.stopPropagation();
  246. // if the directive is globaly disabled, do nothing
  247. if ( typeof attrs.disableProperty !== 'undefined' && item[ attrs.disableProperty ] === true ) {
  248. return false;
  249. }
  250. // if item is disabled, do nothing
  251. if ( typeof attrs.isDisabled !== 'undefined' && $scope.isDisabled === true ) {
  252. return false;
  253. }
  254. // if end group marker is clicked, do nothing
  255. if ( typeof item[ attrs.groupProperty ] !== 'undefined' && item[ attrs.groupProperty ] === false ) {
  256. return false;
  257. }
  258. var index = $scope.filteredModel.indexOf( item );
  259. // if the start of group marker is clicked ( only for multiple selection! )
  260. // how it works:
  261. // - if, in a group, there are items which are not selected, then they all will be selected
  262. // - if, in a group, all items are selected, then they all will be de-selected
  263. if ( typeof item[ attrs.groupProperty ] !== 'undefined' && item[ attrs.groupProperty ] === true ) {
  264. // this is only for multiple selection, so if selection mode is single, do nothing
  265. if ( typeof attrs.selectionMode !== 'undefined' && attrs.selectionMode.toUpperCase() === 'SINGLE' ) {
  266. return false;
  267. }
  268. var i,j,k;
  269. var startIndex = 0;
  270. var endIndex = $scope.filteredModel.length - 1;
  271. var tempArr = [];
  272. // nest level is to mark the depth of the group.
  273. // when you get into a group (start group marker), nestLevel++
  274. // when you exit a group (end group marker), nextLevel--
  275. var nestLevel = 0;
  276. // we loop throughout the filtered model (not whole model)
  277. for( i = index ; i < $scope.filteredModel.length ; i++) {
  278. // this break will be executed when we're done processing each group
  279. if ( nestLevel === 0 && i > index )
  280. {
  281. break;
  282. }
  283. if ( typeof $scope.filteredModel[ i ][ attrs.groupProperty ] !== 'undefined' && $scope.filteredModel[ i ][ attrs.groupProperty ] === true ) {
  284. // To cater multi level grouping
  285. if ( tempArr.length === 0 ) {
  286. startIndex = i + 1;
  287. }
  288. nestLevel = nestLevel + 1;
  289. }
  290. // if group end
  291. else if ( typeof $scope.filteredModel[ i ][ attrs.groupProperty ] !== 'undefined' && $scope.filteredModel[ i ][ attrs.groupProperty ] === false ) {
  292. nestLevel = nestLevel - 1;
  293. // cek if all are ticked or not
  294. if ( tempArr.length > 0 && nestLevel === 0 ) {
  295. var allTicked = true;
  296. endIndex = i;
  297. for ( j = 0; j < tempArr.length ; j++ ) {
  298. if ( typeof tempArr[ j ][ $scope.tickProperty ] !== 'undefined' && tempArr[ j ][ $scope.tickProperty ] === false ) {
  299. allTicked = false;
  300. break;
  301. }
  302. }
  303. if ( allTicked === true ) {
  304. for ( j = startIndex; j <= endIndex ; j++ ) {
  305. if ( typeof $scope.filteredModel[ j ][ attrs.groupProperty ] === 'undefined' ) {
  306. if ( typeof attrs.disableProperty === 'undefined' ) {
  307. $scope.filteredModel[ j ][ $scope.tickProperty ] = false;
  308. // we refresh input model as well
  309. inputModelIndex = $scope.filteredModel[ j ][ $scope.indexProperty ];
  310. $scope.inputModel[ inputModelIndex ][ $scope.tickProperty ] = false;
  311. }
  312. else if ( $scope.filteredModel[ j ][ attrs.disableProperty ] !== true ) {
  313. $scope.filteredModel[ j ][ $scope.tickProperty ] = false;
  314. // we refresh input model as well
  315. inputModelIndex = $scope.filteredModel[ j ][ $scope.indexProperty ];
  316. $scope.inputModel[ inputModelIndex ][ $scope.tickProperty ] = false;
  317. }
  318. }
  319. }
  320. }
  321. else {
  322. for ( j = startIndex; j <= endIndex ; j++ ) {
  323. if ( typeof $scope.filteredModel[ j ][ attrs.groupProperty ] === 'undefined' ) {
  324. if ( typeof attrs.disableProperty === 'undefined' ) {
  325. $scope.filteredModel[ j ][ $scope.tickProperty ] = true;
  326. // we refresh input model as well
  327. inputModelIndex = $scope.filteredModel[ j ][ $scope.indexProperty ];
  328. $scope.inputModel[ inputModelIndex ][ $scope.tickProperty ] = true;
  329. }
  330. else if ( $scope.filteredModel[ j ][ attrs.disableProperty ] !== true ) {
  331. $scope.filteredModel[ j ][ $scope.tickProperty ] = true;
  332. // we refresh input model as well
  333. inputModelIndex = $scope.filteredModel[ j ][ $scope.indexProperty ];
  334. $scope.inputModel[ inputModelIndex ][ $scope.tickProperty ] = true;
  335. }
  336. }
  337. }
  338. }
  339. }
  340. }
  341. // if data
  342. else {
  343. tempArr.push( $scope.filteredModel[ i ] );
  344. }
  345. }
  346. }
  347. // if an item (not group marker) is clicked
  348. else {
  349. // If it's single selection mode
  350. if ( typeof attrs.selectionMode !== 'undefined' && attrs.selectionMode.toUpperCase() === 'SINGLE' ) {
  351. // first, set everything to false
  352. for( i=0 ; i < $scope.filteredModel.length ; i++) {
  353. $scope.filteredModel[ i ][ $scope.tickProperty ] = false;
  354. }
  355. for( i=0 ; i < $scope.inputModel.length ; i++) {
  356. $scope.inputModel[ i ][ $scope.tickProperty ] = false;
  357. }
  358. // then set the clicked item to true
  359. $scope.filteredModel[ index ][ $scope.tickProperty ] = true;
  360. }
  361. // Multiple
  362. else {
  363. $scope.filteredModel[ index ][ $scope.tickProperty ] = !$scope.filteredModel[ index ][ $scope.tickProperty ];
  364. }
  365. // we refresh input model as well
  366. var inputModelIndex = $scope.filteredModel[ index ][ $scope.indexProperty ];
  367. $scope.inputModel[ inputModelIndex ][ $scope.tickProperty ] = $scope.filteredModel[ index ][ $scope.tickProperty ];
  368. }
  369. // we execute the callback function here
  370. clickedItem = angular.copy( item );
  371. if ( clickedItem !== null ) {
  372. $timeout( function() {
  373. delete clickedItem[ $scope.indexProperty ];
  374. delete clickedItem[ $scope.spacingProperty ];
  375. $scope.onItemClick( { data: clickedItem } );
  376. clickedItem = null;
  377. }, 0 );
  378. }
  379. $scope.refreshOutputModel();
  380. $scope.refreshButton();
  381. // We update the index here
  382. prevTabIndex = $scope.tabIndex;
  383. $scope.tabIndex = ng_repeat_index + helperItemsLength;
  384. // Set focus on the hidden checkbox
  385. e.target.focus();
  386. // set & remove CSS style
  387. $scope.removeFocusStyle( prevTabIndex );
  388. $scope.setFocusStyle( $scope.tabIndex );
  389. if ( typeof attrs.selectionMode !== 'undefined' && attrs.selectionMode.toUpperCase() === 'SINGLE' ) {
  390. // on single selection mode, we then hide the checkbox layer
  391. $scope.toggleCheckboxes( e );
  392. }
  393. }
  394. // update $scope.outputModel
  395. $scope.refreshOutputModel = function() {
  396. $scope.outputModel = [];
  397. var
  398. outputProps = [],
  399. tempObj = {};
  400. // v4.0.0
  401. if ( typeof attrs.outputProperties !== 'undefined' ) {
  402. outputProps = attrs.outputProperties.split(' ');
  403. angular.forEach( $scope.inputModel, function( value, key ) {
  404. if (
  405. typeof value !== 'undefined'
  406. && typeof value[ attrs.groupProperty ] === 'undefined'
  407. && value[ $scope.tickProperty ] === true
  408. ) {
  409. tempObj = {};
  410. angular.forEach( value, function( value1, key1 ) {
  411. if ( outputProps.indexOf( key1 ) > -1 ) {
  412. tempObj[ key1 ] = value1;
  413. }
  414. });
  415. var index = $scope.outputModel.push( tempObj );
  416. delete $scope.outputModel[ index - 1 ][ $scope.indexProperty ];
  417. delete $scope.outputModel[ index - 1 ][ $scope.spacingProperty ];
  418. }
  419. });
  420. }
  421. else {
  422. angular.forEach( $scope.inputModel, function( value, key ) {
  423. if (
  424. typeof value !== 'undefined'
  425. && typeof value[ attrs.groupProperty ] === 'undefined'
  426. && value[ $scope.tickProperty ] === true
  427. ) {
  428. var temp = angular.copy( value );
  429. var index = $scope.outputModel.push( temp );
  430. delete $scope.outputModel[ index - 1 ][ $scope.indexProperty ];
  431. delete $scope.outputModel[ index - 1 ][ $scope.spacingProperty ];
  432. }
  433. });
  434. }
  435. }
  436. // refresh button label
  437. $scope.refreshButton = function() {
  438. $scope.varButtonLabel = '';
  439. var ctr = 0;
  440. // refresh button label...
  441. if ( $scope.outputModel.length === 0 ) {
  442. // https://github.com/isteven/angular-multi-select/pull/19
  443. $scope.varButtonLabel = $scope.lang.nothingSelected;
  444. }
  445. else {
  446. var tempMaxLabels = $scope.outputModel.length;
  447. if ( typeof attrs.maxLabels !== 'undefined' && attrs.maxLabels !== '' ) {
  448. tempMaxLabels = attrs.maxLabels;
  449. }
  450. // if max amount of labels displayed..
  451. if ( $scope.outputModel.length > tempMaxLabels ) {
  452. $scope.more = true;
  453. }
  454. else {
  455. $scope.more = false;
  456. }
  457. angular.forEach( $scope.inputModel, function( value, key ) {
  458. if ( typeof value !== 'undefined' && value[ attrs.tickProperty ] === true ) {
  459. if ( ctr < tempMaxLabels ) {
  460. $scope.varButtonLabel += ( $scope.varButtonLabel.length > 0 ? '</div>, <div class="buttonLabel">' : '<div class="buttonLabel">') + $scope.writeLabel( value, 'buttonLabel' );
  461. }
  462. ctr++;
  463. }
  464. });
  465. if ( $scope.more === true ) {
  466. // https://github.com/isteven/angular-multi-select/pull/16
  467. if (tempMaxLabels > 0) {
  468. $scope.varButtonLabel += ', ... ';
  469. }
  470. $scope.varButtonLabel += '(' + $scope.outputModel.length + ')';
  471. }
  472. }
  473. $scope.varButtonLabel = $sce.trustAsHtml( $scope.varButtonLabel + '<span class="caret"></span>' );
  474. }
  475. // Check if a checkbox is disabled or enabled. It will check the granular control (disableProperty) and global control (isDisabled)
  476. // Take note that the granular control has higher priority.
  477. $scope.itemIsDisabled = function( item ) {
  478. if ( typeof attrs.disableProperty !== 'undefined' && item[ attrs.disableProperty ] === true ) {
  479. return true;
  480. }
  481. else {
  482. if ( $scope.isDisabled === true ) {
  483. return true;
  484. }
  485. else {
  486. return false;
  487. }
  488. }
  489. }
  490. // A simple function to parse the item label settings. Used on the buttons and checkbox labels.
  491. $scope.writeLabel = function( item, type ) {
  492. // type is either 'itemLabel' or 'buttonLabel'
  493. var temp = attrs[ type ].split( ' ' );
  494. var label = '';
  495. angular.forEach( temp, function( value, key ) {
  496. item[ value ] && ( label += '&nbsp;' + value.split( '.' ).reduce( function( prev, current ) {
  497. return prev[ current ];
  498. }, item ));
  499. });
  500. if ( type.toUpperCase() === 'BUTTONLABEL' ) {
  501. return label;
  502. }
  503. return $sce.trustAsHtml( label );
  504. }
  505. // UI operations to show/hide checkboxes based on click event..
  506. $scope.toggleCheckboxes = function( e ) {
  507. // We grab the button
  508. var clickedEl = element.children()[0];
  509. // Just to make sure.. had a bug where key events were recorded twice
  510. angular.element( document ).off( 'click', $scope.externalClickListener );
  511. angular.element( document ).off( 'keydown', $scope.keyboardListener );
  512. // The idea below was taken from another multi-select directive - https://github.com/amitava82/angular-multiselect
  513. // His version is awesome if you need a more simple multi-select approach.
  514. // close
  515. if ( angular.element( checkBoxLayer ).hasClass( 'show' )) {
  516. angular.element( checkBoxLayer ).removeClass( 'show' );
  517. angular.element( clickedEl ).removeClass( 'buttonClicked' );
  518. angular.element( document ).off( 'click', $scope.externalClickListener );
  519. angular.element( document ).off( 'keydown', $scope.keyboardListener );
  520. // clear the focused element;
  521. $scope.removeFocusStyle( $scope.tabIndex );
  522. if ( typeof formElements[ $scope.tabIndex ] !== 'undefined' ) {
  523. formElements[ $scope.tabIndex ].blur();
  524. }
  525. // close callback
  526. $timeout( function() {
  527. $scope.onClose();
  528. }, 0 );
  529. // set focus on button again
  530. element.children().children()[ 0 ].focus();
  531. }
  532. // open
  533. else
  534. {
  535. // clear filter
  536. $scope.inputLabel.labelFilter = '';
  537. $scope.updateFilter();
  538. helperItems = [];
  539. helperItemsLength = 0;
  540. angular.element( checkBoxLayer ).addClass( 'show' );
  541. angular.element( clickedEl ).addClass( 'buttonClicked' );
  542. // Attach change event listener on the input filter.
  543. // We need this because ng-change is apparently not an event listener.
  544. angular.element( document ).on( 'click', $scope.externalClickListener );
  545. angular.element( document ).on( 'keydown', $scope.keyboardListener );
  546. // to get the initial tab index, depending on how many helper elements we have.
  547. // priority is to always focus it on the input filter
  548. $scope.getFormElements();
  549. $scope.tabIndex = 0;
  550. var helperContainer = angular.element( element[ 0 ].querySelector( '.helperContainer' ) )[0];
  551. if ( typeof helperContainer !== 'undefined' ) {
  552. for ( var i = 0; i < helperContainer.getElementsByTagName( 'BUTTON' ).length ; i++ ) {
  553. helperItems[ i ] = helperContainer.getElementsByTagName( 'BUTTON' )[ i ];
  554. }
  555. helperItemsLength = helperItems.length + helperContainer.getElementsByTagName( 'INPUT' ).length;
  556. }
  557. // focus on the filter element on open.
  558. if ( element[ 0 ].querySelector( '.inputFilter' ) ) {
  559. element[ 0 ].querySelector( '.inputFilter' ).focus();
  560. $scope.tabIndex = $scope.tabIndex + helperItemsLength - 2;
  561. // blur button in vain
  562. angular.element( element ).children()[ 0 ].blur();
  563. }
  564. // if there's no filter then just focus on the first checkbox item
  565. else {
  566. if ( !$scope.isDisabled ) {
  567. $scope.tabIndex = $scope.tabIndex + helperItemsLength;
  568. if ( $scope.inputModel.length > 0 ) {
  569. formElements[ $scope.tabIndex ].focus();
  570. $scope.setFocusStyle( $scope.tabIndex );
  571. // blur button in vain
  572. angular.element( element ).children()[ 0 ].blur();
  573. }
  574. }
  575. }
  576. // open callback
  577. $scope.onOpen();
  578. }
  579. }
  580. // handle clicks outside the button / multi select layer
  581. $scope.externalClickListener = function( e ) {
  582. var targetsArr = element.find( e.target.tagName );
  583. for (var i = 0; i < targetsArr.length; i++) {
  584. if ( e.target == targetsArr[i] ) {
  585. return;
  586. }
  587. }
  588. angular.element( checkBoxLayer.previousSibling ).removeClass( 'buttonClicked' );
  589. angular.element( checkBoxLayer ).removeClass( 'show' );
  590. angular.element( document ).off( 'click', $scope.externalClickListener );
  591. angular.element( document ).off( 'keydown', $scope.keyboardListener );
  592. // close callback
  593. $timeout( function() {
  594. $scope.onClose();
  595. }, 0 );
  596. // set focus on button again
  597. element.children().children()[ 0 ].focus();
  598. }
  599. // select All / select None / reset buttons
  600. $scope.select = function( type, e ) {
  601. var helperIndex = helperItems.indexOf( e.target );
  602. $scope.tabIndex = helperIndex;
  603. switch( type.toUpperCase() ) {
  604. case 'ALL':
  605. angular.forEach( $scope.filteredModel, function( value, key ) {
  606. if ( typeof value !== 'undefined' && value[ attrs.disableProperty ] !== true ) {
  607. if ( typeof value[ attrs.groupProperty ] === 'undefined' ) {
  608. value[ $scope.tickProperty ] = true;
  609. }
  610. }
  611. });
  612. $scope.refreshOutputModel();
  613. $scope.refreshButton();
  614. $scope.onSelectAll();
  615. break;
  616. case 'NONE':
  617. angular.forEach( $scope.filteredModel, function( value, key ) {
  618. if ( typeof value !== 'undefined' && value[ attrs.disableProperty ] !== true ) {
  619. if ( typeof value[ attrs.groupProperty ] === 'undefined' ) {
  620. value[ $scope.tickProperty ] = false;
  621. }
  622. }
  623. });
  624. $scope.refreshOutputModel();
  625. $scope.refreshButton();
  626. $scope.onSelectNone();
  627. break;
  628. case 'RESET':
  629. angular.forEach( $scope.filteredModel, function( value, key ) {
  630. if ( typeof value[ attrs.groupProperty ] === 'undefined' && typeof value !== 'undefined' && value[ attrs.disableProperty ] !== true ) {
  631. var temp = value[ $scope.indexProperty ];
  632. value[ $scope.tickProperty ] = $scope.backUp[ temp ][ $scope.tickProperty ];
  633. }
  634. });
  635. $scope.refreshOutputModel();
  636. $scope.refreshButton();
  637. $scope.onReset();
  638. break;
  639. case 'CLEAR':
  640. $scope.tabIndex = $scope.tabIndex + 1;
  641. $scope.onClear();
  642. break;
  643. case 'FILTER':
  644. $scope.tabIndex = helperItems.length - 1;
  645. break;
  646. default:
  647. }
  648. }
  649. // just to create a random variable name
  650. function genRandomString( length ) {
  651. var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
  652. var temp = '';
  653. for( var i=0; i < length; i++ ) {
  654. temp += possible.charAt( Math.floor( Math.random() * possible.length ));
  655. }
  656. return temp;
  657. }
  658. // count leading spaces
  659. $scope.prepareGrouping = function() {
  660. var spacing = 0;
  661. angular.forEach( $scope.filteredModel, function( value, key ) {
  662. value[ $scope.spacingProperty ] = spacing;
  663. if ( value[ attrs.groupProperty ] === true ) {
  664. spacing+=2;
  665. }
  666. else if ( value[ attrs.groupProperty ] === false ) {
  667. spacing-=2;
  668. }
  669. });
  670. }
  671. // prepare original index
  672. $scope.prepareIndex = function() {
  673. var ctr = 0;
  674. angular.forEach( $scope.filteredModel, function( value, key ) {
  675. value[ $scope.indexProperty ] = ctr;
  676. ctr++;
  677. });
  678. }
  679. // navigate using up and down arrow
  680. $scope.keyboardListener = function( e ) {
  681. var key = e.keyCode ? e.keyCode : e.which;
  682. var isNavigationKey = false;
  683. // ESC key (close)
  684. if ( key === 27 ) {
  685. e.preventDefault();
  686. e.stopPropagation();
  687. $scope.toggleCheckboxes( e );
  688. }
  689. // next element ( tab, down & right key )
  690. else if ( key === 40 || key === 39 || ( !e.shiftKey && key == 9 ) ) {
  691. isNavigationKey = true;
  692. prevTabIndex = $scope.tabIndex;
  693. $scope.tabIndex++;
  694. if ( $scope.tabIndex > formElements.length - 1 ) {
  695. $scope.tabIndex = 0;
  696. prevTabIndex = formElements.length - 1;
  697. }
  698. while ( formElements[ $scope.tabIndex ].disabled === true ) {
  699. $scope.tabIndex++;
  700. if ( $scope.tabIndex > formElements.length - 1 ) {
  701. $scope.tabIndex = 0;
  702. }
  703. if ( $scope.tabIndex === prevTabIndex ) {
  704. break;
  705. }
  706. }
  707. }
  708. // prev element ( shift+tab, up & left key )
  709. else if ( key === 38 || key === 37 || ( e.shiftKey && key == 9 ) ) {
  710. isNavigationKey = true;
  711. prevTabIndex = $scope.tabIndex;
  712. $scope.tabIndex--;
  713. if ( $scope.tabIndex < 0 ) {
  714. $scope.tabIndex = formElements.length - 1;
  715. prevTabIndex = 0;
  716. }
  717. while ( formElements[ $scope.tabIndex ].disabled === true ) {
  718. $scope.tabIndex--;
  719. if ( $scope.tabIndex === prevTabIndex ) {
  720. break;
  721. }
  722. if ( $scope.tabIndex < 0 ) {
  723. $scope.tabIndex = formElements.length - 1;
  724. }
  725. }
  726. }
  727. if ( isNavigationKey === true ) {
  728. e.preventDefault();
  729. // set focus on the checkbox
  730. formElements[ $scope.tabIndex ].focus();
  731. var actEl = document.activeElement;
  732. if ( actEl.type.toUpperCase() === 'CHECKBOX' ) {
  733. $scope.setFocusStyle( $scope.tabIndex );
  734. $scope.removeFocusStyle( prevTabIndex );
  735. }
  736. else {
  737. $scope.removeFocusStyle( prevTabIndex );
  738. $scope.removeFocusStyle( helperItemsLength );
  739. $scope.removeFocusStyle( formElements.length - 1 );
  740. }
  741. }
  742. isNavigationKey = false;
  743. }
  744. // set (add) CSS style on selected row
  745. $scope.setFocusStyle = function( tabIndex ) {
  746. angular.element( formElements[ tabIndex ] ).parent().parent().parent().addClass( 'multiSelectFocus' );
  747. }
  748. // remove CSS style on selected row
  749. $scope.removeFocusStyle = function( tabIndex ) {
  750. angular.element( formElements[ tabIndex ] ).parent().parent().parent().removeClass( 'multiSelectFocus' );
  751. }
  752. /*********************
  753. *********************
  754. *
  755. * 1) Initializations
  756. *
  757. *********************
  758. *********************/
  759. // attrs to $scope - attrs-$scope - attrs - $scope
  760. // Copy some properties that will be used on the template. They need to be in the $scope.
  761. $scope.groupProperty = attrs.groupProperty;
  762. $scope.tickProperty = attrs.tickProperty;
  763. $scope.directiveId = attrs.directiveId;
  764. // Unfortunately I need to add these grouping properties into the input model
  765. var tempStr = genRandomString( 5 );
  766. $scope.indexProperty = 'idx_' + tempStr;
  767. $scope.spacingProperty = 'spc_' + tempStr;
  768. // set orientation css
  769. if ( typeof attrs.orientation !== 'undefined' ) {
  770. if ( attrs.orientation.toUpperCase() === 'HORIZONTAL' ) {
  771. $scope.orientationH = true;
  772. $scope.orientationV = false;
  773. }
  774. else
  775. {
  776. $scope.orientationH = false;
  777. $scope.orientationV = true;
  778. }
  779. }
  780. // get elements required for DOM operation
  781. checkBoxLayer = element.children().children().next()[0];
  782. // set max-height property if provided
  783. if ( typeof attrs.maxHeight !== 'undefined' ) {
  784. var layer = element.children().children().children()[0];
  785. angular.element( layer ).attr( "style", "height:" + attrs.maxHeight + "; overflow-y:scroll;" );
  786. }
  787. // some flags for easier checking
  788. for ( var property in $scope.helperStatus ) {
  789. if ( $scope.helperStatus.hasOwnProperty( property )) {
  790. if (
  791. typeof attrs.helperElements !== 'undefined'
  792. && attrs.helperElements.toUpperCase().indexOf( property.toUpperCase() ) === -1
  793. ) {
  794. $scope.helperStatus[ property ] = false;
  795. }
  796. }
  797. }
  798. if ( typeof attrs.selectionMode !== 'undefined' && attrs.selectionMode.toUpperCase() === 'SINGLE' ) {
  799. $scope.helperStatus[ 'all' ] = false;
  800. $scope.helperStatus[ 'none' ] = false;
  801. }
  802. // helper button icons.. I guess you can use html tag here if you want to.
  803. $scope.icon = {};
  804. $scope.icon.selectAll = '&#10003;'; // a tick icon
  805. $scope.icon.selectNone = '&times;'; // x icon
  806. $scope.icon.reset = '&#8630;'; // undo icon
  807. // this one is for the selected items
  808. $scope.icon.tickMark = '&#10003;'; // a tick icon
  809. // configurable button labels
  810. if ( typeof attrs.translation !== 'undefined' ) {
  811. $scope.lang.selectAll = $sce.trustAsHtml( $scope.icon.selectAll + '&nbsp;&nbsp;' + $scope.translation.selectAll );
  812. $scope.lang.selectNone = $sce.trustAsHtml( $scope.icon.selectNone + '&nbsp;&nbsp;' + $scope.translation.selectNone );
  813. $scope.lang.reset = $sce.trustAsHtml( $scope.icon.reset + '&nbsp;&nbsp;' + $scope.translation.reset );
  814. $scope.lang.search = $scope.translation.search;
  815. $scope.lang.nothingSelected = $sce.trustAsHtml( $scope.translation.nothingSelected );
  816. }
  817. else {
  818. $scope.lang.selectAll = $sce.trustAsHtml( $scope.icon.selectAll + '&nbsp;&nbsp;Select All' );
  819. $scope.lang.selectNone = $sce.trustAsHtml( $scope.icon.selectNone + '&nbsp;&nbsp;Select None' );
  820. $scope.lang.reset = $sce.trustAsHtml( $scope.icon.reset + '&nbsp;&nbsp;Reset' );
  821. $scope.lang.search = 'Search...';
  822. $scope.lang.nothingSelected = 'None Selected';
  823. }
  824. $scope.icon.tickMark = $sce.trustAsHtml( $scope.icon.tickMark );
  825. // min length of keyword to trigger the filter function
  826. if ( typeof attrs.MinSearchLength !== 'undefined' && parseInt( attrs.MinSearchLength ) > 0 ) {
  827. vMinSearchLength = Math.floor( parseInt( attrs.MinSearchLength ) );
  828. }
  829. /*******************************************************
  830. *******************************************************
  831. *
  832. * 2) Logic starts here, initiated by watch 1 & watch 2
  833. *
  834. *******************************************************
  835. *******************************************************/
  836. // watch1, for changes in input model property
  837. // updates multi-select when user select/deselect a single checkbox programatically
  838. // https://github.com/isteven/angular-multi-select/issues/8
  839. $scope.$watch( 'inputModel' , function( newVal ) {
  840. if ( newVal ) {
  841. $scope.refreshOutputModel();
  842. $scope.refreshButton();
  843. }
  844. }, true );
  845. // watch2 for changes in input model as a whole
  846. // this on updates the multi-select when a user load a whole new input-model. We also update the $scope.backUp variable
  847. $scope.$watch( 'inputModel' , function( newVal ) {
  848. if ( newVal ) {
  849. $scope.backUp = angular.copy( $scope.inputModel );
  850. $scope.updateFilter();
  851. $scope.prepareGrouping();
  852. $scope.prepareIndex();
  853. $scope.refreshOutputModel();
  854. $scope.refreshButton();
  855. }
  856. });
  857. // watch for changes in directive state (disabled or enabled)
  858. $scope.$watch( 'isDisabled' , function( newVal ) {
  859. $scope.isDisabled = newVal;
  860. });
  861. // this is for touch enabled devices. We don't want to hide checkboxes on scroll.
  862. var onTouchStart = function( e ) {
  863. $scope.$apply( function() {
  864. $scope.scrolled = false;
  865. });
  866. };
  867. angular.element( document ).bind( 'touchstart', onTouchStart);
  868. var onTouchMove = function( e ) {
  869. $scope.$apply( function() {
  870. $scope.scrolled = true;
  871. });
  872. };
  873. angular.element( document ).bind( 'touchmove', onTouchMove);
  874. // unbind document events to prevent memory leaks
  875. $scope.$on( '$destroy', function () {
  876. angular.element( document ).unbind( 'touchstart', onTouchStart);
  877. angular.element( document ).unbind( 'touchmove', onTouchMove);
  878. });
  879. }
  880. }
  881. }]).run( [ '$templateCache' , function( $templateCache ) {
  882. var template =
  883. '<span class="multiSelect inlineBlock">' +
  884. // main button
  885. '<button id="{{directiveId}}" type="button"' +
  886. 'ng-click="toggleCheckboxes( $event ); refreshSelectedItems(); refreshButton(); prepareGrouping; prepareIndex();"' +
  887. 'ng-bind-html="varButtonLabel"' +
  888. 'ng-disabled="disable-button"' +
  889. '>' +
  890. '</button>' +
  891. // overlay layer
  892. '<div class="checkboxLayer">' +
  893. // container of the helper elements
  894. '<div class="helperContainer" ng-if="helperStatus.filter || helperStatus.all || helperStatus.none || helperStatus.reset ">' +
  895. // container of the first 3 buttons, select all, none and reset
  896. '<div class="line" ng-if="helperStatus.all || helperStatus.none || helperStatus.reset ">' +
  897. // select all
  898. '<button type="button" class="helperButton"' +
  899. 'ng-disabled="isDisabled"' +
  900. 'ng-if="helperStatus.all"' +
  901. 'ng-click="select( \'all\', $event );"' +
  902. 'ng-bind-html="lang.selectAll">' +
  903. '</button>'+
  904. // select none
  905. '<button type="button" class="helperButton"' +
  906. 'ng-disabled="isDisabled"' +
  907. 'ng-if="helperStatus.none"' +
  908. 'ng-click="select( \'none\', $event );"' +
  909. 'ng-bind-html="lang.selectNone">' +
  910. '</button>'+
  911. // reset
  912. '<button type="button" class="helperButton reset"' +
  913. 'ng-disabled="isDisabled"' +
  914. 'ng-if="helperStatus.reset"' +
  915. 'ng-click="select( \'reset\', $event );"' +
  916. 'ng-bind-html="lang.reset">'+
  917. '</button>' +
  918. '</div>' +
  919. // the search box
  920. '<div class="line" style="position:relative" ng-if="helperStatus.filter">'+
  921. // textfield
  922. '<input placeholder="{{lang.search}}" type="text"' +
  923. 'ng-click="select( \'filter\', $event )" '+
  924. 'ng-model="inputLabel.labelFilter" '+
  925. 'ng-change="searchChanged()" class="inputFilter"'+
  926. '/>'+
  927. // clear button
  928. '<button type="button" class="clearButton" ng-click="clearClicked( $event )" >×</button> '+
  929. '</div> '+
  930. '</div> '+
  931. // selection items
  932. '<div class="checkBoxContainer">'+
  933. '<div '+
  934. 'ng-repeat="item in filteredModel | filter:removeGroupEndMarker" class="multiSelectItem"'+
  935. 'ng-class="{selected: item[ tickProperty ], horizontal: orientationH, vertical: orientationV, multiSelectGroup:item[ groupProperty ], disabled:itemIsDisabled( item )}"'+
  936. 'ng-click="syncItems( item, $event, $index );" '+
  937. 'ng-mouseleave="removeFocusStyle( tabIndex );"> '+
  938. // this is the spacing for grouped items
  939. '<div class="acol" ng-if="item[ spacingProperty ] > 0" ng-repeat="i in numberToArray( item[ spacingProperty ] ) track by $index">'+
  940. '</div> '+
  941. '<div class="acol">'+
  942. '<label>'+
  943. // input, so that it can accept focus on keyboard click
  944. '<input class="checkbox focusable" type="checkbox" '+
  945. 'ng-disabled="itemIsDisabled( item )" '+
  946. 'ng-checked="item[ tickProperty ]" '+
  947. 'ng-click="syncItems( item, $event, $index )" />'+
  948. // item label using ng-bind-hteml
  949. '<span '+
  950. 'ng-class="{disabled:itemIsDisabled( item )}" '+
  951. 'ng-bind-html="writeLabel( item, \'itemLabel\' )">'+
  952. '</span>'+
  953. '</label>'+
  954. '</div>'+
  955. // the tick/check mark
  956. '<span class="tickMark" ng-if="item[ groupProperty ] !== true && item[ tickProperty ] === true" ng-bind-html="icon.tickMark"></span>'+
  957. '</div>'+
  958. '</div>'+
  959. '</div>'+
  960. '</span>';
  961. $templateCache.put( 'isteven-multi-select.htm' , template );
  962. }]);