Commits

Mark Lavin committed 314b6d0

Added multiple select widgets and fields.

Comments (0)

Files changed (4)

selectable/forms/fields.py

 from django.utils.translation import ugettext as _
 
 from selectable.forms.widgets import AutoCompleteSelectWidget, AutoComboboxSelectWidget
+from selectable.forms.widgets import AutoCompleteSelectMultipleWidget, AutoComboboxSelectMultipleWidget
 
 __all__ = (
     'AutoCompleteSelectField',
     'AutoComboboxSelectField',
+    'AutoCompleteSelectMultipleField',
+    'AutoComboboxSelectMultipleField',
 )
 
 
         if value in EMPTY_VALUES:
             return None
         if isinstance(value, list):
-            # Input comes from a AutoCompleteSelectWidget. It's two
+            # Input comes from an AutoComplete widget. It's two
             # components: text and id
             if len(value) != 2:
                 raise ValidationError(self.error_messages['invalid_choice'])
                     if value[0]:
                         raise ValidationError(self.error_messages['invalid_choice'])
                     else:
-                        raise ValidationError(self.error_messages['required'])
+                        return None
                 value = lookup.create_item(value[0])  
             else:
                 value = lookup.get_item(value[1])
                     raise ValidationError(self.error_messages['invalid_choice'])
         return value
 
+
 class AutoComboboxSelectField(AutoCompleteSelectField):
     widget = AutoComboboxSelectWidget
 
+
+class AutoCompleteSelectMultipleField(forms.Field):
+    widget = AutoCompleteSelectMultipleWidget
+
+    default_error_messages = {
+        'invalid_choice': _(u'Select a valid choice. That choice is not one of the available choices.'),
+    }
+
+    def __init__(self, lookup_class, *args, **kwargs):
+        self.lookup_class = lookup_class
+        kwargs['widget'] = self.widget(lookup_class)
+        super(AutoCompleteSelectMultipleField, self).__init__(*args, **kwargs)
+
+
+    def to_python(self, value):
+        if value in EMPTY_VALUES:
+            return None
+        if isinstance(value, list):
+            # Input comes from an AutoComplete widget. It's two
+            # components: text and id
+            if len(value) != 2:
+                raise ValidationError(self.error_messages['invalid_choice'])
+            lookup =self.lookup_class()
+            if value[1] in EMPTY_VALUES:
+                return None
+            else:
+                ids = value[1]
+                if not isinstance(ids, list):
+                    ids = [ids]
+                value = []
+                for v in ids:
+                    item = lookup.get_item(v)
+                    if item is None:
+                        raise ValidationError(self.error_messages['invalid_choice'])
+                    value.append(item)
+        return value
+
+
+class AutoComboboxSelectMultipleField(AutoCompleteSelectMultipleField):
+    widget = AutoComboboxSelectMultipleWidget
+

selectable/forms/widgets.py

 from django import forms
 from django.conf import settings
+from django.forms.util import flatatt
+from django.utils.encoding import force_unicode
+from django.utils.safestring import mark_safe
 
 
 __all__ = (
     'AutoCompleteWidget',
     'AutoCompleteSelectWidget',
     'AutoComboboxWidget',
-    'AutoComboboxSelectWidget'
+    'AutoComboboxSelectWidget',
+    'AutoCompleteSelectMultipleWidget',
+    'AutoComboboxSelectMultipleWidget',
 )
 
 
 
 
 class AutoComboboxWidget(AutoCompleteWidget):
-    
-    def __init__(self, lookup_class, *args, **kwargs):
-        super(AutoComboboxWidget, self).__init__(lookup_class, *args, **kwargs)
-        self.attrs[u'data-selectable-type'] = 'combobox'
 
     def build_attrs(self, extra_attrs=None, **kwargs):
         attrs = super(AutoComboboxWidget, self).build_attrs(extra_attrs, **kwargs)
         return [None, None]
 
 
+class LookupMultipleHiddenInput(forms.MultipleHiddenInput):
+
+    def __init__(self, lookup_class, *args, **kwargs):
+        self.lookup_class = lookup_class
+        super(LookupMultipleHiddenInput, self).__init__(*args, **kwargs)
+
+    def render(self, name, value, attrs=None, choices=()):
+        lookup = self.lookup_class()
+        if value is None: value = []
+        final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
+        id_ = final_attrs.get('id', None)
+        inputs = []
+        for i, v in enumerate(value):
+            input_attrs = dict(value=force_unicode(v), **final_attrs)
+            if id_:
+                # An ID attribute was given. Add a numeric index as a suffix
+                # so that the inputs don't all have the same ID attribute.
+                input_attrs['id'] = '%s_%s' % (id_, i)
+            if v:
+                item = lookup.get_item(v)
+                input_attrs['title'] = lookup.get_item_value(item)
+            inputs.append(u'<input%s />' % flatatt(input_attrs))
+        return mark_safe(u'\n'.join(inputs))
+
+
+class AutoCompleteSelectMultipleWidget(forms.MultiWidget):
+
+    def __init__(self, lookup_class, *args, **kwargs):
+        self.lookup_class = lookup_class
+        widgets = [
+            AutoCompleteWidget(lookup_class, allow_new=False, attrs={u'data-selectable-multiple': 'true'}),
+            LookupMultipleHiddenInput(lookup_class)
+        ]
+        super(AutoCompleteSelectMultipleWidget, self).__init__(widgets, *args, **kwargs)
+
+    def decompress(self, value):
+        if value:
+            if not isinstance(value, list):
+                value = [value]
+            return [None, value]
+        return [None, None]
+
+
+class AutoComboboxSelectMultipleWidget(forms.MultiWidget):
+
+    def __init__(self, lookup_class, *args, **kwargs):
+        self.lookup_class = lookup_class
+        widgets = [
+            AutoComboboxWidget(lookup_class, allow_new=False, attrs={u'data-selectable-multiple': 'true'}),
+            LookupMultipleHiddenInput(lookup_class)
+        ]
+        super(AutoComboboxSelectMultipleWidget, self).__init__(widgets, *args, **kwargs)
+
+    def decompress(self, value):
+        if value:
+            if not isinstance(value, list):
+                value = [value]
+            return [None, value]
+        return [None, None]
+

selectable/static/css/dj.selectable.css

+ul.selectable-deck {
+    list-style: none outside none;
+}
+ul.selectable-deck li.selectable-deck-item {
+    margin: 0;
+    padding: 0.4em;
+}
+ul.selectable-deck li.selectable-deck-item .selectable-deck-remove {
+    float: right;
+}

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

             if (typeof allowAttr !== 'undefined' && allowAttr === 'true') {
                 allowNew = true;
             }
-            if (type === 'combobox' || type === 'text') {
-                $(input).autocomplete({
-                    source: url,
-                    change: function(event, ui) {
-                        if (!ui.item) {
-                            if (!allowNew) {
-                                $(input).val("");
-				                $(input).data("autocomplete").term = "";
-				                return false;
-                            }
+
+            var multipleAttr = $(input).attr('data-selectable-multiple');
+            var allowMultiple = false;
+            var deck = null;
+            var textName = $(input).attr('name');
+            var hiddenName = textName.replace('_0', '_1');
+            var hiddenSelector = 'input[type=hidden][name=' + hiddenName + ']';
+            if (typeof multipleAttr !== 'undefined' && multipleAttr === 'true') {
+                allowMultiple = true;
+                allowNew = false;
+                $(input).val("");
+                deck = $('<ul>').addClass('ui-widget selectable-deck');
+                $(input).after(deck);
+                $(hiddenSelector).each(function(i, elem) {
+                    $('<li>')
+                    .text($(elem).attr('title'))
+                    .addClass('selectable-deck-item')
+                    .appendTo(deck)
+                    .append(
+                        $('<div>')
+                        .addClass('selectable-deck-remove')
+                        .append(
+                            $('<button>')
+                            .button({
+                                icons: {
+                                    primary: "ui-icon-close"
+                                },
+                                text: false
+                            })
+                            .click(function() {
+                                newInput.remove();
+                                $(this).parent('li').remove();
+                            })
+                        )
+                    );
+                });
+            }
+
+            $(input).autocomplete({
+                source: url,
+                change: function(event, ui) {
+                    if (!ui.item) {
+                        if (!allowNew) {
+                            $(input).val("");
+			                $(input).data("autocomplete").term = "";
+			                return false;
+                        } 
+                    } 
+                    if (allowMultiple) {
+                        $(input).val("");
+	                    $(input).data("autocomplete").term = "";
+                    }
+                },
+                select: function(event, ui) {
+                    if (ui.item && allowMultiple) {
+                        $(input).val("");
+		                $(input).data("autocomplete").term = "";
+                        if ($(hiddenSelector + '[value=' + ui.item.id + ']').length === 0) {
+                            // TODO: This won't work in IE...
+                            var newInput = $('<input>').attr({
+                                type: 'hidden',
+                                name: hiddenName,
+                                value: ui.item.id,
+                                title: ui.item.value
+                            });
+                            $(input).after(newInput);
+                            $('<li>')
+                            .text(ui.item.value)
+                            .addClass('selectable-deck-item')
+                            .appendTo(deck)
+                            .append(
+                                $('<div>')
+                                .addClass('selectable-deck-remove')
+                                .append(
+                                    $('<button>')
+                                    .button({
+                                        icons: {
+                                            primary: "ui-icon-close"
+                                        },
+                                        text: false
+                                    })
+                                    .click(function() {
+                                        newInput.remove();
+                                        $(this).parent('li').remove();
+                                    })
+                                )
+                            );
                         }
                     }
-                }).addClass("ui-widget ui-widget-content ui-corner-all");
-                if (type === 'combobox') {
-                    // Change auto-complete options
-                    $(input).autocomplete("option", {
-                        delay: 0,
-                        minLength: 0
-                    })
-                    .removeClass("ui-corner-all")
-                    .addClass("ui-corner-left ui-combo-input");
+                }
+            }).addClass("ui-widget ui-widget-content ui-corner-all");
+            if (type === 'combobox') {
+                // Change auto-complete options
+                $(input).autocomplete("option", {
+                    delay: 0,
+                    minLength: 0
+                })
+                .removeClass("ui-corner-all")
+                .addClass("ui-corner-left ui-combo-input");
 
-                    $("<button>&nbsp;</button>").attr("tabIndex", -1).attr("title", "Show All Items")
-                    .insertAfter($(input))
-                    .button({
-                        icons: {
-                            primary: "ui-icon-triangle-1-s"
-                        },
-                        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;
-                        }
+                $("<button>&nbsp;</button>").attr("tabIndex", -1).attr("title", "Show All Items")
+                .insertAfter($(input))
+                .button({
+                    icons: {
+                        primary: "ui-icon-triangle-1-s"
+                    },
+                    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;
-                    });
-                }
-            } else if (type === 'hidden') {
-                var hiddenName = $(input).attr('name');
-                var textName = hiddenName.replace('_1', '_0');
-                $(":input[name=" + textName + "][data-selectable-url]").bind("autocompletechange", function(event, ui) {
-                    if (ui.item && ui.item.id) {
-                        $(input).val(ui.item.id);
-                    } else {
-                        $(input).val("");
-                    }
+                    // pass empty string as value to search for, displaying all results
+                    $(input).autocomplete("search", "");
+                    $(input).focus();
+                    return false;
                 });
             }
         }
 	});
-
 })(jQuery);
 
 $(document).ready(function() {
-    $(":input[data-selectable-type]").each(function(i, elem) {
-        $(elem).djselectable();
+    $(":input[data-selectable-type=text]").djselectable();
+    $(":input[data-selectable-type=combobox]").djselectable();
+    $(":input[data-selectable-type=hidden]").each(function(i, elem) {
+        var hiddenName = $(elem).attr('name');
+        var textName = hiddenName.replace('_1', '_0');
+        $(":input[name=" + textName + "][data-selectable-url]").bind("autocompletechange", function(event, ui) {
+            if (ui.item && ui.item.id) {
+                $(elem).val(ui.item.id);
+            } else {
+                $(elem).val("");
+            }
+        });
     });
 });
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.