Ticket #14370: ticket-14730.diff
File ticket-14730.diff, 92.3 KB (added by , 14 years ago) |
---|
-
django/contrib/admin/media/css/widgets.css
diff --git a/django/contrib/admin/media/css/widgets.css b/django/contrib/admin/media/css/widgets.css
a b 511 511 border-top: 1px solid #ddd; 512 512 } 513 513 514 /* AUTOCOMPLETE */ 515 /* *** NOTE *** 516 This is mostly culled from css generated by http://jqueryui.com/themeroller 517 */ 518 .ui-menu { 519 display: block; 520 float: left; 521 list-style: none outside none; 522 margin: 0; 523 padding: 2px; 524 } 525 .ui-menu .ui-menu-item a { 526 display: block; 527 line-height: 1.5; 528 padding: 0.2em 0.4em; 529 text-decoration: none; 530 } 531 532 .ui-autocomplete { 533 cursor: default; 534 position: absolute; 535 } 536 537 form .aligned ul.ui-autocomplete { 538 padding: 2px; 539 } 540 .ui-widget-content { 541 background: #fcfdfd; 542 border: 1px solid #a6c9e2; 543 color: #222222; 544 } 545 .ui-menu .ui-menu-item { 546 background: #fcfdfd; 547 clear: left; 548 float: left; 549 margin: 0; 550 padding: 0; 551 width: 100%; 552 } 553 .ui-menu .ui-menu-item a { 554 border: 1px solid #fcfdfd; 555 } 556 ul.ui-menu li, 557 .ui-autocomplete-value { 558 list-style-type: none; 559 } 560 .ui-state-hover, .ui-state-focus { 561 color: #fff; 562 background: #417690; 563 border: 1px solid #417690; 564 } 565 566 .ui-autocomplete-value a { 567 margin-left: 0.5em; 568 } 569 .djangoautocomplete-wrapper > * { 570 float: left; 571 } 572 .djangoautocomplete-wrapper > ul { 573 clear: left; 574 } 575 .related-lookup { 576 position: relative; 577 top: 4px; 578 left: 2px; 579 } -
django/contrib/admin/media/js/admin/RelatedObjectLookups.js
diff --git a/django/contrib/admin/media/js/admin/RelatedObjectLookups.js b/django/contrib/admin/media/js/admin/RelatedObjectLookups.js
a b 44 44 function dismissRelatedLookupPopup(win, chosenId) { 45 45 var name = windowname_to_id(win.name); 46 46 var elem = document.getElementById(name); 47 if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) { 47 var $ = django && django.jQuery; 48 var autocomplete_elem = !!$ && $(elem); 49 if (!!autocomplete_elem && !!autocomplete_elem.data('djangoautocomplete')) { 50 $.getJSON( 51 autocomplete_elem.data('djangoautocomplete').options.source, 52 {term: chosenId, by_id: 1}, 53 function (data) { 54 // Pass the returned item to the normal 55 // autocomplete onSelect handler. 56 autocomplete_elem.data('autocomplete') 57 .options.select({}, {item: data[0]}); 58 }); 59 } 60 else if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) { 48 61 elem.value += ',' + chosenId; 49 62 } else { 50 document.getElementById(name).value = chosenId;63 elem.value = chosenId; 51 64 } 52 65 win.close(); 53 66 } … … 79 92 elem.options[elem.options.length] = o; 80 93 o.selected = true; 81 94 } else if (elem.nodeName == 'INPUT') { 82 if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) { 95 var autocomplete_elem = django && django.jQuery && django.jQuery(elem); 96 if (!!autocomplete_elem && !!autocomplete_elem.data('djangoautocomplete')) { 97 django.jQuery.getJSON( 98 autocomplete_elem.data('djangoautocomplete').options.source, 99 {term: newId, by_id: 1}, 100 function (data) { 101 // Pass the returned item to the normal 102 // autocomplete onSelect handler. 103 autocomplete_elem.data('autocomplete') 104 .options.select({}, {item: data[0]}); 105 }); 106 } else if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) { 83 107 elem.value += ',' + newId; 84 108 } else { 85 109 elem.value = newId; -
new file django/contrib/admin/media/js/admin/autocomplete.js
diff --git a/django/contrib/admin/media/js/admin/autocomplete.js b/django/contrib/admin/media/js/admin/autocomplete.js new file mode 100644
- + 1 (function ($) { 2 "use strict"; 3 4 $.widget("ui.djangoautocomplete", { 5 options: { 6 source: "../autocomplete/$name/", 7 multiple: false, 8 force_selection: true, 9 renderItem: function (ul, item) { 10 return $("<li></li>") 11 .data("item.autocomplete", item) 12 .append($("<a></a>").append(item.label)) 13 .appendTo(ul); 14 }, 15 is_djangoautocomplete: true 16 }, 17 _create: function () { 18 var self = this; 19 this.hidden_input = this.element.prev("input[type=hidden]"); 20 this.name = this.hidden_input.attr("name"); 21 this.element.autocomplete({ 22 appendTo: this.element.parent(), 23 select: function (event, ui) { 24 if (!ui.item) { return; /* unexpected result */ } 25 var item = ui.item.data && ui.item.data("item.autocomplete") || ui.item; 26 self.lastSelected = item; 27 if (self.options.is_djangoautocomplete === true) { 28 if (self.options.multiple) { 29 if ($.inArray(item.id, self.values) < 0) { 30 $('<li></li>') 31 .addClass("ui-autocomplete-value") 32 .data("value.autocomplete", item.id) 33 .append(item.label + '<a href="#">x</a>') 34 .appendTo(self.values_ul); 35 self.values.push(item.id); 36 } 37 } else { 38 self.term = item.value; 39 self.element.val(item.value); 40 } 41 return false; 42 } 43 } 44 }).data("autocomplete")._renderItem = this.options.renderItem; 45 this._initSource(); 46 if (this.options.multiple) { 47 this._initManyToMany(); 48 } else { 49 this.lastSelected = { 50 id: this.hidden_input.val(), 51 value: this.element.val() 52 }; 53 } 54 if (this.options.force_selection) { 55 this.element.focusout(function () { 56 if (self.element.val() !== self.lastSelected.value) { 57 self.element.val(""); 58 } 59 }); 60 } 61 this.element.closest("form").submit(function () { 62 if (self.options.multiple) { 63 self.hidden_input.val(self.values.join(",")); 64 } else if (self.options.force_selection) { 65 self.hidden_input.val(self.element.val() ? self.lastSelected.id : ""); 66 } else { 67 self.hidden_input.val(self.element.val()); 68 } 69 }); 70 }, 71 72 destroy: function () { 73 this.element.autocomplete("destroy"); 74 if (this.options.multiple) { 75 this.values_ul.remove(); 76 } 77 $.Widget.prototype.destroy.call(this); 78 }, 79 80 _setOption: function (key, value) { 81 $.Widget.prototype._setOption.apply(this, arguments); 82 if (key === "source") { 83 this._initSource(); 84 } 85 }, 86 87 _initSource: function () { 88 var source = typeof this.options.source === "string" ? 89 this.options.source.replace("$name", this.hidden_input.attr("name")) : 90 this.options.source; 91 this.element.autocomplete("option", "source", source); 92 }, 93 94 _initManyToMany: function () { 95 var self = this; 96 this.element.bind("autocompleteclose", function (event, ui) { 97 self.element.val(""); 98 }); 99 this.values = []; 100 if (this.hidden_input.val() !== "") { 101 $.each(this.hidden_input.val().split(","), function (i, id) { 102 self.values.push(parseInt(id, 10)); 103 }); 104 } 105 this.values_ul = this.element.nextAll("ul.ui-autocomplete-values"); 106 this.lastSelected = { id: null, value: null }; 107 if (this.values.length && this.values_ul[0]) { 108 this.values_ul.children().each(function (i) { 109 $(this) 110 .addClass("ui-autocomplete-value") 111 .data("value.autocomplete", self.values[i]) 112 .append('<a href="#">x</a>'); 113 }); 114 } else { 115 this.values_ul = $("<ul></ul>").insertAfter(this.element); 116 } 117 this.values_ul.addClass("ui-autocomplete-values"); 118 $(".ui-autocomplete-value a", this.values_ul[0]).live("click", function () { 119 var span = $(this).parent(), 120 id = span.data("value.autocomplete"); 121 $.each(self.values, function (i, v) { 122 if (v === id) { 123 self.values.splice(i, 1); 124 } 125 }); 126 span.remove(); 127 }); 128 } 129 }); 130 131 })(django.jQuery); -
new file django/contrib/admin/media/js/jquery-ui.js
diff --git a/django/contrib/admin/media/js/jquery-ui.js b/django/contrib/admin/media/js/jquery-ui.js new file mode 100644
- + 1 /*! 2 * jQuery UI 1.8.5 3 * 4 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 5 * Dual licensed under the MIT or GPL Version 2 licenses. 6 * http://jquery.org/license 7 * 8 * http://docs.jquery.com/UI 9 */ 10 (function( $, undefined ) { 11 12 // prevent duplicate loading 13 // this is only a problem because we proxy existing functions 14 // and we don't want to double proxy them 15 $.ui = $.ui || {}; 16 if ( $.ui.version ) { 17 return; 18 } 19 20 $.extend( $.ui, { 21 version: "1.8.5", 22 23 keyCode: { 24 ALT: 18, 25 BACKSPACE: 8, 26 CAPS_LOCK: 20, 27 COMMA: 188, 28 COMMAND: 91, 29 COMMAND_LEFT: 91, // COMMAND 30 COMMAND_RIGHT: 93, 31 CONTROL: 17, 32 DELETE: 46, 33 DOWN: 40, 34 END: 35, 35 ENTER: 13, 36 ESCAPE: 27, 37 HOME: 36, 38 INSERT: 45, 39 LEFT: 37, 40 MENU: 93, // COMMAND_RIGHT 41 NUMPAD_ADD: 107, 42 NUMPAD_DECIMAL: 110, 43 NUMPAD_DIVIDE: 111, 44 NUMPAD_ENTER: 108, 45 NUMPAD_MULTIPLY: 106, 46 NUMPAD_SUBTRACT: 109, 47 PAGE_DOWN: 34, 48 PAGE_UP: 33, 49 PERIOD: 190, 50 RIGHT: 39, 51 SHIFT: 16, 52 SPACE: 32, 53 TAB: 9, 54 UP: 38, 55 WINDOWS: 91 // COMMAND 56 } 57 }); 58 59 // plugins 60 $.fn.extend({ 61 _focus: $.fn.focus, 62 focus: function( delay, fn ) { 63 return typeof delay === "number" ? 64 this.each(function() { 65 var elem = this; 66 setTimeout(function() { 67 $( elem ).focus(); 68 if ( fn ) { 69 fn.call( elem ); 70 } 71 }, delay ); 72 }) : 73 this._focus.apply( this, arguments ); 74 }, 75 76 scrollParent: function() { 77 var scrollParent; 78 if (($.browser.msie && (/(static|relative)/).test(this.css('position'))) || (/absolute/).test(this.css('position'))) { 79 scrollParent = this.parents().filter(function() { 80 return (/(relative|absolute|fixed)/).test($.curCSS(this,'position',1)) && (/(auto|scroll)/).test($.curCSS(this,'overflow',1)+$.curCSS(this,'overflow-y',1)+$.curCSS(this,'overflow-x',1)); 81 }).eq(0); 82 } else { 83 scrollParent = this.parents().filter(function() { 84 return (/(auto|scroll)/).test($.curCSS(this,'overflow',1)+$.curCSS(this,'overflow-y',1)+$.curCSS(this,'overflow-x',1)); 85 }).eq(0); 86 } 87 88 return (/fixed/).test(this.css('position')) || !scrollParent.length ? $(document) : scrollParent; 89 }, 90 91 zIndex: function( zIndex ) { 92 if ( zIndex !== undefined ) { 93 return this.css( "zIndex", zIndex ); 94 } 95 96 if ( this.length ) { 97 var elem = $( this[ 0 ] ), position, value; 98 while ( elem.length && elem[ 0 ] !== document ) { 99 // Ignore z-index if position is set to a value where z-index is ignored by the browser 100 // This makes behavior of this function consistent across browsers 101 // WebKit always returns auto if the element is positioned 102 position = elem.css( "position" ); 103 if ( position === "absolute" || position === "relative" || position === "fixed" ) { 104 // IE returns 0 when zIndex is not specified 105 // other browsers return a string 106 // we ignore the case of nested elements with an explicit value of 0 107 // <div style="z-index: -10;"><div style="z-index: 0;"></div></div> 108 value = parseInt( elem.css( "zIndex" ) ); 109 if ( !isNaN( value ) && value != 0 ) { 110 return value; 111 } 112 } 113 elem = elem.parent(); 114 } 115 } 116 117 return 0; 118 }, 119 120 disableSelection: function() { 121 return this.bind( 122 "mousedown.ui-disableSelection selectstart.ui-disableSelection", 123 function( event ) { 124 event.preventDefault(); 125 }); 126 }, 127 128 enableSelection: function() { 129 return this.unbind( ".ui-disableSelection" ); 130 } 131 }); 132 133 $.each( [ "Width", "Height" ], function( i, name ) { 134 var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ], 135 type = name.toLowerCase(), 136 orig = { 137 innerWidth: $.fn.innerWidth, 138 innerHeight: $.fn.innerHeight, 139 outerWidth: $.fn.outerWidth, 140 outerHeight: $.fn.outerHeight 141 }; 142 143 function reduce( elem, size, border, margin ) { 144 $.each( side, function() { 145 size -= parseFloat( $.curCSS( elem, "padding" + this, true) ) || 0; 146 if ( border ) { 147 size -= parseFloat( $.curCSS( elem, "border" + this + "Width", true) ) || 0; 148 } 149 if ( margin ) { 150 size -= parseFloat( $.curCSS( elem, "margin" + this, true) ) || 0; 151 } 152 }); 153 return size; 154 } 155 156 $.fn[ "inner" + name ] = function( size ) { 157 if ( size === undefined ) { 158 return orig[ "inner" + name ].call( this ); 159 } 160 161 return this.each(function() { 162 $.style( this, type, reduce( this, size ) + "px" ); 163 }); 164 }; 165 166 $.fn[ "outer" + name] = function( size, margin ) { 167 if ( typeof size !== "number" ) { 168 return orig[ "outer" + name ].call( this, size ); 169 } 170 171 return this.each(function() { 172 $.style( this, type, reduce( this, size, true, margin ) + "px" ); 173 }); 174 }; 175 }); 176 177 // selectors 178 function visible( element ) { 179 return !$( element ).parents().andSelf().filter(function() { 180 return $.curCSS( this, "visibility" ) === "hidden" || 181 $.expr.filters.hidden( this ); 182 }).length; 183 } 184 185 $.extend( $.expr[ ":" ], { 186 data: function( elem, i, match ) { 187 return !!$.data( elem, match[ 3 ] ); 188 }, 189 190 focusable: function( element ) { 191 var nodeName = element.nodeName.toLowerCase(), 192 tabIndex = $.attr( element, "tabindex" ); 193 if ( "area" === nodeName ) { 194 var map = element.parentNode, 195 mapName = map.name, 196 img; 197 if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { 198 return false; 199 } 200 img = $( "img[usemap=#" + mapName + "]" )[0]; 201 return !!img && visible( img ); 202 } 203 return ( /input|select|textarea|button|object/.test( nodeName ) 204 ? !element.disabled 205 : "a" == nodeName 206 ? element.href || !isNaN( tabIndex ) 207 : !isNaN( tabIndex )) 208 // the element and all of its ancestors must be visible 209 && visible( element ); 210 }, 211 212 tabbable: function( element ) { 213 var tabIndex = $.attr( element, "tabindex" ); 214 return ( isNaN( tabIndex ) || tabIndex >= 0 ) && $( element ).is( ":focusable" ); 215 } 216 }); 217 218 // support 219 $(function() { 220 var div = document.createElement( "div" ), 221 body = document.body; 222 223 $.extend( div.style, { 224 minHeight: "100px", 225 height: "auto", 226 padding: 0, 227 borderWidth: 0 228 }); 229 230 $.support.minHeight = body.appendChild( div ).offsetHeight === 100; 231 // set display to none to avoid a layout bug in IE 232 // http://dev.jquery.com/ticket/4014 233 body.removeChild( div ).style.display = "none"; 234 }); 235 236 237 238 239 240 // deprecated 241 $.extend( $.ui, { 242 // $.ui.plugin is deprecated. Use the proxy pattern instead. 243 plugin: { 244 add: function( module, option, set ) { 245 var proto = $.ui[ module ].prototype; 246 for ( var i in set ) { 247 proto.plugins[ i ] = proto.plugins[ i ] || []; 248 proto.plugins[ i ].push( [ option, set[ i ] ] ); 249 } 250 }, 251 call: function( instance, name, args ) { 252 var set = instance.plugins[ name ]; 253 if ( !set || !instance.element[ 0 ].parentNode ) { 254 return; 255 } 256 257 for ( var i = 0; i < set.length; i++ ) { 258 if ( instance.options[ set[ i ][ 0 ] ] ) { 259 set[ i ][ 1 ].apply( instance.element, args ); 260 } 261 } 262 } 263 }, 264 265 // will be deprecated when we switch to jQuery 1.4 - use jQuery.contains() 266 contains: function( a, b ) { 267 return document.compareDocumentPosition ? 268 a.compareDocumentPosition( b ) & 16 : 269 a !== b && a.contains( b ); 270 }, 271 272 // only used by resizable 273 hasScroll: function( el, a ) { 274 275 //If overflow is hidden, the element might have extra content, but the user wants to hide it 276 if ( $( el ).css( "overflow" ) === "hidden") { 277 return false; 278 } 279 280 var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop", 281 has = false; 282 283 if ( el[ scroll ] > 0 ) { 284 return true; 285 } 286 287 // TODO: determine which cases actually cause this to happen 288 // if the element doesn't have the scroll set, see if it's possible to 289 // set the scroll 290 el[ scroll ] = 1; 291 has = ( el[ scroll ] > 0 ); 292 el[ scroll ] = 0; 293 return has; 294 }, 295 296 // these are odd functions, fix the API or move into individual plugins 297 isOverAxis: function( x, reference, size ) { 298 //Determines when x coordinate is over "b" element axis 299 return ( x > reference ) && ( x < ( reference + size ) ); 300 }, 301 isOver: function( y, x, top, left, height, width ) { 302 //Determines when x, y coordinates is over "b" element 303 return $.ui.isOverAxis( y, top, height ) && $.ui.isOverAxis( x, left, width ); 304 } 305 }); 306 307 })( jQuery ); 308 /*! 309 * jQuery UI Widget 1.8.5 310 * 311 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 312 * Dual licensed under the MIT or GPL Version 2 licenses. 313 * http://jquery.org/license 314 * 315 * http://docs.jquery.com/UI/Widget 316 */ 317 (function( $, undefined ) { 318 319 // jQuery 1.4+ 320 if ( $.cleanData ) { 321 var _cleanData = $.cleanData; 322 $.cleanData = function( elems ) { 323 for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { 324 $( elem ).triggerHandler( "remove" ); 325 } 326 _cleanData( elems ); 327 }; 328 } else { 329 var _remove = $.fn.remove; 330 $.fn.remove = function( selector, keepData ) { 331 return this.each(function() { 332 if ( !keepData ) { 333 if ( !selector || $.filter( selector, [ this ] ).length ) { 334 $( "*", this ).add( [ this ] ).each(function() { 335 $( this ).triggerHandler( "remove" ); 336 }); 337 } 338 } 339 return _remove.call( $(this), selector, keepData ); 340 }); 341 }; 342 } 343 344 $.widget = function( name, base, prototype ) { 345 var namespace = name.split( "." )[ 0 ], 346 fullName; 347 name = name.split( "." )[ 1 ]; 348 fullName = namespace + "-" + name; 349 350 if ( !prototype ) { 351 prototype = base; 352 base = $.Widget; 353 } 354 355 // create selector for plugin 356 $.expr[ ":" ][ fullName ] = function( elem ) { 357 return !!$.data( elem, name ); 358 }; 359 360 $[ namespace ] = $[ namespace ] || {}; 361 $[ namespace ][ name ] = function( options, element ) { 362 // allow instantiation without initializing for simple inheritance 363 if ( arguments.length ) { 364 this._createWidget( options, element ); 365 } 366 }; 367 368 var basePrototype = new base(); 369 // we need to make the options hash a property directly on the new instance 370 // otherwise we'll modify the options hash on the prototype that we're 371 // inheriting from 372 // $.each( basePrototype, function( key, val ) { 373 // if ( $.isPlainObject(val) ) { 374 // basePrototype[ key ] = $.extend( {}, val ); 375 // } 376 // }); 377 basePrototype.options = $.extend( true, {}, basePrototype.options ); 378 $[ namespace ][ name ].prototype = $.extend( true, basePrototype, { 379 namespace: namespace, 380 widgetName: name, 381 widgetEventPrefix: $[ namespace ][ name ].prototype.widgetEventPrefix || name, 382 widgetBaseClass: fullName 383 }, prototype ); 384 385 $.widget.bridge( name, $[ namespace ][ name ] ); 386 }; 387 388 $.widget.bridge = function( name, object ) { 389 $.fn[ name ] = function( options ) { 390 var isMethodCall = typeof options === "string", 391 args = Array.prototype.slice.call( arguments, 1 ), 392 returnValue = this; 393 394 // allow multiple hashes to be passed on init 395 options = !isMethodCall && args.length ? 396 $.extend.apply( null, [ true, options ].concat(args) ) : 397 options; 398 399 // prevent calls to internal methods 400 if ( isMethodCall && options.substring( 0, 1 ) === "_" ) { 401 return returnValue; 402 } 403 404 if ( isMethodCall ) { 405 this.each(function() { 406 var instance = $.data( this, name ); 407 if ( !instance ) { 408 throw "cannot call methods on " + name + " prior to initialization; " + 409 "attempted to call method '" + options + "'"; 410 } 411 if ( !$.isFunction( instance[options] ) ) { 412 throw "no such method '" + options + "' for " + name + " widget instance"; 413 } 414 var methodValue = instance[ options ].apply( instance, args ); 415 if ( methodValue !== instance && methodValue !== undefined ) { 416 returnValue = methodValue; 417 return false; 418 } 419 }); 420 } else { 421 this.each(function() { 422 var instance = $.data( this, name ); 423 if ( instance ) { 424 instance.option( options || {} )._init(); 425 } else { 426 $.data( this, name, new object( options, this ) ); 427 } 428 }); 429 } 430 431 return returnValue; 432 }; 433 }; 434 435 $.Widget = function( options, element ) { 436 // allow instantiation without initializing for simple inheritance 437 if ( arguments.length ) { 438 this._createWidget( options, element ); 439 } 440 }; 441 442 $.Widget.prototype = { 443 widgetName: "widget", 444 widgetEventPrefix: "", 445 options: { 446 disabled: false 447 }, 448 _createWidget: function( options, element ) { 449 // $.widget.bridge stores the plugin instance, but we do it anyway 450 // so that it's stored even before the _create function runs 451 $.data( element, this.widgetName, this ); 452 this.element = $( element ); 453 this.options = $.extend( true, {}, 454 this.options, 455 $.metadata && $.metadata.get( element )[ this.widgetName ], 456 options ); 457 458 var self = this; 459 this.element.bind( "remove." + this.widgetName, function() { 460 self.destroy(); 461 }); 462 463 this._create(); 464 this._init(); 465 }, 466 _create: function() {}, 467 _init: function() {}, 468 469 destroy: function() { 470 this.element 471 .unbind( "." + this.widgetName ) 472 .removeData( this.widgetName ); 473 this.widget() 474 .unbind( "." + this.widgetName ) 475 .removeAttr( "aria-disabled" ) 476 .removeClass( 477 this.widgetBaseClass + "-disabled " + 478 "ui-state-disabled" ); 479 }, 480 481 widget: function() { 482 return this.element; 483 }, 484 485 option: function( key, value ) { 486 var options = key, 487 self = this; 488 489 if ( arguments.length === 0 ) { 490 // don't return a reference to the internal hash 491 return $.extend( {}, self.options ); 492 } 493 494 if (typeof key === "string" ) { 495 if ( value === undefined ) { 496 return this.options[ key ]; 497 } 498 options = {}; 499 options[ key ] = value; 500 } 501 502 $.each( options, function( key, value ) { 503 self._setOption( key, value ); 504 }); 505 506 return self; 507 }, 508 _setOption: function( key, value ) { 509 this.options[ key ] = value; 510 511 if ( key === "disabled" ) { 512 this.widget() 513 [ value ? "addClass" : "removeClass"]( 514 this.widgetBaseClass + "-disabled" + " " + 515 "ui-state-disabled" ) 516 .attr( "aria-disabled", value ); 517 } 518 519 return this; 520 }, 521 522 enable: function() { 523 return this._setOption( "disabled", false ); 524 }, 525 disable: function() { 526 return this._setOption( "disabled", true ); 527 }, 528 529 _trigger: function( type, event, data ) { 530 var callback = this.options[ type ]; 531 532 event = $.Event( event ); 533 event.type = ( type === this.widgetEventPrefix ? 534 type : 535 this.widgetEventPrefix + type ).toLowerCase(); 536 data = data || {}; 537 538 // copy original event properties over to the new event 539 // this would happen if we could call $.event.fix instead of $.Event 540 // but we don't have a way to force an event to be fixed multiple times 541 if ( event.originalEvent ) { 542 for ( var i = $.event.props.length, prop; i; ) { 543 prop = $.event.props[ --i ]; 544 event[ prop ] = event.originalEvent[ prop ]; 545 } 546 } 547 548 this.element.trigger( event, data ); 549 550 return !( $.isFunction(callback) && 551 callback.call( this.element[0], event, data ) === false || 552 event.isDefaultPrevented() ); 553 } 554 }; 555 556 })( jQuery ); 557 /* 558 * jQuery UI Position 1.8.5 559 * 560 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 561 * Dual licensed under the MIT or GPL Version 2 licenses. 562 * http://jquery.org/license 563 * 564 * http://docs.jquery.com/UI/Position 565 */ 566 (function( $, undefined ) { 567 568 $.ui = $.ui || {}; 569 570 var horizontalPositions = /left|center|right/, 571 verticalPositions = /top|center|bottom/, 572 center = "center", 573 _position = $.fn.position, 574 _offset = $.fn.offset; 575 576 $.fn.position = function( options ) { 577 if ( !options || !options.of ) { 578 return _position.apply( this, arguments ); 579 } 580 581 // make a copy, we don't want to modify arguments 582 options = $.extend( {}, options ); 583 584 var target = $( options.of ), 585 targetElem = target[0], 586 collision = ( options.collision || "flip" ).split( " " ), 587 offset = options.offset ? options.offset.split( " " ) : [ 0, 0 ], 588 targetWidth, 589 targetHeight, 590 basePosition; 591 592 if ( targetElem.nodeType === 9 ) { 593 targetWidth = target.width(); 594 targetHeight = target.height(); 595 basePosition = { top: 0, left: 0 }; 596 } else if ( targetElem.scrollTo && targetElem.document ) { 597 targetWidth = target.width(); 598 targetHeight = target.height(); 599 basePosition = { top: target.scrollTop(), left: target.scrollLeft() }; 600 } else if ( targetElem.preventDefault ) { 601 // force left top to allow flipping 602 options.at = "left top"; 603 targetWidth = targetHeight = 0; 604 basePosition = { top: options.of.pageY, left: options.of.pageX }; 605 } else { 606 targetWidth = target.outerWidth(); 607 targetHeight = target.outerHeight(); 608 basePosition = target.offset(); 609 } 610 611 // force my and at to have valid horizontal and veritcal positions 612 // if a value is missing or invalid, it will be converted to center 613 $.each( [ "my", "at" ], function() { 614 var pos = ( options[this] || "" ).split( " " ); 615 if ( pos.length === 1) { 616 pos = horizontalPositions.test( pos[0] ) ? 617 pos.concat( [center] ) : 618 verticalPositions.test( pos[0] ) ? 619 [ center ].concat( pos ) : 620 [ center, center ]; 621 } 622 pos[ 0 ] = horizontalPositions.test( pos[0] ) ? pos[ 0 ] : center; 623 pos[ 1 ] = verticalPositions.test( pos[1] ) ? pos[ 1 ] : center; 624 options[ this ] = pos; 625 }); 626 627 // normalize collision option 628 if ( collision.length === 1 ) { 629 collision[ 1 ] = collision[ 0 ]; 630 } 631 632 // normalize offset option 633 offset[ 0 ] = parseInt( offset[0], 10 ) || 0; 634 if ( offset.length === 1 ) { 635 offset[ 1 ] = offset[ 0 ]; 636 } 637 offset[ 1 ] = parseInt( offset[1], 10 ) || 0; 638 639 if ( options.at[0] === "right" ) { 640 basePosition.left += targetWidth; 641 } else if (options.at[0] === center ) { 642 basePosition.left += targetWidth / 2; 643 } 644 645 if ( options.at[1] === "bottom" ) { 646 basePosition.top += targetHeight; 647 } else if ( options.at[1] === center ) { 648 basePosition.top += targetHeight / 2; 649 } 650 651 basePosition.left += offset[ 0 ]; 652 basePosition.top += offset[ 1 ]; 653 654 return this.each(function() { 655 var elem = $( this ), 656 elemWidth = elem.outerWidth(), 657 elemHeight = elem.outerHeight(), 658 marginLeft = parseInt( $.curCSS( this, "marginLeft", true ) ) || 0, 659 marginTop = parseInt( $.curCSS( this, "marginTop", true ) ) || 0, 660 collisionWidth = elemWidth + marginLeft + 661 parseInt( $.curCSS( this, "marginRight", true ) ) || 0, 662 collisionHeight = elemHeight + marginTop + 663 parseInt( $.curCSS( this, "marginBottom", true ) ) || 0, 664 position = $.extend( {}, basePosition ), 665 collisionPosition; 666 667 if ( options.my[0] === "right" ) { 668 position.left -= elemWidth; 669 } else if ( options.my[0] === center ) { 670 position.left -= elemWidth / 2; 671 } 672 673 if ( options.my[1] === "bottom" ) { 674 position.top -= elemHeight; 675 } else if ( options.my[1] === center ) { 676 position.top -= elemHeight / 2; 677 } 678 679 // prevent fractions (see #5280) 680 position.left = parseInt( position.left ); 681 position.top = parseInt( position.top ); 682 683 collisionPosition = { 684 left: position.left - marginLeft, 685 top: position.top - marginTop 686 }; 687 688 $.each( [ "left", "top" ], function( i, dir ) { 689 if ( $.ui.position[ collision[i] ] ) { 690 $.ui.position[ collision[i] ][ dir ]( position, { 691 targetWidth: targetWidth, 692 targetHeight: targetHeight, 693 elemWidth: elemWidth, 694 elemHeight: elemHeight, 695 collisionPosition: collisionPosition, 696 collisionWidth: collisionWidth, 697 collisionHeight: collisionHeight, 698 offset: offset, 699 my: options.my, 700 at: options.at 701 }); 702 } 703 }); 704 705 if ( $.fn.bgiframe ) { 706 elem.bgiframe(); 707 } 708 elem.offset( $.extend( position, { using: options.using } ) ); 709 }); 710 }; 711 712 $.ui.position = { 713 fit: { 714 left: function( position, data ) { 715 var win = $( window ), 716 over = data.collisionPosition.left + data.collisionWidth - win.width() - win.scrollLeft(); 717 position.left = over > 0 ? position.left - over : Math.max( position.left - data.collisionPosition.left, position.left ); 718 }, 719 top: function( position, data ) { 720 var win = $( window ), 721 over = data.collisionPosition.top + data.collisionHeight - win.height() - win.scrollTop(); 722 position.top = over > 0 ? position.top - over : Math.max( position.top - data.collisionPosition.top, position.top ); 723 } 724 }, 725 726 flip: { 727 left: function( position, data ) { 728 if ( data.at[0] === center ) { 729 return; 730 } 731 var win = $( window ), 732 over = data.collisionPosition.left + data.collisionWidth - win.width() - win.scrollLeft(), 733 myOffset = data.my[ 0 ] === "left" ? 734 -data.elemWidth : 735 data.my[ 0 ] === "right" ? 736 data.elemWidth : 737 0, 738 atOffset = data.at[ 0 ] === "left" ? 739 data.targetWidth : 740 -data.targetWidth, 741 offset = -2 * data.offset[ 0 ]; 742 position.left += data.collisionPosition.left < 0 ? 743 myOffset + atOffset + offset : 744 over > 0 ? 745 myOffset + atOffset + offset : 746 0; 747 }, 748 top: function( position, data ) { 749 if ( data.at[1] === center ) { 750 return; 751 } 752 var win = $( window ), 753 over = data.collisionPosition.top + data.collisionHeight - win.height() - win.scrollTop(), 754 myOffset = data.my[ 1 ] === "top" ? 755 -data.elemHeight : 756 data.my[ 1 ] === "bottom" ? 757 data.elemHeight : 758 0, 759 atOffset = data.at[ 1 ] === "top" ? 760 data.targetHeight : 761 -data.targetHeight, 762 offset = -2 * data.offset[ 1 ]; 763 position.top += data.collisionPosition.top < 0 ? 764 myOffset + atOffset + offset : 765 over > 0 ? 766 myOffset + atOffset + offset : 767 0; 768 } 769 } 770 }; 771 772 // offset setter from jQuery 1.4 773 if ( !$.offset.setOffset ) { 774 $.offset.setOffset = function( elem, options ) { 775 // set position first, in-case top/left are set even on static elem 776 if ( /static/.test( $.curCSS( elem, "position" ) ) ) { 777 elem.style.position = "relative"; 778 } 779 var curElem = $( elem ), 780 curOffset = curElem.offset(), 781 curTop = parseInt( $.curCSS( elem, "top", true ), 10 ) || 0, 782 curLeft = parseInt( $.curCSS( elem, "left", true ), 10) || 0, 783 props = { 784 top: (options.top - curOffset.top) + curTop, 785 left: (options.left - curOffset.left) + curLeft 786 }; 787 788 if ( 'using' in options ) { 789 options.using.call( elem, props ); 790 } else { 791 curElem.css( props ); 792 } 793 }; 794 795 $.fn.offset = function( options ) { 796 var elem = this[ 0 ]; 797 if ( !elem || !elem.ownerDocument ) { return null; } 798 if ( options ) { 799 return this.each(function() { 800 $.offset.setOffset( this, options ); 801 }); 802 } 803 return _offset.call( this ); 804 }; 805 } 806 807 }( jQuery )); 808 /* 809 * jQuery UI Autocomplete 1.8.5 810 * 811 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 812 * Dual licensed under the MIT or GPL Version 2 licenses. 813 * http://jquery.org/license 814 * 815 * http://docs.jquery.com/UI/Autocomplete 816 * 817 * Depends: 818 * jquery.ui.core.js 819 * jquery.ui.widget.js 820 * jquery.ui.position.js 821 */ 822 (function( $, undefined ) { 823 824 $.widget( "ui.autocomplete", { 825 options: { 826 appendTo: "body", 827 delay: 300, 828 minLength: 1, 829 position: { 830 my: "left top", 831 at: "left bottom", 832 collision: "none" 833 }, 834 source: null 835 }, 836 _create: function() { 837 var self = this, 838 doc = this.element[ 0 ].ownerDocument; 839 this.element 840 .addClass( "ui-autocomplete-input" ) 841 .attr( "autocomplete", "off" ) 842 // TODO verify these actually work as intended 843 .attr({ 844 role: "textbox", 845 "aria-autocomplete": "list", 846 "aria-haspopup": "true" 847 }) 848 .bind( "keydown.autocomplete", function( event ) { 849 if ( self.options.disabled ) { 850 return; 851 } 852 853 var keyCode = $.ui.keyCode; 854 switch( event.keyCode ) { 855 case keyCode.PAGE_UP: 856 self._move( "previousPage", event ); 857 break; 858 case keyCode.PAGE_DOWN: 859 self._move( "nextPage", event ); 860 break; 861 case keyCode.UP: 862 self._move( "previous", event ); 863 // prevent moving cursor to beginning of text field in some browsers 864 event.preventDefault(); 865 break; 866 case keyCode.DOWN: 867 self._move( "next", event ); 868 // prevent moving cursor to end of text field in some browsers 869 event.preventDefault(); 870 break; 871 case keyCode.ENTER: 872 case keyCode.NUMPAD_ENTER: 873 // when menu is open or has focus 874 if ( self.menu.element.is( ":visible" ) ) { 875 event.preventDefault(); 876 } 877 //passthrough - ENTER and TAB both select the current element 878 case keyCode.TAB: 879 if ( !self.menu.active ) { 880 return; 881 } 882 self.menu.select( event ); 883 break; 884 case keyCode.ESCAPE: 885 self.element.val( self.term ); 886 self.close( event ); 887 break; 888 default: 889 // keypress is triggered before the input value is changed 890 clearTimeout( self.searching ); 891 self.searching = setTimeout(function() { 892 // only search if the value has changed 893 if ( self.term != self.element.val() ) { 894 self.selectedItem = null; 895 self.search( null, event ); 896 } 897 }, self.options.delay ); 898 break; 899 } 900 }) 901 .bind( "focus.autocomplete", function() { 902 if ( self.options.disabled ) { 903 return; 904 } 905 906 self.selectedItem = null; 907 self.previous = self.element.val(); 908 }) 909 .bind( "blur.autocomplete", function( event ) { 910 if ( self.options.disabled ) { 911 return; 912 } 913 914 clearTimeout( self.searching ); 915 // clicks on the menu (or a button to trigger a search) will cause a blur event 916 self.closing = setTimeout(function() { 917 self.close( event ); 918 self._change( event ); 919 }, 150 ); 920 }); 921 this._initSource(); 922 this.response = function() { 923 return self._response.apply( self, arguments ); 924 }; 925 this.menu = $( "<ul></ul>" ) 926 .addClass( "ui-autocomplete" ) 927 .appendTo( $( this.options.appendTo || "body", doc )[0] ) 928 // prevent the close-on-blur in case of a "slow" click on the menu (long mousedown) 929 .mousedown(function( event ) { 930 // clicking on the scrollbar causes focus to shift to the body 931 // but we can't detect a mouseup or a click immediately afterward 932 // so we have to track the next mousedown and close the menu if 933 // the user clicks somewhere outside of the autocomplete 934 var menuElement = self.menu.element[ 0 ]; 935 if ( event.target === menuElement ) { 936 setTimeout(function() { 937 $( document ).one( 'mousedown', function( event ) { 938 if ( event.target !== self.element[ 0 ] && 939 event.target !== menuElement && 940 !$.ui.contains( menuElement, event.target ) ) { 941 self.close(); 942 } 943 }); 944 }, 1 ); 945 } 946 947 // use another timeout to make sure the blur-event-handler on the input was already triggered 948 setTimeout(function() { 949 clearTimeout( self.closing ); 950 }, 13); 951 }) 952 .menu({ 953 focus: function( event, ui ) { 954 var item = ui.item.data( "item.autocomplete" ); 955 if ( false !== self._trigger( "focus", null, { item: item } ) ) { 956 // use value to match what will end up in the input, if it was a key event 957 if ( /^key/.test(event.originalEvent.type) ) { 958 self.element.val( item.value ); 959 } 960 } 961 }, 962 selected: function( event, ui ) { 963 var item = ui.item.data( "item.autocomplete" ), 964 previous = self.previous; 965 966 // only trigger when focus was lost (click on menu) 967 if ( self.element[0] !== doc.activeElement ) { 968 self.element.focus(); 969 self.previous = previous; 970 } 971 972 if ( false !== self._trigger( "select", event, { item: item } ) ) { 973 self.term = item.value; 974 self.element.val( item.value ); 975 } 976 977 self.close( event ); 978 self.selectedItem = item; 979 }, 980 blur: function( event, ui ) { 981 // don't set the value of the text field if it's already correct 982 // this prevents moving the cursor unnecessarily 983 if ( self.menu.element.is(":visible") && 984 ( self.element.val() !== self.term ) ) { 985 self.element.val( self.term ); 986 } 987 } 988 }) 989 .zIndex( this.element.zIndex() + 1 ) 990 // workaround for jQuery bug #5781 http://dev.jquery.com/ticket/5781 991 .css({ top: 0, left: 0 }) 992 .hide() 993 .data( "menu" ); 994 if ( $.fn.bgiframe ) { 995 this.menu.element.bgiframe(); 996 } 997 }, 998 999 destroy: function() { 1000 this.element 1001 .removeClass( "ui-autocomplete-input" ) 1002 .removeAttr( "autocomplete" ) 1003 .removeAttr( "role" ) 1004 .removeAttr( "aria-autocomplete" ) 1005 .removeAttr( "aria-haspopup" ); 1006 this.menu.element.remove(); 1007 $.Widget.prototype.destroy.call( this ); 1008 }, 1009 1010 _setOption: function( key, value ) { 1011 $.Widget.prototype._setOption.apply( this, arguments ); 1012 if ( key === "source" ) { 1013 this._initSource(); 1014 } 1015 if ( key === "appendTo" ) { 1016 this.menu.element.appendTo( $( value || "body", this.element[0].ownerDocument )[0] ) 1017 } 1018 }, 1019 1020 _initSource: function() { 1021 var self = this, 1022 array, 1023 url; 1024 if ( $.isArray(this.options.source) ) { 1025 array = this.options.source; 1026 this.source = function( request, response ) { 1027 response( $.ui.autocomplete.filter(array, request.term) ); 1028 }; 1029 } else if ( typeof this.options.source === "string" ) { 1030 url = this.options.source; 1031 this.source = function( request, response ) { 1032 if (self.xhr) { 1033 self.xhr.abort(); 1034 } 1035 self.xhr = $.getJSON( url, request, function( data, status, xhr ) { 1036 if ( xhr === self.xhr ) { 1037 response( data ); 1038 } 1039 self.xhr = null; 1040 }); 1041 }; 1042 } else { 1043 this.source = this.options.source; 1044 } 1045 }, 1046 1047 search: function( value, event ) { 1048 value = value != null ? value : this.element.val(); 1049 1050 // always save the actual value, not the one passed as an argument 1051 this.term = this.element.val(); 1052 1053 if ( value.length < this.options.minLength ) { 1054 return this.close( event ); 1055 } 1056 1057 clearTimeout( this.closing ); 1058 if ( this._trigger("search") === false ) { 1059 return; 1060 } 1061 1062 return this._search( value ); 1063 }, 1064 1065 _search: function( value ) { 1066 this.element.addClass( "ui-autocomplete-loading" ); 1067 1068 this.source( { term: value }, this.response ); 1069 }, 1070 1071 _response: function( content ) { 1072 if ( content.length ) { 1073 content = this._normalize( content ); 1074 this._suggest( content ); 1075 this._trigger( "open" ); 1076 } else { 1077 this.close(); 1078 } 1079 this.element.removeClass( "ui-autocomplete-loading" ); 1080 }, 1081 1082 close: function( event ) { 1083 clearTimeout( this.closing ); 1084 if ( this.menu.element.is(":visible") ) { 1085 this._trigger( "close", event ); 1086 this.menu.element.hide(); 1087 this.menu.deactivate(); 1088 } 1089 }, 1090 1091 _change: function( event ) { 1092 if ( this.previous !== this.element.val() ) { 1093 this._trigger( "change", event, { item: this.selectedItem } ); 1094 } 1095 }, 1096 1097 _normalize: function( items ) { 1098 // assume all items have the right format when the first item is complete 1099 if ( items.length && items[0].label && items[0].value ) { 1100 return items; 1101 } 1102 return $.map( items, function(item) { 1103 if ( typeof item === "string" ) { 1104 return { 1105 label: item, 1106 value: item 1107 }; 1108 } 1109 return $.extend({ 1110 label: item.label || item.value, 1111 value: item.value || item.label 1112 }, item ); 1113 }); 1114 }, 1115 1116 _suggest: function( items ) { 1117 var ul = this.menu.element 1118 .empty() 1119 .zIndex( this.element.zIndex() + 1 ), 1120 menuWidth, 1121 textWidth; 1122 this._renderMenu( ul, items ); 1123 // TODO refresh should check if the active item is still in the dom, removing the need for a manual deactivate 1124 this.menu.deactivate(); 1125 this.menu.refresh(); 1126 this.menu.element.show().position( $.extend({ 1127 of: this.element 1128 }, this.options.position )); 1129 1130 menuWidth = ul.width( "" ).outerWidth(); 1131 textWidth = this.element.outerWidth(); 1132 ul.outerWidth( Math.max( menuWidth, textWidth ) ); 1133 }, 1134 1135 _renderMenu: function( ul, items ) { 1136 var self = this; 1137 $.each( items, function( index, item ) { 1138 self._renderItem( ul, item ); 1139 }); 1140 }, 1141 1142 _renderItem: function( ul, item) { 1143 return $( "<li></li>" ) 1144 .data( "item.autocomplete", item ) 1145 .append( $( "<a></a>" ).text( item.label ) ) 1146 .appendTo( ul ); 1147 }, 1148 1149 _move: function( direction, event ) { 1150 if ( !this.menu.element.is(":visible") ) { 1151 this.search( null, event ); 1152 return; 1153 } 1154 if ( this.menu.first() && /^previous/.test(direction) || 1155 this.menu.last() && /^next/.test(direction) ) { 1156 this.element.val( this.term ); 1157 this.menu.deactivate(); 1158 return; 1159 } 1160 this.menu[ direction ]( event ); 1161 }, 1162 1163 widget: function() { 1164 return this.menu.element; 1165 } 1166 }); 1167 1168 $.extend( $.ui.autocomplete, { 1169 escapeRegex: function( value ) { 1170 return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); 1171 }, 1172 filter: function(array, term) { 1173 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" ); 1174 return $.grep( array, function(value) { 1175 return matcher.test( value.label || value.value || value ); 1176 }); 1177 } 1178 }); 1179 1180 }( jQuery )); 1181 1182 /* 1183 * jQuery UI Menu (not officially released) 1184 * 1185 * This widget isn't yet finished and the API is subject to change. We plan to finish 1186 * it for the next release. You're welcome to give it a try anyway and give us feedback, 1187 * as long as you're okay with migrating your code later on. We can help with that, too. 1188 * 1189 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 1190 * Dual licensed under the MIT or GPL Version 2 licenses. 1191 * http://jquery.org/license 1192 * 1193 * http://docs.jquery.com/UI/Menu 1194 * 1195 * Depends: 1196 * jquery.ui.core.js 1197 * jquery.ui.widget.js 1198 */ 1199 (function($) { 1200 1201 $.widget("ui.menu", { 1202 _create: function() { 1203 var self = this; 1204 this.element 1205 .addClass("ui-menu ui-widget ui-widget-content ui-corner-all") 1206 .attr({ 1207 role: "listbox", 1208 "aria-activedescendant": "ui-active-menuitem" 1209 }) 1210 .click(function( event ) { 1211 if ( !$( event.target ).closest( ".ui-menu-item a" ).length ) { 1212 return; 1213 } 1214 // temporary 1215 event.preventDefault(); 1216 self.select( event ); 1217 }); 1218 this.refresh(); 1219 }, 1220 1221 refresh: function() { 1222 var self = this; 1223 1224 // don't refresh list items that are already adapted 1225 var items = this.element.children("li:not(.ui-menu-item):has(a)") 1226 .addClass("ui-menu-item") 1227 .attr("role", "menuitem"); 1228 1229 items.children("a") 1230 .addClass("ui-corner-all") 1231 .attr("tabindex", -1) 1232 // mouseenter doesn't work with event delegation 1233 .mouseenter(function( event ) { 1234 self.activate( event, $(this).parent() ); 1235 }) 1236 .mouseleave(function() { 1237 self.deactivate(); 1238 }); 1239 }, 1240 1241 activate: function( event, item ) { 1242 this.deactivate(); 1243 if (this.hasScroll()) { 1244 var offset = item.offset().top - this.element.offset().top, 1245 scroll = this.element.attr("scrollTop"), 1246 elementHeight = this.element.height(); 1247 if (offset < 0) { 1248 this.element.attr("scrollTop", scroll + offset); 1249 } else if (offset >= elementHeight) { 1250 this.element.attr("scrollTop", scroll + offset - elementHeight + item.height()); 1251 } 1252 } 1253 this.active = item.eq(0) 1254 .children("a") 1255 .addClass("ui-state-hover") 1256 .attr("id", "ui-active-menuitem") 1257 .end(); 1258 this._trigger("focus", event, { item: item }); 1259 }, 1260 1261 deactivate: function() { 1262 if (!this.active) { return; } 1263 1264 this.active.children("a") 1265 .removeClass("ui-state-hover") 1266 .removeAttr("id"); 1267 this._trigger("blur"); 1268 this.active = null; 1269 }, 1270 1271 next: function(event) { 1272 this.move("next", ".ui-menu-item:first", event); 1273 }, 1274 1275 previous: function(event) { 1276 this.move("prev", ".ui-menu-item:last", event); 1277 }, 1278 1279 first: function() { 1280 return this.active && !this.active.prevAll(".ui-menu-item").length; 1281 }, 1282 1283 last: function() { 1284 return this.active && !this.active.nextAll(".ui-menu-item").length; 1285 }, 1286 1287 move: function(direction, edge, event) { 1288 if (!this.active) { 1289 this.activate(event, this.element.children(edge)); 1290 return; 1291 } 1292 var next = this.active[direction + "All"](".ui-menu-item").eq(0); 1293 if (next.length) { 1294 this.activate(event, next); 1295 } else { 1296 this.activate(event, this.element.children(edge)); 1297 } 1298 }, 1299 1300 // TODO merge with previousPage 1301 nextPage: function(event) { 1302 if (this.hasScroll()) { 1303 // TODO merge with no-scroll-else 1304 if (!this.active || this.last()) { 1305 this.activate(event, this.element.children(":first")); 1306 return; 1307 } 1308 var base = this.active.offset().top, 1309 height = this.element.height(), 1310 result = this.element.children("li").filter(function() { 1311 var close = $(this).offset().top - base - height + $(this).height(); 1312 // TODO improve approximation 1313 return close < 10 && close > -10; 1314 }); 1315 1316 // TODO try to catch this earlier when scrollTop indicates the last page anyway 1317 if (!result.length) { 1318 result = this.element.children(":last"); 1319 } 1320 this.activate(event, result); 1321 } else { 1322 this.activate(event, this.element.children(!this.active || this.last() ? ":first" : ":last")); 1323 } 1324 }, 1325 1326 // TODO merge with nextPage 1327 previousPage: function(event) { 1328 if (this.hasScroll()) { 1329 // TODO merge with no-scroll-else 1330 if (!this.active || this.first()) { 1331 this.activate(event, this.element.children(":last")); 1332 return; 1333 } 1334 1335 var base = this.active.offset().top, 1336 height = this.element.height(); 1337 result = this.element.children("li").filter(function() { 1338 var close = $(this).offset().top - base + height - $(this).height(); 1339 // TODO improve approximation 1340 return close < 10 && close > -10; 1341 }); 1342 1343 // TODO try to catch this earlier when scrollTop indicates the last page anyway 1344 if (!result.length) { 1345 result = this.element.children(":first"); 1346 } 1347 this.activate(event, result); 1348 } else { 1349 this.activate(event, this.element.children(!this.active || this.first() ? ":last" : ":first")); 1350 } 1351 }, 1352 1353 hasScroll: function() { 1354 return this.element.height() < this.element.attr("scrollHeight"); 1355 }, 1356 1357 select: function( event ) { 1358 this._trigger("selected", event, { item: this.active }); 1359 } 1360 }); 1361 1362 }(jQuery)); -
new file django/contrib/admin/media/js/jquery-ui.min.js
diff --git a/django/contrib/admin/media/js/jquery-ui.min.js b/django/contrib/admin/media/js/jquery-ui.min.js new file mode 100644
- + 1 /*! 2 * jQuery UI 1.8.5 3 * 4 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 5 * Dual licensed under the MIT or GPL Version 2 licenses. 6 * http://jquery.org/license 7 * 8 * http://docs.jquery.com/UI 9 */ 10 (function(c,j){function k(a){return!c(a).parents().andSelf().filter(function(){return c.curCSS(this,"visibility")==="hidden"||c.expr.filters.hidden(this)}).length}c.ui=c.ui||{};if(!c.ui.version){c.extend(c.ui,{version:"1.8.5",keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106, 11 NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}});c.fn.extend({_focus:c.fn.focus,focus:function(a,b){return typeof a==="number"?this.each(function(){var d=this;setTimeout(function(){c(d).focus();b&&b.call(d)},a)}):this._focus.apply(this,arguments)},scrollParent:function(){var a;a=c.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(c.curCSS(this, 12 "position",1))&&/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0);return/fixed/.test(this.css("position"))||!a.length?c(document):a},zIndex:function(a){if(a!==j)return this.css("zIndex",a);if(this.length){a=c(this[0]);for(var b;a.length&&a[0]!==document;){b=a.css("position"); 13 if(b==="absolute"||b==="relative"||b==="fixed"){b=parseInt(a.css("zIndex"));if(!isNaN(b)&&b!=0)return b}a=a.parent()}}return 0},disableSelection:function(){return this.bind("mousedown.ui-disableSelection selectstart.ui-disableSelection",function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}});c.each(["Width","Height"],function(a,b){function d(f,g,l,m){c.each(e,function(){g-=parseFloat(c.curCSS(f,"padding"+this,true))||0;if(l)g-=parseFloat(c.curCSS(f, 14 "border"+this+"Width",true))||0;if(m)g-=parseFloat(c.curCSS(f,"margin"+this,true))||0});return g}var e=b==="Width"?["Left","Right"]:["Top","Bottom"],h=b.toLowerCase(),i={innerWidth:c.fn.innerWidth,innerHeight:c.fn.innerHeight,outerWidth:c.fn.outerWidth,outerHeight:c.fn.outerHeight};c.fn["inner"+b]=function(f){if(f===j)return i["inner"+b].call(this);return this.each(function(){c.style(this,h,d(this,f)+"px")})};c.fn["outer"+b]=function(f,g){if(typeof f!=="number")return i["outer"+b].call(this,f);return this.each(function(){c.style(this, 15 h,d(this,f,true,g)+"px")})}});c.extend(c.expr[":"],{data:function(a,b,d){return!!c.data(a,d[3])},focusable:function(a){var b=a.nodeName.toLowerCase(),d=c.attr(a,"tabindex");if("area"===b){b=a.parentNode;d=b.name;if(!a.href||!d||b.nodeName.toLowerCase()!=="map")return false;a=c("img[usemap=#"+d+"]")[0];return!!a&&k(a)}return(/input|select|textarea|button|object/.test(b)?!a.disabled:"a"==b?a.href||!isNaN(d):!isNaN(d))&&k(a)},tabbable:function(a){var b=c.attr(a,"tabindex");return(isNaN(b)||b>=0)&&c(a).is(":focusable")}}); 16 c(function(){var a=document.createElement("div"),b=document.body;c.extend(a.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0});c.support.minHeight=b.appendChild(a).offsetHeight===100;b.removeChild(a).style.display="none"});c.extend(c.ui,{plugin:{add:function(a,b,d){a=c.ui[a].prototype;for(var e in d){a.plugins[e]=a.plugins[e]||[];a.plugins[e].push([b,d[e]])}},call:function(a,b,d){if((b=a.plugins[b])&&a.element[0].parentNode)for(var e=0;e<b.length;e++)a.options[b[e][0]]&&b[e][1].apply(a.element, 17 d)}},contains:function(a,b){return document.compareDocumentPosition?a.compareDocumentPosition(b)&16:a!==b&&a.contains(b)},hasScroll:function(a,b){if(c(a).css("overflow")==="hidden")return false;b=b&&b==="left"?"scrollLeft":"scrollTop";var d=false;if(a[b]>0)return true;a[b]=1;d=a[b]>0;a[b]=0;return d},isOverAxis:function(a,b,d){return a>b&&a<b+d},isOver:function(a,b,d,e,h,i){return c.ui.isOverAxis(a,d,h)&&c.ui.isOverAxis(b,e,i)}})}})(jQuery); 18 ;/*! 19 * jQuery UI Widget 1.8.5 20 * 21 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 22 * Dual licensed under the MIT or GPL Version 2 licenses. 23 * http://jquery.org/license 24 * 25 * http://docs.jquery.com/UI/Widget 26 */ 27 (function(b,j){if(b.cleanData){var k=b.cleanData;b.cleanData=function(a){for(var c=0,d;(d=a[c])!=null;c++)b(d).triggerHandler("remove");k(a)}}else{var l=b.fn.remove;b.fn.remove=function(a,c){return this.each(function(){if(!c)if(!a||b.filter(a,[this]).length)b("*",this).add([this]).each(function(){b(this).triggerHandler("remove")});return l.call(b(this),a,c)})}}b.widget=function(a,c,d){var e=a.split(".")[0],f;a=a.split(".")[1];f=e+"-"+a;if(!d){d=c;c=b.Widget}b.expr[":"][f]=function(h){return!!b.data(h, 28 a)};b[e]=b[e]||{};b[e][a]=function(h,g){arguments.length&&this._createWidget(h,g)};c=new c;c.options=b.extend(true,{},c.options);b[e][a].prototype=b.extend(true,c,{namespace:e,widgetName:a,widgetEventPrefix:b[e][a].prototype.widgetEventPrefix||a,widgetBaseClass:f},d);b.widget.bridge(a,b[e][a])};b.widget.bridge=function(a,c){b.fn[a]=function(d){var e=typeof d==="string",f=Array.prototype.slice.call(arguments,1),h=this;d=!e&&f.length?b.extend.apply(null,[true,d].concat(f)):d;if(e&&d.substring(0,1)=== 29 "_")return h;e?this.each(function(){var g=b.data(this,a);if(!g)throw"cannot call methods on "+a+" prior to initialization; attempted to call method '"+d+"'";if(!b.isFunction(g[d]))throw"no such method '"+d+"' for "+a+" widget instance";var i=g[d].apply(g,f);if(i!==g&&i!==j){h=i;return false}}):this.each(function(){var g=b.data(this,a);g?g.option(d||{})._init():b.data(this,a,new c(d,this))});return h}};b.Widget=function(a,c){arguments.length&&this._createWidget(a,c)};b.Widget.prototype={widgetName:"widget", 30 widgetEventPrefix:"",options:{disabled:false},_createWidget:function(a,c){b.data(c,this.widgetName,this);this.element=b(c);this.options=b.extend(true,{},this.options,b.metadata&&b.metadata.get(c)[this.widgetName],a);var d=this;this.element.bind("remove."+this.widgetName,function(){d.destroy()});this._create();this._init()},_create:function(){},_init:function(){},destroy:function(){this.element.unbind("."+this.widgetName).removeData(this.widgetName);this.widget().unbind("."+this.widgetName).removeAttr("aria-disabled").removeClass(this.widgetBaseClass+ 31 "-disabled ui-state-disabled")},widget:function(){return this.element},option:function(a,c){var d=a,e=this;if(arguments.length===0)return b.extend({},e.options);if(typeof a==="string"){if(c===j)return this.options[a];d={};d[a]=c}b.each(d,function(f,h){e._setOption(f,h)});return e},_setOption:function(a,c){this.options[a]=c;if(a==="disabled")this.widget()[c?"addClass":"removeClass"](this.widgetBaseClass+"-disabled ui-state-disabled").attr("aria-disabled",c);return this},enable:function(){return this._setOption("disabled", 32 false)},disable:function(){return this._setOption("disabled",true)},_trigger:function(a,c,d){var e=this.options[a];c=b.Event(c);c.type=(a===this.widgetEventPrefix?a:this.widgetEventPrefix+a).toLowerCase();d=d||{};if(c.originalEvent){a=b.event.props.length;for(var f;a;){f=b.event.props[--a];c[f]=c.originalEvent[f]}}this.element.trigger(c,d);return!(b.isFunction(e)&&e.call(this.element[0],c,d)===false||c.isDefaultPrevented())}}})(jQuery); 33 ;/* 34 * jQuery UI Position 1.8.5 35 * 36 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 37 * Dual licensed under the MIT or GPL Version 2 licenses. 38 * http://jquery.org/license 39 * 40 * http://docs.jquery.com/UI/Position 41 */ 42 (function(c){c.ui=c.ui||{};var n=/left|center|right/,o=/top|center|bottom/,t=c.fn.position,u=c.fn.offset;c.fn.position=function(b){if(!b||!b.of)return t.apply(this,arguments);b=c.extend({},b);var a=c(b.of),d=a[0],g=(b.collision||"flip").split(" "),e=b.offset?b.offset.split(" "):[0,0],h,k,j;if(d.nodeType===9){h=a.width();k=a.height();j={top:0,left:0}}else if(d.scrollTo&&d.document){h=a.width();k=a.height();j={top:a.scrollTop(),left:a.scrollLeft()}}else if(d.preventDefault){b.at="left top";h=k=0;j= 43 {top:b.of.pageY,left:b.of.pageX}}else{h=a.outerWidth();k=a.outerHeight();j=a.offset()}c.each(["my","at"],function(){var f=(b[this]||"").split(" ");if(f.length===1)f=n.test(f[0])?f.concat(["center"]):o.test(f[0])?["center"].concat(f):["center","center"];f[0]=n.test(f[0])?f[0]:"center";f[1]=o.test(f[1])?f[1]:"center";b[this]=f});if(g.length===1)g[1]=g[0];e[0]=parseInt(e[0],10)||0;if(e.length===1)e[1]=e[0];e[1]=parseInt(e[1],10)||0;if(b.at[0]==="right")j.left+=h;else if(b.at[0]==="center")j.left+=h/ 44 2;if(b.at[1]==="bottom")j.top+=k;else if(b.at[1]==="center")j.top+=k/2;j.left+=e[0];j.top+=e[1];return this.each(function(){var f=c(this),l=f.outerWidth(),m=f.outerHeight(),p=parseInt(c.curCSS(this,"marginLeft",true))||0,q=parseInt(c.curCSS(this,"marginTop",true))||0,v=l+p+parseInt(c.curCSS(this,"marginRight",true))||0,w=m+q+parseInt(c.curCSS(this,"marginBottom",true))||0,i=c.extend({},j),r;if(b.my[0]==="right")i.left-=l;else if(b.my[0]==="center")i.left-=l/2;if(b.my[1]==="bottom")i.top-=m;else if(b.my[1]=== 45 "center")i.top-=m/2;i.left=parseInt(i.left);i.top=parseInt(i.top);r={left:i.left-p,top:i.top-q};c.each(["left","top"],function(s,x){c.ui.position[g[s]]&&c.ui.position[g[s]][x](i,{targetWidth:h,targetHeight:k,elemWidth:l,elemHeight:m,collisionPosition:r,collisionWidth:v,collisionHeight:w,offset:e,my:b.my,at:b.at})});c.fn.bgiframe&&f.bgiframe();f.offset(c.extend(i,{using:b.using}))})};c.ui.position={fit:{left:function(b,a){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft(); 46 b.left=d>0?b.left-d:Math.max(b.left-a.collisionPosition.left,b.left)},top:function(b,a){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();b.top=d>0?b.top-d:Math.max(b.top-a.collisionPosition.top,b.top)}},flip:{left:function(b,a){if(a.at[0]!=="center"){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft();var g=a.my[0]==="left"?-a.elemWidth:a.my[0]==="right"?a.elemWidth:0,e=a.at[0]==="left"?a.targetWidth:-a.targetWidth,h=-2*a.offset[0]; 47 b.left+=a.collisionPosition.left<0?g+e+h:d>0?g+e+h:0}},top:function(b,a){if(a.at[1]!=="center"){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();var g=a.my[1]==="top"?-a.elemHeight:a.my[1]==="bottom"?a.elemHeight:0,e=a.at[1]==="top"?a.targetHeight:-a.targetHeight,h=-2*a.offset[1];b.top+=a.collisionPosition.top<0?g+e+h:d>0?g+e+h:0}}}};if(!c.offset.setOffset){c.offset.setOffset=function(b,a){if(/static/.test(c.curCSS(b,"position")))b.style.position="relative";var d= 48 c(b),g=d.offset(),e=parseInt(c.curCSS(b,"top",true),10)||0,h=parseInt(c.curCSS(b,"left",true),10)||0;g={top:a.top-g.top+e,left:a.left-g.left+h};"using"in a?a.using.call(b,g):d.css(g)};c.fn.offset=function(b){var a=this[0];if(!a||!a.ownerDocument)return null;if(b)return this.each(function(){c.offset.setOffset(this,b)});return u.call(this)}}})(jQuery); 49 ;/* 50 * jQuery UI Autocomplete 1.8.5 51 * 52 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 53 * Dual licensed under the MIT or GPL Version 2 licenses. 54 * http://jquery.org/license 55 * 56 * http://docs.jquery.com/UI/Autocomplete 57 * 58 * Depends: 59 * jquery.ui.core.js 60 * jquery.ui.widget.js 61 * jquery.ui.position.js 62 */ 63 (function(e){e.widget("ui.autocomplete",{options:{appendTo:"body",delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null},_create:function(){var a=this,b=this.element[0].ownerDocument;this.element.addClass("ui-autocomplete-input").attr("autocomplete","off").attr({role:"textbox","aria-autocomplete":"list","aria-haspopup":"true"}).bind("keydown.autocomplete",function(c){if(!a.options.disabled){var d=e.ui.keyCode;switch(c.keyCode){case d.PAGE_UP:a._move("previousPage", 64 c);break;case d.PAGE_DOWN:a._move("nextPage",c);break;case d.UP:a._move("previous",c);c.preventDefault();break;case d.DOWN:a._move("next",c);c.preventDefault();break;case d.ENTER:case d.NUMPAD_ENTER:a.menu.element.is(":visible")&&c.preventDefault();case d.TAB:if(!a.menu.active)return;a.menu.select(c);break;case d.ESCAPE:a.element.val(a.term);a.close(c);break;default:clearTimeout(a.searching);a.searching=setTimeout(function(){if(a.term!=a.element.val()){a.selectedItem=null;a.search(null,c)}},a.options.delay); 65 break}}}).bind("focus.autocomplete",function(){if(!a.options.disabled){a.selectedItem=null;a.previous=a.element.val()}}).bind("blur.autocomplete",function(c){if(!a.options.disabled){clearTimeout(a.searching);a.closing=setTimeout(function(){a.close(c);a._change(c)},150)}});this._initSource();this.response=function(){return a._response.apply(a,arguments)};this.menu=e("<ul></ul>").addClass("ui-autocomplete").appendTo(e(this.options.appendTo||"body",b)[0]).mousedown(function(c){var d=a.menu.element[0]; 66 c.target===d&&setTimeout(function(){e(document).one("mousedown",function(f){f.target!==a.element[0]&&f.target!==d&&!e.ui.contains(d,f.target)&&a.close()})},1);setTimeout(function(){clearTimeout(a.closing)},13)}).menu({focus:function(c,d){d=d.item.data("item.autocomplete");false!==a._trigger("focus",null,{item:d})&&/^key/.test(c.originalEvent.type)&&a.element.val(d.value)},selected:function(c,d){d=d.item.data("item.autocomplete");var f=a.previous;if(a.element[0]!==b.activeElement){a.element.focus(); 67 a.previous=f}if(false!==a._trigger("select",c,{item:d})){a.term=d.value;a.element.val(d.value)}a.close(c);a.selectedItem=d},blur:function(){a.menu.element.is(":visible")&&a.element.val()!==a.term&&a.element.val(a.term)}}).zIndex(this.element.zIndex()+1).css({top:0,left:0}).hide().data("menu");e.fn.bgiframe&&this.menu.element.bgiframe()},destroy:function(){this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete").removeAttr("role").removeAttr("aria-autocomplete").removeAttr("aria-haspopup"); 68 this.menu.element.remove();e.Widget.prototype.destroy.call(this)},_setOption:function(a,b){e.Widget.prototype._setOption.apply(this,arguments);a==="source"&&this._initSource();if(a==="appendTo")this.menu.element.appendTo(e(b||"body",this.element[0].ownerDocument)[0])},_initSource:function(){var a=this,b,c;if(e.isArray(this.options.source)){b=this.options.source;this.source=function(d,f){f(e.ui.autocomplete.filter(b,d.term))}}else if(typeof this.options.source==="string"){c=this.options.source;this.source= 69 function(d,f){a.xhr&&a.xhr.abort();a.xhr=e.getJSON(c,d,function(g,i,h){h===a.xhr&&f(g);a.xhr=null})}}else this.source=this.options.source},search:function(a,b){a=a!=null?a:this.element.val();this.term=this.element.val();if(a.length<this.options.minLength)return this.close(b);clearTimeout(this.closing);if(this._trigger("search")!==false)return this._search(a)},_search:function(a){this.element.addClass("ui-autocomplete-loading");this.source({term:a},this.response)},_response:function(a){if(a.length){a= 70 this._normalize(a);this._suggest(a);this._trigger("open")}else this.close();this.element.removeClass("ui-autocomplete-loading")},close:function(a){clearTimeout(this.closing);if(this.menu.element.is(":visible")){this._trigger("close",a);this.menu.element.hide();this.menu.deactivate()}},_change:function(a){this.previous!==this.element.val()&&this._trigger("change",a,{item:this.selectedItem})},_normalize:function(a){if(a.length&&a[0].label&&a[0].value)return a;return e.map(a,function(b){if(typeof b=== 71 "string")return{label:b,value:b};return e.extend({label:b.label||b.value,value:b.value||b.label},b)})},_suggest:function(a){var b=this.menu.element.empty().zIndex(this.element.zIndex()+1),c;this._renderMenu(b,a);this.menu.deactivate();this.menu.refresh();this.menu.element.show().position(e.extend({of:this.element},this.options.position));a=b.width("").outerWidth();c=this.element.outerWidth();b.outerWidth(Math.max(a,c))},_renderMenu:function(a,b){var c=this;e.each(b,function(d,f){c._renderItem(a,f)})}, 72 _renderItem:function(a,b){return e("<li></li>").data("item.autocomplete",b).append(e("<a></a>").text(b.label)).appendTo(a)},_move:function(a,b){if(this.menu.element.is(":visible"))if(this.menu.first()&&/^previous/.test(a)||this.menu.last()&&/^next/.test(a)){this.element.val(this.term);this.menu.deactivate()}else this.menu[a](b);else this.search(null,b)},widget:function(){return this.menu.element}});e.extend(e.ui.autocomplete,{escapeRegex:function(a){return a.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&")}, 73 filter:function(a,b){var c=new RegExp(e.ui.autocomplete.escapeRegex(b),"i");return e.grep(a,function(d){return c.test(d.label||d.value||d)})}})})(jQuery); 74 (function(e){e.widget("ui.menu",{_create:function(){var a=this;this.element.addClass("ui-menu ui-widget ui-widget-content ui-corner-all").attr({role:"listbox","aria-activedescendant":"ui-active-menuitem"}).click(function(b){if(e(b.target).closest(".ui-menu-item a").length){b.preventDefault();a.select(b)}});this.refresh()},refresh:function(){var a=this;this.element.children("li:not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","menuitem").children("a").addClass("ui-corner-all").attr("tabindex", 75 -1).mouseenter(function(b){a.activate(b,e(this).parent())}).mouseleave(function(){a.deactivate()})},activate:function(a,b){this.deactivate();if(this.hasScroll()){var c=b.offset().top-this.element.offset().top,d=this.element.attr("scrollTop"),f=this.element.height();if(c<0)this.element.attr("scrollTop",d+c);else c>=f&&this.element.attr("scrollTop",d+c-f+b.height())}this.active=b.eq(0).children("a").addClass("ui-state-hover").attr("id","ui-active-menuitem").end();this._trigger("focus",a,{item:b})}, 76 deactivate:function(){if(this.active){this.active.children("a").removeClass("ui-state-hover").removeAttr("id");this._trigger("blur");this.active=null}},next:function(a){this.move("next",".ui-menu-item:first",a)},previous:function(a){this.move("prev",".ui-menu-item:last",a)},first:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},last:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},move:function(a,b,c){if(this.active){a=this.active[a+"All"](".ui-menu-item").eq(0); 77 a.length?this.activate(c,a):this.activate(c,this.element.children(b))}else this.activate(c,this.element.children(b))},nextPage:function(a){if(this.hasScroll())if(!this.active||this.last())this.activate(a,this.element.children(":first"));else{var b=this.active.offset().top,c=this.element.height(),d=this.element.children("li").filter(function(){var f=e(this).offset().top-b-c+e(this).height();return f<10&&f>-10});d.length||(d=this.element.children(":last"));this.activate(a,d)}else this.activate(a,this.element.children(!this.active|| 78 this.last()?":first":":last"))},previousPage:function(a){if(this.hasScroll())if(!this.active||this.first())this.activate(a,this.element.children(":last"));else{var b=this.active.offset().top,c=this.element.height();result=this.element.children("li").filter(function(){var d=e(this).offset().top-b+c-e(this).height();return d<10&&d>-10});result.length||(result=this.element.children(":first"));this.activate(a,result)}else this.activate(a,this.element.children(!this.active||this.first()?":last":":first"))}, 79 hasScroll:function(){return this.element.height()<this.element.attr("scrollHeight")},select:function(a){this._trigger("selected",a,{item:this.active})}})})(jQuery); 80 ; 81 No newline at end of file -
django/contrib/admin/options.py
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
a b 1 import operator 1 2 from django import forms, template 2 3 from django.forms.formsets import all_valid 3 4 from django.forms.models import modelform_factory, modelformset_factory, inlineformset_factory … … 13 14 from django.db.models.fields import BLANK_CHOICE_DASH 14 15 from django.http import Http404, HttpResponse, HttpResponseRedirect 15 16 from django.shortcuts import get_object_or_404, render_to_response 17 from django.utils import simplejson 16 18 from django.utils.decorators import method_decorator 17 19 from django.utils.datastructures import SortedDict 18 20 from django.utils.functional import update_wrapper … … 21 23 from django.utils.functional import curry 22 24 from django.utils.text import capfirst, get_text_list 23 25 from django.utils.translation import ugettext as _ 24 from django.utils.translation import ungettext 25 from django.utils.encoding import force_unicode 26 from django.utils.translation import ungettext, ugettext_lazy 27 from django.utils.encoding import force_unicode, smart_str 26 28 27 29 HORIZONTAL, VERTICAL = 1, 2 28 30 # returns the <ul> class for a given radio_admin field … … 50 52 models.FileField: {'widget': widgets.AdminFileWidget}, 51 53 } 52 54 55 AUTOCOMPLETE_FIELDS_DEFAULTS = { 56 'limit': 5, 57 'value': lambda o: unicode(o), 58 'label': lambda o: unicode(o), 59 'show_search': True, 60 } 61 53 62 csrf_protect_m = method_decorator(csrf_protect) 54 63 55 64 class BaseModelAdmin(object): … … 57 66 __metaclass__ = forms.MediaDefiningClass 58 67 59 68 raw_id_fields = () 69 autocomplete_fields = {} 60 70 fields = None 61 71 exclude = None 62 72 fieldsets = None … … 74 84 overrides.update(self.formfield_overrides) 75 85 self.formfield_overrides = overrides 76 86 87 def build_setting(value): 88 if value in settings['queryset'].model._meta.get_all_field_names(): 89 return lambda m: getattr(m, value) 90 return lambda m: value % vars(m) 91 92 autocomplete_fields = {} 93 for (field, values) in self.autocomplete_fields.items(): 94 settings = autocomplete_fields[field] = AUTOCOMPLETE_FIELDS_DEFAULTS.copy() 95 settings.update(values) 96 if hasattr(self.model, field): 97 rel = getattr(self.model, field).field.rel 98 settings['id'] = settings.get('id', rel.get_related_field().name) 99 if not settings.get('queryset'): 100 settings['queryset'] = rel.to._default_manager.complex_filter(rel.limit_choices_to) 101 for option in ('value', 'label'): 102 if isinstance(settings[option], (str, unicode)): 103 settings[option] = build_setting(settings[option]) 104 105 self.autocomplete_fields = autocomplete_fields 106 107 77 108 def formfield_for_dbfield(self, db_field, **kwargs): 78 109 """ 79 110 Hook for specifying the form Field instance for a given database Field … … 107 138 # extra HTML -- the "add other" interface -- to the end of the 108 139 # rendered output. formfield can be None if it came from a 109 140 # OneToOneField with parent_link=True or a M2M intermediary. 110 if formfield and db_field.name not in self.raw_id_fields: 141 if (formfield and db_field.name not in self.raw_id_fields 142 and db_field.name not in self.autocomplete_fields): 111 143 related_modeladmin = self.admin_site._registry.get( 112 144 db_field.rel.to) 113 145 can_add_related = bool(related_modeladmin and … … 118 150 119 151 return formfield 120 152 153 elif db_field.name in self.autocomplete_fields: 154 kwargs['widget'] = widgets.AutocompleteWidget( 155 self.autocomplete_fields[db_field.name], 156 using=kwargs.get('using'), 157 force_selection=False) 158 121 159 # If we've got overrides for the formfield defined, use 'em. **kwargs 122 160 # passed to formfield_for_dbfield override the defaults. 123 161 for klass in db_field.__class__.mro(): … … 153 191 db = kwargs.get('using') 154 192 if db_field.name in self.raw_id_fields: 155 193 kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel, using=db) 194 elif db_field.name in self.autocomplete_fields: 195 kwargs['widget'] = widgets.AutocompleteWidget( 196 self.autocomplete_fields[db_field.name], using=db) 156 197 elif db_field.name in self.radio_fields: 157 198 kwargs['widget'] = widgets.AdminRadioSelect(attrs={ 158 199 'class': get_ul_class(self.radio_fields[db_field.name]), … … 174 215 if db_field.name in self.raw_id_fields: 175 216 kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel, using=db) 176 217 kwargs['help_text'] = '' 218 elif db_field.name in self.autocomplete_fields: 219 kwargs['widget'] = widgets.MultipleAutocompleteWidget( 220 self.autocomplete_fields[db_field.name], using=db) 221 kwargs['help_text'] = '' 177 222 elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)): 178 223 kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical)) 179 224 … … 266 311 url(r'^add/$', 267 312 wrap(self.add_view), 268 313 name='%s_%s_add' % info), 314 url(r'^autocomplete/(?P<field>[\w]+)/$', 315 wrap(self.autocomplete_view), 316 name='%s_%s_autocomplete' % info), 269 317 url(r'^(.+)/history/$', 270 318 wrap(self.history_view), 271 319 name='%s_%s_history' % info), … … 287 335 288 336 js = ['js/core.js', 'js/admin/RelatedObjectLookups.js', 289 337 'js/jquery.min.js', 'js/jquery.init.js'] 338 if self.autocomplete_fields: 339 js.insert(3, 'js/jquery-ui.min.js') 290 340 if self.actions is not None: 291 341 js.extend(['js/actions.min.js']) 292 342 if self.prepopulated_fields: … … 771 821 self.message_user(request, msg) 772 822 return None 773 823 824 def autocomplete_view(self, request, field, extra_content=None): 825 query = request.GET.get('term', None) 826 827 if field not in self.autocomplete_fields or query is None: 828 raise Http404 829 830 settings = self.autocomplete_fields[field] 831 queryset = settings['queryset'] 832 search_fields = settings['fields'] 833 if request.GET.get('by_id', None) is not None: 834 # lookup only via an exact match on id 835 search_fields = ('=%s' % settings['id'],) 836 837 def construct_search(field_name): 838 # use different lookup methods depending on the notation 839 if field_name.startswith('^'): 840 return "%s__istartswith" % field_name[1:] 841 elif field_name.startswith('='): 842 return "%s__iexact" % field_name[1:] 843 elif field_name.startswith('@'): 844 return "%s__search" % field_name[1:] 845 else: 846 return "%s__icontains" % field_name 847 848 for bit in query.split(): 849 or_queries = [models.Q(**{construct_search( 850 smart_str(field_name)): bit}) 851 for field_name in search_fields] 852 queryset = queryset.filter(reduce(operator.or_, or_queries)) 853 854 data = [] 855 for o in queryset[:settings['limit']]: 856 data.append({ 857 'id': getattr(o, settings['id']), 858 'value': settings['value'](o), 859 'label': settings['label'](o), 860 }) 861 862 return HttpResponse(simplejson.dumps(data)) 863 774 864 @csrf_protect_m 775 865 @transaction.commit_on_success 776 866 def add_view(self, request, form_url='', extra_context=None): -
django/contrib/admin/widgets.py
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
a b 7 7 from django import forms 8 8 from django.forms.widgets import RadioFieldRenderer 9 9 from django.forms.util import flatatt 10 from django.utils import simplejson 10 11 from django.utils.html import escape 11 12 from django.utils.text import truncate_words 12 13 from django.utils.translation import ugettext as _ … … 91 92 template_with_clear = (u'<span class="clearable-file-input">%s</span>' 92 93 % forms.ClearableFileInput.template_with_clear) 93 94 95 def _get_search_icon(model, name, value, params, attrs): 96 related_url = '../../../%s/%s/' % (model._meta.app_label, model._meta.object_name.lower()) 97 if params: 98 url = '?' + '&'.join(['%s=%s' % (k, v) for k, v in params.items()]) 99 else: 100 url = '' 101 # TODO: "id_" is hard-coded here. This should instead use the correct 102 # API to determine the ID dynamically. 103 output = [] 104 output.append('<a href="%s%s" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);"> ' % \ 105 (related_url, url, name)) 106 output.append('<img src="%simg/admin/selector-search.gif" width="16" height="16" alt="%s" /></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup'))) 107 return output 94 108 95 109 class ForeignKeyRawIdWidget(forms.TextInput): 96 110 """ … … 105 119 def render(self, name, value, attrs=None): 106 120 if attrs is None: 107 121 attrs = {} 108 related_url = '../../../%s/%s/' % (self.rel.to._meta.app_label, self.rel.to._meta.object_name.lower())109 params = self.url_parameters()110 if params:111 url = '?' + '&'.join(['%s=%s' % (k, v) for k, v in params.items()])112 else:113 url = ''114 122 if "class" not in attrs: 115 123 attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript looks for this hook. 116 124 output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)] 117 # TODO: "id_" is hard-coded here. This should instead use the correct 118 # API to determine the ID dynamically. 119 output.append('<a href="%s%s" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);"> ' % \ 120 (related_url, url, name)) 121 output.append('<img src="%simg/admin/selector-search.gif" width="16" height="16" alt="%s" /></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup'))) 125 126 output += _get_search_icon(self.rel.to, name, value, self.url_parameters(), attrs) 122 127 if value: 123 128 output.append(self.label_for_value(value)) 124 129 return mark_safe(u''.join(output)) … … 150 155 except (ValueError, self.rel.to.DoesNotExist): 151 156 return '' 152 157 158 class AutocompleteWidget(forms.Widget): 159 160 class Media: 161 js = ( 162 settings.ADMIN_MEDIA_PREFIX + 'js/admin/autocomplete.js', 163 ) 164 165 def __init__(self, settings, attrs=None, using=None, **js_options): 166 self.settings = settings 167 self.db = using 168 self.js_options = dict( 169 source = settings.get('source'), 170 multiple = settings.get('multiple', False), 171 force_selection = settings.get('force_selection', True), 172 ) 173 self.js_options.update(js_options) 174 super(AutocompleteWidget, self).__init__(attrs) 175 176 def get_autocomplete_url(self, name): 177 return '../autocomplete/%s/' % name 178 179 def render(self, name, value, attrs=None, hattrs=None, initial_objects=u''): 180 if value is None: 181 value = '' 182 hidden_id = 'id_hidden_%s' % name 183 hidden_attrs = self.build_attrs(type='hidden', name=name, value=value, id=hidden_id) 184 normal_attrs = self.build_attrs(attrs, type='text') 185 normal_attrs['value'] = self.label_for_value(value) 186 if not self.js_options.get('source'): 187 self.js_options['source'] = self.get_autocomplete_url(name) 188 options = simplejson.dumps(self.js_options) 189 if self.settings.get('show_search'): 190 target_key = self.settings.get('id') 191 search_icon = _get_search_icon(self.settings.get('queryset').model, 192 name, value, {'t': target_key}, attrs) 193 search_icon = u''.join(search_icon) + '\n' 194 else: 195 search_icon = u'' 196 return mark_safe(u''.join(( 197 u'<div class="djangoautocomplete-wrapper">\n', 198 u'<input%s />\n' % flatatt(hidden_attrs), 199 u'<input%s />\n' % flatatt(normal_attrs), 200 initial_objects, 201 u'</div>\n', 202 search_icon, 203 u'<script type="text/javascript">', 204 u'django.jQuery("#id_%s").djangoautocomplete(%s);' % (name, options), 205 u'</script>\n', 206 ))) 207 208 def label_for_value(self, value): 209 qs, key, value_fmt = [self.settings[k] for k in ('queryset','id','value')] 210 if not value: 211 return value 212 try: 213 obj = qs.get(**{key: value}) 214 return value_fmt(obj) 215 # XXX this shouldn't happen. 216 except qs.model.DoesNotExist: 217 return value 218 219 153 220 class ManyToManyRawIdWidget(ForeignKeyRawIdWidget): 154 221 """ 155 222 A Widget for displaying ManyToMany ids in the "raw_id" interface rather than … … 188 255 return True 189 256 return False 190 257 258 class MultipleAutocompleteWidget(AutocompleteWidget): 259 260 def __init__(self, settings, attrs=None, using=None, **js_options): 261 js_options['multiple'] = True 262 super(MultipleAutocompleteWidget, self).__init__(settings, attrs, 263 using, **js_options) 264 265 def render(self, name, value, attrs=None, hattrs=None): 266 if value: 267 initial_objects = self.initial_objects(value) 268 value = ','.join([str(v) for v in value]) 269 else: 270 initial_objects = u'' 271 return super(MultipleAutocompleteWidget, self).render( 272 name, value, attrs, hattrs, initial_objects) 273 274 def label_for_value(self, value): 275 return '' 276 277 def value_from_datadict(self, data, files, name): 278 value = data.get(name) 279 if value: 280 return value.split(',') 281 return value 282 283 def initial_objects(self, value): 284 qs, key, label_fmt = [self.settings[k] for k in ('queryset','id','label')] 285 output = [u'<ul class="ui-autocomplete-values">'] 286 for obj in qs.filter(**{'%s__in' % key: value}): 287 output.append(u'<li>%s</li>' % label_fmt(obj)) 288 output.append(u'</ul>\n') 289 return mark_safe(u'\n'.join(output)) 290 291 191 292 class RelatedFieldWidgetWrapper(forms.Widget): 192 293 """ 193 294 This class is a wrapper to a given widget to add the add icon for the -
docs/ref/contrib/admin/index.txt
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
a b 97 97 class AuthorAdmin(admin.ModelAdmin): 98 98 date_hierarchy = 'pub_date' 99 99 100 .. versionadded:: 1.3 101 102 .. attribute:: ModelAdmin.autocomplete_fields 103 104 By default, Django's admin uses a select-box interface (<select>) for fields 105 that are ``ForeignKey`` or ``ManyToMany``. If you know that your users' 106 browsers will have javascript enabled you can give them autocomplete behavior. 107 108 ``autocomplete_fields`` is a list of fields you would like to change 109 into a smart ``Input`` widget for either a ``ForeignKey`` or ``ManyToManyField``:: 110 111 class BookAdmin(admin.ModelAdmin): 112 autocomplete_fields = { 113 'author': { 'fields': ('name',) }, 114 } 115 116 ``autocomplete_fields`` is a dictionary that connects a ``field_name`` to 117 a dictionary of ``field_options``. The ``field_options`` can have the 118 following keys: 119 120 * ``fields`` 121 A tuple of field names used to search for objects associated with 122 ``field_name``. This key is required. 123 124 Example:: 125 126 'fields': ('name', '^user__email',), 127 128 * ``label`` 129 A formatting string or subroutine that controls how each choice 130 is displayed in the list of autocomplete choices. 131 132 Example:: 133 134 'label': 'name' 135 'label': '%(name)s [%(gender)s]' 136 'label': lambda o: o.name.lower() 137 138 * ``limit`` 139 An integer that limits the size of the autocomplete choices displayed. 140 141 * ``show_search`` 142 A boolean that controls the rendering of a clickable search icon. 143 Defaults to `True`. 144 145 * ``value`` 146 A formatting string or subroutine that controls how a selected item 147 is displayed. 148 149 Same syntax as the ``label`` option. 150 100 151 .. attribute:: ModelAdmin.date_hierarchy 101 152 102 153 Set ``date_hierarchy`` to the name of a ``DateField`` or ``DateTimeField`` -
tests/regressiontests/admin_widgets/models.py
diff --git a/tests/regressiontests/admin_widgets/models.py b/tests/regressiontests/admin_widgets/models.py
a b 9 9 name = models.CharField(max_length=100) 10 10 birthdate = models.DateTimeField(blank=True, null=True) 11 11 gender = models.CharField(max_length=1, blank=True, choices=[('M','Male'), ('F', 'Female')]) 12 user = models.ForeignKey(User, blank=True, null=True) 12 13 13 14 def __unicode__(self): 14 15 return self.name -
tests/regressiontests/admin_widgets/tests.py
diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py
a b 8 8 from django.contrib.admin import widgets 9 9 from django.contrib.admin.widgets import (FilteredSelectMultiple, 10 10 AdminSplitDateTime, AdminFileWidget, ForeignKeyRawIdWidget, AdminRadioSelect, 11 RelatedFieldWidgetWrapper, ManyToManyRawIdWidget) 11 RelatedFieldWidgetWrapper, ManyToManyRawIdWidget, 12 AutocompleteWidget, MultipleAutocompleteWidget) 12 13 from django.core.files.storage import default_storage 13 14 from django.core.files.uploadedfile import SimpleUploadedFile 14 15 from django.db.models import DateField 15 16 from django.test import TestCase as DjangoTestCase 17 from django.utils import simplejson 16 18 from django.utils.html import conditional_escape 17 19 from django.utils.translation import activate, deactivate 18 20 from django.utils.unittest import TestCase … … 323 325 # Used to fail with a name error. 324 326 w = RelatedFieldWidgetWrapper(w, rel, admin.site) 325 327 self.assertFalse(w.can_add_related) 328 329 330 class AutocompleteWidgetTest(DjangoTestCase): 331 def test_render(self): 332 band = models.Band.objects.create(name='Linkin Park') 333 band.album_set.create( 334 name='Hybrid Theory', cover_art=r'albums\hybrid_theory.jpg' 335 ) 336 337 widget_settings = { 338 'fields': ('name',), 339 'id': 'id', 340 'limit': 5, 341 'value': lambda o: unicode(o), 342 'label': lambda o: unicode(o), 343 'queryset': models.Band.objects.all(), 344 } 345 w = AutocompleteWidget(widget_settings) 346 field_name = 'test' 347 expected = "\n".join(( 348 '<div class="djangoautocomplete-wrapper">', 349 '<input type="hidden" name="test" value="%(band_pk)s" id="id_hidden_%(field_name)s" />', 350 '<input type="text" value="%(band_name)s" />', 351 '</div>', 352 '<script type="text/javascript">django.jQuery("#id_%(field_name)s").djangoautocomplete({"force_selection": true, "multiple": %(multiple)s, "source": "../autocomplete/%(field_name)s/"});</script>', 353 '' 354 )) % {'field_name': field_name, 355 'band_pk': band.pk, 356 'band_name': band.name, 357 'multiple': 'false', 358 } 359 self.assertEqual( 360 conditional_escape(w.render('test', band.pk, attrs={})), 361 expected, 362 ) 363 364 365 class MultipleAutocompleteWidgetTest(DjangoTestCase): 366 fixtures = ["admin-widgets-users.xml"] 367 admin_root = '/widget_admin' 368 369 def setUp(self): 370 band = models.Band.objects.create(name='Linkin Park') 371 band.album_set.create( 372 name='Hybrid Theory', cover_art=r'albums\hybrid_theory.jpg' 373 ) 374 band2 = models.Band.objects.create(name='Johnny Cash') 375 band2.album_set.create( 376 name='At San Quentin' 377 ) 378 self.bands = (band, band2) 379 380 def _get_expected(self, name, bands, show_search=False): 381 bands = [] if bands is None else bands 382 expected = [ 383 '<div class="djangoautocomplete-wrapper">', 384 '<input type="hidden" name="%(field_name)s" value="%(band_pks)s" id="id_hidden_%(field_name)s" />', 385 '<input type="text" value="" />', # input is for data entry only 386 ] 387 if bands: 388 expected += ['<ul class="ui-autocomplete-values">'] 389 expected += ['<li>%s</li>' % b.name for b in bands] 390 expected += ['</ul>'] 391 expected += ['</div>'] 392 if show_search: 393 expected += ['<a href="../../../admin_widgets/band/?t=id" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a>' % (name, settings.ADMIN_MEDIA_PREFIX)] 394 expected += [ 395 '<script type="text/javascript">django.jQuery("#id_%(field_name)s").djangoautocomplete({"force_selection": true, "multiple": %(multiple)s, "source": "../autocomplete/%(field_name)s/"});</script>', 396 '' 397 ] 398 expected = "\n".join(expected) % { 399 'field_name': name, 400 'band_pks': ','.join([str(b.pk) for b in bands]), 401 'multiple': 'true', 402 } 403 return expected 404 405 def test_render(self): 406 bands = self.bands 407 408 widget_settings = { 409 'fields': ('name',), 410 'id': 'id', 411 'limit': 5, 412 'value': lambda o: unicode(o), 413 'label': lambda o: unicode(o), 414 'queryset': models.Band.objects.all(), 415 'show_search': False, 416 } 417 with_search_settings = widget_settings.copy() 418 with_search_settings['show_search'] = True 419 420 w = MultipleAutocompleteWidget(widget_settings) 421 expected_multiple = self._get_expected('test_multiple', bands) 422 self.assertEqual( 423 conditional_escape(w.render('test_multiple', [b.pk for b in bands], attrs={})), 424 expected_multiple, 425 ) 426 427 w = MultipleAutocompleteWidget(widget_settings) 428 bands_just_one = bands[:1] 429 expected_single = self._get_expected('test_single', bands_just_one) 430 self.assertEqual( 431 conditional_escape(w.render('test_single', [b.pk for b in bands_just_one], attrs={})), 432 expected_single, 433 ) 434 435 w = MultipleAutocompleteWidget(widget_settings) 436 bands_none = None 437 expected_none = self._get_expected('test_none', bands_none) 438 self.assertEqual( 439 conditional_escape(w.render('test_none', bands_none, attrs={})), 440 expected_none, 441 ) 442 443 w = MultipleAutocompleteWidget(with_search_settings) 444 bands_none = None 445 expected_none = self._get_expected('test_none', bands_none, show_search=True) 446 self.assertEqual( 447 conditional_escape(w.render('test_none', bands_none, attrs={})), 448 expected_none, 449 ) 450 451 452 def test_lookup(self): 453 band = self.bands[1] 454 self.client.login(username="super", password="secret") 455 response = self.client.get('%s/admin_widgets/album/autocomplete/band/?term=johnny' % self.admin_root ) 456 self.assertEqual(simplejson.loads(response.content), 457 [{'id': band.id, 458 'value': band.name, 459 'label': band.name}]) 460 response = self.client.get('%s/admin_widgets/album/autocomplete/band/?term=a' % self.admin_root ) 461 self.assertEqual(simplejson.loads(response.content), 462 [{'id': band.id, 463 'value': band.name, 464 'label': band.name} for band in self.bands]) 465 response = self.client.get('%s/admin_widgets/album/autocomplete/band/?term=%s&by_id=1' % (self.admin_root, band.id )) 466 self.assertEqual(simplejson.loads(response.content), 467 [{'id': band.id, 468 'value': band.name, 469 'label': band.name}]) 470 471 472 def test_different_id(self): 473 self.client.login(username="super", password="secret") 474 user = models.User.objects.get(username='testser') 475 user.member_set.create( 476 name='Man Named Sue', 477 user=user, 478 ) 479 expected = [{'id': user.email, 480 'value': user.username, 481 'label': user.username}] 482 lookup_url = "%s/admin_widgets/member/autocomplete/user/?term=%%s" % self.admin_root 483 lookup_by_id_url = "%s&by_id=1" % lookup_url 484 485 response = self.client.get(lookup_url % user.username[:2]) 486 self.assertEqual(simplejson.loads(response.content), 487 expected) 488 response = self.client.get(lookup_url % user.email[:2]) 489 self.assertEqual(simplejson.loads(response.content), 490 expected) 491 response = self.client.get(lookup_url % user.first_name[:2]) 492 self.assertEqual(simplejson.loads(response.content), 493 expected) 494 response = self.client.get(lookup_by_id_url % user.email) 495 self.assertEqual(simplejson.loads(response.content), 496 expected) 497 -
tests/regressiontests/admin_widgets/widgetadmin.py
diff --git a/tests/regressiontests/admin_widgets/widgetadmin.py b/tests/regressiontests/admin_widgets/widgetadmin.py
a b 22 22 class EventAdmin(admin.ModelAdmin): 23 23 raw_id_fields = ['band'] 24 24 25 26 class AlbumAdmin(admin.ModelAdmin): 27 autocomplete_fields = { 28 'band': { 'fields': ('name',) } 29 } 30 31 class MemberAdmin(admin.ModelAdmin): 32 autocomplete_fields = { 33 'user': { 34 'fields': ('^email', '^username', '^first_name', ), 35 'id': 'email', # weird id for test_different_id 36 } 37 } 38 39 25 40 site = WidgetAdmin(name='widget-admin') 26 41 27 42 site.register(models.User) 43 site.register(models.Album, AlbumAdmin) 28 44 site.register(models.Car, CarAdmin) 29 45 site.register(models.CarTire, CarTireAdmin) 30 46 site.register(models.Event, EventAdmin) 47 site.register(models.Member, MemberAdmin)