AbstractBuildForm.php 45 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();
39
//    protected $feDivClass = array(); // Wrap FormElements in <div class="$feDivClass[type]">
40

41
42
    private $formId = null;

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

58
59
//        $sip = $this->store->getVar(CLIENT_SIP, STORE_CLIENT);

60
        // render mode specific
61
        $this->fillWrap();
62
63
64

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

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

99
100
101
        $this->symbol['edit'] = "<span class='glyphicon glyphicon-pencil'></span>";
        $this->symbol['new'] = "<span class='glyphicon glyphicon-plus'></span>";

102
        $this->inputCheckPattern = OnArray::inputCheckPatternArray();
103
104
    }

105
106
    abstract public function fillWrap();

107
    /**
108
     * Builds complete form. Depending of Formspecification, the layout will be 'plain' / 'table' / 'bootstrap'.
109
     *
110
     * @return string The whole form as HTML
111
112
     * @throws CodeException
     * @throws DbException
113
114
115
     */
    public function process() {

116
        // <form>
117
118
        $html = $this->head();

119
        $filter = $this->getProcessFilter();
120

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

123
124
            $parentRecords = $this->db->sql($this->formSpec['multiSql']);
            foreach ($parentRecords as $row) {
125
                $this->store->setVarArray($row, STORE_PARENT_RECORD, true);
126
                $html .= $this->elements($row['_id'], $filter);
127
128
            }
        } else {
129
            $html .= $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), $filter);
130
131
        }

132
        // </form>
133
134
        $html .= $this->tail();

135
        $html .= $this->doSubrecords();
136

137
138
139
        return $html;
    }

140
    /**
141
     * Builds the head area of the form.
142
     *
143
     * @return string
144
     */
145
146
    public function head() {
        $html = '';
147

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

150
151
152
        // Logged in BE User will see a FormEdit Link
        $sipParamString = OnArray::toString($this->store->getStore(STORE_SIP), ':', ', ', "'");
        $formEditUrl = $this->createFormEditUrl();
153

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

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

158
159
160
        $html .= $this->getFormTag();

        return $html;
161
162
163
    }

    /**
164
     * Format's an attribute: $type=$value. If $flagOmitEmpty==true && $value=='': return ''.
165
     *
166
167
168
169
     * @param $type
     * @param $value
     * @param bool|false $flagOmitEmpty
     * @return string
170
     */
171
172
173
    public function getAttribute($type, $value, $flagOmitEmpty = true) {
        if ($flagOmitEmpty && $value === "")
            return '';
174

175
        return $type . '="' . trim($value) . '" ';
176
177
    }

178
    /**
179
     * If SHOW_DEBUG_INFO=yes: create a link (incl. SIP) to edit the current form. Show also the hidden content of the SIP.
180
     *
181
     * @return string String: <a href="?pageId&sip=....">Edit</a> <small>[sip:..., r:..., urlparam:..., ...]</small>
182
     */
183
    public function createFormEditUrl() {
184

185
        if ($this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM) !== 'yes') {
186
187
            return '';
        }
188

189
190
191
192
193
        $queryStringArray = [
            'id' => $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3),
            'form' => 'form',
            'r' => $this->formSpec['id']
        ];
194

195
        $queryString = Support::arrayToQueryString($queryStringArray);
196

197
198
        $sip = $this->store->getSipInstance();
        $url = $sip->queryStringToSip($queryString);
199

200
        return $url;
201
202
203
    }

    /**
204
205
     * Wrap's $this->wrap[$item][WRAP_SETUP_START] around $value. If $flagOmitEmpty==true && $value=='': return ''.
     *
206
207
208
209
210
211
212
213
214
215
216
217
     * @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];
    }

    /**
218
     * Returns '<form ...>'-tag with various attributes.
219
220
221
222
223
224
225
226
227
228
229
230
231
     *
     * @return string
     */
    public function getFormTag() {

        $attribute = $this->getFormTagAtrributes();

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

    /**
     * Build an assoc array with standard form attributes.
     *
232
     * @return array
233
234
235
     */
    public function getFormTagAtrributes() {

236
        //TODO: ttcontent id eintragen
Carsten  Rose's avatar
Carsten Rose committed
237
//        $attribute['id'] = $this->store->getVar(STORE_TYPO3,'1234');
238
        $attribute['id'] = $this->getFormId();
239
240
241
242
243
244
245
246
247
248
        $attribute['method'] = 'post';
        $attribute['action'] = $this->getActionUrl();
        $attribute['target'] = '_top';
        $attribute['accept-charset'] = 'UTF-8';
        $attribute['autocomplete'] = 'on';
        $attribute['enctype'] = $this->getEncType();

        return $attribute;
    }

249
250
251
252
253
254
255
256
257
258
    /**
     * @return string
     */
    public function getFormId() {
        if ($this->formId === null) {
            $this->formId = uniqid('qfq-form-');
        }
        return $this->formId;
    }

259
260
261
    /**
     * Builds the HTML 'form'-tag inlcuding all attributes and target.
     *
262
263
     * Notice: the SIP will be transferred as POST Parameter.
     *
264
265
266
267
268
     * @return string
     * @throws DbException
     */
    public function getActionUrl() {

269
        return API_DIR . '/save.php';
270
271
272
273
274
275
276
277
278
279
280
281
    }

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

282
        $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"');
283
284
285
        return (count($result) === 1) ? 'multipart/form-data' : 'application/x-www-form-urlencoded';

    }
286

287
    abstract public function getProcessFilter();
288
289

    /**
290
291
     * Process all FormElements: build corresponding HTML code. Collect and return all HTML code.
     *
292
     * @param $recordId
293
     * @param string $filter FORM_ELEMENTS_NATIVE | FORM_ELEMENTS_SUBRECORD | FORM_ELEMENTS_NATIVE_SUBRECORD
294
     * @param int $feIdContainer
295
     * @return string
296
297
     * @throws CodeException
     * @throws DbException
298
     * @throws \qfq\UserException
299
     */
300
    public function elements($recordId, $filter = FORM_ELEMENTS_NATIVE, $feIdContainer = 0) {
301
302
303
        $html = '';

        // get current data record
304
        if ($recordId > 0 && $this->store->getVar('id', STORE_RECORD) === false) {
305
306
            $row = $this->db->sql("SELECT * FROM " . $this->formSpec['tableName'] . " WHERE id = ?", ROW_EXPECT_1, array($recordId));
            $this->store->setVarArray($row, STORE_RECORD);
307
        }
308
309
310

        // Iterate over all FormElements
        foreach ($this->feSpecNative as $fe) {
311
            $debugStack = array();
312

313
314
            if (($filter === FORM_ELEMENTS_NATIVE && $fe['type'] === 'subrecord') ||
                ($filter === FORM_ELEMENTS_SUBRECORD && $fe['type'] !== 'subrecord')
315
316
317
318
            ) {
                continue; // skip this FE
            }

319
320
321
322
            // Log / Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, $fe['name'] . ' / ' . $fe['id'], STORE_SYSTEM);

            // evaluate current FormElement
323
            $evaluate = new Evaluate($this->store, $this->db);
324
            $formElement = $evaluate->parseArray($fe, $debugStack);
325

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

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

331
332
333
334
            // Construct Marshaller Name
            $buildElementFunctionName = 'build' . $this->buildElementFunctionName[$formElement['type']];

            // Render pure element
335
336
337
            $elementHtml = $this->$buildElementFunctionName($formElement, $htmlFormElementId, $value, $debugStack);

            // debugStack as Tooltip
338
            if ($this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM) === 'yes' && count($debugStack) > 0) {
339
                $elementHtml = Support::appendTooltip($elementHtml, implode("\n", OnArray::htmlentitiesOnArray($debugStack)));
340
341
            }

342
343
344
345
            // Construct Marshaller Name
            $buildRowName = 'buildRow' . $this->buildRowName[$formElement['type']];

            $html .= $this->$buildRowName($formElement, $elementHtml);
346
        }
347

348
349
350
        // Log / Debug: Last FormElement has been processed.
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

351
352
353
        return $html;
    }

354
355
356
    abstract public function tail();

    abstract public function doSubrecords();
357

358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
    /**
     * Create a link (incl. SIP) to delete the current record.
     *
     * @return string String: "API_DIR/delete.php?sip=...."
     */
    public function createDeleteUrl($table, $recordId) {

        $queryStringArray = [
            SIP_TABLE => $table,
            SIP_RECORD_ID => $recordId
        ];

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

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

        return $sip->queryStringToSip($queryString, RETURN_URL, API_DIR . '/delete.php');
    }

377
378
379
380
381
382
383
384
    abstract public function buildRowNative($formElement, $elementHtml);

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

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

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

385
    /**
386
387
     * Builds a label, typically for an html-'<input>'-element.
     *
388
389
     * @param array $htmlFormElementId
     * @param $label
390
391
     * @return string
     */
392
393
394
395
    public function buildLabel($htmlFormElementId, $label) {
        $html = '<label for="' . $htmlFormElementId . '">' . $label . '</label>';

        return $html;
396
397
    }

398
399
400
401
402
403
404
405
406
407
408
409
410
    /**
     * 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
     */
411
    public function buildInput(array $formElement, $htmlFormElementId, $value) {
412
        $textarea = '';
413

Carsten  Rose's avatar
Carsten Rose committed
414
        $attribute = $this->getAttribute('name', $htmlFormElementId);
415
416
        $htmlTag = '<input';

417
418
419
420
421
422
423
424
425
426
427
428
        // 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;
            }
        }

429
        // Check for input type 'textarea'
430
        $colsRows = explode(',', $formElement['size'], 2);
431
        if (count($colsRows) === 2) {
432
            // <textarea>
433
434
            $htmlTag = '<textarea';

Carsten  Rose's avatar
Carsten Rose committed
435
436
            $attribute .= $this->getAttribute('cols', $colsRows[0]);
            $attribute .= $this->getAttribute('rows', $colsRows[1]);
437
            $textarea = htmlentities($value) . '</textarea>';
438
439

        } else {
440
441
442
            // <input>
            if ($formElement['maxLength'] > 0) {
                $value = substr($value, 0, $formElement['maxLength']);
443
444
445

                $attribute .= $this->getAttributeList($formElement, ['type', 'size', 'maxLength']);
                $attribute .= $this->getAttribute('value', htmlentities($value), false);
446
            }
Carsten  Rose's avatar
Carsten Rose committed
447
        }
448

449
        // 'maxLength' needs an upper 'L': naming convention for DB tables!
450
        $attribute .= $this->getAttributeList($formElement, ['autocomplete', 'autofocus', 'placeholder']);
451
        $attribute .= $this->getAttribute('title', $formElement['tooltip']);
Carsten  Rose's avatar
Carsten Rose committed
452
        $attribute .= $this->getInputCheckPattern($formElement['checkType'], $formElement['checkPattern']);
453

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

456
        return "$htmlTag $attribute>$textarea";
457

458
459
    }

460
    /**
461
462
     * Get column spec from tabledefinition and parse size of it. If nothing defined, return false.
     *
463
     * @param $column
464
     * @return bool|int
465
466
467
468
469
470
     */
    private function getColumnSize($column) {
        $matches = array();

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

471
        // e.g.: string(64), enum('yes','no')
472
473
474
475
476
477
        if (1 === preg_match('/\((.+)\)/', $typeSpec, $matches)) {
            if (is_numeric($matches[1]))
                return $matches[1];
        }

        return false;
478
479
480
481
    }

    /**
     * Builds a HTML attribute list, based on  $attributeList.
482
     *
483
484
485
486
487
488
489
     * 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
490
    private function getAttributeList(array $formElement, array $attributeList) {
491
492
493
        $attribute = '';
        foreach ($attributeList as $item) {
            if (isset($formElement[$item]))
Carsten  Rose's avatar
Carsten Rose committed
494
                $attribute .= $this->getAttribute(strtolower($item), $formElement[$item]);
495
496
497
498
499
500
        }
        return $attribute;
    }

    /**
     * Construct HTML Input attribute for Client Validation:
501
     *
502
503
504
505
506
507
508
509
510
511
512
513
     *   type     data                      predefined
     *   -------  -----------------------   -------------------------------------------------------------------------------
     *   min|max  <min value>|<max value>   min="%s"|max="%s"
     *   pattern  <regexp>                  pattern="%s"
     *   number   -                         pattern="^[0-9]*$"
     *   email    -                         pattern="^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$"
     *
     * For 'min|max' and 'pattern' the 'data' will be injected in the attribute string vai '%s'.
     *
     * @param $type
     * @param $data
     * @return string
514
     * @throws \qfq\UserException
515
     */
Carsten  Rose's avatar
Carsten Rose committed
516
    private function getInputCheckPattern($type, $data) {
517
518
        if ($type === '') {
            return '';
519
        }
520
521
522

        $attribute = '';

523
        $arrAttr = explode("|", $this->inputCheckPattern[$type]);
524
525
526
        $arrData = explode("|", $data);

        for ($ii = 0; $ii < count($arrAttr); $ii++) {
527
528
529
530
531
            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]) . ' ';
            }
532
533
534
535
        }
        return $attribute;
    }

536

537
    /**
538
539
     * Set corresponding html attributes readonly/required/disabled, based on $formElement['mode'].
     *
540
541
542
543
     * @param array $formElement
     * @return string
     * @throws UserException
     */
Carsten  Rose's avatar
Carsten Rose committed
544
    private function getAttributeMode(array $formElement) {
545
546
547
548
549
550
        $attribute = '';

        switch ($formElement['mode']) {
            case 'show':
                break;
            case 'readonly':
Carsten  Rose's avatar
Carsten Rose committed
551
                $attribute .= $this->getAttribute('readonly', 'readonly');
552
553
                break;
            case 'required':
Carsten  Rose's avatar
Carsten Rose committed
554
                $attribute .= $this->getAttribute('required', 'required');
555
556
557
558
                break;
            case 'lock':
                break;
            case 'disabled':
Carsten  Rose's avatar
Carsten Rose committed
559
                $attribute .= $this->getAttribute('disabled', 'disabled');
560
561
562
563
                break;
            default:
                $this->store->setVar(SYSTEM_FORM_ELEMENT, $formElement['name'] . ' / ' . $formElement['id'], STORE_SYSTEM);
                $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'mode', STORE_SYSTEM);
564
                throw new UserException("Unknown mode '" . $formElement['mode'] . "'", ERROR_UNKNOWN_MODE);
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
                break;
        }
        return $attribute;
    }

    /**
     * Builds HTML 'checkbox' 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="checkbox" [autofocus="autofocus"]
     *            [readonly="readonly"] [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return string
     * @throws UserException
     */
585
    public function buildCheckbox(array $formElement, $htmlFormElementId, $value) {
586
587
588
589
        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
590
        $this->getKeyValueListFromSqlEnumSpec($formElement, $itemKey, $itemValue);
591
592
593

        // Get fallback, if 'checkBoxMode' is not defined:
        if (!isset($formElement['checkBoxMode'])) {
594
            // This fallback is problematic if 'set' or 'enum' has 2 : defaults to single but maybe multi is meant.
595
596
597
598
599
600
601
602
603
604
            $formElement['checkBoxMode'] = (count($itemKey) > 2) ? 'multi' : 'single';
        }

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

Carsten  Rose's avatar
Carsten Rose committed
605
606
        $attributeBase = $this->getAttributeMode($formElement);
        $attributeBase .= $this->getAttribute('type', $formElement['type']);
607
608
609
610
611
612
613
614
615
616
617

        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);
        }
618
619
620
        return $html;
    }

621
    /**
622
623
624
625
626
     * Look for key/value list (in this order, first match counts) in
     *  a) `sql1`
     *  b) `parameter:itemList`
     *  c) table.column definition
     *
627
     * Copies the found keys to &$itemKey and the values to &$itemValue
628
     * If there are no &$itemKey, copy &$itemValue to &$itemKey.
629
630
631
632
     *
     * @param array $formElement
     * @param $itemKey
     * @param $itemValue
633
634
     * @throws CodeException
     * @throws \qfq\UserException
635
     */
636
    public function getKeyValueListFromSqlEnumSpec(array $formElement, &$itemKey, &$itemValue) {
637
638
639
640
        $fieldType = '';
        $itemKey = array();
        $itemValue = array();

641
642
643
        if (count($formElement) < 20)
            throw new CodeException("Invalid (none or to small) Formelement", ERROR_MISSING_FORMELEMENT);

644
645
646
        $itemValue = $this->getItemsForEnumOrSet($formElement['name'], $fieldType);

        if (is_array($formElement['sql1'])) {
647
648
649
            if (count($formElement['sql1']) > 0) {
                $keys = array_keys($formElement['sql1'][0]);
                $itemKey = array_column($formElement['sql1'], 'id');
650

651
652
653
654
                // 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]);
                }
655

656
657
658
659
660
661
                $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]);
                }
662
            }
663
        } elseif (isset($formElement['itemList']) && strlen($formElement['itemList']) > 0) {
664
            $arr = KeyValueStringParser::parse($formElement['itemList'], ':', ',', KVP_IF_VALUE_EMPTY_COPY_KEY);
665
666
            $itemValue = array_values($arr);
            $itemKey = array_keys($arr);
667
        } elseif ($fieldType === 'enum' || $fieldType === 'set') {
Carsten  Rose's avatar
Carsten Rose committed
668
            // already done at the beginning with '$this->getItemsForEnumOrSet($formElement['name'], $fieldType);'
669
        } else {
670
            throw new UserException("Missing definition (- nothing found in 'sql1', 'parameter:itemValues', 'enum-' or 'set-definition'", ERROR_MISSING_ITEM_VALUES);
671
672
673
674
675
        }

        if (count($itemKey) === 0) {
            $itemKey = $itemValue;
        }
676
677
678
679
680
681
682
683
684
685

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

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

    /**
689
690
     * Get the attribute definition list of an enum or set column. For strings, get the default value. Return elements as an array.
     *
691
692
693
694
695
696
697
698
699
700
701
     * @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) {
702
            throw new UserException("Column '$column' unknown in table '" . $this->formSpec['tableName'] . "'", ERROR_DB_UNKNOWN_COLUMN);
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
        }

        $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:
730
     *
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
     *    checked: first Element in $itemKey
     *  unchecked: ''
     *
     * @param array $itemKey
     * @param array $formElement
     * @throws UserException
     */
    private function prepareCheckboxCheckedUncheckedValue(array $itemKey, array &$formElement) {

        if (!isset($formElement['checked'])) {
            if (isset($itemKey[0])) {
                // First element in $itemKey list
                $formElement['checked'] = $itemKey[0];
            } else {
                // Take column default value
                $formElement['checked'] = $this->store->getVar($formElement['name'], STORE_TABLE_DEFAULT);
            }
        }

        // unchecked
        if (!isset($formElement['unchecked'])) {
            if (isset($itemKey[1])) {
                $formElement['unchecked'] = ($itemKey[0] === $formElement['checked']) ? $itemKey[1] : $itemKey[0];
            } else {
                $formElement['unchecked'] = '';
            }
        }

        if ($formElement['checked'] === $formElement['unchecked']) {
            throw new UserException('FormElement: type=checkbox - checked and unchecked can\'t be the same: ' . $formElement['checked'], ERROR_CHECKBOX_EQUAL);
        }

    }

    /**
766
767
     * Build a Checkbox based on two values.
     *
768
769
770
771
772
773
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $attribute
     * @param $value
     * @return string
     */
774
    public function buildCheckboxSingle(array $formElement, $htmlFormElementId, $attribute, $value) {
775
        $html = '';
776

Carsten  Rose's avatar
Carsten Rose committed
777
778
        $attribute .= $this->getAttribute('name', $htmlFormElementId);
        $attribute .= $this->getAttribute('value', $formElement['checked'], false);
779
        if ($formElement['checked'] === $value) {
Carsten  Rose's avatar
Carsten Rose committed
780
            $attribute .= $this->getAttribute('checked', 'checked');
781
782
        }

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

785
        $html = $this->buildNativeHidden($htmlFormElementId, $formElement['unchecked']);
786
787
788
789
790
791
792
793
794
795

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

        return $html;
    }

    /**
796
     * Builds a real HTML hidden form element. Useful for checkboxes, Multiple-Select and Radios.
797
798
799
800
801
802
803
804
805
806
807
     *
     * @param $htmlFormElementId
     * @param $value
     * @return string
     */
    public function buildNativeHidden($htmlFormElementId, $value) {
        return '<input type="hidden" name="' . $htmlFormElementId . '" value="' . htmlentities($value) . '">';
    }

    /**
     * Build as many Checkboxes as items
808
809
810
811
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $attributeBase
     * @param $value
812
813
     * @param array $itemKey
     * @param array $itemValue
814
815
     * @return string
     */
816
    public function buildCheckboxMulti(array $formElement, $htmlFormElementId, $attributeBase, $value, array $itemKey, array $itemValue) {
817
818
819
        // Defines which of the checkboxes will be checked.
        $values = explode($value, ',');

Carsten  Rose's avatar
Carsten Rose committed
820
        $attributeBase .= $this->getAttribute('name', $htmlFormElementId);
821

822
        $html = $this->buildNativeHidden($htmlFormElementId, $value);
823
824
825
826
827
828
829
830
831
832
833

        $flagFirst = true;
        $ii = 0;
        $jj = 0;
        foreach ($itemKey as $item) {
            $ii++;
            $jj++;
            $attribute = $attributeBase;
            if ($flagFirst) {
                $flagFirst = false;
                if (isset($formElement['autofocus']))
Carsten  Rose's avatar
Carsten Rose committed
834
                    $attribute .= $this->getAttribute('autofocus', $formElement['autofocus']);
835
            }
Carsten  Rose's avatar
Carsten Rose committed
836
            $attribute .= $this->getAttribute('value', $item);
837
            if ($item === $values[$jj]) {
Carsten  Rose's avatar
Carsten Rose committed
838
                $attribute .= $this->getAttribute('checked', 'checked');
839
840
841
842
843
844
845
846
847
848
849
            }
            $html .= '<input ' . $attribute . '>';
            $html .= $itemValue[$ii];
            if ($ii === $formElement['maxLength']) {
                $ii = 0;
                $html .= '<br>';
            }
        }
        return $html;
    }

850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
    /**
     * 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
     */
    public function buildHidden(array $formElement, $htmlFormElementId, $value) {

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

866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
    /**
     * 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
     */
881
    public function buildRadio(array $formElement, $htmlFormElementId, $value) {
882
883
884
885
        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
886
        $this->getKeyValueListFromSqlEnumSpec($formElement, $itemKey, $itemValue);
887

Carsten  Rose's avatar
Carsten Rose committed
888
889
890
        $attributeBase = $this->getAttributeMode($formElement);
        $attributeBase .= $this->getAttribute('name', $htmlFormElementId);
        $attributeBase .= $this->getAttribute('type', $formElement['type']);
891
892
893
894

        $jj = 0;
        $flagFirst = true;

895
        $html = $this->buildNativeHidden($htmlFormElementId, $value);
896
897
898
899
900
901
        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
902
                    $attribute .= $this->getAttribute('autofocus', $formElement['autofocus']);
903
904
            }

Carsten  Rose's avatar
Carsten Rose committed
905
            $attribute .= $this->getAttribute('value', $itemKey[$ii]);
906
            if ($itemKey[$ii] === $value) {
Carsten  Rose's avatar
Carsten Rose committed
907
                $attribute .= $this->getAttribute('checked', 'checked');
908
            }
909
910
911
912
913
914
915
916
917
918
919

            $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;

920
921
922
923
924
925
926
927
            if ($jj === $formElement['maxLength']) {
                $jj = 0;
                $html .= '<br>';
            }
        }
        return $html;
    }

Carsten  Rose's avatar
Carsten Rose committed
928
    /**
929
930
     * Builds a Selct (Dropdown) Box.
     *
Carsten  Rose's avatar
Carsten Rose committed
931
932
933
934
935
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return mixed
     */
936
    public function buildSelect(array $formElement, $htmlFormElementId, $value) {
Carsten  Rose's avatar
Carsten Rose committed
937
938
939
940
        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
941
        $this->getKeyValueListFromSqlEnumSpec($formElement, $itemKey, $itemValue);
Carsten  Rose's avatar
Carsten Rose committed
942
943
944

        $attribute = $this->getAttributeMode($formElement);
        $attribute .= $this->getAttribute('name', $htmlFormElementId);
945
        $attribute .= $this->getAttributeList($formElement, ['autofocus']);
Carsten  Rose's avatar
Carsten Rose committed
946
947
948
949
950
951
952

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

        $option = '';
953
        $selected = 'selected';
Carsten  Rose's avatar
Carsten Rose committed
954
955
956
957
958
        for ($ii = 0; $ii < count($itemValue); $ii++) {
            $option .= '<option ';

            $option .= $this->getAttribute('value', $itemKey[$ii]);
            if ($itemKey[$ii] === $value) {
959
960
                $option .= $selected;
                $selected = '';
Carsten  Rose's avatar
Carsten Rose committed
961
962
963
964
965
966
            }

            $option .= '>' . $itemValue[$ii] . '</option>';
        }

        return '<select ' . $attribute . '>' . $option . '</select>';
967
968
    }

Carsten  Rose's avatar
Carsten Rose committed
969
970
971
972
973
974
975
976
977
978
    /**
     * Constuct a HTML table of the subrecord data.
     * Column syntax definition: https://wikiit.math.uzh.ch/it/projekt/qfq/qfq-jqwidgets/Documentation#Type:_subrecord
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return string
     * @throws UserException
     */
979
    public function buildSubrecord(array $formElement, $htmlFormElementId, $value) {
980
981
        $html = '';

982
983
984
985
986
987
        $primaryRecord = $this->store->getStore(STORE_RECORD);

        if (!isset($primaryRecord['id'])) {
            return 'Please save main record fist.';
        }

Carsten  Rose's avatar
Carsten Rose committed
988
989
990
991
        if (!is_array($formElement['sql1'])) {
            throw new UserException('Missing \'sql1\' Query', ERROR_MISSING_SQL1);
        }

992
993
994
995
996
        // No records?
        if (count($formElement['sql1']) == 0) {
            return '';
        }

Carsten  Rose's avatar
Carsten Rose committed
997
998
999
1000
1001
1002
        $nameColumnId = 'id';
        if (!isset($formElement['sql1'][0][$nameColumnId]))
            $nameColumnId = '_id';

        if (!isset($formElement['sql1'][0][$nameColumnId])) {
            throw new UserException('Missing column \'id\' (or "@_id") in  \'sql1\' Query', ERROR_DB_MISSING_COLUMN_ID);
1003
1004
        }

Carsten  Rose's avatar
Carsten Rose committed
1005
1006
        // construct column attributes
        $control = $this->getSubrecordColumnControl(array_keys($formElement['sql1'][0]));
1007

1008
//        $html .= '<b>' . $formElement['label'] . '</b>';
1009
//        $html .= '<table border="1">';
1010

Carsten  Rose's avatar
Carsten Rose committed
1011
        $linkNew = $this->createFormLink($formElement, 0, $primaryRecord, $this->symbol[SYMBOL_NEW], 'New');
1012
1013
        $html .= '<p>' . $linkNew . '</p>';

1014
        $html .= '<table class="table">';
1015
        $html .= '<tr><th></th><th>' . implode('</th><th>', $control['title']) . '</th></tr>';
1016

Carsten  Rose's avatar
Carsten Rose committed
1017
        foreach ($formElement['sql1'] as $row) {
1018

Carsten  Rose's avatar
Carsten Rose committed
1019
            $html .= '<tr>';
Carsten  Rose's avatar
Carsten Rose committed
1020
            $html .= '<td>' . $this->createFormLink($formElement, $row[$nameColumnId], $primaryRecord, $this->symbol[SYMBOL_EDIT], 'Edit') . '</td>';
1021

1022
            foreach ($row as $columnName => $value) {
1023
                $html .= '<td>' . $this->renderCell($control, $columnName, $value) . '</td>';
Carsten  Rose's avatar
Carsten Rose committed
1024
1025
1026
1027
1028
            }
            $html .= '</tr>';
        }
        $html .= '</table>';

1029
        return $html;
1030
1031
    }

Carsten  Rose's avatar
Carsten Rose committed
1032
    /**
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
     * Get various column format information based on the 'raw' column title. The attributes are separated by '|' and specified as 'key' or 'key=value'.
     *
     * - Return all parsed values as an assoc array.
     * - For regular columns: If there is no 'width' specified, take the default 'SUBRECORD_COLUMN_WIDTH'
     * - For 'icon /  url / mailto': no width limit.
     *
     * Returned assoc array:
     *  title      Only key. Element is non numeric, which is not a keyword 'width/nostrip/icon/url/mailto'
     *  width      Key/Value Pair. Not provided for 'icon/url/mailto'.
     *  nostrip    Only key. Do not strip HTML Tags from the content.
     *  icon       Only key. Value will rendered (later) as an image.
     *  url        Only key. Value will rendered (later) as a 'href'
     *  mailto     Only key. Value will rendered (later) as a 'href mailto'
     *
     *
Carsten  Rose's avatar
Carsten Rose committed
1048
1049
1050
1051
     * @param $titleRaw
     * @return array
     * @throws UserException
     */
1052
    private function getSubrecordColumnControl(array $titleRaw) {
Carsten  Rose's avatar
Carsten Rose committed
1053
1054
        $control = array();

1055
1056
1057
        foreach ($titleRaw AS $columnName) {
            $flagWidthLimit = true;
            $control['width'][$columnName] = SUBRECORD_COLUMN_WIDTH;
Carsten  Rose's avatar
Carsten Rose committed
1058
1059

            // a) 'City@width=40', b) 'Status@icon', c) 'Mailto@width=80@nostrip'
1060
            $arr = KeyValueStringParser::parse($columnName, '=', '|', KVP_IF_VALUE_EMPTY_COPY_KEY);
1061
1062
            foreach ($arr as $attribute => $value) {
                switch ($attribute) {
Carsten  Rose's avatar
Carsten Rose committed
1063
1064
                    case 'width':
                    case 'nostrip':
1065
                    case 'title':
1066
                        break;
Carsten  Rose's avatar
Carsten Rose committed
1067
                    case 'icon':
1068
1069
1070
                    case 'url':
                    case 'mailto':
                        $flagWidthLimit = false;
Carsten  Rose's avatar
Carsten Rose committed
1071
1072
                        break;
                    default:
1073
                        $attribute = is_numeric($value) ? 'width' : 'title';
Carsten  Rose's avatar
Carsten Rose committed
1074
1075
                        break;
                }
1076
                $control[$attribute][$columnName] = $value;
Carsten  Rose's avatar
Carsten Rose committed
1077
            }
1078

1079
1080
1081
            if (!isset($control['title'][$columnName]))
                $control['title'][$columnName] = ''; // Fallback:  Might be wrong, but better than nothing.

1082
            // Limit title length
1083
1084
1085
1086
1087
1088
            $control['title'][$columnName] = substr($control['title'][$columnName], 0, $control['width'][$columnName]);

            if (!$flagWidthLimit) {
                $control['width'][$columnName] = false;
            }

Carsten  Rose's avatar
Carsten Rose committed
1089
1090
        }
        ret