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')) ; });