bootstrap-tagsinput-rails 0.3.2.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ba46d233e976dcfc103aa6f8dc6f240912699d5e
4
- data.tar.gz: 75fd8a1769bad8e53ae62be25376ee3dcfd9b88e
3
+ metadata.gz: 1f6fe08284f7aba9ccc3149d99de703f7c805714
4
+ data.tar.gz: e3430b9c3fcc1895e2eea24dfe7bd5abc3f869fa
5
5
  SHA512:
6
- metadata.gz: d9f9d69d7f137571b1b8fe3f4375538c1c89e5de18c4164c0614b63015d9d77c64857d579ed663506dc53098809fc00ff464426bed65ae3d7661f32e2df9acb0
7
- data.tar.gz: 648af3bc233397d3654e6fbb235b2c2aa03b6e16f04c87934f322d51f57b942dd95fd695f7d7ad2cb085621ec5fc78a17e13bc4596b9a770639c95412f32bfa3
6
+ metadata.gz: 4f9f0d6036feb6f1a6cd435bc36474fac13313668d88dcb8b6aafdf8179433385b8bbd5a0d68ef77268c1a03402349f3e3fdbeb67a611d8c8edfc84146c2d41c
7
+ data.tar.gz: ab28e5b5518897f9a6c1f26099fc709230d4d216e4935b74ff988cd330c27d8638fcc616486c99dbbb3ba28c3ae0387c6caf1da4080fdb1d419bfd1bb015d8b2
data/README.md CHANGED
@@ -4,6 +4,8 @@ Original Git source - https://github.com/timschlechter/bootstrap-tagsinput
4
4
 
5
5
  To gemify the assets of `bootstrap-tagsinput` jQuery plugin for Rails >= 3.1
6
6
 
7
+ [![Gem Version](https://badge.fury.io/rb/bootstrap-tagsinput-rails.png)](http://badge.fury.io/rb/bootstrap-tagsinput-rails)
8
+
7
9
  ## Compatibility
8
10
 
9
11
  Designed for Bootstrap 2.3.2 and 3
@@ -42,6 +44,13 @@ in form view, you should add `data-role='tagsinput'` within input tag as the fol
42
44
  <%= f.input :tag_list, input_html:{data:{role:'tagsinput'}} %>
43
45
  ```
44
46
 
47
+ Or if using Rails 4 with Bootstrap, use the following,
48
+
49
+ ```
50
+ <%= f.text_field :tag_list, 'data-role'=>'tagsinput' %>
51
+
52
+ ```
53
+
45
54
  That's it
46
55
 
47
56
  ## Contributing
@@ -1,7 +1,7 @@
1
1
  module Bootstrap
2
2
  module Tagsinput
3
3
  module Rails
4
- VERSION = "0.3.2.0"
4
+ VERSION = "0.4.2"
5
5
  end
6
6
  end
7
7
  end
@@ -1,80 +1,87 @@
1
- angular.module('bootstrap-tagsinput', [])
2
- .directive('bootstrapTagsinput', [function() {
3
-
4
- function getItemProperty(scope, property) {
5
- if (!property)
6
- return undefined;
7
-
8
- if (angular.isFunction(scope.$parent[property]))
9
- return scope.$parent[property];
10
-
11
- return function(item) {
12
- return item[property];
13
- };
14
- }
15
-
16
- return {
17
- restrict: 'EA',
18
- scope: {
19
- model: '=ngModel'
20
- },
21
- template: '<select multiple></select>',
22
- replace: false,
23
- link: function(scope, element, attrs) {
24
- $(function() {
25
- if (!angular.isArray(scope.model))
26
- scope.model = [];
27
-
28
- var select = $('select', element);
29
-
30
- select.tagsinput({
31
- typeahead : {
32
- source : angular.isFunction(scope.$parent[attrs.typeaheadSource]) ? scope.$parent[attrs.typeaheadSource] : null
33
- },
34
- itemValue: getItemProperty(scope, attrs.itemvalue),
35
- itemText : getItemProperty(scope, attrs.itemtext),
36
- tagClass : angular.isFunction(scope.$parent[attrs.tagclass]) ? scope.$parent[attrs.tagclass] : function(item) { return attrs.tagclass; }
37
- });
38
-
39
- for (var i = 0; i < scope.model.length; i++) {
40
- select.tagsinput('add', scope.model[i]);
41
- }
42
-
43
- select.on('itemAdded', function(event) {
44
- if (scope.model.indexOf(event.item) === -1)
45
- scope.model.push(event.item);
46
- });
47
-
48
- select.on('itemRemoved', function(event) {
49
- var idx = scope.model.indexOf(event.item);
50
- if (idx !== -1)
51
- scope.model.splice(idx, 1);
52
- });
53
-
54
- // create a shallow copy of model's current state, needed to determine
55
- // diff when model changes
56
- var prev = scope.model.slice();
57
- scope.$watch("model", function() {
58
- var added = scope.model.filter(function(i) {return prev.indexOf(i) === -1;}),
59
- removed = prev.filter(function(i) {return scope.model.indexOf(i) === -1;}),
60
- i;
61
-
62
- prev = scope.model.slice();
63
-
64
- // Remove tags no longer in binded model
65
- for (i = 0; i < removed.length; i++) {
66
- select.tagsinput('remove', removed[i]);
67
- }
68
-
69
- // Refresh remaining tags
70
- select.tagsinput('refresh');
71
-
72
- // Add new items in model as tags
73
- for (i = 0; i < added.length; i++) {
74
- select.tagsinput('add', added[i]);
75
- }
76
- }, true);
77
- });
78
- }
79
- };
80
- }]);
1
+ angular.module('bootstrap-tagsinput', [])
2
+ .directive('bootstrapTagsinput', [function() {
3
+
4
+ function getItemProperty(scope, property) {
5
+ if (!property)
6
+ return undefined;
7
+
8
+ if (angular.isFunction(scope.$parent[property]))
9
+ return scope.$parent[property];
10
+
11
+ return function(item) {
12
+ return item[property];
13
+ };
14
+ }
15
+
16
+ return {
17
+ restrict: 'EA',
18
+ scope: {
19
+ model: '=ngModel'
20
+ },
21
+ template: '<select multiple></select>',
22
+ replace: false,
23
+ link: function(scope, element, attrs) {
24
+ $(function() {
25
+ if (!angular.isArray(scope.model))
26
+ scope.model = [];
27
+
28
+ var select = $('select', element);
29
+ var typeaheadSourceArray = attrs.typeaheadSource ? attrs.typeaheadSource.split('.') : null;
30
+ var typeaheadSource = typeaheadSourceArray ?
31
+ (typeaheadSourceArray.length > 1 ?
32
+ scope.$parent[typeaheadSourceArray[0]][typeaheadSourceArray[1]]
33
+ : scope.$parent[typeaheadSourceArray[0]])
34
+ : null;
35
+
36
+ select.tagsinput(scope.$parent[attrs.options || ''] || {
37
+ typeahead : {
38
+ source : angular.isFunction(typeaheadSource) ? typeaheadSource : null
39
+ },
40
+ itemValue: getItemProperty(scope, attrs.itemvalue),
41
+ itemText : getItemProperty(scope, attrs.itemtext),
42
+ confirmKeys : getItemProperty(scope, attrs.confirmkeys) ? JSON.parse(attrs.confirmkeys) : [13],
43
+ tagClass : angular.isFunction(scope.$parent[attrs.tagclass]) ? scope.$parent[attrs.tagclass] : function(item) { return attrs.tagclass; }
44
+ });
45
+
46
+ for (var i = 0; i < scope.model.length; i++) {
47
+ select.tagsinput('add', scope.model[i]);
48
+ }
49
+
50
+ select.on('itemAdded', function(event) {
51
+ if (scope.model.indexOf(event.item) === -1)
52
+ scope.model.push(event.item);
53
+ });
54
+
55
+ select.on('itemRemoved', function(event) {
56
+ var idx = scope.model.indexOf(event.item);
57
+ if (idx !== -1)
58
+ scope.model.splice(idx, 1);
59
+ });
60
+
61
+ // create a shallow copy of model's current state, needed to determine
62
+ // diff when model changes
63
+ var prev = scope.model.slice();
64
+ scope.$watch("model", function() {
65
+ var added = scope.model.filter(function(i) {return prev.indexOf(i) === -1;}),
66
+ removed = prev.filter(function(i) {return scope.model.indexOf(i) === -1;}),
67
+ i;
68
+
69
+ prev = scope.model.slice();
70
+
71
+ // Remove tags no longer in binded model
72
+ for (i = 0; i < removed.length; i++) {
73
+ select.tagsinput('remove', removed[i]);
74
+ }
75
+
76
+ // Refresh remaining tags
77
+ select.tagsinput('refresh');
78
+
79
+ // Add new items in model as tags
80
+ for (i = 0; i < added.length; i++) {
81
+ select.tagsinput('add', added[i]);
82
+ }
83
+ }, true);
84
+ });
85
+ }
86
+ };
87
+ }]);
@@ -0,0 +1,7 @@
1
+ /*
2
+ * bootstrap-tagsinput v0.4.2 by Tim Schlechter
3
+ *
4
+ */
5
+
6
+ angular.module("bootstrap-tagsinput",[]).directive("bootstrapTagsinput",[function(){function a(a,b){return b?angular.isFunction(a.$parent[b])?a.$parent[b]:function(a){return a[b]}:void 0}return{restrict:"EA",scope:{model:"=ngModel"},template:"<select multiple></select>",replace:!1,link:function(b,c,d){$(function(){angular.isArray(b.model)||(b.model=[]);var e=$("select",c),f=d.typeaheadSource?d.typeaheadSource.split("."):null,g=f?f.length>1?b.$parent[f[0]][f[1]]:b.$parent[f[0]]:null;e.tagsinput(b.$parent[d.options||""]||{typeahead:{source:angular.isFunction(g)?g:null},itemValue:a(b,d.itemvalue),itemText:a(b,d.itemtext),confirmKeys:a(b,d.confirmkeys)?JSON.parse(d.confirmkeys):[13],tagClass:angular.isFunction(b.$parent[d.tagclass])?b.$parent[d.tagclass]:function(){return d.tagclass}});for(var h=0;h<b.model.length;h++)e.tagsinput("add",b.model[h]);e.on("itemAdded",function(a){-1===b.model.indexOf(a.item)&&b.model.push(a.item)}),e.on("itemRemoved",function(a){var c=b.model.indexOf(a.item);-1!==c&&b.model.splice(c,1)});var i=b.model.slice();b.$watch("model",function(){var a,c=b.model.filter(function(a){return-1===i.indexOf(a)}),d=i.filter(function(a){return-1===b.model.indexOf(a)});for(i=b.model.slice(),a=0;a<d.length;a++)e.tagsinput("remove",d[a]);for(e.tagsinput("refresh"),a=0;a<c.length;a++)e.tagsinput("add",c[a])},!0)})}}}]);
7
+ //# sourceMappingURL=bootstrap-tagsinput.min.js.map
@@ -1,405 +1,617 @@
1
- (function ($) {
2
- "use strict";
3
-
4
- var defaultOptions = {
5
- tagClass: function(item) {
6
- return 'label label-info';
7
- },
8
- itemValue: function(item) {
9
- return item ? item.toString() : item;
10
- },
11
- itemText: function(item) {
12
- return this.itemValue(item);
13
- },
14
- freeInput : true
15
- };
16
-
17
- function TagsInput(element, options) {
18
- this.itemsArray = [];
19
-
20
- this.$element = $(element);
21
- this.$element.hide();
22
-
23
- this.isSelect = (element.tagName === 'SELECT');
24
- this.multiple = (this.isSelect && element.hasAttribute('multiple'));
25
- this.objectItems = options && options.itemValue;
26
-
27
- this.$container = $('<div class="bootstrap-tagsinput"></div>');
28
- this.$input = $('<input size="1" type="text" />').appendTo(this.$container);
29
-
30
- this.$element.after(this.$container);
31
-
32
- this.build(options);
33
- }
34
-
35
- TagsInput.prototype = {
36
- constructor: TagsInput,
37
-
38
- add: function(item, dontPushVal) {
39
- var self = this;
40
-
41
- // Ignore falsey values, except false
42
- if (item !== false && !item)
43
- return;
44
-
45
- // Throw an error when trying to add an object while the itemValue option was not set
46
- if (typeof item === "object" && !self.objectItems)
47
- throw("Can't add objects when itemValue option is not set");
48
-
49
- // Ignore strings only containg whitespace
50
- if (item.toString().match(/^\s*$/))
51
- return;
52
-
53
- // If SELECT but not multiple, remove current tag
54
- if (self.isSelect && !self.multiple && self.itemsArray.length > 0)
55
- self.remove(self.itemsArray[0]);
56
-
57
- if (typeof item === "string" && this.$element[0].tagName === 'INPUT') {
58
- var items = item.split(',');
59
- if (items.length > 1) {
60
- for (var i = 0; i < items.length; i++) {
61
- this.add(items[i], true);
62
- }
63
-
64
- if (!dontPushVal)
65
- self.pushVal();
66
- return;
67
- }
68
- }
69
-
70
- // Ignore items allready added
71
- var itemValue = self.options.itemValue(item),
72
- itemText = self.options.itemText(item),
73
- tagClass = self.options.tagClass(item);
74
-
75
- if ($.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0])
76
- return;
77
-
78
- // register item in internal array and map
79
- self.itemsArray.push(item);
80
-
81
- // add a tag element
82
- var $tag = $('<span class="tag ' + htmlEncode(tagClass) + '">' + htmlEncode(itemText) + '<span data-role="remove"></span></span>');
83
- $tag.data('item', item);
84
- self.$input.before($tag);
85
-
86
- // add <option /> if item represents a value not present in one of the <select />'s options
87
- if (self.isSelect && !$('option[value="' + escape(itemValue) + '"]')[0]) {
88
- var $option = $('<option selected>' + htmlEncode(itemText) + '</option>');
89
- $option.data('item', item);
90
- $option.attr('value', itemValue);
91
- self.$element.append($option);
92
- }
93
-
94
- if (!dontPushVal)
95
- self.pushVal();
96
-
97
- self.$element.trigger($.Event('itemAdded', { item: item }));
98
- },
99
-
100
- remove: function(item, dontPushVal) {
101
- var self = this;
102
-
103
- if (self.objectItems) {
104
- if (typeof item === "object")
105
- item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == self.options.itemValue(item); } )[0];
106
- else
107
- item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == item; } )[0];
108
- }
109
-
110
- if (item) {
111
- $('.tag', self.$container).filter(function() { return $(this).data('item') === item; }).remove();
112
- $('option', self.$element).filter(function() { return $(this).data('item') === item; }).remove();
113
- self.itemsArray.splice(self.itemsArray.indexOf(item), 1);
114
- }
115
-
116
- if (!dontPushVal)
117
- self.pushVal();
118
-
119
- self.$element.trigger($.Event('itemRemoved', { item: item }));
120
- },
121
-
122
- removeAll: function() {
123
- var self = this;
124
-
125
- $('.tag', self.$container).remove();
126
- $('option', self.$element).remove();
127
-
128
- while(self.itemsArray.length > 0)
129
- self.itemsArray.pop();
130
-
131
- self.pushVal();
132
- },
133
-
134
- refresh: function() {
135
- var self = this;
136
- $('.tag', self.$container).each(function() {
137
- var $tag = $(this),
138
- item = $tag.data('item'),
139
- itemValue = self.options.itemValue(item),
140
- itemText = self.options.itemText(item),
141
- tagClass = self.options.tagClass(item);
142
-
143
- // Update tag's class and inner text
144
- $tag.attr('class', null);
145
- $tag.addClass('tag ' + htmlEncode(tagClass));
146
- $tag.contents().filter(function() {
147
- return this.nodeType == 3;
148
- })[0].nodeValue = htmlEncode(itemText);
149
-
150
- if (self.isSelect) {
151
- var option = $('option', self.$element).filter(function() { return $(this).data('item') === item; });
152
- option.attr('value', itemValue);
153
- }
154
- });
155
- },
156
-
157
- // Returns the items added as tags
158
- items: function() {
159
- return this.itemsArray;
160
- },
161
-
162
- // Assembly value by retrieving the value of each item, and set it on the element.
163
- pushVal: function() {
164
- var self = this,
165
- val = $.map(self.items(), function(item) {
166
- return self.options.itemValue(item).toString();
167
- });
168
-
169
- self.$element.val(val, true).trigger('change');
170
- },
171
-
172
- build: function(options) {
173
- var self = this;
174
-
175
- self.options = $.extend({}, defaultOptions, options);
176
- var typeahead = self.options.typeahead || {};
177
-
178
- // When itemValue is set, freeInput should always be false
179
- if (self.objectItems)
180
- self.options.freeInput = false;
181
-
182
- makeOptionItemFunction(self.options, 'itemValue');
183
- makeOptionItemFunction(self.options, 'itemText');
184
- makeOptionItemFunction(self.options, 'tagClass');
185
-
186
- // for backwards compatibility, self.options.source is deprecated
187
- if (self.options.source)
188
- typeahead.source = self.options.source;
189
-
190
- if (typeahead.source && $.fn.typeahead) {
191
- makeOptionFunction(typeahead, 'source');
192
-
193
- self.$input.typeahead({
194
- source: function (query, process) {
195
- function processItems(items) {
196
- var texts = [];
197
-
198
- for (var i = 0; i < items.length; i++) {
199
- var text = self.options.itemText(items[i]);
200
- map[text] = items[i];
201
- texts.push(text);
202
- }
203
- process(texts);
204
- }
205
-
206
- this.map = {};
207
- var map = this.map,
208
- data = typeahead.source(query);
209
-
210
- if ($.isFunction(data.success)) {
211
- // support for Angular promises
212
- data.success(processItems);
213
- } else {
214
- // support for functions and jquery promises
215
- $.when(data)
216
- .then(processItems);
217
- }
218
- },
219
- updater: function (text) {
220
- self.add(this.map[text]);
221
- },
222
- matcher: function (text) {
223
- return (text.toLowerCase().indexOf(this.query.trim().toLowerCase()) !== -1);
224
- },
225
- sorter: function (texts) {
226
- return texts.sort();
227
- },
228
- highlighter: function (text) {
229
- var regex = new RegExp( '(' + this.query + ')', 'gi' );
230
- return text.replace( regex, "<strong>$1</strong>" );
231
- }
232
- });
233
- }
234
-
235
- self.$container.on('click', $.proxy(function(event) {
236
- self.$input.focus();
237
- }, self));
238
-
239
- self.$container.on('keydown', 'input', $.proxy(function(event) {
240
- var $input = $(event.target);
241
- switch (event.which) {
242
- // BACKSPACE
243
- case 8:
244
- if (doGetCaretPosition($input[0]) === 0) {
245
- var prev = $input.prev();
246
- if (prev) {
247
- self.remove(prev.data('item'));
248
- }
249
- }
250
- break;
251
-
252
- // DELETE
253
- case 46:
254
- if (doGetCaretPosition($input[0]) === 0) {
255
- var next = $input.next();
256
- if (next) {
257
- self.remove(next.data('item'));
258
- }
259
- }
260
- break;
261
-
262
- // LEFT ARROW
263
- case 37:
264
- // Try to move the input before the previous tag
265
- var $prevTag = $input.prev();
266
- if ($input.val().length === 0 && $prevTag[0]) {
267
- $prevTag.before($input);
268
- $input.focus();
269
- }
270
- break;
271
- // LEFT ARROW
272
- case 39:
273
- // Try to move the input before the previous tag
274
- var $nextTag = $input.next();
275
- if ($input.val().length === 0 && $nextTag[0]) {
276
- $nextTag.after($input);
277
- $input.focus();
278
- }
279
- break;
280
- // ENTER
281
- case 13:
282
- if (self.options.freeInput) {
283
- self.add($input.val());
284
- $input.val('');
285
- event.preventDefault();
286
- }
287
- break;
288
-
289
- }
290
-
291
- $input.attr('size', Math.max(1, $input.val().length));
292
- }, self));
293
-
294
- // Remove icon clicked
295
- self.$container.on('click', '[data-role=remove]', $.proxy(function(event) {
296
- self.remove($(event.target).closest('.tag').data('item'));
297
- }, self));
298
-
299
- if (self.$element[0].tagName === 'INPUT') {
300
- self.add(self.$element.val());
301
- } else {
302
- $('option', self.$element).each(function() {
303
- self.add($(this).attr('value'), true);
304
- });
305
- }
306
- },
307
-
308
- destroy: function() {
309
- var self = this;
310
-
311
- // Unbind events
312
- self.$container.off('keypress', 'input');
313
- self.$container.off('click', '[50role=remove]');
314
-
315
- self.$container.remove();
316
- self.$element.removeData('tagsinput');
317
- self.$element.show();
318
- },
319
-
320
- focus: function() {
321
- this.$input.focus();
322
- }
323
- };
324
-
325
- // Register JQuery plugin
326
- $.fn.tagsinput = function(arg1, arg2) {
327
- var results = [];
328
-
329
- this.each(function() {
330
- var tagsinput = $(this).data('tagsinput');
331
-
332
- // Initialize a new tags input
333
- if (!tagsinput) {
334
- tagsinput = new TagsInput(this, arg1);
335
- $(this).data('tagsinput', tagsinput);
336
- results.push(tagsinput);
337
-
338
- if (this.tagName === 'SELECT') {
339
- $('option', $(this)).attr('selected', 'selected');
340
- }
341
-
342
- // Init tags from $(this).val()
343
- $(this).val($(this).val());
344
- } else {
345
- // Invoke function on existing tags input
346
- var retVal = tagsinput[arg1](arg2);
347
- if (retVal !== undefined)
348
- results.push(retVal);
349
- }
350
- });
351
-
352
- if ( typeof arg1 == 'string') {
353
- // Return the results from the invoked function calls
354
- return results.length > 1 ? results : results[0];
355
- } else {
356
- return results;
357
- }
358
- };
359
-
360
- $.fn.tagsinput.Constructor = TagsInput;
361
-
362
- // Most options support both a string or number as well as a function as
363
- // option value. This function makes sure that the option with the given
364
- // key in the given options is wrapped in a function
365
- function makeOptionItemFunction(options, key) {
366
- if (typeof options[key] !== 'function') {
367
- var value = options[key];
368
- options[key] = function(item) { return item[value]; };
369
- }
370
- }
371
- function makeOptionFunction(options, key) {
372
- if (typeof options[key] !== 'function') {
373
- var value = options[key];
374
- options[key] = function() { return value; };
375
- }
376
- }
377
- // HtmlEncodes the given value
378
- var htmlEncodeContainer = $('<div />');
379
- function htmlEncode(value) {
380
- if (value) {
381
- return htmlEncodeContainer.text(value).html();
382
- } else {
383
- return '';
384
- }
385
- }
386
-
387
- // Returns the position of the caret in the given input field
388
- // http://flightschool.acylt.com/devnotes/caret-position-woes/
389
- function doGetCaretPosition(oField) {
390
- var iCaretPos = 0;
391
- if (document.selection) {
392
- oField.focus ();
393
- var oSel = document.selection.createRange();
394
- oSel.moveStart ('character', -oField.value.length);
395
- iCaretPos = oSel.text.length;
396
- } else if (oField.selectionStart || oField.selectionStart == '0') {
397
- iCaretPos = oField.selectionStart;
398
- }
399
- return (iCaretPos);
400
- }
401
-
402
- $(function() {
403
- $("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput();
404
- });
405
- })(window.jQuery);
1
+ (function ($) {
2
+ "use strict";
3
+
4
+ var defaultOptions = {
5
+ tagClass: function(item) {
6
+ return 'label label-info';
7
+ },
8
+ itemValue: function(item) {
9
+ return item ? item.toString() : item;
10
+ },
11
+ itemText: function(item) {
12
+ return this.itemValue(item);
13
+ },
14
+ freeInput: true,
15
+ addOnBlur: true,
16
+ maxTags: undefined,
17
+ maxChars: undefined,
18
+ confirmKeys: [13, 44],
19
+ onTagExists: function(item, $tag) {
20
+ $tag.hide().fadeIn();
21
+ },
22
+ trimValue: false,
23
+ allowDuplicates: false
24
+ };
25
+
26
+ /**
27
+ * Constructor function
28
+ */
29
+ function TagsInput(element, options) {
30
+ this.itemsArray = [];
31
+
32
+ this.$element = $(element);
33
+ this.$element.hide();
34
+
35
+ this.isSelect = (element.tagName === 'SELECT');
36
+ this.multiple = (this.isSelect && element.hasAttribute('multiple'));
37
+ this.objectItems = options && options.itemValue;
38
+ this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : '';
39
+ this.inputSize = Math.max(1, this.placeholderText.length);
40
+
41
+ this.$container = $('<div class="bootstrap-tagsinput"></div>');
42
+ this.$input = $('<input type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container);
43
+
44
+ this.$element.after(this.$container);
45
+
46
+ var inputWidth = (this.inputSize < 3 ? 3 : this.inputSize) + "em";
47
+ this.$input.get(0).style.cssText = "width: " + inputWidth + " !important;";
48
+ this.build(options);
49
+ }
50
+
51
+ TagsInput.prototype = {
52
+ constructor: TagsInput,
53
+
54
+ /**
55
+ * Adds the given item as a new tag. Pass true to dontPushVal to prevent
56
+ * updating the elements val()
57
+ */
58
+ add: function(item, dontPushVal) {
59
+ var self = this;
60
+
61
+ if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags)
62
+ return;
63
+
64
+ // Ignore falsey values, except false
65
+ if (item !== false && !item)
66
+ return;
67
+
68
+ // Trim value
69
+ if (typeof item === "string" && self.options.trimValue) {
70
+ item = $.trim(item);
71
+ }
72
+
73
+ // Throw an error when trying to add an object while the itemValue option was not set
74
+ if (typeof item === "object" && !self.objectItems)
75
+ throw("Can't add objects when itemValue option is not set");
76
+
77
+ // Ignore strings only containg whitespace
78
+ if (item.toString().match(/^\s*$/))
79
+ return;
80
+
81
+ // If SELECT but not multiple, remove current tag
82
+ if (self.isSelect && !self.multiple && self.itemsArray.length > 0)
83
+ self.remove(self.itemsArray[0]);
84
+
85
+ if (typeof item === "string" && this.$element[0].tagName === 'INPUT') {
86
+ var items = item.split(',');
87
+ if (items.length > 1) {
88
+ for (var i = 0; i < items.length; i++) {
89
+ this.add(items[i], true);
90
+ }
91
+
92
+ if (!dontPushVal)
93
+ self.pushVal();
94
+ return;
95
+ }
96
+ }
97
+
98
+ var itemValue = self.options.itemValue(item),
99
+ itemText = self.options.itemText(item),
100
+ tagClass = self.options.tagClass(item);
101
+
102
+ // Ignore items allready added
103
+ var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0];
104
+ if (existing && !self.options.allowDuplicates) {
105
+ // Invoke onTagExists
106
+ if (self.options.onTagExists) {
107
+ var $existingTag = $(".tag", self.$container).filter(function() { return $(this).data("item") === existing; });
108
+ self.options.onTagExists(item, $existingTag);
109
+ }
110
+ return;
111
+ }
112
+
113
+ // if length greater than limit
114
+ if (self.items().toString().length + item.length + 1 > self.options.maxInputLength)
115
+ return;
116
+
117
+ // raise beforeItemAdd arg
118
+ var beforeItemAddEvent = $.Event('beforeItemAdd', { item: item, cancel: false });
119
+ self.$element.trigger(beforeItemAddEvent);
120
+ if (beforeItemAddEvent.cancel)
121
+ return;
122
+
123
+ // register item in internal array and map
124
+ self.itemsArray.push(item);
125
+
126
+ // add a tag element
127
+ var $tag = $('<span class="tag ' + htmlEncode(tagClass) + '">' + htmlEncode(itemText) + '<span data-role="remove"></span></span>');
128
+ $tag.data('item', item);
129
+ self.findInputWrapper().before($tag);
130
+ $tag.after(' ');
131
+
132
+ // add <option /> if item represents a value not present in one of the <select />'s options
133
+ if (self.isSelect && !$('option[value="' + encodeURIComponent(itemValue) + '"]',self.$element)[0]) {
134
+ var $option = $('<option selected>' + htmlEncode(itemText) + '</option>');
135
+ $option.data('item', item);
136
+ $option.attr('value', itemValue);
137
+ self.$element.append($option);
138
+ }
139
+
140
+ if (!dontPushVal)
141
+ self.pushVal();
142
+
143
+ // Add class when reached maxTags
144
+ if (self.options.maxTags === self.itemsArray.length || self.items().toString().length === self.options.maxInputLength)
145
+ self.$container.addClass('bootstrap-tagsinput-max');
146
+
147
+ self.$element.trigger($.Event('itemAdded', { item: item }));
148
+ },
149
+
150
+ /**
151
+ * Removes the given item. Pass true to dontPushVal to prevent updating the
152
+ * elements val()
153
+ */
154
+ remove: function(item, dontPushVal) {
155
+ var self = this;
156
+
157
+ if (self.objectItems) {
158
+ if (typeof item === "object")
159
+ item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == self.options.itemValue(item); } );
160
+ else
161
+ item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == item; } );
162
+
163
+ item = item[item.length-1];
164
+ }
165
+
166
+ if (item) {
167
+ var beforeItemRemoveEvent = $.Event('beforeItemRemove', { item: item, cancel: false });
168
+ self.$element.trigger(beforeItemRemoveEvent);
169
+ if (beforeItemRemoveEvent.cancel)
170
+ return;
171
+
172
+ $('.tag', self.$container).filter(function() { return $(this).data('item') === item; }).remove();
173
+ $('option', self.$element).filter(function() { return $(this).data('item') === item; }).remove();
174
+ if($.inArray(item, self.itemsArray) !== -1)
175
+ self.itemsArray.splice($.inArray(item, self.itemsArray), 1);
176
+ }
177
+
178
+ if (!dontPushVal)
179
+ self.pushVal();
180
+
181
+ // Remove class when reached maxTags
182
+ if (self.options.maxTags > self.itemsArray.length)
183
+ self.$container.removeClass('bootstrap-tagsinput-max');
184
+
185
+ self.$element.trigger($.Event('itemRemoved', { item: item }));
186
+ },
187
+
188
+ /**
189
+ * Removes all items
190
+ */
191
+ removeAll: function() {
192
+ var self = this;
193
+
194
+ $('.tag', self.$container).remove();
195
+ $('option', self.$element).remove();
196
+
197
+ while(self.itemsArray.length > 0)
198
+ self.itemsArray.pop();
199
+
200
+ self.pushVal();
201
+ },
202
+
203
+ /**
204
+ * Refreshes the tags so they match the text/value of their corresponding
205
+ * item.
206
+ */
207
+ refresh: function() {
208
+ var self = this;
209
+ $('.tag', self.$container).each(function() {
210
+ var $tag = $(this),
211
+ item = $tag.data('item'),
212
+ itemValue = self.options.itemValue(item),
213
+ itemText = self.options.itemText(item),
214
+ tagClass = self.options.tagClass(item);
215
+
216
+ // Update tag's class and inner text
217
+ $tag.attr('class', null);
218
+ $tag.addClass('tag ' + htmlEncode(tagClass));
219
+ $tag.contents().filter(function() {
220
+ return this.nodeType == 3;
221
+ })[0].nodeValue = htmlEncode(itemText);
222
+
223
+ if (self.isSelect) {
224
+ var option = $('option', self.$element).filter(function() { return $(this).data('item') === item; });
225
+ option.attr('value', itemValue);
226
+ }
227
+ });
228
+ },
229
+
230
+ /**
231
+ * Returns the items added as tags
232
+ */
233
+ items: function() {
234
+ return this.itemsArray;
235
+ },
236
+
237
+ /**
238
+ * Assembly value by retrieving the value of each item, and set it on the
239
+ * element.
240
+ */
241
+ pushVal: function() {
242
+ var self = this,
243
+ val = $.map(self.items(), function(item) {
244
+ return self.options.itemValue(item).toString();
245
+ });
246
+
247
+ self.$element.val(val, true).trigger('change');
248
+ },
249
+
250
+ /**
251
+ * Initializes the tags input behaviour on the element
252
+ */
253
+ build: function(options) {
254
+ var self = this;
255
+
256
+ self.options = $.extend({}, defaultOptions, options);
257
+ // When itemValue is set, freeInput should always be false
258
+ if (self.objectItems)
259
+ self.options.freeInput = false;
260
+
261
+ makeOptionItemFunction(self.options, 'itemValue');
262
+ makeOptionItemFunction(self.options, 'itemText');
263
+ makeOptionFunction(self.options, 'tagClass');
264
+
265
+ // Typeahead Bootstrap version 2.3.2
266
+ if (self.options.typeahead) {
267
+ var typeahead = self.options.typeahead || {};
268
+
269
+ makeOptionFunction(typeahead, 'source');
270
+
271
+ self.$input.typeahead($.extend({}, typeahead, {
272
+ source: function (query, process) {
273
+ function processItems(items) {
274
+ var texts = [];
275
+
276
+ for (var i = 0; i < items.length; i++) {
277
+ var text = self.options.itemText(items[i]);
278
+ map[text] = items[i];
279
+ texts.push(text);
280
+ }
281
+ process(texts);
282
+ }
283
+
284
+ this.map = {};
285
+ var map = this.map,
286
+ data = typeahead.source(query);
287
+
288
+ if ($.isFunction(data.success)) {
289
+ // support for Angular callbacks
290
+ data.success(processItems);
291
+ } else if ($.isFunction(data.then)) {
292
+ // support for Angular promises
293
+ data.then(processItems);
294
+ } else {
295
+ // support for functions and jquery promises
296
+ $.when(data)
297
+ .then(processItems);
298
+ }
299
+ },
300
+ updater: function (text) {
301
+ self.add(this.map[text]);
302
+ },
303
+ matcher: function (text) {
304
+ return (text.toLowerCase().indexOf(this.query.trim().toLowerCase()) !== -1);
305
+ },
306
+ sorter: function (texts) {
307
+ return texts.sort();
308
+ },
309
+ highlighter: function (text) {
310
+ var regex = new RegExp( '(' + this.query + ')', 'gi' );
311
+ return text.replace( regex, "<strong>$1</strong>" );
312
+ }
313
+ }));
314
+ }
315
+
316
+ // typeahead.js
317
+ if (self.options.typeaheadjs) {
318
+ var typeaheadjs = self.options.typeaheadjs || {};
319
+
320
+ self.$input.typeahead(null, typeaheadjs).on('typeahead:selected', $.proxy(function (obj, datum) {
321
+ if (typeaheadjs.valueKey)
322
+ self.add(datum[typeaheadjs.valueKey]);
323
+ else
324
+ self.add(datum);
325
+ self.$input.typeahead('val', '');
326
+ }, self));
327
+ }
328
+
329
+ self.$container.on('click', $.proxy(function(event) {
330
+ if (! self.$element.attr('disabled')) {
331
+ self.$input.removeAttr('disabled');
332
+ }
333
+ self.$input.focus();
334
+ }, self));
335
+
336
+ if (self.options.addOnBlur && self.options.freeInput) {
337
+ self.$input.on('focusout', $.proxy(function(event) {
338
+ // HACK: only process on focusout when no typeahead opened, to
339
+ // avoid adding the typeahead text as tag
340
+ if ($('.typeahead, .twitter-typeahead', self.$container).length === 0) {
341
+ self.add(self.$input.val());
342
+ self.$input.val('');
343
+ }
344
+ }, self));
345
+ }
346
+
347
+
348
+ self.$container.on('keydown', 'input', $.proxy(function(event) {
349
+ var $input = $(event.target),
350
+ $inputWrapper = self.findInputWrapper();
351
+
352
+ if (self.$element.attr('disabled')) {
353
+ self.$input.attr('disabled', 'disabled');
354
+ return;
355
+ }
356
+
357
+ switch (event.which) {
358
+ // BACKSPACE
359
+ case 8:
360
+ if (doGetCaretPosition($input[0]) === 0) {
361
+ var prev = $inputWrapper.prev();
362
+ if (prev) {
363
+ self.remove(prev.data('item'));
364
+ }
365
+ }
366
+ break;
367
+
368
+ // DELETE
369
+ case 46:
370
+ if (doGetCaretPosition($input[0]) === 0) {
371
+ var next = $inputWrapper.next();
372
+ if (next) {
373
+ self.remove(next.data('item'));
374
+ }
375
+ }
376
+ break;
377
+
378
+ // LEFT ARROW
379
+ case 37:
380
+ // Try to move the input before the previous tag
381
+ var $prevTag = $inputWrapper.prev();
382
+ if ($input.val().length === 0 && $prevTag[0]) {
383
+ $prevTag.before($inputWrapper);
384
+ $input.focus();
385
+ }
386
+ break;
387
+ // RIGHT ARROW
388
+ case 39:
389
+ // Try to move the input after the next tag
390
+ var $nextTag = $inputWrapper.next();
391
+ if ($input.val().length === 0 && $nextTag[0]) {
392
+ $nextTag.after($inputWrapper);
393
+ $input.focus();
394
+ }
395
+ break;
396
+ default:
397
+ // ignore
398
+ }
399
+
400
+ // Reset internal input's size
401
+ var textLength = $input.val().length,
402
+ wordSpace = Math.ceil(textLength / 5),
403
+ size = textLength + wordSpace + 1;
404
+ $input.attr('size', Math.max(this.inputSize, $input.val().length));
405
+ }, self));
406
+
407
+ self.$container.on('keypress', 'input', $.proxy(function(event) {
408
+ var $input = $(event.target);
409
+
410
+ if (self.$element.attr('disabled')) {
411
+ self.$input.attr('disabled', 'disabled');
412
+ return;
413
+ }
414
+
415
+ var text = $input.val(),
416
+ maxLengthReached = self.options.maxChars && text.length >= self.options.maxChars;
417
+ if (self.options.freeInput && (keyCombinationInList(event, self.options.confirmKeys) || maxLengthReached)) {
418
+ self.add(maxLengthReached ? text.substr(0, self.options.maxChars) : text);
419
+ $input.val('');
420
+ event.preventDefault();
421
+ }
422
+
423
+ // Reset internal input's size
424
+ var textLength = $input.val().length,
425
+ wordSpace = Math.ceil(textLength / 5),
426
+ size = textLength + wordSpace + 1;
427
+ $input.attr('size', Math.max(this.inputSize, $input.val().length));
428
+ }, self));
429
+
430
+ // Remove icon clicked
431
+ self.$container.on('click', '[data-role=remove]', $.proxy(function(event) {
432
+ if (self.$element.attr('disabled')) {
433
+ return;
434
+ }
435
+ self.remove($(event.target).closest('.tag').data('item'));
436
+ }, self));
437
+
438
+ // Only add existing value as tags when using strings as tags
439
+ if (self.options.itemValue === defaultOptions.itemValue) {
440
+ if (self.$element[0].tagName === 'INPUT') {
441
+ self.add(self.$element.val());
442
+ } else {
443
+ $('option', self.$element).each(function() {
444
+ self.add($(this).attr('value'), true);
445
+ });
446
+ }
447
+ }
448
+ },
449
+
450
+ /**
451
+ * Removes all tagsinput behaviour and unregsiter all event handlers
452
+ */
453
+ destroy: function() {
454
+ var self = this;
455
+
456
+ // Unbind events
457
+ self.$container.off('keypress', 'input');
458
+ self.$container.off('click', '[role=remove]');
459
+
460
+ self.$container.remove();
461
+ self.$element.removeData('tagsinput');
462
+ self.$element.show();
463
+ },
464
+
465
+ /**
466
+ * Sets focus on the tagsinput
467
+ */
468
+ focus: function() {
469
+ this.$input.focus();
470
+ },
471
+
472
+ /**
473
+ * Returns the internal input element
474
+ */
475
+ input: function() {
476
+ return this.$input;
477
+ },
478
+
479
+ /**
480
+ * Returns the element which is wrapped around the internal input. This
481
+ * is normally the $container, but typeahead.js moves the $input element.
482
+ */
483
+ findInputWrapper: function() {
484
+ var elt = this.$input[0],
485
+ container = this.$container[0];
486
+ while(elt && elt.parentNode !== container)
487
+ elt = elt.parentNode;
488
+
489
+ return $(elt);
490
+ }
491
+ };
492
+
493
+ /**
494
+ * Register JQuery plugin
495
+ */
496
+ $.fn.tagsinput = function(arg1, arg2) {
497
+ var results = [];
498
+
499
+ this.each(function() {
500
+ var tagsinput = $(this).data('tagsinput');
501
+ // Initialize a new tags input
502
+ if (!tagsinput) {
503
+ tagsinput = new TagsInput(this, arg1);
504
+ $(this).data('tagsinput', tagsinput);
505
+ results.push(tagsinput);
506
+
507
+ if (this.tagName === 'SELECT') {
508
+ $('option', $(this)).attr('selected', 'selected');
509
+ }
510
+
511
+ // Init tags from $(this).val()
512
+ $(this).val($(this).val());
513
+ } else if (!arg1 && !arg2) {
514
+ // tagsinput already exists
515
+ // no function, trying to init
516
+ results.push(tagsinput);
517
+ } else if(tagsinput[arg1] !== undefined) {
518
+ // Invoke function on existing tags input
519
+ var retVal = tagsinput[arg1](arg2);
520
+ if (retVal !== undefined)
521
+ results.push(retVal);
522
+ }
523
+ });
524
+
525
+ if ( typeof arg1 == 'string') {
526
+ // Return the results from the invoked function calls
527
+ return results.length > 1 ? results : results[0];
528
+ } else {
529
+ return results;
530
+ }
531
+ };
532
+
533
+ $.fn.tagsinput.Constructor = TagsInput;
534
+
535
+ /**
536
+ * Most options support both a string or number as well as a function as
537
+ * option value. This function makes sure that the option with the given
538
+ * key in the given options is wrapped in a function
539
+ */
540
+ function makeOptionItemFunction(options, key) {
541
+ if (typeof options[key] !== 'function') {
542
+ var propertyName = options[key];
543
+ options[key] = function(item) { return item[propertyName]; };
544
+ }
545
+ }
546
+ function makeOptionFunction(options, key) {
547
+ if (typeof options[key] !== 'function') {
548
+ var value = options[key];
549
+ options[key] = function() { return value; };
550
+ }
551
+ }
552
+ /**
553
+ * HtmlEncodes the given value
554
+ */
555
+ var htmlEncodeContainer = $('<div />');
556
+ function htmlEncode(value) {
557
+ if (value) {
558
+ return htmlEncodeContainer.text(value).html();
559
+ } else {
560
+ return '';
561
+ }
562
+ }
563
+
564
+ /**
565
+ * Returns the position of the caret in the given input field
566
+ * http://flightschool.acylt.com/devnotes/caret-position-woes/
567
+ */
568
+ function doGetCaretPosition(oField) {
569
+ var iCaretPos = 0;
570
+ if (document.selection) {
571
+ oField.focus ();
572
+ var oSel = document.selection.createRange();
573
+ oSel.moveStart ('character', -oField.value.length);
574
+ iCaretPos = oSel.text.length;
575
+ } else if (oField.selectionStart || oField.selectionStart == '0') {
576
+ iCaretPos = oField.selectionStart;
577
+ }
578
+ return (iCaretPos);
579
+ }
580
+
581
+ /**
582
+ * Returns boolean indicates whether user has pressed an expected key combination.
583
+ * @param object keyPressEvent: JavaScript event object, refer
584
+ * http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
585
+ * @param object lookupList: expected key combinations, as in:
586
+ * [13, {which: 188, shiftKey: true}]
587
+ */
588
+ function keyCombinationInList(keyPressEvent, lookupList) {
589
+ var found = false;
590
+ $.each(lookupList, function (index, keyCombination) {
591
+ if (typeof (keyCombination) === 'number' && keyPressEvent.which === keyCombination) {
592
+ found = true;
593
+ return false;
594
+ }
595
+
596
+ if (keyPressEvent.which === keyCombination.which) {
597
+ var alt = !keyCombination.hasOwnProperty('altKey') || keyPressEvent.altKey === keyCombination.altKey,
598
+ shift = !keyCombination.hasOwnProperty('shiftKey') || keyPressEvent.shiftKey === keyCombination.shiftKey,
599
+ ctrl = !keyCombination.hasOwnProperty('ctrlKey') || keyPressEvent.ctrlKey === keyCombination.ctrlKey;
600
+ if (alt && shift && ctrl) {
601
+ found = true;
602
+ return false;
603
+ }
604
+ }
605
+ });
606
+
607
+ return found;
608
+ }
609
+
610
+ /**
611
+ * Initialize tagsinput behaviour on inputs and selects which have
612
+ * data-role=tagsinput
613
+ */
614
+ $(function() {
615
+ $("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput();
616
+ });
617
+ })(window.jQuery);