4931 lines
127 KiB
JavaScript
4931 lines
127 KiB
JavaScript
;/**
|
|
* jQuery TextExt Plugin
|
|
* http://textextjs.com
|
|
*
|
|
* @version 1.3.1
|
|
* @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
|
|
* @license MIT License
|
|
*/
|
|
(function($, undefined)
|
|
{
|
|
/**
|
|
* TextExt is the main core class which by itself doesn't provide any functionality
|
|
* that is user facing, however it has the underlying mechanics to bring all the
|
|
* plugins together under one roof and make them work with each other or on their
|
|
* own.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt
|
|
*/
|
|
function TextExt() {};
|
|
|
|
/**
|
|
* ItemManager is used to seamlessly convert between string that come from the user input to whatever
|
|
* the format the item data is being passed around in. It's used by all plugins that in one way or
|
|
* another operate with items, such as Tags, Filter, Autocomplete and Suggestions. Default implementation
|
|
* works with `String` type.
|
|
*
|
|
* Each instance of `TextExt` creates a new instance of default implementation of `ItemManager`
|
|
* unless `itemManager` option was set to another implementation.
|
|
*
|
|
* To satisfy requirements of managing items of type other than a `String`, different implementation
|
|
* if `ItemManager` should be supplied.
|
|
*
|
|
* If you wish to bring your own implementation, you need to create a new class and implement all the
|
|
* methods that `ItemManager` has. After, you need to supply your pass via the `itemManager` option during
|
|
* initialization like so:
|
|
*
|
|
* $('#input').textext({
|
|
* itemManager : CustomItemManager
|
|
* })
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id ItemManager
|
|
*/
|
|
function ItemManager() {};
|
|
|
|
/**
|
|
* TextExtPlugin is a base class for all plugins. It provides common methods which are reused
|
|
* by majority of plugins.
|
|
*
|
|
* All plugins must register themselves by calling the `$.fn.textext.addPlugin(name, constructor)`
|
|
* function while providing plugin name and constructor. The plugin name is the same name that user
|
|
* will identify the plugin in the `plugins` option when initializing TextExt component and constructor
|
|
* function will create a new instance of the plugin. *Without registering, the core won't
|
|
* be able to see the plugin.*
|
|
*
|
|
* <span class="new label version">new in 1.2.0</span> You can get instance of each plugin from the core
|
|
* via associated function with the same name as the plugin. For example:
|
|
*
|
|
* $('#input').textext()[0].tags()
|
|
* $('#input').textext()[0].autocomplete()
|
|
* ...
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtPlugin
|
|
*/
|
|
function TextExtPlugin() {};
|
|
|
|
var stringify = (JSON || {}).stringify,
|
|
slice = Array.prototype.slice,
|
|
p,
|
|
UNDEFINED = 'undefined',
|
|
|
|
/**
|
|
* TextExt provides a way to pass in the options to configure the core as well as
|
|
* each plugin that is being currently used. The jQuery exposed plugin `$().textext()`
|
|
* function takes a hash object with key/value set of options. For example:
|
|
*
|
|
* $('textarea').textext({
|
|
* enabled: true
|
|
* })
|
|
*
|
|
* There are multiple ways of passing in the options:
|
|
*
|
|
* 1. Options could be nested multiple levels deep and accessed using all lowercased, dot
|
|
* separated style, eg `foo.bar.world`. The manual is using this style for clarity and
|
|
* consistency. For example:
|
|
*
|
|
* {
|
|
* item: {
|
|
* manager: ...
|
|
* },
|
|
*
|
|
* html: {
|
|
* wrap: ...
|
|
* },
|
|
*
|
|
* autocomplete: {
|
|
* enabled: ...,
|
|
* dropdown: {
|
|
* position: ...
|
|
* }
|
|
* }
|
|
* }
|
|
*
|
|
* 2. Options could be specified using camel cased names in a flat key/value fashion like so:
|
|
*
|
|
* {
|
|
* itemManager: ...,
|
|
* htmlWrap: ...,
|
|
* autocompleteEnabled: ...,
|
|
* autocompleteDropdownPosition: ...
|
|
* }
|
|
*
|
|
* 3. Finally, options could be specified in mixed style. It's important to understand that
|
|
* for each dot separated name, its alternative in camel case is also checked for, eg for
|
|
* `foo.bar.world` it's alternatives could be `fooBarWorld`, `foo.barWorld` or `fooBar.world`,
|
|
* which translates to `{ foo: { bar: { world: ... } } }`, `{ fooBarWorld: ... }`,
|
|
* `{ foo : { barWorld : ... } }` or `{ fooBar: { world: ... } }` respectively. For example:
|
|
*
|
|
* {
|
|
* itemManager : ...,
|
|
* htmlWrap: ...,
|
|
* autocomplete: {
|
|
* enabled: ...,
|
|
* dropdownPosition: ...
|
|
* }
|
|
* }
|
|
*
|
|
* Mixed case is used through out the code, wherever it seems appropriate. However in the code, all option
|
|
* names are specified in the dot notation because it works both ways where as camel case is not
|
|
* being converted to its alternative dot notation.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExt.options
|
|
*/
|
|
|
|
/**
|
|
* Default instance of `ItemManager` which takes `String` type as default for tags.
|
|
*
|
|
* @name item.manager
|
|
* @default ItemManager
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.options.item.manager
|
|
*/
|
|
OPT_ITEM_MANAGER = 'item.manager',
|
|
|
|
/**
|
|
* List of plugins that should be used with the current instance of TextExt. The list could be
|
|
* specified as array of strings or as comma or space separated string.
|
|
*
|
|
* @name plugins
|
|
* @default []
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.options.plugins
|
|
*/
|
|
OPT_PLUGINS = 'plugins',
|
|
|
|
/**
|
|
* TextExt allows for overriding of virtually any method that the core or any of its plugins
|
|
* use. This could be accomplished through the use of the `ext` option.
|
|
*
|
|
* It's possible to specifically target the core or any plugin, as well as overwrite all the
|
|
* desired methods everywhere.
|
|
*
|
|
* 1. Targeting the core:
|
|
*
|
|
* ext: {
|
|
* core: {
|
|
* trigger: function()
|
|
* {
|
|
* console.log('TextExt.trigger', arguments);
|
|
* $.fn.textext.TextExt.prototype.trigger.apply(this, arguments);
|
|
* }
|
|
* }
|
|
* }
|
|
*
|
|
* 2. Targeting individual plugins:
|
|
*
|
|
* ext: {
|
|
* tags: {
|
|
* addTags: function(tags)
|
|
* {
|
|
* console.log('TextExtTags.addTags', tags);
|
|
* $.fn.textext.TextExtTags.prototype.addTags.apply(this, arguments);
|
|
* }
|
|
* }
|
|
* }
|
|
*
|
|
* 3. Targeting `ItemManager` instance:
|
|
*
|
|
* ext: {
|
|
* itemManager: {
|
|
* stringToItem: function(str)
|
|
* {
|
|
* console.log('ItemManager.stringToItem', str);
|
|
* return $.fn.textext.ItemManager.prototype.stringToItem.apply(this, arguments);
|
|
* }
|
|
* }
|
|
* }
|
|
*
|
|
* 4. And finally, in edge cases you can extend everything at once:
|
|
*
|
|
* ext: {
|
|
* '*': {
|
|
* fooBar: function() {}
|
|
* }
|
|
* }
|
|
*
|
|
* @name ext
|
|
* @default {}
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.options.ext
|
|
*/
|
|
OPT_EXT = 'ext',
|
|
|
|
/**
|
|
* HTML source that is used to generate elements necessary for the core and all other
|
|
* plugins to function.
|
|
*
|
|
* @name html.wrap
|
|
* @default '<div class="text-core"><div class="text-wrap"/></div>'
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.options.html.wrap
|
|
*/
|
|
OPT_HTML_WRAP = 'html.wrap',
|
|
|
|
/**
|
|
* HTML source that is used to generate hidden input value of which will be submitted
|
|
* with the HTML form.
|
|
*
|
|
* @name html.hidden
|
|
* @default '<input type="hidden" />'
|
|
* @author agorbatchev
|
|
* @date 2011/08/20
|
|
* @id TextExt.options.html.hidden
|
|
*/
|
|
OPT_HTML_HIDDEN = 'html.hidden',
|
|
|
|
/**
|
|
* Hash table of key codes and key names for which special events will be created
|
|
* by the core. For each entry a `[name]KeyDown`, `[name]KeyUp` and `[name]KeyPress` events
|
|
* will be triggered along side with `anyKeyUp` and `anyKeyDown` events for every
|
|
* key stroke.
|
|
*
|
|
* Here's a list of default keys:
|
|
*
|
|
* {
|
|
* 8 : 'backspace',
|
|
* 9 : 'tab',
|
|
* 13 : 'enter!',
|
|
* 27 : 'escape!',
|
|
* 37 : 'left',
|
|
* 38 : 'up!',
|
|
* 39 : 'right',
|
|
* 40 : 'down!',
|
|
* 46 : 'delete',
|
|
* 108 : 'numpadEnter'
|
|
* }
|
|
*
|
|
* Please note the `!` at the end of some keys. This tells the core that by default
|
|
* this keypress will be trapped and not passed on to the text input.
|
|
*
|
|
* @name keys
|
|
* @default { ... }
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.options.keys
|
|
*/
|
|
OPT_KEYS = 'keys',
|
|
|
|
/**
|
|
* The core triggers or reacts to the following events.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExt.events
|
|
*/
|
|
|
|
/**
|
|
* Core triggers `preInvalidate` event before the dimensions of padding on the text input
|
|
* are set.
|
|
*
|
|
* @name preInvalidate
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.events.preInvalidate
|
|
*/
|
|
EVENT_PRE_INVALIDATE = 'preInvalidate',
|
|
|
|
/**
|
|
* Core triggers `postInvalidate` event after the dimensions of padding on the text input
|
|
* are set.
|
|
*
|
|
* @name postInvalidate
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.events.postInvalidate
|
|
*/
|
|
EVENT_POST_INVALIDATE = 'postInvalidate',
|
|
|
|
/**
|
|
* Core triggers `getFormData` on every key press to collect data that will be populated
|
|
* into the hidden input that will be submitted with the HTML form and data that will
|
|
* be displayed in the input field that user is currently interacting with.
|
|
*
|
|
* All plugins that wish to affect how the data is presented or sent must react to
|
|
* `getFormData` and populate the data in the following format:
|
|
*
|
|
* {
|
|
* input : {String},
|
|
* form : {Object}
|
|
* }
|
|
*
|
|
* The data key must be a numeric weight which will be used to determine which data
|
|
* ends up being used. Data with the highest numerical weight gets the priority. This
|
|
* allows plugins to set the final data regardless of their initialization order, which
|
|
* otherwise would be impossible.
|
|
*
|
|
* For example, the Tags and Autocomplete plugins have to work side by side and Tags
|
|
* plugin must get priority on setting the data. Therefore the Tags plugin sets data
|
|
* with the weight 200 where as the Autocomplete plugin sets data with the weight 100.
|
|
*
|
|
* Here's an example of a typical `getFormData` handler:
|
|
*
|
|
* TextExtPlugin.prototype.onGetFormData = function(e, data, keyCode)
|
|
* {
|
|
* data[100] = self.formDataObject('input value', 'form value');
|
|
* };
|
|
*
|
|
* Core also reacts to the `getFormData` and updates hidden input with data which will be
|
|
* submitted with the HTML form.
|
|
*
|
|
* @name getFormData
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.events.getFormData
|
|
*/
|
|
EVENT_GET_FORM_DATA = 'getFormData',
|
|
|
|
/**
|
|
* Core triggers and reacts to the `setFormData` event to update the actual value in the
|
|
* hidden input that will be submitted with the HTML form. Second argument can be value
|
|
* of any type and by default it will be JSON serialized with `TextExt.serializeData()`
|
|
* function.
|
|
*
|
|
* @name setFormData
|
|
* @author agorbatchev
|
|
* @date 2011/08/22
|
|
* @id TextExt.events.setFormData
|
|
*/
|
|
EVENT_SET_FORM_DATA = 'setFormData',
|
|
|
|
/**
|
|
* Core triggers and reacts to the `setInputData` event to update the actual value in the
|
|
* text input that user is interacting with. Second argument must be of a `String` type
|
|
* the value of which will be set into the text input.
|
|
*
|
|
* @name setInputData
|
|
* @author agorbatchev
|
|
* @date 2011/08/22
|
|
* @id TextExt.events.setInputData
|
|
*/
|
|
EVENT_SET_INPUT_DATA = 'setInputData',
|
|
|
|
/**
|
|
* Core triggers `postInit` event to let plugins run code after all plugins have been
|
|
* created and initialized. This is a good place to set some kind of global values before
|
|
* somebody gets to use them. This is not the right place to expect all plugins to finish
|
|
* their initialization.
|
|
*
|
|
* @name postInit
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.events.postInit
|
|
*/
|
|
EVENT_POST_INIT = 'postInit',
|
|
|
|
/**
|
|
* Core triggers `ready` event after all global configuration and prepearation has been
|
|
* done and the TextExt component is ready for use. Event handlers should expect all
|
|
* values to be set and the plugins to be in the final state.
|
|
*
|
|
* @name ready
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.events.ready
|
|
*/
|
|
EVENT_READY = 'ready',
|
|
|
|
/**
|
|
* Core triggers `anyKeyUp` event for every key up event triggered within the component.
|
|
*
|
|
* @name anyKeyUp
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.events.anyKeyUp
|
|
*/
|
|
|
|
/**
|
|
* Core triggers `anyKeyDown` event for every key down event triggered within the component.
|
|
*
|
|
* @name anyKeyDown
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.events.anyKeyDown
|
|
*/
|
|
|
|
/**
|
|
* Core triggers `[name]KeyUp` event for every key specifid in the `keys` option that is
|
|
* triggered within the component.
|
|
*
|
|
* @name [name]KeyUp
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.events.[name]KeyUp
|
|
*/
|
|
|
|
/**
|
|
* Core triggers `[name]KeyDown` event for every key specified in the `keys` option that is
|
|
* triggered within the component.
|
|
*
|
|
* @name [name]KeyDown
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.events.[name]KeyDown
|
|
*/
|
|
|
|
/**
|
|
* Core triggers `[name]KeyPress` event for every key specified in the `keys` option that is
|
|
* triggered within the component.
|
|
*
|
|
* @name [name]KeyPress
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.events.[name]KeyPress
|
|
*/
|
|
|
|
DEFAULT_OPTS = {
|
|
itemManager : ItemManager,
|
|
|
|
plugins : [],
|
|
ext : {},
|
|
|
|
html : {
|
|
wrap : '<div class="text-core"><div class="text-wrap"/></div>',
|
|
hidden : '<input type="hidden" />'
|
|
},
|
|
|
|
keys : {
|
|
8 : 'backspace',
|
|
9 : 'tab',
|
|
13 : 'enter!',
|
|
27 : 'escape!',
|
|
37 : 'left',
|
|
38 : 'up!',
|
|
39 : 'right',
|
|
40 : 'down!',
|
|
46 : 'delete',
|
|
108 : 'numpadEnter'
|
|
}
|
|
}
|
|
;
|
|
|
|
// Freak out if there's no JSON.stringify function found
|
|
if(!stringify)
|
|
throw new Error('JSON.stringify() not found');
|
|
|
|
/**
|
|
* Returns object property by name where name is dot-separated and object is multiple levels deep.
|
|
* @param target Object Source object.
|
|
* @param name String Dot separated property name, ie `foo.bar.world`
|
|
* @id core.getProperty
|
|
*/
|
|
function getProperty(source, name)
|
|
{
|
|
if(typeof(name) === 'string')
|
|
name = name.split('.');
|
|
|
|
var fullCamelCaseName = name.join('.').replace(/\.(\w)/g, function(match, letter) { return letter.toUpperCase() }),
|
|
nestedName = name.shift(),
|
|
result
|
|
;
|
|
|
|
if(typeof(result = source[fullCamelCaseName]) != UNDEFINED)
|
|
result = result;
|
|
|
|
else if(typeof(result = source[nestedName]) != UNDEFINED && name.length > 0)
|
|
result = getProperty(result, name);
|
|
|
|
// name.length here should be zero
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Hooks up specified events in the scope of the current object.
|
|
* @author agorbatchev
|
|
* @date 2011/08/09
|
|
*/
|
|
function hookupEvents()
|
|
{
|
|
var args = slice.apply(arguments),
|
|
self = this,
|
|
target = args.length === 1 ? self : args.shift(),
|
|
event
|
|
;
|
|
|
|
args = args[0] || {};
|
|
|
|
function bind(event, handler)
|
|
{
|
|
target.bind(event, function()
|
|
{
|
|
// apply handler to our PLUGIN object, not the target
|
|
return handler.apply(self, arguments);
|
|
});
|
|
}
|
|
|
|
for(event in args)
|
|
bind(event, args[event]);
|
|
};
|
|
|
|
function formDataObject(input, form)
|
|
{
|
|
return { 'input' : input, 'form' : form };
|
|
};
|
|
|
|
//--------------------------------------------------------------------------------
|
|
// ItemManager core component
|
|
|
|
p = ItemManager.prototype;
|
|
|
|
/**
|
|
* Initialization method called by the core during instantiation.
|
|
*
|
|
* @signature ItemManager.init(core)
|
|
*
|
|
* @param core {TextExt} Instance of the TextExt core class.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id ItemManager.init
|
|
*/
|
|
p.init = function(core)
|
|
{
|
|
};
|
|
|
|
/**
|
|
* Filters out items from the list that don't match the query and returns remaining items. Default
|
|
* implementation checks if the item starts with the query.
|
|
*
|
|
* @signature ItemManager.filter(list, query)
|
|
*
|
|
* @param list {Array} List of items. Default implementation works with strings.
|
|
* @param query {String} Query string.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id ItemManager.filter
|
|
*/
|
|
p.filter = function(list, query)
|
|
{
|
|
var result = [],
|
|
i, item
|
|
;
|
|
|
|
for(i = 0; i < list.length; i++)
|
|
{
|
|
item = list[i];
|
|
if(this.itemContains(item, query))
|
|
result.push(item);
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Returns `true` if specified item contains another string, `false` otherwise. In the default implementation
|
|
* `String.indexOf()` is used to check if item string begins with the needle string.
|
|
*
|
|
* @signature ItemManager.itemContains(item, needle)
|
|
*
|
|
* @param item {Object} Item to check. Default implementation works with strings.
|
|
* @param needle {String} Search string to be found within the item.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id ItemManager.itemContains
|
|
*/
|
|
p.itemContains = function(item, needle)
|
|
{
|
|
return this.itemToString(item).toLowerCase().indexOf(needle.toLowerCase()) == 0;
|
|
};
|
|
|
|
/**
|
|
* Converts specified string to item. Because default implemenation works with string, input string
|
|
* is simply returned back. To use custom objects, different implementation of this method could
|
|
* return something like `{ name : {String} }`.
|
|
*
|
|
* @signature ItemManager.stringToItem(str)
|
|
*
|
|
* @param str {String} Input string.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id ItemManager.stringToItem
|
|
*/
|
|
p.stringToItem = function(str)
|
|
{
|
|
return str;
|
|
};
|
|
|
|
/**
|
|
* Converts specified item to string. Because default implemenation works with string, input string
|
|
* is simply returned back. To use custom objects, different implementation of this method could
|
|
* for example return `name` field of `{ name : {String} }`.
|
|
*
|
|
* @signature ItemManager.itemToString(item)
|
|
*
|
|
* @param item {Object} Input item to be converted to string.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id ItemManager.itemToString
|
|
*/
|
|
p.itemToString = function(item)
|
|
{
|
|
return item;
|
|
};
|
|
|
|
/**
|
|
* Returns `true` if both items are equal, `false` otherwise. Because default implemenation works with
|
|
* string, input items are compared as strings. To use custom objects, different implementation of this
|
|
* method could for example compare `name` fields of `{ name : {String} }` type object.
|
|
*
|
|
* @signature ItemManager.compareItems(item1, item2)
|
|
*
|
|
* @param item1 {Object} First item.
|
|
* @param item2 {Object} Second item.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id ItemManager.compareItems
|
|
*/
|
|
p.compareItems = function(item1, item2)
|
|
{
|
|
return item1 == item2;
|
|
};
|
|
|
|
//--------------------------------------------------------------------------------
|
|
// TextExt core component
|
|
|
|
p = TextExt.prototype;
|
|
|
|
/**
|
|
* Initializes current component instance with work with the supplied text input and options.
|
|
*
|
|
* @signature TextExt.init(input, opts)
|
|
*
|
|
* @param input {HTMLElement} Text input.
|
|
* @param opts {Object} Options.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.init
|
|
*/
|
|
p.init = function(input, opts)
|
|
{
|
|
var self = this,
|
|
hiddenInput,
|
|
itemManager,
|
|
container
|
|
;
|
|
|
|
self._defaults = $.extend({}, DEFAULT_OPTS);
|
|
self._opts = opts || {};
|
|
self._plugins = {};
|
|
self._itemManager = itemManager = new (self.opts(OPT_ITEM_MANAGER))();
|
|
input = $(input);
|
|
container = $(self.opts(OPT_HTML_WRAP));
|
|
hiddenInput = $(self.opts(OPT_HTML_HIDDEN));
|
|
|
|
input
|
|
.wrap(container)
|
|
.keydown(function(e) { return self.onKeyDown(e) })
|
|
.keyup(function(e) { return self.onKeyUp(e) })
|
|
.data('textext', self)
|
|
;
|
|
|
|
// keep references to html elements using jQuery.data() to avoid circular references
|
|
$(self).data({
|
|
'hiddenInput' : hiddenInput,
|
|
'wrapElement' : input.parents('.text-wrap').first(),
|
|
'input' : input
|
|
});
|
|
|
|
// set the name of the hidden input to the text input's name
|
|
hiddenInput.attr('name', input.attr('name'));
|
|
// remove name attribute from the text input
|
|
input.attr('name', null);
|
|
// add hidden input to the DOM
|
|
hiddenInput.insertAfter(input);
|
|
|
|
$.extend(true, itemManager, self.opts(OPT_EXT + '.item.manager'));
|
|
$.extend(true, self, self.opts(OPT_EXT + '.*'), self.opts(OPT_EXT + '.core'));
|
|
|
|
self.originalWidth = input.outerWidth();
|
|
|
|
self.invalidateBounds();
|
|
|
|
itemManager.init(self);
|
|
|
|
self.initPatches();
|
|
self.initPlugins(self.opts(OPT_PLUGINS), $.fn.textext.plugins);
|
|
|
|
self.on({
|
|
setFormData : self.onSetFormData,
|
|
getFormData : self.onGetFormData,
|
|
setInputData : self.onSetInputData,
|
|
anyKeyUp : self.onAnyKeyUp
|
|
});
|
|
|
|
self.trigger(EVENT_POST_INIT);
|
|
self.trigger(EVENT_READY);
|
|
|
|
self.getFormData(0);
|
|
};
|
|
|
|
/**
|
|
* Initialized all installed patches against current instance. The patches are initialized based on their
|
|
* initialization priority which is returned by each patch's `initPriority()` method. Priority
|
|
* is a `Number` where patches with higher value gets their `init()` method called before patches
|
|
* with lower priority value.
|
|
*
|
|
* This facilitates initializing of patches in certain order to insure proper dependencies
|
|
* regardless of which order they are loaded.
|
|
*
|
|
* By default all patches have the same priority - zero, which means they will be initialized
|
|
* in rorder they are loaded, that is unless `initPriority()` is overriden.
|
|
*
|
|
* @signature TextExt.initPatches()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/10/11
|
|
* @id TextExt.initPatches
|
|
*/
|
|
p.initPatches = function()
|
|
{
|
|
var list = [],
|
|
source = $.fn.textext.patches,
|
|
name
|
|
;
|
|
|
|
for(name in source)
|
|
list.push(name);
|
|
|
|
this.initPlugins(list, source);
|
|
};
|
|
|
|
/**
|
|
* Creates and initializes all specified plugins. The plugins are initialized based on their
|
|
* initialization priority which is returned by each plugin's `initPriority()` method. Priority
|
|
* is a `Number` where plugins with higher value gets their `init()` method called before plugins
|
|
* with lower priority value.
|
|
*
|
|
* This facilitates initializing of plugins in certain order to insure proper dependencies
|
|
* regardless of which order user enters them in the `plugins` option field.
|
|
*
|
|
* By default all plugins have the same priority - zero, which means they will be initialized
|
|
* in the same order as entered by the user.
|
|
*
|
|
* @signature TextExt.initPlugins(plugins)
|
|
*
|
|
* @param plugins {Array} List of plugin names to initialize.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.initPlugins
|
|
*/
|
|
p.initPlugins = function(plugins, source)
|
|
{
|
|
var self = this,
|
|
ext, name, plugin, initList = [], i
|
|
;
|
|
|
|
if(typeof(plugins) == 'string')
|
|
plugins = plugins.split(/\s*,\s*|\s+/g);
|
|
|
|
for(i = 0; i < plugins.length; i++)
|
|
{
|
|
name = plugins[i];
|
|
plugin = source[name];
|
|
|
|
if(plugin)
|
|
{
|
|
self._plugins[name] = plugin = new plugin();
|
|
self[name] = (function(plugin) {
|
|
return function(){ return plugin; }
|
|
})(plugin);
|
|
initList.push(plugin);
|
|
$.extend(true, plugin, self.opts(OPT_EXT + '.*'), self.opts(OPT_EXT + '.' + name));
|
|
}
|
|
}
|
|
|
|
// sort plugins based on their priority values
|
|
initList.sort(function(p1, p2)
|
|
{
|
|
p1 = p1.initPriority();
|
|
p2 = p2.initPriority();
|
|
|
|
return p1 === p2
|
|
? 0
|
|
: p1 < p2 ? 1 : -1
|
|
;
|
|
});
|
|
|
|
for(i = 0; i < initList.length; i++)
|
|
initList[i].init(self);
|
|
};
|
|
|
|
/**
|
|
* Returns true if specified plugin is was instantiated for the current instance of core.
|
|
*
|
|
* @signature TextExt.hasPlugin(name)
|
|
*
|
|
* @param name {String} Name of the plugin to check.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/12/28
|
|
* @id TextExt.hasPlugin
|
|
* @version 1.1
|
|
*/
|
|
p.hasPlugin = function(name)
|
|
{
|
|
return !!this._plugins[name];
|
|
};
|
|
|
|
/**
|
|
* Allows to add multiple event handlers which will be execued in the scope of the current object.
|
|
*
|
|
* @signature TextExt.on([target], handlers)
|
|
*
|
|
* @param target {Object} **Optional**. Target object which has traditional `bind(event, handler)` method.
|
|
* Handler function will still be executed in the current object's scope.
|
|
* @param handlers {Object} Key/value pairs of event names and handlers, eg `{ event: handler }`.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.on
|
|
*/
|
|
p.on = hookupEvents;
|
|
|
|
/**
|
|
* Binds an event handler to the input box that user interacts with.
|
|
*
|
|
* @signature TextExt.bind(event, handler)
|
|
*
|
|
* @param event {String} Event name.
|
|
* @param handler {Function} Event handler.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.bind
|
|
*/
|
|
p.bind = function(event, handler)
|
|
{
|
|
this.input().bind(event, handler);
|
|
};
|
|
|
|
/**
|
|
* Triggers an event on the input box that user interacts with. All core events are originated here.
|
|
*
|
|
* @signature TextExt.trigger(event, ...args)
|
|
*
|
|
* @param event {String} Name of the event to trigger.
|
|
* @param ...args All remaining arguments will be passed to the event handler.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.trigger
|
|
*/
|
|
p.trigger = function()
|
|
{
|
|
var args = arguments;
|
|
this.input().trigger(args[0], slice.call(args, 1));
|
|
};
|
|
|
|
/**
|
|
* Returns instance of `itemManager` that is used by the component.
|
|
*
|
|
* @signature TextExt.itemManager()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.itemManager
|
|
*/
|
|
p.itemManager = function()
|
|
{
|
|
return this._itemManager;
|
|
};
|
|
|
|
/**
|
|
* Returns jQuery input element with which user is interacting with.
|
|
*
|
|
* @signature TextExt.input()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/10
|
|
* @id TextExt.input
|
|
*/
|
|
p.input = function()
|
|
{
|
|
return $(this).data('input');
|
|
};
|
|
|
|
/**
|
|
* Returns option value for the specified option by name. If the value isn't found in the user
|
|
* provided options, it will try looking for default value.
|
|
*
|
|
* @signature TextExt.opts(name)
|
|
*
|
|
* @param name {String} Option name as described in the options.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.opts
|
|
*/
|
|
p.opts = function(name)
|
|
{
|
|
var result = getProperty(this._opts, name);
|
|
return typeof(result) == 'undefined' ? getProperty(this._defaults, name) : result;
|
|
};
|
|
|
|
/**
|
|
* Returns HTML element that was created from the `html.wrap` option. This is the top level HTML
|
|
* container for the text input with which user is interacting with.
|
|
*
|
|
* @signature TextExt.wrapElement()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.wrapElement
|
|
*/
|
|
p.wrapElement = function()
|
|
{
|
|
return $(this).data('wrapElement');
|
|
};
|
|
|
|
/**
|
|
* Updates container to match dimensions of the text input. Triggers `preInvalidate` and `postInvalidate`
|
|
* events.
|
|
*
|
|
* @signature TextExt.invalidateBounds()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.invalidateBounds
|
|
*/
|
|
p.invalidateBounds = function()
|
|
{
|
|
var self = this,
|
|
input = self.input(),
|
|
wrap = self.wrapElement(),
|
|
container = wrap.parent(),
|
|
width = self.originalWidth + 'px',
|
|
height
|
|
;
|
|
|
|
self.trigger(EVENT_PRE_INVALIDATE);
|
|
|
|
height = input.outerHeight() + 'px';
|
|
|
|
// using css() method instead of width() and height() here because they don't seem to do the right thing in jQuery 1.8.x
|
|
// https://github.com/alexgorbatchev/jquery-textext/issues/74
|
|
input.css({ 'width' : width });
|
|
wrap.css({ 'width' : width, 'height' : height });
|
|
container.css({ 'height' : height });
|
|
|
|
self.trigger(EVENT_POST_INVALIDATE);
|
|
};
|
|
|
|
/**
|
|
* Focuses user input on the text box.
|
|
*
|
|
* @signature TextExt.focusInput()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.focusInput
|
|
*/
|
|
p.focusInput = function()
|
|
{
|
|
this.input()[0].focus();
|
|
};
|
|
|
|
/**
|
|
* Serializes data for to be set into the hidden input field and which will be submitted
|
|
* with the HTML form.
|
|
*
|
|
* By default simple JSON serialization is used. It's expected that `JSON.stringify`
|
|
* method would be available either through built in class in most modern browsers
|
|
* or through JSON2 library.
|
|
*
|
|
* @signature TextExt.serializeData(data)
|
|
*
|
|
* @param data {Object} Data to serialize.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/09
|
|
* @id TextExt.serializeData
|
|
*/
|
|
p.serializeData = stringify;
|
|
|
|
/**
|
|
* Returns the hidden input HTML element which will be submitted with the HTML form.
|
|
*
|
|
* @signature TextExt.hiddenInput()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/09
|
|
* @id TextExt.hiddenInput
|
|
*/
|
|
p.hiddenInput = function(value)
|
|
{
|
|
return $(this).data('hiddenInput');
|
|
};
|
|
|
|
/**
|
|
* Abstracted functionality to trigger an event and get the data with maximum weight set by all
|
|
* the event handlers. This functionality is used for the `getFormData` event.
|
|
*
|
|
* @signature TextExt.getWeightedEventResponse(event, args)
|
|
*
|
|
* @param event {String} Event name.
|
|
* @param args {Object} Argument to be passed with the event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/22
|
|
* @id TextExt.getWeightedEventResponse
|
|
*/
|
|
p.getWeightedEventResponse = function(event, args)
|
|
{
|
|
var self = this,
|
|
data = {},
|
|
maxWeight = 0
|
|
;
|
|
|
|
self.trigger(event, data, args);
|
|
|
|
for(var weight in data)
|
|
maxWeight = Math.max(maxWeight, weight);
|
|
|
|
return data[maxWeight];
|
|
};
|
|
|
|
/**
|
|
* Triggers the `getFormData` event to get all the plugins to return their data.
|
|
*
|
|
* After the data is returned, triggers `setFormData` and `setInputData` to update appopriate values.
|
|
*
|
|
* @signature TextExt.getFormData(keyCode)
|
|
*
|
|
* @param keyCode {Number} Key code number which has triggered this update. It's impotant to pass
|
|
* this value to the plugins because they might return different values based on the key that was
|
|
* pressed. For example, the Tags plugin returns an empty string for the `input` value if the enter
|
|
* key was pressed, otherwise it returns whatever is currently in the text input.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/22
|
|
* @id TextExt.getFormData
|
|
*/
|
|
p.getFormData = function(keyCode)
|
|
{
|
|
var self = this,
|
|
data = self.getWeightedEventResponse(EVENT_GET_FORM_DATA, keyCode || 0)
|
|
;
|
|
|
|
self.trigger(EVENT_SET_FORM_DATA , data['form']);
|
|
self.trigger(EVENT_SET_INPUT_DATA , data['input']);
|
|
};
|
|
|
|
//--------------------------------------------------------------------------------
|
|
// Event handlers
|
|
|
|
/**
|
|
* Reacts to the `anyKeyUp` event and triggers the `getFormData` to change data that will be submitted
|
|
* with the form. Default behaviour is that everything that is typed in will be JSON serialized, so
|
|
* the end result will be a JSON string.
|
|
*
|
|
* @signature TextExt.onAnyKeyUp(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.onAnyKeyUp
|
|
*/
|
|
p.onAnyKeyUp = function(e, keyCode)
|
|
{
|
|
this.getFormData(keyCode);
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `setInputData` event and populates the input text field that user is currently
|
|
* interacting with.
|
|
*
|
|
* @signature TextExt.onSetInputData(e, data)
|
|
*
|
|
* @param e {Event} jQuery event.
|
|
* @param data {String} Value to be set.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/22
|
|
* @id TextExt.onSetInputData
|
|
*/
|
|
p.onSetInputData = function(e, data)
|
|
{
|
|
this.input().val(data);
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `setFormData` event and populates the hidden input with will be submitted with
|
|
* the HTML form. The value will be serialized with `serializeData()` method.
|
|
*
|
|
* @signature TextExt.onSetFormData(e, data)
|
|
*
|
|
* @param e {Event} jQuery event.
|
|
* @param data {Object} Data that will be set.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/22
|
|
* @id TextExt.onSetFormData
|
|
*/
|
|
p.onSetFormData = function(e, data)
|
|
{
|
|
var self = this;
|
|
self.hiddenInput().val(self.serializeData(data));
|
|
};
|
|
|
|
/**
|
|
* Reacts to `getFormData` event triggered by the core. At the bare minimum the core will tell
|
|
* itself to use the current value in the text input as the data to be submitted with the HTML
|
|
* form.
|
|
*
|
|
* @signature TextExt.onGetFormData(e, data)
|
|
*
|
|
* @param e {Event} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/09
|
|
* @id TextExt.onGetFormData
|
|
*/
|
|
p.onGetFormData = function(e, data)
|
|
{
|
|
var val = this.input().val();
|
|
data[0] = formDataObject(val, val);
|
|
};
|
|
|
|
//--------------------------------------------------------------------------------
|
|
// User mouse/keyboard input
|
|
|
|
/**
|
|
* Triggers `[name]KeyUp` and `[name]KeyPress` for every keystroke as described in the events.
|
|
*
|
|
* @signature TextExt.onKeyUp(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.onKeyUp
|
|
*/
|
|
|
|
/**
|
|
* Triggers `[name]KeyDown` for every keystroke as described in the events.
|
|
*
|
|
* @signature TextExt.onKeyDown(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.onKeyDown
|
|
*/
|
|
|
|
$(['Down', 'Up']).each(function()
|
|
{
|
|
var type = this.toString();
|
|
|
|
p['onKey' + type] = function(e)
|
|
{
|
|
var self = this,
|
|
keyName = self.opts(OPT_KEYS)[e.keyCode],
|
|
defaultResult = true
|
|
;
|
|
|
|
if(keyName)
|
|
{
|
|
defaultResult = keyName.substr(-1) != '!';
|
|
keyName = keyName.replace('!', '');
|
|
|
|
self.trigger(keyName + 'Key' + type);
|
|
|
|
// manual *KeyPress event fimplementation for the function keys like Enter, Backspace, etc.
|
|
if(type == 'Up' && self._lastKeyDown == e.keyCode)
|
|
{
|
|
self._lastKeyDown = null;
|
|
self.trigger(keyName + 'KeyPress');
|
|
}
|
|
|
|
if(type == 'Down')
|
|
self._lastKeyDown = e.keyCode;
|
|
}
|
|
|
|
self.trigger('anyKey' + type, e.keyCode);
|
|
|
|
return defaultResult;
|
|
};
|
|
});
|
|
|
|
//--------------------------------------------------------------------------------
|
|
// Plugin Base
|
|
|
|
p = TextExtPlugin.prototype;
|
|
|
|
/**
|
|
* Allows to add multiple event handlers which will be execued in the scope of the current object.
|
|
*
|
|
* @signature TextExt.on([target], handlers)
|
|
*
|
|
* @param target {Object} **Optional**. Target object which has traditional `bind(event, handler)` method.
|
|
* Handler function will still be executed in the current object's scope.
|
|
* @param handlers {Object} Key/value pairs of event names and handlers, eg `{ event: handler }`.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtPlugin.on
|
|
*/
|
|
p.on = hookupEvents;
|
|
|
|
/**
|
|
* Returns the hash object that `getFormData` triggered by the core expects.
|
|
*
|
|
* @signature TextExtPlugin.formDataObject(input, form)
|
|
*
|
|
* @param input {String} Value that will go into the text input that user is interacting with.
|
|
* @param form {Object} Value that will be serialized and put into the hidden that will be submitted
|
|
* with the HTML form.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/22
|
|
* @id TextExtPlugin.formDataObject
|
|
*/
|
|
p.formDataObject = formDataObject;
|
|
|
|
/**
|
|
* Initialization method called by the core during plugin instantiation. This method must be implemented
|
|
* by each plugin individually.
|
|
*
|
|
* @signature TextExtPlugin.init(core)
|
|
*
|
|
* @param core {TextExt} Instance of the TextExt core class.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtPlugin.init
|
|
*/
|
|
p.init = function(core) { throw new Error('Not implemented') };
|
|
|
|
/**
|
|
* Initialization method wich should be called by the plugin during the `init()` call.
|
|
*
|
|
* @signature TextExtPlugin.baseInit(core, defaults)
|
|
*
|
|
* @param core {TextExt} Instance of the TextExt core class.
|
|
* @param defaults {Object} Default plugin options. These will be checked if desired value wasn't
|
|
* found in the options supplied by the user.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtPlugin.baseInit
|
|
*/
|
|
p.baseInit = function(core, defaults)
|
|
{
|
|
var self = this;
|
|
|
|
core._defaults = $.extend(true, core._defaults, defaults);
|
|
self._core = core;
|
|
self._timers = {};
|
|
};
|
|
|
|
/**
|
|
* Allows starting of multiple timeout calls. Each time this method is called with the same
|
|
* timer name, the timer is reset. This functionality is useful in cases where an action needs
|
|
* to occur only after a certain period of inactivity. For example, making an AJAX call after
|
|
* user stoped typing for 1 second.
|
|
*
|
|
* @signature TextExtPlugin.startTimer(name, delay, callback)
|
|
*
|
|
* @param name {String} Timer name.
|
|
* @param delay {Number} Delay in seconds.
|
|
* @param callback {Function} Callback function.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/25
|
|
* @id TextExtPlugin.startTimer
|
|
*/
|
|
p.startTimer = function(name, delay, callback)
|
|
{
|
|
var self = this;
|
|
|
|
self.stopTimer(name);
|
|
|
|
self._timers[name] = setTimeout(
|
|
function()
|
|
{
|
|
delete self._timers[name];
|
|
callback.apply(self);
|
|
},
|
|
delay * 1000
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Stops the timer by name without resetting it.
|
|
*
|
|
* @signature TextExtPlugin.stopTimer(name)
|
|
*
|
|
* @param name {String} Timer name.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/25
|
|
* @id TextExtPlugin.stopTimer
|
|
*/
|
|
p.stopTimer = function(name)
|
|
{
|
|
clearTimeout(this._timers[name]);
|
|
};
|
|
|
|
/**
|
|
* Returns instance of the `TextExt` to which current instance of the plugin is attached to.
|
|
*
|
|
* @signature TextExtPlugin.core()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtPlugin.core
|
|
*/
|
|
p.core = function()
|
|
{
|
|
return this._core;
|
|
};
|
|
|
|
/**
|
|
* Shortcut to the core's `opts()` method. Returns option value.
|
|
*
|
|
* @signature TextExtPlugin.opts(name)
|
|
*
|
|
* @param name {String} Option name as described in the options.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtPlugin.opts
|
|
*/
|
|
p.opts = function(name)
|
|
{
|
|
return this.core().opts(name);
|
|
};
|
|
|
|
/**
|
|
* Shortcut to the core's `itemManager()` method. Returns instance of the `ItemManger` that is
|
|
* currently in use.
|
|
*
|
|
* @signature TextExtPlugin.itemManager()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtPlugin.itemManager
|
|
*/
|
|
p.itemManager = function()
|
|
{
|
|
return this.core().itemManager();
|
|
};
|
|
|
|
/**
|
|
* Shortcut to the core's `input()` method. Returns instance of the HTML element that represents
|
|
* current text input.
|
|
*
|
|
* @signature TextExtPlugin.input()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtPlugin.input
|
|
*/
|
|
p.input = function()
|
|
{
|
|
return this.core().input();
|
|
};
|
|
|
|
/**
|
|
* Shortcut to the commonly used `this.input().val()` call to get or set value of the text input.
|
|
*
|
|
* @signature TextExtPlugin.val(value)
|
|
*
|
|
* @param value {String} Optional value. If specified, the value will be set, otherwise it will be
|
|
* returned.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/20
|
|
* @id TextExtPlugin.val
|
|
*/
|
|
p.val = function(value)
|
|
{
|
|
var input = this.input();
|
|
|
|
if(typeof(value) === UNDEFINED)
|
|
return input.val();
|
|
else
|
|
input.val(value);
|
|
};
|
|
|
|
/**
|
|
* Shortcut to the core's `trigger()` method. Triggers specified event with arguments on the
|
|
* component core.
|
|
*
|
|
* @signature TextExtPlugin.trigger(event, ...args)
|
|
*
|
|
* @param event {String} Name of the event to trigger.
|
|
* @param ...args All remaining arguments will be passed to the event handler.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtPlugin.trigger
|
|
*/
|
|
p.trigger = function()
|
|
{
|
|
var core = this.core();
|
|
core.trigger.apply(core, arguments);
|
|
};
|
|
|
|
/**
|
|
* Shortcut to the core's `bind()` method. Binds specified handler to the event.
|
|
*
|
|
* @signature TextExtPlugin.bind(event, handler)
|
|
*
|
|
* @param event {String} Event name.
|
|
* @param handler {Function} Event handler.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/20
|
|
* @id TextExtPlugin.bind
|
|
*/
|
|
p.bind = function(event, handler)
|
|
{
|
|
this.core().bind(event, handler);
|
|
};
|
|
|
|
/**
|
|
* Returns initialization priority for this plugin. If current plugin depends upon some other plugin
|
|
* to be initialized before or after, priority needs to be adjusted accordingly. Plugins with higher
|
|
* priority initialize before plugins with lower priority.
|
|
*
|
|
* Default initialization priority is `0`.
|
|
*
|
|
* @signature TextExtPlugin.initPriority()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/22
|
|
* @id TextExtPlugin.initPriority
|
|
*/
|
|
p.initPriority = function()
|
|
{
|
|
return 0;
|
|
};
|
|
|
|
//--------------------------------------------------------------------------------
|
|
// jQuery Integration
|
|
|
|
/**
|
|
* TextExt integrates as a jQuery plugin available through the `$(selector).textext(opts)` call. If
|
|
* `opts` argument is passed, then a new instance of `TextExt` will be created for all the inputs
|
|
* that match the `selector`. If `opts` wasn't passed and TextExt was already intantiated for
|
|
* inputs that match the `selector`, array of `TextExt` instances will be returned instead.
|
|
*
|
|
* // will create a new instance of `TextExt` for all elements that match `.sample`
|
|
* $('.sample').textext({ ... });
|
|
*
|
|
* // will return array of all `TextExt` instances
|
|
* var list = $('.sample').textext();
|
|
*
|
|
* The following properties are also exposed through the jQuery `$.fn.textext`:
|
|
*
|
|
* * `TextExt` -- `TextExt` class.
|
|
* * `TextExtPlugin` -- `TextExtPlugin` class.
|
|
* * `ItemManager` -- `ItemManager` class.
|
|
* * `plugins` -- Key/value table of all registered plugins.
|
|
* * `addPlugin(name, constructor)` -- All plugins should register themselves using this function.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExt.jquery
|
|
*/
|
|
|
|
var cssInjected = false;
|
|
|
|
var textext = $.fn.textext = function(opts)
|
|
{
|
|
var css;
|
|
|
|
if(!cssInjected && (css = $.fn.textext.css) != null)
|
|
{
|
|
$('head').append('<style>' + css + '</style>');
|
|
cssInjected = true;
|
|
}
|
|
|
|
return this.map(function()
|
|
{
|
|
var self = $(this);
|
|
|
|
if(opts == null)
|
|
return self.data('textext');
|
|
|
|
var instance = new TextExt();
|
|
|
|
instance.init(self, opts);
|
|
self.data('textext', instance);
|
|
|
|
return instance.input()[0];
|
|
});
|
|
};
|
|
|
|
/**
|
|
* This static function registers a new plugin which makes it available through the `plugins` option
|
|
* to the end user. The name specified here is the name the end user would put in the `plugins` option
|
|
* to add this plugin to a new instance of TextExt.
|
|
*
|
|
* @signature $.fn.textext.addPlugin(name, constructor)
|
|
*
|
|
* @param name {String} Name of the plugin.
|
|
* @param constructor {Function} Plugin constructor.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/10/11
|
|
* @id TextExt.addPlugin
|
|
*/
|
|
textext.addPlugin = function(name, constructor)
|
|
{
|
|
textext.plugins[name] = constructor;
|
|
constructor.prototype = new textext.TextExtPlugin();
|
|
};
|
|
|
|
/**
|
|
* This static function registers a new patch which is added to each instance of TextExt. If you are
|
|
* adding a new patch, make sure to call this method.
|
|
*
|
|
* @signature $.fn.textext.addPatch(name, constructor)
|
|
*
|
|
* @param name {String} Name of the patch.
|
|
* @param constructor {Function} Patch constructor.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/10/11
|
|
* @id TextExt.addPatch
|
|
*/
|
|
textext.addPatch = function(name, constructor)
|
|
{
|
|
textext.patches[name] = constructor;
|
|
constructor.prototype = new textext.TextExtPlugin();
|
|
};
|
|
|
|
textext.TextExt = TextExt;
|
|
textext.TextExtPlugin = TextExtPlugin;
|
|
textext.ItemManager = ItemManager;
|
|
textext.plugins = {};
|
|
textext.patches = {};
|
|
})(jQuery);
|
|
|
|
(function($)
|
|
{
|
|
function TextExtIE9Patches() {};
|
|
|
|
$.fn.textext.TextExtIE9Patches = TextExtIE9Patches;
|
|
$.fn.textext.addPatch('ie9',TextExtIE9Patches);
|
|
|
|
var p = TextExtIE9Patches.prototype;
|
|
|
|
p.init = function(core)
|
|
{
|
|
if(navigator.userAgent.indexOf('MSIE 9') == -1)
|
|
return;
|
|
|
|
var self = this;
|
|
|
|
core.on({ postInvalidate : self.onPostInvalidate });
|
|
};
|
|
|
|
p.onPostInvalidate = function()
|
|
{
|
|
var self = this,
|
|
input = self.input(),
|
|
val = input.val()
|
|
;
|
|
|
|
// agorbatchev :: IE9 doesn't seem to update the padding if box-sizing is on until the
|
|
// text box value changes, so forcing this change seems to do the trick of updating
|
|
// IE's padding visually.
|
|
input.val(Math.random());
|
|
input.val(val);
|
|
};
|
|
})(jQuery);
|
|
|
|
;/**
|
|
* jQuery TextExt Plugin
|
|
* http://textextjs.com
|
|
*
|
|
* @version 1.3.1
|
|
* @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
|
|
* @license MIT License
|
|
*/
|
|
(function($)
|
|
{
|
|
/**
|
|
* AJAX plugin is very useful if you want to load list of items from a data point and pass it
|
|
* to the Autocomplete or Filter plugins.
|
|
*
|
|
* Because it meant to be as a helper method for either Autocomplete or Filter plugin, without
|
|
* either of these two present AJAX plugin won't do anything.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/16
|
|
* @id TextExtAjax
|
|
*/
|
|
function TextExtAjax() {};
|
|
|
|
$.fn.textext.TextExtAjax = TextExtAjax;
|
|
$.fn.textext.addPlugin('ajax', TextExtAjax);
|
|
|
|
var p = TextExtAjax.prototype,
|
|
|
|
/**
|
|
* AJAX plugin options are grouped under `ajax` when passed to the `$().textext()` function. Be
|
|
* mindful that the whole `ajax` object is also passed to jQuery `$.ajax` call which means that
|
|
* you can change all jQuery options as well. Please refer to the jQuery documentation on how
|
|
* to set url and all other parameters. For example:
|
|
*
|
|
* $('textarea').textext({
|
|
* plugins: 'ajax',
|
|
* ajax: {
|
|
* url: 'http://...'
|
|
* }
|
|
* })
|
|
*
|
|
* **Important**: Because it's necessary to pass options to `jQuery.ajax()` in a single object,
|
|
* all jQuery related AJAX options like `url`, `dataType`, etc **must** be within the `ajax` object.
|
|
* This is the exception to general rule that TextExt options can be specified in dot or camel case
|
|
* notation.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/16
|
|
* @id TextExtAjax.options
|
|
*/
|
|
|
|
/**
|
|
* By default, when user starts typing into the text input, AJAX plugin will start making requests
|
|
* to the `url` that you have specified and will pass whatever user has typed so far as a parameter
|
|
* named `q`, eg `?q=foo`.
|
|
*
|
|
* If you wish to change this behaviour, you can pass a function as a value for this option which
|
|
* takes one argument (the user input) and should return a key/value object that will be converted
|
|
* to the request parameters. For example:
|
|
*
|
|
* 'dataCallback' : function(query)
|
|
* {
|
|
* return { 'search' : query };
|
|
* }
|
|
*
|
|
* @name ajax.data.callback
|
|
* @default null
|
|
* @author agorbatchev
|
|
* @date 2011/08/16
|
|
* @id TextExtAjax.options.data.callback
|
|
*/
|
|
OPT_DATA_CALLBACK = 'ajax.data.callback',
|
|
|
|
/**
|
|
* By default, the server end point is constantly being reloaded whenever user changes the value
|
|
* in the text input. If you'd rather have the client do result filtering, you can return all
|
|
* possible results from the server and cache them on the client by setting this option to `true`.
|
|
*
|
|
* In such a case, only one call to the server will be made and filtering will be performed on
|
|
* the client side using `ItemManager` attached to the core.
|
|
*
|
|
* @name ajax.data.results
|
|
* @default false
|
|
* @author agorbatchev
|
|
* @date 2011/08/16
|
|
* @id TextExtAjax.options.cache.results
|
|
*/
|
|
OPT_CACHE_RESULTS = 'ajax.cache.results',
|
|
|
|
/**
|
|
* The loading message delay is set in seconds and will specify how long it would take before
|
|
* user sees the message. If you don't want user to ever see this message, set the option value
|
|
* to `Number.MAX_VALUE`.
|
|
*
|
|
* @name ajax.loading.delay
|
|
* @default 0.5
|
|
* @author agorbatchev
|
|
* @date 2011/08/16
|
|
* @id TextExtAjax.options.loading.delay
|
|
*/
|
|
OPT_LOADING_DELAY = 'ajax.loading.delay',
|
|
|
|
/**
|
|
* Whenever an AJAX request is made and the server takes more than the number of seconds specified
|
|
* in `ajax.loading.delay` to respond, the message specified in this option will appear in the drop
|
|
* down.
|
|
*
|
|
* @name ajax.loading.message
|
|
* @default "Loading..."
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAjax.options.loading.message
|
|
*/
|
|
OPT_LOADING_MESSAGE = 'ajax.loading.message',
|
|
|
|
/**
|
|
* When user is typing in or otherwise changing the value of the text input, it's undesirable to make
|
|
* an AJAX request for every keystroke. Instead it's more conservative to send a request every number
|
|
* of seconds while user is typing the value. This number of seconds is specified by the `ajax.type.delay`
|
|
* option.
|
|
*
|
|
* @name ajax.type.delay
|
|
* @default 0.5
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAjax.options.type.delay
|
|
*/
|
|
OPT_TYPE_DELAY = 'ajax.type.delay',
|
|
|
|
/**
|
|
* AJAX plugin dispatches or reacts to the following events.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAjax.events
|
|
*/
|
|
|
|
/**
|
|
* AJAX plugin reacts to the `getSuggestions` event dispatched by the Autocomplete plugin.
|
|
*
|
|
* @name getSuggestions
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAjax.events.getSuggestions
|
|
*/
|
|
|
|
/**
|
|
* In the event of successful AJAX request, the AJAX coponent dispatches the `setSuggestions`
|
|
* event meant to be recieved by the Autocomplete plugin.
|
|
*
|
|
* @name setSuggestions
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAjax.events.setSuggestions
|
|
*/
|
|
EVENT_SET_SUGGESTION = 'setSuggestions',
|
|
|
|
/**
|
|
* AJAX plugin dispatches the `showDropdown` event which Autocomplete plugin is expecting.
|
|
* This is used to temporarily show the loading message if the AJAX request is taking longer
|
|
* than expected.
|
|
*
|
|
* @name showDropdown
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAjax.events.showDropdown
|
|
*/
|
|
EVENT_SHOW_DROPDOWN = 'showDropdown',
|
|
|
|
TIMER_LOADING = 'loading',
|
|
|
|
DEFAULT_OPTS = {
|
|
ajax : {
|
|
typeDelay : 0.5,
|
|
loadingMessage : 'Loading...',
|
|
loadingDelay : 0.5,
|
|
cacheResults : false,
|
|
dataCallback : null
|
|
}
|
|
}
|
|
;
|
|
|
|
/**
|
|
* Initialization method called by the core during plugin instantiation.
|
|
*
|
|
* @signature TextExtAjax.init(core)
|
|
*
|
|
* @param core {TextExt} Instance of the TextExt core class.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAjax.init
|
|
*/
|
|
p.init = function(core)
|
|
{
|
|
var self = this;
|
|
|
|
self.baseInit(core, DEFAULT_OPTS);
|
|
|
|
self.on({
|
|
getSuggestions : self.onGetSuggestions
|
|
});
|
|
|
|
self._suggestions = null;
|
|
};
|
|
|
|
/**
|
|
* Performas an async AJAX with specified options.
|
|
*
|
|
* @signature TextExtAjax.load(query)
|
|
*
|
|
* @param query {String} Value that user has typed into the text area which is
|
|
* presumably the query.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/14
|
|
* @id TextExtAjax.load
|
|
*/
|
|
p.load = function(query)
|
|
{
|
|
var self = this,
|
|
dataCallback = self.opts(OPT_DATA_CALLBACK) || function(query) { return { q : query } },
|
|
opts
|
|
;
|
|
|
|
opts = $.extend(true,
|
|
{
|
|
data : dataCallback(query),
|
|
success : function(data) { self.onComplete(data, query) },
|
|
error : function(jqXHR, message) { console.error(message, query) }
|
|
},
|
|
self.opts('ajax')
|
|
);
|
|
|
|
$.ajax(opts);
|
|
};
|
|
|
|
/**
|
|
* Successful call AJAX handler. Takes the data that came back from AJAX and the
|
|
* original query that was used to make the call.
|
|
*
|
|
* @signature TextExtAjax.onComplete(data, query)
|
|
*
|
|
* @param data {Object} Data loaded from the server, should be an Array of strings
|
|
* by default or whatever data structure your custom `ItemManager` implements.
|
|
*
|
|
* @param query {String} Query string, ie whatever user has typed in.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/14
|
|
* @id TextExtAjax.onComplete
|
|
*/
|
|
p.onComplete = function(data, query)
|
|
{
|
|
var self = this,
|
|
result = data
|
|
;
|
|
|
|
self.dontShowLoading();
|
|
|
|
// If results are expected to be cached, then we store the original
|
|
// data set and return the filtered one based on the original query.
|
|
// That means we do filtering on the client side, instead of the
|
|
// server side.
|
|
if(self.opts(OPT_CACHE_RESULTS) == true)
|
|
{
|
|
self._suggestions = data;
|
|
result = self.itemManager().filter(data, query);
|
|
}
|
|
|
|
self.trigger(EVENT_SET_SUGGESTION, { result : result });
|
|
};
|
|
|
|
/**
|
|
* If show loading message timer was started, calling this function disables it,
|
|
* otherwise nothing else happens.
|
|
*
|
|
* @signature TextExtAjax.dontShowLoading()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/16
|
|
* @id TextExtAjax.dontShowLoading
|
|
*/
|
|
p.dontShowLoading = function()
|
|
{
|
|
this.stopTimer(TIMER_LOADING);
|
|
};
|
|
|
|
/**
|
|
* Shows message specified in `ajax.loading.message` if loading data takes more than
|
|
* number of seconds specified in `ajax.loading.delay`.
|
|
*
|
|
* @signature TextExtAjax.showLoading()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/15
|
|
* @id TextExtAjax.showLoading
|
|
*/
|
|
p.showLoading = function()
|
|
{
|
|
var self = this;
|
|
|
|
self.dontShowLoading();
|
|
self.startTimer(
|
|
TIMER_LOADING,
|
|
self.opts(OPT_LOADING_DELAY),
|
|
function()
|
|
{
|
|
self.trigger(EVENT_SHOW_DROPDOWN, function(autocomplete)
|
|
{
|
|
autocomplete.clearItems();
|
|
var node = autocomplete.addDropdownItem(self.opts(OPT_LOADING_MESSAGE));
|
|
node.addClass('text-loading');
|
|
});
|
|
}
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `getSuggestions` event and begin loading suggestions. If
|
|
* `ajax.cache.results` is specified, all calls after the first one will use
|
|
* cached data and filter it with the `core.itemManager.filter()`.
|
|
*
|
|
* @signature TextExtAjax.onGetSuggestions(e, data)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
* @param data {Object} Data structure passed with the `getSuggestions` event
|
|
* which contains the user query, eg `{ query : "..." }`.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/15
|
|
* @id TextExtAjax.onGetSuggestions
|
|
*/
|
|
p.onGetSuggestions = function(e, data)
|
|
{
|
|
var self = this,
|
|
suggestions = self._suggestions,
|
|
query = (data || {}).query || ''
|
|
;
|
|
|
|
if(suggestions && self.opts(OPT_CACHE_RESULTS) === true)
|
|
return self.onComplete(suggestions, query);
|
|
|
|
self.startTimer(
|
|
'ajax',
|
|
self.opts(OPT_TYPE_DELAY),
|
|
function()
|
|
{
|
|
self.showLoading();
|
|
self.load(query);
|
|
}
|
|
);
|
|
};
|
|
})(jQuery);
|
|
;/**
|
|
* jQuery TextExt Plugin
|
|
* http://textextjs.com
|
|
*
|
|
* @version 1.3.1
|
|
* @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
|
|
* @license MIT License
|
|
*/
|
|
(function($)
|
|
{
|
|
/**
|
|
* Displays a dropdown style arrow button. The `TextExtArrow` works together with the
|
|
* `TextExtAutocomplete` plugin and whenever clicked tells the autocomplete plugin to
|
|
* display its suggestions.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/12/27
|
|
* @id TextExtArrow
|
|
*/
|
|
function TextExtArrow() {};
|
|
|
|
$.fn.textext.TextExtArrow = TextExtArrow;
|
|
$.fn.textext.addPlugin('arrow', TextExtArrow);
|
|
|
|
var p = TextExtArrow.prototype,
|
|
/**
|
|
* Arrow plugin only has one option and that is its HTML template. It could be
|
|
* changed when passed to the `$().textext()` function. For example:
|
|
*
|
|
* $('textarea').textext({
|
|
* plugins: 'arrow',
|
|
* html: {
|
|
* arrow: "<span/>"
|
|
* }
|
|
* })
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/12/27
|
|
* @id TextExtArrow.options
|
|
*/
|
|
|
|
/**
|
|
* HTML source that is used to generate markup required for the arrow.
|
|
*
|
|
* @name html.arrow
|
|
* @default '<div class="text-arrow"/>'
|
|
* @author agorbatchev
|
|
* @date 2011/12/27
|
|
* @id TextExtArrow.options.html.arrow
|
|
*/
|
|
OPT_HTML_ARROW = 'html.arrow',
|
|
|
|
DEFAULT_OPTS = {
|
|
html : {
|
|
arrow : '<div class="text-arrow"/>'
|
|
}
|
|
}
|
|
;
|
|
|
|
/**
|
|
* Initialization method called by the core during plugin instantiation.
|
|
*
|
|
* @signature TextExtArrow.init(core)
|
|
*
|
|
* @param core {TextExt} Instance of the TextExt core class.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/12/27
|
|
* @id TextExtArrow.init
|
|
*/
|
|
p.init = function(core)
|
|
{
|
|
var self = this,
|
|
arrow
|
|
;
|
|
|
|
self.baseInit(core, DEFAULT_OPTS);
|
|
|
|
self._arrow = arrow = $(self.opts(OPT_HTML_ARROW));
|
|
self.core().wrapElement().append(arrow);
|
|
arrow.bind('click', function(e) { self.onArrowClick(e); });
|
|
};
|
|
|
|
//--------------------------------------------------------------------------------
|
|
// Event handlers
|
|
|
|
/**
|
|
* Reacts to the `click` event whenever user clicks the arrow.
|
|
*
|
|
* @signature TextExtArrow.onArrowClick(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
* @author agorbatchev
|
|
* @date 2011/12/27
|
|
* @id TextExtArrow.onArrowClick
|
|
*/
|
|
p.onArrowClick = function(e)
|
|
{
|
|
this.trigger('toggleDropdown');
|
|
this.core().focusInput();
|
|
};
|
|
|
|
//--------------------------------------------------------------------------------
|
|
// Core functionality
|
|
|
|
})(jQuery);
|
|
;/**
|
|
* jQuery TextExt Plugin
|
|
* http://textextjs.com
|
|
*
|
|
* @version 1.3.1
|
|
* @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
|
|
* @license MIT License
|
|
*/
|
|
(function($)
|
|
{
|
|
/**
|
|
* Autocomplete plugin brings the classic autocomplete functionality to the TextExt ecosystem.
|
|
* The gist of functionality is when user starts typing in, for example a term or a tag, a
|
|
* dropdown would be presented with possible suggestions to complete the input quicker.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete
|
|
*/
|
|
function TextExtAutocomplete() {};
|
|
|
|
$.fn.textext.TextExtAutocomplete = TextExtAutocomplete;
|
|
$.fn.textext.addPlugin('autocomplete', TextExtAutocomplete);
|
|
|
|
var p = TextExtAutocomplete.prototype,
|
|
|
|
CSS_DOT = '.',
|
|
CSS_SELECTED = 'text-selected',
|
|
CSS_DOT_SELECTED = CSS_DOT + CSS_SELECTED,
|
|
CSS_SUGGESTION = 'text-suggestion',
|
|
CSS_DOT_SUGGESTION = CSS_DOT + CSS_SUGGESTION,
|
|
CSS_LABEL = 'text-label',
|
|
CSS_DOT_LABEL = CSS_DOT + CSS_LABEL,
|
|
|
|
/**
|
|
* Autocomplete plugin options are grouped under `autocomplete` when passed to the
|
|
* `$().textext()` function. For example:
|
|
*
|
|
* $('textarea').textext({
|
|
* plugins: 'autocomplete',
|
|
* autocomplete: {
|
|
* dropdownPosition: 'above'
|
|
* }
|
|
* })
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.options
|
|
*/
|
|
|
|
/**
|
|
* This is a toggle switch to enable or disable the Autucomplete plugin. The value is checked
|
|
* each time at the top level which allows you to toggle this setting on the fly.
|
|
*
|
|
* @name autocomplete.enabled
|
|
* @default true
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.options.autocomplete.enabled
|
|
*/
|
|
OPT_ENABLED = 'autocomplete.enabled',
|
|
|
|
/**
|
|
* This option allows to specify position of the dropdown. The two possible values
|
|
* are `above` and `below`.
|
|
*
|
|
* @name autocomplete.dropdown.position
|
|
* @default "below"
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.options.autocomplete.dropdown.position
|
|
*/
|
|
OPT_POSITION = 'autocomplete.dropdown.position',
|
|
|
|
/**
|
|
* This option allows to specify maximum height of the dropdown. Value is taken directly, so
|
|
* if desired height is 200 pixels, value must be `200px`.
|
|
*
|
|
* @name autocomplete.dropdown.maxHeight
|
|
* @default "100px"
|
|
* @author agorbatchev
|
|
* @date 2011/12/29
|
|
* @id TextExtAutocomplete.options.autocomplete.dropdown.maxHeight
|
|
* @version 1.1
|
|
*/
|
|
OPT_MAX_HEIGHT = 'autocomplete.dropdown.maxHeight',
|
|
|
|
/**
|
|
* This option allows to override how a suggestion item is rendered. The value should be
|
|
* a function, the first argument of which is suggestion to be rendered and `this` context
|
|
* is the current instance of `TextExtAutocomplete`.
|
|
*
|
|
* [Click here](/manual/examples/autocomplete-with-custom-render.html) to see a demo.
|
|
*
|
|
* For example:
|
|
*
|
|
* $('textarea').textext({
|
|
* plugins: 'autocomplete',
|
|
* autocomplete: {
|
|
* render: function(suggestion)
|
|
* {
|
|
* return '<b>' + suggestion + '</b>';
|
|
* }
|
|
* }
|
|
* })
|
|
*
|
|
* @name autocomplete.render
|
|
* @default null
|
|
* @author agorbatchev
|
|
* @date 2011/12/23
|
|
* @id TextExtAutocomplete.options.autocomplete.render
|
|
* @version 1.1
|
|
*/
|
|
OPT_RENDER = 'autocomplete.render',
|
|
|
|
/**
|
|
* HTML source that is used to generate the dropdown.
|
|
*
|
|
* @name html.dropdown
|
|
* @default '<div class="text-dropdown"><div class="text-list"/></div>'
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.options.html.dropdown
|
|
*/
|
|
OPT_HTML_DROPDOWN = 'html.dropdown',
|
|
|
|
/**
|
|
* HTML source that is used to generate each suggestion.
|
|
*
|
|
* @name html.suggestion
|
|
* @default '<div class="text-suggestion"><span class="text-label"/></div>'
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.options.html.suggestion
|
|
*/
|
|
OPT_HTML_SUGGESTION = 'html.suggestion',
|
|
|
|
/**
|
|
* Autocomplete plugin triggers or reacts to the following events.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.events
|
|
*/
|
|
|
|
/**
|
|
* Autocomplete plugin triggers and reacts to the `hideDropdown` to hide the dropdown if it's
|
|
* already visible.
|
|
*
|
|
* @name hideDropdown
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.events.hideDropdown
|
|
*/
|
|
EVENT_HIDE_DROPDOWN = 'hideDropdown',
|
|
|
|
/**
|
|
* Autocomplete plugin triggers and reacts to the `showDropdown` to show the dropdown if it's
|
|
* not already visible.
|
|
*
|
|
* It's possible to pass a render callback function which will be called instead of the
|
|
* default `TextExtAutocomplete.renderSuggestions()`.
|
|
*
|
|
* Here's how another plugin should trigger this event with the optional render callback:
|
|
*
|
|
* this.trigger('showDropdown', function(autocomplete)
|
|
* {
|
|
* autocomplete.clearItems();
|
|
* var node = autocomplete.addDropdownItem('<b>Item</b>');
|
|
* node.addClass('new-look');
|
|
* });
|
|
*
|
|
* @name showDropdown
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.events.showDropdown
|
|
*/
|
|
EVENT_SHOW_DROPDOWN = 'showDropdown',
|
|
|
|
/**
|
|
* Autocomplete plugin reacts to the `setSuggestions` event triggered by other plugins which
|
|
* wish to populate the suggestion items. Suggestions should be passed as event argument in the
|
|
* following format: `{ data : [ ... ] }`.
|
|
*
|
|
* Here's how another plugin should trigger this event:
|
|
*
|
|
* this.trigger('setSuggestions', { data : [ "item1", "item2" ] });
|
|
*
|
|
* @name setSuggestions
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.events.setSuggestions
|
|
*/
|
|
|
|
/**
|
|
* Autocomplete plugin triggers the `getSuggestions` event and expects to get results by listening for
|
|
* the `setSuggestions` event.
|
|
*
|
|
* @name getSuggestions
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.events.getSuggestions
|
|
*/
|
|
EVENT_GET_SUGGESTIONS = 'getSuggestions',
|
|
|
|
/**
|
|
* Autocomplete plugin triggers `getFormData` event with the current suggestion so that the the core
|
|
* will be updated with serialized data to be submitted with the HTML form.
|
|
*
|
|
* @name getFormData
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtAutocomplete.events.getFormData
|
|
*/
|
|
EVENT_GET_FORM_DATA = 'getFormData',
|
|
|
|
/**
|
|
* Autocomplete plugin reacts to `toggleDropdown` event and either shows or hides the dropdown
|
|
* depending if it's currently hidden or visible.
|
|
*
|
|
* @name toggleDropdown
|
|
* @author agorbatchev
|
|
* @date 2011/12/27
|
|
* @id TextExtAutocomplete.events.toggleDropdown
|
|
* @version 1.1
|
|
*/
|
|
EVENT_TOGGLE_DROPDOWN = 'toggleDropdown',
|
|
|
|
POSITION_ABOVE = 'above',
|
|
POSITION_BELOW = 'below',
|
|
|
|
DATA_MOUSEDOWN_ON_AUTOCOMPLETE = 'mousedownOnAutocomplete',
|
|
|
|
DEFAULT_OPTS = {
|
|
autocomplete : {
|
|
enabled : true,
|
|
dropdown : {
|
|
position : POSITION_BELOW,
|
|
maxHeight : '100px'
|
|
}
|
|
},
|
|
|
|
html : {
|
|
dropdown : '<div class="text-dropdown"><div class="text-list"/></div>',
|
|
suggestion : '<div class="text-suggestion"><span class="text-label"/></div>'
|
|
}
|
|
}
|
|
;
|
|
|
|
/**
|
|
* Initialization method called by the core during plugin instantiation.
|
|
*
|
|
* @signature TextExtAutocomplete.init(core)
|
|
*
|
|
* @param core {TextExt} Instance of the TextExt core class.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.init
|
|
*/
|
|
p.init = function(core)
|
|
{
|
|
var self = this;
|
|
|
|
self.baseInit(core, DEFAULT_OPTS);
|
|
|
|
var input = self.input(),
|
|
container
|
|
;
|
|
|
|
if(self.opts(OPT_ENABLED) === true)
|
|
{
|
|
self.on({
|
|
blur : self.onBlur,
|
|
anyKeyUp : self.onAnyKeyUp,
|
|
deleteKeyUp : self.onAnyKeyUp,
|
|
backspaceKeyPress : self.onBackspaceKeyPress,
|
|
enterKeyPress : self.onEnterKeyPress,
|
|
escapeKeyPress : self.onEscapeKeyPress,
|
|
setSuggestions : self.onSetSuggestions,
|
|
showDropdown : self.onShowDropdown,
|
|
hideDropdown : self.onHideDropdown,
|
|
toggleDropdown : self.onToggleDropdown,
|
|
postInvalidate : self.positionDropdown,
|
|
getFormData : self.onGetFormData,
|
|
|
|
// using keyDown for up/down keys so that repeat events are
|
|
// captured and user can scroll up/down by holding the keys
|
|
downKeyDown : self.onDownKeyDown,
|
|
upKeyDown : self.onUpKeyDown
|
|
});
|
|
|
|
container = $(self.opts(OPT_HTML_DROPDOWN));
|
|
container.insertAfter(input);
|
|
|
|
self.on(container, {
|
|
mouseover : self.onMouseOver,
|
|
mousedown : self.onMouseDown,
|
|
click : self.onClick
|
|
});
|
|
|
|
container
|
|
.css('maxHeight', self.opts(OPT_MAX_HEIGHT))
|
|
.addClass('text-position-' + self.opts(OPT_POSITION))
|
|
;
|
|
|
|
$(self).data('container', container);
|
|
|
|
$(document.body).click(function(e)
|
|
{
|
|
if (self.isDropdownVisible() && !self.withinWrapElement(e.target))
|
|
self.trigger(EVENT_HIDE_DROPDOWN);
|
|
});
|
|
|
|
self.positionDropdown();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns top level dropdown container HTML element.
|
|
*
|
|
* @signature TextExtAutocomplete.containerElement()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/15
|
|
* @id TextExtAutocomplete.containerElement
|
|
*/
|
|
p.containerElement = function()
|
|
{
|
|
return $(this).data('container');
|
|
};
|
|
|
|
//--------------------------------------------------------------------------------
|
|
// User mouse/keyboard input
|
|
|
|
/**
|
|
* Reacts to the `mouseOver` event triggered by the TextExt core.
|
|
*
|
|
* @signature TextExtAutocomplete.onMouseOver(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.onMouseOver
|
|
*/
|
|
p.onMouseOver = function(e)
|
|
{
|
|
var self = this,
|
|
target = $(e.target)
|
|
;
|
|
|
|
if(target.is(CSS_DOT_SUGGESTION))
|
|
{
|
|
self.clearSelected();
|
|
target.addClass(CSS_SELECTED);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `mouseDown` event triggered by the TextExt core.
|
|
*
|
|
* @signature TextExtAutocomplete.onMouseDown(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author adamayres
|
|
* @date 2012/01/13
|
|
* @id TextExtAutocomplete.onMouseDown
|
|
*/
|
|
p.onMouseDown = function(e)
|
|
{
|
|
this.containerElement().data(DATA_MOUSEDOWN_ON_AUTOCOMPLETE, true);
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `click` event triggered by the TextExt core.
|
|
*
|
|
* @signature TextExtAutocomplete.onClick(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.onClick
|
|
*/
|
|
p.onClick = function(e)
|
|
{
|
|
var self = this,
|
|
target = $(e.target)
|
|
;
|
|
|
|
if(target.is(CSS_DOT_SUGGESTION) || target.is(CSS_DOT_LABEL))
|
|
self.trigger('enterKeyPress');
|
|
|
|
if (self.core().hasPlugin('tags'))
|
|
self.val('');
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `blur` event triggered by the TextExt core.
|
|
*
|
|
* @signature TextExtAutocomplete.onBlur(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.onBlur
|
|
*/
|
|
p.onBlur = function(e)
|
|
{
|
|
var self = this,
|
|
container = self.containerElement(),
|
|
isBlurByMousedown = container.data(DATA_MOUSEDOWN_ON_AUTOCOMPLETE) === true
|
|
;
|
|
|
|
// only trigger a close event if the blur event was
|
|
// not triggered by a mousedown event on the autocomplete
|
|
// otherwise set focus back back on the input
|
|
if(self.isDropdownVisible())
|
|
isBlurByMousedown ? self.core().focusInput() : self.trigger(EVENT_HIDE_DROPDOWN);
|
|
|
|
container.removeData(DATA_MOUSEDOWN_ON_AUTOCOMPLETE);
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `backspaceKeyPress` event triggered by the TextExt core.
|
|
*
|
|
* @signature TextExtAutocomplete.onBackspaceKeyPress(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.onBackspaceKeyPress
|
|
*/
|
|
p.onBackspaceKeyPress = function(e)
|
|
{
|
|
var self = this,
|
|
isEmpty = self.val().length > 0
|
|
;
|
|
|
|
if(isEmpty || self.isDropdownVisible())
|
|
self.getSuggestions();
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `anyKeyUp` event triggered by the TextExt core.
|
|
*
|
|
* @signature TextExtAutocomplete.onAnyKeyUp(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.onAnyKeyUp
|
|
*/
|
|
p.onAnyKeyUp = function(e, keyCode)
|
|
{
|
|
var self = this,
|
|
isFunctionKey = self.opts('keys.' + keyCode) != null
|
|
;
|
|
|
|
if(self.val().length > 0 && !isFunctionKey)
|
|
self.getSuggestions();
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `downKeyDown` event triggered by the TextExt core.
|
|
*
|
|
* @signature TextExtAutocomplete.onDownKeyDown(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.onDownKeyDown
|
|
*/
|
|
p.onDownKeyDown = function(e)
|
|
{
|
|
var self = this;
|
|
|
|
self.isDropdownVisible()
|
|
? self.toggleNextSuggestion()
|
|
: self.getSuggestions()
|
|
;
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `upKeyDown` event triggered by the TextExt core.
|
|
*
|
|
* @signature TextExtAutocomplete.onUpKeyDown(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.onUpKeyDown
|
|
*/
|
|
p.onUpKeyDown = function(e)
|
|
{
|
|
this.togglePreviousSuggestion();
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `enterKeyPress` event triggered by the TextExt core.
|
|
*
|
|
* @signature TextExtAutocomplete.onEnterKeyPress(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.onEnterKeyPress
|
|
*/
|
|
p.onEnterKeyPress = function(e)
|
|
{
|
|
var self = this;
|
|
|
|
if(self.isDropdownVisible())
|
|
self.selectFromDropdown();
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `escapeKeyPress` event triggered by the TextExt core. Hides the dropdown
|
|
* if it's currently visible.
|
|
*
|
|
* @signature TextExtAutocomplete.onEscapeKeyPress(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.onEscapeKeyPress
|
|
*/
|
|
p.onEscapeKeyPress = function(e)
|
|
{
|
|
var self = this;
|
|
|
|
if(self.isDropdownVisible())
|
|
self.trigger(EVENT_HIDE_DROPDOWN);
|
|
};
|
|
|
|
//--------------------------------------------------------------------------------
|
|
// Core functionality
|
|
|
|
/**
|
|
* Positions dropdown either below or above the input based on the `autocomplete.dropdown.position`
|
|
* option specified, which could be either `above` or `below`.
|
|
*
|
|
* @signature TextExtAutocomplete.positionDropdown()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/15
|
|
* @id TextExtAutocomplete.positionDropdown
|
|
*/
|
|
p.positionDropdown = function()
|
|
{
|
|
var self = this,
|
|
container = self.containerElement(),
|
|
direction = self.opts(OPT_POSITION),
|
|
height = self.core().wrapElement().outerHeight(),
|
|
css = {}
|
|
;
|
|
|
|
css[direction === POSITION_ABOVE ? 'bottom' : 'top'] = height + 'px';
|
|
container.css(css);
|
|
};
|
|
|
|
/**
|
|
* Returns list of all the suggestion HTML elements in the dropdown.
|
|
*
|
|
* @signature TextExtAutocomplete.suggestionElements()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.suggestionElements
|
|
*/
|
|
p.suggestionElements = function()
|
|
{
|
|
return this.containerElement().find(CSS_DOT_SUGGESTION);
|
|
};
|
|
|
|
|
|
/**
|
|
* Highlights specified suggestion as selected in the dropdown.
|
|
*
|
|
* @signature TextExtAutocomplete.setSelectedSuggestion(suggestion)
|
|
*
|
|
* @param suggestion {Object} Suggestion object. With the default `ItemManager` this
|
|
* is expected to be a string, anything else with custom implementations.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.setSelectedSuggestion
|
|
*/
|
|
p.setSelectedSuggestion = function(suggestion)
|
|
{
|
|
if(!suggestion)
|
|
return;
|
|
|
|
var self = this,
|
|
all = self.suggestionElements(),
|
|
target = all.first(),
|
|
item, i
|
|
;
|
|
|
|
self.clearSelected();
|
|
|
|
for(i = 0; i < all.length; i++)
|
|
{
|
|
item = $(all[i]);
|
|
|
|
if(self.itemManager().compareItems(item.data(CSS_SUGGESTION), suggestion))
|
|
{
|
|
target = item.addClass(CSS_SELECTED);
|
|
break;
|
|
}
|
|
}
|
|
|
|
target.addClass(CSS_SELECTED);
|
|
self.scrollSuggestionIntoView(target);
|
|
};
|
|
|
|
/**
|
|
* Returns the first suggestion HTML element from the dropdown that is highlighted as selected.
|
|
*
|
|
* @signature TextExtAutocomplete.selectedSuggestionElement()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.selectedSuggestionElement
|
|
*/
|
|
p.selectedSuggestionElement = function()
|
|
{
|
|
return this.suggestionElements().filter(CSS_DOT_SELECTED).first();
|
|
};
|
|
|
|
/**
|
|
* Returns `true` if dropdown is currently visible, `false` otherwise.
|
|
*
|
|
* @signature TextExtAutocomplete.isDropdownVisible()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.isDropdownVisible
|
|
*/
|
|
p.isDropdownVisible = function()
|
|
{
|
|
return this.containerElement().is(':visible') === true;
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `getFormData` event triggered by the core. Returns data with the
|
|
* weight of 100 to be *less than the Tags plugin* data weight. The weights system is
|
|
* covered in greater detail in the [`getFormData`][1] event documentation.
|
|
*
|
|
* [1]: /manual/textext.html#getformdata
|
|
*
|
|
* @signature TextExtAutocomplete.onGetFormData(e, data, keyCode)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
* @param data {Object} Data object to be populated.
|
|
* @param keyCode {Number} Key code that triggered the original update request.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/22
|
|
* @id TextExtAutocomplete.onGetFormData
|
|
*/
|
|
p.onGetFormData = function(e, data, keyCode)
|
|
{
|
|
var self = this,
|
|
val = self.val(),
|
|
inputValue = val,
|
|
formValue = val
|
|
;
|
|
data[100] = self.formDataObject(inputValue, formValue);
|
|
};
|
|
|
|
/**
|
|
* Returns initialization priority of the Autocomplete plugin which is expected to be
|
|
* *greater than the Tags plugin* because of the dependencies. The value is 200.
|
|
*
|
|
* @signature TextExtAutocomplete.initPriority()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/22
|
|
* @id TextExtAutocomplete.initPriority
|
|
*/
|
|
p.initPriority = function()
|
|
{
|
|
return 200;
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `hideDropdown` event and hides the dropdown if it's already visible.
|
|
*
|
|
* @signature TextExtAutocomplete.onHideDropdown(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.onHideDropdown
|
|
*/
|
|
p.onHideDropdown = function(e)
|
|
{
|
|
this.hideDropdown();
|
|
};
|
|
|
|
/**
|
|
* Reacts to the 'toggleDropdown` event and shows or hides the dropdown depending if
|
|
* it's currently hidden or visible.
|
|
*
|
|
* @signature TextExtAutocomplete.onToggleDropdown(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/12/27
|
|
* @id TextExtAutocomplete.onToggleDropdown
|
|
* @version 1.1.0
|
|
*/
|
|
p.onToggleDropdown = function(e)
|
|
{
|
|
var self = this;
|
|
self.trigger(self.containerElement().is(':visible') ? EVENT_HIDE_DROPDOWN : EVENT_SHOW_DROPDOWN);
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `showDropdown` event and shows the dropdown if it's not already visible.
|
|
* It's possible to pass a render callback function which will be called instead of the
|
|
* default `TextExtAutocomplete.renderSuggestions()`.
|
|
*
|
|
* If no suggestion were previously loaded, it will fire `getSuggestions` event and exit.
|
|
*
|
|
* Here's how another plugin should trigger this event with the optional render callback:
|
|
*
|
|
* this.trigger('showDropdown', function(autocomplete)
|
|
* {
|
|
* autocomplete.clearItems();
|
|
* var node = autocomplete.addDropdownItem('<b>Item</b>');
|
|
* node.addClass('new-look');
|
|
* });
|
|
*
|
|
* @signature TextExtAutocomplete.onShowDropdown(e, renderCallback)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
* @param renderCallback {Function} Optional callback function which would be used to
|
|
* render dropdown items. As a first argument, reference to the current instance of
|
|
* Autocomplete plugin will be supplied. It's assumed, that if this callback is provided
|
|
* rendering will be handled completely manually.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.onShowDropdown
|
|
*/
|
|
p.onShowDropdown = function(e, renderCallback)
|
|
{
|
|
var self = this,
|
|
current = self.selectedSuggestionElement().data(CSS_SUGGESTION),
|
|
suggestions = self._suggestions
|
|
;
|
|
|
|
if(!suggestions)
|
|
return self.trigger(EVENT_GET_SUGGESTIONS);
|
|
|
|
if($.isFunction(renderCallback))
|
|
{
|
|
renderCallback(self);
|
|
}
|
|
else
|
|
{
|
|
self.renderSuggestions(self._suggestions);
|
|
self.toggleNextSuggestion();
|
|
}
|
|
|
|
self.showDropdown(self.containerElement());
|
|
self.setSelectedSuggestion(current);
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `setSuggestions` event. Expects to recieve the payload as the second argument
|
|
* in the following structure:
|
|
*
|
|
* {
|
|
* result : [ "item1", "item2" ],
|
|
* showHideDropdown : false
|
|
* }
|
|
*
|
|
* Notice the optional `showHideDropdown` option. By default, ie without the `showHideDropdown`
|
|
* value the method will trigger either `showDropdown` or `hideDropdown` depending if there are
|
|
* suggestions. If set to `false`, no event is triggered.
|
|
*
|
|
* @signature TextExtAutocomplete.onSetSuggestions(e, data)
|
|
*
|
|
* @param data {Object} Data payload.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.onSetSuggestions
|
|
*/
|
|
p.onSetSuggestions = function(e, data)
|
|
{
|
|
var self = this,
|
|
suggestions = self._suggestions = data.result
|
|
;
|
|
|
|
if(data.showHideDropdown !== false)
|
|
self.trigger(suggestions === null || suggestions.length === 0 ? EVENT_HIDE_DROPDOWN : EVENT_SHOW_DROPDOWN);
|
|
};
|
|
|
|
/**
|
|
* Prepears for and triggers the `getSuggestions` event with the `{ query : {String} }` as second
|
|
* argument.
|
|
*
|
|
* @signature TextExtAutocomplete.getSuggestions()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.getSuggestions
|
|
*/
|
|
p.getSuggestions = function()
|
|
{
|
|
var self = this,
|
|
val = self.val()
|
|
;
|
|
|
|
if(self._previousInputValue == val)
|
|
return;
|
|
|
|
// if user clears input, then we want to select first suggestion
|
|
// instead of the last one
|
|
if(val == '')
|
|
current = null;
|
|
|
|
self._previousInputValue = val;
|
|
self.trigger(EVENT_GET_SUGGESTIONS, { query : val });
|
|
};
|
|
|
|
/**
|
|
* Removes all HTML suggestion items from the dropdown.
|
|
*
|
|
* @signature TextExtAutocomplete.clearItems()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.clearItems
|
|
*/
|
|
p.clearItems = function()
|
|
{
|
|
this.containerElement().find('.text-list').children().remove();
|
|
};
|
|
|
|
/**
|
|
* Clears all and renders passed suggestions.
|
|
*
|
|
* @signature TextExtAutocomplete.renderSuggestions(suggestions)
|
|
*
|
|
* @param suggestions {Array} List of suggestions to render.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.renderSuggestions
|
|
*/
|
|
p.renderSuggestions = function(suggestions)
|
|
{
|
|
var self = this;
|
|
|
|
self.clearItems();
|
|
|
|
$.each(suggestions || [], function(index, item)
|
|
{
|
|
self.addSuggestion(item);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Shows the dropdown.
|
|
*
|
|
* @signature TextExtAutocomplete.showDropdown()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.showDropdown
|
|
*/
|
|
p.showDropdown = function()
|
|
{
|
|
this.containerElement().show();
|
|
};
|
|
|
|
/**
|
|
* Hides the dropdown.
|
|
*
|
|
* @signature TextExtAutocomplete.hideDropdown()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.hideDropdown
|
|
*/
|
|
p.hideDropdown = function()
|
|
{
|
|
var self = this,
|
|
dropdown = self.containerElement()
|
|
;
|
|
|
|
self._previousInputValue = null;
|
|
dropdown.hide();
|
|
};
|
|
|
|
/**
|
|
* Adds single suggestion to the bottom of the dropdown. Uses `ItemManager.itemToString()` to
|
|
* serialize provided suggestion to string.
|
|
*
|
|
* @signature TextExtAutocomplete.addSuggestion(suggestion)
|
|
*
|
|
* @param suggestion {Object} Suggestion item. By default expected to be a string.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.addSuggestion
|
|
*/
|
|
p.addSuggestion = function(suggestion)
|
|
{
|
|
var self = this,
|
|
renderer = self.opts(OPT_RENDER),
|
|
node = self.addDropdownItem(renderer ? renderer.call(self, suggestion) : self.itemManager().itemToString(suggestion))
|
|
;
|
|
|
|
node.data(CSS_SUGGESTION, suggestion);
|
|
};
|
|
|
|
/**
|
|
* Adds and returns HTML node to the bottom of the dropdown.
|
|
*
|
|
* @signature TextExtAutocomplete.addDropdownItem(html)
|
|
*
|
|
* @param html {String} HTML to be inserted into the item.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.addDropdownItem
|
|
*/
|
|
p.addDropdownItem = function(html)
|
|
{
|
|
var self = this,
|
|
container = self.containerElement().find('.text-list'),
|
|
node = $(self.opts(OPT_HTML_SUGGESTION))
|
|
;
|
|
|
|
node.find('.text-label').html(html);
|
|
container.append(node);
|
|
return node;
|
|
};
|
|
|
|
/**
|
|
* Removes selection highlight from all suggestion elements.
|
|
*
|
|
* @signature TextExtAutocomplete.clearSelected()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/02
|
|
* @id TextExtAutocomplete.clearSelected
|
|
*/
|
|
p.clearSelected = function()
|
|
{
|
|
this.suggestionElements().removeClass(CSS_SELECTED);
|
|
};
|
|
|
|
/**
|
|
* Selects next suggestion relative to the current one. If there's no
|
|
* currently selected suggestion, it will select the first one. Selected
|
|
* suggestion will always be scrolled into view.
|
|
*
|
|
* @signature TextExtAutocomplete.toggleNextSuggestion()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/02
|
|
* @id TextExtAutocomplete.toggleNextSuggestion
|
|
*/
|
|
p.toggleNextSuggestion = function()
|
|
{
|
|
var self = this,
|
|
selected = self.selectedSuggestionElement(),
|
|
next
|
|
;
|
|
|
|
if(selected.length > 0)
|
|
{
|
|
next = selected.next();
|
|
|
|
if(next.length > 0)
|
|
selected.removeClass(CSS_SELECTED);
|
|
}
|
|
else
|
|
{
|
|
next = self.suggestionElements().first();
|
|
}
|
|
|
|
next.addClass(CSS_SELECTED);
|
|
self.scrollSuggestionIntoView(next);
|
|
};
|
|
|
|
/**
|
|
* Selects previous suggestion relative to the current one. Selected
|
|
* suggestion will always be scrolled into view.
|
|
*
|
|
* @signature TextExtAutocomplete.togglePreviousSuggestion()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/02
|
|
* @id TextExtAutocomplete.togglePreviousSuggestion
|
|
*/
|
|
p.togglePreviousSuggestion = function()
|
|
{
|
|
var self = this,
|
|
selected = self.selectedSuggestionElement(),
|
|
prev = selected.prev()
|
|
;
|
|
|
|
if(prev.length == 0)
|
|
return;
|
|
|
|
self.clearSelected();
|
|
prev.addClass(CSS_SELECTED);
|
|
self.scrollSuggestionIntoView(prev);
|
|
};
|
|
|
|
/**
|
|
* Scrolls specified HTML suggestion element into the view.
|
|
*
|
|
* @signature TextExtAutocomplete.scrollSuggestionIntoView(item)
|
|
*
|
|
* @param item {HTMLElement} jQuery HTML suggestion element which needs to
|
|
* scrolled into view.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.scrollSuggestionIntoView
|
|
*/
|
|
p.scrollSuggestionIntoView = function(item)
|
|
{
|
|
var itemHeight = item.outerHeight(),
|
|
dropdown = this.containerElement(),
|
|
dropdownHeight = dropdown.innerHeight(),
|
|
scrollPos = dropdown.scrollTop(),
|
|
itemTop = (item.position() || {}).top,
|
|
scrollTo = null,
|
|
paddingTop = parseInt(dropdown.css('paddingTop'))
|
|
;
|
|
|
|
if(itemTop == null)
|
|
return;
|
|
|
|
// if scrolling down and item is below the bottom fold
|
|
if(itemTop + itemHeight > dropdownHeight)
|
|
scrollTo = itemTop + scrollPos + itemHeight - dropdownHeight + paddingTop;
|
|
|
|
// if scrolling up and item is above the top fold
|
|
if(itemTop < 0)
|
|
scrollTo = itemTop + scrollPos - paddingTop;
|
|
|
|
if(scrollTo != null)
|
|
dropdown.scrollTop(scrollTo);
|
|
};
|
|
|
|
/**
|
|
* Uses the value from the text input to finish autocomplete action. Currently selected
|
|
* suggestion from the dropdown will be used to complete the action. Triggers `hideDropdown`
|
|
* event.
|
|
*
|
|
* @signature TextExtAutocomplete.selectFromDropdown()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtAutocomplete.selectFromDropdown
|
|
*/
|
|
p.selectFromDropdown = function()
|
|
{
|
|
var self = this,
|
|
suggestion = self.selectedSuggestionElement().data(CSS_SUGGESTION)
|
|
;
|
|
|
|
if(suggestion)
|
|
{
|
|
self.val(self.itemManager().itemToString(suggestion));
|
|
self.core().getFormData();
|
|
}
|
|
|
|
self.trigger(EVENT_HIDE_DROPDOWN);
|
|
};
|
|
|
|
/**
|
|
* Determines if the specified HTML element is within the TextExt core wrap HTML element.
|
|
*
|
|
* @signature TextExtAutocomplete.withinWrapElement(element)
|
|
*
|
|
* @param element {HTMLElement} element to check if contained by wrap element
|
|
*
|
|
* @author adamayres
|
|
* @version 1.3.0
|
|
* @date 2012/01/15
|
|
* @id TextExtAutocomplete.withinWrapElement
|
|
*/
|
|
p.withinWrapElement = function(element)
|
|
{
|
|
return this.core().wrapElement().find(element).size() > 0;
|
|
}
|
|
})(jQuery);
|
|
;/**
|
|
* jQuery TextExt Plugin
|
|
* http://textextjs.com
|
|
*
|
|
* @version 1.3.1
|
|
* @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
|
|
* @license MIT License
|
|
*/
|
|
(function($)
|
|
{
|
|
/**
|
|
* The Filter plugin introduces ability to limit input that the text field
|
|
* will accept. If the Tags plugin is used, Filter plugin will limit which
|
|
* tags it's possible to add.
|
|
*
|
|
* The list of allowed items can be either specified through the
|
|
* options, can come from the Suggestions plugin or be loaded by the Ajax
|
|
* plugin. All these plugins have one thing in common -- they
|
|
* trigger `setSuggestions` event which the Filter plugin is expecting.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtFilter
|
|
*/
|
|
function TextExtFilter() {};
|
|
|
|
$.fn.textext.TextExtFilter = TextExtFilter;
|
|
$.fn.textext.addPlugin('filter', TextExtFilter);
|
|
|
|
var p = TextExtFilter.prototype,
|
|
|
|
/**
|
|
* Filter plugin options are grouped under `filter` when passed to the
|
|
* `$().textext()` function. For example:
|
|
*
|
|
* $('textarea').textext({
|
|
* plugins: 'filter',
|
|
* filter: {
|
|
* items: [ "item1", "item2" ]
|
|
* }
|
|
* })
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtFilter.options
|
|
*/
|
|
|
|
/**
|
|
* This is a toggle switch to enable or disable the Filter plugin. The value is checked
|
|
* each time at the top level which allows you to toggle this setting on the fly.
|
|
*
|
|
* @name filter.enabled
|
|
* @default true
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtFilter.options.enabled
|
|
*/
|
|
OPT_ENABLED = 'filter.enabled',
|
|
|
|
/**
|
|
* Arra of items that the Filter plugin will allow the Tag plugin to add to the list of
|
|
* its resut tags. Each item by default is expected to be a string which default `ItemManager`
|
|
* can work with. You can change the item type by supplying custom `ItemManager`.
|
|
*
|
|
* @name filter.items
|
|
* @default null
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtFilter.options.items
|
|
*/
|
|
OPT_ITEMS = 'filter.items',
|
|
|
|
/**
|
|
* Filter plugin dispatches and reacts to the following events.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtFilter.events
|
|
*/
|
|
|
|
/**
|
|
* Filter plugin reacts to the `isTagAllowed` event triggered by the Tags plugin before
|
|
* adding a new tag to the list. If the new tag is among the `items` specified in options,
|
|
* then the new tag will be allowed.
|
|
*
|
|
* @name isTagAllowed
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtFilter.events.isTagAllowed
|
|
*/
|
|
|
|
/**
|
|
* Filter plugin reacts to the `setSuggestions` event triggered by other plugins like
|
|
* Suggestions and Ajax.
|
|
*
|
|
* However, event if this event is handled and items are passed with it and stored, if `items`
|
|
* option was supplied, it will always take precedense.
|
|
*
|
|
* @name setSuggestions
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtFilter.events.setSuggestions
|
|
*/
|
|
|
|
DEFAULT_OPTS = {
|
|
filter : {
|
|
enabled : true,
|
|
items : null
|
|
}
|
|
}
|
|
;
|
|
|
|
/**
|
|
* Initialization method called by the core during plugin instantiation.
|
|
*
|
|
* @signature TextExtFilter.init(core)
|
|
*
|
|
* @param core {TextExt} Instance of the TextExt core class.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtFilter.init
|
|
*/
|
|
p.init = function(core)
|
|
{
|
|
var self = this;
|
|
self.baseInit(core, DEFAULT_OPTS);
|
|
|
|
self.on({
|
|
getFormData : self.onGetFormData,
|
|
isTagAllowed : self.onIsTagAllowed,
|
|
setSuggestions : self.onSetSuggestions
|
|
});
|
|
|
|
self._suggestions = null;
|
|
};
|
|
|
|
//--------------------------------------------------------------------------------
|
|
// Core functionality
|
|
|
|
/**
|
|
* Reacts to the [`getFormData`][1] event triggered by the core. Returns data with the
|
|
* weight of 200 to be *greater than the Autocomplete plugins* data weights.
|
|
* The weights system is covered in greater detail in the [`getFormData`][1] event
|
|
* documentation.
|
|
*
|
|
* This method does nothing if Tags tag is also present.
|
|
*
|
|
* [1]: /manual/textext.html#getformdata
|
|
*
|
|
* @signature TextExtFilter.onGetFormData(e, data, keyCode)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
* @param data {Object} Data object to be populated.
|
|
* @param keyCode {Number} Key code that triggered the original update request.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/12/28
|
|
* @id TextExtFilter.onGetFormData
|
|
* @version 1.1
|
|
*/
|
|
p.onGetFormData = function(e, data, keyCode)
|
|
{
|
|
var self = this,
|
|
val = self.val(),
|
|
inputValue = val,
|
|
formValue = ''
|
|
;
|
|
|
|
if(!self.core().hasPlugin('tags'))
|
|
{
|
|
if(self.isValueAllowed(inputValue))
|
|
formValue = val;
|
|
|
|
data[300] = self.formDataObject(inputValue, formValue);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Checks given value if it's present in `filterItems` or was loaded for the Autocomplete
|
|
* or by the Suggestions plugins. `value` is compared to each item using `ItemManager.compareItems`
|
|
* method which is currently attached to the core. Returns `true` if value is known or
|
|
* Filter plugin is disabled.
|
|
*
|
|
* @signature TextExtFilter.isValueAllowed(value)
|
|
*
|
|
* @param value {Object} Value to check.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/12/28
|
|
* @id TextExtFilter.isValueAllowed
|
|
* @version 1.1
|
|
*/
|
|
p.isValueAllowed = function(value)
|
|
{
|
|
var self = this,
|
|
list = self.opts('filterItems') || self._suggestions || [],
|
|
itemManager = self.itemManager(),
|
|
result = !self.opts(OPT_ENABLED), // if disabled, should just return true
|
|
i
|
|
;
|
|
|
|
for(i = 0; i < list.length && !result; i++)
|
|
if(itemManager.compareItems(value, list[i]))
|
|
result = true;
|
|
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Handles `isTagAllowed` event dispatched by the Tags plugin. If supplied tag is not
|
|
* in the `items` list, method sets `result` on the `data` argument to `false`.
|
|
*
|
|
* @signature TextExtFilter.onIsTagAllowed(e, data)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
* @param data {Object} Payload in the following format : `{ tag : {Object}, result : {Boolean} }`.
|
|
* @author agorbatchev
|
|
* @date 2011/08/04
|
|
* @id TextExtFilter.onIsTagAllowed
|
|
*/
|
|
p.onIsTagAllowed = function(e, data)
|
|
{
|
|
data.result = this.isValueAllowed(data.tag);
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `setSuggestions` events and stores supplied suggestions for future use.
|
|
*
|
|
* @signature TextExtFilter.onSetSuggestions(e, data)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
* @param data {Object} Payload in the following format : `{ result : {Array} } }`.
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtFilter.onSetSuggestions
|
|
*/
|
|
p.onSetSuggestions = function(e, data)
|
|
{
|
|
this._suggestions = data.result;
|
|
};
|
|
})(jQuery);
|
|
;/**
|
|
* jQuery TextExt Plugin
|
|
* http://textextjs.com
|
|
*
|
|
* @version 1.3.1
|
|
* @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
|
|
* @license MIT License
|
|
*/
|
|
(function($)
|
|
{
|
|
/**
|
|
* Focus plugin displays a visual effect whenever user sets focus
|
|
* into the text area.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtFocus
|
|
*/
|
|
function TextExtFocus() {};
|
|
|
|
$.fn.textext.TextExtFocus = TextExtFocus;
|
|
$.fn.textext.addPlugin('focus', TextExtFocus);
|
|
|
|
var p = TextExtFocus.prototype,
|
|
/**
|
|
* Focus plugin only has one option and that is its HTML template. It could be
|
|
* changed when passed to the `$().textext()` function. For example:
|
|
*
|
|
* $('textarea').textext({
|
|
* plugins: 'focus',
|
|
* html: {
|
|
* focus: "<span/>"
|
|
* }
|
|
* })
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtFocus.options
|
|
*/
|
|
|
|
/**
|
|
* HTML source that is used to generate markup required for the focus effect.
|
|
*
|
|
* @name html.focus
|
|
* @default '<div class="text-focus"/>'
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtFocus.options.html.focus
|
|
*/
|
|
OPT_HTML_FOCUS = 'html.focus',
|
|
|
|
/**
|
|
* Focus plugin dispatches or reacts to the following events.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtFocus.events
|
|
*/
|
|
|
|
/**
|
|
* Focus plugin reacts to the `focus` event and shows the markup generated from
|
|
* the `html.focus` option.
|
|
*
|
|
* @name focus
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtFocus.events.focus
|
|
*/
|
|
|
|
/**
|
|
* Focus plugin reacts to the `blur` event and hides the effect.
|
|
*
|
|
* @name blur
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtFocus.events.blur
|
|
*/
|
|
|
|
DEFAULT_OPTS = {
|
|
html : {
|
|
focus : '<div class="text-focus"/>'
|
|
}
|
|
}
|
|
;
|
|
|
|
/**
|
|
* Initialization method called by the core during plugin instantiation.
|
|
*
|
|
* @signature TextExtFocus.init(core)
|
|
*
|
|
* @param core {TextExt} Instance of the TextExt core class.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtFocus.init
|
|
*/
|
|
p.init = function(core)
|
|
{
|
|
var self = this;
|
|
|
|
self.baseInit(core, DEFAULT_OPTS);
|
|
self.core().wrapElement().append(self.opts(OPT_HTML_FOCUS));
|
|
self.on({
|
|
blur : self.onBlur,
|
|
focus : self.onFocus
|
|
});
|
|
|
|
self._timeoutId = 0;
|
|
};
|
|
|
|
//--------------------------------------------------------------------------------
|
|
// Event handlers
|
|
|
|
/**
|
|
* Reacts to the `blur` event and hides the focus effect with a slight delay which
|
|
* allows quick refocusing without effect blinking in and out.
|
|
*
|
|
* @signature TextExtFocus.onBlur(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/08
|
|
* @id TextExtFocus.onBlur
|
|
*/
|
|
p.onBlur = function(e)
|
|
{
|
|
var self = this;
|
|
|
|
clearTimeout(self._timeoutId);
|
|
|
|
self._timeoutId = setTimeout(function()
|
|
{
|
|
self.getFocus().hide();
|
|
},
|
|
100);
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `focus` event and shows the focus effect.
|
|
*
|
|
* @signature TextExtFocus.onFocus
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
* @author agorbatchev
|
|
* @date 2011/08/08
|
|
* @id TextExtFocus.onFocus
|
|
*/
|
|
p.onFocus = function(e)
|
|
{
|
|
var self = this;
|
|
|
|
clearTimeout(self._timeoutId);
|
|
|
|
self.getFocus().show();
|
|
};
|
|
|
|
//--------------------------------------------------------------------------------
|
|
// Core functionality
|
|
|
|
/**
|
|
* Returns focus effect HTML element.
|
|
*
|
|
* @signature TextExtFocus.getFocus()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/08
|
|
* @id TextExtFocus.getFocus
|
|
*/
|
|
p.getFocus = function()
|
|
{
|
|
return this.core().wrapElement().find('.text-focus');
|
|
};
|
|
})(jQuery);
|
|
;/**
|
|
* jQuery TextExt Plugin
|
|
* http://textextjs.com
|
|
*
|
|
* @version 1.3.1
|
|
* @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
|
|
* @license MIT License
|
|
*/
|
|
(function($)
|
|
{
|
|
/**
|
|
* Prompt plugin displays a visual user propmpt in the text input area. If user focuses
|
|
* on the input, the propt is hidden and only shown again when user focuses on another
|
|
* element and text input doesn't have a value.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtPrompt
|
|
*/
|
|
function TextExtPrompt() {};
|
|
|
|
$.fn.textext.TextExtPrompt = TextExtPrompt;
|
|
$.fn.textext.addPlugin('prompt', TextExtPrompt);
|
|
|
|
var p = TextExtPrompt.prototype,
|
|
|
|
CSS_HIDE_PROMPT = 'text-hide-prompt',
|
|
|
|
/**
|
|
* Prompt plugin has options to change the prompt label and its HTML template. The options
|
|
* could be changed when passed to the `$().textext()` function. For example:
|
|
*
|
|
* $('textarea').textext({
|
|
* plugins: 'prompt',
|
|
* prompt: 'Your email address'
|
|
* })
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtPrompt.options
|
|
*/
|
|
|
|
/**
|
|
* Prompt message that is displayed to the user whenever there's no value in the input.
|
|
*
|
|
* @name prompt
|
|
* @default 'Awaiting input...'
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtPrompt.options.prompt
|
|
*/
|
|
OPT_PROMPT = 'prompt',
|
|
|
|
/**
|
|
* HTML source that is used to generate markup required for the prompt effect.
|
|
*
|
|
* @name html.prompt
|
|
* @default '<div class="text-prompt"/>'
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtPrompt.options.html.prompt
|
|
*/
|
|
OPT_HTML_PROMPT = 'html.prompt',
|
|
|
|
/**
|
|
* Prompt plugin dispatches or reacts to the following events.
|
|
* @id TextExtPrompt.events
|
|
*/
|
|
|
|
/**
|
|
* Prompt plugin reacts to the `focus` event and hides the markup generated from
|
|
* the `html.prompt` option.
|
|
*
|
|
* @name focus
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtPrompt.events.focus
|
|
*/
|
|
|
|
/**
|
|
* Prompt plugin reacts to the `blur` event and shows the prompt back if user
|
|
* hasn't entered any value.
|
|
*
|
|
* @name blur
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtPrompt.events.blur
|
|
*/
|
|
|
|
DEFAULT_OPTS = {
|
|
prompt : 'Awaiting input...',
|
|
|
|
html : {
|
|
prompt : '<div class="text-prompt"/>'
|
|
}
|
|
}
|
|
;
|
|
|
|
/**
|
|
* Initialization method called by the core during plugin instantiation.
|
|
*
|
|
* @signature TextExtPrompt.init(core)
|
|
*
|
|
* @param core {TextExt} Instance of the TextExt core class.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtPrompt.init
|
|
*/
|
|
p.init = function(core)
|
|
{
|
|
var self = this,
|
|
placeholderKey = 'placeholder',
|
|
container,
|
|
prompt
|
|
;
|
|
|
|
self.baseInit(core, DEFAULT_OPTS);
|
|
|
|
container = $(self.opts(OPT_HTML_PROMPT));
|
|
$(self).data('container', container);
|
|
|
|
self.core().wrapElement().append(container);
|
|
self.setPrompt(self.opts(OPT_PROMPT));
|
|
|
|
prompt = core.input().attr(placeholderKey);
|
|
|
|
if(!prompt)
|
|
prompt = self.opts(OPT_PROMPT);
|
|
|
|
// clear placeholder attribute if set
|
|
core.input().attr(placeholderKey, '');
|
|
|
|
if(prompt)
|
|
self.setPrompt(prompt);
|
|
|
|
if($.trim(self.val()).length > 0)
|
|
self.hidePrompt();
|
|
|
|
self.on({
|
|
blur : self.onBlur,
|
|
focus : self.onFocus,
|
|
postInvalidate : self.onPostInvalidate,
|
|
postInit : self.onPostInit
|
|
});
|
|
};
|
|
|
|
//--------------------------------------------------------------------------------
|
|
// Event handlers
|
|
|
|
/**
|
|
* Reacts to the `postInit` and configures the plugin for initial display.
|
|
*
|
|
* @signature TextExtPrompt.onPostInit(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/24
|
|
* @id TextExtPrompt.onPostInit
|
|
*/
|
|
p.onPostInit = function(e)
|
|
{
|
|
this.invalidateBounds();
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `postInvalidate` and insures that prompt display remains correct.
|
|
*
|
|
* @signature TextExtPrompt.onPostInvalidate(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/24
|
|
* @id TextExtPrompt.onPostInvalidate
|
|
*/
|
|
p.onPostInvalidate = function(e)
|
|
{
|
|
this.invalidateBounds();
|
|
};
|
|
|
|
/**
|
|
* Repositions the prompt to make sure it's always at the same place as in the text input carret.
|
|
*
|
|
* @signature TextExtPrompt.invalidateBounds()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/24
|
|
* @id TextExtPrompt.invalidateBounds
|
|
*/
|
|
p.invalidateBounds = function()
|
|
{
|
|
var self = this,
|
|
input = self.input()
|
|
;
|
|
|
|
self.containerElement().css({
|
|
paddingLeft : input.css('paddingLeft'),
|
|
paddingTop : input.css('paddingTop')
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `blur` event and shows the prompt effect with a slight delay which
|
|
* allows quick refocusing without effect blinking in and out.
|
|
*
|
|
* The prompt is restored if the text box has no value.
|
|
*
|
|
* @signature TextExtPrompt.onBlur(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/08
|
|
* @id TextExtPrompt.onBlur
|
|
*/
|
|
p.onBlur = function(e)
|
|
{
|
|
var self = this;
|
|
|
|
self.startTimer('prompt', 0.1, function()
|
|
{
|
|
self.showPrompt();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Shows prompt HTML element.
|
|
*
|
|
* @signature TextExtPrompt.showPrompt()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/22
|
|
* @id TextExtPrompt.showPrompt
|
|
*/
|
|
p.showPrompt = function()
|
|
{
|
|
var self = this,
|
|
input = self.input()
|
|
;
|
|
|
|
if($.trim(self.val()).length === 0 && !input.is(':focus'))
|
|
self.containerElement().removeClass(CSS_HIDE_PROMPT);
|
|
};
|
|
|
|
/**
|
|
* Hides prompt HTML element.
|
|
*
|
|
* @signature TextExtPrompt.hidePrompt()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/22
|
|
* @id TextExtPrompt.hidePrompt
|
|
*/
|
|
p.hidePrompt = function()
|
|
{
|
|
this.stopTimer('prompt');
|
|
this.containerElement().addClass(CSS_HIDE_PROMPT);
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `focus` event and hides the prompt effect.
|
|
*
|
|
* @signature TextExtPrompt.onFocus
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
* @author agorbatchev
|
|
* @date 2011/08/08
|
|
* @id TextExtPrompt.onFocus
|
|
*/
|
|
p.onFocus = function(e)
|
|
{
|
|
this.hidePrompt();
|
|
};
|
|
|
|
//--------------------------------------------------------------------------------
|
|
// Core functionality
|
|
|
|
/**
|
|
* Sets the prompt display to the specified string.
|
|
*
|
|
* @signature TextExtPrompt.setPrompt(str)
|
|
*
|
|
* @oaram str {String} String that will be displayed in the prompt.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtPrompt.setPrompt
|
|
*/
|
|
p.setPrompt = function(str)
|
|
{
|
|
this.containerElement().text(str);
|
|
};
|
|
|
|
/**
|
|
* Returns prompt effect HTML element.
|
|
*
|
|
* @signature TextExtPrompt.containerElement()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/08
|
|
* @id TextExtPrompt.containerElement
|
|
*/
|
|
p.containerElement = function()
|
|
{
|
|
return $(this).data('container');
|
|
};
|
|
})(jQuery);
|
|
;/**
|
|
* jQuery TextExt Plugin
|
|
* http://textextjs.com
|
|
*
|
|
* @version 1.3.1
|
|
* @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
|
|
* @license MIT License
|
|
*/
|
|
(function($)
|
|
{
|
|
/**
|
|
* Suggestions plugin allows to easily specify the list of suggestion items that the
|
|
* Autocomplete plugin would present to the user.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtSuggestions
|
|
*/
|
|
function TextExtSuggestions() {};
|
|
|
|
$.fn.textext.TextExtSuggestions = TextExtSuggestions;
|
|
$.fn.textext.addPlugin('suggestions', TextExtSuggestions);
|
|
|
|
var p = TextExtSuggestions.prototype,
|
|
/**
|
|
* Suggestions plugin only has one option and that is to set suggestion items. It could be
|
|
* changed when passed to the `$().textext()` function. For example:
|
|
*
|
|
* $('textarea').textext({
|
|
* plugins: 'suggestions',
|
|
* suggestions: [ "item1", "item2" ]
|
|
* })
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtSuggestions.options
|
|
*/
|
|
|
|
/**
|
|
* List of items that Autocomplete plugin would display in the dropdown.
|
|
*
|
|
* @name suggestions
|
|
* @default null
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtSuggestions.options.suggestions
|
|
*/
|
|
OPT_SUGGESTIONS = 'suggestions',
|
|
|
|
/**
|
|
* Suggestions plugin dispatches or reacts to the following events.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtSuggestions.events
|
|
*/
|
|
|
|
/**
|
|
* Suggestions plugin reacts to the `getSuggestions` event and returns `suggestions` items
|
|
* from the options.
|
|
*
|
|
* @name getSuggestions
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtSuggestions.events.getSuggestions
|
|
*/
|
|
|
|
/**
|
|
* Suggestions plugin triggers the `setSuggestions` event to pass its own list of `Suggestions`
|
|
* to the Autocomplete plugin.
|
|
*
|
|
* @name setSuggestions
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtSuggestions.events.setSuggestions
|
|
*/
|
|
|
|
/**
|
|
* Suggestions plugin reacts to the `postInit` event to pass its list of `suggestions` to the
|
|
* Autocomplete right away.
|
|
*
|
|
* @name postInit
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtSuggestions.events.postInit
|
|
*/
|
|
|
|
DEFAULT_OPTS = {
|
|
suggestions : null
|
|
}
|
|
;
|
|
|
|
/**
|
|
* Initialization method called by the core during plugin instantiation.
|
|
*
|
|
* @signature TextExtSuggestions.init(core)
|
|
*
|
|
* @param core {TextExt} Instance of the TextExt core class.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/18
|
|
* @id TextExtSuggestions.init
|
|
*/
|
|
p.init = function(core)
|
|
{
|
|
var self = this;
|
|
|
|
self.baseInit(core, DEFAULT_OPTS);
|
|
|
|
self.on({
|
|
getSuggestions : self.onGetSuggestions,
|
|
postInit : self.onPostInit
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Triggers `setSuggestions` and passes supplied suggestions to the Autocomplete plugin.
|
|
*
|
|
* @signature TextExtSuggestions.setSuggestions(suggestions, showHideDropdown)
|
|
*
|
|
* @param suggestions {Array} List of suggestions. With the default `ItemManager` it should
|
|
* be a list of strings.
|
|
* @param showHideDropdown {Boolean} If it's undesirable to show the dropdown right after
|
|
* suggestions are set, `false` should be passed for this argument.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtSuggestions.setSuggestions
|
|
*/
|
|
p.setSuggestions = function(suggestions, showHideDropdown)
|
|
{
|
|
this.trigger('setSuggestions', { result : suggestions, showHideDropdown : showHideDropdown != false });
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `postInit` event and triggers `setSuggestions` event to set suggestions list
|
|
* right after initialization.
|
|
*
|
|
* @signature TextExtSuggestions.onPostInit(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtSuggestions.onPostInit
|
|
*/
|
|
p.onPostInit = function(e)
|
|
{
|
|
var self = this;
|
|
self.setSuggestions(self.opts(OPT_SUGGESTIONS), false);
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `getSuggestions` event and triggers `setSuggestions` event with the list
|
|
* of `suggestions` specified in the options.
|
|
*
|
|
* @signature TextExtSuggestions.onGetSuggestions(e, data)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
* @param data {Object} Payload from the `getSuggestions` event with the user query, eg `{ query: {String} }`.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtSuggestions.onGetSuggestions
|
|
*/
|
|
p.onGetSuggestions = function(e, data)
|
|
{
|
|
var self = this,
|
|
suggestions = self.opts(OPT_SUGGESTIONS)
|
|
;
|
|
|
|
suggestions.sort();
|
|
self.setSuggestions(self.itemManager().filter(suggestions, data.query));
|
|
};
|
|
})(jQuery);
|
|
;/**
|
|
* jQuery TextExt Plugin
|
|
* http://textextjs.com
|
|
*
|
|
* @version 1.3.1
|
|
* @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
|
|
* @license MIT License
|
|
*/
|
|
(function($)
|
|
{
|
|
/**
|
|
* Tags plugin brings in the traditional tag functionality where user can assemble and
|
|
* edit list of tags. Tags plugin works especially well together with Autocomplete, Filter,
|
|
* Suggestions and Ajax plugins to provide full spectrum of features. It can also work on
|
|
* its own and just do one thing -- tags.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtTags
|
|
*/
|
|
function TextExtTags() {};
|
|
|
|
$.fn.textext.TextExtTags = TextExtTags;
|
|
$.fn.textext.addPlugin('tags', TextExtTags);
|
|
|
|
var p = TextExtTags.prototype,
|
|
|
|
CSS_DOT = '.',
|
|
CSS_TAGS_ON_TOP = 'text-tags-on-top',
|
|
CSS_DOT_TAGS_ON_TOP = CSS_DOT + CSS_TAGS_ON_TOP,
|
|
CSS_TAG = 'text-tag',
|
|
CSS_DOT_TAG = CSS_DOT + CSS_TAG,
|
|
CSS_TAGS = 'text-tags',
|
|
CSS_DOT_TAGS = CSS_DOT + CSS_TAGS,
|
|
CSS_LABEL = 'text-label',
|
|
CSS_DOT_LABEL = CSS_DOT + CSS_LABEL,
|
|
CSS_REMOVE = 'text-remove',
|
|
CSS_DOT_REMOVE = CSS_DOT + CSS_REMOVE,
|
|
|
|
/**
|
|
* Tags plugin options are grouped under `tags` when passed to the
|
|
* `$().textext()` function. For example:
|
|
*
|
|
* $('textarea').textext({
|
|
* plugins: 'tags',
|
|
* tags: {
|
|
* items: [ "tag1", "tag2" ]
|
|
* }
|
|
* })
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtTags.options
|
|
*/
|
|
|
|
/**
|
|
* This is a toggle switch to enable or disable the Tags plugin. The value is checked
|
|
* each time at the top level which allows you to toggle this setting on the fly.
|
|
*
|
|
* @name tags.enabled
|
|
* @default true
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtTags.options.tags.enabled
|
|
*/
|
|
OPT_ENABLED = 'tags.enabled',
|
|
|
|
/**
|
|
* Allows to specify tags which will be added to the input by default upon initialization.
|
|
* Each item in the array must be of the type that current `ItemManager` can understand.
|
|
* Default type is `String`.
|
|
*
|
|
* @name tags.items
|
|
* @default null
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtTags.options.tags.items
|
|
*/
|
|
OPT_ITEMS = 'tags.items',
|
|
|
|
/**
|
|
* HTML source that is used to generate a single tag.
|
|
*
|
|
* @name html.tag
|
|
* @default '<div class="text-tags"/>'
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtTags.options.html.tag
|
|
*/
|
|
OPT_HTML_TAG = 'html.tag',
|
|
|
|
/**
|
|
* HTML source that is used to generate container for the tags.
|
|
*
|
|
* @name html.tags
|
|
* @default '<div class="text-tag"><div class="text-button"><span class="text-label"/><a class="text-remove"/></div></div>'
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtTags.options.html.tags
|
|
*/
|
|
OPT_HTML_TAGS = 'html.tags',
|
|
|
|
/**
|
|
* Tags plugin dispatches or reacts to the following events.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/17
|
|
* @id TextExtTags.events
|
|
*/
|
|
|
|
/**
|
|
* Tags plugin triggers the `isTagAllowed` event before adding each tag to the tag list. Other plugins have
|
|
* an opportunity to interrupt this by setting `result` of the second argument to `false`. For example:
|
|
*
|
|
* $('textarea').textext({...}).bind('isTagAllowed', function(e, data)
|
|
* {
|
|
* if(data.tag === 'foo')
|
|
* data.result = false;
|
|
* })
|
|
*
|
|
* The second argument `data` has the following format: `{ tag : {Object}, result : {Boolean} }`. `tag`
|
|
* property is in the format that the current `ItemManager` can understand.
|
|
*
|
|
* @name isTagAllowed
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtTags.events.isTagAllowed
|
|
*/
|
|
EVENT_IS_TAG_ALLOWED = 'isTagAllowed',
|
|
|
|
/**
|
|
* Tags plugin triggers the `tagClick` event when user clicks on one of the tags. This allows to process
|
|
* the click and potentially change the value of the tag (for example in case of user feedback).
|
|
*
|
|
* $('textarea').textext({...}).bind('tagClick', function(e, tag, value, callback)
|
|
* {
|
|
* var newValue = window.prompt('New value', value);
|
|
|
|
* if(newValue)
|
|
* callback(newValue, true);
|
|
* })
|
|
*
|
|
* Callback argument has the following signature:
|
|
*
|
|
* function(newValue, refocus)
|
|
* {
|
|
* ...
|
|
* }
|
|
*
|
|
* Please check out [example](/manual/examples/tags-changing.html).
|
|
*
|
|
* @name tagClick
|
|
* @version 1.3.0
|
|
* @author s.stok
|
|
* @date 2011/01/23
|
|
* @id TextExtTags.events.tagClick
|
|
*/
|
|
EVENT_TAG_CLICK = 'tagClick',
|
|
|
|
DEFAULT_OPTS = {
|
|
tags : {
|
|
enabled : true,
|
|
items : null
|
|
},
|
|
|
|
html : {
|
|
tags : '<div class="text-tags"/>',
|
|
tag : '<div class="text-tag"><div class="text-button"><span class="text-label"/><a class="text-remove"/></div></div>'
|
|
}
|
|
}
|
|
;
|
|
|
|
/**
|
|
* Initialization method called by the core during plugin instantiation.
|
|
*
|
|
* @signature TextExtTags.init(core)
|
|
*
|
|
* @param core {TextExt} Instance of the TextExt core class.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtTags.init
|
|
*/
|
|
p.init = function(core)
|
|
{
|
|
this.baseInit(core, DEFAULT_OPTS);
|
|
var self = this,
|
|
input = self.input(),
|
|
container
|
|
;
|
|
|
|
if(self.opts(OPT_ENABLED))
|
|
{
|
|
container = $(self.opts(OPT_HTML_TAGS));
|
|
input.after(container);
|
|
|
|
$(self).data('container', container);
|
|
|
|
self.on({
|
|
enterKeyPress : self.onEnterKeyPress,
|
|
backspaceKeyDown : self.onBackspaceKeyDown,
|
|
preInvalidate : self.onPreInvalidate,
|
|
postInit : self.onPostInit,
|
|
getFormData : self.onGetFormData
|
|
});
|
|
|
|
self.on(container, {
|
|
click : self.onClick,
|
|
mousemove : self.onContainerMouseMove
|
|
});
|
|
|
|
self.on(input, {
|
|
mousemove : self.onInputMouseMove
|
|
});
|
|
}
|
|
|
|
self._originalPadding = {
|
|
left : parseInt(input.css('paddingLeft') || 0),
|
|
top : parseInt(input.css('paddingTop') || 0)
|
|
};
|
|
|
|
self._paddingBox = {
|
|
left : 0,
|
|
top : 0
|
|
};
|
|
|
|
self.updateFormCache();
|
|
};
|
|
|
|
/**
|
|
* Returns HTML element in which all tag HTML elements are residing.
|
|
*
|
|
* @signature TextExtTags.containerElement()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/15
|
|
* @id TextExtTags.containerElement
|
|
*/
|
|
p.containerElement = function()
|
|
{
|
|
return $(this).data('container');
|
|
};
|
|
|
|
//--------------------------------------------------------------------------------
|
|
// Event handlers
|
|
|
|
/**
|
|
* Reacts to the `postInit` event triggered by the core and sets default tags
|
|
* if any were specified.
|
|
*
|
|
* @signature TextExtTags.onPostInit(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/09
|
|
* @id TextExtTags.onPostInit
|
|
*/
|
|
p.onPostInit = function(e)
|
|
{
|
|
var self = this;
|
|
self.addTags(self.opts(OPT_ITEMS));
|
|
};
|
|
|
|
/**
|
|
* Reacts to the [`getFormData`][1] event triggered by the core. Returns data with the
|
|
* weight of 200 to be *greater than the Autocomplete plugin* data weight. The weights
|
|
* system is covered in greater detail in the [`getFormData`][1] event documentation.
|
|
*
|
|
* [1]: /manual/textext.html#getformdata
|
|
*
|
|
* @signature TextExtTags.onGetFormData(e, data, keyCode)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
* @param data {Object} Data object to be populated.
|
|
* @param keyCode {Number} Key code that triggered the original update request.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/22
|
|
* @id TextExtTags.onGetFormData
|
|
*/
|
|
p.onGetFormData = function(e, data, keyCode)
|
|
{
|
|
var self = this,
|
|
inputValue = keyCode === 13 ? '' : self.val(),
|
|
formValue = self._formData
|
|
;
|
|
|
|
data[200] = self.formDataObject(inputValue, formValue);
|
|
};
|
|
|
|
/**
|
|
* Returns initialization priority of the Tags plugin which is expected to be
|
|
* *less than the Autocomplete plugin* because of the dependencies. The value is
|
|
* 100.
|
|
*
|
|
* @signature TextExtTags.initPriority()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/22
|
|
* @id TextExtTags.initPriority
|
|
*/
|
|
p.initPriority = function()
|
|
{
|
|
return 100;
|
|
};
|
|
|
|
/**
|
|
* Reacts to user moving mouse over the text area when cursor is over the text
|
|
* and not over the tags. Whenever mouse cursor is over the area covered by
|
|
* tags, the tags container is flipped to be on top of the text area which
|
|
* makes all tags functional with the mouse.
|
|
*
|
|
* @signature TextExtTags.onInputMouseMove(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/08
|
|
* @id TextExtTags.onInputMouseMove
|
|
*/
|
|
p.onInputMouseMove = function(e)
|
|
{
|
|
this.toggleZIndex(e);
|
|
};
|
|
|
|
/**
|
|
* Reacts to user moving mouse over the tags. Whenever the cursor moves out
|
|
* of the tags and back into where the text input is happening visually,
|
|
* the tags container is sent back under the text area which allows user
|
|
* to interact with the text using mouse cursor as expected.
|
|
*
|
|
* @signature TextExtTags.onContainerMouseMove(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/08
|
|
* @id TextExtTags.onContainerMouseMove
|
|
*/
|
|
p.onContainerMouseMove = function(e)
|
|
{
|
|
this.toggleZIndex(e);
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `backspaceKeyDown` event. When backspace key is pressed in an empty text field,
|
|
* deletes last tag from the list.
|
|
*
|
|
* @signature TextExtTags.onBackspaceKeyDown(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/02
|
|
* @id TextExtTags.onBackspaceKeyDown
|
|
*/
|
|
p.onBackspaceKeyDown = function(e)
|
|
{
|
|
var self = this,
|
|
lastTag = self.tagElements().last()
|
|
;
|
|
|
|
if(self.val().length == 0)
|
|
self.removeTag(lastTag);
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `preInvalidate` event and updates the input box to look like the tags are
|
|
* positioned inside it.
|
|
*
|
|
* @signature TextExtTags.onPreInvalidate(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtTags.onPreInvalidate
|
|
*/
|
|
p.onPreInvalidate = function(e)
|
|
{
|
|
var self = this,
|
|
lastTag = self.tagElements().last(),
|
|
pos = lastTag.position()
|
|
;
|
|
|
|
if(lastTag.length > 0)
|
|
pos.left += lastTag.innerWidth();
|
|
else
|
|
pos = self._originalPadding;
|
|
|
|
self._paddingBox = pos;
|
|
|
|
self.input().css({
|
|
paddingLeft : pos.left,
|
|
paddingTop : pos.top
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Reacts to the mouse `click` event.
|
|
*
|
|
* @signature TextExtTags.onClick(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtTags.onClick
|
|
*/
|
|
p.onClick = function(e)
|
|
{
|
|
var self = this,
|
|
core = self.core(),
|
|
source = $(e.target),
|
|
focus = 0,
|
|
tag
|
|
;
|
|
|
|
if(source.is(CSS_DOT_TAGS))
|
|
{
|
|
focus = 1;
|
|
}
|
|
else if(source.is(CSS_DOT_REMOVE))
|
|
{
|
|
self.removeTag(source.parents(CSS_DOT_TAG + ':first'));
|
|
focus = 1;
|
|
}
|
|
else if(source.is(CSS_DOT_LABEL))
|
|
{
|
|
tag = source.parents(CSS_DOT_TAG + ':first');
|
|
self.trigger(EVENT_TAG_CLICK, tag, tag.data(CSS_TAG), tagClickCallback);
|
|
}
|
|
|
|
function tagClickCallback(newValue, refocus)
|
|
{
|
|
tag.data(CSS_TAG, newValue);
|
|
tag.find(CSS_DOT_LABEL).text(self.itemManager().itemToString(newValue));
|
|
|
|
self.updateFormCache();
|
|
core.getFormData();
|
|
core.invalidateBounds();
|
|
|
|
if(refocus)
|
|
core.focusInput();
|
|
}
|
|
|
|
if(focus)
|
|
core.focusInput();
|
|
};
|
|
|
|
/**
|
|
* Reacts to the `enterKeyPress` event and adds whatever is currently in the text input
|
|
* as a new tag. Triggers `isTagAllowed` to check if the tag could be added first.
|
|
*
|
|
* @signature TextExtTags.onEnterKeyPress(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtTags.onEnterKeyPress
|
|
*/
|
|
p.onEnterKeyPress = function(e)
|
|
{
|
|
var self = this,
|
|
val = self.val(),
|
|
tag = self.itemManager().stringToItem(val)
|
|
;
|
|
|
|
if(self.isTagAllowed(tag))
|
|
{
|
|
self.addTags([ tag ]);
|
|
// refocus the textarea just in case it lost the focus
|
|
self.core().focusInput();
|
|
}
|
|
};
|
|
|
|
//--------------------------------------------------------------------------------
|
|
// Core functionality
|
|
|
|
/**
|
|
* Creates a cache object with all the tags currently added which will be returned
|
|
* in the `onGetFormData` handler.
|
|
*
|
|
* @signature TextExtTags.updateFormCache()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/09
|
|
* @id TextExtTags.updateFormCache
|
|
*/
|
|
p.updateFormCache = function()
|
|
{
|
|
var self = this,
|
|
result = []
|
|
;
|
|
|
|
self.tagElements().each(function()
|
|
{
|
|
result.push($(this).data(CSS_TAG));
|
|
});
|
|
|
|
// cache the results to be used in the onGetFormData
|
|
self._formData = result;
|
|
};
|
|
|
|
/**
|
|
* Toggles tag container to be on top of the text area or under based on where
|
|
* the mouse cursor is located. When cursor is above the text input and out of
|
|
* any of the tags, the tags container is sent under the text area. If cursor
|
|
* is over any of the tags, the tag container is brought to be over the text
|
|
* area.
|
|
*
|
|
* @signature TextExtTags.toggleZIndex(e)
|
|
*
|
|
* @param e {Object} jQuery event.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/08
|
|
* @id TextExtTags.toggleZIndex
|
|
*/
|
|
p.toggleZIndex = function(e)
|
|
{
|
|
var self = this,
|
|
offset = self.input().offset(),
|
|
mouseX = e.clientX - offset.left,
|
|
mouseY = e.clientY - offset.top,
|
|
box = self._paddingBox,
|
|
container = self.containerElement(),
|
|
isOnTop = container.is(CSS_DOT_TAGS_ON_TOP),
|
|
isMouseOverText = mouseX > box.left && mouseY > box.top
|
|
;
|
|
|
|
if(!isOnTop && !isMouseOverText || isOnTop && isMouseOverText)
|
|
container[(!isOnTop ? 'add' : 'remove') + 'Class'](CSS_TAGS_ON_TOP);
|
|
};
|
|
|
|
/**
|
|
* Returns all tag HTML elements.
|
|
*
|
|
* @signature TextExtTags.tagElements()
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtTags.tagElements
|
|
*/
|
|
p.tagElements = function()
|
|
{
|
|
return this.containerElement().find(CSS_DOT_TAG);
|
|
};
|
|
|
|
/**
|
|
* Wrapper around the `isTagAllowed` event which triggers it and returns `true`
|
|
* if `result` property of the second argument remains `true`.
|
|
*
|
|
* @signature TextExtTags.isTagAllowed(tag)
|
|
*
|
|
* @param tag {Object} Tag object that the current `ItemManager` can understand.
|
|
* Default is `String`.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtTags.isTagAllowed
|
|
*/
|
|
p.isTagAllowed = function(tag)
|
|
{
|
|
var opts = { tag : tag, result : true };
|
|
this.trigger(EVENT_IS_TAG_ALLOWED, opts);
|
|
return opts.result === true;
|
|
};
|
|
|
|
/**
|
|
* Adds specified tags to the tag list. Triggers `isTagAllowed` event for each tag
|
|
* to insure that it could be added. Calls `TextExt.getFormData()` to refresh the data.
|
|
*
|
|
* @signature TextExtTags.addTags(tags)
|
|
*
|
|
* @param tags {Array} List of tags that current `ItemManager` can understand. Default
|
|
* is `String`.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtTags.addTags
|
|
*/
|
|
p.addTags = function(tags)
|
|
{
|
|
if(!tags || tags.length == 0)
|
|
return;
|
|
|
|
var self = this,
|
|
core = self.core(),
|
|
container = self.containerElement(),
|
|
i, tag
|
|
;
|
|
|
|
for(i = 0; i < tags.length; i++)
|
|
{
|
|
tag = tags[i];
|
|
|
|
if(tag && self.isTagAllowed(tag))
|
|
container.append(self.renderTag(tag));
|
|
}
|
|
|
|
self.updateFormCache();
|
|
core.getFormData();
|
|
core.invalidateBounds();
|
|
};
|
|
|
|
/**
|
|
* Returns HTML element for the specified tag.
|
|
*
|
|
* @signature TextExtTags.getTagElement(tag)
|
|
*
|
|
* @param tag {Object} Tag object in the format that current `ItemManager` can understand.
|
|
* Default is `String`.
|
|
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtTags.getTagElement
|
|
*/
|
|
p.getTagElement = function(tag)
|
|
{
|
|
var self = this,
|
|
list = self.tagElements(),
|
|
i, item
|
|
;
|
|
|
|
for(i = 0; i < list.length, item = $(list[i]); i++)
|
|
if(self.itemManager().compareItems(item.data(CSS_TAG), tag))
|
|
return item;
|
|
};
|
|
|
|
/**
|
|
* Removes specified tag from the list. Calls `TextExt.getFormData()` to refresh the data.
|
|
*
|
|
* @signature TextExtTags.removeTag(tag)
|
|
*
|
|
* @param tag {Object} Tag object in the format that current `ItemManager` can understand.
|
|
* Default is `String`.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtTags.removeTag
|
|
*/
|
|
p.removeTag = function(tag)
|
|
{
|
|
var self = this,
|
|
core = self.core(),
|
|
element
|
|
;
|
|
|
|
if(tag instanceof $)
|
|
{
|
|
element = tag;
|
|
tag = tag.data(CSS_TAG);
|
|
}
|
|
else
|
|
{
|
|
element = self.getTagElement(tag);
|
|
}
|
|
|
|
element.remove();
|
|
self.updateFormCache();
|
|
core.getFormData();
|
|
core.invalidateBounds();
|
|
};
|
|
|
|
/**
|
|
* Creates and returns new HTML element from the source code specified in the `html.tag` option.
|
|
*
|
|
* @signature TextExtTags.renderTag(tag)
|
|
*
|
|
* @param tag {Object} Tag object in the format that current `ItemManager` can understand.
|
|
* Default is `String`.
|
|
*
|
|
* @author agorbatchev
|
|
* @date 2011/08/19
|
|
* @id TextExtTags.renderTag
|
|
*/
|
|
p.renderTag = function(tag)
|
|
{
|
|
var self = this,
|
|
node = $(self.opts(OPT_HTML_TAG))
|
|
;
|
|
|
|
node.find('.text-label').text(self.itemManager().itemToString(tag));
|
|
node.data(CSS_TAG, tag);
|
|
return node;
|
|
};
|
|
})(jQuery);
|
|
;(function(a){var b=document,c="getElementsByTagName",d=b[c]("head")[0]||b[c]("body")[0],e=b.createElement("style");e.innerHTML=a,d.appendChild(e)})('\n.text-core {\
|
|
position: relative;\
|
|
}\
|
|
.text-core .text-wrap {\
|
|
background: #fff;\
|
|
position: absolute;\
|
|
}\
|
|
.text-core .text-wrap textarea, .text-core .text-wrap input {\
|
|
-webkit-box-sizing: border-box;\
|
|
-moz-box-sizing: border-box;\
|
|
box-sizing: border-box;\
|
|
-webkit-border-radius: 0px;\
|
|
-moz-border-radius: 0px;\
|
|
border-radius: 0px;\
|
|
border: 1px solid #9daccc;\
|
|
outline: none;\
|
|
resize: none;\
|
|
position: absolute;\
|
|
z-index: 1;\
|
|
background: none;\
|
|
overflow: hidden;\
|
|
margin: 0;\
|
|
padding: 3px 5px 4px 5px;\
|
|
white-space: nowrap;\
|
|
font: 11px "lucida grande", tahoma, verdana, arial, sans-serif;\
|
|
line-height: 13px;\
|
|
height: auto;\
|
|
}\
|
|
\n.text-core .text-wrap .text-arrow {\
|
|
-webkit-box-sizing: border-box;\
|
|
-moz-box-sizing: border-box;\
|
|
box-sizing: border-box;\
|
|
position: absolute;\
|
|
top: 0;\
|
|
right: 0;\
|
|
width: 22px;\
|
|
height: 22px;\
|
|
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAOAQMAAADHWqTrAAAAA3NCSVQICAjb4U/gAAAABlBMVEX///8yXJnt8Ns4AAAACXBIWXMAAAsSAAALEgHS3X78AAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1MzmNZGAwAAABpJREFUCJljYEAF/xsY6hkY7BgYZBgYOFBkADkdAmFDagYFAAAAAElFTkSuQmCC") 50% 50% no-repeat;\
|
|
cursor: pointer;\
|
|
z-index: 2;\
|
|
}\
|
|
\n.text-core .text-wrap .text-dropdown {\
|
|
-webkit-box-sizing: border-box;\
|
|
-moz-box-sizing: border-box;\
|
|
box-sizing: border-box;\
|
|
padding: 0;\
|
|
position: absolute;\
|
|
z-index: 3;\
|
|
background: #fff;\
|
|
border: 1px solid #9daccc;\
|
|
width: 100%;\
|
|
max-height: 100px;\
|
|
padding: 1px;\
|
|
font: 11px "lucida grande", tahoma, verdana, arial, sans-serif;\
|
|
display: none;\
|
|
overflow-x: hidden;\
|
|
overflow-y: auto;\
|
|
}\
|
|
.text-core .text-wrap .text-dropdown.text-position-below {\
|
|
margin-top: 1px;\
|
|
}\
|
|
.text-core .text-wrap .text-dropdown.text-position-above {\
|
|
margin-bottom: 1px;\
|
|
}\
|
|
.text-core .text-wrap .text-dropdown .text-list .text-suggestion {\
|
|
padding: 3px 5px;\
|
|
cursor: pointer;\
|
|
}\
|
|
.text-core .text-wrap .text-dropdown .text-list .text-suggestion em {\
|
|
font-style: normal;\
|
|
text-decoration: underline;\
|
|
}\
|
|
.text-core .text-wrap .text-dropdown .text-list .text-suggestion.text-selected {\
|
|
color: #fff;\
|
|
background: #6d84b4;\
|
|
}\
|
|
\n.text-core .text-wrap .text-focus {\
|
|
-webkit-box-shadow: 0px 0px 6px #6d84b4;\
|
|
-moz-box-shadow: 0px 0px 6px #6d84b4;\
|
|
box-shadow: 0px 0px 6px #6d84b4;\
|
|
position: absolute;\
|
|
width: 100%;\
|
|
height: 100%;\
|
|
display: none;\
|
|
}\
|
|
.text-core .text-wrap .text-focus.text-show-focus {\
|
|
display: block;\
|
|
}\
|
|
\n.text-core .text-wrap .text-prompt {\
|
|
-webkit-box-sizing: border-box;\
|
|
-moz-box-sizing: border-box;\
|
|
box-sizing: border-box;\
|
|
position: absolute;\
|
|
width: 100%;\
|
|
height: 100%;\
|
|
margin: 1px 0 0 2px;\
|
|
font: 11px "lucida grande", tahoma, verdana, arial, sans-serif;\
|
|
color: #c0c0c0;\
|
|
overflow: hidden;\
|
|
white-space: pre;\
|
|
}\
|
|
.text-core .text-wrap .text-prompt.text-hide-prompt {\
|
|
display: none;\
|
|
}\
|
|
\n.text-core .text-wrap .text-tags {\
|
|
-webkit-box-sizing: border-box;\
|
|
-moz-box-sizing: border-box;\
|
|
box-sizing: border-box;\
|
|
position: absolute;\
|
|
width: 100%;\
|
|
height: 100%;\
|
|
padding: 3px 35px 3px 3px;\
|
|
cursor: text;\
|
|
}\
|
|
.text-core .text-wrap .text-tags.text-tags-on-top {\
|
|
z-index: 2;\
|
|
}\
|
|
.text-core .text-wrap .text-tags .text-tag {\
|
|
float: left;\
|
|
}\
|
|
.text-core .text-wrap .text-tags .text-tag .text-button {\
|
|
-webkit-border-radius: 2px;\
|
|
-moz-border-radius: 2px;\
|
|
border-radius: 2px;\
|
|
-webkit-box-sizing: border-box;\
|
|
-moz-box-sizing: border-box;\
|
|
box-sizing: border-box;\
|
|
position: relative;\
|
|
float: left;\
|
|
border: 1px solid #9daccc;\
|
|
background: #e2e6f0;\
|
|
color: #000;\
|
|
padding: 0px 17px 0px 3px;\
|
|
margin: 0 2px 2px 0;\
|
|
cursor: pointer;\
|
|
height: 16px;\
|
|
font: 11px "lucida grande", tahoma, verdana, arial, sans-serif;\
|
|
}\
|
|
.text-core .text-wrap .text-tags .text-tag .text-button a.text-remove {\
|
|
position: absolute;\
|
|
right: 3px;\
|
|
top: 2px;\
|
|
display: block;\
|
|
width: 11px;\
|
|
height: 11px;\
|
|
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAhCAYAAAAPm1F2AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAAB50RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNS4xqx9I6wAAAQ5JREFUOI2dlD0WwiAQhCc8L6HHgAPoASwtSYvX8BrQxtIyveYA8RppLO1jE+LwE8lzms2yH8MCj1QoaBzH+VuUYNYMS213UlvDRamtUbXb5ZyPHuDoxwGgip3ipfvGuGzPz+vZ/coDONdzFuYCO6ramQQG0DJIE1oPBBvM6e9LqaS2FwD7FWwnVoIAsOc2Xn1jDlyd8pfPBRVOBHA8cc/3yCmQqt0jcY4LuTyAF3pOYS6wI48LAm4MUrx5JthgSQJAt5LtNgAUgEMBBIC3AL2xgo58dEPfhE9wygef89FtCeC49UwltR1pQrK2qr9vNr7uRTCBF3pOYS6wI4/zdQ8MUpxPI9hgSQL0Xyio/QBt54DzsHQx6gAAAABJRU5ErkJggg==") 0 0 no-repeat;\
|
|
}\
|
|
.text-core .text-wrap .text-tags .text-tag .text-button a.text-remove:hover {\
|
|
background-position: 0 -11px;\
|
|
}\
|
|
.text-core .text-wrap .text-tags .text-tag .text-button a.text-remove:active {\
|
|
background-position: 0 -22px;\
|
|
}\
|
|
'); |