Index: /trunk/admin/themes/default/template/batch_manager_global.tpl
===================================================================
--- /trunk/admin/themes/default/template/batch_manager_global.tpl	(revision 23274)
+++ /trunk/admin/themes/default/template/batch_manager_global.tpl	(revision 23275)
@@ -56,4 +56,5 @@
 {/literal}{/footer_script}
 
+{combine_css path='themes/default/js/plugins/jquery.tokeninput.css'}
 {combine_script id='jquery.tokeninput' load='footer' require='jquery' path='themes/default/js/plugins/jquery.tokeninput.js'}
 {combine_script id='jquery.progressBar' load='footer' path='themes/default/js/plugins/jquery.progressbar.min.js'}
@@ -74,5 +75,5 @@
       animateDropdown: false,
       preventDuplicates: true,
-      allowCreation: true
+      allowFreeTagging: true
     }
   );
@@ -86,5 +87,5 @@
       animateDropdown: false,
       preventDuplicates: true,
-      allowCreation: false
+      allowFreeTagging: false
     }
   );
Index: /trunk/admin/themes/default/template/batch_manager_unit.tpl
===================================================================
--- /trunk/admin/themes/default/template/batch_manager_unit.tpl	(revision 23274)
+++ /trunk/admin/themes/default/template/batch_manager_unit.tpl	(revision 23275)
@@ -3,4 +3,5 @@
 {include file='include/colorbox.inc.tpl'}
 
+{combine_css path='themes/default/js/plugins/jquery.tokeninput.css'}
 {combine_script id='jquery.tokeninput' load='async' require='jquery' path='themes/default/js/plugins/jquery.tokeninput.js'}
 {footer_script require='jquery.tokeninput'}
@@ -15,5 +16,5 @@
       animateDropdown: false,
       preventDuplicates: true,
-      allowCreation: true
+      allowFreeTagging: true
     }
   );
Index: /trunk/admin/themes/default/template/picture_modify.tpl
===================================================================
--- /trunk/admin/themes/default/template/picture_modify.tpl	(revision 23274)
+++ /trunk/admin/themes/default/template/picture_modify.tpl	(revision 23275)
@@ -12,4 +12,5 @@
 {/literal}{/footer_script}
 
+{combine_css path='themes/default/js/plugins/jquery.tokeninput.css'}
 {combine_script id='jquery.tokeninput' load='async' require='jquery' path='themes/default/js/plugins/jquery.tokeninput.js'}
 {footer_script require='jquery.tokeninput'}
@@ -24,5 +25,5 @@
       animateDropdown: false,
       preventDuplicates: true,
-      allowCreation: true
+      allowFreeTagging: true
     }
   );
Index: /trunk/admin/themes/default/theme.css
===================================================================
--- /trunk/admin/themes/default/theme.css	(revision 23274)
+++ /trunk/admin/themes/default/theme.css	(revision 23275)
@@ -963,22 +963,4 @@
 #batchManagerGlobal #filter_dimension .ui-slider-horizontal {width:650px;margin:5px 0 10px 0;}
 
-
-/* TokenInput (with Facebook style) */
-ul.token-input-list {overflow: hidden; height: auto !important; height: 1%;width: 400px;border: 1px solid #8496ba;cursor: text;font-size: 12px;font-family: Verdana;min-height: 1px;z-index: 999;margin: 0;padding: 0;background-color: #fff;list-style-type: none;clear: left;}
-ul.token-input-list li input {border: 0;width: 100px;padding: 3px 8px;background-color: white;margin: 2px 0;-webkit-appearance: caret;}
-li.token-input-token {overflow: hidden; height: auto !important; height: 15px;margin: 3px;padding: 1px 3px;background-color: #eff2f7;color: #000;cursor: default;border: 1px solid #ccd5e4;font-size: 11px;border-radius: 5px;-moz-border-radius: 5px;-webkit-border-radius: 5px;float: left;white-space: nowrap;}
-li.token-input-token p {display: inline;padding: 0;margin: 0;}
-li.token-input-token span {color: #a6b3cf;margin-left: 5px;font-weight: bold;cursor: pointer;}
-li.token-input-selected-token {background-color: #5670a6;border: 1px solid #3b5998;color: #fff;}
-li.token-input-input-token {float: left;margin: 0;padding: 0;list-style-type: none;width:10px;}
-div.token-input-dropdown {position: absolute;width: 400px;background-color: #fff;overflow: hidden;border-left: 1px solid #ccc;border-right: 1px solid #ccc;border-bottom: 1px solid #ccc;cursor: default;font-size: 11px;font-family: Verdana;z-index: 1;}
-div.token-input-dropdown p {margin: 0;padding: 5px;font-weight: bold;color: #777;}
-div.token-input-dropdown ul {margin: 0;padding: 0;}
-div.token-input-dropdown ul li {background-color: #fff;padding: 3px;margin: 0;list-style-type: none;}
-div.token-input-dropdown ul li.token-input-dropdown-item {background-color: #fff;}
-div.token-input-dropdown ul li.token-input-dropdown-item2 {background-color: #fff;}
-div.token-input-dropdown ul li em {font-weight: bold;font-style: normal;}
-div.token-input-dropdown ul li.token-input-selected-dropdown-item {background-color: #3b5998;color: #fff;}
-
 #mainConf a.addFilter {font-weight:normal;margin-left:20px;}
 #mainConf a.removeFilter {font-weight:normal;}
Index: /trunk/admin/themes/roma/theme.css
===================================================================
--- /trunk/admin/themes/roma/theme.css	(revision 23274)
+++ /trunk/admin/themes/roma/theme.css	(revision 23275)
@@ -275,10 +275,10 @@
 
 /* TokenInput (with Facebook style for ROMA) */
-ul.token-input-list {border-color:#666;background-color:#444;}
-ul.token-input-list li input {background-color:#444;}
-li.token-input-token span {color:#878787;}
-div.token-input-dropdown {background-color:#eee;border-color:#666;}
-div.token-input-dropdown ul li {background-color:#eee;}
-div.token-input-dropdown ul li.token-input-selected-dropdown-item {background-color:#FF7800;}
+ul.token-input-list {border-color:#666 !important;background-color:#444 !important;}
+ul.token-input-list li input {background-color:#444 !important;}
+li.token-input-token span {color:#878787 !important;}
+div.token-input-dropdown {background-color:#eee !important;border-color:#666 !important;}
+div.token-input-dropdown ul li {background-color:#eee !important;}
+div.token-input-dropdown ul li.token-input-selected-dropdown-item {background-color:#FF7800 !important;}
 
 #progressbar {border:1px solid #666; background-color:#666;}
Index: /trunk/themes/default/js/plugins/jquery.tokeninput.css
===================================================================
--- /trunk/themes/default/js/plugins/jquery.tokeninput.css	(revision 23275)
+++ /trunk/themes/default/js/plugins/jquery.tokeninput.css	(revision 23275)
@@ -0,0 +1,16 @@
+/* TokenInput (with Facebook style) */
+ul.token-input-list {overflow: hidden; height: auto !important; height: 1%;width: 400px;border: 1px solid #8496ba;cursor: text;font-size: 12px;font-family: Verdana;min-height: 1px;z-index: 999;margin: 0;padding: 0;background-color: #fff;list-style-type: none;clear: left;}
+ul.token-input-list li input {border: 0;width: 100px;padding: 3px 8px;background-color: white;margin: 2px 0;-webkit-appearance: caret;}
+li.token-input-token {overflow: hidden; height: auto !important; height: 15px;margin: 3px;padding: 1px 3px;background-color: #eff2f7;color: #000;cursor: default;border: 1px solid #ccd5e4;font-size: 11px;border-radius: 5px;-moz-border-radius: 5px;-webkit-border-radius: 5px;float: left;white-space: nowrap;}
+li.token-input-token p {display: inline;padding: 0;margin: 0;}
+li.token-input-token span {color: #a6b3cf;margin-left: 5px;font-weight: bold;cursor: pointer;}
+li.token-input-selected-token {background-color: #5670a6;border: 1px solid #3b5998;color: #fff;}
+li.token-input-input-token {float: left;margin: 0;padding: 0;list-style-type: none;width:10px;}
+div.token-input-dropdown {position: absolute;width: 400px;background-color: #fff;overflow: hidden;border-left: 1px solid #ccc;border-right: 1px solid #ccc;border-bottom: 1px solid #ccc;cursor: default;font-size: 11px;font-family: Verdana;z-index: 1;}
+div.token-input-dropdown p {margin: 0;padding: 5px;font-weight: bold;color: #777;}
+div.token-input-dropdown ul {margin: 0;padding: 0;}
+div.token-input-dropdown ul li {background-color: #fff;padding: 3px;margin: 0;list-style-type: none;}
+div.token-input-dropdown ul li.token-input-dropdown-item {background-color: #fff;}
+div.token-input-dropdown ul li.token-input-dropdown-item2 {background-color: #fff;}
+div.token-input-dropdown ul li em {font-weight: bold;font-style: normal;}
+div.token-input-dropdown ul li.token-input-selected-dropdown-item {background-color: #3b5998;color: #fff;}
Index: /trunk/themes/default/js/plugins/jquery.tokeninput.js
===================================================================
--- /trunk/themes/default/js/plugins/jquery.tokeninput.js	(revision 23274)
+++ /trunk/themes/default/js/plugins/jquery.tokeninput.js	(revision 23275)
@@ -1,12 +1,5 @@
-/**
-  DON'T MAKE AUTOMATIC UPGRADE
-  This is a merged version of :
-  + https://github.com/gr2m/jquery-tokeninput/
-  + https://github.com/mistic100/jquery-tokeninput/
-*/
-
 /*
  * jQuery Plugin: Tokenizing Autocomplete Text Entry
- * Version 1.4.2
+ * Version 1.6.1
  *
  * Copyright (c) 2009 James Smith (http://loopj.com)
@@ -14,4 +7,5 @@
  * choose which one suits your project best!
  *
+ * https://github.com/mistic100/jquery-tokeninput
  */
 
@@ -19,26 +13,65 @@
 // Default settings
 var DEFAULT_SETTINGS = {
+    // Search settings
+    method: "GET",
+    queryParam: "q",
+    searchDelay: 300,
+    minChars: 1,
+    propertyToSearch: "name",
+    jsonContainer: null,
+    contentType: "json",
+
+    // Prepopulation settings
+    prePopulate: null,
+    processPrePopulate: false,
+
+    // Display settings
     hintText: "Type in a search term",
     noResultsText: "No results",
     searchingText: "Searching...",
-    newText: "(new)",
     deleteText: "&times;",
-    searchDelay: 300,
-    minChars: 1,
+    newText: " (new)",
+    animateDropdown: true,
+    placeholder: null,
+    theme: null,
+    zindex: 999,
+    resultsLimit: null,
+
+    enableHTML: false,
+
+    resultsFormatter: function(item) {
+      var string = item[this.propertyToSearch];
+      return "<li>" + (this.enableHTML ? string : _escapeHTML(string)) + "</li>";
+    },
+
+    tokenFormatter: function(item) {
+      var string = item[this.propertyToSearch];
+      return "<li><p>" + (this.enableHTML ? string : _escapeHTML(string)) + "</p></li>";
+    },
+
+    // Tokenization settings
     tokenLimit: null,
-    jsonContainer: null,
-    method: "GET",
-    contentType: "json",
-    queryParam: "q",
     tokenDelimiter: ",",
     preventDuplicates: false,
-    prePopulate: null,
-    processPrePopulate: false,
-    animateDropdown: true,
+    tokenValue: "id",
+
+    // Behavioral settings
+    allowFreeTagging: false,
+    freeTaggingHint: true,
+    allowTabOut: false,
+
+    // Callbacks
     onResult: null,
+    onCachedResult: null,
     onAdd: null,
+    onFreeTaggingAdd: null,
     onDelete: null,
-    allowCreation: false,
-    caseSensitive: false
+    onReady: null,
+
+    // Other settings
+    idPrefix: "token-input-",
+
+    // Keep track if the input is currently in disabled mode
+    disabled: false
 };
 
@@ -47,4 +80,5 @@
     tokenList: "token-input-list",
     token: "token-input-token",
+    tokenReadOnly: "token-input-token-readonly",
     tokenDelete: "token-input-delete-token",
     selectedToken: "token-input-selected-token",
@@ -54,5 +88,7 @@
     dropdownItem2: "token-input-dropdown-item2",
     selectedDropdownItem: "token-input-selected-dropdown-item",
-    inputToken: "token-input-input-token"
+    inputToken: "token-input-input-token",
+    focused: "token-input-focused",
+    disabled: "token-input-disabled"
 };
 
@@ -83,14 +119,80 @@
 };
 
+var HTML_ESCAPES = {
+  '&': '&amp;',
+  '<': '&lt;',
+  '>': '&gt;',
+  '"': '&quot;',
+  "'": '&#x27;',
+  '/': '&#x2F;'
+};
+
+var HTML_ESCAPE_CHARS = /[&<>"'\/]/g;
+
+function coerceToString(val) {
+  return String((val === null || val === undefined) ? '' : val);
+}
+
+function _escapeHTML(text) {
+  return coerceToString(text).replace(HTML_ESCAPE_CHARS, function(match) {
+    return HTML_ESCAPES[match];
+  });
+}
+
+// Additional public (exposed) methods
+var methods = {
+    init: function(url_or_data_or_function, options) {
+        var settings = $.extend({}, DEFAULT_SETTINGS, options || {});
+
+        return this.each(function () {
+            $(this).data("settings", settings);
+            $(this).data("tokenInputObject", new $.TokenList(this, url_or_data_or_function, settings));
+        });
+    },
+    clear: function() {
+        this.data("tokenInputObject").clear();
+        return this;
+    },
+    add: function(item) {
+        this.data("tokenInputObject").add(item);
+        return this;
+    },
+    remove: function(item) {
+        this.data("tokenInputObject").remove(item);
+        return this;
+    },
+    get: function() {
+        return this.data("tokenInputObject").getTokens();
+    },
+    toggleDisabled: function(disable) {
+        this.data("tokenInputObject").toggleDisabled(disable);
+        return this;
+    },
+    setOptions: function(options){
+        $(this).data("settings", $.extend({}, $(this).data("settings"), options || {}));
+        return this;
+    },
+    destroy: function () {
+        if(this.data("tokenInputObject")){
+            this.data("tokenInputObject").clear();
+            var tmpInput = this;
+            var closest = this.parent();
+            closest.empty();
+            tmpInput.show();
+            closest.append(tmpInput);
+            return tmpInput;
+        }
+    }
+};
 
 // Expose the .tokenInput function to jQuery as a plugin
-$.fn.tokenInput = function (url_or_data, options) {
-    var settings = $.extend({}, DEFAULT_SETTINGS, options || {});
-
-    return this.each(function () {
-        new $.TokenList(this, url_or_data, settings);
-    });
+$.fn.tokenInput = function (method) {
+    // Method calling and initialization logic
+    if(methods[method]) {
+        return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
+    } else {
+        return methods.init.apply(this, arguments);
+    }
 };
-
 
 // TokenList class for each input
@@ -101,33 +203,36 @@
 
     // Configure the data source
-    if(typeof(url_or_data) === "string") {
+    if($.type(url_or_data) === "string" || $.type(url_or_data) === "function") {
         // Set the url to query against
-        settings.url = url_or_data;
+        $(input).data("settings").url = url_or_data;
+
+        // If the URL is a function, evaluate it here to do our initalization work
+        var url = computeURL();
 
         // Make a smart guess about cross-domain if it wasn't explicitly specified
-        if(settings.crossDomain === undefined) {
-            if(settings.url.indexOf("://") === -1) {
-                settings.crossDomain = false;
+        if($(input).data("settings").crossDomain === undefined && typeof url === "string") {
+            if(url.indexOf("://") === -1) {
+                $(input).data("settings").crossDomain = false;
             } else {
-                settings.crossDomain = (location.href.split(/\/+/g)[1] !== settings.url.split(/\/+/g)[1]);
+                $(input).data("settings").crossDomain = (location.href.split(/\/+/g)[1] !== url.split(/\/+/g)[1]);
             }
         }
     } else if(typeof(url_or_data) === "object") {
         // Set the local data to search through
-        settings.local_data = url_or_data;
+        $(input).data("settings").local_data = url_or_data;
     }
 
     // Build class names
-    if(settings.classes) {
+    if($(input).data("settings").classes) {
         // Use custom class names
-        settings.classes = $.extend({}, DEFAULT_CLASSES, settings.classes);
-    } else if(settings.theme) {
+        $(input).data("settings").classes = $.extend({}, DEFAULT_CLASSES, $(input).data("settings").classes);
+    } else if($(input).data("settings").theme) {
         // Use theme-suffixed default class names
-        settings.classes = {};
+        $(input).data("settings").classes = {};
         $.each(DEFAULT_CLASSES, function(key, value) {
-            settings.classes[key] = value + "-" + settings.theme;
+            $(input).data("settings").classes[key] = value + "-" + $(input).data("settings").theme;
         });
     } else {
-        settings.classes = DEFAULT_CLASSES;
+        $(input).data("settings").classes = DEFAULT_CLASSES;
     }
 
@@ -147,15 +252,27 @@
 
     // Create a new text input an attach keyup events
-    var input_box = $("<input type=\"text\"  autocomplete=\"off\">")
+    var input_box = $("<input type=\"text\"  autocomplete=\"off\" autocapitalize=\"off\">")
         .css({
             outline: "none"
         })
+        .attr("id", $(input).data("settings").idPrefix + input.id)
         .focus(function () {
-            if (settings.tokenLimit === null || settings.tokenLimit !== token_count) {
+            if ($(input).data("settings").disabled) {
+                return false;
+            } else
+            if ($(input).data("settings").tokenLimit === null || $(input).data("settings").tokenLimit !== token_count) {
                 show_dropdown_hint();
             }
+            token_list.addClass($(input).data("settings").classes.focused);
         })
         .blur(function () {
             hide_dropdown();
+
+            if ($(input).data("settings").allowFreeTagging) {
+              add_freetagging_tokens();
+            }
+
+            $(this).val("");
+            token_list.removeClass($(input).data("settings").classes.focused);
         })
         .bind("keyup keydown blur update", resize_input)
@@ -199,6 +316,6 @@
                             select_dropdown_item(dropdown_item);
                         }
-                        return false;
                     }
+                    return false;
                     break;
 
@@ -209,4 +326,5 @@
                         if(selected_token) {
                             delete_token($(selected_token));
+                            hidden_input.change();
                         } else if(previous_token.length) {
                             select_token($(previous_token.get(0)));
@@ -227,8 +345,23 @@
                 case KEY.COMMA:
                   if(selected_dropdown_item) {
-                    add_token($(selected_dropdown_item));
-                    return false;
+                    add_token($(selected_dropdown_item).data("tokeninput"));
+                    hidden_input.change();
+                  } else {
+                    if ($(input).data("settings").allowFreeTagging) {
+                      if($(input).data("settings").allowTabOut && $(this).val() === "") {
+                        return true;
+                      } else {
+                        add_freetagging_tokens();
+                      }
+                    } else {
+                      $(this).val("");
+                      if($(input).data("settings").allowTabOut) {
+                        return true;
+                      }
+                    }
+                    event.stopPropagation();
+                    event.preventDefault();
                   }
-                  break;
+                  return false;
 
                 case KEY.ESCAPE:
@@ -245,27 +378,50 @@
         });
 
+    // Keep reference for placeholder
+    if (settings.placeholder)
+        input_box.attr("placeholder", settings.placeholder)
+
     if ($(input).get(0).tagName == 'SELECT') {
-    // Create a new input to store selected tokens, original will be delete later
-      var hidden_input = $("<input type=\"text\"  name=\"" + $(input).attr('name') + "\" autocomplete=\"off\">")
-                             .hide()
-                             .val("")
-                             .focus(function () {
-                                 input_box.focus();
-                             })
-                             .blur(function () {
-                                 input_box.blur();
-                             })
-                             .insertBefore(input);
+        // Create a new input to store selected tokens, original will be removed later
+        var hidden_input = $("<input type=\"text\"  name=\"" + $(input).attr('name') + "\" autocomplete=\"off\">")
+                               .hide()
+                               .val("")
+                               .focus(function () {
+                                   focus_with_timeout(input_box);
+                               })
+                               .blur(function () {
+                                   input_box.blur();
+                                   return hidden_input;
+                               })
+                               .insertBefore(input);
+
+        // get prepopulate options and store them in hidden_input
+        var select_data = [];
+        $(input).children('option').each(function () {
+            var item = {};
+            item[$(input).data("settings").tokenValue] = $(this).attr('value');
+            item[$(input).data("settings").propertyToSearch] = $(this).text();
+            select_data[ select_data.length ] = item;
+        });
+        hidden_input.data("pre", select_data);
+        
+        // remove the SELECT object
+        hidden_input.data("settings", $(input).data("settings"));
+        $(input).remove();
+        input = hidden_input[0];
+
     } else {
-    // Keep a reference to the original input box
-      var hidden_input = $(input)
-                             .hide()
-                             .val("")
-                             .focus(function () {
-                                 input_box.focus();
-                             })
-                             .blur(function () {
-                                 input_box.blur();
-                             });
+        // Keep a reference to the original input box
+        var hidden_input = $(input)
+                               .hide()
+                               .val("")
+                               .focus(function () {
+                                   focus_with_timeout(input_box);
+                               })
+                               .blur(function () {
+                                   input_box.blur();
+                                   //return the object to this can be referenced in the callback functions.
+                                   return hidden_input;
+                               });
     }
 
@@ -277,5 +433,5 @@
     // The list to store the token items in
     var token_list = $("<ul />")
-        .addClass(settings.classes.tokenList)
+        .addClass($(input).data("settings").classes.tokenList)
         .click(function (event) {
             var li = $(event.target).closest("li");
@@ -289,5 +445,5 @@
 
                 // Focus input box
-                input_box.focus();
+                focus_with_timeout(input_box);
             }
         })
@@ -295,5 +451,5 @@
             var li = $(event.target).closest("li");
             if(li && selected_token !== this) {
-                li.addClass(settings.classes.highlightedToken);
+                li.addClass($(input).data("settings").classes.highlightedToken);
             }
         })
@@ -301,5 +457,5 @@
             var li = $(event.target).closest("li");
             if(li && selected_token !== this) {
-                li.removeClass(settings.classes.highlightedToken);
+                li.removeClass($(input).data("settings").classes.highlightedToken);
             }
         })
@@ -308,5 +464,5 @@
     // The token holding the input box
     var input_token = $("<li />")
-        .addClass(settings.classes.inputToken)
+        .addClass($(input).data("settings").classes.inputToken)
         .appendTo(token_list)
         .append(input_box);
@@ -314,5 +470,5 @@
     // The list to store the dropdown items in
     var dropdown = $("<div>")
-        .addClass(settings.classes.dropdown)
+        .addClass($(input).data("settings").classes.dropdown)
         .appendTo("body")
         .hide();
@@ -335,22 +491,70 @@
     // Pre-populate list if items exist
     hidden_input.val("");
-    var li_data = settings.prePopulate || hidden_input.data("pre");
-    if(settings.processPrePopulate && $.isFunction(settings.onResult)) {
-        li_data = settings.onResult.call(hidden_input, li_data);
-    }    
+    var li_data = $(input).data("settings").prePopulate || hidden_input.data("pre");
+    if($(input).data("settings").processPrePopulate && $.isFunction($(input).data("settings").onResult)) {
+        li_data = $(input).data("settings").onResult.call(hidden_input, li_data);
+    }
     if(li_data && li_data.length) {
         $.each(li_data, function (index, value) {
-            insert_token(value.id, value.name);
+            insert_token(value);
+            checkTokenLimit();
+            input_box.attr("placeholder", null)
         });
     }
-    
-    // Pre-populate from SELECT options
-    if ($(input).get(0).tagName == 'SELECT') {
-      $(input).children('option').each(function () {
-        insert_token($(this).attr('value'), $(this).text());
-      });
-    }
-
-
+
+    // Check if widget should initialize as disabled
+    if ($(input).data("settings").disabled) {
+        toggleDisabled(true);
+    }
+
+    // Initialization is done
+    if($.isFunction($(input).data("settings").onReady)) {
+        $(input).data("settings").onReady.call();
+    }
+
+    //
+    // Public functions
+    //
+
+    this.clear = function() {
+        token_list.children("li").each(function() {
+            if ($(this).children("input").length === 0) {
+                delete_token($(this));
+            }
+        });
+    };
+
+    this.add = function(item) {
+        add_token(item);
+    };
+
+    this.remove = function(item) {
+        token_list.children("li").each(function() {
+            if ($(this).children("input").length === 0) {
+                var currToken = $(this).data("tokeninput");
+                var match = true;
+                for (var prop in item) {
+                    if (item[prop] !== currToken[prop]) {
+                        match = false;
+                        break;
+                    }
+                }
+                if (match) {
+                    delete_token($(this));
+                }
+            }
+        });
+    };
+
+    this.getTokens = function() {
+        return saved_tokens;
+    };
+
+    this.toggleDisabled = function(disable) {
+        toggleDisabled(disable);
+    };
+
+    // Resize input to maximum width so the placeholder can be seen
+    resize_input();
 
     //
@@ -358,11 +562,43 @@
     //
 
+    function escapeHTML(text) {
+      return $(input).data("settings").enableHTML ? text : _escapeHTML(text);
+    }
+
+    // Toggles the widget between enabled and disabled state, or according
+    // to the [disable] parameter.
+    function toggleDisabled(disable) {
+        if (typeof disable === 'boolean') {
+            $(input).data("settings").disabled = disable
+        } else {
+            $(input).data("settings").disabled = !$(input).data("settings").disabled;
+        }
+        input_box.attr('disabled', $(input).data("settings").disabled);
+        token_list.toggleClass($(input).data("settings").classes.disabled, $(input).data("settings").disabled);
+        // if there is any token selected we deselect it
+        if(selected_token) {
+            deselect_token($(selected_token), POSITION.END);
+        }
+        hidden_input.attr('disabled', $(input).data("settings").disabled);
+    }
+
+    function checkTokenLimit() {
+        if($(input).data("settings").tokenLimit !== null && token_count >= $(input).data("settings").tokenLimit) {
+            input_box.hide();
+            hide_dropdown();
+            return;
+        }
+    }
+
     function resize_input() {
         if(input_val === (input_val = input_box.val())) {return;}
 
+        // Get width left on the current line
+        var width_left = token_list.width() - input_box.offset().left - token_list.offset().left;
         // Enter new content into resizer and resize input accordingly
-        var escaped = input_val.replace(/&/g, '&amp;').replace(/\s/g,' ').replace(/</g, '&lt;').replace(/>/g, '&gt;');
-        input_resizer.html(escaped);
-        input_box.width(input_resizer.width() + 30);
+        input_resizer.html(_escapeHTML(input_val));
+        // Get maximum width, minimum the size of input and maximum the widget's width
+        input_box.width(Math.min(token_list.width(),
+                                 Math.max(width_left, input_resizer.width() + 30)));
     }
 
@@ -374,22 +610,47 @@
     }
 
+    function add_freetagging_tokens() {
+        var value = $.trim(input_box.val());
+        var tokens = value.split($(input).data("settings").tokenDelimiter);
+        $.each(tokens, function(i, token) {
+          if (!token) {
+            return;
+          }
+
+          if ($.isFunction($(input).data("settings").onFreeTaggingAdd)) {
+            token = $(input).data("settings").onFreeTaggingAdd.call(hidden_input, token);
+          }
+          var object = {};
+          object[$(input).data("settings").tokenValue] = object[$(input).data("settings").propertyToSearch] = token;
+          add_token(object);
+        });
+    }
+
     // Inner function to a token to the list
-    function insert_token(id, value) {
-        var this_token = $("<li><p>"+ value +"</p></li>")
-          .addClass(settings.classes.token)
-          .insertBefore(input_token);
+    function insert_token(item) {
+        var $this_token = $($(input).data("settings").tokenFormatter(item));
+        var readonly = item.readonly === true ? true : false;
+
+        if(readonly) $this_token.addClass($(input).data("settings").classes.tokenReadOnly);
+
+        $this_token.addClass($(input).data("settings").classes.token).insertBefore(input_token);
 
         // The 'delete token' button
-        $("<span>" + settings.deleteText + "</span>")
-            .addClass(settings.classes.tokenDelete)
-            .appendTo(this_token)
-            .click(function () {
-                delete_token($(this).parent());
-                return false;
-            });
+        if(!readonly) {
+          $("<span>" + $(input).data("settings").deleteText + "</span>")
+              .addClass($(input).data("settings").classes.tokenDelete)
+              .appendTo($this_token)
+              .click(function () {
+                  if (!$(input).data("settings").disabled) {
+                      delete_token($(this).parent());
+                      hidden_input.change();
+                      return false;
+                  }
+              });
+        }
 
         // Store data on the token
-        var token_data = {"id": id, "name": value};
-        $.data(this_token.get(0), "tokeninput", token_data);
+        var token_data = item;
+        $.data($this_token.get(0), "tokeninput", item);
 
         // Save this token for duplicate checking
@@ -398,26 +659,28 @@
 
         // Update the hidden input
-        var token_ids = $.map(saved_tokens, function (el) {
-            return el.id;
-        });
-        hidden_input.val(token_ids.join(settings.tokenDelimiter));
+        update_hidden_input(saved_tokens, hidden_input);
 
         token_count += 1;
 
-        return this_token;
+        // Check the token limit
+        if($(input).data("settings").tokenLimit !== null && token_count >= $(input).data("settings").tokenLimit) {
+            input_box.hide();
+            hide_dropdown();
+        }
+
+        return $this_token;
     }
 
     // Add a token to the token list based on user input
     function add_token (item) {
-        var li_data = $.data(item.get(0), "tokeninput");
-        var callback = settings.onAdd;
+        var callback = $(input).data("settings").onAdd;
 
         // See if the token already exists and select it if we don't want duplicates
-        if(token_count > 0 && settings.preventDuplicates) {
+        if(token_count > 0 && $(input).data("settings").preventDuplicates) {
             var found_existing_token = null;
             token_list.children().each(function () {
                 var existing_token = $(this);
                 var existing_data = $.data(existing_token.get(0), "tokeninput");
-                if(existing_data && existing_data.id === li_data.id) {
+                if(existing_data && existing_data[settings.tokenValue] === item[settings.tokenValue]) {
                     found_existing_token = existing_token;
                     return false;
@@ -428,19 +691,18 @@
                 select_token(found_existing_token);
                 input_token.insertAfter(found_existing_token);
-                input_box.focus();
+                focus_with_timeout(input_box);
                 return;
             }
         }
 
+        // Squeeze input_box so we force no unnecessary line break
+        input_box.width(0);
+
         // Insert the new tokens
-        insert_token(li_data.id, li_data.name);
-
-        // Check the token limit
-        if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) {
-            input_box.hide();
-            hide_dropdown();
-            return;
-        } else {
-            input_box.focus();
+        if($(input).data("settings").tokenLimit == null || token_count < $(input).data("settings").tokenLimit) {
+            insert_token(item);
+            // Remove the placeholder so it's not seen after you've added a token
+            input_box.attr("placeholder", null)
+            checkTokenLimit();
         }
 
@@ -453,5 +715,5 @@
         // Execute the onAdd callback if defined
         if($.isFunction(callback)) {
-            callback.call(hidden_input,li_data);
+            callback.call(hidden_input,item);
         }
     }
@@ -459,17 +721,19 @@
     // Select a token in the token list
     function select_token (token) {
-        token.addClass(settings.classes.selectedToken);
-        selected_token = token.get(0);
-
-        // Hide input box
-        input_box.val("");
-
-        // Hide dropdown if it is visible (eg if we clicked to select token)
-        hide_dropdown();
+        if (!$(input).data("settings").disabled) {
+            token.addClass($(input).data("settings").classes.selectedToken);
+            selected_token = token.get(0);
+
+            // Hide input box
+            input_box.val("");
+
+            // Hide dropdown if it is visible (eg if we clicked to select token)
+            hide_dropdown();
+        }
     }
 
     // Deselect a token in the token list
     function deselect_token (token, position) {
-        token.removeClass(settings.classes.selectedToken);
+        token.removeClass($(input).data("settings").classes.selectedToken);
         selected_token = null;
 
@@ -486,5 +750,5 @@
 
         // Show the input box and give it focus again
-        input_box.focus();
+        focus_with_timeout(input_box);
     }
 
@@ -508,5 +772,5 @@
         // Remove the id from the saved list
         var token_data = $.data(token.get(0), "tokeninput");
-        var callback = settings.onDelete;
+        var callback = $(input).data("settings").onDelete;
 
         var index = token.prevAll().length;
@@ -518,23 +782,23 @@
 
         // Show the input box and give it focus again
-        input_box.focus();
+        focus_with_timeout(input_box);
 
         // Remove this token from the saved list
         saved_tokens = saved_tokens.slice(0,index).concat(saved_tokens.slice(index+1));
+        if (saved_tokens.length == 0) {
+            input_box.attr("placeholder", settings.placeholder)
+        }
         if(index < selected_token_index) selected_token_index--;
 
         // Update the hidden input
-        var token_ids = $.map(saved_tokens, function (el) {
-            return el.id;
-        });
-        hidden_input.val(token_ids.join(settings.tokenDelimiter));
+        update_hidden_input(saved_tokens, hidden_input);
 
         token_count -= 1;
 
-        if(settings.tokenLimit !== null) {
+        if($(input).data("settings").tokenLimit !== null) {
             input_box
                 .show()
-                .val("")
-                .focus();
+                .val("");
+            focus_with_timeout(input_box);
         }
 
@@ -543,4 +807,16 @@
             callback.call(hidden_input,token_data);
         }
+    }
+
+    // Update the hidden input box value
+    function update_hidden_input(saved_tokens, hidden_input) {
+        var token_values = $.map(saved_tokens, function (el) {
+            if(typeof $(input).data("settings").tokenValue == 'function')
+              return $(input).data("settings").tokenValue.call(this, el);
+
+            return el[$(input).data("settings").tokenValue];
+        });
+        hidden_input.val(token_values.join($(input).data("settings").tokenDelimiter));
+
     }
 
@@ -555,7 +831,8 @@
             .css({
                 position: "absolute",
-                top: $(token_list).offset().top + $(token_list).outerHeight(),
-                left: $(token_list).offset().left,
-                zindex: 999
+                top: token_list.offset().top + token_list.outerHeight(),
+                left: token_list.offset().left,
+                width: token_list.width(),
+                'z-index': $(input).data("settings").zindex
             })
             .show();
@@ -563,6 +840,6 @@
 
     function show_dropdown_searching () {
-        if(settings.searchingText) {
-            dropdown.html("<p>"+settings.searchingText+"</p>");
+        if($(input).data("settings").searchingText) {
+            dropdown.html("<p>" + escapeHTML($(input).data("settings").searchingText) + "</p>");
             show_dropdown();
         }
@@ -570,20 +847,29 @@
 
     function show_dropdown_hint () {
-        if(settings.hintText) {
-            dropdown.html("<p>"+settings.hintText+"</p>");
+        if($(input).data("settings").hintText) {
+            dropdown.html("<p>" + escapeHTML($(input).data("settings").hintText) + "</p>");
             show_dropdown();
         }
+    }
+
+    var regexp_special_chars = new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\-]', 'g');
+    function regexp_escape(term) {
+        return term.replace(regexp_special_chars, '\\$&');
     }
 
     // Highlight the query part of the search term
     function highlight_term(value, term) {
-        var param = "g";
-        if (!settings.caseSensitive) param+= "i";
-        return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + escape_regexp_chars(term) + ")(?![^<>]*>)(?![^&;]+;)", param), "<b>$1</b>");
-    }
-    
-    function escape_regexp_chars(string) {
-        var specials = new RegExp("[.*+?|()\\[\\]{}\\\\]", "g"); // .*+?|()[]{}\
-        return string.replace(specials, "\\$&");
+        return value.replace(
+          new RegExp(
+            "(?![^&;]+;)(?!<[^<>]*)(" + regexp_escape(term) + ")(?![^<>]*>)(?![^&;]+;)",
+            "gi"
+          ), function(match, p1) {
+            return "<b>" + escapeHTML(p1) + "</b>";
+          }
+        );
+    }
+
+    function find_value_and_highlight_term(template, value, term) {
+        return template.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + regexp_escape(value) + ")(?![^<>]*>)(?![^&;]+;)", "g"), highlight_term(value, term));
     }
 
@@ -598,17 +884,25 @@
                 })
                 .mousedown(function (event) {
-                    add_token($(event.target).closest("li"));
+                    add_token($(event.target).closest("li").data("tokeninput"));
+                    hidden_input.change();
                     return false;
                 })
                 .hide();
 
+            if ($(input).data("settings").resultsLimit && results.length > $(input).data("settings").resultsLimit) {
+                results = results.slice(0, $(input).data("settings").resultsLimit);
+            }
+
             $.each(results, function(index, value) {
-                var this_li = $("<li>" + highlight_term(value.name, query) + "</li>")
-                                  .appendTo(dropdown_ul);
+                var this_li = $(input).data("settings").resultsFormatter(value);
+
+                this_li = find_value_and_highlight_term(this_li ,value[$(input).data("settings").propertyToSearch], query);
+
+                this_li = $(this_li).appendTo(dropdown_ul);
 
                 if(index % 2) {
-                    this_li.addClass(settings.classes.dropdownItem);
+                    this_li.addClass($(input).data("settings").classes.dropdownItem);
                 } else {
-                    this_li.addClass(settings.classes.dropdownItem2);
+                    this_li.addClass($(input).data("settings").classes.dropdownItem2);
                 }
 
@@ -617,10 +911,10 @@
                 }
 
-                $.data(this_li.get(0), "tokeninput", {"id": value.id, "name": value.name});
+                $.data(this_li.get(0), "tokeninput", value);
             });
 
             show_dropdown();
 
-            if(settings.animateDropdown) {
+            if($(input).data("settings").animateDropdown) {
                 dropdown_ul.slideDown("fast");
             } else {
@@ -628,6 +922,6 @@
             }
         } else {
-            if(settings.noResultsText) {
-                dropdown.html("<p>"+settings.noResultsText+"</p>");
+            if($(input).data("settings").noResultsText) {
+                dropdown.html("<p>" + escapeHTML($(input).data("settings").noResultsText) + "</p>");
                 show_dropdown();
             }
@@ -642,5 +936,5 @@
             }
 
-            item.addClass(settings.classes.selectedDropdownItem);
+            item.addClass($(input).data("settings").classes.selectedDropdownItem);
             selected_dropdown_item = item.get(0);
         }
@@ -649,13 +943,12 @@
     // Remove highlighting from an item in the results dropdown
     function deselect_dropdown_item (item) {
-        item.removeClass(settings.classes.selectedDropdownItem);
+        item.removeClass($(input).data("settings").classes.selectedDropdownItem);
         selected_dropdown_item = null;
     }
 
     // Do a search and show the "searching" dropdown if the input is longer
-    // than settings.minChars
+    // than $(input).data("settings").minChars
     function do_search() {
         var query = input_box.val();
-        if (!settings.caseSensitive) query = query.toLowerCase();
 
         if(query && query.length) {
@@ -664,5 +957,5 @@
             }
 
-            if(query.length >= settings.minChars) {
+            if(query.length >= $(input).data("settings").minChars) {
                 show_dropdown_searching();
                 clearTimeout(timeout);
@@ -670,5 +963,5 @@
                 timeout = setTimeout(function(){
                     run_search(query);
-                }, settings.searchDelay);
+                }, $(input).data("settings").searchDelay);
             } else {
                 hide_dropdown();
@@ -679,15 +972,20 @@
     // Do the actual search
     function run_search(query) {
-        var cached_results = cache.get(query);
+        var cache_key = query + computeURL();
+        var cached_results = cache.get(cache_key);
         if(cached_results) {
+            if ($.isFunction($(input).data("settings").onCachedResult)) {
+              cached_results = $(input).data("settings").onCachedResult.call(hidden_input, cached_results);
+            }
             populate_dropdown(query, cached_results);
         } else {
             // Are we doing an ajax search or local data search?
-            if(settings.url) {
+            if($(input).data("settings").url) {
+                var url = computeURL();
                 // Extract exisiting get params
                 var ajax_params = {};
                 ajax_params.data = {};
-                if(settings.url.indexOf("?") > -1) {
-                    var parts = settings.url.split("?");
+                if(url.indexOf("?") > -1) {
+                    var parts = url.split("?");
                     ajax_params.url = parts[0];
 
@@ -698,12 +996,12 @@
                     });
                 } else {
-                    ajax_params.url = settings.url;
+                    ajax_params.url = url;
                 }
 
                 // Prepare the request
-                ajax_params.data[settings.queryParam] = query;
-                ajax_params.type = settings.method;
-                ajax_params.dataType = settings.contentType;
-                if(settings.crossDomain) {
+                ajax_params.data[$(input).data("settings").queryParam] = query;
+                ajax_params.type = $(input).data("settings").method;
+                ajax_params.dataType = $(input).data("settings").contentType;
+                if($(input).data("settings").crossDomain) {
                     ajax_params.dataType = "jsonp";
                 }
@@ -711,18 +1009,16 @@
                 // Attach the success callback
                 ajax_params.success = function(results) {
-                  if($.isFunction(settings.onResult)) {
-                      results = settings.onResult.call(hidden_input, results);
+                  cache.add(cache_key, $(input).data("settings").jsonContainer ? results[$(input).data("settings").jsonContainer] : results);
+                  if($.isFunction($(input).data("settings").onResult)) {
+                      results = $(input).data("settings").onResult.call(hidden_input, results);
                   }
                   
-                  if(settings.allowCreation) {
-                      results.push({name: input_box.val() + settings.newText, id: input_box.val()});
+                  if($(input).data("settings").allowFreeTagging && $(input).data("settings").freeTaggingHint) {
+                      results.push({name: input_box.val() + $(input).data("settings").newText, id: input_box.val()});
                   }
-                  cache.add(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
 
                   // only populate the dropdown if the results are associated with the active search query
-                  var value = input_box.val();
-                  if (!settings.caseSensitive) value = value.toLowerCase();
-                  if(value === query) {
-                      populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
+                  if(input_box.val() === query) {
+                      populate_dropdown(query, $(input).data("settings").jsonContainer ? results[$(input).data("settings").jsonContainer] : results);
                   }
                 };
@@ -730,33 +1026,41 @@
                 // Make the request
                 $.ajax(ajax_params);
-            } else if(settings.local_data) {
+            } else if($(input).data("settings").local_data) {
                 // Do the search through local data
-                var results = $.grep(settings.local_data, function (row) {
-                    if (settings.caseSensitive) {
-                        return row.name.indexOf(query) > -1;
-                    }
-                    else {
-                        return row.name.toLowerCase().indexOf(query.toLowerCase()) > -1;
-                    }
+                var results = $.grep($(input).data("settings").local_data, function (row) {
+                    return row[$(input).data("settings").propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1;
                 });
-
-                if($.isFunction(settings.onResult)) {
-                    results = settings.onResult.call(hidden_input, results);
+                
+                if($(input).data("settings").allowFreeTagging && $(input).data("settings").freeTaggingHint) {
+                    results.push({name: input_box.val() + $(input).data("settings").newText, id: input_box.val()});
                 }
-                
-                if(settings.allowCreation) {
-                    results.push({name: input_box.val() + settings.newText, id: input_box.val()});
+
+                cache.add(cache_key, results);
+                if($.isFunction($(input).data("settings").onResult)) {
+                    results = $(input).data("settings").onResult.call(hidden_input, results);
                 }
-                
-                cache.add(query, results);                
-                
                 populate_dropdown(query, results);
             }
         }
     }
-    
-    if ($(input).get(0).tagName == 'SELECT') {
-      $(input).remove();
-    }
+
+    // compute the dynamic URL
+    function computeURL() {
+        var url = $(input).data("settings").url;
+        if(typeof $(input).data("settings").url == 'function') {
+            url = $(input).data("settings").url.call($(input).data("settings"));
+        }
+        return url;
+    }
+
+    // Bring browser focus to the specified object.
+    // Use of setTimeout is to get around an IE bug.
+    // (See, e.g., http://stackoverflow.com/questions/2600186/focus-doesnt-work-in-ie)
+    //
+    // obj: a jQuery object to focus()
+    function focus_with_timeout(obj) {
+        setTimeout(function() { obj.focus(); }, 50);
+    }
+
 };
 
@@ -792,2 +1096,3 @@
 };
 }(jQuery));
+
