CRM_Utils_System_UnitTests - Work-around for _drupal_flush_css_js() crash
[civicrm-core.git] / CRM / Core / Form.php
index 0cbcedd9905c6333d15597afc460220b9bdb2b18..4c5337d99cfa4132fa928aa490e43da560b0802c 100644 (file)
  */
 
 require_once 'HTML/QuickForm/Page.php';
+
+/**
+ * Class CRM_Core_Form
+ */
 class CRM_Core_Form extends HTML_QuickForm_Page {
 
   /**
@@ -137,10 +141,16 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
    * If u have multiple groups of checkboxes, you will need to give them different
    * ids to avoid potential name collision
    *
-   * @var const string / int
+   * @var string|int
    */
   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
    *
@@ -148,12 +158,12 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
    * of default convenient functions, rules and buttons
    *
    * @param object $state State associated with this form
-   * @param \const|\enum $action The mode the form is operating in (None/Create/View/Update/Delete)
+   * @param \const|\enum|int $action The mode the form is operating in (None/Create/View/Update/Delete)
    * @param string $method The type of http method used (GET/POST)
    * @param string $name The name of the form if different from class name
    *
    * @return \CRM_Core_Form
-  @access public
+   * @access public
    */
   function __construct(
     $state = NULL,
@@ -166,6 +176,7 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
       $this->_name = $name;
     }
     else {
+      // CRM-15153 - FIXME this name translates to a DOM id and is not always unique!
       $this->_name = CRM_Utils_String::getClassName(CRM_Utils_System::getClassName($this));
     }
 
@@ -183,6 +194,8 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
     if (!isset(self::$_template)) {
       self::$_template = CRM_Core_Smarty::singleton();
     }
+    // Workaround for CRM-15153 - give each form a reasonably unique css class
+    $this->addClass(CRM_Utils_System::getClassName($this));
 
     $this->assign('snippet', CRM_Utils_Array::value('snippet', $_GET));
   }
@@ -190,6 +203,15 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
   static function generateID() {
   }
 
+  /**
+   * Add one or more css classes to the form
+   * @param $className
+   */
+  public function addClass($className) {
+    $classes = $this->getAttribute('class');
+    $this->setAttribute('class', ($classes ? "$classes " : '') . $className);
+  }
+
   /**
    * register all the standard rules that most forms potentially use
    *
@@ -218,19 +240,12 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
    * Simple easy to use wrapper around addElement. Deal with
    * simple validation rules
    *
-   * @param $type
-   * @param $name
+   * @param string $type
+   * @param string $name
    * @param string $label
-   * @param string $attributes
+   * @param string|array $attributes (options for select elements)
    * @param bool $required
-   * @param null $extra
-   *
-   * @internal param \type $string of html element to be added
-   * @internal param \name $string of the html element
-   * @internal param \display $string label for the html element
-   * @internal param \attributes $string used for this element.
-   *               These are not default values
-   * @internal param \is $bool this a required field
+   * @param array $extra (attributes for select elements)
    *
    * @return HTML_QuickForm_Element could be an error object
    * @access public
@@ -238,9 +253,23 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
   function &add($type, $name, $label = '',
     $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)) {
+      // Normalize this property
+      if (!empty($extra['multiple'])) {
+        $extra['multiple'] = 'multiple';
+      }
+      else {
+        unset($extra['multiple']);
+      }
+      // Add placeholder option for select
+      if (isset($extra['placeholder'])) {
+        if ($extra['placeholder'] === TRUE) {
+          $extra['placeholder'] = $required ? ts('- select -') : ts('- none -');
+        }
+        if (($extra['placeholder'] || $extra['placeholder'] === '') && empty($extra['multiple']) && is_array($attributes) && !isset($attributes[''])) {
+          $attributes = array('' => $extra['placeholder']) + $attributes;
+        }
+      }
     }
     $element = $this->addElement($type, $name, $label, $attributes, $extra);
     if (HTML_QuickForm::isError($element)) {
@@ -292,13 +321,15 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
 
   /**
    * This function is just a wrapper, so that we can call all the hook functions
+   * @param bool $allowAjax - FIXME: This feels kind of hackish, ideally we would take the json-related code from this function
+   *                          and bury it deeper down in the controller
    */
-  function mainProcess() {
+  function mainProcess($allowAjax = TRUE) {
     $this->postProcess();
     $this->postProcessHook();
 
     // Respond with JSON if in AJAX context (also support legacy value '6')
-    if (!empty($_REQUEST['snippet']) && in_array($_REQUEST['snippet'], array(CRM_Core_Smarty::PRINT_JSON, 6))) {
+    if ($allowAjax && !empty($_REQUEST['snippet']) && in_array($_REQUEST['snippet'], array(CRM_Core_Smarty::PRINT_JSON, 6))) {
       $this->ajaxResponse['buttonName'] = str_replace('_qf_' . $this->getAttribute('id') . '_', '', $this->controller->getButtonName());
       $this->ajaxResponse['action'] = $this->_action;
       if (isset($this->_id) || isset($this->id)) {
@@ -355,9 +386,18 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
    */
   function addRules() {}
 
+  /**
+   * Performs the server side validation
+   * @access    public
+   * @since     1.0
+   * @return    boolean   true if no error found
+   * @throws    HTML_QuickForm_Error
+   */
   function validate() {
     $error = parent::validate();
 
+    $this->validateChainSelectFields();
+
     $hookErrors = CRM_Utils_Hook::validate(
       get_class($this),
       $this->_submitValues,
@@ -458,10 +498,10 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
       $js = CRM_Utils_Array::value('js', $button);
       $isDefault = CRM_Utils_Array::value('isDefault', $button, FALSE);
       if ($isDefault) {
-        $attrs = array('class' => 'form-submit default');
+        $attrs = array('class' => 'crm-form-submit default');
       }
       else {
-        $attrs = array('class' => 'form-submit');
+        $attrs = array('class' => 'crm-form-submit');
       }
 
       if ($js) {
@@ -620,10 +660,13 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
    * @access public
    */
   function toSmarty() {
+    $this->preProcessChainSelectFields();
     $renderer = $this->getRenderer();
     $this->accept($renderer);
     $content = $renderer->toArray();
     $content['formName'] = $this->getName();
+    // CRM-15153
+    $content['formClass'] = CRM_Utils_System::getClassName($this);
     return $content;
   }
 
@@ -805,6 +848,16 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
     return self::$_template->get_template_vars($name);
   }
 
+  /**
+   * @param $name
+   * @param $title
+   * @param $values
+   * @param array $attributes
+   * @param null $separator
+   * @param bool $required
+   *
+   * @return HTML_QuickForm_group
+   */
   function &addRadio($name, $title, $values, $attributes = array(), $separator = NULL, $required = FALSE) {
     $options = array();
     $attributes = $attributes ? $attributes : array();
@@ -824,6 +877,13 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
     return $group;
   }
 
+  /**
+   * @param $id
+   * @param $title
+   * @param bool $allowClear
+   * @param null $required
+   * @param array $attributes
+   */
   function addYesNo($id, $title, $allowClear = FALSE, $required = NULL, $attributes = array()) {
     $attributes += array('id_suffix' => $id);
     $choice   = array();
@@ -839,6 +899,17 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
     }
   }
 
+  /**
+   * @param $id
+   * @param $title
+   * @param $values
+   * @param null $other
+   * @param null $attributes
+   * @param null $required
+   * @param null $javascriptMethod
+   * @param string $separator
+   * @param bool $flipValues
+   */
   function addCheckBox($id, $title, $values, $other = NULL,
     $attributes       = NULL, $required = NULL,
     $javascriptMethod = NULL,
@@ -921,6 +992,15 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
     $this->addButtons($buttons);
   }
 
+  /**
+   * @param $name
+   * @param string $from
+   * @param string $to
+   * @param string $label
+   * @param string $dateFormat
+   * @param bool $required
+   * @param bool $displayTime
+   */
   function addDateRange($name, $from = '_from', $to = '_to', $label = 'From:', $dateFormat = 'searchDate', $required = FALSE, $displayTime = FALSE) {
     if ($displayTime) {
       $this->addDateTime($name . $from, $label, $required, array('formatType' => $dateFormat));
@@ -962,9 +1042,6 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
     if (!array_key_exists('placeholder', $props)) {
       $props['placeholder'] = $required ? ts('- select -') : ts('- none -');
     }
-    if ($props['placeholder'] !== NULL && empty($props['multiple'])) {
-      $options = array('' => '') + $options;
-    }
     // Handle custom field
     if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
       list(, $id) = explode('_', $name);
@@ -1015,6 +1092,12 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
     ));
   }
 
+  /**
+   * @param $name
+   * @param $label
+   * @param $attributes
+   * @param bool $forceTextarea
+   */
   function addWysiwyg($name, $label, $attributes, $forceTextarea = FALSE) {
     // 1. Get configuration option for editor (tinymce, ckeditor, pure textarea)
     // 2. Based on the option, initialise proper editor
@@ -1053,6 +1136,12 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
     $this->assign('includeWysiwygEditor', $includeWysiwygEditor);
   }
 
+  /**
+   * @param $id
+   * @param $title
+   * @param null $required
+   * @param null $extra
+   */
   function addCountry($id, $title, $required = NULL, $extra = NULL) {
     $this->addElement('select', $id, $title,
       array(
@@ -1063,6 +1152,14 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
     }
   }
 
+  /**
+   * @param $name
+   * @param $label
+   * @param $options
+   * @param $attributes
+   * @param null $required
+   * @param null $javascriptMethod
+   */
   function addSelectOther($name, $label, $options, $attributes, $required = NULL, $javascriptMethod = NULL) {
 
     $this->addElement('select', $name . '_id', $label, $options, $javascriptMethod);
@@ -1072,18 +1169,30 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
     }
   }
 
+  /**
+   * @return null
+   */
   public function getRootTitle() {
     return NULL;
   }
 
+  /**
+   * @return string
+   */
   public function getCompleteTitle() {
     return $this->getRootTitle() . $this->getTitle();
   }
 
+  /**
+   * @return CRM_Core_Smarty
+   */
   static function &getTemplate() {
     return self::$_template;
   }
 
+  /**
+   * @param $elementName
+   */
   function addUploadElement($elementName) {
     $uploadNames = $this->get('uploadNames');
     if (!$uploadNames) {
@@ -1109,6 +1218,9 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
     }
   }
 
+  /**
+   * @return string
+   */
   function buttonType() {
     $uploadNames = $this->get('uploadNames');
     $buttonType = (is_array($uploadNames) && !empty($uploadNames)) ? 'upload' : 'next';
@@ -1116,10 +1228,19 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
     return $buttonType;
   }
 
+  /**
+   * @param $name
+   *
+   * @return null
+   */
   function getVar($name) {
     return isset($this->$name) ? $this->$name : NULL;
   }
 
+  /**
+   * @param $name
+   * @param $value
+   */
   function setVar($name, $value) {
     $this->$name = $value;
   }
@@ -1364,6 +1485,9 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
     }
   }
 
+  /**
+   * @param $elementName
+   */
   function removeFileRequiredRules($elementName) {
     $this->_required = array_diff($this->_required, array($elementName));
     if (isset($this->_rules[$elementName])) {
@@ -1577,5 +1701,114 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
   function allowAjaxSubmit() {
     $this->removeAttribute('data-no-ajax-submit');
   }
+
+  /**
+   * Sets page title based on entity and action
+   * @param string $entityLabel
+   */
+  function setPageTitle($entityLabel) {
+    switch ($this->_action) {
+      case CRM_Core_Action::ADD:
+        CRM_Utils_System::setTitle(ts('New %1', array(1 => $entityLabel)));
+        break;
+      case CRM_Core_Action::UPDATE:
+        CRM_Utils_System::setTitle(ts('Edit %1', array(1 => $entityLabel)));
+        break;
+      case CRM_Core_Action::VIEW:
+      case CRM_Core_Action::PREVIEW:
+        CRM_Utils_System::setTitle(ts('View %1', array(1 => $entityLabel)));
+        break;
+      case CRM_Core_Action::DELETE:
+        CRM_Utils_System::setTitle(ts('Delete %1', array(1 => $entityLabel)));
+        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);
+      $controlType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'stateProvince' : 'country';
+
+      $css = (string) $controlField->getAttribute('class');
+      $controlField->updateAttributes(array(
+        'class' => ($css ? "$css " : 'crm-select2 ') . 'crm-chain-select-control',
+        'data-target' => $target,
+      ));
+      $controlValue = $controlField->getValue();
+      $options = array();
+      if ($controlValue) {
+        $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, 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);
+    }
+  }
+
+  /**
+   * Validate country / state / county match and suppress unwanted "required" errors
+   */
+  private function validateChainSelectFields() {
+    foreach ($this->_chainSelectFields as $control => $target) {
+      $controlValue = (array) $this->getElementValue($control);
+      $targetField = $this->getElement($target);
+      $controlType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'stateProvince' : 'country';
+      $targetValue = array_filter((array) $targetField->getValue());
+      if ($targetValue || $this->getElementError($target)) {
+        $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
+        if ($targetValue) {
+          if (!array_intersect($targetValue, array_keys($options))) {
+            $this->setElementError($target, $controlType == 'country' ? ts('State/Province does not match the selected Country') : ts('County does not match the selected State/Province'));
+          }
+        }
+        // Suppress "required" error for field if it has no options
+        elseif (!$options) {
+          $this->setElementError($target, NULL);
+        }
+      }
+    }
+  }
 }