├── .bowerrc
├── .gitignore
├── .travis.yml
├── Gruntfile.js
├── LICENSE
├── README.md
├── bower.json
├── dist
├── angular-tags-0.3.1-tpls.js
├── angular-tags-0.3.1-tpls.map.js
├── angular-tags-0.3.1-tpls.min.js
├── angular-tags-0.3.1.css
├── angular-tags-0.3.1.js
├── angular-tags-0.3.1.less
├── angular-tags-0.3.1.map.js
├── angular-tags-0.3.1.min.js
└── templates
│ └── tags.html
├── less
└── tags.less
├── package.json
├── src
└── tags.js
├── templates
└── tags.html
└── test
├── test-tags.html
└── test-tags.js
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "bower_components"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lib-cov
2 | *.seed
3 | *.log
4 | *.csv
5 | *.dat
6 | *.out
7 | *.pid
8 | *.gz
9 |
10 | pids
11 | logs
12 | results
13 |
14 | npm-debug.log
15 | .idea
16 | test/lib
17 | dist/generated
18 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.11"
4 | - "0.10"
5 | - "0.8"
6 |
7 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function (grunt) {
2 |
3 | // Project configuration.
4 | grunt.initConfig({
5 | pkg: require('./package.json'),
6 |
7 | qunit: {
8 | all: {
9 | options: {
10 | urls: [
11 | 'http://localhost:8000/test/test-tags.html'
12 | ],
13 | force: true
14 | }
15 | }
16 | },
17 | connect: {
18 | server: {
19 | options: {
20 | port: 8000,
21 | base: '.'
22 | }
23 | }
24 | },
25 | bower: {
26 | install: {
27 | options: {
28 | targetDir: './test/lib',
29 | cleanup: true
30 | }
31 | }
32 | },
33 | watch: {
34 | scripts: {
35 | files: [
36 | 'src/tags.js',
37 | 'templates/tags.html',
38 | 'test/test-tags.html',
39 | 'test/test-tags.js'
40 | ],
41 | tasks: ['test']
42 | }
43 | },
44 | html2js: {
45 | options: {
46 | base: '.'
47 | },
48 | dist: {
49 | src: ['./templates/tags.html'],
50 | dest: 'dist/generated/templates.js',
51 | module: 'decipher.tags.templates'
52 |
53 | }
54 | },
55 | less: {
56 | dist: {
57 | options: {
58 | paths: ["."],
59 | yuicompress: false
60 | },
61 | files: {
62 | "dist/<%=pkg.name%>-<%=pkg.version%>.css": "less/tags.less"
63 | }
64 | }
65 | },
66 | uglify: {
67 | dist: {
68 | files: {
69 | 'dist/<%= pkg.name %>-<%= pkg.version %>.min.js': [
70 | 'dist/generated/tags.js'
71 | ]
72 | },
73 | options: {
74 | report: 'min',
75 | sourceMap: 'dist/<%=' +
76 | ' pkg.name %>-<%= pkg.version %>.map.js',
77 | sourceMapRoot: '/',
78 | sourceMapPrefix: 1,
79 | sourceMappingURL: '<%= pkg.name %>-<%= pkg.version %>.map.js'
80 | }
81 | },
82 | distTpls: {
83 | files: {
84 | 'dist/<%= pkg.name %>-<%= pkg.version %>-tpls.min.js': [
85 | 'dist/generated/*.js'
86 | ]
87 | },
88 | options: {
89 | report: 'min',
90 | sourceMap: 'dist/<%= pkg.name %>-<%= pkg.version %>-tpls.map.js',
91 | sourceMapRoot: '/',
92 | sourceMapPrefix: 1,
93 | sourceMappingURL: '<%= pkg.name %>-<%= pkg.version %>-tpls.map.js'
94 | }
95 | }
96 | },
97 | concat: {
98 | dist: {
99 | src: ['dist/generated/tags.js'],
100 | dest: 'dist/<%=pkg.name%>-<%=pkg.version%>.js'
101 | },
102 | distTpls: {
103 | src: ['dist/generated/templates.js', 'dist/generated/tags.js'],
104 | dest: 'dist/<%=pkg.name%>-<%=pkg.version%>-tpls.js'
105 | }
106 | },
107 | copy: {
108 | dist: {
109 | files: [
110 | {
111 | src: ['templates/tags.html'],
112 | dest: 'dist/templates/tags.html'
113 | },
114 | {
115 | src: ['less/tags.less'],
116 | dest: 'dist/<%=pkg.name%>-<%=pkg.version%>.less'
117 | },
118 | {
119 | src: ['src/tags.js'],
120 | dest: 'dist/generated/tags.js'
121 | }
122 | ]
123 | }
124 | }
125 | });
126 |
127 | grunt.loadNpmTasks('grunt-contrib-qunit');
128 | grunt.loadNpmTasks('grunt-contrib-connect');
129 | grunt.loadNpmTasks('grunt-bower-task');
130 | grunt.loadNpmTasks('grunt-contrib-watch');
131 | grunt.loadNpmTasks('grunt-html2js');
132 | grunt.loadNpmTasks('grunt-contrib-less');
133 | grunt.loadNpmTasks('grunt-contrib-uglify');
134 | grunt.loadNpmTasks('grunt-contrib-concat');
135 | grunt.loadNpmTasks('grunt-contrib-copy');
136 |
137 | grunt.registerTask('test',
138 | ['build', 'bower:install', 'connect', 'qunit']);
139 | grunt.registerTask('build', ['less', 'html2js', 'copy', 'concat', 'uglify']);
140 | grunt.registerTask('default', ['build']);
141 |
142 | grunt.event.on('qunit.log',
143 | function (result, actual, expected, message) {
144 | if (!!result) {
145 | grunt.log.ok(message);
146 | }
147 | });
148 | };
149 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013 Christopher Hiller
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Achtung!
2 |
3 | > I don't have much time to work on this project and I no longer use it for anything.
4 |
5 | > I'd love it if I could work with somebody to get v1.0.0 out the door. I have refactors, bug fixes, and new tests in the pipeline, but there's more work to do.
6 |
7 | > If any user of this library wants to assist me, I'll happily answer any questions, provide tasks, and discuss direction.
8 |
9 | > Once we release [v1.0.0](https://github.com/boneskull/angular-tags/tree/v1.0.0), I could potentially transfer ownership of this project to another collaborator.
10 |
11 | > *Please comment in [issue #48](https://github.com/boneskull/angular-tags/issues/48) if you are interested!*
12 |
13 | # angular-tags [](https://travis-ci.org/boneskull/angular-tags)
14 |
15 | Pure AngularJS tagging widget with typeahead support courtesy of
16 | [ui-bootstrap](http://angular-ui.github.io/bootstrap).
17 |
18 | Current Version
19 | ---------------
20 | ```
21 | 0.3.1
22 | ```
23 |
24 | Installation
25 | ------------
26 | ```
27 | bower install angular-tags
28 | ```
29 |
30 | Requirements
31 | ------------
32 |
33 | - [AngularJS](http://angularjs.org)
34 | - [ui-bootstrap](http://angular-ui.github.io/bootstrap) (ui.bootstrap.typeahead module)
35 | - [Bootstrap CSS](http://getbootstrap.com) (optional)
36 | - [Font Awesome](http://fortawesome.github.io/Font-Awesome/) (optional)
37 |
38 | Running Tests
39 | -------------
40 |
41 | Clone this repo and execute:
42 |
43 | ```
44 | npm install
45 | ```
46 |
47 | to grab the dependencies. Then execute:
48 |
49 | ```
50 | grunt test
51 | ```
52 |
53 | to run the tests. This will grab the test deps from bower, and run them against QUnit in a local server on port 8000.
54 |
55 |
56 | Usage
57 | =====
58 |
59 | ### Demo
60 |
61 | Demo here.
62 |
63 |
64 | ### Setup
65 |
66 | angular-tags comes in two versions; one with embedded templates and another without. Without templates:
67 |
68 | ```html
69 |
70 | ```
71 |
72 | With templates:
73 |
74 | ```html
75 |
76 | ```
77 |
78 | You will also want to include the CSS if you are using this version:
79 |
80 | ```html
81 |
82 | ```
83 |
84 | Templates are included in the `templates/` directory if you want to load them manually and/or modify them.
85 |
86 | You'll also need to make sure you have included the ui-bootstrap source.
87 |
88 | Finally, include the module in your code, and the required `ui.bootstrap.typeahead` module:
89 |
90 | ```javascript
91 | angular.module('myModule', ['decipher.tags', 'ui.bootstrap.typeahead'];
92 | ```
93 |
94 | ### Directive
95 |
96 | This is a directive, so at its most basic:
97 |
98 | ```html
99 |
100 | ```
101 |
102 | This will render the tags contained in `foo` (if anything) and provide an input prompt for more tags.
103 |
104 | `foo` can be a delimited string, array of strings, or array of objects with `name` properties:
105 |
106 | ```javascript
107 | foo = 'foo,bar';
108 | foo = ['foo', 'bar'];
109 | foo = [{name: 'foo'}, {name: 'bar'}];
110 | ```
111 |
112 | All will render identically. Depending on the format you use, you will get the same type back when adding tags via the input. For example, if you add "baz" in the input and your original model happened to be a delimited string, you will get:
113 |
114 | ```javascript
115 | 'foo,bar,baz'
116 | ```
117 |
118 | Likewise if you had an array of strings:
119 |
120 | ```javascript
121 | ['foo', 'bar', 'baz']
122 | ```
123 |
124 | With Typeahead
125 | --------------
126 |
127 | The above directive usage will not use the typeahead functionality of ui-bootstrap. To use the typehead functionality, which provides a list of tags from which to choose, you have to specify some values to read from:
128 |
129 | ```html
130 |
131 | ```
132 |
133 | The value of `src` is a comprehension expression, like found in [ngOptions](http://docs.angularjs.org/api/ng.directive:select). `baz` here should resemble `foo` as above; a delimited string, an array of strings, or an array of objects. See Tag Objects below.
134 |
135 | *Note*: Here we're using `b` (the entire object) for the value; feel free to use something else, but if we use `b`, the directive will retain any *extra data* you have put in the tag objects:
136 |
137 | ```javascript
138 | baz = [
139 | {foo: 'bar', value: 'baz', name: 'derp'},
140 | {foo: 'spam', value: 'baz', name: 'herp'},
141 | ]
142 | ```
143 |
144 | and
145 |
146 | ```html
147 |
148 | ```
149 |
150 | The resulting source tags will look like this:
151 |
152 | ```javascript
153 | baz = [
154 | {value: 'baz', name: 'derp'},
155 | {value: 'baz', name: 'herp'},
156 | ]
157 | ```
158 |
159 | Basically, whatever you set here will become the `value` of these tags unless you specify an entire object.
160 |
161 | ### Typeahead Options
162 |
163 | You can pass options through to the typeahead module. Simply pass a `typeahead-options` attribute to the `` element. Available options are shown here:
164 |
165 | ```javascript
166 | $scope.typeaheadOpts = {
167 | inputFormatter: myInputFormatterFunction,
168 | loading: myLoadingBoolean,
169 | minLength: 3,
170 | onSelect: myOnSelectFunction, // this will be run in addition to directive internals
171 | templateUrl: '/path/to/my/template.html',
172 | waitMs: 500,
173 | allowsEditable: true
174 | };
175 | ```
176 |
177 | and:
178 |
179 | ```html
180 |
181 | ```
182 |
183 | Tag Objects
184 | -----------
185 |
186 | Tag objects have three main properties:
187 |
188 | - `name` The name (display name) of the tag
189 | - `group` (optional) The "group" of the tag, for assigning class names
190 | - `value` (optional) The "value" of the tag, which is not displayed
191 |
192 | Tag objects can include any other properties you wish to add.
193 |
194 | Options
195 | -----------
196 |
197 | ### Global Options
198 |
199 | To set defaults module-wide, inject the `decipherTagsOptions` constant into anything and modify it:
200 |
201 | ```javascript
202 | myModule.config(function(decipherTagsOptions) {
203 | decipherTagsOptions.delimiter = ':';
204 | decipherTagsOptions.classes = {
205 | myGroup: 'myClass',
206 | myOtherGroup: 'myOtherClass'
207 | };
208 | });
209 | ```
210 |
211 | ### Available Options
212 |
213 | - `addable` whether or not the user is allowed to type arbitrary tags into the input (defaults to `false` by default if a `src` is supplied, otherwise defaults to `true`; see Adding Tags below.
214 | - `delimiter` what to use for a delimiter when typing or pasting into the input. Defaults to `,`
215 | - `classes` An object mapping of group names to class names
216 | - `templateUrl` URL to the main template. Defaults to `templates/tags.html`
217 | - `tagTemplateUrl` URL to the "tag" template. Defaults to `templates/tag.html`
218 |
219 | #### Adding Tags
220 |
221 | If you neglect to supply a `src` (thus not using typeahead) you will be able to enter whatever you like into the tags input, adding tags willy-nilly. If you *do* supply a `src`, by default the user will be limited to what's in the list. You can override this by passing an `addable` property to the options:
222 |
223 | ```html
224 |
225 | ```
226 |
227 | #### Classes
228 |
229 | If you specify classes, your tags will each be assigned a class name based on the group. For example:
230 |
231 | ```html
232 | \n" +
6 | "\n" +
7 | "
\n" +
15 | "\n" +
16 | " \n" +
17 | " \n" +
19 | " \n" +
21 | " \n" +
32 | "\n" +
33 | " \n" +
34 | "\n" +
35 | "");
36 | }]);
37 |
38 | /*global angular*/
39 | (function () {
40 | 'use strict';
41 |
42 | try {
43 | angular.module('decipher.tags.templates');
44 | } catch (e) {
45 | angular.module('decipher.tags.templates', []);
46 | }
47 |
48 | var tags = angular.module('decipher.tags',
49 | ['ui.bootstrap.typeahead', 'decipher.tags.templates']);
50 |
51 | var defaultOptions = {
52 | delimiter: ',', // if given a string model, it splits on this
53 | classes: {} // obj of group names to classes
54 | },
55 |
56 | // for parsing comprehension expression
57 | SRC_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/,
58 |
59 | // keycodes
60 | kc = {
61 | enter: 13,
62 | esc: 27,
63 | backspace: 8
64 | },
65 | kcCompleteTag = [kc.enter],
66 | kcRemoveTag = [kc.backspace],
67 | kcCancelInput = [kc.esc],
68 | id = 0;
69 |
70 | tags.constant('decipherTagsOptions', {});
71 |
72 | /**
73 | * TODO: do we actually share functionality here? We're using this
74 | * controller on both the subdirective and its parent, but I'm not sure
75 | * if we actually use the same functions in both.
76 | */
77 | tags.controller('TagsCtrl',
78 | ['$scope', '$timeout', '$q', function ($scope, $timeout, $q) {
79 |
80 | /**
81 | * Figures out what classes to put on the tag span. It'll add classes
82 | * if defined by group, and it'll add a selected class if the tag
83 | * is preselected to delete.
84 | * @param tag
85 | * @returns {{}}
86 | */
87 | $scope.getClasses = function getGroupClass(tag) {
88 | var r = {};
89 |
90 | if (tag === $scope.toggles.selectedTag) {
91 | r.selected = true;
92 | }
93 | angular.forEach($scope.options.classes, function (klass, groupName) {
94 | if (tag.group === groupName) {
95 | r[klass] = true;
96 | }
97 | });
98 | return r;
99 | };
100 |
101 | /**
102 | * Finds a tag in the src list and removes it.
103 | * @param tag
104 | * @returns {boolean}
105 | */
106 | $scope._filterSrcTags = function filterSrcTags(tag) {
107 | // wrapped in timeout or typeahead becomes confused
108 | return $timeout(function () {
109 | var idx = $scope.srcTags.indexOf(tag);
110 | if (idx >= 0) {
111 | $scope.srcTags.splice(idx, 1);
112 | $scope._deletedSrcTags.push(tag);
113 | return;
114 | }
115 | return $q.reject();
116 | });
117 | };
118 |
119 | /**
120 | * Adds a tag to the list of tags, and if in the typeahead list,
121 | * removes it from that list (and saves it). emits decipher.tags.added
122 | * @param tag
123 | */
124 | $scope.add = function add(tag) {
125 | var _add = function _add(tag) {
126 | $scope.tags.push(tag);
127 | delete $scope.inputTag;
128 | $scope.$emit('decipher.tags.added', {
129 | tag: tag,
130 | $id: $scope.$id
131 | });
132 | },
133 | fail = function fail() {
134 | $scope.$emit('decipher.tags.addfailed', {
135 | tag: tag,
136 | $id: $scope.$id
137 | });
138 | dfrd.reject();
139 | },
140 | i,
141 | dfrd = $q.defer();
142 |
143 | // don't add dupe names
144 | i = $scope.tags.length;
145 | while (i--) {
146 | if ($scope.tags[i].name === tag.name) {
147 | fail();
148 | }
149 | }
150 |
151 | $scope._filterSrcTags(tag)
152 | .then(function () {
153 | _add(tag);
154 | }, function () {
155 | if ($scope.options.addable) {
156 | _add(tag);
157 | dfrd.resolve();
158 | }
159 | else {
160 | fail();
161 | }
162 | });
163 |
164 | return dfrd.promise;
165 | };
166 |
167 | $scope.trust = function(tag) {
168 | return $sce.trustAsHtml(tag.name);
169 | };
170 | /**
171 | * Toggle the input box active.
172 | */
173 | $scope.selectArea = function selectArea() {
174 | $scope.toggles.inputActive = true;
175 | };
176 |
177 | /**
178 | * Removes a tag. Restores stuff into srcTags if it came from there.
179 | * Kills any selected tag. Emit a decipher.tags.removed event.
180 | * @param tag
181 | */
182 | $scope.remove = function remove(tag) {
183 | var idx;
184 | $scope.tags.splice($scope.tags.indexOf(tag), 1);
185 |
186 | if (idx = $scope._deletedSrcTags.indexOf(tag) >= 0) {
187 | $scope._deletedSrcTags.splice(idx, 1);
188 | if ($scope.srcTags.indexOf(tag) === -1) {
189 | $scope.srcTags.push(tag);
190 | }
191 | }
192 |
193 | delete $scope.toggles.selectedTag;
194 |
195 | $scope.$emit('decipher.tags.removed', {
196 | tag: tag,
197 | $id: $scope.$id
198 | });
199 | };
200 |
201 | }]);
202 |
203 | /**
204 | * Directive for the 'input' tag itself, which is of class
205 | * decipher-tags-input.
206 | */
207 | tags.directive('decipherTagsInput',
208 | ['$timeout', '$filter', '$rootScope',
209 | function ($timeout, $filter, $rootScope) {
210 | return {
211 | restrict: 'C',
212 | require: 'ngModel',
213 | link: function (scope, element, attrs, ngModel) {
214 | var delimiterRx = new RegExp('^' +
215 | scope.options.delimiter +
216 | '+$'),
217 |
218 | /**
219 | * Cancels the text input box.
220 | */
221 | cancel = function cancel() {
222 | ngModel.$setViewValue('');
223 | ngModel.$render();
224 | },
225 |
226 | /**
227 | * Adds a tag you typed/pasted in unless it's a bunch of delimiters.
228 | * @param value
229 | */
230 | addTag = function addTag(value) {
231 | if (value) {
232 | if (value.match(delimiterRx)) {
233 | cancel();
234 | return;
235 | }
236 | if (scope.add({
237 | name: value
238 | })) {
239 | cancel();
240 | }
241 | }
242 | },
243 |
244 | /**
245 | * Adds multiple tags in case you pasted them.
246 | * @param tags
247 | */
248 | addTags = function (tags) {
249 | var i;
250 | for (i = 0; i < tags.length;
251 | i++) {
252 | addTag(tags[i]);
253 | }
254 | },
255 |
256 | /**
257 | * Backspace one to select, and a second time to delete.
258 | */
259 | removeLastTag = function removeLastTag() {
260 | var orderedTags;
261 | if (scope.toggles.selectedTag) {
262 | scope.remove(scope.toggles.selectedTag);
263 | delete scope.toggles.selectedTag;
264 | }
265 | // only do this if the input field is empty.
266 | else if (!ngModel.$viewValue) {
267 | orderedTags =
268 | $filter('orderBy')(scope.tags,
269 | scope.orderBy);
270 | scope.toggles.selectedTag =
271 | orderedTags[orderedTags.length - 1];
272 | }
273 | };
274 |
275 | /**
276 | * When we focus the text input area, drop the selected tag
277 | */
278 | element.bind('focus', function () {
279 | // this avoids what looks like a bug in typeahead. It seems
280 | // to be calling element[0].focus() somewhere within a digest loop.
281 | if ($rootScope.$$phase) {
282 | delete scope.toggles.selectedTag;
283 | } else {
284 | scope.$apply(function () {
285 | delete scope.toggles.selectedTag;
286 | });
287 | }
288 | });
289 |
290 | /**
291 | * Detects the delimiter.
292 | */
293 | element.bind('keypress',
294 | function (evt) {
295 | scope.$apply(function () {
296 | if (scope.options.delimiter.charCodeAt() ===
297 | evt.which) {
298 | addTag(ngModel.$viewValue);
299 | }
300 | });
301 | });
302 |
303 | /**
304 | * Inspects whatever you typed to see if there were character(s) of
305 | * concern.
306 | */
307 | element.bind('keydown',
308 | function (evt) {
309 | scope.$apply(function () {
310 | // to "complete" a tag
311 |
312 | if (kcCompleteTag.indexOf(evt.which) >=
313 | 0) {
314 | addTag(ngModel.$viewValue);
315 |
316 | // or if you want to get out of the text area
317 | } else if (kcCancelInput.indexOf(evt.which) >=
318 | 0 && !evt.isPropagationStopped()) {
319 | cancel();
320 | scope.toggles.inputActive =
321 | false;
322 |
323 | // or if you're trying to delete something
324 | } else if (kcRemoveTag.indexOf(evt.which) >=
325 | 0) {
326 | removeLastTag();
327 |
328 | // otherwise if we're typing in here, just drop the selected tag.
329 | } else {
330 | delete scope.toggles.selectedTag;
331 | scope.$emit('decipher.tags.keyup',
332 | {
333 | value: ngModel.$viewValue,
334 | $id: scope.$id
335 | });
336 | }
337 | });
338 | });
339 |
340 | /**
341 | * When inputActive toggle changes to true, focus the input.
342 | * And no I have no idea why this has to be in a timeout.
343 | */
344 | scope.$watch('toggles.inputActive',
345 | function (newVal) {
346 | if (newVal) {
347 | $timeout(function () {
348 | element[0].focus();
349 | });
350 | }
351 | });
352 |
353 | /**
354 | * Detects a paste or someone jamming on the delimiter key.
355 | */
356 | ngModel.$parsers.unshift(function (value) {
357 | var values = value.split(scope.options.delimiter);
358 | if (values.length > 1) {
359 | addTags(values);
360 | }
361 | if (value.match(delimiterRx)) {
362 | element.val('');
363 | return;
364 | }
365 | return value;
366 | });
367 |
368 | /**
369 | * Resets the input field if we selected something from typeahead.
370 | */
371 | ngModel.$formatters.push(function (tag) {
372 | if (tag && tag.value) {
373 | element.val('');
374 | return;
375 | }
376 | return tag;
377 | });
378 | }
379 | };
380 | }]);
381 |
382 | /**
383 | * Main directive
384 | */
385 | tags.directive('tags',
386 | ['$document', '$timeout', '$parse', 'decipherTagsOptions',
387 | function ($document, $timeout, $parse, decipherTagsOptions) {
388 |
389 | return {
390 | controller: 'TagsCtrl',
391 | restrict: 'E',
392 | replace: true,
393 | // IE8 is really, really fussy about this.
394 | template: '
',
395 | scope: {
396 | model: '='
397 | },
398 | link: function (scope, element, attrs) {
399 | var srcResult,
400 | source,
401 | tags,
402 | group,
403 | i,
404 | tagsWatch,
405 | srcWatch,
406 | modelWatch,
407 | model,
408 | pureStrings = false,
409 | stringArray = false,
410 | defaults = angular.copy(defaultOptions),
411 | userDefaults = angular.copy(decipherTagsOptions),
412 |
413 | /**
414 | * Parses the comprehension expression and gives us interesting bits.
415 | * @param input
416 | * @returns {{itemName: *, source: *, viewMapper: *, modelMapper: *}}
417 | */
418 | parse = function parse(input) {
419 | var match = input.match(SRC_REGEXP);
420 | if (!match) {
421 | throw new Error(
422 | "Expected src specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" +
423 | " but got '" + input + "'.");
424 | }
425 |
426 | return {
427 | itemName: match[3],
428 | source: $parse(match[4]),
429 | sourceName: match[4],
430 | viewMapper: $parse(match[2] || match[1]),
431 | modelMapper: $parse(match[1])
432 | };
433 |
434 | },
435 |
436 | watchModel = function watchModel() {
437 | modelWatch = scope.$watch('model', function (newVal) {
438 | var deletedTag, idx;
439 | if (angular.isDefined(newVal)) {
440 | tagsWatch();
441 | scope.tags = format(newVal);
442 |
443 | // remove already used tags
444 | i = scope.tags.length;
445 | while (i--) {
446 | scope._filterSrcTags(scope.tags[i]);
447 | }
448 |
449 | // restore any deleted things to the src array that happen to not
450 | // be in the new value.
451 | i = scope._deletedSrcTags.length;
452 | while (i--) {
453 | deletedTag = scope._deletedSrcTags[i];
454 | if (idx = newVal.indexOf(deletedTag) === -1 &&
455 | scope.srcTags.indexOf(deletedTag) === -1) {
456 | scope.srcTags.push(deletedTag);
457 | scope._deletedSrcTags.splice(i, 1);
458 | }
459 | }
460 |
461 | watchTags();
462 | }
463 | }, true);
464 |
465 | },
466 |
467 | watchTags = function watchTags() {
468 |
469 | /**
470 | * Watches tags for changes and propagates to outer model
471 | * in the format which we originally specified (see below)
472 | */
473 | tagsWatch = scope.$watch('tags', function (value, oldValue) {
474 | var i;
475 | if (value !== oldValue) {
476 | modelWatch();
477 | if (stringArray || pureStrings) {
478 | value = value.map(function (tag) {
479 | return tag.name;
480 | });
481 | if (angular.isArray(scope.model)) {
482 | scope.model.length = 0;
483 | for (i = 0; i < value.length; i++) {
484 | scope.model.push(value[i]);
485 | }
486 | }
487 | if (pureStrings) {
488 | scope.model = value.join(scope.options.delimiter);
489 | }
490 | }
491 | else {
492 | scope.model.length = 0;
493 | for (i = 0; i < value.length; i++) {
494 | scope.model.push(value[i]);
495 | }
496 | }
497 | watchModel();
498 |
499 | }
500 | }, true);
501 | },
502 | /**
503 | * Takes a raw model value and returns something suitable
504 | * to assign to scope.tags
505 | * @param value
506 | */
507 | format = function format(value) {
508 | var arr = [];
509 |
510 | if (angular.isUndefined(value)) {
511 | return;
512 | }
513 | if (angular.isString(value)) {
514 | arr = value
515 | .split(scope.options.delimiter)
516 | .map(function (item) {
517 | return {
518 | name: item.trim()
519 | };
520 | });
521 | }
522 | else if (angular.isArray(value)) {
523 | arr = value.map(function (item) {
524 | if (angular.isString(item)) {
525 | return {
526 | name: item.trim()
527 | };
528 | }
529 | else if (item.name) {
530 | item.name = item.name.trim();
531 | }
532 | return item;
533 | });
534 | }
535 | else if (angular.isDefined(value)) {
536 | throw 'list of tags must be an array or delimited string';
537 | }
538 | return arr;
539 | },
540 | /**
541 | * Updates the source tag information. Sets a watch so we
542 | * know if the source values change.
543 | */
544 | updateSrc = function updateSrc() {
545 | var locals,
546 | i,
547 | o,
548 | obj;
549 | // default to NOT letting users add new tags in this case.
550 | scope.options.addable = scope.options.addable || false;
551 | scope.srcTags = [];
552 | srcResult = parse(attrs.src);
553 | source = srcResult.source(scope.$parent);
554 | if (angular.isUndefined(source)) {
555 | return;
556 | }
557 | if (angular.isFunction(srcWatch)) {
558 | srcWatch();
559 | }
560 | locals = {};
561 | if (angular.isDefined(source)) {
562 | for (i = 0; i < source.length; i++) {
563 | locals[srcResult.itemName] = source[i];
564 | obj = {};
565 | obj.value = srcResult.modelMapper(scope.$parent, locals);
566 | o = {};
567 | if (angular.isObject(obj.value)) {
568 | o = angular.extend(obj.value, {
569 | name: srcResult.viewMapper(scope.$parent, locals),
570 | value: obj.value.value,
571 | group: obj.value.group
572 | });
573 | }
574 | else {
575 | o = {
576 | name: srcResult.viewMapper(scope.$parent, locals),
577 | value: obj.value,
578 | group: group
579 | };
580 | }
581 | scope.srcTags.push(o);
582 | }
583 | }
584 |
585 | srcWatch =
586 | scope.$parent.$watch(srcResult.sourceName,
587 | function (newVal, oldVal) {
588 | if (newVal !== oldVal) {
589 | updateSrc();
590 | }
591 | }, true);
592 | };
593 |
594 | // merge options
595 | scope.options = angular.extend(defaults,
596 | angular.extend(userDefaults, scope.$eval(attrs.options)));
597 | // break out orderBy for view
598 | scope.orderBy = scope.options.orderBy;
599 |
600 | // this should be named something else since it's just a collection
601 | // of random shit.
602 | scope.toggles = {
603 | inputActive: false
604 | };
605 |
606 | /**
607 | * When we receive this event, sort.
608 | */
609 | scope.$on('decipher.tags.sort', function (evt, data) {
610 | scope.orderBy = data;
611 | });
612 |
613 | // pass typeahead options through
614 | attrs.$observe('typeaheadOptions', function (newVal) {
615 | if (newVal) {
616 | scope.typeaheadOptions = $parse(newVal)(scope.$parent);
617 | } else {
618 | scope.typeaheadOptions = {};
619 | }
620 | });
621 |
622 | // determine what format we're in
623 | model = scope.model;
624 | if (angular.isString(model)) {
625 | pureStrings = true;
626 | }
627 | // XXX: avoid for now while fixing "empty array" bug
628 | else if (angular.isArray(model) && false) {
629 | stringArray = true;
630 | i = model.length;
631 | while (i--) {
632 | if (!angular.isString(model[i])) {
633 | stringArray = false;
634 | break;
635 | }
636 | }
637 | }
638 |
639 | // watch model for changes and update tags as appropriate
640 | scope.tags = [];
641 | scope._deletedSrcTags = [];
642 | watchTags();
643 | watchModel();
644 |
645 | // this stuff takes the parsed comprehension expression and
646 | // makes a srcTags array full of tag objects out of it.
647 | scope.srcTags = [];
648 | if (angular.isDefined(attrs.src)) {
649 | updateSrc();
650 | } else {
651 | // if you didn't specify a src, you must be able to type in new tags.
652 | scope.options.addable = true;
653 | }
654 |
655 | // emit identifier
656 | scope.$id = ++id;
657 | scope.$emit('decipher.tags.initialized', {
658 | $id: scope.$id,
659 | model: scope.model
660 | });
661 | }
662 | };
663 | }]);
664 |
665 | })();
666 |
--------------------------------------------------------------------------------
/dist/angular-tags-0.3.1-tpls.map.js:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"dist/angular-tags-0.3.1-tpls.min.js","sources":["generated/tags.js","generated/templates.js"],"names":["angular","module","e","tags","defaultOptions","delimiter","classes","SRC_REGEXP","kc","enter","esc","backspace","kcCompleteTag","kcRemoveTag","kcCancelInput","id","constant","controller","$scope","$timeout","$q","getClasses","tag","r","toggles","selectedTag","selected","forEach","options","klass","groupName","group","_filterSrcTags","idx","srcTags","indexOf","splice","_deletedSrcTags","push","reject","add","i","_add","inputTag","$emit","$id","fail","dfrd","defer","length","name","then","addable","resolve","promise","trust","$sce","trustAsHtml","selectArea","inputActive","remove","directive","$filter","$rootScope","restrict","require","link","scope","element","attrs","ngModel","delimiterRx","RegExp","cancel","$setViewValue","$render","addTag","value","match","addTags","removeLastTag","orderedTags","$viewValue","orderBy","bind","$$phase","$apply","evt","charCodeAt","which","isPropagationStopped","$watch","newVal","focus","$parsers","unshift","values","split","val","$formatters","$document","$parse","decipherTagsOptions","replace","template","model","srcResult","source","tagsWatch","srcWatch","modelWatch","pureStrings","stringArray","defaults","copy","userDefaults","parse","input","Error","itemName","sourceName","viewMapper","modelMapper","watchModel","deletedTag","isDefined","format","watchTags","oldValue","map","isArray","join","arr","isUndefined","isString","item","trim","updateSrc","locals","o","obj","src","$parent","isFunction","isObject","extend","oldVal","$eval","$on","data","$observe","typeaheadOptions","run","$templateCache","put"],"mappings":"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","sourceRoot":"/"}
--------------------------------------------------------------------------------
/dist/angular-tags-0.3.1-tpls.min.js:
--------------------------------------------------------------------------------
1 | !function(){"use strict";try{angular.module("decipher.tags.templates")}catch(a){angular.module("decipher.tags.templates",[])}var b=angular.module("decipher.tags",["ui.bootstrap.typeahead","decipher.tags.templates"]),c={delimiter:",",classes:{}},d=/^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/,e={enter:13,esc:27,backspace:8},f=[e.enter],g=[e.backspace],h=[e.esc],i=0;b.constant("decipherTagsOptions",{}),b.controller("TagsCtrl",["$scope","$timeout","$q",function(a,b,c){a.getClasses=function(b){var c={};return b===a.toggles.selectedTag&&(c.selected=!0),angular.forEach(a.options.classes,function(a,d){b.group===d&&(c[a]=!0)}),c},a._filterSrcTags=function(d){return b(function(){var b=a.srcTags.indexOf(d);return b>=0?(a.srcTags.splice(b,1),a._deletedSrcTags.push(d),void 0):c.reject()})},a.add=function(b){var d,e=function(b){a.tags.push(b),delete a.inputTag,a.$emit("decipher.tags.added",{tag:b,$id:a.$id})},f=function(){a.$emit("decipher.tags.addfailed",{tag:b,$id:a.$id}),g.reject()},g=c.defer();for(d=a.tags.length;d--;)a.tags[d].name===b.name&&f();return a._filterSrcTags(b).then(function(){e(b)},function(){a.options.addable?(e(b),g.resolve()):f()}),g.promise},a.trust=function(a){return $sce.trustAsHtml(a.name)},a.selectArea=function(){a.toggles.inputActive=!0},a.remove=function(b){var c;a.tags.splice(a.tags.indexOf(b),1),(c=a._deletedSrcTags.indexOf(b)>=0)&&(a._deletedSrcTags.splice(c,1),-1===a.srcTags.indexOf(b)&&a.srcTags.push(b)),delete a.toggles.selectedTag,a.$emit("decipher.tags.removed",{tag:b,$id:a.$id})}}]),b.directive("decipherTagsInput",["$timeout","$filter","$rootScope",function(a,b,c){return{restrict:"C",require:"ngModel",link:function(d,e,i,j){var k=new RegExp("^"+d.options.delimiter+"+$"),l=function(){j.$setViewValue(""),j.$render()},m=function(a){if(a){if(a.match(k))return l(),void 0;d.add({name:a})&&l()}},n=function(a){var b;for(b=0;b=0?m(j.$viewValue):h.indexOf(a.which)>=0&&!a.isPropagationStopped()?(l(),d.toggles.inputActive=!1):g.indexOf(a.which)>=0?o():(delete d.toggles.selectedTag,d.$emit("decipher.tags.keyup",{value:j.$viewValue,$id:d.$id}))})}),d.$watch("toggles.inputActive",function(b){b&&a(function(){e[0].focus()})}),j.$parsers.unshift(function(a){var b=a.split(d.options.delimiter);return b.length>1&&n(b),a.match(k)?(e.val(""),void 0):a}),j.$formatters.push(function(a){return a&&a.value?(e.val(""),void 0):a})}}}]),b.directive("tags",["$document","$timeout","$parse","decipherTagsOptions",function(a,b,e,f){return{controller:"TagsCtrl",restrict:"E",replace:!0,template:"
",scope:{model:"="},link:function(a,b,g){var h,j,k,l,m,n,o,p,q=!1,r=!1,s=angular.copy(c),t=angular.copy(f),u=function(a){var b=a.match(d);if(!b)throw new Error("Expected src specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_' but got '"+a+"'.");return{itemName:b[3],source:e(b[4]),sourceName:b[4],viewMapper:e(b[2]||b[1]),modelMapper:e(b[1])}},v=function(){o=a.$watch("model",function(b){var c,d;if(angular.isDefined(b)){for(m(),a.tags=x(b),l=a.tags.length;l--;)a._filterSrcTags(a.tags[l]);for(l=a._deletedSrcTags.length;l--;)c=a._deletedSrcTags[l],(d=-1===b.indexOf(c)&&-1===a.srcTags.indexOf(c))&&(a.srcTags.push(c),a._deletedSrcTags.splice(l,1));w()}},!0)},w=function(){m=a.$watch("tags",function(b,c){var d;if(b!==c){if(o(),r||q){if(b=b.map(function(a){return a.name}),angular.isArray(a.model))for(a.model.length=0,d=0;d\n\n
\n \n{{tag.name}}\n \n\n \n
\n\n \n \n \n \n\n \n\n')}]);
2 | //# sourceMappingURL=angular-tags-0.3.1-tpls.map.js
--------------------------------------------------------------------------------
/dist/angular-tags-0.3.1.css:
--------------------------------------------------------------------------------
1 | .decipher-tags {
2 | min-height: 24px;
3 | border: 1px solid #ccc;
4 | margin: 0px 10px;
5 | padding: 0px 5px 5px 5px;
6 | }
7 | .decipher-tags .decipher-tags-taglist {
8 | margin-bottom: 4px;
9 | display: inline;
10 | }
11 | .decipher-tags .decipher-tags-taglist .decipher-tags-tag {
12 | display: inline-block;
13 | border-radius: 5px;
14 | background-color: #EFEFEF;
15 | border: 1px solid #DDD;
16 | padding: 2px;
17 | margin-right: 5px;
18 | margin-top: 5px;
19 | }
20 | .decipher-tags .decipher-tags-taglist .decipher-tags-tag i {
21 | color: black;
22 | text-decoration: none;
23 | }
24 | .decipher-tags .decipher-tags-taglist .decipher-tags-tag.selected {
25 | border: 1px solid red;
26 | }
27 | .decipher-tags .decipher-tags-taglist .decipher-tags-tag:hover i {
28 | color: red;
29 | text-decoration: none;
30 | }
31 | .decipher-tags .decipher-tags-taglist .decipher-tags-tag i {
32 | cursor: pointer;
33 | }
34 | .decipher-tags .decipher-tags-input {
35 | border: none;
36 | outline: 0;
37 | }
38 | .decipher-tags .decipher-tags-invalid {
39 | color: #FFF;
40 | }
41 | .decipher-tags .decipher-tags-controls {
42 | margin-top: 8px;
43 | min-height: 28px;
44 | }
45 |
--------------------------------------------------------------------------------
/dist/angular-tags-0.3.1.js:
--------------------------------------------------------------------------------
1 | /*global angular*/
2 | (function () {
3 | 'use strict';
4 |
5 | try {
6 | angular.module('decipher.tags.templates');
7 | } catch (e) {
8 | angular.module('decipher.tags.templates', []);
9 | }
10 |
11 | var tags = angular.module('decipher.tags',
12 | ['ui.bootstrap.typeahead', 'decipher.tags.templates']);
13 |
14 | var defaultOptions = {
15 | delimiter: ',', // if given a string model, it splits on this
16 | classes: {} // obj of group names to classes
17 | },
18 |
19 | // for parsing comprehension expression
20 | SRC_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/,
21 |
22 | // keycodes
23 | kc = {
24 | enter: 13,
25 | esc: 27,
26 | backspace: 8
27 | },
28 | kcCompleteTag = [kc.enter],
29 | kcRemoveTag = [kc.backspace],
30 | kcCancelInput = [kc.esc],
31 | id = 0;
32 |
33 | tags.constant('decipherTagsOptions', {});
34 |
35 | /**
36 | * TODO: do we actually share functionality here? We're using this
37 | * controller on both the subdirective and its parent, but I'm not sure
38 | * if we actually use the same functions in both.
39 | */
40 | tags.controller('TagsCtrl',
41 | ['$scope', '$timeout', '$q', function ($scope, $timeout, $q) {
42 |
43 | /**
44 | * Figures out what classes to put on the tag span. It'll add classes
45 | * if defined by group, and it'll add a selected class if the tag
46 | * is preselected to delete.
47 | * @param tag
48 | * @returns {{}}
49 | */
50 | $scope.getClasses = function getGroupClass(tag) {
51 | var r = {};
52 |
53 | if (tag === $scope.toggles.selectedTag) {
54 | r.selected = true;
55 | }
56 | angular.forEach($scope.options.classes, function (klass, groupName) {
57 | if (tag.group === groupName) {
58 | r[klass] = true;
59 | }
60 | });
61 | return r;
62 | };
63 |
64 | /**
65 | * Finds a tag in the src list and removes it.
66 | * @param tag
67 | * @returns {boolean}
68 | */
69 | $scope._filterSrcTags = function filterSrcTags(tag) {
70 | // wrapped in timeout or typeahead becomes confused
71 | return $timeout(function () {
72 | var idx = $scope.srcTags.indexOf(tag);
73 | if (idx >= 0) {
74 | $scope.srcTags.splice(idx, 1);
75 | $scope._deletedSrcTags.push(tag);
76 | return;
77 | }
78 | return $q.reject();
79 | });
80 | };
81 |
82 | /**
83 | * Adds a tag to the list of tags, and if in the typeahead list,
84 | * removes it from that list (and saves it). emits decipher.tags.added
85 | * @param tag
86 | */
87 | $scope.add = function add(tag) {
88 | var _add = function _add(tag) {
89 | $scope.tags.push(tag);
90 | delete $scope.inputTag;
91 | $scope.$emit('decipher.tags.added', {
92 | tag: tag,
93 | $id: $scope.$id
94 | });
95 | },
96 | fail = function fail() {
97 | $scope.$emit('decipher.tags.addfailed', {
98 | tag: tag,
99 | $id: $scope.$id
100 | });
101 | dfrd.reject();
102 | },
103 | i,
104 | dfrd = $q.defer();
105 |
106 | // don't add dupe names
107 | i = $scope.tags.length;
108 | while (i--) {
109 | if ($scope.tags[i].name === tag.name) {
110 | fail();
111 | }
112 | }
113 |
114 | $scope._filterSrcTags(tag)
115 | .then(function () {
116 | _add(tag);
117 | }, function () {
118 | if ($scope.options.addable) {
119 | _add(tag);
120 | dfrd.resolve();
121 | }
122 | else {
123 | fail();
124 | }
125 | });
126 |
127 | return dfrd.promise;
128 | };
129 |
130 | $scope.trust = function(tag) {
131 | return $sce.trustAsHtml(tag.name);
132 | };
133 | /**
134 | * Toggle the input box active.
135 | */
136 | $scope.selectArea = function selectArea() {
137 | $scope.toggles.inputActive = true;
138 | };
139 |
140 | /**
141 | * Removes a tag. Restores stuff into srcTags if it came from there.
142 | * Kills any selected tag. Emit a decipher.tags.removed event.
143 | * @param tag
144 | */
145 | $scope.remove = function remove(tag) {
146 | var idx;
147 | $scope.tags.splice($scope.tags.indexOf(tag), 1);
148 |
149 | if (idx = $scope._deletedSrcTags.indexOf(tag) >= 0) {
150 | $scope._deletedSrcTags.splice(idx, 1);
151 | if ($scope.srcTags.indexOf(tag) === -1) {
152 | $scope.srcTags.push(tag);
153 | }
154 | }
155 |
156 | delete $scope.toggles.selectedTag;
157 |
158 | $scope.$emit('decipher.tags.removed', {
159 | tag: tag,
160 | $id: $scope.$id
161 | });
162 | };
163 |
164 | }]);
165 |
166 | /**
167 | * Directive for the 'input' tag itself, which is of class
168 | * decipher-tags-input.
169 | */
170 | tags.directive('decipherTagsInput',
171 | ['$timeout', '$filter', '$rootScope',
172 | function ($timeout, $filter, $rootScope) {
173 | return {
174 | restrict: 'C',
175 | require: 'ngModel',
176 | link: function (scope, element, attrs, ngModel) {
177 | var delimiterRx = new RegExp('^' +
178 | scope.options.delimiter +
179 | '+$'),
180 |
181 | /**
182 | * Cancels the text input box.
183 | */
184 | cancel = function cancel() {
185 | ngModel.$setViewValue('');
186 | ngModel.$render();
187 | },
188 |
189 | /**
190 | * Adds a tag you typed/pasted in unless it's a bunch of delimiters.
191 | * @param value
192 | */
193 | addTag = function addTag(value) {
194 | if (value) {
195 | if (value.match(delimiterRx)) {
196 | cancel();
197 | return;
198 | }
199 | if (scope.add({
200 | name: value
201 | })) {
202 | cancel();
203 | }
204 | }
205 | },
206 |
207 | /**
208 | * Adds multiple tags in case you pasted them.
209 | * @param tags
210 | */
211 | addTags = function (tags) {
212 | var i;
213 | for (i = 0; i < tags.length;
214 | i++) {
215 | addTag(tags[i]);
216 | }
217 | },
218 |
219 | /**
220 | * Backspace one to select, and a second time to delete.
221 | */
222 | removeLastTag = function removeLastTag() {
223 | var orderedTags;
224 | if (scope.toggles.selectedTag) {
225 | scope.remove(scope.toggles.selectedTag);
226 | delete scope.toggles.selectedTag;
227 | }
228 | // only do this if the input field is empty.
229 | else if (!ngModel.$viewValue) {
230 | orderedTags =
231 | $filter('orderBy')(scope.tags,
232 | scope.orderBy);
233 | scope.toggles.selectedTag =
234 | orderedTags[orderedTags.length - 1];
235 | }
236 | };
237 |
238 | /**
239 | * When we focus the text input area, drop the selected tag
240 | */
241 | element.bind('focus', function () {
242 | // this avoids what looks like a bug in typeahead. It seems
243 | // to be calling element[0].focus() somewhere within a digest loop.
244 | if ($rootScope.$$phase) {
245 | delete scope.toggles.selectedTag;
246 | } else {
247 | scope.$apply(function () {
248 | delete scope.toggles.selectedTag;
249 | });
250 | }
251 | });
252 |
253 | /**
254 | * Detects the delimiter.
255 | */
256 | element.bind('keypress',
257 | function (evt) {
258 | scope.$apply(function () {
259 | if (scope.options.delimiter.charCodeAt() ===
260 | evt.which) {
261 | addTag(ngModel.$viewValue);
262 | }
263 | });
264 | });
265 |
266 | /**
267 | * Inspects whatever you typed to see if there were character(s) of
268 | * concern.
269 | */
270 | element.bind('keydown',
271 | function (evt) {
272 | scope.$apply(function () {
273 | // to "complete" a tag
274 |
275 | if (kcCompleteTag.indexOf(evt.which) >=
276 | 0) {
277 | addTag(ngModel.$viewValue);
278 |
279 | // or if you want to get out of the text area
280 | } else if (kcCancelInput.indexOf(evt.which) >=
281 | 0 && !evt.isPropagationStopped()) {
282 | cancel();
283 | scope.toggles.inputActive =
284 | false;
285 |
286 | // or if you're trying to delete something
287 | } else if (kcRemoveTag.indexOf(evt.which) >=
288 | 0) {
289 | removeLastTag();
290 |
291 | // otherwise if we're typing in here, just drop the selected tag.
292 | } else {
293 | delete scope.toggles.selectedTag;
294 | scope.$emit('decipher.tags.keyup',
295 | {
296 | value: ngModel.$viewValue,
297 | $id: scope.$id
298 | });
299 | }
300 | });
301 | });
302 |
303 | /**
304 | * When inputActive toggle changes to true, focus the input.
305 | * And no I have no idea why this has to be in a timeout.
306 | */
307 | scope.$watch('toggles.inputActive',
308 | function (newVal) {
309 | if (newVal) {
310 | $timeout(function () {
311 | element[0].focus();
312 | });
313 | }
314 | });
315 |
316 | /**
317 | * Detects a paste or someone jamming on the delimiter key.
318 | */
319 | ngModel.$parsers.unshift(function (value) {
320 | var values = value.split(scope.options.delimiter);
321 | if (values.length > 1) {
322 | addTags(values);
323 | }
324 | if (value.match(delimiterRx)) {
325 | element.val('');
326 | return;
327 | }
328 | return value;
329 | });
330 |
331 | /**
332 | * Resets the input field if we selected something from typeahead.
333 | */
334 | ngModel.$formatters.push(function (tag) {
335 | if (tag && tag.value) {
336 | element.val('');
337 | return;
338 | }
339 | return tag;
340 | });
341 | }
342 | };
343 | }]);
344 |
345 | /**
346 | * Main directive
347 | */
348 | tags.directive('tags',
349 | ['$document', '$timeout', '$parse', 'decipherTagsOptions',
350 | function ($document, $timeout, $parse, decipherTagsOptions) {
351 |
352 | return {
353 | controller: 'TagsCtrl',
354 | restrict: 'E',
355 | replace: true,
356 | // IE8 is really, really fussy about this.
357 | template: '
',
358 | scope: {
359 | model: '='
360 | },
361 | link: function (scope, element, attrs) {
362 | var srcResult,
363 | source,
364 | tags,
365 | group,
366 | i,
367 | tagsWatch,
368 | srcWatch,
369 | modelWatch,
370 | model,
371 | pureStrings = false,
372 | stringArray = false,
373 | defaults = angular.copy(defaultOptions),
374 | userDefaults = angular.copy(decipherTagsOptions),
375 |
376 | /**
377 | * Parses the comprehension expression and gives us interesting bits.
378 | * @param input
379 | * @returns {{itemName: *, source: *, viewMapper: *, modelMapper: *}}
380 | */
381 | parse = function parse(input) {
382 | var match = input.match(SRC_REGEXP);
383 | if (!match) {
384 | throw new Error(
385 | "Expected src specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" +
386 | " but got '" + input + "'.");
387 | }
388 |
389 | return {
390 | itemName: match[3],
391 | source: $parse(match[4]),
392 | sourceName: match[4],
393 | viewMapper: $parse(match[2] || match[1]),
394 | modelMapper: $parse(match[1])
395 | };
396 |
397 | },
398 |
399 | watchModel = function watchModel() {
400 | modelWatch = scope.$watch('model', function (newVal) {
401 | var deletedTag, idx;
402 | if (angular.isDefined(newVal)) {
403 | tagsWatch();
404 | scope.tags = format(newVal);
405 |
406 | // remove already used tags
407 | i = scope.tags.length;
408 | while (i--) {
409 | scope._filterSrcTags(scope.tags[i]);
410 | }
411 |
412 | // restore any deleted things to the src array that happen to not
413 | // be in the new value.
414 | i = scope._deletedSrcTags.length;
415 | while (i--) {
416 | deletedTag = scope._deletedSrcTags[i];
417 | if (idx = newVal.indexOf(deletedTag) === -1 &&
418 | scope.srcTags.indexOf(deletedTag) === -1) {
419 | scope.srcTags.push(deletedTag);
420 | scope._deletedSrcTags.splice(i, 1);
421 | }
422 | }
423 |
424 | watchTags();
425 | }
426 | }, true);
427 |
428 | },
429 |
430 | watchTags = function watchTags() {
431 |
432 | /**
433 | * Watches tags for changes and propagates to outer model
434 | * in the format which we originally specified (see below)
435 | */
436 | tagsWatch = scope.$watch('tags', function (value, oldValue) {
437 | var i;
438 | if (value !== oldValue) {
439 | modelWatch();
440 | if (stringArray || pureStrings) {
441 | value = value.map(function (tag) {
442 | return tag.name;
443 | });
444 | if (angular.isArray(scope.model)) {
445 | scope.model.length = 0;
446 | for (i = 0; i < value.length; i++) {
447 | scope.model.push(value[i]);
448 | }
449 | }
450 | if (pureStrings) {
451 | scope.model = value.join(scope.options.delimiter);
452 | }
453 | }
454 | else {
455 | scope.model.length = 0;
456 | for (i = 0; i < value.length; i++) {
457 | scope.model.push(value[i]);
458 | }
459 | }
460 | watchModel();
461 |
462 | }
463 | }, true);
464 | },
465 | /**
466 | * Takes a raw model value and returns something suitable
467 | * to assign to scope.tags
468 | * @param value
469 | */
470 | format = function format(value) {
471 | var arr = [];
472 |
473 | if (angular.isUndefined(value)) {
474 | return;
475 | }
476 | if (angular.isString(value)) {
477 | arr = value
478 | .split(scope.options.delimiter)
479 | .map(function (item) {
480 | return {
481 | name: item.trim()
482 | };
483 | });
484 | }
485 | else if (angular.isArray(value)) {
486 | arr = value.map(function (item) {
487 | if (angular.isString(item)) {
488 | return {
489 | name: item.trim()
490 | };
491 | }
492 | else if (item.name) {
493 | item.name = item.name.trim();
494 | }
495 | return item;
496 | });
497 | }
498 | else if (angular.isDefined(value)) {
499 | throw 'list of tags must be an array or delimited string';
500 | }
501 | return arr;
502 | },
503 | /**
504 | * Updates the source tag information. Sets a watch so we
505 | * know if the source values change.
506 | */
507 | updateSrc = function updateSrc() {
508 | var locals,
509 | i,
510 | o,
511 | obj;
512 | // default to NOT letting users add new tags in this case.
513 | scope.options.addable = scope.options.addable || false;
514 | scope.srcTags = [];
515 | srcResult = parse(attrs.src);
516 | source = srcResult.source(scope.$parent);
517 | if (angular.isUndefined(source)) {
518 | return;
519 | }
520 | if (angular.isFunction(srcWatch)) {
521 | srcWatch();
522 | }
523 | locals = {};
524 | if (angular.isDefined(source)) {
525 | for (i = 0; i < source.length; i++) {
526 | locals[srcResult.itemName] = source[i];
527 | obj = {};
528 | obj.value = srcResult.modelMapper(scope.$parent, locals);
529 | o = {};
530 | if (angular.isObject(obj.value)) {
531 | o = angular.extend(obj.value, {
532 | name: srcResult.viewMapper(scope.$parent, locals),
533 | value: obj.value.value,
534 | group: obj.value.group
535 | });
536 | }
537 | else {
538 | o = {
539 | name: srcResult.viewMapper(scope.$parent, locals),
540 | value: obj.value,
541 | group: group
542 | };
543 | }
544 | scope.srcTags.push(o);
545 | }
546 | }
547 |
548 | srcWatch =
549 | scope.$parent.$watch(srcResult.sourceName,
550 | function (newVal, oldVal) {
551 | if (newVal !== oldVal) {
552 | updateSrc();
553 | }
554 | }, true);
555 | };
556 |
557 | // merge options
558 | scope.options = angular.extend(defaults,
559 | angular.extend(userDefaults, scope.$eval(attrs.options)));
560 | // break out orderBy for view
561 | scope.orderBy = scope.options.orderBy;
562 |
563 | // this should be named something else since it's just a collection
564 | // of random shit.
565 | scope.toggles = {
566 | inputActive: false
567 | };
568 |
569 | /**
570 | * When we receive this event, sort.
571 | */
572 | scope.$on('decipher.tags.sort', function (evt, data) {
573 | scope.orderBy = data;
574 | });
575 |
576 | // pass typeahead options through
577 | attrs.$observe('typeaheadOptions', function (newVal) {
578 | if (newVal) {
579 | scope.typeaheadOptions = $parse(newVal)(scope.$parent);
580 | } else {
581 | scope.typeaheadOptions = {};
582 | }
583 | });
584 |
585 | // determine what format we're in
586 | model = scope.model;
587 | if (angular.isString(model)) {
588 | pureStrings = true;
589 | }
590 | // XXX: avoid for now while fixing "empty array" bug
591 | else if (angular.isArray(model) && false) {
592 | stringArray = true;
593 | i = model.length;
594 | while (i--) {
595 | if (!angular.isString(model[i])) {
596 | stringArray = false;
597 | break;
598 | }
599 | }
600 | }
601 |
602 | // watch model for changes and update tags as appropriate
603 | scope.tags = [];
604 | scope._deletedSrcTags = [];
605 | watchTags();
606 | watchModel();
607 |
608 | // this stuff takes the parsed comprehension expression and
609 | // makes a srcTags array full of tag objects out of it.
610 | scope.srcTags = [];
611 | if (angular.isDefined(attrs.src)) {
612 | updateSrc();
613 | } else {
614 | // if you didn't specify a src, you must be able to type in new tags.
615 | scope.options.addable = true;
616 | }
617 |
618 | // emit identifier
619 | scope.$id = ++id;
620 | scope.$emit('decipher.tags.initialized', {
621 | $id: scope.$id,
622 | model: scope.model
623 | });
624 | }
625 | };
626 | }]);
627 |
628 | })();
629 |
--------------------------------------------------------------------------------
/dist/angular-tags-0.3.1.less:
--------------------------------------------------------------------------------
1 | .decipher-tags {
2 |
3 | min-height: 24px;
4 | border: 1px solid #ccc;
5 | margin: 0px 10px;
6 | padding: 0px 5px 5px 5px;
7 |
8 | .decipher-tags-taglist {
9 | margin-bottom: 4px;
10 | display: inline;
11 |
12 | .decipher-tags-tag {
13 | display: inline-block;
14 | border-radius: 5px;
15 | background-color: #EFEFEF;
16 | border: 1px solid #DDD;
17 | padding: 2px;
18 | margin-right: 5px;
19 | margin-top: 5px;
20 | }
21 |
22 | .decipher-tags-tag i {
23 | color: black;
24 | text-decoration: none;
25 | }
26 |
27 | .decipher-tags-tag.selected
28 | {
29 | border: 1px solid red;
30 | }
31 |
32 | .decipher-tags-tag:hover i {
33 | color: red;
34 |
35 | text-decoration: none;
36 | }
37 |
38 | .decipher-tags-tag i {
39 | cursor: pointer;
40 | }
41 |
42 | }
43 |
44 | .decipher-tags-input {
45 | border: none;
46 | outline: 0;
47 | }
48 |
49 | .decipher-tags-invalid {
50 | color: #FFF;
51 | }
52 |
53 | .decipher-tags-controls {
54 | margin-top: 8px;
55 | min-height: 28px;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/dist/angular-tags-0.3.1.map.js:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"dist/angular-tags-0.3.1.min.js","sources":["generated/tags.js"],"names":["angular","module","e","tags","defaultOptions","delimiter","classes","SRC_REGEXP","kc","enter","esc","backspace","kcCompleteTag","kcRemoveTag","kcCancelInput","id","constant","controller","$scope","$timeout","$q","getClasses","tag","r","toggles","selectedTag","selected","forEach","options","klass","groupName","group","_filterSrcTags","idx","srcTags","indexOf","splice","_deletedSrcTags","push","reject","add","i","_add","inputTag","$emit","$id","fail","dfrd","defer","length","name","then","addable","resolve","promise","trust","$sce","trustAsHtml","selectArea","inputActive","remove","directive","$filter","$rootScope","restrict","require","link","scope","element","attrs","ngModel","delimiterRx","RegExp","cancel","$setViewValue","$render","addTag","value","match","addTags","removeLastTag","orderedTags","$viewValue","orderBy","bind","$$phase","$apply","evt","charCodeAt","which","isPropagationStopped","$watch","newVal","focus","$parsers","unshift","values","split","val","$formatters","$document","$parse","decipherTagsOptions","replace","template","model","srcResult","source","tagsWatch","srcWatch","modelWatch","pureStrings","stringArray","defaults","copy","userDefaults","parse","input","Error","itemName","sourceName","viewMapper","modelMapper","watchModel","deletedTag","isDefined","format","watchTags","oldValue","map","isArray","join","arr","isUndefined","isString","item","trim","updateSrc","locals","o","obj","src","$parent","isFunction","isObject","extend","oldVal","$eval","$on","data","$observe","typeaheadOptions"],"mappings":"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","sourceRoot":"/"}
--------------------------------------------------------------------------------
/dist/angular-tags-0.3.1.min.js:
--------------------------------------------------------------------------------
1 | !function(){"use strict";try{angular.module("decipher.tags.templates")}catch(a){angular.module("decipher.tags.templates",[])}var b=angular.module("decipher.tags",["ui.bootstrap.typeahead","decipher.tags.templates"]),c={delimiter:",",classes:{}},d=/^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/,e={enter:13,esc:27,backspace:8},f=[e.enter],g=[e.backspace],h=[e.esc],i=0;b.constant("decipherTagsOptions",{}),b.controller("TagsCtrl",["$scope","$timeout","$q",function(a,b,c){a.getClasses=function(b){var c={};return b===a.toggles.selectedTag&&(c.selected=!0),angular.forEach(a.options.classes,function(a,d){b.group===d&&(c[a]=!0)}),c},a._filterSrcTags=function(d){return b(function(){var b=a.srcTags.indexOf(d);return b>=0?(a.srcTags.splice(b,1),a._deletedSrcTags.push(d),void 0):c.reject()})},a.add=function(b){var d,e=function(b){a.tags.push(b),delete a.inputTag,a.$emit("decipher.tags.added",{tag:b,$id:a.$id})},f=function(){a.$emit("decipher.tags.addfailed",{tag:b,$id:a.$id}),g.reject()},g=c.defer();for(d=a.tags.length;d--;)a.tags[d].name===b.name&&f();return a._filterSrcTags(b).then(function(){e(b)},function(){a.options.addable?(e(b),g.resolve()):f()}),g.promise},a.trust=function(a){return $sce.trustAsHtml(a.name)},a.selectArea=function(){a.toggles.inputActive=!0},a.remove=function(b){var c;a.tags.splice(a.tags.indexOf(b),1),(c=a._deletedSrcTags.indexOf(b)>=0)&&(a._deletedSrcTags.splice(c,1),-1===a.srcTags.indexOf(b)&&a.srcTags.push(b)),delete a.toggles.selectedTag,a.$emit("decipher.tags.removed",{tag:b,$id:a.$id})}}]),b.directive("decipherTagsInput",["$timeout","$filter","$rootScope",function(a,b,c){return{restrict:"C",require:"ngModel",link:function(d,e,i,j){var k=new RegExp("^"+d.options.delimiter+"+$"),l=function(){j.$setViewValue(""),j.$render()},m=function(a){if(a){if(a.match(k))return l(),void 0;d.add({name:a})&&l()}},n=function(a){var b;for(b=0;b=0?m(j.$viewValue):h.indexOf(a.which)>=0&&!a.isPropagationStopped()?(l(),d.toggles.inputActive=!1):g.indexOf(a.which)>=0?o():(delete d.toggles.selectedTag,d.$emit("decipher.tags.keyup",{value:j.$viewValue,$id:d.$id}))})}),d.$watch("toggles.inputActive",function(b){b&&a(function(){e[0].focus()})}),j.$parsers.unshift(function(a){var b=a.split(d.options.delimiter);return b.length>1&&n(b),a.match(k)?(e.val(""),void 0):a}),j.$formatters.push(function(a){return a&&a.value?(e.val(""),void 0):a})}}}]),b.directive("tags",["$document","$timeout","$parse","decipherTagsOptions",function(a,b,e,f){return{controller:"TagsCtrl",restrict:"E",replace:!0,template:"
",scope:{model:"="},link:function(a,b,g){var h,j,k,l,m,n,o,p,q=!1,r=!1,s=angular.copy(c),t=angular.copy(f),u=function(a){var b=a.match(d);if(!b)throw new Error("Expected src specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_' but got '"+a+"'.");return{itemName:b[3],source:e(b[4]),sourceName:b[4],viewMapper:e(b[2]||b[1]),modelMapper:e(b[1])}},v=function(){o=a.$watch("model",function(b){var c,d;if(angular.isDefined(b)){for(m(),a.tags=x(b),l=a.tags.length;l--;)a._filterSrcTags(a.tags[l]);for(l=a._deletedSrcTags.length;l--;)c=a._deletedSrcTags[l],(d=-1===b.indexOf(c)&&-1===a.srcTags.indexOf(c))&&(a.srcTags.push(c),a._deletedSrcTags.splice(l,1));w()}},!0)},w=function(){m=a.$watch("tags",function(b,c){var d;if(b!==c){if(o(),r||q){if(b=b.map(function(a){return a.name}),angular.isArray(a.model))for(a.model.length=0,d=0;d
2 |
3 |
4 |
6 | {{tag.name}}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
15 |
17 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/less/tags.less:
--------------------------------------------------------------------------------
1 | .decipher-tags {
2 |
3 | min-height: 24px;
4 | border: 1px solid #ccc;
5 | margin: 0px 10px;
6 | padding: 0px 5px 5px 5px;
7 |
8 | .decipher-tags-taglist {
9 | margin-bottom: 4px;
10 | display: inline;
11 |
12 | .decipher-tags-tag {
13 | display: inline-block;
14 | border-radius: 5px;
15 | background-color: #EFEFEF;
16 | border: 1px solid #DDD;
17 | padding: 2px;
18 | margin-right: 5px;
19 | margin-top: 5px;
20 | }
21 |
22 | .decipher-tags-tag i {
23 | color: black;
24 | text-decoration: none;
25 | }
26 |
27 | .decipher-tags-tag.selected
28 | {
29 | border: 1px solid red;
30 | }
31 |
32 | .decipher-tags-tag:hover i {
33 | color: red;
34 |
35 | text-decoration: none;
36 | }
37 |
38 | .decipher-tags-tag i {
39 | cursor: pointer;
40 | }
41 |
42 | }
43 |
44 | .decipher-tags-input {
45 | border: none;
46 | outline: 0;
47 | }
48 |
49 | .decipher-tags-invalid {
50 | color: #FFF;
51 | }
52 |
53 | .decipher-tags-controls {
54 | margin-top: 8px;
55 | min-height: 28px;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-tags",
3 | "version": "0.3.1",
4 | "repository": "git://github.com/decipherinc/angular-tags.git",
5 | "devDependencies": {
6 | "grunt-contrib-connect": "~0.4.1",
7 | "grunt-contrib-qunit": "~0.2.2",
8 | "grunt-bower-task": "~0.3.1",
9 | "grunt-contrib-watch": "~0.5.3",
10 | "grunt-cli": "~0.1.9",
11 | "grunt-html2js": "~0.1.7",
12 | "grunt-contrib-less": "~0.7.0",
13 | "grunt-contrib-uglify": "~0.2.4",
14 | "grunt-ngmin": "0.0.3",
15 | "grunt-contrib-concat": "~0.3.0",
16 | "grunt-contrib-copy": "~0.4.1"
17 | },
18 | "scripts": {
19 | "test": "grunt test"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/tags.js:
--------------------------------------------------------------------------------
1 | /*global angular*/
2 | (function () {
3 | 'use strict';
4 |
5 | try {
6 | angular.module('decipher.tags.templates');
7 | } catch (e) {
8 | angular.module('decipher.tags.templates', []);
9 | }
10 |
11 | var tags = angular.module('decipher.tags',
12 | ['ui.bootstrap.typeahead', 'decipher.tags.templates']);
13 |
14 | var defaultOptions = {
15 | delimiter: ',', // if given a string model, it splits on this
16 | classes: {} // obj of group names to classes
17 | },
18 |
19 | // for parsing comprehension expression
20 | SRC_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/,
21 |
22 | // keycodes
23 | kc = {
24 | enter: 13,
25 | esc: 27,
26 | backspace: 8
27 | },
28 | kcCompleteTag = [kc.enter],
29 | kcRemoveTag = [kc.backspace],
30 | kcCancelInput = [kc.esc],
31 | id = 0;
32 |
33 | tags.constant('decipherTagsOptions', {});
34 |
35 | /**
36 | * TODO: do we actually share functionality here? We're using this
37 | * controller on both the subdirective and its parent, but I'm not sure
38 | * if we actually use the same functions in both.
39 | */
40 | tags.controller('TagsCtrl',
41 | ['$scope', '$timeout', '$q', function ($scope, $timeout, $q) {
42 |
43 | /**
44 | * Figures out what classes to put on the tag span. It'll add classes
45 | * if defined by group, and it'll add a selected class if the tag
46 | * is preselected to delete.
47 | * @param tag
48 | * @returns {{}}
49 | */
50 | $scope.getClasses = function getGroupClass(tag) {
51 | var r = {};
52 |
53 | if (tag === $scope.toggles.selectedTag) {
54 | r.selected = true;
55 | }
56 | angular.forEach($scope.options.classes, function (klass, groupName) {
57 | if (tag.group === groupName) {
58 | r[klass] = true;
59 | }
60 | });
61 | return r;
62 | };
63 |
64 | /**
65 | * Finds a tag in the src list and removes it.
66 | * @param tag
67 | * @returns {boolean}
68 | */
69 | $scope._filterSrcTags = function filterSrcTags(tag) {
70 | // wrapped in timeout or typeahead becomes confused
71 | return $timeout(function () {
72 | var idx = $scope.srcTags.indexOf(tag);
73 | if (idx >= 0) {
74 | $scope.srcTags.splice(idx, 1);
75 | $scope._deletedSrcTags.push(tag);
76 | return;
77 | }
78 | return $q.reject();
79 | });
80 | };
81 |
82 | /**
83 | * Adds a tag to the list of tags, and if in the typeahead list,
84 | * removes it from that list (and saves it). emits decipher.tags.added
85 | * @param tag
86 | */
87 | $scope.add = function add(tag) {
88 | var _add = function _add(tag) {
89 | $scope.tags.push(tag);
90 | delete $scope.inputTag;
91 | $scope.$emit('decipher.tags.added', {
92 | tag: tag,
93 | $id: $scope.$id
94 | });
95 | },
96 | fail = function fail() {
97 | $scope.$emit('decipher.tags.addfailed', {
98 | tag: tag,
99 | $id: $scope.$id
100 | });
101 | dfrd.reject();
102 | },
103 | i,
104 | dfrd = $q.defer();
105 |
106 | // don't add dupe names
107 | i = $scope.tags.length;
108 | while (i--) {
109 | if ($scope.tags[i].name === tag.name) {
110 | fail();
111 | }
112 | }
113 |
114 | $scope._filterSrcTags(tag)
115 | .then(function () {
116 | _add(tag);
117 | }, function () {
118 | if ($scope.options.addable) {
119 | _add(tag);
120 | dfrd.resolve();
121 | }
122 | else {
123 | fail();
124 | }
125 | });
126 |
127 | return dfrd.promise;
128 | };
129 |
130 | $scope.trust = function(tag) {
131 | return $sce.trustAsHtml(tag.name);
132 | };
133 | /**
134 | * Toggle the input box active.
135 | */
136 | $scope.selectArea = function selectArea() {
137 | $scope.toggles.inputActive = true;
138 | };
139 |
140 | /**
141 | * Removes a tag. Restores stuff into srcTags if it came from there.
142 | * Kills any selected tag. Emit a decipher.tags.removed event.
143 | * @param tag
144 | */
145 | $scope.remove = function remove(tag) {
146 | var idx;
147 | $scope.tags.splice($scope.tags.indexOf(tag), 1);
148 |
149 | if (idx = $scope._deletedSrcTags.indexOf(tag) >= 0) {
150 | $scope._deletedSrcTags.splice(idx, 1);
151 | if ($scope.srcTags.indexOf(tag) === -1) {
152 | $scope.srcTags.push(tag);
153 | }
154 | }
155 |
156 | delete $scope.toggles.selectedTag;
157 |
158 | $scope.$emit('decipher.tags.removed', {
159 | tag: tag,
160 | $id: $scope.$id
161 | });
162 | };
163 |
164 | }]);
165 |
166 | /**
167 | * Directive for the 'input' tag itself, which is of class
168 | * decipher-tags-input.
169 | */
170 | tags.directive('decipherTagsInput',
171 | ['$timeout', '$filter', '$rootScope',
172 | function ($timeout, $filter, $rootScope) {
173 | return {
174 | restrict: 'C',
175 | require: 'ngModel',
176 | link: function (scope, element, attrs, ngModel) {
177 | var delimiterRx = new RegExp('^' +
178 | scope.options.delimiter +
179 | '+$'),
180 |
181 | /**
182 | * Cancels the text input box.
183 | */
184 | cancel = function cancel() {
185 | ngModel.$setViewValue('');
186 | ngModel.$render();
187 | },
188 |
189 | /**
190 | * Adds a tag you typed/pasted in unless it's a bunch of delimiters.
191 | * @param value
192 | */
193 | addTag = function addTag(value) {
194 | if (value) {
195 | if (value.match(delimiterRx)) {
196 | cancel();
197 | return;
198 | }
199 | if (scope.add({
200 | name: value
201 | })) {
202 | cancel();
203 | }
204 | }
205 | },
206 |
207 | /**
208 | * Adds multiple tags in case you pasted them.
209 | * @param tags
210 | */
211 | addTags = function (tags) {
212 | var i;
213 | for (i = 0; i < tags.length;
214 | i++) {
215 | addTag(tags[i]);
216 | }
217 | },
218 |
219 | /**
220 | * Backspace one to select, and a second time to delete.
221 | */
222 | removeLastTag = function removeLastTag() {
223 | var orderedTags;
224 | if (scope.toggles.selectedTag) {
225 | scope.remove(scope.toggles.selectedTag);
226 | delete scope.toggles.selectedTag;
227 | }
228 | // only do this if the input field is empty.
229 | else if (!ngModel.$viewValue) {
230 | orderedTags =
231 | $filter('orderBy')(scope.tags,
232 | scope.orderBy);
233 | scope.toggles.selectedTag =
234 | orderedTags[orderedTags.length - 1];
235 | }
236 | };
237 |
238 | /**
239 | * When we focus the text input area, drop the selected tag
240 | */
241 | element.bind('focus', function () {
242 | // this avoids what looks like a bug in typeahead. It seems
243 | // to be calling element[0].focus() somewhere within a digest loop.
244 | if ($rootScope.$$phase) {
245 | delete scope.toggles.selectedTag;
246 | } else {
247 | scope.$apply(function () {
248 | delete scope.toggles.selectedTag;
249 | });
250 | }
251 | });
252 |
253 | /**
254 | * Detects the delimiter.
255 | */
256 | element.bind('keypress',
257 | function (evt) {
258 | scope.$apply(function () {
259 | if (scope.options.delimiter.charCodeAt() ===
260 | evt.which) {
261 | addTag(ngModel.$viewValue);
262 | }
263 | });
264 | });
265 |
266 | /**
267 | * Inspects whatever you typed to see if there were character(s) of
268 | * concern.
269 | */
270 | element.bind('keydown',
271 | function (evt) {
272 | scope.$apply(function () {
273 | // to "complete" a tag
274 |
275 | if (kcCompleteTag.indexOf(evt.which) >=
276 | 0) {
277 | addTag(ngModel.$viewValue);
278 |
279 | // or if you want to get out of the text area
280 | } else if (kcCancelInput.indexOf(evt.which) >=
281 | 0 && !evt.isPropagationStopped()) {
282 | cancel();
283 | scope.toggles.inputActive =
284 | false;
285 |
286 | // or if you're trying to delete something
287 | } else if (kcRemoveTag.indexOf(evt.which) >=
288 | 0) {
289 | removeLastTag();
290 |
291 | // otherwise if we're typing in here, just drop the selected tag.
292 | } else {
293 | delete scope.toggles.selectedTag;
294 | scope.$emit('decipher.tags.keyup',
295 | {
296 | value: ngModel.$viewValue,
297 | $id: scope.$id
298 | });
299 | }
300 | });
301 | });
302 |
303 | /**
304 | * When inputActive toggle changes to true, focus the input.
305 | * And no I have no idea why this has to be in a timeout.
306 | */
307 | scope.$watch('toggles.inputActive',
308 | function (newVal) {
309 | if (newVal) {
310 | $timeout(function () {
311 | element[0].focus();
312 | });
313 | }
314 | });
315 |
316 | /**
317 | * Detects a paste or someone jamming on the delimiter key.
318 | */
319 | ngModel.$parsers.unshift(function (value) {
320 | var values = value.split(scope.options.delimiter);
321 | if (values.length > 1) {
322 | addTags(values);
323 | }
324 | if (value.match(delimiterRx)) {
325 | element.val('');
326 | return;
327 | }
328 | return value;
329 | });
330 |
331 | /**
332 | * Resets the input field if we selected something from typeahead.
333 | */
334 | ngModel.$formatters.push(function (tag) {
335 | if (tag && tag.value) {
336 | element.val('');
337 | return;
338 | }
339 | return tag;
340 | });
341 | }
342 | };
343 | }]);
344 |
345 | /**
346 | * Main directive
347 | */
348 | tags.directive('tags',
349 | ['$document', '$timeout', '$parse', 'decipherTagsOptions',
350 | function ($document, $timeout, $parse, decipherTagsOptions) {
351 |
352 | return {
353 | controller: 'TagsCtrl',
354 | restrict: 'E',
355 | replace: true,
356 | // IE8 is really, really fussy about this.
357 | template: '
',
358 | scope: {
359 | model: '='
360 | },
361 | link: function (scope, element, attrs) {
362 | var srcResult,
363 | source,
364 | tags,
365 | group,
366 | i,
367 | tagsWatch,
368 | srcWatch,
369 | modelWatch,
370 | model,
371 | pureStrings = false,
372 | stringArray = false,
373 | defaults = angular.copy(defaultOptions),
374 | userDefaults = angular.copy(decipherTagsOptions),
375 |
376 | /**
377 | * Parses the comprehension expression and gives us interesting bits.
378 | * @param input
379 | * @returns {{itemName: *, source: *, viewMapper: *, modelMapper: *}}
380 | */
381 | parse = function parse(input) {
382 | var match = input.match(SRC_REGEXP);
383 | if (!match) {
384 | throw new Error(
385 | "Expected src specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" +
386 | " but got '" + input + "'.");
387 | }
388 |
389 | return {
390 | itemName: match[3],
391 | source: $parse(match[4]),
392 | sourceName: match[4],
393 | viewMapper: $parse(match[2] || match[1]),
394 | modelMapper: $parse(match[1])
395 | };
396 |
397 | },
398 |
399 | watchModel = function watchModel() {
400 | modelWatch = scope.$watch('model', function (newVal) {
401 | var deletedTag, idx;
402 | if (angular.isDefined(newVal)) {
403 | tagsWatch();
404 | scope.tags = format(newVal);
405 |
406 | // remove already used tags
407 | i = scope.tags.length;
408 | while (i--) {
409 | scope._filterSrcTags(scope.tags[i]);
410 | }
411 |
412 | // restore any deleted things to the src array that happen to not
413 | // be in the new value.
414 | i = scope._deletedSrcTags.length;
415 | while (i--) {
416 | deletedTag = scope._deletedSrcTags[i];
417 | if (idx = newVal.indexOf(deletedTag) === -1 &&
418 | scope.srcTags.indexOf(deletedTag) === -1) {
419 | scope.srcTags.push(deletedTag);
420 | scope._deletedSrcTags.splice(i, 1);
421 | }
422 | }
423 |
424 | watchTags();
425 | }
426 | }, true);
427 |
428 | },
429 |
430 | watchTags = function watchTags() {
431 |
432 | /**
433 | * Watches tags for changes and propagates to outer model
434 | * in the format which we originally specified (see below)
435 | */
436 | tagsWatch = scope.$watch('tags', function (value, oldValue) {
437 | var i;
438 | if (value !== oldValue) {
439 | modelWatch();
440 | if (stringArray || pureStrings) {
441 | value = value.map(function (tag) {
442 | return tag.name;
443 | });
444 | if (angular.isArray(scope.model)) {
445 | scope.model.length = 0;
446 | for (i = 0; i < value.length; i++) {
447 | scope.model.push(value[i]);
448 | }
449 | }
450 | if (pureStrings) {
451 | scope.model = value.join(scope.options.delimiter);
452 | }
453 | }
454 | else {
455 | scope.model.length = 0;
456 | for (i = 0; i < value.length; i++) {
457 | scope.model.push(value[i]);
458 | }
459 | }
460 | watchModel();
461 |
462 | }
463 | }, true);
464 | },
465 | /**
466 | * Takes a raw model value and returns something suitable
467 | * to assign to scope.tags
468 | * @param value
469 | */
470 | format = function format(value) {
471 | var arr = [];
472 |
473 | if (angular.isUndefined(value)) {
474 | return;
475 | }
476 | if (angular.isString(value)) {
477 | arr = value
478 | .split(scope.options.delimiter)
479 | .map(function (item) {
480 | return {
481 | name: item.trim()
482 | };
483 | });
484 | }
485 | else if (angular.isArray(value)) {
486 | arr = value.map(function (item) {
487 | if (angular.isString(item)) {
488 | return {
489 | name: item.trim()
490 | };
491 | }
492 | else if (item.name) {
493 | item.name = item.name.trim();
494 | }
495 | return item;
496 | });
497 | }
498 | else if (angular.isDefined(value)) {
499 | throw 'list of tags must be an array or delimited string';
500 | }
501 | return arr;
502 | },
503 | /**
504 | * Updates the source tag information. Sets a watch so we
505 | * know if the source values change.
506 | */
507 | updateSrc = function updateSrc() {
508 | var locals,
509 | i,
510 | o,
511 | obj;
512 | // default to NOT letting users add new tags in this case.
513 | scope.options.addable = scope.options.addable || false;
514 | scope.srcTags = [];
515 | srcResult = parse(attrs.src);
516 | source = srcResult.source(scope.$parent);
517 | if (angular.isUndefined(source)) {
518 | return;
519 | }
520 | if (angular.isFunction(srcWatch)) {
521 | srcWatch();
522 | }
523 | locals = {};
524 | if (angular.isDefined(source)) {
525 | for (i = 0; i < source.length; i++) {
526 | locals[srcResult.itemName] = source[i];
527 | obj = {};
528 | obj.value = srcResult.modelMapper(scope.$parent, locals);
529 | o = {};
530 | if (angular.isObject(obj.value)) {
531 | o = angular.extend(obj.value, {
532 | name: srcResult.viewMapper(scope.$parent, locals),
533 | value: obj.value.value,
534 | group: obj.value.group
535 | });
536 | }
537 | else {
538 | o = {
539 | name: srcResult.viewMapper(scope.$parent, locals),
540 | value: obj.value,
541 | group: group
542 | };
543 | }
544 | scope.srcTags.push(o);
545 | }
546 | }
547 |
548 | srcWatch =
549 | scope.$parent.$watch(srcResult.sourceName,
550 | function (newVal, oldVal) {
551 | if (newVal !== oldVal) {
552 | updateSrc();
553 | }
554 | }, true);
555 | };
556 |
557 | // merge options
558 | scope.options = angular.extend(defaults,
559 | angular.extend(userDefaults, scope.$eval(attrs.options)));
560 | // break out orderBy for view
561 | scope.orderBy = scope.options.orderBy;
562 |
563 | // this should be named something else since it's just a collection
564 | // of random shit.
565 | scope.toggles = {
566 | inputActive: false
567 | };
568 |
569 | /**
570 | * When we receive this event, sort.
571 | */
572 | scope.$on('decipher.tags.sort', function (evt, data) {
573 | scope.orderBy = data;
574 | });
575 |
576 | // pass typeahead options through
577 | attrs.$observe('typeaheadOptions', function (newVal) {
578 | if (newVal) {
579 | scope.typeaheadOptions = $parse(newVal)(scope.$parent);
580 | } else {
581 | scope.typeaheadOptions = {};
582 | }
583 | });
584 |
585 | // determine what format we're in
586 | model = scope.model;
587 | if (angular.isString(model)) {
588 | pureStrings = true;
589 | }
590 | // XXX: avoid for now while fixing "empty array" bug
591 | else if (angular.isArray(model) && false) {
592 | stringArray = true;
593 | i = model.length;
594 | while (i--) {
595 | if (!angular.isString(model[i])) {
596 | stringArray = false;
597 | break;
598 | }
599 | }
600 | }
601 |
602 | // watch model for changes and update tags as appropriate
603 | scope.tags = [];
604 | scope._deletedSrcTags = [];
605 | watchTags();
606 | watchModel();
607 |
608 | // this stuff takes the parsed comprehension expression and
609 | // makes a srcTags array full of tag objects out of it.
610 | scope.srcTags = [];
611 | if (angular.isDefined(attrs.src)) {
612 | updateSrc();
613 | } else {
614 | // if you didn't specify a src, you must be able to type in new tags.
615 | scope.options.addable = true;
616 | }
617 |
618 | // emit identifier
619 | scope.$id = ++id;
620 | scope.$emit('decipher.tags.initialized', {
621 | $id: scope.$id,
622 | model: scope.model
623 | });
624 | }
625 | };
626 | }]);
627 |
628 | })();
629 |
--------------------------------------------------------------------------------
/templates/tags.html:
--------------------------------------------------------------------------------
1 |