diff --git a/CRM/Core/BAO/CustomField.php b/CRM/Core/BAO/CustomField.php index a656f5048c2d..02a6ca4d88ae 100644 --- a/CRM/Core/BAO/CustomField.php +++ b/CRM/Core/BAO/CustomField.php @@ -803,6 +803,7 @@ public static function addQuickFormElement( $field = self::getFieldObject($fieldId); $widget = $field->html_type; $element = NULL; + $customFieldAttributes = array(); // Custom field HTML should indicate group+field name $groupName = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $field->custom_group_id); @@ -837,23 +838,27 @@ public static function addQuickFormElement( if ($search || ($widget !== 'AdvMulti-Select' && strpos($widget, 'Select') !== FALSE)) { $widget = 'Select'; } - $selectAttributes = array( - 'data-crm-custom' => $dataCrmCustomVal, - 'class' => 'crm-select2', - ); + + $customFieldAttributes['data-crm-custom'] = $dataCrmCustomVal; + $selectAttributes = array('class' => 'crm-select2'); + // Search field is always multi-select if ($search || strpos($field->html_type, 'Multi') !== FALSE) { $selectAttributes['class'] .= ' huge'; $selectAttributes['multiple'] = 'multiple'; $selectAttributes['placeholder'] = $placeholder; } + // Add data for popup link. Normally this is handled by CRM_Core_Form->addSelect - if ($field->option_group_id && !$search && $widget == 'Select' && CRM_Core_Permission::check('administer CiviCRM')) { - $selectAttributes += array( + $isSupportedWidget = in_array($widget, ['Select', 'Radio']); + $canEditOptions = CRM_Core_Permission::check('administer CiviCRM'); + if ($field->option_group_id && !$search && $isSelect && $canEditOptions) { + $customFieldAttributes += array( 'data-api-entity' => $field->getEntity(), 'data-api-field' => 'custom_' . $field->id, 'data-option-edit-path' => 'civicrm/admin/options/' . CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionGroup', $field->option_group_id), ); + $selectAttributes += $customFieldAttributes; } } @@ -933,10 +938,18 @@ public static function addQuickFormElement( case 'Radio': $choice = array(); + parse_str($field->attributes, $radioAttributes); + $radioAttributes = array_merge($radioAttributes, $customFieldAttributes); + foreach ($options as $v => $l) { - $choice[] = $qf->createElement('radio', NULL, '', $l, (string) $v, $field->attributes); + $choice[] = $qf->createElement('radio', NULL, '', $l, (string) $v, $radioAttributes); } $element = $qf->addGroup($choice, $elementName, $label); + $optionEditKey = 'data-option-edit-path'; + if (isset($selectAttributes[$optionEditKey])) { + $element->setAttribute($optionEditKey, $selectAttributes[$optionEditKey]); + } + if ($useRequired && !$search) { $qf->addRule($elementName, ts('%1 is a required field.', array(1 => $label)), 'required'); } @@ -988,9 +1001,15 @@ public static function addQuickFormElement( case 'CheckBox': $check = array(); foreach ($options as $v => $l) { - $check[] = &$qf->addElement('advcheckbox', $v, NULL, $l, array('data-crm-custom' => $dataCrmCustomVal)); + $check[] = &$qf->addElement('advcheckbox', $v, NULL, $l, $customFieldAttributes); } - $element = $qf->addGroup($check, $elementName, $label); + + $group = $element = $qf->addGroup($check, $elementName, $label); + $optionEditKey = 'data-option-edit-path'; + if (isset($customFieldAttributes[$optionEditKey])) { + $group->setAttribute($optionEditKey, $customFieldAttributes[$optionEditKey]); + } + if ($useRequired && !$search) { $qf->addRule($elementName, ts('%1 is a required field.', array(1 => $label)), 'required'); } diff --git a/CRM/Core/Form.php b/CRM/Core/Form.php index 3d52eb9b7b25..015b8390a7ca 100644 --- a/CRM/Core/Form.php +++ b/CRM/Core/Form.php @@ -1091,6 +1091,12 @@ public function &addRadio($name, $title, $values, $attributes = array(), $separa $options[] = $this->createElement('radio', NULL, NULL, $var, $key, $attributes); } $group = $this->addGroup($options, $name, $title, $separator); + + $optionEditKey = 'data-option-edit-path'; + if (!empty($attributes[$optionEditKey])) { + $group->setAttribute($optionEditKey, $attributes[$optionEditKey]); + } + if ($required) { $this->addRule($name, ts('%1 is a required field.', array(1 => $title)), 'required'); } @@ -1144,25 +1150,29 @@ public function addCheckBox( if ($javascriptMethod) { foreach ($values as $key => $var) { if (!$flipValues) { - $options[] = $this->createElement('checkbox', $var, NULL, $key, $javascriptMethod); + $options[] = $this->createElement('checkbox', $var, NULL, $key, $javascriptMethod, $attributes); } else { - $options[] = $this->createElement('checkbox', $key, NULL, $var, $javascriptMethod); + $options[] = $this->createElement('checkbox', $key, NULL, $var, $javascriptMethod, $attributes); } } } else { foreach ($values as $key => $var) { if (!$flipValues) { - $options[] = $this->createElement('checkbox', $var, NULL, $key); + $options[] = $this->createElement('checkbox', $var, NULL, $key, $attributes); } else { - $options[] = $this->createElement('checkbox', $key, NULL, $var); + $options[] = $this->createElement('checkbox', $key, NULL, $var, $attributes); } } } - $this->addGroup($options, $id, $title, $separator); + $group = $this->addGroup($options, $id, $title, $separator); + $optionEditKey = 'data-option-edit-path'; + if (!empty($attributes[$optionEditKey])) { + $group->setAttribute($optionEditKey, $attributes[$optionEditKey]); + } if ($other) { $this->addElement('text', $id . '_other', ts('Other'), $attributes[$id . '_other']); @@ -1425,8 +1435,16 @@ public function addField($name, $props = array(), $required = FALSE, $legacyDate } // Add data for popup link. - if ((!empty($props['option_url']) || !array_key_exists('option_url', $props)) && ($context != 'search' && $widget == 'Select' && CRM_Core_Permission::check('administer CiviCRM'))) { - $props['data-option-edit-path'] = !empty($props['option_url']) ? $props['option_url'] : CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec); + $canEditOptions = CRM_Core_Permission::check('administer CiviCRM'); + $hasOptionUrl = !empty($props['option_url']); + $optionUrlKeyIsSet = array_key_exists('option_url', $props); + $shouldAdd = $context !== 'search' && $isSelect && $canEditOptions; + + // Only add if key is not set, or if non-empty option url is provided + if (($hasOptionUrl || !$optionUrlKeyIsSet) && $shouldAdd) { + $optionUrl = $hasOptionUrl ? $props['option_url'] : + CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec); + $props['data-option-edit-path'] = $optionUrl; $props['data-api-entity'] = $props['entity']; $props['data-api-field'] = $props['name']; } diff --git a/CRM/Core/Form/Renderer.php b/CRM/Core/Form/Renderer.php index 293935923d07..17bf8d03087c 100644 --- a/CRM/Core/Form/Renderer.php +++ b/CRM/Core/Form/Renderer.php @@ -138,7 +138,10 @@ public function _elementToArray(&$element, $required, $error) { } // Active form elements else { - if ($element->getType() == 'select' && $element->getAttribute('data-option-edit-path')) { + $typesToShowEditLink = array('select', 'group'); + $hasEditPath = NULL !== $element->getAttribute('data-option-edit-path'); + + if (in_array($element->getType(), $typesToShowEditLink) && $hasEditPath) { $this->addOptionsEditLink($el, $element); } diff --git a/js/crm.optionEdit.js b/js/crm.optionEdit.js index 5f8c48342864..1b1338ce00a7 100644 --- a/js/crm.optionEdit.js +++ b/js/crm.optionEdit.js @@ -5,12 +5,162 @@ jQuery(function($) { .on('click', 'a.crm-option-edit-link', CRM.popup) .on('crmPopupFormSuccess', 'a.crm-option-edit-link', function() { $(this).trigger('crmOptionsEdited'); - var $elects = $('select[data-option-edit-path="' + $(this).data('option-edit-path') + '"]'); - if ($elects.data('api-entity') && $elects.data('api-field')) { - CRM.api3($elects.data('api-entity'), 'getoptions', {sequential: 1, field: $elects.data('api-field')}) - .done(function (data) { - CRM.utils.setOptions($elects, data.values); - }); + var optionEditPath = $(this).data('option-edit-path'); + var $selects = $('select[data-option-edit-path="' + optionEditPath + '"]'); + var $inputs = $('input[data-option-edit-path="' + optionEditPath + '"]'); + var $radios = $inputs.filter('[type=radio]'); + var $checkboxes = $inputs.filter('[type=checkbox]'); + + if ($selects.length > 0) { + rebuildOptions($selects, CRM.utils.setOptions); } + else if ($radios.length > 0) { + rebuildOptions($radios, rebuildRadioOptions); + } + else if ($checkboxes.length > 0) { + rebuildOptions($checkboxes, rebuildCheckboxOptions); + } + }); + + /** + * Fetches options using metadata from the existing ones and calls the + * function to rebuild them + * @param $existing {object} The existing options, used as metadata store + * @param rebuilder {function} Function to be called to rebuild the options + */ + function rebuildOptions($existing, rebuilder) { + if ($existing.data('api-entity') && $existing.data('api-field')) { + CRM.api3($existing.data('api-entity'), 'getoptions', { + sequential: 1, + field: $existing.data('api-field') + }) + .done(function(data) { + rebuilder($existing, data.values); + }); + } + } + + /** + * Rebuild checkbox input options, overwriting the existing options + * + * @param $existing {object} the existing checkbox options + * @param newOptions {array} in format returned by api.getoptions + */ + function rebuildCheckboxOptions($existing, newOptions) { + var $parent = $existing.first().parent(), + $firstExisting = $existing.first(), + optionName = $firstExisting.attr('name'), + optionAttributes = + 'data-option-edit-path =' + $firstExisting.data('option-edit-path') + + ' data-api-entity = ' + $firstExisting.data('api-entity') + + ' data-api-field = ' + $firstExisting.data('api-field'); + + var prefix = optionName.substr(0, optionName.lastIndexOf("[")); + + var checkedBoxes = []; + $parent.find('input:checked').each(function() { + checkedBoxes.push($(this).attr('id')); + }); + + // remove existing checkboxes + $parent.find('input[type=checkbox]').remove(); + + // find existing labels for the checkboxes + var $checkboxLabels = $parent.find('label').filter(function() { + var forAttr = $(this).attr('for') || ''; + + return forAttr.indexOf(prefix) !== -1; + }); + + // find what is used to separate the elements; spaces or linebreaks + var $elementAfterLabel = $checkboxLabels.first().next(); + var separator = $elementAfterLabel.is('br') ? '
' : ' '; + + // remove existing labels + $checkboxLabels.remove(); + + // remove linebreaks in container + $parent.find('br').remove(); + + // remove separator whitespace in container + $parent.html(function (i, html) { + return html.replace(/ /g, ''); + }); + + var renderedOptions = ''; + // replace missing br at start of element + if (separator === '
') { + $parent.prepend(separator); + renderedOptions = separator; + } + + newOptions.forEach(function(option) { + var optionId = prefix + '_' + option.key, + checked = ''; + + if ($.inArray(optionId, checkedBoxes) !== -1) { + checked = ' checked="checked"'; + } + + renderedOptions += '' + + separator; + }); + + // remove final separator + renderedOptions = renderedOptions.substring(0, renderedOptions.lastIndexOf(separator)); + + var $editLink = $parent.find('.crm-option-edit-link'); + + // try to insert before the edit link to maintain structure + if ($editLink.length > 0) { + $(renderedOptions).insertBefore($editLink); + } + else { + $parent.append(renderedOptions); + } + } + + /** + * Rebuild radio input options, overwriting the existing options + * + * @param $existing {object} the existing input options + * @param newOptions {array} in format returned by api.getoptions + */ + function rebuildRadioOptions($existing, newOptions) { + var $parent = $existing.first().parent(), + $firstExisting = $existing.first(), + optionName = $firstExisting.attr('name'), + renderedOptions = '', + checkedValue = parseInt($parent.find('input:checked').attr('value')), + optionAttributes = + 'data-option-edit-path =' + $firstExisting.attr('data-option-edit-path') + + ' data-api-entity = ' + $firstExisting.attr('data-api-entity') + + ' data-api-field = ' + $firstExisting.attr('data-api-field'); + + // remove existing radio inputs and labels + $parent.find('input, label').remove(); + + newOptions.forEach(function(option) { + var optionId = 'CIVICRM_QFID_' + option.key + '_' + optionName, + checked = (option.key === checkedValue) ? ' checked="checked"' : ''; + + renderedOptions += ' '; }); + + $parent.prepend(renderedOptions); + } });