BuildFormBootstrap.php 36.7 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

597
598
599
        $flag = $this->store::getVar(F_MODE_GLOBAL, STORE_SIP);
        if ($flag == F_MODE_SKIP_REQUIRED_CHECK || $flag == F_MODE_REQUIRED_OFF) {
            $attribute[DATA_SKIP_REQUIRED_CHECK] = 'true';
600
        }
601
        if (isset($this->formSpec[F_SAVE_BUTTON_ACTIVE]) && $this->formSpec[F_SAVE_BUTTON_ACTIVE] != '0') {
602
603
            $attribute[DATA_ENABLE_SAVE_BUTTON] = 'true';
        }
604
605

        $honeypot = $this->getHoneypotVars();
606
        $md5 = $this->buildInputRecordHashMd5();
607

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

611
612
    /**
     * @return string
Marc Egger's avatar
Marc Egger committed
613
614
615
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
616
     */
617
    public function tail() {
618

619
        $html = '';
Carsten  Rose's avatar
Carsten Rose committed
620
        $deleteUrl = '';
621

622
623
        $formId = $this->getFormId();

624
625
        // Button Save at bottom of form - only if there is a button text given.
        if ($this->formSpec[F_SUBMIT_BUTTON_TEXT] !== '') {
626
627
628
629

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

630
631
            $buttonText = $this->formSpec[F_SUBMIT_BUTTON_TEXT];

632
633
634
            $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]);
635
636
637
638
639
640
641
642

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

643
        $html .= '</div> <!--class="tab-content" -->';  //  <div class="tab-content">
644
//        $html .= '<input type="submit" value="Submit">';
645

Carsten  Rose's avatar
Carsten Rose committed
646
647
648
        $formId = $this->getFormId();
        $tabId = $this->getTabId();

Carsten  Rose's avatar
Carsten Rose committed
649
        if (0 < ($recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP))) {
650
            $deleteUrl = $this->createDeleteUrl($this->formSpec[F_FINAL_DELETE_FORM], $recordId);
Carsten  Rose's avatar
Carsten Rose committed
651
        }
652

653
654
655
        $actionUpload = FILE_ACTION . '=' . FILE_ACTION_UPLOAD;
        $actionDelete = FILE_ACTION . '=' . FILE_ACTION_DELETE;

656
657
658
        $apiDir = API_DIR;
        $apiDeletePhp = API_DIR . '/' . API_DELETE_PHP;

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

661
        $html .= '</form>';  //  <form class="form-horizontal" ...
662
663
664
665
666
667
        $html .= <<<EOF
        <script type="text/javascript">
            $(function () {
                'use strict';
                QfqNS.Log.level = 0;

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

679
               
680
                var qfqRecordList = new QfqNS.QfqRecordList('$apiDeletePhp');
Carsten  Rose's avatar
Carsten Rose committed
681
            })
682
683
         </script>
EOF;
684
        $html .= '</div>';  //  <div class="container-fluid"> === main <div class=...> around everything
685

686
687
688
689
690
        return $html;
    }

    /**
     * @param array $formElement
Carsten  Rose's avatar
Carsten Rose committed
691
692
693
     * @param       $htmlFormElementName
     * @param       $value
     *
694
     * @param array $json
695
     * @return mixed
Marc Egger's avatar
Marc Egger committed
696
697
698
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
699
700
701
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
Carsten  Rose's avatar
Carsten Rose committed
702
703
704
705
706
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
707
     */
708
    public function buildPill(array $formElement, $htmlFormElementName, $value, array &$json) {
709
        $html = '';
710

711
        if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
712
713
714
            return '';
        }

715
716
717
718
        // save parent processed FE's
        $tmpStore = $this->feSpecNative;

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

Carsten  Rose's avatar
Carsten Rose committed
722
        $html = $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), FORM_ELEMENTS_NATIVE_SUBRECORD, 0, $json);
723
724
725
726

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

727
728
729
        return $html;
    }

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
770
771
772
773
774
775
776
777
    /**
     * 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];
    }

778
    /**
Carsten  Rose's avatar
Carsten Rose committed
779
     * @param array $formElement Complete FormElement, especially some FE_WRAP
780
     * @param string $htmlElement Content to wrap.
Carsten  Rose's avatar
Carsten Rose committed
781
782
     * @param        $htmlFormElementName
     *
783
     * @return string               Wrapped $htmlElement
Marc Egger's avatar
Marc Egger committed
784
785
     * @throws \CodeException
     * @throws \UserFormException
786
     */
787
    public function buildRowNative(array $formElement, $htmlElement, $htmlFormElementName) {
788
        $html = '';
789
        $htmlLabel = '';
790
791
        $classHideRow = '';
        $classHideElement = '';
792
        $addClassRequired = array();
793

794
        if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
795
            if ($formElement[FE_FLAG_ROW_OPEN_TAG] && $formElement[FE_FLAG_ROW_CLOSE_TAG]) {
796
797
798
799
800
801
                $classHideRow = 'hidden';
            } else {
                $classHideElement = 'hidden';
            }
        }

802
        if ($formElement[FE_MODE] == FE_MODE_REQUIRED) {
803
804
805
            $addClassRequired = HelperFormElement::getRequiredPositionClass($formElement[F_FE_REQUIRED_POSITION]);
        }

Carsten  Rose's avatar
Carsten Rose committed
806
        // Label
807
        if ($formElement[FE_BS_LABEL_COLUMNS] != '0') {
808
            $htmlLabel = $this->buildLabel($htmlFormElementName, $formElement[FE_LABEL], $addClassRequired[FE_LABEL] ?? '');
809
        }
810

811
812
        $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);
813

Carsten  Rose's avatar
Carsten Rose committed
814
        // Input
815
816
817
        if (!empty($addClassRequired[FE_INPUT])) {
            $htmlElement = Support::wrapTag('<span class="' . $addClassRequired[FE_INPUT] . '">', $htmlElement);
        }
818
819
        $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]],
820
            $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_INPUT, $classHideElement);
821

822
823
824
825

        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
826
        // Note
827
        $html .= $this->customWrap($formElement, $formElement[FE_NOTE], FE_WRAP_NOTE, $formElement[FE_BS_NOTE_COLUMNS],
828
            [$this->wrap[WRAP_SETUP_NOTE][WRAP_SETUP_START], $this->wrap[WRAP_SETUP_NOTE][WRAP_SETUP_END]], $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_NOTE);
829

830
        // Row
831
832
833
834
        $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);
835
836
837
838

        return $html;
    }

839
840
841
    /**
     * @param $formElement
     * @param $elementHtml
Carsten  Rose's avatar
Carsten Rose committed
842
     *
843
844
     * @return string
     */
845
    public function buildRowPill(array $formElement, $elementHtml) {
846
847
        $html = '';

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

850
851
852
853
        if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
            $class = ' hidden';
        } else {
            $class = $this->isFirstPill ? 'active ' : '';
854
855
            $this->isFirstPill = false;
        }
856

857
        $html = Support::wrapTag('<div role="tabpanel" class="tab-pane ' . $class . '" id="' . $this->createAnker($formElement['id']) . '">', $html);
858
859
860
861
862


        return $html;
    }

863
    /**
864
865
     * Builds a fieldset
     *
Carsten  Rose's avatar
Carsten Rose committed
866
     * @param array $formElement
867
     * @param $elementHtml
Carsten  Rose's avatar
Carsten Rose committed
868
     * @return mixed
869
     */
870
    public function buildRowFieldset(array $formElement, $elementHtml) {
871
872
873
        $html = $elementHtml;

        return $html;
874
875
    }

876
877
878
879
880
    /**
     * Builds a templateGroup
     *
     * @param $formElement
     * @param $elementHtml
881
     * @return mixed
882
883
884
885
886
887
888
     */
    public function buildRowTemplateGroup(array $formElement, $elementHtml) {
        $html = $elementHtml;

        return $html;
    }

889
    /**
890
     * @param array $formElement
891
     * @param $elementHtml
Carsten  Rose's avatar
Carsten Rose committed
892
     *
893
     * @return string