CRM-15172 - Add chainSelect method to CRM_Core_Form
authorColeman Watts <coleman@civicrm.org>
Sun, 24 Aug 2014 20:11:51 +0000 (21:11 +0100)
committerColeman Watts <coleman@civicrm.org>
Sun, 24 Aug 2014 20:36:46 +0000 (21:36 +0100)
CRM/Core/BAO/Location.php
CRM/Core/Form.php
CRM/Core/Page/AJAX/Location.php
css/civicrm.css
js/Common.js

index 092e86e08936ca1dd8e6600b640d118b22ea9b4e..e7108aaea6d18ea61d1658bcafbe5f69f07099a7 100644 (file)
@@ -397,5 +397,53 @@ WHERE e.id = %1";
       }
     }
   }
+
+  /**
+   * @param mixed $values
+   * @param string $valueType
+   * @param bool $flatten
+   *
+   * @return array
+   */
+  static function getChainSelectValues($values, $valueType, $flatten = FALSE) {
+    if (!$values) {
+      return array();
+    }
+    $values = (array) $values;
+    $elements = array();
+    $list = &$elements;
+    $method = $valueType == 'country' ? 'stateProvinceForCountry' : 'countyForState';
+    foreach ($values as $val) {
+      $result = CRM_Core_PseudoConstant::$method($val);
+
+      // Format for quickform
+      if ($flatten) {
+        // Option-groups for multiple categories
+        if ($result && count($values) > 1) {
+          $elements["crm_optgroup_$val"] = CRM_Core_PseudoConstant::$valueType($val, FALSE);
+        }
+        $elements += $result;
+      }
+
+      // Format for js
+      else {
+       // Option-groups for multiple categories
+        if ($result && count($values) > 1) {
+          $elements[] = array(
+            'value' => CRM_Core_PseudoConstant::$valueType($val, FALSE),
+            'children' => array(),
+          );
+          $list = & $elements[count($elements) - 1]['children'];
+        }
+        foreach ($result as $id => $name) {
+          $list[] = array(
+            'value' => $name,
+            'key' => $id,
+          );
+        }
+      }
+    }
+    return $elements;
+  }
 }
 
index 3fa2bed56f1a640b2257c7c50bba12b56185ace6..76d4a6c3d34770bac91ed0626dd36e9065a3d7ec 100644 (file)
@@ -145,6 +145,12 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
    */
   CONST CB_PREFIX = 'mark_x_', CB_PREFIY = 'mark_y_', CB_PREFIZ = 'mark_z_', CB_PREFIX_LEN = 7;
 
+  /**
+   * @internal to keep track of chain-select fields
+   * @var array
+   */
+  private $_chainSelectFields = array();
+
   /**
    * Constructor for the basic form page
    *
@@ -255,8 +261,13 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
     $attributes = '', $required = FALSE, $extra = NULL
   ) {
     // Normalize this property
-    if ($type == 'select' && is_array($extra) && !empty($extra['multiple'])) {
-      $extra['multiple'] = 'multiple';
+    if ($type == 'select' && is_array($extra)) {
+      if (!empty($extra['multiple'])) {
+        $extra['multiple'] = 'multiple';
+      }
+      else {
+        unset($extra['multiple']);
+      }
     }
     $element = $this->addElement($type, $name, $label, $attributes, $extra);
     if (HTML_QuickForm::isError($element)) {
@@ -645,6 +656,7 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
    * @access public
    */
   function toSmarty() {
+    $this->preProcessChainSelectFields();
     $renderer = $this->getRenderer();
     $this->accept($renderer);
     $content = $renderer->toArray();
@@ -1710,5 +1722,67 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
         break;
     }
   }
+
+  /**
+   * Create a chain-select target field. All settings are optional; the defaults usually work.
+   *
+   * @param string $elementName
+   * @param array $settings
+   *
+   * @return HTML_QuickForm_Element
+   */
+  public function addChainSelect($elementName, $settings = array()) {
+    $props = $settings += array(
+      'control_field' => str_replace(array('state_province', 'StateProvince', 'county', 'County'), array('country', 'Country', 'state_province', 'StateProvince'), $elementName),
+      'data-callback' => strpos($elementName, 'rovince') ? 'civicrm/ajax/jqState' : 'civicrm/ajax/jqCounty',
+      'label' => strpos($elementName, 'rovince') ? ts('State / Province') : ts('County'),
+      'data-empty-prompt' => strpos($elementName, 'rovince') ? ts('Choose country first') : ts('Choose state first'),
+      'data-none-prompt' => ts('- N/A -'),
+      'multiple' => FALSE,
+      'required' => FALSE,
+      'placeholder' => empty($settings['required']) ? ts('- none -') : ts('- select -'),
+    );
+    CRM_Utils_Array::remove($props, 'label', 'required', 'control_field');
+    $props['class'] = (empty($props['class']) ? '' : "{$props['class']} ") . 'crm-chain-select-target crm-select2';
+    $props['data-select-prompt'] = $props['placeholder'];
+    $props['data-name'] = $elementName;
+
+    $this->_chainSelectFields[$settings['control_field']] = $elementName;
+
+    return $this->add('select', $elementName, $settings['label'], array(), $settings['required'], $props);
+  }
+
+  /**
+   * Set options and attributes for chain select fields based on the controlling field's value
+   */
+  private function preProcessChainSelectFields() {
+    foreach ($this->_chainSelectFields as $control => $target) {
+      $controlField = $this->getElement($control);
+      $targetField = $this->getElement($target);
+
+      $css = (string) $controlField->getAttribute('class');
+      $controlField->updateAttributes(array(
+        'class' => ($css ? "$css " : 'crm-select2 ') . 'crm-chain-select-control',
+        'data-target' => $target,
+      ));
+      $selection = $controlField->getValue();
+      $options = array();
+      if ($selection) {
+        $options = CRM_Core_BAO_Location::getChainSelectValues($selection, strpos($control, 'rovince') ? 'stateProvince' : 'country', TRUE);
+        if (!$options) {
+          $targetField->setAttribute('placeholder', $targetField->getAttribute('data-none-prompt'));
+        }
+      }
+      else {
+        $targetField->setAttribute('placeholder', $targetField->getAttribute('data-empty-prompt'));
+        $targetField->setAttribute('disabled', 'disabled');
+      }
+      if (!$targetField->getAttribute('multiple')) {
+        $options = array('' => $targetField->getAttribute('placeholder')) + $options;
+        $targetField->removeAttribute('placeholder');
+      }
+      $targetField->loadArray($options);
+    }
+  }
 }
 
index 1527af263aa509c0d4f5969bf17cba06f1008f82..d4e126ae97c15c027a2349aa0c4f6112f86e267c 100644 (file)
@@ -200,69 +200,11 @@ class CRM_Core_Page_AJAX_Location {
   }
 
   static function jqState() {
-    if (empty($_GET['_value'])) {
-      CRM_Utils_System::civiExit();
-    }
-    $countries = (array) $_GET['_value'];
-    $elements = array();
-    $list = &$elements;
-    foreach ($countries as $val) {
-      $result = CRM_Core_PseudoConstant::stateProvinceForCountry($val);
-
-      // Option-groups for multiple countries
-      if ($result && count($countries) > 1) {
-        $elements[] = array(
-          'name' => CRM_Core_PseudoConstant::country($val, FALSE),
-          'children' => array(),
-        );
-        $list = &$elements[count($elements)-1]['children'];
-      }
-      foreach ($result as $id => $name) {
-        $list[] = array(
-          'name' => $name,
-          'value' => $id,
-        );
-      }
-    }
-    $placeholder = array(array('value' => '', 'name' => $elements ? ts('- select -') : ts('- N/A -')));
-    echo json_encode(array_merge($placeholder, $elements));
-    CRM_Utils_System::civiExit();
+    CRM_Utils_JSON::output(CRM_Core_BAO_Location::getChainSelectValues($_GET['_value'], 'country'));
   }
 
   static function jqCounty() {
-    $elements = array();
-    if (!isset($_GET['_value']) || CRM_Utils_System::isNull($_GET['_value'])) {
-      $elements = array(
-        array('name' => ts('Choose state first'), 'value' => '')
-      );
-    }
-    else {
-      $states = (array) $_GET['_value'];
-      $list = &$elements;
-      foreach ($states as $val) {
-        $result = CRM_Core_PseudoConstant::countyForState($val);
-
-        // Option-groups for multiple countries
-        if ($result && count($states) > 1) {
-          $elements[] = array(
-            'name' => CRM_Core_PseudoConstant::stateProvince($val, FALSE),
-            'children' => array(),
-          );
-          $list = &$elements[count($elements)-1]['children'];
-        }
-        foreach ($result as $id => $name) {
-          $list[] = array(
-            'name' => $name,
-            'value' => $id,
-          );
-        }
-      }
-      $placeholder = array(array('value' => '', 'name' => $elements ? ts('- select -') : ts('- N/A -')));
-      $elements = array_merge($placeholder, $elements);
-    }
-
-    echo json_encode($elements);
-    CRM_Utils_System::civiExit();
+    CRM_Utils_JSON::output(CRM_Core_BAO_Location::getChainSelectValues($_GET['_value'], 'stateProvince'));
   }
 
   static function getLocBlock() {
index 56f66425d1b3d6c5af13e67882da2cbbc48d59b5..383fe7ffa7f531e41cef3a6b1ee18d882dcc927c 100644 (file)
@@ -3652,6 +3652,10 @@ div.m ul#civicrm-menu,
 .crm-container .select2-container-multi.crm-ajax-select .select2-choices:before {
   background-position: right -26px;
 }
+.crm-container .select2-container-multi.loading .select2-choices:before,
+.crm-container .select2-container.loading .select2-choice .select2-arrow b {
+  background: url('../i/loading.gif') no-repeat center center;
+}
 /* Reduce select2 size to match other inputs */
 .crm-container .select2-container-multi .select2-choices {
   min-height: 25px;
index 9d4496d72480d41e206d90bb5ab9a509df638d2c..8d437351f2ae4cbbb99612d6c7bc86b53e1c1dc5 100644 (file)
@@ -214,26 +214,65 @@ CRM.strings = CRM.strings || {};
    * Populate a select list, overwriting the existing options except for the placeholder.
    * @param $el jquery collection - 1 or more select elements
    * @param options array in format returned by api.getoptions
-   * @param removePlaceholder bool
+   * @param placeholder string
    */
-  CRM.utils.setOptions = function($el, options, removePlaceholder) {
+  CRM.utils.setOptions = function($el, options, placeholder) {
     $el.each(function() {
       var
         $elect = $(this),
         val = $elect.val() || [],
-        opts = removePlaceholder ? '' : '[value!=""]';
+        multiple = $el.is('[multiple]'),
+        opts = placeholder || placeholder === '' ? '' : '[value!=""]',
+        newOptions = '',
+        theme = function(options) {
+          _.each(options, function(option) {
+            if (option.children) {
+              newOptions += '<optgroup label="' + option.value + '">';
+              theme(option.children);
+              newOptions += '</optgroup>';
+            } else {
+              var selected = ($.inArray('' + option.key, val) > -1) ? 'selected="selected"' : '';
+              newOptions += '<option value="' + option.key + '"' + selected + '>' + option.value + '</option>';
+            }
+          });
+        };
       if (!$.isArray(val)) {
         val = [val];
       }
       $elect.find('option' + opts).remove();
-      _.each(options, function(option) {
-        var selected = ($.inArray(''+option.key, val) > -1) ? 'selected="selected"' : '';
-        $elect.append('<option value="' + option.key + '"' + selected + '>' + option.value + '</option>');
-      });
+      theme(options);
+      if (typeof placeholder === 'string') {
+        if (multiple) {
+          $el.attr('placeholder', placeholder);
+        } else {
+          newOptions = '<option value="">' + placeholder + '</option>' + newOptions;
+        }
+      }
+      $elect.append(newOptions);
       $elect.trigger('crmOptionsUpdated', $.extend({}, options)).trigger('change');
     });
   };
 
+  function chainSelect() {
+    var $form = $(this).closest('form'),
+      $target = $('select[data-name="' + $(this).data('target') + '"]', $form),
+      data = $target.data(),
+      val = $(this).val();
+    $target.prop('disabled', true);
+    if ($target.is('select.crm-chain-select-control')) {
+      $('select[data-name="' + $target.data('target') + '"]', $form).prop('disabled', true).blur();
+    }
+    if (!(val && val.length)) {
+      CRM.utils.setOptions($target.blur(), [], data.emptyPrompt);
+    } else {
+      $target.addClass('loading');
+      $.getJSON(CRM.url(data.callback), {_value: val}, function(vals) {
+        $target.prop('disabled', false).removeClass('loading');
+        CRM.utils.setOptions($target, vals || [], (vals && vals.length ? data.selectPrompt : data.nonePrompt));
+      });
+    }
+  }
+
 /**
  * Compare Form Input values against cached initial value.
  *
@@ -489,6 +528,7 @@ CRM.strings = CRM.strings || {};
       }
       $('.crm-select2:not(.select2-offscreen, .select2-container)', e.target).crmSelect2();
       $('.crm-form-entityref:not(.select2-offscreen, .select2-container)', e.target).crmEntityRef();
+      $('select.crm-chain-select-control', e.target).off('.chainSelect').on('change.chainSelect', chainSelect);
       // Cache Form Input initial values
       $('form[data-warn-changes] :input', e.target).each(function() {
         $(this).data('crm-initial-value', $(this).val());