From 7af897be5278ccd37b53d5a2bcadba0ba3ae9996 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Thu, 3 May 2012 17:18:56 +0200 Subject: [PATCH 01/15] [imp] added textext bzr revid: nicolas.vanhoren@openerp.com-20120503151856-72cblanpkertrwz7 --- addons/web/__openerp__.py | 2 + .../lib/jquery.textext/jquery.textext.css | 186 + .../lib/jquery.textext/jquery.textext.js | 4777 +++++++++++++++++ 3 files changed, 4965 insertions(+) create mode 100644 addons/web/static/lib/jquery.textext/jquery.textext.css create mode 100644 addons/web/static/lib/jquery.textext/jquery.textext.js diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py index 1ca0553c8e2..da67fd60196 100644 --- a/addons/web/__openerp__.py +++ b/addons/web/__openerp__.py @@ -29,6 +29,7 @@ "static/lib/jquery.deferred-queue/jquery.deferred-queue.js", "static/lib/jquery.scrollTo/jquery.scrollTo-min.js", "static/lib/jquery.tipsy/jquery.tipsy.js", + "static/lib/jquery.textext/jquery.textext.js", "static/lib/json/json2.js", "static/lib/qweb/qweb2.js", "static/lib/underscore/underscore.js", @@ -73,6 +74,7 @@ "static/lib/jquery.ui.timepicker/css/jquery-ui-timepicker-addon.css", "static/lib/jquery.ui.notify/css/ui.notify.css", "static/lib/jquery.tipsy/tipsy.css", + "static/lib/jquery.textext/jquery.textext.css", "static/lib/visualsearch/lib/css/reset.css", "static/lib/visualsearch/lib/css/workspace.css", "static/lib/visualsearch/lib/css/icons.css", diff --git a/addons/web/static/lib/jquery.textext/jquery.textext.css b/addons/web/static/lib/jquery.textext/jquery.textext.css new file mode 100644 index 00000000000..e4f2dbc193a --- /dev/null +++ b/addons/web/static/lib/jquery.textext/jquery.textext.css @@ -0,0 +1,186 @@ +.text-core { + position: relative; +} + +.text-core .text-wrap { + position: absolute; + width: 100% !important; +} +.text-core .text-wrap textarea { + resize: none !important; +} + +.text-core .text-wrap textarea, .text-core .text-wrap input { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-border-radius: 0px; + -moz-border-radius: 0px; + border-radius: 0px; + outline: none; + resize: none; + position: absolute; + background: none; + overflow: hidden; + margin: 0; + padding: 3px 5px 4px 5px; + white-space: nowrap; + line-height: 13px; + width: 100% !important; +} + +.extra_suggestions { + border-top: 1px solid #9daccc !important; +} + +.text-core .text-wrap .text-arrow { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + position: absolute; + top: 0; + right: 0; + width: 22px; + height: 22px; + background: url("/web/static/src/img/down-arrow.png") 50% 50% no-repeat; + cursor: pointer; + z-index: 2; +} + +.text-core .text-wrap .text-dropdown { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0; + position: absolute; + z-index: 3; + background: #fff; + border: 1px solid #9daccc; + width: 100%; + padding: 1px; + display: none; + overflow-x: hidden; + overflow-y: auto; +} + +.text-core .text-wrap .text-dropdown.text-position-below { + margin-top: 1px; +} + +.text-core .text-wrap .text-dropdown.text-position-above { + margin-bottom: 1px; +} + +.text-core .text-wrap .text-dropdown .text-list .text-suggestion { + padding: 3px 5px; + cursor: pointer; +} + +.text-core .text-wrap .text-dropdown .text-list .text-suggestion em { + font-style: normal; + text-decoration: underline; +} + +.text-core .text-wrap .text-dropdown .text-list .text-suggestion.text-selected { + color: #fff; + background: #0064CD; +} + +.text-core .text-wrap .text-focus { + -webkit-box-shadow: 0px 0px 6px #6d84b4; + -moz-box-shadow: 0px 0px 6px #6d84b4; + box-shadow: 0px 0px 6px #6d84b4; + position: absolute; + display: none; +} + +.text-core .text-wrap .text-focus.text-show-focus { + display: block; +} + +.text-core .text-wrap .text-prompt { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + position: absolute; + width: 100%; + height: 100%; + margin: 1px 0 0 2px; + color: #c0c0c0; + overflow: hidden; + white-space: pre; +} + +.text-core .text-wrap .text-prompt.text-hide-prompt { + display: none; +} + +.text-core .text-wrap .text-tags { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + position: absolute; + width: 100%; + height: 100%; + padding: 3px 35px 3px 3px; + cursor: text; +} + +.text-core .text-wrap .text-tags.text-tags-on-top { + z-index: 2; +} + +.text-core .text-wrap .text-tags .text-tag { + float: left; +} + +.text-core .text-wrap .text-tags .text-tag .text-button { + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + position: relative; + float: left; + border: 1px solid #9daccc; + background: #e2e6f0; + color: #000; + padding: 0px 17px 0px 3px; + margin: 0 2px 2px 0; + cursor: pointer; + height: 18px; +} + +.text-core .text-wrap .text-tags .text-tag .text-button a.text-remove { + position: absolute; + right: 3px; + top: 2px; + display: block; + width: 11px; + height: 11px; + background: url("remove.png") 0 0 no-repeat; +} + +.text-core .text-wrap .text-tags .text-tag .text-button a.text-remove:hover { + background-position: 0 -11px; +} + +.text-core .text-wrap .text-tags .text-tag .text-button a.text-remove:active { + background-position: 0 -22px; +} +.oe_form_field_many2many span { + display: block; + width: auto; + margin: 0 3px 3px 0; + padding: 3px 8px 4px 8px; + position: relative; + float: left; + text-indent: 0; + background-color: #EEE; + border: 1px solid #AAA; + -moz-border-radius: 7px; + -webkit-border-radius: 7px; + border-radius: 7px; + color: #333; +} diff --git a/addons/web/static/lib/jquery.textext/jquery.textext.js b/addons/web/static/lib/jquery.textext/jquery.textext.js new file mode 100644 index 00000000000..2a66d33108f --- /dev/null +++ b/addons/web/static/lib/jquery.textext/jquery.textext.js @@ -0,0 +1,4777 @@ +;/** + * 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, + + 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); + +;/** + * 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); + console.log('on>>>>>', self, core, DEFAULT_OPTS); + self.on({ + getSuggestions : self.onGetSuggestions + }); + + self._suggestions = null; + }; + + /** + * Performas an async AJAX with specified options. + * + * @signature TextExtAjax.load(query) + * + * @param query {String} Value that user has typed into the text area which is + * presumably the query. + * + * @author agorbatchev + * @date 2011/08/14 + * @id TextExtAjax.load + */ + p.load = function(query) + { + var self = this, + dataCallback = self.opts(OPT_DATA_CALLBACK) || function(query) { return { q : query } }, + opts + ; + + opts = $.extend(true, + { + data : dataCallback(query), + success : function(data) { self.onComplete(data, query) }, + error : function(jqXHR, message) { console.error(message, query) } + }, + self.opts('ajax') + ); + + $.ajax(opts); + }; + + /** + * Successful call AJAX handler. Takes the data that came back from AJAX and the + * original query that was used to make the call. + * + * @signature TextExtAjax.onComplete(data, query) + * + * @param data {Object} Data loaded from the server, should be an Array of strings + * by default or whatever data structure your custom `ItemManager` implements. + * + * @param query {String} Query string, ie whatever user has typed in. + * + * @author agorbatchev + * @date 2011/08/14 + * @id TextExtAjax.onComplete + */ + p.onComplete = function(data, query) + { + var self = this, + result = data + ; + + self.dontShowLoading(); + + // If results are expected to be cached, then we store the original + // data set and return the filtered one based on the original query. + // That means we do filtering on the client side, instead of the + // server side. + if(self.opts(OPT_CACHE_RESULTS) == true) + { + self._suggestions = data; + result = self.itemManager().filter(data, query); + } + + self.trigger(EVENT_SET_SUGGESTION, { result : result }); + }; + + /** + * If show loading message timer was started, calling this function disables it, + * otherwise nothing else happens. + * + * @signature TextExtAjax.dontShowLoading() + * + * @author agorbatchev + * @date 2011/08/16 + * @id TextExtAjax.dontShowLoading + */ + p.dontShowLoading = function() + { + this.stopTimer(TIMER_LOADING); + }; + + /** + * Shows message specified in `ajax.loading.message` if loading data takes more than + * number of seconds specified in `ajax.loading.delay`. + * + * @signature TextExtAjax.showLoading() + * + * @author agorbatchev + * @date 2011/08/15 + * @id TextExtAjax.showLoading + */ + p.showLoading = function() + { + var self = this; + + self.dontShowLoading(); + self.startTimer( + TIMER_LOADING, + self.opts(OPT_LOADING_DELAY), + function() + { + self.trigger(EVENT_SHOW_DROPDOWN, function(autocomplete) + { + autocomplete.clearItems(); + var node = autocomplete.addDropdownItem(self.opts(OPT_LOADING_MESSAGE)); + node.addClass('text-loading'); + }); + } + ); + }; + + /** + * Reacts to the `getSuggestions` event and begin loading suggestions. If + * `ajax.cache.results` is specified, all calls after the first one will use + * cached data and filter it with the `core.itemManager.filter()`. + * + * @signature TextExtAjax.onGetSuggestions(e, data) + * + * @param e {Object} jQuery event. + * @param data {Object} Data structure passed with the `getSuggestions` event + * which contains the user query, eg `{ query : "..." }`. + * + * @author agorbatchev + * @date 2011/08/15 + * @id TextExtAjax.onGetSuggestions + */ + p.onGetSuggestions = function(e, data) + { + var self = this, + suggestions = self._suggestions, + query = (data || {}).query || '' + ; + + if(suggestions && self.opts(OPT_CACHE_RESULTS) === true) + return self.onComplete(suggestions, query); + + self.startTimer( + 'ajax', + self.opts(OPT_TYPE_DELAY), + function() + { + self.showLoading(); + self.load(query); + } + ); + }; +})(jQuery); +;/** + * jQuery TextExt Plugin + * http://textextjs.com + * + * @version 1.3.0 + * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved. + * @license MIT License + */ +(function($) +{ + /** + * Displays a dropdown style arrow button. The `TextExtArrow` works together with the + * `TextExtAutocomplete` plugin and whenever clicked tells the autocomplete plugin to + * display its suggestions. + * + * @author agorbatchev + * @date 2011/12/27 + * @id TextExtArrow + */ + function TextExtArrow() {}; + + $.fn.textext.TextExtArrow = TextExtArrow; + $.fn.textext.addPlugin('arrow', TextExtArrow); + + var p = TextExtArrow.prototype, + /** + * Arrow plugin only has one option and that is its HTML template. It could be + * changed when passed to the `$().textext()` function. For example: + * + * $('textarea').textext({ + * plugins: 'arrow', + * html: { + * arrow: "" + * } + * }) + * + * @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', + + 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 + ; + + self.baseInit(core, DEFAULT_OPTS); + + self._arrow = arrow = $(self.opts(OPT_HTML_ARROW)); + self.core().wrapElement().append(arrow); + arrow.bind('click', function(e) { self.onArrowClick(e); }); + }; + + //-------------------------------------------------------------------------------- + // Event handlers + + /** + * Reacts to the `click` event whenever user clicks the arrow. + * + * @signature TextExtArrow.onArrowClick(e) + * + * @param e {Object} jQuery event. + * @author agorbatchev + * @date 2011/12/27 + * @id TextExtArrow.onArrowClick + */ + p.onArrowClick = function(e) + { + this.trigger('toggleDropdown'); + this.core().focusInput(); + }; + + //-------------------------------------------------------------------------------- + // Core functionality + +})(jQuery); +;/** + * jQuery TextExt Plugin + * http://textextjs.com + * + * @version 1.3.0 + * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved. + * @license MIT License + */ +(function($) +{ + /** + * Autocomplete plugin brings the classic autocomplete functionality to the TextExt echosystem. + * 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 + .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(typeof suggestion == 'object') { + suggestion = suggestion['name']; + } + if(suggestion) + { + self.val(self.itemManager().itemToString(suggestion)); + self.core().getFormData(); + } + + self.trigger(EVENT_HIDE_DROPDOWN); + }; + + /** + * Determines if the specified HTML element is within the TextExt core wrap HTML element. + * + * @signature TextExtAutocomplete.withinWrapElement(element) + * + * @param element {HTMLElement} element to check if contained by wrap element + * + * @author adamayres + * @version 1.3.0 + * @date 2012/01/15 + * @id TextExtAutocomplete.withinWrapElement + */ + p.withinWrapElement = function(element) + { + return this.core().wrapElement().find(element).size() > 0; + } +})(jQuery); +;/** + * jQuery TextExt Plugin + * http://textextjs.com + * + * @version 1.3.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); +;/** + * jQuery TextExt Plugin + * http://textextjs.com + * + * @version 1.3.0 + * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved. + * @license MIT License + */ +(function($) +{ + /** + * Focus plugin displays a visual effect whenever user sets focus + * into the text area. + * + * @author agorbatchev + * @date 2011/08/18 + * @id TextExtFocus + */ + function TextExtFocus() {}; + + $.fn.textext.TextExtFocus = TextExtFocus; + $.fn.textext.addPlugin('focus', TextExtFocus); + + var p = TextExtFocus.prototype, + /** + * Focus plugin only has one option and that is its HTML template. It could be + * changed when passed to the `$().textext()` function. For example: + * + * $('textarea').textext({ + * plugins: 'focus', + * html: { + * focus: "" + * } + * }) + * + * @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', + + /** + * Focus plugin dispatches or reacts to the following events. + * + * @author agorbatchev + * @date 2011/08/17 + * @id TextExtFocus.events + */ + + /** + * Focus plugin reacts to the `focus` event and shows the markup generated from + * the `html.focus` option. + * + * @name focus + * @author agorbatchev + * @date 2011/08/18 + * @id TextExtFocus.events.focus + */ + + /** + * Focus plugin reacts to the `blur` event and hides the effect. + * + * @name blur + * @author agorbatchev + * @date 2011/08/18 + * @id TextExtFocus.events.blur + */ + + DEFAULT_OPTS = { + html : { + focus : '
' + } + } + ; + + /** + * Initialization method called by the core during plugin instantiation. + * + * @signature TextExtFocus.init(core) + * + * @param core {TextExt} Instance of the TextExt core class. + * + * @author agorbatchev + * @date 2011/08/18 + * @id TextExtFocus.init + */ + p.init = function(core) + { + var self = this; + + self.baseInit(core, DEFAULT_OPTS); + self.core().wrapElement().append(self.opts(OPT_HTML_FOCUS)); + self.on({ + blur : self.onBlur, + focus : self.onFocus + }); + + self._timeoutId = 0; + }; + + //-------------------------------------------------------------------------------- + // Event handlers + + /** + * Reacts to the `blur` event and hides the focus effect with a slight delay which + * allows quick refocusing without effect blinking in and out. + * + * @signature TextExtFocus.onBlur(e) + * + * @param e {Object} jQuery event. + * + * @author agorbatchev + * @date 2011/08/08 + * @id TextExtFocus.onBlur + */ + p.onBlur = function(e) + { + var self = this; + + clearTimeout(self._timeoutId); + + self._timeoutId = setTimeout(function() + { + self.getFocus().hide(); + }, + 100); + }; + + /** + * Reacts to the `focus` event and shows the focus effect. + * + * @signature TextExtFocus.onFocus + * + * @param e {Object} jQuery event. + * @author agorbatchev + * @date 2011/08/08 + * @id TextExtFocus.onFocus + */ + p.onFocus = function(e) + { + var self = this; + + clearTimeout(self._timeoutId); + + self.getFocus().show(); + }; + + //-------------------------------------------------------------------------------- + // Core functionality + + /** + * Returns focus effect HTML element. + * + * @signature TextExtFocus.getFocus() + * + * @author agorbatchev + * @date 2011/08/08 + * @id TextExtFocus.getFocus + */ + p.getFocus = function() + { + return this.core().wrapElement().find('.text-focus'); + }; +})(jQuery); +;/** + * jQuery TextExt Plugin + * http://textextjs.com + * + * @version 1.3.0 + * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved. + * @license MIT License + */ +(function($) +{ + /** + * Prompt plugin displays a visual user propmpt in the text input area. If user focuses + * on the input, the propt is hidden and only shown again when user focuses on another + * element and text input doesn't have a value. + * + * @author agorbatchev + * @date 2011/08/18 + * @id TextExtPrompt + */ + function TextExtPrompt() {}; + + $.fn.textext.TextExtPrompt = TextExtPrompt; + $.fn.textext.addPlugin('prompt', TextExtPrompt); + + var p = TextExtPrompt.prototype, + + CSS_HIDE_PROMPT = 'text-hide-prompt', + + /** + * Prompt plugin has options to change the prompt label and its HTML template. The options + * could be changed when passed to the `$().textext()` function. For example: + * + * $('textarea').textext({ + * plugins: 'prompt', + * prompt: 'Your email address' + * }) + * + * @author agorbatchev + * @date 2011/08/18 + * @id TextExtPrompt.options + */ + + /** + * Prompt message that is displayed to the user whenever there's no value in the input. + * + * @name prompt + * @default 'Awaiting input...' + * @author agorbatchev + * @date 2011/08/18 + * @id TextExtPrompt.options.prompt + */ + OPT_PROMPT = 'prompt', + + /** + * HTML source that is used to generate markup required for the prompt effect. + * + * @name html.prompt + * @default '
' + * @author agorbatchev + * @date 2011/08/18 + * @id TextExtPrompt.options.html.prompt + */ + OPT_HTML_PROMPT = 'html.prompt', + + /** + * Prompt plugin dispatches or reacts to the following events. + * @id TextExtPrompt.events + */ + + /** + * Prompt plugin reacts to the `focus` event and hides the markup generated from + * the `html.prompt` option. + * + * @name focus + * @author agorbatchev + * @date 2011/08/18 + * @id TextExtPrompt.events.focus + */ + + /** + * Prompt plugin reacts to the `blur` event and shows the prompt back if user + * hasn't entered any value. + * + * @name blur + * @author agorbatchev + * @date 2011/08/18 + * @id TextExtPrompt.events.blur + */ + + DEFAULT_OPTS = { + prompt : 'Awaiting input...', + + html : { + prompt : '
' + } + } + ; + + /** + * Initialization method called by the core during plugin instantiation. + * + * @signature TextExtPrompt.init(core) + * + * @param core {TextExt} Instance of the TextExt core class. + * + * @author agorbatchev + * @date 2011/08/18 + * @id TextExtPrompt.init + */ + p.init = function(core) + { + var self = this, + placeholderKey = 'placeholder', + container, + prompt + ; + + self.baseInit(core, DEFAULT_OPTS); + + container = $(self.opts(OPT_HTML_PROMPT)); + $(self).data('container', container); + + self.core().wrapElement().append(container); + self.setPrompt(self.opts(OPT_PROMPT)); + + prompt = core.input().attr(placeholderKey); + + if(!prompt) + prompt = self.opts(OPT_PROMPT); + + // clear placeholder attribute if set + core.input().attr(placeholderKey, ''); + + if(prompt) + self.setPrompt(prompt); + + if($.trim(self.val()).length > 0) + self.hidePrompt(); + + self.on({ + blur : self.onBlur, + focus : self.onFocus, + postInvalidate : self.onPostInvalidate, + postInit : self.onPostInit + }); + }; + + //-------------------------------------------------------------------------------- + // Event handlers + + /** + * Reacts to the `postInit` and configures the plugin for initial display. + * + * @signature TextExtPrompt.onPostInit(e) + * + * @param e {Object} jQuery event. + * + * @author agorbatchev + * @date 2011/08/24 + * @id TextExtPrompt.onPostInit + */ + p.onPostInit = function(e) + { + this.invalidateBounds(); + }; + + /** + * Reacts to the `postInvalidate` and insures that prompt display remains correct. + * + * @signature TextExtPrompt.onPostInvalidate(e) + * + * @param e {Object} jQuery event. + * + * @author agorbatchev + * @date 2011/08/24 + * @id TextExtPrompt.onPostInvalidate + */ + p.onPostInvalidate = function(e) + { + this.invalidateBounds(); + }; + + /** + * Repositions the prompt to make sure it's always at the same place as in the text input carret. + * + * @signature TextExtPrompt.invalidateBounds() + * + * @author agorbatchev + * @date 2011/08/24 + * @id TextExtPrompt.invalidateBounds + */ + p.invalidateBounds = function() + { + var self = this, + input = self.input() + ; + + self.containerElement().css({ + paddingLeft : input.css('paddingLeft'), + paddingTop : input.css('paddingTop') + }); + }; + + /** + * Reacts to the `blur` event and shows the prompt effect with a slight delay which + * allows quick refocusing without effect blinking in and out. + * + * The prompt is restored if the text box has no value. + * + * @signature TextExtPrompt.onBlur(e) + * + * @param e {Object} jQuery event. + * + * @author agorbatchev + * @date 2011/08/08 + * @id TextExtPrompt.onBlur + */ + p.onBlur = function(e) + { + var self = this; + + self.startTimer('prompt', 0.1, function() + { + self.showPrompt(); + }); + }; + + /** + * Shows prompt HTML element. + * + * @signature TextExtPrompt.showPrompt() + * + * @author agorbatchev + * @date 2011/08/22 + * @id TextExtPrompt.showPrompt + */ + p.showPrompt = function() + { + var self = this, + input = self.input() + ; + + if($.trim(self.val()).length === 0 && !input.is(':focus')) + self.containerElement().removeClass(CSS_HIDE_PROMPT); + }; + + /** + * Hides prompt HTML element. + * + * @signature TextExtPrompt.hidePrompt() + * + * @author agorbatchev + * @date 2011/08/22 + * @id TextExtPrompt.hidePrompt + */ + p.hidePrompt = function() + { + this.stopTimer('prompt'); + this.containerElement().addClass(CSS_HIDE_PROMPT); + }; + + /** + * Reacts to the `focus` event and hides the prompt effect. + * + * @signature TextExtPrompt.onFocus + * + * @param e {Object} jQuery event. + * @author agorbatchev + * @date 2011/08/08 + * @id TextExtPrompt.onFocus + */ + p.onFocus = function(e) + { + this.hidePrompt(); + }; + + //-------------------------------------------------------------------------------- + // Core functionality + + /** + * Sets the prompt display to the specified string. + * + * @signature TextExtPrompt.setPrompt(str) + * + * @oaram str {String} String that will be displayed in the prompt. + * + * @author agorbatchev + * @date 2011/08/18 + * @id TextExtPrompt.setPrompt + */ + p.setPrompt = function(str) + { + this.containerElement().text(str); + }; + + /** + * Returns prompt effect HTML element. + * + * @signature TextExtPrompt.containerElement() + * + * @author agorbatchev + * @date 2011/08/08 + * @id TextExtPrompt.containerElement + */ + p.containerElement = function() + { + return $(this).data('container'); + }; +})(jQuery); +;/** + * jQuery TextExt Plugin + * http://textextjs.com + * + * @version 1.3.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); +;/** + * jQuery TextExt Plugin + * http://textextjs.com + * + * @version 1.3.0 + * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved. + * @license MIT License + */ +(function($) +{ + /** + * Tags plugin brings in the traditional tag functionality where user can assemble and + * edit list of tags. Tags plugin works especially well together with Autocomplete, Filter, + * Suggestions and Ajax plugins to provide full spectrum of features. It can also work on + * its own and just do one thing -- tags. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtTags + */ + function TextExtTags() {}; + + $.fn.textext.TextExtTags = TextExtTags; + $.fn.textext.addPlugin('tags', TextExtTags); + + var p = TextExtTags.prototype, + + CSS_DOT = '.', + CSS_TAGS_ON_TOP = 'text-tags-on-top', + CSS_DOT_TAGS_ON_TOP = CSS_DOT + CSS_TAGS_ON_TOP, + CSS_TAG = 'text-tag', + CSS_DOT_TAG = CSS_DOT + CSS_TAG, + CSS_TAGS = 'text-tags', + CSS_DOT_TAGS = CSS_DOT + CSS_TAGS, + CSS_LABEL = 'text-label', + CSS_DOT_LABEL = CSS_DOT + CSS_LABEL, + CSS_REMOVE = 'text-remove', + CSS_DOT_REMOVE = CSS_DOT + CSS_REMOVE, + + /** + * Tags plugin options are grouped under `tags` when passed to the + * `$().textext()` function. For example: + * + * $('textarea').textext({ + * plugins: 'tags', + * tags: { + * items: [ "tag1", "tag2" ] + * } + * }) + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtTags.options + */ + + /** + * This is a toggle switch to enable or disable the Tags plugin. The value is checked + * each time at the top level which allows you to toggle this setting on the fly. + * + * @name tags.enabled + * @default true + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtTags.options.tags.enabled + */ + OPT_ENABLED = 'tags.enabled', + + /** + * Allows to specify tags which will be added to the input by default upon initialization. + * Each item in the array must be of the type that current `ItemManager` can understand. + * Default type is `String`. + * + * @name tags.items + * @default null + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtTags.options.tags.items + */ + OPT_ITEMS = 'tags.items', + + /** + * HTML source that is used to generate a single tag. + * + * @name html.tag + * @default '
' + * @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, + paddingBottom: 4 + }); + }; + + /** + * Reacts to the mouse `click` event. + * + * @signature TextExtTags.onClick(e) + * + * @param e {Object} jQuery event. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtTags.onClick + */ + p.onClick = function(e) + { + var self = this, + core = self.core(), + source = $(e.target), + focus = 0, + tag + ; + + if(source.is(CSS_DOT_TAGS)) + { + focus = 1; + } + else if(source.is(CSS_DOT_REMOVE)) + { + self.removeTag(source.parents(CSS_DOT_TAG + ':first')); + focus = 1; + } + else if(source.is(CSS_DOT_LABEL)) + { + tag = source.parents(CSS_DOT_TAG + ':first'); + self.trigger(EVENT_TAG_CLICK, tag, tag.data(CSS_TAG), tagClickCallback); + } + + function tagClickCallback(newValue, refocus) + { + tag.data(CSS_TAG, newValue); + tag.find(CSS_DOT_LABEL).text(self.itemManager().itemToString(newValue)); + + self.updateFormCache(); + core.getFormData(); + core.invalidateBounds(); + + if(refocus) + core.focusInput(); + } + + if(focus) + core.focusInput(); + }; + + /** + * Reacts to the `enterKeyPress` event and adds whatever is currently in the text input + * as a new tag. Triggers `isTagAllowed` to check if the tag could be added first. + * + * @signature TextExtTags.onEnterKeyPress(e) + * + * @param e {Object} jQuery event. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtTags.onEnterKeyPress + */ + p.onEnterKeyPress = function(e) + { + var self = this, + val = self.val(), + tag = self.itemManager().stringToItem(val) + ; + + if(self.isTagAllowed(tag)) + { + self.addTags([ tag ]); + // refocus the textarea just in case it lost the focus + self.core().focusInput(); + } + }; + + //-------------------------------------------------------------------------------- + // Core functionality + + /** + * Creates a cache object with all the tags currently added which will be returned + * in the `onGetFormData` handler. + * + * @signature TextExtTags.updateFormCache() + * + * @author agorbatchev + * @date 2011/08/09 + * @id TextExtTags.updateFormCache + */ + p.updateFormCache = function() + { + var self = this, + result = [] + ; + + self.tagElements().each(function() + { + result.push($(this).data(CSS_TAG)); + }); + + // cache the results to be used in the onGetFormData + self._formData = result; + }; + + /** + * Toggles tag container to be on top of the text area or under based on where + * the mouse cursor is located. When cursor is above the text input and out of + * any of the tags, the tags container is sent under the text area. If cursor + * is over any of the tags, the tag container is brought to be over the text + * area. + * + * @signature TextExtTags.toggleZIndex(e) + * + * @param e {Object} jQuery event. + * + * @author agorbatchev + * @date 2011/08/08 + * @id TextExtTags.toggleZIndex + */ + p.toggleZIndex = function(e) + { + var self = this, + offset = self.input().offset(), + mouseX = e.clientX - offset.left, + mouseY = e.clientY - offset.top, + box = self._paddingBox, + container = self.containerElement(), + isOnTop = container.is(CSS_DOT_TAGS_ON_TOP), + isMouseOverText = mouseX > box.left && mouseY > box.top + ; + + if(!isOnTop && !isMouseOverText || isOnTop && isMouseOverText) + container[(!isOnTop ? 'add' : 'remove') + 'Class'](CSS_TAGS_ON_TOP); + }; + + /** + * Returns all tag HTML elements. + * + * @signature TextExtTags.tagElements() + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtTags.tagElements + */ + p.tagElements = function() + { + return this.containerElement().find(CSS_DOT_TAG); + }; + + /** + * Wrapper around the `isTagAllowed` event which triggers it and returns `true` + * if `result` property of the second argument remains `true`. + * + * @signature TextExtTags.isTagAllowed(tag) + * + * @param tag {Object} Tag object that the current `ItemManager` can understand. + * Default is `String`. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtTags.isTagAllowed + */ + p.isTagAllowed = function(tag) + { + var opts = { tag : tag, result : true }; + this.trigger(EVENT_IS_TAG_ALLOWED, opts); + return opts.result === true; + }; + + /** + * Adds specified tags to the tag list. Triggers `isTagAllowed` event for each tag + * to insure that it could be added. Calls `TextExt.getFormData()` to refresh the data. + * + * @signature TextExtTags.addTags(tags) + * + * @param tags {Array} List of tags that current `ItemManager` can understand. Default + * is `String`. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtTags.addTags + */ + p.addTags = function(tags) + { + if(!tags || tags.length == 0) + return; + + var self = this, + core = self.core(), + container = self.containerElement(), + i, tag + ; + + for(i = 0; i < tags.length; i++) + { + tag = tags[i]; + + if(tag && self.isTagAllowed(tag)) + container.append(self.renderTag(tag)); + } + + self.updateFormCache(); + core.getFormData(); + core.invalidateBounds(); + }; + + /** + * Returns HTML element for the specified tag. + * + * @signature TextExtTags.getTagElement(tag) + * + * @param tag {Object} Tag object in the format that current `ItemManager` can understand. + * Default is `String`. + + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtTags.getTagElement + */ + p.getTagElement = function(tag) + { + var self = this, + list = self.tagElements(), + i, item + ; + + for(i = 0; i < list.length, item = $(list[i]); i++) + if(self.itemManager().compareItems(item.data(CSS_TAG), tag)) + return item; + }; + + /** + * Removes specified tag from the list. Calls `TextExt.getFormData()` to refresh the data. + * + * @signature TextExtTags.removeTag(tag) + * + * @param tag {Object} Tag object in the format that current `ItemManager` can understand. + * Default is `String`. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtTags.removeTag + */ + p.removeTag = function(tag) + { + var self = this, + core = self.core(), + element + ; + + if(tag instanceof $) + { + element = tag; + tag = tag.data(CSS_TAG); + } + else + { + element = self.getTagElement(tag); + } + + element.remove(); + self.updateFormCache(); + core.getFormData(); + core.invalidateBounds(); + }; + + /** + * Creates and returns new HTML element from the source code specified in the `html.tag` option. + * + * @signature TextExtTags.renderTag(tag) + * + * @param tag {Object} Tag object in the format that current `ItemManager` can understand. + * Default is `String`. + * + * @author agorbatchev + * @date 2011/08/19 + * @id TextExtTags.renderTag + */ + p.renderTag = function(tag) + { + var self = this, + node = $(self.opts(OPT_HTML_TAG)) + ; + node.find('.text-label').text(self.itemManager().itemToString(tag)); + node.data(CSS_TAG, tag); + return node; + }; +})(jQuery); \ No newline at end of file From 2f07494fa48ff7c54d8eb60bc08b1496f227a014 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Thu, 3 May 2012 17:33:05 +0200 Subject: [PATCH 02/15] [imp] tagged old m2m as 61 bzr revid: nicolas.vanhoren@openerp.com-20120503153305-vxdras5o5bb1t7zp --- addons/web/static/src/js/view_form.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 09dd69074d8..00c64b66b74 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -3039,7 +3039,7 @@ instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({ /* * TODO niv: clean those deferred stuff, it could be better */ -instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ +instance.web.form.FieldMany2Many61 = instance.web.form.AbstractField.extend({ multi_selection: false, disable_utility_classes: true, init: function(field_manager, node) { @@ -3053,7 +3053,7 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ var self = this; - this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation); + this.dataset = new instance.web.form.Many2ManyDataSet61(this, this.field.relation); this.dataset.m2m = this; this.dataset.on_unlink.add_last(function(ids) { self.dataset_changed(); @@ -3086,7 +3086,7 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ }, load_view: function() { var self = this; - this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, { + this.list_view = new instance.web.form.Many2ManyListView61(this, this.dataset, false, { 'addable': self.get("effective_readonly") ? null : _t("Add"), 'deletable': self.get("effective_readonly") ? false : true, 'selectable': self.multi_selection, @@ -3118,7 +3118,7 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ }, }); -instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({ +instance.web.form.Many2ManyDataSet61 = instance.web.DataSetStatic.extend({ get_context: function() { this.context = this.m2m.build_context(); return this.context; @@ -3129,7 +3129,7 @@ instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({ * @class * @extends instance.web.ListView */ -instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{ +instance.web.form.Many2ManyListView61 = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView61# */{ do_add_record: function () { var pop = new instance.web.form.SelectCreatePopup(this); pop.select_element( @@ -3822,7 +3822,8 @@ instance.web.form.widgets = new instance.web.Registry({ 'datetime' : 'instance.web.form.FieldDatetime', 'selection' : 'instance.web.form.FieldSelection', 'many2one' : 'instance.web.form.FieldMany2One', - 'many2many' : 'instance.web.form.FieldMany2Many', + 'many2many' : 'instance.web.form.FieldMany2Many61', + 'many2many61' : 'instance.web.form.FieldMany2Many61', 'one2many' : 'instance.web.form.FieldOne2Many', 'one2many_list' : 'instance.web.form.FieldOne2Many', 'reference' : 'instance.web.form.FieldReference', From 29e02f62aa567be28108463451077ca15c408985 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 4 May 2012 13:06:15 +0200 Subject: [PATCH 03/15] wip bzr revid: nicolas.vanhoren@openerp.com-20120504110615-xk8u7x7d7zmnb4a5 --- addons/web/static/src/js/view_form.js | 39 ++++++++++++++++++++++++++- addons/web/static/src/xml/base.xml | 5 ++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 00c64b66b74..0cfe753b881 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -3036,6 +3036,43 @@ instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({ }); } +instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ + template: "FieldMany2Many", + start: function() { + $("textarea", this.$element).textext({ + plugins : 'arrow prompt autocomplete', + prompt : "Add one...", + autocomplete: { + render: function(suggestion) { + return $('', {'data-id': suggestion['id']}).html(suggestion['label']); + } + }, + }).bind('getSuggestions', function(e, data) { + var list = [ + 'Basic', + 'Closure', + 'Cobol', + 'Delphi', + 'Erlang', + 'Fortran', + 'Go', + 'Groovy', + 'Haskel', + 'Java', + 'JavaScript', + 'OCAML', + 'PHP', + 'Perl', + 'Python', + 'Ruby', + 'Scala' + ]; + + $(this).trigger('setSuggestions', {result : _.map(list, function(el) {return {id:1, label:el};})}); + }); + }, +}); + /* * TODO niv: clean those deferred stuff, it could be better */ @@ -3822,7 +3859,7 @@ instance.web.form.widgets = new instance.web.Registry({ 'datetime' : 'instance.web.form.FieldDatetime', 'selection' : 'instance.web.form.FieldSelection', 'many2one' : 'instance.web.form.FieldMany2One', - 'many2many' : 'instance.web.form.FieldMany2Many61', + 'many2many' : 'instance.web.form.FieldMany2Many', 'many2many61' : 'instance.web.form.FieldMany2Many61', 'one2many' : 'instance.web.form.FieldOne2Many', 'one2many_list' : 'instance.web.form.FieldOne2Many', diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 84179ea0042..5c9fc166471 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1038,6 +1038,11 @@ + +
+ +
+
From d0991554553af1f0f05708c7f9e48c8457c85ded Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 4 May 2012 15:31:11 +0200 Subject: [PATCH 04/15] [imp] switched many2many & many2manytags bzr revid: nicolas.vanhoren@openerp.com-20120504133111-2c5v5jmiaj15islk --- addons/web/static/src/js/view_form.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 61e1521dd6b..675b01b4dee 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -3037,7 +3037,7 @@ instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({ }); } -instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ +instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend({ template: "FieldMany2Many", start: function() { $("textarea", this.$element).textext({ @@ -3077,7 +3077,7 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ /* * TODO niv: clean those deferred stuff, it could be better */ -instance.web.form.FieldMany2Many61 = instance.web.form.AbstractField.extend({ +instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ multi_selection: false, disable_utility_classes: true, init: function(field_manager, node) { @@ -3091,7 +3091,7 @@ instance.web.form.FieldMany2Many61 = instance.web.form.AbstractField.extend({ var self = this; - this.dataset = new instance.web.form.Many2ManyDataSet61(this, this.field.relation); + this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation); this.dataset.m2m = this; this.dataset.on_unlink.add_last(function(ids) { self.dataset_changed(); @@ -3124,7 +3124,7 @@ instance.web.form.FieldMany2Many61 = instance.web.form.AbstractField.extend({ }, load_view: function() { var self = this; - this.list_view = new instance.web.form.Many2ManyListView61(this, this.dataset, false, { + this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, { 'addable': self.get("effective_readonly") ? null : _t("Add"), 'deletable': self.get("effective_readonly") ? false : true, 'selectable': self.multi_selection, @@ -3156,7 +3156,7 @@ instance.web.form.FieldMany2Many61 = instance.web.form.AbstractField.extend({ }, }); -instance.web.form.Many2ManyDataSet61 = instance.web.DataSetStatic.extend({ +instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({ get_context: function() { this.context = this.m2m.build_context(); return this.context; @@ -3167,7 +3167,7 @@ instance.web.form.Many2ManyDataSet61 = instance.web.DataSetStatic.extend({ * @class * @extends instance.web.ListView */ -instance.web.form.Many2ManyListView61 = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView61# */{ +instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{ do_add_record: function () { var pop = new instance.web.form.SelectCreatePopup(this); pop.select_element( @@ -3861,7 +3861,7 @@ instance.web.form.widgets = new instance.web.Registry({ 'selection' : 'instance.web.form.FieldSelection', 'many2one' : 'instance.web.form.FieldMany2One', 'many2many' : 'instance.web.form.FieldMany2Many', - 'many2many61' : 'instance.web.form.FieldMany2Many61', + 'many2manytags' : 'instance.web.form.FieldMany2ManyTags', 'one2many' : 'instance.web.form.FieldOne2Many', 'one2many_list' : 'instance.web.form.FieldOne2Many', 'reference' : 'instance.web.form.FieldReference', From 1950963f07cbb87631ca81a40ebd6f237e726fc3 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Mon, 7 May 2012 14:22:46 +0200 Subject: [PATCH 05/15] [wip] bzr revid: nicolas.vanhoren@openerp.com-20120507122246-q4f06v9n2et27aqp --- addons/web/__openerp__.py | 1 - .../lib/jquery.textext/jquery.textext.css | 186 ------------ .../lib/jquery.textext/jquery.textext.js | 167 ++++++++++- addons/web/static/src/js/view_form.js | 269 +++++++++++------- 4 files changed, 320 insertions(+), 303 deletions(-) delete mode 100644 addons/web/static/lib/jquery.textext/jquery.textext.css diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py index da67fd60196..26ee5613dc5 100644 --- a/addons/web/__openerp__.py +++ b/addons/web/__openerp__.py @@ -74,7 +74,6 @@ "static/lib/jquery.ui.timepicker/css/jquery-ui-timepicker-addon.css", "static/lib/jquery.ui.notify/css/ui.notify.css", "static/lib/jquery.tipsy/tipsy.css", - "static/lib/jquery.textext/jquery.textext.css", "static/lib/visualsearch/lib/css/reset.css", "static/lib/visualsearch/lib/css/workspace.css", "static/lib/visualsearch/lib/css/icons.css", diff --git a/addons/web/static/lib/jquery.textext/jquery.textext.css b/addons/web/static/lib/jquery.textext/jquery.textext.css deleted file mode 100644 index e4f2dbc193a..00000000000 --- a/addons/web/static/lib/jquery.textext/jquery.textext.css +++ /dev/null @@ -1,186 +0,0 @@ -.text-core { - position: relative; -} - -.text-core .text-wrap { - position: absolute; - width: 100% !important; -} -.text-core .text-wrap textarea { - resize: none !important; -} - -.text-core .text-wrap textarea, .text-core .text-wrap input { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - -webkit-border-radius: 0px; - -moz-border-radius: 0px; - border-radius: 0px; - outline: none; - resize: none; - position: absolute; - background: none; - overflow: hidden; - margin: 0; - padding: 3px 5px 4px 5px; - white-space: nowrap; - line-height: 13px; - width: 100% !important; -} - -.extra_suggestions { - border-top: 1px solid #9daccc !important; -} - -.text-core .text-wrap .text-arrow { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - position: absolute; - top: 0; - right: 0; - width: 22px; - height: 22px; - background: url("/web/static/src/img/down-arrow.png") 50% 50% no-repeat; - cursor: pointer; - z-index: 2; -} - -.text-core .text-wrap .text-dropdown { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - padding: 0; - position: absolute; - z-index: 3; - background: #fff; - border: 1px solid #9daccc; - width: 100%; - padding: 1px; - display: none; - overflow-x: hidden; - overflow-y: auto; -} - -.text-core .text-wrap .text-dropdown.text-position-below { - margin-top: 1px; -} - -.text-core .text-wrap .text-dropdown.text-position-above { - margin-bottom: 1px; -} - -.text-core .text-wrap .text-dropdown .text-list .text-suggestion { - padding: 3px 5px; - cursor: pointer; -} - -.text-core .text-wrap .text-dropdown .text-list .text-suggestion em { - font-style: normal; - text-decoration: underline; -} - -.text-core .text-wrap .text-dropdown .text-list .text-suggestion.text-selected { - color: #fff; - background: #0064CD; -} - -.text-core .text-wrap .text-focus { - -webkit-box-shadow: 0px 0px 6px #6d84b4; - -moz-box-shadow: 0px 0px 6px #6d84b4; - box-shadow: 0px 0px 6px #6d84b4; - position: absolute; - display: none; -} - -.text-core .text-wrap .text-focus.text-show-focus { - display: block; -} - -.text-core .text-wrap .text-prompt { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - position: absolute; - width: 100%; - height: 100%; - margin: 1px 0 0 2px; - color: #c0c0c0; - overflow: hidden; - white-space: pre; -} - -.text-core .text-wrap .text-prompt.text-hide-prompt { - display: none; -} - -.text-core .text-wrap .text-tags { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - position: absolute; - width: 100%; - height: 100%; - padding: 3px 35px 3px 3px; - cursor: text; -} - -.text-core .text-wrap .text-tags.text-tags-on-top { - z-index: 2; -} - -.text-core .text-wrap .text-tags .text-tag { - float: left; -} - -.text-core .text-wrap .text-tags .text-tag .text-button { - -webkit-border-radius: 2px; - -moz-border-radius: 2px; - border-radius: 2px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - position: relative; - float: left; - border: 1px solid #9daccc; - background: #e2e6f0; - color: #000; - padding: 0px 17px 0px 3px; - margin: 0 2px 2px 0; - cursor: pointer; - height: 18px; -} - -.text-core .text-wrap .text-tags .text-tag .text-button a.text-remove { - position: absolute; - right: 3px; - top: 2px; - display: block; - width: 11px; - height: 11px; - background: url("remove.png") 0 0 no-repeat; -} - -.text-core .text-wrap .text-tags .text-tag .text-button a.text-remove:hover { - background-position: 0 -11px; -} - -.text-core .text-wrap .text-tags .text-tag .text-button a.text-remove:active { - background-position: 0 -22px; -} -.oe_form_field_many2many span { - display: block; - width: auto; - margin: 0 3px 3px 0; - padding: 3px 8px 4px 8px; - position: relative; - float: left; - text-indent: 0; - background-color: #EEE; - border: 1px solid #AAA; - -moz-border-radius: 7px; - -webkit-border-radius: 7px; - border-radius: 7px; - color: #333; -} diff --git a/addons/web/static/lib/jquery.textext/jquery.textext.js b/addons/web/static/lib/jquery.textext/jquery.textext.js index 2a66d33108f..294ceea8c73 100644 --- a/addons/web/static/lib/jquery.textext/jquery.textext.js +++ b/addons/web/static/lib/jquery.textext/jquery.textext.js @@ -1811,7 +1811,7 @@ var self = this; self.baseInit(core, DEFAULT_OPTS); - console.log('on>>>>>', self, core, DEFAULT_OPTS); + self.on({ getSuggestions : self.onGetSuggestions }); @@ -2375,6 +2375,7 @@ }); container + .css('maxHeight', self.opts(OPT_MAX_HEIGHT)) .addClass('text-position-' + self.opts(OPT_POSITION)) ; @@ -3155,9 +3156,7 @@ var self = this, suggestion = self.selectedSuggestionElement().data(CSS_SUGGESTION) ; - if(typeof suggestion == 'object') { - suggestion = suggestion['name']; - } + if(suggestion) { self.val(self.itemManager().itemToString(suggestion)); @@ -4480,8 +4479,7 @@ self.input().css({ paddingLeft : pos.left, - paddingTop : pos.top, - paddingBottom: 4 + paddingTop : pos.top }); }; @@ -4770,8 +4768,163 @@ 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); \ No newline at end of file +})(jQuery); +;(function(a){var b=document,c="getElementsByTagName",d=b[c]("head")[0]||b[c]("body")[0],e=b.createElement("style");e.innerHTML=a,d.appendChild(e)})('\n.text-core {\ + position: relative;\ +}\ +.text-core .text-wrap {\ + background: #fff;\ + position: absolute;\ +}\ +.text-core .text-wrap textarea, .text-core .text-wrap input {\ + -webkit-box-sizing: border-box;\ + -moz-box-sizing: border-box;\ + box-sizing: border-box;\ + -webkit-border-radius: 0px;\ + -moz-border-radius: 0px;\ + border-radius: 0px;\ + border: 1px solid #9daccc;\ + outline: none;\ + resize: none;\ + position: absolute;\ + z-index: 1;\ + background: none;\ + overflow: hidden;\ + margin: 0;\ + padding: 3px 5px 4px 5px;\ + white-space: nowrap;\ + font: 11px "lucida grande", tahoma, verdana, arial, sans-serif;\ + line-height: 13px;\ + height: auto;\ +}\ +\n.text-core .text-wrap .text-arrow {\ + -webkit-box-sizing: border-box;\ + -moz-box-sizing: border-box;\ + box-sizing: border-box;\ + position: absolute;\ + top: 0;\ + right: 0;\ + width: 22px;\ + height: 22px;\ + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAOAQMAAADHWqTrAAAAA3NCSVQICAjb4U/gAAAABlBMVEX///8yXJnt8Ns4AAAACXBIWXMAAAsSAAALEgHS3X78AAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1MzmNZGAwAAABpJREFUCJljYEAF/xsY6hkY7BgYZBgYOFBkADkdAmFDagYFAAAAAElFTkSuQmCC") 50% 50% no-repeat;\ + cursor: pointer;\ + z-index: 2;\ +}\ +\n.text-core .text-wrap .text-dropdown {\ + -webkit-box-sizing: border-box;\ + -moz-box-sizing: border-box;\ + box-sizing: border-box;\ + padding: 0;\ + position: absolute;\ + z-index: 3;\ + background: #fff;\ + border: 1px solid #9daccc;\ + width: 100%;\ + max-height: 100px;\ + padding: 1px;\ + font: 11px "lucida grande", tahoma, verdana, arial, sans-serif;\ + display: none;\ + overflow-x: hidden;\ + overflow-y: auto;\ +}\ +.text-core .text-wrap .text-dropdown.text-position-below {\ + margin-top: 1px;\ +}\ +.text-core .text-wrap .text-dropdown.text-position-above {\ + margin-bottom: 1px;\ +}\ +.text-core .text-wrap .text-dropdown .text-list .text-suggestion {\ + padding: 3px 5px;\ + cursor: pointer;\ +}\ +.text-core .text-wrap .text-dropdown .text-list .text-suggestion em {\ + font-style: normal;\ + text-decoration: underline;\ +}\ +.text-core .text-wrap .text-dropdown .text-list .text-suggestion.text-selected {\ + color: #fff;\ + background: #6d84b4;\ +}\ +\n.text-core .text-wrap .text-focus {\ + -webkit-box-shadow: 0px 0px 6px #6d84b4;\ + -moz-box-shadow: 0px 0px 6px #6d84b4;\ + box-shadow: 0px 0px 6px #6d84b4;\ + position: absolute;\ + width: 100%;\ + height: 100%;\ + display: none;\ +}\ +.text-core .text-wrap .text-focus.text-show-focus {\ + display: block;\ +}\ +\n.text-core .text-wrap .text-prompt {\ + -webkit-box-sizing: border-box;\ + -moz-box-sizing: border-box;\ + box-sizing: border-box;\ + position: absolute;\ + width: 100%;\ + height: 100%;\ + margin: 1px 0 0 2px;\ + font: 11px "lucida grande", tahoma, verdana, arial, sans-serif;\ + color: #c0c0c0;\ + overflow: hidden;\ + white-space: pre;\ +}\ +.text-core .text-wrap .text-prompt.text-hide-prompt {\ + display: none;\ +}\ +\n.text-core .text-wrap .text-tags {\ + -webkit-box-sizing: border-box;\ + -moz-box-sizing: border-box;\ + box-sizing: border-box;\ + position: absolute;\ + width: 100%;\ + height: 100%;\ + padding: 3px 35px 3px 3px;\ + cursor: text;\ +}\ +.text-core .text-wrap .text-tags.text-tags-on-top {\ + z-index: 2;\ +}\ +.text-core .text-wrap .text-tags .text-tag {\ + float: left;\ +}\ +.text-core .text-wrap .text-tags .text-tag .text-button {\ + -webkit-border-radius: 2px;\ + -moz-border-radius: 2px;\ + border-radius: 2px;\ + -webkit-box-sizing: border-box;\ + -moz-box-sizing: border-box;\ + box-sizing: border-box;\ + position: relative;\ + float: left;\ + border: 1px solid #9daccc;\ + background: #e2e6f0;\ + color: #000;\ + padding: 0px 17px 0px 3px;\ + margin: 0 2px 2px 0;\ + cursor: pointer;\ + height: 16px;\ + font: 11px "lucida grande", tahoma, verdana, arial, sans-serif;\ +}\ +.text-core .text-wrap .text-tags .text-tag .text-button a.text-remove {\ + position: absolute;\ + right: 3px;\ + top: 2px;\ + display: block;\ + width: 11px;\ + height: 11px;\ + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAhCAYAAAAPm1F2AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAAB50RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNS4xqx9I6wAAAQ5JREFUOI2dlD0WwiAQhCc8L6HHgAPoASwtSYvX8BrQxtIyveYA8RppLO1jE+LwE8lzms2yH8MCj1QoaBzH+VuUYNYMS213UlvDRamtUbXb5ZyPHuDoxwGgip3ipfvGuGzPz+vZ/coDONdzFuYCO6ramQQG0DJIE1oPBBvM6e9LqaS2FwD7FWwnVoIAsOc2Xn1jDlyd8pfPBRVOBHA8cc/3yCmQqt0jcY4LuTyAF3pOYS6wI48LAm4MUrx5JthgSQJAt5LtNgAUgEMBBIC3AL2xgo58dEPfhE9wygef89FtCeC49UwltR1pQrK2qr9vNr7uRTCBF3pOYS6wI4/zdQ8MUpxPI9hgSQL0Xyio/QBt54DzsHQx6gAAAABJRU5ErkJggg==") 0 0 no-repeat;\ +}\ +.text-core .text-wrap .text-tags .text-tag .text-button a.text-remove:hover {\ + background-position: 0 -11px;\ +}\ +.text-core .text-wrap .text-tags .text-tag .text-button a.text-remove:active {\ + background-position: 0 -22px;\ +}\ +'); \ No newline at end of file diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 675b01b4dee..3d6fc9c9525 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -2305,17 +2305,114 @@ instance.web.form.dialog = function(content, options) { return dialog.$element; }; -instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { +/** + * A mixin containing some useful methods to handle completion inputs. + */ +instance.web.form.CompletionFieldMixin = { + init: function() { + this.limit = 7; + this.orderer = new instance.web.DropMisordered(); + }, + /** + * Call this method to search using a string. + */ + get_search_result: function(request, response) { + var search_val = request.term; + var self = this; + + var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context()); + + this.orderer.add(dataset.name_search( + search_val, self.build_domain(), 'ilike', this.limit + 1)).then(function(data) { + self.last_search = data; + // possible selections for the m2o + var values = _.map(data, function(x) { + return { + label: _.str.escapeHTML(x[1]), + value:x[1], + name:x[1], + id:x[0] + }; + }); + + // search more... if more results that max + if (values.length > self.limit) { + values = values.slice(0, self.limit); + values.push({label: _t("   Search More..."), action: function() { + dataset.name_search(search_val, self.build_domain(), 'ilike' + , false, function(data) { + self._search_create_popup("search", data); + }); + }}); + } + // quick create + var raw_result = _(data.result).map(function(x) {return x[1];}); + if (search_val.length > 0 && + !_.include(raw_result, search_val) && + (!self.get("value") || self.floating)) { + values.push({label: _.str.sprintf(_t('   Create "%s"'), + $('').text(search_val).html()), action: function() { + self._quick_create(search_val); + }}); + } + // create... + values.push({label: _t("   Create and Edit..."), action: function() { + self._search_create_popup("form", undefined, {"default_name": search_val}); + }}); + + response(values); + }); + }, + _quick_create: function(name) { + var self = this; + var slow_create = function () { + self._search_create_popup("form", undefined, {"default_name": name}); + }; + if (self.get_definition_options().quick_create === undefined || self.get_definition_options().quick_create) { + new instance.web.DataSet(this, this.field.relation, self.build_context()) + .name_create(name, function(data) { + self.display_value = {}; + self.display_value["" + data[0]] = data[1]; + self.set({value: data[0]}); + }).fail(function(error, event) { + event.preventDefault(); + slow_create(); + }); + } else + slow_create(); + }, + // all search/create popup handling + _search_create_popup: function(view, ids, context) { + var self = this; + var pop = new instance.web.form.SelectCreatePopup(this); + pop.select_element( + self.field.relation, + { + title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + (this.string || this.name), + initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined, + initial_view: view, + disable_multiple_selection: true + }, + self.build_domain(), + new instance.web.CompoundContext(self.build_context(), context || {}) + ); + pop.on_select_elements.add(function(element_ids) { + self.set({value: element_ids[0]}); + }); + }, +}; + +instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, + instance.web.form.CompletionFieldMixin, { template: "FieldMany2One", init: function(field_manager, node) { this._super(field_manager, node); - this.limit = 7; + instance.web.form.CompletionFieldMixin.init.call(this); this.set({'value': false}); this.display_value = {}; this.last_search = []; this.floating = false; this.inhibit_on_change = false; - this.orderer = new instance.web.DropMisordered(); }, start: function() { this._super(); @@ -2459,91 +2556,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten isSelecting = false; }); }, - // autocomplete component content handling - get_search_result: function(request, response) { - var search_val = request.term; - var self = this; - var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context()); - - this.orderer.add(dataset.name_search( - search_val, self.build_domain(), 'ilike', this.limit + 1)).then(function(data) { - self.last_search = data; - // possible selections for the m2o - var values = _.map(data, function(x) { - return { - label: _.str.escapeHTML(x[1]), - value:x[1], - name:x[1], - id:x[0] - }; - }); - - // search more... if more results that max - if (values.length > self.limit) { - values = values.slice(0, self.limit); - values.push({label: _t("   Search More..."), action: function() { - dataset.name_search(search_val, self.build_domain(), 'ilike' - , false, function(data) { - self._search_create_popup("search", data); - }); - }}); - } - // quick create - var raw_result = _(data.result).map(function(x) {return x[1];}); - if (search_val.length > 0 && - !_.include(raw_result, search_val) && - (!self.get("value") || self.floating)) { - values.push({label: _.str.sprintf(_t('   Create "%s"'), - $('').text(search_val).html()), action: function() { - self._quick_create(search_val); - }}); - } - // create... - values.push({label: _t("   Create and Edit..."), action: function() { - self._search_create_popup("form", undefined, {"default_name": search_val}); - }}); - - response(values); - }); - }, - _quick_create: function(name) { - var self = this; - var slow_create = function () { - self._search_create_popup("form", undefined, {"default_name": name}); - }; - if (self.get_definition_options().quick_create === undefined || self.get_definition_options().quick_create) { - new instance.web.DataSet(this, this.field.relation, self.build_context()) - .name_create(name, function(data) { - self.display_value = {}; - self.display_value["" + data[0]] = data[1]; - self.set({value: data[0]}); - }).fail(function(error, event) { - event.preventDefault(); - slow_create(); - }); - } else - slow_create(); - }, - // all search/create popup handling - _search_create_popup: function(view, ids, context) { - var self = this; - var pop = new instance.web.form.SelectCreatePopup(this); - pop.select_element( - self.field.relation, - { - title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + (this.string || this.name), - initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined, - initial_view: view, - disable_multiple_selection: true - }, - self.build_domain(), - new instance.web.CompoundContext(self.build_context(), context || {}) - ); - pop.on_select_elements.add(function(element_ids) { - self.set({value: element_ids[0]}); - }); - }, render_value: function(no_recurse) { var self = this; if (! this.get("value")) { @@ -3039,37 +3052,75 @@ instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend({ template: "FieldMany2Many", + init: function() { + this._super.apply(this, arguments); + this.limit = 7; + this.orderer = new instance.web.DropMisordered(); + }, start: function() { - $("textarea", this.$element).textext({ + var self = this; + var textarea = $("textarea", this.$element).textext({ plugins : 'arrow prompt autocomplete', prompt : "Add one...", autocomplete: { render: function(suggestion) { - return $('', {'data-id': suggestion['id']}).html(suggestion['label']); + return $('
', {'data-index': suggestion['index']}).html(suggestion['label']); } }, + ext: { + autocomplete: { + selectFromDropdown: function(a, b, c) { + var index = this.selectedSuggestionElement().children().children().data('index'); + debugger; + $(this).trigger('hideDropdown'); + }, + }, + }, }).bind('getSuggestions', function(e, data) { - var list = [ - 'Basic', - 'Closure', - 'Cobol', - 'Delphi', - 'Erlang', - 'Fortran', - 'Go', - 'Groovy', - 'Haskel', - 'Java', - 'JavaScript', - 'OCAML', - 'PHP', - 'Perl', - 'Python', - 'Ruby', - 'Scala' - ]; + var str = !!data ? data.query || '' : ''; + self.get_search_result(str); + }); + }, + get_search_result: function(str) { + var self = this; + var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context()); + this.orderer.add(dataset.name_search( + str, self.build_domain(), 'ilike', this.limit + 1)).then(function(data) { + + // possible selections for the m2m + self.values = _.map(data, function(x) { + return { + type: 'data', + label: _.str.escapeHTML(x[1]), + data: x, + }; + }); - $(this).trigger('setSuggestions', {result : _.map(list, function(el) {return {id:1, label:el};})}); + // search more... if more results than max + if (values.length > self.limit) { + values = values.slice(0, self.limit); + values.push({type: 'action', label: _t("   Search More..."), action: function() { + dataset.name_search(search_val, self.build_domain(), 'ilike' + , false, function(data) { + self._search_create_popup("search", data); + }); + }}); + } + // quick create + var raw_result = _(data.result).map(function(x) {return x[1];}); + if (search_val.length > 0 && + !_.include(raw_result, search_val)) { + values.push({label: _.str.sprintf(_t('   Create "%s"'), + $('').text(search_val).html()), action: function() { + self._quick_create(search_val); + }}); + } + // create... + values.push({label: _t("   Create and Edit..."), action: function() { + self._search_create_popup("form", undefined, {"default_name": search_val}); + }}); + + $(this).trigger('setSuggestions', {result : _.map(list, function(el, i) {return {index:i, label:el};})}); }); }, }); From d20b5295628cedef0593391de8af46493b884350 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Mon, 7 May 2012 14:37:28 +0200 Subject: [PATCH 06/15] [imp] small refactoring to be able to use get_search_result in m2m bzr revid: nicolas.vanhoren@openerp.com-20120507123728-31bya7tgoh8vquig --- addons/web/static/src/js/view_form.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 3d6fc9c9525..7463df1c50f 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -2316,14 +2316,13 @@ instance.web.form.CompletionFieldMixin = { /** * Call this method to search using a string. */ - get_search_result: function(request, response) { - var search_val = request.term; + get_search_result: function(search_val) { var self = this; var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context()); - this.orderer.add(dataset.name_search( - search_val, self.build_domain(), 'ilike', this.limit + 1)).then(function(data) { + return this.orderer.add(dataset.name_search( + search_val, self.build_domain(), 'ilike', this.limit + 1)).pipe(function(data) { self.last_search = data; // possible selections for the m2o var values = _.map(data, function(x) { @@ -2347,9 +2346,7 @@ instance.web.form.CompletionFieldMixin = { } // quick create var raw_result = _(data.result).map(function(x) {return x[1];}); - if (search_val.length > 0 && - !_.include(raw_result, search_val) && - (!self.get("value") || self.floating)) { + if (search_val.length > 0 && !_.include(raw_result, search_val)) { values.push({label: _.str.sprintf(_t('   Create "%s"'), $('').text(search_val).html()), action: function() { self._quick_create(search_val); @@ -2360,7 +2357,7 @@ instance.web.form.CompletionFieldMixin = { self._search_create_popup("form", undefined, {"default_name": search_val}); }}); - response(values); + return values; }); }, _quick_create: function(name) { @@ -2524,7 +2521,11 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten var isSelecting = false; // autocomplete this.$input.autocomplete({ - source: function(req, resp) { self.get_search_result(req, resp); }, + source: function(req, resp) { + self.get_search_result(req.term).then(function(result) { + resp(result); + }); + }, select: function(event, ui) { isSelecting = true; var item = ui.item; From 36c91aa98e249fed0151295c4aedc7a2b96de26e Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Mon, 7 May 2012 14:54:34 +0200 Subject: [PATCH 07/15] wip bzr revid: nicolas.vanhoren@openerp.com-20120507125434-c5v8u7zj8vzhqtvo --- addons/web/static/src/js/view_form.js | 56 +++++---------------------- addons/web/static/src/xml/base.xml | 4 +- 2 files changed, 12 insertions(+), 48 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 7463df1c50f..6f4503b76bf 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -3051,8 +3051,8 @@ instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({ }); } -instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend({ - template: "FieldMany2Many", +instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.CompletionFieldMixin, { + template: "FieldMany2ManyTags", init: function() { this._super.apply(this, arguments); this.limit = 7; @@ -3060,7 +3060,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend({ }, start: function() { var self = this; - var textarea = $("textarea", this.$element).textext({ + var $textarea = $("textarea", this.$element).textext({ plugins : 'arrow prompt autocomplete', prompt : "Add one...", autocomplete: { @@ -3078,53 +3078,17 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend({ }, }, }).bind('getSuggestions', function(e, data) { + var _this = this; var str = !!data ? data.query || '' : ''; - self.get_search_result(str); - }); - }, - get_search_result: function(str) { - var self = this; - var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context()); - this.orderer.add(dataset.name_search( - str, self.build_domain(), 'ilike', this.limit + 1)).then(function(data) { - - // possible selections for the m2m - self.values = _.map(data, function(x) { - return { - type: 'data', - label: _.str.escapeHTML(x[1]), - data: x, - }; + self.get_search_result(str).then(function(result) { + self.search_result = result; + $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) { + return _.extend(el, {index:i}); + })}); }); - - // search more... if more results than max - if (values.length > self.limit) { - values = values.slice(0, self.limit); - values.push({type: 'action', label: _t("   Search More..."), action: function() { - dataset.name_search(search_val, self.build_domain(), 'ilike' - , false, function(data) { - self._search_create_popup("search", data); - }); - }}); - } - // quick create - var raw_result = _(data.result).map(function(x) {return x[1];}); - if (search_val.length > 0 && - !_.include(raw_result, search_val)) { - values.push({label: _.str.sprintf(_t('   Create "%s"'), - $('').text(search_val).html()), action: function() { - self._quick_create(search_val); - }}); - } - // create... - values.push({label: _t("   Create and Edit..."), action: function() { - self._search_create_popup("form", undefined, {"default_name": search_val}); - }}); - - $(this).trigger('setSuggestions', {result : _.map(list, function(el, i) {return {index:i, label:el};})}); }); }, -}); +})); /* * TODO niv: clean those deferred stuff, it could be better diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index fa8048b07cc..b48789f6e0f 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1039,9 +1039,9 @@ - +
- +
From 8e8adf320c6fe00c7aa9ecbebdd244ecf26e3425 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Mon, 7 May 2012 15:19:36 +0200 Subject: [PATCH 08/15] [imp] solved css problem bzr revid: nicolas.vanhoren@openerp.com-20120507131936-k6w7zgrljo88ijj5 --- addons/web/static/src/css/base.css | 6 ++++++ addons/web/static/src/css/base.sass | 4 ++++ addons/web/static/src/xml/base.xml | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index fab545d09b8..07c6468a4b1 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -1461,6 +1461,12 @@ .openerp .oe_form .oe_form_field_datetime input { min-width: 11em; } +.openerp .oe_form .oe_form_field_many2manytags .text-wrap { + width: 100% !important; +} +.openerp .oe_form .oe_form_field_many2manytags .text-wrap textarea { + width: 100% !important; +} .openerp .oe_form .oe_datepicker_container { display: none; } diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index 447bfc48abf..5e43a411ef9 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -1211,6 +1211,10 @@ $colour4: #8a89ba padding-top: 4px .oe_form_field_datetime input min-width: 11em + .oe_form_field_many2manytags .text-wrap + width: 100% !important + textarea + width: 100% !important .oe_datepicker_container display: none .oe_datepicker_root diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index b48789f6e0f..e0ae9adf9e2 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1040,7 +1040,7 @@
-
+
From 7eb29a47870e7fb62f97cffb3d361eba03e58801 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Mon, 7 May 2012 17:15:48 +0200 Subject: [PATCH 09/15] [imp] made readonly mode bzr revid: nicolas.vanhoren@openerp.com-20120507151548-pr82xavkghp9by3s --- addons/web/static/src/css/base.css | 14 ++++++ addons/web/static/src/css/base.sass | 20 +++++++-- addons/web/static/src/js/view_form.js | 61 ++++++++++++++++++++++++--- addons/web/static/src/xml/base.xml | 13 +++++- 4 files changed, 97 insertions(+), 11 deletions(-) diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index 07c6468a4b1..228e2ec7e5c 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -1467,6 +1467,20 @@ .openerp .oe_form .oe_form_field_many2manytags .text-wrap textarea { width: 100% !important; } +.openerp .oe_form .oe_form_field_many2manytags .oe_form_field_many2manytags_box { + border-radius: 2px; + box-sizing: border-box; + position: relative; + float: left; + border: 1px solid #9daccc; + background: #e2e6f0; + color: black; + padding: 0px 3px 0px 3px; + margin: 0 2px 2px 0; + cursor: pointer; + height: 16px; + font: 11px "lucida grande", tahoma, verdana, arial, sans-serif; +} .openerp .oe_form .oe_datepicker_container { display: none; } diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index 5e43a411ef9..262cc738dfe 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -1211,10 +1211,24 @@ $colour4: #8a89ba padding-top: 4px .oe_form_field_datetime input min-width: 11em - .oe_form_field_many2manytags .text-wrap - width: 100% !important - textarea + .oe_form_field_many2manytags + .text-wrap width: 100% !important + textarea + width: 100% !important + .oe_form_field_many2manytags_box + border-radius: 2px + box-sizing: border-box + position: relative + float: left + border: 1px solid #9DACCC + background: #E2E6F0 + color: black + padding: 0px 3px 0px 3px + margin: 0 2px 2px 0 + cursor: pointer + height: 16px + font: 11px "lucida grande", tahoma, verdana, arial, sans-serif .oe_datepicker_container display: none .oe_datepicker_root diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 6f4503b76bf..486fd85f855 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -3051,17 +3051,27 @@ instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({ }); } -instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.CompletionFieldMixin, { +instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.CompletionFieldMixin, + instance.web.form.ReinitializeFieldMixin, { template: "FieldMany2ManyTags", init: function() { this._super.apply(this, arguments); - this.limit = 7; - this.orderer = new instance.web.DropMisordered(); + instance.web.form.CompletionFieldMixin.init.call(this); + this.set({"value": []}); + this.display_orderer = new instance.web.DropMisordered(); }, start: function() { + this._super(); + instance.web.form.ReinitializeFieldMixin.start.call(this); + this.on("change:value", this, this.render_value); + }, + initialize_content: function() { + if (this.get("effective_readonly")) + return; var self = this; - var $textarea = $("textarea", this.$element).textext({ - plugins : 'arrow prompt autocomplete', + self. $text = $("textarea", this.$element); + self.$text.textext({ + plugins : 'tags arrow prompt autocomplete', prompt : "Add one...", autocomplete: { render: function(suggestion) { @@ -3071,9 +3081,22 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. ext: { autocomplete: { selectFromDropdown: function(a, b, c) { - var index = this.selectedSuggestionElement().children().children().data('index'); - debugger; $(this).trigger('hideDropdown'); + var index = Number(this.selectedSuggestionElement().children().children().data('index')); + var data = self.search_result[index]; + self.set({'value': self.get('value').concat([data.id])}); + }, + }, + tags: { + isTagAllowed: function(tag) { + if (! tag.name) + return false; + return true; + }, + }, + itemManager: { + itemToString: function(item) { + return item.name; }, }, }, @@ -3087,6 +3110,30 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. })}); }); }); + self.tags = self.$text.textext()[0].tags(); + }, + set_value: function(value_) { + value_ = value_ || []; + if (value_.length >= 1 && value_[0] instanceof Array) { + value_ = value_[0][2]; + } + this._super(value_); + }, + get_value: function() { + var tmp = this._super(); + return tmp; + }, + render_value: function() { + var self = this; + var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.view.dataset.get_context()); + this.display_orderer.add(dataset.name_get(self.get("value"))).then(function(data) { + if (! self.get("effective_readonly")) { + self.tags.containerElement().children().remove(); + self.tags.addTags(_.map(data, function(el) {return {name: el[1]};})); + } else { + self.$element.html(QWeb.render("FieldMany2ManyTags.box", {elements: data})); + } + }); }, })); diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index e0ae9adf9e2..a142ac40756 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1041,9 +1041,20 @@
- + + +
+ + + + + + + + +
From 6d0d0d8531c23f2444cf8dddf652e061a2d13a0a Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 8 May 2012 12:00:14 +0200 Subject: [PATCH 10/15] Did stuff bzr revid: nicolas.vanhoren@openerp.com-20120508100014-zegm7echijq8xnb7 --- addons/web/static/src/js/view_form.js | 34 +++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 486fd85f855..5e2ee923aff 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -3075,7 +3075,8 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. prompt : "Add one...", autocomplete: { render: function(suggestion) { - return $('
', {'data-index': suggestion['index']}).html(suggestion['label']); + return $('
', + {'data-index': suggestion['index']}).html(suggestion['label']); } }, ext: { @@ -3109,6 +3110,19 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. return _.extend(el, {index:i}); })}); }); + }).bind('tagClick', function(e, tag, value, callback) { + var pop = new instance.web.form.FormOpenPopup(self.view); + pop.show_element( + self.field.relation, + value.id, + self.build_context(), + { + title: _t("Open: ") + (self.string || self.name) + } + ); + pop.on_write_completed.add_last(function() { + self.render_value(); + }); }); self.tags = self.$text.textext()[0].tags(); }, @@ -3127,11 +3141,27 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. var self = this; var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.view.dataset.get_context()); this.display_orderer.add(dataset.name_get(self.get("value"))).then(function(data) { + var indexed = {}; + _.each(data, function(el) { + indexed[el[0]] = el; + }); + data = _.map(self.get("value"), function(el) { return indexed[el]; }); if (! self.get("effective_readonly")) { self.tags.containerElement().children().remove(); - self.tags.addTags(_.map(data, function(el) {return {name: el[1]};})); + self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};})); } else { self.$element.html(QWeb.render("FieldMany2ManyTags.box", {elements: data})); + $(".oe_form_field_many2manytags_box", self.$element).click(function() { + var index = Number($(this).data("index")); + self.do_action({ + type: 'ir.actions.act_window', + res_model: self.field.relation, + res_id: self.get("value")[index], + context: self.build_context(), + views: [[false, 'form']], + target: 'current' + }); + }); } }); }, From 56cf93e993a9042d3063e8668edb25081aad4ad4 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 8 May 2012 17:03:11 +0200 Subject: [PATCH 11/15] [imp] added suppression bzr revid: nicolas.vanhoren@openerp.com-20120508150311-v3hoje2fu3krxtgh --- addons/web/static/src/js/view_form.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 5e2ee923aff..c942f931d16 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -3085,7 +3085,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. $(this).trigger('hideDropdown'); var index = Number(this.selectedSuggestionElement().children().children().data('index')); var data = self.search_result[index]; - self.set({'value': self.get('value').concat([data.id])}); + self.set({'value': _.uniq(self.get('value').concat([data.id]))}); }, }, tags: { @@ -3094,6 +3094,14 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. return false; return true; }, + removeTag: function(tag) { + var id = tag.data("id"); + self.set({"value": _.without(self.get("value"), id)}); + }, + renderTag: function(stuff) { + return $.fn.textext.TextExtTags.prototype.renderTag. + call(this, stuff).data("id", stuff.id); + }, }, itemManager: { itemToString: function(item) { @@ -3134,7 +3142,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. this._super(value_); }, get_value: function() { - var tmp = this._super(); + var tmp = [commands.replace_with(this.get("value"))]; return tmp; }, render_value: function() { From b8c5a037f1f51f3e017237e682ac55dfdff5f325 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Wed, 9 May 2012 15:24:19 +0200 Subject: [PATCH 12/15] [fix] a few bugs bzr revid: nicolas.vanhoren@openerp.com-20120509132419-031utpsxqyorb7sh --- addons/web/static/src/js/view_form.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 86362b7af26..a07e397116e 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -3070,12 +3070,11 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. var self = this; self. $text = $("textarea", this.$element); self.$text.textext({ - plugins : 'tags arrow prompt autocomplete', - prompt : "Add one...", + plugins : 'tags arrow autocomplete', autocomplete: { render: function(suggestion) { - return $('
', - {'data-index': suggestion['index']}).html(suggestion['label']); + return $(''). + data('index', suggestion['index']).html(suggestion['label']); } }, ext: { @@ -3155,6 +3154,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. data = _.map(self.get("value"), function(el) { return indexed[el]; }); if (! self.get("effective_readonly")) { self.tags.containerElement().children().remove(); + $("textarea", self.$element).css("padding-left", "3px"); self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};})); } else { self.$element.html(QWeb.render("FieldMany2ManyTags.box", {elements: data})); From c7a17d90cc29866a2fc4125c1d63d79deeed35ec Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Wed, 9 May 2012 15:38:30 +0200 Subject: [PATCH 13/15] [fix] css problem bzr revid: nicolas.vanhoren@openerp.com-20120509133830-ugfphuqej6ijpm2e --- addons/web/static/src/css/base.css | 4 ++++ addons/web/static/src/css/base.sass | 3 +++ 2 files changed, 7 insertions(+) diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index 228e2ec7e5c..b3082126142 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -1481,6 +1481,10 @@ height: 16px; font: 11px "lucida grande", tahoma, verdana, arial, sans-serif; } +.openerp .oe_form .oe_form_field_many2manytags .text-core .text-wrap .text-dropdown .text-list .text-suggestion em { + font-style: italic; + text-decoration: none; +} .openerp .oe_form .oe_datepicker_container { display: none; } diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index 262cc738dfe..412c36b3ee5 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -1229,6 +1229,9 @@ $colour4: #8a89ba cursor: pointer height: 16px font: 11px "lucida grande", tahoma, verdana, arial, sans-serif + .text-core .text-wrap .text-dropdown .text-list .text-suggestion em + font-style: italic + text-decoration: none .oe_datepicker_container display: none .oe_datepicker_root From 44bfc37829477c75338447b436031e964c97b660 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Wed, 9 May 2012 17:32:46 +0200 Subject: [PATCH 14/15] [imp] added other actions bzr revid: nicolas.vanhoren@openerp.com-20120509153246-4i2tw0scg2taz1ht --- addons/web/static/src/js/view_form.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index a07e397116e..671ffc93cae 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -2367,9 +2367,7 @@ instance.web.form.CompletionFieldMixin = { if (self.get_definition_options().quick_create === undefined || self.get_definition_options().quick_create) { new instance.web.DataSet(this, this.field.relation, self.build_context()) .name_create(name, function(data) { - self.display_value = {}; - self.display_value["" + data[0]] = data[1]; - self.set({value: data[0]}); + self.add_id(data[0]); }).fail(function(error, event) { event.preventDefault(); slow_create(); @@ -2393,9 +2391,13 @@ instance.web.form.CompletionFieldMixin = { new instance.web.CompoundContext(self.build_context(), context || {}) ); pop.on_select_elements.add(function(element_ids) { - self.set({value: element_ids[0]}); + self.add_id(element_ids[0]); }); }, + /** + * To implement. + */ + add_id: function(id) {}, }; instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, @@ -2609,6 +2611,10 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten this._super(value_); this.inhibit_on_change = false; }, + add_id: function(id) { + this.display_value = {}; + this.set({value: id}); + }, is_false: function() { return ! this.get("value"); }, @@ -3083,7 +3089,11 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. $(this).trigger('hideDropdown'); var index = Number(this.selectedSuggestionElement().children().children().data('index')); var data = self.search_result[index]; - self.set({'value': _.uniq(self.get('value').concat([data.id]))}); + if (data.id) { + self.add_id(data.id); + } else { + data.action(); + } }, }, tags: { @@ -3172,6 +3182,9 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. } }); }, + add_id: function(id) { + this.set({'value': _.uniq(this.get('value').concat([id]))}); + }, })); /* From e68da3b1dccfd6926a6809c897da135e22c41e9c Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Wed, 9 May 2012 17:41:24 +0200 Subject: [PATCH 15/15] [imp] optimized name_search bzr revid: nicolas.vanhoren@openerp.com-20120509154124-6x83am250nigsuwj --- addons/web/static/src/js/view_form.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 671ffc93cae..b2a0c0d955c 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -3156,7 +3156,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. render_value: function() { var self = this; var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.view.dataset.get_context()); - this.display_orderer.add(dataset.name_get(self.get("value"))).then(function(data) { + var handle_names = function(data) { var indexed = {}; _.each(data, function(el) { indexed[el[0]] = el; @@ -3180,7 +3180,12 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. }); }); } - }); + }; + if (! self.get('values') || self.get('values').length > 0) { + this.display_orderer.add(dataset.name_get(self.get("value"))).then(handle_names); + } else { + handle_names([]); + } }, add_id: function(id) { this.set({'value': _.uniq(this.get('value').concat([id]))});