# HG changeset patch # User Aurelien Campeas # Date 1389186031 -3600 # Node ID 4e0d8f06efbc3b899b48c0036627f1eb4ba2f4c8 # Parent 2ed0d091e9b145bbcf7cd949a9a6f3f1e30f3579 [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. diff -r 2ed0d091e9b1 -r 4e0d8f06efbc web/data/cubicweb.widgets.js --- 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); diff -r 2ed0d091e9b1 -r 4e0d8f06efbc web/formwidgets.py --- 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 : diff -r 2ed0d091e9b1 -r 4e0d8f06efbc web/test/unittest_form.py --- 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')))