From: Coleman Watts Date: Sun, 24 Aug 2014 20:11:51 +0000 (+0100) Subject: CRM-15172 - Add chainSelect method to CRM_Core_Form X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=1d07e7ab02fde43dc0dd49ad069a4d401ba8249c;p=civicrm-core.git CRM-15172 - Add chainSelect method to CRM_Core_Form --- diff --git a/CRM/Core/BAO/Location.php b/CRM/Core/BAO/Location.php index 092e86e089..e7108aaea6 100644 --- a/CRM/Core/BAO/Location.php +++ b/CRM/Core/BAO/Location.php @@ -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; + } } diff --git a/CRM/Core/Form.php b/CRM/Core/Form.php index 3fa2bed56f..76d4a6c3d3 100644 --- a/CRM/Core/Form.php +++ b/CRM/Core/Form.php @@ -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); + } + } } diff --git a/CRM/Core/Page/AJAX/Location.php b/CRM/Core/Page/AJAX/Location.php index 1527af263a..d4e126ae97 100644 --- a/CRM/Core/Page/AJAX/Location.php +++ b/CRM/Core/Page/AJAX/Location.php @@ -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() { diff --git a/css/civicrm.css b/css/civicrm.css index 56f66425d1..383fe7ffa7 100644 --- a/css/civicrm.css +++ b/css/civicrm.css @@ -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; diff --git a/js/Common.js b/js/Common.js index 9d4496d724..8d437351f2 100644 --- a/js/Common.js +++ b/js/Common.js @@ -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 += ''; + theme(option.children); + newOptions += ''; + } else { + var selected = ($.inArray('' + option.key, val) > -1) ? 'selected="selected"' : ''; + newOptions += ''; + } + }); + }; 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(''); - }); + theme(options); + if (typeof placeholder === 'string') { + if (multiple) { + $el.attr('placeholder', placeholder); + } else { + newOptions = '' + 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());