├── .gitignore
├── GruntFile.js
├── LICENSE
├── README.md
├── bower.json
├── jsTag
├── compiled
│ ├── jsTag.css
│ ├── jsTag.debug.js
│ └── jsTag.min.js
└── source
│ ├── javascripts
│ ├── app.js
│ ├── controllers.js
│ ├── directives.js
│ ├── filters.js
│ ├── models
│ │ └── default
│ │ │ ├── jsTag.js
│ │ │ └── jsTagsCollection.js
│ ├── services.js
│ └── services
│ │ ├── inputService.js
│ │ └── tagsInputService.js
│ ├── stylesheets
│ └── js-tag.css
│ └── templates
│ ├── default
│ └── js-tag.html
│ └── typeahead
│ └── js-tag.html
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Node modules
2 | node_modules
3 | tmp
--------------------------------------------------------------------------------
/GruntFile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 | // Project configuration.
3 | grunt.initConfig({
4 | pkg: grunt.file.readJSON('package.json'),
5 | debugFilePath: 'jsTag/compiled/<%= pkg.name %>.debug.js',
6 | srcFiles: [
7 | 'jsTag/source/javascripts/app.js',
8 | 'jsTag/source/javascripts/filters.js',
9 | 'jsTag/source/javascripts/models/default/jsTag.js',
10 | 'jsTag/source/javascripts/models/default/jsTagsCollection.js',
11 | 'jsTag/source/javascripts/services/inputService.js',
12 | 'jsTag/source/javascripts/services/tagsInputService.js',
13 | 'jsTag/source/javascripts/services.js',
14 | 'jsTag/source/javascripts/controllers.js',
15 | 'jsTag/source/javascripts/directives.js',
16 | '<%= ngtemplates.jsTag.dest %>'
17 | ],
18 | ngtemplates: {
19 | jsTag: {
20 | src: ['jsTag/source/templates/*/**.html'],
21 | dest: 'tmp/templates.js'
22 | }
23 | },
24 | concat: {
25 | versionJS: {
26 | options: {
27 | banner: '/************************************************\n' +
28 | '* jsTag JavaScript Library - Editing tags based on angularJS \n' +
29 | '* Git: https://github.com/eranhirs/jsTag/tree/master\n' +
30 | '* License: MIT (http://www.opensource.org/licenses/mit-license.php)\n' +
31 | '* Compiled At: <%= grunt.template.today("mm/dd/yyyy HH:MM") %>\n' +
32 | '**************************************************/\n' +
33 | '\'use strict\';\n',
34 | footer: '\n\n'
35 | },
36 | src: ['<%= srcFiles %>'],
37 | dest: '<%= debugFilePath%>'
38 | },
39 | versionCSS: {
40 | src: ['jsTag/source/stylesheets/js-tag.css'],
41 | dest: 'jsTag/compiled/<%= pkg.name %>.css'
42 | }
43 | },
44 | uglify: {
45 | versionJS: {
46 | src: ['<%= debugFilePath%>'],
47 | dest: 'jsTag/compiled/<%= pkg.name %>.min.js'
48 | }
49 | },
50 | clean: {
51 | tempFiles: {
52 | src: ['tmp/']
53 | }
54 | }
55 | });
56 |
57 | // Load used plugins
58 | grunt.loadNpmTasks('grunt-contrib-uglify');
59 | grunt.loadNpmTasks('grunt-contrib-concat');
60 | grunt.loadNpmTasks('grunt-angular-templates');
61 | grunt.loadNpmTasks('grunt-contrib-clean');
62 |
63 | // Build task
64 | grunt.registerTask('version', ['ngtemplates', 'concat:versionCSS', 'concat:versionJS', 'uglify:versionJS', 'clean']);
65 | };
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013-2014 Eran Hirsch
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | jsTag
2 | =====
3 | jsTag is an AngularJS input tags project. Demo available [here](http://eranhirs.github.io/jsTag/ "jsTag Demo").
4 |
5 | Features
6 | --------
7 | * Creating tags
8 | * Editing tags
9 | * Removing tags
10 | * Autocomplete (Integration with [Twitter's typeahead](http://twitter.github.io/typeahead.js/ "Twitter's typeahead github"))
11 |
12 | Demo
13 | ----
14 | Demo available [here](http://eranhirs.github.io/jsTag/ "jsTag Demo").
15 |
16 | How to use?
17 | -----------
18 | See demo for code examples.
19 |
20 | Why jsTag?
21 | ----------
22 | * Pure AngularJS
23 | * Contains all common features
24 | * Highly customizable for your own needs (by following Dependency Injection principles)
25 | * Autocomplete is implemented by external source
26 |
27 | Contributing
28 | ------------
29 | * Open an issue
30 | * Fork the project
31 | * Start a feature/bugfix branch
32 | * Commit and push freely
33 | * Submit your changes as a Pull Request
34 | * Mention the issue fixed in the Pull Request
35 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jsTag",
3 | "version": "0.3.7",
4 | "keywords": [
5 | "jstag",
6 | "js-tag",
7 | "tags",
8 | "angular",
9 | "angularjs"
10 | ],
11 | "license": "MIT",
12 | "main": ["./jsTag/compiled/jsTag.min.js", "./jsTag/compiled/jsTag.css"],
13 | "ignore": [
14 | "jsTag/source",
15 | ".gitignore",
16 | "GruntFile.js",
17 | "package.json",
18 | "README.md",
19 | "bower.json"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/jsTag/compiled/jsTag.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Container *
3 | Explaining some of the CSS:
4 | # background-color: #FFFFFF; | To simulate an input box - in case background of the whole page is different
5 | */
6 | .jt-editor {
7 | padding: 6px 12px 5px 12px;
8 | display: block;
9 | height: auto;
10 |
11 | vertical-align: middle;
12 | font-size: 14px;
13 | line-height: 1.428571429;
14 | color: #555;
15 | border: 1px solid #BFBFBF;
16 | border-radius: 2px;
17 | cursor: text;
18 | background-color: #FFFFFF;
19 | }
20 |
21 | .jt-editor.focused-true {
22 | border-color: #66AFE9;
23 | outline: 0;
24 | }
25 |
26 | /* Tag */
27 | .jt-tag {
28 | background: #DEE7F8;
29 | border: 1px solid #949494;
30 | padding: 0px 0px 20px 2px;
31 | cursor: default;
32 |
33 | display: inline-block;
34 | -webkit-border-radius: 2px 3px 3px 2px;
35 | -moz-border-radius: 2px 3px 3px 2px;
36 | border-radius: 2px 3px 3px 2px;
37 | height: 22px;
38 | }
39 |
40 | /* Because bootstrap uses it and we don't want that if bootstrap is not included to look different */
41 | .jt-tag {
42 | box-sizing: border-box;
43 | }
44 |
45 | .jt-tag:hover {
46 | border-color: #BCBCBC;
47 | }
48 |
49 | /* Value inside jt-tag */
50 | .jt-tag .value {
51 | padding-left: 4px;
52 | }
53 |
54 | /* Tag when active */
55 | .jt-tag.active-true {
56 | border-color: rgba(82, 168, 236, 0.8);
57 | }
58 |
59 | /* Tag remove button ('x') */
60 | .jt-tag .remove-button {
61 | cursor: pointer;
62 | padding-right: 4px;
63 | }
64 | .jt-tag .remove-button:hover {
65 | font-weight: bold;
66 | }
67 |
68 | /* New tag input & Edit tag input */
69 | .jt-tag-new, .jt-tag-edit {
70 | border: none;
71 | outline: 0px;
72 | min-width: 50px; /* Will keep autogrow from lowering width more than 60 */
73 | }
74 |
75 | /* New tag input & Edit tag input & Tag */
76 | .jt-tag-new, .jt-tag-edit, .jt-tag {
77 | margin: 1px 4px 1px 1px;
78 | }
79 |
80 | /* Should not be displayed, only used to capture keydown */
81 | .jt-fake-input {
82 | float: left;
83 | position: absolute;
84 | left: -10000px;
85 | width: 1px;
86 | border: 0px;
87 | }
--------------------------------------------------------------------------------
/jsTag/compiled/jsTag.debug.js:
--------------------------------------------------------------------------------
1 | /************************************************
2 | * jsTag JavaScript Library - Editing tags based on angularJS
3 | * Git: https://github.com/eranhirs/jsTag/tree/master
4 | * License: MIT (http://www.opensource.org/licenses/mit-license.php)
5 | * Compiled At: 11/12/2015 12:45
6 | **************************************************/
7 | 'use strict';
8 | var jsTag = angular.module('jsTag', []);
9 |
10 | // Defaults for jsTag (can be overriden as shown in example)
11 | jsTag.constant('jsTagDefaults', {
12 | 'edit': true,
13 | 'defaultTags': [],
14 | 'breakCodes': [
15 | 13, // Return
16 | 44 // Comma
17 | ],
18 | 'splitter': ',',
19 | 'texts': {
20 | 'inputPlaceHolder': "Input text",
21 | 'removeSymbol': String.fromCharCode(215)
22 | }
23 | });
24 | var jsTag = angular.module('jsTag');
25 |
26 | // Checks if item (needle) exists in array (haystack)
27 | jsTag.filter('inArray', function() {
28 | return function(needle, haystack) {
29 | for(var key in haystack)
30 | {
31 | if (needle === haystack[key])
32 | {
33 | return true;
34 | }
35 | }
36 |
37 | return false;
38 | };
39 | });
40 |
41 | // TODO: Currently the tags in JSTagsCollection is an object with indexes,
42 | // and this filter turns it into an array so we can sort them in ng-repeat.
43 | // An array should be used from the beginning.
44 | jsTag.filter('toArray', function() {
45 | return function(input) {
46 | var objectsAsArray = [];
47 | for (var key in input) {
48 | var value = input[key];
49 | objectsAsArray.push(value);
50 | }
51 |
52 | return objectsAsArray;
53 | };
54 | });
55 | var jsTag = angular.module('jsTag');
56 |
57 | // Tag Model
58 | jsTag.factory('JSTag', function() {
59 | function JSTag(value, id) {
60 | this.value = value;
61 | this.id = id;
62 | }
63 |
64 | return JSTag;
65 | });
66 | var jsTag = angular.module('jsTag');
67 |
68 | // TagsCollection Model
69 | jsTag.factory('JSTagsCollection', ['JSTag', '$filter', function(JSTag, $filter) {
70 |
71 | // Constructor
72 | function JSTagsCollection(defaultTags) {
73 | this.tags = {};
74 | this.tagsCounter = 0;
75 | for (var defaultTagKey in defaultTags) {
76 | var defaultTagValue = defaultTags[defaultTagKey];
77 | this.addTag(defaultTagValue);
78 | }
79 |
80 | this._onAddListenerList = [];
81 | this._onRemoveListenerList = [];
82 |
83 | this.unsetActiveTags();
84 | this.unsetEditedTag();
85 |
86 | this._valueFormatter = null;
87 | this._valueValidator = null;
88 | }
89 |
90 | // *** Methods *** //
91 |
92 | // *** Object manipulation methods *** //
93 | JSTagsCollection.prototype.setValueValidator = function(validator) {
94 | this._valueValidator = validator;
95 | };
96 | JSTagsCollection.prototype.setValueFormatter = function(formatter) {
97 | this._valueFormatter = formatter;
98 | };
99 |
100 | // Adds a tag with received value
101 | JSTagsCollection.prototype.addTag = function(value) {
102 | var tagIndex = this.tagsCounter;
103 | this.tagsCounter++;
104 |
105 | var newTag = new JSTag(value, tagIndex);
106 | this.tags[tagIndex] = newTag;
107 | angular.forEach(this._onAddListenerList, function (callback) {
108 | callback(newTag);
109 | });
110 | };
111 |
112 | // Removes the received tag
113 | JSTagsCollection.prototype.removeTag = function(tagIndex) {
114 | var tag = this.tags[tagIndex];
115 | delete this.tags[tagIndex];
116 | angular.forEach(this._onRemoveListenerList, function (callback) {
117 | callback(tag);
118 | });
119 | };
120 |
121 | JSTagsCollection.prototype.onAdd = function onAdd(callback) {
122 | this._onAddListenerList.push(callback);
123 | };
124 |
125 | JSTagsCollection.prototype.onRemove = function onRemove(callback) {
126 | this._onRemoveListenerList.push(callback);
127 | };
128 |
129 | // Returns the number of tags in collection
130 | JSTagsCollection.prototype.getNumberOfTags = function() {
131 | return getNumberOfProperties(this.tags);
132 | };
133 |
134 | // Returns an array with all values of the tags
135 | JSTagsCollection.prototype.getTagValues = function() {
136 | var tagValues = [];
137 | for (var tag in this.tags) {
138 | tagValues.push(this.tags[tag].value);
139 | }
140 | return tagValues;
141 | };
142 |
143 | // Returns the previous tag before the tag received as input
144 | // Returns same tag if it's the first
145 | JSTagsCollection.prototype.getPreviousTag = function(tag) {
146 | var firstTag = getFirstProperty(this.tags);
147 | if (firstTag.id === tag.id) {
148 | // Return same tag if we reached the beginning
149 | return tag;
150 | } else {
151 | return getPreviousProperty(this.tags, tag.id);
152 | }
153 | };
154 |
155 | // Returns the next tag after the tag received as input
156 | // Returns same tag if it's the last
157 | JSTagsCollection.prototype.getNextTag = function(tag) {
158 | var lastTag = getLastProperty(this.tags);
159 | if (tag.id === lastTag.id) {
160 | // Return same tag if we reached the end
161 | return tag;
162 | } else {
163 | return getNextProperty(this.tags, tag.id);
164 | }
165 | };
166 |
167 | // *** Active methods *** //
168 |
169 | // Checks if a specific tag is active
170 | JSTagsCollection.prototype.isTagActive = function(tag) {
171 | return $filter("inArray")(tag, this._activeTags);
172 | };
173 |
174 | // Sets tag to active
175 | JSTagsCollection.prototype.setActiveTag = function(tag) {
176 | if (!this.isTagActive(tag)) {
177 | this._activeTags.push(tag);
178 | }
179 | };
180 |
181 | // Sets the last tag to be active
182 | JSTagsCollection.prototype.setLastTagActive = function() {
183 | if (getNumberOfProperties(this.tags) > 0) {
184 | var lastTag = getLastProperty(this.tags);
185 | this.setActiveTag(lastTag);
186 | }
187 | };
188 |
189 | // Unsets an active tag
190 | JSTagsCollection.prototype.unsetActiveTag = function(tag) {
191 | var removedTag = this._activeTags.splice(this._activeTags.indexOf(tag), 1);
192 | };
193 |
194 | // Unsets all active tag
195 | JSTagsCollection.prototype.unsetActiveTags = function() {
196 | this._activeTags = [];
197 | };
198 |
199 | // Returns a JSTag only if there is 1 exactly active tags, otherwise null
200 | JSTagsCollection.prototype.getActiveTag = function() {
201 | var activeTag = null;
202 | if (this._activeTags.length === 1) {
203 | activeTag = this._activeTags[0];
204 | }
205 |
206 | return activeTag;
207 | };
208 |
209 | // Returns number of active tags
210 | JSTagsCollection.prototype.getNumOfActiveTags = function() {
211 | return this._activeTags.length;
212 | };
213 |
214 | // *** Edit methods *** //
215 |
216 | // Gets the edited tag
217 | JSTagsCollection.prototype.getEditedTag = function() {
218 | return this._editedTag;
219 | };
220 |
221 | // Checks if a tag is edited
222 | JSTagsCollection.prototype.isTagEdited = function(tag) {
223 | return tag === this._editedTag;
224 | };
225 |
226 | // Sets the tag in the _editedTag member
227 | JSTagsCollection.prototype.setEditedTag = function(tag) {
228 | this._editedTag = tag;
229 | };
230 |
231 | // Unsets the 'edit' flag on a tag by it's given index
232 | JSTagsCollection.prototype.unsetEditedTag = function() {
233 | // Kill empty tags!
234 | if (this._editedTag !== undefined &&
235 | this._editedTag !== null &&
236 | this._editedTag.value === "") {
237 | this.removeTag(this._editedTag.id);
238 | }
239 |
240 | this._editedTag = null;
241 | };
242 |
243 | return JSTagsCollection;
244 | }]);
245 |
246 | // *** Extension methods used to iterate object like a dictionary. Used for the tags. *** //
247 | // TODO: Find another place for these extension methods. Maybe filter.js
248 | // TODO: Maybe use a regular array instead and delete them all :)
249 |
250 | // Gets the number of properties, including inherited
251 | function getNumberOfProperties(obj) {
252 | return Object.keys(obj).length;
253 | }
254 |
255 | // Get the first property of an object, including inherited properties
256 | function getFirstProperty(obj) {
257 | var keys = Object.keys(obj);
258 | return obj[keys[0]];
259 | }
260 |
261 | // Get the last property of an object, including inherited properties
262 | function getLastProperty(obj) {
263 | var keys = Object.keys(obj);
264 | return obj[keys[keys.length - 1]];
265 | }
266 |
267 | // Get the next property of an object whos' properties keys are numbers, including inherited properties
268 | function getNextProperty(obj, propertyId) {
269 | var keys = Object.keys(obj);
270 | var indexOfProperty = keys.indexOf(propertyId.toString());
271 | var keyOfNextProperty = keys[indexOfProperty + 1];
272 | return obj[keyOfNextProperty];
273 | }
274 |
275 | // Get the previous property of an object whos' properties keys are numbers, including inherited properties
276 | function getPreviousProperty(obj, propertyId) {
277 | var keys = Object.keys(obj);
278 | var indexOfProperty = keys.indexOf(propertyId.toString());
279 | var keyOfPreviousProperty = keys[indexOfProperty - 1];
280 | return obj[keyOfPreviousProperty];
281 | }
282 |
283 | var jsTag = angular.module('jsTag');
284 |
285 | // This service handles everything related to input (when to focus input, key pressing, breakcodeHit).
286 | jsTag.factory('InputService', ['$filter', function($filter) {
287 |
288 | // Constructor
289 | function InputService(options) {
290 | this.input = "";
291 | this.isWaitingForInput = options.autoFocus || false;
292 | this.options = options;
293 | }
294 |
295 | // *** Events *** //
296 |
297 | // Handles an input of a new tag keydown
298 | InputService.prototype.onKeydown = function(inputService, tagsCollection, options) {
299 | var e = options.$event;
300 | var $element = angular.element(e.currentTarget);
301 | var keycode = e.which;
302 | // In order to know how to handle a breakCode or a backspace, we must know if the typeahead
303 | // input value is empty or not. e.g. if user hits backspace and typeahead input is not empty
304 | // then we have nothing to do as user si not trying to remove a tag but simply tries to
305 | // delete some character in typeahead's input.
306 | // To know the value in the typeahead input, we can't use `this.input` because when
307 | // typeahead is in uneditable mode, the model (i.e. `this.input`) is not updated and is set
308 | // to undefined. So we have to fetch the value directly from the typeahead input element.
309 | //
310 | // We have to test this.input first, because $element.typeahead is a function and can be set
311 | // even if we are not in the typeahead mode.
312 | // So in this case, the value is always null and the preventDefault is never fired
313 | // This cause the form to always submit after hitting the Enter key.
314 | //var value = ($element.typeahead !== undefined) ? $element.typeahead('val') : this.input;
315 | var value = this.input || (($element.typeahead !== undefined) ? $element.typeahead('val') : undefined) ;
316 | var valueIsEmpty = (value === null || value === undefined || value === "");
317 |
318 | // Check if should break by breakcodes
319 | if ($filter("inArray")(keycode, this.options.breakCodes) !== false) {
320 |
321 | inputService.breakCodeHit(tagsCollection, this.options);
322 |
323 | // Trigger breakcodeHit event allowing extensions (used in twitter's typeahead directive)
324 | $element.triggerHandler('jsTag:breakcodeHit');
325 |
326 | // Do not trigger form submit if value is not empty.
327 | if (!valueIsEmpty) {
328 | e.preventDefault();
329 | }
330 |
331 | } else {
332 | switch (keycode) {
333 | case 9: // Tab
334 |
335 | break;
336 | case 37: // Left arrow
337 | case 8: // Backspace
338 | if (valueIsEmpty) {
339 | // TODO: Call removing tag event instead of calling a method, easier to customize
340 | tagsCollection.setLastTagActive();
341 | }
342 |
343 | break;
344 | }
345 | }
346 | };
347 |
348 | // Handles an input of an edited tag keydown
349 | InputService.prototype.tagInputKeydown = function(tagsCollection, options) {
350 | var e = options.$event;
351 | var keycode = e.which;
352 |
353 | // Check if should break by breakcodes
354 | if ($filter("inArray")(keycode, this.options.breakCodes) !== false) {
355 | this.breakCodeHitOnEdit(tagsCollection, options);
356 | }
357 | };
358 |
359 |
360 | InputService.prototype.onBlur = function(tagsCollection) {
361 | this.breakCodeHit(tagsCollection, this.options);
362 | };
363 |
364 | // *** Methods *** //
365 |
366 | InputService.prototype.resetInput = function() {
367 | var value = this.input;
368 | this.input = "";
369 | return value;
370 | };
371 |
372 | // Sets focus on input
373 | InputService.prototype.focusInput = function() {
374 | this.isWaitingForInput = true;
375 | };
376 |
377 | // breakCodeHit is called when finished creating tag
378 | InputService.prototype.breakCodeHit = function(tagsCollection, options) {
379 | if (this.input !== "") {
380 | if(tagsCollection._valueFormatter) {
381 | this.input = tagsCollection._valueFormatter(this.input);
382 | }
383 | if(tagsCollection._valueValidator) {
384 | if(!tagsCollection._valueValidator(this.input)) {
385 | return;
386 | };
387 | }
388 |
389 | var originalValue = this.resetInput();
390 |
391 | // Input is an object when using typeahead (the key is chosen by the user)
392 | if (originalValue instanceof Object)
393 | {
394 | originalValue = originalValue[options.tagDisplayKey || Object.keys(originalValue)[0]];
395 | }
396 |
397 | // Split value by spliter (usually ,)
398 | var values = originalValue.split(options.splitter);
399 | // Remove empty string objects from the values
400 | for (var i = 0; i < values.length; i++) {
401 | if (!values[i]) {
402 | values.splice(i, 1);
403 | i--;
404 | }
405 | }
406 |
407 | // Add tags to collection
408 | for (var key in values) {
409 | if ( ! values.hasOwnProperty(key)) continue; // for IE 8
410 | var value = values[key];
411 | tagsCollection.addTag(value);
412 | }
413 | }
414 | };
415 |
416 | // breakCodeHit is called when finished editing tag
417 | InputService.prototype.breakCodeHitOnEdit = function(tagsCollection, options) {
418 | // Input is an object when using typeahead (the key is chosen by the user)
419 | var editedTag = tagsCollection.getEditedTag();
420 | if (editedTag.value instanceof Object) {
421 | editedTag.value = editedTag.value[options.tagDisplayKey || Object.keys(editedTag.value)[0]];
422 | }
423 |
424 | tagsCollection.unsetEditedTag();
425 | this.isWaitingForInput = true;
426 | };
427 |
428 | return InputService;
429 | }]);
430 |
431 | var jsTag = angular.module('jsTag');
432 |
433 | // TagsCollection Model
434 | jsTag.factory('TagsInputService', ['JSTag', 'JSTagsCollection', function(JSTag, JSTagsCollection) {
435 | // Constructor
436 | function TagsHandler(options) {
437 | this.options = options;
438 | var tags = options.tags;
439 |
440 | // Received ready JSTagsCollection
441 | if (tags && Object.getPrototypeOf(tags) === JSTagsCollection.prototype) {
442 | this.tagsCollection = tags;
443 | }
444 | // Received array with default tags or did not receive tags
445 | else {
446 | var defaultTags = options.defaultTags;
447 | this.tagsCollection = new JSTagsCollection(defaultTags);
448 | }
449 | this.shouldBlurActiveTag = true;
450 | }
451 |
452 | // *** Methods *** //
453 |
454 | TagsHandler.prototype.tagClicked = function(tag) {
455 | this.tagsCollection.setActiveTag(tag);
456 | };
457 |
458 | TagsHandler.prototype.tagDblClicked = function(tag) {
459 | var editAllowed = this.options.edit;
460 | if (editAllowed) {
461 | // Set tag as edit
462 | this.tagsCollection.setEditedTag(tag);
463 | }
464 | };
465 |
466 | // Keydown was pressed while a tag was active.
467 | // Important Note: The target of the event is actually a fake input used to capture the keydown.
468 | TagsHandler.prototype.onActiveTagKeydown = function(inputService, options) {
469 | var activeTag = this.tagsCollection.getActiveTag();
470 |
471 | // Do nothing in unexpected situations
472 | if (activeTag !== null) {
473 | var e = options.$event;
474 |
475 | // Mimics blur of the active tag though the focus is on the input.
476 | // This will cause expected features like unseting active tag
477 | var blurActiveTag = function() {
478 | // Expose the option not to blur the active tag
479 | if (this.shouldBlurActiveTag) {
480 | this.onActiveTagBlur(options);
481 | }
482 | };
483 |
484 | switch (e.which) {
485 | case 13: // Return
486 | var editAllowed = this.options.edit;
487 | if (editAllowed) {
488 | blurActiveTag.apply(this);
489 | this.tagsCollection.setEditedTag(activeTag);
490 | }
491 |
492 | break;
493 | case 8: // Backspace
494 | this.tagsCollection.removeTag(activeTag.id);
495 | inputService.isWaitingForInput = true;
496 |
497 | break;
498 | case 37: // Left arrow
499 | blurActiveTag.apply(this);
500 | var previousTag = this.tagsCollection.getPreviousTag(activeTag);
501 | this.tagsCollection.setActiveTag(previousTag);
502 |
503 | break;
504 | case 39: // Right arrow
505 | blurActiveTag.apply(this);
506 |
507 | var nextTag = this.tagsCollection.getNextTag(activeTag);
508 | if (nextTag !== activeTag) {
509 | this.tagsCollection.setActiveTag(nextTag);
510 | } else {
511 | inputService.isWaitingForInput = true;
512 | }
513 |
514 | break;
515 | }
516 | }
517 | };
518 |
519 | // Jumps when active tag calls blur event.
520 | // Because the focus is not on the tag's div itself but a fake input,
521 | // this is called also when clicking the active tag.
522 | // (Which is good because we want the tag to be unactive, then it will be reactivated on the click event)
523 | // It is also called when entering edit mode (ex. when pressing enter while active, it will call blur)
524 | TagsHandler.prototype.onActiveTagBlur = function(options) {
525 | var activeTag = this.tagsCollection.getActiveTag();
526 |
527 | // Do nothing in unexpected situations
528 | if (activeTag !== null) {
529 | this.tagsCollection.unsetActiveTag(activeTag);
530 | }
531 | };
532 |
533 | // Jumps when an edited tag calls blur event
534 | TagsHandler.prototype.onEditTagBlur = function(tagsCollection, inputService) {
535 | tagsCollection.unsetEditedTag();
536 | this.isWaitingForInput = true;
537 | };
538 |
539 | return TagsHandler;
540 | }]);
541 |
542 | var jsTag = angular.module('jsTag');
543 | var jsTag = angular.module('jsTag');
544 |
545 | jsTag.controller('JSTagMainCtrl', ['$attrs', '$scope', 'InputService', 'TagsInputService', 'jsTagDefaults', function($attrs, $scope, InputService, TagsInputService, jsTagDefaults) {
546 | // Parse user options and merge with defaults
547 | var userOptions = {};
548 | try {
549 | userOptions = $scope.$eval($attrs.jsTagOptions);
550 | } catch(e) {
551 | console.log("jsTag Error: Invalid user options, using defaults only");
552 | }
553 |
554 | // Copy so we don't override original values
555 | var options = angular.copy(jsTagDefaults);
556 |
557 | // Use user defined options
558 | if (userOptions !== undefined) {
559 | userOptions.texts = angular.extend(options.texts, userOptions.texts || {});
560 | angular.extend(options, userOptions);
561 | }
562 |
563 | $scope.options = options;
564 |
565 | // Export handlers to view
566 | $scope.tagsInputService = new TagsInputService($scope.options);
567 | $scope.inputService = new InputService($scope.options);
568 |
569 | // Export tagsCollection separately since it's used alot
570 | var tagsCollection = $scope.tagsInputService.tagsCollection;
571 | $scope.tagsCollection = tagsCollection;
572 |
573 | // TODO: Should be inside inside tagsCollection.js
574 | // On every change to editedTags keep isThereAnEditedTag posted
575 | $scope.$watch('tagsCollection._editedTag', function(newValue, oldValue) {
576 | $scope.isThereAnEditedTag = newValue !== null;
577 | });
578 |
579 | // TODO: Should be inside inside tagsCollection.js
580 | // On every change to activeTags keep isThereAnActiveTag posted
581 | $scope.$watchCollection('tagsCollection._activeTags', function(newValue, oldValue) {
582 | $scope.isThereAnActiveTag = newValue.length > 0;
583 | });
584 | }]);
585 | var jsTag = angular.module('jsTag');
586 |
587 | // TODO: Maybe add A to 'restrict: E' for support in IE 8?
588 | jsTag.directive('jsTag', ['$templateCache', function($templateCache) {
589 | return {
590 | restrict: 'E',
591 | scope: true,
592 | controller: 'JSTagMainCtrl',
593 | templateUrl: function($element, $attrs) {
594 | var mode = $attrs.jsTagMode || "default";
595 | return 'jsTag/source/templates/' + mode + '/js-tag.html';
596 | }
597 | };
598 | }]);
599 |
600 | // TODO: Replace this custom directive by a supported angular-js directive for blur
601 | jsTag.directive('ngBlur', ['$parse', function($parse) {
602 | return {
603 | restrict: 'A',
604 | link: function(scope, elem, attrs) {
605 | // this next line will convert the string
606 | // function name into an actual function
607 | var functionToCall = $parse(attrs.ngBlur);
608 | elem.bind('blur', function(event) {
609 |
610 | // on the blur event, call my function
611 | scope.$apply(function() {
612 | functionToCall(scope, {$event:event});
613 | });
614 | });
615 | }
616 | };
617 | }]);
618 |
619 |
620 | // Notice that focus me also sets the value to false when blur is called
621 | // TODO: Replace this custom directive by a supported angular-js directive for focus
622 | // http://stackoverflow.com/questions/14833326/how-to-set-focus-in-angularjs
623 | jsTag.directive('focusMe', ['$parse', '$timeout', function($parse, $timeout) {
624 | return {
625 | restrict: 'A',
626 | link: function(scope, element, attrs) {
627 | var model = $parse(attrs.focusMe);
628 | scope.$watch(model, function(value) {
629 | if (value === true) {
630 | $timeout(function() {
631 | element[0].focus();
632 | });
633 | }
634 | });
635 |
636 | // to address @blesh's comment, set attribute value to 'false'
637 | // on blur event:
638 | element.bind('blur', function() {
639 | scope.$apply(model.assign(scope, false));
640 | });
641 | }
642 | };
643 | }]);
644 |
645 | // focusOnce is used to focus an element once when first appearing
646 | // Not like focusMe that binds to an input boolean and keeps focusing by it
647 | jsTag.directive('focusOnce', ['$timeout', function($timeout) {
648 | return {
649 | restrict: 'A',
650 | link: function(scope, element, attrs) {
651 | $timeout(function() {
652 | element[0].select();
653 | });
654 | }
655 | };
656 | }]);
657 |
658 | // auto-grow directive by the "shadow" tag concept
659 | jsTag.directive('autoGrow', ['$timeout', function($timeout) {
660 | return {
661 | link: function(scope, element, attr){
662 | var paddingLeft = element.css('paddingLeft'),
663 | paddingRight = element.css('paddingRight');
664 |
665 | var minWidth = 60;
666 |
667 | var $shadow = angular.element('').css({
668 | 'position': 'absolute',
669 | 'top': '-10000px',
670 | 'left': '-10000px',
671 | 'fontSize': element.css('fontSize'),
672 | 'fontFamily': element.css('fontFamily'),
673 | 'white-space': 'pre'
674 | });
675 | element.after($shadow);
676 |
677 | var update = function() {
678 | var val = element.val()
679 | .replace(//g, '>')
681 | .replace(/&/g, '&')
682 | ;
683 |
684 | // If empty calculate by placeholder
685 | if (val !== "") {
686 | $shadow.html(val);
687 | } else {
688 | $shadow.html(element[0].placeholder);
689 | }
690 |
691 | var newWidth = ($shadow[0].offsetWidth + 10) + "px";
692 | element.css('width', newWidth);
693 | };
694 |
695 | var ngModel = element.attr('ng-model');
696 | if (ngModel) {
697 | scope.$watch(ngModel, update);
698 | } else {
699 | element.bind('keyup keydown', update);
700 | }
701 |
702 | // Update on the first link
703 | // $timeout is needed because the value of element is updated only after the $digest cycle
704 | // TODO: Maybe on compile time if we call update we won't need $timeout
705 | $timeout(update);
706 | }
707 | };
708 | }]);
709 |
710 | // Small directive for twitter's typeahead
711 | jsTag.directive('jsTagTypeahead', function () {
712 | return {
713 | restrict: 'A', // Only apply on an attribute or class
714 | require: '?ngModel', // The two-way data bound value that is returned by the directive
715 | link: function (scope, element, attrs, ngModel) {
716 |
717 | element.bind('jsTag:breakcodeHit', function(event) {
718 |
719 | /* Do not clear typeahead input if typeahead option 'editable' is set to false
720 | * so custom tags are not allowed and breakcode hit shouldn't trigger any change. */
721 | if (scope.$eval(attrs.options).editable === false) {
722 | return;
723 | }
724 |
725 | // Tell typeahead to remove the value (after it was also removed in input)
726 | $(event.currentTarget).typeahead('val', '');
727 | });
728 |
729 | }
730 | };
731 | });
732 |
733 | angular.module("jsTag").run(["$templateCache", function($templateCache) {
734 |
735 | $templateCache.put("jsTag/source/templates/default/js-tag.html",
736 | "
\r" +
741 | "\n" +
742 | " \r" +
747 | "\n" +
748 | " \r" +
753 | "\n" +
754 | " \r" +
761 | "\n" +
762 | " \r" +
763 | "\n" +
764 | " {{tag.value}}\r" +
765 | "\n" +
766 | " \r" +
767 | "\n" +
768 | " {{options.texts.removeSymbol}}\r" +
769 | "\n" +
770 | " \r" +
771 | "\n" +
772 | " \r" +
775 | "\n" +
776 | " \r" +
795 | "\n" +
796 | " \r" +
797 | "\n" +
798 | " \r" +
799 | "\n" +
800 | " \r" +
821 | "\n" +
822 | " \r" +
831 | "\n" +
832 | "
\r" +
833 | "\n"
834 | );
835 |
836 | $templateCache.put("jsTag/source/templates/typeahead/js-tag.html",
837 | "\r" +
842 | "\n" +
843 | " \r" +
848 | "\n" +
849 | " \r" +
854 | "\n" +
855 | " \r" +
862 | "\n" +
863 | " \r" +
864 | "\n" +
865 | " {{tag.value}}\r" +
866 | "\n" +
867 | " \r" +
868 | "\n" +
869 | " {{options.texts.removeSymbol}}\r" +
870 | "\n" +
871 | " \r" +
872 | "\n" +
873 | " \r" +
876 | "\n" +
877 | " \r" +
900 | "\n" +
901 | " \r" +
902 | "\n" +
903 | " \r" +
904 | "\n" +
905 | " \r" +
932 | "\n" +
933 | " \r" +
942 | "\n" +
943 | "
\r" +
944 | "\n"
945 | );
946 |
947 | }]);
948 |
949 |
950 |
--------------------------------------------------------------------------------
/jsTag/compiled/jsTag.min.js:
--------------------------------------------------------------------------------
1 | "use strict";function getNumberOfProperties(a){return Object.keys(a).length}function getFirstProperty(a){var b=Object.keys(a);return a[b[0]]}function getLastProperty(a){var b=Object.keys(a);return a[b[b.length-1]]}function getNextProperty(a,b){var c=Object.keys(a),d=c.indexOf(b.toString()),e=c[d+1];return a[e]}function getPreviousProperty(a,b){var c=Object.keys(a),d=c.indexOf(b.toString()),e=c[d-1];return a[e]}var jsTag=angular.module("jsTag",[]);jsTag.constant("jsTagDefaults",{edit:!0,defaultTags:[],breakCodes:[13,44],splitter:",",texts:{inputPlaceHolder:"Input text",removeSymbol:String.fromCharCode(215)}});var jsTag=angular.module("jsTag");jsTag.filter("inArray",function(){return function(a,b){for(var c in b)if(a===b[c])return!0;return!1}}),jsTag.filter("toArray",function(){return function(a){var b=[];for(var c in a){var d=a[c];b.push(d)}return b}});var jsTag=angular.module("jsTag");jsTag.factory("JSTag",function(){function a(a,b){this.value=a,this.id=b}return a});var jsTag=angular.module("jsTag");jsTag.factory("JSTagsCollection",["JSTag","$filter",function(a,b){function c(a){this.tags={},this.tagsCounter=0;for(var b in a){var c=a[b];this.addTag(c)}this._onAddListenerList=[],this._onRemoveListenerList=[],this.unsetActiveTags(),this.unsetEditedTag(),this._valueFormatter=null,this._valueValidator=null}return c.prototype.setValueValidator=function(a){this._valueValidator=a},c.prototype.setValueFormatter=function(a){this._valueFormatter=a},c.prototype.addTag=function(b){var c=this.tagsCounter;this.tagsCounter++;var d=new a(b,c);this.tags[c]=d,angular.forEach(this._onAddListenerList,function(a){a(d)})},c.prototype.removeTag=function(a){var b=this.tags[a];delete this.tags[a],angular.forEach(this._onRemoveListenerList,function(a){a(b)})},c.prototype.onAdd=function(a){this._onAddListenerList.push(a)},c.prototype.onRemove=function(a){this._onRemoveListenerList.push(a)},c.prototype.getNumberOfTags=function(){return getNumberOfProperties(this.tags)},c.prototype.getTagValues=function(){var a=[];for(var b in this.tags)a.push(this.tags[b].value);return a},c.prototype.getPreviousTag=function(a){var b=getFirstProperty(this.tags);return b.id===a.id?a:getPreviousProperty(this.tags,a.id)},c.prototype.getNextTag=function(a){var b=getLastProperty(this.tags);return a.id===b.id?a:getNextProperty(this.tags,a.id)},c.prototype.isTagActive=function(a){return b("inArray")(a,this._activeTags)},c.prototype.setActiveTag=function(a){this.isTagActive(a)||this._activeTags.push(a)},c.prototype.setLastTagActive=function(){if(getNumberOfProperties(this.tags)>0){var a=getLastProperty(this.tags);this.setActiveTag(a)}},c.prototype.unsetActiveTag=function(a){this._activeTags.splice(this._activeTags.indexOf(a),1)},c.prototype.unsetActiveTags=function(){this._activeTags=[]},c.prototype.getActiveTag=function(){var a=null;return 1===this._activeTags.length&&(a=this._activeTags[0]),a},c.prototype.getNumOfActiveTags=function(){return this._activeTags.length},c.prototype.getEditedTag=function(){return this._editedTag},c.prototype.isTagEdited=function(a){return a===this._editedTag},c.prototype.setEditedTag=function(a){this._editedTag=a},c.prototype.unsetEditedTag=function(){void 0!==this._editedTag&&null!==this._editedTag&&""===this._editedTag.value&&this.removeTag(this._editedTag.id),this._editedTag=null},c}]);var jsTag=angular.module("jsTag");jsTag.factory("InputService",["$filter",function(a){function b(a){this.input="",this.isWaitingForInput=a.autoFocus||!1,this.options=a}return b.prototype.onKeydown=function(b,c,d){var e=d.$event,f=angular.element(e.currentTarget),g=e.which,h=this.input||(void 0!==f.typeahead?f.typeahead("val"):void 0),i=null===h||void 0===h||""===h;if(a("inArray")(g,this.options.breakCodes)!==!1)b.breakCodeHit(c,this.options),f.triggerHandler("jsTag:breakcodeHit"),i||e.preventDefault();else switch(g){case 9:break;case 37:case 8:i&&c.setLastTagActive()}},b.prototype.tagInputKeydown=function(b,c){var d=c.$event,e=d.which;a("inArray")(e,this.options.breakCodes)!==!1&&this.breakCodeHitOnEdit(b,c)},b.prototype.onBlur=function(a){this.breakCodeHit(a,this.options)},b.prototype.resetInput=function(){var a=this.input;return this.input="",a},b.prototype.focusInput=function(){this.isWaitingForInput=!0},b.prototype.breakCodeHit=function(a,b){if(""!==this.input){if(a._valueFormatter&&(this.input=a._valueFormatter(this.input)),a._valueValidator&&!a._valueValidator(this.input))return;var c=this.resetInput();c instanceof Object&&(c=c[b.tagDisplayKey||Object.keys(c)[0]]);for(var d=c.split(b.splitter),e=0;e0})}]);var jsTag=angular.module("jsTag");jsTag.directive("jsTag",["$templateCache",function(a){return{restrict:"E",scope:!0,controller:"JSTagMainCtrl",templateUrl:function(a,b){var c=b.jsTagMode||"default";return"jsTag/source/templates/"+c+"/js-tag.html"}}}]),jsTag.directive("ngBlur",["$parse",function(a){return{restrict:"A",link:function(b,c,d){var e=a(d.ngBlur);c.bind("blur",function(a){b.$apply(function(){e(b,{$event:a})})})}}}]),jsTag.directive("focusMe",["$parse","$timeout",function(a,b){return{restrict:"A",link:function(c,d,e){var f=a(e.focusMe);c.$watch(f,function(a){a===!0&&b(function(){d[0].focus()})}),d.bind("blur",function(){c.$apply(f.assign(c,!1))})}}}]),jsTag.directive("focusOnce",["$timeout",function(a){return{restrict:"A",link:function(b,c,d){a(function(){c[0].select()})}}}]),jsTag.directive("autoGrow",["$timeout",function(a){return{link:function(b,c,d){var e=(c.css("paddingLeft"),c.css("paddingRight"),angular.element("").css({position:"absolute",top:"-10000px",left:"-10000px",fontSize:c.css("fontSize"),fontFamily:c.css("fontFamily"),"white-space":"pre"}));c.after(e);var f=function(){var a=c.val().replace(//g,">").replace(/&/g,"&");""!==a?e.html(a):e.html(c[0].placeholder);var b=e[0].offsetWidth+10+"px";c.css("width",b)},g=c.attr("ng-model");g?b.$watch(g,f):c.bind("keyup keydown",f),a(f)}}}]),jsTag.directive("jsTagTypeahead",function(){return{restrict:"A",require:"?ngModel",link:function(a,b,c,d){b.bind("jsTag:breakcodeHit",function(b){a.$eval(c.options).editable!==!1&&$(b.currentTarget).typeahead("val","")})}}}),angular.module("jsTag").run(["$templateCache",function(a){a.put("jsTag/source/templates/default/js-tag.html",'\r\n \r\n \r\n \r\n \r\n {{tag.value}}\r\n \r\n {{options.texts.removeSymbol}}\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n'),a.put("jsTag/source/templates/typeahead/js-tag.html",'\r\n \r\n \r\n \r\n \r\n {{tag.value}}\r\n \r\n {{options.texts.removeSymbol}}\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n')}]);
--------------------------------------------------------------------------------
/jsTag/source/javascripts/app.js:
--------------------------------------------------------------------------------
1 | var jsTag = angular.module('jsTag', []);
2 |
3 | // Defaults for jsTag (can be overriden as shown in example)
4 | jsTag.constant('jsTagDefaults', {
5 | 'edit': true,
6 | 'defaultTags': [],
7 | 'breakCodes': [
8 | 13, // Return
9 | 44 // Comma
10 | ],
11 | 'splitter': ',',
12 | 'texts': {
13 | 'inputPlaceHolder': "Input text",
14 | 'removeSymbol': String.fromCharCode(215)
15 | }
16 | });
--------------------------------------------------------------------------------
/jsTag/source/javascripts/controllers.js:
--------------------------------------------------------------------------------
1 | var jsTag = angular.module('jsTag');
2 |
3 | jsTag.controller('JSTagMainCtrl', ['$attrs', '$scope', 'InputService', 'TagsInputService', 'jsTagDefaults', function($attrs, $scope, InputService, TagsInputService, jsTagDefaults) {
4 | // Parse user options and merge with defaults
5 | var userOptions = {};
6 | try {
7 | userOptions = $scope.$eval($attrs.jsTagOptions);
8 | } catch(e) {
9 | console.log("jsTag Error: Invalid user options, using defaults only");
10 | }
11 |
12 | // Copy so we don't override original values
13 | var options = angular.copy(jsTagDefaults);
14 |
15 | // Use user defined options
16 | if (userOptions !== undefined) {
17 | userOptions.texts = angular.extend(options.texts, userOptions.texts || {});
18 | angular.extend(options, userOptions);
19 | }
20 |
21 | $scope.options = options;
22 |
23 | // Export handlers to view
24 | $scope.tagsInputService = new TagsInputService($scope.options);
25 | $scope.inputService = new InputService($scope.options);
26 |
27 | // Export tagsCollection separately since it's used alot
28 | var tagsCollection = $scope.tagsInputService.tagsCollection;
29 | $scope.tagsCollection = tagsCollection;
30 |
31 | // TODO: Should be inside inside tagsCollection.js
32 | // On every change to editedTags keep isThereAnEditedTag posted
33 | $scope.$watch('tagsCollection._editedTag', function(newValue, oldValue) {
34 | $scope.isThereAnEditedTag = newValue !== null;
35 | });
36 |
37 | // TODO: Should be inside inside tagsCollection.js
38 | // On every change to activeTags keep isThereAnActiveTag posted
39 | $scope.$watchCollection('tagsCollection._activeTags', function(newValue, oldValue) {
40 | $scope.isThereAnActiveTag = newValue.length > 0;
41 | });
42 | }]);
--------------------------------------------------------------------------------
/jsTag/source/javascripts/directives.js:
--------------------------------------------------------------------------------
1 | var jsTag = angular.module('jsTag');
2 |
3 | // TODO: Maybe add A to 'restrict: E' for support in IE 8?
4 | jsTag.directive('jsTag', ['$templateCache', function($templateCache) {
5 | return {
6 | restrict: 'E',
7 | scope: true,
8 | controller: 'JSTagMainCtrl',
9 | templateUrl: function($element, $attrs) {
10 | var mode = $attrs.jsTagMode || "default";
11 | return 'jsTag/source/templates/' + mode + '/js-tag.html';
12 | }
13 | };
14 | }]);
15 |
16 | // TODO: Replace this custom directive by a supported angular-js directive for blur
17 | jsTag.directive('ngBlur', ['$parse', function($parse) {
18 | return {
19 | restrict: 'A',
20 | link: function(scope, elem, attrs) {
21 | // this next line will convert the string
22 | // function name into an actual function
23 | var functionToCall = $parse(attrs.ngBlur);
24 | elem.bind('blur', function(event) {
25 |
26 | // on the blur event, call my function
27 | scope.$apply(function() {
28 | functionToCall(scope, {$event:event});
29 | });
30 | });
31 | }
32 | };
33 | }]);
34 |
35 |
36 | // Notice that focus me also sets the value to false when blur is called
37 | // TODO: Replace this custom directive by a supported angular-js directive for focus
38 | // http://stackoverflow.com/questions/14833326/how-to-set-focus-in-angularjs
39 | jsTag.directive('focusMe', ['$parse', '$timeout', function($parse, $timeout) {
40 | return {
41 | restrict: 'A',
42 | link: function(scope, element, attrs) {
43 | var model = $parse(attrs.focusMe);
44 | scope.$watch(model, function(value) {
45 | if (value === true) {
46 | $timeout(function() {
47 | element[0].focus();
48 | });
49 | }
50 | });
51 |
52 | // to address @blesh's comment, set attribute value to 'false'
53 | // on blur event:
54 | element.bind('blur', function() {
55 | scope.$apply(model.assign(scope, false));
56 | });
57 | }
58 | };
59 | }]);
60 |
61 | // focusOnce is used to focus an element once when first appearing
62 | // Not like focusMe that binds to an input boolean and keeps focusing by it
63 | jsTag.directive('focusOnce', ['$timeout', function($timeout) {
64 | return {
65 | restrict: 'A',
66 | link: function(scope, element, attrs) {
67 | $timeout(function() {
68 | element[0].select();
69 | });
70 | }
71 | };
72 | }]);
73 |
74 | // auto-grow directive by the "shadow" tag concept
75 | jsTag.directive('autoGrow', ['$timeout', function($timeout) {
76 | return {
77 | link: function(scope, element, attr){
78 | var paddingLeft = element.css('paddingLeft'),
79 | paddingRight = element.css('paddingRight');
80 |
81 | var minWidth = 60;
82 |
83 | var $shadow = angular.element('').css({
84 | 'position': 'absolute',
85 | 'top': '-10000px',
86 | 'left': '-10000px',
87 | 'fontSize': element.css('fontSize'),
88 | 'fontFamily': element.css('fontFamily'),
89 | 'white-space': 'pre'
90 | });
91 | element.after($shadow);
92 |
93 | var update = function() {
94 | var val = element.val()
95 | .replace(//g, '>')
97 | .replace(/&/g, '&')
98 | ;
99 |
100 | // If empty calculate by placeholder
101 | if (val !== "") {
102 | $shadow.html(val);
103 | } else {
104 | $shadow.html(element[0].placeholder);
105 | }
106 |
107 | var newWidth = ($shadow[0].offsetWidth + 10) + "px";
108 | element.css('width', newWidth);
109 | };
110 |
111 | var ngModel = element.attr('ng-model');
112 | if (ngModel) {
113 | scope.$watch(ngModel, update);
114 | } else {
115 | element.bind('keyup keydown', update);
116 | }
117 |
118 | // Update on the first link
119 | // $timeout is needed because the value of element is updated only after the $digest cycle
120 | // TODO: Maybe on compile time if we call update we won't need $timeout
121 | $timeout(update);
122 | }
123 | };
124 | }]);
125 |
126 | // Small directive for twitter's typeahead
127 | jsTag.directive('jsTagTypeahead', function () {
128 | return {
129 | restrict: 'A', // Only apply on an attribute or class
130 | require: '?ngModel', // The two-way data bound value that is returned by the directive
131 | link: function (scope, element, attrs, ngModel) {
132 |
133 | element.bind('jsTag:breakcodeHit', function(event) {
134 |
135 | /* Do not clear typeahead input if typeahead option 'editable' is set to false
136 | * so custom tags are not allowed and breakcode hit shouldn't trigger any change. */
137 | if (scope.$eval(attrs.options).editable === false) {
138 | return;
139 | }
140 |
141 | // Tell typeahead to remove the value (after it was also removed in input)
142 | $(event.currentTarget).typeahead('val', '');
143 | });
144 |
145 | }
146 | };
147 | });
148 |
--------------------------------------------------------------------------------
/jsTag/source/javascripts/filters.js:
--------------------------------------------------------------------------------
1 | var jsTag = angular.module('jsTag');
2 |
3 | // Checks if item (needle) exists in array (haystack)
4 | jsTag.filter('inArray', function() {
5 | return function(needle, haystack) {
6 | for(var key in haystack)
7 | {
8 | if (needle === haystack[key])
9 | {
10 | return true;
11 | }
12 | }
13 |
14 | return false;
15 | };
16 | });
17 |
18 | // TODO: Currently the tags in JSTagsCollection is an object with indexes,
19 | // and this filter turns it into an array so we can sort them in ng-repeat.
20 | // An array should be used from the beginning.
21 | jsTag.filter('toArray', function() {
22 | return function(input) {
23 | var objectsAsArray = [];
24 | for (var key in input) {
25 | var value = input[key];
26 | objectsAsArray.push(value);
27 | }
28 |
29 | return objectsAsArray;
30 | };
31 | });
--------------------------------------------------------------------------------
/jsTag/source/javascripts/models/default/jsTag.js:
--------------------------------------------------------------------------------
1 | var jsTag = angular.module('jsTag');
2 |
3 | // Tag Model
4 | jsTag.factory('JSTag', function() {
5 | function JSTag(value, id) {
6 | this.value = value;
7 | this.id = id;
8 | }
9 |
10 | return JSTag;
11 | });
--------------------------------------------------------------------------------
/jsTag/source/javascripts/models/default/jsTagsCollection.js:
--------------------------------------------------------------------------------
1 | var jsTag = angular.module('jsTag');
2 |
3 | // TagsCollection Model
4 | jsTag.factory('JSTagsCollection', ['JSTag', '$filter', function(JSTag, $filter) {
5 |
6 | // Constructor
7 | function JSTagsCollection(defaultTags) {
8 | this.tags = {};
9 | this.tagsCounter = 0;
10 | for (var defaultTagKey in defaultTags) {
11 | var defaultTagValue = defaultTags[defaultTagKey];
12 | this.addTag(defaultTagValue);
13 | }
14 |
15 | this._onAddListenerList = [];
16 | this._onRemoveListenerList = [];
17 |
18 | this.unsetActiveTags();
19 | this.unsetEditedTag();
20 |
21 | this._valueFormatter = null;
22 | this._valueValidator = null;
23 | }
24 |
25 | // *** Methods *** //
26 |
27 | // *** Object manipulation methods *** //
28 | JSTagsCollection.prototype.setValueValidator = function(validator) {
29 | this._valueValidator = validator;
30 | };
31 | JSTagsCollection.prototype.setValueFormatter = function(formatter) {
32 | this._valueFormatter = formatter;
33 | };
34 |
35 | // Adds a tag with received value
36 | JSTagsCollection.prototype.addTag = function(value) {
37 | var tagIndex = this.tagsCounter;
38 | this.tagsCounter++;
39 |
40 | var newTag = new JSTag(value, tagIndex);
41 | this.tags[tagIndex] = newTag;
42 | angular.forEach(this._onAddListenerList, function (callback) {
43 | callback(newTag);
44 | });
45 | };
46 |
47 | // Removes the received tag
48 | JSTagsCollection.prototype.removeTag = function(tagIndex) {
49 | var tag = this.tags[tagIndex];
50 | delete this.tags[tagIndex];
51 | angular.forEach(this._onRemoveListenerList, function (callback) {
52 | callback(tag);
53 | });
54 | };
55 |
56 | JSTagsCollection.prototype.onAdd = function onAdd(callback) {
57 | this._onAddListenerList.push(callback);
58 | };
59 |
60 | JSTagsCollection.prototype.onRemove = function onRemove(callback) {
61 | this._onRemoveListenerList.push(callback);
62 | };
63 |
64 | // Returns the number of tags in collection
65 | JSTagsCollection.prototype.getNumberOfTags = function() {
66 | return getNumberOfProperties(this.tags);
67 | };
68 |
69 | // Returns an array with all values of the tags
70 | JSTagsCollection.prototype.getTagValues = function() {
71 | var tagValues = [];
72 | for (var tag in this.tags) {
73 | tagValues.push(this.tags[tag].value);
74 | }
75 | return tagValues;
76 | };
77 |
78 | // Returns the previous tag before the tag received as input
79 | // Returns same tag if it's the first
80 | JSTagsCollection.prototype.getPreviousTag = function(tag) {
81 | var firstTag = getFirstProperty(this.tags);
82 | if (firstTag.id === tag.id) {
83 | // Return same tag if we reached the beginning
84 | return tag;
85 | } else {
86 | return getPreviousProperty(this.tags, tag.id);
87 | }
88 | };
89 |
90 | // Returns the next tag after the tag received as input
91 | // Returns same tag if it's the last
92 | JSTagsCollection.prototype.getNextTag = function(tag) {
93 | var lastTag = getLastProperty(this.tags);
94 | if (tag.id === lastTag.id) {
95 | // Return same tag if we reached the end
96 | return tag;
97 | } else {
98 | return getNextProperty(this.tags, tag.id);
99 | }
100 | };
101 |
102 | // *** Active methods *** //
103 |
104 | // Checks if a specific tag is active
105 | JSTagsCollection.prototype.isTagActive = function(tag) {
106 | return $filter("inArray")(tag, this._activeTags);
107 | };
108 |
109 | // Sets tag to active
110 | JSTagsCollection.prototype.setActiveTag = function(tag) {
111 | if (!this.isTagActive(tag)) {
112 | this._activeTags.push(tag);
113 | }
114 | };
115 |
116 | // Sets the last tag to be active
117 | JSTagsCollection.prototype.setLastTagActive = function() {
118 | if (getNumberOfProperties(this.tags) > 0) {
119 | var lastTag = getLastProperty(this.tags);
120 | this.setActiveTag(lastTag);
121 | }
122 | };
123 |
124 | // Unsets an active tag
125 | JSTagsCollection.prototype.unsetActiveTag = function(tag) {
126 | var removedTag = this._activeTags.splice(this._activeTags.indexOf(tag), 1);
127 | };
128 |
129 | // Unsets all active tag
130 | JSTagsCollection.prototype.unsetActiveTags = function() {
131 | this._activeTags = [];
132 | };
133 |
134 | // Returns a JSTag only if there is 1 exactly active tags, otherwise null
135 | JSTagsCollection.prototype.getActiveTag = function() {
136 | var activeTag = null;
137 | if (this._activeTags.length === 1) {
138 | activeTag = this._activeTags[0];
139 | }
140 |
141 | return activeTag;
142 | };
143 |
144 | // Returns number of active tags
145 | JSTagsCollection.prototype.getNumOfActiveTags = function() {
146 | return this._activeTags.length;
147 | };
148 |
149 | // *** Edit methods *** //
150 |
151 | // Gets the edited tag
152 | JSTagsCollection.prototype.getEditedTag = function() {
153 | return this._editedTag;
154 | };
155 |
156 | // Checks if a tag is edited
157 | JSTagsCollection.prototype.isTagEdited = function(tag) {
158 | return tag === this._editedTag;
159 | };
160 |
161 | // Sets the tag in the _editedTag member
162 | JSTagsCollection.prototype.setEditedTag = function(tag) {
163 | this._editedTag = tag;
164 | };
165 |
166 | // Unsets the 'edit' flag on a tag by it's given index
167 | JSTagsCollection.prototype.unsetEditedTag = function() {
168 | // Kill empty tags!
169 | if (this._editedTag !== undefined &&
170 | this._editedTag !== null &&
171 | this._editedTag.value === "") {
172 | this.removeTag(this._editedTag.id);
173 | }
174 |
175 | this._editedTag = null;
176 | };
177 |
178 | return JSTagsCollection;
179 | }]);
180 |
181 | // *** Extension methods used to iterate object like a dictionary. Used for the tags. *** //
182 | // TODO: Find another place for these extension methods. Maybe filter.js
183 | // TODO: Maybe use a regular array instead and delete them all :)
184 |
185 | // Gets the number of properties, including inherited
186 | function getNumberOfProperties(obj) {
187 | return Object.keys(obj).length;
188 | }
189 |
190 | // Get the first property of an object, including inherited properties
191 | function getFirstProperty(obj) {
192 | var keys = Object.keys(obj);
193 | return obj[keys[0]];
194 | }
195 |
196 | // Get the last property of an object, including inherited properties
197 | function getLastProperty(obj) {
198 | var keys = Object.keys(obj);
199 | return obj[keys[keys.length - 1]];
200 | }
201 |
202 | // Get the next property of an object whos' properties keys are numbers, including inherited properties
203 | function getNextProperty(obj, propertyId) {
204 | var keys = Object.keys(obj);
205 | var indexOfProperty = keys.indexOf(propertyId.toString());
206 | var keyOfNextProperty = keys[indexOfProperty + 1];
207 | return obj[keyOfNextProperty];
208 | }
209 |
210 | // Get the previous property of an object whos' properties keys are numbers, including inherited properties
211 | function getPreviousProperty(obj, propertyId) {
212 | var keys = Object.keys(obj);
213 | var indexOfProperty = keys.indexOf(propertyId.toString());
214 | var keyOfPreviousProperty = keys[indexOfProperty - 1];
215 | return obj[keyOfPreviousProperty];
216 | }
217 |
--------------------------------------------------------------------------------
/jsTag/source/javascripts/services.js:
--------------------------------------------------------------------------------
1 | var jsTag = angular.module('jsTag');
--------------------------------------------------------------------------------
/jsTag/source/javascripts/services/inputService.js:
--------------------------------------------------------------------------------
1 | var jsTag = angular.module('jsTag');
2 |
3 | // This service handles everything related to input (when to focus input, key pressing, breakcodeHit).
4 | jsTag.factory('InputService', ['$filter', function($filter) {
5 |
6 | // Constructor
7 | function InputService(options) {
8 | this.input = "";
9 | this.isWaitingForInput = options.autoFocus || false;
10 | this.options = options;
11 | }
12 |
13 | // *** Events *** //
14 |
15 | // Handles an input of a new tag keydown
16 | InputService.prototype.onKeydown = function(inputService, tagsCollection, options) {
17 | var e = options.$event;
18 | var $element = angular.element(e.currentTarget);
19 | var keycode = e.which;
20 | // In order to know how to handle a breakCode or a backspace, we must know if the typeahead
21 | // input value is empty or not. e.g. if user hits backspace and typeahead input is not empty
22 | // then we have nothing to do as user si not trying to remove a tag but simply tries to
23 | // delete some character in typeahead's input.
24 | // To know the value in the typeahead input, we can't use `this.input` because when
25 | // typeahead is in uneditable mode, the model (i.e. `this.input`) is not updated and is set
26 | // to undefined. So we have to fetch the value directly from the typeahead input element.
27 | //
28 | // We have to test this.input first, because $element.typeahead is a function and can be set
29 | // even if we are not in the typeahead mode.
30 | // So in this case, the value is always null and the preventDefault is never fired
31 | // This cause the form to always submit after hitting the Enter key.
32 | //var value = ($element.typeahead !== undefined) ? $element.typeahead('val') : this.input;
33 | var value = this.input || (($element.typeahead !== undefined) ? $element.typeahead('val') : undefined) ;
34 | var valueIsEmpty = (value === null || value === undefined || value === "");
35 |
36 | // Check if should break by breakcodes
37 | if ($filter("inArray")(keycode, this.options.breakCodes) !== false) {
38 |
39 | inputService.breakCodeHit(tagsCollection, this.options);
40 |
41 | // Trigger breakcodeHit event allowing extensions (used in twitter's typeahead directive)
42 | $element.triggerHandler('jsTag:breakcodeHit');
43 |
44 | // Do not trigger form submit if value is not empty.
45 | if (!valueIsEmpty) {
46 | e.preventDefault();
47 | }
48 |
49 | } else {
50 | switch (keycode) {
51 | case 9: // Tab
52 |
53 | break;
54 | case 37: // Left arrow
55 | case 8: // Backspace
56 | if (valueIsEmpty) {
57 | // TODO: Call removing tag event instead of calling a method, easier to customize
58 | tagsCollection.setLastTagActive();
59 | }
60 |
61 | break;
62 | }
63 | }
64 | };
65 |
66 | // Handles an input of an edited tag keydown
67 | InputService.prototype.tagInputKeydown = function(tagsCollection, options) {
68 | var e = options.$event;
69 | var keycode = e.which;
70 |
71 | // Check if should break by breakcodes
72 | if ($filter("inArray")(keycode, this.options.breakCodes) !== false) {
73 | this.breakCodeHitOnEdit(tagsCollection, options);
74 | }
75 | };
76 |
77 |
78 | InputService.prototype.onBlur = function(tagsCollection) {
79 | this.breakCodeHit(tagsCollection, this.options);
80 | };
81 |
82 | // *** Methods *** //
83 |
84 | InputService.prototype.resetInput = function() {
85 | var value = this.input;
86 | this.input = "";
87 | return value;
88 | };
89 |
90 | // Sets focus on input
91 | InputService.prototype.focusInput = function() {
92 | this.isWaitingForInput = true;
93 | };
94 |
95 | // breakCodeHit is called when finished creating tag
96 | InputService.prototype.breakCodeHit = function(tagsCollection, options) {
97 | if (this.input !== "") {
98 | if(tagsCollection._valueFormatter) {
99 | this.input = tagsCollection._valueFormatter(this.input);
100 | }
101 | if(tagsCollection._valueValidator) {
102 | if(!tagsCollection._valueValidator(this.input)) {
103 | return;
104 | };
105 | }
106 |
107 | var originalValue = this.resetInput();
108 |
109 | // Input is an object when using typeahead (the key is chosen by the user)
110 | if (originalValue instanceof Object)
111 | {
112 | originalValue = originalValue[options.tagDisplayKey || Object.keys(originalValue)[0]];
113 | }
114 |
115 | // Split value by spliter (usually ,)
116 | var values = originalValue.split(options.splitter);
117 | // Remove empty string objects from the values
118 | for (var i = 0; i < values.length; i++) {
119 | if (!values[i]) {
120 | values.splice(i, 1);
121 | i--;
122 | }
123 | }
124 |
125 | // Add tags to collection
126 | for (var key in values) {
127 | if ( ! values.hasOwnProperty(key)) continue; // for IE 8
128 | var value = values[key];
129 | tagsCollection.addTag(value);
130 | }
131 | }
132 | };
133 |
134 | // breakCodeHit is called when finished editing tag
135 | InputService.prototype.breakCodeHitOnEdit = function(tagsCollection, options) {
136 | // Input is an object when using typeahead (the key is chosen by the user)
137 | var editedTag = tagsCollection.getEditedTag();
138 | if (editedTag.value instanceof Object) {
139 | editedTag.value = editedTag.value[options.tagDisplayKey || Object.keys(editedTag.value)[0]];
140 | }
141 |
142 | tagsCollection.unsetEditedTag();
143 | this.isWaitingForInput = true;
144 | };
145 |
146 | return InputService;
147 | }]);
148 |
--------------------------------------------------------------------------------
/jsTag/source/javascripts/services/tagsInputService.js:
--------------------------------------------------------------------------------
1 | var jsTag = angular.module('jsTag');
2 |
3 | // TagsCollection Model
4 | jsTag.factory('TagsInputService', ['JSTag', 'JSTagsCollection', function(JSTag, JSTagsCollection) {
5 | // Constructor
6 | function TagsHandler(options) {
7 | this.options = options;
8 | var tags = options.tags;
9 |
10 | // Received ready JSTagsCollection
11 | if (tags && Object.getPrototypeOf(tags) === JSTagsCollection.prototype) {
12 | this.tagsCollection = tags;
13 | }
14 | // Received array with default tags or did not receive tags
15 | else {
16 | var defaultTags = options.defaultTags;
17 | this.tagsCollection = new JSTagsCollection(defaultTags);
18 | }
19 | this.shouldBlurActiveTag = true;
20 | }
21 |
22 | // *** Methods *** //
23 |
24 | TagsHandler.prototype.tagClicked = function(tag) {
25 | this.tagsCollection.setActiveTag(tag);
26 | };
27 |
28 | TagsHandler.prototype.tagDblClicked = function(tag) {
29 | var editAllowed = this.options.edit;
30 | if (editAllowed) {
31 | // Set tag as edit
32 | this.tagsCollection.setEditedTag(tag);
33 | }
34 | };
35 |
36 | // Keydown was pressed while a tag was active.
37 | // Important Note: The target of the event is actually a fake input used to capture the keydown.
38 | TagsHandler.prototype.onActiveTagKeydown = function(inputService, options) {
39 | var activeTag = this.tagsCollection.getActiveTag();
40 |
41 | // Do nothing in unexpected situations
42 | if (activeTag !== null) {
43 | var e = options.$event;
44 |
45 | // Mimics blur of the active tag though the focus is on the input.
46 | // This will cause expected features like unseting active tag
47 | var blurActiveTag = function() {
48 | // Expose the option not to blur the active tag
49 | if (this.shouldBlurActiveTag) {
50 | this.onActiveTagBlur(options);
51 | }
52 | };
53 |
54 | switch (e.which) {
55 | case 13: // Return
56 | var editAllowed = this.options.edit;
57 | if (editAllowed) {
58 | blurActiveTag.apply(this);
59 | this.tagsCollection.setEditedTag(activeTag);
60 | }
61 |
62 | break;
63 | case 8: // Backspace
64 | this.tagsCollection.removeTag(activeTag.id);
65 | inputService.isWaitingForInput = true;
66 |
67 | break;
68 | case 37: // Left arrow
69 | blurActiveTag.apply(this);
70 | var previousTag = this.tagsCollection.getPreviousTag(activeTag);
71 | this.tagsCollection.setActiveTag(previousTag);
72 |
73 | break;
74 | case 39: // Right arrow
75 | blurActiveTag.apply(this);
76 |
77 | var nextTag = this.tagsCollection.getNextTag(activeTag);
78 | if (nextTag !== activeTag) {
79 | this.tagsCollection.setActiveTag(nextTag);
80 | } else {
81 | inputService.isWaitingForInput = true;
82 | }
83 |
84 | break;
85 | }
86 | }
87 | };
88 |
89 | // Jumps when active tag calls blur event.
90 | // Because the focus is not on the tag's div itself but a fake input,
91 | // this is called also when clicking the active tag.
92 | // (Which is good because we want the tag to be unactive, then it will be reactivated on the click event)
93 | // It is also called when entering edit mode (ex. when pressing enter while active, it will call blur)
94 | TagsHandler.prototype.onActiveTagBlur = function(options) {
95 | var activeTag = this.tagsCollection.getActiveTag();
96 |
97 | // Do nothing in unexpected situations
98 | if (activeTag !== null) {
99 | this.tagsCollection.unsetActiveTag(activeTag);
100 | }
101 | };
102 |
103 | // Jumps when an edited tag calls blur event
104 | TagsHandler.prototype.onEditTagBlur = function(tagsCollection, inputService) {
105 | tagsCollection.unsetEditedTag();
106 | this.isWaitingForInput = true;
107 | };
108 |
109 | return TagsHandler;
110 | }]);
111 |
--------------------------------------------------------------------------------
/jsTag/source/stylesheets/js-tag.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Container *
3 | Explaining some of the CSS:
4 | # background-color: #FFFFFF; | To simulate an input box - in case background of the whole page is different
5 | */
6 | .jt-editor {
7 | padding: 6px 12px 5px 12px;
8 | display: block;
9 | height: auto;
10 |
11 | vertical-align: middle;
12 | font-size: 14px;
13 | line-height: 1.428571429;
14 | color: #555;
15 | border: 1px solid #BFBFBF;
16 | border-radius: 2px;
17 | cursor: text;
18 | background-color: #FFFFFF;
19 | }
20 |
21 | .jt-editor.focused-true {
22 | border-color: #66AFE9;
23 | outline: 0;
24 | }
25 |
26 | /* Tag */
27 | .jt-tag {
28 | background: #DEE7F8;
29 | border: 1px solid #949494;
30 | padding: 0px 0px 20px 2px;
31 | cursor: default;
32 |
33 | display: inline-block;
34 | -webkit-border-radius: 2px 3px 3px 2px;
35 | -moz-border-radius: 2px 3px 3px 2px;
36 | border-radius: 2px 3px 3px 2px;
37 | height: 22px;
38 | }
39 |
40 | /* Because bootstrap uses it and we don't want that if bootstrap is not included to look different */
41 | .jt-tag {
42 | box-sizing: border-box;
43 | }
44 |
45 | .jt-tag:hover {
46 | border-color: #BCBCBC;
47 | }
48 |
49 | /* Value inside jt-tag */
50 | .jt-tag .value {
51 | padding-left: 4px;
52 | }
53 |
54 | /* Tag when active */
55 | .jt-tag.active-true {
56 | border-color: rgba(82, 168, 236, 0.8);
57 | }
58 |
59 | /* Tag remove button ('x') */
60 | .jt-tag .remove-button {
61 | cursor: pointer;
62 | padding-right: 4px;
63 | }
64 | .jt-tag .remove-button:hover {
65 | font-weight: bold;
66 | }
67 |
68 | /* New tag input & Edit tag input */
69 | .jt-tag-new, .jt-tag-edit {
70 | border: none;
71 | outline: 0px;
72 | min-width: 50px; /* Will keep autogrow from lowering width more than 60 */
73 | }
74 |
75 | /* New tag input & Edit tag input & Tag */
76 | .jt-tag-new, .jt-tag-edit, .jt-tag {
77 | margin: 1px 4px 1px 1px;
78 | }
79 |
80 | /* Should not be displayed, only used to capture keydown */
81 | .jt-fake-input {
82 | float: left;
83 | position: absolute;
84 | left: -10000px;
85 | width: 1px;
86 | border: 0px;
87 | }
--------------------------------------------------------------------------------
/jsTag/source/templates/default/js-tag.html:
--------------------------------------------------------------------------------
1 |
4 |
7 |
10 |
14 |
15 | {{tag.value}}
16 |
17 | {{options.texts.removeSymbol}}
18 |
19 |
21 |
31 |
32 |
33 |
44 |
49 |
50 |
--------------------------------------------------------------------------------
/jsTag/source/templates/typeahead/js-tag.html:
--------------------------------------------------------------------------------
1 |
4 |
7 |
10 |
14 |
15 | {{tag.value}}
16 |
17 | {{options.texts.removeSymbol}}
18 |
19 |
21 |
33 |
34 |
35 |
49 |
54 |
55 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jsTag",
3 | "version": "0.3.7",
4 | "repository": {
5 | "type": "git",
6 | "url": "https://github.com/eranhirs/jsTag.git"
7 | },
8 | "keywords": [
9 | "jstag",
10 | "js-tag",
11 | "tags",
12 | "angular",
13 | "angularjs"
14 | ],
15 | "author": "eranhirs",
16 | "license": "MIT",
17 | "devDependencies": {
18 | "grunt": "~0.4.2",
19 | "grunt-contrib-jshint": "~0.6.5",
20 | "grunt-contrib-nodeunit": "~0.2.2",
21 | "grunt-contrib-concat": "~0.1.3",
22 | "grunt-contrib-uglify": "~0.2.7",
23 | "grunt-angular-templates": "~0.3.12",
24 | "grunt-contrib-clean": "~0.4.1"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------