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

namespace qfq;

use qfq;

require_once(__DIR__ . '/../qfq/Constants.php');
14
require_once(__DIR__ . '/../qfq/helper/OnArray.php');
15
require_once(__DIR__ . '/../qfq/AbstractBuildForm.php');
16
require_once(__DIR__ . '/../qfq/exceptions/UserFormException.php');
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
31
     */
32
33
    public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative, array $db) {
        parent::__construct($formSpec, $feSpecAction, $feSpecNative, $db);
34
35
36
        $this->isFirstPill = true;
    }

Carsten  Rose's avatar
Carsten Rose committed
37
38
39
    /**
     *
     */
40
    public function fillWrap() {
41
42
43
44

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

45
46
        $this->wrap[WRAP_SETUP_TITLE][WRAP_SETUP_START] = "<div class='row hidden-xs'><div class='col-md-12'><h1>";
        $this->wrap[WRAP_SETUP_TITLE][WRAP_SETUP_END] = "</h1></div></div>";
47

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

53
54
        $this->wrap[WRAP_SETUP_SUBRECORD][WRAP_SETUP_START] = "<div class='col-md-12'>";
        $this->wrap[WRAP_SETUP_SUBRECORD][WRAP_SETUP_END] = "</div>";
55

56
57
        $this->wrap[WRAP_SETUP_IN_FIELDSET][WRAP_SETUP_START] = "";
        $this->wrap[WRAP_SETUP_IN_FIELDSET][WRAP_SETUP_END] = "";
58

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

62
63
//        $this->feDivClass['radio'] = 'radio';
//        $this->feDivClass['checkbox'] = 'checkbox';
64
65
    }

66
67
    /**
     * @param string $addClass
Carsten  Rose's avatar
Carsten Rose committed
68
     *
69
70
71
72
73
74
75
76
     * @return string
     */
    public function getRowOpenTag($addClass = '') {
        $class = Support::doAttribute('class', [$this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_CLASS], $addClass]);

        return "<div $class>";

    }
77

78
79
80
81
82
83
    /**
     * @param $label
     * @param $input
     * @param $note
     */
    public function fillWrapLabelInputNote($label, $input, $note) {
84
        $this->wrap[WRAP_SETUP_LABEL][WRAP_SETUP_START] = "<div class='col-md-$label qfq-label'>";
85
86
87
        $this->wrap[WRAP_SETUP_LABEL][WRAP_SETUP_END] = "</div>";
        $this->wrap[WRAP_SETUP_INPUT][WRAP_SETUP_START] = "<div class='col-md-$input'>";
        $this->wrap[WRAP_SETUP_INPUT][WRAP_SETUP_END] = "</div>";
Carsten  Rose's avatar
Carsten Rose committed
88
        $this->wrap[WRAP_SETUP_NOTE][WRAP_SETUP_START] = "<div class='col-md-$note qfq-note'>";
89
90
91
92
        $this->wrap[WRAP_SETUP_NOTE][WRAP_SETUP_END] = "</div>";

    }

Carsten  Rose's avatar
Carsten Rose committed
93
94
95
    /**
     * @return string
     */
96
97
98
99
    public function getProcessFilter() {
        return FORM_ELEMENTS_NATIVE_SUBRECORD;
    }

Carsten  Rose's avatar
Carsten Rose committed
100
101
102
    /**
     * @return string
     */
103
104
105
106
107
108
109
    public function doSubrecords() {
        return '';
    }

    /**
     * @return string
     */
110
    public function head($mode = FORM_LOAD) {
111
        $html = '';
112

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

115
        //TODO: nicer error reporting - make test with 'unknown index' here - unset($this->formSpec['title']) - See #3424
116
        $title = Support::wrapTag('<div class="hidden-xs col-sm-6 col-md-8">', Support::wrapTag('<h3>', $this->formSpec[F_TITLE]));
117
118
        $button = Support::wrapTag('<div class="col-xs-12 col-sm-6 col-md-4">', $this->buildButtons());
        $html .= Support::wrapTag('<div class="row">', $title . $button);
119

120
        $dummy = array();
121
        $pill = $this->buildPillNavigation($mode, OnArray::filter($this->feSpecNative, FE_TYPE, FE_TYPE_PILL), $dummy);
122
        $html .= Support::wrapTag('<div class="row">', $pill);
123

124
125
        $html .= $this->getFormTag();

126
127
128
129
130
        $class = ['tab-content', $this->formSpec[F_CLASS_BODY]];
        if ($pill == '') {
            $class[] = 'col-md-12';
        }
        $html .= "<div " . Support::doAttribute('class', $class) . ">";
131
132
133
134

        return $html;
    }

135
136
137
138
139
140
    /**
     * 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
141
        // EditFormElement Icons
142
143
        $js = '$(".' . CLASS_FORM_ELEMENT_EDIT . '").toggleClass("hidden")';
        $element = "<input type='checkbox' onchange='" . $js . "'>" .
144
            Support::wrapTag("<span title='Toggle: Edit form element icons' class='" . GLYPH_ICON . ' ' . GLYPH_ICON_TASKS . "'>", '');
145
        $element = Support::wrapTag('<label class="btn btn-default navbar-btn">', $element);
Carsten  Rose's avatar
Carsten Rose committed
146

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

Carsten  Rose's avatar
Carsten Rose committed
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
    /**
     * Creates a button to open 'CopyForm' with the current form as source.
     *
     * @return string - the rendered button
     */
    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
165
            'id' => $this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM),
Carsten  Rose's avatar
Carsten Rose committed
166
            'form' => 'copyForm',
Carsten  Rose's avatar
Carsten Rose committed
167
            'r' => 0,
Carsten  Rose's avatar
Carsten Rose committed
168
169
170
171
172
173
174
175
176
177
178
179
            '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');
    }

Carsten  Rose's avatar
Carsten Rose committed
180
181
182
183
184
185
186
    /**
     * Creates a link to open current form loaded in FormEditor
     *
     * @return string - the rendered Checkbox
     */
    private function buildViewForm() {

187
188
189
190
        $form = false;
        $url = '';
        $status = '';

Carsten  Rose's avatar
Carsten Rose committed
191
192
193
194
195
        switch ($this->formSpec[F_NAME]) {
            case 'form':
                $form = $this->store->getVar(F_NAME, STORE_RECORD);
                break;
            case 'formElement':
196
                if (false !== ($formId = $this->store->getVar(FE_FORM_ID, STORE_SIP . STORE_RECORD))) {
197
                    $row = $this->dbArray[$this->dbIndexQfq]->sql("SELECT f.name FROM Form AS f WHERE id=" . $formId, ROW_EXPECT_1);
198
199
                    $form = current($row);
                }
Carsten  Rose's avatar
Carsten Rose committed
200
201
202
203
204
205
                break;
            default:
                return '';
        }

        if ($form === false) {
206
207
208
209
210
211
212
            $toolTip = "Form not 'form' or 'formElement'";
            $status = 'disabled';
        } else {

            $queryStringArray = [
                'id' => $this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM),
                'form' => $form,
Carsten  Rose's avatar
Carsten Rose committed
213
                'r' => 0,
214
215
216
217
218
219
            ];
            $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
220
221
        }

222
        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
223
224
    }

225
    /**
226
     * Build Buttons panel on top right corner of form.
227
     * Simulate Submit Button: http://www.javascript-coder.com/javascript-form/javascript-form-submit.phtml
228
229
230
231
232
     *
     * @return string
     */
    private function buildButtons() {
        $buttonNew = '';
233
234
235
236
        $buttonDelete = '';
        $buttonClose = '';
        $buttonSave = '';
        $buttonEditForm = '';
Carsten  Rose's avatar
Carsten Rose committed
237
238
239
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);

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

Carsten  Rose's avatar
Carsten Rose committed
244
245
            $buttonEditForm = $this->buildViewForm() .
                $this->buildShowEditFormElementCheckbox() .
Carsten  Rose's avatar
Carsten Rose committed
246
                $this->buildButtonCopyForm() .
247
                $this->buildButtonAnchor($url, 'form-edit-button', '', $toolTip, GLYPH_ICON_TOOL, '', 'btn btn-default navbar-btn');
248
249
250
        }

        // Button: Save
Carsten  Rose's avatar
Carsten Rose committed
251
        if (Support::findInSet(FORM_BUTTON_SAVE, $this->formSpec[F_SHOW_BUTTON]) && $this->formSpec[F_SUBMIT_BUTTON_TEXT] === '') {
252
253
            $toolTip = $this->formSpec[F_SAVE_BUTTON_TOOLTIP];
            // In debugMode every button link should show the information behind the SIP.
254
            if ($this->showDebugInfoFlag) {
255
                $toolTip .= PHP_EOL . "table = '" . $this->formSpec[F_TABLE_NAME] . "'" . PHP_EOL . "r = '" . $recordId . "'";
256
257
            }

258
259
            $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]);
260
261
262
        }

        // Button: Close
Carsten  Rose's avatar
Carsten Rose committed
263
        if (Support::findInSet(FORM_BUTTON_CLOSE, $this->formSpec[F_SHOW_BUTTON])) {
264
265
266
            $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
267
        }
268

Carsten  Rose's avatar
Carsten Rose committed
269
        // Button: Delete
Carsten  Rose's avatar
Carsten Rose committed
270
        if (Support::findInSet(FORM_BUTTON_DELETE, $this->formSpec[F_SHOW_BUTTON])) {
271
            $toolTip = $this->formSpec[F_DELETE_BUTTON_TOOLTIP];
272

273
            if ($this->showDebugInfoFlag && $recordId > 0) {
274
                $toolTip .= PHP_EOL . "form = '" . $this->formSpec[F_FINAL_DELETE_FORM] . "'" . PHP_EOL . "r = '" . $recordId . "'";
Carsten  Rose's avatar
Carsten Rose committed
275
            }
276
            $disabled = ($recordId > 0) ? '' : 'disabled';
Carsten  Rose's avatar
Carsten Rose committed
277

278
279
            $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]);
280
281
        }

Carsten  Rose's avatar
Carsten Rose committed
282
        // Button: New
Carsten  Rose's avatar
Carsten Rose committed
283
        if (Support::findInSet(FORM_BUTTON_NEW, $this->formSpec[F_SHOW_BUTTON])) {
284
            $url = $this->deriveNewRecordUrlFromExistingSip($toolTip);
Carsten  Rose's avatar
Carsten Rose committed
285

286
287
            $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]);
288
289
        }

290
        // Arrangement: Edit Form / Save / Close / Delete / New
Carsten  Rose's avatar
Carsten Rose committed
291
        // Specified in reverse order cause 'pull-right' inverts the order. http://getbootstrap.com/css/#helper-classes-floats
292
293
294
295
296
        $html = '';
        $html .= Support::wrapTag('<div class="btn-group pull-right" role="group">', $buttonNew);
        $html .= Support::wrapTag('<div class="btn-group pull-right" role="group">', $buttonDelete);
        $html .= Support::wrapTag('<div class="btn-group pull-right" role="group">', $buttonSave . $buttonClose);
        $html .= Support::wrapTag('<div class="btn-group pull-right" role="group">', $buttonEditForm);
297
298
299

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

300
301
302
        return $html;
    }

303
    /**
304
305
     * Generic function to create a button with a given $buttonHtmlId, $url, $title, $icon (=glyph), $disabled
     *
Carsten  Rose's avatar
Carsten Rose committed
306
     * @param string $url
307
308
309
     * @param string $buttonHtmlId
     * @param string $text
     * @param string $toolTip
Carsten  Rose's avatar
Carsten Rose committed
310
     * @param string $icon
311
     * @param string $disabled
312
     * @param string $class
Carsten  Rose's avatar
Carsten Rose committed
313
     *
314
315
     * @return string
     */
316
317
318
319
320
321
322
    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
323
324

        $attribute = Support::doAttribute('href', $url);
325
326
327
        $attribute .= Support::doAttribute('id', $buttonHtmlId);
        $attribute .= Support::doAttribute('class', "$class $disabled");
        $attribute .= Support::doAttribute('title', $toolTip);
Carsten  Rose's avatar
Carsten Rose committed
328

329
        return Support::wrapTag("<a $attribute>", $element);
330
331
332
    }

    /**
333
334
     * Creates a button with the given attributes. If there is no $icon given, render the button without glyph.
     *
335
336
337
     * @param string $buttonHtmlId
     * @param string $text
     * @param string $tooltip
338
     * @param string $icon
339
     * @param string $disabled
Carsten  Rose's avatar
Carsten Rose committed
340
     *
341
342
     * @return string
     */
343
    private function buildButtonCode($buttonHtmlId, $text, $tooltip, $icon, $disabled = '', $buttonOnChangeClass = '', $class = '') {
344
345

        if ($icon === '') {
346
347
            $element = $text;
        } else {
348
            $element = "<span class='glyphicon $icon'></span>" . ' ' . $text;
349
350
        }

351
        $class = Support::doAttribute('class', $class);
352
        $dataClassOnChange = Support::doAttribute('data-class-on-change', $buttonOnChangeClass);
353
354
355
        $tooltip = Support::doAttribute('title', $tooltip);

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

Carsten  Rose's avatar
Carsten Rose committed
358
    /**
359
360
361
     * Builds the BS-pills on top of a form.
     *
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
362
     * @param array $pillArray
Carsten  Rose's avatar
Carsten Rose committed
363
     *
364
     * @param array $json
Carsten  Rose's avatar
Carsten Rose committed
365
     * @return string
366
     * @throws CodeException
367
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
368
     */
369
    private function buildPillNavigation($mode = FORM_LOAD, array $pillArray, array &$json) {
370
371
        $pillButton = '';
        $pillDropdown = '';
372
        $htmlDropdown = '';
373

374
        if ($pillArray == null) {
375
            return '';
376
        }
377
378
379

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

380
381
        $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM);

382
383
384
        // Iterate over all 'pill'
        $ii = 0;
        $active = 'class="active"';
385
        $recordId = $this->store->getVar(COLUMN_ID, STORE_RECORD . STORE_ZERO);
386
        foreach ($pillArray as $formElement) {
387

388
389
390
391
            if ($mode != FORM_LOAD && $formElement[FE_DYNAMIC_UPDATE] !== 'yes') {
                continue; // During save/update: Process only FE dynamic_update=yes
            }

392
393
            $htmlIdLi = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_PILL_LI;
            $htmlIdLiA = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_PILL_LI_A;
394
395
396
397

            $formElement = $this->evaluate->parseArray($formElement);
            HelperFormElement::explodeParameter($formElement, F_PARAMETER);
            $formElement = HelperFormElement::setLanguage($formElement, $parameterLanguageFieldName);
398
399
400
            if (!empty($formElement[FE_MODE_SQL])) {
                $formElement[FE_MODE] = $formElement[FE_MODE_SQL];
            }
401

402
403
            $ii++;

404
            if ($formElement[FE_NAME] === '' || $formElement[FE_LABEL] === '') {
405
                $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM);
406
                $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'name, label', STORE_SYSTEM);
407
                throw new UserFormException("Field 'name' and/or 'label' are empty", ERROR_NAME_LABEL_EMPTY);
408
409
            }

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

413
            $attributeLiA = 'data-toggle="tab" ';
414
415
            $hrefTarget = '#' . $this->createAnker($formElement[FE_ID]);

416
            $htmlFormElementName = HelperFormElement::buildFormElementName($formElement, $recordId);
417
418
419
420
            switch ($formElement[FE_MODE]) {
                case FE_MODE_SHOW:
                case FE_MODE_REQUIRED:
                    $attributeLi = '';
421
422
                $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLi][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = '';
                $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLiA][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = '';
423
                    break;
424

425
                case FE_MODE_READONLY:
426
427
                    $hrefTarget = '#';

428
                    $attributeLi = Support::doAttribute('class', 'disabled');
429
                    $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLi][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = 'disabled';
430
431

                    $attributeLiA .= Support::doAttribute('class', 'noclick');
432
                    $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLiA][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = 'noclick';
433
                    break;
434

435
                case FE_MODE_HIDDEN:
436
437
//                    $attributeLi = Support::doAttribute('style', 'display: none');
                    $attributeLi = Support::doAttribute('class', 'hidden');
438
                    $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLi][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = 'hidden';
439
440
                    $a = '';
                    break;
441

442
443
444
                default:
                    throw new UserFormException("Unknown Mode: " . $formElement[FE_MODE], ERROR_UNKNOWN_MODE);
            }
445

446
447
            $attributeLi .= Support::doAttribute(HTML_ATTR_ID, $htmlIdLi);
            $attributeLiA .= Support::doAttribute(HTML_ATTR_ID, $htmlIdLiA);
448
            $a = Support::wrapTag("<a $attributeLiA" . Support::doAttribute('href', $hrefTarget) . ">", $formElement[FE_LABEL]);
449
            $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLiA][API_ELEMENT_CONTENT] = $formElement[FE_LABEL];
450

451
            if ($ii <= $maxVisiblePill) {
452
                $pillButton .= '<li role="presentation"' . $attributeLi . $active . ">" . $a . "</li>";
453
            } else {
454
                $pillDropdown .= '<li ' . $attributeLi . '>' . $a . "</li>";
455
            }
456

457
458
            $active = '';
        }
459

460
461
        // Pill Dropdown necessary?
        if ($ii > $maxVisiblePill) {
462
            $htmlDropdown = Support::wrapTag('<ul class="dropdown-menu qfq-form-pill ' . $this->formSpec[F_CLASS_PILL] . '">', $pillDropdown, true);
463
            $htmlDropdown = '<a class="dropdown-toggle" data-toggle="dropdown" href="#" role="button">more <span class="caret"></span></a>' . $htmlDropdown;
464
            $htmlDropdown = Support::wrapTag('<li role="presentation" class="dropdown">', $htmlDropdown, false);
465
        }
466

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

470
        return $htmlDropdown;
471
472
473
474
475
476
    }

    /**
     * Create an identifier for the pill navigation menu
     *
     * @param $id
Carsten  Rose's avatar
Carsten Rose committed
477
     *
478
479
480
     * @return string
     */
    private function createAnker($id) {
481
        return $this->formSpec[FE_NAME] . '_' . $id;
482
483
    }

Carsten  Rose's avatar
Carsten Rose committed
484
485
486
487
488
489
490
    /**
     * @return string
     */
    private function getTabId() {
        return 'qfqTabs';
    }

491
492
493
494
495
    /**
     * Builds the complete HTML '<form ...>'-tag
     *
     * @return string
     */
496
    public function getFormTag() {
497

498
        $attribute = $this->getFormTagAttributes();
499
500

        $attribute['class'] = 'form-horizontal';
501
        $attribute['data-toggle'] = 'validator';
502
503
504
        if (isset($this->formSpec[F_SAVE_BUTTON_ACTIVE])) {
            $attribute[DATA_ENABLE_SAVE_BUTTON] = 'true';
        }
505
506

        $honeypot = $this->getHoneypotVars();
507
        $md5 = $this->buildInputRecordHashMd5();
508

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

512
513
514
    /**
     * @return string
     */
515
    public function tail() {
516

517
        $html = '';
Carsten  Rose's avatar
Carsten Rose committed
518
        $deleteUrl = '';
519

520
521
        $formId = $this->getFormId();

522
523
        // Button Save at bottom of form - only if there is a button text given.
        if ($this->formSpec[F_SUBMIT_BUTTON_TEXT] !== '') {
524
525
526
527

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

528
529
            $buttonText = $this->formSpec[F_SUBMIT_BUTTON_TEXT];

530
            $htmlElement = $this->buildButtonCode('save-button', $buttonText, $buttonText, '', '', $this->formSpec[F_BUTTON_ON_CHANGE_CLASS], 'btn btn-default');
531
532
533
534
535
536
537
538

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

539
        $html .= '</div> <!--class="tab-content" -->';  //  <div class="tab-content">
540
//        $html .= '<input type="submit" value="Submit">';
541

Carsten  Rose's avatar
Carsten Rose committed
542
543
544
        $formId = $this->getFormId();
        $tabId = $this->getTabId();

Carsten  Rose's avatar
Carsten Rose committed
545
        if (0 < ($recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP))) {
546
            $deleteUrl = $this->createDeleteUrl($this->formSpec[F_FINAL_DELETE_FORM], $recordId);
Carsten  Rose's avatar
Carsten Rose committed
547
        }
548

549
550
551
        $actionUpload = FILE_ACTION . '=' . FILE_ACTION_UPLOAD;
        $actionDelete = FILE_ACTION . '=' . FILE_ACTION_DELETE;

552
553
554
        $apiDir = API_DIR;
        $apiDeletePhp = API_DIR . '/' . API_DELETE_PHP;

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

557
        $html .= '</form>';  //  <form class="form-horizontal" ...
558
559
560
561
562
563
        $html .= <<<EOF
        <script type="text/javascript">
            $(function () {
                'use strict';
                QfqNS.Log.level = 0;

Carsten  Rose's avatar
Carsten Rose committed
564
565
566
                var qfqPage = new QfqNS.QfqPage({
                    tabsId: '$tabId',
                    formId: '$formId',
567
                    submitTo: '$apiDir/save.php',
Carsten  Rose's avatar
Carsten Rose committed
568
                    $dirtyAction
Carsten  Rose's avatar
Carsten Rose committed
569
                    deleteUrl: '$deleteUrl',
570
571
572
                    refreshUrl: '$apiDir/load.php',
                    fileUploadTo: '$apiDir/file.php?$actionUpload',
                    fileDeleteUrl: '$apiDir/file.php?$actionDelete'
Carsten  Rose's avatar
Carsten Rose committed
573
574
                });

575
                var qfqRecordList = new QfqNS.QfqRecordList('$apiDeletePhp');
Carsten  Rose's avatar
Carsten Rose committed
576
            })
577
578
         </script>
EOF;
579
        $html .= '</div>';  //  <div class="container-fluid"> === main <div class=...> around everything
580

581
582
583
584
585
        return $html;
    }

    /**
     * @param array $formElement
Carsten  Rose's avatar
Carsten Rose committed
586
587
588
     * @param       $htmlFormElementName
     * @param       $value
     *
589
590
     * @return mixed
     */
591
    public function buildPill(array $formElement, $htmlFormElementName, $value, array &$json) {
592
        $html = '';
593
594
595
596
        // save parent processed FE's
        $tmpStore = $this->feSpecNative;

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

Carsten  Rose's avatar
Carsten Rose committed
600
        $html = $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), FORM_ELEMENTS_NATIVE_SUBRECORD, 0, $json);
601
602
603
604

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

605
606
607
        return $html;
    }

608
    /**
Carsten  Rose's avatar
Carsten Rose committed
609
     * @param array $formElement Complete FormElement, especially some FE_WRAP
610
     * @param string $htmlElement Content to wrap.
Carsten  Rose's avatar
Carsten Rose committed
611
612
     * @param        $htmlFormElementName
     *
613
     * @return string               Wrapped $htmlElement
614
     * @throws \qfq\UserFormException
615
     */
616
    public function buildRowNative(array $formElement, $htmlElement, $htmlFormElementName) {
617
        $html = '';
618
        $htmlLabel = '';
619
620
        $classHideRow = '';
        $classHideElement = '';
621

622
        if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
623
            if ($formElement[FE_FLAG_ROW_OPEN_TAG] && $formElement[FE_FLAG_ROW_CLOSE_TAG]) {
624
625
626
627
628
629
                $classHideRow = 'hidden';
            } else {
                $classHideElement = 'hidden';
            }
        }

Carsten  Rose's avatar
Carsten Rose committed
630
        // Label
631
        if ($formElement[FE_BS_LABEL_COLUMNS] > 0) {
632
633
            $addClass = ($formElement[FE_MODE] == FE_MODE_REQUIRED) ? CSS_REQUIRED : '';
            $htmlLabel = $this->buildLabel($htmlFormElementName, $formElement[FE_LABEL], $addClass);
634
        }
635

636
        $html .= $this->customWrap($formElement, $htmlLabel, FE_WRAP_LABEL, $formElement[FE_BS_LABEL_COLUMNS],
637
            [$this->wrap[WRAP_SETUP_LABEL][WRAP_SETUP_START], $this->wrap[WRAP_SETUP_LABEL][WRAP_SETUP_END]], $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_LABEL);
638

Carsten  Rose's avatar
Carsten Rose committed
639
        // Input
640
        $html .= $this->customWrap($formElement, $htmlElement, FE_WRAP_INPUT, $formElement[FE_BS_INPUT_COLUMNS],
641
642
            [$this->wrap[WRAP_SETUP_INPUT][WRAP_SETUP_START], $this->wrap[WRAP_SETUP_INPUT][WRAP_SETUP_END]],
            $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_INPUT, $classHideElement);
643

Carsten  Rose's avatar
Carsten Rose committed
644
        // Note
Carsten  Rose's avatar
Carsten Rose committed
645
        $note = $formElement[FE_NOTE];
646
        $html .= $this->customWrap($formElement, $note, FE_WRAP_NOTE, $formElement[FE_BS_NOTE_COLUMNS],
647
            [$this->wrap[WRAP_SETUP_NOTE][WRAP_SETUP_START], $this->wrap[WRAP_SETUP_NOTE][WRAP_SETUP_END]], $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_NOTE);
648

649
        // Row
650
651
        $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] : '';
652
653

        $html = $this->customWrap($formElement, $html, FE_WRAP_ROW, -1, [$openTag, $closeTag], $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_ROW);
654
655
656
657

        return $html;
    }

658
    /**
659
660
661
662
663
664
     * 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.
665
     *
Carsten  Rose's avatar
Carsten Rose committed
666
     * @param array $formElement Complete FormElement, especially some FE_WRAP
667
     * @param string $htmlElement Content to wrap.
Carsten  Rose's avatar
Carsten Rose committed
668
669
670
     * @param string $wrapName FE_WRAP_ROW, FE_WRAP_LABEL, FE_WRAP_INPUT, FE_WRAP_NOTE
     * @param int $bsColumns
     * @param array $wrapArray Systemwide Defaults: [ 'open wrap', 'close wrap' ]
671
672
     * @param string $htmlId
     * @param string $class
Carsten  Rose's avatar
Carsten Rose committed
673
     *
674
     * @return string Wrapped $htmlElement
675
     * @throws CodeException
676
677
     * @throws \qfq\UserFormException
     */
678
    private function customWrap(array $formElement, $htmlElement, $wrapName, $bsColumns, array $wrapArray, $htmlId = '', $class = '') {
679

680
        // If $bsColumns==0: do not wrap with default.
681
682
683
684
        if ($bsColumns == 0) {
            $wrapArray[0] = '';
            $wrapArray[1] = '';
        }
685

686
        // If there is a 'per FormElement'-wrap, take it.
687
688
689
690
691
692
693
694
        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);
        }

695
696
        if ($wrapArray[0] != '') {
            $wrapArray[0] = Support::insertAttribute($wrapArray[0], 'id', $htmlId);
697
            $wrapArray[0] = Support::insertAttribute($wrapArray[0], 'class', $class); // might be problematic, if there is already a 'class' defined.
698
699
        }

700
701
702
        return $wrapArray[0] . $htmlElement . $wrapArray[1];
    }

703

704
705
706
    /**
     * @param $formElement
     * @param $elementHtml
Carsten  Rose's avatar
Carsten Rose committed
707
     *
708
709
     * @return string
     */
710
    public function buildRowPill(array $formElement, $elementHtml) {
711
712
        $html = '';

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

        $active = $this->isFirstPill ? ' active' : '';

717
        $html = Support::wrapTag('<div role="tabpanel" class="tab-pane' . $active . '" id="' . $this->createAnker($formElement['id']) . '">', $html);
718
719
720
721
722
723

        $this->isFirstPill = false;

        return $html;
    }

724
    /**
725
726
     * Builds a fieldset
     *
727
728
729
     * @param $formElement
     * @param $elementHtml
     */
730
    public function buildRowFieldset(array $formElement, $elementHtml) {
731
732
733
        $html = $elementHtml;

        return $html;
734
735
    }

736
737
738
739
740
741
742
743
744
745
746
747
    /**
     * Builds a templateGroup
     *
     * @param $formElement
     * @param $elementHtml
     */
    public function buildRowTemplateGroup(array $formElement, $elementHtml) {
        $html = $elementHtml;

        return $html;
    }

748
749
750
    /**
     * @param $formElement
     * @param $elementHtml
Carsten  Rose's avatar
Carsten Rose committed
751
     *
752
753
     * @return string
     */
754
    public function buildRowSubrecord(array $formElement, $elementHtml) {
755

756
        $formElement[FE_LABEL] = Support::wrapTag("<label class='control-label'>", $formElement[FE_LABEL], true);
757
        $html = $this->wrapItem(WRAP_SETUP_ELEMENT, $this->wrapItem(WRAP_SETUP_SUBRECORD, $formElement[FE_LABEL]));
758
        $html .= $this->wrapItem(WRAP_SETUP_ELEMENT, $this->wrapItem(WRAP_SETUP_SUBRECORD, $elementHtml));
759
        $html .= $this->wrapItem(WRAP_SETUP_ELEMENT, $this->wrapItem(WRAP_SETUP_SUBRECORD, $formElement[FE_NOTE]));
760

761
762
763
        $attribute = ($formElement[FE_MODE] == FE_MODE_HIDDEN) ? ' style="display: none;"' : '';
        $attribute .= Support::doAttribute('id', $formElement[FE_HTML_ID]);

764

765
        return Support::wrapTag("<span name='qfq-subrecord' $attribute>", $html);
766
    }
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782


    /**
     * Builds complete 'form'.
     *
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
     *
     * @return string|array   $mode=LOAD_FORM: The whole form as HTML, $mode=FORM_UPDATE /FORM_SAVE: array of all
     *                        formElement.dynamicUpdate-yes  values/states
     * @throws CodeException
     * @throws DbException
     * @throws \qfq\UserFormException
     */
    public function process($mode, $htmlElementNameIdZero = false, $latestFeSpecNative = array()) {

        $json = array();
783
784
        $data = parent::process($mode, $htmlElementNameIdZero, $latestFeSpecNative);

785
786
787
        switch ($mode) {
            case FORM_SAVE:
            case FORM_UPDATE:
788
789
            $pillArray = OnArray::filter($this->feSpecNative, FE_TYPE, FE_TYPE_PILL);
            $this->buildPillNavigation($mode, $pillArray, $json);
790
791
792
793
794
795
796
797
                $data = array_merge($data, $json);
                break;
            default:
                break;
        }

        return $data;
    }
798
}