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() |
|
|
|
|
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() |
|
|
|
|
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() |
|
|
|
|
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() |
|
|
|
|
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
|
|
|
|
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.