From 8c642640c4b6d6f214c8d6656f358bf85f85b75d Mon Sep 17 00:00:00 2001
From: Carsten  Rose <carsten.rose@math.uzh.ch>
Date: Tue, 28 Feb 2017 13:51:13 +0100
Subject: [PATCH] #3063, Radios / Checkboxes als Buttons (Bootstrap)
 Implemented for Checkbox.

Index.rst, AbstractBuildForm.php: split buildCheckbox() in constructCheckbox(Simple|Multi)Plain() and constructCheckbox(Simple|Multi)Button.

AbstractBuildForm.php, OnArray.php: new function removeEmptyElementsFromArray(). Replace old check of isset() (which seems never have been worked) against ==''.
---
 extension/Documentation/UsersManual/Index.rst |  36 ++-
 extension/qfq/qfq/AbstractBuildForm.php       | 205 ++++++++++++++++--
 extension/qfq/qfq/helper/OnArray.php          |  29 +++
 extension/qfq/tests/phpunit/OnArrayTest.php   |  46 +++-
 4 files changed, 296 insertions(+), 20 deletions(-)

diff --git a/extension/Documentation/UsersManual/Index.rst b/extension/Documentation/UsersManual/Index.rst
index f1d91a97a..036e6914f 100644
--- a/extension/Documentation/UsersManual/Index.rst
+++ b/extension/Documentation/UsersManual/Index.rst
@@ -1056,6 +1056,34 @@ Checkboxes can be rendered in mode:
      * Value: '', 0, 1 - The check boxes will be aligned vertical.
      * Value: >1 - The  check boxes will be aligned horizontal, with a linebreak every 'value' elements.
 
+* *FormElement.parameter*:
+
+  * *emptyHide*: Existence of this item hides an entry with an empty string. This is useful for e.g. Enums, which have an empty
+    entry, but the empty value should not be selectable.
+  * *emptyItemAtStart*: Existence of this item inserts an empty entry at the beginning of the selectlist.
+  * *emptyItemAtEnd*: Existence of this item inserts an empty entry at the end of the selectlist.
+  * *buttonClass*: Instead of the plain HTML  checkbox fields, Bootstrap
+    `buttons <http://getbootstrap.com/javascript/#buttons-checkbox-radio>`_. are rendered as `checkbox` elements. Use
+    one of the following `classes <http://getbootstrap.com/css/#buttons-options>`_:
+    * `btn-default` (default, grey),
+    * `btn-primary` (blue),
+    * `btn-success` (green),
+    * `btn-info` (light blue),
+    * `btn-warning` (orange),
+    * `btn-danger` (red).
+    With a given *buttonClass*, all buttons (=radios) are rendered horizontal. A value in *FormElement.maxlength* has no effect.
+
+* *No preselection*:
+
+  * If there is a default configured on a table column, such a value is selected by default. If the user should actively
+    choose an option, the 'preselection' can be omitted by specifying an explicit definition on the FormElement field `value`::
+
+      {{<columnname>:RZ}}
+
+    For existing records the shown value is as expected the value of the record. For new records, it's the value `0`,
+    which is typically not one of the ENUM / SET values and therefore nothing is selected.
+
+
 Type: date
 ^^^^^^^^^^
 
@@ -1168,10 +1196,10 @@ Type: radio
 
 * *FormElement.parameter*:
 
+  * *emptyHide*: Existence of this item hides an entry with an empty string. This is useful for e.g. Enums, which have an empty
+    entry, but the empty value should not be selectable.
   * *emptyItemAtStart*: Existence of this item inserts an empty entry at the beginning of the selectlist.
   * *emptyItemAtEnd*: Existence of this item inserts an empty entry at the end of the selectlist.
-  * *emptyHide*: Existence of this item hides the empty entry. This is useful for e.g. Enums, which have an empty
-    entry but the empty value should not be an option to be selected.
   * *buttonClass*: Instead of the plain radio fields, Bootstrap
     `buttons <http://getbootstrap.com/javascript/#buttons-checkbox-radio>`_. are rendered as `radio` elements. Use
     one of the following `classes <http://getbootstrap.com/css/#buttons-options>`_:
@@ -1183,10 +1211,6 @@ Type: radio
     * `btn-danger` (red).
     With a given *buttonClass*, all buttons (=radios) are rendered horizontal. A value in *FormElement.maxlength* has no effect.
 
-
-
-
-
 * *No preselection*:
 
   * If there is a default configured on a table column, such a value is selected by default. If the user should actively
diff --git a/extension/qfq/qfq/AbstractBuildForm.php b/extension/qfq/qfq/AbstractBuildForm.php
index e63878b2f..8ed190dbf 100644
--- a/extension/qfq/qfq/AbstractBuildForm.php
+++ b/extension/qfq/qfq/AbstractBuildForm.php
@@ -1072,6 +1072,15 @@ abstract class AbstractBuildForm {
             $itemKey = $itemValue;
         }
 
+        if (isset($formElement['emptyHide'])) {
+//            if (isset($itemValue['']))
+//                unset($itemValue['']);
+//            if (isset($itemKey['']))
+//                unset($itemKey['']);
+            $itemKey = OnArray::removeEmptyElementsFromArray($itemKey);
+            $itemValue = OnArray::removeEmptyElementsFromArray($itemValue);
+        }
+
         if (isset($formElement['emptyItemAtStart'])) {
             array_unshift($itemKey, '');
             array_unshift($itemValue, '');
@@ -1082,13 +1091,6 @@ abstract class AbstractBuildForm {
             $itemKey[] = '';
         }
 
-        if (isset($formElement['emptyHide'])) {
-            if (isset($itemValue['']))
-                unset($itemValue['']);
-            if (isset($itemKey['']))
-                unset($itemKey['']);
-
-        }
     }
 
     /**
@@ -1170,7 +1172,94 @@ abstract class AbstractBuildForm {
     }
 
     /**
-     * Build a Checkbox based on two values.
+     * Build a Checkbox based on two values. Either in HTML plain layout or with Bootstrap Button class.
+     *
+     * @param array $formElement
+     * @param $htmlFormElementId
+     * @param $value
+     * @param array $json
+     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
+     * @return string
+     * @throws CodeException
+     * @throws \qfq\UserFormException
+     */
+    public function buildCheckboxSingle(array $formElement, $htmlFormElementId, $attribute, $value, array &$json, $mode = FORM_LOAD) {
+
+        if (isset($formElement[FE_BUTTON_CLASS])) {
+
+            if ($formElement[FE_BUTTON_CLASS] == '') {
+                $formElement[FE_BUTTON_CLASS] = 'btn-default';
+            }
+
+            return $this->constructCheckboxSingleButton($formElement, $htmlFormElementId, $attribute, $value, $json);
+        } else {
+            return $this->constructCheckboxSinglePlain($formElement, $htmlFormElementId, $attribute, $value, $json);
+        }
+    }
+
+    /**
+     * Build a Checkbox based on two values with Bootstrap Button class.
+     *
+     * <div class="btn-group" data-toggle="buttons">
+     *    <input type="hidden" name="$htmlFormElementId" value="$valueUnChecked">
+     *    <label class="btn btn-primary active">
+     *       <input type="checkbox" autocomplete="off" name="$htmlFormElementId" value="$valueChecked"checked> Checkbox 1 (pre-checked)
+     *    </label>
+     * </div>
+     *
+     * @param array $formElement
+     * @param $htmlFormElementId
+     * @param $attribute
+     * @param $value
+     * @param array $json
+     * @return string
+     */
+    public function constructCheckboxSingleButton(array $formElement, $htmlFormElementId, $attribute, $value, array &$json) {
+        $html = '';
+        $valueJson = false;
+
+        $attribute .= Support::doAttribute('name', $htmlFormElementId);
+        $attribute .= Support::doAttribute('value', $formElement['checked'], false);
+        $attribute .= Support::doAttribute('title', $formElement['tooltip']);
+        $attribute .= Support::doAttribute('data-load', ($formElement[FE_DYNAMIC_UPDATE] === 'yes') ? 'data-load' : '');
+        $attribute .= Support::doAttribute('autocomplete', 'off');
+
+        $classActive = '';
+        if ($formElement['checked'] === $value) {
+            $attribute .= Support::doAttribute('checked', 'checked');
+            $valueJson = true;
+            $classActive = ' active';
+        }
+
+        $attribute .= $this->getAttributeList($formElement, ['autofocus']);
+        $attribute .= $this->getAttributeList($formElement, [F_FE_DATA_PATTERN_ERROR, F_FE_DATA_REQUIRED_ERROR, F_FE_DATA_MATCH_ERROR, F_FE_DATA_ERROR]);
+
+        $html = $this->buildNativeHidden($htmlFormElementId, $formElement['unchecked']);
+
+        $htmlElement = '<input ' . $attribute . '>';
+        if (isset($formElement['label2'])) {
+            $htmlElement .= $formElement['label2'];
+        } else {
+            $htmlElement .= $formElement['checked'];
+        }
+
+        $html .= Support::wrapTag("<label class='btn " . $formElement[FE_BUTTON_CLASS] . "$classActive'>",
+            $htmlElement, true);
+        $html = Support::wrapTag('<div class="btn-group" data-toggle="buttons">', $html);
+
+        $json = $this->getJsonElementUpdate($htmlFormElementId, $valueJson, $formElement[FE_MODE]);
+
+        return $html;
+    }
+
+    /**
+     * Build a single HTML plain checkbox based on two values.
+     * Create a 'hidden' input field and a 'checkbox' input field - both with the same HTML 'name'.
+     * HTML does not submit an unchecked checkbox. Then only the 'hidden' input field is submitted.
+     *
+     * Format: <input type="hidden" name="$htmlFormElementId" value="$valueUnChecked">
+     *         <input name="$htmlFormElementId" type="radio" [autofocus="autofocus"]
+     *            [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
      *
      * @param array $formElement
      * @param $htmlFormElementId
@@ -1179,7 +1268,7 @@ abstract class AbstractBuildForm {
      * @param array $json
      * @return string
      */
-    public function buildCheckboxSingle(array $formElement, $htmlFormElementId, $attribute, $value, array &$json) {
+    public function constructCheckboxSinglePlain(array $formElement, $htmlFormElementId, $attribute, $value, array &$json) {
         $html = '';
         $valueJson = false;
 
@@ -1211,6 +1300,32 @@ abstract class AbstractBuildForm {
         return $html;
     }
 
+    /**
+     * Build a Checkbox based on two values. Either in HTML plain layout or with Bootstrap Button class.
+     *
+     * @param array $formElement
+     * @param $htmlFormElementId
+     * @param $value
+     * @param array $json
+     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
+     * @return string
+     * @throws CodeException
+     * @throws \qfq\UserFormException
+     */
+    public function buildCheckboxMulti(array $formElement, $htmlFormElementId, $attributeBase, $value, array $itemKey, array $itemValue, array &$json) {
+
+        if (isset($formElement[FE_BUTTON_CLASS])) {
+
+            if ($formElement[FE_BUTTON_CLASS] == '') {
+                $formElement[FE_BUTTON_CLASS] = 'btn-default';
+            }
+
+            return $this->constructCheckboxMultiButton($formElement, $htmlFormElementId, $attributeBase, $value, $itemKey, $itemValue, $json);
+        } else {
+            return $this->constructCheckboxMultiPlain($formElement, $htmlFormElementId, $attributeBase, $value, $itemKey, $itemValue, $json);
+        }
+    }
+
     /**
      * Build as many Checkboxes as items.
      *
@@ -1226,7 +1341,73 @@ abstract class AbstractBuildForm {
      * @param array $json
      * @return string
      */
-    public function buildCheckboxMulti(array $formElement, $htmlFormElementId, $attributeBase, $value, array $itemKey, array $itemValue, array &$json) {
+    public function constructCheckboxMultiButton(array $formElement, $htmlFormElementId, $attributeBase, $value, array $itemKey, array $itemValue, array &$json) {
+        $json = array();
+
+        // Defines which of the checkboxes will be checked.
+        $values = explode(',', $value);
+
+//        $attributeBase .= Support::doAttribute('name', $htmlFormElementId);
+        $attributeBase .= Support::doAttribute('data-load', ($formElement[FE_DYNAMIC_UPDATE] === 'yes') ? 'data-load' : '');
+        $attributeBase .= $this->getAttributeList($formElement, [F_FE_DATA_PATTERN_ERROR, F_FE_DATA_REQUIRED_ERROR, F_FE_DATA_MATCH_ERROR, F_FE_DATA_ERROR]);
+
+        $html = $this->buildNativeHidden(HelperFormElement::prependFormElementIdCheckBoxMulti($htmlFormElementId, 'h'), '');
+
+        $attribute = $attributeBase;
+        if (isset($formElement['autofocus'])) {
+            $attribute .= Support::doAttribute('autofocus', $formElement['autofocus']);
+        }
+
+        for ($ii = 0, $jj = 1; $ii < count($itemKey); $ii++, $jj++) {
+            $jsonValue = false;
+            $classActive = '';
+            $htmlFormElementIdUniq = HelperFormElement::prependFormElementIdCheckBoxMulti($htmlFormElementId, $ii);
+            $attribute .= Support::doAttribute('name', $htmlFormElementIdUniq);
+
+            $attribute .= Support::doAttribute('value', $itemKey[$ii]);
+
+            // Check if the given key is found in field.
+            if (false !== array_search($itemKey[$ii], $values)) {
+                $attribute .= Support::doAttribute('checked', 'checked');
+                $jsonValue = true;
+                $classActive = ' active';
+            }
+
+            // '&nbsp;' - This is necessary to correctly align an empty input.
+            $value = ($itemValue[$ii] === '') ? '&nbsp;' : $itemValue[$ii];
+
+            $htmlElement = '<input ' . $attribute . '>' . $value;
+
+            $html .= Support::wrapTag("<label class='btn " . $formElement[FE_BUTTON_CLASS] . "$classActive'>",
+                $htmlElement, true);
+
+            $json[] = $this->getJsonElementUpdate($htmlFormElementIdUniq, $jsonValue, $formElement[FE_MODE]);
+
+            // Init for the next checkbox
+            $attribute = $attributeBase;
+        }
+
+        $html = Support::wrapTag('<div class="btn-group" data-toggle="buttons">', $html);
+
+        return $html;
+    }
+
+    /**
+     * Build as many Checkboxes as items.
+     *
+     * Layout: The Bootstrap Layout needs very special setup, the checkboxes are wrapped differently with <div class=checkbox>
+     *         depending of if they aligned horizontal or vertical.
+     *
+     * @param array $formElement
+     * @param $htmlFormElementId
+     * @param $attributeBase
+     * @param $value
+     * @param array $itemKey
+     * @param array $itemValue
+     * @param array $json
+     * @return string
+     */
+    public function constructCheckboxMultiPlain(array $formElement, $htmlFormElementId, $attributeBase, $value, array $itemKey, array $itemValue, array &$json) {
         $json = array();
 
         // Defines which of the checkboxes will be checked.
@@ -1292,9 +1473,6 @@ abstract class AbstractBuildForm {
 
         }
 
-//        if (isset($formElement[CHECKBOX_ORIENTATION]) && $formElement[CHECKBOX_ORIENTATION] !== 'vertical')
-//            $html = Support::wrapTag("<div class='checkbox'>", $html, true);
-//
         return $html;
     }
 
@@ -1393,6 +1571,7 @@ abstract class AbstractBuildForm {
         $attributeBase .= Support::doAttribute('name', $htmlFormElementId);
         $attributeBase .= Support::doAttribute('type', $formElement[FE_TYPE]);
         $attributeBase .= Support::doAttribute('data-load', ($formElement[FE_DYNAMIC_UPDATE] === 'yes') ? 'data-load' : '');
+        $attributeBase .= Support::doAttribute('autocomplete', 'off');
 
         $attribute = $attributeBase;
         if (isset($formElement['autofocus'])) {
diff --git a/extension/qfq/qfq/helper/OnArray.php b/extension/qfq/qfq/helper/OnArray.php
index a9b638964..54b48b57f 100644
--- a/extension/qfq/qfq/helper/OnArray.php
+++ b/extension/qfq/qfq/helper/OnArray.php
@@ -138,4 +138,33 @@ class OnArray {
         return $arr;
     }
 
+    /**
+     * Iterates over $arr and removes all empty (='') elements.
+     *
+     * @param array $arr
+     * @return array
+     */
+    public static function removeEmptyElementsFromArray(array $arr) {
+
+        $new = array();
+
+//        for($ii=0; $ii<count($arr); $ii++) {
+//            if($arr[$ii]!='') {
+//                $new[]=$arr[$ii];
+//            }
+//        }
+
+        $ii = 0;
+        foreach ($arr as $key => $value) {
+            if ($value != '') {
+                if (is_int($key)) {
+                    $key = $ii;
+                    $ii++;
+                }
+                $new[$key] = $value;
+            }
+        }
+
+        return $new;
+    }
 }
\ No newline at end of file
diff --git a/extension/qfq/tests/phpunit/OnArrayTest.php b/extension/qfq/tests/phpunit/OnArrayTest.php
index 22a0d6151..c60e3c7e6 100644
--- a/extension/qfq/tests/phpunit/OnArrayTest.php
+++ b/extension/qfq/tests/phpunit/OnArrayTest.php
@@ -43,6 +43,50 @@ class OnArrayTest extends \PHPUnit_Framework_TestCase {
         $raw = ['hello', '"next"', '"without trailing', 'without leading"', ' with whitespace ', '" with tick and whitespace "', ''];
         $expected = ['hello', 'next', 'without trailing', 'without leading', ' with whitespace ', ' with tick and whitespace ', ''];
 
-        $this->assertEquals(OnArray::trimArray($raw, '"'), $expected);
+        $this->assertEquals($expected, OnArray::trimArray($raw, '"'));
+    }
+
+    public function testRemoveEmptyElementsFromArray() {
+        $this->assertEquals(array(), OnArray::removeEmptyElementsFromArray(array()));
+
+        $expected = array('Hello world');
+        $this->assertEquals($expected, OnArray::removeEmptyElementsFromArray($expected));
+
+        $expected = array('Hello world', 'Blue sky');
+        $this->assertEquals($expected, OnArray::removeEmptyElementsFromArray($expected));
+
+        $raw2 = array('Hello world', '', 'Blue sky');
+        $this->assertEquals($expected, OnArray::removeEmptyElementsFromArray($raw2));
+
+        $raw2 = array('Hello world', 'Blue sky', '');
+        $this->assertEquals($expected, OnArray::removeEmptyElementsFromArray($raw2));
+
+        $raw2 = array('', 'Hello world', 'Blue sky');
+        $this->assertEquals($expected, OnArray::removeEmptyElementsFromArray($raw2));
+
+
+        $expected = array('first' => 'Hello world');
+        $this->assertEquals($expected, OnArray::removeEmptyElementsFromArray($expected));
+
+        $expected = array('first' => 'Hello world', 'second' => 'Blue sky');
+        $this->assertEquals($expected, OnArray::removeEmptyElementsFromArray($expected));
+
+        $raw2 = array('first' => 'Hello world', 'second' => '', 'third' => 'Blue sky');
+        $expected = array('first' => 'Hello world', 'third' => 'Blue sky');
+        $this->assertEquals($expected, OnArray::removeEmptyElementsFromArray($raw2));
+
+        $raw2 = array('first' => 'Hello world', 'second' => 'Blue sky', 'third' => '');
+        $expected = array('first' => 'Hello world', 'second' => 'Blue sky');
+        $this->assertEquals($expected, OnArray::removeEmptyElementsFromArray($raw2));
+
+        $raw2 = array('first' => '', 'second' => 'Hello world', 'third' => 'Blue sky');
+        $expected = array('second' => 'Hello world', 'third' => 'Blue sky');
+        $this->assertEquals($expected, OnArray::removeEmptyElementsFromArray($raw2));
+
+
+        $raw2 = array('first' => '', '' => 'Hello world', 'third' => 'Blue sky');
+        $expected = array('' => 'Hello world', 'third' => 'Blue sky');
+        $this->assertEquals($expected, OnArray::removeEmptyElementsFromArray($raw2));
+
     }
 }
-- 
GitLab