bootstrap-tagsinput-rails 0.3.2.0 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
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);