├── .gitignore ├── Guardfile ├── .travis.yml ├── src ├── templates │ ├── 2 │ │ ├── input.coffee │ │ └── tag.coffee │ ├── 3 │ │ ├── input.coffee │ │ └── tag.coffee │ ├── shared │ │ ├── tags_container.coffee │ │ ├── suggestion_list.coffee │ │ └── tags_suggestion.coffee │ └── templates.coffee ├── helpers.coffee └── bootstrap-tags.coffee ├── tag-demo.html ├── bower.json ├── component.json ├── LICENSE ├── package.json ├── dist ├── css │ └── bootstrap-tags.css └── js │ ├── bootstrap-tags.min.js │ └── bootstrap-tags.js ├── sass └── bootstrap-tags.scss ├── demo-3.html ├── demo-2.html ├── Gruntfile.js ├── README.md └── spec ├── bootstrap-tags-spec.coffee └── bootstrap-tags-spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | tmp 4 | .sass-cache -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard 'rake', :task => 'compile' do 2 | watch /src\/.*.coffee/ 3 | end 4 | 5 | guard 'rake', :task => 'spec' do 6 | watch /spec\/*.coffee/ 7 | end -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.8" 4 | - "0.10" 5 | before_install: 6 | - npm install -g grunt-cli 7 | - npm install -g bower 8 | - bower install -------------------------------------------------------------------------------- /src/templates/shared/tags_container.coffee: -------------------------------------------------------------------------------- 1 | window.Tags ||= {} 2 | Tags.Templates ||= {} 3 | Tags.Templates.shared ||= {} 4 | Tags.Templates.shared.tags_container = (options = {}) -> 5 | '
' -------------------------------------------------------------------------------- /src/templates/3/input.coffee: -------------------------------------------------------------------------------- 1 | window.Tags ||= {} 2 | Tags.Templates ||= {} 3 | Tags.Templates["3"] ||= {} 4 | Tags.Templates["3"].input = (options = {}) -> 5 | "" -------------------------------------------------------------------------------- /src/templates/shared/suggestion_list.coffee: -------------------------------------------------------------------------------- 1 | window.Tags ||= {} 2 | Tags.Templates ||= {} 3 | Tags.Templates.shared ||= {} 4 | Tags.Templates.shared.suggestion_list = (options = {}) -> 5 | '' -------------------------------------------------------------------------------- /src/templates/shared/tags_suggestion.coffee: -------------------------------------------------------------------------------- 1 | window.Tags ||= {} 2 | Tags.Templates ||= {} 3 | Tags.Templates.shared ||= {} 4 | Tags.Templates.shared.tags_suggestion = (options = {}) -> 5 | "
  • #{options.suggestion}
  • " -------------------------------------------------------------------------------- /src/helpers.coffee: -------------------------------------------------------------------------------- 1 | window.Tags ||= {} 2 | Tags.Helpers ||= {} 3 | 4 | Tags.Helpers.addPadding = (string, amount = 1, doPadding = true) -> 5 | return string unless doPadding 6 | return string if amount == 0 7 | Tags.Helpers.addPadding(" #{string} ", amount - 1) -------------------------------------------------------------------------------- /src/templates/templates.coffee: -------------------------------------------------------------------------------- 1 | window.Tags ||= {} 2 | Tags.Templates ||= {} 3 | Tags.Templates.Template = (version, templateName, options) -> 4 | if Tags.Templates[version]? 5 | return Tags.Templates[version][templateName](options) if Tags.Templates[version][templateName]? 6 | Tags.Templates.shared[templateName](options) -------------------------------------------------------------------------------- /src/templates/2/input.coffee: -------------------------------------------------------------------------------- 1 | window.Tags ||= {} 2 | Tags.Templates ||= {} 3 | Tags.Templates["2"] ||= {} 4 | Tags.Templates["2"].input = (options = {}) -> 5 | tagSize = switch options.tagSize 6 | when 'sm' then 'small' 7 | when 'md' then 'medium' 8 | when 'lg' then 'large' 9 | "" -------------------------------------------------------------------------------- /src/templates/2/tag.coffee: -------------------------------------------------------------------------------- 1 | window.Tags ||= {} 2 | Tags.Templates ||= {} 3 | Tags.Templates["2"] ||= {} 4 | Tags.Templates["2"].tag = (options = {}) -> 5 | "
    6 | #{Tags.Helpers.addPadding options.tag, 2, options.isReadOnly} 7 | #{ if options.isReadOnly then "" else "" } 8 |
    " -------------------------------------------------------------------------------- /src/templates/3/tag.coffee: -------------------------------------------------------------------------------- 1 | window.Tags ||= {} 2 | Tags.Templates ||= {} 3 | Tags.Templates["3"] ||= {} 4 | Tags.Templates["3"].tag = (options = {}) -> 5 | "
    6 | #{Tags.Helpers.addPadding options.tag, 2, options.isReadOnly} 7 | #{ if options.isReadOnly then "" else "" } 8 |
    " -------------------------------------------------------------------------------- /tag-demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tag Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap-tags", 3 | "version": "1.1.5", 4 | "homepage": "http://github.com/maxwells/bootstrap-tags", 5 | "bugs": "https://github.com/maxwells/bootstrap-tags/issues", 6 | "authors": [ 7 | "Max Lahey " 8 | ], 9 | "description": "Bootstrap-themed jquery tag interface", 10 | "main": [ 11 | "dist/js/bootstrap-tags.js", 12 | "dist/css/bootstrap-tags.css" 13 | ], 14 | "keywords": [ 15 | "jquery", 16 | "bootstrap", 17 | "bootstrap3", 18 | "tagging", 19 | "tags", 20 | "typeahead", 21 | "autocomplete" 22 | ], 23 | "license": "MIT", 24 | "ignore": [ 25 | "src", 26 | "spec", 27 | "*.html" 28 | ], 29 | "dependencies": { 30 | "jquery": ">=1.9.0", 31 | "bootstrap": ">=2.3.2" 32 | }, 33 | "scripts": { 34 | "test": "grunt jasmine" 35 | } 36 | } -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap-tags", 3 | "version": "1.1.5", 4 | "homepage": "http://github.com/maxwells/bootstrap-tags", 5 | "bugs": "https://github.com/maxwells/bootstrap-tags/issues", 6 | "authors": [ 7 | "Max Lahey " 8 | ], 9 | "description": "Bootstrap-themed jquery tag interface", 10 | "main": [ 11 | "dist/js/bootstrap-tags.js", 12 | "dist/css/bootstrap-tags.css" 13 | ], 14 | "keywords": [ 15 | "jquery", 16 | "bootstrap", 17 | "bootstrap3", 18 | "tagging", 19 | "tags", 20 | "typeahead", 21 | "autocomplete" 22 | ], 23 | "license": "MIT", 24 | "ignore": [ 25 | "src", 26 | "spec", 27 | "*.html" 28 | ], 29 | "dependencies": { 30 | "jquery": ">=1.9.0", 31 | "bootstrap": ">=2.3.2" 32 | }, 33 | "scripts": { 34 | "test": "grunt jasmine" 35 | } 36 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2014 Max Lahey 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap-tags", 3 | "description": "Bootstrap-themed jquery tag interface", 4 | "keywords": [ 5 | "jquery", 6 | "bootstrap", 7 | "bootstrap3", 8 | "tagging", 9 | "tags", 10 | "typeahead", 11 | "autocomplete" 12 | ], 13 | "homepage": "http://github.com/maxwells/bootstrap-tags", 14 | "bugs": "https://github.com/maxwells/bootstrap-tags/issues", 15 | "author": { 16 | "name": "maxwells", 17 | "url": "http://maxwells.github.io" 18 | }, 19 | "devDependencies": { 20 | "grunt": "~0.4", 21 | "grunt-contrib-watch": "~0.2", 22 | "grunt-contrib-uglify": "git://github.com/jharding/grunt-contrib-uglify.git#support-enclose-option", 23 | "grunt-contrib-jasmine": "~0.5.2", 24 | "grunt-template-jasmine-requirejs": "0.1.8", 25 | "grunt-contrib-clean": "~0.4.0", 26 | "grunt-contrib-coffee": "~0.7.0", 27 | "grunt-contrib-jst": "~0.5.0", 28 | "grunt-contrib-sass": "~0.6.0", 29 | "semver": "~1.1.3", 30 | "grunt-parallel": "0.0.2", 31 | "grunt-lib-phantomjs": "~0.4.0", 32 | "grunt-growl": "latest" 33 | }, 34 | "scripts": { 35 | "test": "grunt test" 36 | }, 37 | "private": true, 38 | "version": "1.1.5" 39 | } -------------------------------------------------------------------------------- /dist/css/bootstrap-tags.css: -------------------------------------------------------------------------------- 1 | /* bootstrap-tags styles */ 2 | .bootstrap-tags.bootstrap-3 .tag a { 3 | margin: 0 0 0 .3em; } 4 | .bootstrap-tags.bootstrap-3 .glyphicon-white { 5 | color: #fff; } 6 | 7 | .bootstrap-tags.bootstrap-2 .tag.md { 8 | padding: .3em .4em .4em; } 9 | .bootstrap-tags.bootstrap-2 .tag.lg { 10 | padding: .4em .4em .5em; } 11 | 12 | .bootstrap-tags { 13 | position: relative; } 14 | .bootstrap-tags .tags { 15 | width: inherit; 16 | height: 0; 17 | position: absolute; 18 | padding: 0; 19 | margin: 0; } 20 | .bootstrap-tags .tag-data { 21 | display: none; } 22 | .bootstrap-tags .tags-input { 23 | width: 100%; 24 | margin: 0; 25 | padding: 0; 26 | height: 1.7em; 27 | box-sizing: content-box; 28 | -webkit-box-sizing: content-box; 29 | -moz-box-sizing: content-box; } 30 | .bootstrap-tags .tag-list { 31 | width: 280px; 32 | height: auto; 33 | min-height: 26px; 34 | left: 2px; 35 | top: 2px; 36 | position: relative; } 37 | .bootstrap-tags .tag { 38 | padding: .4em .4em .4em; 39 | margin: 0 .1em; 40 | float: left; } 41 | .bootstrap-tags .tag.sm { 42 | padding: .4em .4em .5em; 43 | font-size: 12px; } 44 | .bootstrap-tags .tag.md { 45 | font-size: 14px; } 46 | .bootstrap-tags .tag.lg { 47 | font-size: 18px; 48 | padding: .4em .4em .4em; 49 | margin: 0 .2em .2em 0; } 50 | .bootstrap-tags .tag a { 51 | color: #bbb; 52 | cursor: pointer; 53 | opacity: .5; } 54 | .bootstrap-tags .tag .remove { 55 | vertical-align: bottom; 56 | top: 0; } 57 | .bootstrap-tags ul.tags-suggestion-list { 58 | width: 300px; 59 | height: auto; 60 | list-style: none; 61 | margin: 0; 62 | z-index: 2; 63 | max-height: 160px; 64 | overflow: scroll; } 65 | .bootstrap-tags ul.tags-suggestion-list li.tags-suggestion { 66 | padding: 3px 20px; 67 | height: auto; } 68 | .bootstrap-tags ul.tags-suggestion-list li.tags-suggestion-highlighted { 69 | color: white; 70 | text-decoration: none; 71 | background-color: #0081C2; 72 | background-image: -moz-linear-gradient(top, #0088cc, #0077b3); 73 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); 74 | background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); 75 | background-image: -o-linear-gradient(top, #0088cc, #0077b3); 76 | background-image: linear-gradient(to bottom, #0088cc, #0077b3); 77 | background-repeat: repeat-x; 78 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); } 79 | -------------------------------------------------------------------------------- /sass/bootstrap-tags.scss: -------------------------------------------------------------------------------- 1 | /* bootstrap-tags styles */ 2 | 3 | .bootstrap-tags.bootstrap-3 { 4 | .tag a { 5 | margin: 0 0 0 .3em; 6 | } 7 | .glyphicon-white { 8 | color:#fff; 9 | } 10 | } 11 | 12 | .bootstrap-tags.bootstrap-2 { 13 | .tag.md { 14 | padding: .3em .4em .4em; 15 | } 16 | .tag.lg { 17 | padding: .4em .4em .5em; 18 | } 19 | } 20 | 21 | .bootstrap-tags { 22 | 23 | position:relative; 24 | 25 | .tags { 26 | width:inherit; 27 | height:0; 28 | position:absolute; 29 | padding:0; 30 | margin:0; 31 | } 32 | 33 | .tag-data { 34 | display:none; 35 | } 36 | 37 | .tags-input { 38 | width:100%; 39 | margin:0; 40 | padding:0; 41 | height:1.7em; 42 | box-sizing: content-box; 43 | -webkit-box-sizing: content-box; 44 | -moz-box-sizing: content-box; 45 | } 46 | 47 | .tag-list { 48 | width: 280px; 49 | height: auto; 50 | min-height: 26px; 51 | left:2px; 52 | top:2px; 53 | position:relative; 54 | } 55 | 56 | .tag { 57 | padding: .4em .4em .4em; 58 | margin:0 .1em; 59 | float:left; 60 | } 61 | 62 | .tag.sm { 63 | padding: .4em .4em .5em; 64 | font-size: 12px; 65 | } 66 | .tag.md { 67 | font-size: 14px; 68 | // min-height: 16px; 69 | } 70 | .tag.lg { 71 | font-size: 18px; 72 | padding: .4em .4em .4em; 73 | margin:0 .2em .2em 0; 74 | } 75 | 76 | .tag a { 77 | color: #bbb; 78 | cursor:pointer; 79 | opacity: .5; 80 | } 81 | 82 | .tag .remove { 83 | vertical-align:bottom; 84 | top:0; 85 | } 86 | 87 | ul.tags-suggestion-list { 88 | width:300px; 89 | height:auto; 90 | list-style:none; 91 | margin:0; 92 | z-index:2; 93 | max-height:160px; 94 | overflow: scroll; 95 | 96 | li.tags-suggestion { 97 | padding:3px 20px; 98 | height:auto; 99 | } 100 | li.tags-suggestion-highlighted { 101 | color: white; 102 | text-decoration: none; 103 | background-color: #0081C2; 104 | background-image: -moz-linear-gradient(top, #08C, #0077B3); 105 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#08C), to(#0077B3)); 106 | background-image: -webkit-linear-gradient(top, #08C, #0077B3); 107 | background-image: -o-linear-gradient(top, #08C, #0077B3); 108 | background-image: linear-gradient(to bottom, #08C, #0077B3); 109 | background-repeat: repeat-x; 110 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /demo-3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tag Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
    15 | 16 |

    Bootstrap 3.0.3

    17 | 18 |

    Small

    19 |
    20 |

    Large

    21 |
    22 |

    Medium

    23 |
    24 |

    maxNumTags = 3

    25 |
    26 |

    suggestOnClick

    27 |
    28 |

    caseInsensitive

    29 |
    30 |

    readOnly

    31 |
    32 | 33 | 34 | 72 |
    73 | 74 | -------------------------------------------------------------------------------- /demo-2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tag Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
    15 | 16 |

    Bootstrap 2.3.2

    17 | 18 |

    Small

    19 |
    20 |

    Large

    21 |
    22 |

    Medium

    23 |
    24 |

    maxNumTags = 3

    25 |
    26 |

    suggestOnClick

    27 |
    28 |

    caseInsensitive

    29 |
    30 |

    readOnly

    31 |
    32 | 33 | 72 |
    73 | 74 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var jsFiles = [ 2 | 'src/bootstrap-tags.js' 3 | ]; 4 | 5 | var coffeeFiles = 'src/**/*.coffee' 6 | 7 | var specCoffeeFiles = [ 8 | 'spec/*.coffee' 9 | ]; 10 | 11 | module.exports = function(grunt) { 12 | grunt.initConfig({ 13 | version: grunt.file.readJSON('component.json').version, 14 | 15 | uglify: { 16 | options: { 17 | banner: [ 18 | '/*!', 19 | ' * bootstrap-tags 1.1.5', 20 | ' * https://github.com/maxwells/bootstrap-tags', 21 | ' * Copyright 2013 Max Lahey; Licensed MIT', 22 | ' */\n\n' 23 | ].join('\n'), 24 | enclose: { 'window.jQuery': '$' } 25 | }, 26 | js : { 27 | options: { 28 | mangle: false, 29 | beautify: true, 30 | compress: false 31 | }, 32 | src: jsFiles, 33 | dest: 'dist/js/bootstrap-tags.js' 34 | }, 35 | jsmin: { 36 | options: { 37 | mangle: true, 38 | compress: true 39 | }, 40 | src: jsFiles, 41 | dest: 'dist/js/bootstrap-tags.min.js' 42 | } 43 | }, 44 | 45 | sed: { 46 | version: { 47 | pattern: '%VERSION%', 48 | replacement: '<%= version %>', 49 | path: ['<%= uglify.js.dest %>', '<%= uglify.jsmin.dest %>'] 50 | } 51 | }, 52 | 53 | watch: { 54 | buildCoffee: { 55 | files: coffeeFiles, 56 | tasks: ['build', 'jasmine:test'] 57 | }, 58 | buildSpec: { 59 | files: specCoffeeFiles, 60 | tasks: ['build', 'jasmine:test'] 61 | }, 62 | buildCss: { 63 | files: ['sass/bootstrap-tags.scss'], 64 | tasks: ['sass'] 65 | } 66 | }, 67 | 68 | coffee: { 69 | compile: { 70 | files: { 71 | 'src/bootstrap-tags.js': coffeeFiles 72 | } 73 | }, 74 | spec: { 75 | files: { 76 | 'spec/bootstrap-tags-spec.js': specCoffeeFiles 77 | } 78 | } 79 | }, 80 | 81 | clean: { 82 | dist: 'dist' 83 | }, 84 | 85 | jasmine: { 86 | test: { 87 | src: './dist/js/bootstrap-tags.js', 88 | options: { 89 | specs: 'spec/*spec.js', 90 | keepRunner: false, 91 | template: require('grunt-template-jasmine-requirejs'), 92 | templateOptions: { 93 | requireConfig: { 94 | paths: { 95 | "jquery": "./bower_components/jquery/dist/jquery", 96 | }, 97 | deps: ['jquery'] 98 | } 99 | } 100 | } 101 | } 102 | }, 103 | 104 | sass: { 105 | dist: { 106 | files: { 107 | 'dist/css/bootstrap-tags.css': 'sass/bootstrap-tags.scss' 108 | } 109 | } 110 | } 111 | 112 | }); 113 | 114 | grunt.registerTask('default', 'build'); 115 | grunt.registerTask('build', ['coffee', 'uglify']); 116 | grunt.registerTask('server', 'connect:server'); 117 | grunt.registerTask('lint', 'jshint'); 118 | grunt.registerTask('test', ['coffee:compile', 'coffee:spec', 'uglify:js', 'jasmine:test']); 119 | grunt.registerTask('dev', 'parallel:dev'); 120 | 121 | // load tasks 122 | // ---------- 123 | grunt.loadNpmTasks('grunt-contrib-watch'); 124 | grunt.loadNpmTasks('grunt-contrib-clean'); 125 | grunt.loadNpmTasks('grunt-contrib-uglify'); 126 | grunt.loadNpmTasks('grunt-contrib-coffee'); 127 | grunt.loadNpmTasks('grunt-contrib-jasmine'); 128 | grunt.loadNpmTasks('grunt-contrib-sass'); 129 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bootstrap Tags 2 | 3 | Bootstrap Tags is a jQuery plugin meant to extend Twitter Bootstrap to include tagging functionality. It supports Bootstrap 2.3.2 and ≥ 3.0. 4 | 5 | [![Build Status](https://travis-ci.org/maxwells/bootstrap-tags.png?branch=master)](https://travis-ci.org/maxwells/bootstrap-tags) 6 | 7 | ## Call for Maintainers 8 | 9 | If you find this project useful and are interested in contributing or taking over, please add yourself to [https://github.com/maxwells/bootstrap-tags/issues/70](https://github.com/maxwells/bootstrap-tags/issues/70). 10 | 11 | ## Demo 12 | [http://maxwells.github.com/bootstrap-tags.html](http://maxwells.github.com/bootstrap-tags.html) 13 | 14 | ## Installation 15 | 16 | $ bower install bootstrap-tags 17 | 18 | or 19 | 20 | $ git clone https://github.com/maxwells/bootstrap-tags.git 21 | --> js files are located in dist/js, CSS in dist/css 22 | 23 | ## Features 24 | - Support for Bootstrap 2.3.2 and 3+ 25 | - Autosuggest (for typing or activated by pressing the down key when empty) 26 | - Bootstrap Popovers (for extended information on a tag) 27 | - Exclusions (denial of a specified list) 28 | - Filters (allowance of only a specified list) 29 | - Placeholder prompts 30 | - Uses bootstrap button-[type] class styling (customizing your bootstrap will change tag styles accordingly) 31 | - Extensible with custom functions (eg, before/after tag addition/deletion, key presses, exclusions) 32 | 33 | ## Usage 34 | 35 | 36 | 37 | 38 | 39 |
    40 | 51 | 52 | ## Documentation 53 | 54 | ### Settings 55 | 56 | The following options are supported. Pass them as a javascript object to the `tags` jQuery function: 57 | 58 | ```javascript 59 | $('selector').tags({ 60 | readOnly: true, 61 | tagData: ["a", "prepopulated", "list", "of", tags], 62 | beforeAddingTag: function(tag){ console.log(tag); } 63 | }); 64 | ``` 65 | 66 | option | type | description | default 67 | -------|------|-------------|--------- 68 | `bootstrapVersion` | `String` | specify which version of bootstrap to format generated HTML for. Acceptable values are "2", "3" | `3` 69 | `tagData` | `Array` | a list of tags to initialize the tagging interface with | `[]` 70 | `tagSize` | `String` | describes what size input to use. Acceptable values are "lg", "md", or "sm" | `md` 71 | `readOnly` | `Boolean` | whether or not to disable user input | `false` 72 | `suggestions` | `Array` | a list of terms that will populate the autosuggest feature when a user types in the first character | `[]` 73 | `suggestOnClick` | `Boolean` | whether or not the autosuggest feature is triggered on click | `false` 74 | `caseInsensitive` | `Boolean` | whether or not autosuggest should ignore case sensitivity | `false` 75 | `restrictTo` | `Array` | a list of allowed tags (will be combined with suggestions, if provided). User inputted tags that aren't included in this list will be ignored | `[]` 76 | `exclude` | `Array` | a list of case insensitive disallowed tags. Supports wildcarding (eg. `['*offensive*']` will ignore any word that has `offensive` in it) | `[]` 77 | `popoverData` | `Array` | a list of popover data. The index of each element should match the index of corresponding tag in `tagData` array | `null` 78 | `popovers` | `Boolean` | whether or not to enable bootstrap popovers on tag mouseover | whether `popoverData` was provided 79 | `popoverTrigger` | `String` | indicates how popovers should be triggered. Acceptable values are 'click', 'hover', 'hoverShowClickHide' | `hover` 80 | `tagClass` | `String` | which class the tag div will have for styling | `btn-info` 81 | `promptText` | `String` | placeholder string when there are no tags and nothing typed in | `Enter tags…` 82 | `maxNumTags` | `Integer` | Maximum number of allowable (user-added) tags. All tags that are initialized in the tagData property are retained. If set, then input is disabled when the number of tags exceeds this value | `-1` (no limit) 83 | `readOnlyEmptyMessage` | `String` | text to be displayed if there are no tags in readonly mode. Can be HTML | `No tags to display...` 84 | `beforeAddingTag` | `function(String tag)` | anything external you'd like to do with the tag before adding it. Returning false will stop tag from being added | `null` 85 | `afterAddingTag` | `function(String tag)` | anything external you'd like to do with the tag after adding it | `null` 86 | `beforeDeletingTag` | `function(String tag)` | find out which tag is about to be deleted. Returning false will stop tag from being deleted | `null` 87 | `afterDeletingTag` | `function(String tag)` | find out which tag was removed by either pressing delete key or clicking the (x) | `null` 88 | `definePopover` | `function(String tag)` | must return the popover content for the tag that is being added. (eg "Content for [tag]") | `null` 89 | `excludes` | `function(String tag)` | return true if you want the tag to be excluded, false if allowed | `null` 90 | 91 | ### Controlling tags 92 | Some functions are chainable (returns a `Tagger` object), and can be used to move the data around outside of the plugin. 93 | 94 | function | return type | description 95 | ---------|-------------|------------- 96 | `hasTag(tag:string)` | `Boolean` | whether tag is in tag list 97 | `getTags()` | `Array` | a list of tags currently in the interface 98 | `getTagsWithContent()` | `Array` | a list of javascript objects with a `tag` property and `content` property 99 | `getTag(tag:string)` | `String` | returns tag as string 100 | `getTagWithContent(tag:string)` | `Object` | returns object with `tag` and `content` property (popover) 101 | `addTag(tag:string)` | `Tagger` | add a tag 102 | `renameTag(tag:string, newTag:string)` | `Tagger` | rename one tag to another value 103 | `removeLastTag()` | `Tagger` | removes last tag if it exists 104 | `removeTag(tag:string)` | `Tagger` | removes tag specified by string if it exists 105 | `addTagWithContent(tag:string, popoverContent:string)` | `Tagger` | Add a tag with associated popover content 106 | `setPopover(tag:string, popoverContent:string)` | `Tagger` | update a tag's associated popover content, if that tag exists 107 | 108 | Example: 109 | 110 | ```javascript 111 | var tags = $('#one').tags( { 112 | suggestions : ["here", "are", "some", "suggestions"], 113 | popoverData : ["What a wonderful day", "to make some stuff", "up so that I", "can show it works"], 114 | tagData: ["tag a", "tag b", "tag c", "tag d"], 115 | excludeList : ["excuse", "my", "vulgarity"], 116 | } ); 117 | tags.addTag("tag e!!").removeTag("tag b").setPopover("tag c", "Changed popover content"); 118 | console.log(tags.getTags()); 119 | ``` 120 | 121 | To reference a tags instance that you've already attached to a selector, (eg. $(selector).tags(options)) you can use $(selector).tags() or $(selector).tags(index) 122 | 123 | ### Building 124 | 125 | For a one off: 126 | 127 | $ grunt build 128 | 129 | _To build continuously_: 130 | 131 | $ grunt watch 132 | 133 | ### Testing 134 | 135 | $ grunt test 136 | 137 | ### Contributing 138 | 139 | If you spot a bug, experience browser incompatibility, or have feature requests, please submit them [here](https://github.com/maxwells/bootstrap-tags/issues). 140 | 141 | If you want to hack away to provide a new feature/bug-fix, please follow these guidelines: 142 | 143 | - Make changes to the coffeescript and sass files, not js/css. This is to ensure that the next person who comes in and edits upstream from js/css will not overwrite your changes. 144 | - Create a pull request for your feature branch, updating README documentation if necessary 145 | 146 | ### License 147 | 148 | MIT. 149 | 150 | ### Author 151 | 152 | Max Lahey 153 | -------------------------------------------------------------------------------- /dist/js/bootstrap-tags.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * bootstrap-tags 1.1.5 3 | * https://github.com/maxwells/bootstrap-tags 4 | * Copyright 2013 Max Lahey; Licensed MIT 5 | */ 6 | 7 | !function(a){!function(){window.Tags||(window.Tags={}),jQuery(function(){return a.tags=function(b,c){var d,e,f,g,h,i,j,k=this;null==c&&(c={});for(d in c)g=c[d],this[d]=g;if(this.bootstrapVersion||(this.bootstrapVersion="3"),this.readOnly||(this.readOnly=!1),this.suggestOnClick||(this.suggestOnClick=!1),this.suggestions||(this.suggestions=[]),this.restrictTo=null!=c.restrictTo?c.restrictTo.concat(this.suggestions):!1,this.exclude||(this.exclude=!1),this.displayPopovers=null!=c.popovers?!0:null!=c.popoverData,this.popoverTrigger||(this.popoverTrigger="hover"),this.tagClass||(this.tagClass="btn-info"),this.tagSize||(this.tagSize="md"),this.promptText||(this.promptText="Enter tags..."),this.caseInsensitive||(this.caseInsensitive=!1),this.readOnlyEmptyMessage||(this.readOnlyEmptyMessage="No tags to display..."),this.maxNumTags||(this.maxNumTags=-1),this.beforeAddingTag||(this.beforeAddingTag=function(){}),this.afterAddingTag||(this.afterAddingTag=function(){}),this.beforeDeletingTag||(this.beforeDeletingTag=function(){}),this.afterDeletingTag||(this.afterDeletingTag=function(){}),this.definePopover||(this.definePopover=function(a){return'associated content for "'+a+'"'}),this.excludes||(this.excludes=function(){return!1}),this.tagRemoved||(this.tagRemoved=function(){}),this.pressedReturn||(this.pressedReturn=function(){}),this.pressedDelete||(this.pressedDelete=function(){}),this.pressedDown||(this.pressedDown=function(){}),this.pressedUp||(this.pressedUp=function(){}),this.$element=a(b),null!=c.tagData?this.tagsArray=c.tagData:(f=a(".tag-data",this.$element).html(),this.tagsArray=null!=f?f.split(","):[]),c.popoverData)this.popoverArray=c.popoverData;else for(this.popoverArray=[],j=this.tagsArray,h=0,i=j.length;i>h;h++)e=j[h],this.popoverArray.push(null);return this.getTags=function(){return k.tagsArray},this.getTagsContent=function(){return k.popoverArray},this.getTagsWithContent=function(){var a,b,c,d;for(a=[],b=c=0,d=k.tagsArray.length-1;d>=0?d>=c:c>=d;b=d>=0?++c:--c)a.push({tag:k.tagsArray[b],content:k.popoverArray[b]});return a},this.getTag=function(a){var b;return b=k.tagsArray.indexOf(a),b>-1?k.tagsArray[b]:null},this.getTagWithContent=function(a){var b;return b=k.tagsArray.indexOf(a),{tag:k.tagsArray[b],content:k.popoverArray[b]}},this.hasTag=function(a){return k.tagsArray.indexOf(a)>-1},this.removeTagClicked=function(b){return"A"===b.currentTarget.tagName&&(k.removeTag(a("span",b.currentTarget.parentElement).html()),a(b.currentTarget.parentNode).remove()),k},this.removeLastTag=function(){return k.tagsArray.length>0&&(k.removeTag(k.tagsArray[k.tagsArray.length-1]),k.canAddByMaxNum()&&k.enableInput()),k},this.removeTag=function(a){if(k.tagsArray.indexOf(a)>-1){if(k.beforeDeletingTag(a)===!1)return;k.popoverArray.splice(k.tagsArray.indexOf(a),1),k.tagsArray.splice(k.tagsArray.indexOf(a),1),k.renderTags(),k.afterDeletingTag(a),k.canAddByMaxNum()&&k.enableInput()}return k},this.canAddByRestriction=function(a){return this.restrictTo===!1||-1!==this.restrictTo.indexOf(a)},this.canAddByExclusion=function(a){return(this.exclude===!1||-1===this.exclude.indexOf(a))&&!this.excludes(a)},this.canAddByMaxNum=function(){return-1===this.maxNumTags||this.tagsArray.length0&&k.canAddByExclusion(a)&&k.canAddByMaxNum()){if(k.beforeAddingTag(a)===!1)return;b=k.definePopover(a),k.popoverArray.push(b||null),k.tagsArray.push(a),k.afterAddingTag(a),k.renderTags(),k.canAddByMaxNum()||k.disableInput()}return k},this.addTagWithContent=function(a,b){if(k.canAddByRestriction(a)&&!k.hasTag(a)&&a.length>0){if(k.beforeAddingTag(a)===!1)return;k.tagsArray.push(a),k.popoverArray.push(b),k.afterAddingTag(a),k.renderTags()}return k},this.renameTag=function(a,b){return k.tagsArray[k.tagsArray.indexOf(a)]=b,k.renderTags(),k},this.setPopover=function(a,b){return k.popoverArray[k.tagsArray.indexOf(a)]=b,k.renderTags(),k},this.clickHandler=function(a){return k.makeSuggestions(a,!0)},this.keyDownHandler=function(a){var b,c;switch(b=null!=a.keyCode?a.keyCode:a.which){case 13:return a.preventDefault(),k.pressedReturn(a),e=a.target.value,-1!==k.suggestedIndex&&(e=k.suggestionList[k.suggestedIndex]),k.addTag(e),a.target.value="",k.renderTags(),k.hideSuggestions();case 46:case 8:if(k.pressedDelete(a),""===a.target.value&&k.removeLastTag(),1===a.target.value.length)return k.hideSuggestions();break;case 40:if(k.pressedDown(a),""!==k.input.val()||-1!==k.suggestedIndex&&null!=k.suggestedIndex||k.makeSuggestions(a,!0),c=k.suggestionList.length,k.suggestedIndex=k.suggestedIndex=0)return k.scrollSuggested(k.suggestedIndex);break;case 38:if(k.pressedUp(a),k.suggestedIndex=k.suggestedIndex>0?k.suggestedIndex-1:0,k.selectSuggested(k.suggestedIndex),k.suggestedIndex>=0)return k.scrollSuggested(k.suggestedIndex);break;case 9:case 27:return k.hideSuggestions(),k.suggestedIndex=-1}},this.keyUpHandler=function(a){var b;return b=null!=a.keyCode?a.keyCode:a.which,40!==b&&38!==b&&27!==b?k.makeSuggestions(a,!1):void 0},this.getSuggestions=function(b,c){var d=this;return this.suggestionList=[],this.caseInsensitive&&(b=b.toLowerCase()),a.each(this.suggestions,function(a,e){var f;return f=d.caseInsensitive?e.substring(0,b.length).toLowerCase():e.substring(0,b.length),d.tagsArray.indexOf(e)<0&&f===b&&(b.length>0||c)?d.suggestionList.push(e):void 0}),this.suggestionList},this.makeSuggestions=function(b,c,d){return null==d&&(d=null!=b.target.value?b.target.value:b.target.textContent),k.suggestedIndex=-1,k.$suggestionList.html(""),a.each(k.getSuggestions(d,c),function(a,b){return k.$suggestionList.append(k.template("tags_suggestion",{suggestion:b}))}),k.$(".tags-suggestion").mouseover(k.selectSuggestedMouseOver),k.$(".tags-suggestion").click(k.suggestedClicked),k.suggestionList.length>0?k.showSuggestions():k.hideSuggestions()},this.suggestedClicked=function(a){return e=a.target.textContent,-1!==k.suggestedIndex&&(e=k.suggestionList[k.suggestedIndex]),k.addTag(e),k.input.val(""),k.makeSuggestions(a,!1,""),k.input.focus(),k.hideSuggestions()},this.hideSuggestions=function(){return k.$(".tags-suggestion-list").css({display:"none"})},this.showSuggestions=function(){return k.$(".tags-suggestion-list").css({display:"block"})},this.selectSuggestedMouseOver=function(b){return a(".tags-suggestion").removeClass("tags-suggestion-highlighted"),a(b.target).addClass("tags-suggestion-highlighted"),a(b.target).mouseout(k.selectSuggestedMousedOut),k.suggestedIndex=k.$(".tags-suggestion").index(a(b.target))},this.selectSuggestedMousedOut=function(b){return a(b.target).removeClass("tags-suggestion-highlighted")},this.selectSuggested=function(b){var c;return a(".tags-suggestion").removeClass("tags-suggestion-highlighted"),c=k.$(".tags-suggestion").eq(b),c.addClass("tags-suggestion-highlighted")},this.scrollSuggested=function(a){var b,c,d,e;return c=k.$(".tags-suggestion").eq(a),d=k.$(".tags-suggestion").eq(0),b=c.position(),e=d.position(),null!=b?k.$(".tags-suggestion-list").scrollTop(b.top-e.top):void 0},this.adjustInputPosition=function(){var b,c,d,e,f,g;return f=k.$(".tag").last(),g=f.position(),c=null!=g?g.left+f.outerWidth(!0):0,d=null!=g?g.top:0,e=k.$element.width()-c,a(".tags-input",k.$element).css({paddingLeft:Math.max(c,0),paddingTop:Math.max(d,0),width:e}),b=null!=g?g.top+f.outerHeight(!0):22,k.$element.css({paddingBottom:b-k.$element.height()})},this.renderTags=function(){var b;return b=k.$(".tags"),b.html(""),k.input.attr("placeholder",0===k.tagsArray.length?k.promptText:""),a.each(k.tagsArray,function(c,d){return d=a(k.formatTag(c,d)),a("a",d).click(k.removeTagClicked),a("a",d).mouseover(k.toggleCloseColor),a("a",d).mouseout(k.toggleCloseColor),k.displayPopovers&&k.initializePopoverFor(d,k.tagsArray[c],k.popoverArray[c]),b.append(d)}),k.adjustInputPosition()},this.renderReadOnly=function(){var b;return b=k.$(".tags"),b.html(0===k.tagsArray.length?k.readOnlyEmptyMessage:""),a.each(k.tagsArray,function(c,d){return d=a(k.formatTag(c,d,!0)),k.displayPopovers&&k.initializePopoverFor(d,k.tagsArray[c],k.popoverArray[c]),b.append(d)})},this.disableInput=function(){return this.$("input").prop("disabled",!0)},this.enableInput=function(){return this.$("input").prop("disabled",!1)},this.initializePopoverFor=function(b,d,e){return c={title:d,content:e,placement:"bottom"},"hoverShowClickHide"===k.popoverTrigger?(a(b).mouseover(function(){return a(b).popover("show"),a(".tag").not(b).popover("hide")}),a(document).click(function(){return a(b).popover("hide")})):c.trigger=k.popoverTrigger,a(b).popover(c)},this.toggleCloseColor=function(b){var c,d;return d=a(b.currentTarget),c=d.css("opacity"),c=.8>c?1:.6,d.css({opacity:c})},this.formatTag=function(a,b,c){var d;return null==c&&(c=!1),d=b.replace("<","<").replace(">",">"),k.template("tag",{tag:d,tagClass:k.tagClass,isPopover:k.displayPopovers,isReadOnly:c,tagSize:k.tagSize})},this.addDocumentListeners=function(){return a(document).mouseup(function(a){var b;return b=k.$(".tags-suggestion-list"),0===b.has(a.target).length?k.hideSuggestions():void 0})},this.template=function(a,b){return Tags.Templates.Template(this.getBootstrapVersion(),a,b)},this.$=function(b){return a(b,this.$element)},this.getBootstrapVersion=function(){return Tags.bootstrapVersion||this.bootstrapVersion},this.initializeDom=function(){return this.$element.append(this.template("tags_container"))},this.init=function(){return this.$element.addClass("bootstrap-tags").addClass("bootstrap-"+this.getBootstrapVersion()),this.initializeDom(),this.readOnly?(this.renderReadOnly(),this.removeTag=function(){},this.removeTagClicked=function(){},this.removeLastTag=function(){},this.addTag=function(){},this.addTagWithContent=function(){},this.renameTag=function(){},this.setPopover=function(){}):(this.input=a(this.template("input",{tagSize:this.tagSize})),this.suggestOnClick&&this.input.click(this.clickHandler),this.input.keydown(this.keyDownHandler),this.input.keyup(this.keyUpHandler),this.$element.append(this.input),this.$suggestionList=a(this.template("suggestion_list")),this.$element.append(this.$suggestionList),this.renderTags(),this.canAddByMaxNum()||this.disableInput(),this.addDocumentListeners())},this.init(),this},a.fn.tags=function(b){var c,d;return d={},c="number"==typeof b?b:-1,this.each(function(e,f){var g;return g=a(f),null==g.data("tags")&&g.data("tags",new a.tags(this,b)),c===e||0===e?d=g.data("tags"):void 0}),d}})}.call(this),function(){window.Tags||(window.Tags={}),Tags.Helpers||(Tags.Helpers={}),Tags.Helpers.addPadding=function(a,b,c){return null==b&&(b=1),null==c&&(c=!0),c?0===b?a:Tags.Helpers.addPadding(" "+a+" ",b-1):a}}.call(this),function(){var a;window.Tags||(window.Tags={}),Tags.Templates||(Tags.Templates={}),(a=Tags.Templates)["2"]||(a["2"]={}),Tags.Templates["2"].input=function(a){var b;return null==a&&(a={}),b=function(){switch(a.tagSize){case"sm":return"small";case"md":return"medium";case"lg":return"large"}}(),""}}.call(this),function(){var a;window.Tags||(window.Tags={}),Tags.Templates||(Tags.Templates={}),(a=Tags.Templates)["2"]||(a["2"]={}),Tags.Templates["2"].tag=function(a){return null==a&&(a={}),"
    "+Tags.Helpers.addPadding(a.tag,2,a.isReadOnly)+" "+(a.isReadOnly?"":"")+"
    "}}.call(this),function(){var a;window.Tags||(window.Tags={}),Tags.Templates||(Tags.Templates={}),(a=Tags.Templates)["3"]||(a["3"]={}),Tags.Templates["3"].input=function(a){return null==a&&(a={}),""}}.call(this),function(){var a;window.Tags||(window.Tags={}),Tags.Templates||(Tags.Templates={}),(a=Tags.Templates)["3"]||(a["3"]={}),Tags.Templates["3"].tag=function(a){return null==a&&(a={}),"
    "+Tags.Helpers.addPadding(a.tag,2,a.isReadOnly)+" "+(a.isReadOnly?"":"")+"
    "}}.call(this),function(){var a;window.Tags||(window.Tags={}),Tags.Templates||(Tags.Templates={}),(a=Tags.Templates).shared||(a.shared={}),Tags.Templates.shared.suggestion_list=function(a){return null==a&&(a={}),''}}.call(this),function(){var a;window.Tags||(window.Tags={}),Tags.Templates||(Tags.Templates={}),(a=Tags.Templates).shared||(a.shared={}),Tags.Templates.shared.tags_container=function(a){return null==a&&(a={}),'
    '}}.call(this),function(){var a;window.Tags||(window.Tags={}),Tags.Templates||(Tags.Templates={}),(a=Tags.Templates).shared||(a.shared={}),Tags.Templates.shared.tags_suggestion=function(a){return null==a&&(a={}),"
  • "+a.suggestion+"
  • "}}.call(this),function(){window.Tags||(window.Tags={}),Tags.Templates||(Tags.Templates={}),Tags.Templates.Template=function(a,b,c){return null!=Tags.Templates[a]&&null!=Tags.Templates[a][b]?Tags.Templates[a][b](c):Tags.Templates.shared[b](c)}}.call(this)}(window.jQuery); -------------------------------------------------------------------------------- /spec/bootstrap-tags-spec.coffee: -------------------------------------------------------------------------------- 1 | newTagger = (id, options) -> 2 | $('body').append("
    ") 3 | $("##{id}").tags options 4 | 5 | describe "Bootstrap Tags", -> 6 | 7 | afterEach -> 8 | $('.tagger').remove() 9 | 10 | it "defaults to bootstrap 3", -> 11 | tags = newTagger "tagger2" 12 | expect(tags.bootstrapVersion).toEqual "3" 13 | 14 | describe "when templating", -> 15 | 16 | it "uses appropriate version", -> 17 | tags = newTagger "tagger2", 18 | bootstrapVersion: "2" 19 | tagsInputHtml = tags.template "input", tagSize: "sm" 20 | version2Html = Tags.Templates["2"].input tagSize: "sm" 21 | expect(tagsInputHtml).toEqual version2Html 22 | 23 | it "defaults to shared when template by name is not available in a version", -> 24 | tags = newTagger "tagger2", 25 | bootstrapVersion: "2" 26 | tagsInputHtml = tags.template "suggestion_list" 27 | version2Html = Tags.Templates.shared.suggestion_list() 28 | expect(tagsInputHtml).toEqual version2Html 29 | 30 | describe "when using readOnly", -> 31 | 32 | beforeEach -> 33 | @initTagData = ['one', 'two', 'three'] 34 | @tags = newTagger "tagger", 35 | tagData: @initTagData 36 | readOnly: true 37 | 38 | it "can't add tags", -> 39 | tagLength = @tags.getTags().length 40 | @tags.addTag('new tag') 41 | expect(@tags.getTags().length).toEqual tagLength 42 | 43 | it "can't remove tags", -> 44 | tagLength = @tags.getTags().length 45 | @tags.removeTag('one') 46 | expect(@tags.hasTag('one')).toBeTruthy() 47 | 48 | it "can't rename tag", -> 49 | @tags.renameTag('one', 'new name') 50 | expect(@tags.hasTag('new name')).toBeFalsy() 51 | expect(@tags.hasTag('one')).toBeTruthy() 52 | 53 | it "can get the list of tags", -> 54 | expect(@tags.getTags()).toEqual @initTagData 55 | 56 | describe "when no tags are provided", -> 57 | 58 | it "sets readOnlyEmptyMessage to tags body if provided", -> 59 | @tags = newTagger "tagger2", 60 | readOnly: true 61 | readOnlyEmptyMessage: "foo" 62 | expect($('#tagger2 .tags', @$domElement).html()).toEqual "foo" 63 | 64 | it "sets default empty message to tags body if readOnlyEmptyMessage is not provided", -> 65 | @tags = newTagger "tagger2", 66 | readOnly: true 67 | expect($('#tagger2 .tags').html()).toEqual $('#tagger2').tags().readOnlyEmptyMessage 68 | 69 | describe "when the enter key is pressed before any text is entered", -> 70 | 71 | beforeEach -> 72 | @tags = newTagger "tagger2", {} 73 | $('#tagger2 .tags-input').trigger($.Event('keydown', which: 13)) 74 | 75 | it "does not add any tags", -> 76 | expect(@tags.getTags()).toEqual [] 77 | 78 | describe "when normally operating", -> 79 | 80 | beforeEach -> 81 | @initTagData = ['one', 'two', 'three'] 82 | @tags = newTagger "tagger", 83 | tagData: @initTagData 84 | 85 | it "can add tag", -> 86 | tagLength = @tags.getTags().length 87 | @tags.addTag('new tag') 88 | expect(@tags.getTags().length).toEqual tagLength + 1 89 | expect(@tags.hasTag('new tag')).toBeTruthy() 90 | 91 | it "can get the list of tags", -> 92 | expect(@tags.getTags()).toEqual @initTagData 93 | 94 | it "can remove tag, specified by string", -> 95 | expect(@tags.hasTag('one')).toBeTruthy() 96 | @tags.removeTag('one') 97 | expect(@tags.hasTag('one')).toBeFalsy() 98 | 99 | it "can remove the last tag", -> 100 | tagList = @tags.getTags() 101 | lastTag = tagList[tagList.length-1] 102 | @tags.removeLastTag() 103 | expect(@tags.hasTag(lastTag)).toBeFalsy() 104 | 105 | it "can add tag with popover content", -> 106 | @tags.addTagWithContent('new tag', 'new content') 107 | tagsWithContent = @tags.getTagsWithContent() 108 | expect(tagsWithContent[tagsWithContent.length-1].content).toEqual 'new content' 109 | 110 | it "can change the popover content for a tag", -> 111 | content = 'new tag content for the first tag' 112 | @tags.setPopover('one', content) 113 | expect(@tags.getTagWithContent('one').content).toEqual content 114 | 115 | it "can rename tag", -> 116 | @tags.renameTag('one', 'new name') 117 | expect(@tags.hasTag('new name')).toBeTruthy() 118 | expect(@tags.hasTag('one')).toBeFalsy() 119 | 120 | it "can getTagWithContent", -> 121 | @tags.addTagWithContent('new tag', 'new content') 122 | expect(@tags.getTagWithContent('new tag').content).toEqual 'new content' 123 | 124 | describe "when defining popover for an existing tag", -> 125 | 126 | it "should set the associated content", -> 127 | tags = newTagger "tagger2", 128 | definePopover: (tag) -> 129 | "popover for #{tag}" 130 | tags.addTag("foo") 131 | expect(tags.getTagWithContent("foo").content).toEqual "popover for foo" 132 | 133 | describe "when provided a promptText option", -> 134 | 135 | it "applies it to the input placeholder when there are no tags", -> 136 | tags = newTagger "tagger2", 137 | promptText: "foo" 138 | expect($("#tagger2 input").attr("placeholder")).toEqual "foo" 139 | 140 | it "does not apply it to input placeholder when there are tags", -> 141 | tags = newTagger "tagger2", 142 | promptText: "foo" 143 | tagData: ["one"] 144 | expect($("#tagger2 input").attr("placeholder")).toEqual "" 145 | 146 | describe "when provided with a tagClass option", -> 147 | 148 | it "uses it to style tags", -> 149 | @tags = newTagger "tagger2", 150 | tagClass: "btn-warning" 151 | tagData: ["a", "b"] 152 | expect($('#tagger2 .tag').hasClass("btn-warning")).toBeTruthy() 153 | 154 | describe "when provided a tagSize option", -> 155 | 156 | it "defaults to md", -> 157 | tags = newTagger "tagger2", {} 158 | expect(tags.tagSize).toEqual("md") 159 | 160 | it "applies it to tags", -> 161 | tags = newTagger "tagger2", 162 | tagSize: "sm" 163 | tagData: ["one", "two"] 164 | expect($("#tagger2 .tag").hasClass("sm")).toBeTruthy() 165 | 166 | it "applies it to input", -> 167 | tags = newTagger "tagger2", 168 | tagSize: "lg" 169 | expect($("#tagger2 input").hasClass("input-lg")).toBeTruthy() 170 | 171 | describe "when provided a maxNumTags option", -> 172 | 173 | it "cannot add more tags than maxNumTags", -> 174 | tags = newTagger "tagger2", 175 | maxNumTags: 3 176 | tagData: ["one", "two", "three"] 177 | expect(tags.addTag("four").getTags()).toEqual ["one", "two", "three"] 178 | 179 | it "can add tags when numTags is less than maxNumTags", -> 180 | tags = newTagger "tagger2", 181 | maxNumTags: 3 182 | tagData: ["one", "two"] 183 | expect(tags.addTag("three").getTags()).toEqual ["one", "two", "three"] 184 | 185 | it "defaults to allow multiple tags", -> 186 | tags = newTagger "tagger2", 187 | tagData: ["one", "two", "three", "four", "five"] 188 | expect(tags.addTag("six").getTags()).toEqual ["one", "two", "three", "four", "five", "six"] 189 | 190 | describe "when providing before/after adding/deleting callbacks", -> 191 | 192 | describe "when adding tags", -> 193 | 194 | it "calls beforeAddingTag before a tag is added, providing the tag as first parameter", -> 195 | wasCalled = false 196 | tagAdded = "not this" 197 | tags = newTagger "tagger2", 198 | beforeAddingTag: (tag) -> 199 | wasCalled = true 200 | tagAdded = tag 201 | tags.addTag "this" 202 | expect(wasCalled and tagAdded == "this").toBeTruthy() 203 | 204 | it "will not add a tag if beforeAddingTag returns false", -> 205 | tags = newTagger "tagger2", 206 | beforeAddingTag: (tag) -> 207 | false 208 | tags.addTag "this" 209 | expect(tags.getTags()).toEqual [] 210 | 211 | it "calls afterAddingTag after a tag is added, providing the tag as first parameter", -> 212 | wasCalled = false 213 | tagAdded = "not this" 214 | tags = newTagger "tagger2", 215 | afterAddingTag: (tag) -> 216 | wasCalled = true 217 | tagAdded = tag 218 | tags.addTag "this" 219 | expect(wasCalled and tagAdded == "this").toBeTruthy() 220 | 221 | describe "when deleting tags", -> 222 | 223 | it "calls beforeDeletingTag before a tag is removed, providing the tag as first parameter", -> 224 | wasCalled = false 225 | tags = newTagger "tagger2", 226 | tagData: ["a", "b", "c"] 227 | beforeDeletingTag: (tag) -> 228 | wasCalled = true 229 | expect(tag).toEqual("a") 230 | tags.removeTag "a" 231 | expect(wasCalled).toBeTruthy() 232 | 233 | it "will not add a tag if beforeDeletingTag returns false", -> 234 | tags = newTagger "tagger2", 235 | tagData: ["a", "b", "c"] 236 | beforeDeletingTag: (tag) -> 237 | false 238 | tags.removeTag "a" 239 | expect(tags.getTags()).toEqual ["a", "b", "c"] 240 | 241 | it "calls afterDeletingTag after a tag is removed, providing the tag as first parameter", -> 242 | wasCalled = false 243 | tags = newTagger "tagger2", 244 | tagData: ["a", "b", "c"] 245 | afterDeletingTag: (tag) -> 246 | wasCalled = true 247 | expect(tag).toEqual("a") 248 | tags.removeTag "a" 249 | expect(wasCalled).toBeTruthy() 250 | 251 | describe "when restricting tags using restrictTo option", -> 252 | 253 | it "will not add any tags that aren't approved", -> 254 | tags = newTagger "tagger2", 255 | restrictTo: ["a", "b", "c"] 256 | tags.addTag('foo').addTag('bar').addTag('baz').addTag('a') 257 | expect(tags.getTags()).toEqual ['a'] 258 | $('#tagger2').remove() 259 | 260 | describe "when providing exclusion options", -> 261 | 262 | it "can exclude tags via the excludes function option", -> 263 | excludesFunction = (tag) -> 264 | return false if tag.indexOf('foo') > -1 265 | true 266 | tags = newTagger "tagger2", 267 | excludes: excludesFunction 268 | tags.addTag('foo').addTag('bar').addTag('baz').addTag('foobarbaz') 269 | expect(tags.getTags()).toEqual ['foo', 'foobarbaz'] 270 | $('#tagger2').remove() 271 | 272 | it "can exclude tags via the exclude option", -> 273 | tags = newTagger "tagger2", 274 | exclude: ["a", "b", "c"] 275 | tags.addTag('a').addTag('b').addTag('c').addTag('d') 276 | expect(tags.getTags()).toEqual ['d'] 277 | $('#tagger2').remove() 278 | 279 | describe "when auto-suggesting", -> 280 | 281 | describe "and caseInsensitive is true", -> 282 | 283 | describe "and tags are uppercase", -> 284 | 285 | it "should find suggestions for lowercase input", -> 286 | tags = newTagger "tagger2", 287 | caseInsensitive: true 288 | suggestions: ["Alpha", "Bravo", "Charlie"] 289 | expect(tags.getSuggestions("a", true)).toEqual ["Alpha"] 290 | 291 | it "should find suggestions for uppercase input", -> 292 | tags = newTagger "tagger2", 293 | caseInsensitive: true 294 | suggestions: ["Alpha", "Bravo", "Charlie"] 295 | expect(tags.getSuggestions("A", true)).toEqual ["Alpha"] 296 | 297 | describe "and tags are lowercase", -> 298 | 299 | it "should find suggestions for lowercase input", -> 300 | tags = newTagger "tagger2", 301 | caseInsensitive: true 302 | suggestions: ["alpha", "bravo", "charlie"] 303 | expect(tags.getSuggestions("a", true)).toEqual ["alpha"] 304 | 305 | it "should find suggestions for uppercase input", -> 306 | tags = newTagger "tagger2", 307 | caseInsensitive: true 308 | suggestions: ["alpha", "bravo", "charlie"] 309 | expect(tags.getSuggestions("A", true)).toEqual ["alpha"] 310 | 311 | describe "and caseInsensitive is false", -> 312 | 313 | describe "and tags are uppercase", -> 314 | 315 | it "should not find suggestions for lowercase input", -> 316 | tags = newTagger "tagger2", 317 | suggestions: ["Alpha", "Bravo", "Charlie"] 318 | expect(tags.getSuggestions("a", true)).toEqual [] 319 | 320 | it "should find suggestions for uppercase input", -> 321 | tags = newTagger "tagger2", 322 | suggestions: ["Alpha", "Bravo", "Charlie"] 323 | expect(tags.getSuggestions("A", true)).toEqual ["Alpha"] 324 | 325 | describe "and tags are lowercase", -> 326 | 327 | it "should find suggestions for lowercase input", -> 328 | tags = newTagger "tagger2", 329 | suggestions: ["alpha", "bravo", "charlie"] 330 | expect(tags.getSuggestions("a", true)).toEqual ["alpha"] 331 | 332 | it "should not find suggestions for uppercase input", -> 333 | tags = newTagger "tagger2", 334 | suggestions: ["alpha", "bravo", "charlie"] 335 | expect(tags.getSuggestions("A", true)).toEqual [] 336 | -------------------------------------------------------------------------------- /src/bootstrap-tags.coffee: -------------------------------------------------------------------------------- 1 | # Bootstrap Tags 2 | # Max Lahey 3 | # November, 2012 4 | 5 | window.Tags ||= {} 6 | 7 | jQuery -> 8 | $.tags = (element, options = {}) -> 9 | 10 | # set options for tags 11 | for key, value of options 12 | this[key] = value 13 | 14 | @bootstrapVersion ||= "3" 15 | 16 | # set defaults if no option was set 17 | @readOnly ||= false 18 | @suggestOnClick ||= false 19 | @suggestions ||= [] 20 | @restrictTo = (if options.restrictTo? then options.restrictTo.concat @suggestions else false) 21 | @exclude ||= false 22 | @displayPopovers = (if options.popovers? then true else options.popoverData?) 23 | @popoverTrigger ||= 'hover' 24 | @tagClass ||= 'btn-info' 25 | @tagSize ||= 'md' 26 | @promptText ||= 'Enter tags...' 27 | @caseInsensitive ||= false 28 | @readOnlyEmptyMessage ||= 'No tags to display...' 29 | @maxNumTags ||= -1 30 | 31 | # callbacks 32 | @beforeAddingTag ||= (tag) -> 33 | @afterAddingTag ||= (tag) -> 34 | @beforeDeletingTag ||= (tag) -> 35 | @afterDeletingTag ||= (tag) -> 36 | 37 | # override-able functions 38 | @definePopover ||= (tag) -> "associated content for \""+tag+"\"" 39 | @excludes ||= -> false 40 | @tagRemoved ||= (tag) -> 41 | 42 | # override-able key press functions 43 | @pressedReturn ||= (e) -> 44 | @pressedDelete ||= (e) -> 45 | @pressedDown ||= (e) -> 46 | @pressedUp ||= (e) -> 47 | 48 | # hang on to so we know who we are 49 | @$element = $(element) 50 | 51 | # tagsArray is list of tags -> define it based on what may or may not be in the dom element 52 | if options.tagData? 53 | @tagsArray = options.tagData 54 | else 55 | tagData = $('.tag-data', @$element).html() 56 | @tagsArray = (if tagData? then tagData.split ',' else []) 57 | 58 | # initialize associated content array 59 | if options.popoverData 60 | @popoverArray = options.popoverData 61 | else 62 | @popoverArray = [] 63 | @popoverArray.push null for tag in @tagsArray 64 | 65 | # returns list of tags 66 | @getTags = => 67 | @tagsArray 68 | 69 | @getTagsContent = => 70 | @popoverArray 71 | 72 | @getTagsWithContent = => 73 | combined = [] 74 | for i in [0..@tagsArray.length-1] 75 | combined.push 76 | tag: @tagsArray[i] 77 | content: @popoverArray[i] 78 | combined 79 | 80 | @getTag = (tag) => 81 | index = @tagsArray.indexOf tag 82 | if index > -1 then @tagsArray[index] else null 83 | 84 | @getTagWithContent = (tag) => 85 | index = @tagsArray.indexOf tag 86 | tag: @tagsArray[index], content: @popoverArray[index] 87 | 88 | @hasTag = (tag) => 89 | @tagsArray.indexOf(tag) > -1 90 | 91 | #################### 92 | # add/remove methods 93 | #################### 94 | 95 | # removeTagClicked is called when user clicks remove tag anchor (x) 96 | @removeTagClicked = (e) => # 97 | if e.currentTarget.tagName == "A" 98 | @removeTag $("span", e.currentTarget.parentElement).text() 99 | $(e.currentTarget.parentNode).remove() 100 | @ 101 | 102 | # removeLastTag is called when user presses delete on empty input. 103 | @removeLastTag = => 104 | if @tagsArray.length > 0 105 | @removeTag @tagsArray[@tagsArray.length-1] 106 | @enableInput() if @canAddByMaxNum() 107 | @ 108 | 109 | # removeTag removes specified tag. 110 | # - Helper method for removeTagClicked and removeLast Tag 111 | # - also an exposed method (can be called from page javascript) 112 | @removeTag = (tag) => # removes specified tag 113 | if @tagsArray.indexOf(tag) > -1 114 | return if @beforeDeletingTag(tag) == false 115 | @popoverArray.splice(@tagsArray.indexOf(tag),1) 116 | @tagsArray.splice(@tagsArray.indexOf(tag), 1) 117 | @renderTags() 118 | @afterDeletingTag(tag) 119 | @enableInput() if @canAddByMaxNum() 120 | @ 121 | 122 | @canAddByRestriction = (tag) -> 123 | (@restrictTo == false or @restrictTo.indexOf(tag) != -1) 124 | 125 | @canAddByExclusion = (tag) -> 126 | (@exclude == false || @exclude.indexOf(tag) == -1) and !@excludes(tag) 127 | 128 | @canAddByMaxNum = -> 129 | @maxNumTags == -1 or @tagsArray.length < @maxNumTags 130 | 131 | # addTag adds the specified tag 132 | # - Helper method for keyDownHandler and suggestedClicked 133 | # - exposed: can be called from page javascript 134 | @addTag = (tag) => 135 | if @canAddByRestriction(tag) and !@hasTag(tag) and tag.length > 0 and @canAddByExclusion(tag) and @canAddByMaxNum() 136 | return if @beforeAddingTag(tag) == false 137 | associatedContent = @definePopover(tag) 138 | @popoverArray.push associatedContent or null 139 | @tagsArray.push tag 140 | @afterAddingTag(tag) 141 | @renderTags() 142 | @disableInput() unless @canAddByMaxNum() 143 | @ 144 | 145 | # addTagWithContent adds the specified tag with associated popover content 146 | # It is an exposed method: can be called from page javascript 147 | @addTagWithContent = (tag, content) => 148 | if @canAddByRestriction(tag) and !@hasTag(tag) and tag.length > 0 149 | return if @beforeAddingTag(tag) == false 150 | @tagsArray.push tag 151 | @popoverArray.push content 152 | @afterAddingTag(tag) 153 | @renderTags() 154 | @ 155 | 156 | # renameTag renames the specified tag 157 | # It is an exposed method: can be called from page javascript 158 | @renameTag = (name, newName) => 159 | @tagsArray[@tagsArray.indexOf name] = newName 160 | @renderTags() 161 | @ 162 | 163 | # setPopover sets the specified (existing) tag with associated popover content 164 | # It is an exposed method: can be called from page javascript 165 | @setPopover = (tag, popoverContent) => 166 | @popoverArray[@tagsArray.indexOf tag] = popoverContent 167 | @renderTags() 168 | @ 169 | 170 | ########################### 171 | # User Input & Key handlers 172 | ########################### 173 | 174 | @clickHandler = (e) => 175 | @makeSuggestions e, true 176 | 177 | @keyDownHandler = (e) => 178 | k = (if e.keyCode? then e.keyCode else e.which) 179 | switch k 180 | when 13 # enter (submit tag or selected suggestion) 181 | e.preventDefault() 182 | @pressedReturn(e) 183 | tag = e.target.value 184 | if @suggestedIndex? && @suggestedIndex != -1 185 | tag = @suggestionList[@suggestedIndex] 186 | @addTag tag 187 | e.target.value = '' 188 | @renderTags() 189 | @hideSuggestions() 190 | when 46, 8 # delete 191 | @pressedDelete(e) 192 | if e.target.value == '' 193 | @removeLastTag() 194 | if e.target.value.length == 1 # is one (which will be deleted after JS processing) 195 | @hideSuggestions() 196 | when 40 # down 197 | @pressedDown(e) 198 | if @input.val() == '' and (@suggestedIndex == -1 || !@suggestedIndex?) 199 | @makeSuggestions e, true 200 | numSuggestions = @suggestionList.length 201 | @suggestedIndex = (if @suggestedIndex < numSuggestions-1 then @suggestedIndex+1 else numSuggestions-1) 202 | @selectSuggested @suggestedIndex 203 | @scrollSuggested @suggestedIndex if @suggestedIndex >= 0 204 | when 38 # up 205 | @pressedUp(e) 206 | @suggestedIndex = (if @suggestedIndex > 0 then @suggestedIndex-1 else 0) 207 | @selectSuggested @suggestedIndex 208 | @scrollSuggested @suggestedIndex if @suggestedIndex >= 0 209 | when 9, 27 # tab, escape 210 | @hideSuggestions() 211 | @suggestedIndex = -1 212 | else 213 | 214 | @keyUpHandler = (e) => 215 | k = (if e.keyCode? then e.keyCode else e.which) 216 | if k != 40 and k != 38 and k != 27 217 | @makeSuggestions e, false 218 | 219 | @getSuggestions = (str, overrideLengthCheck) -> 220 | @suggestionList = [] 221 | str = str.toLowerCase() if @caseInsensitive 222 | $.each @suggestions, (i, suggestion) => 223 | suggestionVal = if @caseInsensitive then suggestion.substring(0, str.length).toLowerCase() else suggestion.substring(0, str.length) 224 | if @tagsArray.indexOf(suggestion) < 0 and suggestionVal == str and (str.length > 0 or overrideLengthCheck) 225 | @suggestionList.push suggestion 226 | @suggestionList 227 | 228 | # makeSuggestions creates auto suggestions that match the value in the input 229 | # if overrideLengthCheck is set to true, then if the input value is empty (''), return all possible suggestions 230 | @makeSuggestions = (e, overrideLengthCheck, val) => 231 | val ?= (if e.target.value? then e.target.value else e.target.textContent) 232 | @suggestedIndex = -1 233 | @$suggestionList.html '' 234 | $.each @getSuggestions(val, overrideLengthCheck), (i, suggestion) => 235 | @$suggestionList.append @template 'tags_suggestion', 236 | suggestion: suggestion 237 | @$('.tags-suggestion').mouseover @selectSuggestedMouseOver 238 | @$('.tags-suggestion').click @suggestedClicked 239 | if @suggestionList.length > 0 240 | @showSuggestions() 241 | else 242 | @hideSuggestions() # so the rounded parts on top & bottom of dropdown do not show up 243 | 244 | # triggered when user clicked on a suggestion 245 | @suggestedClicked = (e) => 246 | tag = e.target.textContent 247 | if @suggestedIndex != -1 248 | tag = @suggestionList[@suggestedIndex] 249 | @addTag tag 250 | @input.val '' 251 | @makeSuggestions e, false, '' 252 | @input.focus() # return focus to input so user can continue typing 253 | @hideSuggestions() 254 | 255 | ################# 256 | # display methods 257 | ################# 258 | 259 | # hideSuggestions is called when: 260 | # - user clicks out of suggestions list 261 | # - user selects a suggestion 262 | # - user presses escape 263 | @hideSuggestions = => 264 | @$('.tags-suggestion-list').css display: "none" 265 | 266 | # showSuggetions is called when: 267 | # - user types in start of a suggested tag 268 | # - user presses down arrow in empty text input 269 | @showSuggestions = => 270 | @$('.tags-suggestion-list').css display: "block" 271 | 272 | # selectSuggestedMouseOver triggered when user mouses over suggestion 273 | @selectSuggestedMouseOver = (e) => 274 | $('.tags-suggestion').removeClass('tags-suggestion-highlighted') 275 | $(e.target).addClass('tags-suggestion-highlighted') 276 | $(e.target).mouseout @selectSuggestedMousedOut 277 | @suggestedIndex = @$('.tags-suggestion').index($(e.target)) 278 | 279 | # selectSuggestedMouseOver triggered when user mouses out of suggestion 280 | @selectSuggestedMousedOut = (e) => 281 | $(e.target).removeClass('tags-suggestion-highlighted') 282 | 283 | # selectSuggested is called when up or down arrows are pressed in 284 | # a suggestions list (to highlight whatever the specified index is) 285 | @selectSuggested = (i) => 286 | $('.tags-suggestion').removeClass('tags-suggestion-highlighted') 287 | tagElement = @$('.tags-suggestion').eq(i) 288 | tagElement.addClass 'tags-suggestion-highlighted' 289 | 290 | # scrollSuggested is called from up and down arrow key presses 291 | # to scroll the suggestions list so that the selected index is always visible 292 | @scrollSuggested = (i) => 293 | tagElement = @$('.tags-suggestion').eq i 294 | topElement = @$('.tags-suggestion').eq 0 295 | pos = tagElement.position() 296 | topPos = topElement.position() 297 | @$('.tags-suggestion-list').scrollTop pos.top - topPos.top if pos? 298 | 299 | # adjustInputPadding adjusts padding of input so that what the 300 | # user types shows up next to last tag (or on new line if insufficient space) 301 | @adjustInputPosition = => 302 | tagElement = @$('.tag').last() 303 | tagPosition = tagElement.position() 304 | pLeft = if tagPosition? then tagPosition.left + tagElement.outerWidth(true) else 0 305 | pTop = if tagPosition? then tagPosition.top else 0 306 | pWidth = @$element.width() - pLeft 307 | $('.tags-input', @$element).css 308 | paddingLeft : Math.max pLeft, 0 309 | paddingTop : Math.max pTop, 0 310 | width : pWidth 311 | pBottom = if tagPosition? then tagPosition.top + tagElement.outerHeight(true) else 22 312 | @$element.css paddingBottom : pBottom - @$element.height() 313 | 314 | # renderTags renders tags... 315 | @renderTags = => 316 | tagList = @$('.tags') 317 | tagList.html('') 318 | @input.attr 'placeholder', (if @tagsArray.length == 0 then @promptText else '') 319 | $.each @tagsArray, (i, tag) => 320 | tag = $(@formatTag i, tag) 321 | $('a', tag).click @removeTagClicked 322 | $('a', tag).mouseover @toggleCloseColor 323 | $('a', tag).mouseout @toggleCloseColor 324 | @initializePopoverFor(tag, @tagsArray[i], @popoverArray[i]) if @displayPopovers 325 | tagList.append tag 326 | @adjustInputPosition() 327 | 328 | @renderReadOnly = => 329 | tagList = @$('.tags') 330 | tagList.html (if @tagsArray.length == 0 then @readOnlyEmptyMessage else '') 331 | $.each @tagsArray, (i, tag) => 332 | tag = $(@formatTag i, tag, true) 333 | @initializePopoverFor(tag, @tagsArray[i], @popoverArray[i]) if @displayPopovers 334 | tagList.append tag 335 | 336 | 337 | @disableInput = -> 338 | @$('input').prop 'disabled', true 339 | 340 | @enableInput = -> 341 | @$('input').prop 'disabled', false 342 | 343 | # set up popover for a given tag to be toggled by specific action 344 | # - 'click': need to click a tag to show/hide popover 345 | # - 'hover': need to mouseover/out to show/hide popover 346 | # - 'hoverShowClickHide': need to mouseover to show popover, mouseover another to hide others, or click document to hide others 347 | @initializePopoverFor = (tag, title, content) => 348 | options = 349 | title: title 350 | content: content 351 | placement: 'bottom' 352 | if @popoverTrigger == 'hoverShowClickHide' 353 | $(tag).mouseover -> 354 | $(tag).popover('show') 355 | $('.tag').not(tag).popover('hide') 356 | $(document).click -> 357 | $(tag).popover('hide') 358 | else 359 | options.trigger = @popoverTrigger 360 | $(tag).popover options 361 | 362 | 363 | # toggles remove button opacity for a tag when moused over or out 364 | @toggleCloseColor = (e) -> 365 | tagAnchor = $ e.currentTarget 366 | opacity = tagAnchor.css('opacity') 367 | opacity = (if opacity < 0.8 then 1.0 else 0.6) 368 | tagAnchor.css opacity:opacity 369 | 370 | # formatTag spits out the html for a tag (with or without it's popovers) 371 | @formatTag = (i, tag, isReadOnly = false) => 372 | escapedTag = tag.replace("<",'<').replace(">",'>') 373 | @template "tag", 374 | tag: escapedTag 375 | tagClass: @tagClass 376 | isPopover: @displayPopovers 377 | isReadOnly: isReadOnly 378 | tagSize: @tagSize 379 | 380 | @addDocumentListeners = => 381 | $(document).mouseup (e) => 382 | container = @$('.tags-suggestion-list') 383 | if container.has(e.target).length == 0 384 | @hideSuggestions() 385 | 386 | @template = (name, options) -> 387 | Tags.Templates.Template(@getBootstrapVersion(), name, options) 388 | 389 | @$ = (selector) -> 390 | $(selector, @$element) 391 | 392 | @getBootstrapVersion = -> Tags.bootstrapVersion or @bootstrapVersion 393 | 394 | @initializeDom = -> 395 | @$element.append @template("tags_container") 396 | 397 | @init = -> 398 | # build out tags from specified markup 399 | @$element.addClass("bootstrap-tags").addClass("bootstrap-#{@getBootstrapVersion()}") 400 | @initializeDom() 401 | if @readOnly 402 | @renderReadOnly() 403 | # unexpose exposed functions to add & remove functions 404 | @removeTag = -> 405 | @removeTagClicked = -> 406 | @removeLastTag = -> 407 | @addTag = -> 408 | @addTagWithContent = -> 409 | @renameTag = -> 410 | @setPopover = -> 411 | else 412 | @input = $ @template "input", 413 | tagSize: @tagSize 414 | if @suggestOnClick 415 | @input.click @clickHandler 416 | @input.keydown @keyDownHandler 417 | @input.keyup @keyUpHandler 418 | @$element.append @input 419 | @$suggestionList = $(@template("suggestion_list")) 420 | @$element.append @$suggestionList 421 | # show it 422 | @renderTags() 423 | 424 | @disableInput() unless @canAddByMaxNum() 425 | 426 | @addDocumentListeners() 427 | 428 | @init() 429 | 430 | @ 431 | 432 | # $(selector).tags(index = 0) will return specified index tags object 433 | # associated with selector or (if none specified), the first 434 | $.fn.tags = (options) -> 435 | tagsObject = {} 436 | stopOn = (if typeof options == "number" then options else -1) 437 | @each (i, el) -> 438 | $el = $ el 439 | unless $el.data('tags')? 440 | $el.data 'tags', new $.tags(this, options) 441 | if stopOn == i or i == 0 # return first or specified by index tags object 442 | tagsObject = $el.data 'tags' 443 | tagsObject 444 | -------------------------------------------------------------------------------- /spec/bootstrap-tags-spec.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var newTagger; 3 | 4 | newTagger = function(id, options) { 5 | $('body').append("
    "); 6 | return $("#" + id).tags(options); 7 | }; 8 | 9 | describe("Bootstrap Tags", function() { 10 | afterEach(function() { 11 | return $('.tagger').remove(); 12 | }); 13 | it("defaults to bootstrap 3", function() { 14 | var tags; 15 | tags = newTagger("tagger2"); 16 | return expect(tags.bootstrapVersion).toEqual("3"); 17 | }); 18 | describe("when templating", function() { 19 | it("uses appropriate version", function() { 20 | var tags, tagsInputHtml, version2Html; 21 | tags = newTagger("tagger2", { 22 | bootstrapVersion: "2" 23 | }); 24 | tagsInputHtml = tags.template("input", { 25 | tagSize: "sm" 26 | }); 27 | version2Html = Tags.Templates["2"].input({ 28 | tagSize: "sm" 29 | }); 30 | return expect(tagsInputHtml).toEqual(version2Html); 31 | }); 32 | return it("defaults to shared when template by name is not available in a version", function() { 33 | var tags, tagsInputHtml, version2Html; 34 | tags = newTagger("tagger2", { 35 | bootstrapVersion: "2" 36 | }); 37 | tagsInputHtml = tags.template("suggestion_list"); 38 | version2Html = Tags.Templates.shared.suggestion_list(); 39 | return expect(tagsInputHtml).toEqual(version2Html); 40 | }); 41 | }); 42 | describe("when using readOnly", function() { 43 | beforeEach(function() { 44 | this.initTagData = ['one', 'two', 'three']; 45 | return this.tags = newTagger("tagger", { 46 | tagData: this.initTagData, 47 | readOnly: true 48 | }); 49 | }); 50 | it("can't add tags", function() { 51 | var tagLength; 52 | tagLength = this.tags.getTags().length; 53 | this.tags.addTag('new tag'); 54 | return expect(this.tags.getTags().length).toEqual(tagLength); 55 | }); 56 | it("can't remove tags", function() { 57 | var tagLength; 58 | tagLength = this.tags.getTags().length; 59 | this.tags.removeTag('one'); 60 | return expect(this.tags.hasTag('one')).toBeTruthy(); 61 | }); 62 | it("can't rename tag", function() { 63 | this.tags.renameTag('one', 'new name'); 64 | expect(this.tags.hasTag('new name')).toBeFalsy(); 65 | return expect(this.tags.hasTag('one')).toBeTruthy(); 66 | }); 67 | it("can get the list of tags", function() { 68 | return expect(this.tags.getTags()).toEqual(this.initTagData); 69 | }); 70 | return describe("when no tags are provided", function() { 71 | it("sets readOnlyEmptyMessage to tags body if provided", function() { 72 | this.tags = newTagger("tagger2", { 73 | readOnly: true, 74 | readOnlyEmptyMessage: "foo" 75 | }); 76 | return expect($('#tagger2 .tags', this.$domElement).html()).toEqual("foo"); 77 | }); 78 | return it("sets default empty message to tags body if readOnlyEmptyMessage is not provided", function() { 79 | this.tags = newTagger("tagger2", { 80 | readOnly: true 81 | }); 82 | return expect($('#tagger2 .tags').html()).toEqual($('#tagger2').tags().readOnlyEmptyMessage); 83 | }); 84 | }); 85 | }); 86 | return describe("when normally operating", function() { 87 | beforeEach(function() { 88 | this.initTagData = ['one', 'two', 'three']; 89 | return this.tags = newTagger("tagger", { 90 | tagData: this.initTagData 91 | }); 92 | }); 93 | it("can add tag", function() { 94 | var tagLength; 95 | tagLength = this.tags.getTags().length; 96 | this.tags.addTag('new tag'); 97 | expect(this.tags.getTags().length).toEqual(tagLength + 1); 98 | return expect(this.tags.hasTag('new tag')).toBeTruthy(); 99 | }); 100 | it("can get the list of tags", function() { 101 | return expect(this.tags.getTags()).toEqual(this.initTagData); 102 | }); 103 | it("can remove tag, specified by string", function() { 104 | expect(this.tags.hasTag('one')).toBeTruthy(); 105 | this.tags.removeTag('one'); 106 | return expect(this.tags.hasTag('one')).toBeFalsy(); 107 | }); 108 | it("can remove the last tag", function() { 109 | var lastTag, tagList; 110 | tagList = this.tags.getTags(); 111 | lastTag = tagList[tagList.length - 1]; 112 | this.tags.removeLastTag(); 113 | return expect(this.tags.hasTag(lastTag)).toBeFalsy(); 114 | }); 115 | it("can add tag with popover content", function() { 116 | var tagsWithContent; 117 | this.tags.addTagWithContent('new tag', 'new content'); 118 | tagsWithContent = this.tags.getTagsWithContent(); 119 | return expect(tagsWithContent[tagsWithContent.length - 1].content).toEqual('new content'); 120 | }); 121 | it("can change the popover content for a tag", function() { 122 | var content; 123 | content = 'new tag content for the first tag'; 124 | this.tags.setPopover('one', content); 125 | return expect(this.tags.getTagWithContent('one').content).toEqual(content); 126 | }); 127 | it("can rename tag", function() { 128 | this.tags.renameTag('one', 'new name'); 129 | expect(this.tags.hasTag('new name')).toBeTruthy(); 130 | return expect(this.tags.hasTag('one')).toBeFalsy(); 131 | }); 132 | it("can getTagWithContent", function() { 133 | this.tags.addTagWithContent('new tag', 'new content'); 134 | return expect(this.tags.getTagWithContent('new tag').content).toEqual('new content'); 135 | }); 136 | describe("when defining popover for an existing tag", function() { 137 | return it("should set the associated content", function() { 138 | var tags; 139 | tags = newTagger("tagger2", { 140 | definePopover: function(tag) { 141 | return "popover for " + tag; 142 | } 143 | }); 144 | tags.addTag("foo"); 145 | return expect(tags.getTagWithContent("foo").content).toEqual("popover for foo"); 146 | }); 147 | }); 148 | describe("when provided a promptText option", function() { 149 | it("applies it to the input placeholder when there are no tags", function() { 150 | var tags; 151 | tags = newTagger("tagger2", { 152 | promptText: "foo" 153 | }); 154 | return expect($("#tagger2 input").attr("placeholder")).toEqual("foo"); 155 | }); 156 | return it("does not apply it to input placeholder when there are tags", function() { 157 | var tags; 158 | tags = newTagger("tagger2", { 159 | promptText: "foo", 160 | tagData: ["one"] 161 | }); 162 | return expect($("#tagger2 input").attr("placeholder")).toEqual(""); 163 | }); 164 | }); 165 | describe("when provided with a tagClass option", function() { 166 | return it("uses it to style tags", function() { 167 | this.tags = newTagger("tagger2", { 168 | tagClass: "btn-warning", 169 | tagData: ["a", "b"] 170 | }); 171 | return expect($('#tagger2 .tag').hasClass("btn-warning")).toBeTruthy(); 172 | }); 173 | }); 174 | describe("when provided a tagSize option", function() { 175 | it("defaults to md", function() { 176 | var tags; 177 | tags = newTagger("tagger2", {}); 178 | return expect(tags.tagSize).toEqual("md"); 179 | }); 180 | it("applies it to tags", function() { 181 | var tags; 182 | tags = newTagger("tagger2", { 183 | tagSize: "sm", 184 | tagData: ["one", "two"] 185 | }); 186 | return expect($("#tagger2 .tag").hasClass("sm")).toBeTruthy(); 187 | }); 188 | return it("applies it to input", function() { 189 | var tags; 190 | tags = newTagger("tagger2", { 191 | tagSize: "lg" 192 | }); 193 | return expect($("#tagger2 input").hasClass("input-lg")).toBeTruthy(); 194 | }); 195 | }); 196 | describe("when provided a maxNumTags option", function() { 197 | it("cannot add more tags than maxNumTags", function() { 198 | var tags; 199 | tags = newTagger("tagger2", { 200 | maxNumTags: 3, 201 | tagData: ["one", "two", "three"] 202 | }); 203 | return expect(tags.addTag("four").getTags()).toEqual(["one", "two", "three"]); 204 | }); 205 | it("can add tags when numTags is less than maxNumTags", function() { 206 | var tags; 207 | tags = newTagger("tagger2", { 208 | maxNumTags: 3, 209 | tagData: ["one", "two"] 210 | }); 211 | return expect(tags.addTag("three").getTags()).toEqual(["one", "two", "three"]); 212 | }); 213 | return it("defaults to allow multiple tags", function() { 214 | var tags; 215 | tags = newTagger("tagger2", { 216 | tagData: ["one", "two", "three", "four", "five"] 217 | }); 218 | return expect(tags.addTag("six").getTags()).toEqual(["one", "two", "three", "four", "five", "six"]); 219 | }); 220 | }); 221 | describe("when providing before/after adding/deleting callbacks", function() { 222 | describe("when adding tags", function() { 223 | it("calls beforeAddingTag before a tag is added, providing the tag as first parameter", function() { 224 | var tagAdded, tags, wasCalled; 225 | wasCalled = false; 226 | tagAdded = "not this"; 227 | tags = newTagger("tagger2", { 228 | beforeAddingTag: function(tag) { 229 | wasCalled = true; 230 | return tagAdded = tag; 231 | } 232 | }); 233 | tags.addTag("this"); 234 | return expect(wasCalled && tagAdded === "this").toBeTruthy(); 235 | }); 236 | it("will not add a tag if beforeAddingTag returns false", function() { 237 | var tags; 238 | tags = newTagger("tagger2", { 239 | beforeAddingTag: function(tag) { 240 | return false; 241 | } 242 | }); 243 | tags.addTag("this"); 244 | return expect(tags.getTags()).toEqual([]); 245 | }); 246 | return it("calls afterAddingTag after a tag is added, providing the tag as first parameter", function() { 247 | var tagAdded, tags, wasCalled; 248 | wasCalled = false; 249 | tagAdded = "not this"; 250 | tags = newTagger("tagger2", { 251 | afterAddingTag: function(tag) { 252 | wasCalled = true; 253 | return tagAdded = tag; 254 | } 255 | }); 256 | tags.addTag("this"); 257 | return expect(wasCalled && tagAdded === "this").toBeTruthy(); 258 | }); 259 | }); 260 | return describe("when deleting tags", function() { 261 | it("calls beforeDeletingTag before a tag is removed, providing the tag as first parameter", function() { 262 | var tags, wasCalled; 263 | wasCalled = false; 264 | tags = newTagger("tagger2", { 265 | tagData: ["a", "b", "c"], 266 | beforeDeletingTag: function(tag) { 267 | wasCalled = true; 268 | return expect(tag).toEqual("a"); 269 | } 270 | }); 271 | tags.removeTag("a"); 272 | return expect(wasCalled).toBeTruthy(); 273 | }); 274 | it("will not add a tag if beforeDeletingTag returns false", function() { 275 | var tags; 276 | tags = newTagger("tagger2", { 277 | tagData: ["a", "b", "c"], 278 | beforeDeletingTag: function(tag) { 279 | return false; 280 | } 281 | }); 282 | tags.removeTag("a"); 283 | return expect(tags.getTags()).toEqual(["a", "b", "c"]); 284 | }); 285 | return it("calls afterDeletingTag after a tag is removed, providing the tag as first parameter", function() { 286 | var tags, wasCalled; 287 | wasCalled = false; 288 | tags = newTagger("tagger2", { 289 | tagData: ["a", "b", "c"], 290 | afterDeletingTag: function(tag) { 291 | wasCalled = true; 292 | return expect(tag).toEqual("a"); 293 | } 294 | }); 295 | tags.removeTag("a"); 296 | return expect(wasCalled).toBeTruthy(); 297 | }); 298 | }); 299 | }); 300 | describe("when restricting tags using restrictTo option", function() { 301 | return it("will not add any tags that aren't approved", function() { 302 | var tags; 303 | tags = newTagger("tagger2", { 304 | restrictTo: ["a", "b", "c"] 305 | }); 306 | tags.addTag('foo').addTag('bar').addTag('baz').addTag('a'); 307 | expect(tags.getTags()).toEqual(['a']); 308 | return $('#tagger2').remove(); 309 | }); 310 | }); 311 | describe("when providing exclusion options", function() { 312 | it("can exclude tags via the excludes function option", function() { 313 | var excludesFunction, tags; 314 | excludesFunction = function(tag) { 315 | if (tag.indexOf('foo') > -1) { 316 | return false; 317 | } 318 | return true; 319 | }; 320 | tags = newTagger("tagger2", { 321 | excludes: excludesFunction 322 | }); 323 | tags.addTag('foo').addTag('bar').addTag('baz').addTag('foobarbaz'); 324 | expect(tags.getTags()).toEqual(['foo', 'foobarbaz']); 325 | return $('#tagger2').remove(); 326 | }); 327 | return it("can exclude tags via the exclude option", function() { 328 | var tags; 329 | tags = newTagger("tagger2", { 330 | exclude: ["a", "b", "c"] 331 | }); 332 | tags.addTag('a').addTag('b').addTag('c').addTag('d'); 333 | expect(tags.getTags()).toEqual(['d']); 334 | return $('#tagger2').remove(); 335 | }); 336 | }); 337 | return describe("when auto-suggesting", function() { 338 | describe("and caseInsensitive is true", function() { 339 | describe("and tags are uppercase", function() { 340 | it("should find suggestions for lowercase input", function() { 341 | var tags; 342 | tags = newTagger("tagger2", { 343 | caseInsensitive: true, 344 | suggestions: ["Alpha", "Bravo", "Charlie"] 345 | }); 346 | return expect(tags.getSuggestions("a", true)).toEqual(["Alpha"]); 347 | }); 348 | return it("should find suggestions for uppercase input", function() { 349 | var tags; 350 | tags = newTagger("tagger2", { 351 | caseInsensitive: true, 352 | suggestions: ["Alpha", "Bravo", "Charlie"] 353 | }); 354 | return expect(tags.getSuggestions("A", true)).toEqual(["Alpha"]); 355 | }); 356 | }); 357 | return describe("and tags are lowercase", function() { 358 | it("should find suggestions for lowercase input", function() { 359 | var tags; 360 | tags = newTagger("tagger2", { 361 | caseInsensitive: true, 362 | suggestions: ["alpha", "bravo", "charlie"] 363 | }); 364 | return expect(tags.getSuggestions("a", true)).toEqual(["alpha"]); 365 | }); 366 | return it("should find suggestions for uppercase input", function() { 367 | var tags; 368 | tags = newTagger("tagger2", { 369 | caseInsensitive: true, 370 | suggestions: ["alpha", "bravo", "charlie"] 371 | }); 372 | return expect(tags.getSuggestions("A", true)).toEqual(["alpha"]); 373 | }); 374 | }); 375 | }); 376 | return describe("and caseInsensitive is false", function() { 377 | describe("and tags are uppercase", function() { 378 | it("should not find suggestions for lowercase input", function() { 379 | var tags; 380 | tags = newTagger("tagger2", { 381 | suggestions: ["Alpha", "Bravo", "Charlie"] 382 | }); 383 | return expect(tags.getSuggestions("a", true)).toEqual([]); 384 | }); 385 | return it("should find suggestions for uppercase input", function() { 386 | var tags; 387 | tags = newTagger("tagger2", { 388 | suggestions: ["Alpha", "Bravo", "Charlie"] 389 | }); 390 | return expect(tags.getSuggestions("A", true)).toEqual(["Alpha"]); 391 | }); 392 | }); 393 | return describe("and tags are lowercase", function() { 394 | it("should find suggestions for lowercase input", function() { 395 | var tags; 396 | tags = newTagger("tagger2", { 397 | suggestions: ["alpha", "bravo", "charlie"] 398 | }); 399 | return expect(tags.getSuggestions("a", true)).toEqual(["alpha"]); 400 | }); 401 | return it("should not find suggestions for uppercase input", function() { 402 | var tags; 403 | tags = newTagger("tagger2", { 404 | suggestions: ["alpha", "bravo", "charlie"] 405 | }); 406 | return expect(tags.getSuggestions("A", true)).toEqual([]); 407 | }); 408 | }); 409 | }); 410 | }); 411 | }); 412 | }); 413 | 414 | }).call(this); 415 | -------------------------------------------------------------------------------- /dist/js/bootstrap-tags.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * bootstrap-tags 1.1.5 3 | * https://github.com/maxwells/bootstrap-tags 4 | * Copyright 2013 Max Lahey; Licensed MIT 5 | */ 6 | 7 | (function($) { 8 | (function() { 9 | window.Tags || (window.Tags = {}); 10 | jQuery(function() { 11 | $.tags = function(element, options) { 12 | var key, tag, tagData, value, _i, _len, _ref, _this = this; 13 | if (options == null) { 14 | options = {}; 15 | } 16 | for (key in options) { 17 | value = options[key]; 18 | this[key] = value; 19 | } 20 | this.bootstrapVersion || (this.bootstrapVersion = "3"); 21 | this.readOnly || (this.readOnly = false); 22 | this.suggestOnClick || (this.suggestOnClick = false); 23 | this.suggestions || (this.suggestions = []); 24 | this.restrictTo = options.restrictTo != null ? options.restrictTo.concat(this.suggestions) : false; 25 | this.exclude || (this.exclude = false); 26 | this.displayPopovers = options.popovers != null ? true : options.popoverData != null; 27 | this.popoverTrigger || (this.popoverTrigger = "hover"); 28 | this.tagClass || (this.tagClass = "btn-info"); 29 | this.tagSize || (this.tagSize = "md"); 30 | this.promptText || (this.promptText = "Enter tags..."); 31 | this.caseInsensitive || (this.caseInsensitive = false); 32 | this.readOnlyEmptyMessage || (this.readOnlyEmptyMessage = "No tags to display..."); 33 | this.maxNumTags || (this.maxNumTags = -1); 34 | this.beforeAddingTag || (this.beforeAddingTag = function(tag) {}); 35 | this.afterAddingTag || (this.afterAddingTag = function(tag) {}); 36 | this.beforeDeletingTag || (this.beforeDeletingTag = function(tag) {}); 37 | this.afterDeletingTag || (this.afterDeletingTag = function(tag) {}); 38 | this.definePopover || (this.definePopover = function(tag) { 39 | return 'associated content for "' + tag + '"'; 40 | }); 41 | this.excludes || (this.excludes = function() { 42 | return false; 43 | }); 44 | this.tagRemoved || (this.tagRemoved = function(tag) {}); 45 | this.pressedReturn || (this.pressedReturn = function(e) {}); 46 | this.pressedDelete || (this.pressedDelete = function(e) {}); 47 | this.pressedDown || (this.pressedDown = function(e) {}); 48 | this.pressedUp || (this.pressedUp = function(e) {}); 49 | this.$element = $(element); 50 | if (options.tagData != null) { 51 | this.tagsArray = options.tagData; 52 | } else { 53 | tagData = $(".tag-data", this.$element).html(); 54 | this.tagsArray = tagData != null ? tagData.split(",") : []; 55 | } 56 | if (options.popoverData) { 57 | this.popoverArray = options.popoverData; 58 | } else { 59 | this.popoverArray = []; 60 | _ref = this.tagsArray; 61 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 62 | tag = _ref[_i]; 63 | this.popoverArray.push(null); 64 | } 65 | } 66 | this.getTags = function() { 67 | return _this.tagsArray; 68 | }; 69 | this.getTagsContent = function() { 70 | return _this.popoverArray; 71 | }; 72 | this.getTagsWithContent = function() { 73 | var combined, i, _j, _ref1; 74 | combined = []; 75 | for (i = _j = 0, _ref1 = _this.tagsArray.length - 1; 0 <= _ref1 ? _j <= _ref1 : _j >= _ref1; i = 0 <= _ref1 ? ++_j : --_j) { 76 | combined.push({ 77 | tag: _this.tagsArray[i], 78 | content: _this.popoverArray[i] 79 | }); 80 | } 81 | return combined; 82 | }; 83 | this.getTag = function(tag) { 84 | var index; 85 | index = _this.tagsArray.indexOf(tag); 86 | if (index > -1) { 87 | return _this.tagsArray[index]; 88 | } else { 89 | return null; 90 | } 91 | }; 92 | this.getTagWithContent = function(tag) { 93 | var index; 94 | index = _this.tagsArray.indexOf(tag); 95 | return { 96 | tag: _this.tagsArray[index], 97 | content: _this.popoverArray[index] 98 | }; 99 | }; 100 | this.hasTag = function(tag) { 101 | return _this.tagsArray.indexOf(tag) > -1; 102 | }; 103 | this.removeTagClicked = function(e) { 104 | if (e.currentTarget.tagName === "A") { 105 | _this.removeTag($("span", e.currentTarget.parentElement).html()); 106 | $(e.currentTarget.parentNode).remove(); 107 | } 108 | return _this; 109 | }; 110 | this.removeLastTag = function() { 111 | if (_this.tagsArray.length > 0) { 112 | _this.removeTag(_this.tagsArray[_this.tagsArray.length - 1]); 113 | if (_this.canAddByMaxNum()) { 114 | _this.enableInput(); 115 | } 116 | } 117 | return _this; 118 | }; 119 | this.removeTag = function(tag) { 120 | if (_this.tagsArray.indexOf(tag) > -1) { 121 | if (_this.beforeDeletingTag(tag) === false) { 122 | return; 123 | } 124 | _this.popoverArray.splice(_this.tagsArray.indexOf(tag), 1); 125 | _this.tagsArray.splice(_this.tagsArray.indexOf(tag), 1); 126 | _this.renderTags(); 127 | _this.afterDeletingTag(tag); 128 | if (_this.canAddByMaxNum()) { 129 | _this.enableInput(); 130 | } 131 | } 132 | return _this; 133 | }; 134 | this.canAddByRestriction = function(tag) { 135 | return this.restrictTo === false || this.restrictTo.indexOf(tag) !== -1; 136 | }; 137 | this.canAddByExclusion = function(tag) { 138 | return (this.exclude === false || this.exclude.indexOf(tag) === -1) && !this.excludes(tag); 139 | }; 140 | this.canAddByMaxNum = function() { 141 | return this.maxNumTags === -1 || this.tagsArray.length < this.maxNumTags; 142 | }; 143 | this.addTag = function(tag) { 144 | var associatedContent; 145 | if (_this.canAddByRestriction(tag) && !_this.hasTag(tag) && tag.length > 0 && _this.canAddByExclusion(tag) && _this.canAddByMaxNum()) { 146 | if (_this.beforeAddingTag(tag) === false) { 147 | return; 148 | } 149 | associatedContent = _this.definePopover(tag); 150 | _this.popoverArray.push(associatedContent || null); 151 | _this.tagsArray.push(tag); 152 | _this.afterAddingTag(tag); 153 | _this.renderTags(); 154 | if (!_this.canAddByMaxNum()) { 155 | _this.disableInput(); 156 | } 157 | } 158 | return _this; 159 | }; 160 | this.addTagWithContent = function(tag, content) { 161 | if (_this.canAddByRestriction(tag) && !_this.hasTag(tag) && tag.length > 0) { 162 | if (_this.beforeAddingTag(tag) === false) { 163 | return; 164 | } 165 | _this.tagsArray.push(tag); 166 | _this.popoverArray.push(content); 167 | _this.afterAddingTag(tag); 168 | _this.renderTags(); 169 | } 170 | return _this; 171 | }; 172 | this.renameTag = function(name, newName) { 173 | _this.tagsArray[_this.tagsArray.indexOf(name)] = newName; 174 | _this.renderTags(); 175 | return _this; 176 | }; 177 | this.setPopover = function(tag, popoverContent) { 178 | _this.popoverArray[_this.tagsArray.indexOf(tag)] = popoverContent; 179 | _this.renderTags(); 180 | return _this; 181 | }; 182 | this.clickHandler = function(e) { 183 | return _this.makeSuggestions(e, true); 184 | }; 185 | this.keyDownHandler = function(e) { 186 | var k, numSuggestions; 187 | k = e.keyCode != null ? e.keyCode : e.which; 188 | switch (k) { 189 | case 13: 190 | e.preventDefault(); 191 | _this.pressedReturn(e); 192 | tag = e.target.value; 193 | if (_this.suggestedIndex !== -1) { 194 | tag = _this.suggestionList[_this.suggestedIndex]; 195 | } 196 | _this.addTag(tag); 197 | e.target.value = ""; 198 | _this.renderTags(); 199 | return _this.hideSuggestions(); 200 | 201 | case 46: 202 | case 8: 203 | _this.pressedDelete(e); 204 | if (e.target.value === "") { 205 | _this.removeLastTag(); 206 | } 207 | if (e.target.value.length === 1) { 208 | return _this.hideSuggestions(); 209 | } 210 | break; 211 | 212 | case 40: 213 | _this.pressedDown(e); 214 | if (_this.input.val() === "" && (_this.suggestedIndex === -1 || _this.suggestedIndex == null)) { 215 | _this.makeSuggestions(e, true); 216 | } 217 | numSuggestions = _this.suggestionList.length; 218 | _this.suggestedIndex = _this.suggestedIndex < numSuggestions - 1 ? _this.suggestedIndex + 1 : numSuggestions - 1; 219 | _this.selectSuggested(_this.suggestedIndex); 220 | if (_this.suggestedIndex >= 0) { 221 | return _this.scrollSuggested(_this.suggestedIndex); 222 | } 223 | break; 224 | 225 | case 38: 226 | _this.pressedUp(e); 227 | _this.suggestedIndex = _this.suggestedIndex > 0 ? _this.suggestedIndex - 1 : 0; 228 | _this.selectSuggested(_this.suggestedIndex); 229 | if (_this.suggestedIndex >= 0) { 230 | return _this.scrollSuggested(_this.suggestedIndex); 231 | } 232 | break; 233 | 234 | case 9: 235 | case 27: 236 | _this.hideSuggestions(); 237 | return _this.suggestedIndex = -1; 238 | } 239 | }; 240 | this.keyUpHandler = function(e) { 241 | var k; 242 | k = e.keyCode != null ? e.keyCode : e.which; 243 | if (k !== 40 && k !== 38 && k !== 27) { 244 | return _this.makeSuggestions(e, false); 245 | } 246 | }; 247 | this.getSuggestions = function(str, overrideLengthCheck) { 248 | var _this = this; 249 | this.suggestionList = []; 250 | if (this.caseInsensitive) { 251 | str = str.toLowerCase(); 252 | } 253 | $.each(this.suggestions, function(i, suggestion) { 254 | var suggestionVal; 255 | suggestionVal = _this.caseInsensitive ? suggestion.substring(0, str.length).toLowerCase() : suggestion.substring(0, str.length); 256 | if (_this.tagsArray.indexOf(suggestion) < 0 && suggestionVal === str && (str.length > 0 || overrideLengthCheck)) { 257 | return _this.suggestionList.push(suggestion); 258 | } 259 | }); 260 | return this.suggestionList; 261 | }; 262 | this.makeSuggestions = function(e, overrideLengthCheck, val) { 263 | if (val == null) { 264 | val = e.target.value != null ? e.target.value : e.target.textContent; 265 | } 266 | _this.suggestedIndex = -1; 267 | _this.$suggestionList.html(""); 268 | $.each(_this.getSuggestions(val, overrideLengthCheck), function(i, suggestion) { 269 | return _this.$suggestionList.append(_this.template("tags_suggestion", { 270 | suggestion: suggestion 271 | })); 272 | }); 273 | _this.$(".tags-suggestion").mouseover(_this.selectSuggestedMouseOver); 274 | _this.$(".tags-suggestion").click(_this.suggestedClicked); 275 | if (_this.suggestionList.length > 0) { 276 | return _this.showSuggestions(); 277 | } else { 278 | return _this.hideSuggestions(); 279 | } 280 | }; 281 | this.suggestedClicked = function(e) { 282 | tag = e.target.textContent; 283 | if (_this.suggestedIndex !== -1) { 284 | tag = _this.suggestionList[_this.suggestedIndex]; 285 | } 286 | _this.addTag(tag); 287 | _this.input.val(""); 288 | _this.makeSuggestions(e, false, ""); 289 | _this.input.focus(); 290 | return _this.hideSuggestions(); 291 | }; 292 | this.hideSuggestions = function() { 293 | return _this.$(".tags-suggestion-list").css({ 294 | display: "none" 295 | }); 296 | }; 297 | this.showSuggestions = function() { 298 | return _this.$(".tags-suggestion-list").css({ 299 | display: "block" 300 | }); 301 | }; 302 | this.selectSuggestedMouseOver = function(e) { 303 | $(".tags-suggestion").removeClass("tags-suggestion-highlighted"); 304 | $(e.target).addClass("tags-suggestion-highlighted"); 305 | $(e.target).mouseout(_this.selectSuggestedMousedOut); 306 | return _this.suggestedIndex = _this.$(".tags-suggestion").index($(e.target)); 307 | }; 308 | this.selectSuggestedMousedOut = function(e) { 309 | return $(e.target).removeClass("tags-suggestion-highlighted"); 310 | }; 311 | this.selectSuggested = function(i) { 312 | var tagElement; 313 | $(".tags-suggestion").removeClass("tags-suggestion-highlighted"); 314 | tagElement = _this.$(".tags-suggestion").eq(i); 315 | return tagElement.addClass("tags-suggestion-highlighted"); 316 | }; 317 | this.scrollSuggested = function(i) { 318 | var pos, tagElement, topElement, topPos; 319 | tagElement = _this.$(".tags-suggestion").eq(i); 320 | topElement = _this.$(".tags-suggestion").eq(0); 321 | pos = tagElement.position(); 322 | topPos = topElement.position(); 323 | if (pos != null) { 324 | return _this.$(".tags-suggestion-list").scrollTop(pos.top - topPos.top); 325 | } 326 | }; 327 | this.adjustInputPosition = function() { 328 | var pBottom, pLeft, pTop, pWidth, tagElement, tagPosition; 329 | tagElement = _this.$(".tag").last(); 330 | tagPosition = tagElement.position(); 331 | pLeft = tagPosition != null ? tagPosition.left + tagElement.outerWidth(true) : 0; 332 | pTop = tagPosition != null ? tagPosition.top : 0; 333 | pWidth = _this.$element.width() - pLeft; 334 | $(".tags-input", _this.$element).css({ 335 | paddingLeft: Math.max(pLeft, 0), 336 | paddingTop: Math.max(pTop, 0), 337 | width: pWidth 338 | }); 339 | pBottom = tagPosition != null ? tagPosition.top + tagElement.outerHeight(true) : 22; 340 | return _this.$element.css({ 341 | paddingBottom: pBottom - _this.$element.height() 342 | }); 343 | }; 344 | this.renderTags = function() { 345 | var tagList; 346 | tagList = _this.$(".tags"); 347 | tagList.html(""); 348 | _this.input.attr("placeholder", _this.tagsArray.length === 0 ? _this.promptText : ""); 349 | $.each(_this.tagsArray, function(i, tag) { 350 | tag = $(_this.formatTag(i, tag)); 351 | $("a", tag).click(_this.removeTagClicked); 352 | $("a", tag).mouseover(_this.toggleCloseColor); 353 | $("a", tag).mouseout(_this.toggleCloseColor); 354 | if (_this.displayPopovers) { 355 | _this.initializePopoverFor(tag, _this.tagsArray[i], _this.popoverArray[i]); 356 | } 357 | return tagList.append(tag); 358 | }); 359 | return _this.adjustInputPosition(); 360 | }; 361 | this.renderReadOnly = function() { 362 | var tagList; 363 | tagList = _this.$(".tags"); 364 | tagList.html(_this.tagsArray.length === 0 ? _this.readOnlyEmptyMessage : ""); 365 | return $.each(_this.tagsArray, function(i, tag) { 366 | tag = $(_this.formatTag(i, tag, true)); 367 | if (_this.displayPopovers) { 368 | _this.initializePopoverFor(tag, _this.tagsArray[i], _this.popoverArray[i]); 369 | } 370 | return tagList.append(tag); 371 | }); 372 | }; 373 | this.disableInput = function() { 374 | return this.$("input").prop("disabled", true); 375 | }; 376 | this.enableInput = function() { 377 | return this.$("input").prop("disabled", false); 378 | }; 379 | this.initializePopoverFor = function(tag, title, content) { 380 | options = { 381 | title: title, 382 | content: content, 383 | placement: "bottom" 384 | }; 385 | if (_this.popoverTrigger === "hoverShowClickHide") { 386 | $(tag).mouseover(function() { 387 | $(tag).popover("show"); 388 | return $(".tag").not(tag).popover("hide"); 389 | }); 390 | $(document).click(function() { 391 | return $(tag).popover("hide"); 392 | }); 393 | } else { 394 | options.trigger = _this.popoverTrigger; 395 | } 396 | return $(tag).popover(options); 397 | }; 398 | this.toggleCloseColor = function(e) { 399 | var opacity, tagAnchor; 400 | tagAnchor = $(e.currentTarget); 401 | opacity = tagAnchor.css("opacity"); 402 | opacity = opacity < .8 ? 1 : .6; 403 | return tagAnchor.css({ 404 | opacity: opacity 405 | }); 406 | }; 407 | this.formatTag = function(i, tag, isReadOnly) { 408 | var escapedTag; 409 | if (isReadOnly == null) { 410 | isReadOnly = false; 411 | } 412 | escapedTag = tag.replace("<", "<").replace(">", ">"); 413 | return _this.template("tag", { 414 | tag: escapedTag, 415 | tagClass: _this.tagClass, 416 | isPopover: _this.displayPopovers, 417 | isReadOnly: isReadOnly, 418 | tagSize: _this.tagSize 419 | }); 420 | }; 421 | this.addDocumentListeners = function() { 422 | return $(document).mouseup(function(e) { 423 | var container; 424 | container = _this.$(".tags-suggestion-list"); 425 | if (container.has(e.target).length === 0) { 426 | return _this.hideSuggestions(); 427 | } 428 | }); 429 | }; 430 | this.template = function(name, options) { 431 | return Tags.Templates.Template(this.getBootstrapVersion(), name, options); 432 | }; 433 | this.$ = function(selector) { 434 | return $(selector, this.$element); 435 | }; 436 | this.getBootstrapVersion = function() { 437 | return Tags.bootstrapVersion || this.bootstrapVersion; 438 | }; 439 | this.initializeDom = function() { 440 | return this.$element.append(this.template("tags_container")); 441 | }; 442 | this.init = function() { 443 | this.$element.addClass("bootstrap-tags").addClass("bootstrap-" + this.getBootstrapVersion()); 444 | this.initializeDom(); 445 | if (this.readOnly) { 446 | this.renderReadOnly(); 447 | this.removeTag = function() {}; 448 | this.removeTagClicked = function() {}; 449 | this.removeLastTag = function() {}; 450 | this.addTag = function() {}; 451 | this.addTagWithContent = function() {}; 452 | this.renameTag = function() {}; 453 | return this.setPopover = function() {}; 454 | } else { 455 | this.input = $(this.template("input", { 456 | tagSize: this.tagSize 457 | })); 458 | if (this.suggestOnClick) { 459 | this.input.click(this.clickHandler); 460 | } 461 | this.input.keydown(this.keyDownHandler); 462 | this.input.keyup(this.keyUpHandler); 463 | this.$element.append(this.input); 464 | this.$suggestionList = $(this.template("suggestion_list")); 465 | this.$element.append(this.$suggestionList); 466 | this.renderTags(); 467 | if (!this.canAddByMaxNum()) { 468 | this.disableInput(); 469 | } 470 | return this.addDocumentListeners(); 471 | } 472 | }; 473 | this.init(); 474 | return this; 475 | }; 476 | return $.fn.tags = function(options) { 477 | var stopOn, tagsObject; 478 | tagsObject = {}; 479 | stopOn = typeof options === "number" ? options : -1; 480 | this.each(function(i, el) { 481 | var $el; 482 | $el = $(el); 483 | if ($el.data("tags") == null) { 484 | $el.data("tags", new $.tags(this, options)); 485 | } 486 | if (stopOn === i || i === 0) { 487 | return tagsObject = $el.data("tags"); 488 | } 489 | }); 490 | return tagsObject; 491 | }; 492 | }); 493 | }).call(this); 494 | (function() { 495 | window.Tags || (window.Tags = {}); 496 | Tags.Helpers || (Tags.Helpers = {}); 497 | Tags.Helpers.addPadding = function(string, amount, doPadding) { 498 | if (amount == null) { 499 | amount = 1; 500 | } 501 | if (doPadding == null) { 502 | doPadding = true; 503 | } 504 | if (!doPadding) { 505 | return string; 506 | } 507 | if (amount === 0) { 508 | return string; 509 | } 510 | return Tags.Helpers.addPadding(" " + string + " ", amount - 1); 511 | }; 512 | }).call(this); 513 | (function() { 514 | var _base; 515 | window.Tags || (window.Tags = {}); 516 | Tags.Templates || (Tags.Templates = {}); 517 | (_base = Tags.Templates)["2"] || (_base["2"] = {}); 518 | Tags.Templates["2"].input = function(options) { 519 | var tagSize; 520 | if (options == null) { 521 | options = {}; 522 | } 523 | tagSize = function() { 524 | switch (options.tagSize) { 525 | case "sm": 526 | return "small"; 527 | 528 | case "md": 529 | return "medium"; 530 | 531 | case "lg": 532 | return "large"; 533 | } 534 | }(); 535 | return ""; 536 | }; 537 | }).call(this); 538 | (function() { 539 | var _base; 540 | window.Tags || (window.Tags = {}); 541 | Tags.Templates || (Tags.Templates = {}); 542 | (_base = Tags.Templates)["2"] || (_base["2"] = {}); 543 | Tags.Templates["2"].tag = function(options) { 544 | if (options == null) { 545 | options = {}; 546 | } 547 | return "
    " + Tags.Helpers.addPadding(options.tag, 2, options.isReadOnly) + " " + (options.isReadOnly ? "" : "") + "
    "; 548 | }; 549 | }).call(this); 550 | (function() { 551 | var _base; 552 | window.Tags || (window.Tags = {}); 553 | Tags.Templates || (Tags.Templates = {}); 554 | (_base = Tags.Templates)["3"] || (_base["3"] = {}); 555 | Tags.Templates["3"].input = function(options) { 556 | if (options == null) { 557 | options = {}; 558 | } 559 | return ""; 560 | }; 561 | }).call(this); 562 | (function() { 563 | var _base; 564 | window.Tags || (window.Tags = {}); 565 | Tags.Templates || (Tags.Templates = {}); 566 | (_base = Tags.Templates)["3"] || (_base["3"] = {}); 567 | Tags.Templates["3"].tag = function(options) { 568 | if (options == null) { 569 | options = {}; 570 | } 571 | return "
    " + Tags.Helpers.addPadding(options.tag, 2, options.isReadOnly) + " " + (options.isReadOnly ? "" : "") + "
    "; 572 | }; 573 | }).call(this); 574 | (function() { 575 | var _base; 576 | window.Tags || (window.Tags = {}); 577 | Tags.Templates || (Tags.Templates = {}); 578 | (_base = Tags.Templates).shared || (_base.shared = {}); 579 | Tags.Templates.shared.suggestion_list = function(options) { 580 | if (options == null) { 581 | options = {}; 582 | } 583 | return ''; 584 | }; 585 | }).call(this); 586 | (function() { 587 | var _base; 588 | window.Tags || (window.Tags = {}); 589 | Tags.Templates || (Tags.Templates = {}); 590 | (_base = Tags.Templates).shared || (_base.shared = {}); 591 | Tags.Templates.shared.tags_container = function(options) { 592 | if (options == null) { 593 | options = {}; 594 | } 595 | return '
    '; 596 | }; 597 | }).call(this); 598 | (function() { 599 | var _base; 600 | window.Tags || (window.Tags = {}); 601 | Tags.Templates || (Tags.Templates = {}); 602 | (_base = Tags.Templates).shared || (_base.shared = {}); 603 | Tags.Templates.shared.tags_suggestion = function(options) { 604 | if (options == null) { 605 | options = {}; 606 | } 607 | return "
  • " + options.suggestion + "
  • "; 608 | }; 609 | }).call(this); 610 | (function() { 611 | window.Tags || (window.Tags = {}); 612 | Tags.Templates || (Tags.Templates = {}); 613 | Tags.Templates.Template = function(version, templateName, options) { 614 | if (Tags.Templates[version] != null) { 615 | if (Tags.Templates[version][templateName] != null) { 616 | return Tags.Templates[version][templateName](options); 617 | } 618 | } 619 | return Tags.Templates.shared[templateName](options); 620 | }; 621 | }).call(this); 622 | })(window.jQuery); --------------------------------------------------------------------------------