mangroveorg/datawinners

View on GitHub
datawinners/media/javascript/jquery-textntags/jquery-textntags.js

Summary

Maintainability
F
5 days
Test Coverage
/*
 * Text'N'Tags (textntags)
 * Version 0.1.2
 * Written by: Daniel Zahariev
 *
 * Dependencies: jQuery, underscore.js
 *
 * License: MIT License - http://www.opensource.org/licenses/mit-license.php
 */
(function ($, _, undefined) {

    // Keys "enum"
    var KEY = { V: 86, Z: 90, BACKSPACE : 8, TAB : 9, RETURN : 13, ESC : 27, LEFT : 37, UP : 38, RIGHT : 39, DOWN : 40, COMMA : 188, SPACE : 32, HOME : 36, END : 35, 'DELETE': 46 };
    var defaultSettings = {
        onDataRequest   : $.noop,
        realValOnSubmit : true,
        triggers        : {'@' : {}},
        templates       : {
            wrapper           : _.template('<div class="textntags-wrapper"></div>'),
            beautifier        : _.template('<div class="textntags-beautifier"><div></div></div>'),
            tagHighlight      : _.template('<strong class="<%= class_name %>"><span>$<%= idx %></span></strong>'),
            tagList           : _.template('<div class="textntags-tag-list"></div>'),
            tagsListItem      : _.template('<li><%= title %></li>'),
            tagsListItemImage : _.template('<img src="<%= img %>" />'),
            tagsListItemIcon  : _.template('<div class="icon <%= no_img_class %>"></div>')
        }
    };
    var trigger_defaults = {
        minChars        : 2,
        uniqueTags      : true,
        showImageOrIcon : true,
        keys_map        : {id: 'id', title: 'name', description: '', img: 'avatar', no_img_class: 'icon', type: 'type'},
        syntax          : _.template('@{<%= id %>:<%= type %>:<%= title %>}'),
//        parser          : /(@)\[\[(\d+):([\w\s\.\-]+):([\w\s@\.,-\/#!$%\^&\*;:{}=\-_`~()]+)\]\]/gi,
//        parser          : /(@)\{(\d+):([\w\s\.\-]+):([\w\s@\.,-\/#!$%\^&\*;:=\-_`~()]+)\}/gi,
        parser          : /(@)\{(\d+):([\w\s\.\-]+):([^}]+)\}/gi,
        parserGroups    : {id: 2, type: 3, title: 4},
        classes         : {
            tagsDropDown      : '',
            tagActiveDropDown : 'active',
            tagHighlight      : ''
        }
    };
    
    function transformObjectPropertiesFn(keys_map) {
        return function (obj, localToPublic) {
            var new_obj = {};
            if (localToPublic) {
                _.each(keys_map, function (v, k) { new_obj[v] = obj[k]; });
            } else {
                _.each(keys_map, function (v, k) { new_obj[k] = obj[v]; });
            }
            return new_obj;
        };
    }
    var transformObjectProperties = _.memoize(transformObjectPropertiesFn);
    
    var utils = {
        htmlEncode: function (str) {
            return _.escape(str);
        },
        highlightTerm: function (value, term) {
            if (!term && !term.length) {
                return value;
            }
            return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
        },
        setCaratPosition: function (domNode, caretPos) {
            if (domNode.createTextRange) {
                var range = domNode.createTextRange();
                range.move('character', caretPos);
                range.select();
            } else {
                if (domNode.selectionStart) {
                    domNode.focus();
                    domNode.setSelectionRange(caretPos, caretPos);
                } else {
                    domNode.focus();
                }
            }
        }
    };
    
    var TextNTags = function (editor) {
        var settings = null, templates;
        var elContainer, elEditor, elBeautifier, elTagList, elTagListItemActive;
        var tagsCollection;
        var currentTriggerChar, currentDataQuery;
        var editorSelectionLength = 0, editorTextLength = 0, editorKeyCode = 0, editorAddingTag = false;
        var editorInPasteMode = false, editorPasteStartPosition = 0, editorPasteCutCharacters = 0;
        var REGEX_ESCAPE_CHARS = ['[', '^', '$', '.', '|', '?', '*', '+', '(', ')', '\\'];
        
        function setSettings (options) {
            if (settings != null) {
                return false;
            }
            
            settings = $.extend(true, {}, defaultSettings, options);
            delete settings.triggers[''];
            _.each(settings.triggers, function (val, key) {
                settings.triggers[key] = $.extend(true, {}, trigger_defaults, val);
                if (_.find(REGEX_ESCAPE_CHARS, function(character){ return character === key })) {
                  var regex_key = "\\" + key;
                } else {
                  var regex_key = key;
                }
                settings.triggers[key].finder = new RegExp(regex_key + '\\w+(\\s+\\w+)?\\s?$', 'gi');
            });
            
            templates = settings.templates;
            
            return true;
        }
        
        function initTextarea () {
            elEditor = $(editor).bind({
                click:    onEditorClick,
                keydown:  onEditorKeyDown,
                keypress: onEditorKeyPress,
//                keyup:    onEditorKeyUp,
                input:    onEditorInput,
                blur:     onEditorBlur,
                dblclick:     onEditorDoubleClick,
                textchange:     onEditorInputIe8,
                paste: onEditorPaste
            });

            elContainer = elEditor.wrapAll($(templates.wrapper())).parent();
            
            if (settings.realValOnSubmit) {
                elEditor.closest('form').bind('submit.textntags', function (event) {
                    elContainer.css('visibility', 'hidden');
                    elEditor.val(getTaggedText());
                });
            }
        }
        
        function initTagList () {
            elTagList = $(templates.tagList());
            elTagList.appendTo(elContainer);
            elTagList.delegate('li', 'click', onTagListItemClick);
        }
        
        function initBeautifier () {
            elBeautifier = $(templates.beautifier());
            elBeautifier.prependTo(elContainer);
        }
        
        function initState () {
            var text_with_tags = getEditorValue(), initialState = parseTaggedText(text_with_tags);
            tagsCollection = initialState.tagsCollection;
            elEditor.val(initialState.plain_text);
            updateBeautifier();
            
            if (tagsCollection.length > 0) {
                var addedTags = _.uniq(_.map(tagsCollection, function (tagPos) { return tagPos[3]; }));
                elEditor.trigger('tagsAdded.textntags', [addedTags]);
            }
        }
        
        function getEditorValue () {
            return elEditor.val();
        }
        
        function getBeautifiedText (tagged_text) {
            var beautified_text = tagged_text || getTaggedText();
        beautified_text = beautified_text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
            _.each(settings.triggers, function (trigger) {
                var markup = templates.tagHighlight({idx: trigger.parserGroups.title, class_name: trigger.classes.tagHighlight});
                beautified_text = beautified_text.replace(trigger.parser, markup);
            });
            
            beautified_text = beautified_text.replace(/\n/g, '<br />&shy;');
            beautified_text = beautified_text.replace(/ {2}/g, ' &nbsp;') + '&shy;';
            return beautified_text;
        }
        
        function getTaggedText() {
            var plain_text = getEditorValue(),
                position = 0, tagged_text, triggers = settings.triggers;

            tagged_text = _.map(tagsCollection, function (tagPos) {
                var diff_pos = tagPos[0] - position,
                    diff_text = diff_pos > 0 ? plain_text.substr(position, diff_pos) : '',
                    objPropTransformer = transformObjectProperties(triggers[tagPos[2]].keys_map),
                    tagText = triggers[tagPos[2]].syntax(objPropTransformer(tagPos[3], false));
                
                position = tagPos[0] + tagPos[1];
                return diff_text + tagText;
            });
            
            return tagged_text.join('') + plain_text.substr(position);
        }
        
        // it's ready for export
        function parseTaggedText (tagged_text) {
            if (_.isString(tagged_text) == false) {
                return null;
            } 
            var plain_text = '' + tagged_text, tagsColl = [], triggers = settings.triggers;
            
            _.each(triggers, function (opts, tchar) {
                var parts = tagged_text.split(opts.parser),
                    idx = 0, pos = 0, len = parts.length,
                    found_tag, found_len, part_len,
                    max_group = _.max(opts.parserGroups);

                while (idx < len) {
                    if (parts[idx] == tchar) {
                        found_tag = {};
                        _.each(opts.parserGroups, function (v, k) {
                            found_tag[opts.keys_map[k]] = parts[idx + v - 1];
                            if (k == 'title') {
                                found_len = parts[idx + v - 1].length;
                            }
                        });
                        tagsColl.push([pos, found_len, tchar, found_tag]);
                        part_len = found_len;
                        idx += max_group;
                    } else {
                        part_len = parts[idx].length;
                        idx += 1;
                    }
                    pos += part_len;
                }
            });
            
            tagsColl = _.sortBy(tagsColl, function (tagPos) { return tagPos[0]; });
            
            _.each(triggers, function (opts, tchar) {
                plain_text = plain_text.replace(opts.parser, '$' + opts.parserGroups.title);
            });
            
            return {
                plain_text: plain_text,
                tagged_text: tagged_text,
                tagsCollection: tagsColl
            };
        }
        
        function updateBeautifier () {
            elBeautifier.find('div').html(getBeautifiedText());
            elEditor.css('height', elBeautifier.outerHeight() + 'px');
        }
        
        function checkForTrigger(look_ahead) {
            look_ahead = look_ahead || 0;
            
            var selectionStartFix = $.browser.webkit ? 0 : -1,
                sStart = getInputSelection(elEditor[0]).start + selectionStartFix,
                left_text = elEditor.val().substr(0, sStart + look_ahead),
                found_trigger, found_trigger_char = null, query;

            if (!left_text || !left_text.length) {
                return;
            }
            
            found_trigger = _.find(settings.triggers, function (trigger, tchar) {
                var matches = left_text.match(trigger.finder);
                if (matches) {
                    found_trigger_char = tchar;
                    query = matches[0].substr(tchar.length);
                    return true;
                }
                return false;
            });

            if (!found_trigger_char || (found_trigger &&(query.length < found_trigger.minChars))) {
                hideTagList();
            } else {
                currentDataQuery = query;
                currentTriggerChar = found_trigger_char;
                _.defer(_.bind(searchTags, this, currentDataQuery, found_trigger_char));
            }
        }
        
        function onEditorClick (e) {
            checkForTrigger(0);
        }

        function is_nav_key(key){
            var nav_keys = [KEY.UP, KEY.DOWN, KEY.LEFT, KEY.RIGHT, KEY.HOME, KEY.END, KEY.TAB];
            return nav_keys.indexOf(key) != -1
        }

        function getInputSelection(el) {
            var start = 0, end = 0, normalizedValue, range,
                textInputRange, len, endRange;

            if (typeof el.selectionStart == "number" && typeof el.selectionEnd == "number") {
                start = el.selectionStart;
                end = el.selectionEnd;
            } else {
                range = document.selection.createRange();

                if (range && range.parentElement() == el) {
                    len = el.value.length;
                    normalizedValue = el.value.replace(/\r\n/g, "\n");

                    // Create a working TextRange that lives only in the input
                    textInputRange = el.createTextRange();
                    textInputRange.moveToBookmark(range.getBookmark());

                    // Check if the start and end of the selection are at the very end
                    // of the input, since moveStart/moveEnd doesn't return what we want
                    // in those cases
                    endRange = el.createTextRange();
                    endRange.collapse(false);

                    if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
                        start = end = len;
                    } else {
                        start = -textInputRange.moveStart("character", -len);
                        start += normalizedValue.slice(0, start).split("\n").length - 1;

                        if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
                            end = len;
                        } else {
                            end = -textInputRange.moveEnd("character", -len);
                            end += normalizedValue.slice(0, end).split("\n").length - 1;
                        }
                    }
                }
            }

            return {
                start: start,
                end: end
            };
        }
        function onEditorKeyDown (e) {
            var keys = KEY, // store in local var for faster lookup
                sStart = getInputSelection(elEditor[0]).start,
                sEnd = getInputSelection(elEditor[0]).end,
//                sStart = elEditor[0].selectionStart,
//                sEnd = elEditor[0].selectionEnd,
                plain_text = elEditor.val();
            
            editorSelectionLength = sEnd - sStart;
            editorTextLength = plain_text.length;
            editorKeyCode = e.keyCode;

            if(sEnd >= sStart && !is_nav_key(editorKeyCode)) {
                for(var i=0; i<tagsCollection.length; i++){
                    var tag = tagsCollection[i];

                    var tagStart = tag[0] ;
                    var tagEnd = tagStart + tag[1];
                    var cursorStart = sStart;
                    var cursorEnd = sEnd;
                    if(editorKeyCode == keys['DELETE']) cursorEnd +=1;
                    if(editorKeyCode == keys.BACKSPACE) cursorStart--;
                    if (cursorStart<=tagEnd && cursorEnd>=tagStart) {
                        e.preventDefault();
                        return
                    }
                }
            }

            switch (e.keyCode) {
                case keys.UP:
                case keys.DOWN:
                    if (!elTagList.is(':visible')) {
                        return true;
                    }
                    
                    var elCurrentTagListItem = null;
                    if (e.keyCode == keys.DOWN) {
                        if (elTagListItemActive && elTagListItemActive.length) {
                            elCurrentTagListItem = elTagListItemActive.next();
                        } else {
                            elCurrentTagListItem = elTagList.find('li').first();
                        }
                    } else {
                        if (elTagListItemActive && elTagListItemActive.length) {
                            elCurrentTagListItem = elTagListItemActive.prev();
                        } else {
                            elCurrentTagListItem = elTagList.find('li').last();
                        }
                    }

                    selectTagListItem(elCurrentTagListItem, settings.triggers[currentTriggerChar].classes.tagActiveDropDown);
                    return false;

                case keys.RETURN:
                case keys.TAB:
                    if (elTagListItemActive && elTagListItemActive.length) {
                        editorAddingTag = true;
                        elTagListItemActive.click();
                        return false;
                    }
                    return true;

                case keys.BACKSPACE:
                case keys['DELETE']:
                    if (e.ctrlKey || e.altKey) {
                        // forbid word delete
                        return false;
                    }
                    if (e.keyCode == keys.BACKSPACE && sStart == sEnd && sStart > 0) {
                        sStart -= 1;
                    }
                    if(e.keyCode == keys['DELETE'] && sStart == sEnd && sStart >= 0){
                        sEnd += 1;
                    }
                    if(sEnd > sStart) {
                        removeTagsInRange(sStart, sEnd);
                        shiftTagsPosition(sStart, sStart - sEnd);
                    }
                    return true;

                case keys.LEFT:
                case keys.RIGHT:
                case keys.HOME:
                case keys.END:
                    _.defer(function () { checkForTrigger.call(this, 0); });
                    break;
//                case keys.V:
//                    // checking for paste
//                    if (e.ctrlKey || e.metaKey) {
//                        editorInPasteMode = true;
//                        editorPasteStartPosition = sStart;
//                        editorPasteCutCharacters = sEnd - sStart;
//                        removeTagsInRange(sStart, sEnd);
//                    }
//                    break;
                case keys.Z:
                    if (e.ctrlKey || e.metaKey) {
                        // forbid undo
                        return false;
                    }
                    break;
            }

            return true;
        }
        
        function onEditorKeyPress (e) {
            if (e.keyCode == KEY.RETURN) {
                updateBeautifier(elEditor.val());
            }
            if (editorAddingTag) {
                if (e.keyCode == KEY.RETURN || e.keyCode == KEY.TAB) {
                    e.preventDefault();
                }
                editorAddingTag = false;
            }
        }

        function onEditorKeyUp (e) {
            if (editorInPasteMode) {
                editorInPasteMode = false;

                if (editorSelectionLength > 0) {
                    return;
                }

                var sStart = getInputSelection(elEditor[0]).start,
                    sEnd = getInputSelection(elEditor[0]).end;

                shiftTagsPosition(editorPasteStartPosition, sEnd - editorPasteStartPosition - editorPasteCutCharacters);
                updateBeautifier();
            }
        }
        function onEditorPaste(e){
            var sStart = getInputSelection(elEditor[0]).start,
                sEnd = getInputSelection(elEditor[0]).end;
            editorInPasteMode = true;
            editorPasteStartPosition = sStart;
            editorPasteCutCharacters = sEnd - sStart;
        }

        function onEditorInputIe8(e){
            var timer;
            if ($.browser.msie && $.browser.version <= 9){
                if([17,18,91].indexOf(editorKeyCode) != -1){
                    return false;
                }
                timer = setTimeout(onEditorInput(e), 500);
                clearTimeout(timer);
            }
        }

        function onEditorInput (e) {
            var selectionStartFix = $.browser.webkit ? 0 : -1;
            if (editorKeyCode != KEY.BACKSPACE && editorKeyCode != KEY['DELETE']) {
                if (editorSelectionLength > 0) {
                    // delete of selection occured
                    var sStart = getInputSelection(elEditor[0]).start + selectionStartFix,
                        selectionLength = editorSelectionLength,
                        sEnd = sStart + selectionLength,
                        tags_shift_positions = elEditor.val().length - editorTextLength;
                    if(editorInPasteMode){
                        editorInPasteMode = false;
                    }
                    shiftTagsPosition(sStart, tags_shift_positions);
                } else if (!editorInPasteMode) {
                    // char input - shift with 1
                    var sStart = getInputSelection(elEditor[0]).start + selectionStartFix,
                        sEnd = getInputSelection(elEditor[0]).end + selectionStartFix,
                        selectionLength = sEnd - sStart;

                    if (editorKeyCode == KEY.RETURN) {
                        shiftTagsPosition(sStart - 1, 1);
                        removeTagsInRange(sStart, sStart);
                    } else {
                        shiftTagsPosition(sStart, 1);
                        removeTagsInRange(sStart, sStart + 1);
                    }
                }else if(editorInPasteMode){
                    onEditorKeyUp(e);
                }

            }
            
            updateBeautifier();
            
            checkForTrigger(1);
        }
        
        function onEditorBlur (e) {
            _.delay(hideTagList, 100);
        }
        function onEditorDoubleClick(e){
            if (window.getSelection) {
                if (window.getSelection().empty) {  // Chrome
                    window.getSelection().empty();
                }
                else if (window.getSelection().removeAllRanges) {  // Firefox
                    window.getSelection().removeAllRanges();
                }
            }
            else if (document.selection) {  // IE?
                 document.selection.empty();
            }
        }
        function hideTagList () {
            elTagListItemActive = null;
            elTagList.hide().empty();
        }
        
        function onTagListItemClick (e) {
            addTag($(this).data('tag'));
            return false;
        }
        
        function removeTagsInRange (start, end) {
            var removedTags = [];
            tagsCollection = _.filter(tagsCollection, function (tagPos) {
                var s = tagPos[0], e = s + tagPos[1],
                    inRange = ((s >= start && s < end) || (e > start && e <= end) || (s < start && e > end));
                if (inRange) {
                    removedTags.push(tagPos[3]);
                }
                return !inRange;
            });
            
            if (removedTags.length > 0) {
                elEditor.trigger('tagsRemoved.textntags', [removedTags]);
            }
        }
        
        function shiftTagsPosition (afterPosition, position_shift) {
            tagsCollection = _.map(tagsCollection, function (tagPos) {
                if (tagPos[0] + tagPos[1] >= afterPosition) {
                    tagPos[0] += position_shift;
                }
                return tagPos;
            });
        }
        
        function addTag (tag) {
            var trigger = settings.triggers[currentTriggerChar],
                objPropTransformer = transformObjectProperties(trigger.keys_map),
                localTag = objPropTransformer(tag, false),
                plain_text = getEditorValue(),
                sStart = getInputSelection(elEditor[0]).start,
                tagStart = sStart - currentTriggerChar.length - currentDataQuery.length,
                newCaretPosition = tagStart + localTag.title.length,
                left_text = plain_text.substr(0, tagStart),
                right_text = plain_text.substr(sStart),
                new_text = left_text + localTag.title + right_text;

            // shift the tags after the current new one
            shiftTagsPosition(sStart, newCaretPosition - sStart);

            // explicitly convert to string for comparisons later
            tag[trigger.keys_map.id] = '' + tag[trigger.keys_map.id];
            
            tagsCollection.push([tagStart, localTag.title.length, currentTriggerChar, tag]);
            tagsCollection = _.sortBy(tagsCollection, function (t) { return t[0]; });
            
            currentTriggerChar = '';
            currentDataQuery = '';
            hideTagList();
            
            elEditor.val(new_text);
            updateBeautifier();

            elEditor.focus();
            utils.setCaratPosition(elEditor[0], newCaretPosition);
            
            elEditor.trigger('tagsAdded.textntags', [[tag]]);
        }
        
        function selectTagListItem (tagItem, class_name) {
            if (tagItem && tagItem.length) {
                tagItem.addClass(class_name);
                tagItem.siblings().removeClass(class_name);
                elTagListItemActive = tagItem;
            } else {
                elTagListItemActive.removeClass(class_name);
                elTagListItemActive = null;
            }
        }
        
        function populateTagList (query, triggerChar, results) {
            var trigger = settings.triggers[triggerChar];
            
            if (trigger.uniqueTags) {
                // Filter items that has already been mentioned
                var id_key = trigger.keys_map.id, tagIds = _.map(tagsCollection, function (tagPos) { return tagPos[3][id_key]; });
                results = _.reject(results, function (item) {
                    // converting to string ids
                    return _.include(tagIds, '' + item[id_key]);
                });
            }

            if (!results.length) {
                return;
            }

            var tagsDropDown = $("<ul />").addClass(trigger.classes.tagsDropDown).appendTo(elTagList),
                imgOrIconTpl = trigger.showImageOrIcon ? templates.tagsListItemImage : templates.tagsListItemIcon,
                objPropTransformer = transformObjectProperties(trigger.keys_map);

            _.each(results, function (tag, index) {
                var tagItem, localTag = objPropTransformer(tag, false);
                localTag.title = utils.highlightTerm(utils.htmlEncode((localTag.title)), query);
                tagItem = $(templates.tagsListItem(localTag)).data('tag', tag);
                if(localTag.img) {
                  tagItem = tagItem.prepend(imgOrIconTpl(localTag));
                }
                tagItem.appendTo(tagsDropDown);

                if (index === 0) { 
                    selectTagListItem(tagItem, trigger.classes.tagActiveDropDown);
                }
            });

            elTagList.show();
        }
        
        function searchTags (query, triggerChar) {
            hideTagList();
            settings.onDataRequest.call(this, 'search', query, triggerChar, function (responseData) {
                populateTagList(query, triggerChar, responseData);
            });
        }
        
        // Public methods
        return {
            init : function (options) {
                if (setSettings(options)) {
                    initTextarea();
                    initTagList();
                    initBeautifier();
                    initState();
                }
            },
            val : function (callback) {
                if (_.isString(callback)) {
                    var removedTags = _.uniq(_.map(tagsCollection, function (tagPos) { return tagPos[3]; }));
                    elEditor.trigger('tagsRemoved.textntags', [removedTags]);
                    elEditor.val(callback);
                    initState();
                    return;
                } else if (!_.isFunction(callback)) {
                    return;
                }

                var value = tagsCollection.length ? getTaggedText() : getEditorValue();
                callback.call(this, value);
            },
            reset : function () {
                var removedTags = _.uniq(_.map(tagsCollection, function (tagPos) { return tagPos[3]; }));
                elEditor.trigger('tagsRemoved.textntags', [removedTags]);
                elEditor.val('');
                tagsCollection = [];
                updateBeautifier();
            },
            getTags : function (callback) {
                if (!_.isFunction(callback)) {
                    return;
                }
                var tags = _.map(tagsCollection, function (tagPos) { return tagPos[3]; });

                callback.call(this, _.uniq(tags));
            },
            getTagsMap : function (callback) {
                if (!_.isFunction(callback)) {
                    return;
                }

                callback.call(this, tagsCollection);
            },
            getTagsMapFacebook : function (callback) {
                if (!_.isFunction(callback)) {
                    return;
                }
                var fbTagsCollection = {}, triggers = settings.triggers;
                    
                _.each(tagsCollection, function (tagPos) {
                    var objPropTransformer = transformObjectProperties(triggers[tagPos[2]].keys_map),
                        localTag = objPropTransformer(tagPos[3], false);
                    fbTagsCollection[tagPos[0]] = [{
                        id: localTag.id,
                        name: localTag.title,
                        type: localTag.type,
                        offset: tagPos[0],
                        length: tagPos[1]
                    }];
                });

                callback.call(this, fbTagsCollection);
            },
            parseTaggedText: function (tagged_text, callback) {
                if (!_.isFunction(callback)) {
                    return;
                }
                
                callback.call(this, parseTaggedText(tagged_text));
            }
        };
    };
    
    $.fn.textntags = function (methodOrSettings) {
        var outerArguments = arguments;

        return this.each(function () {
            var ms = methodOrSettings, instance = $.data(this, 'textntags') || $.data(this, 'textntags', new TextNTags(this));

            if (_.isFunction(instance[ms])) {
                return instance[ms].apply(this, Array.prototype.slice.call(outerArguments, 1));
            } else if (typeof ms === 'object' || !ms) {
                return instance.init.call(this, ms);
            } else {
                $.error('Method ' + ms + ' does not exist');
            }
        });
    };

})(jQuery, _);