diff --git a/.gitignore b/.gitignore
index 4119c82..5ff2dc7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
/node_modules/
/src/css/_common.css
tests/*.png
-tests/*.jar
\ No newline at end of file
+tests/*.jar
+*.jar
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..b056ba1
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "doc"]
+ path = doc
+ url = git@github.com:alexgorbatchev/jquery-textext-doc.git
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..e74fc7c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,22 @@
+PATH := "/Applications/Firefox.app/Contents/MacOS":$(PATH)
+
+selenium:
+ @echo "Starting Selenium RC server"
+ @cd tests && java -jar selenium-server-standalone-2.15.0.jar -firefoxProfileTemplate "./firefox_profile"
+
+download-selenium:
+ @echo "Downloading Selenium RC"
+ @cd tests && curl -O "http://selenium.googlecode.com/files/selenium-server-standalone-2.15.0.jar"
+
+install-textext:
+ @echo "Installing TextExt.js dependencies"
+ @git submodule init
+ @git submodule update
+ @npm install
+
+install:
+ @echo "Installing dependencies for jQuery TextExt.js plugin"
+ @make install-textext
+ @make download-selenium
+ @echo "Success"
+
diff --git a/README.md b/README.md
index fa63389..3995c78 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,11 @@ The steps to using TextExt are as follows:
## History
+### 1.4.0
+
+#### New Features
+* `ItemManager` can now be inlined. See the [manual](http://textextjs.com/manual/itemmanager.html).
+
### 1.3.0
#### New Features
diff --git a/bin/stylus b/bin/stylus
deleted file mode 100755
index 1084591..0000000
--- a/bin/stylus
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-../node_modules/.bin/stylus --watch --include ../src/stylus --out ../src/css ../src/stylus/*.styl
-
diff --git a/doc b/doc
new file mode 160000
index 0000000..5b53d91
--- /dev/null
+++ b/doc
@@ -0,0 +1 @@
+Subproject commit 5b53d917e4b3a93cb69bf35f509690837fdec36d
diff --git a/grunt.js b/grunt.js
new file mode 100644
index 0000000..b54cabd
--- /dev/null
+++ b/grunt.js
@@ -0,0 +1,33 @@
+module.exports = function(grunt)
+{
+ grunt.loadNpmTasks('grunt-contrib-watch');
+ grunt.loadNpmTasks('grunt-contrib-less');
+
+ grunt.initConfig({
+ less: {
+ development: {
+ options: {
+ yuicompress: false
+ },
+ files: {
+ "src/css/*.css": "src/less/*.less"
+ }
+ }
+ },
+ watch: {
+ gruntfile: {
+ files: 'grunt.js',
+ tasks: ['jshint:gruntfile'],
+ options: {
+ nocase: true
+ }
+ },
+ less: {
+ files: ['src/less/*.less'],
+ tasks: ['less']
+ }
+ }
+ });
+
+ // grunt.registerTask('watch', 'watch');
+};
\ No newline at end of file
diff --git a/package.json b/package.json
index 990274b..b4cd253 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,13 @@
{
- "name" : "app_name",
- "version" : "0.0.0",
- "dependencies" : {
- "stylus" : "x.x.x",
- "soda" : ">= 0.2.x",
- "uglify-js" : "x.x.x"
- }
+ "name" : "app_name",
+ "version" : "0.0.0",
+ "dependencies" : {
+ "soda" : ">= 0.2.x",
+ "less" : ">= 1.3.x",
+
+ "grunt" : ">= 0.3.x",
+ "grunt-contrib-watch" : ">= 0.1.x",
+ "grunt-contrib-less" : ">= 0.3.x",
+ }
}
diff --git a/src/css/loading.gif b/src/css/loading.gif
new file mode 100644
index 0000000..325f2c5
Binary files /dev/null and b/src/css/loading.gif differ
diff --git a/src/css/textext.core.css b/src/css/textext.css
similarity index 100%
rename from src/css/textext.core.css
rename to src/css/textext.css
index ad3fec0..779e581 100644
--- a/src/css/textext.core.css
+++ b/src/css/textext.css
@@ -2,8 +2,8 @@
position: relative;
}
.text-core .text-wrap {
- background: #fff;
position: absolute;
+ background: #fff;
}
.text-core .text-wrap textarea,
.text-core .text-wrap input {
diff --git a/src/css/textext.itemmanager.ajax.css b/src/css/textext.itemmanager.ajax.css
new file mode 100644
index 0000000..c72bec6
--- /dev/null
+++ b/src/css/textext.itemmanager.ajax.css
@@ -0,0 +1,4 @@
+.text-core .text-wrap textarea.text-loading,
+.text-core .text-wrap input.text-loading {
+ background: url(loading.gif) 99% 50% no-repeat;
+}
diff --git a/src/css/textext.plugin.arrow.css b/src/css/textext.plugin.arrow.css
index 6df863c..d7689b5 100644
--- a/src/css/textext.plugin.arrow.css
+++ b/src/css/textext.plugin.arrow.css
@@ -7,7 +7,7 @@
right: 0;
width: 22px;
height: 22px;
- background: url("arrow.png") 50% 50% no-repeat;
+ background: url(arrow.png) 50% 50% no-repeat;
cursor: pointer;
z-index: 2;
}
diff --git a/src/css/textext.plugin.focus.css b/src/css/textext.plugin.focus.css
index 9579128..4bbb92c 100644
--- a/src/css/textext.plugin.focus.css
+++ b/src/css/textext.plugin.focus.css
@@ -3,9 +3,9 @@
-moz-box-shadow: 0px 0px 6px #6d84b4;
box-shadow: 0px 0px 6px #6d84b4;
position: absolute;
- width: 100%;
- height: 100%;
- display: none;
+ width: 100%
+ height : 100%
+ display : none;
}
.text-core .text-wrap .text-focus.text-show-focus {
display: block;
diff --git a/src/css/textext.plugin.prompt.css b/src/css/textext.plugin.prompt.css
index 49eab49..21f692c 100644
--- a/src/css/textext.plugin.prompt.css
+++ b/src/css/textext.plugin.prompt.css
@@ -3,11 +3,11 @@
-moz-box-sizing: border-box;
box-sizing: border-box;
position: absolute;
- width: 100%;
- height: 100%;
- margin: 1px 0 0 2px;
+ width: 100%
+ height : 100%
+ margin : 1px 0 0 2px;
font: 11px "lucida grande", tahoma, verdana, arial, sans-serif;
- color: #c0c0c0;
+ color: silver;
overflow: hidden;
white-space: pre;
}
diff --git a/src/css/textext.plugin.tags.css b/src/css/textext.plugin.tags.css
index 9c5b7a1..d594e81 100644
--- a/src/css/textext.plugin.tags.css
+++ b/src/css/textext.plugin.tags.css
@@ -23,8 +23,8 @@
box-sizing: border-box;
position: relative;
float: left;
- border: 1px solid #9daccc;
- background: #e2e6f0;
+ border: 1px solid #9DACCC;
+ background: #E2E6F0;
color: #000;
padding: 0px 17px 0px 3px;
margin: 0 2px 2px 0;
@@ -39,7 +39,7 @@
display: block;
width: 11px;
height: 11px;
- background: url("close.png") 0 0 no-repeat;
+ background: url('close.png') 0 0 no-repeat;
}
.text-core .text-wrap .text-tags .text-tag .text-button a.text-remove:hover {
background-position: 0 -11px;
diff --git a/src/js/textext.core.js b/src/js/textext.core.js
deleted file mode 100644
index 2272f70..0000000
--- a/src/js/textext.core.js
+++ /dev/null
@@ -1,1615 +0,0 @@
-/**
- * jQuery TextExt Plugin
- * http://textextjs.com
- *
- * @version 1.3.0
- * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
- * @license MIT License
- */
-(function($, undefined)
-{
- /**
- * TextExt is the main core class which by itself doesn't provide any functionality
- * that is user facing, however it has the underlying mechanics to bring all the
- * plugins together under one roof and make them work with each other or on their
- * own.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExt
- */
- function TextExt() {};
-
- /**
- * ItemManager is used to seamlessly convert between string that come from the user input to whatever
- * the format the item data is being passed around in. It's used by all plugins that in one way or
- * another operate with items, such as Tags, Filter, Autocomplete and Suggestions. Default implementation
- * works with `String` type.
- *
- * Each instance of `TextExt` creates a new instance of default implementation of `ItemManager`
- * unless `itemManager` option was set to another implementation.
- *
- * To satisfy requirements of managing items of type other than a `String`, different implementation
- * if `ItemManager` should be supplied.
- *
- * If you wish to bring your own implementation, you need to create a new class and implement all the
- * methods that `ItemManager` has. After, you need to supply your pass via the `itemManager` option during
- * initialization like so:
- *
- * $('#input').textext({
- * itemManager : CustomItemManager
- * })
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id ItemManager
- */
- function ItemManager() {};
-
- /**
- * TextExtPlugin is a base class for all plugins. It provides common methods which are reused
- * by majority of plugins.
- *
- * All plugins must register themselves by calling the `$.fn.textext.addPlugin(name, constructor)`
- * function while providing plugin name and constructor. The plugin name is the same name that user
- * will identify the plugin in the `plugins` option when initializing TextExt component and constructor
- * function will create a new instance of the plugin. *Without registering, the core won't
- * be able to see the plugin.*
- *
- * new in 1.2.0 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 '
'
- * @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 ''
- * @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 : '
',
- hidden : ''
- },
-
- keys : {
- 8 : 'backspace',
- 9 : 'tab',
- 13 : 'enter!',
- 27 : 'escape!',
- 37 : 'left',
- 38 : 'up!',
- 39 : 'right',
- 40 : 'down!',
- 46 : 'delete',
- 108 : 'numpadEnter'
- }
- }
- ;
-
- // Freak out if there's no JSON.stringify function found
- if(!stringify)
- throw new Error('JSON.stringify() not found');
-
- /**
- * Returns object property by name where name is dot-separated and object is multiple levels deep.
- * @param target Object Source object.
- * @param name String Dot separated property name, ie `foo.bar.world`
- * @id core.getProperty
- */
- function getProperty(source, name)
- {
- if(typeof(name) === 'string')
- name = name.split('.');
-
- var fullCamelCaseName = name.join('.').replace(/\.(\w)/g, function(match, letter) { return letter.toUpperCase() }),
- nestedName = name.shift(),
- result
- ;
-
- if(typeof(result = source[fullCamelCaseName]) != UNDEFINED)
- result = result;
-
- else if(typeof(result = source[nestedName]) != UNDEFINED && name.length > 0)
- result = getProperty(result, name);
-
- // name.length here should be zero
- return result;
- };
-
- /**
- * Hooks up specified events in the scope of the current object.
- * @author agorbatchev
- * @date 2011/08/09
- */
- function hookupEvents()
- {
- var args = slice.apply(arguments),
- self = this,
- target = args.length === 1 ? self : args.shift(),
- event
- ;
-
- args = args[0] || {};
-
- function bind(event, handler)
- {
- target.bind(event, function()
- {
- // apply handler to our PLUGIN object, not the target
- return handler.apply(self, arguments);
- });
- }
-
- for(event in args)
- bind(event, args[event]);
- };
-
- function formDataObject(input, form)
- {
- return { 'input' : input, 'form' : form };
- };
-
- //--------------------------------------------------------------------------------
- // ItemManager core component
-
- p = ItemManager.prototype;
-
- /**
- * Initialization method called by the core during instantiation.
- *
- * @signature ItemManager.init(core)
- *
- * @param core {TextExt} Instance of the TextExt core class.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id ItemManager.init
- */
- p.init = function(core)
- {
- };
-
- /**
- * Filters out items from the list that don't match the query and returns remaining items. Default
- * implementation checks if the item starts with the query.
- *
- * @signature ItemManager.filter(list, query)
- *
- * @param list {Array} List of items. Default implementation works with strings.
- * @param query {String} Query string.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id ItemManager.filter
- */
- p.filter = function(list, query)
- {
- var result = [],
- i, item
- ;
-
- for(i = 0; i < list.length; i++)
- {
- item = list[i];
- if(this.itemContains(item, query))
- result.push(item);
- }
-
- return result;
- };
-
- /**
- * Returns `true` if specified item contains another string, `false` otherwise. In the default implementation
- * `String.indexOf()` is used to check if item string begins with the needle string.
- *
- * @signature ItemManager.itemContains(item, needle)
- *
- * @param item {Object} Item to check. Default implementation works with strings.
- * @param needle {String} Search string to be found within the item.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id ItemManager.itemContains
- */
- p.itemContains = function(item, needle)
- {
- return this.itemToString(item).toLowerCase().indexOf(needle.toLowerCase()) == 0;
- };
-
- /**
- * Converts specified string to item. Because default implemenation works with string, input string
- * is simply returned back. To use custom objects, different implementation of this method could
- * return something like `{ name : {String} }`.
- *
- * @signature ItemManager.stringToItem(str)
- *
- * @param str {String} Input string.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id ItemManager.stringToItem
- */
- p.stringToItem = function(str)
- {
- return str;
- };
-
- /**
- * Converts specified item to string. Because default implemenation works with string, input string
- * is simply returned back. To use custom objects, different implementation of this method could
- * for example return `name` field of `{ name : {String} }`.
- *
- * @signature ItemManager.itemToString(item)
- *
- * @param item {Object} Input item to be converted to string.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id ItemManager.itemToString
- */
- p.itemToString = function(item)
- {
- return item;
- };
-
- /**
- * Returns `true` if both items are equal, `false` otherwise. Because default implemenation works with
- * string, input items are compared as strings. To use custom objects, different implementation of this
- * method could for example compare `name` fields of `{ name : {String} }` type object.
- *
- * @signature ItemManager.compareItems(item1, item2)
- *
- * @param item1 {Object} First item.
- * @param item2 {Object} Second item.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id ItemManager.compareItems
- */
- p.compareItems = function(item1, item2)
- {
- return item1 == item2;
- };
-
- //--------------------------------------------------------------------------------
- // TextExt core component
-
- p = TextExt.prototype;
-
- /**
- * Initializes current component instance with work with the supplied text input and options.
- *
- * @signature TextExt.init(input, opts)
- *
- * @param input {HTMLElement} Text input.
- * @param opts {Object} Options.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExt.init
- */
- p.init = function(input, opts)
- {
- var self = this,
- hiddenInput,
- itemManager,
- container
- ;
-
- self._defaults = $.extend({}, DEFAULT_OPTS);
- self._opts = opts || {};
- self._plugins = {};
- self._itemManager = itemManager = new (self.opts(OPT_ITEM_MANAGER))();
- input = $(input);
- container = $(self.opts(OPT_HTML_WRAP));
- hiddenInput = $(self.opts(OPT_HTML_HIDDEN));
-
- input
- .wrap(container)
- .keydown(function(e) { return self.onKeyDown(e) })
- .keyup(function(e) { return self.onKeyUp(e) })
- .data('textext', self)
- ;
-
- // keep references to html elements using jQuery.data() to avoid circular references
- $(self).data({
- 'hiddenInput' : hiddenInput,
- 'wrapElement' : input.parents('.text-wrap').first(),
- 'input' : input
- });
-
- // set the name of the hidden input to the text input's name
- hiddenInput.attr('name', input.attr('name'));
- // remove name attribute from the text input
- input.attr('name', null);
- // add hidden input to the DOM
- hiddenInput.insertAfter(input);
-
- $.extend(true, itemManager, self.opts(OPT_EXT + '.item.manager'));
- $.extend(true, self, self.opts(OPT_EXT + '.*'), self.opts(OPT_EXT + '.core'));
-
- self.originalWidth = input.outerWidth();
-
- self.invalidateBounds();
-
- itemManager.init(self);
-
- self.initPatches();
- self.initPlugins(self.opts(OPT_PLUGINS), $.fn.textext.plugins);
-
- self.on({
- setFormData : self.onSetFormData,
- getFormData : self.onGetFormData,
- setInputData : self.onSetInputData,
- anyKeyUp : self.onAnyKeyUp
- });
-
- self.trigger(EVENT_POST_INIT);
- self.trigger(EVENT_READY);
-
- self.getFormData(0);
- };
-
- /**
- * Initialized all installed patches against current instance. The patches are initialized based on their
- * initialization priority which is returned by each patch's `initPriority()` method. Priority
- * is a `Number` where patches with higher value gets their `init()` method called before patches
- * with lower priority value.
- *
- * This facilitates initializing of patches in certain order to insure proper dependencies
- * regardless of which order they are loaded.
- *
- * By default all patches have the same priority - zero, which means they will be initialized
- * in rorder they are loaded, that is unless `initPriority()` is overriden.
- *
- * @signature TextExt.initPatches()
- *
- * @author agorbatchev
- * @date 2011/10/11
- * @id TextExt.initPatches
- */
- p.initPatches = function()
- {
- var list = [],
- source = $.fn.textext.patches,
- name
- ;
-
- for(name in source)
- list.push(name);
-
- this.initPlugins(list, source);
- };
-
- /**
- * Creates and initializes all specified plugins. The plugins are initialized based on their
- * initialization priority which is returned by each plugin's `initPriority()` method. Priority
- * is a `Number` where plugins with higher value gets their `init()` method called before plugins
- * with lower priority value.
- *
- * This facilitates initializing of plugins in certain order to insure proper dependencies
- * regardless of which order user enters them in the `plugins` option field.
- *
- * By default all plugins have the same priority - zero, which means they will be initialized
- * in the same order as entered by the user.
- *
- * @signature TextExt.initPlugins(plugins)
- *
- * @param plugins {Array} List of plugin names to initialize.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExt.initPlugins
- */
- p.initPlugins = function(plugins, source)
- {
- var self = this,
- ext, name, plugin, initList = [], i
- ;
-
- if(typeof(plugins) == 'string')
- plugins = plugins.split(/\s*,\s*|\s+/g);
-
- for(i = 0; i < plugins.length; i++)
- {
- name = plugins[i];
- plugin = source[name];
-
- if(plugin)
- {
- self._plugins[name] = plugin = new plugin();
- self[name] = (function(plugin) {
- return function(){ return plugin; }
- })(plugin);
- initList.push(plugin);
- $.extend(true, plugin, self.opts(OPT_EXT + '.*'), self.opts(OPT_EXT + '.' + name));
- }
- }
-
- // sort plugins based on their priority values
- initList.sort(function(p1, p2)
- {
- p1 = p1.initPriority();
- p2 = p2.initPriority();
-
- return p1 === p2
- ? 0
- : p1 < p2 ? 1 : -1
- ;
- });
-
- for(i = 0; i < initList.length; i++)
- initList[i].init(self);
- };
-
- /**
- * Returns true if specified plugin is was instantiated for the current instance of core.
- *
- * @signature TextExt.hasPlugin(name)
- *
- * @param name {String} Name of the plugin to check.
- *
- * @author agorbatchev
- * @date 2011/12/28
- * @id TextExt.hasPlugin
- * @version 1.1
- */
- p.hasPlugin = function(name)
- {
- return !!this._plugins[name];
- };
-
- /**
- * Allows to add multiple event handlers which will be execued in the scope of the current object.
- *
- * @signature TextExt.on([target], handlers)
- *
- * @param target {Object} **Optional**. Target object which has traditional `bind(event, handler)` method.
- * Handler function will still be executed in the current object's scope.
- * @param handlers {Object} Key/value pairs of event names and handlers, eg `{ event: handler }`.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExt.on
- */
- p.on = hookupEvents;
-
- /**
- * Binds an event handler to the input box that user interacts with.
- *
- * @signature TextExt.bind(event, handler)
- *
- * @param event {String} Event name.
- * @param handler {Function} Event handler.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExt.bind
- */
- p.bind = function(event, handler)
- {
- this.input().bind(event, handler);
- };
-
- /**
- * Triggers an event on the input box that user interacts with. All core events are originated here.
- *
- * @signature TextExt.trigger(event, ...args)
- *
- * @param event {String} Name of the event to trigger.
- * @param ...args All remaining arguments will be passed to the event handler.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExt.trigger
- */
- p.trigger = function()
- {
- var args = arguments;
- this.input().trigger(args[0], slice.call(args, 1));
- };
-
- /**
- * Returns instance of `itemManager` that is used by the component.
- *
- * @signature TextExt.itemManager()
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExt.itemManager
- */
- p.itemManager = function()
- {
- return this._itemManager;
- };
-
- /**
- * Returns jQuery input element with which user is interacting with.
- *
- * @signature TextExt.input()
- *
- * @author agorbatchev
- * @date 2011/08/10
- * @id TextExt.input
- */
- p.input = function()
- {
- return $(this).data('input');
- };
-
- /**
- * Returns option value for the specified option by name. If the value isn't found in the user
- * provided options, it will try looking for default value.
- *
- * @signature TextExt.opts(name)
- *
- * @param name {String} Option name as described in the options.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExt.opts
- */
- p.opts = function(name)
- {
- var result = getProperty(this._opts, name);
- return typeof(result) == 'undefined' ? getProperty(this._defaults, name) : result;
- };
-
- /**
- * Returns HTML element that was created from the `html.wrap` option. This is the top level HTML
- * container for the text input with which user is interacting with.
- *
- * @signature TextExt.wrapElement()
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExt.wrapElement
- */
- p.wrapElement = function()
- {
- return $(this).data('wrapElement');
- };
-
- /**
- * Updates container to match dimensions of the text input. Triggers `preInvalidate` and `postInvalidate`
- * events.
- *
- * @signature TextExt.invalidateBounds()
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExt.invalidateBounds
- */
- p.invalidateBounds = function()
- {
- var self = this,
- input = self.input(),
- wrap = self.wrapElement(),
- container = wrap.parent(),
- width = self.originalWidth,
- height
- ;
-
- self.trigger(EVENT_PRE_INVALIDATE);
-
- height = input.outerHeight();
-
- input.width(width);
- wrap.width(width).height(height);
- container.height(height);
-
- self.trigger(EVENT_POST_INVALIDATE);
- };
-
- /**
- * Focuses user input on the text box.
- *
- * @signature TextExt.focusInput()
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExt.focusInput
- */
- p.focusInput = function()
- {
- this.input()[0].focus();
- };
-
- /**
- * Serializes data for to be set into the hidden input field and which will be submitted
- * with the HTML form.
- *
- * By default simple JSON serialization is used. It's expected that `JSON.stringify`
- * method would be available either through built in class in most modern browsers
- * or through JSON2 library.
- *
- * @signature TextExt.serializeData(data)
- *
- * @param data {Object} Data to serialize.
- *
- * @author agorbatchev
- * @date 2011/08/09
- * @id TextExt.serializeData
- */
- p.serializeData = stringify;
-
- /**
- * Returns the hidden input HTML element which will be submitted with the HTML form.
- *
- * @signature TextExt.hiddenInput()
- *
- * @author agorbatchev
- * @date 2011/08/09
- * @id TextExt.hiddenInput
- */
- p.hiddenInput = function(value)
- {
- return $(this).data('hiddenInput');
- };
-
- /**
- * Abstracted functionality to trigger an event and get the data with maximum weight set by all
- * the event handlers. This functionality is used for the `getFormData` event.
- *
- * @signature TextExt.getWeightedEventResponse(event, args)
- *
- * @param event {String} Event name.
- * @param args {Object} Argument to be passed with the event.
- *
- * @author agorbatchev
- * @date 2011/08/22
- * @id TextExt.getWeightedEventResponse
- */
- p.getWeightedEventResponse = function(event, args)
- {
- var self = this,
- data = {},
- maxWeight = 0
- ;
-
- self.trigger(event, data, args);
-
- for(var weight in data)
- maxWeight = Math.max(maxWeight, weight);
-
- return data[maxWeight];
- };
-
- /**
- * Triggers the `getFormData` event to get all the plugins to return their data.
- *
- * After the data is returned, triggers `setFormData` and `setInputData` to update appopriate values.
- *
- * @signature TextExt.getFormData(keyCode)
- *
- * @param keyCode {Number} Key code number which has triggered this update. It's impotant to pass
- * this value to the plugins because they might return different values based on the key that was
- * pressed. For example, the Tags plugin returns an empty string for the `input` value if the enter
- * key was pressed, otherwise it returns whatever is currently in the text input.
- *
- * @author agorbatchev
- * @date 2011/08/22
- * @id TextExt.getFormData
- */
- p.getFormData = function(keyCode)
- {
- var self = this,
- data = self.getWeightedEventResponse(EVENT_GET_FORM_DATA, keyCode || 0)
- ;
-
- self.trigger(EVENT_SET_FORM_DATA , data['form']);
- self.trigger(EVENT_SET_INPUT_DATA , data['input']);
- };
-
- //--------------------------------------------------------------------------------
- // Event handlers
-
- /**
- * Reacts to the `anyKeyUp` event and triggers the `getFormData` to change data that will be submitted
- * with the form. Default behaviour is that everything that is typed in will be JSON serialized, so
- * the end result will be a JSON string.
- *
- * @signature TextExt.onAnyKeyUp(e)
- *
- * @param e {Object} jQuery event.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExt.onAnyKeyUp
- */
- p.onAnyKeyUp = function(e, keyCode)
- {
- this.getFormData(keyCode);
- };
-
- /**
- * Reacts to the `setInputData` event and populates the input text field that user is currently
- * interacting with.
- *
- * @signature TextExt.onSetInputData(e, data)
- *
- * @param e {Event} jQuery event.
- * @param data {String} Value to be set.
- *
- * @author agorbatchev
- * @date 2011/08/22
- * @id TextExt.onSetInputData
- */
- p.onSetInputData = function(e, data)
- {
- this.input().val(data);
- };
-
- /**
- * Reacts to the `setFormData` event and populates the hidden input with will be submitted with
- * the HTML form. The value will be serialized with `serializeData()` method.
- *
- * @signature TextExt.onSetFormData(e, data)
- *
- * @param e {Event} jQuery event.
- * @param data {Object} Data that will be set.
- *
- * @author agorbatchev
- * @date 2011/08/22
- * @id TextExt.onSetFormData
- */
- p.onSetFormData = function(e, data)
- {
- var self = this;
- self.hiddenInput().val(self.serializeData(data));
- };
-
- /**
- * Reacts to `getFormData` event triggered by the core. At the bare minimum the core will tell
- * itself to use the current value in the text input as the data to be submitted with the HTML
- * form.
- *
- * @signature TextExt.onGetFormData(e, data)
- *
- * @param e {Event} jQuery event.
- *
- * @author agorbatchev
- * @date 2011/08/09
- * @id TextExt.onGetFormData
- */
- p.onGetFormData = function(e, data)
- {
- var val = this.input().val();
- data[0] = formDataObject(val, val);
- };
-
- //--------------------------------------------------------------------------------
- // User mouse/keyboard input
-
- /**
- * Triggers `[name]KeyUp` and `[name]KeyPress` for every keystroke as described in the events.
- *
- * @signature TextExt.onKeyUp(e)
- *
- * @param e {Object} jQuery event.
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExt.onKeyUp
- */
-
- /**
- * Triggers `[name]KeyDown` for every keystroke as described in the events.
- *
- * @signature TextExt.onKeyDown(e)
- *
- * @param e {Object} jQuery event.
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExt.onKeyDown
- */
-
- $(['Down', 'Up']).each(function()
- {
- var type = this.toString();
-
- p['onKey' + type] = function(e)
- {
- var self = this,
- keyName = self.opts(OPT_KEYS)[e.keyCode],
- defaultResult = true
- ;
-
- if(keyName)
- {
- defaultResult = keyName.substr(-1) != '!';
- keyName = keyName.replace('!', '');
-
- self.trigger(keyName + 'Key' + type);
-
- // manual *KeyPress event fimplementation for the function keys like Enter, Backspace, etc.
- if(type == 'Up' && self._lastKeyDown == e.keyCode)
- {
- self._lastKeyDown = null;
- self.trigger(keyName + 'KeyPress');
- }
-
- if(type == 'Down')
- self._lastKeyDown = e.keyCode;
- }
-
- self.trigger('anyKey' + type, e.keyCode);
-
- return defaultResult;
- };
- });
-
- //--------------------------------------------------------------------------------
- // Plugin Base
-
- p = TextExtPlugin.prototype;
-
- /**
- * Allows to add multiple event handlers which will be execued in the scope of the current object.
- *
- * @signature TextExt.on([target], handlers)
- *
- * @param target {Object} **Optional**. Target object which has traditional `bind(event, handler)` method.
- * Handler function will still be executed in the current object's scope.
- * @param handlers {Object} Key/value pairs of event names and handlers, eg `{ event: handler }`.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExtPlugin.on
- */
- p.on = hookupEvents;
-
- /**
- * Returns the hash object that `getFormData` triggered by the core expects.
- *
- * @signature TextExtPlugin.formDataObject(input, form)
- *
- * @param input {String} Value that will go into the text input that user is interacting with.
- * @param form {Object} Value that will be serialized and put into the hidden that will be submitted
- * with the HTML form.
- *
- * @author agorbatchev
- * @date 2011/08/22
- * @id TextExtPlugin.formDataObject
- */
- p.formDataObject = formDataObject;
-
- /**
- * Initialization method called by the core during plugin instantiation. This method must be implemented
- * by each plugin individually.
- *
- * @signature TextExtPlugin.init(core)
- *
- * @param core {TextExt} Instance of the TextExt core class.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExtPlugin.init
- */
- p.init = function(core) { throw new Error('Not implemented') };
-
- /**
- * Initialization method wich should be called by the plugin during the `init()` call.
- *
- * @signature TextExtPlugin.baseInit(core, defaults)
- *
- * @param core {TextExt} Instance of the TextExt core class.
- * @param defaults {Object} Default plugin options. These will be checked if desired value wasn't
- * found in the options supplied by the user.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExtPlugin.baseInit
- */
- p.baseInit = function(core, defaults)
- {
- var self = this;
-
- core._defaults = $.extend(true, core._defaults, defaults);
- self._core = core;
- self._timers = {};
- };
-
- /**
- * Allows starting of multiple timeout calls. Each time this method is called with the same
- * timer name, the timer is reset. This functionality is useful in cases where an action needs
- * to occur only after a certain period of inactivity. For example, making an AJAX call after
- * user stoped typing for 1 second.
- *
- * @signature TextExtPlugin.startTimer(name, delay, callback)
- *
- * @param name {String} Timer name.
- * @param delay {Number} Delay in seconds.
- * @param callback {Function} Callback function.
- *
- * @author agorbatchev
- * @date 2011/08/25
- * @id TextExtPlugin.startTimer
- */
- p.startTimer = function(name, delay, callback)
- {
- var self = this;
-
- self.stopTimer(name);
-
- self._timers[name] = setTimeout(
- function()
- {
- delete self._timers[name];
- callback.apply(self);
- },
- delay * 1000
- );
- };
-
- /**
- * Stops the timer by name without resetting it.
- *
- * @signature TextExtPlugin.stopTimer(name)
- *
- * @param name {String} Timer name.
- *
- * @author agorbatchev
- * @date 2011/08/25
- * @id TextExtPlugin.stopTimer
- */
- p.stopTimer = function(name)
- {
- clearTimeout(this._timers[name]);
- };
-
- /**
- * Returns instance of the `TextExt` to which current instance of the plugin is attached to.
- *
- * @signature TextExtPlugin.core()
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExtPlugin.core
- */
- p.core = function()
- {
- return this._core;
- };
-
- /**
- * Shortcut to the core's `opts()` method. Returns option value.
- *
- * @signature TextExtPlugin.opts(name)
- *
- * @param name {String} Option name as described in the options.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExtPlugin.opts
- */
- p.opts = function(name)
- {
- return this.core().opts(name);
- };
-
- /**
- * Shortcut to the core's `itemManager()` method. Returns instance of the `ItemManger` that is
- * currently in use.
- *
- * @signature TextExtPlugin.itemManager()
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExtPlugin.itemManager
- */
- p.itemManager = function()
- {
- return this.core().itemManager();
- };
-
- /**
- * Shortcut to the core's `input()` method. Returns instance of the HTML element that represents
- * current text input.
- *
- * @signature TextExtPlugin.input()
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExtPlugin.input
- */
- p.input = function()
- {
- return this.core().input();
- };
-
- /**
- * Shortcut to the commonly used `this.input().val()` call to get or set value of the text input.
- *
- * @signature TextExtPlugin.val(value)
- *
- * @param value {String} Optional value. If specified, the value will be set, otherwise it will be
- * returned.
- *
- * @author agorbatchev
- * @date 2011/08/20
- * @id TextExtPlugin.val
- */
- p.val = function(value)
- {
- var input = this.input();
-
- if(typeof(value) === UNDEFINED)
- return input.val();
- else
- input.val(value);
- };
-
- /**
- * Shortcut to the core's `trigger()` method. Triggers specified event with arguments on the
- * component core.
- *
- * @signature TextExtPlugin.trigger(event, ...args)
- *
- * @param event {String} Name of the event to trigger.
- * @param ...args All remaining arguments will be passed to the event handler.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExtPlugin.trigger
- */
- p.trigger = function()
- {
- var core = this.core();
- core.trigger.apply(core, arguments);
- };
-
- /**
- * Shortcut to the core's `bind()` method. Binds specified handler to the event.
- *
- * @signature TextExtPlugin.bind(event, handler)
- *
- * @param event {String} Event name.
- * @param handler {Function} Event handler.
- *
- * @author agorbatchev
- * @date 2011/08/20
- * @id TextExtPlugin.bind
- */
- p.bind = function(event, handler)
- {
- this.core().bind(event, handler);
- };
-
- /**
- * Returns initialization priority for this plugin. If current plugin depends upon some other plugin
- * to be initialized before or after, priority needs to be adjusted accordingly. Plugins with higher
- * priority initialize before plugins with lower priority.
- *
- * Default initialization priority is `0`.
- *
- * @signature TextExtPlugin.initPriority()
- *
- * @author agorbatchev
- * @date 2011/08/22
- * @id TextExtPlugin.initPriority
- */
- p.initPriority = function()
- {
- return 0;
- };
-
- //--------------------------------------------------------------------------------
- // jQuery Integration
-
- /**
- * TextExt integrates as a jQuery plugin available through the `$(selector).textext(opts)` call. If
- * `opts` argument is passed, then a new instance of `TextExt` will be created for all the inputs
- * that match the `selector`. If `opts` wasn't passed and TextExt was already intantiated for
- * inputs that match the `selector`, array of `TextExt` instances will be returned instead.
- *
- * // will create a new instance of `TextExt` for all elements that match `.sample`
- * $('.sample').textext({ ... });
- *
- * // will return array of all `TextExt` instances
- * var list = $('.sample').textext();
- *
- * The following properties are also exposed through the jQuery `$.fn.textext`:
- *
- * * `TextExt` -- `TextExt` class.
- * * `TextExtPlugin` -- `TextExtPlugin` class.
- * * `ItemManager` -- `ItemManager` class.
- * * `plugins` -- Key/value table of all registered plugins.
- * * `addPlugin(name, constructor)` -- All plugins should register themselves using this function.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExt.jquery
- */
-
- var cssInjected = false;
-
- var textext = $.fn.textext = function(opts)
- {
- var css;
-
- if(!cssInjected && (css = $.fn.textext.css) != null)
- {
- $('head').append('');
- 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);
-
diff --git a/src/js/textext.itemmanager.ajax.js b/src/js/textext.itemmanager.ajax.js
new file mode 100644
index 0000000..577cbba
--- /dev/null
+++ b/src/js/textext.itemmanager.ajax.js
@@ -0,0 +1,262 @@
+/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.0
+ * @copyright Copyright (C) 2011-2012 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($, undefined)
+{
+ function AjaxItemManager()
+ {
+ };
+
+ $.fn.textext.AjaxItemManager = AjaxItemManager;
+ $.fn.textext.addItemManager('ajax', AjaxItemManager);
+
+ var p = AjaxItemManager.prototype,
+
+ CSS_LOADING = 'text-loading',
+
+ /**
+ * 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 AjaxItemManager.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(filter)
+ * {
+ * return { 'search' : filter };
+ * }
+ *
+ * @name ajax.data.callback
+ * @default null
+ * @author agorbatchev
+ * @date 2011/08/16
+ * @id AjaxItemManager.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 `AjaxItemManager` attached to the core.
+ *
+ * @name ajax.data.results
+ * @default false
+ * @author agorbatchev
+ * @date 2011/08/16
+ * @id AjaxItemManager.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 AjaxItemManager.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 AjaxItemManager.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 AjaxItemManager.options.type.delay
+ */
+ OPT_TYPE_DELAY = 'ajax.type.delay',
+
+ TIMER_LOADING = 'loading',
+
+ DEFAULT_OPTS = {
+ ajax : {
+ typeDelay : 0.5,
+ loadingDelay : 0.5,
+ cacheResults : false,
+ dataCallback : null
+ }
+ }
+ ;
+
+ /**
+ * Initialization method called by the core during plugin instantiation.
+ *
+ * @signature AjaxItemManager.init(core)
+ *
+ * @param core {TextExt} Instance of the TextExt core class.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AjaxItemManager.init
+ */
+ p.init = function(core)
+ {
+ this.baseInit(core, DEFAULT_OPTS);
+ };
+
+ p.getSuggestions = function(filter, callback)
+ {
+ var self = this;
+
+ self.startTimer(
+ 'ajax',
+ self.opts(OPT_TYPE_DELAY),
+ function()
+ {
+ self.beginLoading();
+ self.load(filter, callback);
+ }
+ );
+ };
+
+ p.load = function(filter, callback)
+ {
+ var self = this,
+ dataCallback = self.opts(OPT_DATA_CALLBACK),
+ opts
+ ;
+
+ if(self._cached && self.opts(OPT_CACHE_RESULTS))
+ {
+ self.stopLoading();
+ return self.filter(self.data, filter, callback);
+ }
+
+ opts = $.extend(true,
+ {
+ data : dataCallback ? dataCallback(filter) : self.getAjaxData(filter),
+ success : function(data) { self.onSuccess(data, filter, callback); },
+ error : function(jqXHR, message) { self.onError(jqXHR, message, filter, callback); }
+ },
+ self.opts('ajax')
+ );
+
+ $.ajax(opts);
+ };
+
+ p.getAjaxData = function(filter)
+ {
+ return { q : filter };
+ };
+
+ p.getItemsFromAjax = function(data)
+ {
+ return data;
+ };
+
+ p.onSuccess = function(data, filter, callback)
+ {
+ var self = this;
+
+ self.stopLoading();
+
+ data = self.data = self.getItemsFromAjax(data);
+
+ if(self.opts(OPT_CACHE_RESULTS))
+ self._cached = 1;
+
+ self.filter(data, filter, callback);
+ };
+
+ p.onError = function(jqXHR, message, filter, callback)
+ {
+ this.stopLoading();
+ callback(new Error(message));
+ };
+
+ /**
+ * If show loading message timer was started, calling this function disables it,
+ * otherwise nothing else happens.
+ *
+ * @signature AjaxItemManager.stopLoading()
+ *
+ * @author agorbatchev
+ * @date 2011/08/16
+ * @id AjaxItemManager.stopLoading
+ */
+ p.stopLoading = function()
+ {
+ this.stopTimer(TIMER_LOADING);
+ this.input().removeClass(CSS_LOADING);
+ };
+
+ /**
+ * Shows message specified in `ajax.loading.message` if loading data takes more than
+ * number of seconds specified in `ajax.loading.delay`.
+ *
+ * @signature AjaxItemManager.beginLoading()
+ *
+ * @author agorbatchev
+ * @date 2011/08/15
+ * @id AjaxItemManager.beginLoading
+ */
+ p.beginLoading = function()
+ {
+ var self = this;
+
+ self.stopLoading();
+ self.startTimer(
+ TIMER_LOADING,
+ self.opts(OPT_LOADING_DELAY),
+ function()
+ {
+ self.input().addClass(CSS_LOADING);
+ }
+ );
+ };
+})(jQuery);
+
diff --git a/src/js/textext.itemmanager.default.js b/src/js/textext.itemmanager.default.js
new file mode 100644
index 0000000..0aef6ec
--- /dev/null
+++ b/src/js/textext.itemmanager.default.js
@@ -0,0 +1,20 @@
+/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.0
+ * @copyright Copyright (C) 2011-2012 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($, undefined)
+{
+ function DefaultItemManager()
+ {
+ };
+
+ $.fn.textext.DefaultItemManager = DefaultItemManager;
+ $.fn.textext.addItemManager('default', DefaultItemManager);
+
+ var p = DefaultItemManager.prototype;
+})(jQuery);
+
diff --git a/src/js/textext.itemmanager.js b/src/js/textext.itemmanager.js
new file mode 100644
index 0000000..17b8890
--- /dev/null
+++ b/src/js/textext.itemmanager.js
@@ -0,0 +1,194 @@
+/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.0
+ * @copyright Copyright (C) 2011-2012 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($, undefined)
+{
+ /**
+ * 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
+ * })
+ *
+ * New in 1.4 is ability to inline `ItemManager` as an object
+ * instead of a constructor. Here's an example:
+ *
+ * $('#input').textext({
+ * itemManager : {
+ * init : function(core)
+ * {
+ * },
+ *
+ * filter : function(list, query)
+ * {
+ * },
+ *
+ * itemContains : function(item, needle)
+ * {
+ * },
+ *
+ * stringToItem : function(str)
+ * {
+ * },
+ *
+ * itemToString : function(item)
+ * {
+ * },
+ *
+ * compareItems : function(item1, item2)
+ * {
+ * }
+ * }
+ * })
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id ItemManager
+ */
+ function ItemManager()
+ {
+ };
+
+ var textext = $.fn.textext,
+ p = ItemManager.prototype = new textext.Plugin()
+ ;
+
+ textext.ItemManager = ItemManager;
+
+ p.init = function(core)
+ {
+ this.baseInit(core);
+ };
+
+ p.serialize = JSON.stringify;
+
+ /**
+ * Filters out items from the list that don't match the query and returns remaining items. Default
+ * implementation checks if the string item starts with the query. Should be using the data that
+ * is passed to the `setSuggestions` method.
+ *
+ * @signature ItemManager.getSuggestions()
+ *
+ * @author agorbatchev
+ * @date 2012/06/16
+ * @id ItemManager.getSuggestions
+ */
+ p.getSuggestions = function(filter, callback)
+ {
+ this.filter(this.core().opts('suggestions'), filter, callback);
+ };
+
+ p.filter = function(items, filter, callback)
+ {
+ var self = this,
+ result = []
+ ;
+
+ self.each(items, function(err, item)
+ {
+ if(self.itemContains(item, filter))
+ result.push(item);
+ });
+
+ callback(null, result);
+ };
+
+ p.each = function(items, callback)
+ {
+ if(items)
+ for(var i = 0; i < items.length; i++)
+ callback(null, items[i], i);
+ };
+
+ /**
+ * 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, callback)
+ {
+ callback(null, 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;
+ };
+})(jQuery);
+
diff --git a/src/js/textext.itemvalidator.default.js b/src/js/textext.itemvalidator.default.js
new file mode 100644
index 0000000..c50b437
--- /dev/null
+++ b/src/js/textext.itemvalidator.default.js
@@ -0,0 +1,30 @@
+/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.0
+ * @copyright Copyright (C) 2011-2012 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($, undefined)
+{
+ function DefaultItemValidator()
+ {
+ };
+
+ $.fn.textext.DefaultItemValidator = DefaultItemValidator;
+ $.fn.textext.addItemValidator('default', DefaultItemValidator);
+
+ var p = DefaultItemValidator.prototype;
+
+ p.init = function(core)
+ {
+ this.baseInit(core);
+ };
+
+ p.isValid = function(item, callback)
+ {
+ callback(null, item && item.length > 0);
+ };
+})(jQuery);
+
diff --git a/src/js/textext.itemvalidator.js b/src/js/textext.itemvalidator.js
new file mode 100644
index 0000000..950f935
--- /dev/null
+++ b/src/js/textext.itemvalidator.js
@@ -0,0 +1,31 @@
+/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.0
+ * @copyright Copyright (C) 2011-2012 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($, undefined)
+{
+ function ItemValidator()
+ {
+ };
+
+ var textext = $.fn.textext,
+ p = ItemValidator.prototype = new textext.Plugin()
+ ;
+
+ textext.ItemValidator = ItemValidator;
+
+ p.init = function(core)
+ {
+ this.baseInit(core);
+ };
+
+ p.isValid = function(item, callback)
+ {
+ throw new Error('TextExt.js: please implement `ItemValidator.isValid`');
+ };
+})(jQuery);
+
diff --git a/src/js/textext.itemvalidator.suggestions.js b/src/js/textext.itemvalidator.suggestions.js
new file mode 100644
index 0000000..83dc9ca
--- /dev/null
+++ b/src/js/textext.itemvalidator.suggestions.js
@@ -0,0 +1,67 @@
+/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.0
+ * @copyright Copyright (C) 2011-2012 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($, undefined)
+{
+ function SuggestionsItemValidator()
+ {
+ };
+
+ $.fn.textext.SuggestionsItemValidator = SuggestionsItemValidator;
+ $.fn.textext.addItemValidator('suggestions', SuggestionsItemValidator);
+
+ var p = SuggestionsItemValidator.prototype;
+
+ p.init = function(core)
+ {
+ var self = this;
+
+ self.baseInit(core);
+ self.on({ enterKeyPress : self.onEnterKeyPress });
+ };
+
+ p.isValid = function(item, callback)
+ {
+ var self = this,
+ core = self.core(),
+ itemManager = core.itemManager()
+ ;
+
+ itemManager.getSuggestions(itemManager.itemToString(item), function(err, items)
+ {
+ callback(err, items && itemManager.compareItems(item, items[0]));
+ });
+ };
+
+ p.onEnterKeyPress = function(e)
+ {
+ var self = this;
+
+ self.isValid(self.val(), function(err, isValid)
+ {
+ if(isValid)
+ self.core().invalidateData();
+ });
+ };
+
+ p.getFormData = function(callback)
+ {
+ var self = this,
+ itemManager = self.itemManager(),
+ inputValue = self.val(),
+ formValue
+ ;
+
+ itemManager.stringToItem(inputValue, function(err, item)
+ {
+ formValue = itemManager.serialize(item);
+ callback(null, formValue, inputValue);
+ });
+ };
+})(jQuery);
+
diff --git a/src/js/textext.js b/src/js/textext.js
new file mode 100644
index 0000000..7f4b6ea
--- /dev/null
+++ b/src/js/textext.js
@@ -0,0 +1,1268 @@
+/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.0
+ * @copyright Copyright (C) 2011-2012 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($, undefined)
+{
+ // Freak out if there's no JSON.stringify function found
+ if(!JSON.stringify)
+ throw new Error('TextExt.js: `JSON.stringify()` not found');
+
+ /**
+ * 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() {};
+
+ var slice = Array.prototype.slice,
+ UNDEFINED = 'undefined',
+ p,
+
+ /**
+ * 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:
+ *
+ * ### Hierarchical
+ *
+ * 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: ...
+ * }
+ * }
+ * }
+ *
+ * ### Flat
+ *
+ * Options could be specified using camel cased names in a flat key/value fashion like so:
+ *
+ * {
+ * itemManager: ...,
+ * htmlWrap: ...,
+ * autocompleteEnabled: ...,
+ * autocompleteDropdownPosition: ...
+ * }
+ *
+ * ### Mixed
+ *
+ * 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
+ */
+
+ /**
+ * Allows to change which [`ItemManager`](core-itemmanager.html) is used to manage this instance of `TextExt`.
+ *
+ * @name item.manager
+ * @default ItemManagerDefault
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.options.item.manager
+ */
+ OPT_ITEM_MANAGER = 'item.manager',
+
+ /**
+ * Allows to change which [`ItemValidator`](core-itemvalidator.html) is used to validate entries in this instance of `TextExt`.
+ *
+ * @name item.validator
+ * @default ItemValidatorDefault
+ * @author agorbatchev
+ * @date 2012/09/12
+ * @id TextExt.options.item.validator
+ */
+ OPT_ITEM_VALIDATOR = 'item.validator',
+
+ /**
+ * List of plugins that should be used with the current instance of TextExt. Here are all the ways
+ * that you can set this. The order in which plugins are specified is significant. First plugin in
+ * the list that has `getFormData` method will be used as [`dataSource`](#dataSource).
+ *
+ * // array
+ * [ 'autocomplete', 'tags', 'prompt' ]
+ *
+ * // space separated string
+ * 'autocomplete tags prompt'
+ *
+ * // comma separated string
+ * 'autocomplete, tags, prompt'
+ *
+ * // bracket separated string
+ * 'autocomplete > tags > prompt'
+ *
+ * @name plugins
+ * @default []
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.options.plugins
+ */
+ OPT_PLUGINS = 'plugins',
+
+ /**
+ * Name of the plugin that will be used as primary data source to populate form data that `TextExt` generates.
+ *
+ * `TextExt` always tries to automatically determine best `dataSource` plugin to use. It uses the first plugin in the
+ * `plugins` option which has `getFormData((function(err, form, input) {})` function. You can always specify
+ * exactly which plugin you wish to use either by setting `dataSource` value or by simply adding `*` after
+ * the plugin name in the `plugins` option.
+ *
+ * // In this example `autocomplete` will be automatically selected as `dataSource`
+ * // because it's the first plugin in the list that has `getFormData` method.
+ * $('#text').textext({ plugins : 'autocomplete tags' })
+ *
+ * // In this example we specifically set `dataSource` to use `tags` plugin.
+ * $('#text').textext({
+ * plugins : 'autocomplete tags',
+ * dataSource : 'tags'
+ * })
+ *
+ * // Same result as the above using `*` shorthand
+ * $('#text').textext({ plugins : 'autocomplete tags*' })
+ *
+ * @name dataSource
+ * @default null
+ * @author agorbatchev
+ * @date 2012/09/12
+ * @id TextExt.options.dataSource
+ */
+ OPT_DATA_SOURCE = 'dataSource',
+
+ /**
+ * 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.
+ *
+ * // Targeting the core:
+ * ext: {
+ * core: {
+ * trigger: function()
+ * {
+ * console.log('TextExt.trigger', arguments);
+ * $.fn.textext.TextExt.prototype.trigger.apply(this, arguments);
+ * }
+ * }
+ * }
+ *
+ * // In this case we monkey patch currently used instance of the `Tags` plugin.
+ * ext: {
+ * tags: {
+ * addTags: function(tags)
+ * {
+ * console.log('TextExtTags.addTags', tags);
+ * $.fn.textext.TextExtTags.prototype.addTags.apply(this, arguments);
+ * }
+ * }
+ * }
+ *
+ * // Targeting currently used `ItemManager` instance:
+ * ext: {
+ * itemManager: {
+ * stringToItem: function(str)
+ * {
+ * console.log('ItemManager.stringToItem', str);
+ * return $.fn.textext.ItemManager.prototype.stringToItem.apply(this, arguments);
+ * }
+ * }
+ * }
+ *
+ * // ... 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 '
'
+ * @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 ''
+ * @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`](#KeyDown), [`[name]KeyUp`](#KeyUp)
+ * and [`[name]KeyPress`](#KeyPress) events will be triggered along side with
+ * [`anyKeyUp`](#anyKeyUp) and [`anyKeyDown`](#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 `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 `inputDataChange` event after the value of the visible `` tag is changed.
+ *
+ * @name inputDataChange
+ * @author agorbatchev
+ * @date 2012/09/12
+ * @id TextExt.events.inputDataChange
+ */
+ EVENT_INPUT_DATA_CHANGE = 'inputDataChange',
+
+ /**
+ * Core triggers `formDataChange` event after the value of the hidden `` tag is changed.
+ * This hidden tag carries the form value that TextExt produces.
+ *
+ * @name formDataChange
+ * @author agorbatchev
+ * @date 2012/09/12
+ * @id TextExt.events.formDataChange
+ */
+ EVENT_FORM_DATA_CHANGE = 'formDataChange',
+
+ /**
+ * Core triggers `anyKeyPress` event for every key pressed.
+ *
+ * @name anyKeyPress
+ * @author agorbatchev
+ * @date 2012/09/12
+ * @id TextExt.events.anyKeyPress
+ */
+ EVENT_ANY_KEY_PRESS = 'anyKeyPress',
+
+ /**
+ * 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 : 'default',
+ itemValidator : 'default',
+ dataSource : null,
+ plugins : [],
+ ext : {},
+
+ html : {
+ wrap : '
',
+ hidden : ''
+ },
+
+ keys : {
+ 8 : 'backspace',
+ 9 : 'tab',
+ 13 : 'enter!',
+ 27 : 'escape!',
+ 37 : 'left',
+ 38 : 'up!',
+ 39 : 'right',
+ 40 : 'down!',
+ 46 : 'delete',
+ 108 : 'numpadEnter'
+ }
+ }
+ ;
+
+ /**
+ * Shorthand for executing a function asynchronously at the first possible opportunity.
+ *
+ * @signature nextTick(callback)
+ *
+ * @param callback {Function} Callback function to be executed asynchronously.
+ *
+ * @author agorbatchev
+ * @date 2012/09/12
+ * @id TextExt.methods.nextTick
+ */
+ function nextTick(callback)
+ {
+ setTimeout(callback, 1);
+ }
+
+ /**
+ * Returns `true` if passed value is a string, `false` otherwise.
+ *
+ * @signature isString(val)
+ *
+ * @param val {Anything} Value to be checked.
+ *
+ * @author agorbatchev
+ * @date 2012/09/12
+ * @id TextExt.methods.isString
+ */
+ function isString(val)
+ {
+ return typeof(val) === 'string';
+ }
+
+ /**
+ * Returns object property value by name where name is dot-separated and object is multiple levels deep. This is a helper
+ * method for retrieving option values from a config object using a single string key.
+ *
+ * @signature getProperty(source, name)
+ *
+ * @param source {Object} Source object.
+ * @param name {String} Dot separated property name, ie `foo.bar.world`
+ *
+ * @author agorbatchev
+ * @date 2011/08/09
+ * @id TextExt.methods.getProperty
+ */
+ function getProperty(source, name)
+ {
+ if(isString(name))
+ 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 events specified in the scope of the current object.
+ *
+ * @signature hookupEvents([target], events)
+ *
+ * @param target {Object} Optional target object to the scope of which events will be bound. Defaults to current scope if not specified.
+ * @param events {Object} Events in the following format : `{ event_name : handler_function() }`.
+ *
+ * @author agorbatchev
+ * @date 2011/08/09
+ * @id TextExt.methods.hookupEvents
+ */
+ function hookupEvents(/* [target], events */)
+ {
+ var events = slice.apply(arguments),
+ self = this,
+ target = args.length === 1 ? self : args.shift(),
+ event
+ ;
+
+ events = events[0] || {};
+
+ function bind(event, handler)
+ {
+ target.bind(event, function()
+ {
+ // apply handler to our PLUGIN object, not the target
+ return handler.apply(self, arguments);
+ });
+ }
+
+ for(name in events)
+ bind(name , events[name]);
+ };
+
+ //--------------------------------------------------------------------------------
+ // TextExt core component
+
+ p = TextExt.prototype;
+
+ /**
+ * Initializes current component instance with the supplied text input HTML element and options. Upon completion
+ * this method triggers [`postInit`](#postInit) event followed by [`ready`](#ready) event.
+ *
+ * @signature TextExt.init(input, opts)
+ *
+ * @param input {HTMLElement} Text input HTML dom element.
+ * @param opts {Object} Options object.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.methods.init
+ */
+ p.init = function(input, opts)
+ {
+ var self = this,
+ hiddenInput,
+ container
+ ;
+
+ self.defaultOptions = $.extend({}, DEFAULT_OPTS);
+ self.userOptions = opts || {};
+ self.plugins = {};
+ self.dataSource = self.opts(OPT_DATA_SOURCE);
+ input = $(input);
+ container = $(self.opts(OPT_HTML_WRAP));
+ hiddenInput = $(self.opts(OPT_HTML_HIDDEN));
+
+ if(isString(self.selectionKey))
+ self.selectionKey = self.selectionKey.charCodeAt(0);
+
+ if(input.is('textarea'))
+ input.attr('rows', 1);
+
+ 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, self, self.opts(OPT_EXT + '.*'), self.opts(OPT_EXT + '.core'));
+
+ self.originalWidth = input.outerWidth();
+
+ self.initPatches();
+ self.initTooling();
+ self.initPlugins(self.opts(OPT_PLUGINS), $.fn.textext.plugins);
+
+ self.invalidateBounds();
+
+ nextTick(function()
+ {
+ self.trigger(EVENT_POST_INIT);
+ self.trigger(EVENT_READY);
+ self.invalidateData();
+ });
+ };
+
+ /**
+ * Initializes all patches installed via [`addPatch()`](#addPatch) method call.
+ *
+ * @signature TextExt.initPatches()
+ *
+ * @author agorbatchev
+ * @date 2011/10/11
+ * @id TextExt.methods.initPatches
+ */
+ p.initPatches = function()
+ {
+ var list = [],
+ source = $.fn.textext.patches,
+ name
+ ;
+
+ for(name in source)
+ list.push(name);
+
+ this.initPlugins(list, source);
+ };
+
+ /**
+ * Initializes instances of [`ItemManager`](itemmanager.html) and [`ItemValidator`](itemvalidator.html)
+ * that are specified via [`itemManager`](#manager) and [`dataSource`](#dataSource) options.
+ *
+ * @signature TextExt.initTooling()
+ *
+ * @author agorbatchev
+ * @date 2012/09/12
+ * @id TextExt.methods.initTooling
+ */
+ p.initTooling = function()
+ {
+ var self = this,
+ itemManager = self.opts(OPT_ITEM_MANAGER),
+ itemValidator = self.opts(OPT_ITEM_VALIDATOR)
+ ;
+
+ if(isString(itemManager))
+ itemManager = textext.itemManagers[itemManager];
+
+ if(isString(itemValidator))
+ itemValidator = textext.itemValidators[itemValidator];
+
+ $.extend(true, itemValidator, self.opts(OPT_EXT + '.itemValidator'));
+ $.extend(true, itemManager, self.opts(OPT_EXT + '.itemManager'));
+
+ this.initPlugins(
+ 'itemManager itemValidator',
+ {
+ 'itemManager' : itemManager,
+ 'itemValidator' : itemValidator
+ }
+ );
+ };
+
+ /**
+ * Initializes all plugins installed via [`addPlugin()`](#addPlugin) method call.
+ *
+ * @signature TextExt.initPlugins(plugins, source)
+ *
+ * @param plugins {Array} List of plugin names to initialize.
+ * @param source {Object} Key/value object where a key is plugin name and value is plugin constructor.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.methods.initPlugins
+ */
+ p.initPlugins = function(plugins, source)
+ {
+ var self = this,
+ initList = [],
+ ext,
+ name,
+ plugin,
+ i
+ ;
+
+ if(isString(plugins))
+ plugins = plugins.split(/\s*[,>]\s*|\s+/g);
+
+ function createGetter(name, plugin)
+ {
+ self[name] = function()
+ {
+ return plugin;
+ };
+ }
+
+ for(i = 0; i < plugins.length; i++)
+ {
+ name = plugins[i];
+
+ if(name.charAt(name.length - 1) === '*')
+ self.dataSource = name = name.substr(0, name.length - 1);
+
+ plugin = source[name];
+
+ if(plugin)
+ {
+ self.plugins[name] = plugin = new plugin();
+
+ initList.push(plugin);
+ $.extend(true, plugin, self.opts(OPT_EXT + '.*'), self.opts(OPT_EXT + '.' + name));
+
+ // Create a function on the current instance to get this plugin instance
+ // For example for `autocomplete` plugin we will have `textext.autocomplete()`
+ // function returning this isntance.
+ createGetter(name, plugin);
+
+ plugin.init(self);
+ }
+ else
+ {
+ throw new Error('TextExt.js: unknown plugin: ' + name);
+ }
+ }
+
+ for(i = 0; i < initList.length; i++)
+ {
+ plugin = initList[i];
+
+ if(!self.dataSource && plugin.getFormData)
+ self.dataSource = plugin;
+
+ }
+ };
+
+ /**
+ * Returns `true` if specified plugin is was instantiated for the current instance of TextExt, `false` otherwise.
+ *
+ * @signature TextExt.hasPlugin(name)
+ *
+ * @param name {String} Name of the plugin to check.
+ *
+ * @author agorbatchev
+ * @date 2011/12/28
+ * @id TextExt.methods.hasPlugin
+ */
+ p.hasPlugin = function(name)
+ {
+ return !!this.plugins[name];
+ };
+
+ /**
+ * Allows to add multiple event handlers which will be execued in the TextExt instance scope. Same as calling [`hookupEvents(this, ...)`](#hookupEvents).
+ *
+ * @signature TextExt.on([target], handlers)
+ *
+ * @param target {Object} Optional target object to the scope of which events will be bound. Defaults to current scope if not specified.
+ * @param events {Object} Events in the following format : `{ event_name : handler_function() }`.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.methods.on
+ */
+ p.on = hookupEvents;
+
+ /**
+ * Binds an event handler to the HTML dom element that user interacts with. Usually it's the original input element.
+ *
+ * @signature TextExt.bind(event, handler)
+ *
+ * @param event {String} Event name.
+ * @param handler {Function} Event handler.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.methods.bind
+ */
+ p.bind = function(event, handler)
+ {
+ this.input().bind(event, handler);
+ };
+
+ /**
+ * Triggers an event on the HTML dom element that user interacts with. Usually it's the original input element. 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.methods.trigger
+ */
+ p.trigger = function()
+ {
+ var args = arguments;
+ this.input().trigger(args[0], slice.call(args, 1));
+ };
+
+ /**
+ * Returns jQuery input element with which user is interacting with. Usually it's the original input element.
+ *
+ * @signature TextExt.input()
+ *
+ * @author agorbatchev
+ * @date 2011/08/10
+ * @id TextExt.methods.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. This method relies on [`getProperty`](#getProperty)
+ * for most of its functionality.
+ *
+ * @signature TextExt.opts(name)
+ *
+ * @param name {String} Option name as described in the options.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.methods.opts
+ */
+ p.opts = function(name)
+ {
+ var result = getProperty(this.userOptions, name);
+ return typeof(result) == UNDEFINED ? getProperty(this.defaultOptions, name) : result;
+ };
+
+ /**
+ * Returns HTML element that was created from the [`html.wrap`](#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.methods.wrapElement
+ */
+ p.wrapElement = function()
+ {
+ return $(this).data('wrapElement');
+ };
+
+ /**
+ * Updates TextExt elements to match dimensions of the HTML dom text input. Triggers [`preInvalidate`](#preInvalidate)
+ * event before making any changes and [`postInvalidate`](#postInvalidate) event after everything is done.
+ *
+ * @signature TextExt.invalidateBounds()
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.methods.invalidateBounds
+ */
+ p.invalidateBounds = function()
+ {
+ var self = this,
+ input = self.input(),
+ wrap = self.wrapElement(),
+ container = wrap.parent(),
+ width = self.originalWidth,
+ height
+ ;
+
+ self.trigger(EVENT_PRE_INVALIDATE);
+
+ height = input.outerHeight();
+
+ input.width(width);
+ wrap.width(width).height(height);
+ container.height(height);
+
+ self.trigger(EVENT_POST_INVALIDATE);
+ };
+
+ /**
+ * Focuses user input on the text box.
+ *
+ * @signature TextExt.focusInput()
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.methods.focusInput
+ */
+ p.focusInput = function()
+ {
+ this.input()[0].focus();
+ };
+
+ /**
+ * 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.methods.hiddenInput
+ */
+ p.hiddenInput = function(value)
+ {
+ return $(this).data('hiddenInput');
+ };
+
+ /**
+ * Updates the values that are displayed in the HTML input box to the user and that will be submitted
+ * with the form. Uses [`dataSource`](#dataSource) option to its best ability to determine which plugin
+ * acts as the main data source for the current instance. If option isn't set, the first plugin with
+ * `getFormData()` method will be used.
+ *
+ * @signature TextExt.invalidateData(callback)
+ *
+ * @param callback {Function} Optional callback function that is executed when hidden and visible inputs
+ * are updated.
+ *
+ * @author agorbatchev
+ * @date 2011/08/22
+ * @id TextExt.methods.invalidateData
+ */
+ p.invalidateData = function(callback)
+ {
+ var self = this,
+ dataSource = self.dataSource,
+ key = 'getFormData',
+ plugin,
+ getFormData
+ ;
+
+ function error(msg)
+ {
+ throw new Error('TextExt.js: ' + msg);
+ }
+
+ if(!dataSource)
+ error('no `dataSource` set and no plugin supports `getFormData`');
+
+ if(isString(dataSource))
+ {
+ plugin = self.plugins[dataSource];
+
+ if(!plugin)
+ error('`dataSource` plugin not found: ' + dataSource);
+ }
+ else
+ {
+ if(dataSource instanceof textext.Plugin)
+ {
+ plugin = dataSource;
+ dataSource = null;
+ }
+ }
+
+ if(plugin && plugin[key])
+ // need to insure `dataSource` below is executing with plugin as plugin scop and
+ // if we just reference the `getFormData` function it will be in the window scope.
+ getFormData = function()
+ {
+ plugin[key].apply(plugin, arguments);
+ };
+
+ if(!getFormData)
+ error('specified `dataSource` plugin does not have `getFormData` function: ' + dataSource);
+
+ nextTick(function()
+ {
+ getFormData(function(err, form, input)
+ {
+ self.inputValue(input);
+ self.formValue(form);
+
+ callback && callback();
+ });
+ });
+ };
+
+ /**
+ * Gets or sets visible HTML elment's value. This method could be used by a plugin to change displayed value
+ * in the input box. After the value is changed, triggers the [`inputDataChange`](#inputDataChange) event.
+ *
+ * @signature TextExt.inputValue([value])
+ *
+ * @param value {Object} Optional value to set. If argument isn't supplied, method returns current value instead.
+ *
+ * @author agorbatchev
+ * @date 2011/08/22
+ * @id TextExt.methods.inputValue
+ */
+ p.inputValue = function(value)
+ {
+ var self = this,
+ input = self.input()
+ ;
+
+ if(typeof(value) === UNDEFINED)
+ return self._inputValue;
+
+ if(self._inputValue !== value)
+ {
+ input.val(value);
+ self._inputValue = value;
+ self.trigger(EVENT_INPUT_DATA_CHANGE, value);
+ }
+ };
+
+ /**
+ * Gets or sets hidden HTML elment's value. This method could be used by a plugin to change value submitted
+ * with the form. After the value is changed, triggers the [`formDataChange`](#formDataChange) event.
+ *
+ * @signature TextExt.formValue([value])
+ *
+ * @param value {Object} Optional value to set. If argument isn't supplied, method returns current value instead.
+ *
+ * @author agorbatchev
+ * @date 2011/08/22
+ * @id TextExt.methods.formValue
+ */
+ p.formValue = function(value)
+ {
+ var self = this,
+ hiddenInput = self.hiddenInput()
+ ;
+
+ if(typeof(value) === UNDEFINED)
+ return self._formValue;
+
+ if(self._formValue !== value)
+ {
+ self._formValue = value;
+ hiddenInput.val(value);
+ self.trigger(EVENT_FORM_DATA_CHANGE, value);
+ }
+ };
+
+ //--------------------------------------------------------------------------------
+ // Event handlers
+
+ //--------------------------------------------------------------------------------
+ // User mouse/keyboard input
+
+ /**
+ * Triggers [`[name]KeyUp`](#KeyUp), [`[name]KeyPress`](#KeyPress) and [`anyKeyPress`](#anyKeyPress)
+ * for every keystroke.
+ *
+ * @signature TextExt.onKeyUp(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.methods.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.methods.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');
+ self.trigger(EVENT_ANY_KEY_PRESS, e.keyCode);
+ }
+
+ if(type == 'Down')
+ self._lastKeyDown = e.keyCode;
+ }
+
+ self.trigger('anyKey' + type, e.keyCode);
+
+ return defaultResult;
+ };
+ });
+
+ //--------------------------------------------------------------------------------
+ // 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.
+ * * [`Plugin`](core-plugin.html) -- `Plugin` class.
+ * * [`ItemManager`](core-itemmanager.html) -- `ItemManager` class.
+ * * [`ItemValidator`](core-itemvalidator.html) -- `ItemValidator` class.
+ * * `plugins` -- Key/value table of all registered plugins.
+ * * [`addPlugin(name, constructor)`](#addPlugin) -- All plugins should register themselves using this function.
+ * * [`addPatch(name, constructor)`](#addPatch) -- All patches should register themselves using this function.
+ * * [`addItemManager(name, constructor)`](#addItemManager) -- All item managers should register themselves using this function.
+ * * [`addItemValidator(name, constructor)`](#addItemValidator) -- All item validators 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('');
+ 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 which it will be identified in the options by.
+ * @param constructor {Function} Plugin constructor.
+ *
+ * @author agorbatchev
+ * @date 2011/10/11
+ * @id TextExt.methods.addPlugin
+ */
+ textext.addPlugin = function(name, constructor)
+ {
+ textext.plugins[name] = constructor;
+ constructor.prototype = new textext.Plugin();
+ };
+
+ /**
+ * 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 2012/10/27
+ * @id TextExt.methods.addPatch
+ */
+ textext.addPatch = function(name, constructor)
+ {
+ textext.patches[name] = constructor;
+ constructor.prototype = new textext.Plugin();
+ };
+
+ /**
+ * This static function registers a new [`ItemManager`](core-itemmanager.html) is then could be used
+ * by a new TextExt instance.
+ *
+ * @signature $.fn.textext.addItemManager(name, constructor)
+ *
+ * @param name {String} Name of the item manager which it will be identified in the options by.
+ * @param constructor {Function} Item Manager constructor.
+ *
+ * @author agorbatchev
+ * @date 2012/10/27
+ * @id TextExt.methods.addItemManager
+ */
+ textext.addItemManager = function(name, constructor)
+ {
+ textext.itemManagers[name] = constructor;
+ constructor.prototype = new textext.ItemManager();
+ };
+
+ /**
+ * This static function registers a new [`ItemValidator`](core-itemvalidator.html) is then could be used
+ * by a new TextExt instance.
+ *
+ * @signature $.fn.textext.addItemValidator(name, constructor)
+ *
+ * @param name {String} Name of the item validator which it will be identified in the options by.
+ * @param constructor {Function} Item Validator constructor.
+ *
+ * @author agorbatchev
+ * @date 2012/10/27
+ * @id TextExt.methods.addItemValidator
+ */
+ textext.addItemValidator = function(name, constructor)
+ {
+ textext.itemValidators[name] = constructor;
+ constructor.prototype = new textext.ItemValidator();
+ };
+
+ textext.TextExt = TextExt;
+ textext.plugins = {};
+ textext.patches = {};
+ textext.itemManagers = {};
+ textext.itemValidators = {};
+})(jQuery);
+
diff --git a/src/js/textext.patch.ie9.js b/src/js/textext.patch.ie9.js
new file mode 100644
index 0000000..4e59bbf
--- /dev/null
+++ b/src/js/textext.patch.ie9.js
@@ -0,0 +1,34 @@
+(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);
+
diff --git a/src/js/textext.plugin.ajax.js b/src/js/textext.plugin.ajax.js
deleted file mode 100644
index 31595b9..0000000
--- a/src/js/textext.plugin.ajax.js
+++ /dev/null
@@ -1,354 +0,0 @@
-/**
- * jQuery TextExt Plugin
- * http://textextjs.com
- *
- * @version 1.3.0
- * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
- * @license MIT License
- */
-(function($)
-{
- /**
- * AJAX plugin is very useful if you want to load list of items from a data point and pass it
- * to the Autocomplete or Filter plugins.
- *
- * Because it meant to be as a helper method for either Autocomplete or Filter plugin, without
- * either of these two present AJAX plugin won't do anything.
- *
- * @author agorbatchev
- * @date 2011/08/16
- * @id TextExtAjax
- */
- function TextExtAjax() {};
-
- $.fn.textext.TextExtAjax = TextExtAjax;
- $.fn.textext.addPlugin('ajax', TextExtAjax);
-
- var p = TextExtAjax.prototype,
-
- /**
- * AJAX plugin options are grouped under `ajax` when passed to the `$().textext()` function. Be
- * mindful that the whole `ajax` object is also passed to jQuery `$.ajax` call which means that
- * you can change all jQuery options as well. Please refer to the jQuery documentation on how
- * to set url and all other parameters. For example:
- *
- * $('textarea').textext({
- * plugins: 'ajax',
- * ajax: {
- * url: 'http://...'
- * }
- * })
- *
- * **Important**: Because it's necessary to pass options to `jQuery.ajax()` in a single object,
- * all jQuery related AJAX options like `url`, `dataType`, etc **must** be within the `ajax` object.
- * This is the exception to general rule that TextExt options can be specified in dot or camel case
- * notation.
- *
- * @author agorbatchev
- * @date 2011/08/16
- * @id TextExtAjax.options
- */
-
- /**
- * By default, when user starts typing into the text input, AJAX plugin will start making requests
- * to the `url` that you have specified and will pass whatever user has typed so far as a parameter
- * named `q`, eg `?q=foo`.
- *
- * If you wish to change this behaviour, you can pass a function as a value for this option which
- * takes one argument (the user input) and should return a key/value object that will be converted
- * to the request parameters. For example:
- *
- * 'dataCallback' : function(query)
- * {
- * return { 'search' : query };
- * }
- *
- * @name ajax.data.callback
- * @default null
- * @author agorbatchev
- * @date 2011/08/16
- * @id TextExtAjax.options.data.callback
- */
- OPT_DATA_CALLBACK = 'ajax.data.callback',
-
- /**
- * By default, the server end point is constantly being reloaded whenever user changes the value
- * in the text input. If you'd rather have the client do result filtering, you can return all
- * possible results from the server and cache them on the client by setting this option to `true`.
- *
- * In such a case, only one call to the server will be made and filtering will be performed on
- * the client side using `ItemManager` attached to the core.
- *
- * @name ajax.data.results
- * @default false
- * @author agorbatchev
- * @date 2011/08/16
- * @id TextExtAjax.options.cache.results
- */
- OPT_CACHE_RESULTS = 'ajax.cache.results',
-
- /**
- * The loading message delay is set in seconds and will specify how long it would take before
- * user sees the message. If you don't want user to ever see this message, set the option value
- * to `Number.MAX_VALUE`.
- *
- * @name ajax.loading.delay
- * @default 0.5
- * @author agorbatchev
- * @date 2011/08/16
- * @id TextExtAjax.options.loading.delay
- */
- OPT_LOADING_DELAY = 'ajax.loading.delay',
-
- /**
- * Whenever an AJAX request is made and the server takes more than the number of seconds specified
- * in `ajax.loading.delay` to respond, the message specified in this option will appear in the drop
- * down.
- *
- * @name ajax.loading.message
- * @default "Loading..."
- * @author agorbatchev
- * @date 2011/08/17
- * @id TextExtAjax.options.loading.message
- */
- OPT_LOADING_MESSAGE = 'ajax.loading.message',
-
- /**
- * When user is typing in or otherwise changing the value of the text input, it's undesirable to make
- * an AJAX request for every keystroke. Instead it's more conservative to send a request every number
- * of seconds while user is typing the value. This number of seconds is specified by the `ajax.type.delay`
- * option.
- *
- * @name ajax.type.delay
- * @default 0.5
- * @author agorbatchev
- * @date 2011/08/17
- * @id TextExtAjax.options.type.delay
- */
- OPT_TYPE_DELAY = 'ajax.type.delay',
-
- /**
- * AJAX plugin dispatches or reacts to the following events.
- *
- * @author agorbatchev
- * @date 2011/08/17
- * @id TextExtAjax.events
- */
-
- /**
- * AJAX plugin reacts to the `getSuggestions` event dispatched by the Autocomplete plugin.
- *
- * @name getSuggestions
- * @author agorbatchev
- * @date 2011/08/17
- * @id TextExtAjax.events.getSuggestions
- */
-
- /**
- * In the event of successful AJAX request, the AJAX coponent dispatches the `setSuggestions`
- * event meant to be recieved by the Autocomplete plugin.
- *
- * @name setSuggestions
- * @author agorbatchev
- * @date 2011/08/17
- * @id TextExtAjax.events.setSuggestions
- */
- EVENT_SET_SUGGESTION = 'setSuggestions',
-
- /**
- * AJAX plugin dispatches the `showDropdown` event which Autocomplete plugin is expecting.
- * This is used to temporarily show the loading message if the AJAX request is taking longer
- * than expected.
- *
- * @name showDropdown
- * @author agorbatchev
- * @date 2011/08/17
- * @id TextExtAjax.events.showDropdown
- */
- EVENT_SHOW_DROPDOWN = 'showDropdown',
-
- TIMER_LOADING = 'loading',
-
- DEFAULT_OPTS = {
- ajax : {
- typeDelay : 0.5,
- loadingMessage : 'Loading...',
- loadingDelay : 0.5,
- cacheResults : false,
- dataCallback : null
- }
- }
- ;
-
- /**
- * Initialization method called by the core during plugin instantiation.
- *
- * @signature TextExtAjax.init(core)
- *
- * @param core {TextExt} Instance of the TextExt core class.
- *
- * @author agorbatchev
- * @date 2011/08/17
- * @id TextExtAjax.init
- */
- p.init = function(core)
- {
- var self = this;
-
- self.baseInit(core, DEFAULT_OPTS);
-
- self.on({
- getSuggestions : self.onGetSuggestions
- });
-
- self._suggestions = null;
- };
-
- /**
- * Performas an async AJAX with specified options.
- *
- * @signature TextExtAjax.load(query)
- *
- * @param query {String} Value that user has typed into the text area which is
- * presumably the query.
- *
- * @author agorbatchev
- * @date 2011/08/14
- * @id TextExtAjax.load
- */
- p.load = function(query)
- {
- var self = this,
- dataCallback = self.opts(OPT_DATA_CALLBACK) || function(query) { return { q : query } },
- opts
- ;
-
- opts = $.extend(true,
- {
- data : dataCallback(query),
- success : function(data) { self.onComplete(data, query) },
- error : function(jqXHR, message) { console.error(message, query) }
- },
- self.opts('ajax')
- );
-
- $.ajax(opts);
- };
-
- /**
- * Successful call AJAX handler. Takes the data that came back from AJAX and the
- * original query that was used to make the call.
- *
- * @signature TextExtAjax.onComplete(data, query)
- *
- * @param data {Object} Data loaded from the server, should be an Array of strings
- * by default or whatever data structure your custom `ItemManager` implements.
- *
- * @param query {String} Query string, ie whatever user has typed in.
- *
- * @author agorbatchev
- * @date 2011/08/14
- * @id TextExtAjax.onComplete
- */
- p.onComplete = function(data, query)
- {
- var self = this,
- result = data
- ;
-
- self.dontShowLoading();
-
- // If results are expected to be cached, then we store the original
- // data set and return the filtered one based on the original query.
- // That means we do filtering on the client side, instead of the
- // server side.
- if(self.opts(OPT_CACHE_RESULTS) == true)
- {
- self._suggestions = data;
- result = self.itemManager().filter(data, query);
- }
-
- self.trigger(EVENT_SET_SUGGESTION, { result : result });
- };
-
- /**
- * If show loading message timer was started, calling this function disables it,
- * otherwise nothing else happens.
- *
- * @signature TextExtAjax.dontShowLoading()
- *
- * @author agorbatchev
- * @date 2011/08/16
- * @id TextExtAjax.dontShowLoading
- */
- p.dontShowLoading = function()
- {
- this.stopTimer(TIMER_LOADING);
- };
-
- /**
- * Shows message specified in `ajax.loading.message` if loading data takes more than
- * number of seconds specified in `ajax.loading.delay`.
- *
- * @signature TextExtAjax.showLoading()
- *
- * @author agorbatchev
- * @date 2011/08/15
- * @id TextExtAjax.showLoading
- */
- p.showLoading = function()
- {
- var self = this;
-
- self.dontShowLoading();
- self.startTimer(
- TIMER_LOADING,
- self.opts(OPT_LOADING_DELAY),
- function()
- {
- self.trigger(EVENT_SHOW_DROPDOWN, function(autocomplete)
- {
- autocomplete.clearItems();
- var node = autocomplete.addDropdownItem(self.opts(OPT_LOADING_MESSAGE));
- node.addClass('text-loading');
- });
- }
- );
- };
-
- /**
- * Reacts to the `getSuggestions` event and begin loading suggestions. If
- * `ajax.cache.results` is specified, all calls after the first one will use
- * cached data and filter it with the `core.itemManager.filter()`.
- *
- * @signature TextExtAjax.onGetSuggestions(e, data)
- *
- * @param e {Object} jQuery event.
- * @param data {Object} Data structure passed with the `getSuggestions` event
- * which contains the user query, eg `{ query : "..." }`.
- *
- * @author agorbatchev
- * @date 2011/08/15
- * @id TextExtAjax.onGetSuggestions
- */
- p.onGetSuggestions = function(e, data)
- {
- var self = this,
- suggestions = self._suggestions,
- query = (data || {}).query || ''
- ;
-
- if(suggestions && self.opts(OPT_CACHE_RESULTS) === true)
- return self.onComplete(suggestions, query);
-
- self.startTimer(
- 'ajax',
- self.opts(OPT_TYPE_DELAY),
- function()
- {
- self.showLoading();
- self.load(query);
- }
- );
- };
-})(jQuery);
diff --git a/src/js/textext.plugin.arrow.js b/src/js/textext.plugin.arrow.js
index c192826..438d5a9 100644
--- a/src/js/textext.plugin.arrow.js
+++ b/src/js/textext.plugin.arrow.js
@@ -3,104 +3,111 @@
* http://textextjs.com
*
* @version 1.3.0
- * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @copyright Copyright (C) 2011-2012 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() {};
+ /**
+ * Displays a dropdown style arrow button. The `ArrowPlugin` works together with the
+ * `TextExtAutocomplete` plugin and whenever clicked tells the autocomplete plugin to
+ * display its suggestions.
+ *
+ * @author agorbatchev
+ * @date 2011/12/27
+ * @id ArrowPlugin
+ */
+ function ArrowPlugin() {};
- $.fn.textext.TextExtArrow = TextExtArrow;
- $.fn.textext.addPlugin('arrow', TextExtArrow);
+ $.fn.textext.ArrowPlugin = ArrowPlugin;
+ $.fn.textext.addPlugin('arrow', ArrowPlugin);
- 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: ""
- * }
- * })
- *
- * @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 ''
- * @author agorbatchev
- * @date 2011/12/27
- * @id TextExtArrow.options.html.arrow
- */
- OPT_HTML_ARROW = 'html.arrow',
+ var p = ArrowPlugin.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: ""
+ * }
+ * })
+ *
+ * @author agorbatchev
+ * @date 2011/12/27
+ * @id ArrowPlugin.options
+ */
+
+ /**
+ * HTML source that is used to generate markup required for the arrow.
+ *
+ * @name html.arrow
+ * @default ''
+ * @author agorbatchev
+ * @date 2011/12/27
+ * @id ArrowPlugin.options.html.arrow
+ */
+ OPT_HTML_ARROW = 'html.arrow',
- DEFAULT_OPTS = {
- html : {
- arrow : ''
- }
- }
- ;
+ DEFAULT_OPTS = {
+ html : {
+ 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
- ;
+ /**
+ * Initialization method called by the core during plugin instantiation.
+ *
+ * @signature ArrowPlugin.init(core)
+ *
+ * @param core {TextExt} Instance of the TextExt core class.
+ *
+ * @author agorbatchev
+ * @date 2011/12/27
+ * @id ArrowPlugin.init
+ */
+ p.init = function(core)
+ {
+ var self = this,
+ arrow
+ ;
- self.baseInit(core, DEFAULT_OPTS);
+ 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); });
- };
+ self._arrow = arrow = $(self.opts(OPT_HTML_ARROW));
+ self.core().wrapElement().append(arrow);
- //--------------------------------------------------------------------------------
- // 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
+ self.on(arrow, {
+ click : self.onArrowClick
+ });
+ };
+ //--------------------------------------------------------------------------------
+ // Event handlers
+
+ /**
+ * Reacts to the `click` event whenever user clicks the arrow.
+ *
+ * @signature ArrowPlugin.onArrowClick(e)
+ *
+ * @param e {Object} jQuery event.
+ * @author agorbatchev
+ * @date 2011/12/27
+ * @id ArrowPlugin.onArrowClick
+ */
+ p.onArrowClick = function(e)
+ {
+ var self = this,
+ core = self.core(),
+ autocomplete = core.autocomplete && core.autocomplete()
+ ;
+
+ if(autocomplete)
+ {
+ autocomplete.renderSuggestions();
+ core.focusInput();
+ }
+ };
})(jQuery);
diff --git a/src/js/textext.plugin.autocomplete.js b/src/js/textext.plugin.autocomplete.js
index b07d813..4c153fb 100644
--- a/src/js/textext.plugin.autocomplete.js
+++ b/src/js/textext.plugin.autocomplete.js
@@ -3,1108 +3,919 @@
* http://textextjs.com
*
* @version 1.3.0
- * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @copyright Copyright (C) 2011-2012 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 '' + suggestion + '';
- * }
- * }
- * })
- *
- * @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 '
'
- * @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 '
'
- * @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('Item');
- * 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 : '
',
- suggestion : '
'
- }
- }
- ;
-
- /**
- * 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('Item');
- * 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;
- }
+ /**
+ * 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 AutocompletePlugin
+ */
+ function AutocompletePlugin() {};
+
+ $.fn.textext.AutocompletePlugin = AutocompletePlugin;
+ $.fn.textext.addPlugin('autocomplete', AutocompletePlugin);
+
+ var p = AutocompletePlugin.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 AutocompletePlugin.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 AutocompletePlugin.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 AutocompletePlugin.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 AutocompletePlugin.options.autocomplete.dropdown.maxHeight
+ */
+ 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 `AutocompletePlugin`.
+ *
+ * [Click here](/manual/examples/autocomplete-with-custom-render.html) to see a demo.
+ *
+ * For example:
+ *
+ * $('textarea').textext({
+ * plugins: 'autocomplete',
+ * autocomplete: {
+ * render: function(suggestion)
+ * {
+ * return '' + suggestion + '';
+ * }
+ * }
+ * })
+ *
+ * @name autocomplete.render
+ * @default null
+ * @author agorbatchev
+ * @date 2011/12/23
+ * @id AutocompletePlugin.options.autocomplete.render
+ */
+ OPT_RENDER = 'autocomplete.render',
+
+ /**
+ * HTML source that is used to generate the dropdown.
+ *
+ * @name html.dropdown
+ * @default '
'
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.options.html.dropdown
+ */
+ OPT_HTML_DROPDOWN = 'html.dropdown',
+
+ /**
+ * HTML source that is used to generate each suggestion.
+ *
+ * @name html.suggestion
+ * @default '
'
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.options.html.suggestion
+ */
+ OPT_HTML_SUGGESTION = 'html.suggestion',
+
+ /**
+ * Autocomplete plugin triggers or reacts to the following events.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.events
+ */
+
+ /**
+ * 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 AutocompletePlugin.events.getFormData
+ */
+ EVENT_GET_FORM_DATA = 'getFormData',
+
+ POSITION_ABOVE = 'above',
+ POSITION_BELOW = 'below',
+
+ DATA_MOUSEDOWN_ON_AUTOCOMPLETE = 'mousedownOnAutocomplete',
+
+ DEFAULT_OPTS = {
+ autocomplete : {
+ enabled : true,
+ dropdown : {
+ position : POSITION_BELOW,
+ maxHeight : '100px'
+ }
+ },
+
+ html : {
+ dropdown : '
',
+ suggestion : '
'
+ }
+ }
+ ;
+
+ /**
+ * Initialization method called by the core during plugin instantiation.
+ *
+ * @signature AutocompletePlugin.init(core)
+ *
+ * @param core {TextExt} Instance of the TextExt core class.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.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,
+ postInvalidate : self.positionDropdown,
+
+ // 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.hideDropdown();
+ });
+
+ self.positionDropdown();
+ }
+ };
+
+ /**
+ * Returns top level dropdown container HTML element.
+ *
+ * @signature AutocompletePlugin.containerElement()
+ *
+ * @author agorbatchev
+ * @date 2011/08/15
+ * @id AutocompletePlugin.containerElement
+ */
+ p.containerElement = function()
+ {
+ return $(this).data('container');
+ };
+
+ //--------------------------------------------------------------------------------
+ // User mouse/keyboard input
+
+ /**
+ * Reacts to the `mouseOver` event triggered by the TextExt core.
+ *
+ * @signature AutocompletePlugin.onMouseOver(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.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 AutocompletePlugin.onMouseDown(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author adamayres
+ * @date 2012/01/13
+ * @id AutocompletePlugin.onMouseDown
+ */
+ p.onMouseDown = function(e)
+ {
+ this.containerElement().data(DATA_MOUSEDOWN_ON_AUTOCOMPLETE, true);
+ };
+
+ /**
+ * Reacts to the `click` event triggered by the TextExt core.
+ *
+ * @signature AutocompletePlugin.onClick(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.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 AutocompletePlugin.onBlur(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.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.hideDropdown();
+
+ container.removeData(DATA_MOUSEDOWN_ON_AUTOCOMPLETE);
+ };
+
+ /**
+ * Reacts to the `backspaceKeyPress` event triggered by the TextExt core.
+ *
+ * @signature AutocompletePlugin.onBackspaceKeyPress(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.onBackspaceKeyPress
+ */
+ p.onBackspaceKeyPress = function(e)
+ {
+ var self = this,
+ isEmpty = self.val().length > 0
+ ;
+
+ if(isEmpty || self.isDropdownVisible())
+ self.renderSuggestions();
+ };
+
+ /**
+ * Reacts to the `anyKeyUp` event triggered by the TextExt core.
+ *
+ * @signature AutocompletePlugin.onAnyKeyUp(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.onAnyKeyUp
+ */
+ p.onAnyKeyUp = function(e, keyCode)
+ {
+ var self = this,
+ isFunctionKey = self.opts('keys.' + keyCode) != null
+ ;
+
+ if(self.val().length > 0 && !isFunctionKey)
+ self.renderSuggestions();
+ };
+
+ /**
+ * Reacts to the `downKeyDown` event triggered by the TextExt core.
+ *
+ * @signature AutocompletePlugin.onDownKeyDown(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.onDownKeyDown
+ */
+ p.onDownKeyDown = function(e)
+ {
+ var self = this;
+
+ if(self.isDropdownVisible())
+ self.toggleNextSuggestion();
+ else
+ self.renderSuggestions();
+ };
+
+ /**
+ * Reacts to the `upKeyDown` event triggered by the TextExt core.
+ *
+ * @signature AutocompletePlugin.onUpKeyDown(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.onUpKeyDown
+ */
+ p.onUpKeyDown = function(e)
+ {
+ this.togglePreviousSuggestion();
+ };
+
+ /**
+ * Reacts to the `enterKeyPress` event triggered by the TextExt core.
+ *
+ * @signature AutocompletePlugin.onEnterKeyPress(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.onEnterKeyPress
+ */
+ p.onEnterKeyPress = function(e)
+ {
+ var self = this;
+
+ if(self.isDropdownVisible())
+ self.selectFromDropdown();
+ else
+ self.invalidateData();
+ };
+
+ /**
+ * Reacts to the `escapeKeyPress` event triggered by the TextExt core. Hides the dropdown
+ * if it's currently visible.
+ *
+ * @signature AutocompletePlugin.onEscapeKeyPress(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.onEscapeKeyPress
+ */
+ p.onEscapeKeyPress = function(e)
+ {
+ var self = this;
+
+ if(self.isDropdownVisible())
+ self.hideDropdown();
+ };
+
+ //--------------------------------------------------------------------------------
+ // 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 AutocompletePlugin.positionDropdown()
+ *
+ * @author agorbatchev
+ * @date 2011/08/15
+ * @id AutocompletePlugin.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 AutocompletePlugin.suggestionElements()
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.suggestionElements
+ */
+ p.suggestionElements = function()
+ {
+ return this.containerElement().find(CSS_DOT_SUGGESTION);
+ };
+
+ /**
+ * Highlights specified suggestion as selected in the dropdown.
+ *
+ * @signature AutocompletePlugin.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 AutocompletePlugin.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 AutocompletePlugin.selectedSuggestionElement()
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.selectedSuggestionElement
+ */
+ p.selectedSuggestionElement = function()
+ {
+ return this.suggestionElements().filter(CSS_DOT_SELECTED).first();
+ };
+
+ /**
+ * Returns `true` if dropdown is currently visible, `false` otherwise.
+ *
+ * @signature AutocompletePlugin.isDropdownVisible()
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.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 AutocompletePlugin.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 AutocompletePlugin.onGetFormData
+ */
+ p.getFormData = function(callback)
+ {
+ var self = this,
+ itemManager = self.itemManager(),
+ inputValue = self.val(),
+ formValue
+ ;
+
+ itemManager.stringToItem(inputValue, function(err, item)
+ {
+ formValue = itemManager.serialize(item);
+ callback(null, formValue, inputValue);
+ });
+ };
+
+ p.dropdownItems = function()
+ {
+ return this.containerElement().find('.text-list').children();
+ };
+
+ /**
+ * Removes all HTML suggestion items from the dropdown.
+ *
+ * @signature AutocompletePlugin.clearItems()
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.clearItems
+ */
+ p.clearItems = function()
+ {
+ this.dropdownItems().remove();
+ };
+
+ /**
+ * Clears all and renders passed suggestions.
+ *
+ * @signature AutocompletePlugin.renderSuggestions(suggestions)
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.renderSuggestions
+ */
+ p.renderSuggestions = function()
+ {
+ var self = this,
+ filter = self.val(),
+ itemManager = self.itemManager(),
+ i
+ ;
+
+ if(self._lastValue !== filter)
+ {
+ // if user clears input, then we want to select first suggestion instead of the last one
+ if(filter === '')
+ current = null;
+
+ self._lastValue = filter;
+
+ itemManager.getSuggestions(filter, function(err, suggestions)
+ {
+ self.clearItems();
+
+ if(suggestions.length > 0)
+ {
+ itemManager.each(suggestions, function(err, item)
+ {
+ self.addSuggestion(item);
+ });
+
+ self.showDropdown();
+ }
+ else
+ {
+ self.hideDropdown();
+ }
+ });
+ }
+ };
+
+ /**
+ * Shows the dropdown.
+ *
+ * @signature AutocompletePlugin.showDropdown()
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.showDropdown
+ */
+ p.showDropdown = function()
+ {
+ var self = this,
+ current = self.selectedSuggestionElement().data(CSS_SUGGESTION)
+ ;
+
+ self.containerElement().show();
+
+ if(current)
+ self.setSelectedSuggestion(current);
+ else
+ self.toggleNextSuggestion();
+ };
+
+ /**
+ * Hides the dropdown.
+ *
+ * @signature AutocompletePlugin.hideDropdown()
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.hideDropdown
+ */
+ p.hideDropdown = function()
+ {
+ var self = this;
+
+ self._lastValue = null;
+ self.containerElement().hide();
+ };
+
+ /**
+ * Adds single suggestion to the bottom of the dropdown. Uses `ItemManager.itemToString()` to
+ * serialize provided suggestion to string.
+ *
+ * @signature AutocompletePlugin.addSuggestion(suggestion)
+ *
+ * @param suggestion {Object} Suggestion item. By default expected to be a string.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.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 AutocompletePlugin.addDropdownItem(html)
+ *
+ * @param html {String} HTML to be inserted into the item.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.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 AutocompletePlugin.clearSelected()
+ *
+ * @author agorbatchev
+ * @date 2011/08/02
+ * @id AutocompletePlugin.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 AutocompletePlugin.toggleNextSuggestion()
+ *
+ * @author agorbatchev
+ * @date 2011/08/02
+ * @id AutocompletePlugin.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 AutocompletePlugin.togglePreviousSuggestion()
+ *
+ * @author agorbatchev
+ * @date 2011/08/02
+ * @id AutocompletePlugin.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 AutocompletePlugin.scrollSuggestionIntoView(item)
+ *
+ * @param item {HTMLElement} jQuery HTML suggestion element which needs to
+ * scrolled into view.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.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 AutocompletePlugin.selectFromDropdown()
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id AutocompletePlugin.selectFromDropdown
+ */
+ p.selectFromDropdown = function()
+ {
+ var self = this,
+ suggestion = self.selectedSuggestionElement().data(CSS_SUGGESTION)
+ ;
+
+ if(suggestion)
+ {
+ self.val(self.itemManager().itemToString(suggestion));
+ self.invalidateData();
+ }
+
+ self.hideDropdown();
+ };
+
+ p.invalidateData = function()
+ {
+ var self = this;
+
+ self.itemValidator().isValid(self.val(), function(err, isValid)
+ {
+ if(isValid)
+ self.core().invalidateData();
+ });
+ };
+
+ /**
+ * Determines if the specified HTML element is within the TextExt core wrap HTML element.
+ *
+ * @signature AutocompletePlugin.withinWrapElement(element)
+ *
+ * @param element {HTMLElement} element to check if contained by wrap element
+ *
+ * @author adamayres
+ * @date 2012/01/15
+ * @id AutocompletePlugin.withinWrapElement
+ */
+ p.withinWrapElement = function(element)
+ {
+ return this.core().wrapElement().find(element).size() > 0;
+ }
})(jQuery);
diff --git a/src/js/textext.plugin.filter.js b/src/js/textext.plugin.filter.js
deleted file mode 100644
index 7f25b57..0000000
--- a/src/js/textext.plugin.filter.js
+++ /dev/null
@@ -1,242 +0,0 @@
-/**
- * jQuery TextExt Plugin
- * http://textextjs.com
- *
- * @version 1.3.0
- * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
- * @license MIT License
- */
-(function($)
-{
- /**
- * The Filter plugin introduces ability to limit input that the text field
- * will accept. If the Tags plugin is used, Filter plugin will limit which
- * tags it's possible to add.
- *
- * The list of allowed items can be either specified through the
- * options, can come from the Suggestions plugin or be loaded by the Ajax
- * plugin. All these plugins have one thing in common -- they
- * trigger `setSuggestions` event which the Filter plugin is expecting.
- *
- * @author agorbatchev
- * @date 2011/08/18
- * @id TextExtFilter
- */
- function TextExtFilter() {};
-
- $.fn.textext.TextExtFilter = TextExtFilter;
- $.fn.textext.addPlugin('filter', TextExtFilter);
-
- var p = TextExtFilter.prototype,
-
- /**
- * Filter plugin options are grouped under `filter` when passed to the
- * `$().textext()` function. For example:
- *
- * $('textarea').textext({
- * plugins: 'filter',
- * filter: {
- * items: [ "item1", "item2" ]
- * }
- * })
- *
- * @author agorbatchev
- * @date 2011/08/18
- * @id TextExtFilter.options
- */
-
- /**
- * This is a toggle switch to enable or disable the Filter plugin. The value is checked
- * each time at the top level which allows you to toggle this setting on the fly.
- *
- * @name filter.enabled
- * @default true
- * @author agorbatchev
- * @date 2011/08/18
- * @id TextExtFilter.options.enabled
- */
- OPT_ENABLED = 'filter.enabled',
-
- /**
- * Arra of items that the Filter plugin will allow the Tag plugin to add to the list of
- * its resut tags. Each item by default is expected to be a string which default `ItemManager`
- * can work with. You can change the item type by supplying custom `ItemManager`.
- *
- * @name filter.items
- * @default null
- * @author agorbatchev
- * @date 2011/08/18
- * @id TextExtFilter.options.items
- */
- OPT_ITEMS = 'filter.items',
-
- /**
- * Filter plugin dispatches and reacts to the following events.
- *
- * @author agorbatchev
- * @date 2011/08/18
- * @id TextExtFilter.events
- */
-
- /**
- * Filter plugin reacts to the `isTagAllowed` event triggered by the Tags plugin before
- * adding a new tag to the list. If the new tag is among the `items` specified in options,
- * then the new tag will be allowed.
- *
- * @name isTagAllowed
- * @author agorbatchev
- * @date 2011/08/18
- * @id TextExtFilter.events.isTagAllowed
- */
-
- /**
- * Filter plugin reacts to the `setSuggestions` event triggered by other plugins like
- * Suggestions and Ajax.
- *
- * However, event if this event is handled and items are passed with it and stored, if `items`
- * option was supplied, it will always take precedense.
- *
- * @name setSuggestions
- * @author agorbatchev
- * @date 2011/08/18
- * @id TextExtFilter.events.setSuggestions
- */
-
- DEFAULT_OPTS = {
- filter : {
- enabled : true,
- items : null
- }
- }
- ;
-
- /**
- * Initialization method called by the core during plugin instantiation.
- *
- * @signature TextExtFilter.init(core)
- *
- * @param core {TextExt} Instance of the TextExt core class.
- *
- * @author agorbatchev
- * @date 2011/08/18
- * @id TextExtFilter.init
- */
- p.init = function(core)
- {
- var self = this;
- self.baseInit(core, DEFAULT_OPTS);
-
- self.on({
- getFormData : self.onGetFormData,
- isTagAllowed : self.onIsTagAllowed,
- setSuggestions : self.onSetSuggestions
- });
-
- self._suggestions = null;
- };
-
- //--------------------------------------------------------------------------------
- // Core functionality
-
- /**
- * Reacts to the [`getFormData`][1] event triggered by the core. Returns data with the
- * weight of 200 to be *greater than the Autocomplete plugins* data weights.
- * The weights system is covered in greater detail in the [`getFormData`][1] event
- * documentation.
- *
- * This method does nothing if Tags tag is also present.
- *
- * [1]: /manual/textext.html#getformdata
- *
- * @signature TextExtFilter.onGetFormData(e, data, keyCode)
- *
- * @param e {Object} jQuery event.
- * @param data {Object} Data object to be populated.
- * @param keyCode {Number} Key code that triggered the original update request.
- *
- * @author agorbatchev
- * @date 2011/12/28
- * @id TextExtFilter.onGetFormData
- * @version 1.1
- */
- p.onGetFormData = function(e, data, keyCode)
- {
- var self = this,
- val = self.val(),
- inputValue = val,
- formValue = ''
- ;
-
- if(!self.core().hasPlugin('tags'))
- {
- if(self.isValueAllowed(inputValue))
- formValue = val;
-
- data[300] = self.formDataObject(inputValue, formValue);
- }
- };
-
- /**
- * Checks given value if it's present in `filterItems` or was loaded for the Autocomplete
- * or by the Suggestions plugins. `value` is compared to each item using `ItemManager.compareItems`
- * method which is currently attached to the core. Returns `true` if value is known or
- * Filter plugin is disabled.
- *
- * @signature TextExtFilter.isValueAllowed(value)
- *
- * @param value {Object} Value to check.
- *
- * @author agorbatchev
- * @date 2011/12/28
- * @id TextExtFilter.isValueAllowed
- * @version 1.1
- */
- p.isValueAllowed = function(value)
- {
- var self = this,
- list = self.opts('filterItems') || self._suggestions || [],
- itemManager = self.itemManager(),
- result = !self.opts(OPT_ENABLED), // if disabled, should just return true
- i
- ;
-
- for(i = 0; i < list.length && !result; i++)
- if(itemManager.compareItems(value, list[i]))
- result = true;
-
- return result;
- };
-
- /**
- * Handles `isTagAllowed` event dispatched by the Tags plugin. If supplied tag is not
- * in the `items` list, method sets `result` on the `data` argument to `false`.
- *
- * @signature TextExtFilter.onIsTagAllowed(e, data)
- *
- * @param e {Object} jQuery event.
- * @param data {Object} Payload in the following format : `{ tag : {Object}, result : {Boolean} }`.
- * @author agorbatchev
- * @date 2011/08/04
- * @id TextExtFilter.onIsTagAllowed
- */
- p.onIsTagAllowed = function(e, data)
- {
- data.result = this.isValueAllowed(data.tag);
- };
-
- /**
- * Reacts to the `setSuggestions` events and stores supplied suggestions for future use.
- *
- * @signature TextExtFilter.onSetSuggestions(e, data)
- *
- * @param e {Object} jQuery event.
- * @param data {Object} Payload in the following format : `{ result : {Array} } }`.
- * @author agorbatchev
- * @date 2011/08/18
- * @id TextExtFilter.onSetSuggestions
- */
- p.onSetSuggestions = function(e, data)
- {
- this._suggestions = data.result;
- };
-})(jQuery);
diff --git a/src/js/textext.plugin.focus.js b/src/js/textext.plugin.focus.js
index d6ef93e..ded1f4c 100644
--- a/src/js/textext.plugin.focus.js
+++ b/src/js/textext.plugin.focus.js
@@ -3,172 +3,172 @@
* http://textextjs.com
*
* @version 1.3.0
- * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @copyright Copyright (C) 2011-2012 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() {};
+ /**
+ * Focus plugin displays a visual effect whenever user sets focus
+ * into the text area.
+ *
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id FocusPlugin
+ */
+ function FocusPlugin() {};
- $.fn.textext.TextExtFocus = TextExtFocus;
- $.fn.textext.addPlugin('focus', TextExtFocus);
+ $.fn.textext.FocusPlugin = FocusPlugin;
+ $.fn.textext.addPlugin('focus', FocusPlugin);
- 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: ""
- * }
- * })
- *
- * @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 ''
- * @author agorbatchev
- * @date 2011/08/18
- * @id TextExtFocus.options.html.focus
- */
- OPT_HTML_FOCUS = 'html.focus',
+ var p = FocusPlugin.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: ""
+ * }
+ * })
+ *
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id FocusPlugin.options
+ */
+
+ /**
+ * HTML source that is used to generate markup required for the focus effect.
+ *
+ * @name html.focus
+ * @default ''
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id FocusPlugin.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 dispatches or reacts to the following events.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id FocusPlugin.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 `focus` event and shows the markup generated from
+ * the `html.focus` option.
+ *
+ * @name focus
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id FocusPlugin.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
- */
+ /**
+ * Focus plugin reacts to the `blur` event and hides the effect.
+ *
+ * @name blur
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id FocusPlugin.events.blur
+ */
- DEFAULT_OPTS = {
- html : {
- focus : ''
- }
- }
- ;
+ DEFAULT_OPTS = {
+ html : {
+ 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;
+ /**
+ * Initialization method called by the core during plugin instantiation.
+ *
+ * @signature FocusPlugin.init(core)
+ *
+ * @param core {TextExt} Instance of the TextExt core class.
+ *
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id FocusPlugin.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.baseInit(core, DEFAULT_OPTS);
+ self.core().wrapElement().append(self.opts(OPT_HTML_FOCUS));
+ self.on({
+ blur : self.onBlur,
+ focus : self.onFocus
+ });
- self._timeoutId = 0;
- };
+ 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;
+ //--------------------------------------------------------------------------------
+ // 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 FocusPlugin.onBlur(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/08
+ * @id FocusPlugin.onBlur
+ */
+ p.onBlur = function(e)
+ {
+ var self = this;
- clearTimeout(self._timeoutId);
+ clearTimeout(self._timeoutId);
- self._timeoutId = setTimeout(function()
- {
- self.getFocus().hide();
- },
- 100);
- };
+ 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;
+ /**
+ * Reacts to the `focus` event and shows the focus effect.
+ *
+ * @signature FocusPlugin.onFocus
+ *
+ * @param e {Object} jQuery event.
+ * @author agorbatchev
+ * @date 2011/08/08
+ * @id FocusPlugin.onFocus
+ */
+ p.onFocus = function(e)
+ {
+ var self = this;
- clearTimeout(self._timeoutId);
-
- self.getFocus().show();
- };
-
- //--------------------------------------------------------------------------------
- // Core functionality
+ 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');
- };
+ /**
+ * Returns focus effect HTML element.
+ *
+ * @signature FocusPlugin.getFocus()
+ *
+ * @author agorbatchev
+ * @date 2011/08/08
+ * @id FocusPlugin.getFocus
+ */
+ p.getFocus = function()
+ {
+ return this.core().wrapElement().find('.text-focus');
+ };
})(jQuery);
diff --git a/src/js/textext.plugin.js b/src/js/textext.plugin.js
new file mode 100644
index 0000000..22a1c6a
--- /dev/null
+++ b/src/js/textext.plugin.js
@@ -0,0 +1,265 @@
+/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.0
+ * @copyright Copyright (C) 2011-2012 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($, undefined)
+{
+ /**
+ * Plugin 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.*
+ *
+ * new in 1.2.0 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 Plugin
+ */
+ function Plugin() {};
+
+ var textext = $.fn.textext,
+ p = Plugin.prototype
+ ;
+
+ textext.Plugin = Plugin;
+
+ /**
+ * 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 on
+ */
+ p.on = textext.TextExt.prototype.on;
+
+ /**
+ * Initialization method called by the core during plugin instantiation. This method must be implemented
+ * by each plugin individually.
+ *
+ * @signature Plugin.init(core)
+ *
+ * @param core {TextExt} Instance of the TextExt core class.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id init
+ */
+ p.init = function(core)
+ {
+ throw new Error('Plugin must implement init() method');
+ };
+
+ /**
+ * Initialization method wich should be called by the plugin during the `init()` call.
+ *
+ * @signature Plugin.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 baseInit
+ */
+ p.baseInit = function(core, defaults)
+ {
+ var self = this;
+
+ self._core = core;
+ core.defaultOptions = $.extend(true, core.defaultOptions, defaults);
+ 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 Plugin.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 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 Plugin.stopTimer(name)
+ *
+ * @param name {String} Timer name.
+ *
+ * @author agorbatchev
+ * @date 2011/08/25
+ * @id 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 Plugin.core()
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id core
+ */
+ p.core = function()
+ {
+ return this._core;
+ };
+
+ /**
+ * Shortcut to the core's `opts()` method. Returns option value.
+ *
+ * @signature Plugin.opts(name)
+ *
+ * @param name {String} Option name as described in the options.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id 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 Plugin.itemManager()
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id itemManager
+ */
+ p.itemManager = function()
+ {
+ return this.core().itemManager();
+ };
+
+ p.itemValidator = function()
+ {
+ return this.core().itemValidator();
+ };
+
+ /**
+ * Shortcut to the core's `input()` method. Returns instance of the HTML element that represents
+ * current text input.
+ *
+ * @signature Plugin.input()
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id 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 Plugin.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 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 Plugin.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 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 Plugin.bind(event, handler)
+ *
+ * @param event {String} Event name.
+ * @param handler {Function} Event handler.
+ *
+ * @author agorbatchev
+ * @date 2011/08/20
+ * @id bind
+ */
+ p.bind = function(event, handler)
+ {
+ this.core().bind(event, handler);
+ };
+})(jQuery);
+
diff --git a/src/js/textext.plugin.prompt.js b/src/js/textext.plugin.prompt.js
index f25831c..ad77765 100644
--- a/src/js/textext.plugin.prompt.js
+++ b/src/js/textext.plugin.prompt.js
@@ -3,307 +3,307 @@
* http://textextjs.com
*
* @version 1.3.0
- * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @copyright Copyright (C) 2011-2012 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() {};
+ /**
+ * 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 PromptPlugin
+ */
+ function PromptPlugin() {};
- $.fn.textext.TextExtPrompt = TextExtPrompt;
- $.fn.textext.addPlugin('prompt', TextExtPrompt);
+ $.fn.textext.PromptPlugin = PromptPlugin;
+ $.fn.textext.addPlugin('prompt', PromptPlugin);
- var p = TextExtPrompt.prototype,
+ var p = PromptPlugin.prototype,
- CSS_HIDE_PROMPT = 'text-hide-prompt',
+ 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 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 PromptPlugin.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',
+ /**
+ * 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 PromptPlugin.options.prompt
+ */
+ OPT_PROMPT = 'prompt',
- /**
- * HTML source that is used to generate markup required for the prompt effect.
- *
- * @name html.prompt
- * @default ''
- * @author agorbatchev
- * @date 2011/08/18
- * @id TextExtPrompt.options.html.prompt
- */
- OPT_HTML_PROMPT = 'html.prompt',
+ /**
+ * HTML source that is used to generate markup required for the prompt effect.
+ *
+ * @name html.prompt
+ * @default ''
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id PromptPlugin.options.html.prompt
+ */
+ OPT_HTML_PROMPT = 'html.prompt',
- /**
- * Prompt plugin dispatches or reacts to the following events.
- * @id TextExtPrompt.events
- */
+ /**
+ * Prompt plugin dispatches or reacts to the following events.
+ * @id PromptPlugin.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 `focus` event and hides the markup generated from
+ * the `html.prompt` option.
+ *
+ * @name focus
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id PromptPlugin.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...',
+ /**
+ * 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 PromptPlugin.events.blur
+ */
+
+ DEFAULT_OPTS = {
+ prompt : 'Awaiting input...',
- html : {
- prompt : ''
- }
- }
- ;
+ html : {
+ 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
- ;
+ /**
+ * Initialization method called by the core during plugin instantiation.
+ *
+ * @signature PromptPlugin.init(core)
+ *
+ * @param core {TextExt} Instance of the TextExt core class.
+ *
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id PromptPlugin.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.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);
+ self.core().wrapElement().append(container);
+ self.setPrompt(self.opts(OPT_PROMPT));
+
+ prompt = core.input().attr(placeholderKey);
- if(!prompt)
- prompt = self.opts(OPT_PROMPT);
+ if(!prompt)
+ prompt = self.opts(OPT_PROMPT);
- // clear placeholder attribute if set
- core.input().attr(placeholderKey, '');
+ // clear placeholder attribute if set
+ core.input().attr(placeholderKey, '');
- if(prompt)
- self.setPrompt(prompt);
+ if(prompt)
+ self.setPrompt(prompt);
- if($.trim(self.val()).length > 0)
- self.hidePrompt();
+ if($.trim(self.val()).length > 0)
+ self.hidePrompt();
- self.on({
- blur : self.onBlur,
- focus : self.onFocus,
- postInvalidate : self.onPostInvalidate,
- postInit : self.onPostInit
- });
- };
+ 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();
- };
+ //--------------------------------------------------------------------------------
+ // Event handlers
+
+ /**
+ * Reacts to the `postInit` and configures the plugin for initial display.
+ *
+ * @signature PromptPlugin.onPostInit(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/24
+ * @id PromptPlugin.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();
- };
+ /**
+ * Reacts to the `postInvalidate` and insures that prompt display remains correct.
+ *
+ * @signature PromptPlugin.onPostInvalidate(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/24
+ * @id PromptPlugin.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()
- ;
+ /**
+ * Repositions the prompt to make sure it's always at the same place as in the text input carret.
+ *
+ * @signature PromptPlugin.invalidateBounds()
+ *
+ * @author agorbatchev
+ * @date 2011/08/24
+ * @id PromptPlugin.invalidateBounds
+ */
+ p.invalidateBounds = function()
+ {
+ var self = this,
+ input = self.input()
+ ;
- self.containerElement().css({
- paddingLeft : input.css('paddingLeft'),
- paddingTop : input.css('paddingTop')
- });
- };
+ 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;
+ /**
+ * 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 PromptPlugin.onBlur(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/08
+ * @id PromptPlugin.onBlur
+ */
+ p.onBlur = function(e)
+ {
+ var self = this;
- self.startTimer('prompt', 0.1, function()
- {
- self.showPrompt();
- });
- };
+ 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);
- };
+ /**
+ * Shows prompt HTML element.
+ *
+ * @signature PromptPlugin.showPrompt()
+ *
+ * @author agorbatchev
+ * @date 2011/08/22
+ * @id PromptPlugin.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);
- };
+ /**
+ * Hides prompt HTML element.
+ *
+ * @signature PromptPlugin.hidePrompt()
+ *
+ * @author agorbatchev
+ * @date 2011/08/22
+ * @id PromptPlugin.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
+ /**
+ * Reacts to the `focus` event and hides the prompt effect.
+ *
+ * @signature PromptPlugin.onFocus
+ *
+ * @param e {Object} jQuery event.
+ * @author agorbatchev
+ * @date 2011/08/08
+ * @id PromptPlugin.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);
- };
+ /**
+ * Sets the prompt display to the specified string.
+ *
+ * @signature PromptPlugin.setPrompt(str)
+ *
+ * @oaram str {String} String that will be displayed in the prompt.
+ *
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id PromptPlugin.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');
- };
+ /**
+ * Returns prompt effect HTML element.
+ *
+ * @signature PromptPlugin.containerElement()
+ *
+ * @author agorbatchev
+ * @date 2011/08/08
+ * @id PromptPlugin.containerElement
+ */
+ p.containerElement = function()
+ {
+ return $(this).data('container');
+ };
})(jQuery);
diff --git a/src/js/textext.plugin.suggestions.js b/src/js/textext.plugin.suggestions.js
deleted file mode 100644
index 9573f91..0000000
--- a/src/js/textext.plugin.suggestions.js
+++ /dev/null
@@ -1,175 +0,0 @@
-/**
- * jQuery TextExt Plugin
- * http://textextjs.com
- *
- * @version 1.3.0
- * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
- * @license MIT License
- */
-(function($)
-{
- /**
- * Suggestions plugin allows to easily specify the list of suggestion items that the
- * Autocomplete plugin would present to the user.
- *
- * @author agorbatchev
- * @date 2011/08/18
- * @id TextExtSuggestions
- */
- function TextExtSuggestions() {};
-
- $.fn.textext.TextExtSuggestions = TextExtSuggestions;
- $.fn.textext.addPlugin('suggestions', TextExtSuggestions);
-
- var p = TextExtSuggestions.prototype,
- /**
- * Suggestions plugin only has one option and that is to set suggestion items. It could be
- * changed when passed to the `$().textext()` function. For example:
- *
- * $('textarea').textext({
- * plugins: 'suggestions',
- * suggestions: [ "item1", "item2" ]
- * })
- *
- * @author agorbatchev
- * @date 2011/08/18
- * @id TextExtSuggestions.options
- */
-
- /**
- * List of items that Autocomplete plugin would display in the dropdown.
- *
- * @name suggestions
- * @default null
- * @author agorbatchev
- * @date 2011/08/18
- * @id TextExtSuggestions.options.suggestions
- */
- OPT_SUGGESTIONS = 'suggestions',
-
- /**
- * Suggestions plugin dispatches or reacts to the following events.
- *
- * @author agorbatchev
- * @date 2011/08/17
- * @id TextExtSuggestions.events
- */
-
- /**
- * Suggestions plugin reacts to the `getSuggestions` event and returns `suggestions` items
- * from the options.
- *
- * @name getSuggestions
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExtSuggestions.events.getSuggestions
- */
-
- /**
- * Suggestions plugin triggers the `setSuggestions` event to pass its own list of `Suggestions`
- * to the Autocomplete plugin.
- *
- * @name setSuggestions
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExtSuggestions.events.setSuggestions
- */
-
- /**
- * Suggestions plugin reacts to the `postInit` event to pass its list of `suggestions` to the
- * Autocomplete right away.
- *
- * @name postInit
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExtSuggestions.events.postInit
- */
-
- DEFAULT_OPTS = {
- suggestions : null
- }
- ;
-
- /**
- * Initialization method called by the core during plugin instantiation.
- *
- * @signature TextExtSuggestions.init(core)
- *
- * @param core {TextExt} Instance of the TextExt core class.
- *
- * @author agorbatchev
- * @date 2011/08/18
- * @id TextExtSuggestions.init
- */
- p.init = function(core)
- {
- var self = this;
-
- self.baseInit(core, DEFAULT_OPTS);
-
- self.on({
- getSuggestions : self.onGetSuggestions,
- postInit : self.onPostInit
- });
- };
-
- /**
- * Triggers `setSuggestions` and passes supplied suggestions to the Autocomplete plugin.
- *
- * @signature TextExtSuggestions.setSuggestions(suggestions, showHideDropdown)
- *
- * @param suggestions {Array} List of suggestions. With the default `ItemManager` it should
- * be a list of strings.
- * @param showHideDropdown {Boolean} If it's undesirable to show the dropdown right after
- * suggestions are set, `false` should be passed for this argument.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExtSuggestions.setSuggestions
- */
- p.setSuggestions = function(suggestions, showHideDropdown)
- {
- this.trigger('setSuggestions', { result : suggestions, showHideDropdown : showHideDropdown != false });
- };
-
- /**
- * Reacts to the `postInit` event and triggers `setSuggestions` event to set suggestions list
- * right after initialization.
- *
- * @signature TextExtSuggestions.onPostInit(e)
- *
- * @param e {Object} jQuery event.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExtSuggestions.onPostInit
- */
- p.onPostInit = function(e)
- {
- var self = this;
- self.setSuggestions(self.opts(OPT_SUGGESTIONS), false);
- };
-
- /**
- * Reacts to the `getSuggestions` event and triggers `setSuggestions` event with the list
- * of `suggestions` specified in the options.
- *
- * @signature TextExtSuggestions.onGetSuggestions(e, data)
- *
- * @param e {Object} jQuery event.
- * @param data {Object} Payload from the `getSuggestions` event with the user query, eg `{ query: {String} }`.
- *
- * @author agorbatchev
- * @date 2011/08/19
- * @id TextExtSuggestions.onGetSuggestions
- */
- p.onGetSuggestions = function(e, data)
- {
- var self = this,
- suggestions = self.opts(OPT_SUGGESTIONS)
- ;
-
- suggestions.sort();
- self.setSuggestions(self.itemManager().filter(suggestions, data.query));
- };
-})(jQuery);
diff --git a/src/js/textext.plugin.tags.js b/src/js/textext.plugin.tags.js
index 47ebf9d..acbb030 100644
--- a/src/js/textext.plugin.tags.js
+++ b/src/js/textext.plugin.tags.js
@@ -3,689 +3,702 @@
* http://textextjs.com
*
* @version 1.3.0
- * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @copyright Copyright (C) 2011-2012 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 ''
- * @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 '
'
- * @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 : '',
- tag : '
'
- }
- }
- ;
-
- /**
- * 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;
- };
+ /**
+ * 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 TagsPlugin
+ */
+ function TagsPlugin() {};
+
+ $.fn.textext.TagsPlugin = TagsPlugin;
+ $.fn.textext.addPlugin('tags', TagsPlugin);
+
+ var p = TagsPlugin.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 TagsPlugin.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 TagsPlugin.options.tags.enabled
+ */
+ OPT_ENABLED = 'tags.enabled',
+
+ OPT_HOT_KEY = 'tags.hotKey',
+
+ /**
+ * 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 TagsPlugin.options.tags.items
+ */
+ OPT_ITEMS = 'tags.items',
+
+ /**
+ * @author agorbatchev
+ * @date 2012/08/06
+ */
+ OPT_ALLOW_DUPLICATES = 'tags.allowDuplicates',
+
+ /**
+ * HTML source that is used to generate a single tag.
+ *
+ * @name html.tag
+ * @default ''
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TagsPlugin.options.html.tag
+ */
+ OPT_HTML_TAG = 'html.tag',
+
+ /**
+ * HTML source that is used to generate container for the tags.
+ *
+ * @name html.tags
+ * @default '
'
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TagsPlugin.options.html.tags
+ */
+ OPT_HTML_TAGS = 'html.tags',
+
+ /**
+ * Tags plugin dispatches or reacts to the following events.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TagsPlugin.events
+ */
+
+ /**
+ * 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
+ * @author s.stok
+ * @date 2011/01/23
+ * @id TagsPlugin.events.tagClick
+ */
+ EVENT_TAG_CLICK = 'tagClick',
+
+ EVENT_TAG_REMOVE = 'tagRemove',
+
+ EVENT_TAG_ADD = 'tagAdd',
+
+ DEFAULT_OPTS = {
+ tags : {
+ enabled : true,
+ items : null,
+ allowDuplicates : true,
+ hotKey : 13
+ },
+
+ html : {
+ tags : '',
+ tag : '
'
+ }
+ }
+ ;
+
+ /**
+ * Initialization method called by the core during plugin instantiation.
+ *
+ * @signature TagsPlugin.init(core)
+ *
+ * @param core {TextExt} Instance of the TextExt core class.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TagsPlugin.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({
+ backspaceKeyDown : self.onBackspaceKeyDown,
+ preInvalidate : self.onPreInvalidate,
+ postInit : self.onPostInit,
+ anyKeyPress : self.onAnyKeyPress
+ });
+
+ self.on(container, {
+ click : self.onClick,
+ mousemove : self.onContainerMouseMove
+ });
+
+ self.on(input, {
+ mousemove : self.onInputMouseMove
+ });
+
+ self._hotKey = self.opts(OPT_HOT_KEY);
+
+ self._originalPadding = {
+ left : parseInt(input.css('paddingLeft') || 0),
+ top : parseInt(input.css('paddingTop') || 0)
+ };
+
+ self._paddingBox = {
+ left : 0,
+ top : 0
+ };
+ }
+ };
+
+ /**
+ * Returns HTML element in which all tag HTML elements are residing.
+ *
+ * @signature TagsPlugin.containerElement()
+ *
+ * @author agorbatchev
+ * @date 2011/08/15
+ * @id TagsPlugin.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 TagsPlugin.onPostInit(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/09
+ * @id TagsPlugin.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 TagsPlugin.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 TagsPlugin.onGetFormData
+ */
+ p.getFormData = function(callback)
+ {
+ var self = this,
+ inputValue = self.val(),
+ tags = self.getTags(),
+ formValue = self.itemManager().serialize(tags)
+ ;
+
+ callback(null, formValue, inputValue);
+ };
+
+ /**
+ * 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 TagsPlugin.onInputMouseMove(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/08
+ * @id TagsPlugin.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 TagsPlugin.onContainerMouseMove(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/08
+ * @id TagsPlugin.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 TagsPlugin.onBackspaceKeyDown(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/02
+ * @id TagsPlugin.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 TagsPlugin.onPreInvalidate(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TagsPlugin.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 TagsPlugin.onClick(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TagsPlugin.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));
+
+ core.invalidateData();
+ core.invalidateBounds();
+
+ if(refocus)
+ core.focusInput();
+ }
+
+ if(focus)
+ core.focusInput();
+ };
+
+ /**
+ * 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.onAnyKeyPress(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TagsPlugin.onAnyKeyPress
+ */
+ p.onAnyKeyPress = function(e, keyCode)
+ {
+ var self = this,
+ core = self.core(),
+ val
+ ;
+
+ if(self._hotKey === keyCode)
+ {
+ val = self.val();
+
+ if(val && val.length > 0)
+ {
+ self.itemManager().stringToItem(self.val(), function(err, item)
+ {
+ self.itemValidator().isValid(item, function(err, isValid)
+ {
+ if(isValid)
+ {
+ self.val('');
+ self.addTags([ item ]);
+ // refocus the textarea just in case it lost the focus
+ core.focusInput();
+ core.invalidateData();
+ }
+ });
+ });
+ }
+ }
+ };
+
+ //--------------------------------------------------------------------------------
+ // Core functionality
+
+ /**
+ * @author agorbatchev
+ * @date 2012/08/06
+ */
+ p.hasTag = function(tag)
+ {
+ var self = this,
+ elements = this.tagElements(),
+ itemManager = self.core().itemManager(),
+ item,
+ i
+ ;
+
+ for(i = 0; i < elements.length; i++)
+ {
+ item = $(elements[i]).data(CSS_TAG);
+
+ if(itemManager.compareItems(item, tag))
+ return true;
+ }
+
+ return false;
+ };
+
+ /**
+ * Creates a cache object with all the tags currently added which will be returned
+ * in the `onGetFormData` handler.
+ *
+ * @signature TagsPlugin.updateFromTags()
+ *
+ * @author agorbatchev
+ * @date 2011/08/09
+ * @id TagsPlugin.updateFromTags
+ */
+ p.getTags = function()
+ {
+ var self = this,
+ result = []
+ ;
+
+ self.tagElements().each(function()
+ {
+ result.push($(this).data(CSS_TAG));
+ });
+
+ return 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 TagsPlugin.toggleZIndex(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/08
+ * @id TagsPlugin.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 TagsPlugin.tagElements()
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TagsPlugin.tagElements
+ */
+ p.tagElements = function()
+ {
+ return this.containerElement().find(CSS_DOT_TAG);
+ };
+
+ /**
+ * 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 TagsPlugin.addTags(tags)
+ *
+ * @param tags {Array} List of tags that current `ItemManager` can understand. Default
+ * is `String`.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TagsPlugin.addTags
+ */
+ p.addTags = function(tags)
+ {
+ if(!tags || tags.length == 0)
+ return;
+
+ var self = this,
+ core = self.core(),
+ container = self.containerElement(),
+ allowDuplicates = self.opts(OPT_ALLOW_DUPLICATES),
+ nodes = [],
+ node,
+ i,
+ tag
+ ;
+
+ for(i = 0; i < tags.length; i++)
+ {
+ tag = tags[i];
+
+ if(allowDuplicates || !self.hasTag(tag))
+ {
+ node = self.renderTag(tag);
+
+ container.append(node);
+ nodes.push(node);
+ }
+ }
+
+ // only trigger events and invalidate if at least one tag was added
+ if(nodes.length)
+ {
+ core.invalidateData();
+ core.invalidateBounds();
+ self.trigger(EVENT_TAG_ADD, nodes, tags);
+ }
+ };
+
+ /**
+ * Returns HTML element for the specified tag.
+ *
+ * @signature TagsPlugin.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 TagsPlugin.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 TagsPlugin.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 TagsPlugin.removeTag
+ */
+ p.removeTag = function(tag)
+ {
+ var self = this,
+ core = self.core(),
+ element,
+ item
+ ;
+
+ if(tag instanceof $)
+ {
+ element = tag;
+ tag = tag.data(CSS_TAG);
+ }
+ else
+ {
+ element = self.getTagElement(tag);
+ }
+
+ item = element.data(CSS_TAG);
+
+ element.remove();
+ core.invalidateData();
+ core.invalidateBounds();
+
+ self.trigger(EVENT_TAG_REMOVE, item);
+ };
+
+ /**
+ * Creates and returns new HTML element from the source code specified in the `html.tag` option.
+ *
+ * @signature TagsPlugin.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 TagsPlugin.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);
diff --git a/src/less/_common.less b/src/less/_common.less
new file mode 100644
index 0000000..6b1edf1
--- /dev/null
+++ b/src/less/_common.less
@@ -0,0 +1,21 @@
+@border : #9DACCC;
+@selected : #6D84B4;
+@font : 11px "lucida grande",tahoma,verdana,arial,sans-serif;
+
+.border_box() {
+ -webkit-box-sizing : border-box;
+ -moz-box-sizing : border-box;
+ box-sizing : border-box;
+}
+
+.shadow(@_) {
+ -webkit-box-shadow : @arguments;
+ -moz-box-shadow : @arguments;
+ box-shadow : @arguments;
+}
+
+.round_corners(@_) {
+ -webkit-border-radius : @arguments;
+ -moz-border-radius : @arguments;
+ border-radius : @arguments;
+}
diff --git a/src/less/textext.itemmanager.ajax.less b/src/less/textext.itemmanager.ajax.less
new file mode 100644
index 0000000..c9be94d
--- /dev/null
+++ b/src/less/textext.itemmanager.ajax.less
@@ -0,0 +1,9 @@
+@import '_common';
+
+.text-core .text-wrap {
+ textarea, input {
+ &.text-loading {
+ background : url(loading.gif) 99% 50% no-repeat;
+ }
+ }
+}
diff --git a/src/less/textext.less b/src/less/textext.less
new file mode 100644
index 0000000..d89e1c3
--- /dev/null
+++ b/src/less/textext.less
@@ -0,0 +1,28 @@
+@import '_common';
+
+.text-core {
+ position : relative;
+
+ .text-wrap {
+ position : absolute;
+ background : #fff;
+
+ textarea, input {
+ .border_box;
+ .round_corners(0px);
+ border : 1px solid @border;
+ outline : none;
+ resize : none;
+ position : absolute;
+ z-index : 1;
+ background : none;
+ overflow : hidden;
+ margin : 0;
+ padding : 3px 5px 4px 5px;
+ white-space : nowrap;
+ font : @font;
+ line-height : 13px;
+ height : auto;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/less/textext.plugin.arrow.less b/src/less/textext.plugin.arrow.less
new file mode 100644
index 0000000..7a57c5e
--- /dev/null
+++ b/src/less/textext.plugin.arrow.less
@@ -0,0 +1,13 @@
+@import '_common';
+
+.text-core .text-wrap .text-arrow {
+ .border_box;
+ position : absolute;
+ top : 0;
+ right : 0;
+ width : 22px;
+ height : 22px;
+ background : url(arrow.png) 50% 50% no-repeat;
+ cursor : pointer;
+ z-index : 2;
+}
diff --git a/src/less/textext.plugin.autocomplete.less b/src/less/textext.plugin.autocomplete.less
new file mode 100644
index 0000000..aff16f9
--- /dev/null
+++ b/src/less/textext.plugin.autocomplete.less
@@ -0,0 +1,42 @@
+@import '_common';
+
+.text-core .text-wrap .text-dropdown {
+ .border_box;
+ padding : 0;
+ position : absolute;
+ z-index : 3;
+ background : #fff;
+ border : 1px solid @border;
+ width : 100%;
+ max-height : 100px;
+ padding : 1px;
+ font : @font;
+ display : none;
+ overflow-x : hidden;
+ overflow-y : auto;
+
+ &.text-position-below {
+ margin-top: 1px;
+ }
+
+ &.text-position-above {
+ margin-bottom: 1px;
+ }
+
+ .text-list {
+ .text-suggestion {
+ padding : 3px 5px;
+ cursor : pointer;
+
+ em {
+ font-style : normal;
+ text-decoration : underline;
+ }
+
+ &.text-selected {
+ color : #fff;
+ background : @selected;
+ }
+ }
+ }
+}
diff --git a/src/less/textext.plugin.focus.less b/src/less/textext.plugin.focus.less
new file mode 100644
index 0000000..f5bfb7a
--- /dev/null
+++ b/src/less/textext.plugin.focus.less
@@ -0,0 +1,13 @@
+@import '_common';
+
+.text-core .text-wrap .text-focus {
+ .shadow(0px 0px 6px @selected);
+ position : absolute;
+ width : 100%
+ height : 100%
+ display : none;
+
+ &.text-show-focus {
+ display : block;
+ }
+}
diff --git a/src/less/textext.plugin.prompt.less b/src/less/textext.plugin.prompt.less
new file mode 100644
index 0000000..e29c631
--- /dev/null
+++ b/src/less/textext.plugin.prompt.less
@@ -0,0 +1,17 @@
+@import '_common';
+
+.text-core .text-wrap .text-prompt {
+ .border_box;
+ position : absolute;
+ width : 100%
+ height : 100%
+ margin : 1px 0 0 2px;
+ font : @font;
+ color : silver;
+ overflow : hidden;
+ white-space : pre;
+
+ &.text-hide-prompt {
+ display : none;
+ }
+}
diff --git a/src/less/textext.plugin.tags.less b/src/less/textext.plugin.tags.less
new file mode 100644
index 0000000..fe5c4db
--- /dev/null
+++ b/src/less/textext.plugin.tags.less
@@ -0,0 +1,51 @@
+@import '_common';
+
+.text-core .text-wrap .text-tags {
+ .border_box;
+ position : absolute;
+ width : 100%;
+ height : 100%;
+ padding : 3px 35px 3px 3px;
+ cursor : text;
+
+ &.text-tags-on-top {
+ z-index : 2;
+ }
+
+ .text-tag {
+ float : left;
+
+ .text-button {
+ .round_corners(2px);
+ .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 : @font;
+
+ a.text-remove {
+ position : absolute;
+ right : 3px;
+ top : 2px;
+ display : block;
+ width : 11px;
+ height : 11px;
+ background : url('close.png') 0 0 no-repeat;
+
+ &:hover {
+ background-position: 0 -11px;
+ }
+
+ &:active {
+ background-position: 0 -22px;
+ }
+ }
+ }
+ }
+}
diff --git a/src/stylus/_common.styl b/src/stylus/_common.styl
deleted file mode 100644
index 4f2f095..0000000
--- a/src/stylus/_common.styl
+++ /dev/null
@@ -1,20 +0,0 @@
-$close = 'close.png'
-$border = #9DACCC
-$selected = #6D84B4
-$prefix = 'text-'
-$font = 11px "lucida grande",tahoma,verdana,arial,sans-serif
-
-vendor(prop, args)
- -webkit-{prop} : args
- -moz-{prop} : args
- {prop} : args
-
-border_box()
- vendor('box-sizing', border-box)
-
-shadow(args...)
- vendor('box-shadow', args)
-
-round_corners(args...)
- vendor('border-radius', args)
-
diff --git a/src/stylus/textext.core.styl b/src/stylus/textext.core.styl
deleted file mode 100644
index 265ae39..0000000
--- a/src/stylus/textext.core.styl
+++ /dev/null
@@ -1,26 +0,0 @@
-@import '_common'
-
-.{$prefix}core
- position : relative
-
- .{$prefix}wrap
- position : absolute
- background : #fff
-
- textarea, input
- border_box()
- round_corners(0px)
- border : 1px solid $border
- outline : none
- resize : none
- position : absolute
- z-index : 1
- background : none
- overflow : hidden
- margin : 0
- padding : 3px 5px 4px 5px
- white-space : nowrap
- font : $font
- line-height : 13px
- height : auto
-
diff --git a/src/stylus/textext.plugin.arrow.styl b/src/stylus/textext.plugin.arrow.styl
deleted file mode 100644
index 8ece444..0000000
--- a/src/stylus/textext.plugin.arrow.styl
+++ /dev/null
@@ -1,14 +0,0 @@
-@import '_common'
-
-.{$prefix}core .{$prefix}wrap
- .{$prefix}arrow
- border_box()
- position : absolute
- top : 0
- right : 0
- width : 22px
- height : 22px
- background : url(arrow.png) 50% 50% no-repeat;
- cursor : pointer
- z-index : 2
-
diff --git a/src/stylus/textext.plugin.autocomplete.styl b/src/stylus/textext.plugin.autocomplete.styl
deleted file mode 100644
index 4008701..0000000
--- a/src/stylus/textext.plugin.autocomplete.styl
+++ /dev/null
@@ -1,36 +0,0 @@
-@import '_common'
-
-.{$prefix}core .{$prefix}wrap .{$prefix}dropdown
- border_box()
- padding : 0
- position : absolute
- z-index : 3
- background : #fff
- border : 1px solid $border
- width : 100%
- max-height : 100px
- padding : 1px
- font : $font
- display : none
- overflow-x : hidden
- overflow-y : auto
-
- &.{$prefix}position-below
- margin-top: 1px;
-
- &.{$prefix}position-above
- margin-bottom: 1px;
-
- .{$prefix}list
- .{$prefix}suggestion
- padding : 3px 5px
- cursor : pointer
-
- em
- font-style : normal
- text-decoration : underline
-
- &.{$prefix}selected
- color : #fff
- background : $selected
-
diff --git a/src/stylus/textext.plugin.focus.styl b/src/stylus/textext.plugin.focus.styl
deleted file mode 100644
index 64f894f..0000000
--- a/src/stylus/textext.plugin.focus.styl
+++ /dev/null
@@ -1,13 +0,0 @@
-@import '_common'
-
-.{$prefix}core .{$prefix}wrap
- .{$prefix}focus
- vendor('box-shadow', 0px 0px 6px $selected)
- position : absolute
- width : 100%
- height : 100%
- display : none
-
- &.{$prefix}show-focus
- display : block
-
diff --git a/src/stylus/textext.plugin.prompt.styl b/src/stylus/textext.plugin.prompt.styl
deleted file mode 100644
index 89bf2de..0000000
--- a/src/stylus/textext.plugin.prompt.styl
+++ /dev/null
@@ -1,17 +0,0 @@
-@import '_common'
-
-.{$prefix}core .{$prefix}wrap
- .{$prefix}prompt
- border_box()
- position : absolute
- width : 100%
- height : 100%
- margin : 1px 0 0 2px
- font : $font
- color : silver
- overflow : hidden
- white-space : pre
-
- &.{$prefix}hide-prompt
- display : none
-
diff --git a/src/stylus/textext.plugin.tags.styl b/src/stylus/textext.plugin.tags.styl
deleted file mode 100644
index a0d72e3..0000000
--- a/src/stylus/textext.plugin.tags.styl
+++ /dev/null
@@ -1,45 +0,0 @@
-@import '_common'
-
-.{$prefix}core .{$prefix}wrap .{$prefix}tags
- border_box()
- position : absolute
- width : 100%
- height : 100%
- padding : 3px 35px 3px 3px
- cursor : text
-
- &.{$prefix}tags-on-top
- z-index : 2
-
- .{$prefix}tag
- float : left
-
- .{$prefix}button
- round_corners(2px)
- 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 : $font
-
- a.{$prefix}remove
- position : absolute
- right : 3px
- top : 2px
- display : block
- width : 11px
- height : 11px
- background : url($close) 0 0 no-repeat
-
- &:hover
- background-position: 0 -11px
-
- &:active
- background-position: 0 -22px
-
diff --git a/tests/common.js b/tests/common.js
index b02a576..95e80eb 100644
--- a/tests/common.js
+++ b/tests/common.js
@@ -4,7 +4,8 @@ var prefix = 'css=.text-core > .text-wrap > ',
focus = prefix + '.text-focus',
textarea = prefix + 'textarea',
dropdown = prefix + '.text-dropdown',
- prompt = prefix + '.text-prompt'
+ prompt = prefix + '.text-prompt',
+ arrow = prefix + '.text-arrow'
;
var DOWN = 40,
@@ -61,7 +62,7 @@ function suggestionsXPath(selected, index)
function assertSuggestionItem(test)
{
- return function(browser) { browser.assertVisible(suggestionsXPath() + '//span[text()="Basic"]') };
+ return function(browser) { browser.assertVisible(suggestionsXPath() + '//span[text()="' + test + '"]') };
};
function assertOutput(value)
@@ -176,8 +177,6 @@ function testAjaxFunctionality()
function testArrowFunctionality()
{
- var arrow = prefix + '.text-arrow';
-
return function(browser)
{
browser
@@ -194,7 +193,6 @@ function testArrowFunctionality()
.waitForNotVisible(dropdown)
.and(assertOutput('Basic'))
.assertValue(textarea, 'Basic')
- .assertNotVisible(prompt)
;
};
};
@@ -410,7 +408,8 @@ module.exports = {
css : {
focus : focus,
textarea : textarea,
- dropdown : dropdown
+ dropdown : dropdown,
+ arrow : arrow
}
};
diff --git a/tests/firefox_profile/.parentlock b/tests/firefox_profile/.parentlock
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/firefox_profile/compatibility.ini b/tests/firefox_profile/compatibility.ini
deleted file mode 100644
index acf9f1e..0000000
--- a/tests/firefox_profile/compatibility.ini
+++ /dev/null
@@ -1,7 +0,0 @@
-[Compatibility]
-LastVersion=9.0.1_20111220165912/20111220165912
-LastOSABI=Darwin_x86_64-gcc3
-LastPlatformDir=/Applications/Firefox 9.0.1.app/Contents/MacOS
-LastAppDir=/Applications/Firefox 9.0.1.app/Contents/MacOS
-
-InvalidateCaches=1
diff --git a/tests/firefox_profile/extensions.ini b/tests/firefox_profile/extensions.ini
deleted file mode 100644
index 846a2af..0000000
--- a/tests/firefox_profile/extensions.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[ExtensionDirs]
-
-[ThemeDirs]
-Extension0=/Applications/Firefox 9.0.1.app/Contents/MacOS/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}
diff --git a/tests/firefox_profile/localstore.rdf b/tests/firefox_profile/localstore.rdf
deleted file mode 100644
index 5bcf788..0000000
--- a/tests/firefox_profile/localstore.rdf
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/tests/firefox_profile/pluginreg.dat b/tests/firefox_profile/pluginreg.dat
deleted file mode 100644
index b604682..0000000
--- a/tests/firefox_profile/pluginreg.dat
+++ /dev/null
@@ -1,190 +0,0 @@
-Generated File. Do not edit.
-
-[HEADER]
-Version:0.15:$
-Arch:x86_64-gcc3:$
-
-[PLUGINS]
-net.juniper.DSSafariExtensions.plugin:$
-/Library/Internet Plug-Ins/net.juniper.DSSafariExtensions.plugin:$
-19243:$
-1321897505000:0:1:$
-Juniper Networks Safari Extensions:$
-Juniper Networks Safari Extensions:$
-1
-0:application/x-net-juniper-dssafariextensions:Juniper Networks Extension Type::$
-AmazonMP3DownloaderPlugin.plugin:$
-/Applications/Amazon MP3 Downloader.app/Contents/Resources/AmazonMP3DownloaderPlugin.plugin:$
-AmazonMP3DownloaderPlugin 1.0.15:$
-1321560836000:0:1:$
-AmazonMP3DownloaderPlugin 1.0.15:$
-AmazonMP3DownloaderPlugin:$
-1
-0:application/x-amz:AmazonMP3DownloaderPlugin 1.0.15::$
-Flash Player.plugin:$
-/Library/Internet Plug-Ins/Flash Player.plugin:$
-10.3.181.14:$
-1310162668000:0:1:$
-Shockwave Flash 10.3 r181:$
-Shockwave Flash:$
-2
-0:application/x-shockwave-flash:Shockwave Flash:swf:$
-1:application/futuresplash:FutureSplash Player:spl:$
-npgtpo3dautoplugin.plugin:$
-/Library/Internet Plug-Ins/npgtpo3dautoplugin.plugin:$
-0.1.44.5:$
-1308958693000:0:1:$
-Google Talk Plugin Video Accelerator version:0.1.44.5:$
-Google Talk Plugin Video Accelerator:$
-1
-0:application/vnd.gtpo3d.auto:Google Talk Plugin Video Accelerator Type::$
-googletalkbrowserplugin.plugin:$
-/Library/Internet Plug-Ins/googletalkbrowserplugin.plugin:$
-2.1.7.2968:$
-1308958693000:0:1:$
-Version 2.1.7.2968:$
-Google Talk NPAPI Plugin:$
-1
-0:application/googletalk:Google voice and video chat:googletalk:$
-QuickTime Plugin.plugin:$
-/Library/Internet Plug-Ins/QuickTime Plugin.plugin:$
-7.7.1:$
-1308549020000:0:1:$
-The QuickTime Plugin allows you to view a wide variety of multimedia content in web pages. For more information, visit the QuickTime Web site.:$
-QuickTime Plug-in 7.7.1:$
-59
-0:audio/x-midi:MIDI:mid,midi,smf,kar:$
-1:audio/x-ac3:AC3 audio:ac3:$
-2:video/mp4:MPEG-4 media:mp4:$
-3:video/x-m4v:Video:m4v:$
-4:application/x-sdp:SDP stream descriptor:sdp:$
-5:audio/AMR:AMR audio:AMR:$
-6:application/x-rtsp:RTSP stream descriptor:rtsp,rts:$
-7:image/tiff:TIFF image:tif,tiff:$
-8:video/3gpp2:3GPP2 media:3g2,3gp2:$
-9:image/jpeg2000:JPEG2000 image:jp2:$
-10:audio/mpeg:MPEG audio:mpeg,mpg,m1s,m1a,mp2,mpm,mpa,m2a,mp3,swa:$
-11:image/x-quicktime:QuickTime image:qtif,qti:$
-12:audio/mpeg3:MP3 audio:mp3,swa:$
-13:application/sdp:SDP stream descriptor:sdp:$
-14:application/x-mpeg:AMC media:amc:$
-15:image/x-macpaint:MacPaint image:pntg,pnt,mac:$
-16:video/mpeg:MPEG media:mpeg,mpg,m1s,m1v,m1a,m75,m15,mp2,mpm,mpv,mpa:$
-17:video/x-msvideo:Video For Windows:avi,vfw:$
-18:audio/aac:AAC audio:aac,adts:$
-19:audio/x-gsm:GSM audio:gsm:$
-20:video/sd-video:SD video:sdv:$
-21:audio/x-caf:CAF audio:caf:$
-22:image/x-jpeg2000-image:JPEG2000 image:jp2:$
-23:audio/3gpp:3GPP media:3gp,3gpp:$
-24:audio/mp3:MP3 audio:mp3,swa:$
-25:image/x-targa:TGA image:targa,tga:$
-26:video/avi:Video For Windows:avi,vfw:$
-27:image/png:PNG image:png:$
-28:image/x-tiff:TIFF image:tif,tiff:$
-29:audio/mp4:MPEG-4 media:mp4:$
-30:image/jp2:JPEG2000 image:jp2:$
-31:audio/x-aiff:AIFF audio:aiff,aif,aifc,cdda:$
-32:video/x-mpeg:MPEG media:mpeg,mpg,m1s,m1v,m1a,m75,m15,mp2,mpm,mpv,mpa:$
-33:video/3gpp:3GPP media:3gp,3gpp:$
-34:audio/x-mpeg:MPEG audio:mpeg,mpg,m1s,m1a,mp2,mpm,mpa,m2a,mp3,swa:$
-35:audio/x-aac:AAC audio:aac,adts:$
-36:audio/mid:MIDI:mid,midi,smf,kar:$
-37:audio/3gpp2:3GPP2 media:3g2,3gp2:$
-38:audio/midi:MIDI:mid,midi,smf,kar:$
-39:audio/x-wav:WAVE audio:wav,bwf:$
-40:audio/x-mp3:MP3 audio:mp3,swa:$
-41:video/quicktime:QuickTime Movie:mov,qt,mqv:$
-42:audio/x-mpeg3:MP3 audio:mp3,swa:$
-43:image/x-bmp:BMP image:bmp,dib:$
-44:audio/ac3:AC3 audio:ac3:$
-45:image/pict:PICT image:pict,pic,pct:$
-46:audio/x-m4p:AAC audio:m4p:$
-47:audio/x-m4a:AAC audio:m4a:$
-48:image/x-png:PNG image:png:$
-49:image/x-pict:PICT image:pict,pic,pct:$
-50:image/x-sgi:SGI image:sgi,rgb:$
-51:audio/x-m4b:AAC audio book:m4b:$
-52:video/flc:AutoDesk Animator:flc,fli,cel:$
-53:audio/aiff:AIFF audio:aiff,aif,aifc,cdda:$
-54:video/msvideo:Video For Windows:avi,vfw:$
-55:audio/basic:uLaw/AU audio:au,snd,ulw:$
-56:image/jpeg2000-image:JPEG2000 image:jp2:$
-57:audio/wav:WAVE audio:wav,bwf:$
-58:audio/vnd.qcelp:QUALCOMM PureVoice audio:qcp,qcp:$
-JavaAppletPlugin.plugin:$
-/System/Library/Java/Support/CoreDeploy.bundle/Contents/JavaAppletPlugin.plugin:$
-14.1.0:$
-1308272440000:0:1:$
-Displays Java applet content, or a placeholder if Java is not installed.:$
-Java Applet Plug-in:$
-17
-0:application/x-java-applet;version=1.1.3:Java applet::$
-1:application/x-java-applet:Basic Java Applets:javaapplet:$
-2:application/x-java-applet;version=1.2.2:Java applet::$
-3:application/x-java-applet;version=1.5:Java applet::$
-4:application/x-java-vm:Java applet::$
-5:application/x-java-applet;version=1.3.1:Java applet::$
-6:application/x-java-applet;version=1.3:Java applet::$
-7:application/x-java-applet;version=1.1.2:Java applet::$
-8:application/x-java-applet;version=1.1:Java applet::$
-9:application/x-java-applet;version=1.2.1:Java applet::$
-10:application/x-java-applet;version=1.6:Java applet::$
-11:application/x-java-applet;version=1.4.2:Java applet::$
-12:application/x-java-applet;version=1.4:Java applet::$
-13:application/x-java-applet;version=1.1.1:Java applet::$
-14:application/x-java-applet;version=1.2:Java applet::$
-15:application/x-java-applet;jpi-version=1.6.0_29:Java applet::$
-16:application/x-java-vm-npruntime:::$
-WebEx64.plugin:$
-/Users/agorbatchev/Library/Internet Plug-Ins/WebEx64.plugin:$
-1.0:$
-1307651021000:0:1:$
-WebEx64 General Plugin Container Version 202:$
-WebEx64 General Plugin Container:$
-1
-0:application/webx-gpc-plugin64:gpc::$
-Silverlight.plugin:$
-/Library/Internet Plug-Ins/Silverlight.plugin:$
-4.0.60129.0:$
-1296448496000:0:1:$
-4.0.60129.0:$
-Silverlight Plug-In:$
-2
-0:application/x-silverlight:Microsoft Silverlight:xaml:$
-1:application/x-silverlight-2:Microsoft Silverlight:xaml:$
-DirectorShockwave.plugin:$
-/Library/Internet Plug-Ins/DirectorShockwave.plugin:$
-11.5.7r609:$
-1272547663000:0:1:$
-:$
-:$
-0
-iPhotoPhotocast.plugin:$
-/Library/Internet Plug-Ins/iPhotoPhotocast.plugin:$
-7.0:$
-1258508974000:0:1:$
-iPhoto6:$
-iPhotoPhotocast:$
-1
-0:application/photo:iPhoto 700::$
-OfficeLiveBrowserPlugin.plugin:$
-/Library/Internet Plug-Ins/OfficeLiveBrowserPlugin.plugin:$
-12.2.5:$
-1256000273000:0:1:$
-Office Live Update v1.0:$
-Microsoft Office Live Plug-in:$
-1
-0:application/officelive:Office Live Update v1.0::$
-
-[INVALID]
-/Library/Internet Plug-Ins/Quartz Composer.webplugin:$
-1308270108000:$
-/Library/Internet Plug-Ins/nsIQTScriptablePlugin.xpt:$
-1321292803000:$
-/Library/Internet Plug-Ins/flashplayer.xpt:$
-1304635372000:$
-/Library/Internet Plug-Ins/Disabled Plug-Ins:$
-1274731912000:$
-/Library/Internet Plug-Ins/AdobePDFViewer.plugin:$
-1274910092000:$
diff --git a/tests/firefox_profile/prefs.js b/tests/firefox_profile/prefs.js
deleted file mode 100644
index c333b24..0000000
--- a/tests/firefox_profile/prefs.js
+++ /dev/null
@@ -1,41 +0,0 @@
-# Mozilla User Preferences
-
-/* Do not edit this file.
- *
- * If you make changes to this file while the application is running,
- * the changes will be overwritten when the application exits.
- *
- * To make a manual change to preferences, you can visit the URL about:config
- * For more information, see http://www.mozilla.org/unix/customizing.html#prefs
- */
-
-user_pref("browser.bookmarks.restore_default_bookmarks", false);
-user_pref("browser.cache.disk.capacity", 1048576);
-user_pref("browser.cache.disk.smart_size.first_run", false);
-user_pref("browser.cache.disk.smart_size_cached_value", 1048576);
-user_pref("browser.migration.version", 5);
-user_pref("browser.places.smartBookmarksVersion", 2);
-user_pref("browser.rights.3.shown", true);
-user_pref("browser.shell.checkDefaultBrowser", false);
-user_pref("browser.startup.homepage_override.buildID", "20111220165912");
-user_pref("browser.startup.homepage_override.mstone", "rv:9.0.1");
-user_pref("extensions.blocklist.pingCountVersion", 0);
-user_pref("extensions.bootstrappedAddons", "{}");
-user_pref("extensions.databaseSchema", 6);
-user_pref("extensions.enabledAddons", "{972ce4c6-7e08-4474-a285-3208198ce6fd}:9.0.1");
-user_pref("extensions.installCache", "[{\"name\":\"app-global\",\"addons\":{\"{972ce4c6-7e08-4474-a285-3208198ce6fd}\":{\"descriptor\":\"/Applications/Firefox 9.0.1.app/Contents/MacOS/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}\",\"mtime\":1324444015000}}}]");
-user_pref("extensions.lastAppVersion", "9.0.1");
-user_pref("extensions.lastPlatformVersion", "9.0.1");
-user_pref("extensions.pendingOperations", false);
-user_pref("extensions.shownSelectionUI", true);
-user_pref("intl.charsetmenu.browser.cache", "UTF-8");
-user_pref("network.cookie.prefsMigrated", true);
-user_pref("places.history.expiration.transient_current_max_pages", 104858);
-user_pref("places.history.expiration.transient_optimal_database_size", 167772160);
-user_pref("privacy.cpd.siteSettings", true);
-user_pref("privacy.sanitize.migrateFx3Prefs", true);
-user_pref("privacy.sanitize.timeSpan", 0);
-user_pref("toolkit.telemetry.prompted", 2);
-user_pref("urlclassifier.keyupdatetime.https://sb-ssl.google.com/safebrowsing/newkey", 1327691121);
-user_pref("xpinstall.whitelist.add", "");
-user_pref("xpinstall.whitelist.add.36", "");
diff --git a/tests/firefox_profile/search.json b/tests/firefox_profile/search.json
deleted file mode 100644
index 1a94704..0000000
--- a/tests/firefox_profile/search.json
+++ /dev/null
@@ -1 +0,0 @@
-{"version":7,"buildID":"20111220165912","locale":"en-US","directories":{"/Applications/Firefox 9.0.1.app/Contents/MacOS/searchplugins":{"lastModifiedTime":1324444016000,"engines":[{"_id":"[app]/amazondotcom.xml","_name":"Amazon.com","_hidden":false,"description":"Amazon.com Search","__searchForm":"http://www.amazon.com/","_iconURL":"data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHgSURBVHjalFM9TNtQEP4cB7PwM1RITUXIgsRaYEEVEyKZwhiyZAQyd0BhpFOlIjoBqhjSqVQMoVMLLAjEwECCQJkSkBqJYDOAFOMKFSf28d7DTUxiUDnp/Pzeu/vuu7t3ICKF6SLTMv2/lB0fRWKfjwDm4JJisYh0Oo3fpZLYT0SjSCQS8JAFMADNDZ3NZsnf1taiqVTKi4nGASruk5lkkmTmMB6JUKFQqO+DfX1eABWeQoVR6f7HSdM0obqu48Yw8G1tDT82NsRd1TSbU9BbGPCog8PDj+jLzurFoAVgMh4XxoNDQ6SqKi0tL9eBvAB8zZwymYxYY7EYAoEA8vm82BNTg6XUIs0MeGTZoR1mhXSnwNl4pmAbjU7mcjkKhkL1ynMnntZ4OEw3VyrV8utk7s5TdW++0QXz+1i3P7IK36t+PCfVn1OQOoOA0gXr5DPak+cPXbBK+/T3S69AtY3LJ98vZ1or/iLr+pTuvr59/A6s003UdqZFJF/PCKQ3o5CUznoBST2AfbEF/9iqYEDaIfwj73VJPEfgNTe0tWNYR0uwy9uOW0OkrgHI7z5ADo2C7v48nLV3XHKAT+x/1m1sX58xsBxg8rZJrDYD8DHHp4aJj/MK09sXjPOt46PcCzAACXY8/u34wN0AAAAASUVORK5CYII=","_urls":[{"template":"http://www.amazon.com/exec/obidos/external-search/","rels":[],"params":[{"name":"field-keywords","value":"{searchTerms}"},{"name":"mode","value":"blended"},{"name":"tag","value":"mozilla-20"},{"name":"sourceid","value":"Mozilla-search"}]}],"filePath":"/Applications/Firefox 9.0.1.app/Contents/MacOS/searchplugins/amazondotcom.xml"},{"_id":"[app]/bing.xml","_name":"Bing","_hidden":false,"description":"Bing. Search by Microsoft.","__searchForm":"http://www.bing.com/search","_iconURL":"data:image/x-icon;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAABMLAAATCwAAAAAAAAAAAAAVpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8ysf97zf+24//F6f/F6f/F6f+K0/9QvP8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8krP+Z2P/////////w+f/F6f/F6f/i9P/////////T7v9Bt/8Vpv8Vpv8Vpv8Vpv/T7v/////w+f97zf8Vpv8Vpv8Vpv8Vpv9QvP/T7v/////w+f9Bt/8Vpv8Vpv97zf////////9QvP8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8krP/i9P/////i9P8Vpv8Vpv+24//////i9P8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv+K0/////////8Vpv8Vpv/F6f////////8krP8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv+n3v/////w+f8Vpv8Vpv/F6f////////+n3v8krP8Vpv8Vpv8Vpv8Vpv8Vpv9tx/////////+Z2P8Vpv8Vpv/F6f/////////////i9P+K0/9QvP9QvP9tx//F6f////////+n3v8Vpv8Vpv8Vpv/F6f/////T7v+Z2P/i9P////////////////////+24/9QvP8Vpv8Vpv8Vpv8Vpv/F6f/////F6f8Vpv8Vpv8krP9QvP9QvP9Bt/8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv/F6f/////F6f8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv9Bt/9QvP9Bt/8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8Vpv8AAHBsAABhdAAAbiAAAHJ0AABsaQAAdGkAACBDAABlbgAAUEEAAEVYAAAuQwAAOy4AAEU7AABBVAAAQ00AAC5W","_urls":[{"template":"http://api.bing.com/osjson.aspx","rels":[],"type":"application/x-suggestions+json","params":[{"name":"query","value":"{searchTerms}"},{"name":"form","value":"OSDJAS"}]},{"template":"http://www.bing.com/search","rels":[],"params":[{"name":"q","value":"{searchTerms}"},{"name":"form","value":"MOZSBR"},{"pref":"ms-pc","name":"pc","condition":"pref","mozparam":true}]},{"template":"http://www.bing.com/search","rels":[],"type":"application/x-moz-keywordsearch","params":[{"name":"q","value":"{searchTerms}"},{"name":"form","value":"MOZLBR"},{"pref":"ms-pc","name":"pc","condition":"pref","mozparam":true}]}],"filePath":"/Applications/Firefox 9.0.1.app/Contents/MacOS/searchplugins/bing.xml","queryCharset":"UTF-8"},{"_id":"[app]/eBay.xml","_name":"eBay","_hidden":false,"description":"eBay - Online auctions","__searchForm":"http://search.ebay.com/","_iconURL":"data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABFUlEQVQ4jdWTvUoDQRSFvxUfQMFSyBvYpLGSSWFpncY6lsLWFiupBBtLBRsfQcQ2a782PoCkSrONlUGy5LPYn6wbu4DghcOcYs65595hIpVNamsj9V8ajOeFzgsFLmo+LxTXcWJVX8WyppIgKSVPkQQ/F0u3gSFwBfTqdoPoBYDnxRFcDgA4Z4cbPtazqblZptBgxJ2BtGydv+vbkyahSUGC0zxT7VeZ0DguBXFsRs9AKtzq/amOKA2sTAylzMDKoIM6wfXhcWmcBKd51ukeWq8Qx6V0MmFAuppxdx/OIgB6e/32+SoTUGfdHTxy0CRodtF6jZpW2R2qs/alQNrgYTytR8Cf1Rh08VuNGkECJCtd5L//TN/BEWxoE8dlIQAAAABJRU5ErkJggg==","_urls":[{"template":"http://anywhere.ebay.com/services/suggest/","rels":[],"type":"application/x-suggestions+json","params":[{"name":"s","value":"0"},{"name":"q","value":"{searchTerms}"}]},{"template":"http://rover.ebay.com/rover/1/711-47294-18009-3/4","rels":[],"params":[{"name":"mpre","value":"http://shop.ebay.com/?_nkw={searchTerms}"}]}],"filePath":"/Applications/Firefox 9.0.1.app/Contents/MacOS/searchplugins/eBay.xml"},{"_id":"[app]/google.xml","_name":"Google","_hidden":false,"description":"Google Search","__searchForm":"http://www.google.com/","_iconURL":"data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA","_urls":[{"template":"http://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}","rels":[],"type":"application/x-suggestions+json","params":[]},{"template":"http://www.google.com/search","rels":[],"params":[{"name":"q","value":"{searchTerms}"},{"name":"ie","value":"utf-8"},{"name":"oe","value":"utf-8"},{"name":"aq","value":"t"},{"name":"rls","value":"{moz:distributionID}:{moz:locale}:{moz:official}"},{"name":"client","falseValue":"firefox","trueValue":"firefox-a","condition":"defaultEngine","mozparam":true}]}],"filePath":"/Applications/Firefox 9.0.1.app/Contents/MacOS/searchplugins/google.xml","queryCharset":"UTF-8"},{"_id":"[app]/twitter.xml","_name":"Twitter","_hidden":false,"description":"Realtime Twitter Search","__searchForm":"https://twitter.com/search/","_iconURL":"data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A/v7+D/7+/j/+/v5g/v7+YP7+/mD+/v5I/v7+KP///wD///8A////AP///wD///8A////AP///wD+/v4H/v7+UPbv4pHgx47B1K9Y3tWwWN7Ur1je3sKCx+rbuKj+/v5n/v7+GP///wD///8A////AP///wD+/v4Y+fbweM2ycMe2iB7/vI0f/8STIf/KlyL/zJki/8yZIv/LmCL/0ahK5/Hp1JH+/v4Y////AP///wD///8A7OTTaquHN+CujkXPs5ZTv6N6G/+2iB7/xpUh/8yZIv/MmSL/zJki/8yZIv/Kmy738OjUi////wD///8A////AMKtfY7w6+Ef////AP///wD///8A3sqbp8iWIf/MmSL/zJki/8yZIv/MmSL/y5gi/8mePO7+/v4w////AP///wD///8A////AP///wD+/v4H/v7+V9CtWN3KmCL/zJki/8yZIv/MmSL/zJki/8yZIv/JlyH/5tSqp/7+/mD+/v4/////AP///wD///8A+PXvJtGyZdXNnS/3y5gi/8qYIv/LmCL/zJki/8yZIv/MmSL/y5gi/82iPO7LqVfe0byMmf///wD///8A/v7+D/Do1JHKmy73ypci/8KSIP+/jyD/xpQh/8uYIv/MmSL/zJki/8qYIv+/jyD/rIEd/9nKqH7///8A////APPu4TzAlSz3wZEg/7mLH/+sgR3/uZdGz7mLH//JlyH/zJki/8yZIv/GlSH/to0r9eXbxD/Vx6dg////AP7+/h/p38WhtIsq9al/HP+kfyjuybaKgf///wCzjzjlwJAg/8qYIv/JlyH/u4wf/8CkYrn///8A////AP///wDj2sRMnHUa/7meYa7Vx6dg////AP///wD///8A2MmnYK6DHf++jiD/vo4g/62CHf/k2sQ/////AP///wD///8A8OvhH/f07w////8A////AP///wD///8A////AP///wC/p3Cfpnwc/66GKvPg1LZ8////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////ANXHp2DJtoqByLWKgf///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A//8AAP//AADgPwAAwA8AAIAHAAB4BwAA+AMAAPAAAADgAQAA4AMAAMEDAADPhwAA/48AAP/nAAD//wAA//8AAA==","_urls":[{"template":"https://twitter.com/search/{searchTerms}","rels":[],"params":[{"name":"partner","value":"Firefox"},{"name":"source","value":"desktop-search"}]}],"filePath":"/Applications/Firefox 9.0.1.app/Contents/MacOS/searchplugins/twitter.xml","queryCharset":"UTF-8"},{"_id":"[app]/wikipedia.xml","_name":"Wikipedia (en)","_hidden":false,"description":"Wikipedia, the free encyclopedia","__searchForm":"http://en.wikipedia.org/wiki/Special:Search","_iconURL":"data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAEAgQAhIOEAMjHyABIR0gA6ejpAGlqaQCpqKkAKCgoAPz9%2FAAZGBkAmJiYANjZ2ABXWFcAent6ALm6uQA8OjwAiIiIiIiIiIiIiI4oiL6IiIiIgzuIV4iIiIhndo53KIiIiB%2FWvXoYiIiIfEZfWBSIiIEGi%2FfoqoiIgzuL84i9iIjpGIoMiEHoiMkos3FojmiLlUipYliEWIF%2BiDe0GoRa7D6GPbjcu1yIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","_urls":[{"template":"http://en.wikipedia.org/w/api.php","rels":[],"type":"application/x-suggestions+json","params":[{"name":"action","value":"opensearch"},{"name":"search","value":"{searchTerms}"}]},{"template":"http://en.wikipedia.org/wiki/Special:Search","rels":[],"params":[{"name":"search","value":"{searchTerms}"},{"name":"sourceid","value":"Mozilla-search"}]}],"filePath":"/Applications/Firefox 9.0.1.app/Contents/MacOS/searchplugins/wikipedia.xml","queryCharset":"UTF-8"},{"_id":"[app]/yahoo.xml","_name":"Yahoo","_hidden":false,"description":"Yahoo Search","__searchForm":"http://search.yahoo.com/","_iconURL":"data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbgJqAIoCdgCaAnoAnhKCAKYijgCuLpIAskKeALpSpgC+Yq4AzHy8ANqezgDmvt4A7tLqAPz5+wD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKlRFIoABWAKERERE6ADcKMzzu2hOgAAhERK8REWCWBERE36ERMHMEREvo6iEgY6hEn6Pu0mAzqkz/xjMzoDNwpERERDoAMzAKlERIoAAzMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//wAA//8AAP//AADAOQAAgBkAAAAPAAAACQAAAAkAAAAIAAAACAAAAAgAAIAYAADAOAAA//8AAP//AAD//wAA","_urls":[{"template":"http://ff.search.yahoo.com/gossip?output=fxjson&command={searchTerms}","rels":[],"type":"application/x-suggestions+json","params":[]},{"template":"http://search.yahoo.com/search","rels":[],"params":[{"name":"p","value":"{searchTerms}"},{"name":"ei","value":"UTF-8"},{"pref":"yahoo-fr","name":"fr","condition":"pref","mozparam":true}]}],"filePath":"/Applications/Firefox 9.0.1.app/Contents/MacOS/searchplugins/yahoo.xml","queryCharset":"UTF-8"}]}}}
\ No newline at end of file
diff --git a/tests/firefox_profile/sessionstore.bak b/tests/firefox_profile/sessionstore.bak
deleted file mode 100644
index 3205333..0000000
--- a/tests/firefox_profile/sessionstore.bak
+++ /dev/null
@@ -1 +0,0 @@
-{"windows":[{"tabs":[{"entries":[{"url":"about:home","title":"Mozilla Firefox Start Page","ID":0,"docshellID":5,"owner_b64":"NhAra3tiRRqhyKDUVsktxQAAAAAAAAAAwAAAAAAAAEYAAQAAAAAAAS8nfAAOr03buTZBMmukiq45X+BFfRhK26P9r5jIoa8RAAAAAAVhYm91dAAAAARob21lAODaHXAvexHTjNAAYLD8FKM5X+BFfRhK26P9r5jIoa8RAAAAAA5tb3otc2FmZS1hYm91dAAAAARob21lAAAAAA==","docIdentifier":0,"formdata":{},"scroll":"0,0"}],"index":1,"hidden":false,"attributes":{"image":"chrome://branding/content/icon16.png"}}],"selected":1,"_closedTabs":[],"busy":false,"width":1260,"height":1404,"screenX":347,"screenY":22,"sizemode":"normal"}],"selectedWindow":1,"_closedWindows":[],"session":{"state":"stopped","lastUpdate":1325099179995,"startTime":1325099164660}}
\ No newline at end of file
diff --git a/tests/firefox_profile/sessionstore.js b/tests/firefox_profile/sessionstore.js
deleted file mode 100644
index ac63c35..0000000
--- a/tests/firefox_profile/sessionstore.js
+++ /dev/null
@@ -1 +0,0 @@
-{"windows":[{"tabs":[{"entries":[{"url":"about:home","title":"Mozilla Firefox Start Page","ID":0,"docshellID":5,"owner_b64":"NhAra3tiRRqhyKDUVsktxQAAAAAAAAAAwAAAAAAAAEYAAQAAAAAAAS8nfAAOr03buTZBMmukiq45X+BFfRhK26P9r5jIoa8RAAAAAAVhYm91dAAAAARob21lAODaHXAvexHTjNAAYLD8FKM5X+BFfRhK26P9r5jIoa8RAAAAAA5tb3otc2FmZS1hYm91dAAAAARob21lAAAAAA==","docIdentifier":0,"formdata":{},"scroll":"0,0"}],"index":1,"hidden":false,"attributes":{"image":"chrome://branding/content/icon16.png"}}],"selected":1,"_closedTabs":[],"busy":false,"width":1260,"height":1404,"screenX":211,"screenY":22,"sizemode":"normal"}],"selectedWindow":1,"_closedWindows":[],"session":{"state":"stopped","lastUpdate":1325099214574,"startTime":1325099208545}}
\ No newline at end of file
diff --git a/tests/firefox_profile/urlclassifier.pset b/tests/firefox_profile/urlclassifier.pset
deleted file mode 100644
index a210985..0000000
Binary files a/tests/firefox_profile/urlclassifier.pset and /dev/null differ
diff --git a/tests/get_selenium_rc b/tests/get_selenium_rc
deleted file mode 100755
index be88b26..0000000
--- a/tests/get_selenium_rc
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-
-curl -O "http://selenium.googlecode.com/files/selenium-server-standalone-2.15.0.jar"
-
diff --git a/tests/start_selenium_rc b/tests/start_selenium_rc
deleted file mode 100755
index 5b0b506..0000000
--- a/tests/start_selenium_rc
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-PATH="/Applications/Firefox 5.0.1.app/Contents/MacOS":$PATH
-java -jar selenium-server-standalone-2.15.0.jar -firefoxProfileTemplate "./firefox_profile"
-
diff --git a/tests/test_arrow.js b/tests/test_arrow.js
new file mode 100644
index 0000000..1d8c0b0
--- /dev/null
+++ b/tests/test_arrow.js
@@ -0,0 +1,34 @@
+var soda = require('soda'),
+ assert = require('assert'),
+ common = require('./common')
+ ;
+
+function testArrow(exampleId, secondary)
+{
+ return function(browser)
+ {
+ browser
+ .open('/manual/plugins/arrow.html')
+ .clickAndWait('css=#example-' + exampleId)
+
+ .and(common.verifyTextExt)
+ .and(common.testArrowFunctionality())
+ .and(secondary || function(){})
+ .and(common.screenshot('arrow-' + exampleId))
+ ;
+ };
+}
+
+function run(browser)
+{
+ browser
+ .and(testArrow('arrow-with-autocomplete'))
+ .and(testArrow('prompt-with-autocomplete-and-arrow'))
+ ;
+};
+
+module.exports = run;
+
+if(require.main == module)
+ common.runModule(run);
+
diff --git a/tests/test_prompt.js b/tests/test_prompt.js
index 18a0ed5..70192b0 100644
--- a/tests/test_prompt.js
+++ b/tests/test_prompt.js
@@ -25,7 +25,6 @@ function run(browser)
.and(testPrompt('prompt-with-autocomplete-and-arrow', function(browser)
{
browser
- .and(common.testArrowFunctionality())
.and(common.testAutocompleteFunctionality())
;
}))
diff --git a/tests/tests.js b/tests/tests.js
index bd7eed3..fe71575 100644
--- a/tests/tests.js
+++ b/tests/tests.js
@@ -11,6 +11,7 @@ common.runModule(function(browser)
.and(require('./test_filter.js'))
.and(require('./test_focus.js'))
.and(require('./test_prompt.js'))
+ .and(require('./test_arrow.js'))
;
});