BuildFormBootstrap.php 36.4 KB
Newer Older
1
2
3
4
5
6
7
8
<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 1/25/16
 * Time: 10:00 PM
 */

Marc Egger's avatar
Marc Egger committed
9
namespace IMATHUZH\Qfq\Core;
10

11
use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
Marc Egger's avatar
Marc Egger committed
12
13
14
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\Support;
15

16

Carsten  Rose's avatar
Carsten Rose committed
17
18
19
20
/**
 * Class BuildFormBootstrap
 * @package qfq
 */
21
22
class BuildFormBootstrap extends AbstractBuildForm {

23
24
25
26
27
28
    private $isFirstPill;

    /**
     * @param array $formSpec
     * @param array $feSpecAction
     * @param array $feSpecNative
29
     * @param array $db Array of 'Database' instances
Marc Egger's avatar
Marc Egger committed
30
31
32
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
33
     */
34
35
    public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative, array $db) {
        parent::__construct($formSpec, $feSpecAction, $feSpecNative, $db);
36
37
38
        $this->isFirstPill = true;
    }

Carsten  Rose's avatar
Carsten Rose committed
39
40
41
    /**
     *
     */
42
    public function fillWrap() {
43
44
45
46

//        $this->wrap[WRAP_SETUP_OUTER][WRAP_SETUP_START] = '<div class="tab-content">';
//        $this->wrap[WRAP_SETUP_OUTER][WRAP_SETUP_END] = '</div>';

47
        $this->wrap[WRAP_SETUP_TITLE][WRAP_SETUP_START] = "<div class='row'><div class='col-md-12'><h1>";
48
        $this->wrap[WRAP_SETUP_TITLE][WRAP_SETUP_END] = "</h1></div></div>";
49

50
        // Element: Label + Input + Note
51
        $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_CLASS] = "form-group clearfix";
52
        $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_START] = "<div class='" . $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_CLASS] . "'>";
53
        $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_END] = "</div>";
54

55
56
        $this->wrap[WRAP_SETUP_SUBRECORD][WRAP_SETUP_START] = "<div class='col-md-12'>";
        $this->wrap[WRAP_SETUP_SUBRECORD][WRAP_SETUP_END] = "</div>";
57

58
59
        $this->wrap[WRAP_SETUP_IN_FIELDSET][WRAP_SETUP_START] = "";
        $this->wrap[WRAP_SETUP_IN_FIELDSET][WRAP_SETUP_END] = "";
60

61
62
63
        $this->wrap[WRAP_SETUP_IN_TEMPLATE_GROUP][WRAP_SETUP_START] = "";
        $this->wrap[WRAP_SETUP_IN_TEMPLATE_GROUP][WRAP_SETUP_END] = "";

64
65
//        $this->feDivClass['radio'] = 'radio';
//        $this->feDivClass['checkbox'] = 'checkbox';
66
67
    }

68
69
    /**
     * @param string $addClass
Carsten  Rose's avatar
Carsten Rose committed
70
     *
71
     * @return string
Marc Egger's avatar
Marc Egger committed
72
     * @throws \CodeException
73
74
75
76
77
78
79
     */
    public function getRowOpenTag($addClass = '') {
        $class = Support::doAttribute('class', [$this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_CLASS], $addClass]);

        return "<div $class>";

    }
80

81
    /**
82
83
84
     * Fill the BS wrapper for Label/Input/Note.
     * For legacy reasons, $label/$input/$note might be a number (0-12) or the bs column classes ('col-md-12 col-lg9')
     *
85
86
87
88
89
     * @param $label
     * @param $input
     * @param $note
     */
    public function fillWrapLabelInputNote($label, $input, $note) {
90
91
92

        $label = is_numeric($label) ? ('col-md-' . $label) : $label;
        $this->wrap[WRAP_SETUP_LABEL][WRAP_SETUP_START] = "<div class='$label qfq-label'>";
93
        $this->wrap[WRAP_SETUP_LABEL][WRAP_SETUP_END] = "</div>";
94
95
96

        $input = is_numeric($input) ? ('col-md-' . $input) : $input;
        $this->wrap[WRAP_SETUP_INPUT][WRAP_SETUP_START] = "<div class='$input'>";
97
        $this->wrap[WRAP_SETUP_INPUT][WRAP_SETUP_END] = "</div>";
98
99
100

        $note = is_numeric($note) ? ('col-md-' . $note) : $note;
        $this->wrap[WRAP_SETUP_NOTE][WRAP_SETUP_START] = "<div class='$note qfq-note'>";
101
102
103
104
        $this->wrap[WRAP_SETUP_NOTE][WRAP_SETUP_END] = "</div>";

    }

Carsten  Rose's avatar
Carsten Rose committed
105
106
107
    /**
     * @return string
     */
108
109
110
111
    public function getProcessFilter() {
        return FORM_ELEMENTS_NATIVE_SUBRECORD;
    }

Carsten  Rose's avatar
Carsten Rose committed
112
113
114
    /**
     * @return string
     */
115
116
117
118
119
    public function doSubrecords() {
        return '';
    }

    /**
120
     * @param string $mode
121
     * @return string
Marc Egger's avatar
Marc Egger committed
122
123
124
125
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
126
     */
127
    public function head($mode = FORM_LOAD) {
128
        $html = '';
129
        $title = '';
130

Carsten  Rose's avatar
Carsten Rose committed
131
        $html .= '<div ' . Support::doAttribute('class', $this->formSpec[F_CLASS], true) . '>'; // main <div class=...> around everything, Whole FORM; class="container" or class="container-fluid"
132

133
134
        $button = Support::wrapTag('<div class="row"><div class="col-md-12">', $this->buildButtons());

135
        // Show title / frame only if there is a title given.
136
        if (trim($this->formSpec[F_TITLE]) != '') {
137
            $classTitle = isset($this->formSpec[F_CLASS_TITLE]) ? $this->formSpec[F_CLASS_TITLE] : "qfq-form-title";
138
            $title = Support::wrapTag('<div class="row"><div class="col-md-12">', Support::wrapTag('<div class="' . $classTitle . '">', $this->formSpec[F_TITLE]));
139
        }
140
141

        $html .= $button . $title;
142

143
        $dummy = array();
144
        $pill = $this->buildPillNavigation($mode, OnArray::filter($this->feSpecNative, FE_TYPE, FE_TYPE_PILL), $dummy);
145
        $html .= Support::wrapTag('<div class="row">', $pill);
146

147
148
        $html .= $this->getFormTag();

149
150
151
        $class = ['tab-content', $this->formSpec[F_CLASS_BODY]];
        if ($pill == '') {
            $class[] = 'col-md-12';
152
153
154
155
156
            $class[] = 'qfq-form-body'; // Make an outline on form body

            if ($title == '') {
                $class[] = 'qfq-form-no-title';
            }
157
158
        }
        $html .= "<div " . Support::doAttribute('class', $class) . ">";
159
160
161
162

        return $html;
    }

163
164
165
166
167
168
    /**
     * Creates a Checkbox, which toggles 'hide'/'unhide' via JS, on all elements with class= CLASS_FORM_ELEMENT_EDIT.
     *
     * @return string - the rendered Checkbox
     */
    private function buildShowEditFormElementCheckbox() {
Carsten  Rose's avatar
Carsten Rose committed
169
        // EditFormElement Icons
170
171
        $js = '$(".' . CLASS_FORM_ELEMENT_EDIT . '").toggleClass("hidden")';
        $element = "<input type='checkbox' onchange='" . $js . "'>" .
172
            Support::wrapTag("<span title='Toggle: Edit form element icons' class='" . GLYPH_ICON . ' ' . GLYPH_ICON_TASKS . "'>", '');
173
        $element = Support::wrapTag('<label class="btn btn-default navbar-btn">', $element);
Carsten  Rose's avatar
Carsten Rose committed
174

175
176
177
        return Support::wrapTag('<div class="btn-group" data-toggle="buttons">', $element);
    }

Carsten  Rose's avatar
Carsten Rose committed
178
179
180
181
    /**
     * Creates a button to open 'CopyForm' with the current form as source.
     *
     * @return string - the rendered button
Marc Egger's avatar
Marc Egger committed
182
183
     * @throws \CodeException
     * @throws \UserFormException
Carsten  Rose's avatar
Carsten Rose committed
184
185
186
187
188
189
190
191
192
193
194
     */
    private function buildButtonCopyForm() {

        // Show copy icon only on form 'form' and only if there is a form loaded (id>0)
        if ($this->formSpec[COLUMN_ID] != 1) {
            return '';
        }
        // current loaded form.
        $formId = $this->store->getVar(COLUMN_ID, STORE_RECORD . STORE_ZERO);

        $queryStringArray = [
Carsten  Rose's avatar
Carsten Rose committed
195
            'id' => $this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM),
Carsten  Rose's avatar
Carsten Rose committed
196
            'form' => 'copyForm',
Carsten  Rose's avatar
Carsten Rose committed
197
            'r' => 0,
Carsten  Rose's avatar
Carsten Rose committed
198
199
200
201
202
203
204
205
206
207
208
209
            'idSrc' => $formId,
        ];
        $queryString = Support::arrayToQueryString($queryStringArray);
        $sip = $this->store->getSipInstance();
        $url = $sip->queryStringToSip($queryString);

        $toolTip = "Duplicate form" . PHP_EOL . PHP_EOL . OnArray::toString($queryStringArray, ' = ', PHP_EOL, "'");
        $status = ($formId == 0) ? 'disabled' : '';

        return $this->buildButtonAnchor($url, 'form-view-' . $this->formSpec[F_ID], '', $toolTip, GLYPH_ICON_DUPLICATE, $status, 'btn btn-default navbar-btn');
    }

210
211
212
213
    /**
     * Builds a button to open the formLog. The formLog appears on the same page as the form, but the form is not rendered and the log is shown.
     *
     * @return string
Marc Egger's avatar
Marc Egger committed
214
215
216
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
217
218
     */
    private function buildLogForm() {
219
220
221
222
223

        $pageAlias = $this->store::getVar(TYPO3_PAGE_ALIAS, STORE_TYPO3);

        $baseUrl = 'p:' . $pageAlias . '&form=' . $this->formSpec[F_NAME];
        $baseMisc = '|b|G:glyphicon-bell|s|c:btn btn-default navbar-btn';
Carsten  Rose's avatar
Carsten Rose committed
224
        $baseTooltip = '|o:Set form in debugmode and show actions from ';
225
226
227
228

        $stateAll = ($this->formSpec[FORM_LOG_FILE_ALL] == '') ? '' : ' btn-warning';
        $stateSession = ($this->formSpec[FORM_LOG_FILE_SESSION] == '') ? '' : ' btn-warning';

Carsten  Rose's avatar
Carsten Rose committed
229
230
        $formLogAll = $this->link->renderLink($baseUrl . "&" . FORM_LOG_MODE . "=" . FORM_LOG_ALL . $baseMisc . $stateAll . $baseTooltip . 'all user');
        $formLogSession = $this->link->renderLink($baseUrl . "&" . FORM_LOG_MODE . "=" . FORM_LOG_SESSION . $baseMisc . $stateSession . $baseTooltip . 'current user');
231
232

        return $formLogAll . $formLogSession;
233
234
    }

Carsten  Rose's avatar
Carsten Rose committed
235
236
237
238
    /**
     * Creates a link to open current form loaded in FormEditor
     *
     * @return string - the rendered Checkbox
Marc Egger's avatar
Marc Egger committed
239
240
241
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
Carsten  Rose's avatar
Carsten Rose committed
242
243
244
     */
    private function buildViewForm() {

245
246
247
248
        $form = false;
        $url = '';
        $status = '';

Carsten  Rose's avatar
Carsten Rose committed
249
250
251
252
253
        switch ($this->formSpec[F_NAME]) {
            case 'form':
                $form = $this->store->getVar(F_NAME, STORE_RECORD);
                break;
            case 'formElement':
254
                if (false !== ($formId = $this->store->getVar(FE_FORM_ID, STORE_SIP . STORE_RECORD))) {
255
                    $row = $this->dbArray[$this->dbIndexQfq]->sql("SELECT f.name FROM Form AS f WHERE id=" . $formId, ROW_EXPECT_1);
256
257
                    $form = current($row);
                }
Carsten  Rose's avatar
Carsten Rose committed
258
259
260
261
262
263
                break;
            default:
                return '';
        }

        if ($form === false) {
264
265
266
            $toolTip = "Form not 'form' or 'formElement'";
            $status = 'disabled';
        } else {
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
            $requiredNew = $this->store->getVar(F_REQUIRED_PARAMETER_NEW, STORE_RECORD . STORE_EMPTY);
            if (trim($requiredNew) !== '') {
                $toolTip = "Form has 'required new' parameters and therefore cannot be previewed.";
                $status = 'disabled';
            } else {
                $queryStringArray = [
                    'id' => $this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM),
                    'form' => $form,
                    'r' => 0,
                ];
                $queryString = Support::arrayToQueryString($queryStringArray);
                $sip = $this->store->getSipInstance();
                $url = $sip->queryStringToSip($queryString);

                $toolTip = "View current form with r=0" . PHP_EOL . PHP_EOL . OnArray::toString($queryStringArray, ' = ', PHP_EOL, "'");
            }
Carsten  Rose's avatar
Carsten Rose committed
283
284
        }

285
        return $this->buildButtonAnchor($url, 'form-view-' . $this->formSpec[F_ID], '', $toolTip, GLYPH_ICON_VIEW, $status, 'btn btn-default navbar-btn');
Carsten  Rose's avatar
Carsten Rose committed
286
287
    }

288
    /**
289
     * Build Buttons panel on top right corner of form.
290
     * Simulate Submit Button: http://www.javascript-coder.com/javascript-form/javascript-form-submit.phtml
291
292
     *
     * @return string
Marc Egger's avatar
Marc Egger committed
293
294
295
296
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
297
298
299
     */
    private function buildButtons() {
        $buttonNew = '';
300
301
302
        $buttonDelete = '';
        $buttonClose = '';
        $buttonSave = '';
303
        $buttonDebugForm = '';
Carsten  Rose's avatar
Carsten Rose committed
304
305
306
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);

        // Button: FormEdit
307
        if ($this->showDebugInfoFlag) {
308
            $toolTip = "Edit form" . PHP_EOL . PHP_EOL . OnArray::toString($this->store->getStore(STORE_SIP), ' = ', PHP_EOL, "'");
309
            $url = $this->createFormEditorUrl(FORM_NAME_FORM, $this->formSpec[F_ID]);
310

311
312
            $buttonDebugForm = $this->buildLogForm() .
                $this->buildViewForm() .
Carsten  Rose's avatar
Carsten Rose committed
313
                $this->buildShowEditFormElementCheckbox() .
Carsten  Rose's avatar
Carsten Rose committed
314
                $this->buildButtonCopyForm() .
315
                $this->buildButtonAnchor($url, 'form-edit-button', '', $toolTip, GLYPH_ICON_TOOL, '', 'btn btn-default navbar-btn');
316
317
318
        }

        // Button: Save
Carsten  Rose's avatar
Carsten Rose committed
319
        if (Support::findInSet(FORM_BUTTON_SAVE, $this->formSpec[F_SHOW_BUTTON]) && $this->formSpec[F_SUBMIT_BUTTON_TEXT] === '') {
320
            $toolTip = $this->formSpec[F_SAVE_BUTTON_TOOLTIP];
321
322
323
324
325
326
327
328
329
330
331

            if ($toolTip == 'Save') {

                if ($recordId == 0) {
                    $toolTip .= PHP_EOL . 'Create new record';
                } else {
                    $toolTip .= PHP_EOL . 'Record id: ' . $recordId;
                    $toolTip .= PHP_EOL . 'Created: ' . $this->store->getVar('created', STORE_RECORD . STORE_EMPTY);;
                    $toolTip .= PHP_EOL . 'Modified: ' . $this->store->getVar('modified', STORE_RECORD . STORE_EMPTY);;
                }
            }
332
            // In debugMode every button link should show the information behind the SIP.
333
            if ($this->showDebugInfoFlag) {
334
                $toolTip .= PHP_EOL . "Table: " . $this->formSpec[F_TABLE_NAME];
335
336
            }

337
338
            $buttonSave = $this->buildButtonCode('save-button', $this->formSpec[F_SAVE_BUTTON_TEXT], $toolTip,
                $this->formSpec[F_SAVE_BUTTON_GLYPH_ICON], '', $this->formSpec[F_BUTTON_ON_CHANGE_CLASS], $this->formSpec[F_SAVE_BUTTON_CLASS]);
339
340
341
        }

        // Button: Close
Carsten  Rose's avatar
Carsten Rose committed
342
        if (Support::findInSet(FORM_BUTTON_CLOSE, $this->formSpec[F_SHOW_BUTTON])) {
343
344
345
            $buttonClose = $this->buildButtonCode('close-button', $this->formSpec[F_CLOSE_BUTTON_TEXT],
                $this->formSpec[F_CLOSE_BUTTON_TOOLTIP],
                $this->formSpec[F_CLOSE_BUTTON_GLYPH_ICON], '', '', $this->formSpec[F_CLOSE_BUTTON_CLASS]);
Carsten  Rose's avatar
Carsten Rose committed
346
        }
347

Carsten  Rose's avatar
Carsten Rose committed
348
        // Button: Delete
Carsten  Rose's avatar
Carsten Rose committed
349
        if (Support::findInSet(FORM_BUTTON_DELETE, $this->formSpec[F_SHOW_BUTTON])) {
350
            $toolTip = $this->formSpec[F_DELETE_BUTTON_TOOLTIP];
351

352
            if ($this->showDebugInfoFlag && $recordId > 0) {
353
                $toolTip .= PHP_EOL . "form = '" . $this->formSpec[F_FINAL_DELETE_FORM] . "'" . PHP_EOL . "r = '" . $recordId . "'";
Carsten  Rose's avatar
Carsten Rose committed
354
            }
355
            $disabled = ($recordId > 0) ? '' : 'disabled';
Carsten  Rose's avatar
Carsten Rose committed
356

357
358
            $buttonDelete = $this->buildButtonCode('delete-button', $this->formSpec[F_DELETE_BUTTON_TEXT], $toolTip,
                $this->formSpec[F_DELETE_BUTTON_GLYPH_ICON], $disabled, '', $this->formSpec[F_DELETE_BUTTON_CLASS]);
359
360
        }

Carsten  Rose's avatar
Carsten Rose committed
361
        // Button: New
Carsten  Rose's avatar
Carsten Rose committed
362
        if (Support::findInSet(FORM_BUTTON_NEW, $this->formSpec[F_SHOW_BUTTON])) {
363
            $url = $this->deriveNewRecordUrlFromExistingSip($toolTip);
Carsten  Rose's avatar
Carsten Rose committed
364

365
366
            $buttonNew = $this->buildButtonAnchor($url, 'form-new-button', $this->formSpec[F_NEW_BUTTON_TEXT],
                $this->formSpec[F_NEW_BUTTON_TOOLTIP], $this->formSpec[F_NEW_BUTTON_GLYPH_ICON], '', $this->formSpec[F_NEW_BUTTON_CLASS]);
367
368
        }

369
370
        // Arrangement: Edit Form / Save / Close / Delete / New
        $html = '';
371
372
373
374
375

        $html .= Support::wrapTag('<div class="btn-group" role="group">', $buttonDebugForm, true);
        $html .= Support::wrapTag('<div class="btn-group" role="group">', $buttonSave . $buttonClose, true);
        $html .= Support::wrapTag('<div class="btn-group" role="group">', $buttonDelete, true);
        $html .= Support::wrapTag('<div class="btn-group" role="group">', $buttonNew, true);
376

377
        $html = Support::wrapTag('<div class="btn-toolbar pull-right" role="toolbar">', $html);
378

379
380
381
        return $html;
    }

382
    /**
383
384
     * Generic function to create a button with a given $buttonHtmlId, $url, $title, $icon (=glyph), $disabled
     *
Carsten  Rose's avatar
Carsten Rose committed
385
     * @param string $url
386
387
388
     * @param string $buttonHtmlId
     * @param string $text
     * @param string $toolTip
Carsten  Rose's avatar
Carsten Rose committed
389
     * @param string $icon
390
     * @param string $disabled
391
     * @param string $class
Carsten  Rose's avatar
Carsten Rose committed
392
     *
393
     * @return string
Marc Egger's avatar
Marc Egger committed
394
     * @throws \CodeException
395
     */
396
397
398
399
400
401
402
    private function buildButtonAnchor($url, $buttonHtmlId, $text, $toolTip, $icon, $disabled = '', $class = '') {

        if ($icon === '') {
            $element = $text;
        } else {
            $element = Support::wrapTag("<span " . Support::doAttribute('class', "glyphicon $icon") . ">", $text);
        }
Carsten  Rose's avatar
Carsten Rose committed
403
404

        $attribute = Support::doAttribute('href', $url);
405
406
407
        $attribute .= Support::doAttribute('id', $buttonHtmlId);
        $attribute .= Support::doAttribute('class', "$class $disabled");
        $attribute .= Support::doAttribute('title', $toolTip);
Carsten  Rose's avatar
Carsten Rose committed
408

409
410
        // disabled links do not show tooltips -> use a span
        $wrapTag = $disabled == 'disabled' ? 'span' : 'a';
411
412

        return Support::wrapTag("<$wrapTag $attribute>", $element);
413
414
415
    }

    /**
416
417
     * Creates a button with the given attributes. If there is no $icon given, render the button without glyph.
     *
418
419
420
     * @param string $buttonHtmlId
     * @param string $text
     * @param string $tooltip
421
     * @param string $icon
422
     * @param string $disabled
Carsten  Rose's avatar
Carsten Rose committed
423
     *
424
425
     * @param string $buttonOnChangeClass
     * @param string $class
426
     * @return string
Marc Egger's avatar
Marc Egger committed
427
     * @throws \CodeException
428
     */
429
    private function buildButtonCode($buttonHtmlId, $text, $tooltip, $icon, $disabled = '', $buttonOnChangeClass = '', $class = '') {
430
431

        if ($icon === '') {
432
433
            $element = $text;
        } else {
434
            $element = "<span class='glyphicon $icon'></span>" . ' ' . $text;
435
436
        }

437
        $class = Support::doAttribute('class', $class);
438
        $dataClassOnChange = Support::doAttribute('data-class-on-change', $buttonOnChangeClass);
439
440
441
        $tooltip = Support::doAttribute('title', $tooltip);

        return "<button id='$buttonHtmlId' type='button' $class $dataClassOnChange $tooltip $disabled>$element</button>";
442
443
    }

Carsten  Rose's avatar
Carsten Rose committed
444
    /**
445
446
447
     * Builds the BS-pills on top of a form.
     *
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
448
     * @param array $pillArray
Carsten  Rose's avatar
Carsten Rose committed
449
     *
450
     * @param array $json
Carsten  Rose's avatar
Carsten Rose committed
451
     * @return string
Marc Egger's avatar
Marc Egger committed
452
453
454
455
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
456
     */
457
    private function buildPillNavigation($mode, array $pillArray, array &$json) {
458
459
        $pillButton = '';
        $pillDropdown = '';
460
        $htmlDropdown = '';
461

462
        if ($pillArray == null) {
463
            return '';
464
        }
465
466
467

        $maxVisiblePill = (isset($this->formSpec['maxVisiblePill']) && $this->formSpec['maxVisiblePill'] !== '') ? $this->formSpec['maxVisiblePill'] : 1000;

468
469
        $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM);

470
471
        // Iterate over all 'pill'
        $ii = 0;
472
        $isFirstPill = true;
473
        $recordId = $this->store->getVar(COLUMN_ID, STORE_RECORD . STORE_ZERO);
474
        foreach ($pillArray as $formElement) {
475

476
477
478
479
            if ($mode != FORM_LOAD && $formElement[FE_DYNAMIC_UPDATE] !== 'yes') {
                continue; // During save/update: Process only FE dynamic_update=yes
            }

480
481
            $htmlIdLi = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_PILL_LI;
            $htmlIdLiA = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_PILL_LI_A;
482
483
484
485

            $formElement = $this->evaluate->parseArray($formElement);
            HelperFormElement::explodeParameter($formElement, F_PARAMETER);
            $formElement = HelperFormElement::setLanguage($formElement, $parameterLanguageFieldName);
486
487
488
            if (!empty($formElement[FE_MODE_SQL])) {
                $formElement[FE_MODE] = $formElement[FE_MODE_SQL];
            }
489

490
491
            $ii++;

492
            if ($formElement[FE_NAME] === '' || $formElement[FE_LABEL] === '') {
493
                $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM);
494
                $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'name, label', STORE_SYSTEM);
Marc Egger's avatar
Marc Egger committed
495
                throw new \UserFormException("Field 'name' and/or 'label' are empty", ERROR_NAME_LABEL_EMPTY);
496
497
            }

498
            // Anker for pill navigation
499
500
//            $a = '<a ' . Support::doAttribute('href', '#' . $this->createAnker($formElement[FE_ID])) . ' data-toggle="tab">' . $formElement[FE_LABEL] . '</a>';

501
            $attributeLiA = 'data-toggle="tab" ';
502
503
            $hrefTarget = '#' . $this->createAnker($formElement[FE_ID]);

504
            $htmlFormElementName = HelperFormElement::buildFormElementName($formElement, $recordId);
505
506
507
508
            switch ($formElement[FE_MODE]) {
                case FE_MODE_SHOW:
                case FE_MODE_REQUIRED:
                    $attributeLi = '';
509
510
                    $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLi][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = '';
                    $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLiA][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = '';
511
                    break;
512

513
                case FE_MODE_READONLY:
514
515
                    $hrefTarget = '#';

516
                    $attributeLi = Support::doAttribute('class', 'disabled');
517
                    $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLi][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = 'disabled';
518
519

                    $attributeLiA .= Support::doAttribute('class', 'noclick');
520
                    $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLiA][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = 'noclick';
521
                    break;
522

523
                case FE_MODE_HIDDEN:
524
525
//                    $attributeLi = Support::doAttribute('style', 'display: none');
                    $attributeLi = Support::doAttribute('class', 'hidden');
526
                    $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLi][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = 'hidden';
527
                    break;
528

529
                default:
Marc Egger's avatar
Marc Egger committed
530
                    throw new \UserFormException("Unknown Mode: " . $formElement[FE_MODE], ERROR_UNKNOWN_MODE);
531
            }
532

533
            $attributeLi .= Support::doAttribute(HTML_ATTR_ID, $htmlIdLi);
534
            $attributeLi .= Support::doAttribute('title', $formElement[FE_TOOLTIP]);
535
            $attributeLiA .= Support::doAttribute(HTML_ATTR_ID, $htmlIdLiA);
536
            $a = Support::wrapTag("<a $attributeLiA" . Support::doAttribute('href', $hrefTarget) . ">", $formElement[FE_LABEL]);
537
            $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLiA][API_ELEMENT_CONTENT] = $formElement[FE_LABEL];
538

539
            if ($isFirstPill && $formElement[FE_MODE] != FE_MODE_HIDDEN) {
540
                $attributeLi .= 'class="active" ';
541
542
543
                $isFirstPill = false;
            }

544
            if ($ii <= $maxVisiblePill) {
545
                $pillButton .= '<li role="presentation"' . $attributeLi . ">" . $a . "</li>";
546
            } else {
547
                $pillDropdown .= '<li ' . $attributeLi . '>' . $a . "</li>";
548
549
            }
        }
550

551
552
        // Pill Dropdown necessary?
        if ($ii > $maxVisiblePill) {
553
            $htmlDropdown = Support::wrapTag('<ul class="dropdown-menu qfq-form-pill ' . $this->formSpec[F_CLASS_PILL] . '">', $pillDropdown, true);
554
            $htmlDropdown = '<a class="dropdown-toggle" data-toggle="dropdown" href="#" role="button">more <span class="caret"></span></a>' . $htmlDropdown;
555
            $htmlDropdown = Support::wrapTag('<li role="presentation" class="dropdown">', $htmlDropdown, false);
556
        }
557

558
        $htmlDropdown = Support::wrapTag('<ul id="' . $this->getTabId() . '" class="nav nav-pills qfq-form-pill ' . $this->formSpec[F_CLASS_PILL] . '" role="tablist">', $pillButton . $htmlDropdown);
559
        $htmlDropdown = Support::wrapTag('<div class="col-md-12">', $htmlDropdown);
560

561
        return $htmlDropdown;
562
563
564
565
566
567
    }

    /**
     * Create an identifier for the pill navigation menu
     *
     * @param $id
Carsten  Rose's avatar
Carsten Rose committed
568
     *
569
570
571
     * @return string
     */
    private function createAnker($id) {
572
        return $this->formSpec[FE_NAME] . '_' . $id;
573
574
    }

Carsten  Rose's avatar
Carsten Rose committed
575
576
577
578
579
580
581
    /**
     * @return string
     */
    private function getTabId() {
        return 'qfqTabs';
    }

582
583
584
585
    /**
     * Builds the complete HTML '<form ...>'-tag
     *
     * @return string
Marc Egger's avatar
Marc Egger committed
586
587
588
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
589
     */
590
    public function getFormTag() {
591

592
        $attribute = $this->getFormTagAttributes();
593
594

        $attribute['class'] = 'form-horizontal';
595
        $attribute['data-toggle'] = 'validator';
596
        if (isset($this->formSpec[F_SAVE_BUTTON_ACTIVE]) && $this->formSpec[F_SAVE_BUTTON_ACTIVE] != '0') {
597
598
            $attribute[DATA_ENABLE_SAVE_BUTTON] = 'true';
        }
599
600

        $honeypot = $this->getHoneypotVars();
601
        $md5 = $this->buildInputRecordHashMd5();
602

Carsten  Rose's avatar
Carsten Rose committed
603
        return '<form ' . OnArray::toString($attribute, '=', ' ', "'") . '>' . $honeypot . $md5;
604
605
    }

606
607
    /**
     * @return string
Marc Egger's avatar
Marc Egger committed
608
609
610
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
611
     */
612
    public function tail() {
613

614
        $html = '';
Carsten  Rose's avatar
Carsten Rose committed
615
        $deleteUrl = '';
616

617
618
        $formId = $this->getFormId();

619
620
        // Button Save at bottom of form - only if there is a button text given.
        if ($this->formSpec[F_SUBMIT_BUTTON_TEXT] !== '') {
621
622
623
624

            // Default setzen:
            $this->fillWrapLabelInputNote($this->formSpec[F_BS_LABEL_COLUMNS], $this->formSpec[F_BS_INPUT_COLUMNS], $this->formSpec[F_BS_NOTE_COLUMNS]);

625
626
            $buttonText = $this->formSpec[F_SUBMIT_BUTTON_TEXT];

627
628
629
            $htmlElement = $this->buildButtonCode('save-button', $buttonText, $this->formSpec[F_SUBMIT_BUTTON_TOOLTIP],
                $this->formSpec[F_SUBMIT_BUTTON_GLYPH_ICON], '', $this->formSpec[F_BUTTON_ON_CHANGE_CLASS],
                $this->formSpec[F_SUBMIT_BUTTON_CLASS]);
630
631
632
633
634
635
636
637

            $html .= $this->wrapItem(WRAP_SETUP_LABEL, '');
            $html .= $this->wrapItem(WRAP_SETUP_INPUT, $htmlElement);
            $html .= $this->wrapItem(WRAP_SETUP_NOTE, '');

            $html = $this->wrapItem(WRAP_SETUP_ELEMENT, $html);
        }

638
        $html .= '</div> <!--class="tab-content" -->';  //  <div class="tab-content">
639
//        $html .= '<input type="submit" value="Submit">';
640

Carsten  Rose's avatar
Carsten Rose committed
641
642
643
        $formId = $this->getFormId();
        $tabId = $this->getTabId();

Carsten  Rose's avatar
Carsten Rose committed
644
        if (0 < ($recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP))) {
645
            $deleteUrl = $this->createDeleteUrl($this->formSpec[F_FINAL_DELETE_FORM], $recordId);
Carsten  Rose's avatar
Carsten Rose committed
646
        }
647

648
649
650
        $actionUpload = FILE_ACTION . '=' . FILE_ACTION_UPLOAD;
        $actionDelete = FILE_ACTION . '=' . FILE_ACTION_DELETE;

651
652
653
        $apiDir = API_DIR;
        $apiDeletePhp = API_DIR . '/' . API_DELETE_PHP;

654
        $dirtyAction = ($this->formSpec[F_DIRTY_MODE] == DIRTY_MODE_NONE) ? '' : "dirtyUrl: '$apiDir/dirty.php',";
Carsten  Rose's avatar
Carsten Rose committed
655

656
        $html .= '</form>';  //  <form class="form-horizontal" ...
657
658
659
660
661
662
        $html .= <<<EOF
        <script type="text/javascript">
            $(function () {
                'use strict';
                QfqNS.Log.level = 0;

Carsten  Rose's avatar
Carsten Rose committed
663
664
665
                var qfqPage = new QfqNS.QfqPage({
                    tabsId: '$tabId',
                    formId: '$formId',
666
                    submitTo: '$apiDir/save.php',
Carsten  Rose's avatar
Carsten Rose committed
667
                    $dirtyAction
Carsten  Rose's avatar
Carsten Rose committed
668
                    deleteUrl: '$deleteUrl',
669
670
671
                    refreshUrl: '$apiDir/load.php',
                    fileUploadTo: '$apiDir/file.php?$actionUpload',
                    fileDeleteUrl: '$apiDir/file.php?$actionDelete'
Carsten  Rose's avatar
Carsten Rose committed
672
673
                });

674
               
675
                var qfqRecordList = new QfqNS.QfqRecordList('$apiDeletePhp');
Carsten  Rose's avatar
Carsten Rose committed
676
            })
677
678
         </script>
EOF;
679
        $html .= '</div>';  //  <div class="container-fluid"> === main <div class=...> around everything
680

681
682
683
684
685
        return $html;
    }

    /**
     * @param array $formElement
Carsten  Rose's avatar
Carsten Rose committed
686
687
688
     * @param       $htmlFormElementName
     * @param       $value
     *
689
     * @param array $json
690
     * @return mixed
Marc Egger's avatar
Marc Egger committed
691
692
693
694
695
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
     * @throws \UserFormException
     * @throws \UserReportException
696
697
698
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
699
     */
700
    public function buildPill(array $formElement, $htmlFormElementName, $value, array &$json) {
701
        $html = '';
702

703
        if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
704
705
706
            return '';
        }

707
708
709
710
        // save parent processed FE's
        $tmpStore = $this->feSpecNative;

        // child FE's
711
        $this->feSpecNative = $this->dbArray[$this->dbIndexQfq]->getNativeFormElements(SQL_FORM_ELEMENT_SPECIFIC_CONTAINER,
712
713
            ['yes', $this->formSpec["id"], 'native,container', $formElement['id']], $this->formSpec);

Carsten  Rose's avatar
Carsten Rose committed
714
        $html = $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), FORM_ELEMENTS_NATIVE_SUBRECORD, 0, $json);
715
716
717
718

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

719
720
721
        return $html;
    }

722
723
724
725
726
727
728
729
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
766
767
768
769
    /**
     * Wrap content with $wrapArray or, if specified use $formElement[$wrapName]. Inject $htmlId in wrap.
     *
     * Result:
     * - if $bsColumns==0 and empty $formElement[$wrapName]: no wrap
     * - if $formElement[$wrapName] is given: wrap with that one. Else: wrap with $wrapArray
     * - if $htmlId is give, inject it in $wrap.
     *
     * @param array $formElement Complete FormElement, especially some FE_WRAP
     * @param string $htmlElement Content to wrap.
     * @param string $wrapName FE_WRAP_ROW, FE_WRAP_LABEL, FE_WRAP_INPUT, FE_WRAP_NOTE
     * @param int $bsColumns
     * @param array $wrapArray System wide Defaults: [ 'open wrap', 'close wrap' ]
     * @param string $htmlId
     * @param string $class
     *
     * @return string Wrapped $htmlElement
     * @throws \CodeException
     * @throws \UserFormException
     */
    private function customWrap(array $formElement, $htmlElement, $wrapName, $bsColumns, array $wrapArray, $htmlId = '', $class = '') {

        // If $bsColumns==0: do not wrap with default.
        if ($bsColumns == '0') {
            $wrapArray[0] = '';
            $wrapArray[1] = '';
        }

        // If there is a 'per FormElement'-wrap, take it.
        if (isset($formElement[$wrapName])) {
            $wrapArray = explode('|', $formElement[$wrapName], 2);
        }

        if (count($wrapArray) != 2) {
            throw new \UserFormException("Need open & close wrap token for FormElement.parameter" . $wrapName . " - E.g.: <div ...>|</div>", ERROR_MISSING_VALUE);
        }

        if ($wrapArray[0] != '') {
            $wrapArray[0] = Support::insertAttribute($wrapArray[0], 'id', $htmlId);
            $wrapArray[0] = Support::insertAttribute($wrapArray[0], 'class', $class); // might be problematic, if there is already a 'class' defined.
            if ($wrapName == FE_WRAP_LABEL) {
                $wrapArray[0] = Support::insertAttribute($wrapArray[0], 'style', 'text-align: ' . $formElement[F_FE_LABEL_ALIGN] . ';'); // might be problematic, if there is already a 'class' defined.
            }
        }

        return $wrapArray[0] . $htmlElement . $wrapArray[1];
    }

770
    /**
Carsten  Rose's avatar
Carsten Rose committed
771
     * @param array $formElement Complete FormElement, especially some FE_WRAP
772
     * @param string $htmlElement Content to wrap.
Carsten  Rose's avatar
Carsten Rose committed
773
774
     * @param        $htmlFormElementName
     *
775
     * @return string               Wrapped $htmlElement
Marc Egger's avatar
Marc Egger committed
776
777
     * @throws \CodeException
     * @throws \UserFormException
778
     */
779
    public function buildRowNative(array $formElement, $htmlElement, $htmlFormElementName) {
780
        $html = '';
781
        $htmlLabel = '';
782
783
        $classHideRow = '';
        $classHideElement = '';
784
        $addClassRequired = array();
785

786
        if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
787
            if ($formElement[FE_FLAG_ROW_OPEN_TAG] && $formElement[FE_FLAG_ROW_CLOSE_TAG]) {
788
789
790
791
792
793
                $classHideRow = 'hidden';
            } else {
                $classHideElement = 'hidden';
            }
        }

794
795
796
797
        if ($formElement[FE_MODE] == FE_MODE_REQUIRED || $formElement[FE_MODE] == FE_MODE_SHOW_REQUIRED) {
            $addClassRequired = HelperFormElement::getRequiredPositionClass($formElement[F_FE_REQUIRED_POSITION]);
        }

Carsten  Rose's avatar
Carsten Rose committed
798
        // Label
799
        if ($formElement[FE_BS_LABEL_COLUMNS] != '0') {
800
            $htmlLabel = $this->buildLabel($htmlFormElementName, $formElement[FE_LABEL], $addClassRequired[FE_LABEL] ?? '');
801
        }
802

803
804
        $html .= $this->customWrap($formElement, $htmlLabel, FE_WRAP_LABEL, $formElement[FE_BS_LABEL_COLUMNS],
            [$this->wrap[WRAP_SETUP_LABEL][WRAP_SETUP_START], $this->wrap[WRAP_SETUP_LABEL][WRAP_SETUP_END]], $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_LABEL);
805

Carsten  Rose's avatar
Carsten Rose committed
806
        // Input
807
808
809
        if (!empty($addClassRequired[FE_INPUT])) {
            $htmlElement = Support::wrapTag('<span class="' . $addClassRequired[FE_INPUT] . '">', $htmlElement);
        }
810
811
        $html .= $this->customWrap($formElement, $htmlElement, FE_WRAP_INPUT, $formElement[FE_BS_INPUT_COLUMNS],
            [$this->wrap[WRAP_SETUP_INPUT][WRAP_SETUP_START], $this->wrap[WRAP_SETUP_INPUT][WRAP_SETUP_END]],
812
            $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_INPUT, $classHideElement);
813

814
815
816
817

        if (!empty($addClassRequired[FE_NOTE])) {
            $formElement[FE_NOTE] = Support::wrapTag('<span class="' . $addClassRequired[FE_NOTE] . '">', $formElement[FE_NOTE]);
        }
Carsten  Rose's avatar
Carsten Rose committed
818
        // Note
819
        $html .= $this->customWrap($formElement, $formElement[FE_NOTE], FE_WRAP_NOTE, $formElement[FE_BS_NOTE_COLUMNS],
820
            [$this->wrap[WRAP_SETUP_NOTE][WRAP_SETUP_START], $this->wrap[WRAP_SETUP_NOTE][WRAP_SETUP_END]], $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_NOTE);
821

822
        // Row
823
824
825
826
        $openTag = $formElement[FE_FLAG_ROW_OPEN_TAG] ? $this->getRowOpenTag($classHideRow) : '';
        $closeTag = $formElement[FE_FLAG_ROW_CLOSE_TAG] ? $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_END] : '';

        $html = $this->customWrap($formElement, $html, FE_WRAP_ROW, -1, [$openTag, $closeTag], $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_ROW);
827
828
829
830

        return $html;
    }

831
832
833
    /**
     * @param $formElement
     * @param $elementHtml
Carsten  Rose's avatar
Carsten Rose committed
834
     *
835
836
     * @return string
     */
837
    public function buildRowPill(array $formElement, $elementHtml) {
838
839
        $html = '';

840
        $html .= Support::wrapTag('<div class="col-md-12 qfq-form-body ' . $this->formSpec[F_CLASS_BODY] . '">', $elementHtml);
841

842
843
844
845
        if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
            $class = ' hidden';
        } else {
            $class = $this->isFirstPill ? 'active ' : '';
846
847
            $this->isFirstPill = false;
        }
848

849
        $html = Support::wrapTag('<div role="tabpanel" class="tab-pane ' . $class . '" id="' . $this->createAnker($formElement['id']) . '">', $html);
850
851
852
853
854


        return $html;
    }

855
    /**
856
857
     * Builds a fieldset
     *
Carsten  Rose's avatar
Carsten Rose committed
858
     * @param array $formElement
859
     * @param $elementHtml
Carsten  Rose's avatar
Carsten Rose committed
860
     * @return mixed
861
     */
862
    public function buildRowFieldset(array $formElement, $elementHtml) {
863
864
865
        $html = $elementHtml;

        return $html;
866
867
    }

868
869
870
871
872
    /**
     * Builds a templateGroup
     *
     * @param $formElement
     * @param $elementHtml
873
     * @return mixed
874
875
876
877
878
879
880
     */
    public function buildRowTemplateGroup(array $formElement, $elementHtml) {
        $html = $elementHtml;

        return $html;
    }

881
    /**
882
     * @param array $formElement
883
     * @param $elementHtml
Carsten  Rose's avatar
Carsten Rose committed
884
     *
885
     * @return string
Marc Egger's avatar
Marc Egger committed
886
     * @throws \CodeException
887
     */
888
    public function buildRowSubrecord(array $formElement, $elementHtml) {
889

890
        $formElement[FE_LABEL] = Support::wrapTag("<label class='control-label'>", $formElement[FE_LABEL], true);
891
        $html = $this->wrapItem(WRAP_SETUP_ELEMENT, $this->