scratch – Blame information for rev 125

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