├── .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 | ""
--------------------------------------------------------------------------------
/src/templates/3/tag.coffee:
--------------------------------------------------------------------------------
1 | window.Tags ||= {}
2 | Tags.Templates ||= {}
3 | Tags.Templates["3"] ||= {}
4 | Tags.Templates["3"].tag = (options = {}) ->
5 | ""
--------------------------------------------------------------------------------
/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 | [](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={}),""}}.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={}),""}}.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 "";
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 "";
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);
--------------------------------------------------------------------------------