AbstractBuildForm.php 54.6 KB
Newer Older
1
<?php
Carsten  Rose's avatar
Carsten Rose committed
2
3
4
5
6
7
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 1/6/16
 * Time: 8:02 PM
 */
8
9
10
namespace qfq;

use qfq;
11
use qfq\Store;
12
use qfq\UserException;
13
use qfq\OnArray;
14
15
16
17

require_once(__DIR__ . '/../qfq/store/Store.php');
require_once(__DIR__ . '/../qfq/Constants.php');
require_once(__DIR__ . '/../qfq/exceptions/DbException.php');
18
require_once(__DIR__ . '/../qfq/exceptions/UserException.php');
19
require_once(__DIR__ . '/../qfq/Database.php');
20
require_once(__DIR__ . '/../qfq/helper/HelperFormElement.php');
21
require_once(__DIR__ . '/../qfq/helper/Support.php');
22
require_once(__DIR__ . '/../qfq/helper/OnArray.php');
23

24
25

/**
Carsten  Rose's avatar
Carsten Rose committed
26
27
 * Class AbstractBuildForm
 * @package qfq
28
 */
29
abstract class AbstractBuildForm {
30
31
32
    protected $formSpec = array();  // copy of the loaded form
    protected $feSpecAction = array(); // copy of all formElement.class='action' of the loaded form
    protected $feSpecNative = array(); // copy of all formElement.class='native' of the loaded form
33
    protected $store = null;
Carsten  Rose's avatar
Carsten Rose committed
34
    protected $evaluate = null;
35
36
    protected $buildElementFunctionName = array();
    protected $pattern = array();
37
    protected $wrap = array();
38
    protected $symbol = array();
Carsten  Rose's avatar
Carsten Rose committed
39
40
    protected $showDebugInfo = false;

41
//    protected $feDivClass = array(); // Wrap FormElements in <div class="$feDivClass[type]">
42

43
44
    private $formId = null;

45
46
47
48
49
50
51
    /**
     * AbstractBuildForm constructor.
     *
     * @param array $formSpec
     * @param array $feSpecAction
     * @param array $feSpecNative
     */
52
53
54
55
56
    public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative) {
        $this->formSpec = $formSpec;
        $this->feSpecAction = $feSpecAction;
        $this->feSpecNative = $feSpecNative;
        $this->store = Store::getInstance();
57
        $this->db = new Database();
Carsten  Rose's avatar
Carsten Rose committed
58
        $this->evaluate = new Evaluate($this->store, $this->db);
Carsten  Rose's avatar
Carsten Rose committed
59
        $this->showDebugInfo = ($this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM) === 'yes');
60

61
62
//        $sip = $this->store->getVar(CLIENT_SIP, STORE_CLIENT);

63
        // render mode specific
64
        $this->fillWrap();
65
66
67

        $this->buildElementFunctionName = [
            'checkbox' => 'Checkbox',
68
69
            'dateJQW' => 'DateJQW',
            'datetimeJQW' => 'DateJQW',
70
71
72
            'email' => 'Input',
            'gridJQW' => 'GridJQW',
            'hidden' => 'Hidden',
73
            'text' => 'Input',
74
75
76
77
            'note' => 'Note',
            'password' => 'Input',
            'radio' => 'Radio',
            'select' => 'Select',
78
            'subrecord' => 'Subrecord',
Carsten  Rose's avatar
Carsten Rose committed
79
            'upload' => 'File',
80
81
            'fieldset' => 'Fieldset',
            'pill' => 'Pill'
82
83
        ];

84
85
86
87
88
89
90
        $this->buildRowName = [
            'checkbox' => 'Native',
            'dateJQW' => 'Native',
            'datetimeJQW' => 'Native',
            'email' => 'Native',
            'gridJQW' => 'Native',
            'hidden' => 'Native',
91
            'text' => 'Native',
92
93
94
95
96
97
98
99
100
101
            'note' => 'Native',
            'password' => 'Native',
            'radio' => 'Native',
            'select' => 'Native',
            'subrecord' => 'Subrecord',
            'upload' => 'Native',
            'fieldset' => 'Fieldset',
            'pill' => 'Pill'
        ];

102
103
104
        $this->symbol[SYMBOL_EDIT] = "<span class='glyphicon glyphicon-pencil'></span>";
        $this->symbol[SYMBOL_NEW] = "<span class='glyphicon glyphicon-plus'></span>";
        $this->symbol[SYMBOL_DELETE] = "<span class='glyphicon glyphicon-trash'></span>";
105

106
        $this->inputCheckPattern = OnArray::inputCheckPatternArray();
107
108
    }

109
110
    abstract public function fillWrap();

111
    /**
112
     * Builds complete form. Depending of Formspecification, the layout will be 'plain' / 'table' / 'bootstrap'.
113
     *
Carsten  Rose's avatar
Carsten Rose committed
114
115
     * @param $mode
     * @return string|array   $mode=LOAD_FORM: The whole form as HTML, $mode=FORM_UPDATE: array of all formElement.dynamicUpdate-yes  values/states
116
117
     * @throws CodeException
     * @throws DbException
Carsten  Rose's avatar
Carsten Rose committed
118
     * @throws \qfq\UserException
119
     */
Carsten  Rose's avatar
Carsten Rose committed
120
121
122
123
124
125
    public function process($mode) {
        $htmlHead = '';
        $htmlTail = '';
        $htmlSubrecords = '';
        $htmlElements = '';
        $json = array();
126

127
        // <form>
Carsten  Rose's avatar
Carsten Rose committed
128
129
130
131
132
        if ($mode === FORM_LOAD) {
            $htmlHead = $this->head();
            $htmlTail = $this->tail();
            $htmlSubrecords = $this->doSubrecords();
        }
133

134
        $filter = $this->getProcessFilter();
135

136
        if ($this->formSpec['multiMode'] !== 'none') {
137

138
139
            $parentRecords = $this->db->sql($this->formSpec['multiSql']);
            foreach ($parentRecords as $row) {
140
                $this->store->setVarArray($row, STORE_PARENT_RECORD, true);
Carsten  Rose's avatar
Carsten Rose committed
141
142
143
                $jsonTmp = array();
                $htmlElements = $this->elements($row['_id'], $filter, 0, $jsonTmp);
                $json[] = $jsonTmp;
144
145
            }
        } else {
Carsten  Rose's avatar
Carsten Rose committed
146
            $htmlElements = $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), $filter, 0, $json);
147
148
        }

149
        // </form>
150

Carsten  Rose's avatar
Carsten Rose committed
151
        return ($mode === FORM_LOAD) ? $htmlHead . $htmlElements . $htmlTail . $htmlSubrecords : $json;
152
153
    }

154
    /**
155
     * Builds the head area of the form.
156
     *
157
     * @return string
158
     */
159
160
    public function head() {
        $html = '';
161

162
        $html .= '<div ' . $this->getAttribute('class', $this->formSpec['class'], TRUE) . '>'; // main <div class=...> around everything
163

164
165
166
        // Logged in BE User will see a FormEdit Link
        $sipParamString = OnArray::toString($this->store->getStore(STORE_SIP), ':', ', ', "'");
        $formEditUrl = $this->createFormEditUrl();
167

168
        $html .= "<p><a href='$formEditUrl'>Edit</a> <small>[$sipParamString]</small></p>";
169

170
        $html .= $this->wrapItem(WRAP_SETUP_TITLE, $this->formSpec['title'], true);
171

172
173
174
        $html .= $this->getFormTag();

        return $html;
175
176
177
    }

    /**
178
     * Format's an attribute: $type=$value. If $flagOmitEmpty==true && $value=='': return ''.
179
     *
180
181
182
183
     * @param $type
     * @param $value
     * @param bool|false $flagOmitEmpty
     * @return string
184
     */
185
186
187
    public function getAttribute($type, $value, $flagOmitEmpty = true) {
        if ($flagOmitEmpty && $value === "")
            return '';
188

189
        return $type . '="' . trim($value) . '" ';
190
191
    }

192
    /**
193
     * If SHOW_DEBUG_INFO=yes: create a link (incl. SIP) to edit the current form. Show also the hidden content of the SIP.
194
     *
195
     * @return string String: <a href="?pageId&sip=....">Edit</a> <small>[sip:..., r:..., urlparam:..., ...]</small>
196
     */
197
    public function createFormEditUrl() {
198

Carsten  Rose's avatar
Carsten Rose committed
199
        if (!$this->showDebugInfo) {
200
201
            return '';
        }
202

203
204
205
206
207
        $queryStringArray = [
            'id' => $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3),
            'form' => 'form',
            'r' => $this->formSpec['id']
        ];
208

209
        $queryString = Support::arrayToQueryString($queryStringArray);
210

211
212
        $sip = $this->store->getSipInstance();
        $url = $sip->queryStringToSip($queryString);
213

214
        return $url;
215
216
217
    }

    /**
218
219
     * Wrap's $this->wrap[$item][WRAP_SETUP_START] around $value. If $flagOmitEmpty==true && $value=='': return ''.
     *
220
221
222
223
224
225
226
227
228
229
230
231
     * @param $item
     * @param $value
     * @param bool|false $flagOmitEmpty
     * @return string
     */
    public function wrapItem($item, $value, $flagOmitEmpty = false) {
        if ($flagOmitEmpty && $value === "")
            return '';
        return $this->wrap[$item][WRAP_SETUP_START] . $value . $this->wrap[$item][WRAP_SETUP_END];
    }

    /**
232
     * Returns '<form ...>'-tag with various attributes.
233
234
235
236
237
238
239
240
241
242
243
244
245
     *
     * @return string
     */
    public function getFormTag() {

        $attribute = $this->getFormTagAtrributes();

        return '<form ' . OnArray::toString($attribute, '=', ' ', "'") . '>';
    }

    /**
     * Build an assoc array with standard form attributes.
     *
246
     * @return array
247
248
249
     */
    public function getFormTagAtrributes() {

250
        $attribute['id'] = $this->getFormId();
251
252
253
254
255
256
257
258
259
260
        $attribute['method'] = 'post';
        $attribute['action'] = $this->getActionUrl();
        $attribute['target'] = '_top';
        $attribute['accept-charset'] = 'UTF-8';
        $attribute['autocomplete'] = 'on';
        $attribute['enctype'] = $this->getEncType();

        return $attribute;
    }

261
262
263
264
265
266
267
268
269
270
    /**
     * @return string
     */
    public function getFormId() {
        if ($this->formId === null) {
            $this->formId = uniqid('qfq-form-');
        }
        return $this->formId;
    }

271
272
273
    /**
     * Builds the HTML 'form'-tag inlcuding all attributes and target.
     *
274
275
     * Notice: the SIP will be transferred as POST Parameter.
     *
276
277
278
279
280
     * @return string
     * @throws DbException
     */
    public function getActionUrl() {

281
        return API_DIR . '/save.php';
282
283
284
285
286
287
288
289
290
291
292
293
    }

    /**
     * Determines the enctype.
     *
     * See: https://www.w3.org/wiki/HTML/Elements/form#HTML_Attributes
     *
     * @return string
     * @throws DbException
     */
    public function getEncType() {

294
        $result = $this->db->sql("SELECT id FROM FormElement AS fe WHERE fe.formId=? AND fe.type='upload' LIMIT 1", ROW_REGULAR, [$this->formSpec['id']], 'Look for Formelement.type="upload"');
295
296
297
        return (count($result) === 1) ? 'multipart/form-data' : 'application/x-www-form-urlencoded';

    }
298

Carsten  Rose's avatar
Carsten Rose committed
299
300
301
302
    abstract public function tail();

    abstract public function doSubrecords();

303
    abstract public function getProcessFilter();
304
305

    /**
306
307
     * Process all FormElements: build corresponding HTML code. Collect and return all HTML code.
     *
308
     * @param $recordId
309
     * @param string $filter FORM_ELEMENTS_NATIVE | FORM_ELEMENTS_SUBRECORD | FORM_ELEMENTS_NATIVE_SUBRECORD
310
     * @param int $feIdContainer
311
     * @return string
312
313
     * @throws CodeException
     * @throws DbException
314
     * @throws \qfq\UserException
315
     */
Carsten  Rose's avatar
Carsten Rose committed
316
    public function elements($recordId, $filter = FORM_ELEMENTS_NATIVE, $feIdContainer = 0, &$json) {
317
318
319
        $html = '';

        // get current data record
320
        if ($recordId > 0 && $this->store->getVar('id', STORE_RECORD) === false) {
321
322
            $row = $this->db->sql("SELECT * FROM " . $this->formSpec['tableName'] . " WHERE id = ?", ROW_EXPECT_1, array($recordId));
            $this->store->setVarArray($row, STORE_RECORD);
323
        }
324
325
326

        // Iterate over all FormElements
        foreach ($this->feSpecNative as $fe) {
Carsten  Rose's avatar
Carsten Rose committed
327
328
329
            if (($filter === FORM_ELEMENTS_NATIVE && $fe['type'] === 'subrecord')
                || ($filter === FORM_ELEMENTS_SUBRECORD && $fe['type'] !== 'subrecord')
//                || ($filter === FORM_ELEMENTS_DYNAMIC_UPDATE && $fe['dynamicUpdate'] === 'no')
330
331
332
333
            ) {
                continue; // skip this FE
            }

334
335
            $debugStack = array();

336
337
338
339
            // Log / Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, $fe['name'] . ' / ' . $fe['id'], STORE_SYSTEM);

            // evaluate current FormElement
340
            $evaluate = new Evaluate($this->store, $this->db);
341
            $formElement = $evaluate->parseArray($fe, $debugStack);
342

343
            // Get default value
Carsten  Rose's avatar
Carsten Rose committed
344
            $value = ($formElement['value'] === '') ? $this->store->getVar($formElement['name']) : $formElement['value'];
Carsten  Rose's avatar
Carsten Rose committed
345

346
            $htmlFormElementId = HelperFormElement::buildFormElementId($formElement['name'], $recordId);
347

Carsten  Rose's avatar
Carsten Rose committed
348
            // Construct Marshaller Name: buildElement
349
350
            $buildElementFunctionName = 'build' . $this->buildElementFunctionName[$formElement['type']];

Carsten  Rose's avatar
Carsten Rose committed
351
            $jsonElement = array();
352
            // Render pure element
Carsten  Rose's avatar
Carsten Rose committed
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
            $elementHtml = $this->$buildElementFunctionName($formElement, $htmlFormElementId, $value, $jsonElement);

            $fake0 = $fe['dynamicUpdate'];
            $fake1 = $formElement['dynamicUpdate'];

            // container elements do not have dynamicUpdate='yes'. Instead they deliver nested elements.
            if ($formElement['class'] == 'container') {
                if (count($jsonElement) > 0) {
                    $json = array_merge($json, $jsonElement);
                }
            } else {
                // for non container elements: just add the current json status
                if ($fe['dynamicUpdate'] == 'yes') {
                    $json[] = $jsonElement;
                }
            }
369
370

            // debugStack as Tooltip
Carsten  Rose's avatar
Carsten Rose committed
371
            if ($this->showDebugInfo && count($debugStack) > 0) {
372
                $elementHtml = Support::appendTooltip($elementHtml, implode("\n", OnArray::htmlentitiesOnArray($debugStack)));
373
374
            }

Carsten  Rose's avatar
Carsten Rose committed
375
            // Construct Marshaller Name: buildRow
376
377
378
            $buildRowName = 'buildRow' . $this->buildRowName[$formElement['type']];

            $html .= $this->$buildRowName($formElement, $elementHtml);
Carsten  Rose's avatar
Carsten Rose committed
379
//            break;
380
        }
381

382
383
384
        // Log / Debug: Last FormElement has been processed.
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

385
386
387
        return $html;
    }

Carsten  Rose's avatar
Carsten Rose committed
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
    /**
     * Takes the current SIP ('form' and additional parameter), set SIP_RECORD_ID=0 and create a new 'NewRecordUrl'.
     *
     * @throws CodeException
     * @throws \qfq\UserException
     */
    public function deriveNewRecordUrlFromExistingSip(&$toolTipNew) {
        $urlParam = $this->store->getStore(STORE_SIP);
        $urlParam[SIP_RECORD_ID] = 0;
        unset($urlParam[SIP_SIP]);
        unset($urlParam[SIP_URLPARAM]);
        $urlParam['id'] = $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3);
        $urlParam['type'] = $this->store->getVar(TYPO3_PAGE_TYPE, STORE_TYPO3);

        $sip = $this->store->getSipInstance();

        $url = $sip->queryStringToSip(OnArray::toString($urlParam));

        $toolTipNew .= "New" . PHP_EOL . PHP_EOL . OnArray::toString($urlParam, ' = ', PHP_EOL, "'");

        return $url;
    }

411
412
413
414
415
416
417
418
    abstract public function buildRowNative($formElement, $elementHtml);

    abstract public function buildRowPill($formElement, $elementHtml);

    abstract public function buildRowFieldset($formElement, $elementHtml);

    abstract public function buildRowSubrecord($formElement, $elementHtml);

419
    /**
420
421
     * Builds a label, typically for an html-'<input>'-element.
     *
422
423
     * @param array $htmlFormElementId
     * @param $label
424
425
     * @return string
     */
426
427
428
429
    public function buildLabel($htmlFormElementId, $label) {
        $html = '<label for="' . $htmlFormElementId . '">' . $label . '</label>';

        return $html;
430
431
    }

432
433
434
435
436
437
438
439
440
441
442
443
444
    /**
     * Builds HTML 'input' element.
     * Format: <input name="$htmlFormElementId" <type="email|input|password|url" [autocomplete="autocomplete"] [autofocus="autofocus"]
     *           [maxlength="$maxLength"] [placeholder="$placeholder"] [size="$size"] [min="$min"] [max="$max"]
     *           [pattern="$pattern"] [readonly="readonly"] [required="required"] [disabled="disabled"] value="$value">
     *
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return string
     * @throws UserException
     */
Carsten  Rose's avatar
Carsten Rose committed
445
    public function buildInput(array $formElement, $htmlFormElementId, $value, &$json) {
446
        $textarea = '';
447

Carsten  Rose's avatar
Carsten Rose committed
448
        $attribute = $this->getAttribute('name', $htmlFormElementId);
449

450
        // Check for input type 'textarea'
451
        $colsRows = explode(',', $formElement['size'], 2);
452
        if (count($colsRows) === 2) {
453
            // <textarea>
454
455
            $htmlTag = '<textarea';

Carsten  Rose's avatar
Carsten Rose committed
456
457
            $attribute .= $this->getAttribute('cols', $colsRows[0]);
            $attribute .= $this->getAttribute('rows', $colsRows[1]);
458
            $textarea = htmlentities($value) . '</textarea>';
459
460

        } else {
Carsten  Rose's avatar
Carsten Rose committed
461
462
463
464
            $htmlTag = '<input';

            $this->adjustMaxLength($formElement);

465
466
            // <input>
            if ($formElement['maxLength'] > 0) {
Carsten  Rose's avatar
Carsten Rose committed
467
468
469
                // crop string only if it's not empty (substr returns false on empty strings)
                if ($value !== '')
                    $value = substr($value, 0, $formElement['maxLength']);
470

Carsten  Rose's avatar
Carsten Rose committed
471
                // 'maxLength' needs an upper 'L': naming convention for DB tables!
472
473
                $attribute .= $this->getAttributeList($formElement, ['type', 'size', 'maxLength']);
                $attribute .= $this->getAttribute('value', htmlentities($value), false);
474
            }
Carsten  Rose's avatar
Carsten Rose committed
475
        }
476

477
        $attribute .= $this->getAttributeList($formElement, ['autocomplete', 'autofocus', 'placeholder']);
Carsten  Rose's avatar
Carsten Rose committed
478
        $attribute .= $this->getAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : '');
479
        $attribute .= $this->getAttribute('title', $formElement['tooltip']);
Carsten  Rose's avatar
Carsten Rose committed
480
        $attribute .= $this->getInputCheckPattern($formElement['checkType'], $formElement['checkPattern']);
481

Carsten  Rose's avatar
Carsten Rose committed
482
        $attribute .= $this->getAttributeMode($formElement);
483

Carsten  Rose's avatar
Carsten Rose committed
484
485
        $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement['mode']);

486
        return "$htmlTag $attribute>$textarea";
487

488
489
    }

Carsten  Rose's avatar
Carsten Rose committed
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
    /**
     * @param array $formElement
     */
    private function adjustMaxLength(array &$formElement) {

        // MIN( $formElement['maxLength'], tabledefinition)
        $maxLength = $this->getColumnSize($formElement['name']);

        if ($maxLength !== false) {
            if (is_numeric($formElement['maxLength'])) {
                if ($formElement['maxLength'] > $maxLength) {
                    $formElement['maxLength'] = $maxLength;
                }
            } else {
                $formElement['maxLength'] = $maxLength;
            }
        }
    }

509
    /**
510
511
     * Get column spec from tabledefinition and parse size of it. If nothing defined, return false.
     *
512
     * @param $column
513
     * @return bool|int
514
515
516
517
518
519
     */
    private function getColumnSize($column) {
        $matches = array();

        $typeSpec = $this->store->getVar($column, STORE_TABLE_COLUMN_TYPES);

520
        // e.g.: string(64), enum('yes','no')
521
522
523
524
525
526
        if (1 === preg_match('/\((.+)\)/', $typeSpec, $matches)) {
            if (is_numeric($matches[1]))
                return $matches[1];
        }

        return false;
527
528
529
530
    }

    /**
     * Builds a HTML attribute list, based on  $attributeList.
531
     *
532
533
534
535
536
537
538
     * E.g.: attributeList: [ 'type', 'autofocus' ]
     *       generates: 'type="$formElement['type']" autofocus="$formElement['autofocus']" '
     *
     * @param array $formElement
     * @param array $attributeList
     * @return string
     */
Carsten  Rose's avatar
Carsten Rose committed
539
    private function getAttributeList(array $formElement, array $attributeList) {
540
541
542
        $attribute = '';
        foreach ($attributeList as $item) {
            if (isset($formElement[$item]))
Carsten  Rose's avatar
Carsten Rose committed
543
                $attribute .= $this->getAttribute(strtolower($item), $formElement[$item]);
544
545
546
547
548
549
        }
        return $attribute;
    }

    /**
     * Construct HTML Input attribute for Client Validation:
550
     *
551
552
553
554
     *   type     data                      predefined
     *   -------  -----------------------   -------------------------------------------------------------------------------
     *   min|max  <min value>|<max value>   min="%s"|max="%s"
     *   pattern  <regexp>                  pattern="%s"
Carsten  Rose's avatar
Carsten Rose committed
555
     *   digit    -                         pattern="^[0-9]*$"
556
557
     *   email    -                         pattern="^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$"
     *
Carsten  Rose's avatar
Carsten Rose committed
558
     * For 'min|max' and 'pattern' the 'data' will be injected in the attribute string via '%s'.
559
560
561
562
     *
     * @param $type
     * @param $data
     * @return string
563
     * @throws \qfq\UserException
564
     */
Carsten  Rose's avatar
Carsten Rose committed
565
    private function getInputCheckPattern($type, $data) {
566
567
        if ($type === '') {
            return '';
568
        }
569
570
571

        $attribute = '';

572
        $arrAttr = explode("|", $this->inputCheckPattern[$type]);
573
574
575
        $arrData = explode("|", $data);

        for ($ii = 0; $ii < count($arrAttr); $ii++) {
576
577
578
579
580
            if ($arrAttr[$ii]) {
                if (!isset($arrData[$ii]))
                    throw new UserException("Missing MIN|MAX values", ERROR_MISSING_MIN_MAX);
                $attribute .= str_replace('%s', trim($arrData[$ii]), $arrAttr[$ii]) . ' ';
            }
581
582
583
584
585
        }
        return $attribute;
    }

    /**
586
587
     * Set corresponding html attributes readonly/required/disabled, based on $formElement['mode'].
     *
588
589
590
591
     * @param array $formElement
     * @return string
     * @throws UserException
     */
Carsten  Rose's avatar
Carsten Rose committed
592
    private function getAttributeMode(array $formElement) {
593
594
595
596
597
598
        $attribute = '';

        switch ($formElement['mode']) {
            case 'show':
                break;
            case 'readonly':
Carsten  Rose's avatar
Carsten Rose committed
599
                $attribute .= $this->getAttribute('readonly', 'readonly');
600
601
                break;
            case 'required':
Carsten  Rose's avatar
Carsten Rose committed
602
                $attribute .= $this->getAttribute('required', 'required');
603
604
605
606
                break;
            case 'lock':
                break;
            case 'disabled':
Carsten  Rose's avatar
Carsten Rose committed
607
                $attribute .= $this->getAttribute('disabled', 'disabled');
608
609
610
611
                break;
            default:
                $this->store->setVar(SYSTEM_FORM_ELEMENT, $formElement['name'] . ' / ' . $formElement['id'], STORE_SYSTEM);
                $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'mode', STORE_SYSTEM);
612
                throw new UserException("Unknown mode '" . $formElement['mode'] . "'", ERROR_UNKNOWN_MODE);
613
614
615
616
617
                break;
        }
        return $attribute;
    }

Carsten  Rose's avatar
Carsten Rose committed
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
    /**
     * @param $htmlFormElementId
     * @param string|array $value
     * @param $mode
     * @return array
     */
    private function getJsonElementUpdate($htmlFormElementId, $value, $mode) {
        $json = array();

        $json['form-element'] = $htmlFormElementId;
        $json['value'] = $value;
        $json['disabled'] = ($mode === 'disabled');
        $json['readonly'] = ($mode === 'readonly');

        return $json;
    }

635
636
637
    /**
     * Builds HTML 'checkbox' element.
     *
638
     * Checkboxes will only be submitted, if they are checked. Therefore, a hidden element with the unchecked value will be transferred first.
639
640
641
642
643
644
645
646
647
648
649
     *
     * Format: <input type="hidden" name="$htmlFormElementId" value="$valueUnChecked">
     *         <input name="$htmlFormElementId" type="checkbox" [autofocus="autofocus"]
     *            [readonly="readonly"] [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return string
     * @throws UserException
     */
Carsten  Rose's avatar
Carsten Rose committed
650
    public function buildCheckbox(array $formElement, $htmlFormElementId, $value, &$json) {
651
652
653
654
        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
655
        $this->getKeyValueListFromSqlEnumSpec($formElement, $itemKey, $itemValue);
656
657
658

        // Get fallback, if 'checkBoxMode' is not defined:
        if (!isset($formElement['checkBoxMode'])) {
659
            // This fallback is problematic if 'set' or 'enum' has 2 : defaults to single but maybe multi is meant.
660
661
662
663
664
665
            $formElement['checkBoxMode'] = (count($itemKey) > 2) ? 'multi' : 'single';
        }

        if ($formElement['checkBoxMode'] === 'multi') {
            $htmlFormElementId .= '[]';
        } else {
666
            // Fill meaningfull defaults to parameter: checked|unchecked  (CHECKBOX_VALUE_CHECKED|CHECKBOX_VALUE_UNCHECKED)
667
668
669
            $this->prepareCheckboxCheckedUncheckedValue($itemKey, $formElement);
        }

Carsten  Rose's avatar
Carsten Rose committed
670
671
        $attributeBase = $this->getAttributeMode($formElement);
        $attributeBase .= $this->getAttribute('type', $formElement['type']);
672
673
674
675
676
677
678
679
680
681
682

        switch ($formElement['checkBoxMode']) {
            case 'single':
                $html = $this->buildCheckboxSingle($formElement, $htmlFormElementId, $attributeBase, $value);
                break;
            case 'multi';
                $html = $this->buildCheckboxMulti($formElement, $htmlFormElementId, $attributeBase, $value, $itemKey, $itemValue);
                break;
            default:
                throw new UserException('checkBoxMode: \'' . $formElement['checkBoxMode'] . '\' is unknown.', ERROR_CHECKBOXMODE_UNKNOWN);
        }
683

Carsten  Rose's avatar
Carsten Rose committed
684
        $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement['mode']);
685
//        return Support::wrapTag('<div class="checkbox">', $html, true);
686
687
688
        return $html;
    }

689
    /**
690
691
692
693
694
     * Look for key/value list (in this order, first match counts) in
     *  a) `sql1`
     *  b) `parameter:itemList`
     *  c) table.column definition
     *
695
     * Copies the found keys to &$itemKey and the values to &$itemValue
696
     * If there are no &$itemKey, copy &$itemValue to &$itemKey.
697
698
699
700
     *
     * @param array $formElement
     * @param $itemKey
     * @param $itemValue
701
702
     * @throws CodeException
     * @throws \qfq\UserException
703
     */
704
    public function getKeyValueListFromSqlEnumSpec(array $formElement, &$itemKey, &$itemValue) {
705
706
707
708
        $fieldType = '';
        $itemKey = array();
        $itemValue = array();

709
710
711
        if (count($formElement) < 20)
            throw new CodeException("Invalid (none or to small) Formelement", ERROR_MISSING_FORMELEMENT);

712
713
714
        $itemValue = $this->getItemsForEnumOrSet($formElement['name'], $fieldType);

        if (is_array($formElement['sql1'])) {
715
716
717
            if (count($formElement['sql1']) > 0) {
                $keys = array_keys($formElement['sql1'][0]);
                $itemKey = array_column($formElement['sql1'], 'id');
718

719
720
721
722
                // If there is no column 'id' and at least two columns in total
                if (count($itemKey) === 0 && count($keys) >= 2) {
                    $itemKey = array_column($formElement['sql1'], $keys[0]);
                }
723

724
725
726
727
728
729
                $itemValue = array_column($formElement['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['sql1'], $keys[$idx]);
                }
730
            }
731
        } elseif (isset($formElement['itemList']) && strlen($formElement['itemList']) > 0) {
732
            $arr = KeyValueStringParser::parse($formElement['itemList'], ':', ',', KVP_IF_VALUE_EMPTY_COPY_KEY);
733
734
            $itemValue = array_values($arr);
            $itemKey = array_keys($arr);
735
        } elseif ($fieldType === 'enum' || $fieldType === 'set') {
Carsten  Rose's avatar
Carsten Rose committed
736
            // already done at the beginning with '$this->getItemsForEnumOrSet($formElement['name'], $fieldType);'
737
        } else {
738
            throw new UserException("Missing definition (- nothing found in 'sql1', 'parameter:itemValues', 'enum-' or 'set-definition'", ERROR_MISSING_ITEM_VALUES);
739
740
741
742
743
        }

        if (count($itemKey) === 0) {
            $itemKey = $itemValue;
        }
744
745
746
747
748
749
750
751
752
753

        if (isset($formElement['emptyItemAtStart'])) {
            array_unshift($itemKey, '');
            array_unshift($itemValue, '');
        }

        if (isset($formElement['emptyItemAtEnd'])) {
            $itemValue[] = '';
            $itemKey[] = '';
        }
754

Carsten  Rose's avatar
Carsten Rose committed
755
756
        if (isset($formElement['emptyHide'])) {
            if (isset($itemValue['']))
757
                unset($itemValue['']);
Carsten  Rose's avatar
Carsten Rose committed
758
            if (isset($itemKey['']))
759
760
761
                unset($itemKey['']);

        }
762
763
764
    }

    /**
765
766
     * Get the attribute definition list of an enum or set column. For strings, get the default value. Return elements as an array.
     *
767
768
769
770
771
772
773
774
775
776
777
     * @param $column
     * @param $fieldType
     * @return array
     * @throws UserException
     */
    private function getItemsForEnumOrSet($column, &$fieldType) {

        // Get column definition
        $fieldTypeDefinition = $this->store->getVar($column, STORE_TABLE_COLUMN_TYPES);

        if ($fieldTypeDefinition === false) {
778
            throw new UserException("Column '$column' unknown in table '" . $this->formSpec['tableName'] . "'", ERROR_DB_UNKNOWN_COLUMN);
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
        }

        $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', ... ]
        $items = OnArray::trimArray(explode(',', substr($fieldTypeDefinition, $startPosition, $length - $startPosition - 1)), "'");
        $fieldType = substr($fieldTypeDefinition, 0, $startPosition - 1);

        return $items;
    }

    /**
     * For CheckBox's with only one checkbox: if no parameter:checked|unchecked is defined, take defaults:
806
     *
807
808
809
810
811
812
813
814
815
     *    checked: first Element in $itemKey
     *  unchecked: ''
     *
     * @param array $itemKey
     * @param array $formElement
     * @throws UserException
     */
    private function prepareCheckboxCheckedUncheckedValue(array $itemKey, array &$formElement) {

816
        if (!isset($formElement[CHECKBOX_VALUE_CHECKED])) {
817
818
            if (isset($itemKey[0])) {
                // First element in $itemKey list
819
                $formElement[CHECKBOX_VALUE_CHECKED] = $itemKey[0];
820
821
            } else {
                // Take column default value
822
                $formElement[CHECKBOX_VALUE_CHECKED] = $this->store->getVar($formElement['name'], STORE_TABLE_DEFAULT);
823
824
825
826
            }
        }

        // unchecked
827
        if (!isset($formElement[CHECKBOX_VALUE_UNCHECKED])) {
828
            if (isset($itemKey[1])) {
829
                $formElement[CHECKBOX_VALUE_UNCHECKED] = ($itemKey[0] === $formElement['checked']) ? $itemKey[1] : $itemKey[0];
830
            } else {
831
                $formElement[CHECKBOX_VALUE_UNCHECKED] = '';
832
833
834
            }
        }

835
836
        if ($formElement[CHECKBOX_VALUE_CHECKED] === $formElement[CHECKBOX_VALUE_UNCHECKED]) {
            throw new UserException('FormElement: type=checkbox - checked and unchecked can\'t be the same: ' . $formElement[CHECKBOX_VALUE_CHECKED], ERROR_CHECKBOX_EQUAL);
837
838
839
840
841
        }

    }

    /**
842
843
     * Build a Checkbox based on two values.
     *
844
845
846
847
848
849
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $attribute
     * @param $value
     * @return string
     */
850
    public function buildCheckboxSingle(array $formElement, $htmlFormElementId, $attribute, $value) {
851
        $html = '';
852

Carsten  Rose's avatar
Carsten Rose committed
853
854
        $attribute .= $this->getAttribute('name', $htmlFormElementId);
        $attribute .= $this->getAttribute('value', $formElement['checked'], false);
Carsten  Rose's avatar
Carsten Rose committed
855
856
        $attribute .= $this->getAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : '');

857
        if ($formElement['checked'] === $value) {
Carsten  Rose's avatar
Carsten Rose committed
858
            $attribute .= $this->getAttribute('checked', 'checked');
859
860
        }

Carsten  Rose's avatar
Carsten Rose committed
861
        $attribute .= $this->getAttributeList($formElement, ['autofocus']);
862

863
        $html = $this->buildNativeHidden($htmlFormElementId, $formElement['unchecked']);
864
865
866
867
868
869

        $html .= '<input ' . $attribute . '>';
        if (isset($formElement['label2'])) {
            $html .= $formElement['label2'];
        }

870
871
872
        $html = Support::wrapTag("<label>", $html, true);
        $html = Support::wrapTag("<div class='checkbox'>", $html, true);

873
874
875
876
        return $html;
    }

    /**
877
     * Builds a real HTML hidden form element. Useful for checkboxes, Multiple-Select and Radios.
878
879
880
881
882
883
884
885
886
887
     *
     * @param $htmlFormElementId
     * @param $value
     * @return string
     */
    public function buildNativeHidden($htmlFormElementId, $value) {
        return '<input type="hidden" name="' . $htmlFormElementId . '" value="' . htmlentities($value) . '">';
    }

    /**
888
889
890
891
892
     * 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.
     *
893
894
895
896
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $attributeBase
     * @param $value
897
898
     * @param array $itemKey
     * @param array $itemValue
899
900
     * @return string
     */
901
    public function buildCheckboxMulti(array $formElement, $htmlFormElementId, $attributeBase, $value, array $itemKey, array $itemValue) {
902
        // Defines which of the checkboxes will be checked.
903
        $values = explode(',', $value);
904

Carsten  Rose's avatar
Carsten Rose committed
905
        $attributeBase .= $this->getAttribute('name', $htmlFormElementId);
Carsten  Rose's avatar
Carsten Rose committed
906
        $attributeBase .= $this->getAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : '');
907

908
        $html = $this->buildNativeHidden($htmlFormElementId, '');
909

Carsten  Rose's avatar
Carsten Rose committed
910
        $orientation = (isset($formElement[CHECKBOX_ORIENTATION]) && $formElement[CHECKBOX_ORIENTATION] === 'vertical') ? '' : 'checkbox-inline';
911

912
        $flagFirst = true;
913
914
        for ($ii = 0, $jj = 1; $ii < count($itemKey); $ii++, $jj++) {

915
            $attribute = $attributeBase;
916
917

            // Do this only the first round.
918
919
920
            if ($flagFirst) {
                $flagFirst = false;
                if (isset($formElement['autofocus']))
Carsten  Rose's avatar
Carsten Rose committed
921
                    $attribute .= $this->getAttribute('autofocus', $formElement['autofocus']);
922
            }
923
924
925
926
927

            $attribute .= $this->getAttribute('value', $itemKey[$ii]);

            // Check if the given key is found in field.
            if (false !== array_search($itemKey[$ii], $values)) {
Carsten  Rose's avatar
Carsten Rose committed
928
                $attribute .= $this->getAttribute('checked', 'checked');
929
            }
930

931
932
933
934
            $htmlCheckbox = '<input ' . $attribute . '>';
            $htmlCheckbox .= $itemValue[$ii];

            $htmlCheckbox = Support::wrapTag("<label class='$orientation'>", $htmlCheckbox, true);
Carsten  Rose's avatar
Carsten Rose committed
935
            if (isset($formElement[CHECKBOX_ORIENTATION]) && $formElement[CHECKBOX_ORIENTATION] === 'vertical')
936
937
938
                $htmlCheckbox = Support::wrapTag("<div class='checkbox'>", $htmlCheckbox, true);

            $html .= $htmlCheckbox;
939
        }
940

Carsten  Rose's avatar
Carsten Rose committed
941
        if (isset($formElement[CHECKBOX_ORIENTATION]) && $formElement[CHECKBOX_ORIENTATION] !== 'vertical')
942
943
            $html = Support::wrapTag("<div class='checkbox'>", $html, true);

944
945
946
        return $html;
    }

947
948
949
950
951
952
953
954
955
956
957
    /**
     * Submit hidden values by SIP.
     *
     * Sometimes, it's usefull to precalculate values during formload and to submit them as hidden fields.
     * To avoid any manipulation on those fields, the values will be transferred by SIP.
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return string
     */
Carsten  Rose's avatar
Carsten Rose committed
958
    public function buildHidden(array $formElement, $htmlFormElementId, $value, &$json) {
959
960
961
962

        $this->store->setVar($htmlFormElementId, $value, STORE_SIP, false);
    }

963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
    /**
     * Build HTML 'radio' element.
     *
     * Checkboxes will only be submitted, if they are checked. Therefore, a hidden element with the unchecked value will be transfered first.
     *
     * Format: <input type="hidden" name="$htmlFormElementId" value="$valueUnChecked">
     *         <input name="$htmlFormElementId" type="radio" [autofocus="autofocus"]
     *            [readonly="readonly"] [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return string
     * @throws UserException
     */
Carsten  Rose's avatar
Carsten Rose committed
978
    public function buildRadio(array $formElement, $htmlFormElementId, $value, &$json) {
979
980
981
982
        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
983
        $this->getKeyValueListFromSqlEnumSpec($formElement, $itemKey, $itemValue);
984

Carsten  Rose's avatar
Carsten Rose committed
985
986
987
        $attributeBase = $this->getAttributeMode($formElement);
        $attributeBase .= $this->getAttribute('name', $htmlFormElementId);
        $attributeBase .= $this->getAttribute('type', $formElement['type']);
Carsten  Rose's avatar
Carsten Rose committed
988
        $attributeBase .= $this->getAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : '');
989
990
991
992

        $jj = 0;
        $flagFirst = true;

993
        $html = $this->buildNativeHidden($htmlFormElementId, $value);
994
995
996
997
998
999
        for ($ii = 0; $ii < count($itemValue); $ii++) {
            $jj++;
            $attribute = $attributeBase;
            if ($flagFirst) {
                $flagFirst = false;
                if (isset($formElement['autofocus']))
Carsten  Rose's avatar
Carsten Rose committed
1000
                    $attribute .= $this->getAttribute('autofocus', $formElement['autofocus']);
1001
1002
            }

Carsten  Rose's avatar
Carsten Rose committed
1003
            $attribute .= $this->getAttribute('value', $itemKey[$ii]);
1004
            if ($itemKey[$ii] === $value) {
Carsten  Rose's avatar
Carsten Rose committed
1005
                $attribute .= $this->getAttribute('checked', 'checked');
1006
            }
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017

            $element = '<input ' . $attribute . '>' . $itemValue[$ii];

//            $element = Support::wrapTag('<label>',$element);

//            if(isset($this->feDivClass[$formElement['type']]) && $this->feDivClass[$formElement['type']] != '') {
//                $element = Support::wrapTag('<div class="' . $this->feDivClass[$formElement['type']] .'">',  $element);
//            }

            $html .= $element;

1018
1019
1020
1021
1022
            if ($jj === $formElement['maxLength']) {
                $jj = 0;
                $html .= '<br>';
            }
        }
Carsten  Rose's avatar
Carsten Rose committed
1023
1024
1025

        $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement['mode']);

1026
1027
1028
        return $html;
    }

Carsten  Rose's avatar
Carsten Rose committed
1029
    /**
1030
1031
     * Builds a Selct (Dropdown) Box.
     *
Carsten  Rose's avatar
Carsten Rose committed
1032
1033
1034
1035
1036
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return mixed
     */
Carsten  Rose's avatar
Carsten Rose committed
1037
    public function buildSelect(array $formElement, $htmlFormElementId, $value, &$json) {
Carsten  Rose's avatar
Carsten Rose committed
1038
1039
1040
1041
        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
1042
        $this->getKeyValueListFromSqlEnumSpec($formElement, $itemKey, $itemValue);
Carsten  Rose's avatar
Carsten Rose committed
1043
1044
1045

        $attribute = $this->getAttributeMode($formElement);
        $attribute .= $this->getAttribute('name', $htmlFormElementId);
1046
        $attribute .= $this->getAttributeList($formElement, ['autofocus']);
Carsten  Rose's avatar
Carsten Rose committed
1047
        $attribute .= $this->getAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : '');
Carsten  Rose's avatar
Carsten Rose committed
1048
1049
1050
1051
1052
1053
1054

        if (isset($formElement['size']) && $formElement['size'] > 1) {
            $attribute .= $this->getAttribute('size', $formElement['size']);
            $attribute .= $this->getAttribute('multiple', 'multiple');
        }

        $option = '';
Carsten  Rose's avatar
Carsten Rose committed
1055
1056
        $firstSelect = true;
        $jsonValues = array();
Carsten  Rose's avatar
Carsten Rose committed
1057
1058
1059
1060
        for ($ii = 0; $ii < count($itemValue); $ii++) {
            $option .= '<option ';

            $option .= $this->getAttribute('value', $itemKey