ModalButton   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 339
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 37
lcom 1
cbo 11
dl 0
loc 339
ccs 0
cts 169
cp 0
rs 9.44
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
B init() 0 22 6
C initOptions() 0 69 12
A run() 0 12 3
B renderButton() 0 28 7
A getModalId() 0 10 3
A beginForm() 0 7 1
A endForm() 0 4 1
A beginModal() 0 4 1
A endModal() 0 4 1
A registerAjaxSubmit() 0 23 1
A registerFooterButtonScript() 0 12 1
1
<?php
2
/**
3
 * HiPanel core package
4
 *
5
 * @link      https://hipanel.com/
6
 * @package   hipanel-core
7
 * @license   BSD-3-Clause
8
 * @copyright Copyright (c) 2014-2019, HiQDev (http://hiqdev.com/)
9
 */
10
11
namespace hipanel\widgets;
12
13
use Yii;
14
use yii\base\InvalidConfigException;
15
use yii\base\Model;
16
use yii\base\Widget;
17
use yii\bootstrap\ActiveForm;
18
use yii\bootstrap\Modal;
19
use yii\db\ActiveRecord;
20
use yii\helpers\ArrayHelper;
21
use yii\helpers\Html;
22
use yii\helpers\Inflector;
23
use yii\helpers\Json;
24
use yii\web\JsExpression;
25
26
/**
27
 * Class ModalButton.
28
 *
29
 * Renders [[Modal]] widget in form with custom toggle button.
30
 */
31
class ModalButton extends Widget
32
{
33
    /**
34
     * Toggle button will be placed outside of the form.
35
     */
36
    const BUTTON_OUTSIDE = 1;
37
38
    /**
39
     * Toggle button will be rendered inside of the form with [[Modal]] widget.
40
     */
41
    const BUTTON_IN_MODAL = 2;
42
43
    /**
44
     * Submit with HTML POST request.
45
     */
46
    const SUBMIT_HTML = 0;
47
48
    /**
49
     * Submit using PJAX.
50
     */
51
    const SUBMIT_PJAX = 1;
52
53
    /**
54
     * Submit using AJAX.
55
     */
56
    const SUBMIT_AJAX = 2;
57
58
    /**
59
     * @var ActiveRecord the model. Is required.
60
     */
61
    public $model;
62
63
    /**
64
     * @var string Used to generate form action URL and HTML id of modal.
65
     * May be set manually, otherwise will be extracted from the model
66
     */
67
    public $scenario;
68
69
    /**
70
     * @var string Model scenario before widget run
71
     */
72
    protected $_oldScenario;
73
74
    /**
75
     * @var array|ActiveForm The options for the HTML form.
76
     * The following special options are supported:
77
     *
78
     * - action: string|array, the action, that will be passed as first argument to [[Html::beginForm()]]
79
     * - method: string, the method, that will be passed as second argument to [[Html::beginForm()]]
80
     *
81
     * The rest of the options will be passed to the [[ActiveForm::begin()]] method
82
     *
83
     * If the property was not false, it will contain [[ActiveForm]] instance after [[ModalButton::begin()]].
84
     */
85
    public $form = [];
86
87
    /**
88
     * @var array The options for rendering toggle button
89
     * The toggle button is used to toggle the visibility of the modal window.
90
     * If this property is false, no toggle button will be rendered.
91
     *
92
     * The following special options are supported:
93
     *
94
     * - tag: string, the tag name of the button. Defaults to 'button'.
95
     * - label: string, the label of the button. Defaults to 'Show'.
96
     *
97
     * The rest of the options will be rendered as the HTML attributes of the button tag.
98
     */
99
    public $button = [];
100
101
    /**
102
     * @var string|callable The body of modal window.
103
     * If callable - will get the only argument - [[$this->model]]
104
     */
105
    public $body;
106
107
    /**
108
     * @var array HTML options that will be passed to the [[Modal]] widget
109
     * When ```footer``` is array, the following special options are supported:
110
     * - tag: string, the tag name of the button. Defaults to 'button'.
111
     * - label: string, the label of the button. Defaults to 'Show'.
112
     *
113
     * The rest of the options will be rendered as the HTML attributes of the button tag.
114
     */
115
    public $modal = [];
116
117
    /**
118
     * @var integer determines the way of form submit
119
     * @see [[SUBMIT_HTML]]
120
     * @see [[SUBMIT_PJAX]]
121
     * @see [[SUBMIT_AJAX]]
122
     */
123
    public $submit = self::SUBMIT_PJAX;
124
125
    /**
126
     * @var array options that will be passed to ajax submit JS
127
     * @see [[registerAjaxSubmit]]
128
     */
129
    public $ajaxOptions = [];
130
131
    /**
132
     * {@inheritdoc}
133
     * @throws InvalidConfigException
134
     */
135
    public function init()
136
    {
137
        $this->initOptions();
138
139
        if ($this->button['position'] === static::BUTTON_OUTSIDE && isset($this->button['label'])) {
140
            $this->renderButton();
141
        }
142
143
        if ($this->form !== false) {
144
            $this->beginForm();
145
        }
146
147
        $this->beginModal();
148
149
        if (isset($this->body)) {
150
            if ($this->body instanceof \Closure) {
151
                echo call_user_func($this->body, $this->model, $this);
152
            } else {
153
                echo $this->body;
154
            }
155
        }
156
    }
157
158
    /**
159
     * Initialization of options.
160
     * @throws InvalidConfigException
161
     */
162
    protected function initOptions()
163
    {
164
        if (!($this->model instanceof Model)) {
165
            throw new InvalidConfigException('Model is required');
166
        }
167
168
        if (!($this->model->getPrimaryKey())) {
169
            throw new InvalidConfigException('Model has empty primary key');
170
        }
171
172
        if ($this->button !== false) {
173
            $this->button['position'] = isset($this->button['position']) ? $this->button['position'] : static::BUTTON_OUTSIDE;
174
        }
175
176
        if (empty($this->scenario)) {
177
            $this->scenario = $this->model->scenario;
178
        } else {
179
            $this->_oldScenario = $this->model->scenario;
180
            $this->model->scenario = $this->scenario;
181
        }
182
183
        if ($this->form !== false) {
184
            $formConfig = [
185
                'method' => 'POST',
186
                'action' => $this->scenario,
187
                'options' => [
188
                    'class' => 'inline',
189
                    'data' => [
190
                        'modal-form' => true,
191
                    ],
192
                ],
193
            ];
194
            if ($this->submit === static::SUBMIT_PJAX) {
195
                $formConfig['options'] = ArrayHelper::merge($formConfig['options'], [
196
                    'data' => ['pjax' => 1, 'pjax-push' => 0],
197
                ]);
198
            } elseif ($this->submit === static::SUBMIT_AJAX) {
199
                $formConfig['options'] = ArrayHelper::merge($formConfig['options'], [
200
                    'data' => ['ajax-submit' => 1],
201
                ]);
202
203
                $this->registerAjaxSubmit();
204
            }
205
206
            $this->form = ArrayHelper::merge($formConfig, $this->form);
207
        }
208
209
        if (is_array($footer = $this->modal['footer'])) {
210
            $tag = ArrayHelper::remove($footer, 'tag', 'input');
211
            $label = ArrayHelper::remove($footer, 'label', 'OK');
212
            $footer = ArrayHelper::merge([
213
                'data-modal-submit' => true,
214
                'data-loading-text' => '<i class="fa fa-circle-o-notch fa-spin"></i> ' . Yii::t('hipanel', 'loading'),
215
            ], $footer);
216
217
            if ($tag === 'input') {
218
                $footer['type']  = 'submit';
219
                $footer['value'] = $label;
220
            }
221
222
            $this->modal['footer'] = Html::tag($tag, $label, $footer);
223
            $this->registerFooterButtonScript();
224
        }
225
226
        $this->modal = ArrayHelper::merge([
227
            'id' => $this->getModalId(),
228
            'toggleButton' => ($this->button['position'] === static::BUTTON_IN_MODAL) ? $this->button : false,
229
        ], $this->modal);
230
    }
231
232
    /**
233
     * Runs widget.
234
     */
235
    public function run()
236
    {
237
        $this->endModal();
238
239
        if ($this->form !== false) {
240
            $this->endForm();
241
        }
242
243
        if ($this->_oldScenario !== null) {
244
            $this->model->scenario = $this->_oldScenario;
245
        }
246
    }
247
248
    /**
249
     * Renders toggle button.
250
     */
251
    public function renderButton()
252
    {
253
        if (($button = $this->button) !== false) {
254
            $tag = ArrayHelper::remove($button, 'tag', 'a');
255
            $label = ArrayHelper::remove($button, 'label',
256
                Inflector::camel2words(Inflector::id2camel($this->scenario)));
257
            if ($tag === 'button' && !isset($button['type'])) {
258
                $toggleButton['type'] = 'button';
0 ignored issues
show
Coding Style Comprehensibility introduced by
$toggleButton was never initialized. Although not strictly required by PHP, it is generally a good practice to add $toggleButton = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
259
            }
260
261
            if ($button['disabled']) {
262
                $button = ArrayHelper::merge([
263
                    'onClick' => new JsExpression('return false'),
264
                ], $button);
265
            } else {
266
                $button = ArrayHelper::merge([
267
                    'data-toggle' => 'modal',
268
                    'data-target' => "#{$this->getModalId()}",
269
                ], $button);
270
            }
271
272
            if ($tag === 'a' && empty($button['href'])) {
273
                $button['href'] = '#';
274
            }
275
276
            echo Html::tag($tag, $label, $button);
277
        }
278
    }
279
280
    /**
281
     * Constructs model ID, using [[$model]] primary key, or ID of the widget and scenario.
282
     * @return string format: `modal_{id}_{scenario}`
283
     */
284
    public function getModalId()
285
    {
286
        if ($this->getId(false) !== null) {
287
            return $this->getId(false);
288
        }
289
290
        $id = $this->model->getPrimaryKey() ?: $this->id;
291
292
        return "modal_{$id}_{$this->scenario}";
293
    }
294
295
    /**
296
     * Begins form.
297
     */
298
    public function beginForm()
299
    {
300
        $this->form = ActiveForm::begin($this->form);
0 ignored issues
show
Bug introduced by
It seems like $this->form can also be of type object<yii\bootstrap\ActiveForm>; however, yii\base\Widget::begin() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
301
302
        $inputId = $this->getModalId() . '-hidden-value';
303
        echo Html::activeHiddenInput($this->model, 'id', ['id' => $inputId]);
304
    }
305
306
    /**
307
     * Ends form.
308
     */
309
    public function endForm()
310
    {
311
        $this->form->end();
312
    }
313
314
    /**
315
     * Begins modal widget.
316
     */
317
    public function beginModal()
318
    {
319
        Modal::begin($this->modal);
320
    }
321
322
    /**
323
     * Ends modal widget.
324
     */
325
    public function endModal()
326
    {
327
        Modal::end();
328
    }
329
330
    /**
331
     * Registers JavaScript for ajax submit.
332
     */
333
    public function registerAjaxSubmit()
334
    {
335
        $view = Yii::$app->view;
336
337
        $options = ArrayHelper::merge([
338
            'type' => new JsExpression("form.attr('method')"),
339
            'url' => new JsExpression("form.attr('action')"),
340
            'data' => new JsExpression('form.serialize()'),
341
        ], $this->ajaxOptions);
342
        $options = Json::encode($options);
343
344
        $view->registerJs(<<<JS
345
            $('form[data-ajax-submit]').on('submit', function(event) {
346
                var form = $(this);
347
                if (event.eventPhase === 2) {
348
                    $.ajax($options);
349
                    $('.modal-backdrop').remove();
350
                }
351
                event.preventDefault();
352
            });
353
JS
354
        );
355
    }
356
357
    public function registerFooterButtonScript()
358
    {
359
        $view = Yii::$app->view;
360
        $view->registerJs("
361
            $('form[data-modal-form]').on('beforeSubmit', function (e) {
362
                var submit = $(this).find('[data-modal-submit]');
363
                if (!submit) return true;
364
                submit.button('loading');
365
                $('body').removeClass('modal-open');
366
            });
367
        ");
368
    }
369
}
370