[MERGE] Merged with main addons.
[odoo/odoo.git] / addons / web / static / lib / jquery.textext / jquery.textext.js
1 ;/**
2  * jQuery TextExt Plugin
3  * http://textextjs.com
4  *
5  * @version 1.3.0
6  * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
7  * @license MIT License
8  */
9 (function($, undefined)
10 {
11         /**
12          * TextExt is the main core class which by itself doesn't provide any functionality
13          * that is user facing, however it has the underlying mechanics to bring all the
14          * plugins together under one roof and make them work with each other or on their
15          * own.
16          *
17          * @author agorbatchev
18          * @date 2011/08/19
19          * @id TextExt
20          */
21         function TextExt() {};
22
23         /**
24          * ItemManager is used to seamlessly convert between string that come from the user input to whatever 
25          * the format the item data is being passed around in. It's used by all plugins that in one way or 
26          * another operate with items, such as Tags, Filter, Autocomplete and Suggestions. Default implementation 
27          * works with `String` type. 
28          *
29          * Each instance of `TextExt` creates a new instance of default implementation of `ItemManager`
30          * unless `itemManager` option was set to another implementation.
31          *
32          * To satisfy requirements of managing items of type other than a `String`, different implementation
33          * if `ItemManager` should be supplied.
34          *
35          * If you wish to bring your own implementation, you need to create a new class and implement all the 
36          * methods that `ItemManager` has. After, you need to supply your pass via the `itemManager` option during
37          * initialization like so:
38          *
39          *     $('#input').textext({
40          *         itemManager : CustomItemManager
41          *     })
42          *
43          * @author agorbatchev
44          * @date 2011/08/19
45          * @id ItemManager
46          */
47         function ItemManager() {};
48
49         /**
50          * TextExtPlugin is a base class for all plugins. It provides common methods which are reused
51          * by majority of plugins.
52          *
53          * All plugins must register themselves by calling the `$.fn.textext.addPlugin(name, constructor)`
54          * function while providing plugin name and constructor. The plugin name is the same name that user
55          * will identify the plugin in the `plugins` option when initializing TextExt component and constructor
56          * function will create a new instance of the plugin. *Without registering, the core won't
57          * be able to see the plugin.*
58          *
59          * <span class="new label version">new in 1.2.0</span> You can get instance of each plugin from the core 
60          * via associated function with the same name as the plugin. For example:
61          *
62          *     $('#input').textext()[0].tags()
63          *     $('#input').textext()[0].autocomplete()
64          *     ...
65          *
66          * @author agorbatchev
67          * @date 2011/08/19
68          * @id TextExtPlugin
69          */
70         function TextExtPlugin() {};
71
72         var stringify = (JSON || {}).stringify,
73                 slice     = Array.prototype.slice,
74
75                 UNDEFINED = 'undefined',
76
77                 /**
78                  * TextExt provides a way to pass in the options to configure the core as well as
79                  * each plugin that is being currently used. The jQuery exposed plugin `$().textext()` 
80                  * function takes a hash object with key/value set of options. For example:
81                  *
82                  *     $('textarea').textext({
83                  *         enabled: true
84                  *     })
85                  *
86                  * There are multiple ways of passing in the options:
87                  *
88                  * 1. Options could be nested multiple levels deep and accessed using all lowercased, dot
89                  * separated style, eg `foo.bar.world`. The manual is using this style for clarity and
90                  * consistency. For example:
91                  *
92                  *        {
93                  *            item: {
94                  *                manager: ...
95                  *            },
96                  *
97                  *            html: {
98                  *                wrap: ...
99                  *            },
100                  *
101                  *            autocomplete: {
102                  *                enabled: ...,
103                  *                dropdown: {
104                  *                   position: ...
105                  *                }
106                  *            }
107                  *        }
108                  *
109                  * 2. Options could be specified using camel cased names in a flat key/value fashion like so:
110                  *
111                  *        {
112                  *            itemManager: ...,
113                  *            htmlWrap: ...,
114                  *            autocompleteEnabled: ...,
115                  *            autocompleteDropdownPosition: ...
116                  *        }
117                  *
118                  * 3. Finally, options could be specified in mixed style. It's important to understand that
119                  * for each dot separated name, its alternative in camel case is also checked for, eg for 
120                  * `foo.bar.world` it's alternatives could be `fooBarWorld`, `foo.barWorld` or `fooBar.world`, 
121                  * which translates to `{ foo: { bar: { world: ... } } }`, `{ fooBarWorld: ... }`, 
122                  * `{ foo : { barWorld : ... } }` or `{ fooBar: { world: ... } }` respectively. For example:
123                  *
124                  *        {
125                  *            itemManager : ...,
126                  *            htmlWrap: ...,
127                  *            autocomplete: {
128                  *                enabled: ...,
129                  *                dropdownPosition: ...
130                  *            }
131                  *        }
132                  *
133                  * Mixed case is used through out the code, wherever it seems appropriate. However in the code, all option
134                  * names are specified in the dot notation because it works both ways where as camel case is not
135                  * being converted to its alternative dot notation.
136                  *
137                  * @author agorbatchev
138                  * @date 2011/08/17
139                  * @id TextExt.options
140                  */
141
142                 /**
143                  * Default instance of `ItemManager` which takes `String` type as default for tags.
144                  *
145                  * @name item.manager
146                  * @default ItemManager
147                  * @author agorbatchev
148                  * @date 2011/08/19
149                  * @id TextExt.options.item.manager
150                  */
151                 OPT_ITEM_MANAGER = 'item.manager',
152                 
153                 /**
154                  * List of plugins that should be used with the current instance of TextExt. The list could be
155                  * specified as array of strings or as comma or space separated string.
156                  *
157                  * @name plugins
158                  * @default []
159                  * @author agorbatchev
160                  * @date 2011/08/19
161                  * @id TextExt.options.plugins
162                  */
163                 OPT_PLUGINS = 'plugins',
164                 
165                 /**
166                  * TextExt allows for overriding of virtually any method that the core or any of its plugins
167                  * use. This could be accomplished through the use of the `ext` option.
168                  *
169                  * It's possible to specifically target the core or any plugin, as well as overwrite all the
170                  * desired methods everywhere.
171                  *
172                  * 1. Targeting the core:
173                  *
174                  *        ext: {
175                  *            core: {
176                  *                trigger: function()
177                  *                {
178                  *                    console.log('TextExt.trigger', arguments);
179                  *                    $.fn.textext.TextExt.prototype.trigger.apply(this, arguments);
180                  *                }
181                  *            }
182                  *        }
183                  *
184                  * 2. Targeting individual plugins:
185                  *
186                  *        ext: {
187                  *            tags: {
188                  *                addTags: function(tags)
189                  *                {
190                  *                    console.log('TextExtTags.addTags', tags);
191                  *                    $.fn.textext.TextExtTags.prototype.addTags.apply(this, arguments);
192                  *                }
193                  *            }
194                  *        }
195                  *
196                  * 3. Targeting `ItemManager` instance:
197                  *
198                  *        ext: {
199                  *            itemManager: {
200                  *                stringToItem: function(str)
201                  *                {
202                  *                    console.log('ItemManager.stringToItem', str);
203                  *                    return $.fn.textext.ItemManager.prototype.stringToItem.apply(this, arguments);
204                  *                }
205                  *            }
206                  *        }
207                  *
208                  * 4. And finally, in edge cases you can extend everything at once:
209                  *
210                  *        ext: {
211                  *            '*': {
212                  *                fooBar: function() {}
213                  *            }
214                  *        }
215                  *
216                  * @name ext
217                  * @default {}
218                  * @author agorbatchev
219                  * @date 2011/08/19
220                  * @id TextExt.options.ext
221                  */
222                 OPT_EXT = 'ext',
223                 
224                 /**
225                  * HTML source that is used to generate elements necessary for the core and all other
226                  * plugins to function.
227                  *
228                  * @name html.wrap
229                  * @default '<div class="text-core"><div class="text-wrap"/></div>'
230                  * @author agorbatchev
231                  * @date 2011/08/19
232                  * @id TextExt.options.html.wrap
233                  */
234                 OPT_HTML_WRAP = 'html.wrap',
235
236                 /**
237                  * HTML source that is used to generate hidden input value of which will be submitted 
238                  * with the HTML form.
239                  *
240                  * @name html.hidden
241                  * @default '<input type="hidden" />'
242                  * @author agorbatchev
243                  * @date 2011/08/20
244                  * @id TextExt.options.html.hidden
245                  */
246                 OPT_HTML_HIDDEN = 'html.hidden',
247                 
248                 /**
249                  * Hash table of key codes and key names for which special events will be created
250                  * by the core. For each entry a `[name]KeyDown`, `[name]KeyUp` and `[name]KeyPress` events 
251                  * will be triggered along side with `anyKeyUp` and `anyKeyDown` events for every 
252                  * key stroke.
253                  *
254                  * Here's a list of default keys:
255                  *
256                  *     {
257                  *         8   : 'backspace',
258                  *         9   : 'tab',
259                  *         13  : 'enter!',
260                  *         27  : 'escape!',
261                  *         37  : 'left',
262                  *         38  : 'up!',
263                  *         39  : 'right',
264                  *         40  : 'down!',
265                  *         46  : 'delete',
266                  *         108 : 'numpadEnter'
267                  *     }
268                  *
269                  * Please note the `!` at the end of some keys. This tells the core that by default
270                  * this keypress will be trapped and not passed on to the text input.
271                  *
272                  * @name keys
273                  * @default { ... }
274                  * @author agorbatchev
275                  * @date 2011/08/19
276                  * @id TextExt.options.keys
277                  */
278                 OPT_KEYS = 'keys',
279
280                 /**
281                  * The core triggers or reacts to the following events.
282                  *
283                  * @author agorbatchev
284                  * @date 2011/08/17
285                  * @id TextExt.events
286                  */
287
288                 /**
289                  * Core triggers `preInvalidate` event before the dimensions of padding on the text input
290                  * are set.
291                  *
292                  * @name preInvalidate
293                  * @author agorbatchev
294                  * @date 2011/08/19
295                  * @id TextExt.events.preInvalidate
296                  */
297                 EVENT_PRE_INVALIDATE = 'preInvalidate',
298
299                 /**
300                  * Core triggers `postInvalidate` event after the dimensions of padding on the text input
301                  * are set.
302                  *
303                  * @name postInvalidate
304                  * @author agorbatchev
305                  * @date 2011/08/19
306                  * @id TextExt.events.postInvalidate
307                  */
308                 EVENT_POST_INVALIDATE = 'postInvalidate',
309                 
310                 /**
311                  * Core triggers `getFormData` on every key press to collect data that will be populated
312                  * into the hidden input that will be submitted with the HTML form and data that will
313                  * be displayed in the input field that user is currently interacting with.
314                  *
315                  * All plugins that wish to affect how the data is presented or sent must react to 
316                  * `getFormData` and populate the data in the following format:
317                  *
318                  *     {
319                  *         input : {String},
320                  *         form  : {Object}
321                  *     }
322                  *
323                  * The data key must be a numeric weight which will be used to determine which data
324                  * ends up being used. Data with the highest numerical weight gets the priority. This
325                  * allows plugins to set the final data regardless of their initialization order, which
326                  * otherwise would be impossible.
327                  *
328                  * For example, the Tags and Autocomplete plugins have to work side by side and Tags
329                  * plugin must get priority on setting the data. Therefore the Tags plugin sets data
330                  * with the weight 200 where as the Autocomplete plugin sets data with the weight 100.
331                  *
332                  * Here's an example of a typical `getFormData` handler:
333                  * 
334                  *     TextExtPlugin.prototype.onGetFormData = function(e, data, keyCode)
335                  *     {
336                  *         data[100] = self.formDataObject('input value', 'form value');
337                  *     };
338                  *
339                  * Core also reacts to the `getFormData` and updates hidden input with data which will be
340                  * submitted with the HTML form.
341                  *
342                  * @name getFormData
343                  * @author agorbatchev
344                  * @date 2011/08/19
345                  * @id TextExt.events.getFormData
346                  */
347                 EVENT_GET_FORM_DATA = 'getFormData',
348
349                 /**
350                  * Core triggers and reacts to the `setFormData` event to update the actual value in the
351                  * hidden input that will be submitted with the HTML form. Second argument can be value
352                  * of any type and by default it will be JSON serialized with `TextExt.serializeData()`
353                  * function.
354                  *
355                  * @name setFormData
356                  * @author agorbatchev
357                  * @date 2011/08/22
358                  * @id TextExt.events.setFormData
359                  */
360                 EVENT_SET_FORM_DATA = 'setFormData',
361
362                 /**
363                  * Core triggers and reacts to the `setInputData` event to update the actual value in the
364                  * text input that user is interacting with. Second argument must be of a `String` type
365                  * the value of which will be set into the text input.
366                  *
367                  * @name setInputData
368                  * @author agorbatchev
369                  * @date 2011/08/22
370                  * @id TextExt.events.setInputData
371                  */
372                 EVENT_SET_INPUT_DATA = 'setInputData',
373                 
374                 /**
375                  * Core triggers `postInit` event to let plugins run code after all plugins have been 
376                  * created and initialized. This is a good place to set some kind of global values before 
377                  * somebody gets to use them. This is not the right place to expect all plugins to finish
378                  * their initialization.
379                  *
380                  * @name postInit
381                  * @author agorbatchev
382                  * @date 2011/08/19
383                  * @id TextExt.events.postInit
384                  */
385                 EVENT_POST_INIT = 'postInit',
386
387                 /**
388                  * Core triggers `ready` event after all global configuration and prepearation has been
389                  * done and the TextExt component is ready for use. Event handlers should expect all 
390                  * values to be set and the plugins to be in the final state.
391                  *
392                  * @name ready
393                  * @author agorbatchev
394                  * @date 2011/08/19
395                  * @id TextExt.events.ready
396                  */
397                 EVENT_READY = 'ready',
398
399                 /**
400                  * Core triggers `anyKeyUp` event for every key up event triggered within the component.
401                  *
402                  * @name anyKeyUp
403                  * @author agorbatchev
404                  * @date 2011/08/19
405                  * @id TextExt.events.anyKeyUp
406                  */
407
408                 /**
409                  * Core triggers `anyKeyDown` event for every key down event triggered within the component.
410                  *
411                  * @name anyKeyDown
412                  * @author agorbatchev
413                  * @date 2011/08/19
414                  * @id TextExt.events.anyKeyDown
415                  */
416
417                 /**
418                  * Core triggers `[name]KeyUp` event for every key specifid in the `keys` option that is 
419                  * triggered within the component.
420                  *
421                  * @name [name]KeyUp
422                  * @author agorbatchev
423                  * @date 2011/08/19
424                  * @id TextExt.events.[name]KeyUp
425                  */
426
427                 /**
428                  * Core triggers `[name]KeyDown` event for every key specified in the `keys` option that is 
429                  * triggered within the component.
430                  *
431                  * @name [name]KeyDown
432                  * @author agorbatchev
433                  * @date 2011/08/19
434                  * @id TextExt.events.[name]KeyDown
435                  */
436
437                 /**
438                  * Core triggers `[name]KeyPress` event for every key specified in the `keys` option that is 
439                  * triggered within the component.
440                  *
441                  * @name [name]KeyPress
442                  * @author agorbatchev
443                  * @date 2011/08/19
444                  * @id TextExt.events.[name]KeyPress
445                  */
446
447                 DEFAULT_OPTS = {
448                         itemManager : ItemManager,
449
450                         plugins : [],
451                         ext : {},
452
453                         html : {
454                                 wrap   : '<div class="text-core"><div class="text-wrap"/></div>',
455                                 hidden : '<input type="hidden" />'
456                         },
457
458                         keys : {
459                                 8   : 'backspace',
460                                 9   : 'tab',
461                                 13  : 'enter!',
462                                 27  : 'escape!',
463                                 37  : 'left',
464                                 38  : 'up!',
465                                 39  : 'right',
466                                 40  : 'down!',
467                                 46  : 'delete',
468                                 108 : 'numpadEnter'
469                         }
470                 }
471                 ;
472
473         // Freak out if there's no JSON.stringify function found
474         if(!stringify)
475                 throw new Error('JSON.stringify() not found');
476
477         /**
478          * Returns object property by name where name is dot-separated and object is multiple levels deep.
479          * @param target Object Source object.
480          * @param name String Dot separated property name, ie `foo.bar.world`
481          * @id core.getProperty
482          */
483         function getProperty(source, name)
484         {
485                 if(typeof(name) === 'string')
486                         name = name.split('.');
487
488                 var fullCamelCaseName = name.join('.').replace(/\.(\w)/g, function(match, letter) { return letter.toUpperCase() }),
489                         nestedName        = name.shift(),
490                         result
491                         ;
492
493                 if(typeof(result = source[fullCamelCaseName]) != UNDEFINED)
494                         result = result;
495
496                 else if(typeof(result = source[nestedName]) != UNDEFINED && name.length > 0)
497                         result = getProperty(result, name);
498
499                 // name.length here should be zero
500                 return result;
501         };
502
503         /**
504          * Hooks up specified events in the scope of the current object.
505          * @author agorbatchev
506          * @date 2011/08/09
507          */
508         function hookupEvents()
509         {
510                 var args   = slice.apply(arguments),
511                         self   = this,
512                         target = args.length === 1 ? self : args.shift(),
513                         event
514                         ;
515
516                 args = args[0] || {};
517
518                 function bind(event, handler)
519                 {
520                         target.bind(event, function()
521                         {
522                                 // apply handler to our PLUGIN object, not the target
523                                 return handler.apply(self, arguments);
524                         });
525                 }
526
527                 for(event in args)
528                         bind(event, args[event]);
529         };
530
531         function formDataObject(input, form)
532         {
533                 return { 'input' : input, 'form' : form };
534         };
535
536         //--------------------------------------------------------------------------------
537         // ItemManager core component
538         
539         p = ItemManager.prototype;
540
541         /**
542          * Initialization method called by the core during instantiation.
543          *
544          * @signature ItemManager.init(core)
545          *
546          * @param core {TextExt} Instance of the TextExt core class.
547          *
548          * @author agorbatchev
549          * @date 2011/08/19
550          * @id ItemManager.init
551          */
552         p.init = function(core)
553         {
554         };
555
556         /**
557          * Filters out items from the list that don't match the query and returns remaining items. Default 
558          * implementation checks if the item starts with the query.
559          *
560          * @signature ItemManager.filter(list, query)
561          *
562          * @param list {Array} List of items. Default implementation works with strings.
563          * @param query {String} Query string.
564          *
565          * @author agorbatchev
566          * @date 2011/08/19
567          * @id ItemManager.filter
568          */
569         p.filter = function(list, query)
570         {
571                 var result = [],
572                         i, item
573                         ;
574
575                 for(i = 0; i < list.length; i++)
576                 {
577                         item = list[i];
578                         if(this.itemContains(item, query))
579                                 result.push(item);
580                 }
581
582                 return result;
583         };
584
585         /**
586          * Returns `true` if specified item contains another string, `false` otherwise. In the default implementation 
587          * `String.indexOf()` is used to check if item string begins with the needle string.
588          *
589          * @signature ItemManager.itemContains(item, needle)
590          *
591          * @param item {Object} Item to check. Default implementation works with strings.
592          * @param needle {String} Search string to be found within the item.
593          *
594          * @author agorbatchev
595          * @date 2011/08/19
596          * @id ItemManager.itemContains
597          */
598         p.itemContains = function(item, needle)
599         {
600                 return this.itemToString(item).toLowerCase().indexOf(needle.toLowerCase()) == 0;
601         };
602
603         /**
604          * Converts specified string to item. Because default implemenation works with string, input string
605          * is simply returned back. To use custom objects, different implementation of this method could
606          * return something like `{ name : {String} }`.
607          *
608          * @signature ItemManager.stringToItem(str)
609          *
610          * @param str {String} Input string.
611          *
612          * @author agorbatchev
613          * @date 2011/08/19
614          * @id ItemManager.stringToItem
615          */
616         p.stringToItem = function(str)
617         {
618                 return str;
619         };
620
621         /**
622          * Converts specified item to string. Because default implemenation works with string, input string
623          * is simply returned back. To use custom objects, different implementation of this method could
624          * for example return `name` field of `{ name : {String} }`.
625          *
626          * @signature ItemManager.itemToString(item)
627          *
628          * @param item {Object} Input item to be converted to string.
629          *
630          * @author agorbatchev
631          * @date 2011/08/19
632          * @id ItemManager.itemToString
633          */
634         p.itemToString = function(item)
635         {
636                 return item;
637         };
638
639         /**
640          * Returns `true` if both items are equal, `false` otherwise. Because default implemenation works with 
641          * string, input items are compared as strings. To use custom objects, different implementation of this 
642          * method could for example compare `name` fields of `{ name : {String} }` type object.
643          *
644          * @signature ItemManager.compareItems(item1, item2)
645          *
646          * @param item1 {Object} First item.
647          * @param item2 {Object} Second item.
648          *
649          * @author agorbatchev
650          * @date 2011/08/19
651          * @id ItemManager.compareItems
652          */
653         p.compareItems = function(item1, item2)
654         {
655                 return item1 == item2;
656         };
657
658         //--------------------------------------------------------------------------------
659         // TextExt core component
660
661         p = TextExt.prototype;
662                 
663         /**
664          * Initializes current component instance with work with the supplied text input and options.
665          *
666          * @signature TextExt.init(input, opts)
667          *
668          * @param input {HTMLElement} Text input.
669          * @param opts {Object} Options.
670          *
671          * @author agorbatchev
672          * @date 2011/08/19
673          * @id TextExt.init
674          */
675         p.init = function(input, opts)
676         {
677                 var self = this,
678                         hiddenInput,
679                         itemManager,
680                         container
681                         ;
682
683                 self._defaults    = $.extend({}, DEFAULT_OPTS);
684                 self._opts        = opts || {};
685                 self._plugins     = {};
686                 self._itemManager = itemManager = new (self.opts(OPT_ITEM_MANAGER))();
687                 input             = $(input);
688                 container         = $(self.opts(OPT_HTML_WRAP));
689                 hiddenInput       = $(self.opts(OPT_HTML_HIDDEN));
690
691                 input
692                         .wrap(container)
693                         .keydown(function(e) { return self.onKeyDown(e) })
694                         .keyup(function(e) { return self.onKeyUp(e) })
695                         .data('textext', self)
696                         ;
697
698                 // keep references to html elements using jQuery.data() to avoid circular references
699                 $(self).data({
700                         'hiddenInput'   : hiddenInput,
701                         'wrapElement' : input.parents('.text-wrap').first(),
702                         'input'         : input
703                 });
704
705                 // set the name of the hidden input to the text input's name
706                 hiddenInput.attr('name', input.attr('name'));
707                 // remove name attribute from the text input
708                 input.attr('name', null);
709                 // add hidden input to the DOM
710                 hiddenInput.insertAfter(input);
711
712                 $.extend(true, itemManager, self.opts(OPT_EXT + '.item.manager'));
713                 $.extend(true, self, self.opts(OPT_EXT + '.*'), self.opts(OPT_EXT + '.core'));
714                 
715                 self.originalWidth = input.outerWidth();
716
717                 self.invalidateBounds();
718
719                 itemManager.init(self);
720
721                 self.initPatches();
722                 self.initPlugins(self.opts(OPT_PLUGINS), $.fn.textext.plugins);
723
724                 self.on({
725                         setFormData  : self.onSetFormData,
726                         getFormData  : self.onGetFormData,
727                         setInputData : self.onSetInputData,
728                         anyKeyUp     : self.onAnyKeyUp
729                 });
730
731                 self.trigger(EVENT_POST_INIT);
732                 self.trigger(EVENT_READY);
733
734                 self.getFormData(0);
735         };
736
737         /**
738          * Initialized all installed patches against current instance. The patches are initialized based on their
739          * initialization priority which is returned by each patch's `initPriority()` method. Priority
740          * is a `Number` where patches with higher value gets their `init()` method called before patches
741          * with lower priority value.
742          *
743          * This facilitates initializing of patches in certain order to insure proper dependencies
744          * regardless of which order they are loaded.
745          *
746          * By default all patches have the same priority - zero, which means they will be initialized
747          * in rorder they are loaded, that is unless `initPriority()` is overriden.
748          *
749          * @signature TextExt.initPatches()
750          *
751          * @author agorbatchev
752          * @date 2011/10/11
753          * @id TextExt.initPatches
754          */
755         p.initPatches = function()
756         {
757                 var list   = [],
758                         source = $.fn.textext.patches,
759                         name
760                         ;
761
762                 for(name in source)
763                         list.push(name);
764
765                 this.initPlugins(list, source);
766         };
767
768         /**
769          * Creates and initializes all specified plugins. The plugins are initialized based on their
770          * initialization priority which is returned by each plugin's `initPriority()` method. Priority
771          * is a `Number` where plugins with higher value gets their `init()` method called before plugins
772          * with lower priority value.
773          *
774          * This facilitates initializing of plugins in certain order to insure proper dependencies
775          * regardless of which order user enters them in the `plugins` option field.
776          *
777          * By default all plugins have the same priority - zero, which means they will be initialized
778          * in the same order as entered by the user.
779          *
780          * @signature TextExt.initPlugins(plugins)
781          *
782          * @param plugins {Array} List of plugin names to initialize.
783          *
784          * @author agorbatchev
785          * @date 2011/08/19
786          * @id TextExt.initPlugins
787          */
788         p.initPlugins = function(plugins, source)
789         {
790                 var self = this,
791                         ext, name, plugin, initList = [], i
792                         ;
793
794                 if(typeof(plugins) == 'string')
795                         plugins = plugins.split(/\s*,\s*|\s+/g);
796
797                 for(i = 0; i < plugins.length; i++)
798                 {
799                         name   = plugins[i];
800                         plugin = source[name];
801
802                         if(plugin)
803                         {
804                                 self._plugins[name] = plugin = new plugin();
805                                 self[name] = (function(plugin) { 
806                                   return function(){ return plugin; } 
807                                 })(plugin);
808                                 initList.push(plugin);
809                                 $.extend(true, plugin, self.opts(OPT_EXT + '.*'), self.opts(OPT_EXT + '.' + name));
810                         }
811                 }
812
813                 // sort plugins based on their priority values
814                 initList.sort(function(p1, p2)
815                 {
816                         p1 = p1.initPriority();
817                         p2 = p2.initPriority();
818
819                         return p1 === p2
820                                 ? 0
821                                 : p1 < p2 ? 1 : -1
822                                 ;
823                 });
824
825                 for(i = 0; i < initList.length; i++)
826                         initList[i].init(self);
827         };
828
829         /**
830          * Returns true if specified plugin is was instantiated for the current instance of core.
831          *
832          * @signature TextExt.hasPlugin(name)
833          *
834          * @param name {String} Name of the plugin to check.
835          *
836          * @author agorbatchev
837          * @date 2011/12/28
838          * @id TextExt.hasPlugin
839          * @version 1.1
840          */
841         p.hasPlugin = function(name)
842         {
843                 return !!this._plugins[name];
844         };
845
846         /**
847          * Allows to add multiple event handlers which will be execued in the scope of the current object.
848          * 
849          * @signature TextExt.on([target], handlers)
850          *
851          * @param target {Object} **Optional**. Target object which has traditional `bind(event, handler)` method.
852          *                        Handler function will still be executed in the current object's scope.
853          * @param handlers {Object} Key/value pairs of event names and handlers, eg `{ event: handler }`.
854          *
855          * @author agorbatchev
856          * @date 2011/08/19
857          * @id TextExt.on
858          */
859         p.on = hookupEvents;
860
861         /**
862          * Binds an event handler to the input box that user interacts with.
863          *
864          * @signature TextExt.bind(event, handler)
865          *
866          * @param event {String} Event name.
867          * @param handler {Function} Event handler.
868          *
869          * @author agorbatchev
870          * @date 2011/08/19
871          * @id TextExt.bind
872          */
873         p.bind = function(event, handler)
874         {
875                 this.input().bind(event, handler);
876         };
877
878         /**
879          * Triggers an event on the input box that user interacts with. All core events are originated here.
880          * 
881          * @signature TextExt.trigger(event, ...args)
882          *
883          * @param event {String} Name of the event to trigger.
884          * @param ...args All remaining arguments will be passed to the event handler.
885          *
886          * @author agorbatchev
887          * @date 2011/08/19
888          * @id TextExt.trigger
889          */
890         p.trigger = function()
891         {
892                 var args = arguments;
893                 this.input().trigger(args[0], slice.call(args, 1));
894         };
895
896         /**
897          * Returns instance of `itemManager` that is used by the component.
898          *
899          * @signature TextExt.itemManager()
900          *
901          * @author agorbatchev
902          * @date 2011/08/19
903          * @id TextExt.itemManager
904          */
905         p.itemManager = function()
906         {
907                 return this._itemManager;
908         };
909
910         /**
911          * Returns jQuery input element with which user is interacting with.
912          *
913          * @signature TextExt.input()
914          *
915          * @author agorbatchev
916          * @date 2011/08/10
917          * @id TextExt.input
918          */
919         p.input = function()
920         {
921                 return $(this).data('input');
922         };
923
924         /**
925          * Returns option value for the specified option by name. If the value isn't found in the user
926          * provided options, it will try looking for default value.
927          *
928          * @signature TextExt.opts(name)
929          *
930          * @param name {String} Option name as described in the options.
931          *
932          * @author agorbatchev
933          * @date 2011/08/19
934          * @id TextExt.opts
935          */
936         p.opts = function(name)
937         {
938                 var result = getProperty(this._opts, name);
939                 return typeof(result) == 'undefined' ? getProperty(this._defaults, name) : result;
940         };
941
942         /**
943          * Returns HTML element that was created from the `html.wrap` option. This is the top level HTML
944          * container for the text input with which user is interacting with.
945          *
946          * @signature TextExt.wrapElement()
947          *
948          * @author agorbatchev
949          * @date 2011/08/19
950          * @id TextExt.wrapElement
951          */
952         p.wrapElement = function()
953         {
954                 return $(this).data('wrapElement');
955         };
956
957         /**
958          * Updates container to match dimensions of the text input. Triggers `preInvalidate` and `postInvalidate`
959          * events.
960          *
961          * @signature TextExt.invalidateBounds()
962          *
963          * @author agorbatchev
964          * @date 2011/08/19
965          * @id TextExt.invalidateBounds
966          */
967         p.invalidateBounds = function()
968         {
969                 var self      = this,
970                         input     = self.input(),
971                         wrap      = self.wrapElement(),
972                         container = wrap.parent(),
973                         width     = self.originalWidth,
974                         height
975                         ;
976
977                 self.trigger(EVENT_PRE_INVALIDATE);
978
979                 height = input.outerHeight();
980
981                 input.width(width);
982                 wrap.width(width).height(height);
983                 container.height(height);
984
985                 self.trigger(EVENT_POST_INVALIDATE);
986         };
987
988         /**
989          * Focuses user input on the text box.
990          *
991          * @signature TextExt.focusInput()
992          *
993          * @author agorbatchev
994          * @date 2011/08/19
995          * @id TextExt.focusInput
996          */
997         p.focusInput = function()
998         {
999                 this.input()[0].focus();
1000         };
1001
1002         /**
1003          * Serializes data for to be set into the hidden input field and which will be submitted 
1004          * with the HTML form.
1005          *
1006          * By default simple JSON serialization is used. It's expected that `JSON.stringify`
1007          * method would be available either through built in class in most modern browsers
1008          * or through JSON2 library.
1009          *
1010          * @signature TextExt.serializeData(data)
1011          *
1012          * @param data {Object} Data to serialize.
1013          *
1014          * @author agorbatchev
1015          * @date 2011/08/09
1016          * @id TextExt.serializeData
1017          */
1018         p.serializeData = stringify;
1019
1020         /**
1021          * Returns the hidden input HTML element which will be submitted with the HTML form.
1022          *
1023          * @signature TextExt.hiddenInput()
1024          *
1025          * @author agorbatchev
1026          * @date 2011/08/09
1027          * @id TextExt.hiddenInput
1028          */
1029         p.hiddenInput = function(value)
1030         {
1031                 return $(this).data('hiddenInput');
1032         };
1033
1034         /**
1035          * Abstracted functionality to trigger an event and get the data with maximum weight set by all
1036          * the event handlers. This functionality is used for the `getFormData` event.
1037          *
1038          * @signature TextExt.getWeightedEventResponse(event, args)
1039          *
1040          * @param event {String} Event name.
1041          * @param args {Object} Argument to be passed with the event.
1042          *
1043          * @author agorbatchev
1044          * @date 2011/08/22
1045          * @id TextExt.getWeightedEventResponse
1046          */
1047         p.getWeightedEventResponse = function(event, args)
1048         {
1049                 var self      = this,
1050                         data      = {},
1051                         maxWeight = 0
1052                         ;
1053
1054                 self.trigger(event, data, args);
1055
1056                 for(var weight in data)
1057                         maxWeight = Math.max(maxWeight, weight);
1058
1059                 return data[maxWeight];
1060         };
1061
1062         /**
1063          * Triggers the `getFormData` event to get all the plugins to return their data.
1064          *
1065          * After the data is returned, triggers `setFormData` and `setInputData` to update appopriate values.
1066          *
1067          * @signature TextExt.getFormData(keyCode)
1068          *
1069          * @param keyCode {Number} Key code number which has triggered this update. It's impotant to pass
1070          * this value to the plugins because they might return different values based on the key that was 
1071          * pressed. For example, the Tags plugin returns an empty string for the `input` value if the enter
1072          * key was pressed, otherwise it returns whatever is currently in the text input.
1073          *
1074          * @author agorbatchev
1075          * @date 2011/08/22
1076          * @id TextExt.getFormData
1077          */
1078         p.getFormData = function(keyCode)
1079         {
1080                 var self = this,
1081                         data = self.getWeightedEventResponse(EVENT_GET_FORM_DATA, keyCode || 0)
1082                         ;
1083
1084                 self.trigger(EVENT_SET_FORM_DATA  , data['form']);
1085                 self.trigger(EVENT_SET_INPUT_DATA , data['input']);
1086         };
1087
1088         //--------------------------------------------------------------------------------
1089         // Event handlers
1090
1091         /**
1092          * Reacts to the `anyKeyUp` event and triggers the `getFormData` to change data that will be submitted
1093          * with the form. Default behaviour is that everything that is typed in will be JSON serialized, so
1094          * the end result will be a JSON string.
1095          *
1096          * @signature TextExt.onAnyKeyUp(e)
1097          *
1098          * @param e {Object} jQuery event.
1099          *
1100          * @author agorbatchev
1101          * @date 2011/08/19
1102          * @id TextExt.onAnyKeyUp
1103          */
1104         p.onAnyKeyUp = function(e, keyCode)
1105         {
1106                 this.getFormData(keyCode);
1107         };
1108
1109         /**
1110          * Reacts to the `setInputData` event and populates the input text field that user is currently
1111          * interacting with.
1112          *
1113          * @signature TextExt.onSetInputData(e, data)
1114          *
1115          * @param e {Event} jQuery event.
1116          * @param data {String} Value to be set.
1117          *
1118          * @author agorbatchev
1119          * @date 2011/08/22
1120          * @id TextExt.onSetInputData
1121          */
1122         p.onSetInputData = function(e, data)
1123         {
1124                 this.input().val(data);
1125         };
1126
1127         /**
1128          * Reacts to the `setFormData` event and populates the hidden input with will be submitted with
1129          * the HTML form. The value will be serialized with `serializeData()` method.
1130          *
1131          * @signature TextExt.onSetFormData(e, data)
1132          *
1133          * @param e {Event} jQuery event.
1134          * @param data {Object} Data that will be set.
1135          * 
1136          * @author agorbatchev
1137          * @date 2011/08/22
1138          * @id TextExt.onSetFormData
1139          */
1140         p.onSetFormData = function(e, data)
1141         {
1142                 var self = this;
1143                 self.hiddenInput().val(self.serializeData(data));
1144         };
1145
1146         /**
1147          * Reacts to `getFormData` event triggered by the core. At the bare minimum the core will tell
1148          * itself to use the current value in the text input as the data to be submitted with the HTML
1149          * form.
1150          *
1151          * @signature TextExt.onGetFormData(e, data)
1152          *
1153          * @param e {Event} jQuery event.
1154          *
1155          * @author agorbatchev
1156          * @date 2011/08/09
1157          * @id TextExt.onGetFormData
1158          */
1159         p.onGetFormData = function(e, data)
1160         {
1161                 var val = this.input().val();
1162                 data[0] = formDataObject(val, val);
1163         };
1164
1165         //--------------------------------------------------------------------------------
1166         // User mouse/keyboard input
1167
1168         /**
1169          * Triggers `[name]KeyUp` and `[name]KeyPress` for every keystroke as described in the events.
1170          *
1171          * @signature TextExt.onKeyUp(e)
1172          *
1173          * @param e {Object} jQuery event.
1174          * @author agorbatchev
1175          * @date 2011/08/19
1176          * @id TextExt.onKeyUp
1177          */
1178
1179         /**
1180          * Triggers `[name]KeyDown` for every keystroke as described in the events.
1181          *
1182          * @signature TextExt.onKeyDown(e)
1183          *
1184          * @param e {Object} jQuery event.
1185          * @author agorbatchev
1186          * @date 2011/08/19
1187          * @id TextExt.onKeyDown
1188          */
1189         
1190         $(['Down', 'Up']).each(function()
1191         {
1192                 var type = this.toString();
1193
1194                 p['onKey' + type] = function(e)
1195                 {
1196                         var self          = this,
1197                                 keyName       = self.opts(OPT_KEYS)[e.keyCode],
1198                                 defaultResult = true
1199                                 ;
1200
1201                         if(keyName)
1202                         {
1203                                 defaultResult = keyName.substr(-1) != '!';
1204                                 keyName       = keyName.replace('!', '');
1205
1206                                 self.trigger(keyName + 'Key' + type);
1207
1208                                 // manual *KeyPress event fimplementation for the function keys like Enter, Backspace, etc.
1209                                 if(type == 'Up' && self._lastKeyDown == e.keyCode)
1210                                 {
1211                                         self._lastKeyDown = null;
1212                                         self.trigger(keyName + 'KeyPress');
1213                                 }
1214
1215                                 if(type == 'Down')
1216                                         self._lastKeyDown = e.keyCode;
1217                         }
1218
1219                         self.trigger('anyKey' + type, e.keyCode);
1220
1221                         return defaultResult;
1222                 };
1223         });
1224
1225         //--------------------------------------------------------------------------------
1226         // Plugin Base
1227         
1228         p = TextExtPlugin.prototype;
1229
1230         /**
1231          * Allows to add multiple event handlers which will be execued in the scope of the current object.
1232          * 
1233          * @signature TextExt.on([target], handlers)
1234          *
1235          * @param target {Object} **Optional**. Target object which has traditional `bind(event, handler)` method.
1236          *                        Handler function will still be executed in the current object's scope.
1237          * @param handlers {Object} Key/value pairs of event names and handlers, eg `{ event: handler }`.
1238          *
1239          * @author agorbatchev
1240          * @date 2011/08/19
1241          * @id TextExtPlugin.on
1242          */
1243         p.on = hookupEvents;
1244
1245         /**
1246          * Returns the hash object that `getFormData` triggered by the core expects.
1247          *
1248          * @signature TextExtPlugin.formDataObject(input, form)
1249          *
1250          * @param input {String} Value that will go into the text input that user is interacting with.
1251          * @param form {Object} Value that will be serialized and put into the hidden that will be submitted
1252          * with the HTML form.
1253          *
1254          * @author agorbatchev
1255          * @date 2011/08/22
1256          * @id TextExtPlugin.formDataObject
1257          */
1258         p.formDataObject = formDataObject;
1259
1260         /**
1261          * Initialization method called by the core during plugin instantiation. This method must be implemented
1262          * by each plugin individually.
1263          *
1264          * @signature TextExtPlugin.init(core)
1265          *
1266          * @param core {TextExt} Instance of the TextExt core class.
1267          *
1268          * @author agorbatchev
1269          * @date 2011/08/19
1270          * @id TextExtPlugin.init
1271          */
1272         p.init = function(core) { throw new Error('Not implemented') };
1273
1274         /**
1275          * Initialization method wich should be called by the plugin during the `init()` call.
1276          *
1277          * @signature TextExtPlugin.baseInit(core, defaults)
1278          *
1279          * @param core {TextExt} Instance of the TextExt core class.
1280          * @param defaults {Object} Default plugin options. These will be checked if desired value wasn't
1281          * found in the options supplied by the user.
1282          *
1283          * @author agorbatchev
1284          * @date 2011/08/19
1285          * @id TextExtPlugin.baseInit
1286          */
1287         p.baseInit = function(core, defaults)
1288         {
1289                 var self = this;
1290
1291                 core._defaults = $.extend(true, core._defaults, defaults);
1292                 self._core     = core;
1293                 self._timers   = {};
1294         };
1295
1296         /**
1297          * Allows starting of multiple timeout calls. Each time this method is called with the same
1298          * timer name, the timer is reset. This functionality is useful in cases where an action needs
1299          * to occur only after a certain period of inactivity. For example, making an AJAX call after 
1300          * user stoped typing for 1 second.
1301          *
1302          * @signature TextExtPlugin.startTimer(name, delay, callback)
1303          *
1304          * @param name {String} Timer name.
1305          * @param delay {Number} Delay in seconds.
1306          * @param callback {Function} Callback function.
1307          *
1308          * @author agorbatchev
1309          * @date 2011/08/25
1310          * @id TextExtPlugin.startTimer
1311          */
1312         p.startTimer = function(name, delay, callback)
1313         {
1314                 var self = this;
1315
1316                 self.stopTimer(name);
1317
1318                 self._timers[name] = setTimeout(
1319                         function()
1320                         {
1321                                 delete self._timers[name];
1322                                 callback.apply(self);
1323                         },
1324                         delay * 1000
1325                 );
1326         };
1327
1328         /**
1329          * Stops the timer by name without resetting it.
1330          *
1331          * @signature TextExtPlugin.stopTimer(name)
1332          *
1333          * @param name {String} Timer name.
1334          *
1335          * @author agorbatchev
1336          * @date 2011/08/25
1337          * @id TextExtPlugin.stopTimer
1338          */
1339         p.stopTimer = function(name)
1340         {
1341                 clearTimeout(this._timers[name]);
1342         };
1343
1344         /**
1345          * Returns instance of the `TextExt` to which current instance of the plugin is attached to.
1346          *
1347          * @signature TextExtPlugin.core()
1348          *
1349          * @author agorbatchev
1350          * @date 2011/08/19
1351          * @id TextExtPlugin.core
1352          */
1353         p.core = function()
1354         {
1355                 return this._core;
1356         };
1357
1358         /**
1359          * Shortcut to the core's `opts()` method. Returns option value.
1360          *
1361          * @signature TextExtPlugin.opts(name)
1362          * 
1363          * @param name {String} Option name as described in the options.
1364          *
1365          * @author agorbatchev
1366          * @date 2011/08/19
1367          * @id TextExtPlugin.opts
1368          */
1369         p.opts = function(name)
1370         {
1371                 return this.core().opts(name);
1372         };
1373
1374         /**
1375          * Shortcut to the core's `itemManager()` method. Returns instance of the `ItemManger` that is
1376          * currently in use.
1377          *
1378          * @signature TextExtPlugin.itemManager()
1379          *
1380          * @author agorbatchev
1381          * @date 2011/08/19
1382          * @id TextExtPlugin.itemManager
1383          */
1384         p.itemManager = function()
1385         {
1386                 return this.core().itemManager();
1387         };
1388
1389         /**
1390          * Shortcut to the core's `input()` method. Returns instance of the HTML element that represents
1391          * current text input.
1392          *
1393          * @signature TextExtPlugin.input()
1394          *
1395          * @author agorbatchev
1396          * @date 2011/08/19
1397          * @id TextExtPlugin.input
1398          */
1399         p.input = function()
1400         {
1401                 return this.core().input();
1402         };
1403
1404         /**
1405          * Shortcut to the commonly used `this.input().val()` call to get or set value of the text input.
1406          *
1407          * @signature TextExtPlugin.val(value)
1408          *
1409          * @param value {String} Optional value. If specified, the value will be set, otherwise it will be
1410          * returned.
1411          *
1412          * @author agorbatchev
1413          * @date 2011/08/20
1414          * @id TextExtPlugin.val
1415          */
1416         p.val = function(value)
1417         {
1418                 var input = this.input();
1419
1420                 if(typeof(value) === UNDEFINED)
1421                         return input.val();
1422                 else
1423                         input.val(value);
1424         };
1425
1426         /**
1427          * Shortcut to the core's `trigger()` method. Triggers specified event with arguments on the
1428          * component core.
1429          *
1430          * @signature TextExtPlugin.trigger(event, ...args)
1431          *
1432          * @param event {String} Name of the event to trigger.
1433          * @param ...args All remaining arguments will be passed to the event handler.
1434          *
1435          * @author agorbatchev
1436          * @date 2011/08/19
1437          * @id TextExtPlugin.trigger
1438          */
1439         p.trigger = function()
1440         {
1441                 var core = this.core();
1442                 core.trigger.apply(core, arguments);
1443         };
1444
1445         /**
1446          * Shortcut to the core's `bind()` method. Binds specified handler to the event.
1447          *
1448          * @signature TextExtPlugin.bind(event, handler)
1449          *
1450          * @param event {String} Event name.
1451          * @param handler {Function} Event handler.
1452          *
1453          * @author agorbatchev
1454          * @date 2011/08/20
1455          * @id TextExtPlugin.bind
1456          */
1457         p.bind = function(event, handler)
1458         {
1459                 this.core().bind(event, handler);
1460         };
1461
1462         /**
1463          * Returns initialization priority for this plugin. If current plugin depends upon some other plugin
1464          * to be initialized before or after, priority needs to be adjusted accordingly. Plugins with higher
1465          * priority initialize before plugins with lower priority.
1466          *
1467          * Default initialization priority is `0`.
1468          *
1469          * @signature TextExtPlugin.initPriority()
1470          *
1471          * @author agorbatchev
1472          * @date 2011/08/22
1473          * @id TextExtPlugin.initPriority
1474          */
1475         p.initPriority = function()
1476         {
1477                 return 0;
1478         };
1479
1480         //--------------------------------------------------------------------------------
1481         // jQuery Integration
1482         
1483         /**
1484          * TextExt integrates as a jQuery plugin available through the `$(selector).textext(opts)` call. If
1485          * `opts` argument is passed, then a new instance of `TextExt` will be created for all the inputs
1486          * that match the `selector`. If `opts` wasn't passed and TextExt was already intantiated for 
1487          * inputs that match the `selector`, array of `TextExt` instances will be returned instead.
1488          *
1489          *     // will create a new instance of `TextExt` for all elements that match `.sample`
1490          *     $('.sample').textext({ ... });
1491          *
1492          *     // will return array of all `TextExt` instances
1493          *     var list = $('.sample').textext();
1494          *
1495          * The following properties are also exposed through the jQuery `$.fn.textext`:
1496          *
1497          * * `TextExt` -- `TextExt` class.
1498          * * `TextExtPlugin` -- `TextExtPlugin` class.
1499          * * `ItemManager` -- `ItemManager` class.
1500          * * `plugins` -- Key/value table of all registered plugins.
1501          * * `addPlugin(name, constructor)` -- All plugins should register themselves using this function.
1502          *
1503          * @author agorbatchev
1504          * @date 2011/08/19
1505          * @id TextExt.jquery
1506          */
1507
1508         var cssInjected = false;
1509
1510         var textext = $.fn.textext = function(opts)
1511         {
1512                 var css;
1513                 
1514                 if(!cssInjected && (css = $.fn.textext.css) != null)
1515                 {
1516                         $('head').append('<style>' + css + '</style>');
1517                         cssInjected = true;
1518                 }
1519
1520                 return this.map(function()
1521                 {
1522                         var self = $(this);
1523
1524                         if(opts == null)
1525                                 return self.data('textext');
1526
1527                         var instance = new TextExt();
1528
1529                         instance.init(self, opts);
1530                         self.data('textext', instance);
1531
1532                         return instance.input()[0];
1533                 });
1534         };
1535
1536         /**
1537          * This static function registers a new plugin which makes it available through the `plugins` option
1538          * to the end user. The name specified here is the name the end user would put in the `plugins` option
1539          * to add this plugin to a new instance of TextExt.
1540          * 
1541          * @signature $.fn.textext.addPlugin(name, constructor)
1542          *
1543          * @param name {String} Name of the plugin.
1544          * @param constructor {Function} Plugin constructor.
1545          *
1546          * @author agorbatchev
1547          * @date 2011/10/11
1548          * @id TextExt.addPlugin
1549          */
1550         textext.addPlugin = function(name, constructor)
1551         {
1552                 textext.plugins[name] = constructor;
1553                 constructor.prototype = new textext.TextExtPlugin();
1554         };
1555
1556         /**
1557          * This static function registers a new patch which is added to each instance of TextExt. If you are
1558          * adding a new patch, make sure to call this method.
1559          * 
1560          * @signature $.fn.textext.addPatch(name, constructor)
1561          *
1562          * @param name {String} Name of the patch.
1563          * @param constructor {Function} Patch constructor.
1564          *
1565          * @author agorbatchev
1566          * @date 2011/10/11
1567          * @id TextExt.addPatch
1568          */
1569         textext.addPatch = function(name, constructor)
1570         {
1571                 textext.patches[name] = constructor;
1572                 constructor.prototype = new textext.TextExtPlugin();
1573         };
1574
1575         textext.TextExt       = TextExt;
1576         textext.TextExtPlugin = TextExtPlugin;
1577         textext.ItemManager   = ItemManager;
1578         textext.plugins       = {};
1579         textext.patches       = {};
1580 })(jQuery);
1581
1582 (function($)
1583 {
1584         function TextExtIE9Patches() {};
1585
1586         $.fn.textext.TextExtIE9Patches = TextExtIE9Patches;
1587         $.fn.textext.addPatch('ie9',TextExtIE9Patches);
1588
1589         var p = TextExtIE9Patches.prototype;
1590
1591         p.init = function(core)
1592         {
1593                 if(navigator.userAgent.indexOf('MSIE 9') == -1)
1594                         return;
1595
1596                 var self = this;
1597
1598                 core.on({ postInvalidate : self.onPostInvalidate });
1599         };
1600
1601         p.onPostInvalidate = function()
1602         {
1603                 var self  = this,
1604                         input = self.input(),
1605                         val   = input.val()
1606                         ;
1607
1608                 // agorbatchev :: IE9 doesn't seem to update the padding if box-sizing is on until the
1609                 // text box value changes, so forcing this change seems to do the trick of updating
1610                 // IE's padding visually.
1611                 input.val(Math.random());
1612                 input.val(val);
1613         };
1614 })(jQuery);
1615
1616 ;/**
1617  * jQuery TextExt Plugin
1618  * http://textextjs.com
1619  *
1620  * @version 1.3.0
1621  * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
1622  * @license MIT License
1623  */
1624 (function($)
1625 {
1626         /**
1627          * AJAX plugin is very useful if you want to load list of items from a data point and pass it
1628          * to the Autocomplete or Filter plugins.
1629          *
1630          * Because it meant to be as a helper method for either Autocomplete or Filter plugin, without
1631          * either of these two present AJAX plugin won't do anything.
1632          *
1633          * @author agorbatchev
1634          * @date 2011/08/16
1635          * @id TextExtAjax
1636          */
1637         function TextExtAjax() {};
1638
1639         $.fn.textext.TextExtAjax = TextExtAjax;
1640         $.fn.textext.addPlugin('ajax', TextExtAjax);
1641
1642         var p = TextExtAjax.prototype,
1643
1644                 /**
1645                  * AJAX plugin options are grouped under `ajax` when passed to the `$().textext()` function. Be
1646                  * mindful that the whole `ajax` object is also passed to jQuery `$.ajax` call which means that
1647                  * you can change all jQuery options as well. Please refer to the jQuery documentation on how
1648                  * to set url and all other parameters. For example:
1649                  *
1650                  *     $('textarea').textext({
1651                  *         plugins: 'ajax',
1652                  *         ajax: {
1653                  *             url: 'http://...'
1654                  *         }
1655                  *     })
1656                  *
1657                  * **Important**: Because it's necessary to pass options to `jQuery.ajax()` in a single object,
1658                  * all jQuery related AJAX options like `url`, `dataType`, etc **must** be within the `ajax` object.
1659                  * This is the exception to general rule that TextExt options can be specified in dot or camel case 
1660                  * notation.
1661                  * 
1662                  * @author agorbatchev
1663                  * @date 2011/08/16
1664                  * @id TextExtAjax.options
1665                  */
1666
1667                 /**
1668                  * By default, when user starts typing into the text input, AJAX plugin will start making requests
1669                  * to the `url` that you have specified and will pass whatever user has typed so far as a parameter
1670                  * named `q`, eg `?q=foo`.
1671                  *
1672                  * If you wish to change this behaviour, you can pass a function as a value for this option which
1673                  * takes one argument (the user input) and should return a key/value object that will be converted
1674                  * to the request parameters. For example:
1675                  *
1676                  *     'dataCallback' : function(query)
1677                  *     {
1678                  *         return { 'search' : query };
1679                  *     } 
1680                  *
1681                  * @name ajax.data.callback
1682                  * @default null
1683                  * @author agorbatchev
1684                  * @date 2011/08/16
1685                  * @id TextExtAjax.options.data.callback
1686                  */
1687                 OPT_DATA_CALLBACK = 'ajax.data.callback',
1688                 
1689                 /**
1690                  * By default, the server end point is constantly being reloaded whenever user changes the value
1691                  * in the text input. If you'd rather have the client do result filtering, you can return all
1692                  * possible results from the server and cache them on the client by setting this option to `true`.
1693                  *
1694                  * In such a case, only one call to the server will be made and filtering will be performed on
1695                  * the client side using `ItemManager` attached to the core.
1696                  *
1697                  * @name ajax.data.results
1698                  * @default false
1699                  * @author agorbatchev
1700                  * @date 2011/08/16
1701                  * @id TextExtAjax.options.cache.results
1702                  */
1703                 OPT_CACHE_RESULTS = 'ajax.cache.results',
1704                 
1705                 /**
1706                  * The loading message delay is set in seconds and will specify how long it would take before
1707                  * user sees the message. If you don't want user to ever see this message, set the option value
1708                  * to `Number.MAX_VALUE`.
1709                  *
1710                  * @name ajax.loading.delay
1711                  * @default 0.5
1712                  * @author agorbatchev
1713                  * @date 2011/08/16
1714                  * @id TextExtAjax.options.loading.delay
1715                  */
1716                 OPT_LOADING_DELAY = 'ajax.loading.delay',
1717
1718                 /**
1719                  * Whenever an AJAX request is made and the server takes more than the number of seconds specified
1720                  * in `ajax.loading.delay` to respond, the message specified in this option will appear in the drop
1721                  * down.
1722                  *
1723                  * @name ajax.loading.message
1724                  * @default "Loading..."
1725                  * @author agorbatchev
1726                  * @date 2011/08/17
1727                  * @id TextExtAjax.options.loading.message
1728                  */
1729                 OPT_LOADING_MESSAGE = 'ajax.loading.message',
1730
1731                 /**
1732                  * When user is typing in or otherwise changing the value of the text input, it's undesirable to make
1733                  * an AJAX request for every keystroke. Instead it's more conservative to send a request every number
1734                  * of seconds while user is typing the value. This number of seconds is specified by the `ajax.type.delay`
1735                  * option.
1736                  *
1737                  * @name ajax.type.delay
1738                  * @default 0.5
1739                  * @author agorbatchev
1740                  * @date 2011/08/17
1741                  * @id TextExtAjax.options.type.delay
1742                  */
1743                 OPT_TYPE_DELAY = 'ajax.type.delay',
1744
1745                 /**
1746                  * AJAX plugin dispatches or reacts to the following events.
1747                  *
1748                  * @author agorbatchev
1749                  * @date 2011/08/17
1750                  * @id TextExtAjax.events
1751                  */
1752
1753                 /**
1754                  * AJAX plugin reacts to the `getSuggestions` event dispatched by the Autocomplete plugin.
1755                  *
1756                  * @name getSuggestions
1757                  * @author agorbatchev
1758                  * @date 2011/08/17
1759                  * @id TextExtAjax.events.getSuggestions
1760                  */
1761
1762                 /**
1763                  * In the event of successful AJAX request, the AJAX coponent dispatches the `setSuggestions`
1764                  * event meant to be recieved by the Autocomplete plugin.
1765                  *
1766                  * @name setSuggestions
1767                  * @author agorbatchev
1768                  * @date 2011/08/17
1769                  * @id TextExtAjax.events.setSuggestions
1770                  */
1771                 EVENT_SET_SUGGESTION = 'setSuggestions',
1772
1773                 /**
1774                  * AJAX plugin dispatches the `showDropdown` event which Autocomplete plugin is expecting.
1775                  * This is used to temporarily show the loading message if the AJAX request is taking longer
1776                  * than expected.
1777                  *
1778                  * @name showDropdown
1779                  * @author agorbatchev
1780                  * @date 2011/08/17
1781                  * @id TextExtAjax.events.showDropdown
1782                  */
1783                 EVENT_SHOW_DROPDOWN = 'showDropdown',
1784
1785                 TIMER_LOADING = 'loading',
1786
1787                 DEFAULT_OPTS = {
1788                         ajax : {
1789                                 typeDelay      : 0.5,
1790                                 loadingMessage : 'Loading...',
1791                                 loadingDelay   : 0.5,
1792                                 cacheResults   : false,
1793                                 dataCallback   : null
1794                         }
1795                 }
1796                 ;
1797
1798         /**
1799          * Initialization method called by the core during plugin instantiation.
1800          *
1801          * @signature TextExtAjax.init(core)
1802          *
1803          * @param core {TextExt} Instance of the TextExt core class.
1804          *
1805          * @author agorbatchev
1806          * @date 2011/08/17
1807          * @id TextExtAjax.init
1808          */
1809         p.init = function(core)
1810         {
1811                 var self = this;
1812
1813                 self.baseInit(core, DEFAULT_OPTS);
1814
1815                 self.on({
1816                         getSuggestions : self.onGetSuggestions
1817                 });
1818
1819                 self._suggestions = null;
1820         };
1821
1822         /**
1823          * Performas an async AJAX with specified options.
1824          *
1825          * @signature TextExtAjax.load(query)
1826          *
1827          * @param query {String} Value that user has typed into the text area which is
1828          * presumably the query.
1829          *
1830          * @author agorbatchev
1831          * @date 2011/08/14
1832          * @id TextExtAjax.load
1833          */
1834         p.load = function(query)
1835         {
1836                 var self         = this,
1837                         dataCallback = self.opts(OPT_DATA_CALLBACK) || function(query) { return { q : query } },
1838                         opts
1839                         ;
1840
1841                 opts = $.extend(true,
1842                         {
1843                                 data    : dataCallback(query),
1844                                 success : function(data) { self.onComplete(data, query) },
1845                                 error   : function(jqXHR, message) { console.error(message, query) }
1846                         }, 
1847                         self.opts('ajax')
1848                 );
1849
1850                 $.ajax(opts);
1851         };
1852
1853         /**
1854          * Successful call AJAX handler. Takes the data that came back from AJAX and the
1855          * original query that was used to make the call.
1856          *
1857          * @signature TextExtAjax.onComplete(data, query)
1858          *
1859          * @param data {Object} Data loaded from the server, should be an Array of strings
1860          * by default or whatever data structure your custom `ItemManager` implements.
1861          *
1862          * @param query {String} Query string, ie whatever user has typed in.
1863          *
1864          * @author agorbatchev
1865          * @date 2011/08/14
1866          * @id TextExtAjax.onComplete
1867          */
1868         p.onComplete = function(data, query)
1869         {
1870                 var self   = this,
1871                         result = data
1872                         ;
1873                 
1874                 self.dontShowLoading();
1875
1876                 // If results are expected to be cached, then we store the original
1877                 // data set and return the filtered one based on the original query.
1878                 // That means we do filtering on the client side, instead of the
1879                 // server side.
1880                 if(self.opts(OPT_CACHE_RESULTS) == true)
1881                 {
1882                         self._suggestions = data;
1883                         result = self.itemManager().filter(data, query);
1884                 }
1885
1886                 self.trigger(EVENT_SET_SUGGESTION, { result : result });
1887         };
1888
1889         /**
1890          * If show loading message timer was started, calling this function disables it,
1891          * otherwise nothing else happens.
1892          *
1893          * @signature TextExtAjax.dontShowLoading()
1894          *
1895          * @author agorbatchev
1896          * @date 2011/08/16
1897          * @id TextExtAjax.dontShowLoading
1898          */
1899         p.dontShowLoading = function()
1900         {
1901                 this.stopTimer(TIMER_LOADING);
1902         };
1903
1904         /**
1905          * Shows message specified in `ajax.loading.message` if loading data takes more than
1906          * number of seconds specified in `ajax.loading.delay`.
1907          *
1908          * @signature TextExtAjax.showLoading()
1909          *
1910          * @author agorbatchev
1911          * @date 2011/08/15
1912          * @id TextExtAjax.showLoading
1913          */
1914         p.showLoading = function()
1915         {
1916                 var self = this;
1917
1918                 self.dontShowLoading();
1919                 self.startTimer(
1920                         TIMER_LOADING,
1921                         self.opts(OPT_LOADING_DELAY),
1922                         function()
1923                         {
1924                                 self.trigger(EVENT_SHOW_DROPDOWN, function(autocomplete)
1925                                 {
1926                                         autocomplete.clearItems();
1927                                         var node = autocomplete.addDropdownItem(self.opts(OPT_LOADING_MESSAGE));
1928                                         node.addClass('text-loading');
1929                                 });
1930                         }
1931                 );
1932         };
1933
1934         /**
1935          * Reacts to the `getSuggestions` event and begin loading suggestions. If
1936          * `ajax.cache.results` is specified, all calls after the first one will use
1937          * cached data and filter it with the `core.itemManager.filter()`.
1938          *
1939          * @signature TextExtAjax.onGetSuggestions(e, data)
1940          *
1941          * @param e {Object} jQuery event.
1942          * @param data {Object} Data structure passed with the `getSuggestions` event
1943          * which contains the user query, eg `{ query : "..." }`.
1944          *
1945          * @author agorbatchev
1946          * @date 2011/08/15
1947          * @id TextExtAjax.onGetSuggestions
1948          */
1949         p.onGetSuggestions = function(e, data)
1950         {
1951                 var self        = this,
1952                         suggestions = self._suggestions,
1953                         query       = (data || {}).query || ''
1954                         ;
1955
1956                 if(suggestions && self.opts(OPT_CACHE_RESULTS) === true)
1957                         return self.onComplete(suggestions, query);
1958                 
1959                 self.startTimer(
1960                         'ajax',
1961                         self.opts(OPT_TYPE_DELAY),
1962                         function()
1963                         {
1964                                 self.showLoading();
1965                                 self.load(query);
1966                         }
1967                 );
1968         };
1969 })(jQuery);
1970 ;/**
1971  * jQuery TextExt Plugin
1972  * http://textextjs.com
1973  *
1974  * @version 1.3.0
1975  * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
1976  * @license MIT License
1977  */
1978 (function($)
1979 {
1980         /**
1981          * Displays a dropdown style arrow button. The `TextExtArrow` works together with the
1982          * `TextExtAutocomplete` plugin and whenever clicked tells the autocomplete plugin to
1983          * display its suggestions.
1984          *
1985          * @author agorbatchev
1986          * @date 2011/12/27
1987          * @id TextExtArrow
1988          */
1989         function TextExtArrow() {};
1990
1991         $.fn.textext.TextExtArrow = TextExtArrow;
1992         $.fn.textext.addPlugin('arrow', TextExtArrow);
1993
1994         var p = TextExtArrow.prototype,
1995                 /**
1996                  * Arrow plugin only has one option and that is its HTML template. It could be 
1997                  * changed when passed to the `$().textext()` function. For example:
1998                  *
1999                  *     $('textarea').textext({
2000                  *         plugins: 'arrow',
2001                  *         html: {
2002                  *             arrow: "<span/>"
2003                  *         }
2004                  *     })
2005                  *
2006                  * @author agorbatchev
2007                  * @date 2011/12/27
2008                  * @id TextExtArrow.options
2009                  */
2010                 
2011                 /**
2012                  * HTML source that is used to generate markup required for the arrow.
2013                  *
2014                  * @name html.arrow
2015                  * @default '<div class="text-arrow"/>'
2016                  * @author agorbatchev
2017                  * @date 2011/12/27
2018                  * @id TextExtArrow.options.html.arrow
2019                  */
2020                 OPT_HTML_ARROW = 'html.arrow',
2021
2022                 DEFAULT_OPTS = {
2023                         html : {
2024                                 arrow : '<div class="text-arrow"/>'
2025                         }
2026                 }
2027                 ;
2028
2029         /**
2030          * Initialization method called by the core during plugin instantiation.
2031          *
2032          * @signature TextExtArrow.init(core)
2033          *
2034          * @param core {TextExt} Instance of the TextExt core class.
2035          *
2036          * @author agorbatchev
2037          * @date 2011/12/27
2038          * @id TextExtArrow.init
2039          */
2040         p.init = function(core)
2041         {
2042                 var self = this,
2043                         arrow
2044                         ;
2045
2046                 self.baseInit(core, DEFAULT_OPTS);
2047
2048                 self._arrow = arrow = $(self.opts(OPT_HTML_ARROW));
2049                 self.core().wrapElement().append(arrow);
2050                 arrow.bind('click', function(e) { self.onArrowClick(e); });
2051         };
2052
2053         //--------------------------------------------------------------------------------
2054         // Event handlers
2055         
2056         /**
2057          * Reacts to the `click` event whenever user clicks the arrow.
2058          *
2059          * @signature TextExtArrow.onArrowClick(e)
2060          *
2061          * @param e {Object} jQuery event.
2062          * @author agorbatchev
2063          * @date 2011/12/27
2064          * @id TextExtArrow.onArrowClick
2065          */
2066         p.onArrowClick = function(e)
2067         {
2068                 this.trigger('toggleDropdown');
2069                 this.core().focusInput();
2070         };
2071         
2072         //--------------------------------------------------------------------------------
2073         // Core functionality
2074
2075 })(jQuery);
2076 ;/**
2077  * jQuery TextExt Plugin
2078  * http://textextjs.com
2079  *
2080  * @version 1.3.0
2081  * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
2082  * @license MIT License
2083  */
2084 (function($)
2085 {
2086         /**
2087          * Autocomplete plugin brings the classic autocomplete functionality to the TextExt echosystem.
2088          * The gist of functionality is when user starts typing in, for example a term or a tag, a
2089          * dropdown would be presented with possible suggestions to complete the input quicker.
2090          *
2091          * @author agorbatchev
2092          * @date 2011/08/17
2093          * @id TextExtAutocomplete
2094          */
2095         function TextExtAutocomplete() {};
2096
2097         $.fn.textext.TextExtAutocomplete = TextExtAutocomplete;
2098         $.fn.textext.addPlugin('autocomplete', TextExtAutocomplete);
2099
2100         var p = TextExtAutocomplete.prototype,
2101                 
2102                 CSS_DOT            = '.',
2103                 CSS_SELECTED       = 'text-selected',
2104                 CSS_DOT_SELECTED   = CSS_DOT + CSS_SELECTED,
2105                 CSS_SUGGESTION     = 'text-suggestion',
2106                 CSS_DOT_SUGGESTION = CSS_DOT + CSS_SUGGESTION,
2107                 CSS_LABEL          = 'text-label',
2108                 CSS_DOT_LABEL      = CSS_DOT + CSS_LABEL,
2109
2110                 /**
2111                  * Autocomplete plugin options are grouped under `autocomplete` when passed to the 
2112                  * `$().textext()` function. For example:
2113                  *
2114                  *     $('textarea').textext({
2115                  *         plugins: 'autocomplete',
2116                  *         autocomplete: {
2117                  *             dropdownPosition: 'above'
2118                  *         }
2119                  *     })
2120                  *
2121                  * @author agorbatchev
2122                  * @date 2011/08/17
2123                  * @id TextExtAutocomplete.options
2124                  */
2125
2126                 /**
2127                  * This is a toggle switch to enable or disable the Autucomplete plugin. The value is checked
2128                  * each time at the top level which allows you to toggle this setting on the fly.
2129                  *
2130                  * @name autocomplete.enabled
2131                  * @default true
2132                  * @author agorbatchev
2133                  * @date 2011/08/17
2134                  * @id TextExtAutocomplete.options.autocomplete.enabled
2135                  */
2136                 OPT_ENABLED = 'autocomplete.enabled',
2137
2138                 /**
2139                  * This option allows to specify position of the dropdown. The two possible values
2140                  * are `above` and `below`.
2141                  *
2142                  * @name autocomplete.dropdown.position
2143                  * @default "below"
2144                  * @author agorbatchev
2145                  * @date 2011/08/17
2146                  * @id TextExtAutocomplete.options.autocomplete.dropdown.position
2147                  */
2148                 OPT_POSITION = 'autocomplete.dropdown.position',
2149
2150                 /**
2151                  * This option allows to specify maximum height of the dropdown. Value is taken directly, so
2152                  * if desired height is 200 pixels, value must be `200px`.
2153                  *
2154                  * @name autocomplete.dropdown.maxHeight
2155                  * @default "100px"
2156                  * @author agorbatchev
2157                  * @date 2011/12/29
2158                  * @id TextExtAutocomplete.options.autocomplete.dropdown.maxHeight
2159                  * @version 1.1
2160                  */
2161                 OPT_MAX_HEIGHT = 'autocomplete.dropdown.maxHeight',
2162
2163                 /**
2164                  * This option allows to override how a suggestion item is rendered. The value should be
2165                  * a function, the first argument of which is suggestion to be rendered and `this` context
2166                  * is the current instance of `TextExtAutocomplete`. 
2167                  *
2168                  * [Click here](/manual/examples/autocomplete-with-custom-render.html) to see a demo.
2169                  *
2170                  * For example:
2171                  *
2172                  *     $('textarea').textext({
2173                  *         plugins: 'autocomplete',
2174                  *         autocomplete: {
2175                  *             render: function(suggestion)
2176                  *             {
2177                  *                 return '<b>' + suggestion + '</b>';
2178                  *             }
2179                  *         }
2180                  *     })
2181                  *
2182                  * @name autocomplete.render
2183                  * @default null
2184                  * @author agorbatchev
2185                  * @date 2011/12/23
2186                  * @id TextExtAutocomplete.options.autocomplete.render
2187                  * @version 1.1
2188                  */
2189                 OPT_RENDER = 'autocomplete.render',
2190
2191                 /**
2192                  * HTML source that is used to generate the dropdown.
2193                  *
2194                  * @name html.dropdown
2195                  * @default '<div class="text-dropdown"><div class="text-list"/></div>'
2196                  * @author agorbatchev
2197                  * @date 2011/08/17
2198                  * @id TextExtAutocomplete.options.html.dropdown
2199                  */
2200                 OPT_HTML_DROPDOWN = 'html.dropdown',
2201
2202                 /**
2203                  * HTML source that is used to generate each suggestion.
2204                  *
2205                  * @name html.suggestion
2206                  * @default '<div class="text-suggestion"><span class="text-label"/></div>'
2207                  * @author agorbatchev
2208                  * @date 2011/08/17
2209                  * @id TextExtAutocomplete.options.html.suggestion
2210                  */
2211                 OPT_HTML_SUGGESTION = 'html.suggestion',
2212
2213                 /**
2214                  * Autocomplete plugin triggers or reacts to the following events.
2215                  *
2216                  * @author agorbatchev
2217                  * @date 2011/08/17
2218                  * @id TextExtAutocomplete.events
2219                  */
2220         
2221                 /**
2222                  * Autocomplete plugin triggers and reacts to the `hideDropdown` to hide the dropdown if it's 
2223                  * already visible.
2224                  *
2225                  * @name hideDropdown
2226                  * @author agorbatchev
2227                  * @date 2011/08/17
2228                  * @id TextExtAutocomplete.events.hideDropdown
2229                  */
2230                 EVENT_HIDE_DROPDOWN = 'hideDropdown',
2231
2232                 /**
2233                  * Autocomplete plugin triggers and reacts to the `showDropdown` to show the dropdown if it's 
2234                  * not already visible.
2235                  *
2236                  * It's possible to pass a render callback function which will be called instead of the
2237                  * default `TextExtAutocomplete.renderSuggestions()`. 
2238                  *
2239                  * Here's how another plugin should trigger this event with the optional render callback:
2240                  *
2241                  *     this.trigger('showDropdown', function(autocomplete)
2242                  *     {
2243                  *         autocomplete.clearItems();
2244                  *         var node = autocomplete.addDropdownItem('<b>Item</b>');
2245                  *         node.addClass('new-look');
2246                  *     });
2247                  *
2248                  * @name showDropdown
2249                  * @author agorbatchev
2250                  * @date 2011/08/17
2251                  * @id TextExtAutocomplete.events.showDropdown
2252                  */
2253                 EVENT_SHOW_DROPDOWN = 'showDropdown',
2254
2255                 /**
2256                  * Autocomplete plugin reacts to the `setSuggestions` event triggered by other plugins which
2257                  * wish to populate the suggestion items. Suggestions should be passed as event argument in the 
2258                  * following format: `{ data : [ ... ] }`. 
2259                  *
2260                  * Here's how another plugin should trigger this event:
2261                  *
2262                  *     this.trigger('setSuggestions', { data : [ "item1", "item2" ] });
2263                  *
2264                  * @name setSuggestions
2265                  * @author agorbatchev
2266                  * @date 2011/08/17
2267                  * @id TextExtAutocomplete.events.setSuggestions
2268                  */
2269
2270                 /**
2271                  * Autocomplete plugin triggers the `getSuggestions` event and expects to get results by listening for
2272                  * the `setSuggestions` event.
2273                  *
2274                  * @name getSuggestions
2275                  * @author agorbatchev
2276                  * @date 2011/08/17
2277                  * @id TextExtAutocomplete.events.getSuggestions
2278                  */
2279                 EVENT_GET_SUGGESTIONS = 'getSuggestions',
2280
2281                 /**
2282                  * Autocomplete plugin triggers `getFormData` event with the current suggestion so that the the core
2283                  * will be updated with serialized data to be submitted with the HTML form.
2284                  * 
2285                  * @name getFormData
2286                  * @author agorbatchev
2287                  * @date 2011/08/18
2288                  * @id TextExtAutocomplete.events.getFormData
2289                  */
2290                 EVENT_GET_FORM_DATA = 'getFormData',
2291
2292                 /**
2293                  * Autocomplete plugin reacts to `toggleDropdown` event and either shows or hides the dropdown
2294                  * depending if it's currently hidden or visible.
2295                  * 
2296                  * @name toggleDropdown
2297                  * @author agorbatchev
2298                  * @date 2011/12/27
2299                  * @id TextExtAutocomplete.events.toggleDropdown
2300                  * @version 1.1
2301                  */
2302                 EVENT_TOGGLE_DROPDOWN = 'toggleDropdown',
2303
2304                 POSITION_ABOVE = 'above',
2305                 POSITION_BELOW = 'below',
2306                 
2307                 DATA_MOUSEDOWN_ON_AUTOCOMPLETE = 'mousedownOnAutocomplete',
2308
2309                 DEFAULT_OPTS = {
2310                         autocomplete : {
2311                                 enabled : true,
2312                                 dropdown : {
2313                                         position : POSITION_BELOW,
2314                                         maxHeight : '100px'
2315                                 }
2316                         },
2317
2318                         html : {
2319                                 dropdown   : '<div class="text-dropdown"><div class="text-list"/></div>',
2320                                 suggestion : '<div class="text-suggestion"><span class="text-label"/></div>'
2321                         }
2322                 }
2323                 ;
2324
2325         /**
2326          * Initialization method called by the core during plugin instantiation.
2327          *
2328          * @signature TextExtAutocomplete.init(core)
2329          *
2330          * @param core {TextExt} Instance of the TextExt core class.
2331          *
2332          * @author agorbatchev
2333          * @date 2011/08/17
2334          * @id TextExtAutocomplete.init
2335          */
2336         p.init = function(core)
2337         {
2338                 var self = this;
2339
2340                 self.baseInit(core, DEFAULT_OPTS);
2341
2342                 var input = self.input(),
2343                         container
2344                         ;
2345
2346                 if(self.opts(OPT_ENABLED) === true)
2347                 {
2348                         self.on({
2349                                 blur              : self.onBlur,
2350                                 anyKeyUp          : self.onAnyKeyUp,
2351                                 deleteKeyUp       : self.onAnyKeyUp,
2352                                 backspaceKeyPress : self.onBackspaceKeyPress,
2353                                 enterKeyPress     : self.onEnterKeyPress,
2354                                 escapeKeyPress    : self.onEscapeKeyPress,
2355                                 setSuggestions    : self.onSetSuggestions,
2356                                 showDropdown      : self.onShowDropdown,
2357                                 hideDropdown      : self.onHideDropdown,
2358                                 toggleDropdown    : self.onToggleDropdown,
2359                                 postInvalidate    : self.positionDropdown,
2360                                 getFormData       : self.onGetFormData,
2361
2362                                 // using keyDown for up/down keys so that repeat events are
2363                                 // captured and user can scroll up/down by holding the keys
2364                                 downKeyDown : self.onDownKeyDown,
2365                                 upKeyDown   : self.onUpKeyDown
2366                         });
2367
2368                         container = $(self.opts(OPT_HTML_DROPDOWN));
2369                         container.insertAfter(input);
2370
2371                         self.on(container, {
2372                                 mouseover : self.onMouseOver,
2373                                 mousedown : self.onMouseDown,
2374                                 click     : self.onClick
2375                         });
2376
2377                         container
2378                                 .css('maxHeight', self.opts(OPT_MAX_HEIGHT))
2379                                 .addClass('text-position-' + self.opts(OPT_POSITION))
2380                                 ;
2381
2382                         $(self).data('container', container);
2383                         
2384                         $(document.body).click(function(e) 
2385                         {
2386                                 if (self.isDropdownVisible() && !self.withinWrapElement(e.target))
2387                                         self.trigger(EVENT_HIDE_DROPDOWN);
2388                         });
2389
2390                         self.positionDropdown();
2391                 }
2392         };
2393
2394         /**
2395          * Returns top level dropdown container HTML element.
2396          * 
2397          * @signature TextExtAutocomplete.containerElement()
2398          * 
2399          * @author agorbatchev
2400          * @date 2011/08/15
2401          * @id TextExtAutocomplete.containerElement
2402          */
2403         p.containerElement = function()
2404         {
2405                 return $(this).data('container');
2406         };
2407
2408         //--------------------------------------------------------------------------------
2409         // User mouse/keyboard input
2410         
2411         /**
2412          * Reacts to the `mouseOver` event triggered by the TextExt core.
2413          *
2414          * @signature TextExtAutocomplete.onMouseOver(e)
2415          *
2416          * @param e {Object} jQuery event.
2417          *
2418          * @author agorbatchev
2419          * @date 2011/08/17
2420          * @id TextExtAutocomplete.onMouseOver
2421          */
2422         p.onMouseOver = function(e)
2423         {
2424                 var self   = this,
2425                         target = $(e.target)
2426                         ;
2427
2428                 if(target.is(CSS_DOT_SUGGESTION))
2429                 {
2430                         self.clearSelected();
2431                         target.addClass(CSS_SELECTED);
2432                 }
2433         };
2434         
2435         /**
2436          * Reacts to the `mouseDown` event triggered by the TextExt core.
2437          *
2438          * @signature TextExtAutocomplete.onMouseDown(e)
2439          *
2440          * @param e {Object} jQuery event.
2441          *
2442          * @author adamayres
2443          * @date 2012/01/13
2444          * @id TextExtAutocomplete.onMouseDown
2445          */
2446         p.onMouseDown = function(e)
2447         {
2448                 this.containerElement().data(DATA_MOUSEDOWN_ON_AUTOCOMPLETE, true);
2449         };
2450         
2451         /**
2452          * Reacts to the `click` event triggered by the TextExt core.
2453          *
2454          * @signature TextExtAutocomplete.onClick(e)
2455          *
2456          * @param e {Object} jQuery event.
2457          *
2458          * @author agorbatchev
2459          * @date 2011/08/17
2460          * @id TextExtAutocomplete.onClick
2461          */
2462         p.onClick = function(e)
2463         {
2464                 var self   = this,
2465                         target = $(e.target)
2466                         ;
2467
2468                 if(target.is(CSS_DOT_SUGGESTION) || target.is(CSS_DOT_LABEL))
2469                         self.trigger('enterKeyPress');
2470                 
2471                 if (self.core().hasPlugin('tags'))
2472                         self.val('');
2473         };
2474
2475         /**
2476          * Reacts to the `blur` event triggered by the TextExt core.
2477          *
2478          * @signature TextExtAutocomplete.onBlur(e)
2479          *
2480          * @param e {Object} jQuery event.
2481          *
2482          * @author agorbatchev
2483          * @date 2011/08/17
2484          * @id TextExtAutocomplete.onBlur
2485          */
2486         p.onBlur = function(e)
2487         {
2488                 var self              = this,
2489                         container         = self.containerElement(),
2490                         isBlurByMousedown = container.data(DATA_MOUSEDOWN_ON_AUTOCOMPLETE) === true
2491                         ;
2492
2493                 // only trigger a close event if the blur event was 
2494                 // not triggered by a mousedown event on the autocomplete
2495                 // otherwise set focus back back on the input
2496                 if(self.isDropdownVisible())
2497                         isBlurByMousedown ? self.core().focusInput() : self.trigger(EVENT_HIDE_DROPDOWN);
2498                                 
2499                 container.removeData(DATA_MOUSEDOWN_ON_AUTOCOMPLETE);
2500         };
2501
2502         /**
2503          * Reacts to the `backspaceKeyPress` event triggered by the TextExt core. 
2504          *
2505          * @signature TextExtAutocomplete.onBackspaceKeyPress(e)
2506          *
2507          * @param e {Object} jQuery event.
2508          *
2509          * @author agorbatchev
2510          * @date 2011/08/17
2511          * @id TextExtAutocomplete.onBackspaceKeyPress
2512          */
2513         p.onBackspaceKeyPress = function(e)
2514         {
2515                 var self    = this,
2516                         isEmpty = self.val().length > 0
2517                         ;
2518
2519                 if(isEmpty || self.isDropdownVisible())
2520                         self.getSuggestions();
2521         };
2522
2523         /**
2524          * Reacts to the `anyKeyUp` event triggered by the TextExt core.
2525          *
2526          * @signature TextExtAutocomplete.onAnyKeyUp(e)
2527          *
2528          * @param e {Object} jQuery event.
2529          *
2530          * @author agorbatchev
2531          * @date 2011/08/17
2532          * @id TextExtAutocomplete.onAnyKeyUp
2533          */
2534         p.onAnyKeyUp = function(e, keyCode)
2535         {
2536                 var self          = this,
2537                         isFunctionKey = self.opts('keys.' + keyCode) != null
2538                         ;
2539
2540                 if(self.val().length > 0 && !isFunctionKey)
2541                         self.getSuggestions();
2542         };
2543
2544         /**
2545          * Reacts to the `downKeyDown` event triggered by the TextExt core.
2546          *
2547          * @signature TextExtAutocomplete.onDownKeyDown(e)
2548          *
2549          * @param e {Object} jQuery event.
2550          *
2551          * @author agorbatchev
2552          * @date 2011/08/17
2553          * @id TextExtAutocomplete.onDownKeyDown
2554          */
2555         p.onDownKeyDown = function(e)
2556         {
2557                 var self = this;
2558
2559                 self.isDropdownVisible()
2560                         ? self.toggleNextSuggestion() 
2561                         : self.getSuggestions()
2562                         ;
2563         };
2564
2565         /**
2566          * Reacts to the `upKeyDown` event triggered by the TextExt core.
2567          *
2568          * @signature TextExtAutocomplete.onUpKeyDown(e)
2569          *
2570          * @param e {Object} jQuery event.
2571          *
2572          * @author agorbatchev
2573          * @date 2011/08/17
2574          * @id TextExtAutocomplete.onUpKeyDown
2575          */
2576         p.onUpKeyDown = function(e)
2577         {
2578                 this.togglePreviousSuggestion();
2579         };
2580
2581         /**
2582          * Reacts to the `enterKeyPress` event triggered by the TextExt core.
2583          *
2584          * @signature TextExtAutocomplete.onEnterKeyPress(e)
2585          *
2586          * @param e {Object} jQuery event.
2587          *
2588          * @author agorbatchev
2589          * @date 2011/08/17
2590          * @id TextExtAutocomplete.onEnterKeyPress
2591          */
2592         p.onEnterKeyPress = function(e)
2593         {
2594                 var self = this;
2595
2596                 if(self.isDropdownVisible())
2597                         self.selectFromDropdown();
2598         };
2599
2600         /**
2601          * Reacts to the `escapeKeyPress` event triggered by the TextExt core. Hides the dropdown
2602          * if it's currently visible.
2603          *
2604          * @signature TextExtAutocomplete.onEscapeKeyPress(e)
2605          *
2606          * @param e {Object} jQuery event.
2607          *
2608          * @author agorbatchev
2609          * @date 2011/08/17
2610          * @id TextExtAutocomplete.onEscapeKeyPress
2611          */
2612         p.onEscapeKeyPress = function(e)
2613         {
2614                 var self = this;
2615
2616                 if(self.isDropdownVisible())
2617                         self.trigger(EVENT_HIDE_DROPDOWN);
2618         };
2619
2620         //--------------------------------------------------------------------------------
2621         // Core functionality
2622
2623         /**
2624          * Positions dropdown either below or above the input based on the `autocomplete.dropdown.position`
2625          * option specified, which could be either `above` or `below`.
2626          *
2627          * @signature TextExtAutocomplete.positionDropdown()
2628          *
2629          * @author agorbatchev
2630          * @date 2011/08/15
2631          * @id TextExtAutocomplete.positionDropdown
2632          */
2633         p.positionDropdown = function()
2634         {
2635                 var self      = this,
2636                         container = self.containerElement(),
2637                         direction = self.opts(OPT_POSITION),
2638                         height    = self.core().wrapElement().outerHeight(),
2639                         css       = {}
2640                         ;
2641
2642                 css[direction === POSITION_ABOVE ? 'bottom' : 'top'] = height + 'px';
2643                 container.css(css);
2644         };
2645
2646         /**
2647          * Returns list of all the suggestion HTML elements in the dropdown.
2648          *
2649          * @signature TextExtAutocomplete.suggestionElements()
2650          *
2651          * @author agorbatchev
2652          * @date 2011/08/17
2653          * @id TextExtAutocomplete.suggestionElements
2654          */
2655         p.suggestionElements = function()
2656         {
2657                 return this.containerElement().find(CSS_DOT_SUGGESTION);
2658         };
2659
2660
2661         /**
2662          * Highlights specified suggestion as selected in the dropdown.
2663          *
2664          * @signature TextExtAutocomplete.setSelectedSuggestion(suggestion)
2665          *
2666          * @param suggestion {Object} Suggestion object. With the default `ItemManager` this
2667          * is expected to be a string, anything else with custom implementations.
2668          *
2669          * @author agorbatchev
2670          * @date 2011/08/17
2671          * @id TextExtAutocomplete.setSelectedSuggestion
2672          */
2673         p.setSelectedSuggestion = function(suggestion)
2674         {
2675                 if(!suggestion)
2676                         return;
2677
2678                 var self   = this,
2679                         all    = self.suggestionElements(),
2680                         target = all.first(),
2681                         item, i
2682                         ;
2683
2684                 self.clearSelected();
2685
2686                 for(i = 0; i < all.length; i++)
2687                 {
2688                         item = $(all[i]);
2689
2690                         if(self.itemManager().compareItems(item.data(CSS_SUGGESTION), suggestion))
2691                         {
2692                                 target = item.addClass(CSS_SELECTED);
2693                                 break;
2694                         }
2695                 }
2696
2697                 target.addClass(CSS_SELECTED);
2698                 self.scrollSuggestionIntoView(target);
2699         };
2700
2701         /**
2702          * Returns the first suggestion HTML element from the dropdown that is highlighted as selected.
2703          *
2704          * @signature TextExtAutocomplete.selectedSuggestionElement()
2705          *
2706          * @author agorbatchev
2707          * @date 2011/08/17
2708          * @id TextExtAutocomplete.selectedSuggestionElement
2709          */
2710         p.selectedSuggestionElement = function()
2711         {
2712                 return this.suggestionElements().filter(CSS_DOT_SELECTED).first();
2713         };
2714
2715         /**
2716          * Returns `true` if dropdown is currently visible, `false` otherwise.
2717          *
2718          * @signature TextExtAutocomplete.isDropdownVisible()
2719          *
2720          * @author agorbatchev
2721          * @date 2011/08/17
2722          * @id TextExtAutocomplete.isDropdownVisible
2723          */
2724         p.isDropdownVisible = function()
2725         {
2726                 return this.containerElement().is(':visible') === true;
2727         };
2728
2729         /**
2730          * Reacts to the `getFormData` event triggered by the core. Returns data with the
2731          * weight of 100 to be *less than the Tags plugin* data weight. The weights system is
2732          * covered in greater detail in the [`getFormData`][1] event documentation.
2733          *
2734          * [1]: /manual/textext.html#getformdata
2735          *
2736          * @signature TextExtAutocomplete.onGetFormData(e, data, keyCode)
2737          *
2738          * @param e {Object} jQuery event.
2739          * @param data {Object} Data object to be populated.
2740          * @param keyCode {Number} Key code that triggered the original update request.
2741          *
2742          * @author agorbatchev
2743          * @date 2011/08/22
2744          * @id TextExtAutocomplete.onGetFormData
2745          */
2746         p.onGetFormData = function(e, data, keyCode)
2747         {
2748                 var self       = this,
2749                         val        = self.val(),
2750                         inputValue = val,
2751                         formValue  = val
2752                         ;
2753                 data[100] = self.formDataObject(inputValue, formValue);
2754         };
2755
2756         /**
2757          * Returns initialization priority of the Autocomplete plugin which is expected to be
2758          * *greater than the Tags plugin* because of the dependencies. The value is 200.
2759          *
2760          * @signature TextExtAutocomplete.initPriority()
2761          *
2762          * @author agorbatchev
2763          * @date 2011/08/22
2764          * @id TextExtAutocomplete.initPriority
2765          */
2766         p.initPriority = function()
2767         {
2768                 return 200;
2769         };
2770
2771         /**
2772          * Reacts to the `hideDropdown` event and hides the dropdown if it's already visible.
2773          *
2774          * @signature TextExtAutocomplete.onHideDropdown(e)
2775          *
2776          * @param e {Object} jQuery event.
2777          *
2778          * @author agorbatchev
2779          * @date 2011/08/17
2780          * @id TextExtAutocomplete.onHideDropdown
2781          */
2782         p.onHideDropdown = function(e)
2783         {
2784                 this.hideDropdown();
2785         };
2786
2787         /**
2788          * Reacts to the 'toggleDropdown` event and shows or hides the dropdown depending if
2789          * it's currently hidden or visible.
2790          *
2791          * @signature TextExtAutocomplete.onToggleDropdown(e)
2792          *
2793          * @param e {Object} jQuery event.
2794          *
2795          * @author agorbatchev
2796          * @date 2011/12/27
2797          * @id TextExtAutocomplete.onToggleDropdown
2798          * @version 1.1.0
2799          */
2800         p.onToggleDropdown = function(e)
2801         {
2802                 var self = this;
2803                 self.trigger(self.containerElement().is(':visible') ? EVENT_HIDE_DROPDOWN : EVENT_SHOW_DROPDOWN);
2804         };
2805
2806         /**
2807          * Reacts to the `showDropdown` event and shows the dropdown if it's not already visible.
2808          * It's possible to pass a render callback function which will be called instead of the
2809          * default `TextExtAutocomplete.renderSuggestions()`.
2810          *
2811          * If no suggestion were previously loaded, it will fire `getSuggestions` event and exit.
2812          *
2813          * Here's how another plugin should trigger this event with the optional render callback:
2814          *
2815          *     this.trigger('showDropdown', function(autocomplete)
2816          *     {
2817          *         autocomplete.clearItems();
2818          *         var node = autocomplete.addDropdownItem('<b>Item</b>');
2819          *         node.addClass('new-look');
2820          *     });
2821          *
2822          * @signature TextExtAutocomplete.onShowDropdown(e, renderCallback)
2823          *
2824          * @param e {Object} jQuery event.
2825          * @param renderCallback {Function} Optional callback function which would be used to 
2826          * render dropdown items. As a first argument, reference to the current instance of 
2827          * Autocomplete plugin will be supplied. It's assumed, that if this callback is provided
2828          * rendering will be handled completely manually.
2829          *
2830          * @author agorbatchev
2831          * @date 2011/08/17
2832          * @id TextExtAutocomplete.onShowDropdown
2833          */
2834         p.onShowDropdown = function(e, renderCallback)
2835         {
2836                 var self        = this,
2837                         current     = self.selectedSuggestionElement().data(CSS_SUGGESTION),
2838                         suggestions = self._suggestions
2839                         ;
2840
2841                 if(!suggestions)
2842                         return self.trigger(EVENT_GET_SUGGESTIONS);
2843
2844                 if($.isFunction(renderCallback))
2845                 {
2846                         renderCallback(self);
2847                 }
2848                 else
2849                 {
2850                         self.renderSuggestions(self._suggestions);
2851                         self.toggleNextSuggestion();
2852                 }
2853                 
2854                 self.showDropdown(self.containerElement());
2855                 self.setSelectedSuggestion(current);
2856         };
2857
2858         /**
2859          * Reacts to the `setSuggestions` event. Expects to recieve the payload as the second argument
2860          * in the following structure:
2861          *
2862          *     {
2863          *         result : [ "item1", "item2" ],
2864          *         showHideDropdown : false
2865          *     }
2866          *
2867          * Notice the optional `showHideDropdown` option. By default, ie without the `showHideDropdown` 
2868          * value the method will trigger either `showDropdown` or `hideDropdown` depending if there are
2869          * suggestions. If set to `false`, no event is triggered.
2870          *
2871          * @signature TextExtAutocomplete.onSetSuggestions(e, data)
2872          *
2873          * @param data {Object} Data payload.
2874          *
2875          * @author agorbatchev
2876          * @date 2011/08/17
2877          * @id TextExtAutocomplete.onSetSuggestions
2878          */
2879         p.onSetSuggestions = function(e, data)
2880         {
2881                 var self        = this,
2882                         suggestions = self._suggestions = data.result
2883                         ;
2884
2885                 if(data.showHideDropdown !== false)
2886                         self.trigger(suggestions === null || suggestions.length === 0 ? EVENT_HIDE_DROPDOWN : EVENT_SHOW_DROPDOWN);
2887         };
2888
2889         /**
2890          * Prepears for and triggers the `getSuggestions` event with the `{ query : {String} }` as second
2891          * argument.
2892          *
2893          * @signature TextExtAutocomplete.getSuggestions()
2894          *
2895          * @author agorbatchev
2896          * @date 2011/08/17
2897          * @id TextExtAutocomplete.getSuggestions
2898          */
2899         p.getSuggestions = function()
2900         {
2901                 var self = this,
2902                         val  = self.val()
2903                         ;
2904
2905                 if(self._previousInputValue == val)
2906                         return;
2907
2908                 // if user clears input, then we want to select first suggestion
2909                 // instead of the last one
2910                 if(val == '')
2911                         current = null;
2912
2913                 self._previousInputValue = val;
2914                 self.trigger(EVENT_GET_SUGGESTIONS, { query : val });
2915         };
2916
2917         /**
2918          * Removes all HTML suggestion items from the dropdown.
2919          *
2920          * @signature TextExtAutocomplete.clearItems()
2921          *
2922          * @author agorbatchev
2923          * @date 2011/08/17
2924          * @id TextExtAutocomplete.clearItems
2925          */
2926         p.clearItems = function()
2927         {
2928                 this.containerElement().find('.text-list').children().remove();
2929         };
2930
2931         /**
2932          * Clears all and renders passed suggestions.
2933          *
2934          * @signature TextExtAutocomplete.renderSuggestions(suggestions)
2935          *
2936          * @param suggestions {Array} List of suggestions to render.
2937          *
2938          * @author agorbatchev
2939          * @date 2011/08/17
2940          * @id TextExtAutocomplete.renderSuggestions
2941          */
2942         p.renderSuggestions = function(suggestions)
2943         {
2944                 var self = this;
2945
2946                 self.clearItems();
2947
2948                 $.each(suggestions || [], function(index, item)
2949                 {
2950                         self.addSuggestion(item);
2951                 });
2952         };
2953
2954         /**
2955          * Shows the dropdown.
2956          *
2957          * @signature TextExtAutocomplete.showDropdown()
2958          *
2959          * @author agorbatchev
2960          * @date 2011/08/17
2961          * @id TextExtAutocomplete.showDropdown
2962          */
2963         p.showDropdown = function()
2964         {
2965                 this.containerElement().show();
2966         };
2967
2968         /**
2969          * Hides the dropdown.
2970          *
2971          * @signature TextExtAutocomplete.hideDropdown()
2972          *
2973          * @author agorbatchev
2974          * @date 2011/08/17
2975          * @id TextExtAutocomplete.hideDropdown
2976          */
2977         p.hideDropdown = function()
2978         {
2979                 var self     = this,
2980                         dropdown = self.containerElement()
2981                         ;
2982
2983                 self._previousInputValue = null;
2984                 dropdown.hide();
2985         };
2986
2987         /**
2988          * Adds single suggestion to the bottom of the dropdown. Uses `ItemManager.itemToString()` to
2989          * serialize provided suggestion to string.
2990          *
2991          * @signature TextExtAutocomplete.addSuggestion(suggestion)
2992          *
2993          * @param suggestion {Object} Suggestion item. By default expected to be a string.
2994          *
2995          * @author agorbatchev
2996          * @date 2011/08/17
2997          * @id TextExtAutocomplete.addSuggestion
2998          */
2999         p.addSuggestion = function(suggestion)
3000         {
3001                 var self     = this,
3002                         renderer = self.opts(OPT_RENDER),
3003                         node     = self.addDropdownItem(renderer ? renderer.call(self, suggestion) : self.itemManager().itemToString(suggestion))
3004                         ;
3005
3006                 node.data(CSS_SUGGESTION, suggestion);
3007         };
3008
3009         /**
3010          * Adds and returns HTML node to the bottom of the dropdown.
3011          *
3012          * @signature TextExtAutocomplete.addDropdownItem(html)
3013          *
3014          * @param html {String} HTML to be inserted into the item.
3015          *
3016          * @author agorbatchev
3017          * @date 2011/08/17
3018          * @id TextExtAutocomplete.addDropdownItem
3019          */
3020         p.addDropdownItem = function(html)
3021         {
3022                 var self      = this,
3023                         container = self.containerElement().find('.text-list'),
3024                         node      = $(self.opts(OPT_HTML_SUGGESTION))
3025                         ;
3026
3027                 node.find('.text-label').html(html);
3028                 container.append(node);
3029                 return node;
3030         };
3031
3032         /**
3033          * Removes selection highlight from all suggestion elements.
3034          *
3035          * @signature TextExtAutocomplete.clearSelected()
3036          *
3037          * @author agorbatchev
3038          * @date 2011/08/02
3039          * @id TextExtAutocomplete.clearSelected
3040          */
3041         p.clearSelected = function()
3042         {
3043                 this.suggestionElements().removeClass(CSS_SELECTED);
3044         };
3045
3046         /**
3047          * Selects next suggestion relative to the current one. If there's no
3048          * currently selected suggestion, it will select the first one. Selected
3049          * suggestion will always be scrolled into view.
3050          *
3051          * @signature TextExtAutocomplete.toggleNextSuggestion()
3052          *
3053          * @author agorbatchev
3054          * @date 2011/08/02
3055          * @id TextExtAutocomplete.toggleNextSuggestion
3056          */
3057         p.toggleNextSuggestion = function()
3058         {
3059                 var self     = this,
3060                         selected = self.selectedSuggestionElement(),
3061                         next
3062                         ;
3063
3064                 if(selected.length > 0)
3065                 {
3066                         next = selected.next();
3067
3068                         if(next.length > 0)
3069                                 selected.removeClass(CSS_SELECTED);
3070                 }
3071                 else
3072                 {
3073                         next = self.suggestionElements().first();
3074                 }
3075
3076                 next.addClass(CSS_SELECTED);
3077                 self.scrollSuggestionIntoView(next);
3078         };
3079
3080         /**
3081          * Selects previous suggestion relative to the current one. Selected
3082          * suggestion will always be scrolled into view.
3083          *
3084          * @signature TextExtAutocomplete.togglePreviousSuggestion()
3085          *
3086          * @author agorbatchev
3087          * @date 2011/08/02
3088          * @id TextExtAutocomplete.togglePreviousSuggestion
3089          */
3090         p.togglePreviousSuggestion = function()
3091         {
3092                 var self     = this,
3093                         selected = self.selectedSuggestionElement(),
3094                         prev     = selected.prev()
3095                         ;
3096
3097                 if(prev.length == 0)
3098                         return;
3099
3100                 self.clearSelected();
3101                 prev.addClass(CSS_SELECTED);
3102                 self.scrollSuggestionIntoView(prev);
3103         };
3104
3105         /**
3106          * Scrolls specified HTML suggestion element into the view.
3107          *
3108          * @signature TextExtAutocomplete.scrollSuggestionIntoView(item)
3109          *
3110          * @param item {HTMLElement} jQuery HTML suggestion element which needs to
3111          * scrolled into view.
3112          *
3113          * @author agorbatchev
3114          * @date 2011/08/17
3115          * @id TextExtAutocomplete.scrollSuggestionIntoView
3116          */
3117         p.scrollSuggestionIntoView = function(item)
3118         {
3119                 var itemHeight     = item.outerHeight(),
3120                         dropdown       = this.containerElement(),
3121                         dropdownHeight = dropdown.innerHeight(),
3122                         scrollPos      = dropdown.scrollTop(),
3123                         itemTop        = (item.position() || {}).top,
3124                         scrollTo       = null,
3125                         paddingTop     = parseInt(dropdown.css('paddingTop'))
3126                         ;
3127
3128                 if(itemTop == null)
3129                         return;
3130
3131                 // if scrolling down and item is below the bottom fold
3132                 if(itemTop + itemHeight > dropdownHeight)
3133                         scrollTo = itemTop + scrollPos + itemHeight - dropdownHeight + paddingTop;
3134
3135                 // if scrolling up and item is above the top fold
3136                 if(itemTop < 0)
3137                         scrollTo = itemTop + scrollPos - paddingTop;
3138
3139                 if(scrollTo != null)
3140                         dropdown.scrollTop(scrollTo);
3141         };
3142
3143         /**
3144          * Uses the value from the text input to finish autocomplete action. Currently selected
3145          * suggestion from the dropdown will be used to complete the action. Triggers `hideDropdown`
3146          * event.
3147          *
3148          * @signature TextExtAutocomplete.selectFromDropdown()
3149          *
3150          * @author agorbatchev
3151          * @date 2011/08/17
3152          * @id TextExtAutocomplete.selectFromDropdown
3153          */
3154         p.selectFromDropdown = function()
3155         {
3156                 var self       = this,
3157                         suggestion = self.selectedSuggestionElement().data(CSS_SUGGESTION)
3158                         ;
3159
3160                 if(suggestion)
3161                 {
3162                         self.val(self.itemManager().itemToString(suggestion));
3163                         self.core().getFormData();      
3164                 }
3165
3166                 self.trigger(EVENT_HIDE_DROPDOWN);
3167         };
3168         
3169         /**
3170          * Determines if the specified HTML element is within the TextExt core wrap HTML element.
3171          *
3172          * @signature TextExtAutocomplete.withinWrapElement(element)
3173          *
3174          * @param element {HTMLElement} element to check if contained by wrap element
3175          *
3176          * @author adamayres
3177          * @version 1.3.0
3178          * @date 2012/01/15
3179          * @id TextExtAutocomplete.withinWrapElement
3180          */
3181         p.withinWrapElement = function(element) 
3182         {
3183                 return this.core().wrapElement().find(element).size() > 0;
3184         }
3185 })(jQuery);
3186 ;/**
3187  * jQuery TextExt Plugin
3188  * http://textextjs.com
3189  *
3190  * @version 1.3.0
3191  * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
3192  * @license MIT License
3193  */
3194 (function($)
3195 {
3196         /**
3197          * The Filter plugin introduces ability to limit input that the text field
3198          * will accept. If the Tags plugin is used, Filter plugin will limit which
3199          * tags it's possible to add.
3200          *
3201          * The list of allowed items can be either specified through the
3202          * options, can come from the Suggestions plugin or be loaded by the Ajax 
3203          * plugin. All these plugins have one thing in common -- they 
3204          * trigger `setSuggestions` event which the Filter plugin is expecting.
3205          * 
3206          * @author agorbatchev
3207          * @date 2011/08/18
3208          * @id TextExtFilter
3209          */
3210         function TextExtFilter() {};
3211
3212         $.fn.textext.TextExtFilter = TextExtFilter;
3213         $.fn.textext.addPlugin('filter', TextExtFilter);
3214
3215         var p = TextExtFilter.prototype,
3216
3217                 /**
3218                  * Filter plugin options are grouped under `filter` when passed to the 
3219                  * `$().textext()` function. For example:
3220                  *
3221                  *     $('textarea').textext({
3222                  *         plugins: 'filter',
3223                  *         filter: {
3224                  *             items: [ "item1", "item2" ]
3225                  *         }
3226                  *     })
3227                  *
3228                  * @author agorbatchev
3229                  * @date 2011/08/18
3230                  * @id TextExtFilter.options
3231                  */
3232                 
3233                 /**
3234                  * This is a toggle switch to enable or disable the Filter plugin. The value is checked
3235                  * each time at the top level which allows you to toggle this setting on the fly.
3236                  *
3237                  * @name filter.enabled
3238                  * @default true
3239                  * @author agorbatchev
3240                  * @date 2011/08/18
3241                  * @id TextExtFilter.options.enabled
3242                  */
3243                 OPT_ENABLED = 'filter.enabled',
3244
3245                 /**
3246                  * Arra of items that the Filter plugin will allow the Tag plugin to add to the list of
3247                  * its resut tags. Each item by default is expected to be a string which default `ItemManager`
3248                  * can work with. You can change the item type by supplying custom `ItemManager`.
3249                  *
3250                  * @name filter.items
3251                  * @default null
3252                  * @author agorbatchev
3253                  * @date 2011/08/18
3254                  * @id TextExtFilter.options.items
3255                  */
3256                 OPT_ITEMS = 'filter.items',
3257
3258                 /**
3259                  * Filter plugin dispatches and reacts to the following events.
3260                  *
3261                  * @author agorbatchev
3262                  * @date 2011/08/18
3263                  * @id TextExtFilter.events
3264                  */
3265
3266                 /**
3267                  * Filter plugin reacts to the `isTagAllowed` event triggered by the Tags plugin before
3268                  * adding a new tag to the list. If the new tag is among the `items` specified in options,
3269                  * then the new tag will be allowed.
3270                  *
3271                  * @name isTagAllowed
3272                  * @author agorbatchev
3273                  * @date 2011/08/18
3274                  * @id TextExtFilter.events.isTagAllowed
3275                  */
3276
3277                 /**
3278                  * Filter plugin reacts to the `setSuggestions` event triggered by other plugins like 
3279                  * Suggestions and Ajax.
3280                  *
3281                  * However, event if this event is handled and items are passed with it and stored, if `items`
3282                  * option was supplied, it will always take precedense.
3283                  *
3284                  * @name setSuggestions
3285                  * @author agorbatchev
3286                  * @date 2011/08/18
3287                  * @id TextExtFilter.events.setSuggestions
3288                  */
3289
3290                 DEFAULT_OPTS = {
3291                         filter : {
3292                                 enabled : true,
3293                                 items : null
3294                         }
3295                 }
3296                 ;
3297
3298         /**
3299          * Initialization method called by the core during plugin instantiation.
3300          *
3301          * @signature TextExtFilter.init(core)
3302          *
3303          * @param core {TextExt} Instance of the TextExt core class.
3304          *
3305          * @author agorbatchev
3306          * @date 2011/08/18
3307          * @id TextExtFilter.init
3308          */
3309         p.init = function(core)
3310         {
3311                 var self = this;
3312                 self.baseInit(core, DEFAULT_OPTS);
3313
3314                 self.on({
3315                         getFormData    : self.onGetFormData,
3316                         isTagAllowed   : self.onIsTagAllowed,
3317                         setSuggestions : self.onSetSuggestions
3318                 });
3319
3320                 self._suggestions = null;
3321         };
3322
3323         //--------------------------------------------------------------------------------
3324         // Core functionality
3325
3326         /**
3327          * Reacts to the [`getFormData`][1] event triggered by the core. Returns data with the
3328          * weight of 200 to be *greater than the Autocomplete plugins* data weights. 
3329          * The weights system is covered in greater detail in the [`getFormData`][1] event 
3330          * documentation.
3331          *
3332          * This method does nothing if Tags tag is also present.
3333          *
3334          * [1]: /manual/textext.html#getformdata
3335          *
3336          * @signature TextExtFilter.onGetFormData(e, data, keyCode)
3337          *
3338          * @param e {Object} jQuery event.
3339          * @param data {Object} Data object to be populated.
3340          * @param keyCode {Number} Key code that triggered the original update request.
3341          *
3342          * @author agorbatchev
3343          * @date 2011/12/28
3344          * @id TextExtFilter.onGetFormData
3345          * @version 1.1
3346          */
3347         p.onGetFormData = function(e, data, keyCode)
3348         {
3349                 var self       = this,
3350                         val        = self.val(),
3351                         inputValue = val,
3352                         formValue  = ''
3353                         ;
3354
3355                 if(!self.core().hasPlugin('tags'))
3356                 {
3357                         if(self.isValueAllowed(inputValue))
3358                                 formValue = val;
3359
3360                         data[300] = self.formDataObject(inputValue, formValue);
3361                 }
3362         };
3363
3364         /**
3365          * Checks given value if it's present in `filterItems` or was loaded for the Autocomplete
3366          * or by the Suggestions plugins. `value` is compared to each item using `ItemManager.compareItems`
3367          * method which is currently attached to the core. Returns `true` if value is known or
3368          * Filter plugin is disabled.
3369          *
3370          * @signature TextExtFilter.isValueAllowed(value)
3371          *
3372          * @param value {Object} Value to check.
3373          *
3374          * @author agorbatchev
3375          * @date 2011/12/28
3376          * @id TextExtFilter.isValueAllowed
3377          * @version 1.1
3378          */
3379         p.isValueAllowed = function(value)
3380         {
3381                 var self        = this,
3382                         list        = self.opts('filterItems') || self._suggestions || [],
3383                         itemManager = self.itemManager(),
3384                         result      = !self.opts(OPT_ENABLED), // if disabled, should just return true
3385                         i
3386                         ;
3387
3388                 for(i = 0; i < list.length && !result; i++)
3389                         if(itemManager.compareItems(value, list[i]))
3390                                 result = true;
3391
3392                 return result;
3393         };
3394
3395         /**
3396          * Handles `isTagAllowed` event dispatched by the Tags plugin. If supplied tag is not
3397          * in the `items` list, method sets `result` on the `data` argument to `false`.
3398          *
3399          * @signature TextExtFilter.onIsTagAllowed(e, data)
3400          *
3401          * @param e {Object} jQuery event.
3402          * @param data {Object} Payload in the following format : `{ tag : {Object}, result : {Boolean} }`.
3403          * @author agorbatchev
3404          * @date 2011/08/04
3405          * @id TextExtFilter.onIsTagAllowed
3406          */
3407         p.onIsTagAllowed = function(e, data)
3408         {
3409                 data.result = this.isValueAllowed(data.tag);
3410         };
3411
3412         /**
3413          * Reacts to the `setSuggestions` events and stores supplied suggestions for future use.
3414          * 
3415          * @signature TextExtFilter.onSetSuggestions(e, data)
3416          *
3417          * @param e {Object} jQuery event.
3418          * @param data {Object} Payload in the following format : `{ result : {Array} } }`.
3419          * @author agorbatchev
3420          * @date 2011/08/18
3421          * @id TextExtFilter.onSetSuggestions
3422          */
3423         p.onSetSuggestions = function(e, data)
3424         {
3425                 this._suggestions = data.result;
3426         };
3427 })(jQuery);
3428 ;/**
3429  * jQuery TextExt Plugin
3430  * http://textextjs.com
3431  *
3432  * @version 1.3.0
3433  * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
3434  * @license MIT License
3435  */
3436 (function($)
3437 {
3438         /**
3439          * Focus plugin displays a visual effect whenever user sets focus
3440          * into the text area.
3441          *
3442          * @author agorbatchev
3443          * @date 2011/08/18
3444          * @id TextExtFocus
3445          */
3446         function TextExtFocus() {};
3447
3448         $.fn.textext.TextExtFocus = TextExtFocus;
3449         $.fn.textext.addPlugin('focus', TextExtFocus);
3450
3451         var p = TextExtFocus.prototype,
3452                 /**
3453                  * Focus plugin only has one option and that is its HTML template. It could be 
3454                  * changed when passed to the `$().textext()` function. For example:
3455                  *
3456                  *     $('textarea').textext({
3457                  *         plugins: 'focus',
3458                  *         html: {
3459                  *             focus: "<span/>"
3460                  *         }
3461                  *     })
3462                  *
3463                  * @author agorbatchev
3464                  * @date 2011/08/18
3465                  * @id TextExtFocus.options
3466                  */
3467                 
3468                 /**
3469                  * HTML source that is used to generate markup required for the focus effect.
3470                  *
3471                  * @name html.focus
3472                  * @default '<div class="text-focus"/>'
3473                  * @author agorbatchev
3474                  * @date 2011/08/18
3475                  * @id TextExtFocus.options.html.focus
3476                  */
3477                 OPT_HTML_FOCUS = 'html.focus',
3478
3479                 /**
3480                  * Focus plugin dispatches or reacts to the following events.
3481                  *
3482                  * @author agorbatchev
3483                  * @date 2011/08/17
3484                  * @id TextExtFocus.events
3485                  */
3486
3487                 /**
3488                  * Focus plugin reacts to the `focus` event and shows the markup generated from
3489                  * the `html.focus` option.
3490                  *
3491                  * @name focus
3492                  * @author agorbatchev
3493                  * @date 2011/08/18
3494                  * @id TextExtFocus.events.focus
3495                  */
3496
3497                 /**
3498                  * Focus plugin reacts to the `blur` event and hides the effect.
3499                  *
3500                  * @name blur
3501                  * @author agorbatchev
3502                  * @date 2011/08/18
3503                  * @id TextExtFocus.events.blur
3504                  */
3505
3506                 DEFAULT_OPTS = {
3507                         html : {
3508                                 focus : '<div class="text-focus"/>'
3509                         }
3510                 }
3511                 ;
3512
3513         /**
3514          * Initialization method called by the core during plugin instantiation.
3515          *
3516          * @signature TextExtFocus.init(core)
3517          *
3518          * @param core {TextExt} Instance of the TextExt core class.
3519          *
3520          * @author agorbatchev
3521          * @date 2011/08/18
3522          * @id TextExtFocus.init
3523          */
3524         p.init = function(core)
3525         {
3526                 var self = this;
3527
3528                 self.baseInit(core, DEFAULT_OPTS);
3529                 self.core().wrapElement().append(self.opts(OPT_HTML_FOCUS));
3530                 self.on({
3531                         blur  : self.onBlur,
3532                         focus : self.onFocus
3533                 });
3534
3535                 self._timeoutId = 0;
3536         };
3537
3538         //--------------------------------------------------------------------------------
3539         // Event handlers
3540         
3541         /**
3542          * Reacts to the `blur` event and hides the focus effect with a slight delay which 
3543          * allows quick refocusing without effect blinking in and out.
3544          *
3545          * @signature TextExtFocus.onBlur(e)
3546          *
3547          * @param e {Object} jQuery event.
3548          *
3549          * @author agorbatchev
3550          * @date 2011/08/08
3551          * @id TextExtFocus.onBlur
3552          */
3553         p.onBlur = function(e)
3554         {
3555                 var self = this;
3556
3557                 clearTimeout(self._timeoutId);
3558
3559                 self._timeoutId = setTimeout(function()
3560                 {
3561                         self.getFocus().hide();
3562                 },
3563                 100);
3564         };
3565
3566         /**
3567          * Reacts to the `focus` event and shows the focus effect.
3568          *
3569          * @signature TextExtFocus.onFocus
3570          *
3571          * @param e {Object} jQuery event.
3572          * @author agorbatchev
3573          * @date 2011/08/08
3574          * @id TextExtFocus.onFocus
3575          */
3576         p.onFocus = function(e)
3577         {
3578                 var self = this;
3579
3580                 clearTimeout(self._timeoutId);
3581                 
3582                 self.getFocus().show();
3583         };
3584         
3585         //--------------------------------------------------------------------------------
3586         // Core functionality
3587
3588         /**
3589          * Returns focus effect HTML element.
3590          *
3591          * @signature TextExtFocus.getFocus()
3592          *
3593          * @author agorbatchev
3594          * @date 2011/08/08
3595          * @id TextExtFocus.getFocus
3596          */
3597         p.getFocus = function()
3598         {
3599                 return this.core().wrapElement().find('.text-focus');
3600         };
3601 })(jQuery);
3602 ;/**
3603  * jQuery TextExt Plugin
3604  * http://textextjs.com
3605  *
3606  * @version 1.3.0
3607  * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
3608  * @license MIT License
3609  */
3610 (function($)
3611 {
3612         /**
3613          * Prompt plugin displays a visual user propmpt in the text input area. If user focuses
3614          * on the input, the propt is hidden and only shown again when user focuses on another
3615          * element and text input doesn't have a value.
3616          *
3617          * @author agorbatchev
3618          * @date 2011/08/18
3619          * @id TextExtPrompt
3620          */
3621         function TextExtPrompt() {};
3622
3623         $.fn.textext.TextExtPrompt = TextExtPrompt;
3624         $.fn.textext.addPlugin('prompt', TextExtPrompt);
3625
3626         var p = TextExtPrompt.prototype,
3627
3628                 CSS_HIDE_PROMPT = 'text-hide-prompt',
3629
3630                 /**
3631                  * Prompt plugin has options to change the prompt label and its HTML template. The options
3632                  * could be changed when passed to the `$().textext()` function. For example:
3633                  *
3634                  *     $('textarea').textext({
3635                  *         plugins: 'prompt',
3636                  *         prompt: 'Your email address'
3637                  *     })
3638                  *
3639                  * @author agorbatchev
3640                  * @date 2011/08/18
3641                  * @id TextExtPrompt.options
3642                  */
3643
3644                 /**
3645                  * Prompt message that is displayed to the user whenever there's no value in the input.
3646                  *
3647                  * @name prompt
3648                  * @default 'Awaiting input...'
3649                  * @author agorbatchev
3650                  * @date 2011/08/18
3651                  * @id TextExtPrompt.options.prompt
3652                  */
3653                 OPT_PROMPT = 'prompt',
3654
3655                 /**
3656                  * HTML source that is used to generate markup required for the prompt effect.
3657                  *
3658                  * @name html.prompt
3659                  * @default '<div class="text-prompt"/>'
3660                  * @author agorbatchev
3661                  * @date 2011/08/18
3662                  * @id TextExtPrompt.options.html.prompt
3663                  */
3664                 OPT_HTML_PROMPT = 'html.prompt',
3665
3666                 /**
3667                  * Prompt plugin dispatches or reacts to the following events.
3668                  * @id TextExtPrompt.events
3669                  */
3670
3671                 /**
3672                  * Prompt plugin reacts to the `focus` event and hides the markup generated from
3673                  * the `html.prompt` option.
3674                  *
3675                  * @name focus
3676                  * @author agorbatchev
3677                  * @date 2011/08/18
3678                  * @id TextExtPrompt.events.focus
3679                  */
3680
3681                 /**
3682                  * Prompt plugin reacts to the `blur` event and shows the prompt back if user
3683                  * hasn't entered any value.
3684                  *
3685                  * @name blur
3686                  * @author agorbatchev
3687                  * @date 2011/08/18
3688                  * @id TextExtPrompt.events.blur
3689                  */
3690         
3691                 DEFAULT_OPTS = {
3692                         prompt : 'Awaiting input...',
3693
3694                         html : {
3695                                 prompt : '<div class="text-prompt"/>'
3696                         }
3697                 }
3698                 ;
3699
3700         /**
3701          * Initialization method called by the core during plugin instantiation.
3702          *
3703          * @signature TextExtPrompt.init(core)
3704          *
3705          * @param core {TextExt} Instance of the TextExt core class.
3706          *
3707          * @author agorbatchev
3708          * @date 2011/08/18
3709          * @id TextExtPrompt.init
3710          */
3711         p.init = function(core)
3712         {
3713                 var self           = this,
3714                         placeholderKey = 'placeholder',
3715                         container,
3716                         prompt
3717                         ;
3718
3719                 self.baseInit(core, DEFAULT_OPTS);
3720                 
3721                 container = $(self.opts(OPT_HTML_PROMPT));
3722                 $(self).data('container', container);
3723
3724                 self.core().wrapElement().append(container);
3725                 self.setPrompt(self.opts(OPT_PROMPT));
3726                 
3727                 prompt = core.input().attr(placeholderKey);
3728
3729                 if(!prompt)
3730                         prompt = self.opts(OPT_PROMPT);
3731
3732                 // clear placeholder attribute if set
3733                 core.input().attr(placeholderKey, '');
3734
3735                 if(prompt)
3736                         self.setPrompt(prompt);
3737
3738                 if($.trim(self.val()).length > 0)
3739                         self.hidePrompt();
3740
3741                 self.on({
3742                         blur           : self.onBlur,
3743                         focus          : self.onFocus,
3744                         postInvalidate : self.onPostInvalidate,
3745                         postInit       : self.onPostInit
3746                 });
3747         };
3748
3749         //--------------------------------------------------------------------------------
3750         // Event handlers
3751         
3752         /**
3753          * Reacts to the `postInit` and configures the plugin for initial display.
3754          *
3755          * @signature TextExtPrompt.onPostInit(e)
3756          *
3757          * @param e {Object} jQuery event.
3758          *
3759          * @author agorbatchev
3760          * @date 2011/08/24
3761          * @id TextExtPrompt.onPostInit
3762          */
3763         p.onPostInit = function(e)
3764         {
3765                 this.invalidateBounds();
3766         };
3767
3768         /**
3769          * Reacts to the `postInvalidate` and insures that prompt display remains correct.
3770          *
3771          * @signature TextExtPrompt.onPostInvalidate(e)
3772          *
3773          * @param e {Object} jQuery event.
3774          *
3775          * @author agorbatchev
3776          * @date 2011/08/24
3777          * @id TextExtPrompt.onPostInvalidate
3778          */
3779         p.onPostInvalidate = function(e)
3780         {
3781                 this.invalidateBounds();
3782         };
3783
3784         /**
3785          * Repositions the prompt to make sure it's always at the same place as in the text input carret.
3786          *
3787          * @signature TextExtPrompt.invalidateBounds()
3788          *
3789          * @author agorbatchev
3790          * @date 2011/08/24
3791          * @id TextExtPrompt.invalidateBounds
3792          */
3793         p.invalidateBounds = function()
3794         {
3795                 var self  = this,
3796                         input = self.input()
3797                         ;
3798
3799                 self.containerElement().css({
3800                         paddingLeft : input.css('paddingLeft'),
3801                         paddingTop  : input.css('paddingTop')
3802                 });
3803         };
3804
3805         /**
3806          * Reacts to the `blur` event and shows the prompt effect with a slight delay which 
3807          * allows quick refocusing without effect blinking in and out.
3808          *
3809          * The prompt is restored if the text box has no value.
3810          *
3811          * @signature TextExtPrompt.onBlur(e)
3812          *
3813          * @param e {Object} jQuery event.
3814          *
3815          * @author agorbatchev
3816          * @date 2011/08/08
3817          * @id TextExtPrompt.onBlur
3818          */
3819         p.onBlur = function(e)
3820         {
3821                 var self = this;
3822
3823                 self.startTimer('prompt', 0.1, function()
3824                 {
3825                         self.showPrompt();
3826                 });
3827         };
3828
3829         /**
3830          * Shows prompt HTML element.
3831          *
3832          * @signature TextExtPrompt.showPrompt()
3833          *
3834          * @author agorbatchev
3835          * @date 2011/08/22
3836          * @id TextExtPrompt.showPrompt
3837          */
3838         p.showPrompt = function()
3839         {
3840                 var self     = this,
3841                         input    = self.input()
3842                         ;
3843                 
3844                 if($.trim(self.val()).length === 0 && !input.is(':focus'))
3845                         self.containerElement().removeClass(CSS_HIDE_PROMPT);
3846         };
3847
3848         /**
3849          * Hides prompt HTML element.
3850          *
3851          * @signature TextExtPrompt.hidePrompt()
3852          *
3853          * @author agorbatchev
3854          * @date 2011/08/22
3855          * @id TextExtPrompt.hidePrompt
3856          */
3857         p.hidePrompt = function()
3858         {
3859                 this.stopTimer('prompt');
3860                 this.containerElement().addClass(CSS_HIDE_PROMPT);
3861         };
3862
3863         /**
3864          * Reacts to the `focus` event and hides the prompt effect.
3865          *
3866          * @signature TextExtPrompt.onFocus
3867          *
3868          * @param e {Object} jQuery event.
3869          * @author agorbatchev
3870          * @date 2011/08/08
3871          * @id TextExtPrompt.onFocus
3872          */
3873         p.onFocus = function(e)
3874         {
3875                 this.hidePrompt();
3876         };
3877         
3878         //--------------------------------------------------------------------------------
3879         // Core functionality
3880
3881         /**
3882          * Sets the prompt display to the specified string.
3883          *
3884          * @signature TextExtPrompt.setPrompt(str)
3885          *
3886          * @oaram str {String} String that will be displayed in the prompt.
3887          *
3888          * @author agorbatchev
3889          * @date 2011/08/18
3890          * @id TextExtPrompt.setPrompt
3891          */
3892         p.setPrompt = function(str)
3893         {
3894                 this.containerElement().text(str);
3895         };
3896
3897         /**
3898          * Returns prompt effect HTML element.
3899          *
3900          * @signature TextExtPrompt.containerElement()
3901          *
3902          * @author agorbatchev
3903          * @date 2011/08/08
3904          * @id TextExtPrompt.containerElement
3905          */
3906         p.containerElement = function()
3907         {
3908                 return $(this).data('container');
3909         };
3910 })(jQuery);
3911 ;/**
3912  * jQuery TextExt Plugin
3913  * http://textextjs.com
3914  *
3915  * @version 1.3.0
3916  * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
3917  * @license MIT License
3918  */
3919 (function($)
3920 {
3921         /**
3922          * Suggestions plugin allows to easily specify the list of suggestion items that the
3923          * Autocomplete plugin would present to the user.
3924          *
3925          * @author agorbatchev
3926          * @date 2011/08/18
3927          * @id TextExtSuggestions
3928          */
3929         function TextExtSuggestions() {};
3930
3931         $.fn.textext.TextExtSuggestions = TextExtSuggestions;
3932         $.fn.textext.addPlugin('suggestions', TextExtSuggestions);
3933
3934         var p = TextExtSuggestions.prototype,
3935                 /**
3936                  * Suggestions plugin only has one option and that is to set suggestion items. It could be 
3937                  * changed when passed to the `$().textext()` function. For example:
3938                  *
3939                  *     $('textarea').textext({
3940                  *         plugins: 'suggestions',
3941                  *         suggestions: [ "item1", "item2" ]
3942                  *     })
3943                  *
3944                  * @author agorbatchev
3945                  * @date 2011/08/18
3946                  * @id TextExtSuggestions.options
3947                  */
3948
3949                 /**
3950                  * List of items that Autocomplete plugin would display in the dropdown.
3951                  *
3952                  * @name suggestions
3953                  * @default null
3954                  * @author agorbatchev
3955                  * @date 2011/08/18
3956                  * @id TextExtSuggestions.options.suggestions
3957                  */
3958                 OPT_SUGGESTIONS = 'suggestions',
3959
3960                 /**
3961                  * Suggestions plugin dispatches or reacts to the following events.
3962                  *
3963                  * @author agorbatchev
3964                  * @date 2011/08/17
3965                  * @id TextExtSuggestions.events
3966                  */
3967
3968                 /**
3969                  * Suggestions plugin reacts to the `getSuggestions` event and returns `suggestions` items
3970                  * from the options.
3971                  *
3972                  * @name getSuggestions
3973                  * @author agorbatchev
3974                  * @date 2011/08/19
3975                  * @id TextExtSuggestions.events.getSuggestions
3976                  */
3977
3978                 /**
3979                  * Suggestions plugin triggers the `setSuggestions` event to pass its own list of `Suggestions`
3980                  * to the Autocomplete plugin.
3981                  *
3982                  * @name setSuggestions
3983                  * @author agorbatchev
3984                  * @date 2011/08/19
3985                  * @id TextExtSuggestions.events.setSuggestions
3986                  */
3987
3988                 /**
3989                  * Suggestions plugin reacts to the `postInit` event to pass its list of `suggestions` to the
3990                  * Autocomplete right away.
3991                  *
3992                  * @name postInit
3993                  * @author agorbatchev
3994                  * @date 2011/08/19
3995                  * @id TextExtSuggestions.events.postInit
3996                  */
3997
3998                 DEFAULT_OPTS = {
3999                         suggestions : null
4000                 }
4001                 ;
4002
4003         /**
4004          * Initialization method called by the core during plugin instantiation.
4005          *
4006          * @signature TextExtSuggestions.init(core)
4007          *
4008          * @param core {TextExt} Instance of the TextExt core class.
4009          *
4010          * @author agorbatchev
4011          * @date 2011/08/18
4012          * @id TextExtSuggestions.init
4013          */
4014         p.init = function(core)
4015         {
4016                 var self = this;
4017
4018                 self.baseInit(core, DEFAULT_OPTS);
4019
4020                 self.on({
4021                         getSuggestions : self.onGetSuggestions,
4022                         postInit       : self.onPostInit
4023                 });
4024         };
4025
4026         /**
4027          * Triggers `setSuggestions` and passes supplied suggestions to the Autocomplete plugin.
4028          *
4029          * @signature TextExtSuggestions.setSuggestions(suggestions, showHideDropdown)
4030          *
4031          * @param suggestions {Array} List of suggestions. With the default `ItemManager` it should
4032          * be a list of strings.
4033          * @param showHideDropdown {Boolean} If it's undesirable to show the dropdown right after
4034          * suggestions are set, `false` should be passed for this argument.
4035          *
4036          * @author agorbatchev
4037          * @date 2011/08/19
4038          * @id TextExtSuggestions.setSuggestions
4039          */
4040         p.setSuggestions = function(suggestions, showHideDropdown)
4041         {
4042                 this.trigger('setSuggestions', { result : suggestions, showHideDropdown : showHideDropdown != false });
4043         };
4044
4045         /**
4046          * Reacts to the `postInit` event and triggers `setSuggestions` event to set suggestions list 
4047          * right after initialization.
4048          *
4049          * @signature TextExtSuggestions.onPostInit(e)
4050          *
4051          * @param e {Object} jQuery event.
4052          *
4053          * @author agorbatchev
4054          * @date 2011/08/19
4055          * @id TextExtSuggestions.onPostInit
4056          */
4057         p.onPostInit = function(e)
4058         {
4059                 var self = this;
4060                 self.setSuggestions(self.opts(OPT_SUGGESTIONS), false);
4061         };
4062
4063         /**
4064          * Reacts to the `getSuggestions` event and triggers `setSuggestions` event with the list
4065          * of `suggestions` specified in the options.
4066          *
4067          * @signature TextExtSuggestions.onGetSuggestions(e, data)
4068          *
4069          * @param e {Object} jQuery event.
4070          * @param data {Object} Payload from the `getSuggestions` event with the user query, eg `{ query: {String} }`.
4071          *
4072          * @author agorbatchev
4073          * @date 2011/08/19
4074          * @id TextExtSuggestions.onGetSuggestions
4075          */
4076         p.onGetSuggestions = function(e, data)
4077         {
4078                 var self        = this,
4079                         suggestions = self.opts(OPT_SUGGESTIONS)
4080                         ;
4081
4082                 suggestions.sort();
4083                 self.setSuggestions(self.itemManager().filter(suggestions, data.query));
4084         };
4085 })(jQuery);
4086 ;/**
4087  * jQuery TextExt Plugin
4088  * http://textextjs.com
4089  *
4090  * @version 1.3.0
4091  * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
4092  * @license MIT License
4093  */
4094 (function($)
4095 {
4096         /**
4097          * Tags plugin brings in the traditional tag functionality where user can assemble and
4098          * edit list of tags. Tags plugin works especially well together with Autocomplete, Filter,
4099          * Suggestions and Ajax plugins to provide full spectrum of features. It can also work on
4100          * its own and just do one thing -- tags.
4101          *
4102          * @author agorbatchev
4103          * @date 2011/08/19
4104          * @id TextExtTags
4105          */
4106         function TextExtTags() {};
4107
4108         $.fn.textext.TextExtTags = TextExtTags;
4109         $.fn.textext.addPlugin('tags', TextExtTags);
4110
4111         var p = TextExtTags.prototype,
4112
4113                 CSS_DOT             = '.',
4114                 CSS_TAGS_ON_TOP     = 'text-tags-on-top',
4115                 CSS_DOT_TAGS_ON_TOP = CSS_DOT + CSS_TAGS_ON_TOP,
4116                 CSS_TAG             = 'text-tag',
4117                 CSS_DOT_TAG         = CSS_DOT + CSS_TAG,
4118                 CSS_TAGS            = 'text-tags',
4119                 CSS_DOT_TAGS        = CSS_DOT + CSS_TAGS,
4120                 CSS_LABEL           = 'text-label',
4121                 CSS_DOT_LABEL       = CSS_DOT + CSS_LABEL,
4122                 CSS_REMOVE          = 'text-remove',
4123                 CSS_DOT_REMOVE      = CSS_DOT + CSS_REMOVE,
4124
4125                 /**
4126                  * Tags plugin options are grouped under `tags` when passed to the
4127                  * `$().textext()` function. For example:
4128                  *
4129                  *     $('textarea').textext({
4130                  *         plugins: 'tags',
4131                  *         tags: {
4132                  *             items: [ "tag1", "tag2" ]
4133                  *         }
4134                  *     })
4135                  *
4136                  * @author agorbatchev
4137                  * @date 2011/08/19
4138                  * @id TextExtTags.options
4139                  */
4140
4141                 /**
4142                  * This is a toggle switch to enable or disable the Tags plugin. The value is checked
4143                  * each time at the top level which allows you to toggle this setting on the fly.
4144                  *
4145                  * @name tags.enabled
4146                  * @default true
4147                  * @author agorbatchev
4148                  * @date 2011/08/19
4149                  * @id TextExtTags.options.tags.enabled
4150                  */
4151                 OPT_ENABLED = 'tags.enabled',
4152
4153                 /**
4154                  * Allows to specify tags which will be added to the input by default upon initialization.
4155                  * Each item in the array must be of the type that current `ItemManager` can understand.
4156                  * Default type is `String`.
4157                  *
4158                  * @name tags.items
4159                  * @default null
4160                  * @author agorbatchev
4161                  * @date 2011/08/19
4162                  * @id TextExtTags.options.tags.items
4163                  */
4164                 OPT_ITEMS = 'tags.items',
4165
4166                 /**
4167                  * HTML source that is used to generate a single tag.
4168                  *
4169                  * @name html.tag
4170                  * @default '<div class="text-tags"/>'
4171                  * @author agorbatchev
4172                  * @date 2011/08/19
4173                  * @id TextExtTags.options.html.tag
4174                  */
4175                 OPT_HTML_TAG  = 'html.tag',
4176
4177                 /**
4178                  * HTML source that is used to generate container for the tags.
4179                  *
4180                  * @name html.tags
4181                  * @default '<div class="text-tag"><div class="text-button"><span class="text-label"/><a class="text-remove"/></div></div>'
4182                  * @author agorbatchev
4183                  * @date 2011/08/19
4184                  * @id TextExtTags.options.html.tags
4185                  */
4186                 OPT_HTML_TAGS = 'html.tags',
4187
4188                 /**
4189                  * Tags plugin dispatches or reacts to the following events.
4190                  *
4191                  * @author agorbatchev
4192                  * @date 2011/08/17
4193                  * @id TextExtTags.events
4194                  */
4195
4196                 /**
4197                  * Tags plugin triggers the `isTagAllowed` event before adding each tag to the tag list. Other plugins have
4198                  * an opportunity to interrupt this by setting `result` of the second argument to `false`. For example:
4199                  *
4200                  *     $('textarea').textext({...}).bind('isTagAllowed', function(e, data)
4201                  *     {
4202                  *         if(data.tag === 'foo')
4203                  *             data.result = false;
4204                  *     })
4205                  *
4206                  * The second argument `data` has the following format: `{ tag : {Object}, result : {Boolean} }`. `tag`
4207                  * property is in the format that the current `ItemManager` can understand.
4208                  *
4209                  * @name isTagAllowed
4210                  * @author agorbatchev
4211                  * @date 2011/08/19
4212                  * @id TextExtTags.events.isTagAllowed
4213                  */
4214                 EVENT_IS_TAG_ALLOWED = 'isTagAllowed',
4215
4216                 /**
4217                  * Tags plugin triggers the `tagClick` event when user clicks on one of the tags. This allows to process
4218                  * the click and potentially change the value of the tag (for example in case of user feedback).
4219                  *
4220                  *     $('textarea').textext({...}).bind('tagClick', function(e, tag, value, callback)
4221                  *     {
4222                  *         var newValue = window.prompt('New value', value);
4223
4224                  *         if(newValue)
4225                  *             callback(newValue, true);
4226                  *     })
4227                  *
4228                  *  Callback argument has the following signature:
4229                  *
4230                  *     function(newValue, refocus)
4231                  *     {
4232                  *         ...
4233                  *     }
4234                  *
4235                  * Please check out [example](/manual/examples/tags-changing.html).
4236                  *
4237                  * @name tagClick
4238                  * @version 1.3.0
4239                  * @author s.stok
4240                  * @date 2011/01/23
4241                  * @id TextExtTags.events.tagClick
4242                  */
4243                 EVENT_TAG_CLICK = 'tagClick',
4244
4245                 DEFAULT_OPTS = {
4246                         tags : {
4247                                 enabled : true,
4248                                 items   : null
4249                         },
4250
4251                         html : {
4252                                 tags : '<div class="text-tags"/>',
4253                                 tag  : '<div class="text-tag"><div class="text-button"><span class="text-label"/><a class="text-remove"/></div></div>'
4254                         }
4255                 }
4256                 ;
4257
4258         /**
4259          * Initialization method called by the core during plugin instantiation.
4260          *
4261          * @signature TextExtTags.init(core)
4262          *
4263          * @param core {TextExt} Instance of the TextExt core class.
4264          *
4265          * @author agorbatchev
4266          * @date 2011/08/19
4267          * @id TextExtTags.init
4268          */
4269         p.init = function(core)
4270         {
4271                 this.baseInit(core, DEFAULT_OPTS);
4272
4273                 var self  = this,
4274                         input = self.input(),
4275                         container
4276                         ;
4277
4278                 if(self.opts(OPT_ENABLED))
4279                 {
4280                         container = $(self.opts(OPT_HTML_TAGS));
4281                         input.after(container);
4282
4283                         $(self).data('container', container);
4284
4285                         self.on({
4286                                 enterKeyPress    : self.onEnterKeyPress,
4287                                 backspaceKeyDown : self.onBackspaceKeyDown,
4288                                 preInvalidate    : self.onPreInvalidate,
4289                                 postInit         : self.onPostInit,
4290                                 getFormData      : self.onGetFormData
4291                         });
4292
4293                         self.on(container, {
4294                                 click     : self.onClick,
4295                                 mousemove : self.onContainerMouseMove
4296                         });
4297
4298                         self.on(input, {
4299                                 mousemove : self.onInputMouseMove
4300                         });
4301                 }
4302
4303                 self._originalPadding = {
4304                         left : parseInt(input.css('paddingLeft') || 0),
4305                         top  : parseInt(input.css('paddingTop') || 0)
4306                 };
4307
4308                 self._paddingBox = {
4309                         left : 0,
4310                         top  : 0
4311                 };
4312
4313                 self.updateFormCache();
4314         };
4315
4316         /**
4317          * Returns HTML element in which all tag HTML elements are residing.
4318          *
4319          * @signature TextExtTags.containerElement()
4320          *
4321          * @author agorbatchev
4322          * @date 2011/08/15
4323          * @id TextExtTags.containerElement
4324          */
4325         p.containerElement = function()
4326         {
4327                 return $(this).data('container');
4328         };
4329
4330         //--------------------------------------------------------------------------------
4331         // Event handlers
4332
4333         /**
4334          * Reacts to the `postInit` event triggered by the core and sets default tags
4335          * if any were specified.
4336          *
4337          * @signature TextExtTags.onPostInit(e)
4338          *
4339          * @param e {Object} jQuery event.
4340          *
4341          * @author agorbatchev
4342          * @date 2011/08/09
4343          * @id TextExtTags.onPostInit
4344          */
4345         p.onPostInit = function(e)
4346         {
4347                 var self = this;
4348                 self.addTags(self.opts(OPT_ITEMS));
4349         };
4350
4351         /**
4352          * Reacts to the [`getFormData`][1] event triggered by the core. Returns data with the
4353          * weight of 200 to be *greater than the Autocomplete plugin* data weight. The weights
4354          * system is covered in greater detail in the [`getFormData`][1] event documentation.
4355          *
4356          * [1]: /manual/textext.html#getformdata
4357          *
4358          * @signature TextExtTags.onGetFormData(e, data, keyCode)
4359          *
4360          * @param e {Object} jQuery event.
4361          * @param data {Object} Data object to be populated.
4362          * @param keyCode {Number} Key code that triggered the original update request.
4363          *
4364          * @author agorbatchev
4365          * @date 2011/08/22
4366          * @id TextExtTags.onGetFormData
4367          */
4368         p.onGetFormData = function(e, data, keyCode)
4369         {
4370                 var self       = this,
4371                         inputValue = keyCode === 13 ? '' : self.val(),
4372                         formValue  = self._formData
4373                         ;
4374
4375                 data[200] = self.formDataObject(inputValue, formValue);
4376         };
4377
4378         /**
4379          * Returns initialization priority of the Tags plugin which is expected to be
4380          * *less than the Autocomplete plugin* because of the dependencies. The value is
4381          * 100.
4382          *
4383          * @signature TextExtTags.initPriority()
4384          *
4385          * @author agorbatchev
4386          * @date 2011/08/22
4387          * @id TextExtTags.initPriority
4388          */
4389         p.initPriority = function()
4390         {
4391                 return 100;
4392         };
4393
4394         /**
4395          * Reacts to user moving mouse over the text area when cursor is over the text
4396          * and not over the tags. Whenever mouse cursor is over the area covered by
4397          * tags, the tags container is flipped to be on top of the text area which
4398          * makes all tags functional with the mouse.
4399          *
4400          * @signature TextExtTags.onInputMouseMove(e)
4401          *
4402          * @param e {Object} jQuery event.
4403          *
4404          * @author agorbatchev
4405          * @date 2011/08/08
4406          * @id TextExtTags.onInputMouseMove
4407          */
4408         p.onInputMouseMove = function(e)
4409         {
4410                 this.toggleZIndex(e);
4411         };
4412
4413         /**
4414          * Reacts to user moving mouse over the tags. Whenever the cursor moves out
4415          * of the tags and back into where the text input is happening visually,
4416          * the tags container is sent back under the text area which allows user
4417          * to interact with the text using mouse cursor as expected.
4418          *
4419          * @signature TextExtTags.onContainerMouseMove(e)
4420          *
4421          * @param e {Object} jQuery event.
4422          *
4423          * @author agorbatchev
4424          * @date 2011/08/08
4425          * @id TextExtTags.onContainerMouseMove
4426          */
4427         p.onContainerMouseMove = function(e)
4428         {
4429                 this.toggleZIndex(e);
4430         };
4431
4432         /**
4433          * Reacts to the `backspaceKeyDown` event. When backspace key is pressed in an empty text field,
4434          * deletes last tag from the list.
4435          *
4436          * @signature TextExtTags.onBackspaceKeyDown(e)
4437          *
4438          * @param e {Object} jQuery event.
4439          *
4440          * @author agorbatchev
4441          * @date 2011/08/02
4442          * @id TextExtTags.onBackspaceKeyDown
4443          */
4444         p.onBackspaceKeyDown = function(e)
4445         {
4446                 var self    = this,
4447                         lastTag = self.tagElements().last()
4448                         ;
4449
4450                 if(self.val().length == 0)
4451                         self.removeTag(lastTag);
4452         };
4453
4454         /**
4455          * Reacts to the `preInvalidate` event and updates the input box to look like the tags are
4456          * positioned inside it.
4457          *
4458          * @signature TextExtTags.onPreInvalidate(e)
4459          *
4460          * @param e {Object} jQuery event.
4461          *
4462          * @author agorbatchev
4463          * @date 2011/08/19
4464          * @id TextExtTags.onPreInvalidate
4465          */
4466         p.onPreInvalidate = function(e)
4467         {
4468                 var self    = this,
4469                         lastTag = self.tagElements().last(),
4470                         pos     = lastTag.position()
4471                         ;
4472
4473                 if(lastTag.length > 0)
4474                         pos.left += lastTag.innerWidth();
4475                 else
4476                         pos = self._originalPadding;
4477
4478                 self._paddingBox = pos;
4479
4480                 self.input().css({
4481                         paddingLeft : pos.left,
4482                         paddingTop  : pos.top
4483                 });
4484         };
4485
4486         /**
4487          * Reacts to the mouse `click` event.
4488          *
4489          * @signature TextExtTags.onClick(e)
4490          *
4491          * @param e {Object} jQuery event.
4492          *
4493          * @author agorbatchev
4494          * @date 2011/08/19
4495          * @id TextExtTags.onClick
4496          */
4497         p.onClick = function(e)
4498         {
4499                 var self   = this,
4500                         core   = self.core(),
4501                         source = $(e.target),
4502                         focus  = 0,
4503                         tag
4504                         ;
4505
4506                 if(source.is(CSS_DOT_TAGS))
4507                 {
4508                         focus = 1;
4509                 }
4510                 else if(source.is(CSS_DOT_REMOVE))
4511                 {
4512                         self.removeTag(source.parents(CSS_DOT_TAG + ':first'));
4513                         focus = 1;
4514                 }
4515                 else if(source.is(CSS_DOT_LABEL))
4516                 {
4517                         tag = source.parents(CSS_DOT_TAG + ':first');
4518                         self.trigger(EVENT_TAG_CLICK, tag, tag.data(CSS_TAG), tagClickCallback);
4519                 }
4520
4521                 function tagClickCallback(newValue, refocus)
4522                 {
4523                         tag.data(CSS_TAG, newValue);
4524                         tag.find(CSS_DOT_LABEL).text(self.itemManager().itemToString(newValue));
4525
4526                         self.updateFormCache();
4527                         core.getFormData();
4528                         core.invalidateBounds();
4529
4530                         if(refocus)
4531                                 core.focusInput();
4532                 }
4533
4534                 if(focus)
4535                         core.focusInput();
4536         };
4537
4538         /**
4539          * Reacts to the `enterKeyPress` event and adds whatever is currently in the text input
4540          * as a new tag. Triggers `isTagAllowed` to check if the tag could be added first.
4541          *
4542          * @signature TextExtTags.onEnterKeyPress(e)
4543          *
4544          * @param e {Object} jQuery event.
4545          *
4546          * @author agorbatchev
4547          * @date 2011/08/19
4548          * @id TextExtTags.onEnterKeyPress
4549          */
4550         p.onEnterKeyPress = function(e)
4551         {
4552                 var self = this,
4553                         val  = self.val(),
4554                         tag  = self.itemManager().stringToItem(val)
4555                         ;
4556
4557                 if(self.isTagAllowed(tag))
4558                 {
4559                         self.addTags([ tag ]);
4560                         // refocus the textarea just in case it lost the focus
4561                         self.core().focusInput();
4562                 }
4563         };
4564
4565         //--------------------------------------------------------------------------------
4566         // Core functionality
4567
4568         /**
4569          * Creates a cache object with all the tags currently added which will be returned
4570          * in the `onGetFormData` handler.
4571          *
4572          * @signature TextExtTags.updateFormCache()
4573          *
4574          * @author agorbatchev
4575          * @date 2011/08/09
4576          * @id TextExtTags.updateFormCache
4577          */
4578         p.updateFormCache = function()
4579         {
4580                 var self   = this,
4581                         result = []
4582                         ;
4583
4584                 self.tagElements().each(function()
4585                 {
4586                         result.push($(this).data(CSS_TAG));
4587                 });
4588
4589                 // cache the results to be used in the onGetFormData
4590                 self._formData = result;
4591         };
4592
4593         /**
4594          * Toggles tag container to be on top of the text area or under based on where
4595          * the mouse cursor is located. When cursor is above the text input and out of
4596          * any of the tags, the tags container is sent under the text area. If cursor
4597          * is over any of the tags, the tag container is brought to be over the text
4598          * area.
4599          *
4600          * @signature TextExtTags.toggleZIndex(e)
4601          *
4602          * @param e {Object} jQuery event.
4603          *
4604          * @author agorbatchev
4605          * @date 2011/08/08
4606          * @id TextExtTags.toggleZIndex
4607          */
4608         p.toggleZIndex = function(e)
4609         {
4610                 var self            = this,
4611                         offset          = self.input().offset(),
4612                         mouseX          = e.clientX - offset.left,
4613                         mouseY          = e.clientY - offset.top,
4614                         box             = self._paddingBox,
4615                         container       = self.containerElement(),
4616                         isOnTop         = container.is(CSS_DOT_TAGS_ON_TOP),
4617                         isMouseOverText = mouseX > box.left && mouseY > box.top
4618                         ;
4619
4620                 if(!isOnTop && !isMouseOverText || isOnTop && isMouseOverText)
4621                         container[(!isOnTop ? 'add' : 'remove') + 'Class'](CSS_TAGS_ON_TOP);
4622         };
4623
4624         /**
4625          * Returns all tag HTML elements.
4626          *
4627          * @signature TextExtTags.tagElements()
4628          *
4629          * @author agorbatchev
4630          * @date 2011/08/19
4631          * @id TextExtTags.tagElements
4632          */
4633         p.tagElements = function()
4634         {
4635                 return this.containerElement().find(CSS_DOT_TAG);
4636         };
4637
4638         /**
4639          * Wrapper around the `isTagAllowed` event which triggers it and returns `true`
4640          * if `result` property of the second argument remains `true`.
4641          *
4642          * @signature TextExtTags.isTagAllowed(tag)
4643          *
4644          * @param tag {Object} Tag object that the current `ItemManager` can understand.
4645          * Default is `String`.
4646          *
4647          * @author agorbatchev
4648          * @date 2011/08/19
4649          * @id TextExtTags.isTagAllowed
4650          */
4651         p.isTagAllowed = function(tag)
4652         {
4653                 var opts = { tag : tag, result : true };
4654                 this.trigger(EVENT_IS_TAG_ALLOWED, opts);
4655                 return opts.result === true;
4656         };
4657
4658         /**
4659          * Adds specified tags to the tag list. Triggers `isTagAllowed` event for each tag
4660          * to insure that it could be added. Calls `TextExt.getFormData()` to refresh the data.
4661          *
4662          * @signature TextExtTags.addTags(tags)
4663          *
4664          * @param tags {Array} List of tags that current `ItemManager` can understand. Default
4665          * is `String`.
4666          *
4667          * @author agorbatchev
4668          * @date 2011/08/19
4669          * @id TextExtTags.addTags
4670          */
4671         p.addTags = function(tags)
4672         {
4673                 if(!tags || tags.length == 0)
4674                         return;
4675
4676                 var self      = this,
4677                         core      = self.core(),
4678                         container = self.containerElement(),
4679                         i, tag
4680                         ;
4681
4682                 for(i = 0; i < tags.length; i++)
4683                 {
4684                         tag = tags[i];
4685
4686                         if(tag && self.isTagAllowed(tag))
4687                                 container.append(self.renderTag(tag));
4688                 }
4689
4690                 self.updateFormCache();
4691                 core.getFormData();
4692                 core.invalidateBounds();
4693         };
4694
4695         /**
4696          * Returns HTML element for the specified tag.
4697          *
4698          * @signature TextExtTags.getTagElement(tag)
4699          *
4700          * @param tag {Object} Tag object in the format that current `ItemManager` can understand.
4701          * Default is `String`.
4702
4703          * @author agorbatchev
4704          * @date 2011/08/19
4705          * @id TextExtTags.getTagElement
4706          */
4707         p.getTagElement = function(tag)
4708         {
4709                 var self = this,
4710                         list = self.tagElements(),
4711                         i, item
4712                         ;
4713
4714                 for(i = 0; i < list.length, item = $(list[i]); i++)
4715                         if(self.itemManager().compareItems(item.data(CSS_TAG), tag))
4716                                 return item;
4717         };
4718
4719         /**
4720          * Removes specified tag from the list. Calls `TextExt.getFormData()` to refresh the data.
4721          *
4722          * @signature TextExtTags.removeTag(tag)
4723          *
4724          * @param tag {Object} Tag object in the format that current `ItemManager` can understand.
4725          * Default is `String`.
4726          *
4727          * @author agorbatchev
4728          * @date 2011/08/19
4729          * @id TextExtTags.removeTag
4730          */
4731         p.removeTag = function(tag)
4732         {
4733                 var self = this,
4734                         core = self.core(),
4735                         element
4736                         ;
4737
4738                 if(tag instanceof $)
4739                 {
4740                         element = tag;
4741                         tag = tag.data(CSS_TAG);
4742                 }
4743                 else
4744                 {
4745                         element = self.getTagElement(tag);
4746                 }
4747
4748                 element.remove();
4749                 self.updateFormCache();
4750                 core.getFormData();
4751                 core.invalidateBounds();
4752         };
4753
4754         /**
4755          * Creates and returns new HTML element from the source code specified in the `html.tag` option.
4756          *
4757          * @signature TextExtTags.renderTag(tag)
4758          *
4759          * @param tag {Object} Tag object in the format that current `ItemManager` can understand.
4760          * Default is `String`.
4761          *
4762          * @author agorbatchev
4763          * @date 2011/08/19
4764          * @id TextExtTags.renderTag
4765          */
4766         p.renderTag = function(tag)
4767         {
4768                 var self = this,
4769                         node = $(self.opts(OPT_HTML_TAG))
4770                         ;
4771
4772                 node.find('.text-label').text(self.itemManager().itemToString(tag));
4773                 node.data(CSS_TAG, tag);
4774                 return node;
4775         };
4776 })(jQuery);