[js/widgets] fix the InOut widget with modern jQuery versions
authorAurelien Campeas <aurelien.campeas@logilab.fr>
Wed, 08 Jan 2014 14:00:31 +0100
changeset 9377 4e0d8f06efbc
parent 9376 2ed0d091e9b1
child 9378 4a668dcfa0a0
[js/widgets] fix the InOut widget with modern jQuery versions Several things are done there: * reduction in size and complexity of the code * the unused defaultsettings are removed * the initial `unlinked` list is now correctly populated from python-side * the unit test is adjusted because it tested an irrelevant implementation detail which is no longer true (but the widget of course still handles correctly the linkto information) Tested with ie7, ie9, chromium, firefox. Tested with jQuery 1.6 (cw 3.17.x) and 1.10. Closes #3154531.
web/data/cubicweb.widgets.js
web/formwidgets.py
web/test/unittest_form.py
--- a/web/data/cubicweb.widgets.js	Thu Aug 01 09:39:43 2013 +0200
+++ b/web/data/cubicweb.widgets.js	Wed Jan 08 14:00:31 2014 +0100
@@ -544,105 +544,77 @@
 // IE things can not handle hide/show options on select, this cloned list solition (should propably have 2 widgets)
 
 (function ($) {
-    var defaultSettings = {
-        bindDblClick: true
-    };
+
     var methods = {
-        __init__: function(fromSelect, toSelect, options) {
-            var settings = $.extend({}, defaultSettings, options);
-            var bindDblClick = settings['bindDblClick'];
-            var $fromNode = $(cw.jqNode(fromSelect));
-            var clonedSelect = $fromNode.clone();
-            var $toNode = $(cw.jqNode(toSelect));
-            var $addButton = $(this.find('.cwinoutadd')[0]);
-            var $removeButton = $(this.find('.cwinoutremove')[0]);
-            // bind buttons
-            var name = this.attr('id');
-            var instanceData = {'fromNode':fromSelect,
-                                'toNode':toSelect,
-                                'cloned':clonedSelect,
-                                'bindDblClick':bindDblClick,
-                                'name': name};
-            $addButton.bind('click', {'instanceData':instanceData}, methods.inOutWidgetAddValues);
-            $removeButton.bind('click', {'instanceData':instanceData}, methods.inOutWidgetRemoveValues);
-            if(bindDblClick){
-                $toNode.bind('dblclick', {'instanceData': instanceData}, methods.inOutWidgetRemoveValues);
-            }
-            methods.inOutWidgetRemplaceSelect($fromNode, $toNode, clonedSelect, bindDblClick, name);
-        },
+        __init__: function(fromSelect, toSelect) {
+            // closed over state
+            var state = {'$fromNode' : $(cw.escape('#' + fromSelect)),
+                         '$toNode'   : $(cw.escape('#' + toSelect)),
+                         'name'      : this.attr('id')};
 
-        inOutWidgetRemplaceSelect: function($fromNode, $toNode, clonedSelect, bindDblClick, name){
-             var $newSelect = clonedSelect.clone();
-             $toNode.find('option').each(function() {
-                 $newSelect.find('$(this)[value='+$(this).val()+']').remove();
-              });
-             var fromparent = $fromNode.parent();
-             if (bindDblClick) {
-                 //XXX jQuery live binding does not seem to work here
-                 $newSelect.bind('dblclick', {'instanceData': {'fromNode':$fromNode.attr('id'),
-                                     'toNode': $toNode.attr('id'),
-                                     'cloned':clonedSelect,
-                                     'bindDblClick':bindDblClick,
-                                     'name': name}},
-                                 methods.inOutWidgetAddValues);
-             }
-             $fromNode.remove();
-             fromparent.append($newSelect);
-        },
+            function sortoptions($optionlist) {
+                var $sorted = $optionlist.find('option').sort(function(opt1, opt2) {
+                    return $(opt1).text() > $(opt2).text() ? 1 : -1;
+                });
+                // this somehow translates to an inplace sort
+                $optionlist.append($sorted);
+            };
+            sortoptions(state.$fromNode);
+            sortoptions(state.$toNode);
 
-        inOutWidgetAddValues: function(event){
-            var $fromNode = $(cw.jqNode(event.data.instanceData.fromNode));
-            var $toNode = $(cw.jqNode(event.data.instanceData.toNode));
-            $fromNode.find('option:selected').each(function() {
-                var option = $(this);
-                var newoption = OPTION({'value':option.val()},
-	 			 value=option.text());
-                $toNode.append(newoption);
-                var hiddenInput = INPUT({
-                    type: "hidden", name: event.data.instanceData.name,
-                    value:option.val()
+            // will move selected options from one list to the other
+            // and call an option handler on each option
+            function moveoptions ($fromlist, $tolist, opthandler) {
+                $fromlist.find('option:selected').each(function(index, option) {
+                    var $option = $(option);
+                    // add a new option to the target list
+                    $tolist.append(OPTION({'value' : $option.val()},
+	 			          $option.text()));
+                    // process callback on the option
+                    opthandler.call(null, $option);
+                    // remove option from the source list
+                    $option.remove();
                 });
-                $toNode.parent().append(hiddenInput);
-            });
-            methods.inOutWidgetRemplaceSelect($fromNode, $toNode, event.data.instanceData.cloned,
-                                              event.data.instanceData.bindDblClick,
-                                              event.data.instanceData.name);
-            // for ie 7 : ie does not resize correctly the select
-            if($.browser.msie && $.browser.version.substr(0,1) < 8){
-                var p = $toNode.parent();
-                var newtoNode = $toNode.clone();
-                if (event.data.instanceData.bindDblClick) {
-                    newtoNode.bind('dblclick', {'fromNode': $fromNode.attr('id'),
-                                                'toNode': $toNode.attr('id'),
-                                                'cloned': event.data.instanceData.cloned,
-                                                'bindDblClick': true,
-                                                'name': event.data.instanceData.name},
-                                   methods.inOutWidgetRemoveValues);
-                }
-                $toNode.remove();
-                p.append(newtoNode);
-            }
-        },
+                // re-sort both lists
+                sortoptions($fromlist);
+                sortoptions($tolist);
+            };
+
+            function addvalues () {
+                moveoptions(state.$fromNode, state.$toNode, function ($option) {
+                    // add an hidden input for the edit controller
+                    var hiddenInput = INPUT({
+                        type: 'hidden', name: state.name,
+                        value : $option.val()
+                    });
+                    state.$toNode.parent().append(hiddenInput);
+                });
+            };
 
-        inOutWidgetRemoveValues: function(event){
-            var $fromNode = $(cw.jqNode(event.data.instanceData.toNode));
-            var $toNode = $(cw.jqNode(event.data.instanceData.fromNode));
-            var name = event.data.instanceData.name.replace(':', '\\:');
-            $fromNode.find('option:selected').each(function(){
-                var option = $(this);
-                var newoption = OPTION({'value':option.val()},
-	 			 value=option.text());
-                option.remove();
-                $fromNode.parent().find('input[name]='+ name).each(function() {
-                    $(this).val()==option.val()?$(this).remove():null;
-               });
-            });
-            methods.inOutWidgetRemplaceSelect($toNode, $fromNode,  event.data.instanceData.cloned,
-                                              event.data.instanceData.bindDblClick,
-                                              event.data.instanceData.name);
+            function removevalues () {
+                moveoptions(state.$toNode, state.$fromNode, function($option) {
+                    // remove hidden inputs for the edit controller
+                    var selector = 'input[name=' + cw.escape(state.name) + ']'
+                    state.$toNode.parent().find(selector).each(function(index, input) {
+                        if ($(input).val() == $option.val()) {
+                            $(input).remove();
+                        }
+                    });
+                });
+            };
+
+            var $this = $(this);
+            $this.find('.cwinoutadd').bind( // 'add >>>' symbol
+                'click', {'state' : state}, addvalues);
+            $this.find('.cwinoutremove').bind( // 'remove <<<' symbol
+                'click', {'state' : state}, removevalues);
+
+            state.$fromNode.bind('dblclick', {'state': state}, addvalues);
+            state.$toNode.bind('dblclick', {'state': state}, removevalues);
+
         }
     };
-    $.fn.cwinoutwidget = function(fromSelect, toSelect, options){
-        return methods.__init__.apply(this, [fromSelect, toSelect, options]);
+    $.fn.cwinoutwidget = function(fromSelect, toSelect) {
+        return methods.__init__.apply(this, [fromSelect, toSelect]);
     };
-})(jQuery);
\ No newline at end of file
+})(jQuery);
--- a/web/formwidgets.py	Thu Aug 01 09:39:43 2013 +0200
+++ b/web/formwidgets.py	Wed Jan 08 14:00:31 2014 +0100
@@ -515,7 +515,8 @@
                                              name=field.dom_id(form),
                                              type="hidden"))
             else:
-                options.append(tags.option(label, value=value))
+                if value not in values:
+                    options.append(tags.option(label, value=value))
         if 'size' not in attrs:
             attrs['size'] = self.default_size
         if 'id' in attrs :
--- a/web/test/unittest_form.py	Thu Aug 01 09:39:43 2013 +0200
+++ b/web/test/unittest_form.py	Wed Jan 08 14:00:31 2014 +0100
@@ -94,8 +94,7 @@
         form.build_context({})
         self.assertEqual(field.widget.values(form, field), (u'toto',))
 
-
-    def test_linkto_field_duplication(self):
+    def test_linkto_field_duplication_inout(self):
         e = self.vreg['etypes'].etype_class('CWUser')(self.request())
         e.eid = 'A'
         e._cw = self.req
@@ -111,7 +110,6 @@
                 self.assertEqual(optionnode.get('value'), str(geid))
                 self.assertEqual(ok, False)
                 ok = True
-        self.assertEqual(ok, True, 'expected option not found')
         inputs = pageinfo.find_tag('input', False)
         self.assertFalse(list(pageinfo.matching_nodes('input', name='__linkto')))