/**
* jQuery TextExt Plugin
* http://textextjs.com
*
* @version 1.3.1
* @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
});
};
/**
* 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; i++) {
item = $(list[i]);
if(self.itemManager().compareItems(item.data(CSS_TAG), tag))
return item;
}
return null;
};
/**
* 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);
if (element === null) {
//Tag does not exist
return;
}
}
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);