Commits

Mark Lavin committed 0e99e7d Merge

Merge in plugin-refactor branch. Fixes #76 and fixes #88

Comments (0)

Files changed (10)

docs/advanced.rst

 `Autocomplete plug-in <http://jqueryui.com/demos/autocomplete/>`_, the widgets
 expose the events defined by the plugin.
 
-    - autocompletecreate
-    - autocompletesearch
-    - autocompleteopen
-    - autocompletefocus
-    - autocompleteselect
-    - autocompleteclose
-    - autocompletechange
+    - djselectablecreate
+    - djselectablesearch
+    - djselectableopen
+    - djselectablefocus
+    - djselectableselect
+    - djselectableclose
+    - djselectablechange
+
+.. note::
+
+    Prior to v0.7 these event names were under the ``autocomplete`` namespace. If you
+    are upgrading from a previous version and had customizations using these events
+    you should be sure to update the names.
 
 For the most part these event names should be self-explanatory. If you need additional
 detail you should refer to the `jQuery UI docs on these events <http://jqueryui.com/demos/autocomplete/#events>`_.
 --------------------------------------
 
 You might want to help your users by submitting the form once they have selected a valid
-item. To do this you simply need to listen for the ``autocompleteselect`` event. This
+item. To do this you simply need to listen for the ``djselectableselect`` event. This
 event is fired by the text input which has an index of 0. If your field is named ``my_field``
 then input to watch would be ``my_field_0`` such as:
 
 
         <script type="text/javascript">
             $(document).ready(function() {
-                $(':input[name=my_field_0]').bind('autocompleteselect', function(event, ui) {
+                $(':input[name=my_field_0]').bind('djselectableselect', function(event, ui) {
                     $(this).parents("form").submit();
                 });
             });

docs/releases.rst

 v0.7.0 (Released TBD)
 --------------------------------------
 
+This release features a large refactor of the JS plugin used by the widgets. While this
+over makes the plugin more maintainable and allowed for some of the new features in this
+release, it does introduce a few incompatible changes. For the most part places where you
+might have previously used the ``autocomplete`` namespace/plugin, those references should
+be updated to reference the ``djselectable`` plugin.
 
 - Improved the scope of ``prepareQuery`` and ``formatLabel`` options. Not fully backwards compatible. Thanks to Augusto Men.
 - Allow passing the Python path string in place of the lookup class to the fields and widgets. Thanks to Michael Manfre.
-
+- Allow passing JS plugin options through the widget ``attrs`` option. Thanks to Felipe Prenholato.
 
 Bug Fixes
 _________________
 
-- Fix bug with matching hidden input when the name contains '_1'. Thanks to Augusto Men for the report and fix.
+- Fixed bug with matching hidden input when the name contains '_1'. Thanks to Augusto Men for the report and fix.
+- Fixed bug where the enter button would open the combobox options rather than submit the form. Thanks to Felipe Prenholato for the report.
+
+
+Backwards Incompatible Changes
+________________________________
+
+- The JS event namespace has changed from ``autocomplete`` to ``djselectable``.
+- ``data('autocomplete')`` is no longer available on the widgets on the client-side. Use ``data('djselectable')`` instead.
+- Combobox button was changed from a ``<button>`` to ``<a>``. Any customized styles you may have should be updated.
 
 
 v0.6.2 (Released 2012-11-07)
 :ref:`Adding Parameters on the Server Side <server-side-parameters>` for more
 information.
 
+.. versionadded:: 0.7
+
+You can configure the plugin options by passing the configuration dictionary in the ``data-selectable-options``
+attribute. The set of options availble include those define by the base
+`autocomplete plugin <http://api.jqueryui.com/1.9/autocomplete/>`_ as well as the
+:ref:`javascript-removeIcon`, :ref:`javascript-comboboxIcon`, and :ref:`javascript-highlightMatch` options
+which are unique to django-selectable.
+
+    .. code-block:: python
+
+        attrs = {'data-selectable-options': {'highlightMatch': True, 'minLength': 5}}
+        selectable.AutoCompleteSelectWidget(lookup_class=FruitLookup, attrs=attrs)
+
 
 .. _AutoCompleteWidget:
 
 
 The one catch is that you must use ``allow_new=False`` which is the default.
 
-
 .. versionadded:: 0.7
 
 ``lookup_class`` may also be a dotted path.

example/core/admin.py

     def __init__(self, *args, **kwargs):
         super(FarmAdminForm, self).__init__(*args, **kwargs)
         if self.instance and self.instance.pk and self.instance.owner:
-            self.initial['owner'] = self.instance.owner
+            self.initial['owner'] = self.instance.owner.pk
 
     def save(self, *args, **kwargs):
         owner = self.cleaned_data['owner']

example/example/static/css/style.css

-body {padding-top: 60px;}
-input.ui-autocomplete-input {height: 26px; margin: 0;}
-button.ui-combo-button .ui-button-text {line-height: 1;}
-input {-webkit-border-radius: 0; -moz-border-radius: 0; border-radius: 0;}
+body {
+    padding-top: 60px;
+}
+input {
+    -webkit-border-radius: 0;
+    -moz-border-radius: 0;
+    border-radius: 0;
+}
+input.ui-autocomplete-input {
+    padding: 0;
+    margin-top: -3px;
+    margin-bottom: 0;
+    height: 1.5em;
+}
+
+

selectable/forms/widgets.py

+import json
+
 from django import forms
 from django.conf import settings
 from django.forms.util import flatatt
             self.qs['limit'] = self.limit
         if self.qs:
             url = '%s?%s' % (url, urlencode(self.qs))
+        if u'data-selectable-options' in attrs:
+            attrs[u'data-selectable-options'] = json.dumps(attrs[u'data-selectable-options'])
         attrs[u'data-selectable-url'] = url
         attrs[u'data-selectable-type'] = 'text'
         attrs[u'data-selectable-allow-new'] = str(self.allow_new).lower()
         widgets = [
             AutoCompleteWidget(
                 lookup_class, allow_new=self.allow_new,
-                limit=self.limit, query_params=query_params
+                limit=self.limit, query_params=query_params,
+                attrs=kwargs.get('attrs'),
             ),
             forms.HiddenInput(attrs={u'data-selectable-type': 'hidden'})
         ]
         widgets = [
             AutoComboboxWidget(
                 lookup_class, allow_new=self.allow_new,
-                limit=self.limit, query_params=query_params
+                limit=self.limit, query_params=query_params,
+                attrs=kwargs.get('attrs'),
             ),
             forms.HiddenInput(attrs={u'data-selectable-type': 'hidden'})
         ]
             u'data-selectable-multiple': 'true',
             u'data-selectable-position': position
         }
+        attrs.update(kwargs.get('attrs', {}))
         query_params = kwargs.pop('query_params', {})
         widgets = [
             AutoCompleteWidget(
             u'data-selectable-multiple': 'true',
             u'data-selectable-position': position
         }
+        attrs.update(kwargs.get('attrs', {}))
         query_params = kwargs.pop('query_params', {})
         widgets = [
             AutoComboboxWidget(

selectable/static/selectable/css/dj.selectable.css

  * Source: https://bitbucket.org/mlavin/django-selectable
  * Docs: http://django-selectable.readthedocs.org/
  *
- * Copyright 2010-2012, Mark Lavin
+ * Copyright 2010-2013, Mark Lavin
  * BSD License
  *
 */
 ul.ui-autocomplete li.ui-menu-item span.highlight {
     font-weight: bold;
 }
+input.ui-combo-input {
+    margin-right: 0;
+    line-height: 1.3;
+}
+a.ui-combo-button {
+    margin-left: -1px;
+}
+a.ui-combo-button .ui-button-text {
+    padding: 0;
+}

selectable/static/selectable/js/jquery.dj.selectable.js

  *   - jQuery 1.4.4+
  *   - jQuery UI 1.8 widget factory
  *
- * Copyright 2010-2012, Mark Lavin
+ * Copyright 2010-2013, Mark Lavin
  * BSD License
  *
 */
 (function ($) {
 
-	$.widget("ui.djselectable", {
+	$.widget("ui.djselectable", $.ui.autocomplete, {
 
         options: {
             removeIcon: "ui-icon-close",
             Event is the original select event if there is one.
             Event should not be passed if triggered manually.
             */
-            var self = this,
-            input = this.element;
-            $(input).removeClass('ui-state-error');
+            var $input = $(this.element);
+            $input.removeClass('ui-state-error');
+            this._setHidden(item);
             if (item) {
-                if (self.allowMultiple) {
-                    $(input).val("");
-                    $(input).data("autocomplete").term = "";
-                    if ($(self.hiddenMultipleSelector + '[value="' + item.id + '"]').length === 0) {
+                if (this.allowMultiple) {
+                    $input.val("");
+                    this.term = "";
+                    if ($(this.hiddenMultipleSelector + '[value="' + item.id + '"]').length === 0) {
                         var newInput = $('<input />', {
                             'type': 'hidden',
-                            'name': self.hiddenName,
+                            'name': this.hiddenName,
                             'value': item.id,
                             'title': item.value,
                             'data-selectable-type': 'hidden-multiple'
                         });
-                        $(input).after(newInput);
-                        self._addDeckItem(newInput);
+                        $input.after(newInput);
+                        this._addDeckItem(newInput);
                     }
                     return false;
                 } else {
-                    $(input).val(item.value);
+                    $input.val(item.value);
                     var ui = {item: item};
-                    if (typeof(event) === 'undefined' || event.type !== "autocompleteselect") {
-                        $(input).trigger('autocompleteselect', [ui ]);
+                    if (typeof(event) === 'undefined' || event.type !== "djselectableselect") {
+                        this.element.trigger("djselectableselect", [ui ]);
                     }
                 }
             }
         },
 
+        _setHidden: function (item) {
+            /* Set or clear single hidden input */
+            var $elem = $(this.hiddenSelector);
+            if (item && item.id) {
+                $elem.val(item.id);
+            } else {
+                $elem.val("");
+            }
+        },
+
         _create: function () {
             /* Initialize a new selectable widget */
             var self = this,
-            input = this.element,
-            data = $(input).data();
-            self.allowNew = data.selectableAllowNew || data['selectable-allow-new'];
-            self.allowMultiple = data.selectableMultiple || data['selectable-multiple'];
-            self.textName = $(input).attr('name');
-            self.hiddenName = self.textName.replace(new RegExp('_0$'), '_1');
-            self.hiddenSelector = ':input[data-selectable-type=hidden][name=' + self.hiddenName + ']';
-            self.hiddenMultipleSelector = ':input[data-selectable-type=hidden-multiple][name=' + self.hiddenName + ']';
-            if (self.allowMultiple) {
-                self.allowNew = false;
-                $(input).val("");
+            $input = $(this.element),
+            data = $input.data(),
+            options;
+            this.url = data.selectableUrl || data['selectable-url'];
+            this.allowNew = data.selectableAllowNew || data['selectable-allow-new'];
+            this.allowMultiple = data.selectableMultiple || data['selectable-multiple'];
+            this.textName = $input.attr('name');
+            this.hiddenName = this.textName.replace(new RegExp('_0$'), '_1');
+            this.hiddenSelector = ':input[data-selectable-type=hidden][name=' + this.hiddenName + ']';
+            this.hiddenMultipleSelector = ':input[data-selectable-type=hidden-multiple][name=' + this.hiddenName + ']';
+            this.selectableType = data.selectableType || data['selectable-type'];
+            if (this.allowMultiple) {
+                this.allowNew = false;
+                $input.val("");
                 this._initDeck();
             }
+            options = data.selectableOptions || data['selectable-options'];
+            if (options) {
+                this._setOptions(options);
+            }
+            // Call super-create
+            // This could be replaced by this._super() with jQuery UI 1.9
+            $.ui.autocomplete.prototype._create.call(this);
+            $input.addClass("ui-widget ui-widget-content ui-corner-all");
+            // Additional work for combobox widgets
+            if (this.selectableType === 'combobox') {
+                // Change auto-complete options
+                this.option("delay", 0);
+                this.option("minLength", 0);
+                $input.removeClass("ui-corner-all")
+                .addClass("ui-corner-left ui-combo-input");
+                // Add show all items button
+                $("<a>").text("&nbsp;").attr("tabIndex", -1).attr("title", "Show All Items")
+                .insertAfter($input)
+                .button({
+                    icons: {
+                        primary: this.options.comboboxIcon
+                    },
+                    text: false
+                })
+                .removeClass("ui-corner-all")
+                .addClass("ui-corner-right ui-button-icon ui-combo-button")
+                .click(function (e) {
+                    e.preventDefault();
+                    // close if already visible
+                    if (self.widget().is(":visible")) {
+                        self.close();
+                    }
+                    // pass empty string as value to search for, displaying all results
+                    self.search("");
+                    $input.focus();
+                });
+            }
+        },
 
-            function dataSource(request, response) {
+        // Override the default source creation
+        _initSource: function () {
+            var self = this;
+            this.source = function dataSource(request, response) {
                 /* Custom data source to uses the lookup url with pagination
                 Adds hook for adjusting query parameters.
                 Includes timestamp to prevent browser caching the lookup. */
-                var url = data.selectableUrl || data['selectable-url'];
                 var now = new Date().getTime();
                 var query = {term: request.term, timestamp: now};
                 if (self.options.prepareQuery) {
                     self.options.prepareQuery.apply(self, [query]);
                 }
-                var page = $(input).data("page");
+                var page = $(self.element).data("page");
                 if (page) {
                     query.page = page;
                 }
                     }
                     return response(results);
                 }
-				$.getJSON(url, query, unwrapResponse);
+				$.getJSON(self.url, query, unwrapResponse);
+            };
+        },
+        // Override the default auto-complete render.
+        _renderItem: function (ul, item) {
+            /* Adds hook for additional formatting, allows HTML in the label,
+            highlights term matches and handles pagination. */
+            var label = item.label;
+            if (this.options.formatLabel) {
+                label = this.options.formatLabel.apply(this, [label, item]);
             }
-            // Create base auto-complete lookup
-            $(input).autocomplete({
-                source: dataSource,
-                change: function (event, ui) {
-                    $(input).removeClass('ui-state-error');
-                    if ($(input).val() && !ui.item) {
-                        if (!self.allowNew) {
-                            $(input).addClass('ui-state-error');
-                        }
+            if (this.options.highlightMatch && this.term) {
+                var re = new RegExp("(?![^&;]+;)(?!<[^<>]*)(" +
+                $.ui.autocomplete.escapeRegex(this.term) +
+                ")(?![^<>]*>)(?![^&;]+;)", "gi");
+                label = label.replace(re, "<span class='highlight'>$1</span>");
+            }
+            var li = $("<li></li>")
+                .data("item.autocomplete", item)
+                .append($("<a></a>").append(label))
+                .appendTo(ul);
+            if (item.page) {
+                li.addClass('selectable-paginator');
+            }
+            return li;
+        },
+        // Override the default auto-complete suggest.
+        _suggest: function (items) {
+            /* Needed for handling pagination links */
+            var page = $(this.element).data('page');
+            var ul = this.menu.element;
+            if (!page) {
+                ul.empty();
+            }
+            $(this.element).data('page', null);
+            ul.zIndex(this.element.zIndex() + 1);
+            this._renderMenu(ul, items);
+            // jQuery UI menu does not define deactivate
+            if (this.menu.deactivate) this.menu.deactivate();
+            this.menu.refresh();
+            // size and position menu
+            ul.show();
+            this._resizeMenu();
+            ul.position($.extend({of: this.element}, this.options.position));
+            if (this.options.autoFocus) {
+                this.menu.next(new $.Event("mouseover"));
+            }
+        },
+        // Override default trigger for additional change/select logic
+        _trigger: function (type, event, data) {
+            var $input = $(this.element);
+            if (type === "select") {
+                $input.removeClass('ui-state-error');
+                if (data.item && data.item.page) {
+                    // Set current page value
+                    $input.data("page", data.item.page);
+                    $('.selectable-paginator', this.menu).remove();
+                    // Search for next page of results
+                    this.search();
+                    return false;
+                }
+                return this.select(data.item, event);
+            } else if (type === "change") {
+                $input.removeClass('ui-state-error');
+                this._setHidden(data.item);
+                if ($input.val() && !data.item) {
+                    if (!this.allowNew) {
+                        $input.addClass('ui-state-error');
                     }
-                    if (self.allowMultiple && !$(input).hasClass('ui-state-error')) {
-                        $(input).val("");
-                        $(input).data("autocomplete").term = "";
-                    }
-                },
-                select: function (event, ui) {
-                    $(input).removeClass('ui-state-error');
-                    if (ui.item && ui.item.page) {
-                        // Set current page value
-                        $(input).data("page", ui.item.page);
-                        $('.selectable-paginator', self.menu).remove();
-                        // Search for next page of results
-                        $(input).autocomplete("search");
-                        return false;
-                    }
-                    return self.select(ui.item, event);
                 }
-            }).addClass("ui-widget ui-widget-content ui-corner-all");
-            // Override the default auto-complete render.
-            $(input).data("autocomplete")._renderItem = function (ul, item) {
-                /* Adds hook for additional formatting, allows HTML in the label,
-                highlights term matches and handles pagination. */
-                var label = item.label;
-                if (self.options.formatLabel) {
-                    label = self.options.formatLabel.apply(self, [label, item]);
+                if (this.allowMultiple && !$input.hasClass('ui-state-error')) {
+                    $input.val("");
+                    this.term = "";
                 }
-                if (self.options.highlightMatch && this.term) {
-                    var re = new RegExp("(?![^&;]+;)(?!<[^<>]*)(" +
-                    $.ui.autocomplete.escapeRegex(this.term) +
-                    ")(?![^<>]*>)(?![^&;]+;)", "gi");
-                    label = label.replace(re, "<span class='highlight'>$1</span>");
-                }
-                var li =  $("<li></li>")
-                    .data("item.autocomplete", item)
-                    .append($("<a></a>").append(label))
-                    .appendTo(ul);
-                if (item.page) {
-                    li.addClass('selectable-paginator');
-                }
-                return li;
-            };
-            // Override the default auto-complete suggest.
-            $(input).data("autocomplete")._suggest = function (items) {
-                /* Needed for handling pagination links */
-                var page = $(input).data('page');
-                var ul = this.menu.element;
-                if (!page) {
-                    ul.empty();
-                }
-                $(input).data('page', null);
-                ul.zIndex(this.element.zIndex() + 1);
-                this._renderMenu(ul, items);
-                // jQuery UI menu does not define deactivate
-                if (this.menu.deactivate) this.menu.deactivate();
-                this.menu.refresh();
-                // size and position menu
-                ul.show();
-                this._resizeMenu();
-                ul.position($.extend({of: this.element}, this.options.position));
-                if (this.options.autoFocus) {
-                    this.menu.next(new $.Event("mouseover"));
-                }
-            };
-            // Additional work for combobox widgets
-            var selectableType = data.selectableType || data['selectable-type'];
-            if (selectableType === 'combobox') {
-                // Change auto-complete options
-                $(input).autocomplete("option", {
-                    delay: 0,
-                    minLength: 0
-                })
-                .removeClass("ui-corner-all")
-                .addClass("ui-corner-left ui-combo-input");
-                // Add show all items button
-                $("<button>&nbsp;</button>").attr("tabIndex", -1).attr("title", "Show All Items")
-                .insertAfter($(input))
-                .button({
-                    icons: {
-                        primary: self.options.comboboxIcon
-                    },
-                    text: false
-                })
-                .removeClass("ui-corner-all")
-                .addClass("ui-corner-right ui-button-icon ui-combo-button")
-                .click(function () {
-                    // close if already visible
-                    if ($(input).autocomplete("widget").is(":visible")) {
-                        $(input).autocomplete("close");
-                        return false;
-                    }
-                    // pass empty string as value to search for, displaying all results
-                    $(input).autocomplete("search", "");
-                    $(input).focus();
-                    return false;
-                });
             }
+            // Call super trigger
+            // This could be replaced by this._super() with jQuery UI 1.9
+            return $.ui.autocomplete.prototype._trigger.apply(this, arguments);
         }
 	});
 
         */
         $(":input[data-selectable-type=text]", context).djselectable();
         $(":input[data-selectable-type=combobox]", context).djselectable();
-        $(":input[data-selectable-type=hidden]", context).each(function (i, elem) {
-            var hiddenName = $(elem).attr('name');
-            var textName = hiddenName.replace(new RegExp('_1$'), '_0');
-            $(":input[name=" + textName + "][data-selectable-url]").bind(
-                "autocompletechange autocompleteselect",
-                function (event, ui) {
-                    if (ui.item && ui.item.id) {
-                        $(elem).val(ui.item.id);
-                    } else {
-                        $(elem).val("");
-                    }
-                }
-            );
-        });
     };
 
-    /* Monkey-patch Django's dynamic formset, if defined */
-    if (typeof(django) !== "undefined" && typeof(django.jQuery) !== "undefined") {
-        if (django.jQuery.fn.formset) {
-            var oldformset = django.jQuery.fn.formset;
-            django.jQuery.fn.formset = function (opts) {
-                var options = $.extend({}, opts);
-                var addedevent = function (row) {
-                    bindSelectables($(row));
+    function djangoAdminPatches() {
+        /* Monkey-patch Django's dynamic formset, if defined */
+        if (typeof(django) !== "undefined" && typeof(django.jQuery) !== "undefined") {
+            if (django.jQuery.fn.formset) {
+                var oldformset = django.jQuery.fn.formset;
+                django.jQuery.fn.formset = function (opts) {
+                    var options = $.extend({}, opts);
+                    var addedevent = function (row) {
+                        bindSelectables($(row));
+                    };
+                    var added = null;
+                    if (options.added) {
+                        // Wrap previous added function and include call to bindSelectables
+                        var oldadded = options.added;
+                        added = function (row) { oldadded(row); addedevent(row); };
+                    }
+                    options.added = added || addedevent;
+                    return oldformset.call(this, options);
                 };
-                var added = null;
-                if (options.added) {
-                    // Wrap previous added function and include call to bindSelectables
-                    var oldadded = options.added;
-                    added = function (row) { oldadded(row); addedevent(row); };
+            }
+        }
+
+        /* Monkey-patch Django's dismissAddAnotherPopup(), if defined */
+        if (typeof(dismissAddAnotherPopup) !== "undefined" &&
+            typeof(windowname_to_id) !== "undefined" &&
+            typeof(html_unescape) !== "undefined") {
+            var django_dismissAddAnotherPopup = dismissAddAnotherPopup;
+            dismissAddAnotherPopup = function (win, newId, newRepr) {
+                /* See if the popup came from a selectable field.
+                   If not, pass control to Django's code.
+                   If so, handle it. */
+                var fieldName = windowname_to_id(win.name); /* e.g. "id_fieldname" */
+                var field = $('#' + fieldName);
+                var multiField = $('#' + fieldName + '_0');
+                /* Check for bound selectable */
+                var singleWidget = field.data('djselectable');
+                var multiWidget = multiField.data('djselectable');
+                if (singleWidget || multiWidget) {
+                    // newId and newRepr are expected to have previously been escaped by
+                    // django.utils.html.escape.
+                    var item =  {
+                        id: html_unescape(newId),
+                        value: html_unescape(newRepr)
+                    };
+                    if (singleWidget) {
+                        field.djselectable('select', item);
+                    }
+                    if (multiWidget) {
+                        multiField.djselectable('select', item);
+                    }
+                    win.close();
+                } else {
+                    /* Not ours, pass on to original function. */
+                    return django_dismissAddAnotherPopup(win, newId, newRepr);
                 }
-                options.added = added || addedevent;
-                return oldformset.call(this, options);
             };
         }
     }
 
-    /* Monkey-patch Django's dismissAddAnotherPopup(), if defined */
-    if (typeof(dismissAddAnotherPopup) !== "undefined" &&
-        typeof(windowname_to_id) !== "undefined" &&
-        typeof(html_unescape) !== "undefined") {
-        var django_dismissAddAnotherPopup = dismissAddAnotherPopup;
-        dismissAddAnotherPopup = function (win, newId, newRepr) {
-            /* See if the popup came from a selectable field.
-               If not, pass control to Django's code.
-               If so, handle it. */
-            var fieldName = windowname_to_id(win.name); /* e.g. "id_fieldname" */
-            var field = $('#' + fieldName);
-            var multiField = $('#' + fieldName + '_0');
-            /* Check for bound selectable */
-            var singleWidget = field.data('djselectable');
-            var multiWidget = multiField.data('djselectable');
-            if (singleWidget || multiWidget) {
-                // newId and newRepr are expected to have previously been escaped by
-                // django.utils.html.escape.
-                var item =  {
-                    id: html_unescape(newId),
-                    value: html_unescape(newRepr)
-                };
-                if (singleWidget) {
-                    field.djselectable('select', item);
-                }
-                if (multiWidget) {
-                    multiField.djselectable('select', item);
-                }
-                win.close();
-            } else {
-                /* Not ours, pass on to original function. */
-                return django_dismissAddAnotherPopup(win, newId, newRepr);
-            }
-        };
-    }
-
     $(document).ready(function () {
+        // Patch the django admin JS
+        if (typeof(djselectableAdminPatch) === "undefined" || djselectableAdminPatch) {
+            djangoAdminPatches();
+        }
         // Bind existing widgets on document ready
         if (typeof(djselectableAutoLoad) === "undefined" || djselectableAutoLoad) {
             bindSelectables('body');

selectable/tests/qunit/index.html

             bindSelectables('#manual-event');
             var input = $('#id_manualevent');
             var count = 0;
-            input.bind('autocompleteselect', function(e, item) {
+            input.bind('djselectableselect', function(e, item) {
                 count = count + 1;
             });
             var item = {id: "1", value: 'foo'};
             input.djselectable('select', item);
-            equal(count, 1, "autocompleteselect should fire once when manually selected.");
+            equal(count, 1, "djselectableselect should fire once when manually selected.");
         });
 
         test("manual select double bind", function() {
             bindSelectables('#manual-event-double-bind');
             var input = $('#id_manualevent_double_bind');
             var count = 0;
-            input.bind('autocompleteselect', function(e, item) {
+            input.bind('djselectableselect', function(e, item) {
                 count = count + 1;
             });
             var item = {id: "1", value: 'foo'};
             input.djselectable('select', item);
-            equal(count, 1, "autocompleteselect should fire once when manually selected.");
+            equal(count, 1, "djselectableselect should fire once when manually selected.");
         });
 
-        asyncTest("autocompleteselect", function() {
+        asyncTest("djselectableselect", function() {
             expect(1);
             bindSelectables('#select-event');
             var input = $('#id_selectevent');
             var count = 0;
-            input.bind('autocompleteselect', function(e, item) {
+            input.bind('djselectableselect', function(e, item) {
                 count = count + 1;
             });
             // Change source to prevent remote calls
-            input.autocomplete("option", "source", ["javascript", "python"]);
+            input.data("djselectable").source = function (request, response) {
+                response($.ui.autocomplete.filter(["javascript", "python"], request.term));
+            };
             // Simulate typing in and selecting the first item.
             input.val("ja").keydown();
             setTimeout(function() {
                 var enter = jQuery.Event("keydown");
                 enter.keyCode = $.ui.keyCode.ENTER;
                 input.trigger(enter);
-                equal(count, 1, "autocompleteselect should only fire once.");
+                equal(count, 1, "djselectableselect should only fire once.");
                 start();
             }, 500);
         });
 
-        asyncTest("autocompleteselect", function() {
+        asyncTest("double bind djselectableselect", function() {
             expect(1);
             bindSelectables('#select-event-double-bind');
             bindSelectables('#select-event-double-bind');
             var input = $('#id_selectevent_double_bind');
             var count = 0;
-            input.bind('autocompleteselect', function(e, item) {
+            input.bind('djselectableselect', function(e, item) {
                 count = count + 1;
             });
             // Change source to prevent remote calls
-            input.autocomplete("option", "source", ["javascript", "python"]);
+            input.data("djselectable").source = function (request, response) {
+                response($.ui.autocomplete.filter(["javascript", "python"], request.term));
+            };
             // Simulate typing in and selecting the first item.
             input.val("ja").keydown();
             setTimeout(function() {
                 var enter = jQuery.Event("keydown");
                 enter.keyCode = $.ui.keyCode.ENTER;
                 input.trigger(enter);
-                equal(count, 1, "autocompleteselect should only fire once.");
+                equal(count, 1, "djselectableselect should only fire once.");
                 start();
             }, 500);
         });

selectable/tests/widgets.py

+import json
 from urlparse import urlparse
 
 from django import forms
         with self.assertRaises(TypeError):
             self.__class__.widget_cls(dotted_path)
 
+
 class AutoCompleteWidgetTestCase(BaseWidgetTestCase):
     widget_cls = widgets.AutoCompleteWidget
     lookup_cls = ThingLookup
         query = parse.query
         self.assertEqual(query, urlencode(params))
 
+    def test_build_selectable_options(self):
+        "Serialize selectable options as json in data attribute."
+        options = {'autoFocus': True}
+        widget = self.get_widget_instance(attrs={'data-selectable-options': options})
+        attrs = widget.build_attrs()
+        self.assertTrue('data-selectable-options' in attrs)
+        self.assertEqual(attrs['data-selectable-options'], json.dumps(options))
+
 
 class AutoCompleteSelectWidgetTestCase(BaseWidgetTestCase):
     widget_cls = widgets.AutoCompleteSelectWidget
         query = parse.query
         self.assertEqual(query, urlencode(params))
 
+    def test_build_selectable_options(self):
+        "Serialize selectable options as json in data attribute."
+        options = {'autoFocus': True}
+        widget = self.get_widget_instance(attrs={'data-selectable-options': options})
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        self.assertTrue('data-selectable-options' in attrs)
+        self.assertEqual(attrs['data-selectable-options'], json.dumps(options))
+
 
 class AutoComboboxWidgetTestCase(BaseWidgetTestCase):
     widget_cls = widgets.AutoComboboxWidget
         query = parse.query
         self.assertEqual(query, urlencode(params))
 
+    def test_build_selectable_options(self):
+        "Serialize selectable options as json in data attribute."
+        options = {'autoFocus': True}
+        widget = self.get_widget_instance(attrs={'data-selectable-options': options})
+        attrs = widget.build_attrs()
+        self.assertTrue('data-selectable-options' in attrs)
+        self.assertEqual(attrs['data-selectable-options'], json.dumps(options))
+
 
 class AutoComboboxSelectWidgetTestCase(BaseWidgetTestCase):
     widget_cls = widgets.AutoComboboxSelectWidget
         query = parse.query
         self.assertEqual(query, urlencode(params))
 
+    def test_build_selectable_options(self):
+        "Serialize selectable options as json in data attribute."
+        options = {'autoFocus': True}
+        widget = self.get_widget_instance(attrs={'data-selectable-options': options})
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        self.assertTrue('data-selectable-options' in attrs)
+        self.assertEqual(attrs['data-selectable-options'], json.dumps(options))
+
 
 class AutoCompleteSelectMultipleWidgetTestCase(BaseWidgetTestCase):
     widget_cls = widgets.AutoCompleteSelectMultipleWidget
         query = parse.query
         self.assertEqual(query, urlencode(params))
 
+    def test_build_selectable_options(self):
+        "Serialize selectable options as json in data attribute."
+        options = {'autoFocus': True}
+        widget = self.get_widget_instance(attrs={'data-selectable-options': options})
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        self.assertTrue('data-selectable-options' in attrs)
+        self.assertEqual(attrs['data-selectable-options'], json.dumps(options))
+
 
 class AutoComboboxSelectMultipleWidgetTestCase(BaseWidgetTestCase):
     widget_cls = widgets.AutoComboboxSelectMultipleWidget
         query = parse.query
         self.assertEqual(query, urlencode(params))
 
+    def test_build_selectable_options(self):
+        "Serialize selectable options as json in data attribute."
+        options = {'autoFocus': True}
+        widget = self.get_widget_instance(attrs={'data-selectable-options': options})
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        self.assertTrue('data-selectable-options' in attrs)
+        self.assertEqual(attrs['data-selectable-options'], json.dumps(options))