2 * jQuery TextExt Plugin
6 * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
9 (function($, undefined)
12 * TextExt is the main core class which by itself doesn't provide any functionality
13 * that is user facing, however it has the underlying mechanics to bring all the
14 * plugins together under one roof and make them work with each other or on their
21 function TextExt() {};
24 * ItemManager is used to seamlessly convert between string that come from the user input to whatever
25 * the format the item data is being passed around in. It's used by all plugins that in one way or
26 * another operate with items, such as Tags, Filter, Autocomplete and Suggestions. Default implementation
27 * works with `String` type.
29 * Each instance of `TextExt` creates a new instance of default implementation of `ItemManager`
30 * unless `itemManager` option was set to another implementation.
32 * To satisfy requirements of managing items of type other than a `String`, different implementation
33 * if `ItemManager` should be supplied.
35 * If you wish to bring your own implementation, you need to create a new class and implement all the
36 * methods that `ItemManager` has. After, you need to supply your pass via the `itemManager` option during
37 * initialization like so:
39 * $('#input').textext({
40 * itemManager : CustomItemManager
47 function ItemManager() {};
50 * TextExtPlugin is a base class for all plugins. It provides common methods which are reused
51 * by majority of plugins.
53 * All plugins must register themselves by calling the `$.fn.textext.addPlugin(name, constructor)`
54 * function while providing plugin name and constructor. The plugin name is the same name that user
55 * will identify the plugin in the `plugins` option when initializing TextExt component and constructor
56 * function will create a new instance of the plugin. *Without registering, the core won't
57 * be able to see the plugin.*
59 * <span class="new label version">new in 1.2.0</span> You can get instance of each plugin from the core
60 * via associated function with the same name as the plugin. For example:
62 * $('#input').textext()[0].tags()
63 * $('#input').textext()[0].autocomplete()
70 function TextExtPlugin() {};
72 var stringify = (JSON || {}).stringify,
73 slice = Array.prototype.slice,
75 UNDEFINED = 'undefined',
78 * TextExt provides a way to pass in the options to configure the core as well as
79 * each plugin that is being currently used. The jQuery exposed plugin `$().textext()`
80 * function takes a hash object with key/value set of options. For example:
82 * $('textarea').textext({
86 * There are multiple ways of passing in the options:
88 * 1. Options could be nested multiple levels deep and accessed using all lowercased, dot
89 * separated style, eg `foo.bar.world`. The manual is using this style for clarity and
90 * consistency. For example:
109 * 2. Options could be specified using camel cased names in a flat key/value fashion like so:
114 * autocompleteEnabled: ...,
115 * autocompleteDropdownPosition: ...
118 * 3. Finally, options could be specified in mixed style. It's important to understand that
119 * for each dot separated name, its alternative in camel case is also checked for, eg for
120 * `foo.bar.world` it's alternatives could be `fooBarWorld`, `foo.barWorld` or `fooBar.world`,
121 * which translates to `{ foo: { bar: { world: ... } } }`, `{ fooBarWorld: ... }`,
122 * `{ foo : { barWorld : ... } }` or `{ fooBar: { world: ... } }` respectively. For example:
129 * dropdownPosition: ...
133 * Mixed case is used through out the code, wherever it seems appropriate. However in the code, all option
134 * names are specified in the dot notation because it works both ways where as camel case is not
135 * being converted to its alternative dot notation.
137 * @author agorbatchev
139 * @id TextExt.options
143 * Default instance of `ItemManager` which takes `String` type as default for tags.
146 * @default ItemManager
147 * @author agorbatchev
149 * @id TextExt.options.item.manager
151 OPT_ITEM_MANAGER = 'item.manager',
154 * List of plugins that should be used with the current instance of TextExt. The list could be
155 * specified as array of strings or as comma or space separated string.
159 * @author agorbatchev
161 * @id TextExt.options.plugins
163 OPT_PLUGINS = 'plugins',
166 * TextExt allows for overriding of virtually any method that the core or any of its plugins
167 * use. This could be accomplished through the use of the `ext` option.
169 * It's possible to specifically target the core or any plugin, as well as overwrite all the
170 * desired methods everywhere.
172 * 1. Targeting the core:
176 * trigger: function()
178 * console.log('TextExt.trigger', arguments);
179 * $.fn.textext.TextExt.prototype.trigger.apply(this, arguments);
184 * 2. Targeting individual plugins:
188 * addTags: function(tags)
190 * console.log('TextExtTags.addTags', tags);
191 * $.fn.textext.TextExtTags.prototype.addTags.apply(this, arguments);
196 * 3. Targeting `ItemManager` instance:
200 * stringToItem: function(str)
202 * console.log('ItemManager.stringToItem', str);
203 * return $.fn.textext.ItemManager.prototype.stringToItem.apply(this, arguments);
208 * 4. And finally, in edge cases you can extend everything at once:
212 * fooBar: function() {}
218 * @author agorbatchev
220 * @id TextExt.options.ext
225 * HTML source that is used to generate elements necessary for the core and all other
226 * plugins to function.
229 * @default '<div class="text-core"><div class="text-wrap"/></div>'
230 * @author agorbatchev
232 * @id TextExt.options.html.wrap
234 OPT_HTML_WRAP = 'html.wrap',
237 * HTML source that is used to generate hidden input value of which will be submitted
238 * with the HTML form.
241 * @default '<input type="hidden" />'
242 * @author agorbatchev
244 * @id TextExt.options.html.hidden
246 OPT_HTML_HIDDEN = 'html.hidden',
249 * Hash table of key codes and key names for which special events will be created
250 * by the core. For each entry a `[name]KeyDown`, `[name]KeyUp` and `[name]KeyPress` events
251 * will be triggered along side with `anyKeyUp` and `anyKeyDown` events for every
254 * Here's a list of default keys:
266 * 108 : 'numpadEnter'
269 * Please note the `!` at the end of some keys. This tells the core that by default
270 * this keypress will be trapped and not passed on to the text input.
274 * @author agorbatchev
276 * @id TextExt.options.keys
281 * The core triggers or reacts to the following events.
283 * @author agorbatchev
289 * Core triggers `preInvalidate` event before the dimensions of padding on the text input
292 * @name preInvalidate
293 * @author agorbatchev
295 * @id TextExt.events.preInvalidate
297 EVENT_PRE_INVALIDATE = 'preInvalidate',
300 * Core triggers `postInvalidate` event after the dimensions of padding on the text input
303 * @name postInvalidate
304 * @author agorbatchev
306 * @id TextExt.events.postInvalidate
308 EVENT_POST_INVALIDATE = 'postInvalidate',
311 * Core triggers `getFormData` on every key press to collect data that will be populated
312 * into the hidden input that will be submitted with the HTML form and data that will
313 * be displayed in the input field that user is currently interacting with.
315 * All plugins that wish to affect how the data is presented or sent must react to
316 * `getFormData` and populate the data in the following format:
323 * The data key must be a numeric weight which will be used to determine which data
324 * ends up being used. Data with the highest numerical weight gets the priority. This
325 * allows plugins to set the final data regardless of their initialization order, which
326 * otherwise would be impossible.
328 * For example, the Tags and Autocomplete plugins have to work side by side and Tags
329 * plugin must get priority on setting the data. Therefore the Tags plugin sets data
330 * with the weight 200 where as the Autocomplete plugin sets data with the weight 100.
332 * Here's an example of a typical `getFormData` handler:
334 * TextExtPlugin.prototype.onGetFormData = function(e, data, keyCode)
336 * data[100] = self.formDataObject('input value', 'form value');
339 * Core also reacts to the `getFormData` and updates hidden input with data which will be
340 * submitted with the HTML form.
343 * @author agorbatchev
345 * @id TextExt.events.getFormData
347 EVENT_GET_FORM_DATA = 'getFormData',
350 * Core triggers and reacts to the `setFormData` event to update the actual value in the
351 * hidden input that will be submitted with the HTML form. Second argument can be value
352 * of any type and by default it will be JSON serialized with `TextExt.serializeData()`
356 * @author agorbatchev
358 * @id TextExt.events.setFormData
360 EVENT_SET_FORM_DATA = 'setFormData',
363 * Core triggers and reacts to the `setInputData` event to update the actual value in the
364 * text input that user is interacting with. Second argument must be of a `String` type
365 * the value of which will be set into the text input.
368 * @author agorbatchev
370 * @id TextExt.events.setInputData
372 EVENT_SET_INPUT_DATA = 'setInputData',
375 * Core triggers `postInit` event to let plugins run code after all plugins have been
376 * created and initialized. This is a good place to set some kind of global values before
377 * somebody gets to use them. This is not the right place to expect all plugins to finish
378 * their initialization.
381 * @author agorbatchev
383 * @id TextExt.events.postInit
385 EVENT_POST_INIT = 'postInit',
388 * Core triggers `ready` event after all global configuration and prepearation has been
389 * done and the TextExt component is ready for use. Event handlers should expect all
390 * values to be set and the plugins to be in the final state.
393 * @author agorbatchev
395 * @id TextExt.events.ready
397 EVENT_READY = 'ready',
400 * Core triggers `anyKeyUp` event for every key up event triggered within the component.
403 * @author agorbatchev
405 * @id TextExt.events.anyKeyUp
409 * Core triggers `anyKeyDown` event for every key down event triggered within the component.
412 * @author agorbatchev
414 * @id TextExt.events.anyKeyDown
418 * Core triggers `[name]KeyUp` event for every key specifid in the `keys` option that is
419 * triggered within the component.
422 * @author agorbatchev
424 * @id TextExt.events.[name]KeyUp
428 * Core triggers `[name]KeyDown` event for every key specified in the `keys` option that is
429 * triggered within the component.
431 * @name [name]KeyDown
432 * @author agorbatchev
434 * @id TextExt.events.[name]KeyDown
438 * Core triggers `[name]KeyPress` event for every key specified in the `keys` option that is
439 * triggered within the component.
441 * @name [name]KeyPress
442 * @author agorbatchev
444 * @id TextExt.events.[name]KeyPress
448 itemManager : ItemManager,
454 wrap : '<div class="text-core"><div class="text-wrap"/></div>',
455 hidden : '<input type="hidden" />'
473 // Freak out if there's no JSON.stringify function found
475 throw new Error('JSON.stringify() not found');
478 * Returns object property by name where name is dot-separated and object is multiple levels deep.
479 * @param target Object Source object.
480 * @param name String Dot separated property name, ie `foo.bar.world`
481 * @id core.getProperty
483 function getProperty(source, name)
485 if(typeof(name) === 'string')
486 name = name.split('.');
488 var fullCamelCaseName = name.join('.').replace(/\.(\w)/g, function(match, letter) { return letter.toUpperCase() }),
489 nestedName = name.shift(),
493 if(typeof(result = source[fullCamelCaseName]) != UNDEFINED)
496 else if(typeof(result = source[nestedName]) != UNDEFINED && name.length > 0)
497 result = getProperty(result, name);
499 // name.length here should be zero
504 * Hooks up specified events in the scope of the current object.
505 * @author agorbatchev
508 function hookupEvents()
510 var args = slice.apply(arguments),
512 target = args.length === 1 ? self : args.shift(),
516 args = args[0] || {};
518 function bind(event, handler)
520 target.bind(event, function()
522 // apply handler to our PLUGIN object, not the target
523 return handler.apply(self, arguments);
528 bind(event, args[event]);
531 function formDataObject(input, form)
533 return { 'input' : input, 'form' : form };
536 //--------------------------------------------------------------------------------
537 // ItemManager core component
539 p = ItemManager.prototype;
542 * Initialization method called by the core during instantiation.
544 * @signature ItemManager.init(core)
546 * @param core {TextExt} Instance of the TextExt core class.
548 * @author agorbatchev
550 * @id ItemManager.init
552 p.init = function(core)
557 * Filters out items from the list that don't match the query and returns remaining items. Default
558 * implementation checks if the item starts with the query.
560 * @signature ItemManager.filter(list, query)
562 * @param list {Array} List of items. Default implementation works with strings.
563 * @param query {String} Query string.
565 * @author agorbatchev
567 * @id ItemManager.filter
569 p.filter = function(list, query)
575 for(i = 0; i < list.length; i++)
578 if(this.itemContains(item, query))
586 * Returns `true` if specified item contains another string, `false` otherwise. In the default implementation
587 * `String.indexOf()` is used to check if item string begins with the needle string.
589 * @signature ItemManager.itemContains(item, needle)
591 * @param item {Object} Item to check. Default implementation works with strings.
592 * @param needle {String} Search string to be found within the item.
594 * @author agorbatchev
596 * @id ItemManager.itemContains
598 p.itemContains = function(item, needle)
600 return this.itemToString(item).toLowerCase().indexOf(needle.toLowerCase()) == 0;
604 * Converts specified string to item. Because default implemenation works with string, input string
605 * is simply returned back. To use custom objects, different implementation of this method could
606 * return something like `{ name : {String} }`.
608 * @signature ItemManager.stringToItem(str)
610 * @param str {String} Input string.
612 * @author agorbatchev
614 * @id ItemManager.stringToItem
616 p.stringToItem = function(str)
622 * Converts specified item to string. Because default implemenation works with string, input string
623 * is simply returned back. To use custom objects, different implementation of this method could
624 * for example return `name` field of `{ name : {String} }`.
626 * @signature ItemManager.itemToString(item)
628 * @param item {Object} Input item to be converted to string.
630 * @author agorbatchev
632 * @id ItemManager.itemToString
634 p.itemToString = function(item)
640 * Returns `true` if both items are equal, `false` otherwise. Because default implemenation works with
641 * string, input items are compared as strings. To use custom objects, different implementation of this
642 * method could for example compare `name` fields of `{ name : {String} }` type object.
644 * @signature ItemManager.compareItems(item1, item2)
646 * @param item1 {Object} First item.
647 * @param item2 {Object} Second item.
649 * @author agorbatchev
651 * @id ItemManager.compareItems
653 p.compareItems = function(item1, item2)
655 return item1 == item2;
658 //--------------------------------------------------------------------------------
659 // TextExt core component
661 p = TextExt.prototype;
664 * Initializes current component instance with work with the supplied text input and options.
666 * @signature TextExt.init(input, opts)
668 * @param input {HTMLElement} Text input.
669 * @param opts {Object} Options.
671 * @author agorbatchev
675 p.init = function(input, opts)
683 self._defaults = $.extend({}, DEFAULT_OPTS);
684 self._opts = opts || {};
686 self._itemManager = itemManager = new (self.opts(OPT_ITEM_MANAGER))();
688 container = $(self.opts(OPT_HTML_WRAP));
689 hiddenInput = $(self.opts(OPT_HTML_HIDDEN));
693 .keydown(function(e) { return self.onKeyDown(e) })
694 .keyup(function(e) { return self.onKeyUp(e) })
695 .data('textext', self)
698 // keep references to html elements using jQuery.data() to avoid circular references
700 'hiddenInput' : hiddenInput,
701 'wrapElement' : input.parents('.text-wrap').first(),
705 // set the name of the hidden input to the text input's name
706 hiddenInput.attr('name', input.attr('name'));
707 // remove name attribute from the text input
708 input.attr('name', null);
709 // add hidden input to the DOM
710 hiddenInput.insertAfter(input);
712 $.extend(true, itemManager, self.opts(OPT_EXT + '.item.manager'));
713 $.extend(true, self, self.opts(OPT_EXT + '.*'), self.opts(OPT_EXT + '.core'));
715 self.originalWidth = input.outerWidth();
717 self.invalidateBounds();
719 itemManager.init(self);
722 self.initPlugins(self.opts(OPT_PLUGINS), $.fn.textext.plugins);
725 setFormData : self.onSetFormData,
726 getFormData : self.onGetFormData,
727 setInputData : self.onSetInputData,
728 anyKeyUp : self.onAnyKeyUp
731 self.trigger(EVENT_POST_INIT);
732 self.trigger(EVENT_READY);
738 * Initialized all installed patches against current instance. The patches are initialized based on their
739 * initialization priority which is returned by each patch's `initPriority()` method. Priority
740 * is a `Number` where patches with higher value gets their `init()` method called before patches
741 * with lower priority value.
743 * This facilitates initializing of patches in certain order to insure proper dependencies
744 * regardless of which order they are loaded.
746 * By default all patches have the same priority - zero, which means they will be initialized
747 * in rorder they are loaded, that is unless `initPriority()` is overriden.
749 * @signature TextExt.initPatches()
751 * @author agorbatchev
753 * @id TextExt.initPatches
755 p.initPatches = function()
758 source = $.fn.textext.patches,
765 this.initPlugins(list, source);
769 * Creates and initializes all specified plugins. The plugins are initialized based on their
770 * initialization priority which is returned by each plugin's `initPriority()` method. Priority
771 * is a `Number` where plugins with higher value gets their `init()` method called before plugins
772 * with lower priority value.
774 * This facilitates initializing of plugins in certain order to insure proper dependencies
775 * regardless of which order user enters them in the `plugins` option field.
777 * By default all plugins have the same priority - zero, which means they will be initialized
778 * in the same order as entered by the user.
780 * @signature TextExt.initPlugins(plugins)
782 * @param plugins {Array} List of plugin names to initialize.
784 * @author agorbatchev
786 * @id TextExt.initPlugins
788 p.initPlugins = function(plugins, source)
791 ext, name, plugin, initList = [], i
794 if(typeof(plugins) == 'string')
795 plugins = plugins.split(/\s*,\s*|\s+/g);
797 for(i = 0; i < plugins.length; i++)
800 plugin = source[name];
804 self._plugins[name] = plugin = new plugin();
805 self[name] = (function(plugin) {
806 return function(){ return plugin; }
808 initList.push(plugin);
809 $.extend(true, plugin, self.opts(OPT_EXT + '.*'), self.opts(OPT_EXT + '.' + name));
813 // sort plugins based on their priority values
814 initList.sort(function(p1, p2)
816 p1 = p1.initPriority();
817 p2 = p2.initPriority();
825 for(i = 0; i < initList.length; i++)
826 initList[i].init(self);
830 * Returns true if specified plugin is was instantiated for the current instance of core.
832 * @signature TextExt.hasPlugin(name)
834 * @param name {String} Name of the plugin to check.
836 * @author agorbatchev
838 * @id TextExt.hasPlugin
841 p.hasPlugin = function(name)
843 return !!this._plugins[name];
847 * Allows to add multiple event handlers which will be execued in the scope of the current object.
849 * @signature TextExt.on([target], handlers)
851 * @param target {Object} **Optional**. Target object which has traditional `bind(event, handler)` method.
852 * Handler function will still be executed in the current object's scope.
853 * @param handlers {Object} Key/value pairs of event names and handlers, eg `{ event: handler }`.
855 * @author agorbatchev
862 * Binds an event handler to the input box that user interacts with.
864 * @signature TextExt.bind(event, handler)
866 * @param event {String} Event name.
867 * @param handler {Function} Event handler.
869 * @author agorbatchev
873 p.bind = function(event, handler)
875 this.input().bind(event, handler);
879 * Triggers an event on the input box that user interacts with. All core events are originated here.
881 * @signature TextExt.trigger(event, ...args)
883 * @param event {String} Name of the event to trigger.
884 * @param ...args All remaining arguments will be passed to the event handler.
886 * @author agorbatchev
888 * @id TextExt.trigger
890 p.trigger = function()
892 var args = arguments;
893 this.input().trigger(args[0], slice.call(args, 1));
897 * Returns instance of `itemManager` that is used by the component.
899 * @signature TextExt.itemManager()
901 * @author agorbatchev
903 * @id TextExt.itemManager
905 p.itemManager = function()
907 return this._itemManager;
911 * Returns jQuery input element with which user is interacting with.
913 * @signature TextExt.input()
915 * @author agorbatchev
921 return $(this).data('input');
925 * Returns option value for the specified option by name. If the value isn't found in the user
926 * provided options, it will try looking for default value.
928 * @signature TextExt.opts(name)
930 * @param name {String} Option name as described in the options.
932 * @author agorbatchev
936 p.opts = function(name)
938 var result = getProperty(this._opts, name);
939 return typeof(result) == 'undefined' ? getProperty(this._defaults, name) : result;
943 * Returns HTML element that was created from the `html.wrap` option. This is the top level HTML
944 * container for the text input with which user is interacting with.
946 * @signature TextExt.wrapElement()
948 * @author agorbatchev
950 * @id TextExt.wrapElement
952 p.wrapElement = function()
954 return $(this).data('wrapElement');
958 * Updates container to match dimensions of the text input. Triggers `preInvalidate` and `postInvalidate`
961 * @signature TextExt.invalidateBounds()
963 * @author agorbatchev
965 * @id TextExt.invalidateBounds
967 p.invalidateBounds = function()
970 input = self.input(),
971 wrap = self.wrapElement(),
972 container = wrap.parent(),
973 width = self.originalWidth,
977 self.trigger(EVENT_PRE_INVALIDATE);
979 height = input.outerHeight();
982 wrap.width(width).height(height);
983 container.height(height);
985 self.trigger(EVENT_POST_INVALIDATE);
989 * Focuses user input on the text box.
991 * @signature TextExt.focusInput()
993 * @author agorbatchev
995 * @id TextExt.focusInput
997 p.focusInput = function()
999 this.input()[0].focus();
1003 * Serializes data for to be set into the hidden input field and which will be submitted
1004 * with the HTML form.
1006 * By default simple JSON serialization is used. It's expected that `JSON.stringify`
1007 * method would be available either through built in class in most modern browsers
1008 * or through JSON2 library.
1010 * @signature TextExt.serializeData(data)
1012 * @param data {Object} Data to serialize.
1014 * @author agorbatchev
1016 * @id TextExt.serializeData
1018 p.serializeData = stringify;
1021 * Returns the hidden input HTML element which will be submitted with the HTML form.
1023 * @signature TextExt.hiddenInput()
1025 * @author agorbatchev
1027 * @id TextExt.hiddenInput
1029 p.hiddenInput = function(value)
1031 return $(this).data('hiddenInput');
1035 * Abstracted functionality to trigger an event and get the data with maximum weight set by all
1036 * the event handlers. This functionality is used for the `getFormData` event.
1038 * @signature TextExt.getWeightedEventResponse(event, args)
1040 * @param event {String} Event name.
1041 * @param args {Object} Argument to be passed with the event.
1043 * @author agorbatchev
1045 * @id TextExt.getWeightedEventResponse
1047 p.getWeightedEventResponse = function(event, args)
1054 self.trigger(event, data, args);
1056 for(var weight in data)
1057 maxWeight = Math.max(maxWeight, weight);
1059 return data[maxWeight];
1063 * Triggers the `getFormData` event to get all the plugins to return their data.
1065 * After the data is returned, triggers `setFormData` and `setInputData` to update appopriate values.
1067 * @signature TextExt.getFormData(keyCode)
1069 * @param keyCode {Number} Key code number which has triggered this update. It's impotant to pass
1070 * this value to the plugins because they might return different values based on the key that was
1071 * pressed. For example, the Tags plugin returns an empty string for the `input` value if the enter
1072 * key was pressed, otherwise it returns whatever is currently in the text input.
1074 * @author agorbatchev
1076 * @id TextExt.getFormData
1078 p.getFormData = function(keyCode)
1081 data = self.getWeightedEventResponse(EVENT_GET_FORM_DATA, keyCode || 0)
1084 self.trigger(EVENT_SET_FORM_DATA , data['form']);
1085 self.trigger(EVENT_SET_INPUT_DATA , data['input']);
1088 //--------------------------------------------------------------------------------
1092 * Reacts to the `anyKeyUp` event and triggers the `getFormData` to change data that will be submitted
1093 * with the form. Default behaviour is that everything that is typed in will be JSON serialized, so
1094 * the end result will be a JSON string.
1096 * @signature TextExt.onAnyKeyUp(e)
1098 * @param e {Object} jQuery event.
1100 * @author agorbatchev
1102 * @id TextExt.onAnyKeyUp
1104 p.onAnyKeyUp = function(e, keyCode)
1106 this.getFormData(keyCode);
1110 * Reacts to the `setInputData` event and populates the input text field that user is currently
1113 * @signature TextExt.onSetInputData(e, data)
1115 * @param e {Event} jQuery event.
1116 * @param data {String} Value to be set.
1118 * @author agorbatchev
1120 * @id TextExt.onSetInputData
1122 p.onSetInputData = function(e, data)
1124 this.input().val(data);
1128 * Reacts to the `setFormData` event and populates the hidden input with will be submitted with
1129 * the HTML form. The value will be serialized with `serializeData()` method.
1131 * @signature TextExt.onSetFormData(e, data)
1133 * @param e {Event} jQuery event.
1134 * @param data {Object} Data that will be set.
1136 * @author agorbatchev
1138 * @id TextExt.onSetFormData
1140 p.onSetFormData = function(e, data)
1143 self.hiddenInput().val(self.serializeData(data));
1147 * Reacts to `getFormData` event triggered by the core. At the bare minimum the core will tell
1148 * itself to use the current value in the text input as the data to be submitted with the HTML
1151 * @signature TextExt.onGetFormData(e, data)
1153 * @param e {Event} jQuery event.
1155 * @author agorbatchev
1157 * @id TextExt.onGetFormData
1159 p.onGetFormData = function(e, data)
1161 var val = this.input().val();
1162 data[0] = formDataObject(val, val);
1165 //--------------------------------------------------------------------------------
1166 // User mouse/keyboard input
1169 * Triggers `[name]KeyUp` and `[name]KeyPress` for every keystroke as described in the events.
1171 * @signature TextExt.onKeyUp(e)
1173 * @param e {Object} jQuery event.
1174 * @author agorbatchev
1176 * @id TextExt.onKeyUp
1180 * Triggers `[name]KeyDown` for every keystroke as described in the events.
1182 * @signature TextExt.onKeyDown(e)
1184 * @param e {Object} jQuery event.
1185 * @author agorbatchev
1187 * @id TextExt.onKeyDown
1190 $(['Down', 'Up']).each(function()
1192 var type = this.toString();
1194 p['onKey' + type] = function(e)
1197 keyName = self.opts(OPT_KEYS)[e.keyCode],
1198 defaultResult = true
1203 defaultResult = keyName.substr(-1) != '!';
1204 keyName = keyName.replace('!', '');
1206 self.trigger(keyName + 'Key' + type);
1208 // manual *KeyPress event fimplementation for the function keys like Enter, Backspace, etc.
1209 if(type == 'Up' && self._lastKeyDown == e.keyCode)
1211 self._lastKeyDown = null;
1212 self.trigger(keyName + 'KeyPress');
1216 self._lastKeyDown = e.keyCode;
1219 self.trigger('anyKey' + type, e.keyCode);
1221 return defaultResult;
1225 //--------------------------------------------------------------------------------
1228 p = TextExtPlugin.prototype;
1231 * Allows to add multiple event handlers which will be execued in the scope of the current object.
1233 * @signature TextExt.on([target], handlers)
1235 * @param target {Object} **Optional**. Target object which has traditional `bind(event, handler)` method.
1236 * Handler function will still be executed in the current object's scope.
1237 * @param handlers {Object} Key/value pairs of event names and handlers, eg `{ event: handler }`.
1239 * @author agorbatchev
1241 * @id TextExtPlugin.on
1243 p.on = hookupEvents;
1246 * Returns the hash object that `getFormData` triggered by the core expects.
1248 * @signature TextExtPlugin.formDataObject(input, form)
1250 * @param input {String} Value that will go into the text input that user is interacting with.
1251 * @param form {Object} Value that will be serialized and put into the hidden that will be submitted
1252 * with the HTML form.
1254 * @author agorbatchev
1256 * @id TextExtPlugin.formDataObject
1258 p.formDataObject = formDataObject;
1261 * Initialization method called by the core during plugin instantiation. This method must be implemented
1262 * by each plugin individually.
1264 * @signature TextExtPlugin.init(core)
1266 * @param core {TextExt} Instance of the TextExt core class.
1268 * @author agorbatchev
1270 * @id TextExtPlugin.init
1272 p.init = function(core) { throw new Error('Not implemented') };
1275 * Initialization method wich should be called by the plugin during the `init()` call.
1277 * @signature TextExtPlugin.baseInit(core, defaults)
1279 * @param core {TextExt} Instance of the TextExt core class.
1280 * @param defaults {Object} Default plugin options. These will be checked if desired value wasn't
1281 * found in the options supplied by the user.
1283 * @author agorbatchev
1285 * @id TextExtPlugin.baseInit
1287 p.baseInit = function(core, defaults)
1291 core._defaults = $.extend(true, core._defaults, defaults);
1297 * Allows starting of multiple timeout calls. Each time this method is called with the same
1298 * timer name, the timer is reset. This functionality is useful in cases where an action needs
1299 * to occur only after a certain period of inactivity. For example, making an AJAX call after
1300 * user stoped typing for 1 second.
1302 * @signature TextExtPlugin.startTimer(name, delay, callback)
1304 * @param name {String} Timer name.
1305 * @param delay {Number} Delay in seconds.
1306 * @param callback {Function} Callback function.
1308 * @author agorbatchev
1310 * @id TextExtPlugin.startTimer
1312 p.startTimer = function(name, delay, callback)
1316 self.stopTimer(name);
1318 self._timers[name] = setTimeout(
1321 delete self._timers[name];
1322 callback.apply(self);
1329 * Stops the timer by name without resetting it.
1331 * @signature TextExtPlugin.stopTimer(name)
1333 * @param name {String} Timer name.
1335 * @author agorbatchev
1337 * @id TextExtPlugin.stopTimer
1339 p.stopTimer = function(name)
1341 clearTimeout(this._timers[name]);
1345 * Returns instance of the `TextExt` to which current instance of the plugin is attached to.
1347 * @signature TextExtPlugin.core()
1349 * @author agorbatchev
1351 * @id TextExtPlugin.core
1359 * Shortcut to the core's `opts()` method. Returns option value.
1361 * @signature TextExtPlugin.opts(name)
1363 * @param name {String} Option name as described in the options.
1365 * @author agorbatchev
1367 * @id TextExtPlugin.opts
1369 p.opts = function(name)
1371 return this.core().opts(name);
1375 * Shortcut to the core's `itemManager()` method. Returns instance of the `ItemManger` that is
1378 * @signature TextExtPlugin.itemManager()
1380 * @author agorbatchev
1382 * @id TextExtPlugin.itemManager
1384 p.itemManager = function()
1386 return this.core().itemManager();
1390 * Shortcut to the core's `input()` method. Returns instance of the HTML element that represents
1391 * current text input.
1393 * @signature TextExtPlugin.input()
1395 * @author agorbatchev
1397 * @id TextExtPlugin.input
1399 p.input = function()
1401 return this.core().input();
1405 * Shortcut to the commonly used `this.input().val()` call to get or set value of the text input.
1407 * @signature TextExtPlugin.val(value)
1409 * @param value {String} Optional value. If specified, the value will be set, otherwise it will be
1412 * @author agorbatchev
1414 * @id TextExtPlugin.val
1416 p.val = function(value)
1418 var input = this.input();
1420 if(typeof(value) === UNDEFINED)
1427 * Shortcut to the core's `trigger()` method. Triggers specified event with arguments on the
1430 * @signature TextExtPlugin.trigger(event, ...args)
1432 * @param event {String} Name of the event to trigger.
1433 * @param ...args All remaining arguments will be passed to the event handler.
1435 * @author agorbatchev
1437 * @id TextExtPlugin.trigger
1439 p.trigger = function()
1441 var core = this.core();
1442 core.trigger.apply(core, arguments);
1446 * Shortcut to the core's `bind()` method. Binds specified handler to the event.
1448 * @signature TextExtPlugin.bind(event, handler)
1450 * @param event {String} Event name.
1451 * @param handler {Function} Event handler.
1453 * @author agorbatchev
1455 * @id TextExtPlugin.bind
1457 p.bind = function(event, handler)
1459 this.core().bind(event, handler);
1463 * Returns initialization priority for this plugin. If current plugin depends upon some other plugin
1464 * to be initialized before or after, priority needs to be adjusted accordingly. Plugins with higher
1465 * priority initialize before plugins with lower priority.
1467 * Default initialization priority is `0`.
1469 * @signature TextExtPlugin.initPriority()
1471 * @author agorbatchev
1473 * @id TextExtPlugin.initPriority
1475 p.initPriority = function()
1480 //--------------------------------------------------------------------------------
1481 // jQuery Integration
1484 * TextExt integrates as a jQuery plugin available through the `$(selector).textext(opts)` call. If
1485 * `opts` argument is passed, then a new instance of `TextExt` will be created for all the inputs
1486 * that match the `selector`. If `opts` wasn't passed and TextExt was already intantiated for
1487 * inputs that match the `selector`, array of `TextExt` instances will be returned instead.
1489 * // will create a new instance of `TextExt` for all elements that match `.sample`
1490 * $('.sample').textext({ ... });
1492 * // will return array of all `TextExt` instances
1493 * var list = $('.sample').textext();
1495 * The following properties are also exposed through the jQuery `$.fn.textext`:
1497 * * `TextExt` -- `TextExt` class.
1498 * * `TextExtPlugin` -- `TextExtPlugin` class.
1499 * * `ItemManager` -- `ItemManager` class.
1500 * * `plugins` -- Key/value table of all registered plugins.
1501 * * `addPlugin(name, constructor)` -- All plugins should register themselves using this function.
1503 * @author agorbatchev
1505 * @id TextExt.jquery
1508 var cssInjected = false;
1510 var textext = $.fn.textext = function(opts)
1514 if(!cssInjected && (css = $.fn.textext.css) != null)
1516 $('head').append('<style>' + css + '</style>');
1520 return this.map(function()
1525 return self.data('textext');
1527 var instance = new TextExt();
1529 instance.init(self, opts);
1530 self.data('textext', instance);
1532 return instance.input()[0];
1537 * This static function registers a new plugin which makes it available through the `plugins` option
1538 * to the end user. The name specified here is the name the end user would put in the `plugins` option
1539 * to add this plugin to a new instance of TextExt.
1541 * @signature $.fn.textext.addPlugin(name, constructor)
1543 * @param name {String} Name of the plugin.
1544 * @param constructor {Function} Plugin constructor.
1546 * @author agorbatchev
1548 * @id TextExt.addPlugin
1550 textext.addPlugin = function(name, constructor)
1552 textext.plugins[name] = constructor;
1553 constructor.prototype = new textext.TextExtPlugin();
1557 * This static function registers a new patch which is added to each instance of TextExt. If you are
1558 * adding a new patch, make sure to call this method.
1560 * @signature $.fn.textext.addPatch(name, constructor)
1562 * @param name {String} Name of the patch.
1563 * @param constructor {Function} Patch constructor.
1565 * @author agorbatchev
1567 * @id TextExt.addPatch
1569 textext.addPatch = function(name, constructor)
1571 textext.patches[name] = constructor;
1572 constructor.prototype = new textext.TextExtPlugin();
1575 textext.TextExt = TextExt;
1576 textext.TextExtPlugin = TextExtPlugin;
1577 textext.ItemManager = ItemManager;
1578 textext.plugins = {};
1579 textext.patches = {};
1584 function TextExtIE9Patches() {};
1586 $.fn.textext.TextExtIE9Patches = TextExtIE9Patches;
1587 $.fn.textext.addPatch('ie9',TextExtIE9Patches);
1589 var p = TextExtIE9Patches.prototype;
1591 p.init = function(core)
1593 if(navigator.userAgent.indexOf('MSIE 9') == -1)
1598 core.on({ postInvalidate : self.onPostInvalidate });
1601 p.onPostInvalidate = function()
1604 input = self.input(),
1608 // agorbatchev :: IE9 doesn't seem to update the padding if box-sizing is on until the
1609 // text box value changes, so forcing this change seems to do the trick of updating
1610 // IE's padding visually.
1611 input.val(Math.random());
1617 * jQuery TextExt Plugin
1618 * http://textextjs.com
1621 * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
1622 * @license MIT License
1627 * AJAX plugin is very useful if you want to load list of items from a data point and pass it
1628 * to the Autocomplete or Filter plugins.
1630 * Because it meant to be as a helper method for either Autocomplete or Filter plugin, without
1631 * either of these two present AJAX plugin won't do anything.
1633 * @author agorbatchev
1637 function TextExtAjax() {};
1639 $.fn.textext.TextExtAjax = TextExtAjax;
1640 $.fn.textext.addPlugin('ajax', TextExtAjax);
1642 var p = TextExtAjax.prototype,
1645 * AJAX plugin options are grouped under `ajax` when passed to the `$().textext()` function. Be
1646 * mindful that the whole `ajax` object is also passed to jQuery `$.ajax` call which means that
1647 * you can change all jQuery options as well. Please refer to the jQuery documentation on how
1648 * to set url and all other parameters. For example:
1650 * $('textarea').textext({
1657 * **Important**: Because it's necessary to pass options to `jQuery.ajax()` in a single object,
1658 * all jQuery related AJAX options like `url`, `dataType`, etc **must** be within the `ajax` object.
1659 * This is the exception to general rule that TextExt options can be specified in dot or camel case
1662 * @author agorbatchev
1664 * @id TextExtAjax.options
1668 * By default, when user starts typing into the text input, AJAX plugin will start making requests
1669 * to the `url` that you have specified and will pass whatever user has typed so far as a parameter
1670 * named `q`, eg `?q=foo`.
1672 * If you wish to change this behaviour, you can pass a function as a value for this option which
1673 * takes one argument (the user input) and should return a key/value object that will be converted
1674 * to the request parameters. For example:
1676 * 'dataCallback' : function(query)
1678 * return { 'search' : query };
1681 * @name ajax.data.callback
1683 * @author agorbatchev
1685 * @id TextExtAjax.options.data.callback
1687 OPT_DATA_CALLBACK = 'ajax.data.callback',
1690 * By default, the server end point is constantly being reloaded whenever user changes the value
1691 * in the text input. If you'd rather have the client do result filtering, you can return all
1692 * possible results from the server and cache them on the client by setting this option to `true`.
1694 * In such a case, only one call to the server will be made and filtering will be performed on
1695 * the client side using `ItemManager` attached to the core.
1697 * @name ajax.data.results
1699 * @author agorbatchev
1701 * @id TextExtAjax.options.cache.results
1703 OPT_CACHE_RESULTS = 'ajax.cache.results',
1706 * The loading message delay is set in seconds and will specify how long it would take before
1707 * user sees the message. If you don't want user to ever see this message, set the option value
1708 * to `Number.MAX_VALUE`.
1710 * @name ajax.loading.delay
1712 * @author agorbatchev
1714 * @id TextExtAjax.options.loading.delay
1716 OPT_LOADING_DELAY = 'ajax.loading.delay',
1719 * Whenever an AJAX request is made and the server takes more than the number of seconds specified
1720 * in `ajax.loading.delay` to respond, the message specified in this option will appear in the drop
1723 * @name ajax.loading.message
1724 * @default "Loading..."
1725 * @author agorbatchev
1727 * @id TextExtAjax.options.loading.message
1729 OPT_LOADING_MESSAGE = 'ajax.loading.message',
1732 * When user is typing in or otherwise changing the value of the text input, it's undesirable to make
1733 * an AJAX request for every keystroke. Instead it's more conservative to send a request every number
1734 * of seconds while user is typing the value. This number of seconds is specified by the `ajax.type.delay`
1737 * @name ajax.type.delay
1739 * @author agorbatchev
1741 * @id TextExtAjax.options.type.delay
1743 OPT_TYPE_DELAY = 'ajax.type.delay',
1746 * AJAX plugin dispatches or reacts to the following events.
1748 * @author agorbatchev
1750 * @id TextExtAjax.events
1754 * AJAX plugin reacts to the `getSuggestions` event dispatched by the Autocomplete plugin.
1756 * @name getSuggestions
1757 * @author agorbatchev
1759 * @id TextExtAjax.events.getSuggestions
1763 * In the event of successful AJAX request, the AJAX coponent dispatches the `setSuggestions`
1764 * event meant to be recieved by the Autocomplete plugin.
1766 * @name setSuggestions
1767 * @author agorbatchev
1769 * @id TextExtAjax.events.setSuggestions
1771 EVENT_SET_SUGGESTION = 'setSuggestions',
1774 * AJAX plugin dispatches the `showDropdown` event which Autocomplete plugin is expecting.
1775 * This is used to temporarily show the loading message if the AJAX request is taking longer
1778 * @name showDropdown
1779 * @author agorbatchev
1781 * @id TextExtAjax.events.showDropdown
1783 EVENT_SHOW_DROPDOWN = 'showDropdown',
1785 TIMER_LOADING = 'loading',
1790 loadingMessage : 'Loading...',
1792 cacheResults : false,
1799 * Initialization method called by the core during plugin instantiation.
1801 * @signature TextExtAjax.init(core)
1803 * @param core {TextExt} Instance of the TextExt core class.
1805 * @author agorbatchev
1807 * @id TextExtAjax.init
1809 p.init = function(core)
1813 self.baseInit(core, DEFAULT_OPTS);
1816 getSuggestions : self.onGetSuggestions
1819 self._suggestions = null;
1823 * Performas an async AJAX with specified options.
1825 * @signature TextExtAjax.load(query)
1827 * @param query {String} Value that user has typed into the text area which is
1828 * presumably the query.
1830 * @author agorbatchev
1832 * @id TextExtAjax.load
1834 p.load = function(query)
1837 dataCallback = self.opts(OPT_DATA_CALLBACK) || function(query) { return { q : query } },
1841 opts = $.extend(true,
1843 data : dataCallback(query),
1844 success : function(data) { self.onComplete(data, query) },
1845 error : function(jqXHR, message) { console.error(message, query) }
1854 * Successful call AJAX handler. Takes the data that came back from AJAX and the
1855 * original query that was used to make the call.
1857 * @signature TextExtAjax.onComplete(data, query)
1859 * @param data {Object} Data loaded from the server, should be an Array of strings
1860 * by default or whatever data structure your custom `ItemManager` implements.
1862 * @param query {String} Query string, ie whatever user has typed in.
1864 * @author agorbatchev
1866 * @id TextExtAjax.onComplete
1868 p.onComplete = function(data, query)
1874 self.dontShowLoading();
1876 // If results are expected to be cached, then we store the original
1877 // data set and return the filtered one based on the original query.
1878 // That means we do filtering on the client side, instead of the
1880 if(self.opts(OPT_CACHE_RESULTS) == true)
1882 self._suggestions = data;
1883 result = self.itemManager().filter(data, query);
1886 self.trigger(EVENT_SET_SUGGESTION, { result : result });
1890 * If show loading message timer was started, calling this function disables it,
1891 * otherwise nothing else happens.
1893 * @signature TextExtAjax.dontShowLoading()
1895 * @author agorbatchev
1897 * @id TextExtAjax.dontShowLoading
1899 p.dontShowLoading = function()
1901 this.stopTimer(TIMER_LOADING);
1905 * Shows message specified in `ajax.loading.message` if loading data takes more than
1906 * number of seconds specified in `ajax.loading.delay`.
1908 * @signature TextExtAjax.showLoading()
1910 * @author agorbatchev
1912 * @id TextExtAjax.showLoading
1914 p.showLoading = function()
1918 self.dontShowLoading();
1921 self.opts(OPT_LOADING_DELAY),
1924 self.trigger(EVENT_SHOW_DROPDOWN, function(autocomplete)
1926 autocomplete.clearItems();
1927 var node = autocomplete.addDropdownItem(self.opts(OPT_LOADING_MESSAGE));
1928 node.addClass('text-loading');
1935 * Reacts to the `getSuggestions` event and begin loading suggestions. If
1936 * `ajax.cache.results` is specified, all calls after the first one will use
1937 * cached data and filter it with the `core.itemManager.filter()`.
1939 * @signature TextExtAjax.onGetSuggestions(e, data)
1941 * @param e {Object} jQuery event.
1942 * @param data {Object} Data structure passed with the `getSuggestions` event
1943 * which contains the user query, eg `{ query : "..." }`.
1945 * @author agorbatchev
1947 * @id TextExtAjax.onGetSuggestions
1949 p.onGetSuggestions = function(e, data)
1952 suggestions = self._suggestions,
1953 query = (data || {}).query || ''
1956 if(suggestions && self.opts(OPT_CACHE_RESULTS) === true)
1957 return self.onComplete(suggestions, query);
1961 self.opts(OPT_TYPE_DELAY),
1971 * jQuery TextExt Plugin
1972 * http://textextjs.com
1975 * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
1976 * @license MIT License
1981 * Displays a dropdown style arrow button. The `TextExtArrow` works together with the
1982 * `TextExtAutocomplete` plugin and whenever clicked tells the autocomplete plugin to
1983 * display its suggestions.
1985 * @author agorbatchev
1989 function TextExtArrow() {};
1991 $.fn.textext.TextExtArrow = TextExtArrow;
1992 $.fn.textext.addPlugin('arrow', TextExtArrow);
1994 var p = TextExtArrow.prototype,
1996 * Arrow plugin only has one option and that is its HTML template. It could be
1997 * changed when passed to the `$().textext()` function. For example:
1999 * $('textarea').textext({
2006 * @author agorbatchev
2008 * @id TextExtArrow.options
2012 * HTML source that is used to generate markup required for the arrow.
2015 * @default '<div class="text-arrow"/>'
2016 * @author agorbatchev
2018 * @id TextExtArrow.options.html.arrow
2020 OPT_HTML_ARROW = 'html.arrow',
2024 arrow : '<div class="text-arrow"/>'
2030 * Initialization method called by the core during plugin instantiation.
2032 * @signature TextExtArrow.init(core)
2034 * @param core {TextExt} Instance of the TextExt core class.
2036 * @author agorbatchev
2038 * @id TextExtArrow.init
2040 p.init = function(core)
2046 self.baseInit(core, DEFAULT_OPTS);
2048 self._arrow = arrow = $(self.opts(OPT_HTML_ARROW));
2049 self.core().wrapElement().append(arrow);
2050 arrow.bind('click', function(e) { self.onArrowClick(e); });
2053 //--------------------------------------------------------------------------------
2057 * Reacts to the `click` event whenever user clicks the arrow.
2059 * @signature TextExtArrow.onArrowClick(e)
2061 * @param e {Object} jQuery event.
2062 * @author agorbatchev
2064 * @id TextExtArrow.onArrowClick
2066 p.onArrowClick = function(e)
2068 this.trigger('toggleDropdown');
2069 this.core().focusInput();
2072 //--------------------------------------------------------------------------------
2073 // Core functionality
2077 * jQuery TextExt Plugin
2078 * http://textextjs.com
2081 * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
2082 * @license MIT License
2087 * Autocomplete plugin brings the classic autocomplete functionality to the TextExt echosystem.
2088 * The gist of functionality is when user starts typing in, for example a term or a tag, a
2089 * dropdown would be presented with possible suggestions to complete the input quicker.
2091 * @author agorbatchev
2093 * @id TextExtAutocomplete
2095 function TextExtAutocomplete() {};
2097 $.fn.textext.TextExtAutocomplete = TextExtAutocomplete;
2098 $.fn.textext.addPlugin('autocomplete', TextExtAutocomplete);
2100 var p = TextExtAutocomplete.prototype,
2103 CSS_SELECTED = 'text-selected',
2104 CSS_DOT_SELECTED = CSS_DOT + CSS_SELECTED,
2105 CSS_SUGGESTION = 'text-suggestion',
2106 CSS_DOT_SUGGESTION = CSS_DOT + CSS_SUGGESTION,
2107 CSS_LABEL = 'text-label',
2108 CSS_DOT_LABEL = CSS_DOT + CSS_LABEL,
2111 * Autocomplete plugin options are grouped under `autocomplete` when passed to the
2112 * `$().textext()` function. For example:
2114 * $('textarea').textext({
2115 * plugins: 'autocomplete',
2117 * dropdownPosition: 'above'
2121 * @author agorbatchev
2123 * @id TextExtAutocomplete.options
2127 * This is a toggle switch to enable or disable the Autucomplete plugin. The value is checked
2128 * each time at the top level which allows you to toggle this setting on the fly.
2130 * @name autocomplete.enabled
2132 * @author agorbatchev
2134 * @id TextExtAutocomplete.options.autocomplete.enabled
2136 OPT_ENABLED = 'autocomplete.enabled',
2139 * This option allows to specify position of the dropdown. The two possible values
2140 * are `above` and `below`.
2142 * @name autocomplete.dropdown.position
2144 * @author agorbatchev
2146 * @id TextExtAutocomplete.options.autocomplete.dropdown.position
2148 OPT_POSITION = 'autocomplete.dropdown.position',
2151 * This option allows to specify maximum height of the dropdown. Value is taken directly, so
2152 * if desired height is 200 pixels, value must be `200px`.
2154 * @name autocomplete.dropdown.maxHeight
2156 * @author agorbatchev
2158 * @id TextExtAutocomplete.options.autocomplete.dropdown.maxHeight
2161 OPT_MAX_HEIGHT = 'autocomplete.dropdown.maxHeight',
2164 * This option allows to override how a suggestion item is rendered. The value should be
2165 * a function, the first argument of which is suggestion to be rendered and `this` context
2166 * is the current instance of `TextExtAutocomplete`.
2168 * [Click here](/manual/examples/autocomplete-with-custom-render.html) to see a demo.
2172 * $('textarea').textext({
2173 * plugins: 'autocomplete',
2175 * render: function(suggestion)
2177 * return '<b>' + suggestion + '</b>';
2182 * @name autocomplete.render
2184 * @author agorbatchev
2186 * @id TextExtAutocomplete.options.autocomplete.render
2189 OPT_RENDER = 'autocomplete.render',
2192 * HTML source that is used to generate the dropdown.
2194 * @name html.dropdown
2195 * @default '<div class="text-dropdown"><div class="text-list"/></div>'
2196 * @author agorbatchev
2198 * @id TextExtAutocomplete.options.html.dropdown
2200 OPT_HTML_DROPDOWN = 'html.dropdown',
2203 * HTML source that is used to generate each suggestion.
2205 * @name html.suggestion
2206 * @default '<div class="text-suggestion"><span class="text-label"/></div>'
2207 * @author agorbatchev
2209 * @id TextExtAutocomplete.options.html.suggestion
2211 OPT_HTML_SUGGESTION = 'html.suggestion',
2214 * Autocomplete plugin triggers or reacts to the following events.
2216 * @author agorbatchev
2218 * @id TextExtAutocomplete.events
2222 * Autocomplete plugin triggers and reacts to the `hideDropdown` to hide the dropdown if it's
2225 * @name hideDropdown
2226 * @author agorbatchev
2228 * @id TextExtAutocomplete.events.hideDropdown
2230 EVENT_HIDE_DROPDOWN = 'hideDropdown',
2233 * Autocomplete plugin triggers and reacts to the `showDropdown` to show the dropdown if it's
2234 * not already visible.
2236 * It's possible to pass a render callback function which will be called instead of the
2237 * default `TextExtAutocomplete.renderSuggestions()`.
2239 * Here's how another plugin should trigger this event with the optional render callback:
2241 * this.trigger('showDropdown', function(autocomplete)
2243 * autocomplete.clearItems();
2244 * var node = autocomplete.addDropdownItem('<b>Item</b>');
2245 * node.addClass('new-look');
2248 * @name showDropdown
2249 * @author agorbatchev
2251 * @id TextExtAutocomplete.events.showDropdown
2253 EVENT_SHOW_DROPDOWN = 'showDropdown',
2256 * Autocomplete plugin reacts to the `setSuggestions` event triggered by other plugins which
2257 * wish to populate the suggestion items. Suggestions should be passed as event argument in the
2258 * following format: `{ data : [ ... ] }`.
2260 * Here's how another plugin should trigger this event:
2262 * this.trigger('setSuggestions', { data : [ "item1", "item2" ] });
2264 * @name setSuggestions
2265 * @author agorbatchev
2267 * @id TextExtAutocomplete.events.setSuggestions
2271 * Autocomplete plugin triggers the `getSuggestions` event and expects to get results by listening for
2272 * the `setSuggestions` event.
2274 * @name getSuggestions
2275 * @author agorbatchev
2277 * @id TextExtAutocomplete.events.getSuggestions
2279 EVENT_GET_SUGGESTIONS = 'getSuggestions',
2282 * Autocomplete plugin triggers `getFormData` event with the current suggestion so that the the core
2283 * will be updated with serialized data to be submitted with the HTML form.
2286 * @author agorbatchev
2288 * @id TextExtAutocomplete.events.getFormData
2290 EVENT_GET_FORM_DATA = 'getFormData',
2293 * Autocomplete plugin reacts to `toggleDropdown` event and either shows or hides the dropdown
2294 * depending if it's currently hidden or visible.
2296 * @name toggleDropdown
2297 * @author agorbatchev
2299 * @id TextExtAutocomplete.events.toggleDropdown
2302 EVENT_TOGGLE_DROPDOWN = 'toggleDropdown',
2304 POSITION_ABOVE = 'above',
2305 POSITION_BELOW = 'below',
2307 DATA_MOUSEDOWN_ON_AUTOCOMPLETE = 'mousedownOnAutocomplete',
2313 position : POSITION_BELOW,
2319 dropdown : '<div class="text-dropdown"><div class="text-list"/></div>',
2320 suggestion : '<div class="text-suggestion"><span class="text-label"/></div>'
2326 * Initialization method called by the core during plugin instantiation.
2328 * @signature TextExtAutocomplete.init(core)
2330 * @param core {TextExt} Instance of the TextExt core class.
2332 * @author agorbatchev
2334 * @id TextExtAutocomplete.init
2336 p.init = function(core)
2340 self.baseInit(core, DEFAULT_OPTS);
2342 var input = self.input(),
2346 if(self.opts(OPT_ENABLED) === true)
2350 anyKeyUp : self.onAnyKeyUp,
2351 deleteKeyUp : self.onAnyKeyUp,
2352 backspaceKeyPress : self.onBackspaceKeyPress,
2353 enterKeyPress : self.onEnterKeyPress,
2354 escapeKeyPress : self.onEscapeKeyPress,
2355 setSuggestions : self.onSetSuggestions,
2356 showDropdown : self.onShowDropdown,
2357 hideDropdown : self.onHideDropdown,
2358 toggleDropdown : self.onToggleDropdown,
2359 postInvalidate : self.positionDropdown,
2360 getFormData : self.onGetFormData,
2362 // using keyDown for up/down keys so that repeat events are
2363 // captured and user can scroll up/down by holding the keys
2364 downKeyDown : self.onDownKeyDown,
2365 upKeyDown : self.onUpKeyDown
2368 container = $(self.opts(OPT_HTML_DROPDOWN));
2369 container.insertAfter(input);
2371 self.on(container, {
2372 mouseover : self.onMouseOver,
2373 mousedown : self.onMouseDown,
2374 click : self.onClick
2378 .css('maxHeight', self.opts(OPT_MAX_HEIGHT))
2379 .addClass('text-position-' + self.opts(OPT_POSITION))
2382 $(self).data('container', container);
2384 $(document.body).click(function(e)
2386 if (self.isDropdownVisible() && !self.withinWrapElement(e.target))
2387 self.trigger(EVENT_HIDE_DROPDOWN);
2390 self.positionDropdown();
2395 * Returns top level dropdown container HTML element.
2397 * @signature TextExtAutocomplete.containerElement()
2399 * @author agorbatchev
2401 * @id TextExtAutocomplete.containerElement
2403 p.containerElement = function()
2405 return $(this).data('container');
2408 //--------------------------------------------------------------------------------
2409 // User mouse/keyboard input
2412 * Reacts to the `mouseOver` event triggered by the TextExt core.
2414 * @signature TextExtAutocomplete.onMouseOver(e)
2416 * @param e {Object} jQuery event.
2418 * @author agorbatchev
2420 * @id TextExtAutocomplete.onMouseOver
2422 p.onMouseOver = function(e)
2425 target = $(e.target)
2428 if(target.is(CSS_DOT_SUGGESTION))
2430 self.clearSelected();
2431 target.addClass(CSS_SELECTED);
2436 * Reacts to the `mouseDown` event triggered by the TextExt core.
2438 * @signature TextExtAutocomplete.onMouseDown(e)
2440 * @param e {Object} jQuery event.
2444 * @id TextExtAutocomplete.onMouseDown
2446 p.onMouseDown = function(e)
2448 this.containerElement().data(DATA_MOUSEDOWN_ON_AUTOCOMPLETE, true);
2452 * Reacts to the `click` event triggered by the TextExt core.
2454 * @signature TextExtAutocomplete.onClick(e)
2456 * @param e {Object} jQuery event.
2458 * @author agorbatchev
2460 * @id TextExtAutocomplete.onClick
2462 p.onClick = function(e)
2465 target = $(e.target)
2468 if(target.is(CSS_DOT_SUGGESTION) || target.is(CSS_DOT_LABEL))
2469 self.trigger('enterKeyPress');
2471 if (self.core().hasPlugin('tags'))
2476 * Reacts to the `blur` event triggered by the TextExt core.
2478 * @signature TextExtAutocomplete.onBlur(e)
2480 * @param e {Object} jQuery event.
2482 * @author agorbatchev
2484 * @id TextExtAutocomplete.onBlur
2486 p.onBlur = function(e)
2489 container = self.containerElement(),
2490 isBlurByMousedown = container.data(DATA_MOUSEDOWN_ON_AUTOCOMPLETE) === true
2493 // only trigger a close event if the blur event was
2494 // not triggered by a mousedown event on the autocomplete
2495 // otherwise set focus back back on the input
2496 if(self.isDropdownVisible())
2497 isBlurByMousedown ? self.core().focusInput() : self.trigger(EVENT_HIDE_DROPDOWN);
2499 container.removeData(DATA_MOUSEDOWN_ON_AUTOCOMPLETE);
2503 * Reacts to the `backspaceKeyPress` event triggered by the TextExt core.
2505 * @signature TextExtAutocomplete.onBackspaceKeyPress(e)
2507 * @param e {Object} jQuery event.
2509 * @author agorbatchev
2511 * @id TextExtAutocomplete.onBackspaceKeyPress
2513 p.onBackspaceKeyPress = function(e)
2516 isEmpty = self.val().length > 0
2519 if(isEmpty || self.isDropdownVisible())
2520 self.getSuggestions();
2524 * Reacts to the `anyKeyUp` event triggered by the TextExt core.
2526 * @signature TextExtAutocomplete.onAnyKeyUp(e)
2528 * @param e {Object} jQuery event.
2530 * @author agorbatchev
2532 * @id TextExtAutocomplete.onAnyKeyUp
2534 p.onAnyKeyUp = function(e, keyCode)
2537 isFunctionKey = self.opts('keys.' + keyCode) != null
2540 if(self.val().length > 0 && !isFunctionKey)
2541 self.getSuggestions();
2545 * Reacts to the `downKeyDown` event triggered by the TextExt core.
2547 * @signature TextExtAutocomplete.onDownKeyDown(e)
2549 * @param e {Object} jQuery event.
2551 * @author agorbatchev
2553 * @id TextExtAutocomplete.onDownKeyDown
2555 p.onDownKeyDown = function(e)
2559 self.isDropdownVisible()
2560 ? self.toggleNextSuggestion()
2561 : self.getSuggestions()
2566 * Reacts to the `upKeyDown` event triggered by the TextExt core.
2568 * @signature TextExtAutocomplete.onUpKeyDown(e)
2570 * @param e {Object} jQuery event.
2572 * @author agorbatchev
2574 * @id TextExtAutocomplete.onUpKeyDown
2576 p.onUpKeyDown = function(e)
2578 this.togglePreviousSuggestion();
2582 * Reacts to the `enterKeyPress` event triggered by the TextExt core.
2584 * @signature TextExtAutocomplete.onEnterKeyPress(e)
2586 * @param e {Object} jQuery event.
2588 * @author agorbatchev
2590 * @id TextExtAutocomplete.onEnterKeyPress
2592 p.onEnterKeyPress = function(e)
2596 if(self.isDropdownVisible())
2597 self.selectFromDropdown();
2601 * Reacts to the `escapeKeyPress` event triggered by the TextExt core. Hides the dropdown
2602 * if it's currently visible.
2604 * @signature TextExtAutocomplete.onEscapeKeyPress(e)
2606 * @param e {Object} jQuery event.
2608 * @author agorbatchev
2610 * @id TextExtAutocomplete.onEscapeKeyPress
2612 p.onEscapeKeyPress = function(e)
2616 if(self.isDropdownVisible())
2617 self.trigger(EVENT_HIDE_DROPDOWN);
2620 //--------------------------------------------------------------------------------
2621 // Core functionality
2624 * Positions dropdown either below or above the input based on the `autocomplete.dropdown.position`
2625 * option specified, which could be either `above` or `below`.
2627 * @signature TextExtAutocomplete.positionDropdown()
2629 * @author agorbatchev
2631 * @id TextExtAutocomplete.positionDropdown
2633 p.positionDropdown = function()
2636 container = self.containerElement(),
2637 direction = self.opts(OPT_POSITION),
2638 height = self.core().wrapElement().outerHeight(),
2642 css[direction === POSITION_ABOVE ? 'bottom' : 'top'] = height + 'px';
2647 * Returns list of all the suggestion HTML elements in the dropdown.
2649 * @signature TextExtAutocomplete.suggestionElements()
2651 * @author agorbatchev
2653 * @id TextExtAutocomplete.suggestionElements
2655 p.suggestionElements = function()
2657 return this.containerElement().find(CSS_DOT_SUGGESTION);
2662 * Highlights specified suggestion as selected in the dropdown.
2664 * @signature TextExtAutocomplete.setSelectedSuggestion(suggestion)
2666 * @param suggestion {Object} Suggestion object. With the default `ItemManager` this
2667 * is expected to be a string, anything else with custom implementations.
2669 * @author agorbatchev
2671 * @id TextExtAutocomplete.setSelectedSuggestion
2673 p.setSelectedSuggestion = function(suggestion)
2679 all = self.suggestionElements(),
2680 target = all.first(),
2684 self.clearSelected();
2686 for(i = 0; i < all.length; i++)
2690 if(self.itemManager().compareItems(item.data(CSS_SUGGESTION), suggestion))
2692 target = item.addClass(CSS_SELECTED);
2697 target.addClass(CSS_SELECTED);
2698 self.scrollSuggestionIntoView(target);
2702 * Returns the first suggestion HTML element from the dropdown that is highlighted as selected.
2704 * @signature TextExtAutocomplete.selectedSuggestionElement()
2706 * @author agorbatchev
2708 * @id TextExtAutocomplete.selectedSuggestionElement
2710 p.selectedSuggestionElement = function()
2712 return this.suggestionElements().filter(CSS_DOT_SELECTED).first();
2716 * Returns `true` if dropdown is currently visible, `false` otherwise.
2718 * @signature TextExtAutocomplete.isDropdownVisible()
2720 * @author agorbatchev
2722 * @id TextExtAutocomplete.isDropdownVisible
2724 p.isDropdownVisible = function()
2726 return this.containerElement().is(':visible') === true;
2730 * Reacts to the `getFormData` event triggered by the core. Returns data with the
2731 * weight of 100 to be *less than the Tags plugin* data weight. The weights system is
2732 * covered in greater detail in the [`getFormData`][1] event documentation.
2734 * [1]: /manual/textext.html#getformdata
2736 * @signature TextExtAutocomplete.onGetFormData(e, data, keyCode)
2738 * @param e {Object} jQuery event.
2739 * @param data {Object} Data object to be populated.
2740 * @param keyCode {Number} Key code that triggered the original update request.
2742 * @author agorbatchev
2744 * @id TextExtAutocomplete.onGetFormData
2746 p.onGetFormData = function(e, data, keyCode)
2753 data[100] = self.formDataObject(inputValue, formValue);
2757 * Returns initialization priority of the Autocomplete plugin which is expected to be
2758 * *greater than the Tags plugin* because of the dependencies. The value is 200.
2760 * @signature TextExtAutocomplete.initPriority()
2762 * @author agorbatchev
2764 * @id TextExtAutocomplete.initPriority
2766 p.initPriority = function()
2772 * Reacts to the `hideDropdown` event and hides the dropdown if it's already visible.
2774 * @signature TextExtAutocomplete.onHideDropdown(e)
2776 * @param e {Object} jQuery event.
2778 * @author agorbatchev
2780 * @id TextExtAutocomplete.onHideDropdown
2782 p.onHideDropdown = function(e)
2784 this.hideDropdown();
2788 * Reacts to the 'toggleDropdown` event and shows or hides the dropdown depending if
2789 * it's currently hidden or visible.
2791 * @signature TextExtAutocomplete.onToggleDropdown(e)
2793 * @param e {Object} jQuery event.
2795 * @author agorbatchev
2797 * @id TextExtAutocomplete.onToggleDropdown
2800 p.onToggleDropdown = function(e)
2803 self.trigger(self.containerElement().is(':visible') ? EVENT_HIDE_DROPDOWN : EVENT_SHOW_DROPDOWN);
2807 * Reacts to the `showDropdown` event and shows the dropdown if it's not already visible.
2808 * It's possible to pass a render callback function which will be called instead of the
2809 * default `TextExtAutocomplete.renderSuggestions()`.
2811 * If no suggestion were previously loaded, it will fire `getSuggestions` event and exit.
2813 * Here's how another plugin should trigger this event with the optional render callback:
2815 * this.trigger('showDropdown', function(autocomplete)
2817 * autocomplete.clearItems();
2818 * var node = autocomplete.addDropdownItem('<b>Item</b>');
2819 * node.addClass('new-look');
2822 * @signature TextExtAutocomplete.onShowDropdown(e, renderCallback)
2824 * @param e {Object} jQuery event.
2825 * @param renderCallback {Function} Optional callback function which would be used to
2826 * render dropdown items. As a first argument, reference to the current instance of
2827 * Autocomplete plugin will be supplied. It's assumed, that if this callback is provided
2828 * rendering will be handled completely manually.
2830 * @author agorbatchev
2832 * @id TextExtAutocomplete.onShowDropdown
2834 p.onShowDropdown = function(e, renderCallback)
2837 current = self.selectedSuggestionElement().data(CSS_SUGGESTION),
2838 suggestions = self._suggestions
2842 return self.trigger(EVENT_GET_SUGGESTIONS);
2844 if($.isFunction(renderCallback))
2846 renderCallback(self);
2850 self.renderSuggestions(self._suggestions);
2851 self.toggleNextSuggestion();
2854 self.showDropdown(self.containerElement());
2855 self.setSelectedSuggestion(current);
2859 * Reacts to the `setSuggestions` event. Expects to recieve the payload as the second argument
2860 * in the following structure:
2863 * result : [ "item1", "item2" ],
2864 * showHideDropdown : false
2867 * Notice the optional `showHideDropdown` option. By default, ie without the `showHideDropdown`
2868 * value the method will trigger either `showDropdown` or `hideDropdown` depending if there are
2869 * suggestions. If set to `false`, no event is triggered.
2871 * @signature TextExtAutocomplete.onSetSuggestions(e, data)
2873 * @param data {Object} Data payload.
2875 * @author agorbatchev
2877 * @id TextExtAutocomplete.onSetSuggestions
2879 p.onSetSuggestions = function(e, data)
2882 suggestions = self._suggestions = data.result
2885 if(data.showHideDropdown !== false)
2886 self.trigger(suggestions === null || suggestions.length === 0 ? EVENT_HIDE_DROPDOWN : EVENT_SHOW_DROPDOWN);
2890 * Prepears for and triggers the `getSuggestions` event with the `{ query : {String} }` as second
2893 * @signature TextExtAutocomplete.getSuggestions()
2895 * @author agorbatchev
2897 * @id TextExtAutocomplete.getSuggestions
2899 p.getSuggestions = function()
2905 if(self._previousInputValue == val)
2908 // if user clears input, then we want to select first suggestion
2909 // instead of the last one
2913 self._previousInputValue = val;
2914 self.trigger(EVENT_GET_SUGGESTIONS, { query : val });
2918 * Removes all HTML suggestion items from the dropdown.
2920 * @signature TextExtAutocomplete.clearItems()
2922 * @author agorbatchev
2924 * @id TextExtAutocomplete.clearItems
2926 p.clearItems = function()
2928 this.containerElement().find('.text-list').children().remove();
2932 * Clears all and renders passed suggestions.
2934 * @signature TextExtAutocomplete.renderSuggestions(suggestions)
2936 * @param suggestions {Array} List of suggestions to render.
2938 * @author agorbatchev
2940 * @id TextExtAutocomplete.renderSuggestions
2942 p.renderSuggestions = function(suggestions)
2948 $.each(suggestions || [], function(index, item)
2950 self.addSuggestion(item);
2955 * Shows the dropdown.
2957 * @signature TextExtAutocomplete.showDropdown()
2959 * @author agorbatchev
2961 * @id TextExtAutocomplete.showDropdown
2963 p.showDropdown = function()
2965 this.containerElement().show();
2969 * Hides the dropdown.
2971 * @signature TextExtAutocomplete.hideDropdown()
2973 * @author agorbatchev
2975 * @id TextExtAutocomplete.hideDropdown
2977 p.hideDropdown = function()
2980 dropdown = self.containerElement()
2983 self._previousInputValue = null;
2988 * Adds single suggestion to the bottom of the dropdown. Uses `ItemManager.itemToString()` to
2989 * serialize provided suggestion to string.
2991 * @signature TextExtAutocomplete.addSuggestion(suggestion)
2993 * @param suggestion {Object} Suggestion item. By default expected to be a string.
2995 * @author agorbatchev
2997 * @id TextExtAutocomplete.addSuggestion
2999 p.addSuggestion = function(suggestion)
3002 renderer = self.opts(OPT_RENDER),
3003 node = self.addDropdownItem(renderer ? renderer.call(self, suggestion) : self.itemManager().itemToString(suggestion))
3006 node.data(CSS_SUGGESTION, suggestion);
3010 * Adds and returns HTML node to the bottom of the dropdown.
3012 * @signature TextExtAutocomplete.addDropdownItem(html)
3014 * @param html {String} HTML to be inserted into the item.
3016 * @author agorbatchev
3018 * @id TextExtAutocomplete.addDropdownItem
3020 p.addDropdownItem = function(html)
3023 container = self.containerElement().find('.text-list'),
3024 node = $(self.opts(OPT_HTML_SUGGESTION))
3027 node.find('.text-label').html(html);
3028 container.append(node);
3033 * Removes selection highlight from all suggestion elements.
3035 * @signature TextExtAutocomplete.clearSelected()
3037 * @author agorbatchev
3039 * @id TextExtAutocomplete.clearSelected
3041 p.clearSelected = function()
3043 this.suggestionElements().removeClass(CSS_SELECTED);
3047 * Selects next suggestion relative to the current one. If there's no
3048 * currently selected suggestion, it will select the first one. Selected
3049 * suggestion will always be scrolled into view.
3051 * @signature TextExtAutocomplete.toggleNextSuggestion()
3053 * @author agorbatchev
3055 * @id TextExtAutocomplete.toggleNextSuggestion
3057 p.toggleNextSuggestion = function()
3060 selected = self.selectedSuggestionElement(),
3064 if(selected.length > 0)
3066 next = selected.next();
3069 selected.removeClass(CSS_SELECTED);
3073 next = self.suggestionElements().first();
3076 next.addClass(CSS_SELECTED);
3077 self.scrollSuggestionIntoView(next);
3081 * Selects previous suggestion relative to the current one. Selected
3082 * suggestion will always be scrolled into view.
3084 * @signature TextExtAutocomplete.togglePreviousSuggestion()
3086 * @author agorbatchev
3088 * @id TextExtAutocomplete.togglePreviousSuggestion
3090 p.togglePreviousSuggestion = function()
3093 selected = self.selectedSuggestionElement(),
3094 prev = selected.prev()
3097 if(prev.length == 0)
3100 self.clearSelected();
3101 prev.addClass(CSS_SELECTED);
3102 self.scrollSuggestionIntoView(prev);
3106 * Scrolls specified HTML suggestion element into the view.
3108 * @signature TextExtAutocomplete.scrollSuggestionIntoView(item)
3110 * @param item {HTMLElement} jQuery HTML suggestion element which needs to
3111 * scrolled into view.
3113 * @author agorbatchev
3115 * @id TextExtAutocomplete.scrollSuggestionIntoView
3117 p.scrollSuggestionIntoView = function(item)
3119 var itemHeight = item.outerHeight(),
3120 dropdown = this.containerElement(),
3121 dropdownHeight = dropdown.innerHeight(),
3122 scrollPos = dropdown.scrollTop(),
3123 itemTop = (item.position() || {}).top,
3125 paddingTop = parseInt(dropdown.css('paddingTop'))
3131 // if scrolling down and item is below the bottom fold
3132 if(itemTop + itemHeight > dropdownHeight)
3133 scrollTo = itemTop + scrollPos + itemHeight - dropdownHeight + paddingTop;
3135 // if scrolling up and item is above the top fold
3137 scrollTo = itemTop + scrollPos - paddingTop;
3139 if(scrollTo != null)
3140 dropdown.scrollTop(scrollTo);
3144 * Uses the value from the text input to finish autocomplete action. Currently selected
3145 * suggestion from the dropdown will be used to complete the action. Triggers `hideDropdown`
3148 * @signature TextExtAutocomplete.selectFromDropdown()
3150 * @author agorbatchev
3152 * @id TextExtAutocomplete.selectFromDropdown
3154 p.selectFromDropdown = function()
3157 suggestion = self.selectedSuggestionElement().data(CSS_SUGGESTION)
3162 self.val(self.itemManager().itemToString(suggestion));
3163 self.core().getFormData();
3166 self.trigger(EVENT_HIDE_DROPDOWN);
3170 * Determines if the specified HTML element is within the TextExt core wrap HTML element.
3172 * @signature TextExtAutocomplete.withinWrapElement(element)
3174 * @param element {HTMLElement} element to check if contained by wrap element
3179 * @id TextExtAutocomplete.withinWrapElement
3181 p.withinWrapElement = function(element)
3183 return this.core().wrapElement().find(element).size() > 0;
3187 * jQuery TextExt Plugin
3188 * http://textextjs.com
3191 * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
3192 * @license MIT License
3197 * The Filter plugin introduces ability to limit input that the text field
3198 * will accept. If the Tags plugin is used, Filter plugin will limit which
3199 * tags it's possible to add.
3201 * The list of allowed items can be either specified through the
3202 * options, can come from the Suggestions plugin or be loaded by the Ajax
3203 * plugin. All these plugins have one thing in common -- they
3204 * trigger `setSuggestions` event which the Filter plugin is expecting.
3206 * @author agorbatchev
3210 function TextExtFilter() {};
3212 $.fn.textext.TextExtFilter = TextExtFilter;
3213 $.fn.textext.addPlugin('filter', TextExtFilter);
3215 var p = TextExtFilter.prototype,
3218 * Filter plugin options are grouped under `filter` when passed to the
3219 * `$().textext()` function. For example:
3221 * $('textarea').textext({
3222 * plugins: 'filter',
3224 * items: [ "item1", "item2" ]
3228 * @author agorbatchev
3230 * @id TextExtFilter.options
3234 * This is a toggle switch to enable or disable the Filter plugin. The value is checked
3235 * each time at the top level which allows you to toggle this setting on the fly.
3237 * @name filter.enabled
3239 * @author agorbatchev
3241 * @id TextExtFilter.options.enabled
3243 OPT_ENABLED = 'filter.enabled',
3246 * Arra of items that the Filter plugin will allow the Tag plugin to add to the list of
3247 * its resut tags. Each item by default is expected to be a string which default `ItemManager`
3248 * can work with. You can change the item type by supplying custom `ItemManager`.
3250 * @name filter.items
3252 * @author agorbatchev
3254 * @id TextExtFilter.options.items
3256 OPT_ITEMS = 'filter.items',
3259 * Filter plugin dispatches and reacts to the following events.
3261 * @author agorbatchev
3263 * @id TextExtFilter.events
3267 * Filter plugin reacts to the `isTagAllowed` event triggered by the Tags plugin before
3268 * adding a new tag to the list. If the new tag is among the `items` specified in options,
3269 * then the new tag will be allowed.
3271 * @name isTagAllowed
3272 * @author agorbatchev
3274 * @id TextExtFilter.events.isTagAllowed
3278 * Filter plugin reacts to the `setSuggestions` event triggered by other plugins like
3279 * Suggestions and Ajax.
3281 * However, event if this event is handled and items are passed with it and stored, if `items`
3282 * option was supplied, it will always take precedense.
3284 * @name setSuggestions
3285 * @author agorbatchev
3287 * @id TextExtFilter.events.setSuggestions
3299 * Initialization method called by the core during plugin instantiation.
3301 * @signature TextExtFilter.init(core)
3303 * @param core {TextExt} Instance of the TextExt core class.
3305 * @author agorbatchev
3307 * @id TextExtFilter.init
3309 p.init = function(core)
3312 self.baseInit(core, DEFAULT_OPTS);
3315 getFormData : self.onGetFormData,
3316 isTagAllowed : self.onIsTagAllowed,
3317 setSuggestions : self.onSetSuggestions
3320 self._suggestions = null;
3323 //--------------------------------------------------------------------------------
3324 // Core functionality
3327 * Reacts to the [`getFormData`][1] event triggered by the core. Returns data with the
3328 * weight of 200 to be *greater than the Autocomplete plugins* data weights.
3329 * The weights system is covered in greater detail in the [`getFormData`][1] event
3332 * This method does nothing if Tags tag is also present.
3334 * [1]: /manual/textext.html#getformdata
3336 * @signature TextExtFilter.onGetFormData(e, data, keyCode)
3338 * @param e {Object} jQuery event.
3339 * @param data {Object} Data object to be populated.
3340 * @param keyCode {Number} Key code that triggered the original update request.
3342 * @author agorbatchev
3344 * @id TextExtFilter.onGetFormData
3347 p.onGetFormData = function(e, data, keyCode)
3355 if(!self.core().hasPlugin('tags'))
3357 if(self.isValueAllowed(inputValue))
3360 data[300] = self.formDataObject(inputValue, formValue);
3365 * Checks given value if it's present in `filterItems` or was loaded for the Autocomplete
3366 * or by the Suggestions plugins. `value` is compared to each item using `ItemManager.compareItems`
3367 * method which is currently attached to the core. Returns `true` if value is known or
3368 * Filter plugin is disabled.
3370 * @signature TextExtFilter.isValueAllowed(value)
3372 * @param value {Object} Value to check.
3374 * @author agorbatchev
3376 * @id TextExtFilter.isValueAllowed
3379 p.isValueAllowed = function(value)
3382 list = self.opts('filterItems') || self._suggestions || [],
3383 itemManager = self.itemManager(),
3384 result = !self.opts(OPT_ENABLED), // if disabled, should just return true
3388 for(i = 0; i < list.length && !result; i++)
3389 if(itemManager.compareItems(value, list[i]))
3396 * Handles `isTagAllowed` event dispatched by the Tags plugin. If supplied tag is not
3397 * in the `items` list, method sets `result` on the `data` argument to `false`.
3399 * @signature TextExtFilter.onIsTagAllowed(e, data)
3401 * @param e {Object} jQuery event.
3402 * @param data {Object} Payload in the following format : `{ tag : {Object}, result : {Boolean} }`.
3403 * @author agorbatchev
3405 * @id TextExtFilter.onIsTagAllowed
3407 p.onIsTagAllowed = function(e, data)
3409 data.result = this.isValueAllowed(data.tag);
3413 * Reacts to the `setSuggestions` events and stores supplied suggestions for future use.
3415 * @signature TextExtFilter.onSetSuggestions(e, data)
3417 * @param e {Object} jQuery event.
3418 * @param data {Object} Payload in the following format : `{ result : {Array} } }`.
3419 * @author agorbatchev
3421 * @id TextExtFilter.onSetSuggestions
3423 p.onSetSuggestions = function(e, data)
3425 this._suggestions = data.result;
3429 * jQuery TextExt Plugin
3430 * http://textextjs.com
3433 * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
3434 * @license MIT License
3439 * Focus plugin displays a visual effect whenever user sets focus
3440 * into the text area.
3442 * @author agorbatchev
3446 function TextExtFocus() {};
3448 $.fn.textext.TextExtFocus = TextExtFocus;
3449 $.fn.textext.addPlugin('focus', TextExtFocus);
3451 var p = TextExtFocus.prototype,
3453 * Focus plugin only has one option and that is its HTML template. It could be
3454 * changed when passed to the `$().textext()` function. For example:
3456 * $('textarea').textext({
3463 * @author agorbatchev
3465 * @id TextExtFocus.options
3469 * HTML source that is used to generate markup required for the focus effect.
3472 * @default '<div class="text-focus"/>'
3473 * @author agorbatchev
3475 * @id TextExtFocus.options.html.focus
3477 OPT_HTML_FOCUS = 'html.focus',
3480 * Focus plugin dispatches or reacts to the following events.
3482 * @author agorbatchev
3484 * @id TextExtFocus.events
3488 * Focus plugin reacts to the `focus` event and shows the markup generated from
3489 * the `html.focus` option.
3492 * @author agorbatchev
3494 * @id TextExtFocus.events.focus
3498 * Focus plugin reacts to the `blur` event and hides the effect.
3501 * @author agorbatchev
3503 * @id TextExtFocus.events.blur
3508 focus : '<div class="text-focus"/>'
3514 * Initialization method called by the core during plugin instantiation.
3516 * @signature TextExtFocus.init(core)
3518 * @param core {TextExt} Instance of the TextExt core class.
3520 * @author agorbatchev
3522 * @id TextExtFocus.init
3524 p.init = function(core)
3528 self.baseInit(core, DEFAULT_OPTS);
3529 self.core().wrapElement().append(self.opts(OPT_HTML_FOCUS));
3532 focus : self.onFocus
3535 self._timeoutId = 0;
3538 //--------------------------------------------------------------------------------
3542 * Reacts to the `blur` event and hides the focus effect with a slight delay which
3543 * allows quick refocusing without effect blinking in and out.
3545 * @signature TextExtFocus.onBlur(e)
3547 * @param e {Object} jQuery event.
3549 * @author agorbatchev
3551 * @id TextExtFocus.onBlur
3553 p.onBlur = function(e)
3557 clearTimeout(self._timeoutId);
3559 self._timeoutId = setTimeout(function()
3561 self.getFocus().hide();
3567 * Reacts to the `focus` event and shows the focus effect.
3569 * @signature TextExtFocus.onFocus
3571 * @param e {Object} jQuery event.
3572 * @author agorbatchev
3574 * @id TextExtFocus.onFocus
3576 p.onFocus = function(e)
3580 clearTimeout(self._timeoutId);
3582 self.getFocus().show();
3585 //--------------------------------------------------------------------------------
3586 // Core functionality
3589 * Returns focus effect HTML element.
3591 * @signature TextExtFocus.getFocus()
3593 * @author agorbatchev
3595 * @id TextExtFocus.getFocus
3597 p.getFocus = function()
3599 return this.core().wrapElement().find('.text-focus');
3603 * jQuery TextExt Plugin
3604 * http://textextjs.com
3607 * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
3608 * @license MIT License
3613 * Prompt plugin displays a visual user propmpt in the text input area. If user focuses
3614 * on the input, the propt is hidden and only shown again when user focuses on another
3615 * element and text input doesn't have a value.
3617 * @author agorbatchev
3621 function TextExtPrompt() {};
3623 $.fn.textext.TextExtPrompt = TextExtPrompt;
3624 $.fn.textext.addPlugin('prompt', TextExtPrompt);
3626 var p = TextExtPrompt.prototype,
3628 CSS_HIDE_PROMPT = 'text-hide-prompt',
3631 * Prompt plugin has options to change the prompt label and its HTML template. The options
3632 * could be changed when passed to the `$().textext()` function. For example:
3634 * $('textarea').textext({
3635 * plugins: 'prompt',
3636 * prompt: 'Your email address'
3639 * @author agorbatchev
3641 * @id TextExtPrompt.options
3645 * Prompt message that is displayed to the user whenever there's no value in the input.
3648 * @default 'Awaiting input...'
3649 * @author agorbatchev
3651 * @id TextExtPrompt.options.prompt
3653 OPT_PROMPT = 'prompt',
3656 * HTML source that is used to generate markup required for the prompt effect.
3659 * @default '<div class="text-prompt"/>'
3660 * @author agorbatchev
3662 * @id TextExtPrompt.options.html.prompt
3664 OPT_HTML_PROMPT = 'html.prompt',
3667 * Prompt plugin dispatches or reacts to the following events.
3668 * @id TextExtPrompt.events
3672 * Prompt plugin reacts to the `focus` event and hides the markup generated from
3673 * the `html.prompt` option.
3676 * @author agorbatchev
3678 * @id TextExtPrompt.events.focus
3682 * Prompt plugin reacts to the `blur` event and shows the prompt back if user
3683 * hasn't entered any value.
3686 * @author agorbatchev
3688 * @id TextExtPrompt.events.blur
3692 prompt : 'Awaiting input...',
3695 prompt : '<div class="text-prompt"/>'
3701 * Initialization method called by the core during plugin instantiation.
3703 * @signature TextExtPrompt.init(core)
3705 * @param core {TextExt} Instance of the TextExt core class.
3707 * @author agorbatchev
3709 * @id TextExtPrompt.init
3711 p.init = function(core)
3714 placeholderKey = 'placeholder',
3719 self.baseInit(core, DEFAULT_OPTS);
3721 container = $(self.opts(OPT_HTML_PROMPT));
3722 $(self).data('container', container);
3724 self.core().wrapElement().append(container);
3725 self.setPrompt(self.opts(OPT_PROMPT));
3727 prompt = core.input().attr(placeholderKey);
3730 prompt = self.opts(OPT_PROMPT);
3732 // clear placeholder attribute if set
3733 core.input().attr(placeholderKey, '');
3736 self.setPrompt(prompt);
3738 if($.trim(self.val()).length > 0)
3743 focus : self.onFocus,
3744 postInvalidate : self.onPostInvalidate,
3745 postInit : self.onPostInit
3749 //--------------------------------------------------------------------------------
3753 * Reacts to the `postInit` and configures the plugin for initial display.
3755 * @signature TextExtPrompt.onPostInit(e)
3757 * @param e {Object} jQuery event.
3759 * @author agorbatchev
3761 * @id TextExtPrompt.onPostInit
3763 p.onPostInit = function(e)
3765 this.invalidateBounds();
3769 * Reacts to the `postInvalidate` and insures that prompt display remains correct.
3771 * @signature TextExtPrompt.onPostInvalidate(e)
3773 * @param e {Object} jQuery event.
3775 * @author agorbatchev
3777 * @id TextExtPrompt.onPostInvalidate
3779 p.onPostInvalidate = function(e)
3781 this.invalidateBounds();
3785 * Repositions the prompt to make sure it's always at the same place as in the text input carret.
3787 * @signature TextExtPrompt.invalidateBounds()
3789 * @author agorbatchev
3791 * @id TextExtPrompt.invalidateBounds
3793 p.invalidateBounds = function()
3796 input = self.input()
3799 self.containerElement().css({
3800 paddingLeft : input.css('paddingLeft'),
3801 paddingTop : input.css('paddingTop')
3806 * Reacts to the `blur` event and shows the prompt effect with a slight delay which
3807 * allows quick refocusing without effect blinking in and out.
3809 * The prompt is restored if the text box has no value.
3811 * @signature TextExtPrompt.onBlur(e)
3813 * @param e {Object} jQuery event.
3815 * @author agorbatchev
3817 * @id TextExtPrompt.onBlur
3819 p.onBlur = function(e)
3823 self.startTimer('prompt', 0.1, function()
3830 * Shows prompt HTML element.
3832 * @signature TextExtPrompt.showPrompt()
3834 * @author agorbatchev
3836 * @id TextExtPrompt.showPrompt
3838 p.showPrompt = function()
3841 input = self.input()
3844 if($.trim(self.val()).length === 0 && !input.is(':focus'))
3845 self.containerElement().removeClass(CSS_HIDE_PROMPT);
3849 * Hides prompt HTML element.
3851 * @signature TextExtPrompt.hidePrompt()
3853 * @author agorbatchev
3855 * @id TextExtPrompt.hidePrompt
3857 p.hidePrompt = function()
3859 this.stopTimer('prompt');
3860 this.containerElement().addClass(CSS_HIDE_PROMPT);
3864 * Reacts to the `focus` event and hides the prompt effect.
3866 * @signature TextExtPrompt.onFocus
3868 * @param e {Object} jQuery event.
3869 * @author agorbatchev
3871 * @id TextExtPrompt.onFocus
3873 p.onFocus = function(e)
3878 //--------------------------------------------------------------------------------
3879 // Core functionality
3882 * Sets the prompt display to the specified string.
3884 * @signature TextExtPrompt.setPrompt(str)
3886 * @oaram str {String} String that will be displayed in the prompt.
3888 * @author agorbatchev
3890 * @id TextExtPrompt.setPrompt
3892 p.setPrompt = function(str)
3894 this.containerElement().text(str);
3898 * Returns prompt effect HTML element.
3900 * @signature TextExtPrompt.containerElement()
3902 * @author agorbatchev
3904 * @id TextExtPrompt.containerElement
3906 p.containerElement = function()
3908 return $(this).data('container');
3912 * jQuery TextExt Plugin
3913 * http://textextjs.com
3916 * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
3917 * @license MIT License
3922 * Suggestions plugin allows to easily specify the list of suggestion items that the
3923 * Autocomplete plugin would present to the user.
3925 * @author agorbatchev
3927 * @id TextExtSuggestions
3929 function TextExtSuggestions() {};
3931 $.fn.textext.TextExtSuggestions = TextExtSuggestions;
3932 $.fn.textext.addPlugin('suggestions', TextExtSuggestions);
3934 var p = TextExtSuggestions.prototype,
3936 * Suggestions plugin only has one option and that is to set suggestion items. It could be
3937 * changed when passed to the `$().textext()` function. For example:
3939 * $('textarea').textext({
3940 * plugins: 'suggestions',
3941 * suggestions: [ "item1", "item2" ]
3944 * @author agorbatchev
3946 * @id TextExtSuggestions.options
3950 * List of items that Autocomplete plugin would display in the dropdown.
3954 * @author agorbatchev
3956 * @id TextExtSuggestions.options.suggestions
3958 OPT_SUGGESTIONS = 'suggestions',
3961 * Suggestions plugin dispatches or reacts to the following events.
3963 * @author agorbatchev
3965 * @id TextExtSuggestions.events
3969 * Suggestions plugin reacts to the `getSuggestions` event and returns `suggestions` items
3972 * @name getSuggestions
3973 * @author agorbatchev
3975 * @id TextExtSuggestions.events.getSuggestions
3979 * Suggestions plugin triggers the `setSuggestions` event to pass its own list of `Suggestions`
3980 * to the Autocomplete plugin.
3982 * @name setSuggestions
3983 * @author agorbatchev
3985 * @id TextExtSuggestions.events.setSuggestions
3989 * Suggestions plugin reacts to the `postInit` event to pass its list of `suggestions` to the
3990 * Autocomplete right away.
3993 * @author agorbatchev
3995 * @id TextExtSuggestions.events.postInit
4004 * Initialization method called by the core during plugin instantiation.
4006 * @signature TextExtSuggestions.init(core)
4008 * @param core {TextExt} Instance of the TextExt core class.
4010 * @author agorbatchev
4012 * @id TextExtSuggestions.init
4014 p.init = function(core)
4018 self.baseInit(core, DEFAULT_OPTS);
4021 getSuggestions : self.onGetSuggestions,
4022 postInit : self.onPostInit
4027 * Triggers `setSuggestions` and passes supplied suggestions to the Autocomplete plugin.
4029 * @signature TextExtSuggestions.setSuggestions(suggestions, showHideDropdown)
4031 * @param suggestions {Array} List of suggestions. With the default `ItemManager` it should
4032 * be a list of strings.
4033 * @param showHideDropdown {Boolean} If it's undesirable to show the dropdown right after
4034 * suggestions are set, `false` should be passed for this argument.
4036 * @author agorbatchev
4038 * @id TextExtSuggestions.setSuggestions
4040 p.setSuggestions = function(suggestions, showHideDropdown)
4042 this.trigger('setSuggestions', { result : suggestions, showHideDropdown : showHideDropdown != false });
4046 * Reacts to the `postInit` event and triggers `setSuggestions` event to set suggestions list
4047 * right after initialization.
4049 * @signature TextExtSuggestions.onPostInit(e)
4051 * @param e {Object} jQuery event.
4053 * @author agorbatchev
4055 * @id TextExtSuggestions.onPostInit
4057 p.onPostInit = function(e)
4060 self.setSuggestions(self.opts(OPT_SUGGESTIONS), false);
4064 * Reacts to the `getSuggestions` event and triggers `setSuggestions` event with the list
4065 * of `suggestions` specified in the options.
4067 * @signature TextExtSuggestions.onGetSuggestions(e, data)
4069 * @param e {Object} jQuery event.
4070 * @param data {Object} Payload from the `getSuggestions` event with the user query, eg `{ query: {String} }`.
4072 * @author agorbatchev
4074 * @id TextExtSuggestions.onGetSuggestions
4076 p.onGetSuggestions = function(e, data)
4079 suggestions = self.opts(OPT_SUGGESTIONS)
4083 self.setSuggestions(self.itemManager().filter(suggestions, data.query));
4087 * jQuery TextExt Plugin
4088 * http://textextjs.com
4091 * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
4092 * @license MIT License
4097 * Tags plugin brings in the traditional tag functionality where user can assemble and
4098 * edit list of tags. Tags plugin works especially well together with Autocomplete, Filter,
4099 * Suggestions and Ajax plugins to provide full spectrum of features. It can also work on
4100 * its own and just do one thing -- tags.
4102 * @author agorbatchev
4106 function TextExtTags() {};
4108 $.fn.textext.TextExtTags = TextExtTags;
4109 $.fn.textext.addPlugin('tags', TextExtTags);
4111 var p = TextExtTags.prototype,
4114 CSS_TAGS_ON_TOP = 'text-tags-on-top',
4115 CSS_DOT_TAGS_ON_TOP = CSS_DOT + CSS_TAGS_ON_TOP,
4116 CSS_TAG = 'text-tag',
4117 CSS_DOT_TAG = CSS_DOT + CSS_TAG,
4118 CSS_TAGS = 'text-tags',
4119 CSS_DOT_TAGS = CSS_DOT + CSS_TAGS,
4120 CSS_LABEL = 'text-label',
4121 CSS_DOT_LABEL = CSS_DOT + CSS_LABEL,
4122 CSS_REMOVE = 'text-remove',
4123 CSS_DOT_REMOVE = CSS_DOT + CSS_REMOVE,
4126 * Tags plugin options are grouped under `tags` when passed to the
4127 * `$().textext()` function. For example:
4129 * $('textarea').textext({
4132 * items: [ "tag1", "tag2" ]
4136 * @author agorbatchev
4138 * @id TextExtTags.options
4142 * This is a toggle switch to enable or disable the Tags plugin. The value is checked
4143 * each time at the top level which allows you to toggle this setting on the fly.
4145 * @name tags.enabled
4147 * @author agorbatchev
4149 * @id TextExtTags.options.tags.enabled
4151 OPT_ENABLED = 'tags.enabled',
4154 * Allows to specify tags which will be added to the input by default upon initialization.
4155 * Each item in the array must be of the type that current `ItemManager` can understand.
4156 * Default type is `String`.
4160 * @author agorbatchev
4162 * @id TextExtTags.options.tags.items
4164 OPT_ITEMS = 'tags.items',
4167 * HTML source that is used to generate a single tag.
4170 * @default '<div class="text-tags"/>'
4171 * @author agorbatchev
4173 * @id TextExtTags.options.html.tag
4175 OPT_HTML_TAG = 'html.tag',
4178 * HTML source that is used to generate container for the tags.
4181 * @default '<div class="text-tag"><div class="text-button"><span class="text-label"/><a class="text-remove"/></div></div>'
4182 * @author agorbatchev
4184 * @id TextExtTags.options.html.tags
4186 OPT_HTML_TAGS = 'html.tags',
4189 * Tags plugin dispatches or reacts to the following events.
4191 * @author agorbatchev
4193 * @id TextExtTags.events
4197 * Tags plugin triggers the `isTagAllowed` event before adding each tag to the tag list. Other plugins have
4198 * an opportunity to interrupt this by setting `result` of the second argument to `false`. For example:
4200 * $('textarea').textext({...}).bind('isTagAllowed', function(e, data)
4202 * if(data.tag === 'foo')
4203 * data.result = false;
4206 * The second argument `data` has the following format: `{ tag : {Object}, result : {Boolean} }`. `tag`
4207 * property is in the format that the current `ItemManager` can understand.
4209 * @name isTagAllowed
4210 * @author agorbatchev
4212 * @id TextExtTags.events.isTagAllowed
4214 EVENT_IS_TAG_ALLOWED = 'isTagAllowed',
4217 * Tags plugin triggers the `tagClick` event when user clicks on one of the tags. This allows to process
4218 * the click and potentially change the value of the tag (for example in case of user feedback).
4220 * $('textarea').textext({...}).bind('tagClick', function(e, tag, value, callback)
4222 * var newValue = window.prompt('New value', value);
4225 * callback(newValue, true);
4228 * Callback argument has the following signature:
4230 * function(newValue, refocus)
4235 * Please check out [example](/manual/examples/tags-changing.html).
4241 * @id TextExtTags.events.tagClick
4243 EVENT_TAG_CLICK = 'tagClick',
4252 tags : '<div class="text-tags"/>',
4253 tag : '<div class="text-tag"><div class="text-button"><span class="text-label"/><a class="text-remove"/></div></div>'
4259 * Initialization method called by the core during plugin instantiation.
4261 * @signature TextExtTags.init(core)
4263 * @param core {TextExt} Instance of the TextExt core class.
4265 * @author agorbatchev
4267 * @id TextExtTags.init
4269 p.init = function(core)
4271 this.baseInit(core, DEFAULT_OPTS);
4274 input = self.input(),
4278 if(self.opts(OPT_ENABLED))
4280 container = $(self.opts(OPT_HTML_TAGS));
4281 input.after(container);
4283 $(self).data('container', container);
4286 enterKeyPress : self.onEnterKeyPress,
4287 backspaceKeyDown : self.onBackspaceKeyDown,
4288 preInvalidate : self.onPreInvalidate,
4289 postInit : self.onPostInit,
4290 getFormData : self.onGetFormData
4293 self.on(container, {
4294 click : self.onClick,
4295 mousemove : self.onContainerMouseMove
4299 mousemove : self.onInputMouseMove
4303 self._originalPadding = {
4304 left : parseInt(input.css('paddingLeft') || 0),
4305 top : parseInt(input.css('paddingTop') || 0)
4308 self._paddingBox = {
4313 self.updateFormCache();
4317 * Returns HTML element in which all tag HTML elements are residing.
4319 * @signature TextExtTags.containerElement()
4321 * @author agorbatchev
4323 * @id TextExtTags.containerElement
4325 p.containerElement = function()
4327 return $(this).data('container');
4330 //--------------------------------------------------------------------------------
4334 * Reacts to the `postInit` event triggered by the core and sets default tags
4335 * if any were specified.
4337 * @signature TextExtTags.onPostInit(e)
4339 * @param e {Object} jQuery event.
4341 * @author agorbatchev
4343 * @id TextExtTags.onPostInit
4345 p.onPostInit = function(e)
4348 self.addTags(self.opts(OPT_ITEMS));
4352 * Reacts to the [`getFormData`][1] event triggered by the core. Returns data with the
4353 * weight of 200 to be *greater than the Autocomplete plugin* data weight. The weights
4354 * system is covered in greater detail in the [`getFormData`][1] event documentation.
4356 * [1]: /manual/textext.html#getformdata
4358 * @signature TextExtTags.onGetFormData(e, data, keyCode)
4360 * @param e {Object} jQuery event.
4361 * @param data {Object} Data object to be populated.
4362 * @param keyCode {Number} Key code that triggered the original update request.
4364 * @author agorbatchev
4366 * @id TextExtTags.onGetFormData
4368 p.onGetFormData = function(e, data, keyCode)
4371 inputValue = keyCode === 13 ? '' : self.val(),
4372 formValue = self._formData
4375 data[200] = self.formDataObject(inputValue, formValue);
4379 * Returns initialization priority of the Tags plugin which is expected to be
4380 * *less than the Autocomplete plugin* because of the dependencies. The value is
4383 * @signature TextExtTags.initPriority()
4385 * @author agorbatchev
4387 * @id TextExtTags.initPriority
4389 p.initPriority = function()
4395 * Reacts to user moving mouse over the text area when cursor is over the text
4396 * and not over the tags. Whenever mouse cursor is over the area covered by
4397 * tags, the tags container is flipped to be on top of the text area which
4398 * makes all tags functional with the mouse.
4400 * @signature TextExtTags.onInputMouseMove(e)
4402 * @param e {Object} jQuery event.
4404 * @author agorbatchev
4406 * @id TextExtTags.onInputMouseMove
4408 p.onInputMouseMove = function(e)
4410 this.toggleZIndex(e);
4414 * Reacts to user moving mouse over the tags. Whenever the cursor moves out
4415 * of the tags and back into where the text input is happening visually,
4416 * the tags container is sent back under the text area which allows user
4417 * to interact with the text using mouse cursor as expected.
4419 * @signature TextExtTags.onContainerMouseMove(e)
4421 * @param e {Object} jQuery event.
4423 * @author agorbatchev
4425 * @id TextExtTags.onContainerMouseMove
4427 p.onContainerMouseMove = function(e)
4429 this.toggleZIndex(e);
4433 * Reacts to the `backspaceKeyDown` event. When backspace key is pressed in an empty text field,
4434 * deletes last tag from the list.
4436 * @signature TextExtTags.onBackspaceKeyDown(e)
4438 * @param e {Object} jQuery event.
4440 * @author agorbatchev
4442 * @id TextExtTags.onBackspaceKeyDown
4444 p.onBackspaceKeyDown = function(e)
4447 lastTag = self.tagElements().last()
4450 if(self.val().length == 0)
4451 self.removeTag(lastTag);
4455 * Reacts to the `preInvalidate` event and updates the input box to look like the tags are
4456 * positioned inside it.
4458 * @signature TextExtTags.onPreInvalidate(e)
4460 * @param e {Object} jQuery event.
4462 * @author agorbatchev
4464 * @id TextExtTags.onPreInvalidate
4466 p.onPreInvalidate = function(e)
4469 lastTag = self.tagElements().last(),
4470 pos = lastTag.position()
4473 if(lastTag.length > 0)
4474 pos.left += lastTag.innerWidth();
4476 pos = self._originalPadding;
4478 self._paddingBox = pos;
4481 paddingLeft : pos.left,
4482 paddingTop : pos.top
4487 * Reacts to the mouse `click` event.
4489 * @signature TextExtTags.onClick(e)
4491 * @param e {Object} jQuery event.
4493 * @author agorbatchev
4495 * @id TextExtTags.onClick
4497 p.onClick = function(e)
4501 source = $(e.target),
4506 if(source.is(CSS_DOT_TAGS))
4510 else if(source.is(CSS_DOT_REMOVE))
4512 self.removeTag(source.parents(CSS_DOT_TAG + ':first'));
4515 else if(source.is(CSS_DOT_LABEL))
4517 tag = source.parents(CSS_DOT_TAG + ':first');
4518 self.trigger(EVENT_TAG_CLICK, tag, tag.data(CSS_TAG), tagClickCallback);
4521 function tagClickCallback(newValue, refocus)
4523 tag.data(CSS_TAG, newValue);
4524 tag.find(CSS_DOT_LABEL).text(self.itemManager().itemToString(newValue));
4526 self.updateFormCache();
4528 core.invalidateBounds();
4539 * Reacts to the `enterKeyPress` event and adds whatever is currently in the text input
4540 * as a new tag. Triggers `isTagAllowed` to check if the tag could be added first.
4542 * @signature TextExtTags.onEnterKeyPress(e)
4544 * @param e {Object} jQuery event.
4546 * @author agorbatchev
4548 * @id TextExtTags.onEnterKeyPress
4550 p.onEnterKeyPress = function(e)
4554 tag = self.itemManager().stringToItem(val)
4557 if(self.isTagAllowed(tag))
4559 self.addTags([ tag ]);
4560 // refocus the textarea just in case it lost the focus
4561 self.core().focusInput();
4565 //--------------------------------------------------------------------------------
4566 // Core functionality
4569 * Creates a cache object with all the tags currently added which will be returned
4570 * in the `onGetFormData` handler.
4572 * @signature TextExtTags.updateFormCache()
4574 * @author agorbatchev
4576 * @id TextExtTags.updateFormCache
4578 p.updateFormCache = function()
4584 self.tagElements().each(function()
4586 result.push($(this).data(CSS_TAG));
4589 // cache the results to be used in the onGetFormData
4590 self._formData = result;
4594 * Toggles tag container to be on top of the text area or under based on where
4595 * the mouse cursor is located. When cursor is above the text input and out of
4596 * any of the tags, the tags container is sent under the text area. If cursor
4597 * is over any of the tags, the tag container is brought to be over the text
4600 * @signature TextExtTags.toggleZIndex(e)
4602 * @param e {Object} jQuery event.
4604 * @author agorbatchev
4606 * @id TextExtTags.toggleZIndex
4608 p.toggleZIndex = function(e)
4611 offset = self.input().offset(),
4612 mouseX = e.clientX - offset.left,
4613 mouseY = e.clientY - offset.top,
4614 box = self._paddingBox,
4615 container = self.containerElement(),
4616 isOnTop = container.is(CSS_DOT_TAGS_ON_TOP),
4617 isMouseOverText = mouseX > box.left && mouseY > box.top
4620 if(!isOnTop && !isMouseOverText || isOnTop && isMouseOverText)
4621 container[(!isOnTop ? 'add' : 'remove') + 'Class'](CSS_TAGS_ON_TOP);
4625 * Returns all tag HTML elements.
4627 * @signature TextExtTags.tagElements()
4629 * @author agorbatchev
4631 * @id TextExtTags.tagElements
4633 p.tagElements = function()
4635 return this.containerElement().find(CSS_DOT_TAG);
4639 * Wrapper around the `isTagAllowed` event which triggers it and returns `true`
4640 * if `result` property of the second argument remains `true`.
4642 * @signature TextExtTags.isTagAllowed(tag)
4644 * @param tag {Object} Tag object that the current `ItemManager` can understand.
4645 * Default is `String`.
4647 * @author agorbatchev
4649 * @id TextExtTags.isTagAllowed
4651 p.isTagAllowed = function(tag)
4653 var opts = { tag : tag, result : true };
4654 this.trigger(EVENT_IS_TAG_ALLOWED, opts);
4655 return opts.result === true;
4659 * Adds specified tags to the tag list. Triggers `isTagAllowed` event for each tag
4660 * to insure that it could be added. Calls `TextExt.getFormData()` to refresh the data.
4662 * @signature TextExtTags.addTags(tags)
4664 * @param tags {Array} List of tags that current `ItemManager` can understand. Default
4667 * @author agorbatchev
4669 * @id TextExtTags.addTags
4671 p.addTags = function(tags)
4673 if(!tags || tags.length == 0)
4678 container = self.containerElement(),
4682 for(i = 0; i < tags.length; i++)
4686 if(tag && self.isTagAllowed(tag))
4687 container.append(self.renderTag(tag));
4690 self.updateFormCache();
4692 core.invalidateBounds();
4696 * Returns HTML element for the specified tag.
4698 * @signature TextExtTags.getTagElement(tag)
4700 * @param tag {Object} Tag object in the format that current `ItemManager` can understand.
4701 * Default is `String`.
4703 * @author agorbatchev
4705 * @id TextExtTags.getTagElement
4707 p.getTagElement = function(tag)
4710 list = self.tagElements(),
4714 for(i = 0; i < list.length, item = $(list[i]); i++)
4715 if(self.itemManager().compareItems(item.data(CSS_TAG), tag))
4720 * Removes specified tag from the list. Calls `TextExt.getFormData()` to refresh the data.
4722 * @signature TextExtTags.removeTag(tag)
4724 * @param tag {Object} Tag object in the format that current `ItemManager` can understand.
4725 * Default is `String`.
4727 * @author agorbatchev
4729 * @id TextExtTags.removeTag
4731 p.removeTag = function(tag)
4738 if(tag instanceof $)
4741 tag = tag.data(CSS_TAG);
4745 element = self.getTagElement(tag);
4749 self.updateFormCache();
4751 core.invalidateBounds();
4755 * Creates and returns new HTML element from the source code specified in the `html.tag` option.
4757 * @signature TextExtTags.renderTag(tag)
4759 * @param tag {Object} Tag object in the format that current `ItemManager` can understand.
4760 * Default is `String`.
4762 * @author agorbatchev
4764 * @id TextExtTags.renderTag
4766 p.renderTag = function(tag)
4769 node = $(self.opts(OPT_HTML_TAG))
4772 node.find('.text-label').text(self.itemManager().itemToString(tag));
4773 node.data(CSS_TAG, tag);