├── __init__.py
├── autocomplete
├── __init__.py
├── static
│ ├── css
│ │ ├── jquery.autocomplete.css
│ │ └── django-autocomplete.css
│ └── js
│ │ ├── jquery.bgiframe.min.js
│ │ ├── jquery.ajaxQueue.js
│ │ ├── jquery.autocomplete.js
│ │ └── jquery.js
├── templates
│ └── admin
│ │ └── autocomplete
│ │ ├── fk_widget.html
│ │ ├── nolookups_fk_widget.html
│ │ └── inline_widget.html
├── admin.py
└── geoadmin.py
├── .gitignore
├── setup.py
└── README.md
/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/autocomplete/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.pyo
3 | *local_settings.py
4 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from distutils.core import setup
3 |
4 | setup(name='autocomplete',
5 | version='0.2',
6 | description='The Washington Post\'s app for creating admin foreign key autocompletion fields.',
7 | author='Jeremy Bowers',
8 | author_email='jeremyjbowers@gmail.com',
9 | url='https://github.com/jeremyjbowers/django-autocomplete',
10 | packages = ['autocomplete',],
11 | license = 'MIT',
12 | package_data = {'': [
13 | 'static/autocomplete/js/*.js',
14 | 'static/autocomplete/css/*.css',
15 | 'templates/admin/autocomplete/*.html',
16 | ]},
17 | classifiers=[
18 | 'Environment :: Web Environment',
19 | 'Framework :: Django',
20 | 'Intended Audience :: Developers',
21 | 'Operating System :: OS Independent',
22 | 'Programming Language :: Python',
23 | 'Topic :: Utilities'
24 | ],
25 | )
26 |
--------------------------------------------------------------------------------
/autocomplete/static/css/jquery.autocomplete.css:
--------------------------------------------------------------------------------
1 | /*****************************************************************************
2 | * jQuery autocomplete
3 | ****************************************************************************/
4 | .ac_results {
5 | padding: 0px;
6 | border: 1px solid #ccc;
7 | background-color: #fff;
8 | overflow: hidden;
9 | z-index: 99999;
10 | text-align: left;
11 | }
12 |
13 | .ac_results ul {
14 | width: 100%;
15 | list-style-position: outside;
16 | list-style: none;
17 | padding: 0;
18 | margin: 0;
19 | }
20 |
21 | .ac_results li {
22 | margin: 0px;
23 | padding: 3px 5px;
24 | cursor: default;
25 | display: block;
26 | font: menu;
27 | font-size: 12px;
28 | line-height: 14px;
29 | overflow: hidden;
30 | }
31 |
32 | .ac_loading {
33 | background: white url('../img/indicator.gif') right center no-repeat;
34 | }
35 |
36 | .ac_odd {
37 | background-color: #eee;
38 | }
39 |
40 | .ac_over {
41 | background-color: #999;
42 | color: white;
43 | }
44 |
--------------------------------------------------------------------------------
/autocomplete/static/css/django-autocomplete.css:
--------------------------------------------------------------------------------
1 | /*****************************************************************************
2 | * jQuery autocomplete
3 | ****************************************************************************/
4 | .vForeignKeyRawIdHiddenAdminField {display:none;}
5 | .ac_results {
6 | padding: 0px;
7 | border: 1px solid #ccc;
8 | background-color: #fff!important;
9 | overflow: hidden;
10 | z-index: 99999;
11 | text-align: left;
12 | }
13 |
14 | .ac_results ul {
15 | width: 100%;
16 | list-style-position: outside;
17 | list-style: none;
18 | padding: 0;
19 | margin: 0;
20 | }
21 |
22 | .ac_results li {
23 | margin: 0px;
24 | padding: 3px 5px;
25 | cursor: default;
26 | display: block;
27 | font: menu;
28 | font-size: 12px;
29 | line-height: 14px;
30 | overflow: hidden;
31 | }
32 |
33 | .ac_loading {
34 | background: white url('../img/indicator.gif') right center no-repeat;
35 | }
36 |
37 | .ac_odd {
38 | background-color: #eee;
39 | }
40 |
41 | .ac_over {
42 | background-color: #999;
43 | color: white;
44 | }
45 |
--------------------------------------------------------------------------------
/autocomplete/static/js/jquery.bgiframe.min.js:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2006 Brandon Aaron (http://brandonaaron.net)
2 | * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
3 | * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
4 | *
5 | * $LastChangedDate: 2007-07-22 01:45:56 +0200 (Son, 22 Jul 2007) $
6 | * $Rev: 2447 $
7 | *
8 | * Version 2.1.1
9 | */
10 | (function($){$.fn.bgIframe=$.fn.bgiframe=function(s){if($.browser.msie&&/6.0/.test(navigator.userAgent)){s=$.extend({top:'auto',left:'auto',width:'auto',height:'auto',opacity:true,src:'javascript:false;'},s||{});var prop=function(n){return n&&n.constructor==Number?n+'px':n;},html='';return this.each(function(){if($('> iframe.bgiframe',this).length==0)this.insertBefore(document.createElement(html),this.firstChild);});}return this;};})(jQuery);
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | wapo-django-autocomplete
2 | ========================
3 | wapo-django-autocomplete is an installable app which overrides the stock adminraw ID widget with an autocomplete search box. It has two interesting features over competing apps:
4 |
5 | * It limits/filters the search query that the autocomplete logic performs when matching the keyword. This limit/filter logic is on a per-field basis.
6 | * It works with inlines.
7 |
8 | Installation
9 | -------
10 | * The autocomplete templates need to be located by Django, so it's recommended that you add it to your installed apps.
11 | * Install from github using pip:
12 |
13 |
pip install -e git+git@github.com:jeremyjbowers/django-autocomplete.git#egg=autocomplete
14 |
15 | Code examples
16 | -----
17 | Here's an example of how you would use wapo-django-autocomplete to create an autocomplete field on a foreign key field.
18 |
19 | **models.py:**
20 |
21 | class Bar(models.Model):
22 | name = models.CharField(max_length=255)
23 | ...
24 |
25 | class Baz(models.Model):
26 | name = models.CharField(max_length=255)
27 | bar = models.ForeignKey(Bar)
28 | ...
29 |
30 | class Foo(models.Model):
31 | bar = models.ForeignKey(Bar)
32 | baz = models.ForeignKey(Baz)
33 | ...
34 |
35 | **admin.py**
36 |
37 | class FooAdmin(NoLookupsForeignKeyAutocompleteAdmin):
38 | model = Foo
39 | related_search_fields = {
40 | 'bar': ('name',), #search against the name field in the FK bar.
41 | 'baz': ('bar__name',), #traverse a second foreign key to search against the name of bar.
42 | }
43 |
44 | Todos
45 | -----
46 | * Test to make sure templates are properly installing.
47 | * Identify ways to customize returned items in the widget, e.g., limit to 10, alphabetize, etc.
--------------------------------------------------------------------------------
/autocomplete/templates/admin/autocomplete/fk_widget.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/autocomplete/templates/admin/autocomplete/nolookups_fk_widget.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 | {# #}
4 | {#
#}
5 | {# #}
6 |
--------------------------------------------------------------------------------
/autocomplete/templates/admin/autocomplete/inline_widget.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load extrafilters %}
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/autocomplete/static/js/jquery.ajaxQueue.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Ajax Queue Plugin
3 | *
4 | * Homepage: http://jquery.com/plugins/project/ajaxqueue
5 | * Documentation: http://docs.jquery.com/AjaxQueue
6 | */
7 |
8 | /**
9 |
10 |
30 |
31 |
32 | */
33 | /*
34 | * Queued Ajax requests.
35 | * A new Ajax request won't be started until the previous queued
36 | * request has finished.
37 | */
38 |
39 | /*
40 | * Synced Ajax requests.
41 | * The Ajax request will happen as soon as you call this method, but
42 | * the callbacks (success/error/complete) won't fire until all previous
43 | * synced requests have been completed.
44 | */
45 |
46 |
47 | (function($) {
48 |
49 | var ajax = $.ajax;
50 |
51 | var pendingRequests = {};
52 |
53 | var synced = [];
54 | var syncedData = [];
55 |
56 | $.ajax = function(settings) {
57 | // create settings for compatibility with ajaxSetup
58 | settings = jQuery.extend(settings, jQuery.extend({}, jQuery.ajaxSettings, settings));
59 |
60 | var port = settings.port;
61 |
62 | switch(settings.mode) {
63 | case "abort":
64 | if ( pendingRequests[port] ) {
65 | pendingRequests[port].abort();
66 | }
67 | return pendingRequests[port] = ajax.apply(this, arguments);
68 | case "queue":
69 | var _old = settings.complete;
70 | settings.complete = function(){
71 | if ( _old )
72 | _old.apply( this, arguments );
73 | jQuery([ajax]).dequeue("ajax" + port );;
74 | };
75 |
76 | jQuery([ ajax ]).queue("ajax" + port, function(){
77 | ajax( settings );
78 | });
79 | return;
80 | case "sync":
81 | var pos = synced.length;
82 |
83 | synced[ pos ] = {
84 | error: settings.error,
85 | success: settings.success,
86 | complete: settings.complete,
87 | done: false
88 | };
89 |
90 | syncedData[ pos ] = {
91 | error: [],
92 | success: [],
93 | complete: []
94 | };
95 |
96 | settings.error = function(){ syncedData[ pos ].error = arguments; };
97 | settings.success = function(){ syncedData[ pos ].success = arguments; };
98 | settings.complete = function(){
99 | syncedData[ pos ].complete = arguments;
100 | synced[ pos ].done = true;
101 |
102 | if ( pos == 0 || !synced[ pos-1 ] )
103 | for ( var i = pos; i < synced.length && synced[i].done; i++ ) {
104 | if ( synced[i].error ) synced[i].error.apply( jQuery, syncedData[i].error );
105 | if ( synced[i].success ) synced[i].success.apply( jQuery, syncedData[i].success );
106 | if ( synced[i].complete ) synced[i].complete.apply( jQuery, syncedData[i].complete );
107 |
108 | synced[i] = null;
109 | syncedData[i] = null;
110 | }
111 | };
112 | }
113 | return ajax.apply(this, arguments);
114 | };
115 |
116 | })(jQuery);
--------------------------------------------------------------------------------
/autocomplete/admin.py:
--------------------------------------------------------------------------------
1 | import string, operator
2 | from django import forms
3 | from django.conf import settings
4 | from django.conf.urls.defaults import *
5 | from django.contrib import admin
6 | from django.contrib.admin.widgets import ForeignKeyRawIdWidget
7 | from django.db import models
8 | from django.db.models.query import QuerySet
9 | from django.http import HttpResponse, HttpResponseNotFound
10 | from django.template.loader import render_to_string
11 | from django.utils.encoding import smart_str
12 | from django.utils.safestring import mark_safe
13 | from django.utils.text import get_text_list, truncate_words
14 | from django.utils.translation import ugettext as _
15 |
16 |
17 | js_tuple = (
18 | 'js/jquery.js',
19 | 'js/jquery.bgiframe.min.js',
20 | 'js/jquery.ajaxQueue.js',
21 | 'js/jquery.autocomplete.js'
22 | )
23 |
24 | css_dict = {
25 | 'all': ('css/django-autocomplete.css',)
26 | }
27 |
28 | class BaseAutocompleteWidget(ForeignKeyRawIdWidget):
29 | widget_template = None
30 | search_path = '../foreignkey_autocomplete/'
31 |
32 | class Media:
33 | css = css_dict
34 | js = js_tuple
35 | abstract = True
36 |
37 | def label_for_value(self, value):
38 | key = self.rel.get_related_field().name
39 | obj = self.rel.to._default_manager.get(**{key: value})
40 | return truncate_words(obj, 14)
41 |
42 |
43 | class ForeignKeySearchWidget(BaseAutocompleteWidget):
44 | def __init__(self, rel, search_fields, attrs=None):
45 | self.search_fields = search_fields
46 | super(ForeignKeySearchWidget, self).__init__(rel, attrs)
47 |
48 | def render(self, name, value, attrs=None):
49 | if attrs is None:
50 | attrs = {}
51 | opts = self.rel.to._meta
52 | app_label = opts.app_label
53 | model_name = opts.object_name.lower()
54 | related_url = '../../../%s/%s/' % (app_label, model_name)
55 | params = self.url_parameters()
56 | if params:
57 | url = '?' + '&'.join(['%s=%s' % (k, v) for k, v in params.items()])
58 | else:
59 | url = ''
60 | if not attrs.has_key('class'):
61 | attrs['class'] = 'vForeignKeyRawIdHiddenAdminField'
62 | output = [forms.TextInput.render(self, name, value, attrs)]
63 | if value:
64 | label = self.label_for_value(value)
65 | else:
66 | label = u''
67 | context = {
68 | 'url': url,
69 | 'related_url': related_url,
70 | 'admin_media_prefix': settings.ADMIN_MEDIA_PREFIX,
71 | 'search_path': self.search_path,
72 | 'search_fields': ','.join(self.search_fields),
73 | 'model_name': model_name,
74 | 'app_label': app_label,
75 | 'label': label,
76 | 'name': name,
77 | }
78 | output.append(render_to_string(self.widget_template or (
79 | '%s/%s/%s' % (app_label, model_name, 'fk_widget.html'),
80 | '%s/%s' % (app_label, 'fk_widget.html'),
81 | 'admin/autocomplete/%s' % 'fk_widget.html',
82 | ), context))
83 | output.reverse()
84 | return mark_safe(u''.join(output))
85 |
86 | class NoLookupsForeignKeySearchWidget(BaseAutocompleteWidget):
87 | def __init__(self, rel, search_fields, attrs=None):
88 | self.search_fields = search_fields
89 | super(NoLookupsForeignKeySearchWidget, self).__init__(rel, attrs)
90 |
91 | def render(self, name, value, attrs=None):
92 | if attrs is None:
93 | attrs = {}
94 | opts = self.rel.to._meta
95 | app_label = opts.app_label
96 | model_name = opts.object_name.lower()
97 | related_url = '../../../%s/%s/' % (app_label, model_name)
98 | params = self.url_parameters()
99 | if params:
100 | url = '?' + '&'.join(['%s=%s' % (k, v) for k, v in params.items()])
101 | else:
102 | url = ''
103 | if not attrs.has_key('class'):
104 | attrs['class'] = 'vForeignKeyRawIdHiddenAdminField'
105 | output = [forms.TextInput.render(self, name, value, attrs)]
106 | if value:
107 | label = self.label_for_value(value)
108 | else:
109 | label = u''
110 | context = {
111 | 'url': url,
112 | 'related_url': related_url,
113 | 'admin_media_prefix': settings.ADMIN_MEDIA_PREFIX,
114 | 'search_path': self.search_path,
115 | 'search_fields': ','.join(self.search_fields),
116 | 'model_name': model_name,
117 | 'app_label': app_label,
118 | 'label': label,
119 | 'name': name,
120 | }
121 | output.append(render_to_string(self.widget_template or (
122 | '%s/%s/%s' % (app_label, model_name, 'nolookups_fk_widget.html'),
123 | '%s/%s' % (app_label, 'nolookups_fk_widget.html'),
124 | 'admin/autocomplete/%s' % 'nolookups_fk_widget.html',
125 | ), context))
126 | output.reverse()
127 | return mark_safe(u''.join(output))
128 |
129 | class InlineForeignKeySearchWidget(BaseAutocompleteWidget):
130 | def __init__(self, rel, search_fields, attrs=None):
131 | self.search_fields = search_fields
132 | super(InlineForeignKeySearchWidget, self).__init__(rel, attrs)
133 |
134 | def render(self, name, value, attrs=None):
135 | if attrs is None:
136 | attrs = {}
137 | opts = self.rel.to._meta
138 | app_label = opts.app_label
139 | model_name = opts.object_name.lower()
140 | related_url = '../../../%s/%s/' % (app_label, model_name)
141 | params = self.url_parameters()
142 | if params:
143 | url = '?' + '&'.join(['%s=%s' % (k, v) for k, v in params.items()])
144 | else:
145 | url = ''
146 | if not attrs.has_key('class'):
147 | attrs['class'] = 'vForeignKeyRawIdHiddenAdminField'
148 | output = [forms.TextInput.render(self, name, value, attrs)]
149 | if value:
150 | label = self.label_for_value(value)
151 | else:
152 | label = u''
153 | context = {
154 | 'url': url,
155 | 'related_url': related_url,
156 | 'admin_media_prefix': settings.ADMIN_MEDIA_PREFIX,
157 | 'search_path': self.search_path,
158 | 'search_fields': ','.join(self.search_fields),
159 | 'model_name': model_name,
160 | 'app_label': app_label,
161 | 'label': label,
162 | 'name': name,
163 | }
164 | output.append(render_to_string(self.widget_template or (
165 | '%s/%s/%s' % (app_label, model_name, 'inline_widget.html'),
166 | '%s/%s' % (app_label, 'inline_widget.html'),
167 | 'admin/autocomplete/%s' % 'inline_widget.html',
168 | ), context))
169 | output.reverse()
170 | return mark_safe(u''.join(output))
171 |
172 | class BaseAutocompleteAdminMixin(object):
173 | related_search_fields = {}
174 | related_string_functions = {}
175 | related_search_filters = {}
176 |
177 | class Meta:
178 | abstract = True
179 |
180 | def foreignkey_autocomplete(self, request):
181 |
182 | def _restrict_queryset(queryset, search_fields):
183 | for bit in search_fields.split(','):
184 | if bit[0] == '#':
185 | key, val = bit[1:].split('=')
186 | queryset = queryset.filter(**{key: val})
187 | return queryset
188 |
189 | query = request.GET.get('q', None)
190 | app_label = request.GET.get('app_label', None)
191 | model_name = request.GET.get('model_name', None)
192 | search_fields = request.GET.get('search_fields', None)
193 | object_pk = request.GET.get('object_pk', None)
194 | try:
195 | to_string_function = self.related_string_functions[model_name]
196 | except KeyError:
197 | to_string_function = lambda x: x.__unicode__()
198 | if search_fields and app_label and model_name and (query or object_pk):
199 | def construct_search(field_name):
200 | if field_name.startswith('^'):
201 | return "%s__istartswith" % field_name[1:]
202 | elif field_name.startswith('='):
203 | return "%s__iexact" % field_name[1:]
204 | elif field_name.startswith('@'):
205 | return "%s__search" % field_name[1:]
206 | else:
207 | return "%s__icontains" % field_name
208 |
209 | model = models.get_model(app_label, model_name)
210 | queryset = model._default_manager.all()
211 | data = ''
212 | if query:
213 | for bit in query.split():
214 | or_queries = []
215 | for field_name in search_fields.split(','):
216 | if field_name[0] == "#":
217 | continue
218 | or_queries.append(
219 | models.Q(**{construct_search(smart_str(field_name)): smart_str(bit)}))
220 | other_qs = QuerySet(model)
221 | other_qs.dup_select_related(queryset)
222 | other_qs = other_qs.filter(reduce(operator.or_, or_queries))
223 | queryset = queryset & other_qs
224 | queryset = _restrict_queryset(queryset, search_fields)
225 | data = ''.join([u'%s|%s\n' % (
226 | to_string_function(f), f.pk) for f in queryset])
227 | elif object_pk:
228 | try:
229 | obj = queryset.get(pk=object_pk)
230 | except:
231 | pass
232 | else:
233 | data = to_string_function(obj)
234 | return HttpResponse(data)
235 | return HttpResponseNotFound()
236 |
237 | def get_help_text(self, field_name, model_name):
238 | searchable_fields = self.related_search_fields.get(field_name, None)
239 | if searchable_fields:
240 | help_kwargs = {
241 | 'model_name': model_name,
242 | 'field_list': get_text_list(searchable_fields, _('and')),
243 | }
244 | return _('Use the left field to do %(model_name)s lookups in the fields %(field_list)s.') % help_kwargs
245 | return ''
246 |
247 |
248 | class ForeignKeyAutocompleteAdmin(BaseAutocompleteAdminMixin, admin.ModelAdmin):
249 | def formfield_for_dbfield(self, db_field, **kwargs):
250 | if (isinstance(db_field, models.ForeignKey) and
251 | db_field.name in self.related_search_fields):
252 | model_name = db_field.rel.to._meta.object_name
253 | # help_text = self.get_help_text(db_field.name, model_name)
254 | if kwargs.get('help_text'):
255 | help_text = u'%s %s' % (kwargs['help_text'], help_text)
256 | kwargs['widget'] = ForeignKeySearchWidget(db_field.rel, self.related_search_fields[db_field.name])
257 | # kwargs['help_text'] = help_text
258 | return super(ForeignKeyAutocompleteAdmin, self).formfield_for_dbfield(db_field, **kwargs)
259 |
260 | def get_urls(self):
261 | urls = super(ForeignKeyAutocompleteAdmin, self).get_urls()
262 | search_url = patterns('',
263 | (r'^foreignkey_autocomplete/$', self.admin_site.admin_view(self.foreignkey_autocomplete))
264 | )
265 | return search_url + urls
266 |
267 | class NoLookupsForeignKeyAutocompleteAdmin(BaseAutocompleteAdminMixin, admin.ModelAdmin):
268 | def formfield_for_dbfield(self, db_field, **kwargs):
269 | if (isinstance(db_field, models.ForeignKey) and
270 | db_field.name in self.related_search_fields):
271 | model_name = db_field.rel.to._meta.object_name
272 | # help_text = self.get_help_text(db_field.name, model_name)
273 | if kwargs.get('help_text'):
274 | help_text = u'%s %s' % (kwargs['help_text'], help_text)
275 | kwargs['widget'] = NoLookupsForeignKeySearchWidget(db_field.rel, self.related_search_fields[db_field.name])
276 | # kwargs['help_text'] = help_text
277 | return super(NoLookupsForeignKeyAutocompleteAdmin, self).formfield_for_dbfield(db_field, **kwargs)
278 |
279 | def get_urls(self):
280 | urls = super(NoLookupsForeignKeyAutocompleteAdmin, self).get_urls()
281 | search_url = patterns('',
282 | (r'^foreignkey_autocomplete/$', self.admin_site.admin_view(self.foreignkey_autocomplete))
283 | )
284 | return search_url + urls
285 |
286 | class InlineAutocompleteAdmin(BaseAutocompleteAdminMixin, admin.TabularInline):
287 | def formfield_for_dbfield(self, db_field, **kwargs):
288 | if (isinstance(db_field, models.ForeignKey) and
289 | db_field.name in self.related_search_fields):
290 | model_name = db_field.rel.to._meta.object_name
291 | # help_text = self.get_help_text(db_field.name, model_name)
292 | if kwargs.get('help_text'):
293 | help_text = u'%s %s' % (kwargs['help_text'], help_text)
294 | kwargs['widget'] = InlineForeignKeySearchWidget(db_field.rel, self.related_search_fields[db_field.name])
295 | # kwargs['help_text'] = help_text
296 | return super(InlineAutocompleteAdmin, self).formfield_for_dbfield(db_field, **kwargs)
297 |
298 | def get_urls(self):
299 | urls = super(InlineAutocompleteAdmin, self).get_urls()
300 | search_url = patterns('',
301 | (r'^foreignkey_autocomplete/$', self.admin_site.admin_view(self.foreignkey_autocomplete))
302 | )
303 | return search_url + urls
304 |
--------------------------------------------------------------------------------
/autocomplete/geoadmin.py:
--------------------------------------------------------------------------------
1 | import string, operator
2 | from django import forms
3 | from django.conf import settings
4 | from django.conf.urls.defaults import *
5 | from django.contrib.gis import admin
6 | from django.contrib.admin.widgets import ForeignKeyRawIdWidget
7 | from django.db import models
8 | from django.db.models.query import QuerySet
9 | from django.http import HttpResponse, HttpResponseNotFound
10 | from django.template.loader import render_to_string
11 | from django.utils.encoding import smart_str
12 | from django.utils.safestring import mark_safe
13 | from django.utils.text import get_text_list, truncate_words
14 | from django.utils.translation import ugettext as _
15 |
16 |
17 | js_tuple = (
18 | 'js/jquery.js',
19 | 'js/jquery.bgiframe.min.js',
20 | 'js/jquery.ajaxQueue.js',
21 | 'js/jquery.autocomplete.js'
22 | )
23 |
24 | css_dict = {
25 | 'all': ('css/django-autocomplete.css',)
26 | }
27 |
28 | class BaseAutocompleteWidget(ForeignKeyRawIdWidget):
29 | widget_template = None
30 | search_path = '../foreignkey_autocomplete/'
31 |
32 | class Media:
33 | css = css_dict
34 | js = js_tuple
35 | abstract = True
36 |
37 | def label_for_value(self, value):
38 | key = self.rel.get_related_field().name
39 | obj = self.rel.to._default_manager.get(**{key: value})
40 | return truncate_words(obj, 14)
41 |
42 |
43 | class ForeignKeySearchWidget(BaseAutocompleteWidget):
44 | def __init__(self, rel, search_fields, attrs=None):
45 | self.search_fields = search_fields
46 | super(ForeignKeySearchWidget, self).__init__(rel, attrs)
47 |
48 | def render(self, name, value, attrs=None):
49 | if attrs is None:
50 | attrs = {}
51 | opts = self.rel.to._meta
52 | app_label = opts.app_label
53 | model_name = opts.object_name.lower()
54 | related_url = '../../../%s/%s/' % (app_label, model_name)
55 | params = self.url_parameters()
56 | if params:
57 | url = '?' + '&'.join(['%s=%s' % (k, v) for k, v in params.items()])
58 | else:
59 | url = ''
60 | if not attrs.has_key('class'):
61 | attrs['class'] = 'vForeignKeyRawIdHiddenAdminField'
62 | output = [forms.TextInput.render(self, name, value, attrs)]
63 | if value:
64 | label = self.label_for_value(value)
65 | else:
66 | label = u''
67 | context = {
68 | 'url': url,
69 | 'related_url': related_url,
70 | 'admin_media_prefix': settings.ADMIN_MEDIA_PREFIX,
71 | 'search_path': self.search_path,
72 | 'search_fields': ','.join(self.search_fields),
73 | 'model_name': model_name,
74 | 'app_label': app_label,
75 | 'label': label,
76 | 'name': name,
77 | }
78 | output.append(render_to_string(self.widget_template or (
79 | '%s/%s/%s' % (app_label, model_name, 'fk_widget.html'),
80 | '%s/%s' % (app_label, 'fk_widget.html'),
81 | 'admin/autocomplete/%s' % 'fk_widget.html',
82 | ), context))
83 | output.reverse()
84 | return mark_safe(u''.join(output))
85 |
86 | class NoLookupsForeignKeySearchWidget(BaseAutocompleteWidget):
87 | def __init__(self, rel, search_fields, attrs=None):
88 | self.search_fields = search_fields
89 | super(NoLookupsForeignKeySearchWidget, self).__init__(rel, attrs)
90 |
91 | def render(self, name, value, attrs=None):
92 | if attrs is None:
93 | attrs = {}
94 | opts = self.rel.to._meta
95 | app_label = opts.app_label
96 | model_name = opts.object_name.lower()
97 | related_url = '../../../%s/%s/' % (app_label, model_name)
98 | params = self.url_parameters()
99 | if params:
100 | url = '?' + '&'.join(['%s=%s' % (k, v) for k, v in params.items()])
101 | else:
102 | url = ''
103 | if not attrs.has_key('class'):
104 | attrs['class'] = 'vForeignKeyRawIdHiddenAdminField'
105 | output = [forms.TextInput.render(self, name, value, attrs)]
106 | if value:
107 | label = self.label_for_value(value)
108 | else:
109 | label = u''
110 | context = {
111 | 'url': url,
112 | 'related_url': related_url,
113 | 'admin_media_prefix': settings.ADMIN_MEDIA_PREFIX,
114 | 'search_path': self.search_path,
115 | 'search_fields': ','.join(self.search_fields),
116 | 'model_name': model_name,
117 | 'app_label': app_label,
118 | 'label': label,
119 | 'name': name,
120 | }
121 | output.append(render_to_string(self.widget_template or (
122 | '%s/%s/%s' % (app_label, model_name, 'nolookups_fk_widget.html'),
123 | '%s/%s' % (app_label, 'nolookups_fk_widget.html'),
124 | 'admin/autocomplete/%s' % 'nolookups_fk_widget.html',
125 | ), context))
126 | output.reverse()
127 | return mark_safe(u''.join(output))
128 |
129 | class InlineForeignKeySearchWidget(BaseAutocompleteWidget):
130 | def __init__(self, rel, search_fields, attrs=None):
131 | self.search_fields = search_fields
132 | super(InlineForeignKeySearchWidget, self).__init__(rel, attrs)
133 |
134 | def render(self, name, value, attrs=None):
135 | if attrs is None:
136 | attrs = {}
137 | opts = self.rel.to._meta
138 | app_label = opts.app_label
139 | model_name = opts.object_name.lower()
140 | related_url = '../../../%s/%s/' % (app_label, model_name)
141 | params = self.url_parameters()
142 | if params:
143 | url = '?' + '&'.join(['%s=%s' % (k, v) for k, v in params.items()])
144 | else:
145 | url = ''
146 | if not attrs.has_key('class'):
147 | attrs['class'] = 'vForeignKeyRawIdHiddenAdminField'
148 | output = [forms.TextInput.render(self, name, value, attrs)]
149 | if value:
150 | label = self.label_for_value(value)
151 | else:
152 | label = u''
153 | context = {
154 | 'url': url,
155 | 'related_url': related_url,
156 | 'admin_media_prefix': settings.ADMIN_MEDIA_PREFIX,
157 | 'search_path': self.search_path,
158 | 'search_fields': ','.join(self.search_fields),
159 | 'model_name': model_name,
160 | 'app_label': app_label,
161 | 'label': label,
162 | 'name': name,
163 | }
164 | output.append(render_to_string(self.widget_template or (
165 | '%s/%s/%s' % (app_label, model_name, 'inline_widget.html'),
166 | '%s/%s' % (app_label, 'inline_widget.html'),
167 | 'admin/autocomplete/%s' % 'inline_widget.html',
168 | ), context))
169 | output.reverse()
170 | return mark_safe(u''.join(output))
171 |
172 | class BaseAutocompleteAdminMixin(object):
173 | related_search_fields = {}
174 | related_string_functions = {}
175 | related_search_filters = {}
176 |
177 | class Meta:
178 | abstract = True
179 |
180 | def foreignkey_autocomplete(self, request):
181 |
182 | def _restrict_queryset(queryset, search_fields):
183 | for bit in search_fields.split(','):
184 | if bit[0] == '#':
185 | key, val = bit[1:].split('=')
186 | queryset = queryset.filter(**{key: val})
187 | return queryset
188 |
189 | query = request.GET.get('q', None)
190 | app_label = request.GET.get('app_label', None)
191 | model_name = request.GET.get('model_name', None)
192 | search_fields = request.GET.get('search_fields', None)
193 | object_pk = request.GET.get('object_pk', None)
194 | try:
195 | to_string_function = self.related_string_functions[model_name]
196 | except KeyError:
197 | to_string_function = lambda x: x.__unicode__()
198 | if search_fields and app_label and model_name and (query or object_pk):
199 | def construct_search(field_name):
200 | if field_name.startswith('^'):
201 | return "%s__istartswith" % field_name[1:]
202 | elif field_name.startswith('='):
203 | return "%s__iexact" % field_name[1:]
204 | elif field_name.startswith('@'):
205 | return "%s__search" % field_name[1:]
206 | else:
207 | return "%s__icontains" % field_name
208 |
209 | model = models.get_model(app_label, model_name)
210 | queryset = model._default_manager.all()
211 | data = ''
212 | if query:
213 | for bit in query.split():
214 | or_queries = []
215 | for field_name in search_fields.split(','):
216 | if field_name[0] == "#":
217 | continue
218 | or_queries.append(
219 | models.Q(**{construct_search(smart_str(field_name)): smart_str(bit)}))
220 | other_qs = QuerySet(model)
221 | other_qs.dup_select_related(queryset)
222 | other_qs = other_qs.filter(reduce(operator.or_, or_queries))
223 | queryset = queryset & other_qs
224 | queryset = _restrict_queryset(queryset, search_fields)
225 | data = ''.join([u'%s|%s\n' % (
226 | to_string_function(f), f.pk) for f in queryset])
227 | elif object_pk:
228 | try:
229 | obj = queryset.get(pk=object_pk)
230 | except:
231 | pass
232 | else:
233 | data = to_string_function(obj)
234 | return HttpResponse(data)
235 | return HttpResponseNotFound()
236 |
237 | def get_help_text(self, field_name, model_name):
238 | searchable_fields = self.related_search_fields.get(field_name, None)
239 | if searchable_fields:
240 | help_kwargs = {
241 | 'model_name': model_name,
242 | 'field_list': get_text_list(searchable_fields, _('and')),
243 | }
244 | return _('Use the left field to do %(model_name)s lookups in the fields %(field_list)s.') % help_kwargs
245 | return ''
246 |
247 |
248 | class ForeignKeyAutocompleteAdmin(BaseAutocompleteAdminMixin, admin.GeoModelAdmin):
249 | def formfield_for_dbfield(self, db_field, **kwargs):
250 | if (isinstance(db_field, models.ForeignKey) and
251 | db_field.name in self.related_search_fields):
252 | model_name = db_field.rel.to._meta.object_name
253 | # help_text = self.get_help_text(db_field.name, model_name)
254 | if kwargs.get('help_text'):
255 | help_text = u'%s %s' % (kwargs['help_text'], help_text)
256 | kwargs['widget'] = ForeignKeySearchWidget(db_field.rel, self.related_search_fields[db_field.name])
257 | # kwargs['help_text'] = help_text
258 | return super(ForeignKeyAutocompleteAdmin, self).formfield_for_dbfield(db_field, **kwargs)
259 |
260 | def get_urls(self):
261 | urls = super(ForeignKeyAutocompleteAdmin, self).get_urls()
262 | search_url = patterns('',
263 | (r'^foreignkey_autocomplete/$', self.admin_site.admin_view(self.foreignkey_autocomplete))
264 | )
265 | return search_url + urls
266 |
267 | class NoLookupsForeignKeyAutocompleteAdmin(BaseAutocompleteAdminMixin, admin.GeoModelAdmin):
268 | def formfield_for_dbfield(self, db_field, **kwargs):
269 | if (isinstance(db_field, models.ForeignKey) and
270 | db_field.name in self.related_search_fields):
271 | model_name = db_field.rel.to._meta.object_name
272 | # help_text = self.get_help_text(db_field.name, model_name)
273 | if kwargs.get('help_text'):
274 | help_text = u'%s %s' % (kwargs['help_text'], help_text)
275 | kwargs['widget'] = NoLookupsForeignKeySearchWidget(db_field.rel, self.related_search_fields[db_field.name])
276 | # kwargs['help_text'] = help_text
277 | return super(NoLookupsForeignKeyAutocompleteAdmin, self).formfield_for_dbfield(db_field, **kwargs)
278 |
279 | def get_urls(self):
280 | urls = super(NoLookupsForeignKeyAutocompleteAdmin, self).get_urls()
281 | search_url = patterns('',
282 | (r'^foreignkey_autocomplete/$', self.admin_site.admin_view(self.foreignkey_autocomplete))
283 | )
284 | return search_url + urls
285 |
286 | class InlineAutocompleteAdmin(BaseAutocompleteAdminMixin, admin.TabularInline):
287 | def formfield_for_dbfield(self, db_field, **kwargs):
288 | if (isinstance(db_field, models.ForeignKey) and
289 | db_field.name in self.related_search_fields):
290 | model_name = db_field.rel.to._meta.object_name
291 | # help_text = self.get_help_text(db_field.name, model_name)
292 | if kwargs.get('help_text'):
293 | help_text = u'%s %s' % (kwargs['help_text'], help_text)
294 | kwargs['widget'] = InlineForeignKeySearchWidget(db_field.rel, self.related_search_fields[db_field.name])
295 | # kwargs['help_text'] = help_text
296 | return super(InlineAutocompleteAdmin, self).formfield_for_dbfield(db_field, **kwargs)
297 |
298 | def get_urls(self):
299 | urls = super(InlineAutocompleteAdmin, self).get_urls()
300 | search_url = patterns('',
301 | (r'^foreignkey_autocomplete/$', self.admin_site.admin_view(self.foreignkey_autocomplete))
302 | )
303 | return search_url + urls
304 |
--------------------------------------------------------------------------------
/autocomplete/static/js/jquery.autocomplete.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Autocomplete - jQuery plugin 1.0.2
3 | *
4 | * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
5 | *
6 | * Dual licensed under the MIT and GPL licenses:
7 | * http://www.opensource.org/licenses/mit-license.php
8 | * http://www.gnu.org/licenses/gpl.html
9 | *
10 | * Revision: $Id: jquery.autocomplete.js 5747 2008-06-25 18:30:55Z joern.zaefferer $
11 | *
12 | */
13 |
14 | ;(function($) {
15 |
16 | $.fn.extend({
17 | autocomplete: function(urlOrData, options) {
18 | var isUrl = typeof urlOrData == "string";
19 | options = $.extend({}, $.Autocompleter.defaults, {
20 | url: isUrl ? urlOrData : null,
21 | data: isUrl ? null : urlOrData,
22 | delay: isUrl ? $.Autocompleter.defaults.delay : 10,
23 | max: options && !options.scroll ? 10 : 150
24 | }, options);
25 |
26 | // if highlight is set to false, replace it with a do-nothing function
27 | options.highlight = options.highlight || function(value) { return value; };
28 |
29 | // if the formatMatch option is not specified, then use formatItem for backwards compatibility
30 | options.formatMatch = options.formatMatch || options.formatItem;
31 |
32 | return this.each(function() {
33 | new $.Autocompleter(this, options);
34 | });
35 | },
36 | result: function(handler) {
37 | return this.bind("result", handler);
38 | },
39 | search: function(handler) {
40 | return this.trigger("search", [handler]);
41 | },
42 | flushCache: function() {
43 | return this.trigger("flushCache");
44 | },
45 | setOptions: function(options){
46 | return this.trigger("setOptions", [options]);
47 | },
48 | unautocomplete: function() {
49 | return this.trigger("unautocomplete");
50 | }
51 | });
52 |
53 | $.Autocompleter = function(input, options) {
54 |
55 | var KEY = {
56 | UP: 38,
57 | DOWN: 40,
58 | DEL: 46,
59 | TAB: 9,
60 | RETURN: 13,
61 | ESC: 27,
62 | COMMA: 188,
63 | PAGEUP: 33,
64 | PAGEDOWN: 34,
65 | BACKSPACE: 8
66 | };
67 |
68 | // Create $ object for input element
69 | var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
70 |
71 | var timeout;
72 | var previousValue = "";
73 | var cache = $.Autocompleter.Cache(options);
74 | var hasFocus = 0;
75 | var lastKeyPressCode;
76 | var config = {
77 | mouseDownOnSelect: false
78 | };
79 | var select = $.Autocompleter.Select(options, input, selectCurrent, config);
80 |
81 | var blockSubmit;
82 |
83 | // prevent form submit in opera when selecting with return key
84 | $.browser.opera && $(input.form).bind("submit.autocomplete", function() {
85 | if (blockSubmit) {
86 | blockSubmit = false;
87 | return false;
88 | }
89 | });
90 |
91 | // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
92 | $input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
93 | // track last key pressed
94 | lastKeyPressCode = event.keyCode;
95 | switch(event.keyCode) {
96 |
97 | case KEY.UP:
98 | event.preventDefault();
99 | if ( select.visible() ) {
100 | select.prev();
101 | } else {
102 | onChange(0, true);
103 | }
104 | break;
105 |
106 | case KEY.DOWN:
107 | event.preventDefault();
108 | if ( select.visible() ) {
109 | select.next();
110 | } else {
111 | onChange(0, true);
112 | }
113 | break;
114 |
115 | case KEY.PAGEUP:
116 | event.preventDefault();
117 | if ( select.visible() ) {
118 | select.pageUp();
119 | } else {
120 | onChange(0, true);
121 | }
122 | break;
123 |
124 | case KEY.PAGEDOWN:
125 | event.preventDefault();
126 | if ( select.visible() ) {
127 | select.pageDown();
128 | } else {
129 | onChange(0, true);
130 | }
131 | break;
132 |
133 | // matches also semicolon
134 | case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
135 | case KEY.TAB:
136 | case KEY.RETURN:
137 | if( selectCurrent() ) {
138 | // stop default to prevent a form submit, Opera needs special handling
139 | event.preventDefault();
140 | blockSubmit = true;
141 | return false;
142 | }
143 | break;
144 |
145 | case KEY.ESC:
146 | select.hide();
147 | break;
148 |
149 | default:
150 | clearTimeout(timeout);
151 | timeout = setTimeout(onChange, options.delay);
152 | break;
153 | }
154 | }).focus(function(){
155 | // track whether the field has focus, we shouldn't process any
156 | // results if the field no longer has focus
157 | hasFocus++;
158 | }).blur(function() {
159 | hasFocus = 0;
160 | if (!config.mouseDownOnSelect) {
161 | hideResults();
162 | }
163 | }).click(function() {
164 | // show select when clicking in a focused field
165 | if ( hasFocus++ > 1 && !select.visible() ) {
166 | onChange(0, true);
167 | }
168 | }).bind("search", function() {
169 | // TODO why not just specifying both arguments?
170 | var fn = (arguments.length > 1) ? arguments[1] : null;
171 | function findValueCallback(q, data) {
172 | var result;
173 | if( data && data.length ) {
174 | for (var i=0; i < data.length; i++) {
175 | if( data[i].result.toLowerCase() == q.toLowerCase() ) {
176 | result = data[i];
177 | break;
178 | }
179 | }
180 | }
181 | if( typeof fn == "function" ) fn(result);
182 | else $input.trigger("result", result && [result.data, result.value]);
183 | }
184 | $.each(trimWords($input.val()), function(i, value) {
185 | request(value, findValueCallback, findValueCallback);
186 | });
187 | }).bind("flushCache", function() {
188 | cache.flush();
189 | }).bind("setOptions", function() {
190 | $.extend(options, arguments[1]);
191 | // if we've updated the data, repopulate
192 | if ( "data" in arguments[1] )
193 | cache.populate();
194 | }).bind("unautocomplete", function() {
195 | select.unbind();
196 | $input.unbind();
197 | $(input.form).unbind(".autocomplete");
198 | });
199 |
200 |
201 | function selectCurrent() {
202 | var selected = select.selected();
203 | if( !selected )
204 | return false;
205 |
206 | var v = selected.result;
207 | previousValue = v;
208 |
209 | if ( options.multiple ) {
210 | var words = trimWords($input.val());
211 | if ( words.length > 1 ) {
212 | v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
213 | }
214 | v += options.multipleSeparator;
215 | }
216 |
217 | $input.val(v);
218 | hideResultsNow();
219 | $input.trigger("result", [selected.data, selected.value]);
220 | return true;
221 | }
222 |
223 | function onChange(crap, skipPrevCheck) {
224 | if( lastKeyPressCode == KEY.DEL ) {
225 | select.hide();
226 | return;
227 | }
228 |
229 | var currentValue = $input.val();
230 |
231 | if ( !skipPrevCheck && currentValue == previousValue )
232 | return;
233 |
234 | previousValue = currentValue;
235 |
236 | currentValue = lastWord(currentValue);
237 | if ( currentValue.length >= options.minChars) {
238 | $input.addClass(options.loadingClass);
239 | if (!options.matchCase)
240 | currentValue = currentValue.toLowerCase();
241 | request(currentValue, receiveData, hideResultsNow);
242 | } else {
243 | stopLoading();
244 | select.hide();
245 | }
246 | };
247 |
248 | function trimWords(value) {
249 | if ( !value ) {
250 | return [""];
251 | }
252 | var words = value.split( options.multipleSeparator );
253 | var result = [];
254 | $.each(words, function(i, value) {
255 | if ( $.trim(value) )
256 | result[i] = $.trim(value);
257 | });
258 | return result;
259 | }
260 |
261 | function lastWord(value) {
262 | if ( !options.multiple )
263 | return value;
264 | var words = trimWords(value);
265 | return words[words.length - 1];
266 | }
267 |
268 | // fills in the input box w/the first match (assumed to be the best match)
269 | // q: the term entered
270 | // sValue: the first matching result
271 | function autoFill(q, sValue){
272 | // autofill in the complete box w/the first match as long as the user hasn't entered in more data
273 | // if the last user key pressed was backspace, don't autofill
274 | if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
275 | // fill in the value (keep the case the user has typed)
276 | $input.val($input.val() + sValue.substring(lastWord(previousValue).length));
277 | // select the portion of the value not typed by the user (so the next character will erase)
278 | $.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
279 | }
280 | };
281 |
282 | function hideResults() {
283 | clearTimeout(timeout);
284 | timeout = setTimeout(hideResultsNow, 200);
285 | };
286 |
287 | function hideResultsNow() {
288 | var wasVisible = select.visible();
289 | select.hide();
290 | clearTimeout(timeout);
291 | stopLoading();
292 | if (options.mustMatch) {
293 | // call search and run callback
294 | $input.search(
295 | function (result){
296 | // if no value found, clear the input box
297 | if( !result ) {
298 | if (options.multiple) {
299 | var words = trimWords($input.val()).slice(0, -1);
300 | $input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
301 | }
302 | else
303 | $input.val( "" );
304 | }
305 | }
306 | );
307 | }
308 | if (wasVisible)
309 | // position cursor at end of input field
310 | $.Autocompleter.Selection(input, input.value.length, input.value.length);
311 | };
312 |
313 | function receiveData(q, data) {
314 | if ( data && data.length && hasFocus ) {
315 | stopLoading();
316 | select.display(data, q);
317 | autoFill(q, data[0].value);
318 | select.show();
319 | } else {
320 | hideResultsNow();
321 | }
322 | };
323 |
324 | function request(term, success, failure) {
325 | if (!options.matchCase)
326 | term = term.toLowerCase();
327 | var data = cache.load(term);
328 | // recieve the cached data
329 | if (data && data.length) {
330 | success(term, data);
331 | // if an AJAX url has been supplied, try loading the data now
332 | } else if( (typeof options.url == "string") && (options.url.length > 0) ){
333 |
334 | var extraParams = {
335 | timestamp: +new Date()
336 | };
337 | $.each(options.extraParams, function(key, param) {
338 | extraParams[key] = typeof param == "function" ? param() : param;
339 | });
340 |
341 | $.ajax({
342 | // try to leverage ajaxQueue plugin to abort previous requests
343 | mode: "abort",
344 | // limit abortion to this input
345 | port: "autocomplete" + input.name,
346 | dataType: options.dataType,
347 | url: options.url,
348 | data: $.extend({
349 | q: lastWord(term),
350 | limit: options.max
351 | }, extraParams),
352 | success: function(data) {
353 | var parsed = options.parse && options.parse(data) || parse(data);
354 | cache.add(term, parsed);
355 | success(term, parsed);
356 | }
357 | });
358 | } else {
359 | // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
360 | select.emptyList();
361 | failure(term);
362 | }
363 | };
364 |
365 | function parse(data) {
366 | var parsed = [];
367 | var rows = data.split("\n");
368 | for (var i=0; i < rows.length; i++) {
369 | var row = $.trim(rows[i]);
370 | if (row) {
371 | row = row.split("|");
372 | parsed[parsed.length] = {
373 | data: row,
374 | value: row[0],
375 | result: options.formatResult && options.formatResult(row, row[0]) || row[0]
376 | };
377 | }
378 | }
379 | return parsed;
380 | };
381 |
382 | function stopLoading() {
383 | $input.removeClass(options.loadingClass);
384 | };
385 |
386 | };
387 |
388 | $.Autocompleter.defaults = {
389 | inputClass: "ac_input",
390 | resultsClass: "ac_results",
391 | loadingClass: "ac_loading",
392 | minChars: 1,
393 | delay: 400,
394 | matchCase: false,
395 | matchSubset: true,
396 | matchContains: false,
397 | cacheLength: 10,
398 | max: 100,
399 | mustMatch: false,
400 | extraParams: {},
401 | selectFirst: true,
402 | formatItem: function(row) { return row[0]; },
403 | formatMatch: null,
404 | autoFill: false,
405 | width: 0,
406 | multiple: false,
407 | multipleSeparator: ", ",
408 | highlight: function(value, term) {
409 | return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1");
410 | },
411 | scroll: true,
412 | scrollHeight: 180
413 | };
414 |
415 | $.Autocompleter.Cache = function(options) {
416 |
417 | var data = {};
418 | var length = 0;
419 |
420 | function matchSubset(s, sub) {
421 | if (!options.matchCase)
422 | s = s.toLowerCase();
423 | var i = s.indexOf(sub);
424 | if (i == -1) return false;
425 | return i == 0 || options.matchContains;
426 | };
427 |
428 | function add(q, value) {
429 | if (length > options.cacheLength){
430 | flush();
431 | }
432 | if (!data[q]){
433 | length++;
434 | }
435 | data[q] = value;
436 | }
437 |
438 | function populate(){
439 | if( !options.data ) return false;
440 | // track the matches
441 | var stMatchSets = {},
442 | nullData = 0;
443 |
444 | // no url was specified, we need to adjust the cache length to make sure it fits the local data store
445 | if( !options.url ) options.cacheLength = 1;
446 |
447 | // track all options for minChars = 0
448 | stMatchSets[""] = [];
449 |
450 | // loop through the array and create a lookup structure
451 | for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
452 | var rawValue = options.data[i];
453 | // if rawValue is a string, make an array otherwise just reference the array
454 | rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
455 |
456 | var value = options.formatMatch(rawValue, i+1, options.data.length);
457 | if ( value === false )
458 | continue;
459 |
460 | var firstChar = value.charAt(0).toLowerCase();
461 | // if no lookup array for this character exists, look it up now
462 | if( !stMatchSets[firstChar] )
463 | stMatchSets[firstChar] = [];
464 |
465 | // if the match is a string
466 | var row = {
467 | value: value,
468 | data: rawValue,
469 | result: options.formatResult && options.formatResult(rawValue) || value
470 | };
471 |
472 | // push the current match into the set list
473 | stMatchSets[firstChar].push(row);
474 |
475 | // keep track of minChars zero items
476 | if ( nullData++ < options.max ) {
477 | stMatchSets[""].push(row);
478 | }
479 | };
480 |
481 | // add the data items to the cache
482 | $.each(stMatchSets, function(i, value) {
483 | // increase the cache size
484 | options.cacheLength++;
485 | // add to the cache
486 | add(i, value);
487 | });
488 | }
489 |
490 | // populate any existing data
491 | setTimeout(populate, 25);
492 |
493 | function flush(){
494 | data = {};
495 | length = 0;
496 | }
497 |
498 | return {
499 | flush: flush,
500 | add: add,
501 | populate: populate,
502 | load: function(q) {
503 | if (!options.cacheLength || !length)
504 | return null;
505 | /*
506 | * if dealing w/local data and matchContains than we must make sure
507 | * to loop through all the data collections looking for matches
508 | */
509 | if( !options.url && options.matchContains ){
510 | // track all matches
511 | var csub = [];
512 | // loop through all the data grids for matches
513 | for( var k in data ){
514 | // don't search through the stMatchSets[""] (minChars: 0) cache
515 | // this prevents duplicates
516 | if( k.length > 0 ){
517 | var c = data[k];
518 | $.each(c, function(i, x) {
519 | // if we've got a match, add it to the array
520 | if (matchSubset(x.value, q)) {
521 | csub.push(x);
522 | }
523 | });
524 | }
525 | }
526 | return csub;
527 | } else
528 | // if the exact item exists, use it
529 | if (data[q]){
530 | return data[q];
531 | } else
532 | if (options.matchSubset) {
533 | for (var i = q.length - 1; i >= options.minChars; i--) {
534 | var c = data[q.substr(0, i)];
535 | if (c) {
536 | var csub = [];
537 | $.each(c, function(i, x) {
538 | if (matchSubset(x.value, q)) {
539 | csub[csub.length] = x;
540 | }
541 | });
542 | return csub;
543 | }
544 | }
545 | }
546 | return null;
547 | }
548 | };
549 | };
550 |
551 | $.Autocompleter.Select = function (options, input, select, config) {
552 | var CLASSES = {
553 | ACTIVE: "ac_over"
554 | };
555 |
556 | var listItems,
557 | active = -1,
558 | data,
559 | term = "",
560 | needsInit = true,
561 | element,
562 | list;
563 |
564 | // Create results
565 | function init() {
566 | if (!needsInit)
567 | return;
568 | element = $("")
569 | .hide()
570 | .addClass(options.resultsClass)
571 | .css("position", "absolute")
572 | .appendTo(document.body);
573 |
574 | list = $("").appendTo(element).mouseover( function(event) {
575 | if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
576 | active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
577 | $(target(event)).addClass(CLASSES.ACTIVE);
578 | }
579 | }).click(function(event) {
580 | $(target(event)).addClass(CLASSES.ACTIVE);
581 | select();
582 | // TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
583 | input.focus();
584 | return false;
585 | }).mousedown(function() {
586 | config.mouseDownOnSelect = true;
587 | }).mouseup(function() {
588 | config.mouseDownOnSelect = false;
589 | });
590 |
591 | if( options.width > 0 )
592 | element.css("width", options.width);
593 |
594 | needsInit = false;
595 | }
596 |
597 | function target(event) {
598 | var element = event.target;
599 | while(element && element.tagName != "LI")
600 | element = element.parentNode;
601 | // more fun with IE, sometimes event.target is empty, just ignore it then
602 | if(!element)
603 | return [];
604 | return element;
605 | }
606 |
607 | function moveSelect(step) {
608 | listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
609 | movePosition(step);
610 | var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
611 | if(options.scroll) {
612 | var offset = 0;
613 | listItems.slice(0, active).each(function() {
614 | offset += this.offsetHeight;
615 | });
616 | if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
617 | list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
618 | } else if(offset < list.scrollTop()) {
619 | list.scrollTop(offset);
620 | }
621 | }
622 | };
623 |
624 | function movePosition(step) {
625 | active += step;
626 | if (active < 0) {
627 | active = listItems.size() - 1;
628 | } else if (active >= listItems.size()) {
629 | active = 0;
630 | }
631 | }
632 |
633 | function limitNumberOfItems(available) {
634 | return options.max && options.max < available
635 | ? options.max
636 | : available;
637 | }
638 |
639 | function fillList() {
640 | list.empty();
641 | var max = limitNumberOfItems(data.length);
642 | for (var i=0; i < max; i++) {
643 | if (!data[i])
644 | continue;
645 | var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
646 | if ( formatted === false )
647 | continue;
648 | var li = $("").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
649 | $.data(li, "ac_data", data[i]);
650 | }
651 | listItems = list.find("li");
652 | if ( options.selectFirst ) {
653 | listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
654 | active = 0;
655 | }
656 | // apply bgiframe if available
657 | if ( $.fn.bgiframe )
658 | list.bgiframe();
659 | }
660 |
661 | return {
662 | display: function(d, q) {
663 | init();
664 | data = d;
665 | term = q;
666 | fillList();
667 | },
668 | next: function() {
669 | moveSelect(1);
670 | },
671 | prev: function() {
672 | moveSelect(-1);
673 | },
674 | pageUp: function() {
675 | if (active != 0 && active - 8 < 0) {
676 | moveSelect( -active );
677 | } else {
678 | moveSelect(-8);
679 | }
680 | },
681 | pageDown: function() {
682 | if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
683 | moveSelect( listItems.size() - 1 - active );
684 | } else {
685 | moveSelect(8);
686 | }
687 | },
688 | hide: function() {
689 | element && element.hide();
690 | listItems && listItems.removeClass(CLASSES.ACTIVE);
691 | active = -1;
692 | },
693 | visible : function() {
694 | return element && element.is(":visible");
695 | },
696 | current: function() {
697 | return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
698 | },
699 | show: function() {
700 | var offset = $(input).offset();
701 | element.css({
702 | width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
703 | top: offset.top + input.offsetHeight,
704 | left: offset.left
705 | }).show();
706 | if(options.scroll) {
707 | list.scrollTop(0);
708 | list.css({
709 | maxHeight: options.scrollHeight,
710 | overflow: 'auto'
711 | });
712 |
713 | if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
714 | var listHeight = 0;
715 | listItems.each(function() {
716 | listHeight += this.offsetHeight;
717 | });
718 | var scrollbarsVisible = listHeight > options.scrollHeight;
719 | list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
720 | if (!scrollbarsVisible) {
721 | // IE doesn't recalculate width when scrollbar disappears
722 | listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
723 | }
724 | }
725 |
726 | }
727 | },
728 | selected: function() {
729 | var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
730 | return selected && selected.length && $.data(selected[0], "ac_data");
731 | },
732 | emptyList: function (){
733 | list && list.empty();
734 | },
735 | unbind: function() {
736 | element && element.remove();
737 | }
738 | };
739 | };
740 |
741 | $.Autocompleter.Selection = function(field, start, end) {
742 | if( field.createTextRange ){
743 | var selRange = field.createTextRange();
744 | selRange.collapse(true);
745 | selRange.moveStart("character", start);
746 | selRange.moveEnd("character", end);
747 | selRange.select();
748 | } else if( field.setSelectionRange ){
749 | field.setSelectionRange(start, end);
750 | } else {
751 | if( field.selectionStart ){
752 | field.selectionStart = start;
753 | field.selectionEnd = end;
754 | }
755 | }
756 | field.focus();
757 | };
758 |
759 | })(jQuery);
--------------------------------------------------------------------------------
/autocomplete/static/js/jquery.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | /*
3 | * jQuery 1.2.6 - New Wave Javascript
4 | *
5 | * Copyright (c) 2008 John Resig (jquery.com)
6 | * Dual licensed under the MIT (MIT-LICENSE.txt)
7 | * and GPL (GPL-LICENSE.txt) licenses.
8 | *
9 | * $Date: 2008-05-27 21:17:26 +0200 (Di, 27 Mai 2008) $
10 | * $Rev: 5700 $
11 | */
12 |
13 | // Map over jQuery in case of overwrite
14 | var _jQuery = window.jQuery,
15 | // Map over the $ in case of overwrite
16 | _$ = window.$;
17 |
18 | var jQuery = window.jQuery = window.$ = function( selector, context ) {
19 | // The jQuery object is actually just the init constructor 'enhanced'
20 | return new jQuery.fn.init( selector, context );
21 | };
22 |
23 | // A simple way to check for HTML strings or ID strings
24 | // (both of which we optimize for)
25 | var quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/,
26 |
27 | // Is it a simple selector
28 | isSimple = /^.[^:#\[\.]*$/,
29 |
30 | // Will speed up references to undefined, and allows munging its name.
31 | undefined;
32 |
33 | jQuery.fn = jQuery.prototype = {
34 | init: function( selector, context ) {
35 | // Make sure that a selection was provided
36 | selector = selector || document;
37 |
38 | // Handle $(DOMElement)
39 | if ( selector.nodeType ) {
40 | this[0] = selector;
41 | this.length = 1;
42 | return this;
43 | }
44 | // Handle HTML strings
45 | if ( typeof selector == "string" ) {
46 | // Are we dealing with HTML string or an ID?
47 | var match = quickExpr.exec( selector );
48 |
49 | // Verify a match, and that no context was specified for #id
50 | if ( match && (match[1] || !context) ) {
51 |
52 | // HANDLE: $(html) -> $(array)
53 | if ( match[1] )
54 | selector = jQuery.clean( [ match[1] ], context );
55 |
56 | // HANDLE: $("#id")
57 | else {
58 | var elem = document.getElementById( match[3] );
59 |
60 | // Make sure an element was located
61 | if ( elem ){
62 | // Handle the case where IE and Opera return items
63 | // by name instead of ID
64 | if ( elem.id != match[3] )
65 | return jQuery().find( selector );
66 |
67 | // Otherwise, we inject the element directly into the jQuery object
68 | return jQuery( elem );
69 | }
70 | selector = [];
71 | }
72 |
73 | // HANDLE: $(expr, [context])
74 | // (which is just equivalent to: $(content).find(expr)
75 | } else
76 | return jQuery( context ).find( selector );
77 |
78 | // HANDLE: $(function)
79 | // Shortcut for document ready
80 | } else if ( jQuery.isFunction( selector ) )
81 | return jQuery( document )[ jQuery.fn.ready ? "ready" : "load" ]( selector );
82 |
83 | return this.setArray(jQuery.makeArray(selector));
84 | },
85 |
86 | // The current version of jQuery being used
87 | jquery: "1.2.6",
88 |
89 | // The number of elements contained in the matched element set
90 | size: function() {
91 | return this.length;
92 | },
93 |
94 | // The number of elements contained in the matched element set
95 | length: 0,
96 |
97 | // Get the Nth element in the matched element set OR
98 | // Get the whole matched element set as a clean array
99 | get: function( num ) {
100 | return num == undefined ?
101 |
102 | // Return a 'clean' array
103 | jQuery.makeArray( this ) :
104 |
105 | // Return just the object
106 | this[ num ];
107 | },
108 |
109 | // Take an array of elements and push it onto the stack
110 | // (returning the new matched element set)
111 | pushStack: function( elems ) {
112 | // Build a new jQuery matched element set
113 | var ret = jQuery( elems );
114 |
115 | // Add the old object onto the stack (as a reference)
116 | ret.prevObject = this;
117 |
118 | // Return the newly-formed element set
119 | return ret;
120 | },
121 |
122 | // Force the current matched set of elements to become
123 | // the specified array of elements (destroying the stack in the process)
124 | // You should use pushStack() in order to do this, but maintain the stack
125 | setArray: function( elems ) {
126 | // Resetting the length to 0, then using the native Array push
127 | // is a super-fast way to populate an object with array-like properties
128 | this.length = 0;
129 | Array.prototype.push.apply( this, elems );
130 |
131 | return this;
132 | },
133 |
134 | // Execute a callback for every element in the matched set.
135 | // (You can seed the arguments with an array of args, but this is
136 | // only used internally.)
137 | each: function( callback, args ) {
138 | return jQuery.each( this, callback, args );
139 | },
140 |
141 | // Determine the position of an element within
142 | // the matched set of elements
143 | index: function( elem ) {
144 | var ret = -1;
145 |
146 | // Locate the position of the desired element
147 | return jQuery.inArray(
148 | // If it receives a jQuery object, the first element is used
149 | elem && elem.jquery ? elem[0] : elem
150 | , this );
151 | },
152 |
153 | attr: function( name, value, type ) {
154 | var options = name;
155 |
156 | // Look for the case where we're accessing a style value
157 | if ( name.constructor == String )
158 | if ( value === undefined )
159 | return this[0] && jQuery[ type || "attr" ]( this[0], name );
160 |
161 | else {
162 | options = {};
163 | options[ name ] = value;
164 | }
165 |
166 | // Check to see if we're setting style values
167 | return this.each(function(i){
168 | // Set all the styles
169 | for ( name in options )
170 | jQuery.attr(
171 | type ?
172 | this.style :
173 | this,
174 | name, jQuery.prop( this, options[ name ], type, i, name )
175 | );
176 | });
177 | },
178 |
179 | css: function( key, value ) {
180 | // ignore negative width and height values
181 | if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 )
182 | value = undefined;
183 | return this.attr( key, value, "curCSS" );
184 | },
185 |
186 | text: function( text ) {
187 | if ( typeof text != "object" && text != null )
188 | return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );
189 |
190 | var ret = "";
191 |
192 | jQuery.each( text || this, function(){
193 | jQuery.each( this.childNodes, function(){
194 | if ( this.nodeType != 8 )
195 | ret += this.nodeType != 1 ?
196 | this.nodeValue :
197 | jQuery.fn.text( [ this ] );
198 | });
199 | });
200 |
201 | return ret;
202 | },
203 |
204 | wrapAll: function( html ) {
205 | if ( this[0] )
206 | // The elements to wrap the target around
207 | jQuery( html, this[0].ownerDocument )
208 | .clone()
209 | .insertBefore( this[0] )
210 | .map(function(){
211 | var elem = this;
212 |
213 | while ( elem.firstChild )
214 | elem = elem.firstChild;
215 |
216 | return elem;
217 | })
218 | .append(this);
219 |
220 | return this;
221 | },
222 |
223 | wrapInner: function( html ) {
224 | return this.each(function(){
225 | jQuery( this ).contents().wrapAll( html );
226 | });
227 | },
228 |
229 | wrap: function( html ) {
230 | return this.each(function(){
231 | jQuery( this ).wrapAll( html );
232 | });
233 | },
234 |
235 | append: function() {
236 | return this.domManip(arguments, true, false, function(elem){
237 | if (this.nodeType == 1)
238 | this.appendChild( elem );
239 | });
240 | },
241 |
242 | prepend: function() {
243 | return this.domManip(arguments, true, true, function(elem){
244 | if (this.nodeType == 1)
245 | this.insertBefore( elem, this.firstChild );
246 | });
247 | },
248 |
249 | before: function() {
250 | return this.domManip(arguments, false, false, function(elem){
251 | this.parentNode.insertBefore( elem, this );
252 | });
253 | },
254 |
255 | after: function() {
256 | return this.domManip(arguments, false, true, function(elem){
257 | this.parentNode.insertBefore( elem, this.nextSibling );
258 | });
259 | },
260 |
261 | end: function() {
262 | return this.prevObject || jQuery( [] );
263 | },
264 |
265 | find: function( selector ) {
266 | var elems = jQuery.map(this, function(elem){
267 | return jQuery.find( selector, elem );
268 | });
269 |
270 | return this.pushStack( /[^+>] [^+>]/.test( selector ) || selector.indexOf("..") > -1 ?
271 | jQuery.unique( elems ) :
272 | elems );
273 | },
274 |
275 | clone: function( events ) {
276 | // Do the clone
277 | var ret = this.map(function(){
278 | if ( jQuery.browser.msie && !jQuery.isXMLDoc(this) ) {
279 | // IE copies events bound via attachEvent when
280 | // using cloneNode. Calling detachEvent on the
281 | // clone will also remove the events from the orignal
282 | // In order to get around this, we use innerHTML.
283 | // Unfortunately, this means some modifications to
284 | // attributes in IE that are actually only stored
285 | // as properties will not be copied (such as the
286 | // the name attribute on an input).
287 | var clone = this.cloneNode(true),
288 | container = document.createElement("div");
289 | container.appendChild(clone);
290 | return jQuery.clean([container.innerHTML])[0];
291 | } else
292 | return this.cloneNode(true);
293 | });
294 |
295 | // Need to set the expando to null on the cloned set if it exists
296 | // removeData doesn't work here, IE removes it from the original as well
297 | // this is primarily for IE but the data expando shouldn't be copied over in any browser
298 | var clone = ret.find("*").andSelf().each(function(){
299 | if ( this[ expando ] != undefined )
300 | this[ expando ] = null;
301 | });
302 |
303 | // Copy the events from the original to the clone
304 | if ( events === true )
305 | this.find("*").andSelf().each(function(i){
306 | if (this.nodeType == 3)
307 | return;
308 | var events = jQuery.data( this, "events" );
309 |
310 | for ( var type in events )
311 | for ( var handler in events[ type ] )
312 | jQuery.event.add( clone[ i ], type, events[ type ][ handler ], events[ type ][ handler ].data );
313 | });
314 |
315 | // Return the cloned set
316 | return ret;
317 | },
318 |
319 | filter: function( selector ) {
320 | return this.pushStack(
321 | jQuery.isFunction( selector ) &&
322 | jQuery.grep(this, function(elem, i){
323 | return selector.call( elem, i );
324 | }) ||
325 |
326 | jQuery.multiFilter( selector, this ) );
327 | },
328 |
329 | not: function( selector ) {
330 | if ( selector.constructor == String )
331 | // test special case where just one selector is passed in
332 | if ( isSimple.test( selector ) )
333 | return this.pushStack( jQuery.multiFilter( selector, this, true ) );
334 | else
335 | selector = jQuery.multiFilter( selector, this );
336 |
337 | var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType;
338 | return this.filter(function() {
339 | return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector;
340 | });
341 | },
342 |
343 | add: function( selector ) {
344 | return this.pushStack( jQuery.unique( jQuery.merge(
345 | this.get(),
346 | typeof selector == 'string' ?
347 | jQuery( selector ) :
348 | jQuery.makeArray( selector )
349 | )));
350 | },
351 |
352 | is: function( selector ) {
353 | return !!selector && jQuery.multiFilter( selector, this ).length > 0;
354 | },
355 |
356 | hasClass: function( selector ) {
357 | return this.is( "." + selector );
358 | },
359 |
360 | val: function( value ) {
361 | if ( value == undefined ) {
362 |
363 | if ( this.length ) {
364 | var elem = this[0];
365 |
366 | // We need to handle select boxes special
367 | if ( jQuery.nodeName( elem, "select" ) ) {
368 | var index = elem.selectedIndex,
369 | values = [],
370 | options = elem.options,
371 | one = elem.type == "select-one";
372 |
373 | // Nothing was selected
374 | if ( index < 0 )
375 | return null;
376 |
377 | // Loop through all the selected options
378 | for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) {
379 | var option = options[ i ];
380 |
381 | if ( option.selected ) {
382 | // Get the specifc value for the option
383 | value = jQuery.browser.msie && !option.attributes.value.specified ? option.text : option.value;
384 |
385 | // We don't need an array for one selects
386 | if ( one )
387 | return value;
388 |
389 | // Multi-Selects return an array
390 | values.push( value );
391 | }
392 | }
393 |
394 | return values;
395 |
396 | // Everything else, we just grab the value
397 | } else
398 | return (this[0].value || "").replace(/\r/g, "");
399 |
400 | }
401 |
402 | return undefined;
403 | }
404 |
405 | if( value.constructor == Number )
406 | value += '';
407 |
408 | return this.each(function(){
409 | if ( this.nodeType != 1 )
410 | return;
411 |
412 | if ( value.constructor == Array && /radio|checkbox/.test( this.type ) )
413 | this.checked = (jQuery.inArray(this.value, value) >= 0 ||
414 | jQuery.inArray(this.name, value) >= 0);
415 |
416 | else if ( jQuery.nodeName( this, "select" ) ) {
417 | var values = jQuery.makeArray(value);
418 |
419 | jQuery( "option", this ).each(function(){
420 | this.selected = (jQuery.inArray( this.value, values ) >= 0 ||
421 | jQuery.inArray( this.text, values ) >= 0);
422 | });
423 |
424 | if ( !values.length )
425 | this.selectedIndex = -1;
426 |
427 | } else
428 | this.value = value;
429 | });
430 | },
431 |
432 | html: function( value ) {
433 | return value == undefined ?
434 | (this[0] ?
435 | this[0].innerHTML :
436 | null) :
437 | this.empty().append( value );
438 | },
439 |
440 | replaceWith: function( value ) {
441 | return this.after( value ).remove();
442 | },
443 |
444 | eq: function( i ) {
445 | return this.slice( i, i + 1 );
446 | },
447 |
448 | slice: function() {
449 | return this.pushStack( Array.prototype.slice.apply( this, arguments ) );
450 | },
451 |
452 | map: function( callback ) {
453 | return this.pushStack( jQuery.map(this, function(elem, i){
454 | return callback.call( elem, i, elem );
455 | }));
456 | },
457 |
458 | andSelf: function() {
459 | return this.add( this.prevObject );
460 | },
461 |
462 | data: function( key, value ){
463 | var parts = key.split(".");
464 | parts[1] = parts[1] ? "." + parts[1] : "";
465 |
466 | if ( value === undefined ) {
467 | var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);
468 |
469 | if ( data === undefined && this.length )
470 | data = jQuery.data( this[0], key );
471 |
472 | return data === undefined && parts[1] ?
473 | this.data( parts[0] ) :
474 | data;
475 | } else
476 | return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function(){
477 | jQuery.data( this, key, value );
478 | });
479 | },
480 |
481 | removeData: function( key ){
482 | return this.each(function(){
483 | jQuery.removeData( this, key );
484 | });
485 | },
486 |
487 | domManip: function( args, table, reverse, callback ) {
488 | var clone = this.length > 1, elems;
489 |
490 | return this.each(function(){
491 | if ( !elems ) {
492 | elems = jQuery.clean( args, this.ownerDocument );
493 |
494 | if ( reverse )
495 | elems.reverse();
496 | }
497 |
498 | var obj = this;
499 |
500 | if ( table && jQuery.nodeName( this, "table" ) && jQuery.nodeName( elems[0], "tr" ) )
501 | obj = this.getElementsByTagName("tbody")[0] || this.appendChild( this.ownerDocument.createElement("tbody") );
502 |
503 | var scripts = jQuery( [] );
504 |
505 | jQuery.each(elems, function(){
506 | var elem = clone ?
507 | jQuery( this ).clone( true )[0] :
508 | this;
509 |
510 | // execute all scripts after the elements have been injected
511 | if ( jQuery.nodeName( elem, "script" ) )
512 | scripts = scripts.add( elem );
513 | else {
514 | // Remove any inner scripts for later evaluation
515 | if ( elem.nodeType == 1 )
516 | scripts = scripts.add( jQuery( "script", elem ).remove() );
517 |
518 | // Inject the elements into the document
519 | callback.call( obj, elem );
520 | }
521 | });
522 |
523 | scripts.each( evalScript );
524 | });
525 | }
526 | };
527 |
528 | // Give the init function the jQuery prototype for later instantiation
529 | jQuery.fn.init.prototype = jQuery.fn;
530 |
531 | function evalScript( i, elem ) {
532 | if ( elem.src )
533 | jQuery.ajax({
534 | url: elem.src,
535 | async: false,
536 | dataType: "script"
537 | });
538 |
539 | else
540 | jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" );
541 |
542 | if ( elem.parentNode )
543 | elem.parentNode.removeChild( elem );
544 | }
545 |
546 | function now(){
547 | return +new Date;
548 | }
549 |
550 | jQuery.extend = jQuery.fn.extend = function() {
551 | // copy reference to target object
552 | var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options;
553 |
554 | // Handle a deep copy situation
555 | if ( target.constructor == Boolean ) {
556 | deep = target;
557 | target = arguments[1] || {};
558 | // skip the boolean and the target
559 | i = 2;
560 | }
561 |
562 | // Handle case when target is a string or something (possible in deep copy)
563 | if ( typeof target != "object" && typeof target != "function" )
564 | target = {};
565 |
566 | // extend jQuery itself if only one argument is passed
567 | if ( length == i ) {
568 | target = this;
569 | --i;
570 | }
571 |
572 | for ( ; i < length; i++ )
573 | // Only deal with non-null/undefined values
574 | if ( (options = arguments[ i ]) != null )
575 | // Extend the base object
576 | for ( var name in options ) {
577 | var src = target[ name ], copy = options[ name ];
578 |
579 | // Prevent never-ending loop
580 | if ( target === copy )
581 | continue;
582 |
583 | // Recurse if we're merging object values
584 | if ( deep && copy && typeof copy == "object" && !copy.nodeType )
585 | target[ name ] = jQuery.extend( deep,
586 | // Never move original objects, clone them
587 | src || ( copy.length != null ? [ ] : { } )
588 | , copy );
589 |
590 | // Don't bring in undefined values
591 | else if ( copy !== undefined )
592 | target[ name ] = copy;
593 |
594 | }
595 |
596 | // Return the modified object
597 | return target;
598 | };
599 |
600 | var expando = "jQuery" + now(), uuid = 0, windowData = {},
601 | // exclude the following css properties to add px
602 | exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i,
603 | // cache defaultView
604 | defaultView = document.defaultView || {};
605 |
606 | jQuery.extend({
607 | noConflict: function( deep ) {
608 | window.$ = _$;
609 |
610 | if ( deep )
611 | window.jQuery = _jQuery;
612 |
613 | return jQuery;
614 | },
615 |
616 | // See test/unit/core.js for details concerning this function.
617 | isFunction: function( fn ) {
618 | return !!fn && typeof fn != "string" && !fn.nodeName &&
619 | fn.constructor != Array && /^[\s[]?function/.test( fn + "" );
620 | },
621 |
622 | // check if an element is in a (or is an) XML document
623 | isXMLDoc: function( elem ) {
624 | return elem.documentElement && !elem.body ||
625 | elem.tagName && elem.ownerDocument && !elem.ownerDocument.body;
626 | },
627 |
628 | // Evalulates a script in a global context
629 | globalEval: function( data ) {
630 | data = jQuery.trim( data );
631 |
632 | if ( data ) {
633 | // Inspired by code by Andrea Giammarchi
634 | // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html
635 | var head = document.getElementsByTagName("head")[0] || document.documentElement,
636 | script = document.createElement("script");
637 |
638 | script.type = "text/javascript";
639 | if ( jQuery.browser.msie )
640 | script.text = data;
641 | else
642 | script.appendChild( document.createTextNode( data ) );
643 |
644 | // Use insertBefore instead of appendChild to circumvent an IE6 bug.
645 | // This arises when a base node is used (#2709).
646 | head.insertBefore( script, head.firstChild );
647 | head.removeChild( script );
648 | }
649 | },
650 |
651 | nodeName: function( elem, name ) {
652 | return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase();
653 | },
654 |
655 | cache: {},
656 |
657 | data: function( elem, name, data ) {
658 | elem = elem == window ?
659 | windowData :
660 | elem;
661 |
662 | var id = elem[ expando ];
663 |
664 | // Compute a unique ID for the element
665 | if ( !id )
666 | id = elem[ expando ] = ++uuid;
667 |
668 | // Only generate the data cache if we're
669 | // trying to access or manipulate it
670 | if ( name && !jQuery.cache[ id ] )
671 | jQuery.cache[ id ] = {};
672 |
673 | // Prevent overriding the named cache with undefined values
674 | if ( data !== undefined )
675 | jQuery.cache[ id ][ name ] = data;
676 |
677 | // Return the named cache data, or the ID for the element
678 | return name ?
679 | jQuery.cache[ id ][ name ] :
680 | id;
681 | },
682 |
683 | removeData: function( elem, name ) {
684 | elem = elem == window ?
685 | windowData :
686 | elem;
687 |
688 | var id = elem[ expando ];
689 |
690 | // If we want to remove a specific section of the element's data
691 | if ( name ) {
692 | if ( jQuery.cache[ id ] ) {
693 | // Remove the section of cache data
694 | delete jQuery.cache[ id ][ name ];
695 |
696 | // If we've removed all the data, remove the element's cache
697 | name = "";
698 |
699 | for ( name in jQuery.cache[ id ] )
700 | break;
701 |
702 | if ( !name )
703 | jQuery.removeData( elem );
704 | }
705 |
706 | // Otherwise, we want to remove all of the element's data
707 | } else {
708 | // Clean up the element expando
709 | try {
710 | delete elem[ expando ];
711 | } catch(e){
712 | // IE has trouble directly removing the expando
713 | // but it's ok with using removeAttribute
714 | if ( elem.removeAttribute )
715 | elem.removeAttribute( expando );
716 | }
717 |
718 | // Completely remove the data cache
719 | delete jQuery.cache[ id ];
720 | }
721 | },
722 |
723 | // args is for internal usage only
724 | each: function( object, callback, args ) {
725 | var name, i = 0, length = object.length;
726 |
727 | if ( args ) {
728 | if ( length == undefined ) {
729 | for ( name in object )
730 | if ( callback.apply( object[ name ], args ) === false )
731 | break;
732 | } else
733 | for ( ; i < length; )
734 | if ( callback.apply( object[ i++ ], args ) === false )
735 | break;
736 |
737 | // A special, fast, case for the most common use of each
738 | } else {
739 | if ( length == undefined ) {
740 | for ( name in object )
741 | if ( callback.call( object[ name ], name, object[ name ] ) === false )
742 | break;
743 | } else
744 | for ( var value = object[0];
745 | i < length && callback.call( value, i, value ) !== false; value = object[++i] ){}
746 | }
747 |
748 | return object;
749 | },
750 |
751 | prop: function( elem, value, type, i, name ) {
752 | // Handle executable functions
753 | if ( jQuery.isFunction( value ) )
754 | value = value.call( elem, i );
755 |
756 | // Handle passing in a number to a CSS property
757 | return value && value.constructor == Number && type == "curCSS" && !exclude.test( name ) ?
758 | value + "px" :
759 | value;
760 | },
761 |
762 | className: {
763 | // internal only, use addClass("class")
764 | add: function( elem, classNames ) {
765 | jQuery.each((classNames || "").split(/\s+/), function(i, className){
766 | if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) )
767 | elem.className += (elem.className ? " " : "") + className;
768 | });
769 | },
770 |
771 | // internal only, use removeClass("class")
772 | remove: function( elem, classNames ) {
773 | if (elem.nodeType == 1)
774 | elem.className = classNames != undefined ?
775 | jQuery.grep(elem.className.split(/\s+/), function(className){
776 | return !jQuery.className.has( classNames, className );
777 | }).join(" ") :
778 | "";
779 | },
780 |
781 | // internal only, use hasClass("class")
782 | has: function( elem, className ) {
783 | return jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1;
784 | }
785 | },
786 |
787 | // A method for quickly swapping in/out CSS properties to get correct calculations
788 | swap: function( elem, options, callback ) {
789 | var old = {};
790 | // Remember the old values, and insert the new ones
791 | for ( var name in options ) {
792 | old[ name ] = elem.style[ name ];
793 | elem.style[ name ] = options[ name ];
794 | }
795 |
796 | callback.call( elem );
797 |
798 | // Revert the old values
799 | for ( var name in options )
800 | elem.style[ name ] = old[ name ];
801 | },
802 |
803 | css: function( elem, name, force ) {
804 | if ( name == "width" || name == "height" ) {
805 | var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ];
806 |
807 | function getWH() {
808 | val = name == "width" ? elem.offsetWidth : elem.offsetHeight;
809 | var padding = 0, border = 0;
810 | jQuery.each( which, function() {
811 | padding += parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0;
812 | border += parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0;
813 | });
814 | val -= Math.round(padding + border);
815 | }
816 |
817 | if ( jQuery(elem).is(":visible") )
818 | getWH();
819 | else
820 | jQuery.swap( elem, props, getWH );
821 |
822 | return Math.max(0, val);
823 | }
824 |
825 | return jQuery.curCSS( elem, name, force );
826 | },
827 |
828 | curCSS: function( elem, name, force ) {
829 | var ret, style = elem.style;
830 |
831 | // A helper method for determining if an element's values are broken
832 | function color( elem ) {
833 | if ( !jQuery.browser.safari )
834 | return false;
835 |
836 | // defaultView is cached
837 | var ret = defaultView.getComputedStyle( elem, null );
838 | return !ret || ret.getPropertyValue("color") == "";
839 | }
840 |
841 | // We need to handle opacity special in IE
842 | if ( name == "opacity" && jQuery.browser.msie ) {
843 | ret = jQuery.attr( style, "opacity" );
844 |
845 | return ret == "" ?
846 | "1" :
847 | ret;
848 | }
849 | // Opera sometimes will give the wrong display answer, this fixes it, see #2037
850 | if ( jQuery.browser.opera && name == "display" ) {
851 | var save = style.outline;
852 | style.outline = "0 solid black";
853 | style.outline = save;
854 | }
855 |
856 | // Make sure we're using the right name for getting the float value
857 | if ( name.match( /float/i ) )
858 | name = styleFloat;
859 |
860 | if ( !force && style && style[ name ] )
861 | ret = style[ name ];
862 |
863 | else if ( defaultView.getComputedStyle ) {
864 |
865 | // Only "float" is needed here
866 | if ( name.match( /float/i ) )
867 | name = "float";
868 |
869 | name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase();
870 |
871 | var computedStyle = defaultView.getComputedStyle( elem, null );
872 |
873 | if ( computedStyle && !color( elem ) )
874 | ret = computedStyle.getPropertyValue( name );
875 |
876 | // If the element isn't reporting its values properly in Safari
877 | // then some display: none elements are involved
878 | else {
879 | var swap = [], stack = [], a = elem, i = 0;
880 |
881 | // Locate all of the parent display: none elements
882 | for ( ; a && color(a); a = a.parentNode )
883 | stack.unshift(a);
884 |
885 | // Go through and make them visible, but in reverse
886 | // (It would be better if we knew the exact display type that they had)
887 | for ( ; i < stack.length; i++ )
888 | if ( color( stack[ i ] ) ) {
889 | swap[ i ] = stack[ i ].style.display;
890 | stack[ i ].style.display = "block";
891 | }
892 |
893 | // Since we flip the display style, we have to handle that
894 | // one special, otherwise get the value
895 | ret = name == "display" && swap[ stack.length - 1 ] != null ?
896 | "none" :
897 | ( computedStyle && computedStyle.getPropertyValue( name ) ) || "";
898 |
899 | // Finally, revert the display styles back
900 | for ( i = 0; i < swap.length; i++ )
901 | if ( swap[ i ] != null )
902 | stack[ i ].style.display = swap[ i ];
903 | }
904 |
905 | // We should always get a number back from opacity
906 | if ( name == "opacity" && ret == "" )
907 | ret = "1";
908 |
909 | } else if ( elem.currentStyle ) {
910 | var camelCase = name.replace(/\-(\w)/g, function(all, letter){
911 | return letter.toUpperCase();
912 | });
913 |
914 | ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ];
915 |
916 | // From the awesome hack by Dean Edwards
917 | // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
918 |
919 | // If we're not dealing with a regular pixel number
920 | // but a number that has a weird ending, we need to convert it to pixels
921 | if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) {
922 | // Remember the original values
923 | var left = style.left, rsLeft = elem.runtimeStyle.left;
924 |
925 | // Put in the new values to get a computed value out
926 | elem.runtimeStyle.left = elem.currentStyle.left;
927 | style.left = ret || 0;
928 | ret = style.pixelLeft + "px";
929 |
930 | // Revert the changed values
931 | style.left = left;
932 | elem.runtimeStyle.left = rsLeft;
933 | }
934 | }
935 |
936 | return ret;
937 | },
938 |
939 | clean: function( elems, context ) {
940 | var ret = [];
941 | context = context || document;
942 | // !context.createElement fails in IE with an error but returns typeof 'object'
943 | if (typeof context.createElement == 'undefined')
944 | context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
945 |
946 | jQuery.each(elems, function(i, elem){
947 | if ( !elem )
948 | return;
949 |
950 | if ( elem.constructor == Number )
951 | elem += '';
952 |
953 | // Convert html string into DOM nodes
954 | if ( typeof elem == "string" ) {
955 | // Fix "XHTML"-style tags in all browsers
956 | elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){
957 | return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ?
958 | all :
959 | front + ">" + tag + ">";
960 | });
961 |
962 | // Trim whitespace, otherwise indexOf won't work as expected
963 | var tags = jQuery.trim( elem ).toLowerCase(), div = context.createElement("div");
964 |
965 | var wrap =
966 | // option or optgroup
967 | !tags.indexOf("", "" ] ||
969 |
970 | !tags.indexOf("", "" ] ||
972 |
973 | tags.match(/^<(thead|tbody|tfoot|colg|cap)/) &&
974 | [ 1, "" ] ||
975 |
976 | !tags.indexOf("
", "" ] ||
978 |
979 | // matched above
980 | (!tags.indexOf(" | ", "
" ] ||
982 |
983 | !tags.indexOf("", "" ] ||
985 |
986 | // IE can't serialize and