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'; + } + + // ' ' - This is necessary to correctly align an empty input. + $value = ($itemValue[$ii] === '') ? ' ' : $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