PK 6@_Q+V +V ( django-autocomplete-light-0.8/quick.html
The purpose of this documentation is to get you started as fast as possible, because your time matters and you probably have other things to worry about.
Install the package:
pip install django-autocomplete-light
# or the development version
pip install -e git+git://github.com/yourlabs/django-autocomplete-light.git#egg=django-autocomplete-light
Add to INSTALLED_APPS: ‘autocomplete_light’
Add to urls:
url(r'autocomplete/', include('autocomplete_light.urls')),
Add before admin.autodiscover():
import autocomplete_light
autocomplete_light.autodiscover()
At this point, we’re going to assume that you have django.contrib.staticfiles working. This means that static files are automatically served with runserver, and that you have to run collectstatic when using another server (fastcgi, uwsgi, and whatnot). If you don’t use django.contrib.staticfiles, then you’re on your own to manage staticfiles.
This is an example of how you could load the javascript:
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"></script>
{% include 'autocomplete_light/static.html' %}
For AutocompleteWidget to be enabled in the admin, you should create your own admin/base_site.html template as demonstrated in test_project/templates/admin/base_site.html:
{% extends "admin/base.html" %}
{% load i18n %}
{% block footer %}
{{ block.super }}
<script src="{{ STATIC_URL }}jquery.js" type="text/javascript"></script>
{% include 'autocomplete_light/static.html' %}
{% comment %}
Load additionnal script or style dependencies here. For instance, the
double country/city autocomplete widget requires the countrycity deck
bootstrap so we'll load it. But you don't need this one if you don't use
the countrycity widget of the cities_light app.
{% endcomment %}
<script src="{{ STATIC_URL }}cities_light/autocomplete_light.js" type="text/javascript"></script>
{% endblock %}
Create yourapp/autocomplete_light_registry.py, assuming “Author” has a “full_name” CharField:
import autocomplete_light
from models import Author
autocomplete_light.register(Author, search_field='full_name')
See more about the channel registry in Registry.
But still, the default implementation of query_filter() is pretty trivial, you might want to customize how it will filter the queryset. See more about customizing channels in Channels basics.
Anyway, finish by setting BookAdmin.form in yourapp/admin.py:
from django.contrib import admin
import autocomplete_light
from models import Book
class BookAdmin(admin.ModelAdmin):
# use an autocomplete for Author
form = autocomplete_light.modelform_factory(Book)
admin.site.register(Book, BookAdmin)
AutocompleteWidget is usable on ModelChoiceField and ModelMultipleChoiceField.
Widget suitable for ModelChoiceField and ModelMultipleChoiceField.
Example usage:
from django import forms
import autocomplete_light
from models import Author
class AuthorsForm(forms.Form):
lead_author = forms.ModelChoiceField(Author.objects.all(), widget=
autocomplete_light.AutocompleteWidget(
'AuthorChannel', max_items=1))
contributors = forms.ModelMultipleChoiceField(Author.objects.all(),
widget=autocomplete_light.AutocompleteWidget('AuthorChannel'))
AutocompleteWidget constructor decorates SelectMultiple constructor
Arguments: channel_name – the name of the channel that this widget should use.
Keyword arguments are passed to javascript via data attributes of the autocomplete wrapper element:
The purpose of this documentation is to describe every element in a chronological manner. Because you want to know everything about this app and hack like crazy.
It is complementary with the quick documentation.
The registry module provides tools to maintain a registry of channels.
The first thing that should happen when django starts is registration of channels. It should happen first, because channels are required for autocomplete widgets. And autocomplete widgets are required for forms. And forms are required for ModelAdmin.
It looks like this:
Dict with some shortcuts to handle a registry of channels.
Proxy registry.register_model_channel() or registry.register_channel() if there is no apparent model for the channel.
Example usages:
# Will create and register SomeModelChannel, if SomeChannel.model
# is None (which is the case by default):
autocomplete_light.register(SomeModel)
# Same but using SomeChannel as base:
autocomplete_light.register(SomeModel, SomeChannel)
# Register a channel without model, ensure that SomeChannel.model
# is None (which is the default):
autocomplete_light.register(SomeChannel)
# As of 0.5, you may also pass attributes*, ie.:
autocomplete_light.register(SomeModel, search_field='search_names',
result_template='somemodel_result.html')
You may pass attributes via kwargs, only if the registry creates a type:
Add a model to the registry, optionnaly with a given channel class.
Three cases are possible:
To keep things simple, the name of a channel is it’s class name, which is usually generated. In case of conflicts, you may override the default channel name with the channel_name keyword argument.
Check all apps in INSTALLED_APPS for stuff related to autocomplete_light.
For each app, autodiscover imports app.autocomplete_light_registry if available, resulting in execution of register() statements in that module, filling registry.
Consider a standard app called ‘cities_light’ with such a structure:
cities_light/
__init__.py
models.py
urls.py
views.py
autocomplete_light_registry.py
With such a autocomplete_light_registry.py:
from models import City, Country
import autocomplete_light
autocomplete_light.register(City)
autocomplete_light.register(Country)
When autodiscover() imports cities_light.autocomplete_light_registry, both CityChannel and CountryChannel will be registered. For details on how these channel classes are generated, read the documentation of ChannelRegistry.register.
django-cities-light ships the working example.
The channel.base module provides a channel class which you can extend to make your own channel. It also serves as default channel class.
A basic implementation of a channel, which should fit most use cases.
Attributes:
Set result_template and autocomplete_template if necessary.
Return True if the values are valid.
By default, expect values to be a list of object ids, return True if all the ids are found in the queryset.
Return the absolute url for this channel, using autocomplete_light_channel url
Return an iterable of result to display in the autocomplete box.
By default, it will:
Set self.request, self.args and self.kwargs, useful in query_filter.
Return the result list after ordering.
By default, it expects results to be a queryset and order it by search_field.
Filter results using the request.
By default this will expect results to be a queryset, and will filter it with self.search_field + ‘__icontains’=self.request[‘q’].
Render the autocomplete suggestion box.
By default, render self.autocomplete_template with the channel in the context.
Return the html representation of a result for display in the deck and autocomplete box.
By default, render result_template with channel and result in the context.
A simple example from test_project:
from django import forms
import autocomplete_light
from cities_light.models import City
from cities_light.contrib.autocomplete_light_widgets import \
CityAutocompleteWidget
from models import Address
from generic_form_example import TaggedItemForm
class AddressForm(forms.ModelForm):
city = forms.ModelChoiceField(City.objects.all(),
widget=CityAutocompleteWidget('CityChannel', max_items=1))
class Meta:
model = Address
widgets = autocomplete_light.get_widgets_dict(Address,
autocomplete_exclude='city')
A couple of helper functions to help enabling AutocompleteWidget in ModelForms.
Return a dict of field_name: widget_instance for model that is compatible with Django.
Inspect the model’s field and many to many fields, calls registry.channel_for_model to get the channel for the related model. If a channel is returned, then an AutocompleteWidget will be spawned using this channel.
The dict is usable by ModelForm.Meta.widgets. In django 1.4, with modelform_factory too.
Wraps around Django’s django_modelform_factory, using get_widgets_dict.
Basically, it will use the dict returned by get_widgets_dict in order and pass it to django’s modelform_factory, and return the resulting modelform.
It is important to load jQuery first, and then autocomplete_light and application specific javascript, it can look like this:
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"></script>
{% include 'autocomplete_light/static.html' %}
However, autocomplete_light/static.html also includes “remote.js” which is required only by remote channels. If you don’t need it, you could either load the static dependencies directly in your template, or override autocomplete_light/static.html:
<script type="text/javascript" src="{{ STATIC_URL }}autocomplete_light/autocomplete.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}autocomplete_light/deck.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}autocomplete_light/remote.js"></script>
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}autocomplete_light/style.css" />
Or, if you only want to make a global navigation autocomplete, you only need:
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"></script>
<script src="{{ STATIC_URL }}autocomplete_light/autocomplete.js" type="text/javascript"></script>
For AutocompleteWidget to be enabled in the admin, you should create your own admin/base_site.html template as demonstrated in test_project/templates/admin/base_site.html:
{% extends "admin/base.html" %}
{% load i18n %}
{% block footer %}
{{ block.super }}
<script src="{{ STATIC_URL }}jquery.js" type="text/javascript"></script>
{% include 'autocomplete_light/static.html' %}
{% comment %}
Load additionnal script or style dependencies here. For instance, the
double country/city autocomplete widget requires the countrycity deck
bootstrap so we'll load it. But you don't need this one if you don't use
the countrycity widget of the cities_light app.
{% endcomment %}
<script src="{{ STATIC_URL }}cities_light/autocomplete_light.js" type="text/javascript"></script>
{% endblock %}
The first thing that happens is the definition of an AutocompleteWidget in a form.
Widget suitable for ModelChoiceField and ModelMultipleChoiceField.
Example usage:
from django import forms
import autocomplete_light
from models import Author
class AuthorsForm(forms.Form):
lead_author = forms.ModelChoiceField(Author.objects.all(), widget=
autocomplete_light.AutocompleteWidget(
'AuthorChannel', max_items=1))
contributors = forms.ModelMultipleChoiceField(Author.objects.all(),
widget=autocomplete_light.AutocompleteWidget('AuthorChannel'))
AutocompleteWidget constructor decorates SelectMultiple constructor
Arguments: channel_name – the name of the channel that this widget should use.
Keyword arguments are passed to javascript via data attributes of the autocomplete wrapper element:
Render the autocomplete widget.
It will try two templates, like django admin: - autocomplete_light/channelname/widget.html - autocomplete_light/widget.html
Note that it will not pass ‘value’ to the template, because ‘value’ might be a list of model ids in the case of ModelMultipleChoiceField, or a model id in the case of ModelChoiceField. To keep things simple, it will just pass a list, ‘values’, to the template context.
This is what the default widget template looks like:
{% load i18n %}
{% load autocomplete_light_tags %}
{% comment %}
The outer element is called the 'widget wrapper'. It contains some data
attributes to communicate between Python and JavaScript. And of course, it
wraps around everything the widget needs.
{% endcomment %}
<span class="autocomplete_light_widget {{ name }}" id="{{ widget.html_id }}_wrapper" data-bootstrap="{{ widget.bootstrap }}">
{# a deck that should contain the list of selected options #}
<ul id="{{ html_id }}_deck" class="deck" >
{% for result in results %}
{{ result|autocomplete_light_result_as_html:channel }}
{% endfor %}
</ul>
{# a text input, that is the 'autocomplete input' #}
<input type="text" class="autocomplete" name="{{ name }}_autocomplete" id="{{ widget.html_id }}_text" value="" {{ extra_attrs }} />
{# a hidden select, that contains the actual selected values #}
<select style="display:none" class="valueSelect" name="{{ name }}" id="{{ widget.html_id }}" {% if widget.max_items != 1 %}multiple="multiple"{% endif %}>
{% for value in values %}
<option value="{{ value }}" selected="selected">{{ value }}</option>
{% endfor %}
</select>
{# a hidden textarea that contains some json about the widget #}
<textarea class="json_payload" style="display:none">
{{ json_payload }}
</textarea>
{# a hidden div that serves as template for the 'remove from deck' button #}
<div style="display:none" class="remove">
{# This will be appended to results on the deck, it's the remove button #}
X
</div>
<ul style="display:none" class="add_template">
{% comment %}
the contained element will be used to render options that are added to the select
via javascript, for example in django admin with the + sign
The text of the option will be inserted in the html of this tag
{% endcomment %}
<li class="result">
</li>
</ul>
</span>
deck.js initializes all widgets that have bootstrap=’normal’ (the default), as you can see:
$('.autocomplete_light_widget[data-bootstrap=normal]').each(function() {
$(this).yourlabs_deck();
});
If you want to initialize the deck yourself, set the widget or channel bootstrap to something else, say ‘yourinit’. Then, add to yourapp/static/yourapp/autocomplete_light.js something like:
$('.autocomplete_light_widget[data-bootstrap=yourinit]').each(function() {
$(this).yourlabs_deck({
getValue: function(result) {
// your own logic to get the value from an html result
return ...;
}
});
});
yourapp/static/yourapp/autocomplete_light.js will be automatically collected by by autodiscover, and the script tag generated by {% autocomplete_light_static %}.
In django-cities-light source, you can see a more interresting example where two autocompletes depend on each other.
You should take a look at the code of autocomplete.js and deck.js, as it lets you override everything.
One interresting note is that the plugins (yourlabs_autocomplete and yourlabs_deck) hold a registry. Which means that:
deck.js includes a javascript function that is executed every two seconds. It checks each widget’s hidden select for a value that is not in the deck, and adds it to the deck if any.
This is useful for example, when an item was added to the hidden select via the ‘+’ button in django admin. But if you create items yourself in javascript and add them to the select it would work too.
When the autocomplete input is focused, autocomplete.js checks if there are enought caracters in the input to display an autocomplete box. If minCharacters is 0, then it would open even if the input is empty, like a normal select box.
If the autocomplete box is empty, it will fetch the channel view. The channel view will delegate the rendering of the autocomplete box to the actual channel. So that you can override anything you want directly in the channel.
Simple view that routes the request to the appropriate channel.
Constructor. Called in the URLconf; can contain helpful extra keyword arguments, and other things.
Return an HttpResponse with the return value of channel.render_autocomplete().
This view is called by the autocomplete script, it is expected to return the rendered autocomplete box contents.
To do so, it gets the channel class from the registry, given the url keyword argument channel, that should be the channel name.
Then, it instanciates the channel with no argument as usual, and calls channel.init_for_request, passing all arguments it recieved.
Finnaly, it makes an HttpResponse with the result of channel.render_autocomplete(). The javascript will use that to fill the autocomplete suggestion box.
Render the autocomplete suggestion box.
By default, render self.autocomplete_template with the channel in the context.
Return the html representation of a result for display in the deck and autocomplete box.
By default, render result_template with channel and result in the context.
Then, autocomplete.js recognizes options with a selector. By default, it is ‘.result’. This means that any element with the ‘.result’ class in the autocomplete box is considered as an option.
When an option is selected, deck.js calls it’s method getValue() and adds this value to the hidden select. Also, it will copy the result html to the deck.
When an option is removed from the deck, deck.js also removes it from the hidden select.
This is the default HTML template for the autocomplete:
{% load autocomplete_light_tags %}
<ul>
{% for result in channel.get_results %}
{{ result|autocomplete_light_result_as_html:channel }}
{% endfor %}
</ul>
This is the default HTML template for results:
<li class="result" data-value="{{ value|safe }}">
{{ result }} {{ extra_html|safe }}
</li>
This app fills all your ajax autocomplete needs:
This is a simple alternative to django-ajax-selects.
You could subscribe to the mailing list ask questions or just be informed of package updates.
See test_project/README
If you don’t know how to debug, you should learn to use:
If you are able to do that, then you are a professional, enjoy autocomplete_light !!!
If you need help, open an issue on the github issues page.
But make sure you’ve read how to report bugs effectively and how to ask smart questions.
Also, don’t hesitate to do pull requests !
The test_project lives in the test_project subdirectory of django-autocomplete-light’s repository.
We’re going to use virtualenv, so that we don’t pollute your system when installing dependencies. If you don’t already have virtualenv, you can install it either via your package manager, either via python’s package manager with something like:
sudo easy_install virtualenv
Install last release:
rm -rf django-autocomplete-light autocomplete_light_env/
virtualenv autocomplete_light_env
source autocomplete_light_env/bin/activate
git clone https://jpic@github.com/yourlabs/django-autocomplete-light.git
cd django-autocomplete-light/test_project
pip install -r requirements.txt
./manage.py runserver
Install development versions, if you want to contribute hehehe:
AUTOCOMPLETE_LIGHT_VERSION="master"
CITIES_LIGHT_VERSION="master"
rm -rf autocomplete_light_env/
virtualenv autocomplete_light_env
source autocomplete_light_env/bin/activate
pip install -e git+git://github.com/yourlabs/django-cities-light.git@$CITIES_LIGHT_VERSION#egg=cities_light
pip install -e git+git://github.com/yourlabs/django-autocomplete-light.git@$AUTOCOMPLETE_LIGHT_VERSION#egg=autocomplete_light
cd autocomplete_light_env/src/autocomplete-light/test_project
pip install -r requirements.txt
./manage.py runserver
Login with user “test” and password “test”.
If you want to redo the database, but make sure you read README first:
rm db.sqlite
./manage.py syncdb
./manage.py cities_light
Once you have the test_project server running (see INSTALL if you don’t), open the first contact.
You will see two addresses:
The reason for that is that there are several cities in the world with the name “Paris”. This is the reason why the double autocomplete widget is interresting: it filters the cities based on the selected country.
Note that only cities from France, USA and Belgium are in the demo database.
Note that you can test autocompletes for generic foreign keys in this project too.
Assuming you installed the test_project, all you need in addition is to install requirements for this project:
cd autocomplete_light_env/src/autocomplete-light/test_api_project
pip install -r requirements.txt
Then, refer to README.rst in this folder.
This project demonstrates how the autocomplete can suggest results from a remote API - and thus which don’t have a pk in the local database.
In one console:
cd test_project
./manage.py runserver
In another:
cd test_api_project
./manage.py runserver 127.0.0.1:8001
In http://localhost:8001/admin, you should be able to test:
If you’re not going to use localhost:8000 for test_project, then you should update source urls in test_api_project/test_api_project/autocomplete_light_registry.py.
Now, note that there are no or few countries in test_api_project database.
Again, test_project’s database only includes countries France, Belgium and America so there’s no need to try the other one unless you know what you’re doing.
Also note that, city and country autocomplete work the same. The reason for that is that test_api_project uses City and Country remote channel to add results to the autocomplete that are not in the local database.
The app is was developed for Django 1.4. However, there are workarounds to get it to work with Django 1.3 too. This document attemps to provide an exhaustive list of notes that should be taken in account when using the app with django-autocomplete-light.
The provided autocomplete_light.modelform_factory relies on Django 1.4’s modelform_factory that accepts a ‘widgets’ dict.
Django 1.3 does not allow such an argument. You may however define your form as such:
class AuthorForm(forms.ModelForm):
class Meta:
model = Author
widgets = autocomplete_light.get_widgets_dict(Author)
Generic foreign keys are supported since 0.4.
import autocomplete_light
from models import Contact, Address
class MyGenericChannel(autocomplete_light.GenericChannelBase):
def get_querysets(self):
return {
Contact: Contact.objects.all(),
Address: Address.objects.all(),
}
def order_results(self, results):
if results.model == Address:
return results.order_by('street')
elif results.model == Contact:
return results.order_by('name')
def query_filter(self, results):
q = self.request.GET.get('q', None)
if q:
if results.model == Address:
results = results.filter(street__icontains=q)
elif results.model == Contact:
results = results.filter(name__icontains=q)
return results
autocomplete_light.register(MyGenericChannel)
Wraps around multiple querysets, from multiple model classes, rather than just one.
This is also interresting as it overrides all the default model logic from ChannelBase. Hell, you could even copy it and make your CSVChannelBase, a channel that uses a CSV file as backend. But only if you’re really bored or for a milion dollars.
Set result_template and autocomplete_template if necessary.
Return results for each queryset returned by get_querysets().
Note that it limits each queryset’s to self.limit_result. If you want a maximum of 12 suggestions and have a total of 4 querysets, then self.limit_results should be set to 3.
Return results, without doing any ordering.
In most cases, you would not have to override this method as querysets should be ordered by default, based on model.Meta.ordering.
import autocomplete_light
from models import TaggedItem
class TaggedItemForm(autocomplete_light.GenericModelForm):
content_object = autocomplete_light.GenericForeignKeyField(
widget=autocomplete_light.AutocompleteWidget(
'MyGenericChannel', max_items=1))
class Meta:
model = TaggedItem
widgets = autocomplete_light.get_widgets_dict(TaggedItem)
exclude = (
'content_type',
'object_id',
)
This simple subclass of ModelForm fixes a couple of issues with django’s ModelForm.
What ModelForm does, but also add virtual field values to self.initial.
Simple form field that converts strings to models.
Example model with related:
from django.db import models
from django.db.models import signals
from django.contrib.contenttypes import generic
from genericm2m.models import RelatedObjectsDescriptor
class ModelGroup(models.Model):
name = models.CharField(max_length=100)
related = RelatedObjectsDescriptor()
def __unicode__(self):
return self.name
Example generic_m2m.GenericModelForm usage:
import autocomplete_light
from autocomplete_light.contrib.generic_m2m import GenericModelForm, \
GenericManyToMany
from models import ModelGroup
class ModelGroupForm(GenericModelForm):
related = GenericManyToMany(
widget=autocomplete_light.AutocompleteWidget('MyGenericChannel'))
class Meta:
model = ModelGroup
Example ModelAdmin:
from django.contrib import admin
from models import ModelGroup
from forms import ModelGroupForm
class ModelGroupAdmin(admin.ModelAdmin):
form = ModelGroupForm
admin.site.register(ModelGroup, ModelGroupAdmin)
autocomplete_light.contrib.generic_m2m couples django-autocomplete-light with django-generic-m2m.
Generic many to many are supported since 0.5. It depends on django-generic-m2m external apps. Follow django-generic-m2m installation documentation, but at the time of writing it barely consists of adding the genericm2m to INSTALLED_APPS, and adding a field to models that should have a generic m2m relation. So, kudos to the maintainers of django-generic-m2m, fantastic app, use it for generic many to many relations.
See examples in test_project/generic_m2m_example.
Simple form field that converts strings to models.
Extension of autocomplete_light.GenericModelForm, that handles genericm2m’s RelatedObjectsDescriptor.
Add related objects to initial for each generic m2m field.
Yield name, field for each RelatedObjectsDescriptor of the model of this ModelForm.
For AutocompleteWidget to be enabled in the admin, you should create your own admin/base_site.html template as demonstrated in test_project/templates/admin/base_site.html:
{% extends "admin/base.html" %}
{% load i18n %}
{% block footer %}
{{ block.super }}
<script src="{{ STATIC_URL }}jquery.js" type="text/javascript"></script>
{% include 'autocomplete_light/static.html' %}
{% comment %}
Load additionnal script or style dependencies here. For instance, the
double country/city autocomplete widget requires the countrycity deck
bootstrap so we'll load it. But you don't need this one if you don't use
the countrycity widget of the cities_light app.
{% endcomment %}
<script src="{{ STATIC_URL }}cities_light/autocomplete_light.js" type="text/javascript"></script>
{% endblock %}
Please activate JavaScript to enable the search functionality.
From here you can search these documents. Enter your search words into the box below and click "search". Note that the search function will automatically search for all of the words. Pages containing fewer words won't appear in the result list.
This documentation is optionnal, but it is complementary with all other documentation. It aims advanced users.
Consider a social network about music. In order to propose all songs in the world in its autocomplete, it should either:
The purpose of this documentation is to describe every elements involved. Note that a living demonstration is available in test_api_project, where one project serves a full database of cities via an API to another.
In test_api_project, of course you should not hardcode urls like that in actual projects:
import autocomplete_light
from cities_light.contrib.autocomplete_light_restframework import RemoteCountryChannel, RemoteCityChannel
from cities_light.models import City, Country
autocomplete_light.register(Country, RemoteCountryChannel,
source_url = 'http://localhost:8000/cities_light/country/')
autocomplete_light.register(City, RemoteCityChannel,
source_url = 'http://localhost:8000/cities_light/city/')
Check out the documentation of RemoteCountryChannel and RemoteCityChannel for more.
Uses an API to propose suggestions from an HTTP API, tested with djangorestframework.
An example implementation usage is demonstrated in the django-cities-light contrib folder.
Autocomplete box display chronology:
Remote result selection chronology:
Set result_template and autocomplete_template if necessary.
Given an url to a remote object, return the corresponding model from the local database.
The default implementation expects url to respond with a JSON dict of the attributes of an object.
For relation attributes, it expect the value to be another url that will respond with a JSON dict of the attributes of the related object.
It calls model_for_source_url() to find which model class corresponds to which url. This allows fetch() to be recursive.
Take a result’s dict representation, return it’s local pk which might have been just created.
If your channel works with 0 to 1 API call, consider overriding this method. If your channel is susceptible of using several different API calls, consider overriding fetch().
Parses JSON from the API, return model instances.
The JSON should contain a list of dicts. Each dict should contain the attributes of an object. Relation attributes should be represented by their url in the API, which is set to model._source_url.
Returns a list of results from both the local database and the API if in the context of a request.
Using self.limit_results and the number of local results, adds results from get_remote_results().
Return an API url for the current autocomplete request.
By default, return self.source_url with the data dict returned by get_source_url_data().
Given a limit of items, return a dict of data to send to the API.
By default, it passes current request GET arguments, along with format: ‘json’ and the limit.
Take an URL from the API this remote channel is supposed to work with, return the model class to use for that url.
It is only needed for the default implementation of fetch(), because it has to follow relations recursively.
Channels with bootstrap=’remote’ get a deck using RemoteChannelDeck’s getValue() rather than the default getValue() function.
var RemoteChannelDeck = {
// The default deck getValue() implementation just returns the PK from the
// result HTML. RemoteChannelDeck's implementation checks for a textarea
// that would contain a JSON dict in the result's HTML. If the dict has a
// 'value' key, then return this value. Otherwise, make a blocking ajax
// request: POST the json dict to the channel url. It expects that the
// response will contain the value.
getValue: function(result) {
data = $.parseJSON(result.find('textarea').html());
if (data.value) return data.value;
var value = false;
$.ajax(this.payload.channel.url, {
async: false,
type: 'post',
data: {
'result': result.find('textarea').html(),
},
success: function(text, jqXHR, textStatus) {
value = text;
}
});
return value;
}
}
$(document).ready(function() {
// Instanciate decks with RemoteChannelDeck as override for all widgets with
// channel 'remote'.
$('.autocomplete_light_widget[data-bootstrap=remote]').each(function() {
$(this).yourlabs_deck(RemoteChannelDeck);
});
});
The autocomplete class constructor. Basically it takes a takes a text input element as argument, and sets attributes and methods for this instance.
function Autocomplete(el) {
The text input element that should have the suggestion box.
this.el = el;
Disable browser's autocomplete on that element.
this.el.attr('autocomplete', 'off');
Sets the initial value to an empty string.
this.value = '';
Current XMLHttpRequest that is kept so that when another request is started, a unfinished request is aborted. This avoids having several ajax requests at the time.
this.xhr = false;
Url of the autocomplete view, that should parse the queryVariable and return a rendered autocomplete box.
this.url = false;
Time to wait after a key was pressed in the text input before firing an ajax request.
this.timeout = 100;
The id of this autocomplete instance. It should be unique as it is used as key by the plugin registry of Autocomplete instances.
this.id = false;
Fire the autocomplete after that number of characters is in the autocomplete.
this.minCharacters = 2;
Text input default text, used as a placeholder.
this.defaultValue = 'type your search here';
Class of the currently hovered element.
this.activeClass = 'active';
A selector that matches all options of the autocomplete.
this.iterablesSelector = 'li:has(a)';
Name of the variable to pass to the Autocomplete url. For example, if the text input contains 'abc' then it will fetch the autocomplete box from this url this.url + '?' + this.queryVariable + '=abc'.
this.queryVariable = 'q';
Milliseconds after which the script should check if the autocomplete box should be hidden
this.blurTimeout = 500;
Where to append the autocomplete suggestion box, note that it's placed absolutely.
this.appendTo = $('body');
Extra classes to add to the autocomplete box container.
this.outerContainerClasses = '';
Extra data to pass to the autocomplete url.
this.data = {};
Called after an Autocomplete was instanciated and overridden.
this.initialize = function() {
var autocomplete = this;
this.el.val(this.defaultValue);
this.el.live('focus', function() {
if ($(this).val() == autocomplete.defaultValue) {
$(this).val('');
}
});
this.el.live('blur', function() {
if ($(this).val() == '') {
$(this).val(autocomplete.defaultValue);
}
});
$('.yourlabs_autocomplete.inner_container.id_'+this.id+' ' + this.iterablesSelector).live({
mouseenter: function(e) {
$('.yourlabs_autocomplete.inner_container.id_'+autocomplete.id+' ' + autocomplete.iterablesSelector + '.' + autocomplete.activeClass).each(function() {
autocomplete.el.trigger('deactivateOption', [autocomplete, $(this)]);
});
autocomplete.el.trigger('activateOption', [autocomplete, $(this)]);
},
mouseleave: function(e) {
autocomplete.el.trigger('deactivateOption', [autocomplete, $(this)]);
},
click: function(e) {
e.preventDefault();
e.stopPropagation();
autocomplete.el.trigger('selectOption', [$(this)]);
},
});
this.el.keyup(function(e) { autocomplete.refresh(); });
$('<div id="id_'+this.id+'" class="'+this.outerContainerClasses+' yourlabs_autocomplete outer_container id_'+this.id+'" style="position:absolute;z-index:'+this.zindex+';"><div class="yourlabs_autocomplete id_'+this.id+'"><div class="yourlabs_autocomplete inner_container id_'+this.id+'" style="display:none;"></div></div></div>').appendTo(this.appendTo);
this.innerContainer = $('.yourlabs_autocomplete.inner_container.id_'+this.id);
this.outerContainer = $('.yourlabs_autocomplete.outer_container.id_'+this.id);
if (window.opera) {
this.el.keypress(function(e) { autocomplete.onKeyPress(e); });
} else {
this.el.keydown(function(e) { autocomplete.onKeyPress(e); });
}
this.el.blur(function(e) {
window.setTimeout(function() {
autocomplete.hide();
}, autocomplete.blurTimeout);
});
this.el.click(function(e) {
if ($(this).val().length >= autocomplete.minCharacters)
autocomplete.show();
});
}
this.onKeyPress = function(e) {
var option;
switch (e.keyCode) {
case 27: //KEY_ESC:
this.el.val();
this.hide();
break;
case 9: //KEY_TAB:
break;
case 13: //KEY_RETURN:
option = this.innerContainer.find(this.iterablesSelector + '.' + this.activeClass);
if (option) {
e.preventDefault();
e.stopPropagation();
this.el.trigger('selectOption', [option]);
this.hide();
}
if(e.keyCode === 9){ return; }
break;
case 38: //KEY_UP:
this.move('up');
break;
case 40: //KEY_DOWN:
this.move('down');
break;
default:
return;
}
e.stopImmediatePropagation();
e.preventDefault();
}
this.show = function(html) {
this.fixPosition();
if ($.trim(this.innerContainer.html()).length == 0 && !this.xhr) {
this.fetchAutocomplete();
return;
}
if (html) {
this.innerContainer.html(html);
}
if (!this.innerContainer.is(':visible')) {
this.outerContainer.show();
this.innerContainer.show();
}
}
this.hide = function() {
this.outerContainer.hide();
this.innerContainer.hide();
}
this.move = function(way) {
var current, target, first, last;
current = this.innerContainer.find(this.iterablesSelector + '.' + this.activeClass);
first = this.innerContainer.find(this.iterablesSelector + ':first');
last = this.innerContainer.find(this.iterablesSelector + ':last');
this.show();
if (current.length) {
if (way == 'up') {
target = current.prevAll(this.iterablesSelector + ':first');
if (!target.length) {
target = last;
}
} else {
target = current.nextAll(this.iterablesSelector + ':first');
if (!target.length) {
target = first;
}
}
this.el.trigger('deactivateOption', [this, current]);
} else {
if (way == 'up') {
target = last;
} else {
target = first;
}
}
this.el.trigger('activateOption', [this, target]);
}
this.fixPosition = function() {
var css = {
'top': Math.floor(this.el.offset()['top']),
'left': Math.floor(this.el.offset()['left']),
'position': 'absolute',
}
css['top'] += Math.floor(this.el.innerHeight());
this.outerContainer.css(css);
}
this.refresh = function() {
var newValue;
newValue = this.el.val();
if (newValue == this.defaultValue) {
return false;
}
if (newValue.length < this.minCharacters) {
return false;
}
if (newValue == this.value) {
return false;
}
this.value = newValue;
this.fetchAutocomplete();
}
this.fetchAutocomplete = function() {
var autocomplete, data;
if (this.xhr) {
this.xhr.abort();
}
autocomplete = this;
data = this.data;
data[this.queryVariable] = this.value;
this.xhr = $.ajax(this.url, {
'data': data,
'complete': function(jqXHR, textStatus) {
autocomplete.fixPosition();
autocomplete.show(jqXHR.responseText);
autocomplete.xhr = false;
},
});
}
}
$.fn.yourlabs_autocomplete = function(overrides) {
var id;
overrides = overrides ? overrides : {};
id = overrides.id || this.attr('id');
if (!(id && this)) {
alert('failure: the element needs an id attribute, or an id option must be passed');
return false;
}
if ($.fn.yourlabs_autocomplete.registry == undefined) {
$.fn.yourlabs_autocomplete.registry = {};
}
if ($.fn.yourlabs_autocomplete.registry[id] == undefined) {
$.fn.yourlabs_autocomplete.registry[id] = new Autocomplete(this);
$.fn.yourlabs_autocomplete.registry[id] = $.extend($.fn.yourlabs_autocomplete.registry[id], overrides);
$.fn.yourlabs_autocomplete.registry[id].initialize();
}
return $.fn.yourlabs_autocomplete.registry[id];
};
$(document).ready(function() {
$(document).bind('activateOption', function(e, autocomplete, option) {
option.addClass(autocomplete.activeClass);
});
$(document).bind('deactivateOption', function(e, autocomplete, option) {
option.removeClass(autocomplete.activeClass);
});
});
function AutocompleteDeck(el) {
this.wrapper = el;
this.input = this.wrapper.find('input[type=text].autocomplete')
this.valueSelect = this.wrapper.find('select.valueSelect');
this.payload = $.parseJSON(this.wrapper.find('.json_payload').html());
this.deck = this.wrapper.find('.deck');
this.addTemplate = this.wrapper.find('.add_template .result');
this.getValue = function(result) {
return result.data('value');
};
this.initializeAutocomplete = function() {
this.autocomplete = this.input.yourlabs_autocomplete(this.autocompleteOptions);
};
this.bindSelectOption = function() {
this.input.bind('selectOption', function(e, option) {
if (!option.length)
return // placeholder: create option here
var wrapper = $(this).parents('.autocomplete_light_widget');
var deck = wrapper.yourlabs_deck();
deck.selectOption(option);
});
};
this.freeDeck = function() {
Remove an item if the deck is already full
if (this.payload.max_items && this.deck.children().length >= this.payload.max_items) {
var remove = $(this.deck.children()[0]);
this.valueSelect.find('option[data-value='+remove.attr('data-value')+']').attr(
'selected', '').remove();
remove.remove();
}
}
this.updateDisplay = function() {
this.input.val('');
if (this.payload.max_items && this.valueSelect.find('option:selected').length == this.payload.max_items) {
this.input.hide();
} else {
this.input.show();
}
this.deck.show();
}
this.addToDeck = function(result, value) {
var item = this.deck.find('[data-value='+value+']');
if (!item.length) {
var result = result.clone();
Might be necessary for created values.
if (!result.attr('data-value')) {
result.attr('data-value', value);
}
this.deck.append(result);
result.append('<span class="remove">' + this.wrapper.find('.remove').html() + '</span>');
}
}
this.addToSelect = function(result, value) {
var option = this.valueSelect.find('option[value='+value+']');
if (! option.length) {
this.valueSelect.append(
'<option selected="selected" value="'+ value +'"></option>');
option = this.valueSelect.find('option[value='+value+']');
}
option.attr('selected', 'selected');
this.valueSelect.trigger('change');
}
this.selectOption = function(result) {
Get the value for this result.
var value = this.getValue(result);
this.freeDeck();
this.addToDeck(result, value);
this.addToSelect(result, value);
this.updateDisplay();
}
this.deselectOption = function(result) {
var value = this.getValue(result);
this.valueSelect.find('option[value='+value+']').remove();
this.valueSelect.trigger('change');
result.remove();
if (this.deck.find('*').length == 0) {
this.deck.hide();
}
if (this.payload.max_items && this.valueSelect.find('option').length < this.payload.max_items) {
this.input.show();
}
};
this.autocompletId = this.input.attr('id');
this.autocompleteOptions = {
url: this.payload.channel.url,
id: this.autocompletId,
iterablesSelector: '.result',
minCharacters: this.payload.min_characters,
outerContainerClasses: 'autocomplete_light_widget',
defaultValue: this.payload.placeholder,
}
this.initialize = function() {
var results = this.deck.find('.result');
results.append(this.wrapper.find('.remove:last').clone().show());
if (this.payload.max_items > 0 && results.length == this.payload.max_items) {
this.input.hide();
}
this.initializeAutocomplete();
this.bindSelectOption();
}
}
$.fn.yourlabs_deck = function(overrides) {
var id;
overrides = overrides ? overrides : {};
id = overrides.id || this.attr('id');
if (!(id && this)) {
alert('failure: the element needs an id attribute, or an id option must be passed');
return false;
}
if ($.fn.yourlabs_deck.registry == undefined) {
$.fn.yourlabs_deck.registry = {};
}
if ($.fn.yourlabs_deck.registry[id] == undefined) {
$.fn.yourlabs_deck.registry[id] = new AutocompleteDeck(this);
$.fn.yourlabs_deck.registry[id] = $.extend($.fn.yourlabs_deck.registry[id], overrides);
$.fn.yourlabs_deck.registry[id].initialize();
$.fn.yourlabs_deck.registry[id].wrapper.attr('data-deckready', 1);
$.fn.yourlabs_deck.registry[id].wrapper.trigger('deckready');
}
return $.fn.yourlabs_deck.registry[id];
}
$(document).ready(function() {
$('.autocomplete_light_widget[data-bootstrap=normal]').each(function() {
var deck = $(this).yourlabs_deck();
});
$('.autocomplete_light_widget .deck .remove').live('click', function() {
var wrapper = $(this).parents('.autocomplete_light_widget');
if (!wrapper.length) return;
var deck = wrapper.yourlabs_deck();
var selector = deck.input.yourlabs_autocomplete().iterablesSelector;
var result = $(this).parents(selector);
deck.deselectOption(result);
});
support values added directly in the select via js (ie. admin + sign) for this, we make one timer that regularely checks for values in the select that are not in the deck. The reason for that is that change is not triggered when options are added like this: $('select#id_dependencies').append( '')
function updateDecks() {
$('.autocomplete_light_widget[data-deckready=1]').each(function() {
var deck = $(this).yourlabs_deck();
var value = deck.valueSelect.val();
function updateValueDisplay(value) {
if (!value) return;
var result = deck.deck.find('[data-value='+value+']');
if (!result.length) {
var result = deck.addTemplate.clone();
var html = deck.valueSelect.find('option[value='+value+']').html();
result.html(html);
result.attr('data-value', value);
deck.selectOption(result);
}
}
if (value instanceof Array) {
for(var i=0; i<value.length; i++) {
updateValueDisplay(value[i]);
}
} else {
updateValueDisplay(value);
}
});
setTimeout(updateDecks, 2000);
}
setTimeout(updateDecks, 1000);
});