[MERGE] Merge trunk
authorFabien Meghazi <fme@openerp.com>
Mon, 14 May 2012 08:46:23 +0000 (10:46 +0200)
committerFabien Meghazi <fme@openerp.com>
Mon, 14 May 2012 08:46:23 +0000 (10:46 +0200)
bzr revid: fme@openerp.com-20120514084623-kxu8o8bu03qzfl3f

addons/web/__openerp__.py
addons/web/controllers/main.py
addons/web/static/lib/jquery.textext/jquery.textext.js [new file with mode: 0644]
addons/web/static/src/css/base.css
addons/web/static/src/css/base.sass
addons/web/static/src/js/view_form.js
addons/web/static/src/xml/base.xml
addons/web_kanban/static/src/css/kanban.css
addons/web_kanban/static/src/js/kanban.js
addons/web_kanban/static/src/xml/web_kanban.xml

index 1ca0553..26ee561 100644 (file)
@@ -29,6 +29,7 @@
         "static/lib/jquery.deferred-queue/jquery.deferred-queue.js",
         "static/lib/jquery.scrollTo/jquery.scrollTo-min.js",
         "static/lib/jquery.tipsy/jquery.tipsy.js",
+        "static/lib/jquery.textext/jquery.textext.js",
         "static/lib/json/json2.js",
         "static/lib/qweb/qweb2.js",
         "static/lib/underscore/underscore.js",
index 0dd8f74..12ef0b5 100644 (file)
@@ -1818,9 +1818,15 @@ class Reports(View):
             report = zlib.decompress(report)
         report_mimetype = self.TYPES_MAPPING.get(
             report_struct['format'], 'octet-stream')
+        if 'name' not in action:
+            reports = req.session.model('ir.actions.report.xml')
+            res_id = reports.search([('report_name', '=',action['report_name']),],
+                                    0, False, False, context)
+            action['name'] = reports.read(res_id, ['name'], context)[0]['name']
+
         return req.make_response(report,
              headers=[
-                 ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], report_struct['format'])),
+                 ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['name'], report_struct['format'])),
                  ('Content-Type', report_mimetype),
                  ('Content-Length', len(report))],
              cookies={'fileToken': int(token)})
diff --git a/addons/web/static/lib/jquery.textext/jquery.textext.js b/addons/web/static/lib/jquery.textext/jquery.textext.js
new file mode 100644 (file)
index 0000000..294ceea
--- /dev/null
@@ -0,0 +1,4930 @@
+;/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.0
+ * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($, undefined)
+{
+       /**
+        * TextExt is the main core class which by itself doesn't provide any functionality
+        * that is user facing, however it has the underlying mechanics to bring all the
+        * plugins together under one roof and make them work with each other or on their
+        * own.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExt
+        */
+       function TextExt() {};
+
+       /**
+        * ItemManager is used to seamlessly convert between string that come from the user input to whatever 
+        * the format the item data is being passed around in. It's used by all plugins that in one way or 
+        * another operate with items, such as Tags, Filter, Autocomplete and Suggestions. Default implementation 
+        * works with `String` type. 
+        *
+        * Each instance of `TextExt` creates a new instance of default implementation of `ItemManager`
+        * unless `itemManager` option was set to another implementation.
+        *
+        * To satisfy requirements of managing items of type other than a `String`, different implementation
+        * if `ItemManager` should be supplied.
+        *
+        * If you wish to bring your own implementation, you need to create a new class and implement all the 
+        * methods that `ItemManager` has. After, you need to supply your pass via the `itemManager` option during
+        * initialization like so:
+        *
+        *     $('#input').textext({
+        *         itemManager : CustomItemManager
+        *     })
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id ItemManager
+        */
+       function ItemManager() {};
+
+       /**
+        * TextExtPlugin is a base class for all plugins. It provides common methods which are reused
+        * by majority of plugins.
+        *
+        * All plugins must register themselves by calling the `$.fn.textext.addPlugin(name, constructor)`
+        * function while providing plugin name and constructor. The plugin name is the same name that user
+        * will identify the plugin in the `plugins` option when initializing TextExt component and constructor
+        * function will create a new instance of the plugin. *Without registering, the core won't
+        * be able to see the plugin.*
+        *
+        * <span class="new label version">new in 1.2.0</span> You can get instance of each plugin from the core 
+        * via associated function with the same name as the plugin. For example:
+        *
+        *     $('#input').textext()[0].tags()
+        *     $('#input').textext()[0].autocomplete()
+        *     ...
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtPlugin
+        */
+       function TextExtPlugin() {};
+
+       var stringify = (JSON || {}).stringify,
+               slice     = Array.prototype.slice,
+
+               UNDEFINED = 'undefined',
+
+               /**
+                * TextExt provides a way to pass in the options to configure the core as well as
+                * each plugin that is being currently used. The jQuery exposed plugin `$().textext()` 
+                * function takes a hash object with key/value set of options. For example:
+                *
+                *     $('textarea').textext({
+                *         enabled: true
+                *     })
+                *
+                * There are multiple ways of passing in the options:
+                *
+                * 1. Options could be nested multiple levels deep and accessed using all lowercased, dot
+                * separated style, eg `foo.bar.world`. The manual is using this style for clarity and
+                * consistency. For example:
+                *
+                *        {
+                *            item: {
+                *                manager: ...
+                *            },
+                *
+                *            html: {
+                *                wrap: ...
+                *            },
+                *
+                *            autocomplete: {
+                *                enabled: ...,
+                *                dropdown: {
+                *                   position: ...
+                *                }
+                *            }
+                *        }
+                *
+                * 2. Options could be specified using camel cased names in a flat key/value fashion like so:
+                *
+                *        {
+                *            itemManager: ...,
+                *            htmlWrap: ...,
+                *            autocompleteEnabled: ...,
+                *            autocompleteDropdownPosition: ...
+                *        }
+                *
+                * 3. Finally, options could be specified in mixed style. It's important to understand that
+                * for each dot separated name, its alternative in camel case is also checked for, eg for 
+                * `foo.bar.world` it's alternatives could be `fooBarWorld`, `foo.barWorld` or `fooBar.world`, 
+                * which translates to `{ foo: { bar: { world: ... } } }`, `{ fooBarWorld: ... }`, 
+                * `{ foo : { barWorld : ... } }` or `{ fooBar: { world: ... } }` respectively. For example:
+                *
+                *        {
+                *            itemManager : ...,
+                *            htmlWrap: ...,
+                *            autocomplete: {
+                *                enabled: ...,
+                *                dropdownPosition: ...
+                *            }
+                *        }
+                *
+                * Mixed case is used through out the code, wherever it seems appropriate. However in the code, all option
+                * names are specified in the dot notation because it works both ways where as camel case is not
+                * being converted to its alternative dot notation.
+                *
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExt.options
+                */
+
+               /**
+                * Default instance of `ItemManager` which takes `String` type as default for tags.
+                *
+                * @name item.manager
+                * @default ItemManager
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExt.options.item.manager
+                */
+               OPT_ITEM_MANAGER = 'item.manager',
+               
+               /**
+                * List of plugins that should be used with the current instance of TextExt. The list could be
+                * specified as array of strings or as comma or space separated string.
+                *
+                * @name plugins
+                * @default []
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExt.options.plugins
+                */
+               OPT_PLUGINS = 'plugins',
+               
+               /**
+                * TextExt allows for overriding of virtually any method that the core or any of its plugins
+                * use. This could be accomplished through the use of the `ext` option.
+                *
+                * It's possible to specifically target the core or any plugin, as well as overwrite all the
+                * desired methods everywhere.
+                *
+                * 1. Targeting the core:
+                *
+                *        ext: {
+                *            core: {
+                *                trigger: function()
+                *                {
+                *                    console.log('TextExt.trigger', arguments);
+                *                    $.fn.textext.TextExt.prototype.trigger.apply(this, arguments);
+                *                }
+                *            }
+                *        }
+                *
+                * 2. Targeting individual plugins:
+                *
+                *        ext: {
+                *            tags: {
+                *                addTags: function(tags)
+                *                {
+                *                    console.log('TextExtTags.addTags', tags);
+                *                    $.fn.textext.TextExtTags.prototype.addTags.apply(this, arguments);
+                *                }
+                *            }
+                *        }
+                *
+                * 3. Targeting `ItemManager` instance:
+                *
+                *        ext: {
+                *            itemManager: {
+                *                stringToItem: function(str)
+                *                {
+                *                    console.log('ItemManager.stringToItem', str);
+                *                    return $.fn.textext.ItemManager.prototype.stringToItem.apply(this, arguments);
+                *                }
+                *            }
+                *        }
+                *
+                * 4. And finally, in edge cases you can extend everything at once:
+                *
+                *        ext: {
+                *            '*': {
+                *                fooBar: function() {}
+                *            }
+                *        }
+                *
+                * @name ext
+                * @default {}
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExt.options.ext
+                */
+               OPT_EXT = 'ext',
+               
+               /**
+                * HTML source that is used to generate elements necessary for the core and all other
+                * plugins to function.
+                *
+                * @name html.wrap
+                * @default '<div class="text-core"><div class="text-wrap"/></div>'
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExt.options.html.wrap
+                */
+               OPT_HTML_WRAP = 'html.wrap',
+
+               /**
+                * HTML source that is used to generate hidden input value of which will be submitted 
+                * with the HTML form.
+                *
+                * @name html.hidden
+                * @default '<input type="hidden" />'
+                * @author agorbatchev
+                * @date 2011/08/20
+                * @id TextExt.options.html.hidden
+                */
+               OPT_HTML_HIDDEN = 'html.hidden',
+               
+               /**
+                * Hash table of key codes and key names for which special events will be created
+                * by the core. For each entry a `[name]KeyDown`, `[name]KeyUp` and `[name]KeyPress` events 
+                * will be triggered along side with `anyKeyUp` and `anyKeyDown` events for every 
+                * key stroke.
+                *
+                * Here's a list of default keys:
+                *
+                *     {
+                *         8   : 'backspace',
+                *         9   : 'tab',
+                *         13  : 'enter!',
+                *         27  : 'escape!',
+                *         37  : 'left',
+                *         38  : 'up!',
+                *         39  : 'right',
+                *         40  : 'down!',
+                *         46  : 'delete',
+                *         108 : 'numpadEnter'
+                *     }
+                *
+                * Please note the `!` at the end of some keys. This tells the core that by default
+                * this keypress will be trapped and not passed on to the text input.
+                *
+                * @name keys
+                * @default { ... }
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExt.options.keys
+                */
+               OPT_KEYS = 'keys',
+
+               /**
+                * The core triggers or reacts to the following events.
+                *
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExt.events
+                */
+
+               /**
+                * Core triggers `preInvalidate` event before the dimensions of padding on the text input
+                * are set.
+                *
+                * @name preInvalidate
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExt.events.preInvalidate
+                */
+               EVENT_PRE_INVALIDATE = 'preInvalidate',
+
+               /**
+                * Core triggers `postInvalidate` event after the dimensions of padding on the text input
+                * are set.
+                *
+                * @name postInvalidate
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExt.events.postInvalidate
+                */
+               EVENT_POST_INVALIDATE = 'postInvalidate',
+               
+               /**
+                * Core triggers `getFormData` on every key press to collect data that will be populated
+                * into the hidden input that will be submitted with the HTML form and data that will
+                * be displayed in the input field that user is currently interacting with.
+                *
+                * All plugins that wish to affect how the data is presented or sent must react to 
+                * `getFormData` and populate the data in the following format:
+                *
+                *     {
+                *         input : {String},
+                *         form  : {Object}
+                *     }
+                *
+                * The data key must be a numeric weight which will be used to determine which data
+                * ends up being used. Data with the highest numerical weight gets the priority. This
+                * allows plugins to set the final data regardless of their initialization order, which
+                * otherwise would be impossible.
+                *
+                * For example, the Tags and Autocomplete plugins have to work side by side and Tags
+                * plugin must get priority on setting the data. Therefore the Tags plugin sets data
+                * with the weight 200 where as the Autocomplete plugin sets data with the weight 100.
+                *
+                * Here's an example of a typical `getFormData` handler:
+                * 
+                *     TextExtPlugin.prototype.onGetFormData = function(e, data, keyCode)
+                *     {
+                *         data[100] = self.formDataObject('input value', 'form value');
+                *     };
+                *
+                * Core also reacts to the `getFormData` and updates hidden input with data which will be
+                * submitted with the HTML form.
+                *
+                * @name getFormData
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExt.events.getFormData
+                */
+               EVENT_GET_FORM_DATA = 'getFormData',
+
+               /**
+                * Core triggers and reacts to the `setFormData` event to update the actual value in the
+                * hidden input that will be submitted with the HTML form. Second argument can be value
+                * of any type and by default it will be JSON serialized with `TextExt.serializeData()`
+                * function.
+                *
+                * @name setFormData
+                * @author agorbatchev
+                * @date 2011/08/22
+                * @id TextExt.events.setFormData
+                */
+               EVENT_SET_FORM_DATA = 'setFormData',
+
+               /**
+                * Core triggers and reacts to the `setInputData` event to update the actual value in the
+                * text input that user is interacting with. Second argument must be of a `String` type
+                * the value of which will be set into the text input.
+                *
+                * @name setInputData
+                * @author agorbatchev
+                * @date 2011/08/22
+                * @id TextExt.events.setInputData
+                */
+               EVENT_SET_INPUT_DATA = 'setInputData',
+               
+               /**
+                * Core triggers `postInit` event to let plugins run code after all plugins have been 
+                * created and initialized. This is a good place to set some kind of global values before 
+                * somebody gets to use them. This is not the right place to expect all plugins to finish
+                * their initialization.
+                *
+                * @name postInit
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExt.events.postInit
+                */
+               EVENT_POST_INIT = 'postInit',
+
+               /**
+                * Core triggers `ready` event after all global configuration and prepearation has been
+                * done and the TextExt component is ready for use. Event handlers should expect all 
+                * values to be set and the plugins to be in the final state.
+                *
+                * @name ready
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExt.events.ready
+                */
+               EVENT_READY = 'ready',
+
+               /**
+                * Core triggers `anyKeyUp` event for every key up event triggered within the component.
+                *
+                * @name anyKeyUp
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExt.events.anyKeyUp
+                */
+
+               /**
+                * Core triggers `anyKeyDown` event for every key down event triggered within the component.
+                *
+                * @name anyKeyDown
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExt.events.anyKeyDown
+                */
+
+               /**
+                * Core triggers `[name]KeyUp` event for every key specifid in the `keys` option that is 
+                * triggered within the component.
+                *
+                * @name [name]KeyUp
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExt.events.[name]KeyUp
+                */
+
+               /**
+                * Core triggers `[name]KeyDown` event for every key specified in the `keys` option that is 
+                * triggered within the component.
+                *
+                * @name [name]KeyDown
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExt.events.[name]KeyDown
+                */
+
+               /**
+                * Core triggers `[name]KeyPress` event for every key specified in the `keys` option that is 
+                * triggered within the component.
+                *
+                * @name [name]KeyPress
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExt.events.[name]KeyPress
+                */
+
+               DEFAULT_OPTS = {
+                       itemManager : ItemManager,
+
+                       plugins : [],
+                       ext : {},
+
+                       html : {
+                               wrap   : '<div class="text-core"><div class="text-wrap"/></div>',
+                               hidden : '<input type="hidden" />'
+                       },
+
+                       keys : {
+                               8   : 'backspace',
+                               9   : 'tab',
+                               13  : 'enter!',
+                               27  : 'escape!',
+                               37  : 'left',
+                               38  : 'up!',
+                               39  : 'right',
+                               40  : 'down!',
+                               46  : 'delete',
+                               108 : 'numpadEnter'
+                       }
+               }
+               ;
+
+       // Freak out if there's no JSON.stringify function found
+       if(!stringify)
+               throw new Error('JSON.stringify() not found');
+
+       /**
+        * Returns object property by name where name is dot-separated and object is multiple levels deep.
+        * @param target Object Source object.
+        * @param name String Dot separated property name, ie `foo.bar.world`
+        * @id core.getProperty
+        */
+       function getProperty(source, name)
+       {
+               if(typeof(name) === 'string')
+                       name = name.split('.');
+
+               var fullCamelCaseName = name.join('.').replace(/\.(\w)/g, function(match, letter) { return letter.toUpperCase() }),
+                       nestedName        = name.shift(),
+                       result
+                       ;
+
+               if(typeof(result = source[fullCamelCaseName]) != UNDEFINED)
+                       result = result;
+
+               else if(typeof(result = source[nestedName]) != UNDEFINED && name.length > 0)
+                       result = getProperty(result, name);
+
+               // name.length here should be zero
+               return result;
+       };
+
+       /**
+        * Hooks up specified events in the scope of the current object.
+        * @author agorbatchev
+        * @date 2011/08/09
+        */
+       function hookupEvents()
+       {
+               var args   = slice.apply(arguments),
+                       self   = this,
+                       target = args.length === 1 ? self : args.shift(),
+                       event
+                       ;
+
+               args = args[0] || {};
+
+               function bind(event, handler)
+               {
+                       target.bind(event, function()
+                       {
+                               // apply handler to our PLUGIN object, not the target
+                               return handler.apply(self, arguments);
+                       });
+               }
+
+               for(event in args)
+                       bind(event, args[event]);
+       };
+
+       function formDataObject(input, form)
+       {
+               return { 'input' : input, 'form' : form };
+       };
+
+       //--------------------------------------------------------------------------------
+       // ItemManager core component
+       
+       p = ItemManager.prototype;
+
+       /**
+        * Initialization method called by the core during instantiation.
+        *
+        * @signature ItemManager.init(core)
+        *
+        * @param core {TextExt} Instance of the TextExt core class.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id ItemManager.init
+        */
+       p.init = function(core)
+       {
+       };
+
+       /**
+        * Filters out items from the list that don't match the query and returns remaining items. Default 
+        * implementation checks if the item starts with the query.
+        *
+        * @signature ItemManager.filter(list, query)
+        *
+        * @param list {Array} List of items. Default implementation works with strings.
+        * @param query {String} Query string.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id ItemManager.filter
+        */
+       p.filter = function(list, query)
+       {
+               var result = [],
+                       i, item
+                       ;
+
+               for(i = 0; i < list.length; i++)
+               {
+                       item = list[i];
+                       if(this.itemContains(item, query))
+                               result.push(item);
+               }
+
+               return result;
+       };
+
+       /**
+        * Returns `true` if specified item contains another string, `false` otherwise. In the default implementation 
+        * `String.indexOf()` is used to check if item string begins with the needle string.
+        *
+        * @signature ItemManager.itemContains(item, needle)
+        *
+        * @param item {Object} Item to check. Default implementation works with strings.
+        * @param needle {String} Search string to be found within the item.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id ItemManager.itemContains
+        */
+       p.itemContains = function(item, needle)
+       {
+               return this.itemToString(item).toLowerCase().indexOf(needle.toLowerCase()) == 0;
+       };
+
+       /**
+        * Converts specified string to item. Because default implemenation works with string, input string
+        * is simply returned back. To use custom objects, different implementation of this method could
+        * return something like `{ name : {String} }`.
+        *
+        * @signature ItemManager.stringToItem(str)
+        *
+        * @param str {String} Input string.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id ItemManager.stringToItem
+        */
+       p.stringToItem = function(str)
+       {
+               return str;
+       };
+
+       /**
+        * Converts specified item to string. Because default implemenation works with string, input string
+        * is simply returned back. To use custom objects, different implementation of this method could
+        * for example return `name` field of `{ name : {String} }`.
+        *
+        * @signature ItemManager.itemToString(item)
+        *
+        * @param item {Object} Input item to be converted to string.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id ItemManager.itemToString
+        */
+       p.itemToString = function(item)
+       {
+               return item;
+       };
+
+       /**
+        * Returns `true` if both items are equal, `false` otherwise. Because default implemenation works with 
+        * string, input items are compared as strings. To use custom objects, different implementation of this 
+        * method could for example compare `name` fields of `{ name : {String} }` type object.
+        *
+        * @signature ItemManager.compareItems(item1, item2)
+        *
+        * @param item1 {Object} First item.
+        * @param item2 {Object} Second item.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id ItemManager.compareItems
+        */
+       p.compareItems = function(item1, item2)
+       {
+               return item1 == item2;
+       };
+
+       //--------------------------------------------------------------------------------
+       // TextExt core component
+
+       p = TextExt.prototype;
+               
+       /**
+        * Initializes current component instance with work with the supplied text input and options.
+        *
+        * @signature TextExt.init(input, opts)
+        *
+        * @param input {HTMLElement} Text input.
+        * @param opts {Object} Options.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExt.init
+        */
+       p.init = function(input, opts)
+       {
+               var self = this,
+                       hiddenInput,
+                       itemManager,
+                       container
+                       ;
+
+               self._defaults    = $.extend({}, DEFAULT_OPTS);
+               self._opts        = opts || {};
+               self._plugins     = {};
+               self._itemManager = itemManager = new (self.opts(OPT_ITEM_MANAGER))();
+               input             = $(input);
+               container         = $(self.opts(OPT_HTML_WRAP));
+               hiddenInput       = $(self.opts(OPT_HTML_HIDDEN));
+
+               input
+                       .wrap(container)
+                       .keydown(function(e) { return self.onKeyDown(e) })
+                       .keyup(function(e) { return self.onKeyUp(e) })
+                       .data('textext', self)
+                       ;
+
+               // keep references to html elements using jQuery.data() to avoid circular references
+               $(self).data({
+                       'hiddenInput'   : hiddenInput,
+                       'wrapElement' : input.parents('.text-wrap').first(),
+                       'input'         : input
+               });
+
+               // set the name of the hidden input to the text input's name
+               hiddenInput.attr('name', input.attr('name'));
+               // remove name attribute from the text input
+               input.attr('name', null);
+               // add hidden input to the DOM
+               hiddenInput.insertAfter(input);
+
+               $.extend(true, itemManager, self.opts(OPT_EXT + '.item.manager'));
+               $.extend(true, self, self.opts(OPT_EXT + '.*'), self.opts(OPT_EXT + '.core'));
+               
+               self.originalWidth = input.outerWidth();
+
+               self.invalidateBounds();
+
+               itemManager.init(self);
+
+               self.initPatches();
+               self.initPlugins(self.opts(OPT_PLUGINS), $.fn.textext.plugins);
+
+               self.on({
+                       setFormData  : self.onSetFormData,
+                       getFormData  : self.onGetFormData,
+                       setInputData : self.onSetInputData,
+                       anyKeyUp     : self.onAnyKeyUp
+               });
+
+               self.trigger(EVENT_POST_INIT);
+               self.trigger(EVENT_READY);
+
+               self.getFormData(0);
+       };
+
+       /**
+        * Initialized all installed patches against current instance. The patches are initialized based on their
+        * initialization priority which is returned by each patch's `initPriority()` method. Priority
+        * is a `Number` where patches with higher value gets their `init()` method called before patches
+        * with lower priority value.
+        *
+        * This facilitates initializing of patches in certain order to insure proper dependencies
+        * regardless of which order they are loaded.
+        *
+        * By default all patches have the same priority - zero, which means they will be initialized
+        * in rorder they are loaded, that is unless `initPriority()` is overriden.
+        *
+        * @signature TextExt.initPatches()
+        *
+        * @author agorbatchev
+        * @date 2011/10/11
+        * @id TextExt.initPatches
+        */
+       p.initPatches = function()
+       {
+               var list   = [],
+                       source = $.fn.textext.patches,
+                       name
+                       ;
+
+               for(name in source)
+                       list.push(name);
+
+               this.initPlugins(list, source);
+       };
+
+       /**
+        * Creates and initializes all specified plugins. The plugins are initialized based on their
+        * initialization priority which is returned by each plugin's `initPriority()` method. Priority
+        * is a `Number` where plugins with higher value gets their `init()` method called before plugins
+        * with lower priority value.
+        *
+        * This facilitates initializing of plugins in certain order to insure proper dependencies
+        * regardless of which order user enters them in the `plugins` option field.
+        *
+        * By default all plugins have the same priority - zero, which means they will be initialized
+        * in the same order as entered by the user.
+        *
+        * @signature TextExt.initPlugins(plugins)
+        *
+        * @param plugins {Array} List of plugin names to initialize.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExt.initPlugins
+        */
+       p.initPlugins = function(plugins, source)
+       {
+               var self = this,
+                       ext, name, plugin, initList = [], i
+                       ;
+
+               if(typeof(plugins) == 'string')
+                       plugins = plugins.split(/\s*,\s*|\s+/g);
+
+               for(i = 0; i < plugins.length; i++)
+               {
+                       name   = plugins[i];
+                       plugin = source[name];
+
+                       if(plugin)
+                       {
+                               self._plugins[name] = plugin = new plugin();
+                               self[name] = (function(plugin) { 
+                                 return function(){ return plugin; } 
+                               })(plugin);
+                               initList.push(plugin);
+                               $.extend(true, plugin, self.opts(OPT_EXT + '.*'), self.opts(OPT_EXT + '.' + name));
+                       }
+               }
+
+               // sort plugins based on their priority values
+               initList.sort(function(p1, p2)
+               {
+                       p1 = p1.initPriority();
+                       p2 = p2.initPriority();
+
+                       return p1 === p2
+                               ? 0
+                               : p1 < p2 ? 1 : -1
+                               ;
+               });
+
+               for(i = 0; i < initList.length; i++)
+                       initList[i].init(self);
+       };
+
+       /**
+        * Returns true if specified plugin is was instantiated for the current instance of core.
+        *
+        * @signature TextExt.hasPlugin(name)
+        *
+        * @param name {String} Name of the plugin to check.
+        *
+        * @author agorbatchev
+        * @date 2011/12/28
+        * @id TextExt.hasPlugin
+        * @version 1.1
+        */
+       p.hasPlugin = function(name)
+       {
+               return !!this._plugins[name];
+       };
+
+       /**
+        * Allows to add multiple event handlers which will be execued in the scope of the current object.
+        * 
+        * @signature TextExt.on([target], handlers)
+        *
+        * @param target {Object} **Optional**. Target object which has traditional `bind(event, handler)` method.
+        *                        Handler function will still be executed in the current object's scope.
+        * @param handlers {Object} Key/value pairs of event names and handlers, eg `{ event: handler }`.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExt.on
+        */
+       p.on = hookupEvents;
+
+       /**
+        * Binds an event handler to the input box that user interacts with.
+        *
+        * @signature TextExt.bind(event, handler)
+        *
+        * @param event {String} Event name.
+        * @param handler {Function} Event handler.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExt.bind
+        */
+       p.bind = function(event, handler)
+       {
+               this.input().bind(event, handler);
+       };
+
+       /**
+        * Triggers an event on the input box that user interacts with. All core events are originated here.
+        * 
+        * @signature TextExt.trigger(event, ...args)
+        *
+        * @param event {String} Name of the event to trigger.
+        * @param ...args All remaining arguments will be passed to the event handler.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExt.trigger
+        */
+       p.trigger = function()
+       {
+               var args = arguments;
+               this.input().trigger(args[0], slice.call(args, 1));
+       };
+
+       /**
+        * Returns instance of `itemManager` that is used by the component.
+        *
+        * @signature TextExt.itemManager()
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExt.itemManager
+        */
+       p.itemManager = function()
+       {
+               return this._itemManager;
+       };
+
+       /**
+        * Returns jQuery input element with which user is interacting with.
+        *
+        * @signature TextExt.input()
+        *
+        * @author agorbatchev
+        * @date 2011/08/10
+        * @id TextExt.input
+        */
+       p.input = function()
+       {
+               return $(this).data('input');
+       };
+
+       /**
+        * Returns option value for the specified option by name. If the value isn't found in the user
+        * provided options, it will try looking for default value.
+        *
+        * @signature TextExt.opts(name)
+        *
+        * @param name {String} Option name as described in the options.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExt.opts
+        */
+       p.opts = function(name)
+       {
+               var result = getProperty(this._opts, name);
+               return typeof(result) == 'undefined' ? getProperty(this._defaults, name) : result;
+       };
+
+       /**
+        * Returns HTML element that was created from the `html.wrap` option. This is the top level HTML
+        * container for the text input with which user is interacting with.
+        *
+        * @signature TextExt.wrapElement()
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExt.wrapElement
+        */
+       p.wrapElement = function()
+       {
+               return $(this).data('wrapElement');
+       };
+
+       /**
+        * Updates container to match dimensions of the text input. Triggers `preInvalidate` and `postInvalidate`
+        * events.
+        *
+        * @signature TextExt.invalidateBounds()
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExt.invalidateBounds
+        */
+       p.invalidateBounds = function()
+       {
+               var self      = this,
+                       input     = self.input(),
+                       wrap      = self.wrapElement(),
+                       container = wrap.parent(),
+                       width     = self.originalWidth,
+                       height
+                       ;
+
+               self.trigger(EVENT_PRE_INVALIDATE);
+
+               height = input.outerHeight();
+
+               input.width(width);
+               wrap.width(width).height(height);
+               container.height(height);
+
+               self.trigger(EVENT_POST_INVALIDATE);
+       };
+
+       /**
+        * Focuses user input on the text box.
+        *
+        * @signature TextExt.focusInput()
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExt.focusInput
+        */
+       p.focusInput = function()
+       {
+               this.input()[0].focus();
+       };
+
+       /**
+        * Serializes data for to be set into the hidden input field and which will be submitted 
+        * with the HTML form.
+        *
+        * By default simple JSON serialization is used. It's expected that `JSON.stringify`
+        * method would be available either through built in class in most modern browsers
+        * or through JSON2 library.
+        *
+        * @signature TextExt.serializeData(data)
+        *
+        * @param data {Object} Data to serialize.
+        *
+        * @author agorbatchev
+        * @date 2011/08/09
+        * @id TextExt.serializeData
+        */
+       p.serializeData = stringify;
+
+       /**
+        * Returns the hidden input HTML element which will be submitted with the HTML form.
+        *
+        * @signature TextExt.hiddenInput()
+        *
+        * @author agorbatchev
+        * @date 2011/08/09
+        * @id TextExt.hiddenInput
+        */
+       p.hiddenInput = function(value)
+       {
+               return $(this).data('hiddenInput');
+       };
+
+       /**
+        * Abstracted functionality to trigger an event and get the data with maximum weight set by all
+        * the event handlers. This functionality is used for the `getFormData` event.
+        *
+        * @signature TextExt.getWeightedEventResponse(event, args)
+        *
+        * @param event {String} Event name.
+        * @param args {Object} Argument to be passed with the event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/22
+        * @id TextExt.getWeightedEventResponse
+        */
+       p.getWeightedEventResponse = function(event, args)
+       {
+               var self      = this,
+                       data      = {},
+                       maxWeight = 0
+                       ;
+
+               self.trigger(event, data, args);
+
+               for(var weight in data)
+                       maxWeight = Math.max(maxWeight, weight);
+
+               return data[maxWeight];
+       };
+
+       /**
+        * Triggers the `getFormData` event to get all the plugins to return their data.
+        *
+        * After the data is returned, triggers `setFormData` and `setInputData` to update appopriate values.
+        *
+        * @signature TextExt.getFormData(keyCode)
+        *
+        * @param keyCode {Number} Key code number which has triggered this update. It's impotant to pass
+        * this value to the plugins because they might return different values based on the key that was 
+        * pressed. For example, the Tags plugin returns an empty string for the `input` value if the enter
+        * key was pressed, otherwise it returns whatever is currently in the text input.
+        *
+        * @author agorbatchev
+        * @date 2011/08/22
+        * @id TextExt.getFormData
+        */
+       p.getFormData = function(keyCode)
+       {
+               var self = this,
+                       data = self.getWeightedEventResponse(EVENT_GET_FORM_DATA, keyCode || 0)
+                       ;
+
+               self.trigger(EVENT_SET_FORM_DATA  , data['form']);
+               self.trigger(EVENT_SET_INPUT_DATA , data['input']);
+       };
+
+       //--------------------------------------------------------------------------------
+       // Event handlers
+
+       /**
+        * Reacts to the `anyKeyUp` event and triggers the `getFormData` to change data that will be submitted
+        * with the form. Default behaviour is that everything that is typed in will be JSON serialized, so
+        * the end result will be a JSON string.
+        *
+        * @signature TextExt.onAnyKeyUp(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExt.onAnyKeyUp
+        */
+       p.onAnyKeyUp = function(e, keyCode)
+       {
+               this.getFormData(keyCode);
+       };
+
+       /**
+        * Reacts to the `setInputData` event and populates the input text field that user is currently
+        * interacting with.
+        *
+        * @signature TextExt.onSetInputData(e, data)
+        *
+        * @param e {Event} jQuery event.
+        * @param data {String} Value to be set.
+        *
+        * @author agorbatchev
+        * @date 2011/08/22
+        * @id TextExt.onSetInputData
+        */
+       p.onSetInputData = function(e, data)
+       {
+               this.input().val(data);
+       };
+
+       /**
+        * Reacts to the `setFormData` event and populates the hidden input with will be submitted with
+        * the HTML form. The value will be serialized with `serializeData()` method.
+        *
+        * @signature TextExt.onSetFormData(e, data)
+        *
+        * @param e {Event} jQuery event.
+        * @param data {Object} Data that will be set.
+        * 
+        * @author agorbatchev
+        * @date 2011/08/22
+        * @id TextExt.onSetFormData
+        */
+       p.onSetFormData = function(e, data)
+       {
+               var self = this;
+               self.hiddenInput().val(self.serializeData(data));
+       };
+
+       /**
+        * Reacts to `getFormData` event triggered by the core. At the bare minimum the core will tell
+        * itself to use the current value in the text input as the data to be submitted with the HTML
+        * form.
+        *
+        * @signature TextExt.onGetFormData(e, data)
+        *
+        * @param e {Event} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/09
+        * @id TextExt.onGetFormData
+        */
+       p.onGetFormData = function(e, data)
+       {
+               var val = this.input().val();
+               data[0] = formDataObject(val, val);
+       };
+
+       //--------------------------------------------------------------------------------
+       // User mouse/keyboard input
+
+       /**
+        * Triggers `[name]KeyUp` and `[name]KeyPress` for every keystroke as described in the events.
+        *
+        * @signature TextExt.onKeyUp(e)
+        *
+        * @param e {Object} jQuery event.
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExt.onKeyUp
+        */
+
+       /**
+        * Triggers `[name]KeyDown` for every keystroke as described in the events.
+        *
+        * @signature TextExt.onKeyDown(e)
+        *
+        * @param e {Object} jQuery event.
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExt.onKeyDown
+        */
+       
+       $(['Down', 'Up']).each(function()
+       {
+               var type = this.toString();
+
+               p['onKey' + type] = function(e)
+               {
+                       var self          = this,
+                               keyName       = self.opts(OPT_KEYS)[e.keyCode],
+                               defaultResult = true
+                               ;
+
+                       if(keyName)
+                       {
+                               defaultResult = keyName.substr(-1) != '!';
+                               keyName       = keyName.replace('!', '');
+
+                               self.trigger(keyName + 'Key' + type);
+
+                               // manual *KeyPress event fimplementation for the function keys like Enter, Backspace, etc.
+                               if(type == 'Up' && self._lastKeyDown == e.keyCode)
+                               {
+                                       self._lastKeyDown = null;
+                                       self.trigger(keyName + 'KeyPress');
+                               }
+
+                               if(type == 'Down')
+                                       self._lastKeyDown = e.keyCode;
+                       }
+
+                       self.trigger('anyKey' + type, e.keyCode);
+
+                       return defaultResult;
+               };
+       });
+
+       //--------------------------------------------------------------------------------
+       // Plugin Base
+       
+       p = TextExtPlugin.prototype;
+
+       /**
+        * Allows to add multiple event handlers which will be execued in the scope of the current object.
+        * 
+        * @signature TextExt.on([target], handlers)
+        *
+        * @param target {Object} **Optional**. Target object which has traditional `bind(event, handler)` method.
+        *                        Handler function will still be executed in the current object's scope.
+        * @param handlers {Object} Key/value pairs of event names and handlers, eg `{ event: handler }`.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtPlugin.on
+        */
+       p.on = hookupEvents;
+
+       /**
+        * Returns the hash object that `getFormData` triggered by the core expects.
+        *
+        * @signature TextExtPlugin.formDataObject(input, form)
+        *
+        * @param input {String} Value that will go into the text input that user is interacting with.
+        * @param form {Object} Value that will be serialized and put into the hidden that will be submitted
+        * with the HTML form.
+        *
+        * @author agorbatchev
+        * @date 2011/08/22
+        * @id TextExtPlugin.formDataObject
+        */
+       p.formDataObject = formDataObject;
+
+       /**
+        * Initialization method called by the core during plugin instantiation. This method must be implemented
+        * by each plugin individually.
+        *
+        * @signature TextExtPlugin.init(core)
+        *
+        * @param core {TextExt} Instance of the TextExt core class.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtPlugin.init
+        */
+       p.init = function(core) { throw new Error('Not implemented') };
+
+       /**
+        * Initialization method wich should be called by the plugin during the `init()` call.
+        *
+        * @signature TextExtPlugin.baseInit(core, defaults)
+        *
+        * @param core {TextExt} Instance of the TextExt core class.
+        * @param defaults {Object} Default plugin options. These will be checked if desired value wasn't
+        * found in the options supplied by the user.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtPlugin.baseInit
+        */
+       p.baseInit = function(core, defaults)
+       {
+               var self = this;
+
+               core._defaults = $.extend(true, core._defaults, defaults);
+               self._core     = core;
+               self._timers   = {};
+       };
+
+       /**
+        * Allows starting of multiple timeout calls. Each time this method is called with the same
+        * timer name, the timer is reset. This functionality is useful in cases where an action needs
+        * to occur only after a certain period of inactivity. For example, making an AJAX call after 
+        * user stoped typing for 1 second.
+        *
+        * @signature TextExtPlugin.startTimer(name, delay, callback)
+        *
+        * @param name {String} Timer name.
+        * @param delay {Number} Delay in seconds.
+        * @param callback {Function} Callback function.
+        *
+        * @author agorbatchev
+        * @date 2011/08/25
+        * @id TextExtPlugin.startTimer
+        */
+       p.startTimer = function(name, delay, callback)
+       {
+               var self = this;
+
+               self.stopTimer(name);
+
+               self._timers[name] = setTimeout(
+                       function()
+                       {
+                               delete self._timers[name];
+                               callback.apply(self);
+                       },
+                       delay * 1000
+               );
+       };
+
+       /**
+        * Stops the timer by name without resetting it.
+        *
+        * @signature TextExtPlugin.stopTimer(name)
+        *
+        * @param name {String} Timer name.
+        *
+        * @author agorbatchev
+        * @date 2011/08/25
+        * @id TextExtPlugin.stopTimer
+        */
+       p.stopTimer = function(name)
+       {
+               clearTimeout(this._timers[name]);
+       };
+
+       /**
+        * Returns instance of the `TextExt` to which current instance of the plugin is attached to.
+        *
+        * @signature TextExtPlugin.core()
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtPlugin.core
+        */
+       p.core = function()
+       {
+               return this._core;
+       };
+
+       /**
+        * Shortcut to the core's `opts()` method. Returns option value.
+        *
+        * @signature TextExtPlugin.opts(name)
+        * 
+        * @param name {String} Option name as described in the options.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtPlugin.opts
+        */
+       p.opts = function(name)
+       {
+               return this.core().opts(name);
+       };
+
+       /**
+        * Shortcut to the core's `itemManager()` method. Returns instance of the `ItemManger` that is
+        * currently in use.
+        *
+        * @signature TextExtPlugin.itemManager()
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtPlugin.itemManager
+        */
+       p.itemManager = function()
+       {
+               return this.core().itemManager();
+       };
+
+       /**
+        * Shortcut to the core's `input()` method. Returns instance of the HTML element that represents
+        * current text input.
+        *
+        * @signature TextExtPlugin.input()
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtPlugin.input
+        */
+       p.input = function()
+       {
+               return this.core().input();
+       };
+
+       /**
+        * Shortcut to the commonly used `this.input().val()` call to get or set value of the text input.
+        *
+        * @signature TextExtPlugin.val(value)
+        *
+        * @param value {String} Optional value. If specified, the value will be set, otherwise it will be
+        * returned.
+        *
+        * @author agorbatchev
+        * @date 2011/08/20
+        * @id TextExtPlugin.val
+        */
+       p.val = function(value)
+       {
+               var input = this.input();
+
+               if(typeof(value) === UNDEFINED)
+                       return input.val();
+               else
+                       input.val(value);
+       };
+
+       /**
+        * Shortcut to the core's `trigger()` method. Triggers specified event with arguments on the
+        * component core.
+        *
+        * @signature TextExtPlugin.trigger(event, ...args)
+        *
+        * @param event {String} Name of the event to trigger.
+        * @param ...args All remaining arguments will be passed to the event handler.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtPlugin.trigger
+        */
+       p.trigger = function()
+       {
+               var core = this.core();
+               core.trigger.apply(core, arguments);
+       };
+
+       /**
+        * Shortcut to the core's `bind()` method. Binds specified handler to the event.
+        *
+        * @signature TextExtPlugin.bind(event, handler)
+        *
+        * @param event {String} Event name.
+        * @param handler {Function} Event handler.
+        *
+        * @author agorbatchev
+        * @date 2011/08/20
+        * @id TextExtPlugin.bind
+        */
+       p.bind = function(event, handler)
+       {
+               this.core().bind(event, handler);
+       };
+
+       /**
+        * Returns initialization priority for this plugin. If current plugin depends upon some other plugin
+        * to be initialized before or after, priority needs to be adjusted accordingly. Plugins with higher
+        * priority initialize before plugins with lower priority.
+        *
+        * Default initialization priority is `0`.
+        *
+        * @signature TextExtPlugin.initPriority()
+        *
+        * @author agorbatchev
+        * @date 2011/08/22
+        * @id TextExtPlugin.initPriority
+        */
+       p.initPriority = function()
+       {
+               return 0;
+       };
+
+       //--------------------------------------------------------------------------------
+       // jQuery Integration
+       
+       /**
+        * TextExt integrates as a jQuery plugin available through the `$(selector).textext(opts)` call. If
+        * `opts` argument is passed, then a new instance of `TextExt` will be created for all the inputs
+        * that match the `selector`. If `opts` wasn't passed and TextExt was already intantiated for 
+        * inputs that match the `selector`, array of `TextExt` instances will be returned instead.
+        *
+        *     // will create a new instance of `TextExt` for all elements that match `.sample`
+        *     $('.sample').textext({ ... });
+        *
+        *     // will return array of all `TextExt` instances
+        *     var list = $('.sample').textext();
+        *
+        * The following properties are also exposed through the jQuery `$.fn.textext`:
+        *
+        * * `TextExt` -- `TextExt` class.
+        * * `TextExtPlugin` -- `TextExtPlugin` class.
+        * * `ItemManager` -- `ItemManager` class.
+        * * `plugins` -- Key/value table of all registered plugins.
+        * * `addPlugin(name, constructor)` -- All plugins should register themselves using this function.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExt.jquery
+        */
+
+       var cssInjected = false;
+
+       var textext = $.fn.textext = function(opts)
+       {
+               var css;
+               
+               if(!cssInjected && (css = $.fn.textext.css) != null)
+               {
+                       $('head').append('<style>' + css + '</style>');
+                       cssInjected = true;
+               }
+
+               return this.map(function()
+               {
+                       var self = $(this);
+
+                       if(opts == null)
+                               return self.data('textext');
+
+                       var instance = new TextExt();
+
+                       instance.init(self, opts);
+                       self.data('textext', instance);
+
+                       return instance.input()[0];
+               });
+       };
+
+       /**
+        * This static function registers a new plugin which makes it available through the `plugins` option
+        * to the end user. The name specified here is the name the end user would put in the `plugins` option
+        * to add this plugin to a new instance of TextExt.
+        * 
+        * @signature $.fn.textext.addPlugin(name, constructor)
+        *
+        * @param name {String} Name of the plugin.
+        * @param constructor {Function} Plugin constructor.
+        *
+        * @author agorbatchev
+        * @date 2011/10/11
+        * @id TextExt.addPlugin
+        */
+       textext.addPlugin = function(name, constructor)
+       {
+               textext.plugins[name] = constructor;
+               constructor.prototype = new textext.TextExtPlugin();
+       };
+
+       /**
+        * This static function registers a new patch which is added to each instance of TextExt. If you are
+        * adding a new patch, make sure to call this method.
+        * 
+        * @signature $.fn.textext.addPatch(name, constructor)
+        *
+        * @param name {String} Name of the patch.
+        * @param constructor {Function} Patch constructor.
+        *
+        * @author agorbatchev
+        * @date 2011/10/11
+        * @id TextExt.addPatch
+        */
+       textext.addPatch = function(name, constructor)
+       {
+               textext.patches[name] = constructor;
+               constructor.prototype = new textext.TextExtPlugin();
+       };
+
+       textext.TextExt       = TextExt;
+       textext.TextExtPlugin = TextExtPlugin;
+       textext.ItemManager   = ItemManager;
+       textext.plugins       = {};
+       textext.patches       = {};
+})(jQuery);
+
+(function($)
+{
+       function TextExtIE9Patches() {};
+
+       $.fn.textext.TextExtIE9Patches = TextExtIE9Patches;
+       $.fn.textext.addPatch('ie9',TextExtIE9Patches);
+
+       var p = TextExtIE9Patches.prototype;
+
+       p.init = function(core)
+       {
+               if(navigator.userAgent.indexOf('MSIE 9') == -1)
+                       return;
+
+               var self = this;
+
+               core.on({ postInvalidate : self.onPostInvalidate });
+       };
+
+       p.onPostInvalidate = function()
+       {
+               var self  = this,
+                       input = self.input(),
+                       val   = input.val()
+                       ;
+
+               // agorbatchev :: IE9 doesn't seem to update the padding if box-sizing is on until the
+               // text box value changes, so forcing this change seems to do the trick of updating
+               // IE's padding visually.
+               input.val(Math.random());
+               input.val(val);
+       };
+})(jQuery);
+
+;/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.0
+ * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($)
+{
+       /**
+        * AJAX plugin is very useful if you want to load list of items from a data point and pass it
+        * to the Autocomplete or Filter plugins.
+        *
+        * Because it meant to be as a helper method for either Autocomplete or Filter plugin, without
+        * either of these two present AJAX plugin won't do anything.
+        *
+        * @author agorbatchev
+        * @date 2011/08/16
+        * @id TextExtAjax
+        */
+       function TextExtAjax() {};
+
+       $.fn.textext.TextExtAjax = TextExtAjax;
+       $.fn.textext.addPlugin('ajax', TextExtAjax);
+
+       var p = TextExtAjax.prototype,
+
+               /**
+                * AJAX plugin options are grouped under `ajax` when passed to the `$().textext()` function. Be
+                * mindful that the whole `ajax` object is also passed to jQuery `$.ajax` call which means that
+                * you can change all jQuery options as well. Please refer to the jQuery documentation on how
+                * to set url and all other parameters. For example:
+                *
+                *     $('textarea').textext({
+                *         plugins: 'ajax',
+                *         ajax: {
+                *             url: 'http://...'
+                *         }
+                *     })
+                *
+                * **Important**: Because it's necessary to pass options to `jQuery.ajax()` in a single object,
+                * all jQuery related AJAX options like `url`, `dataType`, etc **must** be within the `ajax` object.
+                * This is the exception to general rule that TextExt options can be specified in dot or camel case 
+                * notation.
+                * 
+                * @author agorbatchev
+                * @date 2011/08/16
+                * @id TextExtAjax.options
+                */
+
+               /**
+                * By default, when user starts typing into the text input, AJAX plugin will start making requests
+                * to the `url` that you have specified and will pass whatever user has typed so far as a parameter
+                * named `q`, eg `?q=foo`.
+                *
+                * If you wish to change this behaviour, you can pass a function as a value for this option which
+                * takes one argument (the user input) and should return a key/value object that will be converted
+                * to the request parameters. For example:
+                *
+                *     'dataCallback' : function(query)
+                *     {
+                *         return { 'search' : query };
+                *     } 
+                *
+                * @name ajax.data.callback
+                * @default null
+                * @author agorbatchev
+                * @date 2011/08/16
+                * @id TextExtAjax.options.data.callback
+                */
+               OPT_DATA_CALLBACK = 'ajax.data.callback',
+               
+               /**
+                * By default, the server end point is constantly being reloaded whenever user changes the value
+                * in the text input. If you'd rather have the client do result filtering, you can return all
+                * possible results from the server and cache them on the client by setting this option to `true`.
+                *
+                * In such a case, only one call to the server will be made and filtering will be performed on
+                * the client side using `ItemManager` attached to the core.
+                *
+                * @name ajax.data.results
+                * @default false
+                * @author agorbatchev
+                * @date 2011/08/16
+                * @id TextExtAjax.options.cache.results
+                */
+               OPT_CACHE_RESULTS = 'ajax.cache.results',
+               
+               /**
+                * The loading message delay is set in seconds and will specify how long it would take before
+                * user sees the message. If you don't want user to ever see this message, set the option value
+                * to `Number.MAX_VALUE`.
+                *
+                * @name ajax.loading.delay
+                * @default 0.5
+                * @author agorbatchev
+                * @date 2011/08/16
+                * @id TextExtAjax.options.loading.delay
+                */
+               OPT_LOADING_DELAY = 'ajax.loading.delay',
+
+               /**
+                * Whenever an AJAX request is made and the server takes more than the number of seconds specified
+                * in `ajax.loading.delay` to respond, the message specified in this option will appear in the drop
+                * down.
+                *
+                * @name ajax.loading.message
+                * @default "Loading..."
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExtAjax.options.loading.message
+                */
+               OPT_LOADING_MESSAGE = 'ajax.loading.message',
+
+               /**
+                * When user is typing in or otherwise changing the value of the text input, it's undesirable to make
+                * an AJAX request for every keystroke. Instead it's more conservative to send a request every number
+                * of seconds while user is typing the value. This number of seconds is specified by the `ajax.type.delay`
+                * option.
+                *
+                * @name ajax.type.delay
+                * @default 0.5
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExtAjax.options.type.delay
+                */
+               OPT_TYPE_DELAY = 'ajax.type.delay',
+
+               /**
+                * AJAX plugin dispatches or reacts to the following events.
+                *
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExtAjax.events
+                */
+
+               /**
+                * AJAX plugin reacts to the `getSuggestions` event dispatched by the Autocomplete plugin.
+                *
+                * @name getSuggestions
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExtAjax.events.getSuggestions
+                */
+
+               /**
+                * In the event of successful AJAX request, the AJAX coponent dispatches the `setSuggestions`
+                * event meant to be recieved by the Autocomplete plugin.
+                *
+                * @name setSuggestions
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExtAjax.events.setSuggestions
+                */
+               EVENT_SET_SUGGESTION = 'setSuggestions',
+
+               /**
+                * AJAX plugin dispatches the `showDropdown` event which Autocomplete plugin is expecting.
+                * This is used to temporarily show the loading message if the AJAX request is taking longer
+                * than expected.
+                *
+                * @name showDropdown
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExtAjax.events.showDropdown
+                */
+               EVENT_SHOW_DROPDOWN = 'showDropdown',
+
+               TIMER_LOADING = 'loading',
+
+               DEFAULT_OPTS = {
+                       ajax : {
+                               typeDelay      : 0.5,
+                               loadingMessage : 'Loading...',
+                               loadingDelay   : 0.5,
+                               cacheResults   : false,
+                               dataCallback   : null
+                       }
+               }
+               ;
+
+       /**
+        * Initialization method called by the core during plugin instantiation.
+        *
+        * @signature TextExtAjax.init(core)
+        *
+        * @param core {TextExt} Instance of the TextExt core class.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAjax.init
+        */
+       p.init = function(core)
+       {
+               var self = this;
+
+               self.baseInit(core, DEFAULT_OPTS);
+
+               self.on({
+                       getSuggestions : self.onGetSuggestions
+               });
+
+               self._suggestions = null;
+       };
+
+       /**
+        * Performas an async AJAX with specified options.
+        *
+        * @signature TextExtAjax.load(query)
+        *
+        * @param query {String} Value that user has typed into the text area which is
+        * presumably the query.
+        *
+        * @author agorbatchev
+        * @date 2011/08/14
+        * @id TextExtAjax.load
+        */
+       p.load = function(query)
+       {
+               var self         = this,
+                       dataCallback = self.opts(OPT_DATA_CALLBACK) || function(query) { return { q : query } },
+                       opts
+                       ;
+
+               opts = $.extend(true,
+                       {
+                               data    : dataCallback(query),
+                               success : function(data) { self.onComplete(data, query) },
+                               error   : function(jqXHR, message) { console.error(message, query) }
+                       }, 
+                       self.opts('ajax')
+               );
+
+               $.ajax(opts);
+       };
+
+       /**
+        * Successful call AJAX handler. Takes the data that came back from AJAX and the
+        * original query that was used to make the call.
+        *
+        * @signature TextExtAjax.onComplete(data, query)
+        *
+        * @param data {Object} Data loaded from the server, should be an Array of strings
+        * by default or whatever data structure your custom `ItemManager` implements.
+        *
+        * @param query {String} Query string, ie whatever user has typed in.
+        *
+        * @author agorbatchev
+        * @date 2011/08/14
+        * @id TextExtAjax.onComplete
+        */
+       p.onComplete = function(data, query)
+       {
+               var self   = this,
+                       result = data
+                       ;
+               
+               self.dontShowLoading();
+
+               // If results are expected to be cached, then we store the original
+               // data set and return the filtered one based on the original query.
+               // That means we do filtering on the client side, instead of the
+               // server side.
+               if(self.opts(OPT_CACHE_RESULTS) == true)
+               {
+                       self._suggestions = data;
+                       result = self.itemManager().filter(data, query);
+               }
+
+               self.trigger(EVENT_SET_SUGGESTION, { result : result });
+       };
+
+       /**
+        * If show loading message timer was started, calling this function disables it,
+        * otherwise nothing else happens.
+        *
+        * @signature TextExtAjax.dontShowLoading()
+        *
+        * @author agorbatchev
+        * @date 2011/08/16
+        * @id TextExtAjax.dontShowLoading
+        */
+       p.dontShowLoading = function()
+       {
+               this.stopTimer(TIMER_LOADING);
+       };
+
+       /**
+        * Shows message specified in `ajax.loading.message` if loading data takes more than
+        * number of seconds specified in `ajax.loading.delay`.
+        *
+        * @signature TextExtAjax.showLoading()
+        *
+        * @author agorbatchev
+        * @date 2011/08/15
+        * @id TextExtAjax.showLoading
+        */
+       p.showLoading = function()
+       {
+               var self = this;
+
+               self.dontShowLoading();
+               self.startTimer(
+                       TIMER_LOADING,
+                       self.opts(OPT_LOADING_DELAY),
+                       function()
+                       {
+                               self.trigger(EVENT_SHOW_DROPDOWN, function(autocomplete)
+                               {
+                                       autocomplete.clearItems();
+                                       var node = autocomplete.addDropdownItem(self.opts(OPT_LOADING_MESSAGE));
+                                       node.addClass('text-loading');
+                               });
+                       }
+               );
+       };
+
+       /**
+        * Reacts to the `getSuggestions` event and begin loading suggestions. If
+        * `ajax.cache.results` is specified, all calls after the first one will use
+        * cached data and filter it with the `core.itemManager.filter()`.
+        *
+        * @signature TextExtAjax.onGetSuggestions(e, data)
+        *
+        * @param e {Object} jQuery event.
+        * @param data {Object} Data structure passed with the `getSuggestions` event
+        * which contains the user query, eg `{ query : "..." }`.
+        *
+        * @author agorbatchev
+        * @date 2011/08/15
+        * @id TextExtAjax.onGetSuggestions
+        */
+       p.onGetSuggestions = function(e, data)
+       {
+               var self        = this,
+                       suggestions = self._suggestions,
+                       query       = (data || {}).query || ''
+                       ;
+
+               if(suggestions && self.opts(OPT_CACHE_RESULTS) === true)
+                       return self.onComplete(suggestions, query);
+               
+               self.startTimer(
+                       'ajax',
+                       self.opts(OPT_TYPE_DELAY),
+                       function()
+                       {
+                               self.showLoading();
+                               self.load(query);
+                       }
+               );
+       };
+})(jQuery);
+;/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.0
+ * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($)
+{
+       /**
+        * Displays a dropdown style arrow button. The `TextExtArrow` works together with the
+        * `TextExtAutocomplete` plugin and whenever clicked tells the autocomplete plugin to
+        * display its suggestions.
+        *
+        * @author agorbatchev
+        * @date 2011/12/27
+        * @id TextExtArrow
+        */
+       function TextExtArrow() {};
+
+       $.fn.textext.TextExtArrow = TextExtArrow;
+       $.fn.textext.addPlugin('arrow', TextExtArrow);
+
+       var p = TextExtArrow.prototype,
+               /**
+                * Arrow plugin only has one option and that is its HTML template. It could be 
+                * changed when passed to the `$().textext()` function. For example:
+                *
+                *     $('textarea').textext({
+                *         plugins: 'arrow',
+                *         html: {
+                *             arrow: "<span/>"
+                *         }
+                *     })
+                *
+                * @author agorbatchev
+                * @date 2011/12/27
+                * @id TextExtArrow.options
+                */
+               
+               /**
+                * HTML source that is used to generate markup required for the arrow.
+                *
+                * @name html.arrow
+                * @default '<div class="text-arrow"/>'
+                * @author agorbatchev
+                * @date 2011/12/27
+                * @id TextExtArrow.options.html.arrow
+                */
+               OPT_HTML_ARROW = 'html.arrow',
+
+               DEFAULT_OPTS = {
+                       html : {
+                               arrow : '<div class="text-arrow"/>'
+                       }
+               }
+               ;
+
+       /**
+        * Initialization method called by the core during plugin instantiation.
+        *
+        * @signature TextExtArrow.init(core)
+        *
+        * @param core {TextExt} Instance of the TextExt core class.
+        *
+        * @author agorbatchev
+        * @date 2011/12/27
+        * @id TextExtArrow.init
+        */
+       p.init = function(core)
+       {
+               var self = this,
+                       arrow
+                       ;
+
+               self.baseInit(core, DEFAULT_OPTS);
+
+               self._arrow = arrow = $(self.opts(OPT_HTML_ARROW));
+               self.core().wrapElement().append(arrow);
+               arrow.bind('click', function(e) { self.onArrowClick(e); });
+       };
+
+       //--------------------------------------------------------------------------------
+       // Event handlers
+       
+       /**
+        * Reacts to the `click` event whenever user clicks the arrow.
+        *
+        * @signature TextExtArrow.onArrowClick(e)
+        *
+        * @param e {Object} jQuery event.
+        * @author agorbatchev
+        * @date 2011/12/27
+        * @id TextExtArrow.onArrowClick
+        */
+       p.onArrowClick = function(e)
+       {
+               this.trigger('toggleDropdown');
+               this.core().focusInput();
+       };
+       
+       //--------------------------------------------------------------------------------
+       // Core functionality
+
+})(jQuery);
+;/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.0
+ * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($)
+{
+       /**
+        * Autocomplete plugin brings the classic autocomplete functionality to the TextExt echosystem.
+        * The gist of functionality is when user starts typing in, for example a term or a tag, a
+        * dropdown would be presented with possible suggestions to complete the input quicker.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete
+        */
+       function TextExtAutocomplete() {};
+
+       $.fn.textext.TextExtAutocomplete = TextExtAutocomplete;
+       $.fn.textext.addPlugin('autocomplete', TextExtAutocomplete);
+
+       var p = TextExtAutocomplete.prototype,
+               
+               CSS_DOT            = '.',
+               CSS_SELECTED       = 'text-selected',
+               CSS_DOT_SELECTED   = CSS_DOT + CSS_SELECTED,
+               CSS_SUGGESTION     = 'text-suggestion',
+               CSS_DOT_SUGGESTION = CSS_DOT + CSS_SUGGESTION,
+               CSS_LABEL          = 'text-label',
+               CSS_DOT_LABEL      = CSS_DOT + CSS_LABEL,
+
+               /**
+                * Autocomplete plugin options are grouped under `autocomplete` when passed to the 
+                * `$().textext()` function. For example:
+                *
+                *     $('textarea').textext({
+                *         plugins: 'autocomplete',
+                *         autocomplete: {
+                *             dropdownPosition: 'above'
+                *         }
+                *     })
+                *
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExtAutocomplete.options
+                */
+
+               /**
+                * This is a toggle switch to enable or disable the Autucomplete plugin. The value is checked
+                * each time at the top level which allows you to toggle this setting on the fly.
+                *
+                * @name autocomplete.enabled
+                * @default true
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExtAutocomplete.options.autocomplete.enabled
+                */
+               OPT_ENABLED = 'autocomplete.enabled',
+
+               /**
+                * This option allows to specify position of the dropdown. The two possible values
+                * are `above` and `below`.
+                *
+                * @name autocomplete.dropdown.position
+                * @default "below"
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExtAutocomplete.options.autocomplete.dropdown.position
+                */
+               OPT_POSITION = 'autocomplete.dropdown.position',
+
+               /**
+                * This option allows to specify maximum height of the dropdown. Value is taken directly, so
+                * if desired height is 200 pixels, value must be `200px`.
+                *
+                * @name autocomplete.dropdown.maxHeight
+                * @default "100px"
+                * @author agorbatchev
+                * @date 2011/12/29
+                * @id TextExtAutocomplete.options.autocomplete.dropdown.maxHeight
+                * @version 1.1
+                */
+               OPT_MAX_HEIGHT = 'autocomplete.dropdown.maxHeight',
+
+               /**
+                * This option allows to override how a suggestion item is rendered. The value should be
+                * a function, the first argument of which is suggestion to be rendered and `this` context
+                * is the current instance of `TextExtAutocomplete`. 
+                *
+                * [Click here](/manual/examples/autocomplete-with-custom-render.html) to see a demo.
+                *
+                * For example:
+                *
+                *     $('textarea').textext({
+                *         plugins: 'autocomplete',
+                *         autocomplete: {
+                *             render: function(suggestion)
+                *             {
+                *                 return '<b>' + suggestion + '</b>';
+                *             }
+                *         }
+                *     })
+                *
+                * @name autocomplete.render
+                * @default null
+                * @author agorbatchev
+                * @date 2011/12/23
+                * @id TextExtAutocomplete.options.autocomplete.render
+                * @version 1.1
+                */
+               OPT_RENDER = 'autocomplete.render',
+
+               /**
+                * HTML source that is used to generate the dropdown.
+                *
+                * @name html.dropdown
+                * @default '<div class="text-dropdown"><div class="text-list"/></div>'
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExtAutocomplete.options.html.dropdown
+                */
+               OPT_HTML_DROPDOWN = 'html.dropdown',
+
+               /**
+                * HTML source that is used to generate each suggestion.
+                *
+                * @name html.suggestion
+                * @default '<div class="text-suggestion"><span class="text-label"/></div>'
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExtAutocomplete.options.html.suggestion
+                */
+               OPT_HTML_SUGGESTION = 'html.suggestion',
+
+               /**
+                * Autocomplete plugin triggers or reacts to the following events.
+                *
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExtAutocomplete.events
+                */
+       
+               /**
+                * Autocomplete plugin triggers and reacts to the `hideDropdown` to hide the dropdown if it's 
+                * already visible.
+                *
+                * @name hideDropdown
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExtAutocomplete.events.hideDropdown
+                */
+               EVENT_HIDE_DROPDOWN = 'hideDropdown',
+
+               /**
+                * Autocomplete plugin triggers and reacts to the `showDropdown` to show the dropdown if it's 
+                * not already visible.
+                *
+                * It's possible to pass a render callback function which will be called instead of the
+                * default `TextExtAutocomplete.renderSuggestions()`. 
+                *
+                * Here's how another plugin should trigger this event with the optional render callback:
+                *
+                *     this.trigger('showDropdown', function(autocomplete)
+                *     {
+                *         autocomplete.clearItems();
+                *         var node = autocomplete.addDropdownItem('<b>Item</b>');
+                *         node.addClass('new-look');
+                *     });
+                *
+                * @name showDropdown
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExtAutocomplete.events.showDropdown
+                */
+               EVENT_SHOW_DROPDOWN = 'showDropdown',
+
+               /**
+                * Autocomplete plugin reacts to the `setSuggestions` event triggered by other plugins which
+                * wish to populate the suggestion items. Suggestions should be passed as event argument in the 
+                * following format: `{ data : [ ... ] }`. 
+                *
+                * Here's how another plugin should trigger this event:
+                *
+                *     this.trigger('setSuggestions', { data : [ "item1", "item2" ] });
+                *
+                * @name setSuggestions
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExtAutocomplete.events.setSuggestions
+                */
+
+               /**
+                * Autocomplete plugin triggers the `getSuggestions` event and expects to get results by listening for
+                * the `setSuggestions` event.
+                *
+                * @name getSuggestions
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExtAutocomplete.events.getSuggestions
+                */
+               EVENT_GET_SUGGESTIONS = 'getSuggestions',
+
+               /**
+                * Autocomplete plugin triggers `getFormData` event with the current suggestion so that the the core
+                * will be updated with serialized data to be submitted with the HTML form.
+                * 
+                * @name getFormData
+                * @author agorbatchev
+                * @date 2011/08/18
+                * @id TextExtAutocomplete.events.getFormData
+                */
+               EVENT_GET_FORM_DATA = 'getFormData',
+
+               /**
+                * Autocomplete plugin reacts to `toggleDropdown` event and either shows or hides the dropdown
+                * depending if it's currently hidden or visible.
+                * 
+                * @name toggleDropdown
+                * @author agorbatchev
+                * @date 2011/12/27
+                * @id TextExtAutocomplete.events.toggleDropdown
+                * @version 1.1
+                */
+               EVENT_TOGGLE_DROPDOWN = 'toggleDropdown',
+
+               POSITION_ABOVE = 'above',
+               POSITION_BELOW = 'below',
+               
+               DATA_MOUSEDOWN_ON_AUTOCOMPLETE = 'mousedownOnAutocomplete',
+
+               DEFAULT_OPTS = {
+                       autocomplete : {
+                               enabled : true,
+                               dropdown : {
+                                       position : POSITION_BELOW,
+                                       maxHeight : '100px'
+                               }
+                       },
+
+                       html : {
+                               dropdown   : '<div class="text-dropdown"><div class="text-list"/></div>',
+                               suggestion : '<div class="text-suggestion"><span class="text-label"/></div>'
+                       }
+               }
+               ;
+
+       /**
+        * Initialization method called by the core during plugin instantiation.
+        *
+        * @signature TextExtAutocomplete.init(core)
+        *
+        * @param core {TextExt} Instance of the TextExt core class.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.init
+        */
+       p.init = function(core)
+       {
+               var self = this;
+
+               self.baseInit(core, DEFAULT_OPTS);
+
+               var input = self.input(),
+                       container
+                       ;
+
+               if(self.opts(OPT_ENABLED) === true)
+               {
+                       self.on({
+                               blur              : self.onBlur,
+                               anyKeyUp          : self.onAnyKeyUp,
+                               deleteKeyUp       : self.onAnyKeyUp,
+                               backspaceKeyPress : self.onBackspaceKeyPress,
+                               enterKeyPress     : self.onEnterKeyPress,
+                               escapeKeyPress    : self.onEscapeKeyPress,
+                               setSuggestions    : self.onSetSuggestions,
+                               showDropdown      : self.onShowDropdown,
+                               hideDropdown      : self.onHideDropdown,
+                               toggleDropdown    : self.onToggleDropdown,
+                               postInvalidate    : self.positionDropdown,
+                               getFormData       : self.onGetFormData,
+
+                               // using keyDown for up/down keys so that repeat events are
+                               // captured and user can scroll up/down by holding the keys
+                               downKeyDown : self.onDownKeyDown,
+                               upKeyDown   : self.onUpKeyDown
+                       });
+
+                       container = $(self.opts(OPT_HTML_DROPDOWN));
+                       container.insertAfter(input);
+
+                       self.on(container, {
+                               mouseover : self.onMouseOver,
+                               mousedown : self.onMouseDown,
+                               click     : self.onClick
+                       });
+
+                       container
+                               .css('maxHeight', self.opts(OPT_MAX_HEIGHT))
+                               .addClass('text-position-' + self.opts(OPT_POSITION))
+                               ;
+
+                       $(self).data('container', container);
+                       
+                       $(document.body).click(function(e) 
+                       {
+                               if (self.isDropdownVisible() && !self.withinWrapElement(e.target))
+                                       self.trigger(EVENT_HIDE_DROPDOWN);
+                       });
+
+                       self.positionDropdown();
+               }
+       };
+
+       /**
+        * Returns top level dropdown container HTML element.
+        * 
+        * @signature TextExtAutocomplete.containerElement()
+        * 
+        * @author agorbatchev
+        * @date 2011/08/15
+        * @id TextExtAutocomplete.containerElement
+        */
+       p.containerElement = function()
+       {
+               return $(this).data('container');
+       };
+
+       //--------------------------------------------------------------------------------
+       // User mouse/keyboard input
+       
+       /**
+        * Reacts to the `mouseOver` event triggered by the TextExt core.
+        *
+        * @signature TextExtAutocomplete.onMouseOver(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.onMouseOver
+        */
+       p.onMouseOver = function(e)
+       {
+               var self   = this,
+                       target = $(e.target)
+                       ;
+
+               if(target.is(CSS_DOT_SUGGESTION))
+               {
+                       self.clearSelected();
+                       target.addClass(CSS_SELECTED);
+               }
+       };
+       
+       /**
+        * Reacts to the `mouseDown` event triggered by the TextExt core.
+        *
+        * @signature TextExtAutocomplete.onMouseDown(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author adamayres
+        * @date 2012/01/13
+        * @id TextExtAutocomplete.onMouseDown
+        */
+       p.onMouseDown = function(e)
+       {
+               this.containerElement().data(DATA_MOUSEDOWN_ON_AUTOCOMPLETE, true);
+       };
+       
+       /**
+        * Reacts to the `click` event triggered by the TextExt core.
+        *
+        * @signature TextExtAutocomplete.onClick(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.onClick
+        */
+       p.onClick = function(e)
+       {
+               var self   = this,
+                       target = $(e.target)
+                       ;
+
+               if(target.is(CSS_DOT_SUGGESTION) || target.is(CSS_DOT_LABEL))
+                       self.trigger('enterKeyPress');
+               
+               if (self.core().hasPlugin('tags'))
+                       self.val('');
+       };
+
+       /**
+        * Reacts to the `blur` event triggered by the TextExt core.
+        *
+        * @signature TextExtAutocomplete.onBlur(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.onBlur
+        */
+       p.onBlur = function(e)
+       {
+               var self              = this,
+                       container         = self.containerElement(),
+                       isBlurByMousedown = container.data(DATA_MOUSEDOWN_ON_AUTOCOMPLETE) === true
+                       ;
+
+               // only trigger a close event if the blur event was 
+               // not triggered by a mousedown event on the autocomplete
+               // otherwise set focus back back on the input
+               if(self.isDropdownVisible())
+                       isBlurByMousedown ? self.core().focusInput() : self.trigger(EVENT_HIDE_DROPDOWN);
+                               
+               container.removeData(DATA_MOUSEDOWN_ON_AUTOCOMPLETE);
+       };
+
+       /**
+        * Reacts to the `backspaceKeyPress` event triggered by the TextExt core. 
+        *
+        * @signature TextExtAutocomplete.onBackspaceKeyPress(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.onBackspaceKeyPress
+        */
+       p.onBackspaceKeyPress = function(e)
+       {
+               var self    = this,
+                       isEmpty = self.val().length > 0
+                       ;
+
+               if(isEmpty || self.isDropdownVisible())
+                       self.getSuggestions();
+       };
+
+       /**
+        * Reacts to the `anyKeyUp` event triggered by the TextExt core.
+        *
+        * @signature TextExtAutocomplete.onAnyKeyUp(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.onAnyKeyUp
+        */
+       p.onAnyKeyUp = function(e, keyCode)
+       {
+               var self          = this,
+                       isFunctionKey = self.opts('keys.' + keyCode) != null
+                       ;
+
+               if(self.val().length > 0 && !isFunctionKey)
+                       self.getSuggestions();
+       };
+
+       /**
+        * Reacts to the `downKeyDown` event triggered by the TextExt core.
+        *
+        * @signature TextExtAutocomplete.onDownKeyDown(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.onDownKeyDown
+        */
+       p.onDownKeyDown = function(e)
+       {
+               var self = this;
+
+               self.isDropdownVisible()
+                       ? self.toggleNextSuggestion() 
+                       : self.getSuggestions()
+                       ;
+       };
+
+       /**
+        * Reacts to the `upKeyDown` event triggered by the TextExt core.
+        *
+        * @signature TextExtAutocomplete.onUpKeyDown(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.onUpKeyDown
+        */
+       p.onUpKeyDown = function(e)
+       {
+               this.togglePreviousSuggestion();
+       };
+
+       /**
+        * Reacts to the `enterKeyPress` event triggered by the TextExt core.
+        *
+        * @signature TextExtAutocomplete.onEnterKeyPress(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.onEnterKeyPress
+        */
+       p.onEnterKeyPress = function(e)
+       {
+               var self = this;
+
+               if(self.isDropdownVisible())
+                       self.selectFromDropdown();
+       };
+
+       /**
+        * Reacts to the `escapeKeyPress` event triggered by the TextExt core. Hides the dropdown
+        * if it's currently visible.
+        *
+        * @signature TextExtAutocomplete.onEscapeKeyPress(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.onEscapeKeyPress
+        */
+       p.onEscapeKeyPress = function(e)
+       {
+               var self = this;
+
+               if(self.isDropdownVisible())
+                       self.trigger(EVENT_HIDE_DROPDOWN);
+       };
+
+       //--------------------------------------------------------------------------------
+       // Core functionality
+
+       /**
+        * Positions dropdown either below or above the input based on the `autocomplete.dropdown.position`
+        * option specified, which could be either `above` or `below`.
+        *
+        * @signature TextExtAutocomplete.positionDropdown()
+        *
+        * @author agorbatchev
+        * @date 2011/08/15
+        * @id TextExtAutocomplete.positionDropdown
+        */
+       p.positionDropdown = function()
+       {
+               var self      = this,
+                       container = self.containerElement(),
+                       direction = self.opts(OPT_POSITION),
+                       height    = self.core().wrapElement().outerHeight(),
+                       css       = {}
+                       ;
+
+               css[direction === POSITION_ABOVE ? 'bottom' : 'top'] = height + 'px';
+               container.css(css);
+       };
+
+       /**
+        * Returns list of all the suggestion HTML elements in the dropdown.
+        *
+        * @signature TextExtAutocomplete.suggestionElements()
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.suggestionElements
+        */
+       p.suggestionElements = function()
+       {
+               return this.containerElement().find(CSS_DOT_SUGGESTION);
+       };
+
+
+       /**
+        * Highlights specified suggestion as selected in the dropdown.
+        *
+        * @signature TextExtAutocomplete.setSelectedSuggestion(suggestion)
+        *
+        * @param suggestion {Object} Suggestion object. With the default `ItemManager` this
+        * is expected to be a string, anything else with custom implementations.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.setSelectedSuggestion
+        */
+       p.setSelectedSuggestion = function(suggestion)
+       {
+               if(!suggestion)
+                       return;
+
+               var self   = this,
+                       all    = self.suggestionElements(),
+                       target = all.first(),
+                       item, i
+                       ;
+
+               self.clearSelected();
+
+               for(i = 0; i < all.length; i++)
+               {
+                       item = $(all[i]);
+
+                       if(self.itemManager().compareItems(item.data(CSS_SUGGESTION), suggestion))
+                       {
+                               target = item.addClass(CSS_SELECTED);
+                               break;
+                       }
+               }
+
+               target.addClass(CSS_SELECTED);
+               self.scrollSuggestionIntoView(target);
+       };
+
+       /**
+        * Returns the first suggestion HTML element from the dropdown that is highlighted as selected.
+        *
+        * @signature TextExtAutocomplete.selectedSuggestionElement()
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.selectedSuggestionElement
+        */
+       p.selectedSuggestionElement = function()
+       {
+               return this.suggestionElements().filter(CSS_DOT_SELECTED).first();
+       };
+
+       /**
+        * Returns `true` if dropdown is currently visible, `false` otherwise.
+        *
+        * @signature TextExtAutocomplete.isDropdownVisible()
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.isDropdownVisible
+        */
+       p.isDropdownVisible = function()
+       {
+               return this.containerElement().is(':visible') === true;
+       };
+
+       /**
+        * Reacts to the `getFormData` event triggered by the core. Returns data with the
+        * weight of 100 to be *less than the Tags plugin* data weight. The weights system is
+        * covered in greater detail in the [`getFormData`][1] event documentation.
+        *
+        * [1]: /manual/textext.html#getformdata
+        *
+        * @signature TextExtAutocomplete.onGetFormData(e, data, keyCode)
+        *
+        * @param e {Object} jQuery event.
+        * @param data {Object} Data object to be populated.
+        * @param keyCode {Number} Key code that triggered the original update request.
+        *
+        * @author agorbatchev
+        * @date 2011/08/22
+        * @id TextExtAutocomplete.onGetFormData
+        */
+       p.onGetFormData = function(e, data, keyCode)
+       {
+               var self       = this,
+                       val        = self.val(),
+                       inputValue = val,
+                       formValue  = val
+                       ;
+               data[100] = self.formDataObject(inputValue, formValue);
+       };
+
+       /**
+        * Returns initialization priority of the Autocomplete plugin which is expected to be
+        * *greater than the Tags plugin* because of the dependencies. The value is 200.
+        *
+        * @signature TextExtAutocomplete.initPriority()
+        *
+        * @author agorbatchev
+        * @date 2011/08/22
+        * @id TextExtAutocomplete.initPriority
+        */
+       p.initPriority = function()
+       {
+               return 200;
+       };
+
+       /**
+        * Reacts to the `hideDropdown` event and hides the dropdown if it's already visible.
+        *
+        * @signature TextExtAutocomplete.onHideDropdown(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.onHideDropdown
+        */
+       p.onHideDropdown = function(e)
+       {
+               this.hideDropdown();
+       };
+
+       /**
+        * Reacts to the 'toggleDropdown` event and shows or hides the dropdown depending if
+        * it's currently hidden or visible.
+        *
+        * @signature TextExtAutocomplete.onToggleDropdown(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/12/27
+        * @id TextExtAutocomplete.onToggleDropdown
+        * @version 1.1.0
+        */
+       p.onToggleDropdown = function(e)
+       {
+               var self = this;
+               self.trigger(self.containerElement().is(':visible') ? EVENT_HIDE_DROPDOWN : EVENT_SHOW_DROPDOWN);
+       };
+
+       /**
+        * Reacts to the `showDropdown` event and shows the dropdown if it's not already visible.
+        * It's possible to pass a render callback function which will be called instead of the
+        * default `TextExtAutocomplete.renderSuggestions()`.
+        *
+        * If no suggestion were previously loaded, it will fire `getSuggestions` event and exit.
+        *
+        * Here's how another plugin should trigger this event with the optional render callback:
+        *
+        *     this.trigger('showDropdown', function(autocomplete)
+        *     {
+        *         autocomplete.clearItems();
+        *         var node = autocomplete.addDropdownItem('<b>Item</b>');
+        *         node.addClass('new-look');
+        *     });
+        *
+        * @signature TextExtAutocomplete.onShowDropdown(e, renderCallback)
+        *
+        * @param e {Object} jQuery event.
+        * @param renderCallback {Function} Optional callback function which would be used to 
+        * render dropdown items. As a first argument, reference to the current instance of 
+        * Autocomplete plugin will be supplied. It's assumed, that if this callback is provided
+        * rendering will be handled completely manually.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.onShowDropdown
+        */
+       p.onShowDropdown = function(e, renderCallback)
+       {
+               var self        = this,
+                       current     = self.selectedSuggestionElement().data(CSS_SUGGESTION),
+                       suggestions = self._suggestions
+                       ;
+
+               if(!suggestions)
+                       return self.trigger(EVENT_GET_SUGGESTIONS);
+
+               if($.isFunction(renderCallback))
+               {
+                       renderCallback(self);
+               }
+               else
+               {
+                       self.renderSuggestions(self._suggestions);
+                       self.toggleNextSuggestion();
+               }
+               
+               self.showDropdown(self.containerElement());
+               self.setSelectedSuggestion(current);
+       };
+
+       /**
+        * Reacts to the `setSuggestions` event. Expects to recieve the payload as the second argument
+        * in the following structure:
+        *
+        *     {
+        *         result : [ "item1", "item2" ],
+        *         showHideDropdown : false
+        *     }
+        *
+        * Notice the optional `showHideDropdown` option. By default, ie without the `showHideDropdown` 
+        * value the method will trigger either `showDropdown` or `hideDropdown` depending if there are
+        * suggestions. If set to `false`, no event is triggered.
+        *
+        * @signature TextExtAutocomplete.onSetSuggestions(e, data)
+        *
+        * @param data {Object} Data payload.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.onSetSuggestions
+        */
+       p.onSetSuggestions = function(e, data)
+       {
+               var self        = this,
+                       suggestions = self._suggestions = data.result
+                       ;
+
+               if(data.showHideDropdown !== false)
+                       self.trigger(suggestions === null || suggestions.length === 0 ? EVENT_HIDE_DROPDOWN : EVENT_SHOW_DROPDOWN);
+       };
+
+       /**
+        * Prepears for and triggers the `getSuggestions` event with the `{ query : {String} }` as second
+        * argument.
+        *
+        * @signature TextExtAutocomplete.getSuggestions()
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.getSuggestions
+        */
+       p.getSuggestions = function()
+       {
+               var self = this,
+                       val  = self.val()
+                       ;
+
+               if(self._previousInputValue == val)
+                       return;
+
+               // if user clears input, then we want to select first suggestion
+               // instead of the last one
+               if(val == '')
+                       current = null;
+
+               self._previousInputValue = val;
+               self.trigger(EVENT_GET_SUGGESTIONS, { query : val });
+       };
+
+       /**
+        * Removes all HTML suggestion items from the dropdown.
+        *
+        * @signature TextExtAutocomplete.clearItems()
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.clearItems
+        */
+       p.clearItems = function()
+       {
+               this.containerElement().find('.text-list').children().remove();
+       };
+
+       /**
+        * Clears all and renders passed suggestions.
+        *
+        * @signature TextExtAutocomplete.renderSuggestions(suggestions)
+        *
+        * @param suggestions {Array} List of suggestions to render.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.renderSuggestions
+        */
+       p.renderSuggestions = function(suggestions)
+       {
+               var self = this;
+
+               self.clearItems();
+
+               $.each(suggestions || [], function(index, item)
+               {
+                       self.addSuggestion(item);
+               });
+       };
+
+       /**
+        * Shows the dropdown.
+        *
+        * @signature TextExtAutocomplete.showDropdown()
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.showDropdown
+        */
+       p.showDropdown = function()
+       {
+               this.containerElement().show();
+       };
+
+       /**
+        * Hides the dropdown.
+        *
+        * @signature TextExtAutocomplete.hideDropdown()
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.hideDropdown
+        */
+       p.hideDropdown = function()
+       {
+               var self     = this,
+                       dropdown = self.containerElement()
+                       ;
+
+               self._previousInputValue = null;
+               dropdown.hide();
+       };
+
+       /**
+        * Adds single suggestion to the bottom of the dropdown. Uses `ItemManager.itemToString()` to
+        * serialize provided suggestion to string.
+        *
+        * @signature TextExtAutocomplete.addSuggestion(suggestion)
+        *
+        * @param suggestion {Object} Suggestion item. By default expected to be a string.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.addSuggestion
+        */
+       p.addSuggestion = function(suggestion)
+       {
+               var self     = this,
+                       renderer = self.opts(OPT_RENDER),
+                       node     = self.addDropdownItem(renderer ? renderer.call(self, suggestion) : self.itemManager().itemToString(suggestion))
+                       ;
+
+               node.data(CSS_SUGGESTION, suggestion);
+       };
+
+       /**
+        * Adds and returns HTML node to the bottom of the dropdown.
+        *
+        * @signature TextExtAutocomplete.addDropdownItem(html)
+        *
+        * @param html {String} HTML to be inserted into the item.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.addDropdownItem
+        */
+       p.addDropdownItem = function(html)
+       {
+               var self      = this,
+                       container = self.containerElement().find('.text-list'),
+                       node      = $(self.opts(OPT_HTML_SUGGESTION))
+                       ;
+
+               node.find('.text-label').html(html);
+               container.append(node);
+               return node;
+       };
+
+       /**
+        * Removes selection highlight from all suggestion elements.
+        *
+        * @signature TextExtAutocomplete.clearSelected()
+        *
+        * @author agorbatchev
+        * @date 2011/08/02
+        * @id TextExtAutocomplete.clearSelected
+        */
+       p.clearSelected = function()
+       {
+               this.suggestionElements().removeClass(CSS_SELECTED);
+       };
+
+       /**
+        * Selects next suggestion relative to the current one. If there's no
+        * currently selected suggestion, it will select the first one. Selected
+        * suggestion will always be scrolled into view.
+        *
+        * @signature TextExtAutocomplete.toggleNextSuggestion()
+        *
+        * @author agorbatchev
+        * @date 2011/08/02
+        * @id TextExtAutocomplete.toggleNextSuggestion
+        */
+       p.toggleNextSuggestion = function()
+       {
+               var self     = this,
+                       selected = self.selectedSuggestionElement(),
+                       next
+                       ;
+
+               if(selected.length > 0)
+               {
+                       next = selected.next();
+
+                       if(next.length > 0)
+                               selected.removeClass(CSS_SELECTED);
+               }
+               else
+               {
+                       next = self.suggestionElements().first();
+               }
+
+               next.addClass(CSS_SELECTED);
+               self.scrollSuggestionIntoView(next);
+       };
+
+       /**
+        * Selects previous suggestion relative to the current one. Selected
+        * suggestion will always be scrolled into view.
+        *
+        * @signature TextExtAutocomplete.togglePreviousSuggestion()
+        *
+        * @author agorbatchev
+        * @date 2011/08/02
+        * @id TextExtAutocomplete.togglePreviousSuggestion
+        */
+       p.togglePreviousSuggestion = function()
+       {
+               var self     = this,
+                       selected = self.selectedSuggestionElement(),
+                       prev     = selected.prev()
+                       ;
+
+               if(prev.length == 0)
+                       return;
+
+               self.clearSelected();
+               prev.addClass(CSS_SELECTED);
+               self.scrollSuggestionIntoView(prev);
+       };
+
+       /**
+        * Scrolls specified HTML suggestion element into the view.
+        *
+        * @signature TextExtAutocomplete.scrollSuggestionIntoView(item)
+        *
+        * @param item {HTMLElement} jQuery HTML suggestion element which needs to
+        * scrolled into view.
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.scrollSuggestionIntoView
+        */
+       p.scrollSuggestionIntoView = function(item)
+       {
+               var itemHeight     = item.outerHeight(),
+                       dropdown       = this.containerElement(),
+                       dropdownHeight = dropdown.innerHeight(),
+                       scrollPos      = dropdown.scrollTop(),
+                       itemTop        = (item.position() || {}).top,
+                       scrollTo       = null,
+                       paddingTop     = parseInt(dropdown.css('paddingTop'))
+                       ;
+
+               if(itemTop == null)
+                       return;
+
+               // if scrolling down and item is below the bottom fold
+               if(itemTop + itemHeight > dropdownHeight)
+                       scrollTo = itemTop + scrollPos + itemHeight - dropdownHeight + paddingTop;
+
+               // if scrolling up and item is above the top fold
+               if(itemTop < 0)
+                       scrollTo = itemTop + scrollPos - paddingTop;
+
+               if(scrollTo != null)
+                       dropdown.scrollTop(scrollTo);
+       };
+
+       /**
+        * Uses the value from the text input to finish autocomplete action. Currently selected
+        * suggestion from the dropdown will be used to complete the action. Triggers `hideDropdown`
+        * event.
+        *
+        * @signature TextExtAutocomplete.selectFromDropdown()
+        *
+        * @author agorbatchev
+        * @date 2011/08/17
+        * @id TextExtAutocomplete.selectFromDropdown
+        */
+       p.selectFromDropdown = function()
+       {
+               var self       = this,
+                       suggestion = self.selectedSuggestionElement().data(CSS_SUGGESTION)
+                       ;
+
+               if(suggestion)
+               {
+                       self.val(self.itemManager().itemToString(suggestion));
+                       self.core().getFormData();      
+               }
+
+               self.trigger(EVENT_HIDE_DROPDOWN);
+       };
+       
+       /**
+        * Determines if the specified HTML element is within the TextExt core wrap HTML element.
+        *
+        * @signature TextExtAutocomplete.withinWrapElement(element)
+        *
+        * @param element {HTMLElement} element to check if contained by wrap element
+        *
+        * @author adamayres
+        * @version 1.3.0
+        * @date 2012/01/15
+        * @id TextExtAutocomplete.withinWrapElement
+        */
+       p.withinWrapElement = function(element) 
+       {
+               return this.core().wrapElement().find(element).size() > 0;
+       }
+})(jQuery);
+;/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.0
+ * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($)
+{
+       /**
+        * The Filter plugin introduces ability to limit input that the text field
+        * will accept. If the Tags plugin is used, Filter plugin will limit which
+        * tags it's possible to add.
+        *
+        * The list of allowed items can be either specified through the
+        * options, can come from the Suggestions plugin or be loaded by the Ajax 
+        * plugin. All these plugins have one thing in common -- they 
+        * trigger `setSuggestions` event which the Filter plugin is expecting.
+        * 
+        * @author agorbatchev
+        * @date 2011/08/18
+        * @id TextExtFilter
+        */
+       function TextExtFilter() {};
+
+       $.fn.textext.TextExtFilter = TextExtFilter;
+       $.fn.textext.addPlugin('filter', TextExtFilter);
+
+       var p = TextExtFilter.prototype,
+
+               /**
+                * Filter plugin options are grouped under `filter` when passed to the 
+                * `$().textext()` function. For example:
+                *
+                *     $('textarea').textext({
+                *         plugins: 'filter',
+                *         filter: {
+                *             items: [ "item1", "item2" ]
+                *         }
+                *     })
+                *
+                * @author agorbatchev
+                * @date 2011/08/18
+                * @id TextExtFilter.options
+                */
+               
+               /**
+                * This is a toggle switch to enable or disable the Filter plugin. The value is checked
+                * each time at the top level which allows you to toggle this setting on the fly.
+                *
+                * @name filter.enabled
+                * @default true
+                * @author agorbatchev
+                * @date 2011/08/18
+                * @id TextExtFilter.options.enabled
+                */
+               OPT_ENABLED = 'filter.enabled',
+
+               /**
+                * Arra of items that the Filter plugin will allow the Tag plugin to add to the list of
+                * its resut tags. Each item by default is expected to be a string which default `ItemManager`
+                * can work with. You can change the item type by supplying custom `ItemManager`.
+                *
+                * @name filter.items
+                * @default null
+                * @author agorbatchev
+                * @date 2011/08/18
+                * @id TextExtFilter.options.items
+                */
+               OPT_ITEMS = 'filter.items',
+
+               /**
+                * Filter plugin dispatches and reacts to the following events.
+                *
+                * @author agorbatchev
+                * @date 2011/08/18
+                * @id TextExtFilter.events
+                */
+
+               /**
+                * Filter plugin reacts to the `isTagAllowed` event triggered by the Tags plugin before
+                * adding a new tag to the list. If the new tag is among the `items` specified in options,
+                * then the new tag will be allowed.
+                *
+                * @name isTagAllowed
+                * @author agorbatchev
+                * @date 2011/08/18
+                * @id TextExtFilter.events.isTagAllowed
+                */
+
+               /**
+                * Filter plugin reacts to the `setSuggestions` event triggered by other plugins like 
+                * Suggestions and Ajax.
+                *
+                * However, event if this event is handled and items are passed with it and stored, if `items`
+                * option was supplied, it will always take precedense.
+                *
+                * @name setSuggestions
+                * @author agorbatchev
+                * @date 2011/08/18
+                * @id TextExtFilter.events.setSuggestions
+                */
+
+               DEFAULT_OPTS = {
+                       filter : {
+                               enabled : true,
+                               items : null
+                       }
+               }
+               ;
+
+       /**
+        * Initialization method called by the core during plugin instantiation.
+        *
+        * @signature TextExtFilter.init(core)
+        *
+        * @param core {TextExt} Instance of the TextExt core class.
+        *
+        * @author agorbatchev
+        * @date 2011/08/18
+        * @id TextExtFilter.init
+        */
+       p.init = function(core)
+       {
+               var self = this;
+               self.baseInit(core, DEFAULT_OPTS);
+
+               self.on({
+                       getFormData    : self.onGetFormData,
+                       isTagAllowed   : self.onIsTagAllowed,
+                       setSuggestions : self.onSetSuggestions
+               });
+
+               self._suggestions = null;
+       };
+
+       //--------------------------------------------------------------------------------
+       // Core functionality
+
+       /**
+        * Reacts to the [`getFormData`][1] event triggered by the core. Returns data with the
+        * weight of 200 to be *greater than the Autocomplete plugins* data weights. 
+        * The weights system is covered in greater detail in the [`getFormData`][1] event 
+        * documentation.
+        *
+        * This method does nothing if Tags tag is also present.
+        *
+        * [1]: /manual/textext.html#getformdata
+        *
+        * @signature TextExtFilter.onGetFormData(e, data, keyCode)
+        *
+        * @param e {Object} jQuery event.
+        * @param data {Object} Data object to be populated.
+        * @param keyCode {Number} Key code that triggered the original update request.
+        *
+        * @author agorbatchev
+        * @date 2011/12/28
+        * @id TextExtFilter.onGetFormData
+        * @version 1.1
+        */
+       p.onGetFormData = function(e, data, keyCode)
+       {
+               var self       = this,
+                       val        = self.val(),
+                       inputValue = val,
+                       formValue  = ''
+                       ;
+
+               if(!self.core().hasPlugin('tags'))
+               {
+                       if(self.isValueAllowed(inputValue))
+                               formValue = val;
+
+                       data[300] = self.formDataObject(inputValue, formValue);
+               }
+       };
+
+       /**
+        * Checks given value if it's present in `filterItems` or was loaded for the Autocomplete
+        * or by the Suggestions plugins. `value` is compared to each item using `ItemManager.compareItems`
+        * method which is currently attached to the core. Returns `true` if value is known or
+        * Filter plugin is disabled.
+        *
+        * @signature TextExtFilter.isValueAllowed(value)
+        *
+        * @param value {Object} Value to check.
+        *
+        * @author agorbatchev
+        * @date 2011/12/28
+        * @id TextExtFilter.isValueAllowed
+        * @version 1.1
+        */
+       p.isValueAllowed = function(value)
+       {
+               var self        = this,
+                       list        = self.opts('filterItems') || self._suggestions || [],
+                       itemManager = self.itemManager(),
+                       result      = !self.opts(OPT_ENABLED), // if disabled, should just return true
+                       i
+                       ;
+
+               for(i = 0; i < list.length && !result; i++)
+                       if(itemManager.compareItems(value, list[i]))
+                               result = true;
+
+               return result;
+       };
+
+       /**
+        * Handles `isTagAllowed` event dispatched by the Tags plugin. If supplied tag is not
+        * in the `items` list, method sets `result` on the `data` argument to `false`.
+        *
+        * @signature TextExtFilter.onIsTagAllowed(e, data)
+        *
+        * @param e {Object} jQuery event.
+        * @param data {Object} Payload in the following format : `{ tag : {Object}, result : {Boolean} }`.
+        * @author agorbatchev
+        * @date 2011/08/04
+        * @id TextExtFilter.onIsTagAllowed
+        */
+       p.onIsTagAllowed = function(e, data)
+       {
+               data.result = this.isValueAllowed(data.tag);
+       };
+
+       /**
+        * Reacts to the `setSuggestions` events and stores supplied suggestions for future use.
+        * 
+        * @signature TextExtFilter.onSetSuggestions(e, data)
+        *
+        * @param e {Object} jQuery event.
+        * @param data {Object} Payload in the following format : `{ result : {Array} } }`.
+        * @author agorbatchev
+        * @date 2011/08/18
+        * @id TextExtFilter.onSetSuggestions
+        */
+       p.onSetSuggestions = function(e, data)
+       {
+               this._suggestions = data.result;
+       };
+})(jQuery);
+;/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.0
+ * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($)
+{
+       /**
+        * Focus plugin displays a visual effect whenever user sets focus
+        * into the text area.
+        *
+        * @author agorbatchev
+        * @date 2011/08/18
+        * @id TextExtFocus
+        */
+       function TextExtFocus() {};
+
+       $.fn.textext.TextExtFocus = TextExtFocus;
+       $.fn.textext.addPlugin('focus', TextExtFocus);
+
+       var p = TextExtFocus.prototype,
+               /**
+                * Focus plugin only has one option and that is its HTML template. It could be 
+                * changed when passed to the `$().textext()` function. For example:
+                *
+                *     $('textarea').textext({
+                *         plugins: 'focus',
+                *         html: {
+                *             focus: "<span/>"
+                *         }
+                *     })
+                *
+                * @author agorbatchev
+                * @date 2011/08/18
+                * @id TextExtFocus.options
+                */
+               
+               /**
+                * HTML source that is used to generate markup required for the focus effect.
+                *
+                * @name html.focus
+                * @default '<div class="text-focus"/>'
+                * @author agorbatchev
+                * @date 2011/08/18
+                * @id TextExtFocus.options.html.focus
+                */
+               OPT_HTML_FOCUS = 'html.focus',
+
+               /**
+                * Focus plugin dispatches or reacts to the following events.
+                *
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExtFocus.events
+                */
+
+               /**
+                * Focus plugin reacts to the `focus` event and shows the markup generated from
+                * the `html.focus` option.
+                *
+                * @name focus
+                * @author agorbatchev
+                * @date 2011/08/18
+                * @id TextExtFocus.events.focus
+                */
+
+               /**
+                * Focus plugin reacts to the `blur` event and hides the effect.
+                *
+                * @name blur
+                * @author agorbatchev
+                * @date 2011/08/18
+                * @id TextExtFocus.events.blur
+                */
+
+               DEFAULT_OPTS = {
+                       html : {
+                               focus : '<div class="text-focus"/>'
+                       }
+               }
+               ;
+
+       /**
+        * Initialization method called by the core during plugin instantiation.
+        *
+        * @signature TextExtFocus.init(core)
+        *
+        * @param core {TextExt} Instance of the TextExt core class.
+        *
+        * @author agorbatchev
+        * @date 2011/08/18
+        * @id TextExtFocus.init
+        */
+       p.init = function(core)
+       {
+               var self = this;
+
+               self.baseInit(core, DEFAULT_OPTS);
+               self.core().wrapElement().append(self.opts(OPT_HTML_FOCUS));
+               self.on({
+                       blur  : self.onBlur,
+                       focus : self.onFocus
+               });
+
+               self._timeoutId = 0;
+       };
+
+       //--------------------------------------------------------------------------------
+       // Event handlers
+       
+       /**
+        * Reacts to the `blur` event and hides the focus effect with a slight delay which 
+        * allows quick refocusing without effect blinking in and out.
+        *
+        * @signature TextExtFocus.onBlur(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/08
+        * @id TextExtFocus.onBlur
+        */
+       p.onBlur = function(e)
+       {
+               var self = this;
+
+               clearTimeout(self._timeoutId);
+
+               self._timeoutId = setTimeout(function()
+               {
+                       self.getFocus().hide();
+               },
+               100);
+       };
+
+       /**
+        * Reacts to the `focus` event and shows the focus effect.
+        *
+        * @signature TextExtFocus.onFocus
+        *
+        * @param e {Object} jQuery event.
+        * @author agorbatchev
+        * @date 2011/08/08
+        * @id TextExtFocus.onFocus
+        */
+       p.onFocus = function(e)
+       {
+               var self = this;
+
+               clearTimeout(self._timeoutId);
+               
+               self.getFocus().show();
+       };
+       
+       //--------------------------------------------------------------------------------
+       // Core functionality
+
+       /**
+        * Returns focus effect HTML element.
+        *
+        * @signature TextExtFocus.getFocus()
+        *
+        * @author agorbatchev
+        * @date 2011/08/08
+        * @id TextExtFocus.getFocus
+        */
+       p.getFocus = function()
+       {
+               return this.core().wrapElement().find('.text-focus');
+       };
+})(jQuery);
+;/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.0
+ * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($)
+{
+       /**
+        * Prompt plugin displays a visual user propmpt in the text input area. If user focuses
+        * on the input, the propt is hidden and only shown again when user focuses on another
+        * element and text input doesn't have a value.
+        *
+        * @author agorbatchev
+        * @date 2011/08/18
+        * @id TextExtPrompt
+        */
+       function TextExtPrompt() {};
+
+       $.fn.textext.TextExtPrompt = TextExtPrompt;
+       $.fn.textext.addPlugin('prompt', TextExtPrompt);
+
+       var p = TextExtPrompt.prototype,
+
+               CSS_HIDE_PROMPT = 'text-hide-prompt',
+
+               /**
+                * Prompt plugin has options to change the prompt label and its HTML template. The options
+                * could be changed when passed to the `$().textext()` function. For example:
+                *
+                *     $('textarea').textext({
+                *         plugins: 'prompt',
+                *         prompt: 'Your email address'
+                *     })
+                *
+                * @author agorbatchev
+                * @date 2011/08/18
+                * @id TextExtPrompt.options
+                */
+
+               /**
+                * Prompt message that is displayed to the user whenever there's no value in the input.
+                *
+                * @name prompt
+                * @default 'Awaiting input...'
+                * @author agorbatchev
+                * @date 2011/08/18
+                * @id TextExtPrompt.options.prompt
+                */
+               OPT_PROMPT = 'prompt',
+
+               /**
+                * HTML source that is used to generate markup required for the prompt effect.
+                *
+                * @name html.prompt
+                * @default '<div class="text-prompt"/>'
+                * @author agorbatchev
+                * @date 2011/08/18
+                * @id TextExtPrompt.options.html.prompt
+                */
+               OPT_HTML_PROMPT = 'html.prompt',
+
+               /**
+                * Prompt plugin dispatches or reacts to the following events.
+                * @id TextExtPrompt.events
+                */
+
+               /**
+                * Prompt plugin reacts to the `focus` event and hides the markup generated from
+                * the `html.prompt` option.
+                *
+                * @name focus
+                * @author agorbatchev
+                * @date 2011/08/18
+                * @id TextExtPrompt.events.focus
+                */
+
+               /**
+                * Prompt plugin reacts to the `blur` event and shows the prompt back if user
+                * hasn't entered any value.
+                *
+                * @name blur
+                * @author agorbatchev
+                * @date 2011/08/18
+                * @id TextExtPrompt.events.blur
+                */
+       
+               DEFAULT_OPTS = {
+                       prompt : 'Awaiting input...',
+
+                       html : {
+                               prompt : '<div class="text-prompt"/>'
+                       }
+               }
+               ;
+
+       /**
+        * Initialization method called by the core during plugin instantiation.
+        *
+        * @signature TextExtPrompt.init(core)
+        *
+        * @param core {TextExt} Instance of the TextExt core class.
+        *
+        * @author agorbatchev
+        * @date 2011/08/18
+        * @id TextExtPrompt.init
+        */
+       p.init = function(core)
+       {
+               var self           = this,
+                       placeholderKey = 'placeholder',
+                       container,
+                       prompt
+                       ;
+
+               self.baseInit(core, DEFAULT_OPTS);
+               
+               container = $(self.opts(OPT_HTML_PROMPT));
+               $(self).data('container', container);
+
+               self.core().wrapElement().append(container);
+               self.setPrompt(self.opts(OPT_PROMPT));
+               
+               prompt = core.input().attr(placeholderKey);
+
+               if(!prompt)
+                       prompt = self.opts(OPT_PROMPT);
+
+               // clear placeholder attribute if set
+               core.input().attr(placeholderKey, '');
+
+               if(prompt)
+                       self.setPrompt(prompt);
+
+               if($.trim(self.val()).length > 0)
+                       self.hidePrompt();
+
+               self.on({
+                       blur           : self.onBlur,
+                       focus          : self.onFocus,
+                       postInvalidate : self.onPostInvalidate,
+                       postInit       : self.onPostInit
+               });
+       };
+
+       //--------------------------------------------------------------------------------
+       // Event handlers
+       
+       /**
+        * Reacts to the `postInit` and configures the plugin for initial display.
+        *
+        * @signature TextExtPrompt.onPostInit(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/24
+        * @id TextExtPrompt.onPostInit
+        */
+       p.onPostInit = function(e)
+       {
+               this.invalidateBounds();
+       };
+
+       /**
+        * Reacts to the `postInvalidate` and insures that prompt display remains correct.
+        *
+        * @signature TextExtPrompt.onPostInvalidate(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/24
+        * @id TextExtPrompt.onPostInvalidate
+        */
+       p.onPostInvalidate = function(e)
+       {
+               this.invalidateBounds();
+       };
+
+       /**
+        * Repositions the prompt to make sure it's always at the same place as in the text input carret.
+        *
+        * @signature TextExtPrompt.invalidateBounds()
+        *
+        * @author agorbatchev
+        * @date 2011/08/24
+        * @id TextExtPrompt.invalidateBounds
+        */
+       p.invalidateBounds = function()
+       {
+               var self  = this,
+                       input = self.input()
+                       ;
+
+               self.containerElement().css({
+                       paddingLeft : input.css('paddingLeft'),
+                       paddingTop  : input.css('paddingTop')
+               });
+       };
+
+       /**
+        * Reacts to the `blur` event and shows the prompt effect with a slight delay which 
+        * allows quick refocusing without effect blinking in and out.
+        *
+        * The prompt is restored if the text box has no value.
+        *
+        * @signature TextExtPrompt.onBlur(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/08
+        * @id TextExtPrompt.onBlur
+        */
+       p.onBlur = function(e)
+       {
+               var self = this;
+
+               self.startTimer('prompt', 0.1, function()
+               {
+                       self.showPrompt();
+               });
+       };
+
+       /**
+        * Shows prompt HTML element.
+        *
+        * @signature TextExtPrompt.showPrompt()
+        *
+        * @author agorbatchev
+        * @date 2011/08/22
+        * @id TextExtPrompt.showPrompt
+        */
+       p.showPrompt = function()
+       {
+               var self     = this,
+                       input    = self.input()
+                       ;
+               
+               if($.trim(self.val()).length === 0 && !input.is(':focus'))
+                       self.containerElement().removeClass(CSS_HIDE_PROMPT);
+       };
+
+       /**
+        * Hides prompt HTML element.
+        *
+        * @signature TextExtPrompt.hidePrompt()
+        *
+        * @author agorbatchev
+        * @date 2011/08/22
+        * @id TextExtPrompt.hidePrompt
+        */
+       p.hidePrompt = function()
+       {
+               this.stopTimer('prompt');
+               this.containerElement().addClass(CSS_HIDE_PROMPT);
+       };
+
+       /**
+        * Reacts to the `focus` event and hides the prompt effect.
+        *
+        * @signature TextExtPrompt.onFocus
+        *
+        * @param e {Object} jQuery event.
+        * @author agorbatchev
+        * @date 2011/08/08
+        * @id TextExtPrompt.onFocus
+        */
+       p.onFocus = function(e)
+       {
+               this.hidePrompt();
+       };
+       
+       //--------------------------------------------------------------------------------
+       // Core functionality
+
+       /**
+        * Sets the prompt display to the specified string.
+        *
+        * @signature TextExtPrompt.setPrompt(str)
+        *
+        * @oaram str {String} String that will be displayed in the prompt.
+        *
+        * @author agorbatchev
+        * @date 2011/08/18
+        * @id TextExtPrompt.setPrompt
+        */
+       p.setPrompt = function(str)
+       {
+               this.containerElement().text(str);
+       };
+
+       /**
+        * Returns prompt effect HTML element.
+        *
+        * @signature TextExtPrompt.containerElement()
+        *
+        * @author agorbatchev
+        * @date 2011/08/08
+        * @id TextExtPrompt.containerElement
+        */
+       p.containerElement = function()
+       {
+               return $(this).data('container');
+       };
+})(jQuery);
+;/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.0
+ * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($)
+{
+       /**
+        * Suggestions plugin allows to easily specify the list of suggestion items that the
+        * Autocomplete plugin would present to the user.
+        *
+        * @author agorbatchev
+        * @date 2011/08/18
+        * @id TextExtSuggestions
+        */
+       function TextExtSuggestions() {};
+
+       $.fn.textext.TextExtSuggestions = TextExtSuggestions;
+       $.fn.textext.addPlugin('suggestions', TextExtSuggestions);
+
+       var p = TextExtSuggestions.prototype,
+               /**
+                * Suggestions plugin only has one option and that is to set suggestion items. It could be 
+                * changed when passed to the `$().textext()` function. For example:
+                *
+                *     $('textarea').textext({
+                *         plugins: 'suggestions',
+                *         suggestions: [ "item1", "item2" ]
+                *     })
+                *
+                * @author agorbatchev
+                * @date 2011/08/18
+                * @id TextExtSuggestions.options
+                */
+
+               /**
+                * List of items that Autocomplete plugin would display in the dropdown.
+                *
+                * @name suggestions
+                * @default null
+                * @author agorbatchev
+                * @date 2011/08/18
+                * @id TextExtSuggestions.options.suggestions
+                */
+               OPT_SUGGESTIONS = 'suggestions',
+
+               /**
+                * Suggestions plugin dispatches or reacts to the following events.
+                *
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExtSuggestions.events
+                */
+
+               /**
+                * Suggestions plugin reacts to the `getSuggestions` event and returns `suggestions` items
+                * from the options.
+                *
+                * @name getSuggestions
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExtSuggestions.events.getSuggestions
+                */
+
+               /**
+                * Suggestions plugin triggers the `setSuggestions` event to pass its own list of `Suggestions`
+                * to the Autocomplete plugin.
+                *
+                * @name setSuggestions
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExtSuggestions.events.setSuggestions
+                */
+
+               /**
+                * Suggestions plugin reacts to the `postInit` event to pass its list of `suggestions` to the
+                * Autocomplete right away.
+                *
+                * @name postInit
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExtSuggestions.events.postInit
+                */
+
+               DEFAULT_OPTS = {
+                       suggestions : null
+               }
+               ;
+
+       /**
+        * Initialization method called by the core during plugin instantiation.
+        *
+        * @signature TextExtSuggestions.init(core)
+        *
+        * @param core {TextExt} Instance of the TextExt core class.
+        *
+        * @author agorbatchev
+        * @date 2011/08/18
+        * @id TextExtSuggestions.init
+        */
+       p.init = function(core)
+       {
+               var self = this;
+
+               self.baseInit(core, DEFAULT_OPTS);
+
+               self.on({
+                       getSuggestions : self.onGetSuggestions,
+                       postInit       : self.onPostInit
+               });
+       };
+
+       /**
+        * Triggers `setSuggestions` and passes supplied suggestions to the Autocomplete plugin.
+        *
+        * @signature TextExtSuggestions.setSuggestions(suggestions, showHideDropdown)
+        *
+        * @param suggestions {Array} List of suggestions. With the default `ItemManager` it should
+        * be a list of strings.
+        * @param showHideDropdown {Boolean} If it's undesirable to show the dropdown right after
+        * suggestions are set, `false` should be passed for this argument.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtSuggestions.setSuggestions
+        */
+       p.setSuggestions = function(suggestions, showHideDropdown)
+       {
+               this.trigger('setSuggestions', { result : suggestions, showHideDropdown : showHideDropdown != false });
+       };
+
+       /**
+        * Reacts to the `postInit` event and triggers `setSuggestions` event to set suggestions list 
+        * right after initialization.
+        *
+        * @signature TextExtSuggestions.onPostInit(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtSuggestions.onPostInit
+        */
+       p.onPostInit = function(e)
+       {
+               var self = this;
+               self.setSuggestions(self.opts(OPT_SUGGESTIONS), false);
+       };
+
+       /**
+        * Reacts to the `getSuggestions` event and triggers `setSuggestions` event with the list
+        * of `suggestions` specified in the options.
+        *
+        * @signature TextExtSuggestions.onGetSuggestions(e, data)
+        *
+        * @param e {Object} jQuery event.
+        * @param data {Object} Payload from the `getSuggestions` event with the user query, eg `{ query: {String} }`.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtSuggestions.onGetSuggestions
+        */
+       p.onGetSuggestions = function(e, data)
+       {
+               var self        = this,
+                       suggestions = self.opts(OPT_SUGGESTIONS)
+                       ;
+
+               suggestions.sort();
+               self.setSuggestions(self.itemManager().filter(suggestions, data.query));
+       };
+})(jQuery);
+;/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.0
+ * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($)
+{
+       /**
+        * Tags plugin brings in the traditional tag functionality where user can assemble and
+        * edit list of tags. Tags plugin works especially well together with Autocomplete, Filter,
+        * Suggestions and Ajax plugins to provide full spectrum of features. It can also work on
+        * its own and just do one thing -- tags.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtTags
+        */
+       function TextExtTags() {};
+
+       $.fn.textext.TextExtTags = TextExtTags;
+       $.fn.textext.addPlugin('tags', TextExtTags);
+
+       var p = TextExtTags.prototype,
+
+               CSS_DOT             = '.',
+               CSS_TAGS_ON_TOP     = 'text-tags-on-top',
+               CSS_DOT_TAGS_ON_TOP = CSS_DOT + CSS_TAGS_ON_TOP,
+               CSS_TAG             = 'text-tag',
+               CSS_DOT_TAG         = CSS_DOT + CSS_TAG,
+               CSS_TAGS            = 'text-tags',
+               CSS_DOT_TAGS        = CSS_DOT + CSS_TAGS,
+               CSS_LABEL           = 'text-label',
+               CSS_DOT_LABEL       = CSS_DOT + CSS_LABEL,
+               CSS_REMOVE          = 'text-remove',
+               CSS_DOT_REMOVE      = CSS_DOT + CSS_REMOVE,
+
+               /**
+                * Tags plugin options are grouped under `tags` when passed to the
+                * `$().textext()` function. For example:
+                *
+                *     $('textarea').textext({
+                *         plugins: 'tags',
+                *         tags: {
+                *             items: [ "tag1", "tag2" ]
+                *         }
+                *     })
+                *
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExtTags.options
+                */
+
+               /**
+                * This is a toggle switch to enable or disable the Tags plugin. The value is checked
+                * each time at the top level which allows you to toggle this setting on the fly.
+                *
+                * @name tags.enabled
+                * @default true
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExtTags.options.tags.enabled
+                */
+               OPT_ENABLED = 'tags.enabled',
+
+               /**
+                * Allows to specify tags which will be added to the input by default upon initialization.
+                * Each item in the array must be of the type that current `ItemManager` can understand.
+                * Default type is `String`.
+                *
+                * @name tags.items
+                * @default null
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExtTags.options.tags.items
+                */
+               OPT_ITEMS = 'tags.items',
+
+               /**
+                * HTML source that is used to generate a single tag.
+                *
+                * @name html.tag
+                * @default '<div class="text-tags"/>'
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExtTags.options.html.tag
+                */
+               OPT_HTML_TAG  = 'html.tag',
+
+               /**
+                * HTML source that is used to generate container for the tags.
+                *
+                * @name html.tags
+                * @default '<div class="text-tag"><div class="text-button"><span class="text-label"/><a class="text-remove"/></div></div>'
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExtTags.options.html.tags
+                */
+               OPT_HTML_TAGS = 'html.tags',
+
+               /**
+                * Tags plugin dispatches or reacts to the following events.
+                *
+                * @author agorbatchev
+                * @date 2011/08/17
+                * @id TextExtTags.events
+                */
+
+               /**
+                * Tags plugin triggers the `isTagAllowed` event before adding each tag to the tag list. Other plugins have
+                * an opportunity to interrupt this by setting `result` of the second argument to `false`. For example:
+                *
+                *     $('textarea').textext({...}).bind('isTagAllowed', function(e, data)
+                *     {
+                *         if(data.tag === 'foo')
+                *             data.result = false;
+                *     })
+                *
+                * The second argument `data` has the following format: `{ tag : {Object}, result : {Boolean} }`. `tag`
+                * property is in the format that the current `ItemManager` can understand.
+                *
+                * @name isTagAllowed
+                * @author agorbatchev
+                * @date 2011/08/19
+                * @id TextExtTags.events.isTagAllowed
+                */
+               EVENT_IS_TAG_ALLOWED = 'isTagAllowed',
+
+               /**
+                * Tags plugin triggers the `tagClick` event when user clicks on one of the tags. This allows to process
+                * the click and potentially change the value of the tag (for example in case of user feedback).
+                *
+                *     $('textarea').textext({...}).bind('tagClick', function(e, tag, value, callback)
+                *     {
+                *         var newValue = window.prompt('New value', value);
+
+                *         if(newValue)
+                *             callback(newValue, true);
+                *     })
+                *
+                *  Callback argument has the following signature:
+                *
+                *     function(newValue, refocus)
+                *     {
+                *         ...
+                *     }
+                *
+                * Please check out [example](/manual/examples/tags-changing.html).
+                *
+                * @name tagClick
+                * @version 1.3.0
+                * @author s.stok
+                * @date 2011/01/23
+                * @id TextExtTags.events.tagClick
+                */
+               EVENT_TAG_CLICK = 'tagClick',
+
+               DEFAULT_OPTS = {
+                       tags : {
+                               enabled : true,
+                               items   : null
+                       },
+
+                       html : {
+                               tags : '<div class="text-tags"/>',
+                               tag  : '<div class="text-tag"><div class="text-button"><span class="text-label"/><a class="text-remove"/></div></div>'
+                       }
+               }
+               ;
+
+       /**
+        * Initialization method called by the core during plugin instantiation.
+        *
+        * @signature TextExtTags.init(core)
+        *
+        * @param core {TextExt} Instance of the TextExt core class.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtTags.init
+        */
+       p.init = function(core)
+       {
+               this.baseInit(core, DEFAULT_OPTS);
+
+               var self  = this,
+                       input = self.input(),
+                       container
+                       ;
+
+               if(self.opts(OPT_ENABLED))
+               {
+                       container = $(self.opts(OPT_HTML_TAGS));
+                       input.after(container);
+
+                       $(self).data('container', container);
+
+                       self.on({
+                               enterKeyPress    : self.onEnterKeyPress,
+                               backspaceKeyDown : self.onBackspaceKeyDown,
+                               preInvalidate    : self.onPreInvalidate,
+                               postInit         : self.onPostInit,
+                               getFormData      : self.onGetFormData
+                       });
+
+                       self.on(container, {
+                               click     : self.onClick,
+                               mousemove : self.onContainerMouseMove
+                       });
+
+                       self.on(input, {
+                               mousemove : self.onInputMouseMove
+                       });
+               }
+
+               self._originalPadding = {
+                       left : parseInt(input.css('paddingLeft') || 0),
+                       top  : parseInt(input.css('paddingTop') || 0)
+               };
+
+               self._paddingBox = {
+                       left : 0,
+                       top  : 0
+               };
+
+               self.updateFormCache();
+       };
+
+       /**
+        * Returns HTML element in which all tag HTML elements are residing.
+        *
+        * @signature TextExtTags.containerElement()
+        *
+        * @author agorbatchev
+        * @date 2011/08/15
+        * @id TextExtTags.containerElement
+        */
+       p.containerElement = function()
+       {
+               return $(this).data('container');
+       };
+
+       //--------------------------------------------------------------------------------
+       // Event handlers
+
+       /**
+        * Reacts to the `postInit` event triggered by the core and sets default tags
+        * if any were specified.
+        *
+        * @signature TextExtTags.onPostInit(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/09
+        * @id TextExtTags.onPostInit
+        */
+       p.onPostInit = function(e)
+       {
+               var self = this;
+               self.addTags(self.opts(OPT_ITEMS));
+       };
+
+       /**
+        * Reacts to the [`getFormData`][1] event triggered by the core. Returns data with the
+        * weight of 200 to be *greater than the Autocomplete plugin* data weight. The weights
+        * system is covered in greater detail in the [`getFormData`][1] event documentation.
+        *
+        * [1]: /manual/textext.html#getformdata
+        *
+        * @signature TextExtTags.onGetFormData(e, data, keyCode)
+        *
+        * @param e {Object} jQuery event.
+        * @param data {Object} Data object to be populated.
+        * @param keyCode {Number} Key code that triggered the original update request.
+        *
+        * @author agorbatchev
+        * @date 2011/08/22
+        * @id TextExtTags.onGetFormData
+        */
+       p.onGetFormData = function(e, data, keyCode)
+       {
+               var self       = this,
+                       inputValue = keyCode === 13 ? '' : self.val(),
+                       formValue  = self._formData
+                       ;
+
+               data[200] = self.formDataObject(inputValue, formValue);
+       };
+
+       /**
+        * Returns initialization priority of the Tags plugin which is expected to be
+        * *less than the Autocomplete plugin* because of the dependencies. The value is
+        * 100.
+        *
+        * @signature TextExtTags.initPriority()
+        *
+        * @author agorbatchev
+        * @date 2011/08/22
+        * @id TextExtTags.initPriority
+        */
+       p.initPriority = function()
+       {
+               return 100;
+       };
+
+       /**
+        * Reacts to user moving mouse over the text area when cursor is over the text
+        * and not over the tags. Whenever mouse cursor is over the area covered by
+        * tags, the tags container is flipped to be on top of the text area which
+        * makes all tags functional with the mouse.
+        *
+        * @signature TextExtTags.onInputMouseMove(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/08
+        * @id TextExtTags.onInputMouseMove
+        */
+       p.onInputMouseMove = function(e)
+       {
+               this.toggleZIndex(e);
+       };
+
+       /**
+        * Reacts to user moving mouse over the tags. Whenever the cursor moves out
+        * of the tags and back into where the text input is happening visually,
+        * the tags container is sent back under the text area which allows user
+        * to interact with the text using mouse cursor as expected.
+        *
+        * @signature TextExtTags.onContainerMouseMove(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/08
+        * @id TextExtTags.onContainerMouseMove
+        */
+       p.onContainerMouseMove = function(e)
+       {
+               this.toggleZIndex(e);
+       };
+
+       /**
+        * Reacts to the `backspaceKeyDown` event. When backspace key is pressed in an empty text field,
+        * deletes last tag from the list.
+        *
+        * @signature TextExtTags.onBackspaceKeyDown(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/02
+        * @id TextExtTags.onBackspaceKeyDown
+        */
+       p.onBackspaceKeyDown = function(e)
+       {
+               var self    = this,
+                       lastTag = self.tagElements().last()
+                       ;
+
+               if(self.val().length == 0)
+                       self.removeTag(lastTag);
+       };
+
+       /**
+        * Reacts to the `preInvalidate` event and updates the input box to look like the tags are
+        * positioned inside it.
+        *
+        * @signature TextExtTags.onPreInvalidate(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtTags.onPreInvalidate
+        */
+       p.onPreInvalidate = function(e)
+       {
+               var self    = this,
+                       lastTag = self.tagElements().last(),
+                       pos     = lastTag.position()
+                       ;
+
+               if(lastTag.length > 0)
+                       pos.left += lastTag.innerWidth();
+               else
+                       pos = self._originalPadding;
+
+               self._paddingBox = pos;
+
+               self.input().css({
+                       paddingLeft : pos.left,
+                       paddingTop  : pos.top
+               });
+       };
+
+       /**
+        * Reacts to the mouse `click` event.
+        *
+        * @signature TextExtTags.onClick(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtTags.onClick
+        */
+       p.onClick = function(e)
+       {
+               var self   = this,
+                       core   = self.core(),
+                       source = $(e.target),
+                       focus  = 0,
+                       tag
+                       ;
+
+               if(source.is(CSS_DOT_TAGS))
+               {
+                       focus = 1;
+               }
+               else if(source.is(CSS_DOT_REMOVE))
+               {
+                       self.removeTag(source.parents(CSS_DOT_TAG + ':first'));
+                       focus = 1;
+               }
+               else if(source.is(CSS_DOT_LABEL))
+               {
+                       tag = source.parents(CSS_DOT_TAG + ':first');
+                       self.trigger(EVENT_TAG_CLICK, tag, tag.data(CSS_TAG), tagClickCallback);
+               }
+
+               function tagClickCallback(newValue, refocus)
+               {
+                       tag.data(CSS_TAG, newValue);
+                       tag.find(CSS_DOT_LABEL).text(self.itemManager().itemToString(newValue));
+
+                       self.updateFormCache();
+                       core.getFormData();
+                       core.invalidateBounds();
+
+                       if(refocus)
+                               core.focusInput();
+               }
+
+               if(focus)
+                       core.focusInput();
+       };
+
+       /**
+        * Reacts to the `enterKeyPress` event and adds whatever is currently in the text input
+        * as a new tag. Triggers `isTagAllowed` to check if the tag could be added first.
+        *
+        * @signature TextExtTags.onEnterKeyPress(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtTags.onEnterKeyPress
+        */
+       p.onEnterKeyPress = function(e)
+       {
+               var self = this,
+                       val  = self.val(),
+                       tag  = self.itemManager().stringToItem(val)
+                       ;
+
+               if(self.isTagAllowed(tag))
+               {
+                       self.addTags([ tag ]);
+                       // refocus the textarea just in case it lost the focus
+                       self.core().focusInput();
+               }
+       };
+
+       //--------------------------------------------------------------------------------
+       // Core functionality
+
+       /**
+        * Creates a cache object with all the tags currently added which will be returned
+        * in the `onGetFormData` handler.
+        *
+        * @signature TextExtTags.updateFormCache()
+        *
+        * @author agorbatchev
+        * @date 2011/08/09
+        * @id TextExtTags.updateFormCache
+        */
+       p.updateFormCache = function()
+       {
+               var self   = this,
+                       result = []
+                       ;
+
+               self.tagElements().each(function()
+               {
+                       result.push($(this).data(CSS_TAG));
+               });
+
+               // cache the results to be used in the onGetFormData
+               self._formData = result;
+       };
+
+       /**
+        * Toggles tag container to be on top of the text area or under based on where
+        * the mouse cursor is located. When cursor is above the text input and out of
+        * any of the tags, the tags container is sent under the text area. If cursor
+        * is over any of the tags, the tag container is brought to be over the text
+        * area.
+        *
+        * @signature TextExtTags.toggleZIndex(e)
+        *
+        * @param e {Object} jQuery event.
+        *
+        * @author agorbatchev
+        * @date 2011/08/08
+        * @id TextExtTags.toggleZIndex
+        */
+       p.toggleZIndex = function(e)
+       {
+               var self            = this,
+                       offset          = self.input().offset(),
+                       mouseX          = e.clientX - offset.left,
+                       mouseY          = e.clientY - offset.top,
+                       box             = self._paddingBox,
+                       container       = self.containerElement(),
+                       isOnTop         = container.is(CSS_DOT_TAGS_ON_TOP),
+                       isMouseOverText = mouseX > box.left && mouseY > box.top
+                       ;
+
+               if(!isOnTop && !isMouseOverText || isOnTop && isMouseOverText)
+                       container[(!isOnTop ? 'add' : 'remove') + 'Class'](CSS_TAGS_ON_TOP);
+       };
+
+       /**
+        * Returns all tag HTML elements.
+        *
+        * @signature TextExtTags.tagElements()
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtTags.tagElements
+        */
+       p.tagElements = function()
+       {
+               return this.containerElement().find(CSS_DOT_TAG);
+       };
+
+       /**
+        * Wrapper around the `isTagAllowed` event which triggers it and returns `true`
+        * if `result` property of the second argument remains `true`.
+        *
+        * @signature TextExtTags.isTagAllowed(tag)
+        *
+        * @param tag {Object} Tag object that the current `ItemManager` can understand.
+        * Default is `String`.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtTags.isTagAllowed
+        */
+       p.isTagAllowed = function(tag)
+       {
+               var opts = { tag : tag, result : true };
+               this.trigger(EVENT_IS_TAG_ALLOWED, opts);
+               return opts.result === true;
+       };
+
+       /**
+        * Adds specified tags to the tag list. Triggers `isTagAllowed` event for each tag
+        * to insure that it could be added. Calls `TextExt.getFormData()` to refresh the data.
+        *
+        * @signature TextExtTags.addTags(tags)
+        *
+        * @param tags {Array} List of tags that current `ItemManager` can understand. Default
+        * is `String`.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtTags.addTags
+        */
+       p.addTags = function(tags)
+       {
+               if(!tags || tags.length == 0)
+                       return;
+
+               var self      = this,
+                       core      = self.core(),
+                       container = self.containerElement(),
+                       i, tag
+                       ;
+
+               for(i = 0; i < tags.length; i++)
+               {
+                       tag = tags[i];
+
+                       if(tag && self.isTagAllowed(tag))
+                               container.append(self.renderTag(tag));
+               }
+
+               self.updateFormCache();
+               core.getFormData();
+               core.invalidateBounds();
+       };
+
+       /**
+        * Returns HTML element for the specified tag.
+        *
+        * @signature TextExtTags.getTagElement(tag)
+        *
+        * @param tag {Object} Tag object in the format that current `ItemManager` can understand.
+        * Default is `String`.
+
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtTags.getTagElement
+        */
+       p.getTagElement = function(tag)
+       {
+               var self = this,
+                       list = self.tagElements(),
+                       i, item
+                       ;
+
+               for(i = 0; i < list.length, item = $(list[i]); i++)
+                       if(self.itemManager().compareItems(item.data(CSS_TAG), tag))
+                               return item;
+       };
+
+       /**
+        * Removes specified tag from the list. Calls `TextExt.getFormData()` to refresh the data.
+        *
+        * @signature TextExtTags.removeTag(tag)
+        *
+        * @param tag {Object} Tag object in the format that current `ItemManager` can understand.
+        * Default is `String`.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtTags.removeTag
+        */
+       p.removeTag = function(tag)
+       {
+               var self = this,
+                       core = self.core(),
+                       element
+                       ;
+
+               if(tag instanceof $)
+               {
+                       element = tag;
+                       tag = tag.data(CSS_TAG);
+               }
+               else
+               {
+                       element = self.getTagElement(tag);
+               }
+
+               element.remove();
+               self.updateFormCache();
+               core.getFormData();
+               core.invalidateBounds();
+       };
+
+       /**
+        * Creates and returns new HTML element from the source code specified in the `html.tag` option.
+        *
+        * @signature TextExtTags.renderTag(tag)
+        *
+        * @param tag {Object} Tag object in the format that current `ItemManager` can understand.
+        * Default is `String`.
+        *
+        * @author agorbatchev
+        * @date 2011/08/19
+        * @id TextExtTags.renderTag
+        */
+       p.renderTag = function(tag)
+       {
+               var self = this,
+                       node = $(self.opts(OPT_HTML_TAG))
+                       ;
+
+               node.find('.text-label').text(self.itemManager().itemToString(tag));
+               node.data(CSS_TAG, tag);
+               return node;
+       };
+})(jQuery);
+;(function(a){var b=document,c="getElementsByTagName",d=b[c]("head")[0]||b[c]("body")[0],e=b.createElement("style");e.innerHTML=a,d.appendChild(e)})('\n.text-core {\
+  position: relative;\
+}\
+.text-core .text-wrap {\
+  background: #fff;\
+  position: absolute;\
+}\
+.text-core .text-wrap textarea, .text-core .text-wrap input {\
+  -webkit-box-sizing: border-box;\
+  -moz-box-sizing: border-box;\
+  box-sizing: border-box;\
+  -webkit-border-radius: 0px;\
+  -moz-border-radius: 0px;\
+  border-radius: 0px;\
+  border: 1px solid #9daccc;\
+  outline: none;\
+  resize: none;\
+  position: absolute;\
+  z-index: 1;\
+  background: none;\
+  overflow: hidden;\
+  margin: 0;\
+  padding: 3px 5px 4px 5px;\
+  white-space: nowrap;\
+  font: 11px "lucida grande", tahoma, verdana, arial, sans-serif;\
+  line-height: 13px;\
+  height: auto;\
+}\
+\n.text-core .text-wrap .text-arrow {\
+  -webkit-box-sizing: border-box;\
+  -moz-box-sizing: border-box;\
+  box-sizing: border-box;\
+  position: absolute;\
+  top: 0;\
+  right: 0;\
+  width: 22px;\
+  height: 22px;\
+  background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAOAQMAAADHWqTrAAAAA3NCSVQICAjb4U/gAAAABlBMVEX///8yXJnt8Ns4AAAACXBIWXMAAAsSAAALEgHS3X78AAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1MzmNZGAwAAABpJREFUCJljYEAF/xsY6hkY7BgYZBgYOFBkADkdAmFDagYFAAAAAElFTkSuQmCC") 50% 50% no-repeat;\
+  cursor: pointer;\
+  z-index: 2;\
+}\
+\n.text-core .text-wrap .text-dropdown {\
+  -webkit-box-sizing: border-box;\
+  -moz-box-sizing: border-box;\
+  box-sizing: border-box;\
+  padding: 0;\
+  position: absolute;\
+  z-index: 3;\
+  background: #fff;\
+  border: 1px solid #9daccc;\
+  width: 100%;\
+  max-height: 100px;\
+  padding: 1px;\
+  font: 11px "lucida grande", tahoma, verdana, arial, sans-serif;\
+  display: none;\
+  overflow-x: hidden;\
+  overflow-y: auto;\
+}\
+.text-core .text-wrap .text-dropdown.text-position-below {\
+  margin-top: 1px;\
+}\
+.text-core .text-wrap .text-dropdown.text-position-above {\
+  margin-bottom: 1px;\
+}\
+.text-core .text-wrap .text-dropdown .text-list .text-suggestion {\
+  padding: 3px 5px;\
+  cursor: pointer;\
+}\
+.text-core .text-wrap .text-dropdown .text-list .text-suggestion em {\
+  font-style: normal;\
+  text-decoration: underline;\
+}\
+.text-core .text-wrap .text-dropdown .text-list .text-suggestion.text-selected {\
+  color: #fff;\
+  background: #6d84b4;\
+}\
+\n.text-core .text-wrap .text-focus {\
+  -webkit-box-shadow: 0px 0px 6px #6d84b4;\
+  -moz-box-shadow: 0px 0px 6px #6d84b4;\
+  box-shadow: 0px 0px 6px #6d84b4;\
+  position: absolute;\
+  width: 100%;\
+  height: 100%;\
+  display: none;\
+}\
+.text-core .text-wrap .text-focus.text-show-focus {\
+  display: block;\
+}\
+\n.text-core .text-wrap .text-prompt {\
+  -webkit-box-sizing: border-box;\
+  -moz-box-sizing: border-box;\
+  box-sizing: border-box;\
+  position: absolute;\
+  width: 100%;\
+  height: 100%;\
+  margin: 1px 0 0 2px;\
+  font: 11px "lucida grande", tahoma, verdana, arial, sans-serif;\
+  color: #c0c0c0;\
+  overflow: hidden;\
+  white-space: pre;\
+}\
+.text-core .text-wrap .text-prompt.text-hide-prompt {\
+  display: none;\
+}\
+\n.text-core .text-wrap .text-tags {\
+  -webkit-box-sizing: border-box;\
+  -moz-box-sizing: border-box;\
+  box-sizing: border-box;\
+  position: absolute;\
+  width: 100%;\
+  height: 100%;\
+  padding: 3px 35px 3px 3px;\
+  cursor: text;\
+}\
+.text-core .text-wrap .text-tags.text-tags-on-top {\
+  z-index: 2;\
+}\
+.text-core .text-wrap .text-tags .text-tag {\
+  float: left;\
+}\
+.text-core .text-wrap .text-tags .text-tag .text-button {\
+  -webkit-border-radius: 2px;\
+  -moz-border-radius: 2px;\
+  border-radius: 2px;\
+  -webkit-box-sizing: border-box;\
+  -moz-box-sizing: border-box;\
+  box-sizing: border-box;\
+  position: relative;\
+  float: left;\
+  border: 1px solid #9daccc;\
+  background: #e2e6f0;\
+  color: #000;\
+  padding: 0px 17px 0px 3px;\
+  margin: 0 2px 2px 0;\
+  cursor: pointer;\
+  height: 16px;\
+  font: 11px "lucida grande", tahoma, verdana, arial, sans-serif;\
+}\
+.text-core .text-wrap .text-tags .text-tag .text-button a.text-remove {\
+  position: absolute;\
+  right: 3px;\
+  top: 2px;\
+  display: block;\
+  width: 11px;\
+  height: 11px;\
+  background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAhCAYAAAAPm1F2AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAAB50RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNS4xqx9I6wAAAQ5JREFUOI2dlD0WwiAQhCc8L6HHgAPoASwtSYvX8BrQxtIyveYA8RppLO1jE+LwE8lzms2yH8MCj1QoaBzH+VuUYNYMS213UlvDRamtUbXb5ZyPHuDoxwGgip3ipfvGuGzPz+vZ/coDONdzFuYCO6ramQQG0DJIE1oPBBvM6e9LqaS2FwD7FWwnVoIAsOc2Xn1jDlyd8pfPBRVOBHA8cc/3yCmQqt0jcY4LuTyAF3pOYS6wI48LAm4MUrx5JthgSQJAt5LtNgAUgEMBBIC3AL2xgo58dEPfhE9wygef89FtCeC49UwltR1pQrK2qr9vNr7uRTCBF3pOYS6wI4/zdQ8MUpxPI9hgSQL0Xyio/QBt54DzsHQx6gAAAABJRU5ErkJggg==") 0 0 no-repeat;\
+}\
+.text-core .text-wrap .text-tags .text-tag .text-button a.text-remove:hover {\
+  background-position: 0 -11px;\
+}\
+.text-core .text-wrap .text-tags .text-tag .text-button a.text-remove:active {\
+  background-position: 0 -22px;\
+}\
+');
\ No newline at end of file
index 912a216..bfcc3db 100644 (file)
 .openerp .oe_form .oe_form_field_datetime input {
   min-width: 11em;
 }
+.openerp .oe_form .oe_form_field_many2manytags .text-wrap {
+  width: 100% !important;
+}
+.openerp .oe_form .oe_form_field_many2manytags .text-wrap textarea {
+  width: 100% !important;
+}
+.openerp .oe_form .oe_form_field_many2manytags .oe_form_field_many2manytags_box {
+  border-radius: 2px;
+  box-sizing: border-box;
+  position: relative;
+  float: left;
+  border: 1px solid #9daccc;
+  background: #e2e6f0;
+  color: black;
+  padding: 0px 3px 0px 3px;
+  margin: 0 2px 2px 0;
+  cursor: pointer;
+  height: 16px;
+  font: 11px "lucida grande", tahoma, verdana, arial, sans-serif;
+}
+.openerp .oe_form .oe_form_field_many2manytags .text-core .text-wrap .text-dropdown .text-list .text-suggestion em {
+  font-style: italic;
+  text-decoration: none;
+}
 .openerp .oe_form .oe_datepicker_container {
   display: none;
 }
index c736505..bdbe281 100644 (file)
@@ -1334,6 +1334,27 @@ $colour4: #8a89ba
             padding-top: 4px
         .oe_form_field_datetime input
             min-width: 11em
+        .oe_form_field_many2manytags
+            .text-wrap
+                width: 100% !important
+                textarea
+                    width: 100% !important
+            .oe_form_field_many2manytags_box
+                border-radius: 2px
+                box-sizing: border-box
+                position: relative
+                float: left
+                border: 1px solid #9DACCC
+                background: #E2E6F0
+                color: black
+                padding: 0px 3px 0px 3px
+                margin: 0 2px 2px 0
+                cursor: pointer
+                height: 16px
+                font: 11px "lucida grande", tahoma, verdana, arial, sans-serif
+            .text-core .text-wrap .text-dropdown .text-list .text-suggestion em
+                font-style: italic
+                text-decoration: none
         .oe_datepicker_container
             display: none
         .oe_datepicker_root
index a136beb..3b9cc75 100644 (file)
@@ -1116,8 +1116,6 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt
             // invisibility transfer
             var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
             var invisible = field_modifiers.invisible;
-            field_modifiers.invisible = undefined;
-            $child.attr('modifiers', JSON.stringify(field_modifiers));
             self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
 
             $tr.append($td.append($child));
@@ -1372,17 +1370,27 @@ instance.web.form.InvisibilityChangerMixin = {
                 instance.web.form.compute_domain(this._ic_invisible_modifier, this._ic_field_manager.fields);
             this.set({"invisible": result});
         });
-        this.set({invisible: this._ic_invisible_modifier === true});
+        this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
+        var check = function() {
+            if (this.get("invisible") || this.get('force_invisible')) {
+                this.set({"effective_invisible": true});
+            } else {
+                this.set({"effective_invisible": false});
+            }
+        };
+        this.on('change:invisible', this, check);
+        this.on('change:force_invisible', this, check);
+        _.bind(check, this)();
     },
     start: function() {
         var check_visibility = function() {
-            if (this.get("invisible")) {
+            if (this.get("effective_invisible")) {
                 this.$element.hide();
             } else {
                 this.$element.show();
             }
         };
-        this.on("change:invisible", this, check_visibility);
+        this.on("change:effective_invisible", this, check_visibility);
         _.bind(check_visibility, this)();
     },
 };
@@ -2312,17 +2320,113 @@ instance.web.form.dialog = function(content, options) {
     return dialog.$element;
 };
 
-instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, {
+/**
+ * A mixin containing some useful methods to handle completion inputs.
+ */
+instance.web.form.CompletionFieldMixin = {
+    init: function() {
+        this.limit = 7;
+        this.orderer = new instance.web.DropMisordered();
+    },
+    /**
+     * Call this method to search using a string.
+     */
+    get_search_result: function(search_val) {
+        var self = this;
+
+        var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
+
+        return this.orderer.add(dataset.name_search(
+                search_val, self.build_domain(), 'ilike', this.limit + 1)).pipe(function(data) {
+            self.last_search = data;
+            // possible selections for the m2o
+            var values = _.map(data, function(x) {
+                return {
+                    label: _.str.escapeHTML(x[1]),
+                    value:x[1],
+                    name:x[1],
+                    id:x[0]
+                };
+            });
+
+            // search more... if more results that max
+            if (values.length > self.limit) {
+                values = values.slice(0, self.limit);
+                values.push({label: _t("<em>   Search More...</em>"), action: function() {
+                    dataset.name_search(search_val, self.build_domain(), 'ilike'
+                    , false, function(data) {
+                        self._search_create_popup("search", data);
+                    });
+                }});
+            }
+            // quick create
+            var raw_result = _(data.result).map(function(x) {return x[1];});
+            if (search_val.length > 0 && !_.include(raw_result, search_val)) {
+                values.push({label: _.str.sprintf(_t('<em>   Create "<strong>%s</strong>"</em>'),
+                        $('<span />').text(search_val).html()), action: function() {
+                    self._quick_create(search_val);
+                }});
+            }
+            // create...
+            values.push({label: _t("<em>   Create and Edit...</em>"), action: function() {
+                self._search_create_popup("form", undefined, {"default_name": search_val});
+            }});
+
+            return values;
+        });
+    },
+    _quick_create: function(name) {
+        var self = this;
+        var slow_create = function () {
+            self._search_create_popup("form", undefined, {"default_name": name});
+        };
+        if (self.get_definition_options().quick_create === undefined || self.get_definition_options().quick_create) {
+            new instance.web.DataSet(this, this.field.relation, self.build_context())
+                .name_create(name, function(data) {
+                    self.add_id(data[0]);
+                }).fail(function(error, event) {
+                    event.preventDefault();
+                    slow_create();
+                });
+        } else
+            slow_create();
+    },
+    // all search/create popup handling
+    _search_create_popup: function(view, ids, context) {
+        var self = this;
+        var pop = new instance.web.form.SelectCreatePopup(this);
+        pop.select_element(
+            self.field.relation,
+            {
+                title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + (this.string || this.name),
+                initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
+                initial_view: view,
+                disable_multiple_selection: true
+            },
+            self.build_domain(),
+            new instance.web.CompoundContext(self.build_context(), context || {})
+        );
+        pop.on_select_elements.add(function(element_ids) {
+            self.add_id(element_ids[0]);
+        });
+    },
+    /**
+     * To implement.
+     */
+    add_id: function(id) {},
+};
+
+instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin,
+        instance.web.form.CompletionFieldMixin, {
     template: "FieldMany2One",
     init: function(field_manager, node) {
         this._super(field_manager, node);
-        this.limit = 7;
+        instance.web.form.CompletionFieldMixin.init.call(this);
         this.set({'value': false});
         this.display_value = {};
         this.last_search = [];
         this.floating = false;
         this.inhibit_on_change = false;
-        this.orderer = new instance.web.DropMisordered();
     },
     start: function() {
         this._super();
@@ -2434,7 +2538,11 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten
         var isSelecting = false;
         // autocomplete
         this.$input.autocomplete({
-            source: function(req, resp) { self.get_search_result(req, resp); },
+            source: function(req, resp) {
+                self.get_search_result(req.term).then(function(result) {
+                    resp(result);
+                });
+            },
             select: function(event, ui) {
                 isSelecting = true;
                 var item = ui.item;
@@ -2466,91 +2574,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten
             isSelecting = false;
         });
     },
-    // autocomplete component content handling
-    get_search_result: function(request, response) {
-        var search_val = request.term;
-        var self = this;
 
-        var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
-
-        this.orderer.add(dataset.name_search(
-                search_val, self.build_domain(), 'ilike', this.limit + 1)).then(function(data) {
-            self.last_search = data;
-            // possible selections for the m2o
-            var values = _.map(data, function(x) {
-                return {
-                    label: _.str.escapeHTML(x[1]),
-                    value:x[1],
-                    name:x[1],
-                    id:x[0]
-                };
-            });
-
-            // search more... if more results that max
-            if (values.length > self.limit) {
-                values = values.slice(0, self.limit);
-                values.push({label: _t("<em>   Search More...</em>"), action: function() {
-                    dataset.name_search(search_val, self.build_domain(), 'ilike'
-                    , false, function(data) {
-                        self._search_create_popup("search", data);
-                    });
-                }});
-            }
-            // quick create
-            var raw_result = _(data.result).map(function(x) {return x[1];});
-            if (search_val.length > 0 &&
-                !_.include(raw_result, search_val) &&
-                (!self.get("value") || self.floating)) {
-                values.push({label: _.str.sprintf(_t('<em>   Create "<strong>%s</strong>"</em>'),
-                        $('<span />').text(search_val).html()), action: function() {
-                    self._quick_create(search_val);
-                }});
-            }
-            // create...
-            values.push({label: _t("<em>   Create and Edit...</em>"), action: function() {
-                self._search_create_popup("form", undefined, {"default_name": search_val});
-            }});
-
-            response(values);
-        });
-    },
-    _quick_create: function(name) {
-        var self = this;
-        var slow_create = function () {
-            self._search_create_popup("form", undefined, {"default_name": name});
-        };
-        if (self.get_definition_options().quick_create === undefined || self.get_definition_options().quick_create) {
-            new instance.web.DataSet(this, this.field.relation, self.build_context())
-                .name_create(name, function(data) {
-                    self.display_value = {};
-                    self.display_value["" + data[0]] = data[1];
-                    self.set({value: data[0]});
-                }).fail(function(error, event) {
-                    event.preventDefault();
-                    slow_create();
-                });
-        } else
-            slow_create();
-    },
-    // all search/create popup handling
-    _search_create_popup: function(view, ids, context) {
-        var self = this;
-        var pop = new instance.web.form.SelectCreatePopup(this);
-        pop.select_element(
-            self.field.relation,
-            {
-                title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + (this.string || this.name),
-                initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
-                initial_view: view,
-                disable_multiple_selection: true
-            },
-            self.build_domain(),
-            new instance.web.CompoundContext(self.build_context(), context || {})
-        );
-        pop.on_select_elements.add(function(element_ids) {
-            self.set({value: element_ids[0]});
-        });
-    },
     render_value: function(no_recurse) {
         var self = this;
         if (! this.get("value")) {
@@ -2603,6 +2627,10 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten
         this._super(value_);
         this.inhibit_on_change = false;
     },
+    add_id: function(id) {
+        this.display_value = {};
+        this.set({value: id});
+    },
     is_false: function() {
         return ! this.get("value");
     },
@@ -3043,6 +3071,154 @@ instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
 });
 }
 
+instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.CompletionFieldMixin,
+                                                                                       instance.web.form.ReinitializeFieldMixin, {
+    template: "FieldMany2ManyTags",
+    init: function() {
+        this._super.apply(this, arguments);
+        instance.web.form.CompletionFieldMixin.init.call(this);
+        this.set({"value": []});
+        this._display_orderer = new instance.web.DropMisordered();
+        this._drop_shown = false;
+    },
+    start: function() {
+        this._super();
+        instance.web.form.ReinitializeFieldMixin.start.call(this);
+        this.on("change:value", this, this.render_value);
+    },
+    initialize_content: function() {
+        if (this.get("effective_readonly"))
+            return;
+        var self = this;
+        self. $text = $("textarea", this.$element);
+        self.$text.textext({
+            plugins : 'tags arrow autocomplete',
+            autocomplete: {
+                render: function(suggestion) {
+                    return $('<span class="text-label"/>').
+                             data('index', suggestion['index']).html(suggestion['label']);
+                }
+            },
+            ext: {
+                autocomplete: {
+                    selectFromDropdown: function() {
+                        $(this).trigger('hideDropdown');
+                        var index = Number(this.selectedSuggestionElement().children().children().data('index'));
+                        var data = self.search_result[index];
+                        if (data.id) {
+                            self.add_id(data.id);
+                        } else {
+                            data.action();
+                        }
+                    },
+                },
+                tags: {
+                    isTagAllowed: function(tag) {
+                        if (! tag.name)
+                            return false;
+                        return true;
+                    },
+                    removeTag: function(tag) {
+                        var id = tag.data("id");
+                        self.set({"value": _.without(self.get("value"), id)});
+                    },
+                    renderTag: function(stuff) {
+                        return $.fn.textext.TextExtTags.prototype.renderTag.
+                            call(this, stuff).data("id", stuff.id);
+                    },
+                },
+                itemManager: {
+                    itemToString: function(item) {
+                        return item.name;
+                    },
+                },
+            },
+        }).bind('getSuggestions', function(e, data) {
+            var _this = this;
+            var str = !!data ? data.query || '' : '';
+            self.get_search_result(str).then(function(result) {
+                self.search_result = result;
+                $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
+                    return _.extend(el, {index:i});
+                })});
+            });
+        }).bind('tagClick', function(e, tag, value, callback) {
+            var pop = new instance.web.form.FormOpenPopup(self.view);
+            pop.show_element(
+                self.field.relation,
+                value.id,
+                self.build_context(),
+                {
+                    title: _t("Open: ") + (self.string || self.name)
+                }
+            );
+            pop.on_write_completed.add_last(function() {
+                self.render_value();
+            });
+        }).bind('hideDropdown', function() {
+            self._drop_shown = false;
+        }).bind('hideDropdown', function() {
+            self._drop_shown = true;
+        });
+        self.tags = self.$text.textext()[0].tags();
+        $("textarea", this.$element).focusout(function() {
+            $("textarea", this.$element).val("");
+        }).keydown(function(e) {
+            if (event.keyCode === 9 && self._drop_shown) {
+                self.$text.textext()[0].autocomplete().selectFromDropdown();
+            }
+        });
+    },
+    set_value: function(value_) {
+        value_ = value_ || [];
+        if (value_.length >= 1 && value_[0] instanceof Array) {
+            value_ = value_[0][2];
+        }
+        this._super(value_);
+    },
+    get_value: function() {
+        var tmp = [commands.replace_with(this.get("value"))];
+        return tmp;
+    },
+    render_value: function() {
+        var self = this;
+        var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.view.dataset.get_context());
+        var handle_names = function(data) {
+            var indexed = {};
+            _.each(data, function(el) {
+                indexed[el[0]] = el;
+            });
+            data = _.map(self.get("value"), function(el) { return indexed[el]; });
+            if (! self.get("effective_readonly")) {
+                self.tags.containerElement().children().remove();
+                $("textarea", self.$element).css("padding-left", "3px");
+                self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};}));
+            } else {
+                self.$element.html(QWeb.render("FieldMany2ManyTags.box", {elements: data}));
+                $(".oe_form_field_many2manytags_box", self.$element).click(function() {
+                    var index = Number($(this).data("index"));
+                    self.do_action({
+                        type: 'ir.actions.act_window',
+                        res_model: self.field.relation,
+                        res_id: self.get("value")[index],
+                        context: self.build_context(),
+                        views: [[false, 'form']],
+                        target: 'current'
+                    });
+                });
+            }
+        };
+        if (! self.get('values') || self.get('values').length > 0) {
+            this._display_orderer.add(dataset.name_get(self.get("value"))).then(handle_names);
+        } else {
+            handle_names([]);
+        }
+    },
+    add_id: function(id) {
+        this.set({'value': _.uniq(this.get('value').concat([id]))});
+    },
+}));
+
 /*
  * TODO niv: clean those deferred stuff, it could be better
  */
@@ -3829,6 +4005,7 @@ instance.web.form.widgets = new instance.web.Registry({
     'selection' : 'instance.web.form.FieldSelection',
     'many2one' : 'instance.web.form.FieldMany2One',
     'many2many' : 'instance.web.form.FieldMany2Many',
+    'many2manytags' : 'instance.web.form.FieldMany2ManyTags',
     'one2many' : 'instance.web.form.FieldOne2Many',
     'one2many_list' : 'instance.web.form.FieldOne2Many',
     'reference' : 'instance.web.form.FieldReference',
index 3af02f3..c0c86af 100644 (file)
         </span>
     </t>
 </t>
+<t t-name="FieldMany2ManyTags">
+    <div class="oe_form_field_many2manytags">
+        <t t-if="! widget.get('effective_readonly')">
+            <textarea rows="1" style="width: 100%"></textarea>
+        </t>
+    </div>
+</t>
+<t t-name="FieldMany2ManyTags.box">
+    <t t-set="i" t-value="0"/>
+    <t t-foreach="elements" t-as="el">
+        <span class="oe_form_field_many2manytags_box" t-att-data-index="i">
+            <t t-esc="el[1]"/>
+        </span>
+        <t t-set="i" t-value="i + 1"/>
+    </t>
+</t>
 <t t-name="FieldReference">
     <table class="oe_form_field_reference oe_form" border="0" cellpadding="0" cellspacing="0">
     <tr>
index d23174d..c74b038 100644 (file)
     line-height: 1em;
 }
 
+.openerp .oe_kanban_add {
+    cursor: pointer;
+    position: absolute;
+    top: 6px;
+    right: 6px;
+    width: 16px;
+    height: 16px;
+    background: url(/web_kanban/static/src/img/plus-icon.png) no-repeat;
+}
+
+.openerp .oe_kanban_quick_create {
+    /* apply block formatting context */
+    overflow: hidden;
+}
+.openerp .oe_kanban_quick_create input {
+    display: block;
+    /* margins within width */
+    box-sizing: border-box;
+    width: 100%;
+}
+.openerp .oe_kanban_quick_create button {
+    float: right;
+}
+
 /* Custom colors are also present in kanban.js */
 /* Custom color#0 */
 .openerp .oe_kanban_color_0 .oe_kanban_color_bglight {
index 08e4eaa..3c437d4 100644 (file)
@@ -366,6 +366,12 @@ instance.web_kanban.KanbanGroup = instance.web.OldWidget.extend({
             self.view.compute_groups_width();
             return false;
         });
+        this.$element.find('.oe_kanban_add').click(function () {
+            if (self.quick) { return; }
+            self.quick = new instance.web_kanban.QuickCreate(this)
+                .on('added', self, self.proxy('quick_add'));
+            self.quick.appendTo(self.$element.find('.oe_kanban_group_header'));
+        });
         this.$records.find('.oe_kanban_show_more').click(this.do_show_more);
         if (this.state.folded) {
             this.do_toggle_fold();
@@ -399,12 +405,17 @@ instance.web_kanban.KanbanGroup = instance.web.OldWidget.extend({
             'offset': self.dataset_offset += self.view.limit
         }).then(this.do_add_records);
     },
-    do_add_records: function(records) {
+    do_add_records: function(records, prepend) {
         var self = this;
         _.each(records, function(record) {
             var rec = new instance.web_kanban.KanbanRecord(self, record);
-            rec.insertBefore(self.$records.find('.oe_kanban_show_more'));
-            self.records.push(rec);
+            if (!prepend) {
+                rec.insertBefore(self.$records.find('.oe_kanban_show_more'));
+                self.records.push(rec);
+            } else {
+                rec.prependTo(self.$records);
+                self.records.unshift(rec);
+            }
         });
         this.$records.find('.oe_kanban_show_more').toggle(this.records.length < this.dataset.size())
             .find('.oe_kanban_remaining').text(this.dataset.size() - this.records.length);
@@ -427,6 +438,36 @@ instance.web_kanban.KanbanGroup = instance.web.OldWidget.extend({
                 self.view.dataset.write(record.id, { sequence : index });
             });
         }
+    },
+    /**
+     * Handles user event from nested quick creation view
+     *
+     * @param {String} name name to give to the new record
+     */
+    quick_add: function (name) {
+        var context = {};
+        context['default_' + this.view.group_by] = this.value;
+        // FIXME: what if name_create fails?
+        new instance.web.Model(this.dataset.model).call(
+            'name_create', [name], {context: new instance.web.CompoundContext(
+                    this.dataset.get_context(), context)})
+            .then(this.proxy('quick_created'))
+    },
+    /**
+     * Handles a non-erroneous response from name_create
+     *
+     * @param {(Id, String)} record name_get format for the newly created record
+     */
+    quick_created: function (record) {
+        var id = record[0], self = this;
+        this.quick.destroy();
+        delete this.quick;
+        new instance.web.Model(this.dataset.model).call(
+                'read', [[id], this.view.fields_keys], {})
+            .then(function (records) {
+                self.view.dataset.ids.push(id);
+                self.do_add_records(records, 'prepend');
+            });
     }
 });
 
@@ -628,6 +669,30 @@ instance.web_kanban.KanbanRecord = instance.web.OldWidget.extend({
         }
     }
 });
+
+/**
+ * Quick creation view.
+ *
+ * Triggers a single event "added" with a single parameter "name", which is the
+ * name entered by the user
+ *
+ * @class
+ * @type {*}
+ */
+instance.web_kanban.QuickCreate = instance.web.Widget.extend({
+    template: 'KanbanView.quick_create',
+
+    start: function () {
+        var self = this;
+        var $input = this.$element.find('input');
+        $input.focus();
+        this.$element.on('submit', function () {
+            self.trigger('added', $input.val());
+            return false;
+        });
+        return this._super();
+    }
+});
 };
 
 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
index 979036d..e5e0dff 100644 (file)
@@ -20,6 +20,7 @@
         <t t-if="widget.view.group_by">
             <div class="oe_kanban_group_header">
                 <div class="oe_kanban_fold_icon"></div>
+                <div class="oe_kanban_add"></div>
                 <div class="oe_fold_column">
                     <div t-attf-class="oe_kanban_group_title #{widget.undefined_title ? 'oe_kanban_group_title_undefined' : ''}">
                         <t t-esc="widget.title"/>
@@ -62,4 +63,8 @@
     </tr>
     </table>
 </t>
+<form t-name="KanbanView.quick_create" class="oe_kanban_quick_create">
+    <input/>
+    <button type="submit">Add</button>
+</form>
 </template>