From 2ea3b4e058e13aaa17f813872ac1dba0892a3c40 Mon Sep 17 00:00:00 2001 From: Zain Memon Date: Sat, 5 Dec 2009 00:54:45 +0000 Subject: [PATCH] [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 git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/admin-ui@11795 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- .../admin/media/css/jquery.token.input.css | 110 ++++ .../admin/media/js/jquery.autocomplete.js | 113 +++- .../admin/media/js/jquery.tokeninput.js | 597 ++++++++++++++++++ django/contrib/admin/options.py | 5 +- .../widget/foreignkey_searchinput.html | 37 +- .../templates/widget/m2m_searchinput.html | 22 +- django/contrib/admin/widgets.py | 6 +- 7 files changed, 817 insertions(+), 73 deletions(-) create mode 100644 django/contrib/admin/media/css/jquery.token.input.css create mode 100644 django/contrib/admin/media/js/jquery.tokeninput.js diff --git a/django/contrib/admin/media/css/jquery.token.input.css b/django/contrib/admin/media/css/jquery.token.input.css new file mode 100644 index 0000000000..ed7bc50fd6 --- /dev/null +++ b/django/contrib/admin/media/css/jquery.token.input.css @@ -0,0 +1,110 @@ +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; +} + diff --git a/django/contrib/admin/media/js/jquery.autocomplete.js b/django/contrib/admin/media/js/jquery.autocomplete.js index 5ad9178f86..3e53fc8bae 100644 --- a/django/contrib/admin/media/js/jquery.autocomplete.js +++ b/django/contrib/admin/media/js/jquery.autocomplete.js @@ -1,14 +1,13 @@ /* - * 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($) { @@ -90,6 +89,9 @@ $.Autocompleter = function(input, options) { // 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) { @@ -209,7 +211,21 @@ $.Autocompleter = function(input, options) { 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; } @@ -246,22 +262,27 @@ $.Autocompleter = function(input, options) { }; 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]; } @@ -275,7 +296,7 @@ $.Autocompleter = function(input, options) { // 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); } }; @@ -299,15 +320,14 @@ $.Autocompleter = function(input, options) { 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) { @@ -421,6 +441,9 @@ $.Autocompleter.Cache = function(options) { 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; }; @@ -738,22 +761,48 @@ $.Autocompleter.Select = function (options, input, select, config) { }; }; -$.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); \ No newline at end of file diff --git a/django/contrib/admin/media/js/jquery.tokeninput.js b/django/contrib/admin/media/js/jquery.tokeninput.js new file mode 100644 index 0000000000..b7b57671f1 --- /dev/null +++ b/django/contrib/admin/media/js/jquery.tokeninput.js @@ -0,0 +1,597 @@ +/* + * 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 = $("") + .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 = $("