ArraySpoiler::renderButtonPopover()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 0
cts 11
cp 0
rs 9.6333
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
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 ArrayObject;
14
use Closure;
15
use hipanel\helpers\ArrayHelper;
16
use Yii;
17
use yii\base\InvalidValueException;
18
use yii\base\Widget;
19
use yii\helpers\Html;
20
use yii\helpers\Inflector;
21
use yii\helpers\Json;
22
use yii\helpers\StringHelper;
23
use yii\web\View;
24
25
/**
26
 * ArraySpoiler displays limited count of array's elements and hides all others behind a spoiler (badge).
27
 *
28
 * The following example will show first two elements of array concatenated with semicolon and bold-narrowed
29
 *
30
 * ```php
31
 * ArraySpoiler::widget([
32
 *      'data' => ['10.0.0.1', '10.0.0.2', '10.0.0.3', '10.0.0.4'],
33
 *      'formatter' => function ($v) {
34
 *          return Html::tag('b', $v);
35
 *      },
36
 *      'visibleCount' => 2,
37
 *      'delimiter' => '; ',
38
 *      'button' => [
39
 *          'label' => '+{count}',
40
 *          'popoverOptions' => [
41
 *              'html' => true
42
 *          ],
43
 *      ],
44
 * ]);
45
 * ```
46
 *
47
 * Also widget can split string in 'data' field into array by comma symbol.
48
 *
49
 * @author SilverFire <[email protected]>
50
 */
51
class ArraySpoiler extends Widget
52
{
53
    const MODE_POPOVER = 'popover';
54
    const MODE_SPOILER = 'spoiler';
55
56
    /**
57
     * @var string The mode. See `MODE_` constants for list of available constants.
58
     * Default - [[MODE_POPOVER]]
59
     */
60
    public $mode;
61
62
    /**
63
     * @var array|string|int Data to be proceeded
64
     */
65
    public $data;
66
67
    /**
68
     * @var string the template that is used to arrange show items, activating button and hidden inputs
69
     * The following tokens will be replaced when [[render()]] is called: `{shown}`, `{button}`, `{hidden}`
70
     */
71
    public $template = "{visible}\n{button}\n{hidden}";
72
73
    /**
74
     * @var array different parts of the field (e.g. show, hidden). This will be used together with
75
     * [[template]] to generate the final field HTML code. The keys are the token names in [[template]],
76
     * while the values are the corresponding HTML code. Valid tokens include `{visible}`, `{button}` and `{hidden}`.
77
     * Note that you normally don't need to access this property directly as
78
     * it is maintained by various methods of this class.
79
     */
80
    public $parts = [];
81
82
    /**
83
     * @var callable the function will be called for every element to format it.
84
     * Gets two arguments - value and key
85
     */
86
    public $formatter = null;
87
88
    /**
89
     * @var int count of elements, that are visible out of spoiler
90
     */
91
    public $visibleCount = 1;
92
93
    /**
94
     * @var string delimiter to join elements
95
     */
96
    public $delimiter = ', ';
97
98
    /**
99
     * @var string|null delimiter that is used to join hidden items
100
     * Defaults to `null`, meaning that delimiter is the same as for visible items
101
     */
102
    public $hiddenDelimiter;
103
104
    /**
105
     * @var array|string When string - will be auto-converted to an array. Array will be passed to [[Html::tag()]]
106
     * as option argument. Special options that will be extracted:
107
     *  - label - string
108
     *  - i18n - string|bool whether to pass `label` through [[Yii::t()]]. String will be used as dictionary name.
109
     * Available substitutions: count
110
     *  - tag - html tag that will be rendered (default - `span`)
111
     *
112
     * For other special options see [[renderSpoiler()]] and [[renderPopover()]] methods.
113
     */
114
    public $button = '+{count}';
115
116
    /**
117
     * @var array configuration will be passed to [[Html::tag()]] as options argument
118
     */
119
    public $hidden = [];
120
121
    public function init()
122
    {
123
        parent::init();
124
125
        if (is_string($this->data) || is_numeric($this->data)) {
126
            $this->data = StringHelper::explode($this->data);
127
        } elseif ($this->data === null) {
128
            $this->data = [];
129
        }
130
131
        if (empty($this->mode)) {
132
            $this->mode = static::MODE_POPOVER;
133
        }
134
135
        if (!is_callable([$this, 'renderButton' . Inflector::id2camel($this->mode)])) {
136
            throw new InvalidValueException('Do not know, how to render button of this type');
137
        }
138
139
        if (is_string($this->button)) {
140
            $this->button = ['label' => $this->button];
141
        }
142
143
        $this->button = ArrayHelper::merge([
144
            'tag' => 'a',
145
            'id' => $this->id,
146
        ], $this->button);
147
148
        if (!is_array($this->data)) {
149
            throw new InvalidValueException('Input can not be processed as an array');
150
        }
151
152
        if (is_callable($this->formatter)) {
153
            $this->data = array_map($this->formatter, $this->data, array_keys($this->data));
154
        }
155
156
        if (is_callable($this->button['label'])) {
157
            $this->button['label'] = call_user_func($this->button['label'], $this);
158
        }
159
    }
160
161
    /**
162
     * Method returns first [[visibleCount]] items from [[data]].
163
     * @return array
164
     */
165 View Code Duplication
    protected function getVisibleItems()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
166
    {
167
        if (count($this->data) <= $this->visibleCount) {
168
            return $this->data;
169
        }
170
        $visible = [];
171
        $iterator = (new ArrayObject($this->data))->getIterator();
172
        while (count($visible) < $this->visibleCount && $iterator->valid()) {
173
            $visible[] = $iterator->current();
174
            $iterator->next();
175
        }
176
177
        return $visible;
178
    }
179
180
    /**
181
     * Method returns all items from [[data]], skipping first [[visibleCount]].
182
     * @return array
183
     */
184 View Code Duplication
    protected function getSpoiledItems()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
185
    {
186
        if (count($this->data) <= $this->visibleCount) {
187
            return [];
188
        }
189
        $spoiled = [];
190
        $iterator = (new ArrayObject($this->data))->getIterator();
191
        $iterator->seek($this->visibleCount);
192
        while ($iterator->valid()) {
193
            $spoiled[] = $iterator->current();
194
            $iterator->next();
195
        }
196
197
        return $spoiled;
198
    }
199
200
    /**
201
     * Renders visible part of spoiler.
202
     */
203
    private function renderVisible()
204
    {
205
        $this->parts['{visible}'] = implode($this->delimiter, $this->getVisibleItems());
206
    }
207
208
    /**
209
     * Renders spoiled items. Uses [[$this->mode]] value to run proper renderer.
210
     */
211 View Code Duplication
    private function renderSpoiled()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
212
    {
213
        if (count($this->getSpoiledItems()) === 0) {
214
            $this->parts['{button}'] = '';
215
216
            return;
217
        }
218
219
        $method = 'renderButton' . Inflector::id2camel($this->mode);
220
        call_user_func([$this, $method]);
221
    }
222
223
    /**
224
     * Renders spoiled items. Uses [[$this->mode]] value to run proper renderer.
225
     */
226 View Code Duplication
    private function renderHidden()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
227
    {
228
        if (count($this->getSpoiledItems()) === 0) {
229
            $this->parts['{hidden}'] = '';
230
231
            return;
232
        }
233
234
        $method = 'renderHidden' . Inflector::id2camel($this->mode);
235
        call_user_func([$this, $method]);
236
    }
237
238
    /**
239
     * Renders a popover-activating button.
240
     * Additional special options, that will be extracted from [[$this->button]]:
241
     *  - `data-popover-group` - Group of popovers. Is used to close all other popovers in group, when new one is opening. Default: 'main'
242
     *  - `popoverOptions` - Array of options that will be passed to `popover` JS call. Refer to bootstrap docs.
243
     *
244
     * @see http://getbootstrap.com/javascript/#popovers-options
245
     */
246
    protected function renderButtonPopover()
247
    {
248
        $options = ArrayHelper::merge([
249
            'data-popover-group' => 'main',
250
            'data-content' => $this->renderHiddenPopover(),
251
            'class' => 'badge',
252
            'popoverOptions' => [],
253
        ], $this->button);
254
255
        $label = $this->getButtonLabel(ArrayHelper::remove($options, 'label'));
256
257
        $this->getView()->registerJs("
258
             $('#{$this->id}').popover(" . Json::htmlEncode(ArrayHelper::remove($options, 'popoverOptions')) . ").on('show.bs.popover', function(e) {
259
                $('[data-popover-group=\"{$options['data-popover-group']}\"]').not(e.target).popover('hide');
260
             });
261
         ", View::POS_READY);
262
263
        $this->parts['{button}'] = Html::tag(ArrayHelper::remove($options, 'tag'), $label, $options);
264
    }
265
266
    /**
267
     * Renders a popover-activated hidden part.
268
     * Actually is does not render anything. Sets `{hidden}` to an empty string and returns the value of spoiled items.
269
     * @return string
270
     */
271
    public function renderHiddenPopover()
272
    {
273
        $this->parts['{hidden}'] = '';
274
275
        return implode($this->hiddenDelimiter ?? $this->delimiter, $this->getSpoiledItems());
276
    }
277
278
    /**
279
     * Renders a button for spoiler activator.
280
     *
281
     * Additional special options, that will be extracted from [[$this->button]]:
282
     *  - `data-popover-group` - Group of popovers. Is used to close all other popovers in group, when new one is opening. Default: 'main'
283
     *
284
     * @see http://getbootstrap.com/javascript/#popovers-options
285
     */
286
    protected function renderButtonSpoiler()
287
    {
288
        $options = ArrayHelper::merge([
289
            'role' => 'button',
290
            'data-spoiler-group' => 'main',
291
            'data-spoiler-toggle' => true,
292
            'data-target' => $this->button['id'] . '-body',
293
        ], $this->button);
294
295
        $label = $this->getButtonLabel(ArrayHelper::remove($options, 'label'));
296
297
        $this->parts['{button}'] = Html::tag(ArrayHelper::remove($options, 'tag'), $label, $options);
298
        $this->getView()->registerJs("
299
             $('[data-spoiler-toggle]').click(function (e) {
300
                $('#'+$(this).data('target')).toggle();
301
                $('[data-spoiler-group=\"{$options['data-spoiler-group']}\"]').not($(this)).trigger('hide');
302
             }).on('hide', function () {
303
                $('#'+$(this).data('target')).hide();
304
             })",
305
            View::POS_READY);
306
    }
307
308
    /**
309
     * Renders a spoiler-button-activated hidden part.
310
     */
311
    public function renderHiddenSpoiler()
312
    {
313
        $options = ArrayHelper::merge([
314
            'id' => $this->button['id'] . '-body',
315
            'tag' => 'span',
316
            'value' => implode($this->hiddenDelimiter ?? $this->delimiter, $this->getSpoiledItems()),
317
            'class' => 'collapse',
318
            'data-spoiler-body' => true,
319
        ], $this->hidden);
320
321
        $this->parts['{hidden}'] = Html::tag(ArrayHelper::remove($options, 'tag'), ArrayHelper::remove($options, 'value'), $options);
322
    }
323
324
    /**
325
     * Returns the button label.
326
     *
327
     * @param $label string|Closure
328
     * @return mixed|string
329
     */
330
    public function getButtonLabel($label)
331
    {
332
        if ($label instanceof Closure) {
333
            $label = call_user_func($label, $this);
334
        } else {
335
            $label = Yii::t('hipanel', $label, ['count' => count($this->getSpoiledItems())]);
336
        }
337
338
        return $label;
339
    }
340
341
    /**
342
     * Renders the all the widget.
343
     */
344
    public function run()
345
    {
346
        if (!isset($this->parts['{visible}'])) {
347
            $this->renderVisible();
348
        }
349
        if (!isset($this->parts['{button}'])) {
350
            $this->renderSpoiled();
351
        }
352
        if (!isset($this->parts['{hidden}'])) {
353
            $this->renderHidden();
354
        }
355
356
        echo strtr($this->template, $this->parts);
357
    }
358
}
359