BuildFormBootstrap.php 36.3 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
152
153
        $class = ['tab-content', $this->formSpec[F_CLASS_BODY]];
        if ($pill == '') {
            $class[] = 'col-md-12';
        }
        $html .= "<div " . Support::doAttribute('class', $class) . ">";
154
155
156
157

        return $html;
    }

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

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

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

205
206
207
208
    /**
     * 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
209
210
211
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
212
213
     */
    private function buildLogForm() {
214
215
216
217
218

        $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
219
        $baseTooltip = '|o:Set form in debugmode and show actions from ';
220
221
222
223

        $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
224
225
        $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');
226
227

        return $formLogAll . $formLogSession;
228
229
    }

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

240
241
242
243
        $form = false;
        $url = '';
        $status = '';

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

        if ($form === false) {
259
260
261
            $toolTip = "Form not 'form' or 'formElement'";
            $status = 'disabled';
        } else {
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
            $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
278
279
        }

280
        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
281
282
    }

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

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

306
307
            $buttonDebugForm = $this->buildLogForm() .
                $this->buildViewForm() .
Carsten  Rose's avatar
Carsten Rose committed
308
                $this->buildShowEditFormElementCheckbox() .
Carsten  Rose's avatar
Carsten Rose committed
309
                $this->buildButtonCopyForm() .
310
                $this->buildButtonAnchor($url, 'form-edit-button', '', $toolTip, GLYPH_ICON_TOOL, '', 'btn btn-default navbar-btn');
311
312
313
        }

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

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

332
333
            $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]);
334
335
336
        }

        // Button: Close
Carsten  Rose's avatar
Carsten Rose committed
337
        if (Support::findInSet(FORM_BUTTON_CLOSE, $this->formSpec[F_SHOW_BUTTON])) {
338
339
340
            $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
341
        }
342

Carsten  Rose's avatar
Carsten Rose committed
343
        // Button: Delete
Carsten  Rose's avatar
Carsten Rose committed
344
        if (Support::findInSet(FORM_BUTTON_DELETE, $this->formSpec[F_SHOW_BUTTON])) {
345
            $toolTip = $this->formSpec[F_DELETE_BUTTON_TOOLTIP];
346

347
            if ($this->showDebugInfoFlag && $recordId > 0) {
348
                $toolTip .= PHP_EOL . "form = '" . $this->formSpec[F_FINAL_DELETE_FORM] . "'" . PHP_EOL . "r = '" . $recordId . "'";
Carsten  Rose's avatar
Carsten Rose committed
349
            }
350
            $disabled = ($recordId > 0) ? '' : 'disabled';
Carsten  Rose's avatar
Carsten Rose committed
351

352
353
            $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]);
354
355
        }

Carsten  Rose's avatar
Carsten Rose committed
356
        // Button: New
Carsten  Rose's avatar
Carsten Rose committed
357
        if (Support::findInSet(FORM_BUTTON_NEW, $this->formSpec[F_SHOW_BUTTON])) {
358
            $url = $this->deriveNewRecordUrlFromExistingSip($toolTip);
Carsten  Rose's avatar
Carsten Rose committed
359

360
361
            $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]);
362
363
        }

364
365
        // Arrangement: Edit Form / Save / Close / Delete / New
        $html = '';
366
367
368
369
370

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

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

374
375
376
        return $html;
    }

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

        $attribute = Support::doAttribute('href', $url);
400
401
402
        $attribute .= Support::doAttribute('id', $buttonHtmlId);
        $attribute .= Support::doAttribute('class', "$class $disabled");
        $attribute .= Support::doAttribute('title', $toolTip);
Carsten  Rose's avatar
Carsten Rose committed
403

404
405
        // disabled links do not show tooltips -> use a span
        $wrapTag = $disabled == 'disabled' ? 'span' : 'a';
406
407

        return Support::wrapTag("<$wrapTag $attribute>", $element);
408
409
410
    }

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

        if ($icon === '') {
427
428
            $element = $text;
        } else {
429
            $element = "<span class='glyphicon $icon'></span>" . ' ' . $text;
430
431
        }

432
        $class = Support::doAttribute('class', $class);
433
        $dataClassOnChange = Support::doAttribute('data-class-on-change', $buttonOnChangeClass);
434
435
436
        $tooltip = Support::doAttribute('title', $tooltip);

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

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

457
        if ($pillArray == null) {
458
            return '';
459
        }
460
461
462

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

463
464
        $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM);

465
466
        // Iterate over all 'pill'
        $ii = 0;
467
        $isFirstPill = true;
468
        $recordId = $this->store->getVar(COLUMN_ID, STORE_RECORD . STORE_ZERO);
469
        foreach ($pillArray as $formElement) {
470

471
472
473
474
            if ($mode != FORM_LOAD && $formElement[FE_DYNAMIC_UPDATE] !== 'yes') {
                continue; // During save/update: Process only FE dynamic_update=yes
            }

475
476
            $htmlIdLi = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_PILL_LI;
            $htmlIdLiA = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_PILL_LI_A;
477
478
479
480

            $formElement = $this->evaluate->parseArray($formElement);
            HelperFormElement::explodeParameter($formElement, F_PARAMETER);
            $formElement = HelperFormElement::setLanguage($formElement, $parameterLanguageFieldName);
481
482
483
            if (!empty($formElement[FE_MODE_SQL])) {
                $formElement[FE_MODE] = $formElement[FE_MODE_SQL];
            }
484

485
486
            $ii++;

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

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

496
            $attributeLiA = 'data-toggle="tab" ';
497
498
            $hrefTarget = '#' . $this->createAnker($formElement[FE_ID]);

499
            $htmlFormElementName = HelperFormElement::buildFormElementName($formElement, $recordId);
500
501
502
503
            switch ($formElement[FE_MODE]) {
                case FE_MODE_SHOW:
                case FE_MODE_REQUIRED:
                    $attributeLi = '';
504
505
                    $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLi][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = '';
                    $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLiA][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = '';
506
                    break;
507

508
                case FE_MODE_READONLY:
509
510
                    $hrefTarget = '#';

511
                    $attributeLi = Support::doAttribute('class', 'disabled');
512
                    $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLi][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = 'disabled';
513
514

                    $attributeLiA .= Support::doAttribute('class', 'noclick');
515
                    $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLiA][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = 'noclick';
516
                    break;
517

518
                case FE_MODE_HIDDEN:
519
520
//                    $attributeLi = Support::doAttribute('style', 'display: none');
                    $attributeLi = Support::doAttribute('class', 'hidden');
521
                    $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLi][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = 'hidden';
522
                    break;
523

524
                default:
Marc Egger's avatar
Marc Egger committed
525
                    throw new \UserFormException("Unknown Mode: " . $formElement[FE_MODE], ERROR_UNKNOWN_MODE);
526
            }
527

528
            $attributeLi .= Support::doAttribute(HTML_ATTR_ID, $htmlIdLi);
529
            $attributeLi .= Support::doAttribute('title', $formElement[FE_TOOLTIP]);
530
            $attributeLiA .= Support::doAttribute(HTML_ATTR_ID, $htmlIdLiA);
531
            $a = Support::wrapTag("<a $attributeLiA" . Support::doAttribute('href', $hrefTarget) . ">", $formElement[FE_LABEL]);
532
            $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLiA][API_ELEMENT_CONTENT] = $formElement[FE_LABEL];
533

534
            if ($isFirstPill && $formElement[FE_MODE] != FE_MODE_HIDDEN) {
535
                $attributeLi .= 'class="active" ';
536
537
538
                $isFirstPill = false;
            }

539
            if ($ii <= $maxVisiblePill) {
540
                $pillButton .= '<li role="presentation"' . $attributeLi . ">" . $a . "</li>";
541
            } else {
542
                $pillDropdown .= '<li ' . $attributeLi . '>' . $a . "</li>";
543
544
            }
        }
545

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

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

556
        return $htmlDropdown;
557
558
559
560
561
562
    }

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

Carsten  Rose's avatar
Carsten Rose committed
570
571
572
573
574
575
576
    /**
     * @return string
     */
    private function getTabId() {
        return 'qfqTabs';
    }

577
578
579
580
    /**
     * Builds the complete HTML '<form ...>'-tag
     *
     * @return string
Marc Egger's avatar
Marc Egger committed
581
582
583
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
584
     */
585
    public function getFormTag() {
586

587
        $attribute = $this->getFormTagAttributes();
588
589

        $attribute['class'] = 'form-horizontal';
590
        $attribute['data-toggle'] = 'validator';
591
        if (isset($this->formSpec[F_SAVE_BUTTON_ACTIVE]) && $this->formSpec[F_SAVE_BUTTON_ACTIVE] != '0') {
592
593
            $attribute[DATA_ENABLE_SAVE_BUTTON] = 'true';
        }
594
595

        $honeypot = $this->getHoneypotVars();
596
        $md5 = $this->buildInputRecordHashMd5();
597

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

601
602
    /**
     * @return string
Marc Egger's avatar
Marc Egger committed
603
604
605
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
606
     */
607
    public function tail() {
608

609
        $html = '';
Carsten  Rose's avatar
Carsten Rose committed
610
        $deleteUrl = '';
611

612
613
        $formId = $this->getFormId();

614
615
        // Button Save at bottom of form - only if there is a button text given.
        if ($this->formSpec[F_SUBMIT_BUTTON_TEXT] !== '') {
616
617
618
619

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

620
621
            $buttonText = $this->formSpec[F_SUBMIT_BUTTON_TEXT];

622
623
624
            $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]);
625
626
627
628
629
630
631
632

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

633
        $html .= '</div> <!--class="tab-content" -->';  //  <div class="tab-content">
634
//        $html .= '<input type="submit" value="Submit">';
635

Carsten  Rose's avatar
Carsten Rose committed
636
637
638
        $formId = $this->getFormId();
        $tabId = $this->getTabId();

Carsten  Rose's avatar
Carsten Rose committed
639
        if (0 < ($recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP))) {
640
            $deleteUrl = $this->createDeleteUrl($this->formSpec[F_FINAL_DELETE_FORM], $recordId);
Carsten  Rose's avatar
Carsten Rose committed
641
        }
642

643
644
645
        $actionUpload = FILE_ACTION . '=' . FILE_ACTION_UPLOAD;
        $actionDelete = FILE_ACTION . '=' . FILE_ACTION_DELETE;

646
647
648
        $apiDir = API_DIR;
        $apiDeletePhp = API_DIR . '/' . API_DELETE_PHP;

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

651
        $html .= '</form>';  //  <form class="form-horizontal" ...
652
653
654
655
656
657
        $html .= <<<EOF
        <script type="text/javascript">
            $(function () {
                'use strict';
                QfqNS.Log.level = 0;

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

669
               
670
                var qfqRecordList = new QfqNS.QfqRecordList('$apiDeletePhp');
Carsten  Rose's avatar
Carsten Rose committed
671
            })
672
673
         </script>
EOF;
674
        $html .= '</div>';  //  <div class="container-fluid"> === main <div class=...> around everything
675

676
677
678
679
680
        return $html;
    }

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

698
        if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
699
700
701
            return '';
        }

702
703
704
705
        // save parent processed FE's
        $tmpStore = $this->feSpecNative;

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

Carsten  Rose's avatar
Carsten Rose committed
709
        $html = $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), FORM_ELEMENTS_NATIVE_SUBRECORD, 0, $json);
710
711
712
713

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

714
715
716
        return $html;
    }

717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
    /**
     * 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];
    }

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

781
        if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
782
            if ($formElement[FE_FLAG_ROW_OPEN_TAG] && $formElement[FE_FLAG_ROW_CLOSE_TAG]) {
783
784
785
786
787
788
                $classHideRow = 'hidden';
            } else {
                $classHideElement = 'hidden';
            }
        }

789
790
791
792
        if ($formElement[FE_MODE] == FE_MODE_REQUIRED || $formElement[FE_MODE] == FE_MODE_SHOW_REQUIRED) {
            $addClassRequired = HelperFormElement::getRequiredPositionClass($formElement[F_FE_REQUIRED_POSITION]);
        }

Carsten  Rose's avatar
Carsten Rose committed
793
        // Label
794
        if ($formElement[FE_BS_LABEL_COLUMNS] != '0') {
795
            $htmlLabel = $this->buildLabel($htmlFormElementName, $formElement[FE_LABEL], $addClassRequired[FE_LABEL] ?? '');
796
        }
797

798
799
        $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);
800

Carsten  Rose's avatar
Carsten Rose committed
801
        // Input
802
803
804
        if (!empty($addClassRequired[FE_INPUT])) {
            $htmlElement = Support::wrapTag('<span class="' . $addClassRequired[FE_INPUT] . '">', $htmlElement);
        }
805
806
        $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]],
807
            $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_INPUT, $classHideElement);
808

809
810
811
812

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

817
        // Row
818
819
820
821
        $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);
822
823
824
825

        return $html;
    }

826
827
828
    /**
     * @param $formElement
     * @param $elementHtml
Carsten  Rose's avatar
Carsten Rose committed
829
     *
830
831
     * @return string
     */
832
    public function buildRowPill(array $formElement, $elementHtml) {
833
834
        $html = '';

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

837
838
839
840
        if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
            $class = ' hidden';
        } else {
            $class = $this->isFirstPill ? 'active ' : '';
841
842
            $this->isFirstPill = false;
        }
843

844
        $html = Support::wrapTag('<div role="tabpanel" class="tab-pane ' . $class . '" id="' . $this->createAnker($formElement['id']) . '">', $html);
845
846
847
848
849


        return $html;
    }

850
    /**
851
852
     * Builds a fieldset
     *
Carsten  Rose's avatar
Carsten Rose committed
853
     * @param array $formElement
854
     * @param $elementHtml
Carsten  Rose's avatar
Carsten Rose committed
855
     * @return mixed
856
     */
857
    public function buildRowFieldset(array $formElement, $elementHtml) {
858
859
860
        $html = $elementHtml;

        return $html;
861
862
    }

863
864
865
866
867
    /**
     * Builds a templateGroup
     *
     * @param $formElement
     * @param $elementHtml
868
     * @return mixed
869
870
871
872
873
874
875
     */
    public function buildRowTemplateGroup(array $formElement, $elementHtml) {
        $html = $elementHtml;

        return $html;
    }

876
    /**
877
     * @param array $formElement
878
     * @param $elementHtml
Carsten  Rose's avatar
Carsten Rose committed
879
     *
880
     * @return string
Marc Egger's avatar
Marc Egger committed
881
     * @throws \CodeException
882
     */
883
    public function buildRowSubrecord(array $formElement, $elementHtml) {
884

885
        $formElement[FE_LABEL] = Support::wrapTag("<label class='control-label'>", $formElement[FE_LABEL], true);
886
        $html = $this->wrapItem(WRAP_SETUP_ELEMENT, $this->wrapItem(WRAP_SETUP_SUBRECORD, $formElement[FE_LABEL]));
887
        $html .= $this->wrapItem(WRAP_SETUP_ELEMENT, $this->wrapItem(WRAP_SETUP_SUBRECORD, $elementHtml));
888
        $html .= $this->wrapItem(WRAP_SETUP_ELEMENT, $this->wrapItem(WRAP_SETUP_SUBRECORD, $formElement[FE_NOTE