diff options
author | Alex Bitney | 2016-02-07 00:00:41 +0200 |
---|---|---|
committer | Alex Bitney | 2016-02-07 00:00:41 +0200 |
commit | c2d316449f85bd2b74ae9ffaa3d08b7a5ee282cf (patch) | |
tree | ce808c6d65e4a61913497f5d397473f75fadc701 /src/main/webapp/textext/textext.core.js | |
parent | 1fffebc18bbdbe87b456e2b3bd66e9ce5a6afcb8 (diff) |
added tags when posting new message
added templates engine (rythm engine) and moved something to it.
WARNING: textext plugin does not work when minimized, and also I fixed bug in it.
Diffstat (limited to 'src/main/webapp/textext/textext.core.js')
-rw-r--r-- | src/main/webapp/textext/textext.core.js | 1617 |
1 files changed, 1617 insertions, 0 deletions
diff --git a/src/main/webapp/textext/textext.core.js b/src/main/webapp/textext/textext.core.js new file mode 100644 index 00000000..39cf4d3a --- /dev/null +++ b/src/main/webapp/textext/textext.core.js @@ -0,0 +1,1617 @@ +/** + * jQuery TextExt Plugin + * http://textextjs.com + * + * @version 1.3.1 + * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved. + * @license MIT License + */ +(function($, undefined) +{ + /** + * TextExt is the main core class which by itself doesn't provide any functionality + * that is user facing, however it has the underlying mechanics to bring all the + * plugins together under one roof and make them work with each other or on their + * own. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt + */ + function TextExt() {}; + + /** + * ItemManager is used to seamlessly convert between string that come from the user input to whatever + * the format the item data is being passed around in. It's used by all plugins that in one way or + * another operate with items, such as Tags, Filter, Autocomplete and Suggestions. Default implementation + * works with `String` type. + * + * Each instance of `TextExt` creates a new instance of default implementation of `ItemManager` + * unless `itemManager` option was set to another implementation. + * + * To satisfy requirements of managing items of type other than a `String`, different implementation + * if `ItemManager` should be supplied. + * + * If you wish to bring your own implementation, you need to create a new class and implement all the + * methods that `ItemManager` has. After, you need to supply your pass via the `itemManager` option during + * initialization like so: + * + * $('#input').textext({ + * itemManager : CustomItemManager + * }) + * + * @author agorbatchev + * @date 2011/08/19 + * @id ItemManager + */ + function ItemManager() {}; + + /** + * TextExtPlugin is a base class for all plugins. It provides common methods which are reused + * by majority of plugins. + * + * All plugins must register themselves by calling the `$.fn.textext.addPlugin(name, constructor)` + * function while providing plugin name and constructor. The plugin name is the same name that user + * will identify the plugin in the `plugins` option when initializing TextExt component and constructor + * function will create a new instance of the plugin. *Without registering, the core won't + * be able to see the plugin.* + * + * <span class="new label version">new in 1.2.0</span> You can get instance of each plugin from the core + * via associated function with the same name as the plugin. For example: + * + * $('#input').textext()[0].tags() + * $('#input').textext()[0].autocomplete() + * ... + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtPlugin + */ + function TextExtPlugin() {}; + + var stringify = (JSON || {}).stringify, + slice = Array.prototype.slice, + p, + UNDEFINED = 'undefined', + + /** + * TextExt provides a way to pass in the options to configure the core as well as + * each plugin that is being currently used. The jQuery exposed plugin `$().textext()` + * function takes a hash object with key/value set of options. For example: + * + * $('textarea').textext({ + * enabled: true + * }) + * + * There are multiple ways of passing in the options: + * + * 1. Options could be nested multiple levels deep and accessed using all lowercased, dot + * separated style, eg `foo.bar.world`. The manual is using this style for clarity and + * consistency. For example: + * + * { + * item: { + * manager: ... + * }, + * + * html: { + * wrap: ... + * }, + * + * autocomplete: { + * enabled: ..., + * dropdown: { + * position: ... + * } + * } + * } + * + * 2. Options could be specified using camel cased names in a flat key/value fashion like so: + * + * { + * itemManager: ..., + * htmlWrap: ..., + * autocompleteEnabled: ..., + * autocompleteDropdownPosition: ... + * } + * + * 3. Finally, options could be specified in mixed style. It's important to understand that + * for each dot separated name, its alternative in camel case is also checked for, eg for + * `foo.bar.world` it's alternatives could be `fooBarWorld`, `foo.barWorld` or `fooBar.world`, + * which translates to `{ foo: { bar: { world: ... } } }`, `{ fooBarWorld: ... }`, + * `{ foo : { barWorld : ... } }` or `{ fooBar: { world: ... } }` respectively. For example: + * + * { + * itemManager : ..., + * htmlWrap: ..., + * autocomplete: { + * enabled: ..., + * dropdownPosition: ... + * } + * } + * + * Mixed case is used through out the code, wherever it seems appropriate. However in the code, all option + * names are specified in the dot notation because it works both ways where as camel case is not + * being converted to its alternative dot notation. + * + * @author agorbatchev + * @date 2011/08/17 + * @id TextExt.options + */ + + /** + * Default instance of `ItemManager` which takes `String` type as default for tags. + * + * @name item.manager + * @default ItemManager + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.options.item.manager + */ + OPT_ITEM_MANAGER = 'item.manager', + + /** + * List of plugins that should be used with the current instance of TextExt. The list could be + * specified as array of strings or as comma or space separated string. + * + * @name plugins + * @default [] + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.options.plugins + */ + OPT_PLUGINS = 'plugins', + + /** + * TextExt allows for overriding of virtually any method that the core or any of its plugins + * use. This could be accomplished through the use of the `ext` option. + * + * It's possible to specifically target the core or any plugin, as well as overwrite all the + * desired methods everywhere. + * + * 1. Targeting the core: + * + * ext: { + * core: { + * trigger: function() + * { + * console.log('TextExt.trigger', arguments); + * $.fn.textext.TextExt.prototype.trigger.apply(this, arguments); + * } + * } + * } + * + * 2. Targeting individual plugins: + * + * ext: { + * tags: { + * addTags: function(tags) + * { + * console.log('TextExtTags.addTags', tags); + * $.fn.textext.TextExtTags.prototype.addTags.apply(this, arguments); + * } + * } + * } + * + * 3. Targeting `ItemManager` instance: + * + * ext: { + * itemManager: { + * stringToItem: function(str) + * { + * console.log('ItemManager.stringToItem', str); + * return $.fn.textext.ItemManager.prototype.stringToItem.apply(this, arguments); + * } + * } + * } + * + * 4. And finally, in edge cases you can extend everything at once: + * + * ext: { + * '*': { + * fooBar: function() {} + * } + * } + * + * @name ext + * @default {} + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.options.ext + */ + OPT_EXT = 'ext', + + /** + * HTML source that is used to generate elements necessary for the core and all other + * plugins to function. + * + * @name html.wrap + * @default '<div class="text-core"><div class="text-wrap"/></div>' + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.options.html.wrap + */ + OPT_HTML_WRAP = 'html.wrap', + + /** + * HTML source that is used to generate hidden input value of which will be submitted + * with the HTML form. + * + * @name html.hidden + * @default '<input type="hidden" />' + * @author agorbatchev + * @date 2011/08/20 + * @id TextExt.options.html.hidden + */ + OPT_HTML_HIDDEN = 'html.hidden', + + /** + * Hash table of key codes and key names for which special events will be created + * by the core. For each entry a `[name]KeyDown`, `[name]KeyUp` and `[name]KeyPress` events + * will be triggered along side with `anyKeyUp` and `anyKeyDown` events for every + * key stroke. + * + * Here's a list of default keys: + * + * { + * 8 : 'backspace', + * 9 : 'tab', + * 13 : 'enter!', + * 27 : 'escape!', + * 37 : 'left', + * 38 : 'up!', + * 39 : 'right', + * 40 : 'down!', + * 46 : 'delete', + * 108 : 'numpadEnter' + * } + * + * Please note the `!` at the end of some keys. This tells the core that by default + * this keypress will be trapped and not passed on to the text input. + * + * @name keys + * @default { ... } + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.options.keys + */ + OPT_KEYS = 'keys', + + /** + * The core triggers or reacts to the following events. + * + * @author agorbatchev + * @date 2011/08/17 + * @id TextExt.events + */ + + /** + * Core triggers `preInvalidate` event before the dimensions of padding on the text input + * are set. + * + * @name preInvalidate + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.events.preInvalidate + */ + EVENT_PRE_INVALIDATE = 'preInvalidate', + + /** + * Core triggers `postInvalidate` event after the dimensions of padding on the text input + * are set. + * + * @name postInvalidate + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.events.postInvalidate + */ + EVENT_POST_INVALIDATE = 'postInvalidate', + + /** + * Core triggers `getFormData` on every key press to collect data that will be populated + * into the hidden input that will be submitted with the HTML form and data that will + * be displayed in the input field that user is currently interacting with. + * + * All plugins that wish to affect how the data is presented or sent must react to + * `getFormData` and populate the data in the following format: + * + * { + * input : {String}, + * form : {Object} + * } + * + * The data key must be a numeric weight which will be used to determine which data + * ends up being used. Data with the highest numerical weight gets the priority. This + * allows plugins to set the final data regardless of their initialization order, which + * otherwise would be impossible. + * + * For example, the Tags and Autocomplete plugins have to work side by side and Tags + * plugin must get priority on setting the data. Therefore the Tags plugin sets data + * with the weight 200 where as the Autocomplete plugin sets data with the weight 100. + * + * Here's an example of a typical `getFormData` handler: + * + * TextExtPlugin.prototype.onGetFormData = function(e, data, keyCode) + * { + * data[100] = self.formDataObject('input value', 'form value'); + * }; + * + * Core also reacts to the `getFormData` and updates hidden input with data which will be + * submitted with the HTML form. + * + * @name getFormData + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.events.getFormData + */ + EVENT_GET_FORM_DATA = 'getFormData', + + /** + * Core triggers and reacts to the `setFormData` event to update the actual value in the + * hidden input that will be submitted with the HTML form. Second argument can be value + * of any type and by default it will be JSON serialized with `TextExt.serializeData()` + * function. + * + * @name setFormData + * @author agorbatchev + * @date 2011/08/22 + * @id TextExt.events.setFormData + */ + EVENT_SET_FORM_DATA = 'setFormData', + + /** + * Core triggers and reacts to the `setInputData` event to update the actual value in the + * text input that user is interacting with. Second argument must be of a `String` type + * the value of which will be set into the text input. + * + * @name setInputData + * @author agorbatchev + * @date 2011/08/22 + * @id TextExt.events.setInputData + */ + EVENT_SET_INPUT_DATA = 'setInputData', + + /** + * Core triggers `postInit` event to let plugins run code after all plugins have been + * created and initialized. This is a good place to set some kind of global values before + * somebody gets to use them. This is not the right place to expect all plugins to finish + * their initialization. + * + * @name postInit + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.events.postInit + */ + EVENT_POST_INIT = 'postInit', + + /** + * Core triggers `ready` event after all global configuration and prepearation has been + * done and the TextExt component is ready for use. Event handlers should expect all + * values to be set and the plugins to be in the final state. + * + * @name ready + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.events.ready + */ + EVENT_READY = 'ready', + + /** + * Core triggers `anyKeyUp` event for every key up event triggered within the component. + * + * @name anyKeyUp + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.events.anyKeyUp + */ + + /** + * Core triggers `anyKeyDown` event for every key down event triggered within the component. + * + * @name anyKeyDown + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.events.anyKeyDown + */ + + /** + * Core triggers `[name]KeyUp` event for every key specifid in the `keys` option that is + * triggered within the component. + * + * @name [name]KeyUp + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.events.[name]KeyUp + */ + + /** + * Core triggers `[name]KeyDown` event for every key specified in the `keys` option that is + * triggered within the component. + * + * @name [name]KeyDown + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.events.[name]KeyDown + */ + + /** + * Core triggers `[name]KeyPress` event for every key specified in the `keys` option that is + * triggered within the component. + * + * @name [name]KeyPress + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.events.[name]KeyPress + */ + + DEFAULT_OPTS = { + itemManager : ItemManager, + + plugins : [], + ext : {}, + + html : { + wrap : '<div class="text-core"><div class="text-wrap"/></div>', + hidden : '<input type="hidden" />' + }, + + keys : { + 8 : 'backspace', + 9 : 'tab', + 13 : 'enter!', + 27 : 'escape!', + 37 : 'left', + 38 : 'up!', + 39 : 'right', + 40 : 'down!', + 46 : 'delete', + 108 : 'numpadEnter' + } + } + ; + + // Freak out if there's no JSON.stringify function found + if(!stringify) + throw new Error('JSON.stringify() not found'); + + /** + * Returns object property by name where name is dot-separated and object is multiple levels deep. + * @param target Object Source object. + * @param name String Dot separated property name, ie `foo.bar.world` + * @id core.getProperty + */ + function getProperty(source, name) + { + if(typeof(name) === 'string') + name = name.split('.'); + + var fullCamelCaseName = name.join('.').replace(/\.(\w)/g, function(match, letter) { return letter.toUpperCase() }), + nestedName = name.shift(), + result + ; + + if(typeof(result = source[fullCamelCaseName]) != UNDEFINED) + result = result; + + else if(typeof(result = source[nestedName]) != UNDEFINED && name.length > 0) + result = getProperty(result, name); + + // name.length here should be zero + return result; + }; + + /** + * Hooks up specified events in the scope of the current object. + * @author agorbatchev + * @date 2011/08/09 + */ + function hookupEvents() + { + var args = slice.apply(arguments), + self = this, + target = args.length === 1 ? self : args.shift(), + event + ; + + args = args[0] || {}; + + function bind(event, handler) + { + target.bind(event, function() + { + // apply handler to our PLUGIN object, not the target + return handler.apply(self, arguments); + }); + } + + for(event in args) + bind(event, args[event]); + }; + + function formDataObject(input, form) + { + return { 'input' : input, 'form' : form }; + }; + + //-------------------------------------------------------------------------------- + // ItemManager core component + + p = ItemManager.prototype; + + /** + * Initialization method called by the core during instantiation. + * + * @signature ItemManager.init(core) + * + * @param core {TextExt} Instance of the TextExt core class. + * + * @author agorbatchev + * @date 2011/08/19 + * @id ItemManager.init + */ + p.init = function(core) + { + }; + + /** + * Filters out items from the list that don't match the query and returns remaining items. Default + * implementation checks if the item starts with the query. + * + * @signature ItemManager.filter(list, query) + * + * @param list {Array} List of items. Default implementation works with strings. + * @param query {String} Query string. + * + * @author agorbatchev + * @date 2011/08/19 + * @id ItemManager.filter + */ + p.filter = function(list, query) + { + var result = [], + i, item + ; + + for(i = 0; i < list.length; i++) + { + item = list[i]; + if(this.itemContains(item, query)) + result.push(item); + } + + return result; + }; + + /** + * Returns `true` if specified item contains another string, `false` otherwise. In the default implementation + * `String.indexOf()` is used to check if item string begins with the needle string. + * + * @signature ItemManager.itemContains(item, needle) + * + * @param item {Object} Item to check. Default implementation works with strings. + * @param needle {String} Search string to be found within the item. + * + * @author agorbatchev + * @date 2011/08/19 + * @id ItemManager.itemContains + */ + p.itemContains = function(item, needle) + { + return this.itemToString(item).toLowerCase().indexOf(needle.toLowerCase()) == 0; + }; + + /** + * Converts specified string to item. Because default implemenation works with string, input string + * is simply returned back. To use custom objects, different implementation of this method could + * return something like `{ name : {String} }`. + * + * @signature ItemManager.stringToItem(str) + * + * @param str {String} Input string. + * + * @author agorbatchev + * @date 2011/08/19 + * @id ItemManager.stringToItem + */ + p.stringToItem = function(str) + { + return str; + }; + + /** + * Converts specified item to string. Because default implemenation works with string, input string + * is simply returned back. To use custom objects, different implementation of this method could + * for example return `name` field of `{ name : {String} }`. + * + * @signature ItemManager.itemToString(item) + * + * @param item {Object} Input item to be converted to string. + * + * @author agorbatchev + * @date 2011/08/19 + * @id ItemManager.itemToString + */ + p.itemToString = function(item) + { + return item; + }; + + /** + * Returns `true` if both items are equal, `false` otherwise. Because default implemenation works with + * string, input items are compared as strings. To use custom objects, different implementation of this + * method could for example compare `name` fields of `{ name : {String} }` type object. + * + * @signature ItemManager.compareItems(item1, item2) + * + * @param item1 {Object} First item. + * @param item2 {Object} Second item. + * + * @author agorbatchev + * @date 2011/08/19 + * @id ItemManager.compareItems + */ + p.compareItems = function(item1, item2) + { + return item1 == item2; + }; + + //-------------------------------------------------------------------------------- + // TextExt core component + + p = TextExt.prototype; + + /** + * Initializes current component instance with work with the supplied text input and options. + * + * @signature TextExt.init(input, opts) + * + * @param input {HTMLElement} Text input. + * @param opts {Object} Options. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.init + */ + p.init = function(input, opts) + { + var self = this, + hiddenInput, + itemManager, + container + ; + + self._defaults = $.extend({}, DEFAULT_OPTS); + self._opts = opts || {}; + self._plugins = {}; + self._itemManager = itemManager = new (self.opts(OPT_ITEM_MANAGER))(); + input = $(input); + container = $(self.opts(OPT_HTML_WRAP)); + hiddenInput = $(self.opts(OPT_HTML_HIDDEN)); + + input + .wrap(container) + .keydown(function(e) { return self.onKeyDown(e) }) + .keyup(function(e) { return self.onKeyUp(e) }) + .data('textext', self) + ; + + // keep references to html elements using jQuery.data() to avoid circular references + $(self).data({ + 'hiddenInput' : hiddenInput, + 'wrapElement' : input.parents('.text-wrap').first(), + 'input' : input + }); + + // set the name of the hidden input to the text input's name + hiddenInput.attr('name', input.attr('name')); + // remove name attribute from the text input + input.attr('name', null); + // add hidden input to the DOM + hiddenInput.insertAfter(input); + + $.extend(true, itemManager, self.opts(OPT_EXT + '.item.manager')); + $.extend(true, self, self.opts(OPT_EXT + '.*'), self.opts(OPT_EXT + '.core')); + + self.originalWidth = input.outerWidth(); + + self.invalidateBounds(); + + itemManager.init(self); + + self.initPatches(); + self.initPlugins(self.opts(OPT_PLUGINS), $.fn.textext.plugins); + + self.on({ + setFormData : self.onSetFormData, + getFormData : self.onGetFormData, + setInputData : self.onSetInputData, + anyKeyUp : self.onAnyKeyUp + }); + + self.trigger(EVENT_POST_INIT); + self.trigger(EVENT_READY); + + self.getFormData(0); + }; + + /** + * Initialized all installed patches against current instance. The patches are initialized based on their + * initialization priority which is returned by each patch's `initPriority()` method. Priority + * is a `Number` where patches with higher value gets their `init()` method called before patches + * with lower priority value. + * + * This facilitates initializing of patches in certain order to insure proper dependencies + * regardless of which order they are loaded. + * + * By default all patches have the same priority - zero, which means they will be initialized + * in rorder they are loaded, that is unless `initPriority()` is overriden. + * + * @signature TextExt.initPatches() + * + * @author agorbatchev + * @date 2011/10/11 + * @id TextExt.initPatches + */ + p.initPatches = function() + { + var list = [], + source = $.fn.textext.patches, + name + ; + + for(name in source) + list.push(name); + + this.initPlugins(list, source); + }; + + /** + * Creates and initializes all specified plugins. The plugins are initialized based on their + * initialization priority which is returned by each plugin's `initPriority()` method. Priority + * is a `Number` where plugins with higher value gets their `init()` method called before plugins + * with lower priority value. + * + * This facilitates initializing of plugins in certain order to insure proper dependencies + * regardless of which order user enters them in the `plugins` option field. + * + * By default all plugins have the same priority - zero, which means they will be initialized + * in the same order as entered by the user. + * + * @signature TextExt.initPlugins(plugins) + * + * @param plugins {Array} List of plugin names to initialize. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.initPlugins + */ + p.initPlugins = function(plugins, source) + { + var self = this, + ext, name, plugin, initList = [], i + ; + + if(typeof(plugins) == 'string') + plugins = plugins.split(/\s*,\s*|\s+/g); + + for(i = 0; i < plugins.length; i++) + { + name = plugins[i]; + plugin = source[name]; + + if(plugin) + { + self._plugins[name] = plugin = new plugin(); + self[name] = (function(plugin) { + return function(){ return plugin; } + })(plugin); + initList.push(plugin); + $.extend(true, plugin, self.opts(OPT_EXT + '.*'), self.opts(OPT_EXT + '.' + name)); + } + } + + // sort plugins based on their priority values + initList.sort(function(p1, p2) + { + p1 = p1.initPriority(); + p2 = p2.initPriority(); + + return p1 === p2 + ? 0 + : p1 < p2 ? 1 : -1 + ; + }); + + for(i = 0; i < initList.length; i++) + initList[i].init(self); + }; + + /** + * Returns true if specified plugin is was instantiated for the current instance of core. + * + * @signature TextExt.hasPlugin(name) + * + * @param name {String} Name of the plugin to check. + * + * @author agorbatchev + * @date 2011/12/28 + * @id TextExt.hasPlugin + * @version 1.1 + */ + p.hasPlugin = function(name) + { + return !!this._plugins[name]; + }; + + /** + * Allows to add multiple event handlers which will be execued in the scope of the current object. + * + * @signature TextExt.on([target], handlers) + * + * @param target {Object} **Optional**. Target object which has traditional `bind(event, handler)` method. + * Handler function will still be executed in the current object's scope. + * @param handlers {Object} Key/value pairs of event names and handlers, eg `{ event: handler }`. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.on + */ + p.on = hookupEvents; + + /** + * Binds an event handler to the input box that user interacts with. + * + * @signature TextExt.bind(event, handler) + * + * @param event {String} Event name. + * @param handler {Function} Event handler. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.bind + */ + p.bind = function(event, handler) + { + this.input().bind(event, handler); + }; + + /** + * Triggers an event on the input box that user interacts with. All core events are originated here. + * + * @signature TextExt.trigger(event, ...args) + * + * @param event {String} Name of the event to trigger. + * @param ...args All remaining arguments will be passed to the event handler. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.trigger + */ + p.trigger = function() + { + var args = arguments; + this.input().trigger(args[0], slice.call(args, 1)); + }; + + /** + * Returns instance of `itemManager` that is used by the component. + * + * @signature TextExt.itemManager() + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.itemManager + */ + p.itemManager = function() + { + return this._itemManager; + }; + + /** + * Returns jQuery input element with which user is interacting with. + * + * @signature TextExt.input() + * + * @author agorbatchev + * @date 2011/08/10 + * @id TextExt.input + */ + p.input = function() + { + return $(this).data('input'); + }; + + /** + * Returns option value for the specified option by name. If the value isn't found in the user + * provided options, it will try looking for default value. + * + * @signature TextExt.opts(name) + * + * @param name {String} Option name as described in the options. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.opts + */ + p.opts = function(name) + { + var result = getProperty(this._opts, name); + return typeof(result) == 'undefined' ? getProperty(this._defaults, name) : result; + }; + + /** + * Returns HTML element that was created from the `html.wrap` option. This is the top level HTML + * container for the text input with which user is interacting with. + * + * @signature TextExt.wrapElement() + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.wrapElement + */ + p.wrapElement = function() + { + return $(this).data('wrapElement'); + }; + + /** + * Updates container to match dimensions of the text input. Triggers `preInvalidate` and `postInvalidate` + * events. + * + * @signature TextExt.invalidateBounds() + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.invalidateBounds + */ + p.invalidateBounds = function() + { + var self = this, + input = self.input(), + wrap = self.wrapElement(), + container = wrap.parent(), + width = self.originalWidth + 'px', + height + ; + + self.trigger(EVENT_PRE_INVALIDATE); + + height = input.outerHeight() + 'px'; + + // using css() method instead of width() and height() here because they don't seem to do the right thing in jQuery 1.8.x + // https://github.com/alexgorbatchev/jquery-textext/issues/74 + input.css({ 'width' : width }); + wrap.css({ 'width' : width, 'height' : height }); + container.css({ 'height' : height }); + + self.trigger(EVENT_POST_INVALIDATE); + }; + + /** + * Focuses user input on the text box. + * + * @signature TextExt.focusInput() + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.focusInput + */ + p.focusInput = function() + { + this.input()[0].focus(); + }; + + /** + * Serializes data for to be set into the hidden input field and which will be submitted + * with the HTML form. + * + * By default simple JSON serialization is used. It's expected that `JSON.stringify` + * method would be available either through built in class in most modern browsers + * or through JSON2 library. + * + * @signature TextExt.serializeData(data) + * + * @param data {Object} Data to serialize. + * + * @author agorbatchev + * @date 2011/08/09 + * @id TextExt.serializeData + */ + p.serializeData = stringify; + + /** + * Returns the hidden input HTML element which will be submitted with the HTML form. + * + * @signature TextExt.hiddenInput() + * + * @author agorbatchev + * @date 2011/08/09 + * @id TextExt.hiddenInput + */ + p.hiddenInput = function(value) + { + return $(this).data('hiddenInput'); + }; + + /** + * Abstracted functionality to trigger an event and get the data with maximum weight set by all + * the event handlers. This functionality is used for the `getFormData` event. + * + * @signature TextExt.getWeightedEventResponse(event, args) + * + * @param event {String} Event name. + * @param args {Object} Argument to be passed with the event. + * + * @author agorbatchev + * @date 2011/08/22 + * @id TextExt.getWeightedEventResponse + */ + p.getWeightedEventResponse = function(event, args) + { + var self = this, + data = {}, + maxWeight = 0 + ; + + self.trigger(event, data, args); + + for(var weight in data) + maxWeight = Math.max(maxWeight, weight); + + return data[maxWeight]; + }; + + /** + * Triggers the `getFormData` event to get all the plugins to return their data. + * + * After the data is returned, triggers `setFormData` and `setInputData` to update appopriate values. + * + * @signature TextExt.getFormData(keyCode) + * + * @param keyCode {Number} Key code number which has triggered this update. It's impotant to pass + * this value to the plugins because they might return different values based on the key that was + * pressed. For example, the Tags plugin returns an empty string for the `input` value if the enter + * key was pressed, otherwise it returns whatever is currently in the text input. + * + * @author agorbatchev + * @date 2011/08/22 + * @id TextExt.getFormData + */ + p.getFormData = function(keyCode) + { + var self = this, + data = self.getWeightedEventResponse(EVENT_GET_FORM_DATA, keyCode || 0) + ; + + self.trigger(EVENT_SET_FORM_DATA , data['form']); + self.trigger(EVENT_SET_INPUT_DATA , data['input']); + }; + + //-------------------------------------------------------------------------------- + // Event handlers + + /** + * Reacts to the `anyKeyUp` event and triggers the `getFormData` to change data that will be submitted + * with the form. Default behaviour is that everything that is typed in will be JSON serialized, so + * the end result will be a JSON string. + * + * @signature TextExt.onAnyKeyUp(e) + * + * @param e {Object} jQuery event. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.onAnyKeyUp + */ + p.onAnyKeyUp = function(e, keyCode) + { + this.getFormData(keyCode); + }; + + /** + * Reacts to the `setInputData` event and populates the input text field that user is currently + * interacting with. + * + * @signature TextExt.onSetInputData(e, data) + * + * @param e {Event} jQuery event. + * @param data {String} Value to be set. + * + * @author agorbatchev + * @date 2011/08/22 + * @id TextExt.onSetInputData + */ + p.onSetInputData = function(e, data) + { + this.input().val(data); + }; + + /** + * Reacts to the `setFormData` event and populates the hidden input with will be submitted with + * the HTML form. The value will be serialized with `serializeData()` method. + * + * @signature TextExt.onSetFormData(e, data) + * + * @param e {Event} jQuery event. + * @param data {Object} Data that will be set. + * + * @author agorbatchev + * @date 2011/08/22 + * @id TextExt.onSetFormData + */ + p.onSetFormData = function(e, data) + { + var self = this; + self.hiddenInput().val(self.serializeData(data)); + }; + + /** + * Reacts to `getFormData` event triggered by the core. At the bare minimum the core will tell + * itself to use the current value in the text input as the data to be submitted with the HTML + * form. + * + * @signature TextExt.onGetFormData(e, data) + * + * @param e {Event} jQuery event. + * + * @author agorbatchev + * @date 2011/08/09 + * @id TextExt.onGetFormData + */ + p.onGetFormData = function(e, data) + { + var val = this.input().val(); + data[0] = formDataObject(val, val); + }; + + //-------------------------------------------------------------------------------- + // User mouse/keyboard input + + /** + * Triggers `[name]KeyUp` and `[name]KeyPress` for every keystroke as described in the events. + * + * @signature TextExt.onKeyUp(e) + * + * @param e {Object} jQuery event. + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.onKeyUp + */ + + /** + * Triggers `[name]KeyDown` for every keystroke as described in the events. + * + * @signature TextExt.onKeyDown(e) + * + * @param e {Object} jQuery event. + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.onKeyDown + */ + + $(['Down', 'Up']).each(function() + { + var type = this.toString(); + + p['onKey' + type] = function(e) + { + var self = this, + keyName = self.opts(OPT_KEYS)[e.keyCode], + defaultResult = true + ; + + if(keyName) + { + defaultResult = keyName.substr(-1) != '!'; + keyName = keyName.replace('!', ''); + + self.trigger(keyName + 'Key' + type); + + // manual *KeyPress event fimplementation for the function keys like Enter, Backspace, etc. + if(type == 'Up' && self._lastKeyDown == e.keyCode) + { + self._lastKeyDown = null; + self.trigger(keyName + 'KeyPress'); + } + + if(type == 'Down') + self._lastKeyDown = e.keyCode; + } + + self.trigger('anyKey' + type, e.keyCode); + + return defaultResult; + }; + }); + + //-------------------------------------------------------------------------------- + // Plugin Base + + p = TextExtPlugin.prototype; + + /** + * Allows to add multiple event handlers which will be execued in the scope of the current object. + * + * @signature TextExt.on([target], handlers) + * + * @param target {Object} **Optional**. Target object which has traditional `bind(event, handler)` method. + * Handler function will still be executed in the current object's scope. + * @param handlers {Object} Key/value pairs of event names and handlers, eg `{ event: handler }`. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtPlugin.on + */ + p.on = hookupEvents; + + /** + * Returns the hash object that `getFormData` triggered by the core expects. + * + * @signature TextExtPlugin.formDataObject(input, form) + * + * @param input {String} Value that will go into the text input that user is interacting with. + * @param form {Object} Value that will be serialized and put into the hidden that will be submitted + * with the HTML form. + * + * @author agorbatchev + * @date 2011/08/22 + * @id TextExtPlugin.formDataObject + */ + p.formDataObject = formDataObject; + + /** + * Initialization method called by the core during plugin instantiation. This method must be implemented + * by each plugin individually. + * + * @signature TextExtPlugin.init(core) + * + * @param core {TextExt} Instance of the TextExt core class. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtPlugin.init + */ + p.init = function(core) { throw new Error('Not implemented') }; + + /** + * Initialization method wich should be called by the plugin during the `init()` call. + * + * @signature TextExtPlugin.baseInit(core, defaults) + * + * @param core {TextExt} Instance of the TextExt core class. + * @param defaults {Object} Default plugin options. These will be checked if desired value wasn't + * found in the options supplied by the user. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtPlugin.baseInit + */ + p.baseInit = function(core, defaults) + { + var self = this; + + core._defaults = $.extend(true, core._defaults, defaults); + self._core = core; + self._timers = {}; + }; + + /** + * Allows starting of multiple timeout calls. Each time this method is called with the same + * timer name, the timer is reset. This functionality is useful in cases where an action needs + * to occur only after a certain period of inactivity. For example, making an AJAX call after + * user stoped typing for 1 second. + * + * @signature TextExtPlugin.startTimer(name, delay, callback) + * + * @param name {String} Timer name. + * @param delay {Number} Delay in seconds. + * @param callback {Function} Callback function. + * + * @author agorbatchev + * @date 2011/08/25 + * @id TextExtPlugin.startTimer + */ + p.startTimer = function(name, delay, callback) + { + var self = this; + + self.stopTimer(name); + + self._timers[name] = setTimeout( + function() + { + delete self._timers[name]; + callback.apply(self); + }, + delay * 1000 + ); + }; + + /** + * Stops the timer by name without resetting it. + * + * @signature TextExtPlugin.stopTimer(name) + * + * @param name {String} Timer name. + * + * @author agorbatchev + * @date 2011/08/25 + * @id TextExtPlugin.stopTimer + */ + p.stopTimer = function(name) + { + clearTimeout(this._timers[name]); + }; + + /** + * Returns instance of the `TextExt` to which current instance of the plugin is attached to. + * + * @signature TextExtPlugin.core() + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtPlugin.core + */ + p.core = function() + { + return this._core; + }; + + /** + * Shortcut to the core's `opts()` method. Returns option value. + * + * @signature TextExtPlugin.opts(name) + * + * @param name {String} Option name as described in the options. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtPlugin.opts + */ + p.opts = function(name) + { + return this.core().opts(name); + }; + + /** + * Shortcut to the core's `itemManager()` method. Returns instance of the `ItemManger` that is + * currently in use. + * + * @signature TextExtPlugin.itemManager() + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtPlugin.itemManager + */ + p.itemManager = function() + { + return this.core().itemManager(); + }; + + /** + * Shortcut to the core's `input()` method. Returns instance of the HTML element that represents + * current text input. + * + * @signature TextExtPlugin.input() + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtPlugin.input + */ + p.input = function() + { + return this.core().input(); + }; + + /** + * Shortcut to the commonly used `this.input().val()` call to get or set value of the text input. + * + * @signature TextExtPlugin.val(value) + * + * @param value {String} Optional value. If specified, the value will be set, otherwise it will be + * returned. + * + * @author agorbatchev + * @date 2011/08/20 + * @id TextExtPlugin.val + */ + p.val = function(value) + { + var input = this.input(); + + if(typeof(value) === UNDEFINED) + return input.val(); + else + input.val(value); + }; + + /** + * Shortcut to the core's `trigger()` method. Triggers specified event with arguments on the + * component core. + * + * @signature TextExtPlugin.trigger(event, ...args) + * + * @param event {String} Name of the event to trigger. + * @param ...args All remaining arguments will be passed to the event handler. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtPlugin.trigger + */ + p.trigger = function() + { + var core = this.core(); + core.trigger.apply(core, arguments); + }; + + /** + * Shortcut to the core's `bind()` method. Binds specified handler to the event. + * + * @signature TextExtPlugin.bind(event, handler) + * + * @param event {String} Event name. + * @param handler {Function} Event handler. + * + * @author agorbatchev + * @date 2011/08/20 + * @id TextExtPlugin.bind + */ + p.bind = function(event, handler) + { + this.core().bind(event, handler); + }; + + /** + * Returns initialization priority for this plugin. If current plugin depends upon some other plugin + * to be initialized before or after, priority needs to be adjusted accordingly. Plugins with higher + * priority initialize before plugins with lower priority. + * + * Default initialization priority is `0`. + * + * @signature TextExtPlugin.initPriority() + * + * @author agorbatchev + * @date 2011/08/22 + * @id TextExtPlugin.initPriority + */ + p.initPriority = function() + { + return 0; + }; + + //-------------------------------------------------------------------------------- + // jQuery Integration + + /** + * TextExt integrates as a jQuery plugin available through the `$(selector).textext(opts)` call. If + * `opts` argument is passed, then a new instance of `TextExt` will be created for all the inputs + * that match the `selector`. If `opts` wasn't passed and TextExt was already intantiated for + * inputs that match the `selector`, array of `TextExt` instances will be returned instead. + * + * // will create a new instance of `TextExt` for all elements that match `.sample` + * $('.sample').textext({ ... }); + * + * // will return array of all `TextExt` instances + * var list = $('.sample').textext(); + * + * The following properties are also exposed through the jQuery `$.fn.textext`: + * + * * `TextExt` -- `TextExt` class. + * * `TextExtPlugin` -- `TextExtPlugin` class. + * * `ItemManager` -- `ItemManager` class. + * * `plugins` -- Key/value table of all registered plugins. + * * `addPlugin(name, constructor)` -- All plugins should register themselves using this function. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExt.jquery + */ + + var cssInjected = false; + + var textext = $.fn.textext = function(opts) + { + var css; + + if(!cssInjected && (css = $.fn.textext.css) != null) + { + $('head').append('<style>' + css + '</style>'); + cssInjected = true; + } + + return this.map(function() + { + var self = $(this); + + if(opts == null) + return self.data('textext'); + + var instance = new TextExt(); + + instance.init(self, opts); + self.data('textext', instance); + + return instance.input()[0]; + }); + }; + + /** + * This static function registers a new plugin which makes it available through the `plugins` option + * to the end user. The name specified here is the name the end user would put in the `plugins` option + * to add this plugin to a new instance of TextExt. + * + * @signature $.fn.textext.addPlugin(name, constructor) + * + * @param name {String} Name of the plugin. + * @param constructor {Function} Plugin constructor. + * + * @author agorbatchev + * @date 2011/10/11 + * @id TextExt.addPlugin + */ + textext.addPlugin = function(name, constructor) + { + textext.plugins[name] = constructor; + constructor.prototype = new textext.TextExtPlugin(); + }; + + /** + * This static function registers a new patch which is added to each instance of TextExt. If you are + * adding a new patch, make sure to call this method. + * + * @signature $.fn.textext.addPatch(name, constructor) + * + * @param name {String} Name of the patch. + * @param constructor {Function} Patch constructor. + * + * @author agorbatchev + * @date 2011/10/11 + * @id TextExt.addPatch + */ + textext.addPatch = function(name, constructor) + { + textext.patches[name] = constructor; + constructor.prototype = new textext.TextExtPlugin(); + }; + + textext.TextExt = TextExt; + textext.TextExtPlugin = TextExtPlugin; + textext.ItemManager = ItemManager; + textext.plugins = {}; + textext.patches = {}; +})(jQuery); + +(function($) +{ + function TextExtIE9Patches() {}; + + $.fn.textext.TextExtIE9Patches = TextExtIE9Patches; + $.fn.textext.addPatch('ie9',TextExtIE9Patches); + + var p = TextExtIE9Patches.prototype; + + p.init = function(core) + { + if(navigator.userAgent.indexOf('MSIE 9') == -1) + return; + + var self = this; + + core.on({ postInvalidate : self.onPostInvalidate }); + }; + + p.onPostInvalidate = function() + { + var self = this, + input = self.input(), + val = input.val() + ; + + // agorbatchev :: IE9 doesn't seem to update the padding if box-sizing is on until the + // text box value changes, so forcing this change seems to do the trick of updating + // IE's padding visually. + input.val(Math.random()); + input.val(val); + }; +})(jQuery); + |