├── .jscsrc ├── .gitignore ├── dist ├── np-autocomplete.min.css └── np-autocomplete.min.js ├── src ├── np-autocomplete.less └── np-autocomplete.js ├── TODO.md ├── .jshintrc ├── test └── np-autocomplete.spec.js ├── bower.json ├── package.json ├── Gruntfile.js ├── demos ├── app.js └── index.html ├── karma.conf.js ├── CHANGELOG.md └── README.md /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "maximumLineLength": null 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components 2 | /node_modules 3 | 4 | .sublmie* 5 | *.sublime* 6 | 7 | .DS_Store -------------------------------------------------------------------------------- /dist/np-autocomplete.min.css: -------------------------------------------------------------------------------- 1 | .np-autocomplete-wrapper .list-group{position:absolute;z-index:1020;margin-top:1px}.np-autocomplete-wrapper{position:relative}.np-autocomplete-wrapper .list-group-item{padding:6px 12px} -------------------------------------------------------------------------------- /src/np-autocomplete.less: -------------------------------------------------------------------------------- 1 | .np-autocomplete-wrapper .list-group { 2 | position: absolute; 3 | z-index: 1020; 4 | margin-top: 1px; 5 | } 6 | .np-autocomplete-wrapper { 7 | position: relative; 8 | } 9 | .np-autocomplete-wrapper .list-group-item { 10 | padding: 6px 12px; 11 | } -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ## To-do 2 | 3 | - [x] ~~add .np-autocomplete-list-item class to the list items.~~ 4 | - [x] ~~add "has selection" state.~~ 5 | - [x] ~~add navigation by keyboard arrows functionality.~~ 6 | - [x] ~~define a param to decide either highlight the exact search text or each slice separately in case there are spaces.~~ -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "browser": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "esnext": true, 7 | "latedef": true, 8 | "noarg": true, 9 | "node": true, 10 | "strict": true, 11 | "undef": true, 12 | "unused": true, 13 | "evil": true, 14 | "globals": { 15 | "define": false, 16 | "angular": false 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/np-autocomplete.spec.js: -------------------------------------------------------------------------------- 1 | describe('np-autocomplete', function() { 2 | var $httpBackend, $compile, $timeout, $scope, scope, element, listElement, inputElement; 3 | 4 | beforeEach(module('ng-pros.directive.autocomplete')); 5 | 6 | beforeEach(inject(function(_$httpBackend_, _$compile_, _$timeout_, $rootScope) { 7 | $compile = _$compile_; 8 | $timeout = _$timeout_; 9 | $httpBackend = _$httpBackend_; 10 | 11 | $scope = $rootScope.$new(); 12 | })); 13 | }); -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "np-autocomplete", 3 | "version": "2.2.0-beta.0", 4 | "homepage": "https://github.com/ng-pros/np-autocomplete", 5 | "authors": [ 6 | "raeef-refai " 7 | ], 8 | "description": "Full-functional autocomplete (typeahead alternative) AngularJS directive", 9 | "keywords": [ 10 | "autocomplete", 11 | "typeahead", 12 | "typeahead alternative", 13 | "angularjs", 14 | "angular", 15 | "directive" 16 | ], 17 | "license": "MIT", 18 | "dependencies": { 19 | "jquery": "~2.1", 20 | "angular": "~1.3" 21 | }, 22 | "devDependencies": { 23 | "bootstrap": "~3.3", 24 | "angular-mocks": "~1.4.3" 25 | }, 26 | "ignore": [ 27 | "bower_components", 28 | "node_modules" 29 | ], 30 | "main": [ 31 | "./src/np-autocomplete.js", 32 | "./dist/np-autocomplete.min.css" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "np-autocomplete", 3 | "version": "2.2.0-beta.0", 4 | "description": "Full-functional autocomplete (typeahead alternative) AngularJS directive", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/ng-pros/np-autocomplete.git" 8 | }, 9 | "keywords": [ 10 | "autocomplete", 11 | "typeahead", 12 | "typeahead alternative", 13 | "angularjs", 14 | "angular", 15 | "directive" 16 | ], 17 | "author": "raeef-refai", 18 | "email": "raeef.refai@gmail.com", 19 | "bugs": { 20 | "url": "https://github.com/ng-pros/np-autocomplete/issues" 21 | }, 22 | "style": "dist/np-autocomplete.min.css", 23 | "main": "dist/np-autocomplete.min.js", 24 | "homepage": "https://github.com/ng-pros/np-autocomplete", 25 | "dependencies": { 26 | "jquery": "~2.1", 27 | "angular": "~1.3" 28 | }, 29 | "devDependencies": { 30 | "grunt": "~0.4.5", 31 | "grunt-contrib-clean": "~0.6.0", 32 | "grunt-contrib-less": "~1.0.1", 33 | "grunt-contrib-uglify": "~0.9.1", 34 | "grunt-contrib-watch": "~0.6.1", 35 | "grunt-ng-annotate": "~1.0.1", 36 | "jasmine-core": "^2.3.4", 37 | "jit-grunt": "^0.9.1", 38 | "karma": "^0.13.3", 39 | "karma-chrome-launcher": "^0.2.0", 40 | "karma-jasmine": "^0.3.6", 41 | "karma-jasmine-html-reporter": "^0.1.8" 42 | }, 43 | "license": "MIT" 44 | } 45 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | require('jit-grunt')(grunt); 6 | 7 | var taskConfig = { 8 | pkg: grunt.file.readJSON('package.json'), 9 | 10 | clean: { 11 | all: [ 12 | 'dist/*' 13 | ] 14 | }, 15 | 16 | less: { 17 | options: { 18 | compress: true 19 | }, 20 | 21 | npAutocomplete: { 22 | files: { 23 | 'dist/np-autocomplete.min.css': ['src/np-autocomplete.less'] 24 | } 25 | } 26 | }, 27 | 28 | ngAnnotate: { 29 | npAutocomplete: { 30 | files: [{ 31 | src: ['src/np-autocomplete.js'], 32 | dest: 'dist/np-autocomplete.min.js' 33 | }] 34 | } 35 | }, 36 | 37 | uglify: { 38 | npAutocomplete: { 39 | files: [{ 40 | 'dist/np-autocomplete.min.js': 'dist/np-autocomplete.min.js' 41 | }] 42 | } 43 | }, 44 | 45 | watch: { 46 | scripts: { 47 | options: { 48 | livereload: true, 49 | spawn: false 50 | }, 51 | files: [ 52 | 'src/*', 53 | 'demos/*' 54 | ], 55 | tasks: ['compile'] 56 | } 57 | } 58 | }; 59 | 60 | grunt.initConfig(grunt.util._.extend(taskConfig)); 61 | grunt.registerTask('compile', ['clean', 'less', 'ngAnnotate', 'uglify']); 62 | grunt.registerTask('default', ['compile']); 63 | }; 64 | -------------------------------------------------------------------------------- /demos/app.js: -------------------------------------------------------------------------------- 1 | angular.module('app', ['ng-pros.directive.autocomplete']) 2 | 3 | .controller('ctrl', ['$scope', '$timeout', function($scope, $timeout) { 4 | $scope.inputModel = ''; 5 | $scope.options = { 6 | url: 'https://api.github.com/search/repositories', 7 | delay: 0, 8 | minlength: 1, 9 | nameAttr: 'name', 10 | dataHolder: 'items', 11 | limitParam: 'per_page', 12 | searchParam: 'q', 13 | highlightExactSearch: false, 14 | programmaticallyLoad: true, 15 | loadingClass: 'has-feedback', 16 | itemTemplate: '' 27 | }; 28 | 29 | $scope.programmaticallyLoad = function() { 30 | $scope.autoModel = 'np-autocomplete'; 31 | }; 32 | 33 | // $scope.inputModel = 'asdf' 34 | 35 | /*$timeout(function() { 36 | $scope.selectedItem = { 37 | id: 1, 38 | name: 'np-autocomplete' 39 | }; 40 | 41 | // $scope.idModel = 1; 42 | $scope.inputModel = 'np-autocomplete'; 43 | }, 2000);*/ 44 | }]); -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Wed Jul 29 2015 19:55:56 GMT+0300 (EEST) 3 | module.exports = function(config) { 4 | 5 | 'use strict'; 6 | 7 | config.set({ 8 | 9 | // base path that will be used to resolve all patterns (eg. files, exclude) 10 | basePath: '', 11 | 12 | // frameworks to use 13 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 14 | frameworks: ['jasmine'], 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | // Dependencies 19 | 'bower_components/jquery/dist/jquery.js', 20 | 'bower_components/angular/angular.js', 21 | 'bower_components/angular-mocks/angular-mocks.js', 22 | 23 | // Directive files 24 | 'dist/**/*.js', 25 | 26 | // Tests 27 | 'test/**/*.js' 28 | ], 29 | 30 | // list of files to exclude 31 | exclude: [], 32 | 33 | // preprocess matching files before serving them to the browser 34 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 35 | preprocessors: {}, 36 | 37 | // test results reporter to use 38 | // possible values: 'dots', 'progress' 39 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 40 | reporters: ['progress', 'html'], 41 | 42 | // web server port 43 | port: 9876, 44 | 45 | // enable / disable colors in the output (reporters and logs) 46 | colors: true, 47 | 48 | // level of logging 49 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 50 | logLevel: config.LOG_INFO, 51 | 52 | // enable / disable watching file and executing tests whenever any file changes 53 | autoWatch: false, 54 | 55 | // start these browsers 56 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 57 | browsers: ['Chrome'], 58 | 59 | // Continuous Integration mode 60 | // if true, Karma captures browsers, runs the tests and exits 61 | singleRun: false 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /demos/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | np-autocomplete demos 7 | 8 | 9 | 49 | 50 | 51 |
52 |
53 |
54 | {{idModel}} -- {{inputModel}} 55 | 56 |
57 |
58 | 59 | 60 | 61 | 62 |
63 | 64 |

raeef

65 |
66 |
67 | 68 | 69 |
70 |
71 |
72 |
73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.2.0-beta.0 2 | - New: `nameAttr` and `valueAttr` support integer value. 3 | - Bug: fixes #7. 4 | 5 | ## 2.1.0 6 | - New #4: `queryMode` param which determines if the `searchParam` will be in query mode or a param mode. 7 | 8 | ## 2.0.4 9 | - Bug: fixes #2. 10 | 11 | ## 2.0.3 12 | - Enh: require `angular` when use with `CommonJS` or `AMD`. 13 | - Enh: add the css to the main in `bower.json` to be `wiredep` friendly. 14 | 15 | ## 2.0.2 16 | - Bug: fix `each` iterator. 17 | 18 | ## 2.0.1 19 | - Bug: 20 | - change the position of list element to `absolute`. 21 | - unset error state over input change. 22 | 23 | ## 2.0.0 24 | - Bug: 25 | - fix resize and repositioning on window resize functionality. 26 | - passed options object won't be changed so it's possible to pass it to another np-autocomplete. 27 | - Chg: 28 | - Attributes: 29 | - `npInputModel` and `ngModel` are not linked with `selectedItem` anymore. 30 | - Scope Methods: 31 | - ~~`match`~~: became `highlight`. 32 | - Scope Properties: 33 | - ~~`isLoading`~~: became `loading`. 34 | - Template: 35 | - list element: `id` and `style` attributes has been added. 36 | - list element: ~~`class`~~, ~~`ng-style`~~ and ~~`ng-if`~~ attributes has been removed. 37 | - message items: new one has been added for the error state. 38 | - message items: `ng-class` attribute has been added. 39 | - message items: fixed ~~**list-group-item**~~ class has been removed. 40 | - transclusion element: `id` became **np-autocomplete-transclude**. 41 | - all angular expressions replaced with `ng-bind`. 42 | - Item Template: 43 | - ~~`class`~~: has been removed. 44 | - highlight text: wrapper tag changed from ~~`span`~~ to `mark`. 45 | - highlight text: fixed ~~**np-autocomplete-match**~~ class has been removed. 46 | - Params: 47 | - `openedClass`: became `openStateClass`. 48 | - `closedClass`: became `closeStateClass`. 49 | - `loadingClass`: became `loadStateClass`. 50 | - `loadingMessage`: became `loadStateMessage`. 51 | - Default param value: 52 | - `minlength`: **1**. 53 | - `openStateClass` (~~openedClass~~): **np-autocomplete-open**. 54 | - `loadStateClass` (~~loadingClass~~): **np-autocomplete-load**. 55 | - `closeStateClass` (~~closedClass~~): **np-autocomplete-close**. 56 | - New: 57 | - Core Functionalities: 58 | - navigation by keyboard arrows. 59 | - Attributes: 60 | - `npAuto`: to give the ability to programmatically load. 61 | - Params: 62 | - `onBlur`: defines a callback function to be called when the directive loses focus. 63 | - `listClass`: defines a class or set of classes for the list. 64 | - `itemClass`: defines a class or set of classes for each item in the list. 65 | - `messageClass`: defines a class or set of classes for all messages items. 66 | - `errorStateMessage`: defines the message which will be shown when an error occur. 67 | - `highlightClass`: defines a class or set of classes for the highlighted texts. 68 | - `itemFocusClass`: defines a class or set of classes for the focused list item. 69 | - `errorStateClass`: defines a class or set of classes for error state. 70 | - `hasSelectionClass`: makes you able to add a class or set of classes when a selection is made. 71 | - `highlightExactSearch`: decides either highlight with exact pattern or each portion separately. 72 | - Scope Methods: 73 | - `getItemClasses`: returns all desired classes of the item. 74 | - `onItemMouseenter`: updates `focusedItemIndex` property of scope with currently index of focused item. 75 | - Scope Properties: 76 | - `hasError`: holds a boolean whether there is a load error or not. 77 | - `focusedItemIndex`: holds the current index of the focused item. 78 | 79 | ## 1.0.5 80 | - Enh: replace keyup and paste jquery events with input. 81 | 82 | ## 1.0.4 83 | - Bug: updating input value when npInputModel or npSelectedItem is being programaticlly updated. 84 | 85 | ## 1.0.3 86 | - Bug: trim search text. 87 | 88 | ## 1.0.2 89 | - Bug: escape regex patterns in highlight and escape html tags from source data. 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Np-autocomplete 2 | Np-autocomplete is a full-functional autocomplete (typeahead alternative) AngularJS directive. 3 | 4 | ### Key Features: 5 | - The easiest to setup. 6 | - 100% compatible and optimised by default for bootstrap 3.3.5+ 7 | - Provides 4 models: `ngModel`, `npInputModel`, `npAuto` and `selectedItem`. 8 | - You are free to use built-in angular directives such `ngForm` and `ngRequired`. 9 | - Uses the transclusion, which gives the flexibility with the input element. 10 | - Customizable in the way you like. 11 | - Multiple states (close, open, load and error). 12 | 13 | ### Requirements: 14 | - JQuery 2.1.4+. 15 | - AngularJS 1.3.16+. 16 | - Bootstrap 3.3.5+ (for default template). 17 | 18 | ### Getting Started: 19 | Download the package, then include `dist/np-autocomplete.min.js` and `dist/np-autocomplete.min.css` in your page. 20 | ``` 21 | bower install np-autocomplete --save 22 | ``` 23 | Now, add it to your angular module dependencies list as the following: 24 | ```js 25 | angular.module('yourModule', [ 26 | 'ng-pros.directive.autocomplete', 27 | ... 28 | ]); 29 | ``` 30 | 31 | **Note:** if you were using version `1.0.x` then refer to [CHANGELOG](https://github.com/ng-pros/np-autocomplete/blob/master/CHANGELOG.md). 32 | 33 | ### Quick Usage: 34 | ##### html: 35 | ```html 36 |
37 | 38 | 39 |
40 | ``` 41 | ##### js: 42 | ```js 43 | myAngularApp.controller('ctrl', ['$scope', function($scope) { 44 | $scope.npAutocompleteOptions = { 45 | url: 'https://api.github.com/search/repositories' 46 | }; 47 | }]); 48 | ``` 49 | You can also see the [demos](http://ng-pros.github.io/np-autocomplete/demos.html). 50 | 51 | ### Attributes: 52 | Attribute | Required | Description 53 | :-------- | :------- | :---------- 54 | np-autocomplete | Yes | Passes options object to the directive. 55 | ng-model | No | Holds the value of an attribute of the selected item, e.g. "id". 56 | np-input-model | No | Holds the input element value. 57 | np-selected-item | No | Holds the whole selected item object. 58 | np-auto | No | A model which by updating it the following will happen: update np-input-model with its value, clear ng-model, make a request then flushes itself. 59 | 60 | ### Options: 61 | Attribute | Type | Required | Default Value | Example | Description 62 | :-------- | :--- | :------- | :------------ | :------ | :---------- 63 | url | String | Yes | | http://example.com | Data source url. 64 | nameAttr | String | No | name | full_name | Defines the attribute which will be shown in the list (usually, it is the search field). 65 | valueAttr | String | No | id | downloads_url | Defines the attribute which will be assigned to the ng-model attribute. 66 | limit | Integer | No | 5 | 10 | Sets the value of the limit query param. 67 | limitParam | String | No | limit | per_page | Query param holds the limit value in requests. 68 | searchParam | String | No | search | query | Query param holds the search text in requests. 69 | queryMode | Boolean | No | true | false | Determines if the `searchParam` will be in query mode or param mode, in case it has been set to `false (param mode)` then you should include `:searchParam` string in your url where the search value goes. 70 | delay | Integer | No | 500 (ms) | 1000 (ms) | Time in milliseconds which delays request after changing the search text. 71 | minlength | Integer | No | 1 | 5 | The minimum length of string required before start searching. 72 | dataHoder | String | No | | items | The name of the field in the retrieved data which holds the array of objects those will be used for the autocomplete. 73 | clearOnSelect | Boolean | No | false | true | Either clear the search text after selecting an item or not. 74 | highlightExactSearch | Boolean | No | true | false | either highlight with exact pattern or each portion separately. 75 | template | String (HTML) | No | | | Overrides the default template. 76 | templateUrl | String | No | | | Gets template with $templateCache to overrides the default template. 77 | itemTemplate | String (HTML) | No | | | Overrides the default template of the list item. 78 | itemTemplateUrl | String | No | | | Gets template with $templateCache to overrides the default template of the list item. 79 | params | Object | No | | `{ sort: 'stars' }` | Extra params to send with each request. 80 | errorMessage | String | No | Something went wrong. | An error occurred. | A message to be shown when an error occur. 81 | noResultsMessage | String | No | No results found. | Couldn't find anything. | A message to be shown when no results found. 82 | listClass | String | No | list-group | list-group np-list | Class(es) to be added to the list. 83 | itemClass | String | No | list-group-item | list-group-item np-list-item | Class(es) to be added to each item in the list. 84 | messageClass | String | No | list-group-item | list-group-item np-message-item | Class(es) to be added to each message item. 85 | highlightClass | String | No | bg-info text-info | np-highlight | Class(es) to be added to the highlighted text. 86 | itemFocusClass | String | No | active | np-active | Class(es) to be added to the focused item. 87 | hasSelectionClass | String | No | np-autocomplete-has-selection | has-selection | Class(es) to be added to the directive wrapper when a selection made. 88 | openStateClass | String | No | np-autocomplete-open | np-autocomplete-open open-state | Class(es) to be added to the directive wrapper in 'open' state. 89 | loadStateClass | String | No | np-autocomplete-load | np-autocomplete-load load-state | Class(es) to be added to the directive wrapper in 'load' state. 90 | closeStateClass | String | No | np-autocomplete-close | np-autocomplete-close close-state | Class(es) to be added to the directive wrapper in 'closed' state. 91 | errorStateClass | String | No | np-autocomplete-error | np-autocomplete-error error-state | Class(es) to be added to the directive wrapper in 'load' state. 92 | each | Function | No | | `function(item) {`
`console.log(item);`
`}` | Iterates over elements of retrived data. 93 | onBlur | Function | No | | `function() {`
`console.log('focus lost');`
`}` | a callback function called when the directive loses focus. 94 | onError | Function | No | | `function(errorData) {`
`console.log(errorData);`
`}` | A callback function called when an error occur. 95 | onSelect | Function | No | | `function(item) {`
`console.log(item);`
`}` | A callback function called when a selection is made. 96 | onDeselect |Function | No | | `function() {`
`console.log('Lost selection');`
`}` | A callback function called when the selection is lost. 97 | -------------------------------------------------------------------------------- /dist/np-autocomplete.min.js: -------------------------------------------------------------------------------- 1 | "use strict";!function(a,b){"undefined"!=typeof module&&module.exports?module.exports=b(require("angular")):"function"==typeof define&&define.amd?define(["angular"],b):b(a.angular)}(window,function(angular){angular.module("ng-pros.directive.autocomplete",[]).directive("npAutocomplete",["$timeout","$http","$compile","$templateCache","$sce",function($timeout,$http,$compile,$templateCache,$sce){return $templateCache.put("np-autocomplete/template.tpl.html",'
'),$templateCache.put("np-autocomplete/item-template.tpl.html",'"),{require:"?ngModel",restrict:"A",transclude:!0,scope:{npAuto:"=",ngModel:"=",npInputModel:"=",npSelectedItem:"=",npAutocomplete:"="},link:function(scope,element,attrs,ngModelCtrl,transclude){var id=attrs.id,input=null,template=null,timeoutId=null,closeTimeoutId=null,listElement=null,itemTemplate=null,hasSelection=!1,internalModelChange=!1,internalInputChange=!1,isFocusHandlerActive=!0,options={limit:5,delay:500,params:{},nameAttr:"name",minlength:1,valueAttr:"id",listClass:"list-group",itemClass:"list-group-item",queryMode:!0,limitParam:"limit",templateUrl:"np-autocomplete/template.tpl.html",searchParam:"search",messageClass:"list-group-item",itemFocusClass:"active",highlightClass:"bg-info text-info",openStateClass:"np-autocomplete-open",loadStateClass:"np-autocomplete-load",errorStateClass:"has-error",closeStateClass:"np-autocomplete-close",itemTemplateUrl:"np-autocomplete/item-template.tpl.html",noResultsMessage:"No results found.",loadStateMessage:"Loading...",errorStateMessage:"Something went wrong.",hasSelectionClass:"has-success",highlightExactSearch:!0},open=function(){resize(),listElement.css("display",""),element.removeClass(scope.options.closeStateClass),element.addClass(scope.options.openStateClass)},close=function(){listElement.css("display","none"),element.removeClass(scope.options.openStateClass),element.addClass(scope.options.closeStateClass)},updateSelectionMode=function(a,b){element.removeClass(scope.options.errorStateClass),scope.hasError=!1,a?(element.addClass(scope.options.hasSelectionClass),b&&!hasSelection&&scope.options.onSelect&&scope.options.onSelect(b)):(element.removeClass(scope.options.hasSelectionClass),hasSelection&&scope.options.onDeselect&&scope.options.onDeselect()),hasSelection=a},flush=function(a){return $timeout.cancel(timeoutId),scope.focusedItemIndex=-1,attrs.npSelectedItem&&(scope.npSelectedItem=null),attrs.ngModel&&(internalModelChange=!0,ngModelCtrl.$setViewValue()),updateSelectionMode(!1),a=void 0!==a?a:input.val(),attrs.npInputModel&&(internalInputChange=!0,scope.npInputModel=a),$timeout(function(){scope.searchResults=[]}),a},change=function(delay){close();var val=flush();val&&val.length>=scope.options.minlength&&(timeoutId=$timeout(function(){scope.loading=!0,element.addClass(scope.options.loadStateClass),open();var url=scope.options.url;scope.options.queryMode?scope.options.params[scope.options.searchParam]=val:url=url.replace(new RegExp(":searchParam","g"),val),$http.get(url,{params:scope.options.params})["finally"](function(){scope.loading=!1,element.removeClass(scope.options.loadStateClass)}).then(function(response){var data=response.data;if(scope.options.dataHolder&&(data=eval("data."+scope.options.dataHolder)),scope.options.each)for(var resultsLength=data.length,i=0;resultsLength>i;i++)scope.options.each(data[i]);scope.searchResults=data,scope.focusedItemIndex=0})["catch"](function(a){scope.hasError=!0,element.addClass(scope.options.errorStateClass),scope.options.onError&&scope.options.onError(a)})},delay))},focusoutHandler=function(a){closeTimeoutId=$timeout(function(){close(),scope.options.onBlur&&scope.options.onBlur()},scope.options.delay+100)},focusinHandler=function(a){$timeout.cancel(closeTimeoutId)},focusHandler=function(){if(isFocusHandlerActive){var a=input.val();a&&a.length>=scope.options.minlength&&!hasSelection&&open()}isFocusHandlerActive=!0},resize=function(){var a=input.position();listElement.css({top:a.top+input.outerHeight(),width:input.outerWidth()}),"ltr"===listElement.css("direction")?listElement.css("left",a.left):listElement.css("right",a.right)},focusNextListItem=function(){scope.focusedItemIndex=scope.focusedItemIndex<0?0:++scope.focusedItemIndex%scope.searchResults.length};scope.options=angular.extend({},options,scope.npAutocomplete),scope.searchResults=[],scope.focusedItemIndex=-1,scope.options.delay=scope.options.delay>100?scope.options.delay:100,scope.options.params[scope.options.limitParam]=scope.options.limit,id||(id="np-autocomplete-"+Date.now(),element.attr("id",id)),template=angular.element(scope.options.template||$templateCache.get(scope.options.templateUrl)),itemTemplate=scope.options.itemTemplate||$templateCache.get(scope.options.itemTemplateUrl),listElement=template.closest("#np-do-not-touch"),listElement.removeAttr("id"),listElement.addClass(scope.options.listClass),listElement.append(itemTemplate),element.html($compile(template)(scope)),transclude(scope,function(a){element.find("#np-autocomplete-transclude").replaceWith(a)}),element.addClass("np-autocomplete-wrapper"),element.addClass(scope.options.closeStateClass),input=element.find("input"),resize(),input.on("input",function(){change(scope.options.delay)}),input.keydown(function(a){var b=!1;if(!scope.loading&&scope.searchResults.length&&!hasSelection)switch(b=!0,a.keyCode){case 38:scope.focusedItemIndex=scope.focusedItemIndex<1?scope.searchResults.length-1:scope.focusedItemIndex-1;break;case 40:focusNextListItem();break;case 13:scope.select(scope.searchResults[scope.focusedItemIndex]);break;case 9:focusNextListItem();break;default:b=!1}scope.$$phase||scope.$apply(),b&&a.preventDefault()}),input.focus(focusHandler),angular.element(window).on("resize",resize),angular.element(element).on("focusin",focusinHandler),angular.element(element).on("focusout",focusoutHandler),scope.select=function(a){$timeout.cancel(timeoutId),close(),attrs.ngModel&&(internalModelChange=!0,ngModelCtrl.$setViewValue(a[scope.options.valueAttr])),updateSelectionMode(!0,a);var b=scope.options.clearOnSelect?"":a[scope.options.nameAttr];attrs.npInputModel&&(scope.npInputModel=b),input.val(b),attrs.npSelectedItem&&(scope.npSelectedItem=a),isFocusHandlerActive=!1,input.focus()},scope.clear=function(){close(),input.val(flush("")),scope.searchResults=[],isFocusHandlerActive=!1,input.focus()},scope.highlight=function(a){var b=input.val().trim().replace(/([{}()[\]\\.?*+^$|=!:~-])/g,"\\$1"),c=new RegExp(scope.options.highlightExactSearch?b:b.replace(/\s+/,"|"),"ig"),d=a?a:"";return $sce.trustAsHtml(d.replace(//g,">").replace(c,'$&'))},scope.onItemMouseenter=function(a){scope.focusedItemIndex=a},scope.getItemClasses=function(a){var b={};return b[scope.options.itemClass]=scope.options.itemClass,b[scope.options.itemFocusClass]=scope.options.itemFocusClass&&a===scope.focusedItemIndex,b},attrs.ngModel&&scope.$watch("ngModel",function(a){internalModelChange||($timeout.cancel(timeoutId),updateSelectionMode(void 0!==a&&null!==a&&""!==a),close()),internalModelChange=!1}),attrs.npInputModel&&scope.$watch("npInputModel",function(a){internalInputChange||input.val(a),internalInputChange=!1}),attrs.npAuto&&scope.$watch("npAuto",function(a){a&&($timeout.cancel(timeoutId),scope.npAuto=null,a!==input.val()&&(input.val(a),change(0),isFocusHandlerActive=!1),input.focus())}),scope.$on("$destroy",function(){angular.element(window).off("resize",resize),angular.element(element).off("focusin",focusinHandler),angular.element(element).off("focusout",focusoutHandler)})}}}])}); -------------------------------------------------------------------------------- /src/np-autocomplete.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function(root, factory) { 4 | if (typeof module !== 'undefined' && module.exports) { 5 | // CommonJS 6 | module.exports = factory(require('angular')); 7 | } else if (typeof define === 'function' && define.amd) { 8 | // AMD 9 | define(['angular'], factory); 10 | } else { 11 | // Global Variables 12 | factory(root.angular); 13 | } 14 | }(window, function(angular) { 15 | 16 | angular.module('ng-pros.directive.autocomplete', []) 17 | 18 | .directive('npAutocomplete', ['$timeout', '$http', '$compile', '$templateCache', '$sce', function($timeout, $http, $compile, $templateCache, $sce) { 19 | 20 | $templateCache.put('np-autocomplete/template.tpl.html', 21 | '
' + 22 | '' 43 | ); 44 | 45 | $templateCache.put('np-autocomplete/item-template.tpl.html', 46 | '' 53 | ); 54 | 55 | return { 56 | require: '?ngModel', 57 | restrict: 'A', 58 | transclude: true, 59 | scope: { 60 | npAuto: '=', 61 | ngModel: '=', 62 | npInputModel: '=', 63 | npSelectedItem: '=', 64 | npAutocomplete: '=' 65 | }, 66 | link: function(scope, element, attrs, ngModelCtrl, transclude) { 67 | var id = attrs.id; 68 | var input = null; 69 | var template = null; 70 | var timeoutId = null; 71 | var closeTimeoutId = null; 72 | var listElement = null; 73 | var itemTemplate = null; 74 | var hasSelection = false; 75 | var internalModelChange = false; 76 | var internalInputChange = false; 77 | var isFocusHandlerActive = true; 78 | 79 | var options = { 80 | limit: 5, 81 | delay: 500, 82 | params: {}, 83 | nameAttr: 'name', 84 | minlength: 1, 85 | valueAttr: 'id', 86 | listClass: 'list-group', 87 | itemClass: 'list-group-item', 88 | queryMode: true, 89 | limitParam: 'limit', 90 | templateUrl: 'np-autocomplete/template.tpl.html', 91 | searchParam: 'search', 92 | messageClass: 'list-group-item', 93 | itemFocusClass: 'active', 94 | highlightClass: 'bg-info text-info', 95 | openStateClass: 'np-autocomplete-open', 96 | loadStateClass: 'np-autocomplete-load', 97 | errorStateClass: 'has-error', 98 | closeStateClass: 'np-autocomplete-close', 99 | itemTemplateUrl: 'np-autocomplete/item-template.tpl.html', 100 | noResultsMessage: 'No results found.', 101 | loadStateMessage: 'Loading...', 102 | errorStateMessage: 'Something went wrong.', 103 | hasSelectionClass: 'has-success', 104 | highlightExactSearch: true 105 | }; 106 | 107 | var open = function() { 108 | resize(); 109 | 110 | listElement.css('display', ''); 111 | 112 | element.removeClass(scope.options.closeStateClass); 113 | element.addClass(scope.options.openStateClass); 114 | }; 115 | 116 | var close = function() { 117 | listElement.css('display', 'none'); 118 | 119 | element.removeClass(scope.options.openStateClass); 120 | element.addClass(scope.options.closeStateClass); 121 | }; 122 | 123 | var updateSelectionMode = function(valid, item) { 124 | element.removeClass(scope.options.errorStateClass); 125 | 126 | scope.hasError = false; 127 | 128 | if (valid) { 129 | element.addClass(scope.options.hasSelectionClass); 130 | 131 | if (item && !hasSelection && scope.options.onSelect) { 132 | scope.options.onSelect(item); 133 | } 134 | } else { 135 | element.removeClass(scope.options.hasSelectionClass); 136 | 137 | if (hasSelection && scope.options.onDeselect) { 138 | scope.options.onDeselect(); 139 | } 140 | } 141 | 142 | hasSelection = valid; 143 | }; 144 | 145 | var flush = function(val) { 146 | $timeout.cancel(timeoutId); 147 | 148 | scope.focusedItemIndex = -1; 149 | 150 | if (attrs.npSelectedItem) { 151 | scope.npSelectedItem = null; 152 | } 153 | 154 | if (attrs.ngModel) { 155 | internalModelChange = true; 156 | 157 | ngModelCtrl.$setViewValue(); 158 | } 159 | 160 | updateSelectionMode(false); 161 | 162 | val = val !== undefined ? val : input.val(); 163 | 164 | if (attrs.npInputModel) { 165 | internalInputChange = true; 166 | 167 | scope.npInputModel = val; 168 | } 169 | 170 | $timeout(function() { 171 | scope.searchResults = []; 172 | }); 173 | 174 | return val; 175 | }; 176 | 177 | var change = function(delay) { 178 | close(); 179 | 180 | var val = flush(); 181 | 182 | if (val && val.length >= scope.options.minlength) { 183 | timeoutId = $timeout(function() { 184 | scope.loading = true; 185 | 186 | element.addClass(scope.options.loadStateClass); 187 | 188 | open(); 189 | 190 | var url = scope.options.url; 191 | 192 | if (scope.options.queryMode) { 193 | scope.options.params[scope.options.searchParam] = val; 194 | } else { 195 | url = url.replace(new RegExp(':searchParam', 'g'), val); 196 | } 197 | 198 | $http.get(url, { 199 | params: scope.options.params 200 | }).finally(function() { 201 | scope.loading = false; 202 | 203 | element.removeClass(scope.options.loadStateClass); 204 | }).then(function(response) { 205 | var data = response.data; 206 | 207 | if (scope.options.dataHolder) { 208 | data = eval('data.' + scope.options.dataHolder); 209 | } 210 | 211 | if (scope.options.each) { 212 | var resultsLength = data.length; 213 | 214 | for (var i = 0; i < resultsLength; i++) { 215 | scope.options.each(data[i]); 216 | } 217 | } 218 | 219 | scope.searchResults = data; 220 | 221 | scope.focusedItemIndex = 0; 222 | }).catch(function(data) { 223 | scope.hasError = true; 224 | 225 | element.addClass(scope.options.errorStateClass); 226 | 227 | if (scope.options.onError) { 228 | scope.options.onError(data); 229 | } 230 | }); 231 | 232 | }, delay); 233 | } 234 | }; 235 | 236 | var focusoutHandler = function(evt) { 237 | closeTimeoutId = $timeout(function() { 238 | close(); 239 | if (scope.options.onBlur) { 240 | scope.options.onBlur(); 241 | } 242 | }, scope.options.delay + 100); 243 | }; 244 | 245 | var focusinHandler = function(evt) { 246 | $timeout.cancel(closeTimeoutId); 247 | }; 248 | 249 | var focusHandler = function() { 250 | if (isFocusHandlerActive) { 251 | var val = input.val(); 252 | 253 | if (val && val.length >= scope.options.minlength && !hasSelection) { 254 | open(); 255 | } 256 | } 257 | 258 | isFocusHandlerActive = true; 259 | }; 260 | 261 | var resize = function() { 262 | var inputOffset = input.position(); 263 | 264 | listElement.css({ 265 | top: inputOffset.top + input.outerHeight(), 266 | width: input.outerWidth() 267 | }); 268 | 269 | if (listElement.css('direction') === 'ltr') { 270 | listElement.css('left', inputOffset.left); 271 | } else { 272 | listElement.css('right', inputOffset.right); 273 | } 274 | }; 275 | 276 | var focusNextListItem = function() { 277 | scope.focusedItemIndex = scope.focusedItemIndex < 0 ? 0 : ++scope.focusedItemIndex % scope.searchResults.length; 278 | }; 279 | 280 | // merge options with defaults and initial scope variables. 281 | scope.options = angular.extend({}, options, scope.npAutocomplete); 282 | scope.searchResults = []; 283 | scope.focusedItemIndex = -1; 284 | 285 | scope.options.delay = scope.options.delay > 100 ? scope.options.delay : 100; 286 | scope.options.params[scope.options.limitParam] = scope.options.limit; 287 | 288 | // set directive id if it has not been set. 289 | if (!id) { 290 | id = 'np-autocomplete-' + Date.now(); 291 | element.attr('id', id); 292 | } 293 | 294 | // configure template. 295 | template = angular.element(scope.options.template || $templateCache.get(scope.options.templateUrl)); 296 | 297 | itemTemplate = scope.options.itemTemplate || $templateCache.get(scope.options.itemTemplateUrl); 298 | 299 | listElement = template.closest('#np-do-not-touch'); 300 | listElement.removeAttr('id'); 301 | listElement.addClass(scope.options.listClass); 302 | listElement.append(itemTemplate); 303 | 304 | element.html($compile(template)(scope)); 305 | transclude(scope, function(clone) { 306 | element.find('#np-autocomplete-transclude').replaceWith(clone); 307 | }); 308 | element.addClass('np-autocomplete-wrapper'); 309 | element.addClass(scope.options.closeStateClass); 310 | 311 | // find input element. 312 | input = element.find('input'); 313 | 314 | // resize list once. 315 | resize(); 316 | 317 | // jquery events. 318 | input.on('input', function() { 319 | change(scope.options.delay); 320 | }); 321 | 322 | input.keydown(function(evt) { 323 | var preventDefault = false; 324 | 325 | if (!scope.loading && scope.searchResults.length && !hasSelection) { 326 | 327 | preventDefault = true; 328 | 329 | switch (evt.keyCode) { 330 | case 38: 331 | scope.focusedItemIndex = scope.focusedItemIndex < 1 ? scope.searchResults.length - 1 : scope.focusedItemIndex - 1; 332 | break; 333 | 334 | case 40: 335 | focusNextListItem(); 336 | break; 337 | 338 | case 13: 339 | scope.select(scope.searchResults[scope.focusedItemIndex]); 340 | break; 341 | 342 | case 9: 343 | focusNextListItem(); 344 | break; 345 | 346 | default: 347 | preventDefault = false; 348 | break; 349 | } 350 | } 351 | 352 | if (!scope.$$phase) { 353 | scope.$apply(); 354 | } 355 | 356 | if (preventDefault) { 357 | evt.preventDefault(); 358 | } 359 | }); 360 | 361 | input.focus(focusHandler); 362 | 363 | angular.element(window).on('resize', resize); 364 | angular.element(element).on('focusin', focusinHandler); 365 | angular.element(element).on('focusout', focusoutHandler); 366 | 367 | // scope methods. 368 | scope.select = function(item) { 369 | $timeout.cancel(timeoutId); 370 | 371 | close(); 372 | 373 | if (attrs.ngModel) { 374 | internalModelChange = true; 375 | 376 | ngModelCtrl.$setViewValue(item[scope.options.valueAttr]); 377 | } 378 | 379 | updateSelectionMode(true, item); 380 | 381 | var val = scope.options.clearOnSelect ? '' : item[scope.options.nameAttr]; 382 | 383 | if (attrs.npInputModel) { 384 | scope.npInputModel = val; 385 | } 386 | 387 | input.val(val); 388 | 389 | if (attrs.npSelectedItem) { 390 | scope.npSelectedItem = item; 391 | } 392 | 393 | isFocusHandlerActive = false; 394 | 395 | input.focus(); 396 | }; 397 | 398 | scope.clear = function() { 399 | close(); 400 | 401 | input.val(flush('')); 402 | 403 | scope.searchResults = []; 404 | 405 | isFocusHandlerActive = false; 406 | 407 | input.focus(); 408 | }; 409 | 410 | scope.highlight = function(val) { 411 | var pattern = input.val().trim().replace(/([{}()[\]\\.?*+^$|=!:~-])/g, '\\$1'); 412 | var regex = new RegExp(scope.options.highlightExactSearch ? pattern : pattern.replace(/\s+/, '|'), 'ig'); 413 | var result = val ? val : ''; 414 | 415 | return $sce.trustAsHtml(result.replace(//g, '>').replace(regex, '$&')); 416 | }; 417 | 418 | scope.onItemMouseenter = function(index) { 419 | scope.focusedItemIndex = index; 420 | }; 421 | 422 | scope.getItemClasses = function(index) { 423 | var classes = {}; 424 | 425 | classes[scope.options.itemClass] = scope.options.itemClass; 426 | classes[scope.options.itemFocusClass] = scope.options.itemFocusClass && index === scope.focusedItemIndex; 427 | 428 | return classes; 429 | }; 430 | 431 | // models watchers. 432 | if (attrs.ngModel) { 433 | scope.$watch('ngModel', function(val) { 434 | if (!internalModelChange) { 435 | $timeout.cancel(timeoutId); 436 | 437 | // the following checks exact val states so it won't be false in case if ngModel has been set to 0 438 | updateSelectionMode(val !== undefined && val !== null && val !== ''); 439 | 440 | close(); 441 | } 442 | 443 | internalModelChange = false; 444 | }); 445 | } 446 | 447 | if (attrs.npInputModel) { 448 | scope.$watch('npInputModel', function(val) { 449 | if (!internalInputChange) { 450 | input.val(val); 451 | } 452 | 453 | internalInputChange = false; 454 | }); 455 | } 456 | 457 | if (attrs.npAuto) { 458 | scope.$watch('npAuto', function(val) { 459 | if (val) { 460 | $timeout.cancel(timeoutId); 461 | 462 | scope.npAuto = null; 463 | 464 | if (val !== input.val()) { 465 | input.val(val); 466 | 467 | change(0); 468 | 469 | isFocusHandlerActive = false; 470 | } 471 | 472 | input.focus(); 473 | } 474 | }); 475 | } 476 | 477 | // scope events. 478 | scope.$on('$destroy', function() { 479 | angular.element(window).off('resize', resize); 480 | angular.element(element).off('focusin', focusinHandler); 481 | angular.element(element).off('focusout', focusoutHandler); 482 | }); 483 | } 484 | }; 485 | }]); 486 | })); 487 | --------------------------------------------------------------------------------