Commit ace9f9c8 authored by Carsten  Rose's avatar Carsten Rose
Browse files

Merge branch 'B9720CheckboxVariousSetups' into 'master'

B9720 checkbox various setups

See merge request !221
parents 2baf2b77 dabd2b82
Pipeline #2985 passed with stages
in 3 minutes and 39 seconds
......@@ -53,6 +53,7 @@ composer.lock
/extension/Resources/Public/Css
/extension/Resources/Public/fonts
/extension/Resources/Public/JavaScript
/extension/Tests/selenium/tmp
/fonts
/js
/node_modules
......@@ -63,3 +64,7 @@ composer.lock
/javascript/src/.vscode
/javascript/src/npm-debug.log
/docker/chromedriver
/docker/geckodriver
/docker/run_qfq_docker.output
......@@ -65,7 +65,12 @@ selenium:
- mkdir "$SELENIUM_LOGS_PATH/$CI_COMMIT_SHORT_SHA"
- cp extension/Tests/selenium/selenium_logs/* "$SELENIUM_LOGS_PATH/$CI_COMMIT_SHORT_SHA/"
- echo "Selenium Logs copied to $SELENIUM_LOGS_PATH/$CI_COMMIT_SHORT_SHA/"
- echo "Or download result (log/screenshot) in gitlab under CI/CD > Pipelines <job> > right side 'Artifacts'"
artifacts:
expire_in: 1 week
paths:
- extension/Tests/selenium/selenium_logs/
**To run/update SELENIUM test, please check docker/README.md.**
Create new Tests
================
* Set up new forms and reports.
* Dump new database.
* Copy dump to become now initial database
* Update SELENIUM Python tests
......@@ -7,7 +7,7 @@
.. ;;
.. ,,
..
.. --------------------------------------------------
.. --------------------------------------------used to the update the records specified ------
.. Best Practice T3 reST: https://docs.typo3.org/m/typo3/docs-how-to-document/master/en-us/WritingReST/CheatSheet.html
.. Reference: https://docs.typo3.org/m/typo3/docs-how-to-document/master/en-us/WritingReST/Index.html
.. Italic *italic*
......@@ -7618,7 +7618,7 @@ A dedicated `Form`, without any `FormElements`, is used to define the reorder lo
Fields:
* Name: <custom form name> - used in Part 1 in the `_data-dnd-api` variable.
* Table: <table with the element records> - used to the update the records specified by `dragAndDropOrderSql`.
* Table: <table with the element records> - used to update the records specified by `dragAndDropOrderSql`.
* Parameter:
......
......@@ -440,6 +440,16 @@ module.exports = function (grunt) {
expand: true,
flatten: true
},
{
cwd: 'node_modules/font-awesome/css/',
src: [
'font-awesome.min.css'
],
dest: 'css/',
filter: 'isFile',
expand: true,
flatten: true
},
{
cwd: 'node_modules/font-awesome/fonts/',
expand: true,
......@@ -448,6 +458,15 @@ module.exports = function (grunt) {
],
dest: typo3_fonts,
flatten: true
},
{
cwd: 'node_modules/font-awesome/fonts/',
expand: true,
src: [
'*'
],
dest: 'fonts/',
flatten: true
}
]
},
......
This diff is collapsed.
This diff is collapsed.
......@@ -772,6 +772,11 @@ EOF;
if ($wrapName == FE_WRAP_LABEL) {
$wrapArray[0] = Support::insertAttribute($wrapArray[0], 'style', 'text-align: ' . $formElement[F_FE_LABEL_ALIGN] . ';'); // might be problematic, if there is already a 'class' defined.
}
// Insert 'required="required"' for checkboxes and radios
if ($wrapName == FE_WRAP_INPUT && $formElement[FE_MODE] == FE_MODE_REQUIRED &&
($formElement[FE_TYPE] == FE_TYPE_CHECKBOX || $formElement[FE_TYPE] == FE_TYPE_RADIO)) {
$wrapArray[0] = Support::insertAttribute($wrapArray[0], 'required', 'required'); // might be problematic, if there is already a 'class' defined.
}
}
return $wrapArray[0] . $htmlElement . $wrapArray[1];
......@@ -807,7 +812,7 @@ EOF;
// Label
if ($formElement[FE_BS_LABEL_COLUMNS] != '0') {
$htmlLabel = $this->buildLabel($htmlFormElementName, $formElement[FE_LABEL], $addClassRequired[FE_LABEL] ?? '');
$htmlLabel = HelperFormElement::buildLabel($htmlFormElementName, $formElement[FE_LABEL], $addClassRequired[FE_LABEL] ?? '');
}
$html .= $this->customWrap($formElement, $htmlLabel, FE_WRAP_LABEL, $formElement[FE_BS_LABEL_COLUMNS],
......
......@@ -1252,6 +1252,7 @@ const FE_INPUT_AUTOCOMPLETE = 'autocomplete';
const FE_TMP_EXTRA_BUTTON_HTML = '_extraButtonHtml'; // will be filled on the fly during building extrabutton
const FE_CHECKBOX_CHECKED = 'checked';
const FE_CHECKBOX_UNCHECKED = 'unchecked';
const FE_ITEM_LIST = 'itemList';
const FE_RECORD_DESTINATION_TABLE = 'recordDestinationTable';
const FE_RECORD_SOURCE_TABLE = 'recordSourceTable';
const FE_TRANSLATE_ID_COLUMN = 'translateIdColumn';
......@@ -1328,6 +1329,8 @@ const FE_ORDER_INTERVAL_DEFAULT = '10';
const FE_ORDER_COLUMN = 'orderColumn';
const FE_DND_TABLE = 'dndTable';
const FE_TMP_CLASS_OPTION = '_classOption';
const MODE_ENCODE = 'encode';
const MODE_DECODE = 'decode';
const MODE_NONE = 'none';
......@@ -1812,6 +1815,7 @@ const DIRTY_API_ACTION_EXTEND = 'extend';
const LOCK_NOT_FOUND = 0;
const LOCK_FOUND_OWNER = 1;
const LOCK_FOUND_CONFLICT = 2;
const TAB_UNIQ_ID = 'tabUniqId'; // Currently only only a uniq identifier: no values stored behind the identifier - might change.
// AutoCron
const AUTOCRON_MAX_AGE_MINUTES = 10;
......@@ -1949,3 +1953,20 @@ const SETTING_TABLESORTER_MODE = 'mode';
const SETTING_TABLESORTER_MODE_DELETE = 'delete';
const SETTING_TABLESORTER_CLEAR = 'Clear';
// Object: Type
const T_LABEL = 't_label';
const T_INPUT = 't_input';
const T_NOTE = 't_note';
const T_ = 't_note';
// Object: Item
const I_TYPE = 'type';
const I_ID = 'id';
const I_VALUE = 'value';
const I_CLASS = 'class';
const I_ATTRIBUTE = 'attribute';
// Object: extra for checkboxes
const I_CHECKED = 'checked';
const I_UNCHECKED = 'unchecked';
......@@ -185,6 +185,10 @@ $UPDATE_ARRAY = array(
"ALTER TABLE `Setting` CHANGE `created` `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; ",
],
'19.12.0' => [
"ALTER TABLE `Dirty` ADD `tabUniqId` VARCHAR(32) NOT NULL AFTER `recordHashMd5`;",
],
);
......
This diff is collapsed.
......@@ -8,13 +8,12 @@
namespace IMATHUZH\Qfq\Core\Form;
use IMATHUZH\Qfq\Core\Store\Session;
use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Store\Client;
use IMATHUZH\Qfq\Core\Store\Session;
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Store\Store;
/**
* Class Dirty
......@@ -27,7 +26,7 @@ use IMATHUZH\Qfq\Core\Helper\OnArray;
class Dirty {
/**
* @var Database instantiated class
* @var Database[] - Array of Database instantiated class
*/
protected $dbArray = null;
......@@ -134,7 +133,7 @@ class Dirty {
switch ($this->client[API_LOCK_ACTION]) {
case API_LOCK_ACTION_LOCK:
case API_LOCK_ACTION_EXTEND:
$answer = $this->acquireDirty($recordId, $tableVars, $this->client[DIRTY_RECORD_HASH_MD5]);
$answer = $this->acquireDirty($recordId, $tableVars, $this->client[DIRTY_RECORD_HASH_MD5], $this->client[TAB_UNIQ_ID]);
break;
case API_LOCK_ACTION_RELEASE:
$answer = $this->checkDirtyAndRelease(FORM_SAVE, $tableVars[F_RECORD_LOCK_TIMEOUT_SECONDS], $tableVars[F_DIRTY_MODE], $tableVars[F_TABLE_NAME], $tableVars[F_PRIMARY_KEY], $recordId);
......@@ -153,10 +152,13 @@ class Dirty {
* @param array $tableVars
* @param string $recordHashMd5
*
* @param $tabUniqId
* @return array
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
private function acquireDirty($recordId, array $tableVars, $recordHashMd5) {
private function acquireDirty($recordId, array $tableVars, $recordHashMd5, $tabUniqId) {
$tableName = $tableVars[F_TABLE_NAME];
$primaryKey = $tableVars[F_PRIMARY_KEY];
......@@ -179,10 +181,14 @@ class Dirty {
$answer = [API_STATUS => 'success', API_MESSAGE => ''];
} else {
// No dirty record found.
$answer = $this->writeDirty($this->client[SIP_SIP], $recordId, $tableVars, $feUser, $rcMd5);
$answer = $this->writeDirty($this->client[SIP_SIP], $recordId, $tableVars, $feUser, $rcMd5, $tabUniqId);
}
} else {
$answer = $this->conflict($recordDirty, $formDirtyMode, $primaryKey);
if ($tabUniqId == $recordDirty[TAB_UNIQ_ID]) {
$answer = [API_STATUS => 'success', API_MESSAGE => ''];
} else {
$answer = $this->conflict($recordDirty, $formDirtyMode, $primaryKey);
}
}
return $answer;
......@@ -197,6 +203,8 @@ class Dirty {
*
* @return array DirtyRecord or empty array.
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
private function getRecordDirty($tableName, $recordId) {
......@@ -219,6 +227,9 @@ class Dirty {
*
* @param $primaryKey
* @return array
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
private function conflict(array $recordDirty, $currentFormDirtyMode, $primaryKey) {
$status = API_ANSWER_STATUS_CONFLICT;
......@@ -263,9 +274,13 @@ class Dirty {
* @param string $feUser
* @param string $recordHashMd5
*
* @param $tabUniqId
* @return array
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
private function writeDirty($s, $recordId, array $tableVars, $feUser, $recordHashMd5) {
private function writeDirty($s, $recordId, array $tableVars, $feUser, $recordHashMd5, $tabUniqId) {
$tableName = $tableVars[F_TABLE_NAME];
$primaryKey = $tableVars[F_PRIMARY_KEY];
......@@ -276,9 +291,9 @@ class Dirty {
# Dirty workaround: setting the 'expired timestamp' minus 1 second guarantees that the client ask for relock always if the timeout is expired.
$expire = date('Y-m-d H:i:s', strtotime("+" . $tableVars[F_RECORD_LOCK_TIMEOUT_SECONDS] - 1 . " seconds"));
// Write 'dirty' record
$this->dbArray[$this->dbIndexQfq]->sql("INSERT INTO Dirty (`sip`, `tableName`, `recordId`, `expire`, `recordHashMd5`, `feUser`, `qfqUserSessionCookie`, `dirtyMode`, `remoteAddress`, `created`) " .
"VALUES ( ?,?,?,?,?,?,?,?,?,? )", ROW_REGULAR,
[$s, $tableName, $recordId, $expire, $recordHashMd5, $feUser, $this->client[CLIENT_COOKIE_QFQ], $formDirtyMode,
$this->dbArray[$this->dbIndexQfq]->sql("INSERT INTO Dirty (`sip`, `tableName`, `recordId`, `expire`, `recordHashMd5`, `tabUniqId`, `feUser`, `qfqUserSessionCookie`, `dirtyMode`, `remoteAddress`, `created`) " .
"VALUES ( ?,?,?,?,?,?,?,?,?,?,? )", ROW_REGULAR,
[$s, $tableName, $recordId, $expire, $recordHashMd5, $tabUniqId, $feUser, $this->client[CLIENT_COOKIE_QFQ], $formDirtyMode,
$this->client[CLIENT_REMOTE_ADDRESS], date('YmdHis')]);
return [API_STATUS => API_ANSWER_STATUS_SUCCESS, API_MESSAGE => '',
......@@ -295,6 +310,9 @@ class Dirty {
* @param string $recordHashMd5 - timestamp e.g. '2017-07-27 14:06:56'
* @param $rcMd5
* @return bool true if $recordHashMd5 is different from current record md5 hash.
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
private function isRecordModified($tableName, $primaryKey, $recordId, $recordHashMd5, &$rcMd5) {
......@@ -319,6 +337,8 @@ class Dirty {
*
* @return int LOCK_NOT_FOUND | LOCK_FOUND_OWNER | LOCK_FOUND_CONFLICT,
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
public function getCheckDirty($tableName, $recordId, array &$recordDirty, &$msg) {
......@@ -334,6 +354,11 @@ class Dirty {
return LOCK_NOT_FOUND;
}
// 'Reload Tab' don't send a tab ID - Possible lock conflict will be skipped here and a) pops up later, or b) is not a conflict if it's the same tab.
if (!isset($this->client[TAB_UNIQ_ID]) && $recordDirty[DIRTY_QFQ_USER_SESSION_COOKIE] == $this->client[CLIENT_COOKIE_QFQ]) {
return LOCK_NOT_FOUND;
}
if ($recordDirty[DIRTY_QFQ_USER_SESSION_COOKIE] == $this->client[CLIENT_COOKIE_QFQ]) {
$msgUser = "you";
} else {
......@@ -364,6 +389,7 @@ class Dirty {
* @param bool $flagCheckModifiedFirst
* @return array
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
public function checkDirtyAndRelease($formMode, $lockTimeout, $dirtyMode, $tableName, $primaryKey, $recordId, $flagCheckModifiedFirst = false) {
......@@ -446,6 +472,8 @@ class Dirty {
* @param int $recordDirtyId
*
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
private function deleteDirtyRecord($recordDirtyId) {
......
......@@ -17,6 +17,11 @@ use IMATHUZH\Qfq\Core\Store\Store;
*/
class HelperFormElement {
/**
* @var Store
*/
private static $store = null;
/**
* Expand column $keyName to row array as virtual columns.
* E.g.: [ 'id' => '1', 'name' => 'John', 'parameter' => 'detail=xId:grId\nShowEmptyAtStart=1' ] becomes:
......@@ -88,9 +93,9 @@ class HelperFormElement {
$checkKeys = array_keys($arr);
foreach ($checkKeys AS $checkKey) {
if (!empty($element[$checkKey])) {
$store = Store::getInstance();
$store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($element), STORE_SYSTEM);
$store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, $keyName, STORE_SYSTEM);
self::$store = Store::getInstance();
self::$store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($element), STORE_SYSTEM);
self::$store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, $keyName, STORE_SYSTEM);
throw new \UserFormException("Found reserved keyname '$checkKey'", ERROR_RESERVED_KEY_NAME);
}
}
......@@ -361,12 +366,12 @@ class HelperFormElement {
*/
public static function prepareExtraButton(array $formElement, $showInline) {
$store = Store::getInstance();
self::$store = Store::getInstance();
$infoSymbolInside = $store->getVar(SYSTEM_EXTRA_BUTTON_INFO_INLINE, STORE_SYSTEM);
$infoSymbolOutside = $store->getVar(SYSTEM_EXTRA_BUTTON_INFO_BELOW, STORE_SYSTEM);
$infoSymbolInside = self::$store->getVar(SYSTEM_EXTRA_BUTTON_INFO_INLINE, STORE_SYSTEM);
$infoSymbolOutside = self::$store->getVar(SYSTEM_EXTRA_BUTTON_INFO_BELOW, STORE_SYSTEM);
if (SYSTEM_EXTRA_BUTTON_INFO_POSITION_BELOW == $store->getVar(SYSTEM_EXTRA_BUTTON_INFO_POSITION, STORE_SYSTEM)) {
if (SYSTEM_EXTRA_BUTTON_INFO_POSITION_BELOW == self::$store->getVar(SYSTEM_EXTRA_BUTTON_INFO_POSITION, STORE_SYSTEM)) {
$showInline = false;
}
......@@ -560,4 +565,299 @@ EOF;
return $data == '' || $data == '1';
}
/**
* Look for key/value list (in this order, first match counts) in
* a) `sql1`
* b) `parameter:itemList`
* c) table.column definition
*
* Copies the found keys to &$itemKey and the values to &$itemValue
* If there are no &$itemKey, copy &$itemValue to &$itemKey.
*
* @param array $formElement
* @param array $itemKey
* @param array $itemValue
*
* @throws \CodeException
* @throws \UserFormException
* @throws \UserReportException
*/
public static function getKeyValueListFromSqlEnumSpec(array $formElement, array &$itemKey, array &$itemValue) {
$fieldType = '';
$itemKey = array();
$itemValue = array();
self::$store = Store::getInstance();
// Call getItemsForEnumOrSet() only if there a corresponding column really exist.
if (false !== self::$store->getVar($formElement[FE_NAME], STORE_TABLE_COLUMN_TYPES)) {
$itemValue = self::getItemsForEnumOrSet($formElement[FE_NAME], $fieldType);
}
if (is_array($formElement[FE_SQL1])) {
if (count($formElement[FE_SQL1]) > 0) {
$keys = array_keys($formElement[FE_SQL1][0]);
$itemKey = array_column($formElement[FE_SQL1], 'id');
// If there is no column 'id' and at least two columns in total
if (count($itemKey) === 0 && count($keys) >= 2) {
$itemKey = array_column($formElement[FE_SQL1], $keys[0]);
}
$itemValue = array_column($formElement[FE_SQL1], 'label');
// If there is no column 'label' (e.g.: SHOW tables)
if (count($itemValue) === 0) {
$idx = count($keys) == 1 ? 0 : 1;
$itemValue = array_column($formElement[FE_SQL1], $keys[$idx]);
}
}
} elseif (isset($formElement[FE_ITEM_LIST]) && strlen($formElement[FE_ITEM_LIST]) > 0) {
$arr = KeyValueStringParser::parse($formElement[FE_ITEM_LIST], ':', ',', KVP_IF_VALUE_EMPTY_COPY_KEY);
$itemValue = array_values($arr);
$itemKey = array_keys($arr);
} elseif ($fieldType === 'enum' || $fieldType === 'set') {
// already done at the beginning with '$this->getItemsForEnumOrSet($formElement[FE_NAME], $fieldType);'
} elseif (isset($formElement[FE_CHECKBOX_CHECKED]) && $formElement[FE_TYPE] == FE_TYPE_CHECKBOX) {
// Nothing to do here.
} else {
throw new \UserFormException("Missing definition (- nothing found in 'sql1', 'parameter:itemList', 'enum-' or 'set-definition'", ERROR_MISSING_ITEM_LIST);
}
if (count($itemKey) === 0) {
$itemKey = $itemValue;
}
// Process 'emptyHide' before 'emptyItemAtStart' / 'emptyItemAtEnd': than 'emptyItem*' are still possible.
if (isset($formElement['emptyHide'])) {
$itemKey = OnArray::removeEmptyElementsFromArray($itemKey);
$itemValue = OnArray::removeEmptyElementsFromArray($itemValue);
}
if (isset($formElement[FE_EMPTY_ITEM_AT_START])) {
$placeholder = isset($formElement[FE_PLACEHOLDER]) ? $formElement[FE_PLACEHOLDER] : '';
array_unshift($itemKey, '');
array_unshift($itemValue, $placeholder);
}
if (isset($formElement[FE_EMPTY_ITEM_AT_END])) {
$itemKey[] = '';
$itemValue[] = '';
}
}
/**
* Get the attribute definition list of an enum or set column. For strings, get the default value.
* Return elements as an array.
*
* @param string $column
* @param string $fieldType
*
* @return array
* @throws \CodeException
* @throws \UserFormException
* @throws \UserReportException
*/
private static function getItemsForEnumOrSet($column, &$fieldType) {
self::$store = Store::getInstance();
// Get column definition
$fieldTypeDefinition = self::$store->getVar($column, STORE_TABLE_COLUMN_TYPES);
if ($fieldTypeDefinition === false) {
throw new \UserFormException("Column '$column' in primary table.", ERROR_DB_UNKNOWN_COLUMN);
}
$length = strlen($fieldTypeDefinition);
// enum('... set('
switch (substr($fieldTypeDefinition, 0, 4)) {
case 'enum':
$startPosition = 5;
break;
case 'set(':
$startPosition = 4;
break;
default:
$fieldType = 'string';
return array();
}
// enum('a','b','c', ...) >> [ 'a', 'b', 'c', ... ]
// set('a','b','c', ...) >> [ 'a', 'b', 'c', ... ]
$values = substr($fieldTypeDefinition, $startPosition, $length - $startPosition - 1);
$items = OnArray::trimArray(explode(',', $values), "'");
$fieldType = substr($fieldTypeDefinition, 0, $startPosition - 1);
return $items;
}
/**
* Set corresponding html attributes readonly/required/disabled, based on $formElement[FE_MODE].
*
* @param string $feMode
*
* @param bool $cssDisable
* @return string
* @throws \CodeException
* @throws \UserFormException
*/
public static function getAttributeFeMode($feMode, $cssDisable = true) {
$attribute = '';
self::getFeMode($feMode, $hidden, $disabled, $required);
switch ($feMode) {
case FE_MODE_HIDDEN:
case FE_MODE_SHOW:
case FE_MODE_SHOW_REQUIRED:
break;
case FE_MODE_REQUIRED:
case FE_MODE_READONLY:
$attribute .= Support::doAttribute($feMode, $feMode);
break;
default:
throw new \UserFormException("Unknown mode '$feMode'", ERROR_UNKNOWN_MODE);
break;
}
// Attributes: data-...
$attribute .= Support::doAttribute(DATA_HIDDEN, $hidden);
if ($cssDisable) {
$attribute .= Support::doAttribute(DATA_DISABLED, $disabled);
}
$attribute .= Support::doAttribute(DATA_REQUIRED, $required);
return $attribute;
}
/**
* Depending of $feMode set variables $hidden, $disabled, $required to 'yes' or 'no'.
*
* @param string $feMode
* @param string $hidden
* @param string $disabled
* @param string $required
*
* @throws \UserFormException
*/
public static function getFeMode($feMode, &$hidden, &$disabled, &$required) {
$hidden = 'no';
$disabled = 'no';
$required = 'no';
switch ($feMode) {
case FE_MODE_SHOW:
case FE_MODE_SHOW_REQUIRED:
break;
case FE_MODE_REQUIRED:
$required = 'yes';
break;
case FE_MODE_READONLY:
$disabled = 'yes'; // convert the UI status 'readonly' to the HTML/CSS status 'disabled'.
break;
case FE_MODE_HIDDEN:
$hidden = 'yes';
break;
default:
throw new \UserFormException("Unknown mode '$feMode'", ERROR_UNKNOWN_MODE);
break;
}
}
/**
* Builds a HTML attribute list, based on $attributeList.
*
* E.g.: attributeList: [ 'type', 'autofocus' ]
* generates: 'type="$formElement[FE_TYPE]" autofocus="$formElement[FE_AUTOFOCUS]" '
*
* @param array $formElement
* @param array $attributeList
* @param bool $flagOmitEmpty
*
* @return string
* @throws \CodeException
*/
public static function getAttributeList(array $formElement, array $attributeList, $flagOmitEmpty = true) {
$attribute = '';
foreach ($attributeList as $item) {
if (isset($formElement[$item]))
$attribute .= Support::doAttribute(strtolower($item), $formElement[$item], $flagOmitEmpty);
}
return $attribute;
}