1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Yiisoft\Yii\DataView; |
6
|
|
|
|
7
|
|
|
use Closure; |
8
|
|
|
use Psr\Container\ContainerInterface; |
9
|
|
|
use Stringable; |
10
|
|
|
use Yiisoft\Definitions\Exception\CircularReferenceException; |
11
|
|
|
use Yiisoft\Definitions\Exception\InvalidConfigException; |
12
|
|
|
use Yiisoft\Definitions\Exception\NotInstantiableException; |
13
|
|
|
use Yiisoft\Factory\NotFoundException; |
14
|
|
|
use Yiisoft\Html\Html; |
15
|
|
|
use Yiisoft\Html\Tag\Tr; |
16
|
|
|
use Yiisoft\Router\UrlGeneratorInterface; |
17
|
|
|
use Yiisoft\Translator\TranslatorInterface; |
18
|
|
|
use Yiisoft\Yii\DataView\Column\ActionColumn; |
19
|
|
|
use Yiisoft\Yii\DataView\Column\Base\Cell; |
20
|
|
|
use Yiisoft\Yii\DataView\Column\Base\GlobalContext; |
21
|
|
|
use Yiisoft\Yii\DataView\Column\Base\DataContext; |
22
|
|
|
use Yiisoft\Yii\DataView\Column\ColumnInterface; |
23
|
|
|
use Yiisoft\Yii\DataView\Column\ColumnRendererInterface; |
24
|
|
|
use Yiisoft\Yii\DataView\Column\DataColumn; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* The GridView widget is used to display data in a grid. |
28
|
|
|
* |
29
|
|
|
* It provides features like {@see sorter|sorting}, and {@see filterModel|filtering} the data. |
30
|
|
|
* |
31
|
|
|
* The columns of the grid table are configured in terms of {@see Column} classes, which are configured via |
32
|
|
|
* {@see columns}. |
33
|
|
|
* |
34
|
|
|
* The look and feel of a grid view can be customized using the large amount of properties. |
35
|
|
|
*/ |
36
|
|
|
final class GridView extends BaseListView |
37
|
|
|
{ |
38
|
|
|
public const FILTER_POS_HEADER = 'header'; |
39
|
|
|
public const FILTER_POS_FOOTER = 'footer'; |
40
|
|
|
public const FILTER_POS_BODY = 'body'; |
41
|
|
|
|
42
|
|
|
private Closure|null $afterRow = null; |
43
|
|
|
private Closure|null $beforeRow = null; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* @var ColumnInterface[] |
47
|
|
|
*/ |
48
|
|
|
private array $columns = []; |
49
|
|
|
|
50
|
|
|
private bool $columnsGroupEnabled = false; |
51
|
|
|
private string $emptyCell = ' '; |
52
|
|
|
private ?string $filterModelName = null; |
53
|
|
|
private string $filterPosition = self::FILTER_POS_BODY; |
54
|
|
|
private array $filterRowAttributes = []; |
55
|
|
|
private bool $footerEnabled = false; |
56
|
|
|
private array $footerRowAttributes = []; |
57
|
|
|
private bool $headerTableEnabled = true; |
58
|
|
|
private array $headerRowAttributes = []; |
59
|
|
|
private array $rowAttributes = []; |
60
|
|
|
private array $tableAttributes = []; |
61
|
|
|
private array $tbodyAttributes = []; |
62
|
|
|
private array $headerCellAttributes = []; |
63
|
|
|
private array $bodyCellAttributes = []; |
64
|
|
|
|
65
|
88 |
|
public function __construct( |
66
|
|
|
private ContainerInterface $columnRenderersContainer, |
67
|
|
|
TranslatorInterface|null $translator = null, |
68
|
|
|
UrlGeneratorInterface|null $urlGenerator = null |
69
|
|
|
) { |
70
|
88 |
|
parent::__construct($translator, $urlGenerator); |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Returns a new instance with anonymous function that is called once AFTER rendering each data. |
75
|
|
|
* |
76
|
|
|
* @param Closure|null $value The anonymous function that is called once AFTER rendering each data. |
77
|
|
|
*/ |
78
|
2 |
|
public function afterRow(Closure|null $value): self |
79
|
|
|
{ |
80
|
2 |
|
$new = clone $this; |
81
|
2 |
|
$new->afterRow = $value; |
82
|
|
|
|
83
|
2 |
|
return $new; |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Return a new instance with anonymous function that is called once BEFORE rendering each data. |
88
|
|
|
* |
89
|
|
|
* @param Closure|null $value The anonymous function that is called once BEFORE rendering each data. |
90
|
|
|
*/ |
91
|
2 |
|
public function beforeRow(Closure|null $value): self |
92
|
|
|
{ |
93
|
2 |
|
$new = clone $this; |
94
|
2 |
|
$new->beforeRow = $value; |
95
|
|
|
|
96
|
2 |
|
return $new; |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* Return a new instance the specified columns. |
101
|
|
|
* |
102
|
|
|
* @param ColumnInterface ...$values The grid column configuration. Each array element represents the configuration |
103
|
|
|
* for one particular grid column. For example, |
104
|
|
|
* |
105
|
|
|
* ```php |
106
|
|
|
* [ |
107
|
|
|
* SerialColumn::create(), |
108
|
|
|
* DetailColumn::create() |
109
|
|
|
* ->attribute('identity_id') |
110
|
|
|
* ->filterAttribute('identity_id') |
111
|
|
|
* ->filterValueDefault(0) |
112
|
|
|
* ->filterAttributes(['class' => 'text-center', 'maxlength' => '5', 'style' => 'width:60px']), |
113
|
|
|
* ActionColumn::create()->primaryKey('identity_id')->visibleButtons(['view' => true]), |
114
|
|
|
* ] |
115
|
|
|
* ``` |
116
|
|
|
*/ |
117
|
82 |
|
public function columns(ColumnInterface ...$values): self |
118
|
|
|
{ |
119
|
82 |
|
$new = clone $this; |
120
|
82 |
|
$new->columns = $values; |
121
|
82 |
|
return $new; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* Returns a new instance with the specified column group enabled. |
126
|
|
|
* |
127
|
|
|
* @param bool $value Whether to enable the column group. |
128
|
|
|
*/ |
129
|
3 |
|
public function columnsGroupEnabled(bool $value): self |
130
|
|
|
{ |
131
|
3 |
|
$new = clone $this; |
132
|
3 |
|
$new->columnsGroupEnabled = $value; |
133
|
|
|
|
134
|
3 |
|
return $new; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* Return new instance with the HTML display when the content is empty. |
139
|
|
|
* |
140
|
|
|
* @param string $value The HTML display when the content of a cell is empty. This property is used to render cells |
141
|
|
|
* that have no defined content, e.g. empty footer or filter cells. |
142
|
|
|
*/ |
143
|
2 |
|
public function emptyCell(string $value): self |
144
|
|
|
{ |
145
|
2 |
|
$new = clone $this; |
146
|
2 |
|
$new->emptyCell = $value; |
147
|
|
|
|
148
|
2 |
|
return $new; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* Return new instance with the filter model name. |
153
|
|
|
* |
154
|
|
|
* @param string|null $value The form model name that keeps the user-entered filter data. When this property is set, the |
155
|
|
|
* grid view will enable column-based filtering. Each data column by default will display a text field at the top |
156
|
|
|
* that users can fill in to filter the data. |
157
|
|
|
* |
158
|
|
|
* Note that in order to show an input field for filtering, a column must have its {@see DataColumn::attribute} |
159
|
|
|
* property set and the attribute should be active in the current scenario of $filterModelName or have |
160
|
|
|
* {@see DataColumn::filter} set as the HTML code for the input field. |
161
|
|
|
*/ |
162
|
11 |
|
public function filterModelName(?string $value): self |
163
|
|
|
{ |
164
|
11 |
|
$new = clone $this; |
165
|
11 |
|
$new->filterModelName = $value; |
166
|
|
|
|
167
|
11 |
|
return $new; |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* Return new instance with the filter position. |
172
|
|
|
* |
173
|
|
|
* @param string $filterPosition Whether the filters should be displayed in the grid view. Valid values include: |
174
|
|
|
* |
175
|
|
|
* - {@see FILTER_POS_HEADER}: The filters will be displayed on top of each column's header cell. |
176
|
|
|
* - {@see FILTER_POS_BODY}: The filters will be displayed right below each column's header cell. |
177
|
|
|
* - {@see FILTER_POS_FOOTER}: The filters will be displayed below each column's footer cell. |
178
|
|
|
*/ |
179
|
3 |
|
public function filterPosition(string $filterPosition): self |
180
|
|
|
{ |
181
|
3 |
|
$new = clone $this; |
182
|
3 |
|
$new->filterPosition = $filterPosition; |
183
|
|
|
|
184
|
3 |
|
return $new; |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* Returns a new instance with the HTML attributes for filter row. |
189
|
|
|
* |
190
|
|
|
* @param array $values Attribute values indexed by attribute names. |
191
|
|
|
*/ |
192
|
7 |
|
public function filterRowAttributes(array $values): self |
193
|
|
|
{ |
194
|
7 |
|
$new = clone $this; |
195
|
7 |
|
$new->filterRowAttributes = $values; |
196
|
|
|
|
197
|
7 |
|
return $new; |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* Return new instance whether to show the footer section of the grid. |
202
|
|
|
* |
203
|
|
|
* @param bool $value Whether to show the footer section of the grid. |
204
|
|
|
*/ |
205
|
4 |
|
public function footerEnabled(bool $value): self |
206
|
|
|
{ |
207
|
4 |
|
$new = clone $this; |
208
|
4 |
|
$new->footerEnabled = $value; |
209
|
|
|
|
210
|
4 |
|
return $new; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Returns a new instance with the HTML attributes for footer row. |
215
|
|
|
* |
216
|
|
|
* @param array $values Attribute values indexed by attribute names. |
217
|
|
|
*/ |
218
|
2 |
|
public function footerRowAttributes(array $values): self |
219
|
|
|
{ |
220
|
2 |
|
$new = clone $this; |
221
|
2 |
|
$new->footerRowAttributes = $values; |
222
|
|
|
|
223
|
2 |
|
return $new; |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* Return new instance whether to show the header table section of the grid. |
228
|
|
|
* |
229
|
|
|
* @param bool $value Whether to show the header table section of the grid. |
230
|
|
|
*/ |
231
|
2 |
|
public function headerTableEnabled(bool $value): self |
232
|
|
|
{ |
233
|
2 |
|
$new = clone $this; |
234
|
2 |
|
$new->headerTableEnabled = $value; |
235
|
|
|
|
236
|
2 |
|
return $new; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
/** |
240
|
|
|
* Return new instance with the HTML attributes for the header row. |
241
|
|
|
* |
242
|
|
|
* @param array $values Attribute values indexed by attribute names. |
243
|
|
|
*/ |
244
|
2 |
|
public function headerRowAttributes(array $values): self |
245
|
|
|
{ |
246
|
2 |
|
$new = clone $this; |
247
|
2 |
|
$new->headerRowAttributes = $values; |
248
|
|
|
|
249
|
2 |
|
return $new; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Return new instance with the HTML attributes for row of the grid. |
254
|
|
|
* |
255
|
|
|
* @param array $values Attribute values indexed by attribute names. |
256
|
|
|
* |
257
|
|
|
* This can be either an array specifying the common HTML attributes for all body rows. |
258
|
|
|
*/ |
259
|
2 |
|
public function rowAttributes(array $values): self |
260
|
|
|
{ |
261
|
2 |
|
$new = clone $this; |
262
|
2 |
|
$new->rowAttributes = $values; |
263
|
|
|
|
264
|
2 |
|
return $new; |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
/** |
268
|
|
|
* Return new instance with the HTML attributes for the `table` tag. |
269
|
|
|
* |
270
|
|
|
* @param array $attributes The tag attributes in terms of name-value pairs. |
271
|
|
|
*/ |
272
|
2 |
|
public function tableAttributes(array $attributes): self |
273
|
|
|
{ |
274
|
2 |
|
$new = clone $this; |
275
|
2 |
|
$new->tableAttributes = $attributes; |
276
|
2 |
|
return $new; |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
/** |
280
|
|
|
* Add one or more CSS classes to the `table` tag. |
281
|
|
|
* |
282
|
|
|
* @param string|null ...$class One or many CSS classes. |
283
|
|
|
*/ |
284
|
|
|
public function addTableClass(?string ...$class): self |
285
|
|
|
{ |
286
|
|
|
$new = clone $this; |
287
|
|
|
Html::addCssClass($new->tableAttributes, $class); |
288
|
|
|
return $new; |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* Replace current `table` tag CSS classes with a new set of classes. |
293
|
|
|
* |
294
|
|
|
* @param string|null ...$class One or many CSS classes. |
295
|
|
|
*/ |
296
|
|
|
public function tableClass(?string ...$class): static |
297
|
|
|
{ |
298
|
|
|
$new = clone $this; |
299
|
|
|
$new->tableAttributes['class'] = array_filter($class, static fn ($c) => $c !== null); |
300
|
|
|
return $new; |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* Return new instance with the HTML attributes for the `tbody` tag. |
305
|
|
|
* |
306
|
|
|
* @param array $attributes The tag attributes in terms of name-value pairs. |
307
|
|
|
*/ |
308
|
|
|
public function tbodyAttributes(array $attributes): self |
309
|
|
|
{ |
310
|
|
|
$new = clone $this; |
311
|
|
|
$new->tbodyAttributes = $attributes; |
312
|
|
|
return $new; |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* Add one or more CSS classes to the `tbody` tag. |
317
|
|
|
* |
318
|
|
|
* @param string|null ...$class One or many CSS classes. |
319
|
|
|
*/ |
320
|
|
|
public function addTbodyClass(?string ...$class): self |
321
|
|
|
{ |
322
|
|
|
$new = clone $this; |
323
|
|
|
Html::addCssClass($new->tbodyAttributes, $class); |
324
|
|
|
return $new; |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
/** |
328
|
|
|
* Replace current `tbody` tag CSS classes with a new set of classes. |
329
|
|
|
* |
330
|
|
|
* @param string|null ...$class One or many CSS classes. |
331
|
|
|
*/ |
332
|
|
|
public function tbodyClass(?string ...$class): static |
333
|
|
|
{ |
334
|
|
|
$new = clone $this; |
335
|
|
|
$new->tbodyAttributes['class'] = array_filter($class, static fn ($c) => $c !== null); |
336
|
|
|
return $new; |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
/** |
340
|
|
|
* Return new instance with the HTML attributes for the `th` tag. |
341
|
|
|
* |
342
|
|
|
* @param array $attributes The tag attributes in terms of name-value pairs. |
343
|
|
|
*/ |
344
|
|
|
public function headerCellAttributes(array $attributes): self |
345
|
|
|
{ |
346
|
|
|
$new = clone $this; |
347
|
|
|
$new->headerCellAttributes = $attributes; |
348
|
|
|
return $new; |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
/** |
352
|
|
|
* Return new instance with the HTML attributes for the `td` tag. |
353
|
|
|
* |
354
|
|
|
* @param array $attributes The tag attributes in terms of name-value pairs. |
355
|
|
|
*/ |
356
|
|
|
public function bodyCellAttributes(array $attributes): self |
357
|
|
|
{ |
358
|
|
|
$new = clone $this; |
359
|
|
|
$new->bodyCellAttributes = $attributes; |
360
|
|
|
return $new; |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
/** |
364
|
|
|
* Renders the data active record classes for the grid view. |
365
|
|
|
* |
366
|
|
|
* @throws InvalidConfigException |
367
|
|
|
* @throws NotFoundException |
368
|
|
|
* @throws NotInstantiableException |
369
|
|
|
* @throws CircularReferenceException |
370
|
|
|
*/ |
371
|
84 |
|
protected function renderItems(): string |
372
|
|
|
{ |
373
|
84 |
|
$columns = empty($this->columns) ? $this->guessColumns() : $this->columns; |
374
|
84 |
|
$columns = array_filter( |
375
|
84 |
|
$columns, |
376
|
84 |
|
static fn(ColumnInterface $column) => $column->isVisible() |
377
|
84 |
|
); |
378
|
|
|
|
379
|
84 |
|
$renderers = []; |
380
|
84 |
|
foreach ($columns as $i => $column) { |
381
|
81 |
|
$renderers[$i] = $this->getColumnRenderer($column); |
382
|
|
|
} |
383
|
|
|
|
384
|
84 |
|
$blocks = []; |
385
|
|
|
|
386
|
84 |
|
$globalContext = new GlobalContext( |
387
|
84 |
|
$this->getDataReader(), |
388
|
84 |
|
$this->sortLinkAttributes, |
389
|
84 |
|
$this->urlArguments, |
390
|
84 |
|
$this->urlQueryParameters, |
391
|
84 |
|
$this->filterModelName, |
392
|
84 |
|
); |
393
|
|
|
|
394
|
84 |
|
if ($this->columnsGroupEnabled) { |
395
|
2 |
|
$tags = []; |
396
|
2 |
|
foreach ($columns as $i => $column) { |
397
|
2 |
|
$cell = $renderers[$i]->renderColumn($column, new Cell(), $globalContext); |
398
|
2 |
|
$tags[] = Html::col($cell->getAttributes()); |
399
|
|
|
} |
400
|
2 |
|
$blocks[] = Html::colgroup()->columns(...$tags)->render(); |
401
|
|
|
} |
402
|
|
|
|
403
|
84 |
|
if ($this->filterPosition === self::FILTER_POS_BODY |
404
|
2 |
|
|| $this->filterPosition === self::FILTER_POS_HEADER |
405
|
84 |
|
|| $this->filterPosition === self::FILTER_POS_FOOTER |
406
|
|
|
) { |
407
|
84 |
|
$tags = []; |
408
|
84 |
|
$hasFilters = false; |
409
|
84 |
|
foreach ($columns as $i => $column) { |
410
|
81 |
|
$baseCell = new Cell(encode: false, content: ' '); |
411
|
81 |
|
$cell = $renderers[$i]->renderFilter($column, $baseCell, $globalContext); |
412
|
81 |
|
if ($cell === null) { |
413
|
75 |
|
$cell = $baseCell; |
414
|
|
|
} else { |
415
|
18 |
|
$hasFilters = true; |
416
|
|
|
} |
417
|
|
|
/** @var string|Stringable $content */ |
418
|
81 |
|
$content = $cell->getContent(); |
419
|
81 |
|
$tags[] = Html::td(attributes: $cell->getAttributes()) |
420
|
81 |
|
->content($content) |
421
|
81 |
|
->encode($cell->isEncode()) |
422
|
81 |
|
->doubleEncode($cell->isDoubleEncode()); |
423
|
|
|
} |
424
|
84 |
|
$filterRow = $hasFilters ? Html::tr($this->filterRowAttributes)->cells(...$tags) : null; |
|
|
|
|
425
|
|
|
} else { |
426
|
|
|
$filterRow = null; |
427
|
|
|
} |
428
|
|
|
|
429
|
84 |
|
if ($this->headerTableEnabled) { |
430
|
83 |
|
$tags = []; |
431
|
83 |
|
foreach ($columns as $i => $column) { |
432
|
80 |
|
$cell = $renderers[$i]->renderHeader($column, new Cell($this->headerCellAttributes), $globalContext); |
433
|
|
|
/** @var string|Stringable $content */ |
434
|
80 |
|
$content = $cell?->getContent(); |
435
|
80 |
|
$tags[] = $cell === null |
436
|
5 |
|
? Html::th(' ')->encode(false) |
437
|
80 |
|
: Html::th(attributes: $cell->getAttributes()) |
438
|
80 |
|
->content($content) |
439
|
80 |
|
->encode($cell->isEncode()) |
440
|
80 |
|
->doubleEncode($cell->isDoubleEncode()); |
441
|
|
|
} |
442
|
83 |
|
$headerRow = Html::tr($this->headerRowAttributes)->cells(...$tags); |
443
|
|
|
|
444
|
83 |
|
if ($filterRow === null) { |
|
|
|
|
445
|
65 |
|
$rows = [$headerRow]; |
446
|
18 |
|
} elseif ($this->filterPosition === self::FILTER_POS_HEADER) { |
447
|
1 |
|
$rows = [$filterRow, $headerRow]; |
448
|
17 |
|
} elseif ($this->filterPosition === self::FILTER_POS_BODY) { |
449
|
16 |
|
$rows = [$headerRow, $filterRow]; |
450
|
|
|
} else { |
451
|
1 |
|
$rows = [$headerRow]; |
452
|
|
|
} |
453
|
|
|
|
454
|
83 |
|
$blocks[] = Html::thead()->rows(...$rows)->render(); |
455
|
|
|
} |
456
|
|
|
|
457
|
84 |
|
if ($this->footerEnabled) { |
458
|
3 |
|
$tags = []; |
459
|
3 |
|
foreach ($columns as $i => $column) { |
460
|
3 |
|
$cell = $renderers[$i]->renderFooter( |
461
|
3 |
|
$column, |
462
|
3 |
|
(new Cell())->content(' ')->encode(false), |
463
|
3 |
|
$globalContext |
464
|
3 |
|
); |
465
|
|
|
/** @var string|Stringable $content */ |
466
|
3 |
|
$content = $cell->getContent(); |
467
|
3 |
|
$tags[] = Html::td(attributes: $cell->getAttributes()) |
468
|
3 |
|
->content($content) |
469
|
3 |
|
->encode($cell->isEncode()) |
470
|
3 |
|
->doubleEncode($cell->isDoubleEncode()); |
471
|
|
|
} |
472
|
3 |
|
$footerRow = Html::tr($this->footerRowAttributes)->cells(...$tags); |
473
|
|
|
|
474
|
3 |
|
$rows = [$footerRow]; |
475
|
3 |
|
if ($this->filterPosition === self::FILTER_POS_FOOTER) { |
476
|
|
|
/** @var Tr */ |
477
|
1 |
|
$rows[] = $filterRow; |
478
|
|
|
} |
479
|
|
|
|
480
|
3 |
|
$blocks[] = Html::tfoot()->rows(...$rows)->render(); |
481
|
|
|
} |
482
|
|
|
|
483
|
84 |
|
$rows = []; |
484
|
84 |
|
$index = 0; |
485
|
84 |
|
foreach ($this->getItems() as $key => $value) { |
486
|
77 |
|
if ($this->beforeRow !== null) { |
487
|
|
|
/** @var Tr|null $row */ |
488
|
1 |
|
$row = call_user_func($this->beforeRow, $value, $key, $index, $this); |
489
|
1 |
|
if (!empty($row)) { |
490
|
1 |
|
$rows[] = $row; |
491
|
|
|
} |
492
|
|
|
} |
493
|
|
|
|
494
|
77 |
|
$tags = []; |
495
|
77 |
|
foreach ($columns as $i => $column) { |
496
|
76 |
|
$context = new DataContext($column, $value, $key, $index); |
497
|
76 |
|
$cell = $renderers[$i]->renderBody($column, new Cell(), $context); |
498
|
76 |
|
$contentSource = $cell->getContent(); |
499
|
|
|
/** @var string|Stringable $content */ |
500
|
76 |
|
$content = $contentSource instanceof Closure |
501
|
|
|
? $contentSource($context) |
502
|
76 |
|
: $contentSource; |
503
|
76 |
|
$tags[] = empty($content) |
504
|
2 |
|
? Html::td()->content($this->emptyCell)->encode(false) |
505
|
75 |
|
: Html::td(attributes: $this->prepareBodyAttributes($cell->getAttributes(), $context)) |
506
|
75 |
|
->content($content) |
507
|
75 |
|
->encode($cell->isEncode()) |
508
|
75 |
|
->doubleEncode($cell->isDoubleEncode()); |
509
|
|
|
} |
510
|
77 |
|
$rows[] = Html::tr($this->rowAttributes)->cells(...$tags); |
511
|
|
|
|
512
|
77 |
|
if ($this->afterRow !== null) { |
513
|
|
|
/** @var Tr|null $row */ |
514
|
1 |
|
$row = call_user_func($this->afterRow, $value, $key, $index, $this); |
515
|
1 |
|
if (!empty($row)) { |
516
|
1 |
|
$rows[] = $row; |
517
|
|
|
} |
518
|
|
|
} |
519
|
|
|
|
520
|
77 |
|
$index++; |
521
|
|
|
} |
522
|
84 |
|
$blocks[] = empty($rows) |
523
|
7 |
|
? Html::tbody($this->tbodyAttributes) |
524
|
7 |
|
->rows(Html::tr()->cells($this->renderEmpty(count($columns)))) |
525
|
7 |
|
->render() |
526
|
77 |
|
: Html::tbody($this->tbodyAttributes)->rows(...$rows)->render(); |
527
|
|
|
|
528
|
84 |
|
return Html::tag('table', attributes: $this->tableAttributes)->open() |
529
|
84 |
|
. PHP_EOL |
530
|
84 |
|
. implode(PHP_EOL, $blocks) |
531
|
84 |
|
. PHP_EOL |
532
|
84 |
|
. '</table>'; |
533
|
|
|
} |
534
|
|
|
|
535
|
|
|
/** |
536
|
|
|
* This function tries to guess the columns to show from the given data if {@see columns} are not explicitly |
537
|
|
|
* specified. |
538
|
|
|
* |
539
|
|
|
* @psalm-return list<ColumnInterface> |
540
|
|
|
*/ |
541
|
3 |
|
private function guessColumns(): array |
542
|
|
|
{ |
543
|
3 |
|
$items = $this->getItems(); |
544
|
|
|
|
545
|
3 |
|
$columns = []; |
546
|
3 |
|
foreach ($items as $item) { |
547
|
|
|
/** |
548
|
|
|
* @var string $name |
549
|
|
|
* @var mixed $value |
550
|
|
|
*/ |
551
|
1 |
|
foreach ($item as $name => $value) { |
552
|
1 |
|
if ($value === null || is_scalar($value) || is_callable([$value, '__toString'])) { |
553
|
1 |
|
$columns[] = new DataColumn(property: $name); |
554
|
|
|
} |
555
|
|
|
} |
556
|
1 |
|
break; |
557
|
|
|
} |
558
|
|
|
|
559
|
3 |
|
if (!empty($items)) { |
560
|
1 |
|
$columns[] = new ActionColumn(); |
561
|
|
|
} |
562
|
|
|
|
563
|
3 |
|
return $columns; |
564
|
|
|
} |
565
|
|
|
|
566
|
75 |
|
private function prepareBodyAttributes(array $attributes, DataContext $context): array |
567
|
|
|
{ |
568
|
75 |
|
foreach ($attributes as $i => $attribute) { |
569
|
5 |
|
if (is_callable($attribute)) { |
570
|
1 |
|
$attributes[$i] = $attribute($context); |
571
|
|
|
} |
572
|
|
|
} |
573
|
|
|
|
574
|
75 |
|
return $attributes; |
575
|
|
|
} |
576
|
|
|
|
577
|
81 |
|
private function getColumnRenderer(ColumnInterface $column): ColumnRendererInterface |
578
|
|
|
{ |
579
|
|
|
/** @var ColumnRendererInterface */ |
580
|
81 |
|
return $this->columnRenderersContainer->get($column->getRenderer()); |
581
|
|
|
} |
582
|
|
|
} |
583
|
|
|
|