├── .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 | --------------------------------------------------------------------------------