├── .jshintrc ├── .travis.yml ├── .gitignore ├── .npmignore ├── bower.json ├── LICENSE.md ├── package.json ├── dist ├── angular-advanced-searchbox.min.css ├── angular-advanced-searchbox.html ├── angular-advanced-searchbox.min.js ├── angular-advanced-searchbox-tpls.min.js ├── angular-advanced-searchbox.js └── angular-advanced-searchbox-tpls.js ├── CHANGELOG.md ├── src ├── angular-advanced-searchbox.css ├── angular-advanced-searchbox.html └── angular-advanced-searchbox.js ├── Gruntfile.js └── README.md /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "angular": true, 4 | "document": true 5 | } 6 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_script: 5 | - npm install -g grunt-cli 6 | - npm install 7 | script: 8 | - grunt 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 6 | .grunt 7 | 8 | # Dependency directory 9 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 10 | node_modules 11 | bower_components 12 | 13 | tmp 14 | .idea -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 6 | .grunt 7 | 8 | # Dependency directory 9 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 10 | node_modules 11 | bower_components 12 | 13 | tmp 14 | .idea 15 | 16 | src 17 | test 18 | tests -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-advanced-searchbox", 3 | "version": "3.0.1", 4 | "homepage": "https://github.com/dnauck/angular-advanced-searchbox", 5 | "authors": [ 6 | "Daniel Nauck " 7 | ], 8 | "description": "A directive for AngularJS providing a advanced visual search box", 9 | "main": [ 10 | "dist/angular-advanced-searchbox.min.css", 11 | "dist/angular-advanced-searchbox-tpls.js" 12 | ], 13 | "keywords": [ 14 | "search", 15 | "angular", 16 | "bootstrap" 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "src", 24 | "Gruntfile.js", 25 | "package.json", 26 | "test", 27 | "tests" 28 | ], 29 | "dependencies": { 30 | "angular": "1", 31 | "jquery": "2", 32 | "bootstrap": "3", 33 | "angular-bootstrap": "1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014-2016 Nauck IT KG http://www.nauck-it.de 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-advanced-searchbox", 3 | "version": "3.0.1", 4 | "description": "A directive for AngularJS providing a advanced visual search box", 5 | "main": "dist/angular-advanced-searchbox-tpls.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/dnauck/angular-advanced-searchbox" 9 | }, 10 | "keywords": [ 11 | "search", 12 | "angular", 13 | "bootstrap" 14 | ], 15 | "author": { 16 | "name": "Nauck IT KG", 17 | "url": "http://www.nauck-it.de/" 18 | }, 19 | "contributors": [ 20 | { 21 | "name": "Daniel Nauck", 22 | "email": "d.nauck@nauck-it.de", 23 | "url": "http://www.nauck-it.de/" 24 | } 25 | ], 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/dnauck/angular-advanced-searchbox/issues" 29 | }, 30 | "homepage": "https://github.com/dnauck/angular-advanced-searchbox", 31 | "devDependencies": { 32 | "bower": "latest", 33 | "grunt": "latest", 34 | "grunt-angular-templates": "latest", 35 | "grunt-bump": "latest", 36 | "grunt-contrib-clean": "latest", 37 | "grunt-contrib-concat": "latest", 38 | "grunt-contrib-copy": "latest", 39 | "grunt-contrib-cssmin": "latest", 40 | "grunt-contrib-jshint": "latest", 41 | "grunt-contrib-uglify": "latest" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /dist/angular-advanced-searchbox.min.css: -------------------------------------------------------------------------------- 1 | .advancedSearchBox{display:block;position:relative;margin:5px 0 10px 0;border:1px solid #ccc;border-radius:4px;-webkit-border-radius:4px;-moz-border-radius:4px;min-height:40px;padding:0 9px;background-color:#fff;cursor:text;line-height:38px}.advancedSearchBox.active{border-color:#66afe9}.advancedSearchBox .search-icon{float:right;padding:10px 0 0 10px}.advancedSearchBox .remove-all-icon{float:right;padding:10px 0 0 10px;cursor:pointer}.advancedSearchBox .search-parameter{display:inline-block;height:24px;margin:0 7px 0 0;background-color:#5bc0de;padding:0 5px;border-radius:4px;-webkit-border-radius:4px;-moz-border-radius:4px;line-height:24px;cursor:default;transition:box-shadow .1s linear}.advancedSearchBox .search-parameter:hover{box-shadow:0 3px 3px rgba(0,0,0,.2)}.advancedSearchBox .search-parameter div{float:left;margin:0 2px}.advancedSearchBox .search-parameter .remove{color:#fff;margin-left:5px;cursor:pointer}.advancedSearchBox .search-parameter .key{color:#fff}.advancedSearchBox .search-parameter .value span{color:#fff}.advancedSearchBox .search-parameter .value input{height:24px}.advancedSearchBox .search-parameter-input{display:inline-block;width:auto;height:24px;border:0;margin:0;padding:0}.advancedSearchBox .search-parameter-input:focus{box-shadow:none;outline:0}.advancedSearchBox .search-parameter-suggestions{cursor:auto;display:block}.advancedSearchBox .search-parameter-suggestions .title{display:block;float:left;height:25px;margin:7px 7px 0 0;background-color:transparent;color:#888;font-weight:700;padding:0 5px;font-size:14px;line-height:25px}.advancedSearchBox .search-parameter-suggestions .search-parameter{cursor:pointer;background-color:#bdbdbd;color:#fff} -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 3.0.1 - 22 March 2018 2 | * remove all search params when triggered from the outside, fixed by #50 (Thanks to Ken Petti) 3 | * allow multiple search parameters of the same key; output array of values, fixed #6 4 | * fixed extra backspaces cause the browser to navigate back to previous page, fixed #52 5 | 6 | ### 3.0.0 - 03 May 2016 7 | * update to angular-ui-bootstrap 1.x, fixed #26, #36 8 | * show auto complete dropdown from the beginning on suggested value parameters, fixed #33 9 | * add data attribute (data-key) for better testability, fixed #35 10 | 11 | ### 2.2.0 - 30 March 2016 12 | * add support for events on adding and removing search params, fixed #32 13 | * search parameter input does not close anymore when you click on it, fixed #38 14 | 15 | ### 2.1.0 - 04 February 2016 16 | * add support for suggested values for a search parameter with typeahead support, fixed #11 17 | * fix optional parameters on AngularJS 1.4.9, fixed #31 and #34 18 | 19 | ### 2.0.1 - 14 December 2015 20 | * enter and leave edit mode methods and events was not called every time due ng-if directive, fixed #17 21 | * workaround a small timing issue and enable edit mode explicit on newly added parameters 22 | * allow to override the templateUrl, related to #19 23 | 24 | ### 2.0.0 - 11 December 2015 25 | * Support to add, delete search parameters and change search parameter's values via ng-model, fixed issue #7, #9 and #10 26 | * change main property of package.json to final build in dist folder, fixes #4 27 | * use ng-if for search parameter input to avoid rendering issues and performance 28 | * Hide in use search parameter suggestions, fixed issue #8 29 | * correctly handle isolation scope of 'placeholder' attribute, fixed #15 30 | * revert change for issue #3, click on container element enables focus, fixed #14 31 | * fixed entering the edit mode by clicking on a search parameter, fixed #3 #14 #21 32 | * fixed browser back behaviour when removing queries, pull request #23 33 | * add option to configure the suggested parameter label text 34 | * allow to specify the display limit of search parameter suggestions 35 | * allow to specify the search throttle time 36 | * add certain events you can subscribe to (enter / leave edit mode, model update) 37 | 38 | ### 1.1.1 - 03 February 2015 39 | * update README with latest changes to dist files in bower package 40 | * remove leading 'src/' path from template name 41 | 42 | ### 1.1.0 - 03 February 2015 43 | * Embed template into final build 44 | * Remove setting focus on click on container element, fixed issue #3 45 | * implement a make release target using grunt-bump 46 | 47 | ### 1.0.0 - 08 December 2014 48 | * Initial release 49 | 50 | -------------------------------------------------------------------------------- /src/angular-advanced-searchbox.css: -------------------------------------------------------------------------------- 1 | .advancedSearchBox { 2 | display: block; 3 | position: relative; 4 | margin: 5px 0 10px 0; 5 | border: 1px solid #ccc; 6 | border-radius: 4px; 7 | -webkit-border-radius: 4px; 8 | -moz-border-radius: 4px; 9 | min-height: 40px; 10 | padding: 0 9px; 11 | background-color: #fff; 12 | cursor: text; 13 | line-height: 38px; 14 | } 15 | 16 | .advancedSearchBox.active { 17 | border-color: #66afe9; 18 | } 19 | 20 | .advancedSearchBox .search-icon { 21 | float: right; 22 | padding: 10px 0 0 10px; 23 | } 24 | 25 | .advancedSearchBox .remove-all-icon { 26 | float: right; 27 | padding: 10px 0 0 10px; 28 | cursor: pointer; 29 | } 30 | 31 | .advancedSearchBox .search-parameter { 32 | display: inline-block; 33 | height: 24px; 34 | margin: 0 7px 0 0; 35 | background-color: #5bc0de; 36 | padding: 0 5px; 37 | border-radius: 4px; 38 | -webkit-border-radius: 4px; 39 | -moz-border-radius: 4px; 40 | line-height: 24px; 41 | cursor: default; 42 | transition: box-shadow 100ms linear; 43 | } 44 | 45 | .advancedSearchBox .search-parameter:hover { 46 | box-shadow: 0 3px 3px rgba(0,0,0,0.2); 47 | } 48 | 49 | .advancedSearchBox .search-parameter div { 50 | float: left; 51 | margin: 0 2px; 52 | } 53 | 54 | .advancedSearchBox .search-parameter .remove { 55 | color: #fff; 56 | margin-left: 5px; 57 | cursor: pointer; 58 | } 59 | 60 | .advancedSearchBox .search-parameter .key { 61 | color: #fff; 62 | } 63 | 64 | .advancedSearchBox .search-parameter .value span { 65 | color: #fff; 66 | } 67 | 68 | .advancedSearchBox .search-parameter .value input { 69 | height: 24px; 70 | } 71 | 72 | .advancedSearchBox .search-parameter-input { 73 | display: inline-block; 74 | width: auto; 75 | height: 24px; 76 | border: 0; 77 | margin: 0; 78 | padding: 0; 79 | } 80 | 81 | .advancedSearchBox .search-parameter-input:focus { 82 | box-shadow: none; 83 | outline: none; 84 | } 85 | 86 | 87 | 88 | .advancedSearchBox .search-parameter-suggestions { 89 | cursor: auto; 90 | display: block; 91 | } 92 | 93 | .advancedSearchBox .search-parameter-suggestions .title { 94 | display: block; 95 | float: left; 96 | height: 25px; 97 | margin: 7px 7px 0 0; 98 | background-color: transparent; 99 | color: #888; 100 | font-weight: bold; 101 | padding: 0 5px; 102 | font-size: 14px; 103 | line-height: 25px; 104 | } 105 | 106 | .advancedSearchBox .search-parameter-suggestions .search-parameter { 107 | cursor: pointer; 108 | background-color: #bdbdbd; 109 | color: #fff; 110 | } -------------------------------------------------------------------------------- /dist/angular-advanced-searchbox.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 |
{{searchParam.name}}:
12 |
13 | {{searchParam.value}} 14 | 30 |
31 |
32 | 45 |
46 |
47 | {{parametersLabel}}: 48 | 51 | {{param.name}} 52 | 53 |
54 |
-------------------------------------------------------------------------------- /src/angular-advanced-searchbox.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 |
{{searchParam.name}}:
12 |
13 | {{searchParam.value}} 14 | 30 |
31 |
32 | 45 |
46 |
47 | {{parametersLabel}}: 48 | 51 | {{param.name}} 52 | 53 |
54 |
-------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // Project configuration. 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | banner: '/*! \n * <%= pkg.title || pkg.name %> v<%= pkg.version %>\n' + 7 | ' * <%= pkg.homepage %>\n' + 8 | ' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %> <%= pkg.author.url %>\n' + 9 | ' * License: <%= pkg.license %>\n' + 10 | ' */\n', 11 | jshint: { 12 | all: [ 13 | './src/*.js', '*.js', '*.json' 14 | ], 15 | options: { 16 | jshintrc: '.jshintrc' 17 | } 18 | }, 19 | ngtemplates: { 20 | options: { 21 | module: 'angular-advanced-searchbox', 22 | htmlmin: { 23 | collapseBooleanAttributes: true, 24 | collapseWhitespace: true, 25 | removeAttributeQuotes: true, 26 | removeComments: true, // Only if you don't use comment directives! 27 | removeEmptyAttributes: true, 28 | removeRedundantAttributes: true, 29 | removeScriptTypeAttributes: true, 30 | removeStyleLinkTypeAttributes: true 31 | } 32 | }, 33 | template: { 34 | cwd: 'src/', 35 | src: ['*.html'], 36 | dest: 'tmp/templates.js' 37 | }, 38 | }, 39 | concat: { 40 | template: { 41 | options: { 42 | }, 43 | src: ['src/<%= pkg.name %>.js', '<%= ngtemplates.template.dest %>'], 44 | dest: 'dist/<%= pkg.name %>-tpls.js' 45 | } 46 | }, 47 | copy: { 48 | main : { 49 | files: [ 50 | { 51 | src: ['src/<%= pkg.name %>.js'], 52 | dest: 'dist/<%= pkg.name %>.js' 53 | } 54 | ] 55 | }, 56 | template : { 57 | files: [ 58 | { 59 | src: ['src/<%= pkg.name %>.html'], 60 | dest: 'dist/<%= pkg.name %>.html' 61 | } 62 | ] 63 | } 64 | }, 65 | uglify: { 66 | options: { 67 | banner: '<%= banner %>' 68 | }, 69 | dist: { 70 | files: { 71 | 'dist/<%= pkg.name %>.min.js': ['dist/<%= pkg.name %>.js'], 72 | 'dist/<%= pkg.name %>-tpls.min.js': ['dist/<%= pkg.name %>-tpls.js'], 73 | } 74 | } 75 | }, 76 | cssmin: { 77 | options: { 78 | banner: '<%= banner %>', 79 | report: 'gzip' 80 | }, 81 | minify: { 82 | src: 'src/<%= pkg.name %>.css', 83 | dest: 'dist/<%= pkg.name %>.min.css' 84 | } 85 | }, 86 | clean: { 87 | temp: { 88 | src: [ 'tmp' ] 89 | } 90 | }, 91 | bump: { 92 | options: { 93 | files: ['package.json', 'bower.json'], 94 | updateConfigs: ['pkg'], 95 | commit: true, 96 | commitFiles: ['-a'], 97 | createTag: true, 98 | push: true, 99 | pushTo: 'origin' 100 | } 101 | } 102 | }); 103 | 104 | // Load the plugins 105 | grunt.loadNpmTasks('grunt-bump'); 106 | grunt.loadNpmTasks('grunt-contrib-clean'); 107 | grunt.loadNpmTasks('grunt-contrib-concat'); 108 | grunt.loadNpmTasks('grunt-contrib-copy'); 109 | grunt.loadNpmTasks('grunt-contrib-uglify'); 110 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 111 | grunt.loadNpmTasks('grunt-contrib-jshint'); 112 | grunt.loadNpmTasks('grunt-contrib-copy'); 113 | grunt.loadNpmTasks('grunt-angular-templates'); 114 | 115 | // Default task(s). 116 | grunt.registerTask('default', ['build']); 117 | grunt.registerTask('build', ['clean', 'jshint:all', 'ngtemplates', 'concat', 'copy', 'uglify', 'cssmin']); 118 | grunt.registerTask('makeRelease', ['bump-only', 'build', 'bump-commit']); 119 | 120 | }; -------------------------------------------------------------------------------- /dist/angular-advanced-searchbox.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * angular-advanced-searchbox v3.0.1 3 | * https://github.com/dnauck/angular-advanced-searchbox 4 | * Copyright (c) 2018 Nauck IT KG http://www.nauck-it.de/ 5 | * License: MIT 6 | */ 7 | 8 | !function(){"use strict";angular.module("angular-advanced-searchbox",[]).directive("nitAdvancedSearchbox",function(){return{restrict:"E",scope:{model:"=ngModel",parameters:"=",parametersLabel:"@",parametersDisplayLimit:"=?",placeholder:"@",searchThrottleTime:"=?"},replace:!0,templateUrl:function(e,a){return a.templateUrl||"angular-advanced-searchbox.html"},controller:["$scope","$attrs","$element","$timeout","$filter","setFocusFor",function(i,e,a,o,c,t){var n;i.parametersLabel=i.parametersLabel||"Parameter Suggestions",i.parametersDisplayLimit=i.parametersDisplayLimit||8,i.placeholder=i.placeholder||"Search ...",i.searchThrottleTime=i.searchThrottleTime||1e3,i.searchParams=[],i.searchQuery="",i.setFocusFor=t;var l=[];function d(e,r,a,t){n&&o.cancel(n),(l=c("filter")(l,function(e){return e.key!==r&&e.index!==a})).push({command:e,key:r,index:a,value:t}),n=o(function(){angular.forEach(l,function(e){var a=c("filter")(i.parameters,function(e){return e.key===r})[0];a&&a.allowMultiple?(angular.isArray(i.model[e.key])||(i.model[e.key]=[]),"delete"===e.command?(i.model[e.key].splice(e.index,1),0===i.model[e.key].length&&delete i.model[e.key]):i.model[e.key][e.index]=e.value):"delete"===e.command?delete i.model[e.key]:i.model[e.key]=e.value}),l.length=0,i.$emit("advanced-searchbox:modelUpdated",i.model)},i.searchThrottleTime)}i.$watch("model",function(e,a){if(!angular.equals(e,a)){angular.forEach(i.model,function(e,a){if("query"===a&&i.searchQuery!==e)i.searchQuery=e;else{var t=c("filter")(i.parameters,function(e){return e.key===a})[0],n=c("filter")(i.searchParams,function(e){return e.key===a});if(void 0!==t)if(t.allowMultiple){if(angular.isArray(e)||(e=[e]),e.forEach(function(e,a){if(n.some(function(e){return e.index===a})){var r=n.filter(function(e){return e.index===a});r[0].value!==e&&(r[0].value=e)}else i.addSearchParam(t,e,!1)}),e.length'),i=angular.element(''),o="none"===a.css("maxWidth")?a.parent().innerWidth():a.css("maxWidth");function c(){l(function(){-1!==t.indexOf(a[0].type||"text")&&(i.text(a.val()||a.attr("placeholder")),a.css("width",i.outerWidth()+10))})}a.css("maxWidth",o),angular.forEach(["fontSize","fontFamily","fontWeight","fontStyle","letterSpacing","textTransform","wordSpacing","textIndent","boxSizing","borderLeftWidth","borderRightWidth","borderLeftStyle","borderRightStyle","paddingLeft","paddingRight","marginLeft","marginRight"],function(e){i.css(e,a.css(e))}),angular.element("body").append(n.append(i)),c(),e.model?e.$watch("model",function(){c()}):a.on("keypress keyup keydown focus input propertychange change",function(){c()})}}}])}(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Angular Advanced Searchbox 2 | [![Build Status](https://travis-ci.org/dnauck/angular-advanced-searchbox.png?branch=master)](https://travis-ci.org/dnauck/angular-advanced-searchbox) 3 | 4 | A directive for AngularJS providing a advanced visual search box. 5 | 6 | ### [DEMO](http://dnauck.github.io/angular-advanced-searchbox/) 7 | 8 | ### Usage 9 | 10 | Include with bower 11 | 12 | ```sh 13 | bower install angular-advanced-searchbox 14 | ``` 15 | 16 | The bower package contains files in the ```dist/```directory with the following names: 17 | 18 | - angular-advanced-searchbox.js 19 | - angular-advanced-searchbox.min.js 20 | - angular-advanced-searchbox-tpls.js 21 | - angular-advanced-searchbox-tpls.min.js 22 | 23 | Files with the ```min``` suffix are minified versions to be used in production. The files with ```-tpls``` in their name have the directive template bundled. If you don't need the default template use the ```angular-paginate-anything.min.js``` file and provide your own template with the ```templateUrl``` attribute. 24 | 25 | Load the javascript and css and declare your Angular dependency 26 | 27 | ```html 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ``` 42 | 43 | ```js 44 | angular.module('myModule', ['angular-advanced-searchbox']); 45 | ``` 46 | 47 | Define the available search parameters in your controller: 48 | 49 | ```js 50 | $scope.availableSearchParams = [ 51 | { key: "name", name: "Name", placeholder: "Name..." }, 52 | { key: "city", name: "City", placeholder: "City..." }, 53 | { key: "country", name: "Country", placeholder: "Country..." }, 54 | { key: "emailAddress", name: "E-Mail", placeholder: "E-Mail...", allowMultiple: true }, 55 | { key: "job", name: "Job", placeholder: "Job..." } 56 | ]; 57 | ``` 58 | 59 | Then in your view 60 | 61 | ```html 62 | 66 | 67 | ``` 68 | 69 | The `angular-advanced-searchbox` directive uses an external template stored in 70 | `angular-advanced-searchbox.html`. Host it in a place accessible to 71 | your page and set the `template-url` attribute. Note that the `url` 72 | param can be a scope variable as well as a hard-coded string. 73 | 74 | ### Benefits 75 | 76 | * Handles free text search and/or parameterized searches 77 | * Provides suggestions on available search parameters 78 | * Easy to use with mouse or keyboard 79 | * Model could easily be used as ```params``` for Angular's ```$http``` API 80 | * Twitter Bootstrap compatible markup 81 | * Works perfectly together with [angular-paginate-anything](https://github.com/begriffs/angular-paginate-anything) (use ```ng-model``` as ```url-params```) 82 | 83 | ### Directive Attributes 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 |
NameDescription
ng-modelSearch parameters as object that could be used as params with Angular's $http API.
parametersList of available parameters to search for.
parametersDisplayLimitMaximum number of suggested parameters to display. Default is 8.
parametersLabelText for the suggested parameters label, e.g. "Parameter Suggestions".
placeholderSpecifies a short hint in the search box
searchThrottleTimeSpecifies the time in milliseconds to wait for changes in the ui until the ng-model is updated. Default is 1000ms.
119 | 120 | ### Events 121 | 122 | The directive emits events as search parameters added (`advanced-searchbox:addedSearchParam`), removed (`advanced-searchbox:removedSearchParam` and `advanced-searchbox:removedAllSearchParam`), enters the edit mode (`advanced-searchbox:enteredEditMode`), leaves the edit mode (`advanced-searchbox:leavedEditMode`) or the search model was updated (`advanced-searchbox:modelUpdated`). 123 | To catch these events do the following: 124 | 125 | ```js 126 | $scope.$on('advanced-searchbox:addedSearchParam', function (event, searchParameter) { 127 | /// 128 | }); 129 | 130 | $scope.$on('advanced-searchbox:removedSearchParam', function (event, searchParameter) { 131 | /// 132 | }); 133 | 134 | $scope.$on('advanced-searchbox:removedAllSearchParam', function (event) { 135 | /// 136 | }); 137 | 138 | $scope.$on('advanced-searchbox:enteredEditMode', function (event, searchParameter) { 139 | /// 140 | }); 141 | 142 | $scope.$on('advanced-searchbox:leavedEditMode', function (event, searchParameter) { 143 | /// 144 | }); 145 | 146 | $scope.$on('advanced-searchbox:modelUpdated', function (event, model) { 147 | /// 148 | }); 149 | ``` 150 | 151 | ### Available Search Parameters Properties 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 |
NameDescriptionType
keyUnique key of the search parameter that is used for the ng-model value.string
nameUser friendly display name of the search parameter.string
placeholderSpecifies a short hint in the parameter search boxstring
allowMultipleShould multiple search parameters of the same key allowed? Output type changes to array of values. Default is false.boolean
suggestedValuesAn array of suggested search values, e.g. ['Berlin', 'London', 'Paris']string[]
restrictToSuggestedValuesShould it restrict possible search values to the ones from the suggestedValues array? Default is false.boolean
194 | 195 | Full example: 196 | 197 | ```js 198 | $scope.availableSearchParams = [ 199 | { key: "name", name: "Name", placeholder: "Name..." }, 200 | { key: "city", name: "City", placeholder: "City...", restrictToSuggestedValues: true, suggestedValues: ['Berlin', 'London', 'Paris'] } 201 | { key: "email", name: "E-Mail", placeholder: "E-Mail...", allowMultiple: true }, 202 | ]; 203 | ``` -------------------------------------------------------------------------------- /dist/angular-advanced-searchbox-tpls.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * angular-advanced-searchbox v3.0.1 3 | * https://github.com/dnauck/angular-advanced-searchbox 4 | * Copyright (c) 2018 Nauck IT KG http://www.nauck-it.de/ 5 | * License: MIT 6 | */ 7 | 8 | !function(){"use strict";angular.module("angular-advanced-searchbox",[]).directive("nitAdvancedSearchbox",function(){return{restrict:"E",scope:{model:"=ngModel",parameters:"=",parametersLabel:"@",parametersDisplayLimit:"=?",placeholder:"@",searchThrottleTime:"=?"},replace:!0,templateUrl:function(e,a){return a.templateUrl||"angular-advanced-searchbox.html"},controller:["$scope","$attrs","$element","$timeout","$filter","setFocusFor",function(i,e,a,s,c,t){var n;i.parametersLabel=i.parametersLabel||"Parameter Suggestions",i.parametersDisplayLimit=i.parametersDisplayLimit||8,i.placeholder=i.placeholder||"Search ...",i.searchThrottleTime=i.searchThrottleTime||1e3,i.searchParams=[],i.searchQuery="",i.setFocusFor=t;var o=[];function l(e,r,a,t){n&&s.cancel(n),(o=c("filter")(o,function(e){return e.key!==r&&e.index!==a})).push({command:e,key:r,index:a,value:t}),n=s(function(){angular.forEach(o,function(e){var a=c("filter")(i.parameters,function(e){return e.key===r})[0];a&&a.allowMultiple?(angular.isArray(i.model[e.key])||(i.model[e.key]=[]),"delete"===e.command?(i.model[e.key].splice(e.index,1),0===i.model[e.key].length&&delete i.model[e.key]):i.model[e.key][e.index]=e.value):"delete"===e.command?delete i.model[e.key]:i.model[e.key]=e.value}),o.length=0,i.$emit("advanced-searchbox:modelUpdated",i.model)},i.searchThrottleTime)}i.$watch("model",function(e,a){if(!angular.equals(e,a)){angular.forEach(i.model,function(e,a){if("query"===a&&i.searchQuery!==e)i.searchQuery=e;else{var t=c("filter")(i.parameters,function(e){return e.key===a})[0],n=c("filter")(i.searchParams,function(e){return e.key===a});if(void 0!==t)if(t.allowMultiple){if(angular.isArray(e)||(e=[e]),e.forEach(function(e,a){if(n.some(function(e){return e.index===a})){var r=n.filter(function(e){return e.index===a});r[0].value!==e&&(r[0].value=e)}else i.addSearchParam(t,e,!1)}),e.length'),i=angular.element(''),s="none"===a.css("maxWidth")?a.parent().innerWidth():a.css("maxWidth");function c(){o(function(){-1!==t.indexOf(a[0].type||"text")&&(i.text(a.val()||a.attr("placeholder")),a.css("width",i.outerWidth()+10))})}a.css("maxWidth",s),angular.forEach(["fontSize","fontFamily","fontWeight","fontStyle","letterSpacing","textTransform","wordSpacing","textIndent","boxSizing","borderLeftWidth","borderRightWidth","borderLeftStyle","borderRightStyle","paddingLeft","paddingRight","marginLeft","marginRight"],function(e){i.css(e,a.css(e))}),angular.element("body").append(n.append(i)),c(),e.model?e.$watch("model",function(){c()}):a.on("keypress keyup keydown focus input propertychange change",function(){c()})}}}])}(),angular.module("angular-advanced-searchbox").run(["$templateCache",function(e){"use strict";e.put("angular-advanced-searchbox.html",'
{{searchParam.name}}:
{{searchParam.value}}
{{parametersLabel}}: {{param.name}}
')}]); -------------------------------------------------------------------------------- /dist/angular-advanced-searchbox.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * angular-advanced-searchbox 3 | * https://github.com/dnauck/angular-advanced-searchbox 4 | * Copyright (c) 2016 Nauck IT KG http://www.nauck-it.de/ 5 | * Author: Daniel Nauck 6 | * License: MIT 7 | */ 8 | 9 | (function() { 10 | 11 | 'use strict'; 12 | 13 | angular.module('angular-advanced-searchbox', []) 14 | .directive('nitAdvancedSearchbox', function() { 15 | return { 16 | restrict: 'E', 17 | scope: { 18 | model: '=ngModel', 19 | parameters: '=', 20 | parametersLabel: '@', 21 | parametersDisplayLimit: '=?', 22 | placeholder: '@', 23 | searchThrottleTime: '=?' 24 | }, 25 | replace: true, 26 | templateUrl: function(element, attr) { 27 | return attr.templateUrl || 'angular-advanced-searchbox.html'; 28 | }, 29 | controller: [ 30 | '$scope', '$attrs', '$element', '$timeout', '$filter', 'setFocusFor', 31 | function ($scope, $attrs, $element, $timeout, $filter, setFocusFor) { 32 | 33 | $scope.parametersLabel = $scope.parametersLabel || 'Parameter Suggestions'; 34 | $scope.parametersDisplayLimit = $scope.parametersDisplayLimit || 8; 35 | $scope.placeholder = $scope.placeholder || 'Search ...'; 36 | $scope.searchThrottleTime = $scope.searchThrottleTime || 1000; 37 | $scope.searchParams = []; 38 | $scope.searchQuery = ''; 39 | $scope.setFocusFor = setFocusFor; 40 | var searchThrottleTimer; 41 | var changeBuffer = []; 42 | 43 | $scope.$watch('model', function (newValue, oldValue) { 44 | 45 | if(angular.equals(newValue, oldValue)) 46 | return; 47 | 48 | angular.forEach($scope.model, function (value, key) { 49 | if (key === 'query' && $scope.searchQuery !== value) { 50 | $scope.searchQuery = value; 51 | } else { 52 | var paramTemplate = $filter('filter')($scope.parameters, function (param) { return param.key === key; })[0]; 53 | var searchParams = $filter('filter')($scope.searchParams, function (param) { return param.key === key; }); 54 | 55 | if (paramTemplate !== undefined) { 56 | if (paramTemplate.allowMultiple) { 57 | // ensure array data structure 58 | if(!angular.isArray(value)) 59 | value = [value]; 60 | 61 | // for each value in the value array: check for adding a new parameter or update it's value 62 | value.forEach(function(val, valIndex) { 63 | if (searchParams.some(function (param) { return param.index === valIndex; })) { 64 | var param = searchParams.filter(function (param) {return param.index === valIndex; }); 65 | if(param[0].value !== val) 66 | param[0].value = val; 67 | } else { 68 | $scope.addSearchParam(paramTemplate, val, false); 69 | } 70 | }); 71 | 72 | // check if there're more search parameters active then values and remove them 73 | if (value.length < searchParams.length) { 74 | for (var i = value.length; i < searchParams.length; i++) { 75 | $scope.removeSearchParam($scope.searchParams.indexOf(searchParams[i])); 76 | } 77 | } 78 | } else { 79 | if (searchParams.length === 0) { 80 | // add param if missing 81 | $scope.addSearchParam(paramTemplate, value, false); 82 | } else { 83 | // update value of parameter if not equal 84 | if(searchParams[0].value !== value) 85 | searchParams[0].value = value; 86 | } 87 | } 88 | } 89 | } 90 | }); 91 | 92 | // delete not existing search parameters from internal state array 93 | for (var i = $scope.searchParams.length - 1; i >= 0; i--) { 94 | var value = $scope.searchParams[i]; 95 | if (!$scope.model.hasOwnProperty(value.key)){ 96 | var index = $scope.searchParams.map(function(e) { return e.key; }).indexOf(value.key); 97 | $scope.removeSearchParam(index); 98 | } 99 | } 100 | }, true); 101 | 102 | $scope.searchParamValueChanged = function (param) { 103 | updateModel('change', param.key, param.index, param.value); 104 | }; 105 | 106 | $scope.searchQueryChanged = function (query) { 107 | updateModel('change', 'query', 0, query); 108 | }; 109 | 110 | $scope.enterEditMode = function(e, index) { 111 | if(e !== undefined) 112 | e.stopPropagation(); 113 | 114 | if (index === undefined) 115 | return; 116 | 117 | var searchParam = $scope.searchParams[index]; 118 | searchParam.editMode = true; 119 | setFocusFor('searchParam:' + searchParam.key); 120 | 121 | $scope.$emit('advanced-searchbox:enteredEditMode', searchParam); 122 | }; 123 | 124 | $scope.leaveEditMode = function(e, index) { 125 | if (index === undefined) 126 | return; 127 | 128 | var searchParam = $scope.searchParams[index]; 129 | searchParam.editMode = false; 130 | 131 | $scope.$emit('advanced-searchbox:leavedEditMode', searchParam); 132 | 133 | // remove empty search params 134 | if (!searchParam.value) 135 | $scope.removeSearchParam(index); 136 | }; 137 | 138 | $scope.searchQueryTypeaheadOnSelect = function (item, model, label) { 139 | $scope.addSearchParam(item); 140 | $scope.searchQuery = ''; 141 | updateModel('delete', 'query', 0); 142 | }; 143 | 144 | $scope.searchParamTypeaheadOnSelect = function (suggestedValue, searchParam) { 145 | searchParam.value = suggestedValue; 146 | $scope.searchParamValueChanged(searchParam); 147 | }; 148 | 149 | $scope.isUnsedParameter = function (value, index) { 150 | return $filter('filter')($scope.searchParams, function (param) { return param.key === value.key && !param.allowMultiple; }).length === 0; 151 | }; 152 | 153 | $scope.addSearchParam = function (searchParam, value, enterEditModel) { 154 | if (enterEditModel === undefined) 155 | enterEditModel = true; 156 | 157 | if (!$scope.isUnsedParameter(searchParam)) 158 | return; 159 | 160 | var internalIndex = 0; 161 | if(searchParam.allowMultiple) 162 | internalIndex = $filter('filter')($scope.searchParams, function (param) { return param.key === searchParam.key; }).length; 163 | 164 | var newIndex = 165 | $scope.searchParams.push( 166 | { 167 | key: searchParam.key, 168 | name: searchParam.name, 169 | type: searchParam.type || 'text', 170 | placeholder: searchParam.placeholder, 171 | allowMultiple: searchParam.allowMultiple || false, 172 | suggestedValues: searchParam.suggestedValues || [], 173 | restrictToSuggestedValues: searchParam.restrictToSuggestedValues || false, 174 | index: internalIndex, 175 | value: value || '' 176 | } 177 | ) - 1; 178 | 179 | updateModel('add', searchParam.key, internalIndex, value); 180 | 181 | if (enterEditModel === true) 182 | $timeout(function() { $scope.enterEditMode(undefined, newIndex); }, 100); 183 | 184 | $scope.$emit('advanced-searchbox:addedSearchParam', searchParam); 185 | }; 186 | 187 | $scope.removeSearchParam = function (index) { 188 | if (index === undefined) 189 | return; 190 | 191 | var searchParam = $scope.searchParams[index]; 192 | $scope.searchParams.splice(index, 1); 193 | 194 | // reassign internal index 195 | if(searchParam.allowMultiple){ 196 | var paramsOfSameKey = $filter('filter')($scope.searchParams, function (param) { return param.key === searchParam.key; }); 197 | 198 | for (var i = 0; i < paramsOfSameKey.length; i++) { 199 | paramsOfSameKey[i].index = i; 200 | } 201 | } 202 | 203 | updateModel('delete', searchParam.key, searchParam.index); 204 | 205 | $scope.$emit('advanced-searchbox:removedSearchParam', searchParam); 206 | }; 207 | 208 | $scope.removeAll = function() { 209 | $scope.searchParams.length = 0; 210 | $scope.searchQuery = ''; 211 | 212 | $scope.model = {}; 213 | 214 | $scope.$emit('advanced-searchbox:removedAllSearchParam'); 215 | }; 216 | 217 | $scope.editPrevious = function(currentIndex) { 218 | if (currentIndex !== undefined) 219 | $scope.leaveEditMode(undefined, currentIndex); 220 | 221 | if (currentIndex > 0) { 222 | $scope.enterEditMode(undefined, currentIndex - 1); 223 | } else if ($scope.searchParams.length > 0) { 224 | $scope.enterEditMode(undefined, $scope.searchParams.length - 1); 225 | } else if ($scope.searchParams.length === 0) { 226 | // no search parameter available anymore 227 | setFocusFor('searchbox'); 228 | } 229 | }; 230 | 231 | $scope.editNext = function(currentIndex) { 232 | if (currentIndex === undefined) 233 | return; 234 | 235 | $scope.leaveEditMode(undefined, currentIndex); 236 | 237 | //TODO: check if index == array length - 1 -> what then? 238 | if (currentIndex < $scope.searchParams.length - 1) { 239 | $scope.enterEditMode(undefined, currentIndex + 1); 240 | } else { 241 | setFocusFor('searchbox'); 242 | } 243 | }; 244 | 245 | $scope.keydown = function(e, searchParamIndex) { 246 | var handledKeys = [8, 9, 13, 37, 39]; 247 | if (handledKeys.indexOf(e.which) === -1) 248 | return; 249 | 250 | var cursorPosition = getCurrentCaretPosition(e.target); 251 | 252 | if (e.which == 8) { // backspace 253 | if (cursorPosition === 0) { 254 | e.preventDefault(); 255 | $scope.editPrevious(searchParamIndex); 256 | } 257 | 258 | } else if (e.which == 9) { // tab 259 | if (e.shiftKey) { 260 | e.preventDefault(); 261 | $scope.editPrevious(searchParamIndex); 262 | } else { 263 | e.preventDefault(); 264 | $scope.editNext(searchParamIndex); 265 | } 266 | 267 | } else if (e.which == 13) { // enter 268 | $scope.editNext(searchParamIndex); 269 | 270 | } else if (e.which == 37) { // left 271 | if (cursorPosition === 0) 272 | $scope.editPrevious(searchParamIndex); 273 | 274 | } else if (e.which == 39) { // right 275 | if (cursorPosition === e.target.value.length) 276 | $scope.editNext(searchParamIndex); 277 | } 278 | }; 279 | 280 | function restoreModel() { 281 | angular.forEach($scope.model, function (value, key) { 282 | if (key === 'query') { 283 | $scope.searchQuery = value; 284 | } else { 285 | var searchParam = $filter('filter')($scope.parameters, function (param) { return param.key === key; })[0]; 286 | if (searchParam !== undefined) 287 | $scope.addSearchParam(searchParam, value, false); 288 | } 289 | }); 290 | } 291 | 292 | if ($scope.model === undefined) { 293 | $scope.model = {}; 294 | } else { 295 | restoreModel(); 296 | } 297 | 298 | function updateModel(command, key, index, value) { 299 | if (searchThrottleTimer) 300 | $timeout.cancel(searchThrottleTimer); 301 | 302 | // remove all previous entries to the same search key that was not handled yet 303 | changeBuffer = $filter('filter')(changeBuffer, function (change) { return change.key !== key && change.index !== index; }); 304 | // add new change to list 305 | changeBuffer.push({ 306 | command: command, 307 | key: key, 308 | index: index, 309 | value: value 310 | }); 311 | 312 | searchThrottleTimer = $timeout(function () { 313 | angular.forEach(changeBuffer, function (change) { 314 | var searchParam = $filter('filter')($scope.parameters, function (param) { return param.key === key; })[0]; 315 | if(searchParam && searchParam.allowMultiple){ 316 | if(!angular.isArray($scope.model[change.key])) 317 | $scope.model[change.key] = []; 318 | 319 | if(change.command === 'delete'){ 320 | $scope.model[change.key].splice(change.index, 1); 321 | if($scope.model[change.key].length === 0) 322 | delete $scope.model[change.key]; 323 | } else { 324 | $scope.model[change.key][change.index] = change.value; 325 | } 326 | } else { 327 | if(change.command === 'delete') 328 | delete $scope.model[change.key]; 329 | else 330 | $scope.model[change.key] = change.value; 331 | } 332 | }); 333 | 334 | changeBuffer.length = 0; 335 | 336 | $scope.$emit('advanced-searchbox:modelUpdated', $scope.model); 337 | 338 | }, $scope.searchThrottleTime); 339 | } 340 | 341 | function getCurrentCaretPosition(input) { 342 | if (!input) 343 | return 0; 344 | 345 | try { 346 | // Firefox & co 347 | if (typeof input.selectionStart === 'number') { 348 | return input.selectionDirection === 'backward' ? input.selectionStart : input.selectionEnd; 349 | 350 | } else if (document.selection) { // IE 351 | input.focus(); 352 | var selection = document.selection.createRange(); 353 | var selectionLength = document.selection.createRange().text.length; 354 | selection.moveStart('character', -input.value.length); 355 | return selection.text.length - selectionLength; 356 | } 357 | } catch(err) { 358 | // selectionStart is not supported by HTML 5 input type, so jut ignore it 359 | } 360 | 361 | return 0; 362 | } 363 | } 364 | ] 365 | }; 366 | }) 367 | .directive('setFocusOn', [ 368 | function() { 369 | return { 370 | restrict: 'A', 371 | link: function($scope, $element, $attrs) { 372 | return $scope.$on('advanced-searchbox:setFocusOn', function(e, id) { 373 | if (id === $attrs.setFocusOn) { 374 | return $element[0].focus(); 375 | } 376 | }); 377 | } 378 | }; 379 | } 380 | ]) 381 | .factory('setFocusFor', [ 382 | '$rootScope', '$timeout', 383 | function($rootScope, $timeout) { 384 | return function(id) { 385 | return $timeout(function() { 386 | return $rootScope.$broadcast('advanced-searchbox:setFocusOn', id); 387 | }); 388 | }; 389 | } 390 | ]) 391 | .directive('nitAutoSizeInput', [ 392 | '$timeout', 393 | function($timeout) { 394 | return { 395 | restrict: 'A', 396 | scope: { 397 | model: '=ngModel' 398 | }, 399 | link: function($scope, $element, $attrs) { 400 | var supportedInputTypes = ['text', 'search', 'tel', 'url', 'email', 'password', 'number']; 401 | 402 | 403 | var container = angular.element('
'); 404 | var shadow = angular.element(''); 405 | 406 | var maxWidth = $element.css('maxWidth') === 'none' ? $element.parent().innerWidth() : $element.css('maxWidth'); 407 | $element.css('maxWidth', maxWidth); 408 | 409 | angular.forEach([ 410 | 'fontSize', 'fontFamily', 'fontWeight', 'fontStyle', 411 | 'letterSpacing', 'textTransform', 'wordSpacing', 'textIndent', 412 | 'boxSizing', 'borderLeftWidth', 'borderRightWidth', 'borderLeftStyle', 'borderRightStyle', 413 | 'paddingLeft', 'paddingRight', 'marginLeft', 'marginRight' 414 | ], function(css) { 415 | shadow.css(css, $element.css(css)); 416 | }); 417 | 418 | angular.element('body').append(container.append(shadow)); 419 | 420 | function resize() { 421 | $timeout(function() { 422 | if(supportedInputTypes.indexOf($element[0].type || 'text') === -1) 423 | return; 424 | 425 | shadow.text($element.val() || $element.attr('placeholder')); 426 | $element.css('width', shadow.outerWidth() + 10); 427 | }); 428 | } 429 | 430 | resize(); 431 | 432 | if ($scope.model) { 433 | $scope.$watch('model', function() { resize(); }); 434 | } else { 435 | $element.on('keypress keyup keydown focus input propertychange change', function() { resize(); }); 436 | } 437 | } 438 | }; 439 | } 440 | ]); 441 | })(); 442 | -------------------------------------------------------------------------------- /src/angular-advanced-searchbox.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * angular-advanced-searchbox 3 | * https://github.com/dnauck/angular-advanced-searchbox 4 | * Copyright (c) 2016 Nauck IT KG http://www.nauck-it.de/ 5 | * Author: Daniel Nauck 6 | * License: MIT 7 | */ 8 | 9 | (function() { 10 | 11 | 'use strict'; 12 | 13 | angular.module('angular-advanced-searchbox', []) 14 | .directive('nitAdvancedSearchbox', function() { 15 | return { 16 | restrict: 'E', 17 | scope: { 18 | model: '=ngModel', 19 | parameters: '=', 20 | parametersLabel: '@', 21 | parametersDisplayLimit: '=?', 22 | placeholder: '@', 23 | searchThrottleTime: '=?' 24 | }, 25 | replace: true, 26 | templateUrl: function(element, attr) { 27 | return attr.templateUrl || 'angular-advanced-searchbox.html'; 28 | }, 29 | controller: [ 30 | '$scope', '$attrs', '$element', '$timeout', '$filter', 'setFocusFor', 31 | function ($scope, $attrs, $element, $timeout, $filter, setFocusFor) { 32 | 33 | $scope.parametersLabel = $scope.parametersLabel || 'Parameter Suggestions'; 34 | $scope.parametersDisplayLimit = $scope.parametersDisplayLimit || 8; 35 | $scope.placeholder = $scope.placeholder || 'Search ...'; 36 | $scope.searchThrottleTime = $scope.searchThrottleTime || 1000; 37 | $scope.searchParams = []; 38 | $scope.searchQuery = ''; 39 | $scope.setFocusFor = setFocusFor; 40 | var searchThrottleTimer; 41 | var changeBuffer = []; 42 | 43 | $scope.$watch('model', function (newValue, oldValue) { 44 | 45 | if(angular.equals(newValue, oldValue)) 46 | return; 47 | 48 | angular.forEach($scope.model, function (value, key) { 49 | if (key === 'query' && $scope.searchQuery !== value) { 50 | $scope.searchQuery = value; 51 | } else { 52 | var paramTemplate = $filter('filter')($scope.parameters, function (param) { return param.key === key; })[0]; 53 | var searchParams = $filter('filter')($scope.searchParams, function (param) { return param.key === key; }); 54 | 55 | if (paramTemplate !== undefined) { 56 | if (paramTemplate.allowMultiple) { 57 | // ensure array data structure 58 | if(!angular.isArray(value)) 59 | value = [value]; 60 | 61 | // for each value in the value array: check for adding a new parameter or update it's value 62 | value.forEach(function(val, valIndex) { 63 | if (searchParams.some(function (param) { return param.index === valIndex; })) { 64 | var param = searchParams.filter(function (param) {return param.index === valIndex; }); 65 | if(param[0].value !== val) 66 | param[0].value = val; 67 | } else { 68 | $scope.addSearchParam(paramTemplate, val, false); 69 | } 70 | }); 71 | 72 | // check if there're more search parameters active then values and remove them 73 | if (value.length < searchParams.length) { 74 | for (var i = value.length; i < searchParams.length; i++) { 75 | $scope.removeSearchParam($scope.searchParams.indexOf(searchParams[i])); 76 | } 77 | } 78 | } else { 79 | if (searchParams.length === 0) { 80 | // add param if missing 81 | $scope.addSearchParam(paramTemplate, value, false); 82 | } else { 83 | // update value of parameter if not equal 84 | if(searchParams[0].value !== value) 85 | searchParams[0].value = value; 86 | } 87 | } 88 | } 89 | } 90 | }); 91 | 92 | // delete not existing search parameters from internal state array 93 | for (var i = $scope.searchParams.length - 1; i >= 0; i--) { 94 | var value = $scope.searchParams[i]; 95 | if (!$scope.model.hasOwnProperty(value.key)){ 96 | var index = $scope.searchParams.map(function(e) { return e.key; }).indexOf(value.key); 97 | $scope.removeSearchParam(index); 98 | } 99 | } 100 | }, true); 101 | 102 | $scope.searchParamValueChanged = function (param) { 103 | updateModel('change', param.key, param.index, param.value); 104 | }; 105 | 106 | $scope.searchQueryChanged = function (query) { 107 | updateModel('change', 'query', 0, query); 108 | }; 109 | 110 | $scope.enterEditMode = function(e, index) { 111 | if(e !== undefined) 112 | e.stopPropagation(); 113 | 114 | if (index === undefined) 115 | return; 116 | 117 | var searchParam = $scope.searchParams[index]; 118 | searchParam.editMode = true; 119 | setFocusFor('searchParam:' + searchParam.key); 120 | 121 | $scope.$emit('advanced-searchbox:enteredEditMode', searchParam); 122 | }; 123 | 124 | $scope.leaveEditMode = function(e, index) { 125 | if (index === undefined) 126 | return; 127 | 128 | var searchParam = $scope.searchParams[index]; 129 | searchParam.editMode = false; 130 | 131 | $scope.$emit('advanced-searchbox:leavedEditMode', searchParam); 132 | 133 | // remove empty search params 134 | if (!searchParam.value) 135 | $scope.removeSearchParam(index); 136 | }; 137 | 138 | $scope.searchQueryTypeaheadOnSelect = function (item, model, label) { 139 | $scope.addSearchParam(item); 140 | $scope.searchQuery = ''; 141 | updateModel('delete', 'query', 0); 142 | }; 143 | 144 | $scope.searchParamTypeaheadOnSelect = function (suggestedValue, searchParam) { 145 | searchParam.value = suggestedValue; 146 | $scope.searchParamValueChanged(searchParam); 147 | }; 148 | 149 | $scope.isUnsedParameter = function (value, index) { 150 | return $filter('filter')($scope.searchParams, function (param) { return param.key === value.key && !param.allowMultiple; }).length === 0; 151 | }; 152 | 153 | $scope.addSearchParam = function (searchParam, value, enterEditModel) { 154 | if (enterEditModel === undefined) 155 | enterEditModel = true; 156 | 157 | if (!$scope.isUnsedParameter(searchParam)) 158 | return; 159 | 160 | var internalIndex = 0; 161 | if(searchParam.allowMultiple) 162 | internalIndex = $filter('filter')($scope.searchParams, function (param) { return param.key === searchParam.key; }).length; 163 | 164 | var newIndex = 165 | $scope.searchParams.push( 166 | { 167 | key: searchParam.key, 168 | name: searchParam.name, 169 | type: searchParam.type || 'text', 170 | placeholder: searchParam.placeholder, 171 | allowMultiple: searchParam.allowMultiple || false, 172 | suggestedValues: searchParam.suggestedValues || [], 173 | restrictToSuggestedValues: searchParam.restrictToSuggestedValues || false, 174 | index: internalIndex, 175 | value: value || '' 176 | } 177 | ) - 1; 178 | 179 | updateModel('add', searchParam.key, internalIndex, value); 180 | 181 | if (enterEditModel === true) 182 | $timeout(function() { $scope.enterEditMode(undefined, newIndex); }, 100); 183 | 184 | $scope.$emit('advanced-searchbox:addedSearchParam', searchParam); 185 | }; 186 | 187 | $scope.removeSearchParam = function (index) { 188 | if (index === undefined) 189 | return; 190 | 191 | var searchParam = $scope.searchParams[index]; 192 | $scope.searchParams.splice(index, 1); 193 | 194 | // reassign internal index 195 | if(searchParam.allowMultiple){ 196 | var paramsOfSameKey = $filter('filter')($scope.searchParams, function (param) { return param.key === searchParam.key; }); 197 | 198 | for (var i = 0; i < paramsOfSameKey.length; i++) { 199 | paramsOfSameKey[i].index = i; 200 | } 201 | } 202 | 203 | updateModel('delete', searchParam.key, searchParam.index); 204 | 205 | $scope.$emit('advanced-searchbox:removedSearchParam', searchParam); 206 | }; 207 | 208 | $scope.removeAll = function() { 209 | $scope.searchParams.length = 0; 210 | $scope.searchQuery = ''; 211 | 212 | $scope.model = {}; 213 | 214 | $scope.$emit('advanced-searchbox:removedAllSearchParam'); 215 | }; 216 | 217 | $scope.editPrevious = function(currentIndex) { 218 | if (currentIndex !== undefined) 219 | $scope.leaveEditMode(undefined, currentIndex); 220 | 221 | if (currentIndex > 0) { 222 | $scope.enterEditMode(undefined, currentIndex - 1); 223 | } else if ($scope.searchParams.length > 0) { 224 | $scope.enterEditMode(undefined, $scope.searchParams.length - 1); 225 | } else if ($scope.searchParams.length === 0) { 226 | // no search parameter available anymore 227 | setFocusFor('searchbox'); 228 | } 229 | }; 230 | 231 | $scope.editNext = function(currentIndex) { 232 | if (currentIndex === undefined) 233 | return; 234 | 235 | $scope.leaveEditMode(undefined, currentIndex); 236 | 237 | //TODO: check if index == array length - 1 -> what then? 238 | if (currentIndex < $scope.searchParams.length - 1) { 239 | $scope.enterEditMode(undefined, currentIndex + 1); 240 | } else { 241 | setFocusFor('searchbox'); 242 | } 243 | }; 244 | 245 | $scope.keydown = function(e, searchParamIndex) { 246 | var handledKeys = [8, 9, 13, 37, 39]; 247 | if (handledKeys.indexOf(e.which) === -1) 248 | return; 249 | 250 | var cursorPosition = getCurrentCaretPosition(e.target); 251 | 252 | if (e.which == 8) { // backspace 253 | if (cursorPosition === 0) { 254 | e.preventDefault(); 255 | $scope.editPrevious(searchParamIndex); 256 | } 257 | 258 | } else if (e.which == 9) { // tab 259 | if (e.shiftKey) { 260 | e.preventDefault(); 261 | $scope.editPrevious(searchParamIndex); 262 | } else { 263 | e.preventDefault(); 264 | $scope.editNext(searchParamIndex); 265 | } 266 | 267 | } else if (e.which == 13) { // enter 268 | $scope.editNext(searchParamIndex); 269 | 270 | } else if (e.which == 37) { // left 271 | if (cursorPosition === 0) 272 | $scope.editPrevious(searchParamIndex); 273 | 274 | } else if (e.which == 39) { // right 275 | if (cursorPosition === e.target.value.length) 276 | $scope.editNext(searchParamIndex); 277 | } 278 | }; 279 | 280 | function restoreModel() { 281 | angular.forEach($scope.model, function (value, key) { 282 | if (key === 'query') { 283 | $scope.searchQuery = value; 284 | } else { 285 | var searchParam = $filter('filter')($scope.parameters, function (param) { return param.key === key; })[0]; 286 | if (searchParam !== undefined) 287 | $scope.addSearchParam(searchParam, value, false); 288 | } 289 | }); 290 | } 291 | 292 | if ($scope.model === undefined) { 293 | $scope.model = {}; 294 | } else { 295 | restoreModel(); 296 | } 297 | 298 | function updateModel(command, key, index, value) { 299 | if (searchThrottleTimer) 300 | $timeout.cancel(searchThrottleTimer); 301 | 302 | // remove all previous entries to the same search key that was not handled yet 303 | changeBuffer = $filter('filter')(changeBuffer, function (change) { return change.key !== key && change.index !== index; }); 304 | // add new change to list 305 | changeBuffer.push({ 306 | command: command, 307 | key: key, 308 | index: index, 309 | value: value 310 | }); 311 | 312 | searchThrottleTimer = $timeout(function () { 313 | angular.forEach(changeBuffer, function (change) { 314 | var searchParam = $filter('filter')($scope.parameters, function (param) { return param.key === key; })[0]; 315 | if(searchParam && searchParam.allowMultiple){ 316 | if(!angular.isArray($scope.model[change.key])) 317 | $scope.model[change.key] = []; 318 | 319 | if(change.command === 'delete'){ 320 | $scope.model[change.key].splice(change.index, 1); 321 | if($scope.model[change.key].length === 0) 322 | delete $scope.model[change.key]; 323 | } else { 324 | $scope.model[change.key][change.index] = change.value; 325 | } 326 | } else { 327 | if(change.command === 'delete') 328 | delete $scope.model[change.key]; 329 | else 330 | $scope.model[change.key] = change.value; 331 | } 332 | }); 333 | 334 | changeBuffer.length = 0; 335 | 336 | $scope.$emit('advanced-searchbox:modelUpdated', $scope.model); 337 | 338 | }, $scope.searchThrottleTime); 339 | } 340 | 341 | function getCurrentCaretPosition(input) { 342 | if (!input) 343 | return 0; 344 | 345 | try { 346 | // Firefox & co 347 | if (typeof input.selectionStart === 'number') { 348 | return input.selectionDirection === 'backward' ? input.selectionStart : input.selectionEnd; 349 | 350 | } else if (document.selection) { // IE 351 | input.focus(); 352 | var selection = document.selection.createRange(); 353 | var selectionLength = document.selection.createRange().text.length; 354 | selection.moveStart('character', -input.value.length); 355 | return selection.text.length - selectionLength; 356 | } 357 | } catch(err) { 358 | // selectionStart is not supported by HTML 5 input type, so jut ignore it 359 | } 360 | 361 | return 0; 362 | } 363 | } 364 | ] 365 | }; 366 | }) 367 | .directive('setFocusOn', [ 368 | function() { 369 | return { 370 | restrict: 'A', 371 | link: function($scope, $element, $attrs) { 372 | return $scope.$on('advanced-searchbox:setFocusOn', function(e, id) { 373 | if (id === $attrs.setFocusOn) { 374 | return $element[0].focus(); 375 | } 376 | }); 377 | } 378 | }; 379 | } 380 | ]) 381 | .factory('setFocusFor', [ 382 | '$rootScope', '$timeout', 383 | function($rootScope, $timeout) { 384 | return function(id) { 385 | return $timeout(function() { 386 | return $rootScope.$broadcast('advanced-searchbox:setFocusOn', id); 387 | }); 388 | }; 389 | } 390 | ]) 391 | .directive('nitAutoSizeInput', [ 392 | '$timeout', 393 | function($timeout) { 394 | return { 395 | restrict: 'A', 396 | scope: { 397 | model: '=ngModel' 398 | }, 399 | link: function($scope, $element, $attrs) { 400 | var supportedInputTypes = ['text', 'search', 'tel', 'url', 'email', 'password', 'number']; 401 | 402 | 403 | var container = angular.element('
'); 404 | var shadow = angular.element(''); 405 | 406 | var maxWidth = $element.css('maxWidth') === 'none' ? $element.parent().innerWidth() : $element.css('maxWidth'); 407 | $element.css('maxWidth', maxWidth); 408 | 409 | angular.forEach([ 410 | 'fontSize', 'fontFamily', 'fontWeight', 'fontStyle', 411 | 'letterSpacing', 'textTransform', 'wordSpacing', 'textIndent', 412 | 'boxSizing', 'borderLeftWidth', 'borderRightWidth', 'borderLeftStyle', 'borderRightStyle', 413 | 'paddingLeft', 'paddingRight', 'marginLeft', 'marginRight' 414 | ], function(css) { 415 | shadow.css(css, $element.css(css)); 416 | }); 417 | 418 | angular.element('body').append(container.append(shadow)); 419 | 420 | function resize() { 421 | $timeout(function() { 422 | if(supportedInputTypes.indexOf($element[0].type || 'text') === -1) 423 | return; 424 | 425 | shadow.text($element.val() || $element.attr('placeholder')); 426 | $element.css('width', shadow.outerWidth() + 10); 427 | }); 428 | } 429 | 430 | resize(); 431 | 432 | if ($scope.model) { 433 | $scope.$watch('model', function() { resize(); }); 434 | } else { 435 | $element.on('keypress keyup keydown focus input propertychange change', function() { resize(); }); 436 | } 437 | } 438 | }; 439 | } 440 | ]); 441 | })(); 442 | -------------------------------------------------------------------------------- /dist/angular-advanced-searchbox-tpls.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * angular-advanced-searchbox 3 | * https://github.com/dnauck/angular-advanced-searchbox 4 | * Copyright (c) 2016 Nauck IT KG http://www.nauck-it.de/ 5 | * Author: Daniel Nauck 6 | * License: MIT 7 | */ 8 | 9 | (function() { 10 | 11 | 'use strict'; 12 | 13 | angular.module('angular-advanced-searchbox', []) 14 | .directive('nitAdvancedSearchbox', function() { 15 | return { 16 | restrict: 'E', 17 | scope: { 18 | model: '=ngModel', 19 | parameters: '=', 20 | parametersLabel: '@', 21 | parametersDisplayLimit: '=?', 22 | placeholder: '@', 23 | searchThrottleTime: '=?' 24 | }, 25 | replace: true, 26 | templateUrl: function(element, attr) { 27 | return attr.templateUrl || 'angular-advanced-searchbox.html'; 28 | }, 29 | controller: [ 30 | '$scope', '$attrs', '$element', '$timeout', '$filter', 'setFocusFor', 31 | function ($scope, $attrs, $element, $timeout, $filter, setFocusFor) { 32 | 33 | $scope.parametersLabel = $scope.parametersLabel || 'Parameter Suggestions'; 34 | $scope.parametersDisplayLimit = $scope.parametersDisplayLimit || 8; 35 | $scope.placeholder = $scope.placeholder || 'Search ...'; 36 | $scope.searchThrottleTime = $scope.searchThrottleTime || 1000; 37 | $scope.searchParams = []; 38 | $scope.searchQuery = ''; 39 | $scope.setFocusFor = setFocusFor; 40 | var searchThrottleTimer; 41 | var changeBuffer = []; 42 | 43 | $scope.$watch('model', function (newValue, oldValue) { 44 | 45 | if(angular.equals(newValue, oldValue)) 46 | return; 47 | 48 | angular.forEach($scope.model, function (value, key) { 49 | if (key === 'query' && $scope.searchQuery !== value) { 50 | $scope.searchQuery = value; 51 | } else { 52 | var paramTemplate = $filter('filter')($scope.parameters, function (param) { return param.key === key; })[0]; 53 | var searchParams = $filter('filter')($scope.searchParams, function (param) { return param.key === key; }); 54 | 55 | if (paramTemplate !== undefined) { 56 | if (paramTemplate.allowMultiple) { 57 | // ensure array data structure 58 | if(!angular.isArray(value)) 59 | value = [value]; 60 | 61 | // for each value in the value array: check for adding a new parameter or update it's value 62 | value.forEach(function(val, valIndex) { 63 | if (searchParams.some(function (param) { return param.index === valIndex; })) { 64 | var param = searchParams.filter(function (param) {return param.index === valIndex; }); 65 | if(param[0].value !== val) 66 | param[0].value = val; 67 | } else { 68 | $scope.addSearchParam(paramTemplate, val, false); 69 | } 70 | }); 71 | 72 | // check if there're more search parameters active then values and remove them 73 | if (value.length < searchParams.length) { 74 | for (var i = value.length; i < searchParams.length; i++) { 75 | $scope.removeSearchParam($scope.searchParams.indexOf(searchParams[i])); 76 | } 77 | } 78 | } else { 79 | if (searchParams.length === 0) { 80 | // add param if missing 81 | $scope.addSearchParam(paramTemplate, value, false); 82 | } else { 83 | // update value of parameter if not equal 84 | if(searchParams[0].value !== value) 85 | searchParams[0].value = value; 86 | } 87 | } 88 | } 89 | } 90 | }); 91 | 92 | // delete not existing search parameters from internal state array 93 | for (var i = $scope.searchParams.length - 1; i >= 0; i--) { 94 | var value = $scope.searchParams[i]; 95 | if (!$scope.model.hasOwnProperty(value.key)){ 96 | var index = $scope.searchParams.map(function(e) { return e.key; }).indexOf(value.key); 97 | $scope.removeSearchParam(index); 98 | } 99 | } 100 | }, true); 101 | 102 | $scope.searchParamValueChanged = function (param) { 103 | updateModel('change', param.key, param.index, param.value); 104 | }; 105 | 106 | $scope.searchQueryChanged = function (query) { 107 | updateModel('change', 'query', 0, query); 108 | }; 109 | 110 | $scope.enterEditMode = function(e, index) { 111 | if(e !== undefined) 112 | e.stopPropagation(); 113 | 114 | if (index === undefined) 115 | return; 116 | 117 | var searchParam = $scope.searchParams[index]; 118 | searchParam.editMode = true; 119 | setFocusFor('searchParam:' + searchParam.key); 120 | 121 | $scope.$emit('advanced-searchbox:enteredEditMode', searchParam); 122 | }; 123 | 124 | $scope.leaveEditMode = function(e, index) { 125 | if (index === undefined) 126 | return; 127 | 128 | var searchParam = $scope.searchParams[index]; 129 | searchParam.editMode = false; 130 | 131 | $scope.$emit('advanced-searchbox:leavedEditMode', searchParam); 132 | 133 | // remove empty search params 134 | if (!searchParam.value) 135 | $scope.removeSearchParam(index); 136 | }; 137 | 138 | $scope.searchQueryTypeaheadOnSelect = function (item, model, label) { 139 | $scope.addSearchParam(item); 140 | $scope.searchQuery = ''; 141 | updateModel('delete', 'query', 0); 142 | }; 143 | 144 | $scope.searchParamTypeaheadOnSelect = function (suggestedValue, searchParam) { 145 | searchParam.value = suggestedValue; 146 | $scope.searchParamValueChanged(searchParam); 147 | }; 148 | 149 | $scope.isUnsedParameter = function (value, index) { 150 | return $filter('filter')($scope.searchParams, function (param) { return param.key === value.key && !param.allowMultiple; }).length === 0; 151 | }; 152 | 153 | $scope.addSearchParam = function (searchParam, value, enterEditModel) { 154 | if (enterEditModel === undefined) 155 | enterEditModel = true; 156 | 157 | if (!$scope.isUnsedParameter(searchParam)) 158 | return; 159 | 160 | var internalIndex = 0; 161 | if(searchParam.allowMultiple) 162 | internalIndex = $filter('filter')($scope.searchParams, function (param) { return param.key === searchParam.key; }).length; 163 | 164 | var newIndex = 165 | $scope.searchParams.push( 166 | { 167 | key: searchParam.key, 168 | name: searchParam.name, 169 | type: searchParam.type || 'text', 170 | placeholder: searchParam.placeholder, 171 | allowMultiple: searchParam.allowMultiple || false, 172 | suggestedValues: searchParam.suggestedValues || [], 173 | restrictToSuggestedValues: searchParam.restrictToSuggestedValues || false, 174 | index: internalIndex, 175 | value: value || '' 176 | } 177 | ) - 1; 178 | 179 | updateModel('add', searchParam.key, internalIndex, value); 180 | 181 | if (enterEditModel === true) 182 | $timeout(function() { $scope.enterEditMode(undefined, newIndex); }, 100); 183 | 184 | $scope.$emit('advanced-searchbox:addedSearchParam', searchParam); 185 | }; 186 | 187 | $scope.removeSearchParam = function (index) { 188 | if (index === undefined) 189 | return; 190 | 191 | var searchParam = $scope.searchParams[index]; 192 | $scope.searchParams.splice(index, 1); 193 | 194 | // reassign internal index 195 | if(searchParam.allowMultiple){ 196 | var paramsOfSameKey = $filter('filter')($scope.searchParams, function (param) { return param.key === searchParam.key; }); 197 | 198 | for (var i = 0; i < paramsOfSameKey.length; i++) { 199 | paramsOfSameKey[i].index = i; 200 | } 201 | } 202 | 203 | updateModel('delete', searchParam.key, searchParam.index); 204 | 205 | $scope.$emit('advanced-searchbox:removedSearchParam', searchParam); 206 | }; 207 | 208 | $scope.removeAll = function() { 209 | $scope.searchParams.length = 0; 210 | $scope.searchQuery = ''; 211 | 212 | $scope.model = {}; 213 | 214 | $scope.$emit('advanced-searchbox:removedAllSearchParam'); 215 | }; 216 | 217 | $scope.editPrevious = function(currentIndex) { 218 | if (currentIndex !== undefined) 219 | $scope.leaveEditMode(undefined, currentIndex); 220 | 221 | if (currentIndex > 0) { 222 | $scope.enterEditMode(undefined, currentIndex - 1); 223 | } else if ($scope.searchParams.length > 0) { 224 | $scope.enterEditMode(undefined, $scope.searchParams.length - 1); 225 | } else if ($scope.searchParams.length === 0) { 226 | // no search parameter available anymore 227 | setFocusFor('searchbox'); 228 | } 229 | }; 230 | 231 | $scope.editNext = function(currentIndex) { 232 | if (currentIndex === undefined) 233 | return; 234 | 235 | $scope.leaveEditMode(undefined, currentIndex); 236 | 237 | //TODO: check if index == array length - 1 -> what then? 238 | if (currentIndex < $scope.searchParams.length - 1) { 239 | $scope.enterEditMode(undefined, currentIndex + 1); 240 | } else { 241 | setFocusFor('searchbox'); 242 | } 243 | }; 244 | 245 | $scope.keydown = function(e, searchParamIndex) { 246 | var handledKeys = [8, 9, 13, 37, 39]; 247 | if (handledKeys.indexOf(e.which) === -1) 248 | return; 249 | 250 | var cursorPosition = getCurrentCaretPosition(e.target); 251 | 252 | if (e.which == 8) { // backspace 253 | if (cursorPosition === 0) { 254 | e.preventDefault(); 255 | $scope.editPrevious(searchParamIndex); 256 | } 257 | 258 | } else if (e.which == 9) { // tab 259 | if (e.shiftKey) { 260 | e.preventDefault(); 261 | $scope.editPrevious(searchParamIndex); 262 | } else { 263 | e.preventDefault(); 264 | $scope.editNext(searchParamIndex); 265 | } 266 | 267 | } else if (e.which == 13) { // enter 268 | $scope.editNext(searchParamIndex); 269 | 270 | } else if (e.which == 37) { // left 271 | if (cursorPosition === 0) 272 | $scope.editPrevious(searchParamIndex); 273 | 274 | } else if (e.which == 39) { // right 275 | if (cursorPosition === e.target.value.length) 276 | $scope.editNext(searchParamIndex); 277 | } 278 | }; 279 | 280 | function restoreModel() { 281 | angular.forEach($scope.model, function (value, key) { 282 | if (key === 'query') { 283 | $scope.searchQuery = value; 284 | } else { 285 | var searchParam = $filter('filter')($scope.parameters, function (param) { return param.key === key; })[0]; 286 | if (searchParam !== undefined) 287 | $scope.addSearchParam(searchParam, value, false); 288 | } 289 | }); 290 | } 291 | 292 | if ($scope.model === undefined) { 293 | $scope.model = {}; 294 | } else { 295 | restoreModel(); 296 | } 297 | 298 | function updateModel(command, key, index, value) { 299 | if (searchThrottleTimer) 300 | $timeout.cancel(searchThrottleTimer); 301 | 302 | // remove all previous entries to the same search key that was not handled yet 303 | changeBuffer = $filter('filter')(changeBuffer, function (change) { return change.key !== key && change.index !== index; }); 304 | // add new change to list 305 | changeBuffer.push({ 306 | command: command, 307 | key: key, 308 | index: index, 309 | value: value 310 | }); 311 | 312 | searchThrottleTimer = $timeout(function () { 313 | angular.forEach(changeBuffer, function (change) { 314 | var searchParam = $filter('filter')($scope.parameters, function (param) { return param.key === key; })[0]; 315 | if(searchParam && searchParam.allowMultiple){ 316 | if(!angular.isArray($scope.model[change.key])) 317 | $scope.model[change.key] = []; 318 | 319 | if(change.command === 'delete'){ 320 | $scope.model[change.key].splice(change.index, 1); 321 | if($scope.model[change.key].length === 0) 322 | delete $scope.model[change.key]; 323 | } else { 324 | $scope.model[change.key][change.index] = change.value; 325 | } 326 | } else { 327 | if(change.command === 'delete') 328 | delete $scope.model[change.key]; 329 | else 330 | $scope.model[change.key] = change.value; 331 | } 332 | }); 333 | 334 | changeBuffer.length = 0; 335 | 336 | $scope.$emit('advanced-searchbox:modelUpdated', $scope.model); 337 | 338 | }, $scope.searchThrottleTime); 339 | } 340 | 341 | function getCurrentCaretPosition(input) { 342 | if (!input) 343 | return 0; 344 | 345 | try { 346 | // Firefox & co 347 | if (typeof input.selectionStart === 'number') { 348 | return input.selectionDirection === 'backward' ? input.selectionStart : input.selectionEnd; 349 | 350 | } else if (document.selection) { // IE 351 | input.focus(); 352 | var selection = document.selection.createRange(); 353 | var selectionLength = document.selection.createRange().text.length; 354 | selection.moveStart('character', -input.value.length); 355 | return selection.text.length - selectionLength; 356 | } 357 | } catch(err) { 358 | // selectionStart is not supported by HTML 5 input type, so jut ignore it 359 | } 360 | 361 | return 0; 362 | } 363 | } 364 | ] 365 | }; 366 | }) 367 | .directive('setFocusOn', [ 368 | function() { 369 | return { 370 | restrict: 'A', 371 | link: function($scope, $element, $attrs) { 372 | return $scope.$on('advanced-searchbox:setFocusOn', function(e, id) { 373 | if (id === $attrs.setFocusOn) { 374 | return $element[0].focus(); 375 | } 376 | }); 377 | } 378 | }; 379 | } 380 | ]) 381 | .factory('setFocusFor', [ 382 | '$rootScope', '$timeout', 383 | function($rootScope, $timeout) { 384 | return function(id) { 385 | return $timeout(function() { 386 | return $rootScope.$broadcast('advanced-searchbox:setFocusOn', id); 387 | }); 388 | }; 389 | } 390 | ]) 391 | .directive('nitAutoSizeInput', [ 392 | '$timeout', 393 | function($timeout) { 394 | return { 395 | restrict: 'A', 396 | scope: { 397 | model: '=ngModel' 398 | }, 399 | link: function($scope, $element, $attrs) { 400 | var supportedInputTypes = ['text', 'search', 'tel', 'url', 'email', 'password', 'number']; 401 | 402 | 403 | var container = angular.element('
'); 404 | var shadow = angular.element(''); 405 | 406 | var maxWidth = $element.css('maxWidth') === 'none' ? $element.parent().innerWidth() : $element.css('maxWidth'); 407 | $element.css('maxWidth', maxWidth); 408 | 409 | angular.forEach([ 410 | 'fontSize', 'fontFamily', 'fontWeight', 'fontStyle', 411 | 'letterSpacing', 'textTransform', 'wordSpacing', 'textIndent', 412 | 'boxSizing', 'borderLeftWidth', 'borderRightWidth', 'borderLeftStyle', 'borderRightStyle', 413 | 'paddingLeft', 'paddingRight', 'marginLeft', 'marginRight' 414 | ], function(css) { 415 | shadow.css(css, $element.css(css)); 416 | }); 417 | 418 | angular.element('body').append(container.append(shadow)); 419 | 420 | function resize() { 421 | $timeout(function() { 422 | if(supportedInputTypes.indexOf($element[0].type || 'text') === -1) 423 | return; 424 | 425 | shadow.text($element.val() || $element.attr('placeholder')); 426 | $element.css('width', shadow.outerWidth() + 10); 427 | }); 428 | } 429 | 430 | resize(); 431 | 432 | if ($scope.model) { 433 | $scope.$watch('model', function() { resize(); }); 434 | } else { 435 | $element.on('keypress keyup keydown focus input propertychange change', function() { resize(); }); 436 | } 437 | } 438 | }; 439 | } 440 | ]); 441 | })(); 442 | 443 | angular.module('angular-advanced-searchbox').run(['$templateCache', function($templateCache) { 444 | 'use strict'; 445 | 446 | $templateCache.put('angular-advanced-searchbox.html', 447 | "
0 || searchQuery.length > 0\" ng-click=removeAll() role=button>
{{searchParam.name}}:
{{searchParam.value}}
{{parametersLabel}}: {{param.name}}
" 448 | ); 449 | 450 | }]); 451 | --------------------------------------------------------------------------------