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

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

Marc Egger's avatar
Marc Egger committed
11
12
13
14
15
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
 
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
    /**
Carsten  Rose's avatar
Carsten Rose committed
718
     * @param array $formElement Complete FormElement, especially some FE_WRAP
719
     * @param string $htmlElement Content to wrap.
Carsten  Rose's avatar
Carsten Rose committed
720
721
     * @param        $htmlFormElementName
     *
722
     * @return string               Wrapped $htmlElement
Marc Egger's avatar
Marc Egger committed
723
724
     * @throws \CodeException
     * @throws \UserFormException
725
     */
726
    public function buildRowNative(array $formElement, $htmlElement, $htmlFormElementName) {
727
        $html = '';
728
        $htmlLabel = '';
729
730
        $classHideRow = '';
        $classHideElement = '';
731

732
        if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
733
            if ($formElement[FE_FLAG_ROW_OPEN_TAG] && $formElement[FE_FLAG_ROW_CLOSE_TAG]) {
734
735
736
737
738
739
                $classHideRow = 'hidden';
            } else {
                $classHideElement = 'hidden';
            }
        }

Carsten  Rose's avatar
Carsten Rose committed
740
        // Label
741
        if ($formElement[FE_BS_LABEL_COLUMNS] != '0') {
742
            $addClass = ($formElement[FE_MODE] == FE_MODE_REQUIRED || $formElement[FE_MODE] == FE_MODE_SHOW_REQUIRED) ? CSS_REQUIRED : '';
743
            $htmlLabel = $this->buildLabel($htmlFormElementName, $formElement[FE_LABEL], $addClass);
744
        }
745

746
747
        $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);
748

Carsten  Rose's avatar
Carsten Rose committed
749
        // Input
750
751
        $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]],
752
            $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_INPUT, $classHideElement);
753

Carsten  Rose's avatar
Carsten Rose committed
754
        // Note
Carsten  Rose's avatar
Carsten Rose committed
755
        $note = $formElement[FE_NOTE];
756
757
        $html .= $this->customWrap($formElement, $note, FE_WRAP_NOTE, $formElement[FE_BS_NOTE_COLUMNS],
            [$this->wrap[WRAP_SETUP_NOTE][WRAP_SETUP_START], $this->wrap[WRAP_SETUP_NOTE][WRAP_SETUP_END]], $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_NOTE);
758

759
        // Row
760
761
762
763
        $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);
764
765
766
767

        return $html;
    }

768
    /**
769
     * Wrap content with $wrapArray or, if specified use $formElement[$wrapName]. Inject $htmlId in wrap.
770
771
772
     *
     * Result:
     * - if $bsColumns==0 and empty $formElement[$wrapName]: no wrap
773
774
     * - if $formElement[$wrapName] is given: wrap with that one. Else: wrap with $wrapArray
     * - if $htmlId is give, inject it in $wrap.
775
     *
Carsten  Rose's avatar
Carsten Rose committed
776
     * @param array $formElement Complete FormElement, especially some FE_WRAP
777
     * @param string $htmlElement Content to wrap.
Carsten  Rose's avatar
Carsten Rose committed
778
779
     * @param string $wrapName FE_WRAP_ROW, FE_WRAP_LABEL, FE_WRAP_INPUT, FE_WRAP_NOTE
     * @param int $bsColumns
780
     * @param array $wrapArray System wide Defaults: [ 'open wrap', 'close wrap' ]
781
782
     * @param string $htmlId
     * @param string $class
Carsten  Rose's avatar
Carsten Rose committed
783
     *
784
     * @return string Wrapped $htmlElement
Marc Egger's avatar
Marc Egger committed
785
786
     * @throws \CodeException
     * @throws \UserFormException
787
     */
788
    private function customWrap(array $formElement, $htmlElement, $wrapName, $bsColumns, array $wrapArray, $htmlId = '', $class = '') {
789

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

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

801
        if (count($wrapArray) != 2) {
Marc Egger's avatar
Marc Egger committed
802
            throw new \UserFormException("Need open & close wrap token for FormElement.parameter" . $wrapName . " - E.g.: <div ...>|</div>", ERROR_MISSING_VALUE);
803
804
805
806
807
        }

        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.
808
            if ($wrapName == FE_WRAP_LABEL) {
809
                $wrapArray[0] = Support::insertAttribute($wrapArray[0], 'style', 'text-align: ' . $formElement[F_FE_LABEL_ALIGN] . ';'); // might be problematic, if there is already a 'class' defined.
810
            }
811
812
        }

813
        return $wrapArray[0] . $htmlElement . $wrapArray[1];
814
815
    }

816

817
818
819
    /**
     * @param $formElement
     * @param $elementHtml
Carsten  Rose's avatar
Carsten Rose committed
820
     *
821
822
     * @return string
     */
823
    public function buildRowPill(array $formElement, $elementHtml) {
824
825
        $html = '';

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

828
829
830
831
        if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
            $class = ' hidden';
        } else {
            $class = $this->isFirstPill ? 'active ' : '';
832
833
            $this->isFirstPill = false;
        }
834

835
        $html = Support::wrapTag('<div role="tabpanel" class="tab-pane ' . $class . '" id="' . $this->createAnker($formElement['id']) . '">', $html);
836
837
838
839
840


        return $html;
    }

841
    /**
842
843
     * Builds a fieldset
     *
Carsten  Rose's avatar
Carsten Rose committed
844
     * @param array $formElement
845
     * @param $elementHtml
Carsten  Rose's avatar
Carsten Rose committed
846
     * @return mixed
847
     */
848
    public function buildRowFieldset(array $formElement, $elementHtml) {