Make the DropDownMultiple widget degrade properly.
authorMatthijs Kooijman <matthijs@stdin.nl>
Tue, 3 Feb 2009 17:36:06 +0000 (18:36 +0100)
committerMatthijs Kooijman <matthijs@stdin.nl>
Tue, 3 Feb 2009 17:36:06 +0000 (18:36 +0100)
DropDownMultiple now extends the original SelectMultiple widget and
always renders both widgets. The DropDownMultiple widget is disabled and
hidden by default in the HTML and is enabled by Javascript code, keeping
things working even when Javascript is not available.

As an added advantage, we can now reuse some of the code from
SelectMultiple. In particular, by using the render_options method, we
should now also support more complicated (like grouped) lists of
choices.

tools/widgets/dropdownmultiple.py

index 37e17e48714cc386bb776093d6dec099becb2051..5bbe27ef5691c00deff4c674707aad24624e1d9a 100644 (file)
@@ -1,26 +1,27 @@
+# Code taken from http://www.djangosnippets.org/snippets/747/
 # -*- coding: utf-8 -*-
 from django.forms import widgets
 from django.utils.safestring import mark_safe
 from django.utils.datastructures import MultiValueDict
-from django.newforms.util import flatatt
+from django.forms.util import flatatt
 
 TPL_OPTION = """<option value="%(value)s" %(selected)s>%(desc)s</option>"""
 
 TPL_SELECT = """
-<select class="dropdown_select" %(attrs)s>
+<select class="dropdown_select" %(attrs)s disabled="disabled">
 %(opts)s
 </select>
 """
 
 TPL_SCRIPT = """
 <script>
-    $('span#%(id)s>select.dropdown_select').change(function(){
-        var pattern = 'span#%(id)s>select.dropdown_select';
+    $('span#%(id)s_multiple>select.dropdown_select').change(function(){
+        var pattern = 'span#%(id)s_multiple>select.dropdown_select';
         var last_item = $(pattern+':last');
 
         if (last_item.val()) {
-            last_item.clone(true).appendTo($('span#%(id)s'));
-            $('span#%(id)s').append(' ');
+            last_item.clone(true).appendTo($('span#%(id)s_multiple'));
+            $('span#%(id)s_multiple').append(' ');
         };
 
         var values = [];
@@ -33,25 +34,40 @@ TPL_SCRIPT = """
             }
         };
     });
+    $(document).ready(function(){
+        // Since we do graceful fallback, the JScript driven interface
+        // is hidden and disabled by default and the plain HTML
+        // interface is shown. Here we swap them around.
+        $('select#%(id)s').hide();
+        $('select#%(id)s').attr('disabled', true);
+        $('span#%(id)s_multiple').show();
+        $('span#%(id)s_multiple>select.dropdown_select').attr('disabled', false);
+    });
 </script>
 """
 
 TPL_FULL = """
-<span class="dropdown_multiple" id="%(id)s">
+<span class="dropdown_multiple" id="%(id)s_multiple" style="display:none">
 %(values)s
 %(script)s
 </span>
 """
 
-class DropDownMultiple(widgets.Widget):
-    choices = None
-
+class DropDownMultiple(widgets.SelectMultiple):
     def __init__(self, attrs=None, choices=()):
-        self.choices = choices
+        super(DropDownMultiple, self).__init__(attrs=attrs, choices=choices)
+    
+    def render(self, name, value, attrs=None, choices=()):
+        # Always render both the default SelectMultiple and our
+        # javascript assisted version. This allows for graceful
+        # degradation when javascript is not available or enabled
+        # (together with the javascript code above).
+        nonjs_output = super(DropDownMultiple, self).render(name, value, attrs=attrs, choices=choices)
+        js_output    = self.render_js(name, value, attrs=attrs, choices=choices)
 
-        super(DropDownMultiple, self).__init__(attrs)
+        return nonjs_output + js_output
 
-    def render(self, name, value, attrs=None, choices=()):
+    def render_js(self, name, value, attrs=None, choices=()):
         if value is None: value = []
         final_attrs = self.build_attrs(attrs, name=name)
 
@@ -60,17 +76,17 @@ class DropDownMultiple(widgets.Widget):
         del final_attrs['id']
 
         # Insert blank value
-        choices = [('','---')] + list(self.choices)
+        choices = [('','---')] + list(choices)
 
         # Build values
         items = []
         for val in value:
-            opts = "\n".join([TPL_OPTION %{'value': k, 'desc': v, 'selected': val == k and 'selected="selected"' or ''} for k, v in choices])
+            opts = self.render_options(choices, [val])
             
             items.append(TPL_SELECT %{'attrs': flatatt(final_attrs), 'opts': opts})
 
         # Build blank value
-        opts = "\n".join([TPL_OPTION %{'value': k, 'desc': v, 'selected': ''} for k, v in choices])
+        opts = self.render_options(choices, [''])
         items.append(TPL_SELECT %{'attrs': flatatt(final_attrs), 'opts': opts})
 
         script = TPL_SCRIPT %{'id': id}
@@ -78,8 +94,9 @@ class DropDownMultiple(widgets.Widget):
 
         return mark_safe(output)
 
-    def value_from_datadict(self, data, files, name):
-        if isinstance(data, MultiValueDict):
-            return [i for i in data.getlist(name) if i]
-        
-        return data.get(name, None)
+    def value_from_datadict(self, *args, **kwargs):
+        # Let our parent take care of this, but filter out the empty
+        # value (which is usually present from the last unused
+        # dropdown).
+        values = super(DropDownMultiple, self).value_from_datadict(*args, **kwargs)
+        return [i for i in values if i]