From b8b0b54efd89ec85141bd45041443c8ee4a98249 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Mon, 18 Mar 2019 14:53:39 +0100
Subject: [PATCH] forms: rewrite jsonp select widget with separate js &
 template (#31506)

---
 tests/test_form_pages.py                      |  12 +-
 wcs/qommon/form.py                            | 129 ++----------------
 wcs/qommon/static/js/qommon.forms.js          |  67 +++++++++
 .../qommon/forms/widgets/select_jsonp.html    |   9 ++
 wcs/root.py                                   |   5 +
 5 files changed, 93 insertions(+), 129 deletions(-)
 create mode 100644 wcs/qommon/templates/qommon/forms/widgets/select_jsonp.html

diff --git a/tests/test_form_pages.py b/tests/test_form_pages.py
index eda37dd4a..3e763d8b2 100644
--- a/tests/test_form_pages.py
+++ b/tests/test_form_pages.py
@@ -3875,19 +3875,9 @@ def test_form_jsonp_item_field(http_requests, pub):
     formdef.data_class().wipe()
 
     resp = get_app(pub).get('/test/')
-    assert 'url: "http://remote.example.net/jsonp"' in resp.body
+    assert 'data-select2-url="http://remote.example.net/jsonp"' in resp.body
     assert 'select2.min.js' in resp.body
 
-    formdef.fields = [
-            fields.ItemField(id='1', label='string', type='item',
-                data_source={'type': 'jsonp', 'value': '[var_XXX]'}),
-            ]
-    formdef.store()
-
-    resp = get_app(pub).get('/test/')
-    assert 'url: function(' in resp.body
-    assert "wcs_base_url = '[var_XXX]';" in resp.body
-
 def test_form_string_regex_field_submit(pub):
     formdef = create_formdef()
     formdef.fields = [fields.StringField(id='0', label='string', type='string',
diff --git a/wcs/qommon/form.py b/wcs/qommon/form.py
index 7be78bce3..53cb8efa6 100644
--- a/wcs/qommon/form.py
+++ b/wcs/qommon/form.py
@@ -1791,60 +1791,27 @@ class RankedItemsWidget(CompositeWidget):
 
 
 class JsonpSingleSelectWidget(Widget):
+    template_name = 'qommon/forms/widgets/select_jsonp.html'
+
     def __init__(self, name, value=None, url=None, **kwargs):
-        Widget.__init__(self, name, value=value, **kwargs)
+        super(JsonpSingleSelectWidget, self).__init__(name, value=value, **kwargs)
         self.url = url
 
     def add_media(self):
-        get_response().add_javascript(['jquery.js', 'select2.js'])
+        get_response().add_javascript(['jquery.js', '../../i18n.js', 'qommon.forms.js', 'select2.js'])
         get_response().add_css_include('../js/select2/select2.css')
 
-    def render_content(self):
+    def get_display_value(self):
         if self.value is None:
             value = None
         else:
             value = htmlescape(self.value)
-        r = TemplateIO(html=True)
-
-        attrs = {'id': 'form_' + self.name}
-        r += htmltag('select', name=self.name, value=value, **attrs)
-        r += htmltext('</select>')
-
-        attrs = {'id': 'form_' + self.name + '_display'}
-        if value and get_session().jsonp_display_values:
-            key = '%s_%s' % (self.url, value)
-            if key in get_session().jsonp_display_values:
-                attrs['value'] = get_session().jsonp_display_values.get(key)
-        r += htmltag('input', xml_end=True, type="hidden", name=self.name + '_display', **attrs)
-        initial_display_value = attrs.get('value')
-
-        # init select2 widget
-        allowclear = ""
-        if not self.required:
-            allowclear = "placeholder: '...', allowClear: true,"
-
-        r += htmltext("""
-<script id="script_%(id)s">
-$(document).ready(function() {
-var wcs_select2_%(id)s = $("#form_%(id)s").select2({
-    minimumInputLength: 1,
-    %(allowclear)s
-    language: {
-      errorLoading: function() { return "%(errorloading)s"; },
-      noResults: function () { return "%(nomatches)s"; },
-      inputTooShort: function (input, min) { return "%(tooshort)s"; },
-      loadingMore: function () { return "%(loadmore)s"; },
-      searching: function () { return "%(searching)s"; },
-    },
-    ajax: {
-""" % {'id': self.name,
-                'allowclear': allowclear,
-                'errorloading': _('The results could not be loaded'),
-                'nomatches': _('No matches found'),
-                'tooshort': _('Please enter more characters'),
-                'loadmore': _('Loading more results...'),
-                'searching': _('Searching...')})
+        if not value or not get_session().jsonp_display_values:
+            return None
+        key = '%s_%s' % (self.url, value)
+        return get_session().jsonp_display_values.get(key)
 
+    def get_select2_url(self):
         if Template.is_template_string(self.url):
             vars = get_publisher().substitutions.get_context_variables()
             # skip variables that were not set (None)
@@ -1852,81 +1819,7 @@ var wcs_select2_%(id)s = $("#form_%(id)s").select2({
             url = misc.get_variadic_url(self.url, vars, encode_query=False)
         else:
             url = self.url
-
-        if not '[var_' in url:
-            # if the url is not parametric, set the url directly
-            r += htmltext("""url: "%(url)s",""" % {'url': url})
-        else:
-            # otherwise declare that as a function.
-            r += htmltext("""url: function() { return $(this).data('select2').options.options.ajax.url; },""")
-
-        # setting up the select2 widget continues here
-        r += htmltext("""
-        delay: 250,
-        dataType: 'json',
-        data: function (params) {
-            return {
-                q: params.term,
-                page_limit: 10
-            };
-        },
-        processResults: function (data, params) {
-            return {results: data.data};
-        }
-    },
-    formatResult: function(result) { return result.text; }
-});""")
-
-        if '[var_' in url:
-            # if this is a parametric url, store template url and hook to the
-            # appropriate onchange event to give the url to select2
-            r += htmltext("""
-$("#form_%(id)s").data('select2').options.wcs_base_url = '%(url)s';
-""" % {'id': self.name, 'url': url})
-            variables = re.findall(r'\[(var_.+?)\]', url)
-            r += htmltext("""
-function url_replace_%(id)s() {
-    var url = $("#form_%(id)s").data('select2').options.wcs_base_url;""" % {'id': self.name})
-            for variable in variables:
-                r += htmltext("""
-    selector = '#' + $('#%(variable)s').data('valuecontainerid');
-    url = url.replace('[%(variable)s]', $(selector).val() || '');""" % {'variable': variable})
-            r += htmltext("""
-    $("#form_%(id)s").data('select2').options.options.ajax.url = url;
-    $("#form_%(id)s").data('select2').results.clear();
-}
-""" % {'id': self.name} )
-            for variable in variables:
-                r += htmltext("""
-$('#%(variable)s').change(url_replace_%(id)s);
-$('#%(variable)s').change();
-""" % {'id': self.name, 'variable': variable})
-
-        # finish setting up select2, update the _display hidden field with the
-        # selected text
-        r += htmltext("""
-
-$("#form_%(id)s").change(function() {
-    var text = $("#form_%(id)s").next().find('.select2-selection__rendered').text();
-    if ($("#form_%(id)s").next().find('.select2-selection__rendered .select2-selection__clear').length) {
-        text = text.slice(1);
-    }
-    $('#form_%(id)s_display').val(text);
-});
-
-""" % {'id': self.name})
-
-        if initial_display_value:
-            r += htmltext("""
-var option = $('<option>').attr('value', "%(value)s").text("%(text)s");
-option.appendTo(wcs_select2_%(id)s);
-wcs_select2_%(id)s.val("%(value)s").trigger('change');
-$("#form_%(id)s").select2("data", {id: "%(value)s", text: "%(text)s"});
-""" % {'id': self.name, 'value': self.value, 'text': initial_display_value})
-
-        r += htmltext("""});</script>""")
-
-        return r.getvalue()
+        return url
 
     def parse(self, request=None):
         if request and request.form.get(self.name) and request.form.get(self.name + '_display'):
diff --git a/wcs/qommon/static/js/qommon.forms.js b/wcs/qommon/static/js/qommon.forms.js
index 8f3914725..1e0ee6453 100644
--- a/wcs/qommon/static/js/qommon.forms.js
+++ b/wcs/qommon/static/js/qommon.forms.js
@@ -118,4 +118,71 @@ $(function() {
       $(this).parents('form').trigger('wcs:change', {modified_field: modified_field});
   });
   $('form div[data-live-source]').parents('form').trigger('wcs:change');
+
+  /* searchable select */
+  $('select[data-select2-url]').each(function(i, elem) {
+    var required = $(elem).data('required');
+    // create an additional hidden field to hold the label of the selected
+    // option, it is necessary as the server may not have any knowledge of
+    // possible options.
+    var $input_display_value = $('<input>', {
+            type: 'hidden',
+            name: $(elem).attr('name') + '_display',
+            value: $(elem).data('initial-display-value')
+    });
+    $input_display_value.insertAfter($(elem));
+    var options = {
+      minimumInputLength: 1,
+      formatResult: function(result) { return result.text; },
+      language: {
+        errorLoading: function() { return WCS_I18N.s2_errorloading; },
+        noResults: function () { return WCS_I18N.s2_nomatches; },
+        inputTooShort: function (input, min) { return WCS_I18N.s2_tooshort; },
+        loadingMore: function () { return WCS_I18N.s2_loadmore; },
+        searching: function () { return WCS_I18N.s2_searching; }
+      }
+    };
+    if (!required) {
+      options.placeholder = '...';
+      options.allowClear = true;
+    }
+    options.ajax = {
+      delay: 250,
+      dataType: 'json',
+      data: function(params) {
+        return {q: params.term, page_limit: 10};
+      },
+      processResults: function (data, params) {
+        return {results: data.data};
+      },
+      url: function() {
+        var url = $(elem).data('select2-url');
+        url = url.replace(/\[var_.+?\]/g, function(match, g1, g2) {
+          // compatibility: if there are [var_...] references in the URL
+          // replace them by looking for other select fields on the same
+          // page.
+          var related_select = $('#' + match.slice(1, -1));
+          var value_container_id = $(related_select).data('valuecontainerid');
+          return $('#' + value_container_id).val() || '';
+        });
+        return url;
+      }
+    };
+    var select2 = $(elem).select2(options);
+    $(elem).on('change', function() {
+      // update _display hidden field with selected text
+      var text = $(elem).find(':selected').first().text();
+      $input_display_value.val(text);
+    });
+    if ($input_display_value.val()) {
+      // if the _display hidden field was created with an initial value take it
+      // and create a matching <option> in the real <select> widget, and use it
+      // to set select2 initial state.
+      var option = $('<option></option>', {value: $(elem).data('value')});
+      option.appendTo($(elem));
+      option.text($input_display_value.val());
+      select2.val($(elem).data('value')).trigger('change');
+      $(elem).select2('data', {id: $(elem).data('value'), text: $(elem).data('initial-display-value')});
+    }
+  });
 });
diff --git a/wcs/qommon/templates/qommon/forms/widgets/select_jsonp.html b/wcs/qommon/templates/qommon/forms/widgets/select_jsonp.html
new file mode 100644
index 000000000..3913a0c6b
--- /dev/null
+++ b/wcs/qommon/templates/qommon/forms/widgets/select_jsonp.html
@@ -0,0 +1,9 @@
+{% extends "qommon/forms/widget.html" %}
+{% block widget-control %}
+<select id="form_{{widget.name}}" name="{{widget.name}}"
+    data-select2-url="{{widget.get_select2_url}}"
+    {% if widget.value %}data-value="{{ widget.value }}"{% endif %}
+    data-required="{% if widget.is_required %}true{% endif %}"
+    data-initial-display-value="{{widget.get_display_value|default_if_none:''}}">
+</select>
+{% endblock %}
diff --git a/wcs/root.py b/wcs/root.py
index 899e65454..e02fcb8d4 100644
--- a/wcs/root.py
+++ b/wcs/root.py
@@ -372,6 +372,11 @@ class RootDirectory(Directory):
             'map_zoom_in': _('Zoom in'),
             'map_zoom_out': _('Zoom out'),
             'map_display_position': _('Display my position'),
+            's2_errorloading': _('The results could not be loaded'),
+            's2_nomatches': _('No matches found'),
+            's2_tooshort': _('Please enter more characters'),
+            's2_loadmore': _('Loading more results...'),
+            's2_searching': _('Searching...'),
         }
         return 'WCS_I18N = %s;\n' % json.dumps(strings)
 
-- 
2.20.1

