CRM-21840: Show options edit link for radio and checkbox groups
authorMichael Devery <michael@compucorp.co.uk>
Thu, 15 Mar 2018 13:02:00 +0000 (13:02 +0000)
committerMichael Devery <michael@compucorp.co.uk>
Tue, 20 Mar 2018 16:36:37 +0000 (16:36 +0000)
CRM/Core/BAO/CustomField.php
CRM/Core/Form.php
CRM/Core/Form/Renderer.php
js/crm.optionEdit.js

index a656f5048c2d06ff1ee60add00e1cf0793e44e00..02a6ca4d88ae3a122ca3b838a316c0ab94191a24 100644 (file)
@@ -803,6 +803,7 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_CustomField {
     $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 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_CustomField {
       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 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_CustomField {
 
       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 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_CustomField {
       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');
         }
index 3d52eb9b7b25e78dcf2a8ec71eb95bc019d0ba67..015b8390a7ca7f127234c938dd3723ea348633f3 100644 (file)
@@ -1091,6 +1091,12 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
       $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 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
     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 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
       }
 
       // 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'];
       }
index 293935923d07fe9f5f3e115c25e83eaea25ebeaf..17bf8d03087ce52b6bc572f15eaf8e6022757918 100644 (file)
@@ -138,7 +138,10 @@ class CRM_Core_Form_Renderer extends HTML_QuickForm_Renderer_ArraySmarty {
     }
     // 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);
       }
 
index 5f8c48342864b3e19329b1861dc251cd3a1b2981..1b1338ce00a793cca1f54b80241e7305cce9b634 100644 (file)
@@ -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') ? '<br/>' : '&nbsp;';
+
+    // 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(/&nbsp;/g, '');
+    });
+
+    var renderedOptions = '';
+    // replace missing br at start of element
+    if (separator === '<br/>') {
+      $parent.prepend(separator);
+      renderedOptions = separator;
+    }
+
+    newOptions.forEach(function(option) {
+      var optionId = prefix + '_' + option.key,
+        checked = '';
+
+      if ($.inArray(optionId, checkedBoxes) !== -1) {
+        checked = ' checked="checked"';
+      }
+
+      renderedOptions += '<input type="checkbox" ' +
+        ' value="1"' +
+        ' id="' + optionId + '"' +
+        ' name="' + prefix + '[' + option.key +']' + '"' +
+        checked +
+        ' class="crm-form-checkbox"' +
+        optionAttributes +
+        '><label for="' + optionId + '">' + option.value + '</label>' +
+        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 += '<input type="radio" ' +
+        ' value=' + option.key +
+        ' id="' + optionId +'"' +
+        ' name="' + optionName + '"' +
+        checked +
+        ' class="crm-form-radio"' +
+        optionAttributes +
+        '><label for="' + optionId + '">' + option.value + '</label> ';
     });
+
+    $parent.prepend(renderedOptions);
+  }
 });