BuildFormBootstrap.php 36.9 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
600
601
        $formModeGlobal = $this->store::getVar(F_MODE_GLOBAL, STORE_SIP . STORE_EMPTY);
        if ($formModeGlobal == '') {
            $formModeGlobal = $this->formSpec[F_MODE_GLOBAL] ?? '';
        }

602
        if ($formModeGlobal == F_MODE_REQUIRED_OFF_BUT_MARK || $formModeGlobal == F_MODE_REQUIRED_OFF) {
603
            $attribute[DATA_REQUIRED_OFF_BUT_MARK] = 'true';
604
        }
605

606
        if (isset($this->formSpec[F_SAVE_BUTTON_ACTIVE]) && $this->formSpec[F_SAVE_BUTTON_ACTIVE] != '0') {
607
608
            $attribute[DATA_ENABLE_SAVE_BUTTON] = 'true';
        }
609
610

        $honeypot = $this->getHoneypotVars();
611
        $md5 = $this->buildInputRecordHashMd5();
612

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

616
617
    /**
     * @return string
Marc Egger's avatar
Marc Egger committed
618
619
620
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
621
     */
622
    public function tail() {
623

624
        $html = '';
Carsten  Rose's avatar
Carsten Rose committed
625
        $deleteUrl = '';
626

627
628
        $formId = $this->getFormId();

629
630
        // Button Save at bottom of form - only if there is a button text given.
        if ($this->formSpec[F_SUBMIT_BUTTON_TEXT] !== '') {
631
632
633
634

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

635
636
            $buttonText = $this->formSpec[F_SUBMIT_BUTTON_TEXT];

637
638
639
            $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]);
640
641
642
643
644
645
646
647

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

648
        $html .= '</div> <!--class="tab-content" -->';  //  <div class="tab-content">
649
//        $html .= '<input type="submit" value="Submit">';
650

Carsten  Rose's avatar
Carsten Rose committed
651
652
653
        $formId = $this->getFormId();
        $tabId = $this->getTabId();

Carsten  Rose's avatar
Carsten Rose committed
654
        if (0 < ($recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP))) {
655
            $deleteUrl = $this->createDeleteUrl($this->formSpec[F_FINAL_DELETE_FORM], $recordId);
Carsten  Rose's avatar
Carsten Rose committed
656
        }
657

658
659
660
        $actionUpload = FILE_ACTION . '=' . FILE_ACTION_UPLOAD;
        $actionDelete = FILE_ACTION . '=' . FILE_ACTION_DELETE;

661
662
663
        $apiDir = API_DIR;
        $apiDeletePhp = API_DIR . '/' . API_DELETE_PHP;

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

666
        $html .= '</form>';  //  <form class="form-horizontal" ...
667
668
669
670
671
672
        $html .= <<<EOF
        <script type="text/javascript">
            $(function () {
                'use strict';
                QfqNS.Log.level = 0;

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

684
               
685
                var qfqRecordList = new QfqNS.QfqRecordList('$apiDeletePhp');
Carsten  Rose's avatar
Carsten Rose committed
686
            })
687
688
         </script>
EOF;
689
        $html .= '</div>';  //  <div class="container-fluid"> === main <div class=...> around everything
690

691
692
693
694
695
        return $html;
    }

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

716
        if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
717
718
719
            return '';
        }

720
721
722
723
        // save parent processed FE's
        $tmpStore = $this->feSpecNative;

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

Carsten  Rose's avatar
Carsten Rose committed
727
        $html = $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), FORM_ELEMENTS_NATIVE_SUBRECORD, 0, $json);
728
729
730
731

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

732
733
734
        return $html;
    }

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
778
779
780
781
782
    /**
     * 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];
    }

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

799
        if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
800
            if ($formElement[FE_FLAG_ROW_OPEN_TAG] && $formElement[FE_FLAG_ROW_CLOSE_TAG]) {
801
802
803
804
805
806
                $classHideRow = 'hidden';
            } else {
                $classHideElement = 'hidden';
            }
        }

807
        if ($formElement[FE_MODE] == FE_MODE_REQUIRED || $formElement[FE_MODE] == FE_MODE_SHOW_REQUIRED) {
808
809
810
            $addClassRequired = HelperFormElement::getRequiredPositionClass($formElement[F_FE_REQUIRED_POSITION]);
        }

Carsten  Rose's avatar
Carsten Rose committed
811
        // Label
812
        if ($formElement[FE_BS_LABEL_COLUMNS] != '0') {
813
            $htmlLabel = $this->buildLabel($htmlFormElementName, $formElement[FE_LABEL], $addClassRequired[FE_LABEL] ?? '');
814
        }
815

816
817
        $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);
818

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

827
828
829
830

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

835
        // Row
836
837
838
839
        $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);
840
841
842
843

        return $html;
    }

844
845
846
    /**
     * @param $formElement
     * @param $elementHtml
Carsten  Rose's avatar
Carsten Rose committed
847
     *
848
849
     * @return string
     */
850
    public function buildRowPill(array $formElement, $elementHtml) {
851
852
        $html = '';

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

855
856
857
858
        if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
            $class = ' hidden';
        } else {
            $class = $this->isFirstPill ? 'active ' : '';
859
860
            $this->isFirstPill = false;
        }
861

862
        $html = Support::wrapTag('<div role="tabpanel" class="tab-pane ' . $class . '" id="' . $this->createAnker($formElement['id']) . '">', $html);
863
864
865
866
867


        return $html;
    }

868
    /**
869
870
     * Builds a fieldset
     *
Carsten  Rose's avatar
Carsten Rose committed
871
     * @param array $formElement
872
     * @param $elementHtml
Carsten  Rose's avatar
Carsten Rose committed
873
     * @return mixed
874
     */
875
    public function buildRowFieldset(array $formElement, $elementHtml) {
876
877
878
        $html = $elementHtml;

        return $html;
879
880
    }

881
882
883
884
885
    /**
     * Builds a templateGroup
     *
     * @param $formElement
     * @param $elementHtml
886
     * @return mixed
887
888
889
890
891
892
893
     */
    public function buildRowTemplateGroup(array $formElement, $elementHtml) {
        $html = $elementHtml;

        return $html;
    }

894
    /**
895