Anonymous avatar Anonymous committed 908e2eb

[gsoc2009/admin-ui] Some love for autocomplete.

- A new, nicer m2m autocomplete
- Backend autocomplete view returns JSON instead of a funky parsed format
- Upgraded the FKey autocomplete plugin

Comments (0)

Files changed (7)

django/contrib/admin/media/css/jquery.token.input.css

+ul.token-input-list {
+    overflow: hidden; 
+    height: auto !important; 
+    height: 1%;
+    width: 400px;
+    border: 1px solid #999;
+    cursor: text;
+    font-size: 12px;
+    font-family: Verdana;
+    z-index: 999;
+    margin: 0;
+    padding: 0 !important;
+    background-color: #fff;
+}
+
+ul.token-input-list li {
+    list-style-type: none;
+}
+
+ul.token-input-list li input {
+    border: 0;
+    width: 350px;
+    padding: 3px 8px;
+    background-color: white;
+}
+
+li.token-input-token {
+    overflow: hidden; 
+    height: auto !important; 
+    height: 1%;
+    margin: 3px;
+    padding: 3px 5px;
+    background-color: #d0efa0;
+    color: #000;
+    font-weight: bold;
+    cursor: default;
+    display: block;
+}
+
+li.token-input-token p {
+    float: left;
+    padding: 0;
+    margin: 0;
+}
+
+li.token-input-token span {
+    float: right;
+    color: #777;
+    cursor: pointer;
+}
+
+li.token-input-selected-token {
+    background-color: #08844e;
+    color: #fff;
+}
+
+li.token-input-selected-token span {
+    color: #bbb;
+}
+
+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: 12px;
+    font-family: Verdana;
+    z-index: 1;
+    margin-left: 106px;
+}
+
+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;
+    list-style-type: none;
+}
+
+div.token-input-dropdown ul li.token-input-dropdown-item {
+    background-color: #fafafa;
+}
+
+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: #d0efa0;
+}
+

django/contrib/admin/media/js/jquery.autocomplete.js

 /*
- * Autocomplete - jQuery plugin 1.0.2
+ * jQuery Autocomplete plugin 1.1
  *
- * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
+ * Copyright (c) 2009 Jörn Zaefferer
  *
  * Dual licensed under the MIT and GPL licenses:
  *   http://www.opensource.org/licenses/mit-license.php
  *   http://www.gnu.org/licenses/gpl.html
  *
- * Revision: $Id: jquery.autocomplete.js 5747 2008-06-25 18:30:55Z joern.zaefferer $
- *
+ * Revision: $Id: jquery.autocomplete.js 14 2009-08-22 10:29:29Z joern.zaefferer $
  */
 
 ;(function($) {
 	
 	// only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
 	$input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
+		// a keypress means the input has focus
+		// avoids issue where input had focus before the autocomplete was applied
+		hasFocus = 1;
 		// track last key pressed
 		lastKeyPressCode = event.keyCode;
 		switch(event.keyCode) {
 		if ( options.multiple ) {
 			var words = trimWords($input.val());
 			if ( words.length > 1 ) {
-				v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
+				var seperator = options.multipleSeparator.length;
+				var cursorAt = $(input).selection().start;
+				var wordAt, progress = 0;
+				$.each(words, function(i, word) {
+					progress += word.length;
+					if (cursorAt <= progress) {
+						wordAt = i;
+						return false;
+					}
+					progress += seperator;
+				});
+				words[wordAt] = v;
+				// TODO this should set the cursor to the right position, but it gets overriden somewhere
+				//$.Autocompleter.Selection(input, progress + seperator, progress + seperator);
+				v = words.join( options.multipleSeparator );
 			}
 			v += options.multipleSeparator;
 		}
 	};
 	
 	function trimWords(value) {
-		if ( !value ) {
+		if (!value)
 			return [""];
-		}
-		var words = value.split( options.multipleSeparator );
-		var result = [];
-		$.each(words, function(i, value) {
-			if ( $.trim(value) )
-				result[i] = $.trim(value);
+		if (!options.multiple)
+			return [$.trim(value)];
+		return $.map(value.split(options.multipleSeparator), function(word) {
+			return $.trim(value).length ? $.trim(word) : null;
 		});
-		return result;
 	}
 	
 	function lastWord(value) {
 		if ( !options.multiple )
 			return value;
 		var words = trimWords(value);
+		if (words.length == 1) 
+			return words[0];
+		var cursorAt = $(input).selection().start;
+		if (cursorAt == value.length) {
+			words = trimWords(value)
+		} else {
+			words = trimWords(value.replace(value.substring(cursorAt), ""));
+		}
 		return words[words.length - 1];
 	}
 	
 			// fill in the value (keep the case the user has typed)
 			$input.val($input.val() + sValue.substring(lastWord(previousValue).length));
 			// select the portion of the value not typed by the user (so the next character will erase)
-			$.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
+			$(input).selection(previousValue.length, previousValue.length + sValue.length);
 		}
 	};
 
 							var words = trimWords($input.val()).slice(0, -1);
 							$input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
 						}
-						else
+						else {
 							$input.val( "" );
+							$input.trigger("result", null);
+						}
 					}
 				}
 			);
 		}
-		if (wasVisible)
-			// position cursor at end of input field
-			$.Autocompleter.Selection(input, input.value.length, input.value.length);
 	};
 
 	function receiveData(q, data) {
 		if (!options.matchCase) 
 			s = s.toLowerCase();
 		var i = s.indexOf(sub);
+		if (options.matchContains == "word"){
+			i = s.toLowerCase().search("\\b" + sub.toLowerCase());
+		}
 		if (i == -1) return false;
 		return i == 0 || options.matchContains;
 	};
 	};
 };
 
-$.Autocompleter.Selection = function(field, start, end) {
-	if( field.createTextRange ){
-		var selRange = field.createTextRange();
-		selRange.collapse(true);
-		selRange.moveStart("character", start);
-		selRange.moveEnd("character", end);
-		selRange.select();
-	} else if( field.setSelectionRange ){
-		field.setSelectionRange(start, end);
-	} else {
-		if( field.selectionStart ){
-			field.selectionStart = start;
-			field.selectionEnd = end;
+$.fn.selection = function(start, end) {
+	if (start !== undefined) {
+		return this.each(function() {
+			if( this.createTextRange ){
+				var selRange = this.createTextRange();
+				if (end === undefined || start == end) {
+					selRange.move("character", start);
+					selRange.select();
+				} else {
+					selRange.collapse(true);
+					selRange.moveStart("character", start);
+					selRange.moveEnd("character", end);
+					selRange.select();
+				}
+			} else if( this.setSelectionRange ){
+				this.setSelectionRange(start, end);
+			} else if( this.selectionStart ){
+				this.selectionStart = start;
+				this.selectionEnd = end;
+			}
+		});
+	}
+	var field = this[0];
+	if ( field.createTextRange ) {
+		var range = document.selection.createRange(),
+			orig = field.value,
+			teststring = "<->",
+			textLength = range.text.length;
+		range.text = teststring;
+		var caretAt = field.value.indexOf(teststring);
+		field.value = orig;
+		this.selection(caretAt, caretAt + textLength);
+		return {
+			start: caretAt,
+			end: caretAt + textLength
+		}
+	} else if( field.selectionStart !== undefined ){
+		return {
+			start: field.selectionStart,
+			end: field.selectionEnd
 		}
 	}
-	field.focus();
 };
 
 })(jQuery);

django/contrib/admin/media/js/jquery.tokeninput.js

+/*
+ * jQuery Plugin: Tokenizing Autocomplete Text Entry
+ * Version 1.1
+ *
+ * Copyright (c) 2009 James Smith (http://loopj.com)
+ * Licensed jointly under the GPL and MIT licenses,
+ * choose which one suits your project best!
+ *
+ */
+
+(function($) {
+
+$.fn.tokenInput = function (url, options) {
+    var settings = $.extend({
+        url: url,
+        hintText: "Type in a search term",
+        noResultsText: "No results",
+        searchingText: "Searching...",
+        searchDelay: 300,
+        minChars: 1,
+        tokenLimit: null,
+        jsonContainer: null,
+        method: "GET",
+        contentType: "json",
+        queryParam: "q",
+        onResult: null
+    }, options);
+
+    settings.classes = $.extend({
+        tokenList: "token-input-list",
+        token: "token-input-token",
+        tokenDelete: "token-input-delete-token",
+        selectedToken: "token-input-selected-token",
+        highlightedToken: "token-input-highlighted-token",
+        dropdown: "token-input-dropdown",
+        dropdownItem: "token-input-dropdown-item",
+        dropdownItem2: "token-input-dropdown-item2",
+        selectedDropdownItem: "token-input-selected-dropdown-item",
+        inputToken: "token-input-input-token"
+    }, options.classes);
+
+    return this.each(function () {
+        var list = new $.TokenList(this, settings);
+    });
+};
+
+$.TokenList = function (input, settings) {
+    //
+    // Variables
+    //
+
+    // Input box position "enum"
+    var POSITION = {
+        BEFORE: 0,
+        AFTER: 1,
+        END: 2
+    };
+
+    // Keys "enum"
+    var KEY = {
+        BACKSPACE: 8,
+        TAB: 9,
+        RETURN: 13,
+        ESC: 27,
+        LEFT: 37,
+        UP: 38,
+        RIGHT: 39,
+        DOWN: 40,
+        COMMA: 188
+    };
+
+    // Save the tokens
+    var saved_tokens = [];
+    
+    // Keep track of the number of tokens in the list
+    var token_count = 0;
+
+    // Basic cache to save on db hits
+    var cache = new $.TokenList.Cache();
+
+    // Keep track of the timeout
+    var timeout;
+
+    // Create a new text input an attach keyup events
+    var input_box = $("<input type=\"text\">")
+        .css({
+            outline: "none"
+        })
+        .focus(function () {
+            if (settings.tokenLimit == null || settings.tokenLimit != token_count) {
+                show_dropdown_hint();
+            }
+        })
+        .blur(function () {
+            hide_dropdown();
+        })
+        .keydown(function (event) {
+            var previous_token;
+            var next_token;
+
+            switch(event.keyCode) {
+                case KEY.LEFT:
+                case KEY.RIGHT:
+                case KEY.UP:
+                case KEY.DOWN:
+                    if(!$(this).val()) {
+                        previous_token = input_token.prev();
+                        next_token = input_token.next();
+
+                        if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) {
+                            // Check if there is a previous/next token and it is selected
+                            if(event.keyCode == KEY.LEFT || event.keyCode == KEY.UP) {
+                                deselect_token($(selected_token), POSITION.BEFORE);
+                            } else {
+                                deselect_token($(selected_token), POSITION.AFTER);
+                            }
+                        } else if((event.keyCode == KEY.LEFT || event.keyCode == KEY.UP) && previous_token.length) {
+                            // We are moving left, select the previous token if it exists
+                            select_token($(previous_token.get(0)));
+                        } else if((event.keyCode == KEY.RIGHT || event.keyCode == KEY.DOWN) && next_token.length) {
+                            // We are moving right, select the next token if it exists
+                            select_token($(next_token.get(0)));
+                        }
+                    } else {
+                        var dropdown_item = null;
+
+                        if(event.keyCode == KEY.DOWN || event.keyCode == KEY.RIGHT) {
+                            dropdown_item = $(selected_dropdown_item).next();
+                        } else {
+                            dropdown_item = $(selected_dropdown_item).prev();
+                        }
+
+                        if(dropdown_item.length) {
+                            select_dropdown_item(dropdown_item);
+                        }
+                        return false;
+                    }
+                    break;
+
+                case KEY.BACKSPACE:
+                    previous_token = input_token.prev();
+
+                    if(!$(this).val().length) {
+                        if(selected_token) {
+                            delete_token($(selected_token));
+                        } else if(previous_token.length) {
+                            select_token($(previous_token.get(0)));
+                        }
+
+                        return false;
+                    } else if($(this).val().length == 1) {
+                        hide_dropdown();
+                    } else {
+                        // set a timeout just long enough to let this function finish.
+                        setTimeout(function(){do_search(false);}, 5);
+                    }
+                    break;
+
+                case KEY.TAB:
+                case KEY.RETURN:
+                case KEY.COMMA:
+                  if(selected_dropdown_item) {
+                    add_token($(selected_dropdown_item));
+                    return false;
+                  }
+                  break;
+
+                case KEY.ESC:
+                  hide_dropdown();
+                  return true;
+
+                default:
+                    if(is_printable_character(event.keyCode)) {
+                      // set a timeout just long enough to let this function finish.
+                      setTimeout(function(){do_search(false);}, 5);
+                    }
+                    break;
+            }
+        });
+
+    // Keep a reference to the original input box
+    var hidden_input = $(input)
+                           .hide()
+                           .focus(function () {
+                               input_box.focus();
+                           })
+                           .blur(function () {
+                               input_box.blur();
+                           });
+
+    // Keep a reference to the selected token and dropdown item
+    var selected_token = null;
+    var selected_dropdown_item = null;
+
+    // The list to store the token items in
+    var token_list = $("<ul />")
+        .addClass(settings.classes.tokenList)
+        .insertAfter(hidden_input)
+        .click(function (event) {
+            var li = get_element_from_event(event, "li");
+            if(li && li.get(0) != input_token.get(0)) {
+                toggle_select_token(li);
+                return false;
+            } else {
+                input_box.focus();
+
+                if(selected_token) {
+                    deselect_token($(selected_token), POSITION.END);
+                }
+            }
+        })
+        .mouseover(function (event) {
+            var li = get_element_from_event(event, "li");
+            if(li && selected_token !== this) {
+                li.addClass(settings.classes.highlightedToken);
+            }
+        })
+        .mouseout(function (event) {
+            var li = get_element_from_event(event, "li");
+            if(li && selected_token !== this) {
+                li.removeClass(settings.classes.highlightedToken);
+            }
+        })
+        .mousedown(function (event) {
+            // Stop user selecting text on tokens
+            var li = get_element_from_event(event, "li");
+            if(li){
+                return false;
+            }
+        });
+
+
+    // The list to store the dropdown items in
+    var dropdown = $("<div>")
+        .addClass(settings.classes.dropdown)
+        .insertAfter(token_list)
+        .hide();
+
+    // The token holding the input box
+    var input_token = $("<li />")
+        .addClass(settings.classes.inputToken)
+        .appendTo(token_list)
+        .append(input_box);
+
+    init_list();
+
+    //
+    // Functions
+    //
+
+
+    // Pre-populate list if items exist
+    function init_list () {
+        li_data = settings.prePopulate;
+        if(li_data && li_data.length) {
+            for(var i in li_data) {
+                var this_token = $("<li><p>"+li_data[i].name+"</p> </li>")
+                    .addClass(settings.classes.token)
+                    .insertBefore(input_token);
+
+                $("<span>x</span>")
+                    .addClass(settings.classes.tokenDelete)
+                    .appendTo(this_token)
+                    .click(function () {
+                        delete_token($(this).parent());
+                        return false;
+                    });
+
+                $.data(this_token.get(0), "tokeninput", {"id": li_data[i].id, "name": li_data[i].name});
+
+                // Clear input box and make sure it keeps focus
+                input_box
+                    .val("")
+                    .focus();
+
+                // Don't show the help dropdown, they've got the idea
+                hide_dropdown();
+
+                // Save this token id
+                var id_string = li_data[i].id + ","
+                hidden_input.val(hidden_input.val() + id_string);
+            }
+        }
+    }
+
+    function is_printable_character(keycode) {
+        if((keycode >= 48 && keycode <= 90) ||      // 0-1a-z
+           (keycode >= 96 && keycode <= 111) ||     // numpad 0-9 + - / * .
+           (keycode >= 186 && keycode <= 192) ||    // ; = , - . / ^
+           (keycode >= 219 && keycode <= 222)       // ( \ ) '
+          ) {
+              return true;
+          } else {
+              return false;
+          }
+    }
+
+    // Get an element of a particular type from an event (click/mouseover etc)
+    function get_element_from_event (event, element_type) {
+        var target = $(event.target);
+        var element = null;
+
+        if(target.is(element_type)) {
+            element = target;
+        } else if(target.parent(element_type).length) {
+            element = target.parent(element_type+":first");
+        }
+
+        return element;
+    }
+
+    // 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);
+
+      // The 'delete token' button
+      $("<span>x</span>")
+          .addClass(settings.classes.tokenDelete)
+          .appendTo(this_token)
+          .click(function () {
+              delete_token($(this).parent());
+              return false;
+          });
+
+      $.data(this_token.get(0), "tokeninput", {"id": id, "name": value});
+
+      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 this_token = insert_token(li_data.id, li_data.name);
+
+        // Clear input box and make sure it keeps focus
+        input_box
+            .val("")
+            .focus();
+
+        // Don't show the help dropdown, they've got the idea
+        hide_dropdown();
+
+        // Save this token id
+        var id_string = li_data.id + ","
+        hidden_input.val(hidden_input.val() + id_string);
+        
+        token_count++;
+        
+        if(settings.tokenLimit != null && settings.tokenLimit >= token_count) {
+            input_box.hide();
+            hide_dropdown();
+        }
+    }
+
+    // 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();
+    }
+
+    // Deselect a token in the token list
+    function deselect_token (token, position) {
+        token.removeClass(settings.classes.selectedToken);
+        selected_token = null;
+
+        if(position == POSITION.BEFORE) {
+            input_token.insertBefore(token);
+        } else if(position == POSITION.AFTER) {
+            input_token.insertAfter(token);
+        } else {
+            input_token.appendTo(token_list);
+        }
+
+        // Show the input box and give it focus again
+        input_box.focus();
+    }
+
+    // Toggle selection of a token in the token list
+    function toggle_select_token (token) {
+        if(selected_token == token.get(0)) {
+            deselect_token(token, POSITION.END);
+        } else {
+            if(selected_token) {
+                deselect_token($(selected_token), POSITION.END);
+            }
+            select_token(token);
+        }
+    }
+
+    // Delete a token from the token list
+    function delete_token (token) {
+        // Remove the id from the saved list
+        var token_data = $.data(token.get(0), "tokeninput");
+
+        // Delete the token
+        token.remove();
+        selected_token = null;
+
+        // Show the input box and give it focus again
+        input_box.focus();
+
+        // Delete this token's id from hidden input
+        var str = hidden_input.val()
+        var start = str.indexOf(token_data.id+",");
+        var end = str.indexOf(",", start) + 1;
+
+        if(end >= str.length) {
+            hidden_input.val(str.slice(0, start));
+        } else {
+            hidden_input.val(str.slice(0, start) + str.slice(end, str.length));
+        }
+        
+        token_count--;
+        
+        if (settings.tokenLimit != null) {
+            input_box
+                .show()
+                .val("")
+                .focus();
+        }
+    }
+
+    // Hide and clear the results dropdown
+    function hide_dropdown () {
+        dropdown.hide().empty();
+        selected_dropdown_item = null;
+    }
+
+    function show_dropdown_searching () {
+        dropdown
+            .html("<p>"+settings.searchingText+"</p>")
+            .show();
+    }
+
+    function show_dropdown_hint () {
+        dropdown
+            .html("<p>"+settings.hintText+"</p>")
+            .show();
+    }
+
+    // Highlight the query part of the search term
+	function highlight_term(value, term) {
+		return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
+	}
+
+    // Populate the results dropdown with some results
+    function populate_dropdown (query, results) {
+        if(results.length) {
+            dropdown.empty();
+            var dropdown_ul = $("<ul>")
+                .appendTo(dropdown)
+                .mouseover(function (event) {
+                    select_dropdown_item(get_element_from_event(event, "li"));
+                })
+                .click(function (event) {
+                    add_token(get_element_from_event(event, "li"));
+                })
+                .mousedown(function (event) {
+                    // Stop user selecting text on tokens
+                    return false;
+                })
+                .hide();
+
+            for(var i in results) {
+                if (results.hasOwnProperty(i)) {
+                    var this_li = $("<li>"+highlight_term(results[i].name, query)+"</li>")
+                                      .appendTo(dropdown_ul);
+
+                    if(i%2) {
+                        this_li.addClass(settings.classes.dropdownItem);
+                    } else {
+                        this_li.addClass(settings.classes.dropdownItem2);
+                    }
+
+                    if(i == 0) {
+                        select_dropdown_item(this_li);
+                    }
+
+                    $.data(this_li.get(0), "tokeninput", {"id": results[i].id, "name": results[i].name});
+                }
+            }
+
+            dropdown.show();
+            dropdown_ul.slideDown("fast");
+
+        } else {
+            dropdown
+                .html("<p>"+settings.noResultsText+"</p>")
+                .show();
+        }
+    }
+
+    // Highlight an item in the results dropdown
+    function select_dropdown_item (item) {
+        if(item) {
+            if(selected_dropdown_item) {
+                deselect_dropdown_item($(selected_dropdown_item));
+            }
+
+            item.addClass(settings.classes.selectedDropdownItem);
+            selected_dropdown_item = item.get(0);
+        }
+    }
+
+    // Remove highlighting from an item in the results dropdown
+    function deselect_dropdown_item (item) {
+        item.removeClass(settings.classes.selectedDropdownItem);
+        selected_dropdown_item = null;
+    }
+
+    // Do a search and show the "searching" dropdown if the input is longer
+    // than settings.minChars
+    function do_search(immediate) {
+        var query = input_box.val().toLowerCase();
+
+        if (query && query.length) {
+            if(selected_token) {
+                deselect_token($(selected_token), POSITION.AFTER);
+            }
+            if (query.length >= settings.minChars) {
+                show_dropdown_searching();
+                if (immediate) {
+                    run_search(query);
+                } else {
+                    clearTimeout(timeout);
+                    timeout = setTimeout(function(){run_search(query);}, settings.searchDelay);
+                }
+            } else {
+                hide_dropdown();
+            }
+        }
+    }
+
+    // Do the actual search
+    function run_search(query) {
+        var cached_results = cache.get(query);
+        if(cached_results) {
+            populate_dropdown(query, cached_results);
+        } else {
+			var queryStringDelimiter = settings.url.indexOf("?") < 0 ? "?" : "&";
+			var callback = function(results) {
+			  if($.isFunction(settings.onResult)) {
+			      results = settings.onResult.call(this, results);
+			  }
+              cache.add(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
+              populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
+            };
+            
+            if(settings.method == "POST") {
+			    $.post(settings.url + queryStringDelimiter + settings.queryParam + "=" + query, {}, callback, settings.contentType);
+		    } else {
+		        $.get(settings.url + queryStringDelimiter + settings.queryParam + "=" + query, {}, callback, settings.contentType);
+		    }
+        }
+    }
+};
+
+// Really basic cache for the results
+$.TokenList.Cache = function (options) {
+    var settings = $.extend({
+        max_size: 50
+    }, options);
+
+    var data = {};
+    var size = 0;
+
+    var flush = function () {
+        data = {};
+        size = 0;
+    };
+
+    this.add = function (query, results) {
+        if(size > settings.max_size) {
+            flush();
+        }
+
+        if(!data[query]) {
+            size++;
+        }
+
+        data[query] = results;
+    };
+
+    this.get = function (query) {
+        return data[query];
+    };
+};
+
+})(jQuery);

django/contrib/admin/options.py

 from django.db.models.query import QuerySet
 from django.http import Http404, HttpResponse, HttpResponseRedirect
 from django.shortcuts import get_object_or_404, render_to_response
+from django.utils import simplejson
 from django.utils.datastructures import SortedDict
 from django.utils.functional import update_wrapper
 from django.utils.html import escape
             other_qs = other_qs.filter(reduce(operator.or_, or_queries))
             queryset = queryset & other_qs
         
-        return HttpResponse(''.join([u'%s|%s\n' % (unicode(f), f.pk) for f in queryset]))
+        data = [{"id": f.pk, "name": unicode(f)} for f in queryset]
+        
+        return HttpResponse(simplejson.dumps(data))
     
     def add_view(self, request, form_url='', extra_context=None):
         "The 'add' admin view for this model."

django/contrib/admin/templates/widget/foreignkey_searchinput.html

 $(document).ready(function() {
     // Show lookup input
     $("#lookup_{{ name }}").show();
-    function reset() {
-        $('#id_{{ name }}').val('');
-        $('#lookup_{{ name }}').val('');
-    };
-
-    $('#lookup_{{ name }}').autocomplete('{{ search_path }}').result(
+    
+    $('#lookup_{{ name }}').autocomplete('{{ search_path }}', {
+      dataType: 'json',
+      parse: function(data) {
+        var rows = new Array();
+        for(var i=0; i<data.length; i++){
+            rows[i] = { data:data[i], value:data[i].id, result:data[i].name };
+        }
+        return rows;
+      },
+      formatItem: function(row, i, n) {
+        return row.name;
+      },
+      cacheLength: 0,
+    }).result(
         function(event, data, formatted) {
             if (data) {
-                $('#id_{{ name }}').val(data[1]);
+                $('#id_{{ name }}').val(data.id);
         }
-    }).keyup(function(event){
-        if (event.keyCode == 27) {
-            reset();
-        };
     });
-    var {{ name }}_value = $('#id_{{ name }}').val();
-    function check() {
-        {{ name }}_check = $('#id_{{ name }}').val();
-        if ({{ name }}_check) {
-            if ({{ name }}_check != {{ name }}_value) {
-                lookup({{ name }}_check);
-            }
-        }
-    }
-    timeout = window.setInterval(check, 300);
 });
 </script>

django/contrib/admin/templates/widget/m2m_searchinput.html

 {% load i18n %}
-<textarea id="lookup_{{ name }}" style="display:none;">{{ label }}</textarea>
+
 <a href="{{ related_url }}{{ url }}" class="related-lookup" id="lookup_id_{{ name }}" onclick="return showRelatedObjectLookupPopup(this);">
   <img src="{{ admin_media_prefix }}img/admin/selector-search.gif" width="16" height="16" alt="{% trans "Lookup" %}" />
 </a>
 <script type="text/javascript">
 $(document).ready(function() {
     // Show lookup input
-    $("#lookup_{{ name }}").show();
     
-    $('#lookup_{{ name }}').autocomplete('{{ search_path }}', {
-        multiple: true,
-        mustMatch: true
-    }).result(function(event, data, formatted) {
-        if (data) {
-            if ($('#id_{{ name }}').val()) {
-                $('#id_{{ name }}').val($('#id_{{ name }}').val() + "," + data[1]);
-            }
-            else {
-                $('#id_{{ name }}').val(data[1]);
-            }
-            
-        }
+    $('#id_{{ name }}').tokenInput("{{ search_path }}", {
+        noResultsText: "No results found.",
+        searchingText: "Searching..."
     });
+    
+    $('#add_id_{{ name }}').hide();
+    $('#lookup_id_{{ name }}').hide();
 });
 </script>

django/contrib/admin/widgets.py

  
     class Media:
         css = {
-            'all': (settings.ADMIN_MEDIA_PREFIX + 'css/jquery.autocomplete.css',)
+            'all': (settings.ADMIN_MEDIA_PREFIX + 'css/jquery.token.input.css',)
         }
         js = (
             settings.ADMIN_MEDIA_PREFIX + 'js/jquery.js',
-            settings.ADMIN_MEDIA_PREFIX + 'js/jquery.bgiframe.min.js',
-            settings.ADMIN_MEDIA_PREFIX + 'js/jquery.ajaxQueue.js',
-            settings.ADMIN_MEDIA_PREFIX + 'js/jquery.autocomplete.js',
+            settings.ADMIN_MEDIA_PREFIX + 'js/jquery.tokeninput.js',
         )
     
     def __init__(self, rel, search_fields, attrs=None):
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.