├── .gitignore ├── others ├── Basic_example.gif ├── Custom_example.gif ├── Using_Promise_obj_example.gif ├── Using_typeahead_example2.gif └── Using_Promise_string_example.gif ├── samples ├── app.js ├── samples.css ├── custom_rendering_example.js ├── using_promise_str_example.js ├── basic_example.js ├── using_promise_obj_example.js └── index.html ├── .travis.yml ├── test ├── utils.js ├── custom_rendering_flow_spec.js ├── promise_string_flow_spec.js ├── chip_with_typeahead_flow_spec.js ├── promise_obj_flow_spec.js └── basic_flow_spec.js ├── src ├── js │ ├── directives │ │ ├── chip_tmpl.js │ │ ├── controls │ │ │ ├── chip_control.js │ │ │ └── ng_model_control.js │ │ ├── remove_chip.js │ │ └── chips.js │ └── utils │ │ └── dom_util.js └── css │ └── main.scss ├── dist ├── main.css ├── angular-chips.min.js └── angular-chips.js ├── bower.json ├── LICENSE ├── package.json ├── gulpfile.js ├── karma.config.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | bower_components/ 4 | coverage/ 5 | -------------------------------------------------------------------------------- /others/Basic_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohbasheer/angular-chips/HEAD/others/Basic_example.gif -------------------------------------------------------------------------------- /samples/app.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | angular.module('sample',['angular.chips','ui.bootstrap']); 3 | })(); 4 | -------------------------------------------------------------------------------- /others/Custom_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohbasheer/angular-chips/HEAD/others/Custom_example.gif -------------------------------------------------------------------------------- /others/Using_Promise_obj_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohbasheer/angular-chips/HEAD/others/Using_Promise_obj_example.gif -------------------------------------------------------------------------------- /others/Using_typeahead_example2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohbasheer/angular-chips/HEAD/others/Using_typeahead_example2.gif -------------------------------------------------------------------------------- /others/Using_Promise_string_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohbasheer/angular-chips/HEAD/others/Using_Promise_string_example.gif -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.2" 4 | before_script: 5 | - npm install -g gulp 6 | - npm install -g bower 7 | - bower install 8 | script: gulp test 9 | -------------------------------------------------------------------------------- /samples/samples.css: -------------------------------------------------------------------------------- 1 | body{ 2 | margin: 10px; 3 | } 4 | 5 | .loader-container { 6 | position: relative; 7 | } 8 | 9 | .loader-container .loader { 10 | position: absolute; 11 | right: 50%; 12 | bottom: 3px 13 | } 14 | 15 | .printvalue{ 16 | margin: 10px; 17 | } 18 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | function getChipScope(element, index) { 2 | var elements = element.find('chip-tmpl'); 3 | index = index > 0 ? index : (index < 0 ? elements.length - 1 : 0) 4 | return angular.element(elements[index]).scope(); 5 | } 6 | 7 | function getChipTmpl(element, index) { 8 | var chipTmpls = element.find('chip-tmpl'); 9 | return chipTmpls[index === undefined ? chipTmpls.length - 1 : index]; 10 | } 11 | -------------------------------------------------------------------------------- /samples/custom_rendering_example.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | angular.module('sample') 3 | .controller('customRenderingController', CustomRendering); 4 | 5 | function CustomRendering() { 6 | /*list of countries and it's first letter*/ 7 | this.countries = [{ name: 'India', fl: 'I' }, { name: 'China', fl: 'C' }, { name: 'America', fl: 'A' }]; 8 | /*call back method for chip*/ 9 | this.render = function(val) { 10 | return { name: val, fl: val.charAt(0) } 11 | }; 12 | /*call back method for chip delete*/ 13 | this.deleteChip = function(val) { 14 | return true; 15 | } 16 | } 17 | })(); 18 | -------------------------------------------------------------------------------- /src/js/directives/chip_tmpl.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | angular.module('angular.chips') 3 | .directive('chipTmpl', ChipTmpl); 4 | 5 | function ChipTmpl() { 6 | return { 7 | restrict: 'E', 8 | transclude: true, 9 | link: function(scope, iElement, iAttrs, contrl, transcludefn) { 10 | transcludefn(scope, function(clonedTranscludedContent) { 11 | iElement.append(clonedTranscludedContent); 12 | }); 13 | iElement.on('keydown', function(event) { 14 | if (event.keyCode === 8) { 15 | scope.$broadcast('chip:delete'); 16 | event.preventDefault(); 17 | } 18 | }); 19 | } 20 | } 21 | } 22 | })(); 23 | -------------------------------------------------------------------------------- /dist/main.css: -------------------------------------------------------------------------------- 1 | chip-tmpl { 2 | display: inline-block; 3 | margin: 0 5px 5px 0; } 4 | 5 | .default-chip { 6 | border: 2px solid #e0e0e0; 7 | border-radius: 5px; 8 | background: #e0e0e0; 9 | padding: 5px; } 10 | 11 | chip-tmpl:focus { 12 | outline: none; } 13 | chip-tmpl:focus .default-chip { 14 | border: 2px solid #9e9e9e; 15 | background: #9e9e9e; 16 | color: white; } 17 | 18 | .chip-failed .default-chip { 19 | color: red; } 20 | 21 | chips { 22 | display: block; 23 | padding: 5px; } 24 | chips > div { 25 | display: inline; } 26 | chips > div > input { 27 | height: 100%; 28 | border: none; 29 | font-size: 14px; } 30 | chips > div > input:focus { 31 | outline: none; } 32 | 33 | .chip-out-focus { 34 | border-bottom: 1px solid #e0e0e0; } 35 | 36 | .chip-in-focus { 37 | border-bottom: 1px solid #106cc8; } 38 | -------------------------------------------------------------------------------- /samples/using_promise_str_example.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | angular.module('sample') 3 | .controller('usingPromiseStrController', UsingPromiseStrController); 4 | 5 | function UsingPromiseStrController($scope, $q) { 6 | var self = this; 7 | self.list = ['orange', 'apple', 'grapes']; 8 | /*call back method for chip*/ 9 | self.render = function(val) { 10 | var deferred = $q.defer(); 11 | setTimeout(function() { 12 | self.list.indexOf(val) === -1 ? deferred.resolve(val) : deferred.reject(val); 13 | }, getDelay()); 14 | return deferred.promise; 15 | }; 16 | /*call back method for chip delete*/ 17 | self.deleteChip = function(val) { 18 | return true; 19 | } 20 | 21 | function getDelay(){ 22 | return (Math.floor(Math.random() * 5) + 1) * 1000; 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /src/js/directives/controls/chip_control.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | angular.module('angular.chips') 3 | .directive('chipControl', ChipControl); 4 | 5 | /* 6 | * It's for normal input element 7 | * It send the value to chips directive when press the enter button 8 | */ 9 | function ChipControl() { 10 | return { 11 | restrict: 'A', 12 | require: '^chips', 13 | link: ChipControlLinkFun, 14 | } 15 | }; 16 | /*@ngInject*/ 17 | function ChipControlLinkFun(scope, iElement, iAttrs, chipsCtrl) { 18 | iElement.on('keypress', function(event) { 19 | if (event.keyCode === 13) { 20 | if (event.target.value !== '' && chipsCtrl.addChip(event.target.value)) { 21 | event.target.value = ""; 22 | } 23 | event.preventDefault(); 24 | } 25 | }); 26 | 27 | iElement.on('focus', function() { 28 | chipsCtrl.setFocus(true); 29 | }); 30 | iElement.on('blur', function() { 31 | chipsCtrl.setFocus(false); 32 | }); 33 | }; 34 | })(); 35 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-chips", 3 | "version": "1.0.11", 4 | "homepage": "https://github.com/mohbasheer/angular-chips", 5 | "authors": [ 6 | "Mohammed Basheer" 7 | ], 8 | "description": "Angular-Chips is the angular based component. You can use it to add dynamic chips", 9 | "main": [ 10 | "./dist/angular-chips.js", 11 | "./dist/main.css" 12 | ], 13 | "keywords": [ 14 | "angular", 15 | "chips", 16 | "multiselect", 17 | "single select", 18 | "chip with promise", 19 | "chips with promise", 20 | "tags", 21 | "inline tagging", 22 | "inline tag", 23 | "free form tags", 24 | "promise based tags", 25 | "promise based chip" 26 | ], 27 | "license": "MIT", 28 | "ignore": [ 29 | "**/.*", 30 | "node_modules", 31 | "bower_components", 32 | "test" 33 | ], 34 | "devDependencies": { 35 | "angular-bootstrap": "~1.2.1", 36 | "components-font-awesome": "~4.5.0", 37 | "bootstrap": "~3.3.6", 38 | "angular-mocks": "1.4.8" 39 | }, 40 | "dependencies": { 41 | "angular": "v1.5.*" 42 | }, 43 | "resolutions": { 44 | "angular": "v1.5.*" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mohammed Basheer (ssp.basheer@gmail.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /samples/basic_example.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | angular.module('sample') 3 | .controller('basicController', BasicController); 4 | 5 | function BasicController() { 6 | /*for basic example*/ 7 | this.companies = ['Apple', 'Cisco', 'Verizon', 'Microsoft']; 8 | 9 | /*for bootstrap.ui.typeahead example*/ 10 | this.availableCompanies = ['ACCO Brands', 11 | 'Accuquote', 12 | 'Accuride Corporation', 13 | 'Ace Hardware', 14 | 'Google', 15 | 'FaceBook', 16 | 'Paypal', 17 | 'Pramati', 18 | 'Bennigan', 19 | 'Berkshire Hathaway', 20 | 'Berry Plastics', 21 | 'Best Buy', 22 | 'Carlisle Companies', 23 | 'Carlson Companies', 24 | 'Carlyle Group', 25 | 'Denbury Resources', 26 | 'Denny', 27 | 'Dentsply', 28 | 'Ebonite International', 29 | 'EBSCO Industries', 30 | 'EchoStar', 31 | 'Gateway, Inc.', 32 | 'Gatorade', 33 | 'Home Shopping Network', 34 | 'Honeywell', 35 | ]; 36 | } 37 | })(); 38 | -------------------------------------------------------------------------------- /src/css/main.scss: -------------------------------------------------------------------------------- 1 | $default-color: rgb(224, 224, 224); 2 | $focus-color: rgb(16, 108, 200); 3 | $chip-tmpl-focus-color: rgb(158, 158, 158); 4 | 5 | chip-tmpl { 6 | display: inline-block; 7 | margin: 0 5px 5px 0; 8 | } 9 | 10 | .default-chip { 11 | border: 2px solid $default-color; 12 | border-radius: 5px; 13 | background: $default-color; 14 | padding: 5px; 15 | } 16 | 17 | chip-tmpl:focus { 18 | outline: none; 19 | .default-chip { 20 | border: 2px solid $chip-tmpl-focus-color; 21 | background: $chip-tmpl-focus-color; 22 | color: white; 23 | } 24 | } 25 | 26 | .chip-failed { 27 | .default-chip { 28 | color: red; 29 | } 30 | } 31 | 32 | chips { 33 | display: block; 34 | padding: 5px; 35 | > div { 36 | display: inline; 37 | > input { 38 | height: 100%; 39 | border: none; 40 | font-size: 14px; 41 | } 42 | > input:focus { 43 | outline: none; 44 | } 45 | } 46 | } 47 | 48 | .chip-out-focus { 49 | border-bottom: 1px solid $default-color; 50 | } 51 | 52 | .chip-in-focus { 53 | border-bottom: 1px solid $focus-color; 54 | } 55 | -------------------------------------------------------------------------------- /src/js/utils/dom_util.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | angular.module('angular.chips') 3 | .factory('DomUtil', function() { 4 | return DomUtil; 5 | }); 6 | /*Dom related functionality*/ 7 | function DomUtil(element) { 8 | /* 9 | * addclass will append class to the given element 10 | * ng-class will do the same functionality, in our case 11 | * we don't have access to scope so we are using this util methods 12 | */ 13 | var utilObj = {}; 14 | 15 | utilObj.addClass = function(className) { 16 | utilObj.removeClass(element, className); 17 | element.attr('class', element.attr('class') + ' ' + className); 18 | return utilObj; 19 | }; 20 | 21 | utilObj.removeClass = function(className) { 22 | var classes = element.attr('class').split(' '); 23 | var classIndex = classes.indexOf(className); 24 | if (classIndex !== -1) { 25 | classes.splice(classIndex, 1); 26 | } 27 | element.attr('class', classes.join(' ')); 28 | return utilObj; 29 | }; 30 | 31 | return utilObj; 32 | } 33 | })(); 34 | -------------------------------------------------------------------------------- /src/js/directives/controls/ng_model_control.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | angular.module('angular.chips') 3 | .directive('ngModelControl', NGModelControl); 4 | 5 | /* 6 | * It's for input element which uses ng-model directive 7 | * example: bootstrap typeahead component 8 | */ 9 | function NGModelControl() { 10 | return { 11 | restrict: 'A', 12 | require: ['ngModel', '^chips'], 13 | link: function(scope, iElement, iAttrs, controller) { 14 | var ngModelCtrl = controller[0], 15 | chipsCtrl = controller[1]; 16 | ngModelCtrl.$render = function(event) { 17 | if (!ngModelCtrl.$modelValue) 18 | return; 19 | if (chipsCtrl.addChip(ngModelCtrl.$modelValue)) { 20 | iElement.val(''); 21 | } 22 | } 23 | 24 | iElement.on('focus', function() { 25 | chipsCtrl.setFocus(true); 26 | }); 27 | iElement.on('blur', function() { 28 | chipsCtrl.setFocus(false); 29 | }); 30 | } 31 | } 32 | } 33 | })(); 34 | -------------------------------------------------------------------------------- /samples/using_promise_obj_example.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | angular.module('sample') 3 | .controller('usingPromiseObjController', UsingPromiseObjController); 4 | 5 | function UsingPromiseObjController($scope, $q) { 6 | var self = this; 7 | /*list of countries and it's first letter*/ 8 | this.countries = [{ name: 'India', fl: 'I' }, { name: 'China', fl: 'C' }, { name: 'America', fl: 'A' }]; 9 | /*call back method for chip delete*/ 10 | this.deleteChip = function(val) { 11 | return true; 12 | } 13 | /*call back method for chip*/ 14 | this.render = function(val) { 15 | var index = 0, 16 | isDuplicate = false, 17 | obj = { name: val, fl: val.charAt(0) }; 18 | 19 | var deferred = $q.defer(); 20 | 21 | for (index; index < self.countries.length; index++) { 22 | isDuplicate = self.countries[index].name === val; 23 | if (isDuplicate) 24 | break; 25 | } 26 | 27 | setTimeout(function() { 28 | isDuplicate ? deferred.reject(obj) : deferred.resolve(obj); 29 | }, getDelay()); 30 | 31 | return deferred.promise; 32 | }; 33 | 34 | function getDelay() { 35 | return (Math.floor(Math.random() * 5) + 1) * 1000; 36 | } 37 | } 38 | })(); 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-chips", 3 | "version": "1.0.11", 4 | "description": "Angular-Chips is the angular based component. You can use it to add dynamic chips or free form tags. check samples directory for more information.", 5 | "author": "Mohammed Basheer", 6 | "license": "MIT", 7 | "devDependencies": { 8 | "gulp": "^3.9.0", 9 | "gulp-concat": "^2.6.0", 10 | "gulp-connect": "^2.3.1", 11 | "gulp-connect-multi": "^1.0.8", 12 | "gulp-html2js": "^0.3.1", 13 | "gulp-karma": "0.0.5", 14 | "gulp-livereload": "^3.8.1", 15 | "gulp-ng-annotate": "^2.0.0", 16 | "gulp-open": "^1.0.0", 17 | "gulp-sass": "^2.2.0", 18 | "gulp-uglify": "^1.5.3", 19 | "jasmine-core": "^2.4.1", 20 | "karma": "^0.13.21", 21 | "karma-coverage": "^0.5.4", 22 | "karma-jasmine": "^0.3.7", 23 | "karma-phantomjs-launcher": "^1.0.0", 24 | "run-sequence": "^1.1.5", 25 | "st": "^1.1.0" 26 | }, 27 | "main": "gulpfile.js", 28 | "directories": { 29 | "test": "test" 30 | }, 31 | "keywords": [ 32 | "angular", 33 | "chips", 34 | "multiselect", 35 | "single select", 36 | "chip with promise", 37 | "chips with promise", 38 | "tags", 39 | "inline tagging", 40 | "inline tag", 41 | "free form tags", 42 | "promise based tags", 43 | "promise based chip" 44 | ], 45 | "dependencies": {}, 46 | "scripts": { 47 | "test": "echo \"Error: no test specified\" && exit 1" 48 | }, 49 | "repository": { 50 | "type": "git", 51 | "url": "git+https://github.com/mohbasheer/angular-chips.git" 52 | }, 53 | "bugs": { 54 | "url": "https://github.com/mohbasheer/angular-chips/issues" 55 | }, 56 | "homepage": "https://github.com/mohbasheer/angular-chips#readme" 57 | } 58 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var concat = require('gulp-concat'); 3 | var runSequence = require('run-sequence'); 4 | var livereload = require('gulp-livereload'); 5 | var connect = require('gulp-connect'); 6 | var sass = require('gulp-sass'); 7 | var karmaServer = require('karma').Server; 8 | var ngAnnotate = require('gulp-ng-annotate'); 9 | var uglify = require('gulp-uglify'); 10 | 11 | gulp.task('default', ['build']); 12 | 13 | gulp.task('build', function() { 14 | return runSequence('sass', 'concat', 'concat-uglify', 'connect', 'addwatch'); 15 | }) 16 | 17 | gulp.task('concat-uglify',function(){ 18 | return gulp.src(['src/js/directives/chips.js', 'src/js/**/*.js']) 19 | .pipe(concat('angular-chips.min.js')) 20 | .pipe(ngAnnotate()) 21 | .pipe(uglify()) 22 | .pipe(gulp.dest('dist/')); 23 | }); 24 | 25 | gulp.task('concat', function() { 26 | return gulp.src(['src/js/directives/chips.js', 'src/js/**/*.js']) 27 | .pipe(concat('angular-chips.js')) 28 | .pipe(ngAnnotate()) 29 | .pipe(gulp.dest('dist/')) 30 | .pipe(connect.reload()); 31 | }); 32 | 33 | gulp.task('sass', function() { 34 | return gulp.src('src/css/main.scss') 35 | .pipe(sass().on('error', sass.logError)) 36 | .pipe(gulp.dest('dist/')) 37 | .pipe(connect.reload()); 38 | }); 39 | 40 | 41 | 42 | gulp.task('refreshtml', function() { 43 | gulp.src('samples/index.html') 44 | .pipe(connect.reload()); 45 | }); 46 | 47 | gulp.task('addwatch', function() { 48 | gulp.watch(['src/js/**/*.js', 'samples/*.js'], function() { 49 | gulp.run('concat'); 50 | }); 51 | gulp.watch('samples/index.html', function() { 52 | gulp.run('refreshtml') 53 | }); 54 | 55 | gulp.watch('src/css/main.scss', function() { 56 | gulp.run('sass'); 57 | }); 58 | }); 59 | 60 | gulp.task('connect', function() { 61 | var server = connect.server({ 62 | livereload: true, 63 | port: 9000, 64 | }); 65 | }); 66 | 67 | gulp.task('test', ['sass', 'concat'], function(done) { 68 | new karmaServer({ 69 | configFile: __dirname + '/karma.config.js', 70 | singleRun: true 71 | }, done).start(); 72 | }); 73 | -------------------------------------------------------------------------------- /test/custom_rendering_flow_spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive chips : Custom Rendering', function() { 4 | 5 | beforeEach(module('angular.chips')); 6 | 7 | var element, scope, compile, template, isolateScope, timeout; 8 | 9 | /*** Custom Rendering ***/ 10 | 11 | beforeEach(inject(function($rootScope, $injector) { 12 | scope = $rootScope.$new(); 13 | scope.samples = [{ name: 'India', fl: 'I' }, { name: 'China', fl: 'C' }, { name: 'America', fl: 'A' }]; 14 | scope.cutomize = function(val) { 15 | return { name: val, fl: val.charAt(0) } 16 | }; 17 | scope.deleteChip = function(obj) { 18 | return obj.name === 'India' ? false : true; 19 | }; 20 | compile = $injector.get('$compile'); 21 | template = '' + 22 | '' + 23 | '
{{chip.name}} , {{chip.fl}}
' + 24 | '
' + 25 | '' + 26 | '
'; 27 | element = angular.element(template); 28 | compile(element)(scope); 29 | scope.$digest(); 30 | isolateScope = element.isolateScope(); 31 | })); 32 | 33 | it('check chips.list values', function() { 34 | expect(scope.samples).toEqual(isolateScope.chips.list); 35 | }); 36 | 37 | it('check adding chip by passing string', function() { 38 | isolateScope.chips.addChip('Japan'); 39 | expect(scope.samples[scope.samples.length - 1].name).toBe('Japan'); 40 | }); 41 | 42 | it('check deleting chip by passing string', function() { 43 | getChipScope(element,1).$broadcast('chip:delete') 44 | expect(scope.samples[1].name).not.toBe('China'); 45 | expect(scope.samples[1].name).not.toBe('China'); 46 | }); 47 | 48 | it('check chip delete restriction', function() { 49 | getChipScope(element,0).$broadcast('chip:delete') 50 | // as per logic 'India' should not delete 51 | expect(scope.samples[0].name).toBe('India'); 52 | }); 53 | 54 | }); 55 | -------------------------------------------------------------------------------- /karma.config.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sat Mar 05 2016 13:46:03 GMT+0530 (IST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | // basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'bower_components/angular/angular.js', 19 | 'bower_components/angular-mocks/angular-mocks.js', 20 | 'src/js/directives/chips.js', 21 | 'src/js/**/*.js', 22 | 'test/utils.js', 23 | 'test/*.js' 24 | ], 25 | 26 | 27 | // list of files to exclude 28 | exclude: [ 29 | ], 30 | 31 | 32 | // preprocess matching files before serving them to the browser 33 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 34 | preprocessors: { 35 | 'src/js/**/*.js': ['coverage'], 36 | }, 37 | 38 | coverageReporter: { 39 | type: 'html', 40 | dir: 'coverage/' 41 | }, 42 | // Which plugins to enable 43 | plugins: [ 44 | 'karma-phantomjs-launcher', 45 | 'karma-coverage', 46 | 'karma-jasmine', 47 | ], 48 | 49 | 50 | // test results reporter to use 51 | // possible values: 'dots', 'progress' 52 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 53 | reporters: ['progress','coverage'], 54 | 55 | 56 | // web server port 57 | port: 9876, 58 | 59 | 60 | // enable / disable colors in the output (reporters and logs) 61 | colors: true, 62 | 63 | 64 | // level of logging 65 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 66 | logLevel: config.LOG_INFO, 67 | 68 | 69 | // enable / disable watching file and executing tests whenever any file changes 70 | autoWatch: false, 71 | 72 | 73 | // start these browsers 74 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 75 | browsers: ['PhantomJS'], 76 | 77 | 78 | // Continuous Integration mode 79 | // if true, Karma captures browsers, runs the tests and exits 80 | singleRun: true, 81 | 82 | // Concurrency level 83 | // how many browser should be started simultaneous 84 | concurrency: Infinity 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /test/promise_string_flow_spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive chips : Using promise with list of string', function() { 4 | 5 | beforeEach(module('angular.chips')); 6 | 7 | var element, scope, compile, template, isolateScope, timeout; 8 | 9 | beforeEach(inject(function($rootScope, $injector) { 10 | scope = $rootScope.$new(); 11 | timeout = $injector.get('$timeout'); 12 | scope.samples = ['orange', 'apple', 'grapes']; 13 | 14 | scope.render = function(val) { 15 | var promise = timeout(function() { 16 | return scope.samples.indexOf(val) === -1 ? val : timeout.cancel(promise) 17 | }, 100); 18 | return promise; 19 | }; 20 | 21 | scope.deleteChip = function(obj) { 22 | return true; 23 | }; 24 | 25 | compile = $injector.get('$compile'); 26 | 27 | template = '' + 28 | '' + 29 | '
' + 30 | '{{chip.defer}}' + 31 | '' + 32 | '
' + 33 | '' + 34 | '
' + 35 | '
' + 36 | '
' + 37 | '' + 38 | '
'; 39 | element = angular.element(template); 40 | compile(element)(scope); 41 | scope.$digest(); 42 | isolateScope = element.isolateScope(); 43 | })); 44 | 45 | it('check chips.list values', function() { 46 | expect(scope.samples.length).toEqual(isolateScope.chips.list.length); 47 | }); 48 | 49 | it('check adding chip by passing string', function() { 50 | isolateScope.chips.addChip('Banana'); 51 | timeout.flush() 52 | expect(scope.samples[scope.samples.length - 1]).toBe('Banana'); 53 | }); 54 | 55 | /*as per render logic above, adding existing string should reject the promise*/ 56 | it('check adding existing chip, as per the logic adding existing string should reject the promise', function() { 57 | isolateScope.chips.addChip('orange'); 58 | timeout.flush() 59 | expect(scope.samples[scope.samples.length - 1]).not.toBe('orange'); 60 | }); 61 | 62 | it('check deleting chip by passing string', function() { 63 | getChipScope(element).$broadcast('chip:delete') 64 | expect(scope.samples[0].name).not.toBe('orange'); 65 | }); 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /src/js/directives/remove_chip.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | angular.module('angular.chips') 3 | .directive('removeChip', RemoveChip); 4 | /* 5 | * Will remove the chip 6 | * remove-chip="callback(chip)"> call back will be triggered before remove 7 | * Call back method should return true to remove or false for nothing 8 | */ 9 | function RemoveChip() { 10 | return { 11 | restrict: 'A', 12 | require: '^?chips', 13 | link: function(scope, iElement, iAttrs, chipsCtrl) { 14 | 15 | function getCallBack(scope, prop) { 16 | var target; 17 | if (prop.search('\\(') > 0) { 18 | prop = prop.substr(0, prop.search('\\(')); 19 | } 20 | if (prop !== undefined) { 21 | if (prop.split('.').length > 1) { 22 | var levels = prop.split('.'); 23 | target = scope; 24 | for (var index = 0; index < levels.length; index++) { 25 | target = target[levels[index]]; 26 | } 27 | } else { 28 | target = scope[prop]; 29 | } 30 | } 31 | return target; 32 | }; 33 | 34 | /* 35 | * traverse scope hierarchy and find the scope 36 | */ 37 | function findScope(scope, prop) { 38 | var funStr = prop.indexOf('.') !== -1 ? prop.split('.')[0] : prop.split('(')[0]; 39 | if (!scope.hasOwnProperty(funStr)) { 40 | return findScope(scope.$parent, prop) 41 | } 42 | return scope; 43 | }; 44 | 45 | function deleteChip() { 46 | // don't delete the chip which is loading 47 | if (typeof scope.chip !== 'string' && scope.chip.isLoading) 48 | return; 49 | var callBack, deleteIt = true; 50 | if (iAttrs.hasOwnProperty('removeChip') && iAttrs.removeChip !== '') { 51 | callBack = getCallBack(findScope(scope, iAttrs.removeChip), iAttrs.removeChip); 52 | deleteIt = callBack(scope.chip); 53 | } 54 | if (deleteIt) 55 | chipsCtrl.removeChip(scope.chip, scope.$index); 56 | }; 57 | 58 | iElement.on('click', function() { 59 | deleteChip(); 60 | }); 61 | 62 | scope.$on('chip:delete', function() { 63 | deleteChip(); 64 | }); 65 | 66 | } 67 | } 68 | } 69 | })(); 70 | -------------------------------------------------------------------------------- /test/chip_with_typeahead_flow_spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | describe('Directive chips : Basic flow', function() { 3 | 4 | beforeEach(module('angular.chips')); 5 | 6 | var element, scope, compile, template, isolateScope, timeout; 7 | 8 | /*** Basic flow ***/ 9 | 10 | beforeEach(inject(function($rootScope, $injector) { 11 | scope = $rootScope.$new(); 12 | scope.newlySelectedCompanie; 13 | scope.companies = ['Apple', 'Cisco', 'Verizon', 'Microsoft']; 14 | scope.availableCompanies = ['ACCO Brands', 15 | 'Accuquote', 16 | 'Accuride Corporation', 17 | 'Ace Hardware', 18 | 'Google', 19 | 'FaceBook', 20 | 'Paypal', 21 | 'Pramati', 22 | 'Bennigan', 23 | 'Berkshire Hathaway', 24 | 'Berry Plastics', 25 | 'Best Buy', 26 | 'Carlisle Companies', 27 | 'Carlson Companies', 28 | 'Carlyle Group', 29 | 'Denbury Resources', 30 | 'Denny', 31 | 'Dentsply', 32 | 'Ebonite International', 33 | 'EBSCO Industries', 34 | 'EchoStar', 35 | 'Gateway, Inc.', 36 | 'Gatorade', 37 | 'Home Shopping Network', 38 | 'Honeywell', 39 | ]; 40 | compile = $injector.get('$compile'); 41 | template = '' + 42 | '' + 43 | '
' + 44 | '{{chip}}' + 45 | '' + 46 | '
' + 47 | '
' + 48 | '' + 49 | '
'; 50 | 51 | 52 | element = angular.element(template); 53 | compile(element)(scope); 54 | scope.$digest(); 55 | isolateScope = element.isolateScope(); 56 | })); 57 | 58 | it('check chips.list values', function() { 59 | expect(scope.companies).toEqual(isolateScope.chips.list); 60 | }); 61 | 62 | it('adding chip through ng-model',function(){ 63 | scope.newlySelectedCompanie = 'EchoStar'; 64 | scope.$digest(); 65 | expect(scope.companies[scope.companies.length-1]).toBe('EchoStar'); 66 | }); 67 | 68 | it('check focus and blur on INPUT element ',function(){ 69 | var focusEvent = new Event('focus') 70 | expect(element.hasClass('chip-out-focus')).toBe(true); 71 | element.find('INPUT')[0].dispatchEvent(focusEvent); 72 | expect(element.hasClass('chip-in-focus')).toBe(true); 73 | 74 | var blurEvent = new Event('blur'); 75 | element.find('INPUT')[0].dispatchEvent(blurEvent); 76 | expect(element.hasClass('chip-out-focus')).toBe(true); 77 | }); 78 | 79 | }); 80 | -------------------------------------------------------------------------------- /test/promise_obj_flow_spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive chips : Using promise with list of Object', function() { 4 | 5 | beforeEach(module('angular.chips')); 6 | 7 | var element, scope, compile, template, isolateScope, timeout; 8 | 9 | beforeEach(inject(function($rootScope, $injector) { 10 | scope = $rootScope.$new(); 11 | timeout = $injector.get('$timeout'); 12 | scope.usingPromiseObj = {}; 13 | scope.usingPromiseObj.samples = [{ name: 'India', fl: 'I' }, { name: 'China', fl: 'C' }, { name: 'America', fl: 'A' }]; 14 | 15 | scope.usingPromiseObj.render = function(val) { 16 | var promise = timeout(handleRender, 100); 17 | 18 | function handleRender() { 19 | if (val === 'India') { 20 | timeout.cancel(promise); 21 | } else { 22 | return { name: val, fl: val.charAt(0) }; 23 | } 24 | } 25 | return promise; 26 | }; 27 | 28 | scope.usingPromiseObj.deleteChip = function(obj) { 29 | return true; 30 | }; 31 | 32 | compile = $injector.get('$compile'); 33 | 34 | template = '' + 35 | '' + 36 | '
' + 37 | '{{chip.isLoading ? chip.defer : chip.defer.name}}' + 38 | '({{chip.defer.fl}})' + 39 | '' + 40 | '
' + 41 | '' + 42 | '
' + 43 | '
' + 44 | '
' + 45 | '' + 46 | '
'; 47 | 48 | element = angular.element(template); 49 | compile(element)(scope); 50 | scope.$digest(); 51 | isolateScope = element.isolateScope(); 52 | })); 53 | 54 | it('check chips.list values', function() { 55 | expect(scope.usingPromiseObj.samples.length).toEqual(isolateScope.chips.list.length); 56 | }); 57 | 58 | it('check adding chip by passing string', function() { 59 | isolateScope.chips.addChip('Swedan'); 60 | timeout.flush() 61 | expect(scope.usingPromiseObj.samples[scope.usingPromiseObj.samples.length - 1].name).toBe('Swedan'); 62 | }); 63 | 64 | it('check deleting chip by passing string', function() { 65 | getChipScope(element).$broadcast('chip:delete') 66 | expect(scope.usingPromiseObj.samples[0].name).not.toBe('India'); 67 | }); 68 | 69 | it('check deleting chip while loading', function() { 70 | isolateScope.chips.addChip('Canada'); 71 | var chipTmpls = element.find('chip-tmpl'); 72 | getChipScope(element,-1).$broadcast('chip:delete') 73 | // should not delete while loading 74 | expect(chipTmpls.length).toEqual(element.find('chip-tmpl').length); 75 | }); 76 | 77 | it('check deleting rejected chip', function() { 78 | isolateScope.chips.addChip('India'); 79 | //rejected chip won't get added to scope 80 | expect(scope.usingPromiseObj.samples.length).toBe(3) 81 | timeout.flush(); 82 | var duplicateChipScope = getChipScope(element,-1); 83 | expect(duplicateChipScope.chip.isFailed).toBe(true); 84 | var chipTmpls = element.find('chip-tmpl'); 85 | duplicateChipScope.$broadcast('chip:delete'); 86 | // rejected chip should get deleted from view 87 | expect(chipTmpls.length - 1).toEqual(element.find('chip-tmpl').length); 88 | }); 89 | 90 | }); 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # No longer under maintenance 2 | 3 | # Angular-Chips 4 | 5 | Angular-Chips is the angular based component. You can use it to add dynamic chips or free form tags. check samples directory for more information. 6 | 7 | ### Install: 8 | 9 | `bower install angular-chips --save-dev` 10 | 11 | Include after angular.js script tag 12 | 13 | `` 14 | 15 | `` 16 | 17 | Include css file: 18 | 19 | `` 20 | 21 | Include in you application module. 22 | 23 | `angular.module('sample',['angular.chips']);` 24 | 25 | Basic Markup 26 | 27 | ``` 28 | 29 | 30 |
31 | {{chip}} 32 | 33 |
34 |
35 | 36 |
37 | ``` 38 | 39 | Using Promise Markup 40 | 41 | ``` 42 | 43 | 44 |
45 | {{chip.defer.name}} 46 | ({{chip.defer.fl}}) 47 | 48 |
49 |
50 | 51 |
52 | ``` 53 |
54 | 55 | [![Build Status](https://travis-ci.org/mohbasheer/angular-chips.svg?branch=master)](https://travis-ci.org/mohbasheer/angular-chips) 56 | 57 | [![Gitter](https://badges.gitter.im/mohbasheer/angular-chips.svg)](https://gitter.im/mohbasheer/angular-chips) 58 | 59 |

Documentation

60 | 61 | ### Examples: 62 | 63 |

Edit

64 | 65 | 66 |

Edit

67 | 68 | 69 |

Edit

70 | 71 | 72 |

Edit

73 | 74 | 75 |

Edit

76 | 77 | 78 | 79 | ### MIT License (MIT) 80 | 81 | Copyright (c) 2016 Mohammed Basheer (ssp.basheer@gmail.com) 82 | 83 | Permission is hereby granted, free of charge, to any person obtaining a copy 84 | of this software and associated documentation files (the "Software"), to deal 85 | in the Software without restriction, including without limitation the rights 86 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 87 | copies of the Software, and to permit persons to whom the Software is 88 | furnished to do so, subject to the following conditions: 89 | 90 | The above copyright notice and this permission notice shall be included in all 91 | copies or substantial portions of the Software. 92 | 93 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 94 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 95 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 96 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 97 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 98 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 99 | SOFTWARE. 100 | -------------------------------------------------------------------------------- /dist/angular-chips.min.js: -------------------------------------------------------------------------------- 1 | !function(){function e(e){return e&&angular.isFunction(e.then)}function n(e){return{add:function(n){var t=angular.copy(e.$modelValue)||[];t.push(n),e.$setViewValue(t)},"delete":function(n){var t=angular.copy(e.$modelValue);t.splice(n,1),e.$setViewValue(t)},deleteByValue:function(n){var t,i;for(t=0;tl.chips.list.length-1?0:e}}if(""!==(error=o(a)))throw error;var f=n(s),h=u.hasOwnProperty("defer"),m=i(u.render);l.chips.list,l.chips.addChip=function(n){function i(e){l.chips.list.push(e),f.add(e)}var r,o;return void 0!==l.render&&""!==m?(o={},o[m]=n,r=l.render(o)):r=n,!!r&&(e(r)?(r.then(function(e){f.add(e)}),l.chips.list.push(new t(n,r)),l.$apply()):i(r),!0)},l.chips.deleteChip=function(e){var n=l.chips.list.splice(e,1)[0];return n.isFailed?void l.$apply():void(n instanceof t?f.deleteByValue(n.defer):f["delete"](e))},s.$render=function(){if(h&&s.$modelValue){var e,n=[];for(e=0;e"),C=a.html();C=C.substr(C.indexOf("")-"".length),a.find("chip-tmpl").remove();var $=angular.element(C);$.attr("ng-repeat","chip in chips.list track by $index"),$.attr("ng-class","{'chip-failed':chip.isFailed}"),$.attr("tabindex","-1"),$.attr("index","{{$index+1}}"),g.append($);var y=r(g)(l);a.prepend(y),a.on("click",function(e){"CHIPS"===e.target.nodeName&&a.find("input")[0].focus()}),a.find("input").on("focus",function(){v=null}),l.chips.handleKeyDown=function(e){function n(){var n=parseInt(document.activeElement.getAttribute("index"))||(t=a.find("chip-tmpl")).length;t=a.find("chip-tmpl"),t[n-1].focus(),v=p(n-1),"INPUT"!==e.target.nodeName&&t[v(e.keyCode)].focus()}if(!("INPUT"!==e.target.nodeName&&"CHIP-TMPL"!==e.target.nodeName||0===a.find("chip-tmpl").length&&""===e.target.value)){var t;if(8===e.keyCode){if("INPUT"===e.target.nodeName&&""===e.target.value)n(),e.preventDefault();else if("CHIP-TMPL"===e.target.nodeName){var i=a.find("chip-tmpl");i.length>0&&parseInt(e.target.getAttribute("index"))-1===i.length&&a.find("chip-tmpl")[v(37)].focus()}}else 37!==e.keyCode&&39!==e.keyCode||(null===v?n():a.find("chip-tmpl")[v(e.keyCode)].focus())}},a.on("keydown",l.chips.handleKeyDown),c(a).addClass("chip-out-focus")}return a.$inject=["scope","iElement","iAttrs","ngModelCtrl","transcludefn"],{restrict:"E",scope:{render:"&?"},transclude:!0,require:"ngModel",link:a,controller:"chipsController",controllerAs:"chips",template:"
"}}function o(e){return 0===e.find("chip-tmpl").length?"should have chip-tmpl":e.find("chip-tmpl").length>1?"should have only one chip-tmpl":""}function l(e,n,t){this.setFocus=function(e){e?t(n).removeClass("chip-out-focus").addClass("chip-in-focus"):t(n).removeClass("chip-in-focus").addClass("chip-out-focus")},this.removeChip=function(e,n){this.deleteChip(n)}}r.$inject=["$compile","$timeout","DomUtil"],l.$inject=["$scope","$element","DomUtil"],angular.module("angular.chips",[]).directive("chips",r).controller("chipsController",l)}(),function(){function e(){return{restrict:"E",transclude:!0,link:function(e,n,t,i,r){r(e,function(e){n.append(e)}),n.on("keydown",function(n){8===n.keyCode&&(e.$broadcast("chip:delete"),n.preventDefault())})}}}angular.module("angular.chips").directive("chipTmpl",e)}(),function(){function e(){return{restrict:"A",require:"^?chips",link:function(e,n,t,i){function r(e,n){var t;if(n.search("\\(")>0&&(n=n.substr(0,n.search("\\("))),void 0!==n)if(n.split(".").length>1){var i=n.split(".");t=e;for(var r=0;r 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

Basic Example

16 |
17 | 18 | 19 |
20 | {{chip}} 21 | 22 |
23 |
24 | 25 |
26 | 27 |
28 | Controller: 29 |
inputdemo.companies = {{inputdemo.companies}}
30 |
31 |
32 | 33 | 34 | 35 |
36 |

Custom Rendering

37 |
38 | 39 | 40 |
41 | {{chip.name}} 42 | ({{chip.fl}}) 43 | 44 |
45 |
46 | 47 |
48 | 49 |
50 | Controller: 51 |
custom.countries = {{custom.countries}}
52 |
53 |
54 | 55 | 56 | 57 |
58 |

Using Promise (with list of string)

59 |
60 | 61 | 62 |
63 | {{chip.defer}} 64 | 65 |
66 | 67 |
68 |
69 |
70 | 71 |
72 | 73 |
74 | Controller: 75 |
usingPromise.list = {{usingPromise.list}}
76 |
77 |
78 | 79 | 80 | 81 |
82 |

Using Promise (with list of object)

83 |
84 | 85 | 86 |
87 | {{chip.isLoading ? chip.defer : chip.defer.name}} 88 | ({{chip.defer.fl}}) 89 | 90 |
91 | 92 |
93 |
94 |
95 | 96 |
97 | 98 |
99 | Controller: 100 |
usingPromiseObj.countries = {{usingPromiseObj.countries}}
101 |
102 |
103 | 104 | 105 | 106 |
107 |

Using bootstrap.ui.typeahead

108 |
109 | 110 | 111 |
112 | {{chip}} 113 | 114 |
115 |
116 | 117 |
118 | 119 |
120 | Controller: 121 |
typeahead.companies = {{typeahead.companies}}
122 |
123 |
124 |
125 |
126 |
127 |
128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /test/basic_flow_spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive chips : Basic flow', function() { 4 | 5 | beforeEach(module('angular.chips')); 6 | 7 | var element, scope, compile, template, isolateScope, timeout; 8 | 9 | /*** Basic flow ***/ 10 | 11 | beforeEach(inject(function($rootScope, $injector) { 12 | scope = $rootScope.$new(); 13 | scope.samples = ['Apple', 'Cisco', 'Verizon', 'Microsoft']; 14 | compile = $injector.get('$compile'); 15 | template = '' + 16 | '' + 17 | '
{{chip}}
' + 18 | '
' + 19 | '' + 20 | '
'; 21 | element = angular.element(template); 22 | compile(element)(scope); 23 | scope.$digest(); 24 | isolateScope = element.isolateScope(); 25 | })); 26 | 27 | it('check chips.list values', function() { 28 | expect(scope.samples).toEqual(isolateScope.chips.list); 29 | }); 30 | 31 | it('check adding chip by passing string', function() { 32 | isolateScope.chips.addChip('Pramati'); 33 | expect(scope.samples.indexOf('Pramati')).not.toBe(-1); 34 | }); 35 | 36 | it('pressing Enter key on INPUT element should add the chip',function(){ 37 | var inputEle = element.find('INPUT')[0]; 38 | inputEle.value = 'Spain'; 39 | var event = new Event('keypress'); 40 | event.keyCode = 13; 41 | inputEle.dispatchEvent(event); 42 | expect(scope.samples[scope.samples.length-1]).toBe('Spain'); 43 | }); 44 | 45 | it('check deleting chip by passing string', function() { 46 | isolateScope.chips.deleteChip(scope.samples.indexOf('Pramati')); 47 | expect(scope.samples.indexOf('Pramati')).toBe(-1); 48 | }); 49 | 50 | it('keydown on chips should focus on input', function() { 51 | spyOn(element.find('input')[0], 'focus'); 52 | element[0].click(); 53 | expect(element.find('input')[0].focus).toHaveBeenCalled() 54 | }); 55 | 56 | it('pressing backspace should focus on last chip', function() { 57 | var event = { 58 | keyCode: 8, 59 | preventDefault: angular.noop, 60 | target: { nodeName: 'INPUT', value: '' } 61 | }; 62 | var chipTmpls = element.find('chip-tmpl'); 63 | var lastchipTmpl = chipTmpls[chipTmpls.length - 1]; 64 | spyOn(lastchipTmpl, 'focus'); 65 | isolateScope.chips.handleKeyDown(event); 66 | expect(lastchipTmpl.focus).toHaveBeenCalled(); 67 | }); 68 | 69 | it('keep pressing backspace should delete chip one by one and focus last one', function() { 70 | 71 | var mockEvent = { 72 | keyCode: 8, 73 | preventDefault: angular.noop, 74 | target: { nodeName: 'INPUT', value: '' } 75 | }; 76 | //set focus on last inddex chip which is Microsoft 77 | isolateScope.chips.handleKeyDown(mockEvent); 78 | 79 | var event = new Event('keydown', { bubbles: true }); 80 | event.keyCode = 8; 81 | //will invoke chip_tmpl keydown handler 82 | var microsoftChip = getChipTmpl(element); 83 | microsoftChip.dispatchEvent(event); 84 | //checking is Microsoft removed 85 | expect(angular.element(getChipTmpl(element)).html()).not.toContain('Microsoft') 86 | expect(angular.element(getChipTmpl(element)).html()).toContain('Verizon') 87 | 88 | mockEvent.target = microsoftChip; 89 | spyOn(getChipTmpl(element), 'focus'); 90 | //set focus on last index chip which is Verizon 91 | isolateScope.chips.handleKeyDown(mockEvent); 92 | expect(getChipTmpl(element).focus).toHaveBeenCalled() 93 | 94 | //focus on chip by pressing left arrow 95 | mockEvent.keyCode = 37; 96 | spyOn(getChipTmpl(element, 1), 'focus'); 97 | isolateScope.chips.handleKeyDown(mockEvent); 98 | expect(getChipTmpl(element, 1).focus).toHaveBeenCalled(); 99 | }); 100 | 101 | it('pressing left and right arrow should focus on chips respectivly',function(){ 102 | //['Apple', 'Cisco', 'Verizon', 'Microsoft']; 103 | //should focus on last chip when pressing left arrow 104 | var mockEvent = {keyCode: 37, target: element.find('INPUT')[0]} 105 | spyOn(getChipTmpl(element),'focus'); 106 | //should focus on Microsoft 107 | isolateScope.chips.handleKeyDown(mockEvent); 108 | expect(getChipTmpl(element).focus).toHaveBeenCalled(); 109 | 110 | //checking right arrow selection 111 | //should focus on Verizon 112 | isolateScope.chips.handleKeyDown(mockEvent); 113 | //should focus on Cisco 114 | isolateScope.chips.handleKeyDown(mockEvent); 115 | mockEvent.keyCode = 39 116 | spyOn(getChipTmpl(element,2),'focus'); 117 | //shuld focus on Verizon again 118 | isolateScope.chips.handleKeyDown(mockEvent); 119 | expect(getChipTmpl(element,2).focus).toHaveBeenCalled(); 120 | 121 | //keep pressing left arrow 122 | mockEvent.keyCode = 37; 123 | spyOn(getChipTmpl(element,1),'focus'); 124 | //focus on Cisco 125 | isolateScope.chips.handleKeyDown(mockEvent); 126 | //focus on Apple 127 | isolateScope.chips.handleKeyDown(mockEvent); 128 | //focus on Microsoft 129 | isolateScope.chips.handleKeyDown(mockEvent); 130 | //focus on Verizon 131 | isolateScope.chips.handleKeyDown(mockEvent); 132 | //focus on Cisco 133 | isolateScope.chips.handleKeyDown(mockEvent); 134 | expect(getChipTmpl(element,1).focus).toHaveBeenCalled(); 135 | 136 | 137 | //keep pressing right arrow 138 | mockEvent.keyCode = 39; 139 | spyOn(getChipTmpl(element,0),'focus'); 140 | //focus on Verizon 141 | isolateScope.chips.handleKeyDown(mockEvent); 142 | //focus on Microsoft 143 | isolateScope.chips.handleKeyDown(mockEvent); 144 | //focus on Apple 145 | isolateScope.chips.handleKeyDown(mockEvent); 146 | expect(getChipTmpl(element,0).focus).toHaveBeenCalled(); 147 | 148 | }); 149 | 150 | it('check chip selection using left and right arrow key after mouse click',function(){ 151 | spyOn(getChipTmpl(element,2),'focus'); 152 | var mockEvent = {keyCode: 37, target: getChipTmpl(element,2)} 153 | isolateScope.chips.handleKeyDown(mockEvent); 154 | expect(getChipTmpl(element,2).focus).toHaveBeenCalled(); 155 | }); 156 | 157 | it('check focus and blur on INPUT element ',function(){ 158 | var focusEvent = new Event('focus') 159 | expect(element.hasClass('chip-out-focus')).toBe(true); 160 | element.find('INPUT')[0].dispatchEvent(focusEvent); 161 | expect(element.hasClass('chip-in-focus')).toBe(true); 162 | 163 | var blurEvent = new Event('blur'); 164 | element.find('INPUT')[0].dispatchEvent(blurEvent); 165 | expect(element.hasClass('chip-out-focus')).toBe(true); 166 | }); 167 | 168 | it('clicking on delete icon should delete chip',function(){ 169 | var chip = getChipTmpl(element); 170 | angular.element(chip).find('SPAN')[0].click(); 171 | expect(scope.samples.length).toBe(3); 172 | }); 173 | 174 | it('preventDefault should happen only if target is either INPUT or CHIP-TMPL',function(){ 175 | var event = {target: {}, preventDefault:angular.noop} 176 | spyOn(event,'preventDefault'); 177 | isolateScope.chips.handleKeyDown({target:{}}) 178 | expect(event.preventDefault).not.toHaveBeenCalled(); 179 | }); 180 | 181 | it('missing chip-tmpl should get error', function() { 182 | var str = '' + 183 | '
{{chip}}
' + 184 | '' + 185 | '
'; 186 | var fun = function() { compile(angular.element(str))(scope) }; 187 | expect(fun).toThrow('should have chip-tmpl'); 188 | }); 189 | 190 | it('having more then one chip-tmpl should get error', function() { 191 | var str = '' + 192 | '
'+ 193 | ''+ 194 | '
{{chip}}
' + 195 | '
'+ 196 | '' + 197 | '
'; 198 | var fun = function() { compile(angular.element(str))(scope) }; 199 | expect(fun).toThrow('should have only one chip-tmpl'); 200 | }); 201 | 202 | }); 203 | -------------------------------------------------------------------------------- /src/js/directives/chips.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | angular.module('angular.chips', []) 3 | .directive('chips', Chips) 4 | .controller('chipsController', ChipsController); 5 | 6 | function isPromiseLike(obj) { 7 | return obj && angular.isFunction(obj.then); 8 | } 9 | 10 | /* 11 | * update values to ngModel reference 12 | */ 13 | function ngModel(modelCtrl) { 14 | return { 15 | add: function(val) { 16 | var modelCopy = angular.copy(modelCtrl.$modelValue) || []; 17 | modelCopy.push(val) 18 | modelCtrl.$setViewValue(modelCopy); 19 | }, 20 | delete: function(index) { 21 | var modelCopy = angular.copy(modelCtrl.$modelValue); 22 | modelCopy.splice(index, 1); 23 | modelCtrl.$setViewValue(modelCopy); 24 | }, 25 | deleteByValue: function(val) { 26 | var index, resultIndex; 27 | for (index = 0; index < modelCtrl.$modelValue.length; index++) { 28 | if (angular.equals(modelCtrl.$modelValue[index], val)) { 29 | resultIndex = index; 30 | break; 31 | } 32 | 33 | } 34 | if (resultIndex !== undefined) 35 | this.delete(resultIndex) 36 | } 37 | } 38 | } 39 | 40 | function DeferChip(data, promise) { 41 | var self = this; 42 | this.type = 'defer'; 43 | this.defer = data; 44 | this.isLoading = false; 45 | this.isFailed = false; 46 | 47 | if (promise) { 48 | self.isLoading = true; 49 | promise.then(function(data) { 50 | self.defer = data; 51 | self.isLoading = false; 52 | }, function(data) { 53 | self.defer = data; 54 | self.isLoading = false; 55 | self.isFailed = true; 56 | }); 57 | } 58 | } 59 | 60 | /* 61 | * get function param key 62 | * example: 'render(data)' data is the key here 63 | * getParamKey('render(data)') will return data 64 | */ 65 | function getParamKey(funStr) { 66 | if (funStr === undefined) 67 | return; 68 | var openParenthesisIndex, closeParenthesisIndex; 69 | openParenthesisIndex = funStr.indexOf('(') + 1; 70 | closeParenthesisIndex = funStr.indexOf(')'); 71 | return funStr.substr(openParenthesisIndex, closeParenthesisIndex - openParenthesisIndex); 72 | } 73 | /*@ngInject*/ 74 | function Chips($compile, $timeout, DomUtil) { 75 | /*@ngInject*/ 76 | function linkFun(scope, iElement, iAttrs, ngModelCtrl, transcludefn) { 77 | if ((error = validation(iElement)) !== '') { 78 | throw error; 79 | } 80 | 81 | var model = ngModel(ngModelCtrl); 82 | var isDeferFlow = iAttrs.hasOwnProperty('defer'); 83 | var functionParam = getParamKey(iAttrs.render); 84 | 85 | /* 86 | * @scope.chips.addChip should be called by chipControl directive or custom XXXcontrol directive developed by end user 87 | * @scope.chips.deleteChip will be called by removeChip directive 88 | * 89 | */ 90 | 91 | /* 92 | * ngModel values are copies here 93 | */ 94 | scope.chips.list; 95 | 96 | scope.chips.addChip = function(data) { 97 | var updatedData, paramObj; 98 | 99 | if (scope.render !== undefined && functionParam !== '') { 100 | paramObj = {}; 101 | paramObj[functionParam] = data; 102 | updatedData = scope.render(paramObj); 103 | } else { updatedData = data } 104 | 105 | if (!updatedData) { 106 | return false; 107 | } 108 | 109 | if (isPromiseLike(updatedData)) { 110 | updatedData.then(function(response) { 111 | model.add(response); 112 | }); 113 | scope.chips.list.push(new DeferChip(data, updatedData)); 114 | scope.$apply(); 115 | } else { 116 | update(updatedData); 117 | } 118 | 119 | function update(data) { 120 | scope.chips.list.push(data); 121 | model.add(data); 122 | } 123 | 124 | return true; 125 | }; 126 | 127 | scope.chips.deleteChip = function(index) { 128 | var deletedChip = scope.chips.list.splice(index, 1)[0]; 129 | if (deletedChip.isFailed) { 130 | scope.$apply(); 131 | return; 132 | } 133 | 134 | deletedChip instanceof DeferChip ? model.deleteByValue(deletedChip.defer) : model.delete(index); 135 | } 136 | 137 | /* 138 | * ngModel values are copied when it's updated outside 139 | */ 140 | ngModelCtrl.$render = function() { 141 | if (isDeferFlow && ngModelCtrl.$modelValue) { 142 | var index, list = []; 143 | for (index = 0; index < ngModelCtrl.$modelValue.length; index++) { 144 | // list.push(ngModelCtrl.$modelValue[index]); 145 | list.push(new DeferChip(ngModelCtrl.$modelValue[index])) 146 | } 147 | scope.chips.list = list; 148 | } else { 149 | scope.chips.list = angular.copy(ngModelCtrl.$modelValue) || []; 150 | } 151 | 152 | } 153 | 154 | var chipNavigate = null; 155 | /* 156 | * @index selected chip index 157 | * @return function, which will return the chip index based on left or right arrow pressed 158 | */ 159 | function chipNavigator(index) { 160 | return function(direction) { 161 | direction === 37 ? index-- : index++; 162 | index = index < 0 ? scope.chips.list.length - 1 : index > scope.chips.list.length - 1 ? 0 : index; 163 | return index; 164 | } 165 | } 166 | 167 | /*Extract the chip-tmpl and compile inside the chips directive scope*/ 168 | var rootDiv = angular.element('
'); 169 | var tmplStr = iElement.html(); 170 | tmplStr = tmplStr.substr(tmplStr.indexOf('')-('').length); 171 | iElement.find('chip-tmpl').remove(); 172 | var tmpl = angular.element(tmplStr); 173 | var chipTextNode, chipBindedData, chipBindedDataSuffix; 174 | tmpl.attr('ng-repeat', 'chip in chips.list track by $index'); 175 | tmpl.attr('ng-class', '{\'chip-failed\':chip.isFailed}') 176 | tmpl.attr('tabindex', '-1') 177 | tmpl.attr('index', '{{$index+1}}') 178 | rootDiv.append(tmpl); 179 | var node = $compile(rootDiv)(scope); 180 | iElement.prepend(node); 181 | 182 | 183 | /*clicking on chips element should set the focus on INPUT*/ 184 | iElement.on('click', function(event) { 185 | if (event.target.nodeName === 'CHIPS') 186 | iElement.find('input')[0].focus(); 187 | }); 188 | /*on every focus we need to nullify the chipNavigate*/ 189 | iElement.find('input').on('focus', function() { 190 | chipNavigate = null; 191 | }); 192 | /*this method will handle 'delete or Backspace' and left, right key press*/ 193 | scope.chips.handleKeyDown = function(event) { 194 | if (event.target.nodeName !== 'INPUT' && event.target.nodeName !== 'CHIP-TMPL' || (iElement.find('chip-tmpl').length === 0 && event.target.value === '')) 195 | return; 196 | 197 | var chipTmpls; 198 | 199 | function focusOnChip() { 200 | var index = parseInt(document.activeElement.getAttribute('index')) || (chipTmpls = iElement.find('chip-tmpl')).length; 201 | chipTmpls = iElement.find('chip-tmpl'); 202 | chipTmpls[index - 1].focus(); 203 | chipNavigate = chipNavigator(index-1); 204 | if(event.target.nodeName !== 'INPUT') 205 | chipTmpls[chipNavigate(event.keyCode)].focus(); 206 | } 207 | 208 | if (event.keyCode === 8) { 209 | if (event.target.nodeName === 'INPUT' && event.target.value === '') { 210 | focusOnChip(); 211 | event.preventDefault(); 212 | } else if (event.target.nodeName === 'CHIP-TMPL') { 213 | /* 214 | * This block will be called during chip deletion using delete or Backspace key 215 | * Below code will set the focus of the next available chip 216 | */ 217 | var chipTemplates = iElement.find('chip-tmpl'); 218 | if (chipTemplates.length > 0 && parseInt(event.target.getAttribute('index')) - 1 === chipTemplates.length) 219 | iElement.find('chip-tmpl')[chipNavigate(37)].focus(); 220 | } 221 | 222 | } else if (event.keyCode === 37 || event.keyCode === 39) { 223 | chipNavigate === null ? focusOnChip() : iElement.find('chip-tmpl')[chipNavigate(event.keyCode)].focus(); 224 | } 225 | }; 226 | 227 | iElement.on('keydown', scope.chips.handleKeyDown); 228 | 229 | DomUtil(iElement).addClass('chip-out-focus'); 230 | } 231 | 232 | return { 233 | restrict: 'E', 234 | scope: { 235 | /* 236 | * optional callback, this will be called before rendering the data, 237 | * user can modify the data before it's rendered 238 | */ 239 | render: '&?' 240 | }, 241 | transclude: true, 242 | require: 'ngModel', 243 | link: linkFun, 244 | controller: 'chipsController', 245 | controllerAs: 'chips', 246 | template: '
' 247 | } 248 | 249 | 250 | }; 251 | /* tag is mandatory added validation to confirm that*/ 252 | function validation(element) { 253 | return element.find('chip-tmpl').length === 0 ? 'should have chip-tmpl' : element.find('chip-tmpl').length > 1 ? 'should have only one chip-tmpl' : ''; 254 | } 255 | /*@ngInject*/ 256 | function ChipsController($scope, $element, DomUtil) { 257 | /*toggling input controller focus*/ 258 | this.setFocus = function(flag) { 259 | if (flag) { 260 | DomUtil($element).removeClass('chip-out-focus').addClass('chip-in-focus'); 261 | } else { 262 | DomUtil($element).removeClass('chip-in-focus').addClass('chip-out-focus'); 263 | } 264 | } 265 | this.removeChip = function(data, index) { 266 | this.deleteChip(index); 267 | } 268 | } 269 | })(); 270 | -------------------------------------------------------------------------------- /dist/angular-chips.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | Chips.$inject = ["$compile", "$timeout", "DomUtil"]; 3 | ChipsController.$inject = ["$scope", "$element", "DomUtil"]; 4 | angular.module('angular.chips', []) 5 | .directive('chips', Chips) 6 | .controller('chipsController', ChipsController); 7 | 8 | function isPromiseLike(obj) { 9 | return obj && angular.isFunction(obj.then); 10 | } 11 | 12 | /* 13 | * update values to ngModel reference 14 | */ 15 | function ngModel(modelCtrl) { 16 | return { 17 | add: function(val) { 18 | var modelCopy = angular.copy(modelCtrl.$modelValue) || []; 19 | modelCopy.push(val) 20 | modelCtrl.$setViewValue(modelCopy); 21 | }, 22 | delete: function(index) { 23 | var modelCopy = angular.copy(modelCtrl.$modelValue); 24 | modelCopy.splice(index, 1); 25 | modelCtrl.$setViewValue(modelCopy); 26 | }, 27 | deleteByValue: function(val) { 28 | var index, resultIndex; 29 | for (index = 0; index < modelCtrl.$modelValue.length; index++) { 30 | if (angular.equals(modelCtrl.$modelValue[index], val)) { 31 | resultIndex = index; 32 | break; 33 | } 34 | 35 | } 36 | if (resultIndex !== undefined) 37 | this.delete(resultIndex) 38 | } 39 | } 40 | } 41 | 42 | function DeferChip(data, promise) { 43 | var self = this; 44 | this.type = 'defer'; 45 | this.defer = data; 46 | this.isLoading = false; 47 | this.isFailed = false; 48 | 49 | if (promise) { 50 | self.isLoading = true; 51 | promise.then(function(data) { 52 | self.defer = data; 53 | self.isLoading = false; 54 | }, function(data) { 55 | self.defer = data; 56 | self.isLoading = false; 57 | self.isFailed = true; 58 | }); 59 | } 60 | } 61 | 62 | /* 63 | * get function param key 64 | * example: 'render(data)' data is the key here 65 | * getParamKey('render(data)') will return data 66 | */ 67 | function getParamKey(funStr) { 68 | if (funStr === undefined) 69 | return; 70 | var openParenthesisIndex, closeParenthesisIndex; 71 | openParenthesisIndex = funStr.indexOf('(') + 1; 72 | closeParenthesisIndex = funStr.indexOf(')'); 73 | return funStr.substr(openParenthesisIndex, closeParenthesisIndex - openParenthesisIndex); 74 | } 75 | /*@ngInject*/ 76 | function Chips($compile, $timeout, DomUtil) { 77 | /*@ngInject*/ 78 | linkFun.$inject = ["scope", "iElement", "iAttrs", "ngModelCtrl", "transcludefn"]; 79 | function linkFun(scope, iElement, iAttrs, ngModelCtrl, transcludefn) { 80 | if ((error = validation(iElement)) !== '') { 81 | throw error; 82 | } 83 | 84 | var model = ngModel(ngModelCtrl); 85 | var isDeferFlow = iAttrs.hasOwnProperty('defer'); 86 | var functionParam = getParamKey(iAttrs.render); 87 | 88 | /* 89 | * @scope.chips.addChip should be called by chipControl directive or custom XXXcontrol directive developed by end user 90 | * @scope.chips.deleteChip will be called by removeChip directive 91 | * 92 | */ 93 | 94 | /* 95 | * ngModel values are copies here 96 | */ 97 | scope.chips.list; 98 | 99 | scope.chips.addChip = function(data) { 100 | var updatedData, paramObj; 101 | 102 | if (scope.render !== undefined && functionParam !== '') { 103 | paramObj = {}; 104 | paramObj[functionParam] = data; 105 | updatedData = scope.render(paramObj); 106 | } else { updatedData = data } 107 | 108 | if (!updatedData) { 109 | return false; 110 | } 111 | 112 | if (isPromiseLike(updatedData)) { 113 | updatedData.then(function(response) { 114 | model.add(response); 115 | }); 116 | scope.chips.list.push(new DeferChip(data, updatedData)); 117 | scope.$apply(); 118 | } else { 119 | update(updatedData); 120 | } 121 | 122 | function update(data) { 123 | scope.chips.list.push(data); 124 | model.add(data); 125 | } 126 | 127 | return true; 128 | }; 129 | 130 | scope.chips.deleteChip = function(index) { 131 | var deletedChip = scope.chips.list.splice(index, 1)[0]; 132 | if (deletedChip.isFailed) { 133 | scope.$apply(); 134 | return; 135 | } 136 | 137 | deletedChip instanceof DeferChip ? model.deleteByValue(deletedChip.defer) : model.delete(index); 138 | } 139 | 140 | /* 141 | * ngModel values are copied when it's updated outside 142 | */ 143 | ngModelCtrl.$render = function() { 144 | if (isDeferFlow && ngModelCtrl.$modelValue) { 145 | var index, list = []; 146 | for (index = 0; index < ngModelCtrl.$modelValue.length; index++) { 147 | // list.push(ngModelCtrl.$modelValue[index]); 148 | list.push(new DeferChip(ngModelCtrl.$modelValue[index])) 149 | } 150 | scope.chips.list = list; 151 | } else { 152 | scope.chips.list = angular.copy(ngModelCtrl.$modelValue) || []; 153 | } 154 | 155 | } 156 | 157 | var chipNavigate = null; 158 | /* 159 | * @index selected chip index 160 | * @return function, which will return the chip index based on left or right arrow pressed 161 | */ 162 | function chipNavigator(index) { 163 | return function(direction) { 164 | direction === 37 ? index-- : index++; 165 | index = index < 0 ? scope.chips.list.length - 1 : index > scope.chips.list.length - 1 ? 0 : index; 166 | return index; 167 | } 168 | } 169 | 170 | /*Extract the chip-tmpl and compile inside the chips directive scope*/ 171 | var rootDiv = angular.element('
'); 172 | var tmplStr = iElement.html(); 173 | tmplStr = tmplStr.substr(tmplStr.indexOf('')-('
').length); 174 | iElement.find('chip-tmpl').remove(); 175 | var tmpl = angular.element(tmplStr); 176 | var chipTextNode, chipBindedData, chipBindedDataSuffix; 177 | tmpl.attr('ng-repeat', 'chip in chips.list track by $index'); 178 | tmpl.attr('ng-class', '{\'chip-failed\':chip.isFailed}') 179 | tmpl.attr('tabindex', '-1') 180 | tmpl.attr('index', '{{$index+1}}') 181 | rootDiv.append(tmpl); 182 | var node = $compile(rootDiv)(scope); 183 | iElement.prepend(node); 184 | 185 | 186 | /*clicking on chips element should set the focus on INPUT*/ 187 | iElement.on('click', function(event) { 188 | if (event.target.nodeName === 'CHIPS') 189 | iElement.find('input')[0].focus(); 190 | }); 191 | /*on every focus we need to nullify the chipNavigate*/ 192 | iElement.find('input').on('focus', function() { 193 | chipNavigate = null; 194 | }); 195 | /*this method will handle 'delete or Backspace' and left, right key press*/ 196 | scope.chips.handleKeyDown = function(event) { 197 | if (event.target.nodeName !== 'INPUT' && event.target.nodeName !== 'CHIP-TMPL' || (iElement.find('chip-tmpl').length === 0 && event.target.value === '')) 198 | return; 199 | 200 | var chipTmpls; 201 | 202 | function focusOnChip() { 203 | var index = parseInt(document.activeElement.getAttribute('index')) || (chipTmpls = iElement.find('chip-tmpl')).length; 204 | chipTmpls = iElement.find('chip-tmpl'); 205 | chipTmpls[index - 1].focus(); 206 | chipNavigate = chipNavigator(index-1); 207 | if(event.target.nodeName !== 'INPUT') 208 | chipTmpls[chipNavigate(event.keyCode)].focus(); 209 | } 210 | 211 | if (event.keyCode === 8) { 212 | if (event.target.nodeName === 'INPUT' && event.target.value === '') { 213 | focusOnChip(); 214 | event.preventDefault(); 215 | } else if (event.target.nodeName === 'CHIP-TMPL') { 216 | /* 217 | * This block will be called during chip deletion using delete or Backspace key 218 | * Below code will set the focus of the next available chip 219 | */ 220 | var chipTemplates = iElement.find('chip-tmpl'); 221 | if (chipTemplates.length > 0 && parseInt(event.target.getAttribute('index')) - 1 === chipTemplates.length) 222 | iElement.find('chip-tmpl')[chipNavigate(37)].focus(); 223 | } 224 | 225 | } else if (event.keyCode === 37 || event.keyCode === 39) { 226 | chipNavigate === null ? focusOnChip() : iElement.find('chip-tmpl')[chipNavigate(event.keyCode)].focus(); 227 | } 228 | }; 229 | 230 | iElement.on('keydown', scope.chips.handleKeyDown); 231 | 232 | DomUtil(iElement).addClass('chip-out-focus'); 233 | } 234 | 235 | return { 236 | restrict: 'E', 237 | scope: { 238 | /* 239 | * optional callback, this will be called before rendering the data, 240 | * user can modify the data before it's rendered 241 | */ 242 | render: '&?' 243 | }, 244 | transclude: true, 245 | require: 'ngModel', 246 | link: linkFun, 247 | controller: 'chipsController', 248 | controllerAs: 'chips', 249 | template: '
' 250 | } 251 | 252 | 253 | }; 254 | /* tag is mandatory added validation to confirm that*/ 255 | function validation(element) { 256 | return element.find('chip-tmpl').length === 0 ? 'should have chip-tmpl' : element.find('chip-tmpl').length > 1 ? 'should have only one chip-tmpl' : ''; 257 | } 258 | /*@ngInject*/ 259 | function ChipsController($scope, $element, DomUtil) { 260 | /*toggling input controller focus*/ 261 | this.setFocus = function(flag) { 262 | if (flag) { 263 | DomUtil($element).removeClass('chip-out-focus').addClass('chip-in-focus'); 264 | } else { 265 | DomUtil($element).removeClass('chip-in-focus').addClass('chip-out-focus'); 266 | } 267 | } 268 | this.removeChip = function(data, index) { 269 | this.deleteChip(index); 270 | } 271 | } 272 | })(); 273 | 274 | (function() { 275 | angular.module('angular.chips') 276 | .directive('chipTmpl', ChipTmpl); 277 | 278 | function ChipTmpl() { 279 | return { 280 | restrict: 'E', 281 | transclude: true, 282 | link: function(scope, iElement, iAttrs, contrl, transcludefn) { 283 | transcludefn(scope, function(clonedTranscludedContent) { 284 | iElement.append(clonedTranscludedContent); 285 | }); 286 | iElement.on('keydown', function(event) { 287 | if (event.keyCode === 8) { 288 | scope.$broadcast('chip:delete'); 289 | event.preventDefault(); 290 | } 291 | }); 292 | } 293 | } 294 | } 295 | })(); 296 | 297 | (function() { 298 | angular.module('angular.chips') 299 | .directive('removeChip', RemoveChip); 300 | /* 301 | * Will remove the chip 302 | * remove-chip="callback(chip)"> call back will be triggered before remove 303 | * Call back method should return true to remove or false for nothing 304 | */ 305 | function RemoveChip() { 306 | return { 307 | restrict: 'A', 308 | require: '^?chips', 309 | link: function(scope, iElement, iAttrs, chipsCtrl) { 310 | 311 | function getCallBack(scope, prop) { 312 | var target; 313 | if (prop.search('\\(') > 0) { 314 | prop = prop.substr(0, prop.search('\\(')); 315 | } 316 | if (prop !== undefined) { 317 | if (prop.split('.').length > 1) { 318 | var levels = prop.split('.'); 319 | target = scope; 320 | for (var index = 0; index < levels.length; index++) { 321 | target = target[levels[index]]; 322 | } 323 | } else { 324 | target = scope[prop]; 325 | } 326 | } 327 | return target; 328 | }; 329 | 330 | /* 331 | * traverse scope hierarchy and find the scope 332 | */ 333 | function findScope(scope, prop) { 334 | var funStr = prop.indexOf('.') !== -1 ? prop.split('.')[0] : prop.split('(')[0]; 335 | if (!scope.hasOwnProperty(funStr)) { 336 | return findScope(scope.$parent, prop) 337 | } 338 | return scope; 339 | }; 340 | 341 | function deleteChip() { 342 | // don't delete the chip which is loading 343 | if (typeof scope.chip !== 'string' && scope.chip.isLoading) 344 | return; 345 | var callBack, deleteIt = true; 346 | if (iAttrs.hasOwnProperty('removeChip') && iAttrs.removeChip !== '') { 347 | callBack = getCallBack(findScope(scope, iAttrs.removeChip), iAttrs.removeChip); 348 | deleteIt = callBack(scope.chip); 349 | } 350 | if (deleteIt) 351 | chipsCtrl.removeChip(scope.chip, scope.$index); 352 | }; 353 | 354 | iElement.on('click', function() { 355 | deleteChip(); 356 | }); 357 | 358 | scope.$on('chip:delete', function() { 359 | deleteChip(); 360 | }); 361 | 362 | } 363 | } 364 | } 365 | })(); 366 | 367 | (function() { 368 | angular.module('angular.chips') 369 | .factory('DomUtil', function() { 370 | return DomUtil; 371 | }); 372 | /*Dom related functionality*/ 373 | function DomUtil(element) { 374 | /* 375 | * addclass will append class to the given element 376 | * ng-class will do the same functionality, in our case 377 | * we don't have access to scope so we are using this util methods 378 | */ 379 | var utilObj = {}; 380 | 381 | utilObj.addClass = function(className) { 382 | utilObj.removeClass(element, className); 383 | element.attr('class', element.attr('class') + ' ' + className); 384 | return utilObj; 385 | }; 386 | 387 | utilObj.removeClass = function(className) { 388 | var classes = element.attr('class').split(' '); 389 | var classIndex = classes.indexOf(className); 390 | if (classIndex !== -1) { 391 | classes.splice(classIndex, 1); 392 | } 393 | element.attr('class', classes.join(' ')); 394 | return utilObj; 395 | }; 396 | 397 | return utilObj; 398 | } 399 | })(); 400 | 401 | (function() { 402 | ChipControlLinkFun.$inject = ["scope", "iElement", "iAttrs", "chipsCtrl"]; 403 | angular.module('angular.chips') 404 | .directive('chipControl', ChipControl); 405 | 406 | /* 407 | * It's for normal input element 408 | * It send the value to chips directive when press the enter button 409 | */ 410 | function ChipControl() { 411 | return { 412 | restrict: 'A', 413 | require: '^chips', 414 | link: ChipControlLinkFun, 415 | } 416 | }; 417 | /*@ngInject*/ 418 | function ChipControlLinkFun(scope, iElement, iAttrs, chipsCtrl) { 419 | iElement.on('keypress', function(event) { 420 | if (event.keyCode === 13) { 421 | if (event.target.value !== '' && chipsCtrl.addChip(event.target.value)) { 422 | event.target.value = ""; 423 | } 424 | event.preventDefault(); 425 | } 426 | }); 427 | 428 | iElement.on('focus', function() { 429 | chipsCtrl.setFocus(true); 430 | }); 431 | iElement.on('blur', function() { 432 | chipsCtrl.setFocus(false); 433 | }); 434 | }; 435 | })(); 436 | 437 | (function() { 438 | angular.module('angular.chips') 439 | .directive('ngModelControl', NGModelControl); 440 | 441 | /* 442 | * It's for input element which uses ng-model directive 443 | * example: bootstrap typeahead component 444 | */ 445 | function NGModelControl() { 446 | return { 447 | restrict: 'A', 448 | require: ['ngModel', '^chips'], 449 | link: function(scope, iElement, iAttrs, controller) { 450 | var ngModelCtrl = controller[0], 451 | chipsCtrl = controller[1]; 452 | ngModelCtrl.$render = function(event) { 453 | if (!ngModelCtrl.$modelValue) 454 | return; 455 | if (chipsCtrl.addChip(ngModelCtrl.$modelValue)) { 456 | iElement.val(''); 457 | } 458 | } 459 | 460 | iElement.on('focus', function() { 461 | chipsCtrl.setFocus(true); 462 | }); 463 | iElement.on('blur', function() { 464 | chipsCtrl.setFocus(false); 465 | }); 466 | } 467 | } 468 | } 469 | })(); 470 | --------------------------------------------------------------------------------