AbstractBuildForm.php 36.9 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

23
24

/**
Carsten  Rose's avatar
Carsten Rose committed
25
26
 * Class AbstractBuildForm
 * @package qfq
27
 */
28
abstract class AbstractBuildForm {
29
30
31
    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
32
    protected $store = null;
Carsten  Rose's avatar
Carsten Rose committed
33
    protected $evaluate = null;
34
35
    protected $buildElementFunctionName = array();
    protected $pattern = array();
36
    protected $wrap = array();
37

38
39
40
41
42
    public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative) {
        $this->formSpec = $formSpec;
        $this->feSpecAction = $feSpecAction;
        $this->feSpecNative = $feSpecNative;
        $this->store = Store::getInstance();
43
        $this->db = new Database();
Carsten  Rose's avatar
Carsten Rose committed
44
        $this->evaluate = new Evaluate($this->store, $this->db);
45
46
//        $sip = $this->store->getVar(CLIENT_SIP, STORE_CLIENT);

47
        // render mode specific
48
        $this->fillWrap();
49
50
51

        $this->buildElementFunctionName = [
            'checkbox' => 'Checkbox',
52
53
            'dateJQW' => 'DateJQW',
            'datetimeJQW' => 'DateJQW',
54
55
56
57
58
59
60
61
            'email' => 'Input',
            'gridJQW' => 'GridJQW',
            'hidden' => 'Hidden',
            'input' => 'Input',
            'note' => 'Note',
            'password' => 'Input',
            'radio' => 'Radio',
            'select' => 'Select',
62
            'subrecord' => 'Subrecord',
Carsten  Rose's avatar
Carsten Rose committed
63
            'upload' => 'File',
64
65
            'fieldset' => 'Fieldset',
            'pill' => 'Pill'
66
67
        ];

68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
        $this->buildRowName = [
            'checkbox' => 'Native',
            'dateJQW' => 'Native',
            'datetimeJQW' => 'Native',
            'email' => 'Native',
            'gridJQW' => 'Native',
            'hidden' => 'Native',
            'input' => 'Native',
            'note' => 'Native',
            'password' => 'Native',
            'radio' => 'Native',
            'select' => 'Native',
            'subrecord' => 'Subrecord',
            'upload' => 'Native',
            'fieldset' => 'Fieldset',
            'pill' => 'Pill'
        ];

86
87
88
89
90
91
        $this->inputCheckPattern = [
            'min|max' => 'min="%s"|max="%s"',
            'pattern' => 'pattern="%s"',
            'number' => 'pattern="^[0-9]*$"',
            '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
92

93
94
    }

95
96
    abstract public function fillWrap();

97
98
    /**
     * @return string The whole form as HTML
99
100
     * @throws CodeException
     * @throws DbException
101
102
103
104
105
     */
    public function process() {

        $html = $this->head();

106
        $filter = $this->getProcessFilter();
107

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

110
111
            $parentRecords = $this->db->sql($this->formSpec['multiSql']);
            foreach ($parentRecords as $row) {
112
                $this->store->setVarArray($row, STORE_PARENT_RECORD, true);
113
                $html .= $this->elements($row['_id'], $filter);
114
115
            }
        } else {
116
            $html .= $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), $filter);
117
118
        }

119
        // close the form
120
121
        $html .= $this->tail();

122

123
        $html .= $this->doSubrecords();
124

125
126
127
        return $html;
    }

128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
    public function head() {
        $html = '';

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

        return $html;
    }

    /**
     * @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];
    }

    /**
     * Rreturns complete '<form ...>'
     *
     * @return string
     */
    public function getFormTag() {

        $attribute = $this->getFormTagAtrributes();

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

    /**
     * Build an assoc array with standard form attributes.
     *
     * @return mixed
     */
    public function getFormTagAtrributes() {

        $attribute['method'] = 'post';
        $attribute['action'] = $this->getActionUrl();
        $attribute['target'] = '_top';
        $attribute['accept-charset'] = 'UTF-8';
        $attribute['autocomplete'] = 'on';
        $attribute['enctype'] = $this->getEncType();

        return $attribute;
    }

    /**
     * Builds the HTML 'form'-tag inlcuding all attributes and target.
     *
181
182
     * Notice: the SIP will be transferred as POST Parameter.
     *
183
184
185
186
     * @return string
     * @throws DbException
     */
    public function getActionUrl() {
187
        $queryStringArray = array();
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207

        Support::appendTypo3ParameterToArray($queryStringArray);

        return basename($_SERVER['SCRIPT_NAME']) . "?" . Support::arrayToQueryString($queryStringArray);
    }

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

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

    }
208

209
    abstract public function getProcessFilter();
210
211
212

    /**
     * @param $recordId
213
     * @param string $filter FORM_ELEMENTS_NATIVE | FORM_ELEMENTS_SUBRECORD | FORM_ELEMENTS_NATIVE_SUBRECORD
214
     * @return string
215
216
     * @throws CodeException
     * @throws DbException
217
     */
218
    public function elements($recordId, $filter = FORM_ELEMENTS_NATIVE, $feIdContainer = 0) {
219
220
221
        $html = '';

        // get current data record
222
        if ($recordId > 0 && $this->store->getVar('id', STORE_RECORD) === false) {
223
            $row = $this->db->sql("SELECT * FROM " . $this->formSpec['tableName'] . " WHERE id = ?", ROW_REGULAR, array($recordId));
224
225
            $this->store->setVarArray($row[0], STORE_RECORD);
        }
226
227
228

        // Iterate over all FormElements
        foreach ($this->feSpecNative as $fe) {
229

230
231
            if (($filter === FORM_ELEMENTS_NATIVE && $fe['type'] === 'subrecord') ||
                ($filter === FORM_ELEMENTS_SUBRECORD && $fe['type'] !== 'subrecord')
232
233
234
235
            ) {
                continue; // skip this FE
            }

236
237
238
239
            // Log / Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, $fe['name'] . ' / ' . $fe['id'], STORE_SYSTEM);

            // evaluate current FormElement
240
241
242
243
244
            $evaluate = new Evaluate($this->store, $this->db);
            $formElement = $evaluate->parseArray($fe, $fe['debug'] === 'yes');
            if ($fe['debug'] === 'yes') {
                throw new UserException($evaluate->getDebug(), ERROR_DEBUG);
            }
245

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

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

251
252
253
254
255
256
257
258
259
260
            // Construct Marshaller Name
            $buildElementFunctionName = 'build' . $this->buildElementFunctionName[$formElement['type']];

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

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

            $html .= $this->$buildRowName($formElement, $elementHtml);
261
        }
262

263
264
265
        // Log / Debug: Last FormElement has been processed.
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

266
267
268
        return $html;
    }

269
270
271
    abstract public function tail();

    abstract public function doSubrecords();
272

273
274
275
276
277
278
279
280
    abstract public function buildRowNative($formElement, $elementHtml);

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

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

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

281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
    /**
     * @param $tag
     * @param $value
     * @param bool|false $flagOmitEmpty
     * @return string
     */
    public function wrapTag($tag, $value, $flagOmitEmpty = false) {
        if ($flagOmitEmpty && $value === "")
            return '';

        // <div class="container-fluid"> >> </div>
        $arr = explode(' ', $tag);
        $closing = '</' . substr($arr[0], 1) . '>';

        return $tag . $value . $closing;
    }

298
    /**
299
300
     * @param array $htmlFormElementId
     * @param $label
301
302
     * @return string
     */
303
304
305
306
    public function buildLabel($htmlFormElementId, $label) {
        $html = '<label for="' . $htmlFormElementId . '">' . $label . '</label>';

        return $html;
307
308
    }

309
310
311
312
313
314
315
316
317
318
319
320
321
    /**
     * 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
     */
322
    public function buildInput(array $formElement, $htmlFormElementId, $value) {
323
        $textarea = '';
324

Carsten  Rose's avatar
Carsten Rose committed
325
        $attribute = $this->getAttribute('name', $htmlFormElementId);
326
327
328
        $htmlTag = '<input';

        // Check for input type 'textarea'
329
        $colsRows = explode(',', $formElement['size'], 2);
330
        if (count($colsRows) === 2) {
331
            // <textarea>
332
333
            $htmlTag = '<textarea';

Carsten  Rose's avatar
Carsten Rose committed
334
335
            $attribute .= $this->getAttribute('cols', $colsRows[0]);
            $attribute .= $this->getAttribute('rows', $colsRows[1]);
336
            $textarea = htmlentities($value) . '</textarea>';
337
338

        } else {
339
340
341
342
            // <input>
            if ($formElement['maxLength'] > 0) {
                $value = substr($value, 0, $formElement['maxLength']);
            }
343
            $attribute .= $this->getAttributeList($formElement, ['type', 'size', 'maxLength']);
344
            $attribute .= $this->getAttribute('value', htmlentities($value), false);
Carsten  Rose's avatar
Carsten Rose committed
345
        }
346
        // 'maxLength' needs an upper 'L': naming convention for DB tables!
347
        $attribute .= $this->getAttributeList($formElement, ['autocomplete', 'autofocus', 'placeholder']);
348
349
350
351

        if ($formElement['type'] === 'email' && $formElement['checkType'] === '')
            $formElement['checkType'] = 'email';

Carsten  Rose's avatar
Carsten Rose committed
352
        $attribute .= $this->getInputCheckPattern($formElement['checkType'], $formElement['checkPattern']);
353

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

356
        return "$htmlTag $attribute>$textarea";
357
358
359
360
361
362
363
364
    }

    /**
     * @param $type
     * @param $value
     * @param bool|false $flagOmitEmpty
     * @return string
     */
Carsten  Rose's avatar
Carsten Rose committed
365
    private function getAttribute($type, $value, $flagOmitEmpty = true) {
366
367
        if ($flagOmitEmpty && $value === "")
            return '';
Carsten  Rose's avatar
Carsten Rose committed
368

369
370
371
372
373
374
375
376
377
378
379
380
        return "$type=\"$value\" ";
    }

    /**
     * Builds a HTML attribute list, based on  $attributeList.
     * 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
381
    private function getAttributeList(array $formElement, array $attributeList) {
382
383
384
        $attribute = '';
        foreach ($attributeList as $item) {
            if (isset($formElement[$item]))
Carsten  Rose's avatar
Carsten Rose committed
385
                $attribute .= $this->getAttribute(strtolower($item), $formElement[$item]);
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
        }
        return $attribute;
    }

    /**
     * Construct HTML Input attribute for Client Validation:
     *   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
     */
Carsten  Rose's avatar
Carsten Rose committed
405
    private function getInputCheckPattern($type, $data) {
406
407
        if ($type === '') {
            return '';
408
        }
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426

        $attribute = '';

        $arrAttr = explode("|", $type);
        $arrData = explode("|", $data);

        for ($ii = 0; $ii < count($arrAttr); $ii++) {
            if ($arrAttr[$ii])
                $attribute .= str_replace('%s', $arrData[$ii], $arrAttr[$ii]) . ' ';
        }
        return $attribute;
    }

    /**
     * @param array $formElement
     * @return string
     * @throws UserException
     */
Carsten  Rose's avatar
Carsten Rose committed
427
    private function getAttributeMode(array $formElement) {
428
429
430
431
432
433
        $attribute = '';

        switch ($formElement['mode']) {
            case 'show':
                break;
            case 'readonly':
Carsten  Rose's avatar
Carsten Rose committed
434
                $attribute .= $this->getAttribute('readonly', 'readonly');
435
436
                break;
            case 'required':
Carsten  Rose's avatar
Carsten Rose committed
437
                $attribute .= $this->getAttribute('required', 'required');
438
439
440
441
                break;
            case 'lock':
                break;
            case 'disabled':
Carsten  Rose's avatar
Carsten Rose committed
442
                $attribute .= $this->getAttribute('disabled', 'disabled');
443
444
445
446
                break;
            default:
                $this->store->setVar(SYSTEM_FORM_ELEMENT, $formElement['name'] . ' / ' . $formElement['id'], STORE_SYSTEM);
                $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'mode', STORE_SYSTEM);
447
                throw new UserException("Unknown mode '" . $formElement['mode'] . "'", ERROR_UNKNOWN_MODE);
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
                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
     */
468
    public function buildCheckbox(array $formElement, $htmlFormElementId, $value) {
469
470
471
472
473
474
475
476
        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
        $this->extractKeyValueList($formElement, $itemKey, $itemValue);

        // Get fallback, if 'checkBoxMode' is not defined:
        if (!isset($formElement['checkBoxMode'])) {
477
            // This fallback is problematic if 'set' or 'enum' has 2 : defaults to single but maybe multi is meant.
478
479
480
481
482
483
484
485
486
487
            $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
488
489
490
        $attributeBase = $this->getAttributeMode($formElement);
        $attributeBase .= $this->getAttribute('name', $htmlFormElementId);
        $attributeBase .= $this->getAttribute('type', $formElement['type']);
491
492
493
494
495
496
497
498
499
500
501
502

        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);
        }

503
504
505
        return $html;
    }

506
    /**
507
508
509
510
511
     * Look for key/value list (in this order, first match counts) in
     *  a) `sql1`
     *  b) `parameter:itemList`
     *  c) table.column definition
     *
512
     * Copies the found keys to &$itemKey and the values to &$itemValue
513
     * If there are no &$itemKey, copy &$itemValue to &$itemKey.
514
515
516
517
518
519
520
521
522
523
524
525
526
527
     *
     * @param array $formElement
     * @param $itemKey
     * @param $itemValue
     * @throws UserException
     */
    private function extractKeyValueList(array $formElement, &$itemKey, &$itemValue) {
        $fieldType = '';
        $itemKey = array();
        $itemValue = array();

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

        if (is_array($formElement['sql1'])) {
528
529
530
            if (count($formElement['sql1']) > 0) {
                $keys = array_keys($formElement['sql1'][0]);
                $itemKey = array_column($formElement['sql1'], 'id');
531

532
533
534
535
                // 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]);
                }
536

537
538
539
540
541
542
                $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]);
                }
543
            }
544
        } elseif (isset($formElement['itemList'])) {
545
546
547
548
549
            if (count($formElement['itemList']) > 0) {
                $arr = KeyValueStringParser::parse($formElement['itemList'], ':', ',', IF_VALUE_EMPTY_COPY_KEY);
                $itemValue = array_values($arr);
                $itemKey = array_keys($arr);
            }
550
        } elseif ($fieldType === 'enum' || $fieldType === 'set') {
Carsten  Rose's avatar
Carsten Rose committed
551
            // already done at the beginning with '$this->getItemsForEnumOrSet($formElement['name'], $fieldType);'
552
        } else {
553
            throw new UserException("Missing definition (- nothing found in 'sql1', 'parameter:itemValues', 'enum-' or 'set-definition'", ERROR_MISSING_ITEM_VALUES);
554
555
556
557
558
        }

        if (count($itemKey) === 0) {
            $itemKey = $itemValue;
        }
559
560
561
562
563
564
565
566
567
568

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

        if (isset($formElement['emptyItemAtEnd'])) {
            $itemValue[] = '';
            $itemKey[] = '';
        }
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
    }

    /**
     * @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) {
            throw new UserException("Column '$column' unknown in table '" . $this->formSpec['tableName'] . "'", ERROR_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', ... ]
        $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:
     *    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);
        }

    }

    /**
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $attribute
     * @param $value
     * @return string
     */
652
    public function buildCheckboxSingle(array $formElement, $htmlFormElementId, $attribute, $value) {
653
        $html = '';
654

Carsten  Rose's avatar
Carsten Rose committed
655
656
        $attribute .= $this->getAttribute('name', $htmlFormElementId);
        $attribute .= $this->getAttribute('value', $formElement['checked'], false);
657
        if ($formElement['checked'] === $value) {
Carsten  Rose's avatar
Carsten Rose committed
658
            $attribute .= $this->getAttribute('checked', 'checked');
659
660
        }

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

663
//        $html = $this->buildNativeHidden( $htmlFormElementId, $formElement['unchecked']);
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681

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

        return $html;
    }

    /**
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $attributeBase
     * @param $value
     * @param $itemKey
     * @param $itemValue
     * @return string
     */
682
    public function buildCheckboxMulti(array $formElement, $htmlFormElementId, $attributeBase, $value, $itemKey, $itemValue) {
683
684
685
        // Defines which of the checkboxes will be checked.
        $values = explode($value, ',');

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

688
        $html = $this->buildNativeHidden($htmlFormElementId, $value);
689
690
691
692
693
694
695
696
697
698
699

        $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
700
                    $attribute .= $this->getAttribute('autofocus', $formElement['autofocus']);
701
            }
Carsten  Rose's avatar
Carsten Rose committed
702
            $attribute .= $this->getAttribute('value', $item);
703
            if ($item === $values[$jj]) {
Carsten  Rose's avatar
Carsten Rose committed
704
                $attribute .= $this->getAttribute('checked', 'checked');
705
706
707
708
709
710
711
712
713
714
715
            }
            $html .= '<input ' . $attribute . '>';
            $html .= $itemValue[$ii];
            if ($ii === $formElement['maxLength']) {
                $ii = 0;
                $html .= '<br>';
            }
        }
        return $html;
    }

716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
    /**
     * Builds a real HTML hidden form element. Usefull for Checkboxes, Multiple-Select and Radios.
     *
     * @param $htmlFormElementId
     * @param $value
     * @return string
     */
    public function buildNativeHidden($htmlFormElementId, $value) {
        return '<input type="hidden" name="' . $htmlFormElementId . '" value="' . htmlentities($value) . '">';
    }

    /**
     * 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);
    }

743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
    /**
     * 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
     */
758
    public function buildRadio(array $formElement, $htmlFormElementId, $value) {
759
760
761
762
763
764
        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
        $this->extractKeyValueList($formElement, $itemKey, $itemValue);

Carsten  Rose's avatar
Carsten Rose committed
765
766
767
        $attributeBase = $this->getAttributeMode($formElement);
        $attributeBase .= $this->getAttribute('name', $htmlFormElementId);
        $attributeBase .= $this->getAttribute('type', $formElement['type']);
768
769
770
771

        $jj = 0;
        $flagFirst = true;

772
        $html = $this->buildNativeHidden($htmlFormElementId, $value);
773
774
775
776
777
778
        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
779
                    $attribute .= $this->getAttribute('autofocus', $formElement['autofocus']);
780
781
            }

Carsten  Rose's avatar
Carsten Rose committed
782
            $attribute .= $this->getAttribute('value', $itemKey[$ii]);
783
            if ($itemKey[$ii] === $value) {
Carsten  Rose's avatar
Carsten Rose committed
784
                $attribute .= $this->getAttribute('checked', 'checked');
785
786
787
788
789
790
791
792
793
794
795
            }
            $html .= '<input ' . $attribute . '>';
            $html .= $itemValue[$ii];
            if ($jj === $formElement['maxLength']) {
                $jj = 0;
                $html .= '<br>';
            }
        }
        return $html;
    }

Carsten  Rose's avatar
Carsten Rose committed
796
797
798
799
800
801
    /**
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return mixed
     */
802
    public function buildSelect(array $formElement, $htmlFormElementId, $value) {
Carsten  Rose's avatar
Carsten Rose committed
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
        $this->extractKeyValueList($formElement, $itemKey, $itemValue);

        $attribute = $this->getAttributeMode($formElement);
        $attribute .= $this->getAttribute('name', $htmlFormElementId);
        $attribute .= $this->getAttributeList($formElement, ['type', 'autofocus']);

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

        $option = '';
819
        $selected = 'selected';
Carsten  Rose's avatar
Carsten Rose committed
820
821
822
823
824
        for ($ii = 0; $ii < count($itemValue); $ii++) {
            $option .= '<option ';

            $option .= $this->getAttribute('value', $itemKey[$ii]);
            if ($itemKey[$ii] === $value) {
825
826
                $option .= $selected;
                $selected = '';
Carsten  Rose's avatar
Carsten Rose committed
827
828
829
830
831
832
            }

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

        return '<select ' . $attribute . '>' . $option . '</select>';
833
834
    }

Carsten  Rose's avatar
Carsten Rose committed
835
836
837
838
839
840
841
842
843
844
    /**
     * 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
     */
845
    public function buildSubrecord(array $formElement, $htmlFormElementId, $value) {
846
847
        $html = '';

848
849
850
851
852
853
        $primaryRecord = $this->store->getStore(STORE_RECORD);

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

Carsten  Rose's avatar
Carsten Rose committed
854
855
856
857
        if (!is_array($formElement['sql1'])) {
            throw new UserException('Missing \'sql1\' Query', ERROR_MISSING_SQL1);
        }

858
859
860
861
862
863
864
865
866
        // No records?
        if (count($formElement['sql1']) == 0) {
            return '';
        }

        if (!isset($formElement['sql1'][0]['id'])) {
            throw new UserException('Missing column \'id\' in  \'sql1\' Query', ERROR_MISSING_COLUMN_ID);
        }

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

870
871
//        $html .= '<b>' . $formElement['label'] . '</b>';
        $html .= '<table border="1">';
872
        $html .= '<tr><th></th><th>' . implode('</th><th>', $control['title']) . '</th></tr>';
Carsten  Rose's avatar
Carsten Rose committed
873
        foreach ($formElement['sql1'] as $row) {
874

Carsten  Rose's avatar
Carsten Rose committed
875
            $html .= '<tr>';
876
            $html .= '<td>' . $this->editLink($formElement, $row['id'], $primaryRecord) . '</td>';
877

878
879
            foreach ($row as $columnName => $value) {
                $html .= '<td>' . $this->formatColumn($control, $columnName, $value) . '</td>';
Carsten  Rose's avatar
Carsten Rose committed
880
881
882
883
884
            }
            $html .= '</tr>';
        }
        $html .= '</table>';

885
        return $html;
886
887
    }

Carsten  Rose's avatar
Carsten Rose committed
888
889
890
891
892
    /**
     * @param $titleRaw
     * @return array
     * @throws UserException
     */
893
    private function getSubrecordColumnControl(array $titleRaw) {
Carsten  Rose's avatar
Carsten Rose committed
894
895
        $control = array();

896
897
898
        foreach ($titleRaw AS $columnName) {
            $flagWidthLimit = true;
            $control['width'][$columnName] = SUBRECORD_COLUMN_WIDTH;
Carsten  Rose's avatar
Carsten Rose committed
899
900

            // a) 'City@width=40', b) 'Status@icon', c) 'Mailto@width=80@nostrip'
901
            $arr = KeyValueStringParser::parse($columnName, '=', '@', IF_VALUE_EMPTY_COPY_KEY);
902
903
            foreach ($arr as $attribute => $value) {
                switch ($attribute) {
Carsten  Rose's avatar
Carsten Rose committed
904
905
                    case 'width':
                    case 'nostrip':
906
                        break;
Carsten  Rose's avatar
Carsten Rose committed
907
                    case 'icon':
908
909
910
                    case 'url':
                    case 'mailto':
                        $flagWidthLimit = false;
Carsten  Rose's avatar
Carsten Rose committed
911
912
                        break;
                    default:
913
                        $attribute = is_numeric($value) ? 'width' : 'title';
Carsten  Rose's avatar
Carsten Rose committed
914
915
                        break;
                }
916
                $control[$attribute][$columnName] = $value;
Carsten  Rose's avatar
Carsten Rose committed
917
            }
918

919
            // Limit title length
920
921
922
923
924
925
            $control['title'][$columnName] = substr($control['title'][$columnName], 0, $control['width'][$columnName]);

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

Carsten  Rose's avatar
Carsten Rose committed
926
927
        }
        return $control;
928
929
    }

930
931
932
933
934
935
936
    /**
     * @param $formElement
     * @param $targetRecordId
     * @param $record
     * @return string
     * @throws UserException
     */
937
    private function editLink($formElement, $targetRecordId, $record) {
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971

        $queryStringArray = [
            'form' => $formElement['form'],
            'r' => $targetRecordId,

        ];

        // Add custom query parameter
        if (isset($formElement['detail'])) {
            $detailParam = KeyValueStringParser::parse($formElement['detail']);
            foreach ($detailParam as $src => $dest) {
                if ($src[0] == '#') {
                    $queryStringArray[$dest] = substr($src, 1);
                    continue;
                }

                if (isset($record[$src])) {
                    $queryStringArray[$dest] = $record[$src];
                }
            }
        }

        Support::appendTypo3ParameterToArray($queryStringArray);

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

        $sip = $this->store->getSip();
        $url = $formElement['page'] . $sip->queryStringToSip($queryString);

        return "<a href='$url'><span class='glyphicon glyphicon-pencil'></span></a>";

    }

972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
    /**
     * @param array $control
     * @param $columnName
     * @param $value
     * @return string
     */
    private function formatColumn(array $control, $columnName, $value) {

        $cell = isset($control['nostrip'][$columnName]) ? $value : strip_tags($value);

        if ($control['width'][$columnName] !== false)
            $cell = substr($cell, 0, $control['width'][$columnName]);

        if (isset($control['icon'][$columnName])) {
            $cell = ($cell === '') ? '' : "<image src='fileadmin/icons/$cell'>";
        }

        if (isset($control['mailto'][$columnName])) {
            $cell = "<a href='mailto:$cell'>$cell</a>";
        }

        if (isset($control['url'][$columnName])) {
            $cell = "<a href='$cell'>$cell</a>";
        }

        return $cell;
    }

Carsten  Rose's avatar
Carsten Rose committed
1000
1001
1002
1003
1004
1005
1006
    /**
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return string
     * @throws UserException
     */
1007
    public function buildFile(array $formElement, $htmlFormElementId, $value) {
Carsten  Rose's avatar
Carsten Rose committed
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022

        $attribute = $this->getAttributeMode($formElement);
        $attribute .= $this->getAttribute('type', 'file');
        $attribute .= $this->getAttribute('name', $htmlFormElementId);
        $attribute .= $this->getAttributeList($formElement, ['autofocus', 'accept']);

        return '<input ' . $attribute . '>';
    }

    /**
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @throws UserException
     */
1023
    public function buildDateJQW(array $formElement, $htmlFormElementId, $value) {
1024
        // TODO: implement
1025
        throw new UserException("Not implemented yet: buildDateJQW()", ERROR_NOT_IMPLEMENTED);
1026
1027
    }

Carsten  Rose's avatar
Carsten Rose committed
1028
1029
1030
1031
1032
1033
    /**
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @throws UserException
     */
1034
    public function buildGridJQW(array $formElement, $htmlFormElementId, $value) {
1035
        // TODO: implement
1036
        throw new UserException("Not implemented yet: buildGridJQW()", ERROR_NOT_IMPLEMENTED);
1037
    }
Carsten  Rose's avatar
Carsten Rose committed
1038
1039
1040
1041
1042
1043
1044

    /**
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return mixed
     */
1045
    public function buildNote(array $formElement, $htmlFormElementId, $value) {
Carsten  Rose's avatar
Carsten Rose committed
1046
1047
1048
        return $value;
    }

1049
1050
1051
1052
1053
1054
1055
    /**
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return mixed
     */
    public function buildPill(array $formElement, $htmlFormElementId, $value) {
1056
        return $value;
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
    }

    /**
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return mixed
     */
    public function buildFieldset(array $formElement, $htmlFormElementId, $value) {

        // save parent processed FE's
        $tmpStore = $this->feSpecNative;

        // <fieldset>
        $html = '<fieldset ' . $this->getAttribute('name', $htmlFormElementId) . '>';
1072
        if ($formElement['label'] !== '') {
1073
1074
1075
1076
1077
1078
            $html .= '<legend>' . $formElement['label'] . '</legend>';
        }

        $html .= $this->wrap[WRAP_SETUP_IN_FIELDSET][WRAP_SETUP_START];

        // child FE's
1079
        $sql = SQL_FORM_ELEMENT_SPECIFIC_CONTAINER;
1080
        $this->feSpecNative = $this->db->sql($sql, ROW_REGULAR, ['yes', $this->formSpec["id"], 'native,container', $formElement['id']]);
1081
        HelperFormElement::explodeFieldParameter($this->feSpecNative);
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
        $html .= $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), FORM_ELEMENTS_NATIVE_SUBRECORD);


        $html .= $this->wrap[WRAP_SETUP_IN_FIELDSET][WRAP_SETUP_END];

        $html .= '</fieldset>';

        // restore parent processed FE's
        $this->feSpecNative = $tmpStore;

1092
        return $html;
1093
1094
    }

1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
    /**
     * Create a new sip, based on latest STORE_SIP Values. Return complete HTML 'hidden' element.
     *
     * @return string
     */
    public function builtNewSip() {
        $sipArray = $this->store->getStore(STORE_SIP);
        unset($sipArray[SIP_SIP]);
        unset($sipArray[SIP_URLPARAM]);

        $queryString = Support::arrayToQueryString($sipArray);
        $sip = $this->store->getSip();

        $sipValue = $sip->queryStringToSip($queryString, RETURN_SIP);

        return $this->buildNativeHidden(CLIENT_SIP, $sipValue);
    }
1112

1113
}