BuildFormBootstrap.php 37.5 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
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\OnArray;
14
use IMATHUZH\Qfq\Core\Helper\Path;
Marc Egger's avatar
Marc Egger committed
15
use IMATHUZH\Qfq\Core\Helper\Support;
16

17

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

24
25
26
27
28
29
    private $isFirstPill;

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

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

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

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

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

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

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

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

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

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

        return "<div $class>";

    }
81

82
    /**
83
84
85
     * 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')
     *
86
87
88
89
90
     * @param $label
     * @param $input
     * @param $note
     */
    public function fillWrapLabelInputNote($label, $input, $note) {
91
92
93

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

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

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

    }

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

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

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

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

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

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

        $html .= $button . $title;
143

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

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

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

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

        return $html;
    }

164
165
166
167
168
169
    /**
     * 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
170
        // EditFormElement Icons
171
172
        $js = '$(".' . CLASS_FORM_ELEMENT_EDIT . '").toggleClass("hidden")';
        $element = "<input type='checkbox' onchange='" . $js . "'>" .
173
            Support::wrapTag("<span title='Toggle: Edit form element icons' class='" . GLYPH_ICON . ' ' . GLYPH_ICON_TASKS . "'>", '');
174
        $element = Support::wrapTag('<label class="btn btn-default navbar-btn">', $element);
Carsten  Rose's avatar
Carsten Rose committed
175

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

Carsten  Rose's avatar
Carsten Rose committed
179
180
181
182
    /**
     * Creates a button to open 'CopyForm' with the current form as source.
     *
     * @return string - the rendered button
Marc Egger's avatar
Marc Egger committed
183
184
     * @throws \CodeException
     * @throws \UserFormException
Carsten  Rose's avatar
Carsten Rose committed
185
186
187
188
189
190
191
192
193
194
195
     */
    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
196
            'id' => $this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM),
Carsten  Rose's avatar
Carsten Rose committed
197
            'form' => 'copyForm',
Carsten  Rose's avatar
Carsten Rose committed
198
            'r' => 0,
Carsten  Rose's avatar
Carsten Rose committed
199
200
201
202
203
204
205
206
207
208
209
210
            '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');
    }

211
212
213
214
    /**
     * 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
215
216
217
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
218
219
     */
    private function buildLogForm() {
220
221
222
223
224

        $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
225
        $baseTooltip = '|o:Set form in debugmode and show actions from ';
226
227
228
229

        $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
230
231
        $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');
232
233

        return $formLogAll . $formLogSession;
234
235
    }

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

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

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

        if ($form === false) {
265
266
267
            $toolTip = "Form not 'form' or 'formElement'";
            $status = 'disabled';
        } else {
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
            $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
284
285
        }

286
        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
287
288
    }

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

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

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

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

            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);;
                }
            }
333
            // In debugMode every button link should show the information behind the SIP.
334
            if ($this->showDebugInfoFlag) {
335
                $toolTip .= PHP_EOL . "Table: " . $this->formSpec[F_TABLE_NAME];
336
337
            }

338
339
            $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]);
340
341
342
        }

        // Button: Close
Carsten  Rose's avatar
Carsten Rose committed
343
        if (Support::findInSet(FORM_BUTTON_CLOSE, $this->formSpec[F_SHOW_BUTTON])) {
344
345
346
            $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
347
        }
348

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

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

358
359
            $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]);
360
361
        }

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

366
367
            $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]);
368
369
        }

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

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

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

380
381
382
        return $html;
    }

383
    /**
384
385
     * Generic function to create a button with a given $buttonHtmlId, $url, $title, $icon (=glyph), $disabled
     *
Carsten  Rose's avatar
Carsten Rose committed
386
     * @param string $url
387
388
389
     * @param string $buttonHtmlId
     * @param string $text
     * @param string $toolTip
Carsten  Rose's avatar
Carsten Rose committed
390
     * @param string $icon
391
     * @param string $disabled
392
     * @param string $class
Carsten  Rose's avatar
Carsten Rose committed
393
     *
394
     * @return string
Marc Egger's avatar
Marc Egger committed
395
     * @throws \CodeException
396
     */
397
398
399
400
401
402
403
    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
404
405

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

491
492
            $ii++;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

594
        $attribute = $this->getFormTagAttributes();
595
596

        $attribute['class'] = 'form-horizontal';
597
        $attribute['data-toggle'] = 'validator';
598

599
        $formModeGlobal = Support::getFormModeGlobal($this->formSpec[F_MODE_GLOBAL] ?? '');
600

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

660
        $apiDeletePhp = Path::urlApi(API_DELETE_PHP);
661

662
        $dirtyAction = ($this->formSpec[F_DIRTY_MODE] == DIRTY_MODE_NONE) ? '' : "dirtyUrl: '" . Path::urlApi(API_DIRTY_PHP) . "',";
663

664
665
666
667
        $submitTo = Path::urlApi(API_SAVE_PHP);
        $refreshUrl = Path::urlApi(API_LOAD_PHP);
        $fileUploadTo = Path::urlApi(API_FILE_PHP)  . '?'  . $actionUpload;
        $fileDeleteUrl = Path::urlApi(API_FILE_PHP)  . '?' . $actionDelete;
Carsten  Rose's avatar
Carsten Rose committed
668

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

Carsten  Rose's avatar
Carsten Rose committed
676
677
678
                var qfqPage = new QfqNS.QfqPage({
                    tabsId: '$tabId',
                    formId: '$formId',
679
                    submitTo: '$submitTo',
Carsten  Rose's avatar
Carsten Rose committed
680
                    $dirtyAction
Carsten  Rose's avatar
Carsten Rose committed
681
                    deleteUrl: '$deleteUrl',
682
683
684
                    refreshUrl: '$refreshUrl',
                    fileUploadTo: '$fileUploadTo',
                    fileDeleteUrl: '$fileDeleteUrl'
Carsten  Rose's avatar
Carsten Rose committed
685
686
                });

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

694
695
696
697
698
        return $html;
    }

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

719
        if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
720
721
722
            return '';
        }

723
724
725
726
        // save parent processed FE's
        $tmpStore = $this->feSpecNative;

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

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

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

735
736
737
        return $html;
    }

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
    /**
     * 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.
            }
Carsten  Rose's avatar
Carsten Rose committed
781
782
783
784
785
            // Insert 'required="required"' for checkboxes and radios
            if ($wrapName == FE_WRAP_INPUT && $formElement[FE_MODE] == FE_MODE_REQUIRED &&
                ($formElement[FE_TYPE] == FE_TYPE_CHECKBOX || $formElement[FE_TYPE] == FE_TYPE_RADIO)) {
                $wrapArray[0] = Support::insertAttribute($wrapArray[0], 'required', 'required'); // might be problematic, if there is already a 'class' defined.
            }
786
787
788
789
790
        }

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

791
    /**
Carsten  Rose's avatar
Carsten Rose committed
792
     * @param array $formElement Complete FormElement, especially some FE_WRAP
793
     * @param string $htmlElement Content to wrap.
Carsten  Rose's avatar
Carsten Rose committed
794
795
     * @param        $htmlFormElementName
     *
796
     * @return string               Wrapped $htmlElement
Marc Egger's avatar
Marc Egger committed
797
798
     * @throws \CodeException
     * @throws \UserFormException
799
     */
800
    public function buildRowNative(array $formElement, $htmlElement, $htmlFormElementName) {
801
        $html = '';
802
        $htmlLabel = '';
803
804
        $classHideRow = '';
        $classHideElement = '';
805
        $addClassRequired = array();
806

807
        if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
808
            if ($formElement[FE_FLAG_ROW_OPEN_TAG] && $formElement[FE_FLAG_ROW_CLOSE_TAG]) {
809
810
811
812
813
814
                $classHideRow = 'hidden';
            } else {
                $classHideElement = 'hidden';
            }
        }

815
        if ($formElement[FE_MODE] == FE_MODE_REQUIRED || $formElement[FE_MODE] == FE_MODE_SHOW_REQUIRED) {
816
817
818
            $addClassRequired = HelperFormElement::getRequiredPositionClass($formElement[F_FE_REQUIRED_POSITION]);
        }

Carsten  Rose's avatar
Carsten Rose committed
819
        // Label
820
        if ($formElement[FE_BS_LABEL_COLUMNS] != '0') {
821
            $htmlLabel = HelperFormElement::buildLabel($htmlFormElementName, $formElement[FE_LABEL], $addClassRequired[FE_LABEL] ?? '');
822
        }
823

824
825
        $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);
826

Carsten  Rose's avatar
Carsten Rose committed
827
        // Input
828
829
830
        if (!empty($addClassRequired[FE_INPUT])) {
            $htmlElement = Support::wrapTag('<span class="' . $addClassRequired[FE_INPUT] . '">', $htmlElement);
        }
831
832
        $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]],
833
            $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_INPUT, $classHideElement);
834

835
836
837
838

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

843
        // Row
844
845
846
847
        $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);
848
849
850
851

        return $html;
    }

852
853
854
    /**
     * @param $formElement
     * @param $elementHtml
Carsten  Rose's avatar
Carsten Rose committed
855
     *
856
857
     * @return string
     */
858
    public function buildRowPill(array $formElement, $elementHtml) {
859
860
        $html = '';

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

863
864
865
866
        if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
            $class = ' hidden';
        } else {
            $class = $this->isFirstPill ? 'active ' : '';
867
868
            $this->isFirstPill = false;
        }
869

870
        $html = Support::wrapTag('<div role="tabpanel" class="tab-pane ' . $class . '" id="' . $this->createAnker($formElement['id']) . '">', $html);
871
872
873
874
875


        return $html;
    }

876
    /**
877
878
     * Builds a fieldset
     *
Carsten  Rose's avatar
Carsten Rose committed
879
     * @param array $formElement
880
     * @param $elementHtml
Carsten  Rose's avatar
Carsten Rose committed
881
     * @return mixed
882
     */