/** * jQuery TextExt Plugin * http://textextjs.com * * @version 1.3.1 * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved. * @license MIT License */ (function($, undefined) { /** * TextExt is the main core class which by itself doesn't provide any functionality * that is user facing, however it has the underlying mechanics to bring all the * plugins together under one roof and make them work with each other or on their * own. * * @author agorbatchev * @date 2011/08/19 * @id TextExt */ function TextExt() {}; /** * ItemManager is used to seamlessly convert between string that come from the user input to whatever * the format the item data is being passed around in. It's used by all plugins that in one way or * another operate with items, such as Tags, Filter, Autocomplete and Suggestions. Default implementation * works with `String` type. * * Each instance of `TextExt` creates a new instance of default implementation of `ItemManager` * unless `itemManager` option was set to another implementation. * * To satisfy requirements of managing items of type other than a `String`, different implementation * if `ItemManager` should be supplied. * * If you wish to bring your own implementation, you need to create a new class and implement all the * methods that `ItemManager` has. After, you need to supply your pass via the `itemManager` option during * initialization like so: * * $('#input').textext({ * itemManager : CustomItemManager * }) * * @author agorbatchev * @date 2011/08/19 * @id ItemManager */ function ItemManager() {}; /** * TextExtPlugin is a base class for all plugins. It provides common methods which are reused * by majority of plugins. * * All plugins must register themselves by calling the `$.fn.textext.addPlugin(name, constructor)` * function while providing plugin name and constructor. The plugin name is the same name that user * will identify the plugin in the `plugins` option when initializing TextExt component and constructor * function will create a new instance of the plugin. *Without registering, the core won't * be able to see the plugin.* * * 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 + 'px', height ; self.trigger(EVENT_PRE_INVALIDATE); height = input.outerHeight() + 'px'; // using css() method instead of width() and height() here because they don't seem to do the right thing in jQuery 1.8.x // https://github.com/alexgorbatchev/jquery-textext/issues/74 input.css({ 'width' : width }); wrap.css({ 'width' : width, 'height' : height }); container.css({ 'height' : height }); self.trigger(EVENT_POST_INVALIDATE); }; /** * Focuses user input on the text box. * * @signature TextExt.focusInput() * * @author agorbatchev * @date 2011/08/19 * @id TextExt.focusInput */ p.focusInput = function() { this.input()[0].focus(); }; /** * Serializes data for to be set into the hidden input field and which will be submitted * with the HTML form. * * By default simple JSON serialization is used. It's expected that `JSON.stringify` * method would be available either through built in class in most modern browsers * or through JSON2 library. * * @signature TextExt.serializeData(data) * * @param data {Object} Data to serialize. * * @author agorbatchev * @date 2011/08/09 * @id TextExt.serializeData */ p.serializeData = stringify; /** * Returns the hidden input HTML element which will be submitted with the HTML form. * * @signature TextExt.hiddenInput() * * @author agorbatchev * @date 2011/08/09 * @id TextExt.hiddenInput */ p.hiddenInput = function(value) { return $(this).data('hiddenInput'); }; /** * Abstracted functionality to trigger an event and get the data with maximum weight set by all * the event handlers. This functionality is used for the `getFormData` event. * * @signature TextExt.getWeightedEventResponse(event, args) * * @param event {String} Event name. * @param args {Object} Argument to be passed with the event. * * @author agorbatchev * @date 2011/08/22 * @id TextExt.getWeightedEventResponse */ p.getWeightedEventResponse = function(event, args) { var self = this, data = {}, maxWeight = 0 ; self.trigger(event, data, args); for(var weight in data) maxWeight = Math.max(maxWeight, weight); return data[maxWeight]; }; /** * Triggers the `getFormData` event to get all the plugins to return their data. * * After the data is returned, triggers `setFormData` and `setInputData` to update appopriate values. * * @signature TextExt.getFormData(keyCode) * * @param keyCode {Number} Key code number which has triggered this update. It's impotant to pass * this value to the plugins because they might return different values based on the key that was * pressed. For example, the Tags plugin returns an empty string for the `input` value if the enter * key was pressed, otherwise it returns whatever is currently in the text input. * * @author agorbatchev * @date 2011/08/22 * @id TextExt.getFormData */ p.getFormData = function(keyCode) { var self = this, data = self.getWeightedEventResponse(EVENT_GET_FORM_DATA, keyCode || 0) ; self.trigger(EVENT_SET_FORM_DATA , data['form']); self.trigger(EVENT_SET_INPUT_DATA , data['input']); }; //-------------------------------------------------------------------------------- // Event handlers /** * Reacts to the `anyKeyUp` event and triggers the `getFormData` to change data that will be submitted * with the form. Default behaviour is that everything that is typed in will be JSON serialized, so * the end result will be a JSON string. * * @signature TextExt.onAnyKeyUp(e) * * @param e {Object} jQuery event. * * @author agorbatchev * @date 2011/08/19 * @id TextExt.onAnyKeyUp */ p.onAnyKeyUp = function(e, keyCode) { this.getFormData(keyCode); }; /** * Reacts to the `setInputData` event and populates the input text field that user is currently * interacting with. * * @signature TextExt.onSetInputData(e, data) * * @param e {Event} jQuery event. * @param data {String} Value to be set. * * @author agorbatchev * @date 2011/08/22 * @id TextExt.onSetInputData */ p.onSetInputData = function(e, data) { this.input().val(data); }; /** * Reacts to the `setFormData` event and populates the hidden input with will be submitted with * the HTML form. The value will be serialized with `serializeData()` method. * * @signature TextExt.onSetFormData(e, data) * * @param e {Event} jQuery event. * @param data {Object} Data that will be set. * * @author agorbatchev * @date 2011/08/22 * @id TextExt.onSetFormData */ p.onSetFormData = function(e, data) { var self = this; self.hiddenInput().val(self.serializeData(data)); }; /** * Reacts to `getFormData` event triggered by the core. At the bare minimum the core will tell * itself to use the current value in the text input as the data to be submitted with the HTML * form. * * @signature TextExt.onGetFormData(e, data) * * @param e {Event} jQuery event. * * @author agorbatchev * @date 2011/08/09 * @id TextExt.onGetFormData */ p.onGetFormData = function(e, data) { var val = this.input().val(); data[0] = formDataObject(val, val); }; //-------------------------------------------------------------------------------- // User mouse/keyboard input /** * Triggers `[name]KeyUp` and `[name]KeyPress` for every keystroke as described in the events. * * @signature TextExt.onKeyUp(e) * * @param e {Object} jQuery event. * @author agorbatchev * @date 2011/08/19 * @id TextExt.onKeyUp */ /** * Triggers `[name]KeyDown` for every keystroke as described in the events. * * @signature TextExt.onKeyDown(e) * * @param e {Object} jQuery event. * @author agorbatchev * @date 2011/08/19 * @id TextExt.onKeyDown */ $(['Down', 'Up']).each(function() { var type = this.toString(); p['onKey' + type] = function(e) { var self = this, keyName = self.opts(OPT_KEYS)[e.keyCode], defaultResult = true ; if(keyName) { defaultResult = keyName.substr(-1) != '!'; keyName = keyName.replace('!', ''); self.trigger(keyName + 'Key' + type); // manual *KeyPress event fimplementation for the function keys like Enter, Backspace, etc. if(type == 'Up' && self._lastKeyDown == e.keyCode) { self._lastKeyDown = null; self.trigger(keyName + 'KeyPress'); } if(type == 'Down') self._lastKeyDown = e.keyCode; } self.trigger('anyKey' + type, e.keyCode); return defaultResult; }; }); //-------------------------------------------------------------------------------- // Plugin Base p = TextExtPlugin.prototype; /** * Allows to add multiple event handlers which will be execued in the scope of the current object. * * @signature TextExt.on([target], handlers) * * @param target {Object} **Optional**. Target object which has traditional `bind(event, handler)` method. * Handler function will still be executed in the current object's scope. * @param handlers {Object} Key/value pairs of event names and handlers, eg `{ event: handler }`. * * @author agorbatchev * @date 2011/08/19 * @id TextExtPlugin.on */ p.on = hookupEvents; /** * Returns the hash object that `getFormData` triggered by the core expects. * * @signature TextExtPlugin.formDataObject(input, form) * * @param input {String} Value that will go into the text input that user is interacting with. * @param form {Object} Value that will be serialized and put into the hidden that will be submitted * with the HTML form. * * @author agorbatchev * @date 2011/08/22 * @id TextExtPlugin.formDataObject */ p.formDataObject = formDataObject; /** * Initialization method called by the core during plugin instantiation. This method must be implemented * by each plugin individually. * * @signature TextExtPlugin.init(core) * * @param core {TextExt} Instance of the TextExt core class. * * @author agorbatchev * @date 2011/08/19 * @id TextExtPlugin.init */ p.init = function(core) { throw new Error('Not implemented') }; /** * Initialization method wich should be called by the plugin during the `init()` call. * * @signature TextExtPlugin.baseInit(core, defaults) * * @param core {TextExt} Instance of the TextExt core class. * @param defaults {Object} Default plugin options. These will be checked if desired value wasn't * found in the options supplied by the user. * * @author agorbatchev * @date 2011/08/19 * @id TextExtPlugin.baseInit */ p.baseInit = function(core, defaults) { var self = this; core._defaults = $.extend(true, core._defaults, defaults); self._core = core; self._timers = {}; }; /** * Allows starting of multiple timeout calls. Each time this method is called with the same * timer name, the timer is reset. This functionality is useful in cases where an action needs * to occur only after a certain period of inactivity. For example, making an AJAX call after * user stoped typing for 1 second. * * @signature TextExtPlugin.startTimer(name, delay, callback) * * @param name {String} Timer name. * @param delay {Number} Delay in seconds. * @param callback {Function} Callback function. * * @author agorbatchev * @date 2011/08/25 * @id TextExtPlugin.startTimer */ p.startTimer = function(name, delay, callback) { var self = this; self.stopTimer(name); self._timers[name] = setTimeout( function() { delete self._timers[name]; callback.apply(self); }, delay * 1000 ); }; /** * Stops the timer by name without resetting it. * * @signature TextExtPlugin.stopTimer(name) * * @param name {String} Timer name. * * @author agorbatchev * @date 2011/08/25 * @id TextExtPlugin.stopTimer */ p.stopTimer = function(name) { clearTimeout(this._timers[name]); }; /** * Returns instance of the `TextExt` to which current instance of the plugin is attached to. * * @signature TextExtPlugin.core() * * @author agorbatchev * @date 2011/08/19 * @id TextExtPlugin.core */ p.core = function() { return this._core; }; /** * Shortcut to the core's `opts()` method. Returns option value. * * @signature TextExtPlugin.opts(name) * * @param name {String} Option name as described in the options. * * @author agorbatchev * @date 2011/08/19 * @id TextExtPlugin.opts */ p.opts = function(name) { return this.core().opts(name); }; /** * Shortcut to the core's `itemManager()` method. Returns instance of the `ItemManger` that is * currently in use. * * @signature TextExtPlugin.itemManager() * * @author agorbatchev * @date 2011/08/19 * @id TextExtPlugin.itemManager */ p.itemManager = function() { return this.core().itemManager(); }; /** * Shortcut to the core's `input()` method. Returns instance of the HTML element that represents * current text input. * * @signature TextExtPlugin.input() * * @author agorbatchev * @date 2011/08/19 * @id TextExtPlugin.input */ p.input = function() { return this.core().input(); }; /** * Shortcut to the commonly used `this.input().val()` call to get or set value of the text input. * * @signature TextExtPlugin.val(value) * * @param value {String} Optional value. If specified, the value will be set, otherwise it will be * returned. * * @author agorbatchev * @date 2011/08/20 * @id TextExtPlugin.val */ p.val = function(value) { var input = this.input(); if(typeof(value) === UNDEFINED) return input.val(); else input.val(value); }; /** * Shortcut to the core's `trigger()` method. Triggers specified event with arguments on the * component core. * * @signature TextExtPlugin.trigger(event, ...args) * * @param event {String} Name of the event to trigger. * @param ...args All remaining arguments will be passed to the event handler. * * @author agorbatchev * @date 2011/08/19 * @id TextExtPlugin.trigger */ p.trigger = function() { var core = this.core(); core.trigger.apply(core, arguments); }; /** * Shortcut to the core's `bind()` method. Binds specified handler to the event. * * @signature TextExtPlugin.bind(event, handler) * * @param event {String} Event name. * @param handler {Function} Event handler. * * @author agorbatchev * @date 2011/08/20 * @id TextExtPlugin.bind */ p.bind = function(event, handler) { this.core().bind(event, handler); }; /** * Returns initialization priority for this plugin. If current plugin depends upon some other plugin * to be initialized before or after, priority needs to be adjusted accordingly. Plugins with higher * priority initialize before plugins with lower priority. * * Default initialization priority is `0`. * * @signature TextExtPlugin.initPriority() * * @author agorbatchev * @date 2011/08/22 * @id TextExtPlugin.initPriority */ p.initPriority = function() { return 0; }; //-------------------------------------------------------------------------------- // jQuery Integration /** * TextExt integrates as a jQuery plugin available through the `$(selector).textext(opts)` call. If * `opts` argument is passed, then a new instance of `TextExt` will be created for all the inputs * that match the `selector`. If `opts` wasn't passed and TextExt was already intantiated for * inputs that match the `selector`, array of `TextExt` instances will be returned instead. * * // will create a new instance of `TextExt` for all elements that match `.sample` * $('.sample').textext({ ... }); * * // will return array of all `TextExt` instances * var list = $('.sample').textext(); * * The following properties are also exposed through the jQuery `$.fn.textext`: * * * `TextExt` -- `TextExt` class. * * `TextExtPlugin` -- `TextExtPlugin` class. * * `ItemManager` -- `ItemManager` class. * * `plugins` -- Key/value table of all registered plugins. * * `addPlugin(name, constructor)` -- All plugins should register themselves using this function. * * @author agorbatchev * @date 2011/08/19 * @id TextExt.jquery */ var cssInjected = false; var textext = $.fn.textext = function(opts) { var css; if(!cssInjected && (css = $.fn.textext.css) != null) { $('head').append(''); 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);