1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
*## TbGroupGridView class file |
4
|
|
|
* |
5
|
|
|
* @author Vitaliy Potapov <[email protected]> |
6
|
|
|
* @version 1.1 |
7
|
|
|
* @see http://groupgridview.demopage.ru/ |
8
|
|
|
* |
9
|
|
|
* @since 24/09/2012 added to yiibooster library |
10
|
|
|
* @author antonio ramirez <[email protected]> |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
Yii::import('booster.widgets.TbGridView'); |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
*## TbGroupGridView widget |
17
|
|
|
* |
18
|
|
|
* A Grid View that groups rows by any column(s) |
19
|
|
|
* |
20
|
|
|
* @property TbDataColumn[] $columns |
21
|
|
|
* |
22
|
|
|
* @package booster.widgets.grids |
23
|
|
|
*/ |
24
|
|
|
class TbGroupGridView extends TbGridView |
25
|
|
|
{ |
26
|
|
|
|
27
|
|
|
const MERGE_SIMPLE = 'simple'; |
28
|
|
|
const MERGE_NESTED = 'nested'; |
29
|
|
|
const MERGE_FIRSTROW = 'firstrow'; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* @var array $mergeColumns the columns to merge on the grid |
33
|
|
|
*/ |
34
|
|
|
public $mergeColumns = array(); |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* @var string $mergeType the merge type. Defaults to MERGE_SIMPLE |
38
|
|
|
*/ |
39
|
|
|
public $mergeType = self::MERGE_SIMPLE; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* @var string $mergeCellsCss the styles to apply to merged cells |
43
|
|
|
*/ |
44
|
|
|
public $mergeCellCss = 'text-align: center; vertical-align: middle'; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* @var array $extraRowColumns the group column names |
48
|
|
|
*/ |
49
|
|
|
public $extraRowColumns = array(); |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @var string $extraRowExpression |
53
|
|
|
*/ |
54
|
|
|
public $extraRowExpression; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @var array the HTML options for the extrarow cell tag. |
58
|
|
|
*/ |
59
|
|
|
public $extraRowHtmlOptions = array(); |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* @var string $extraRowCssClass the class to be used to be set on the extrarow cell tag. |
63
|
|
|
*/ |
64
|
|
|
public $extraRowCssClass = 'extrarow'; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* @var array the column data changes |
68
|
|
|
*/ |
69
|
|
|
private $_changes; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* Widget initialization |
73
|
|
|
*/ |
74
|
|
|
public function init() |
75
|
|
|
{ |
76
|
|
|
parent::init(); |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* check whether we have extraRowColumns set, forbid filters |
80
|
|
|
*/ |
81
|
|
|
if (!empty($this->extraRowColumns)) { |
82
|
|
|
foreach ($this->columns as $column) { |
83
|
|
|
if ($column instanceof CDataColumn && in_array($column->name, $this->extraRowColumns)) { |
|
|
|
|
84
|
|
|
$column->filterHtmlOptions = array('style' => 'display:none'); |
85
|
|
|
$column->filter = false; |
86
|
|
|
} |
87
|
|
|
} |
88
|
|
|
} |
89
|
|
|
/** |
90
|
|
|
* setup extra row options |
91
|
|
|
*/ |
92
|
|
|
if (isset($this->extraRowHtmlOptions['class']) && !empty($this->extraRowCssClass)) { |
93
|
|
|
$this->extraRowHtmlOptions['class'] .= ' ' . $this->extraRowCssClass; |
94
|
|
|
} else { |
95
|
|
|
$this->extraRowHtmlOptions['class'] = $this->extraRowCssClass; |
96
|
|
|
} |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* Registers necessary client scripts. |
101
|
|
|
*/ |
102
|
|
|
public function registerClientScript() |
103
|
|
|
{ |
104
|
|
|
$id=$this->getId(); |
105
|
|
|
|
106
|
|
|
if($this->ajaxUpdate===false) |
107
|
|
|
$ajaxUpdate=false; |
108
|
|
|
else |
109
|
|
|
$ajaxUpdate=array_unique(preg_split('/\s*,\s*/',$this->ajaxUpdate.','.$id,-1,PREG_SPLIT_NO_EMPTY)); |
110
|
|
|
$options=array( |
111
|
|
|
'ajaxUpdate'=>$ajaxUpdate, |
112
|
|
|
'ajaxVar'=>$this->ajaxVar, |
113
|
|
|
'pagerClass'=>$this->pagerCssClass, |
114
|
|
|
'loadingClass'=>$this->loadingCssClass, |
115
|
|
|
'filterClass'=>$this->filterCssClass, |
116
|
|
|
'tableClass'=>$this->itemsCssClass, |
117
|
|
|
'selectableRows'=>$this->selectableRows, |
118
|
|
|
'enableHistory'=>$this->enableHistory, |
119
|
|
|
'updateSelector'=>$this->updateSelector, |
120
|
|
|
'filterSelector'=>$this->filterSelector |
121
|
|
|
); |
122
|
|
|
if($this->ajaxUrl!==null) |
123
|
|
|
$options['url']=CHtml::normalizeUrl($this->ajaxUrl); |
124
|
|
|
if($this->ajaxType!==null) |
125
|
|
|
$options['ajaxType']=strtoupper($this->ajaxType); |
126
|
|
|
if($this->enablePagination) |
127
|
|
|
$options['pageVar']=$this->dataProvider->getPagination()->pageVar; |
128
|
|
|
foreach(array('beforeAjaxUpdate', 'afterAjaxUpdate', 'ajaxUpdateError', 'selectionChanged') as $event) |
129
|
|
|
{ |
130
|
|
|
if($this->$event!==null) |
131
|
|
|
{ |
132
|
|
|
if($this->$event instanceof CJavaScriptExpression) |
|
|
|
|
133
|
|
|
$options[$event]=$this->$event; |
134
|
|
|
else |
135
|
|
|
$options[$event]=new CJavaScriptExpression($this->$event); |
136
|
|
|
} |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
$options=CJavaScript::encode($options); |
140
|
|
|
$cs=Yii::app()->getClientScript(); |
141
|
|
|
$cs->registerCoreScript('jquery'); |
142
|
|
|
$cs->registerCoreScript('bbq'); |
143
|
|
|
if($this->enableHistory) |
144
|
|
|
$cs->registerCoreScript('history'); |
145
|
|
|
$cs->registerPackage('group-grid-view'); |
146
|
|
|
$cs->registerScript(__CLASS__.'#'.$id,"jQuery('#$id').yiiGroupGridView($options);"); |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* Renders the table body. |
151
|
|
|
*/ |
152
|
|
|
public function renderTableBody() |
153
|
|
|
{ |
154
|
|
|
if (!empty($this->mergeColumns) || !empty($this->extraRowColumns)) { |
155
|
|
|
$this->groupByColumns(); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
parent::renderTableBody(); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* find and store changing of group columns |
163
|
|
|
*/ |
164
|
|
|
public function groupByColumns() |
165
|
|
|
{ |
166
|
|
|
$data = $this->dataProvider->getData(); |
167
|
|
|
if (count($data) == 0) { |
168
|
|
|
return; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
if (!is_array($this->mergeColumns)) { |
172
|
|
|
$this->mergeColumns = array($this->mergeColumns); |
173
|
|
|
} |
174
|
|
|
if (!is_array($this->extraRowColumns)) { |
175
|
|
|
$this->extraRowColumns = array($this->extraRowColumns); |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
//store columns for group. Set object for existing columns in grid and string for attributes |
179
|
|
|
$groupColumns = array_unique(array_merge($this->mergeColumns, $this->extraRowColumns)); |
180
|
|
|
foreach ($groupColumns as $key => $colName) { |
181
|
|
|
foreach ($this->columns as $column) { |
182
|
|
|
if (property_exists($column, 'name') && $column->name == $colName) { |
183
|
|
|
$groupColumns[$key] = $column; |
184
|
|
|
break; |
185
|
|
|
} |
186
|
|
|
} |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
|
190
|
|
|
//values for first row |
191
|
|
|
$lastStored = $this->getRowValues($groupColumns, $data[0], 0); |
192
|
|
|
foreach ($lastStored as $colName => $value) { |
193
|
|
|
$lastStored[$colName] = array( |
194
|
|
|
'value' => $value, |
195
|
|
|
'count' => 1, |
196
|
|
|
'index' => 0, |
197
|
|
|
); |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
//iterate data |
201
|
|
|
$rowcount = count($data); |
202
|
|
|
for ($i = 1; $i < $rowcount; $i++) { |
203
|
|
|
//save row values in array |
204
|
|
|
$current = $this->getRowValues($groupColumns, $data[$i], $i); |
205
|
|
|
|
206
|
|
|
//define is change occured. Need this extra foreach for correctly proceed extraRows |
207
|
|
|
$changedColumns = array(); |
208
|
|
|
foreach ($current as $colName => $curValue) { |
209
|
|
|
if ($curValue != $lastStored[$colName]['value']) { |
210
|
|
|
$changedColumns[] = $colName; |
211
|
|
|
} |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* if this flag = true -> we will write change (to $this->_changes) for all grouping columns. |
216
|
|
|
* It's required when change of any column from extraRowColumns occurs |
217
|
|
|
*/ |
218
|
|
|
$saveChangeForAllColumns = (count(array_intersect($changedColumns, $this->extraRowColumns)) > 0); |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* this changeOccurred related to foreach below. It is required only for mergeType == self::MERGE_NESTED, |
222
|
|
|
* to write change for all nested columns when change of previous column occurred |
223
|
|
|
*/ |
224
|
|
|
$changeOccurred = false; |
225
|
|
|
foreach ($current as $colName => $curValue) { |
226
|
|
|
//value changed |
227
|
|
|
$valueChanged = ($curValue != $lastStored[$colName]['value']); |
228
|
|
|
//change already occured in this loop and mergeType set to MERGETYPE_NESTED |
229
|
|
|
$saveChange = $valueChanged || ($changeOccurred && $this->mergeType == self::MERGE_NESTED); |
230
|
|
|
|
231
|
|
|
if ($saveChangeForAllColumns || $saveChange) { |
232
|
|
|
$changeOccurred = true; |
233
|
|
|
|
234
|
|
|
//store in class var |
235
|
|
|
$prevIndex = $lastStored[$colName]['index']; |
236
|
|
|
$this->_changes[$prevIndex]['columns'][$colName] = $lastStored[$colName]; |
237
|
|
|
if (!isset($this->_changes[$prevIndex]['count'])) { |
238
|
|
|
$this->_changes[$prevIndex]['count'] = $lastStored[$colName]['count']; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
//update lastStored for particular column |
242
|
|
|
$lastStored[$colName] = array( |
243
|
|
|
'value' => $curValue, |
244
|
|
|
'count' => 1, |
245
|
|
|
'index' => $i, |
246
|
|
|
); |
247
|
|
|
|
248
|
|
|
} else { |
249
|
|
|
$lastStored[$colName]['count']++; |
250
|
|
|
} |
251
|
|
|
} |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
//storing for last row |
255
|
|
|
foreach ($lastStored as $colName => $v) { |
256
|
|
|
$prevIndex = $v['index']; |
257
|
|
|
$this->_changes[$prevIndex]['columns'][$colName] = $v; |
258
|
|
|
|
259
|
|
|
if (!isset($this->_changes[$prevIndex]['count'])) { |
260
|
|
|
$this->_changes[$prevIndex]['count'] = $v['count']; |
261
|
|
|
} |
262
|
|
|
} |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* Renders a table body row. |
267
|
|
|
* |
268
|
|
|
* @param int $row |
269
|
|
|
*/ |
270
|
|
|
public function renderTableRow($row) |
271
|
|
|
{ |
272
|
|
|
$change = false; |
273
|
|
|
if ($this->_changes && array_key_exists($row, $this->_changes)) { |
|
|
|
|
274
|
|
|
$change = $this->_changes[$row]; |
275
|
|
|
//if change in extracolumns --> put extra row |
276
|
|
|
$columnsInExtra = array_intersect(array_keys($change['columns']), $this->extraRowColumns); |
277
|
|
|
if (count($columnsInExtra) > 0) { |
278
|
|
|
$this->renderExtraRow($row, $change, $columnsInExtra); |
279
|
|
|
} |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
// original CGridView code |
283
|
|
|
$htmlOptions = array(); |
284
|
|
|
if ($this->rowHtmlOptionsExpression !== null) { |
285
|
|
|
$data = $this->dataProvider->data[$row]; |
286
|
|
|
$options = $this->evaluateExpression( |
287
|
|
|
$this->rowHtmlOptionsExpression, |
288
|
|
|
array('row' => $row, 'data' => $data) |
289
|
|
|
); |
290
|
|
|
if (is_array($options)) { |
291
|
|
|
$htmlOptions = $options; |
292
|
|
|
} |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
if ($this->rowCssClassExpression !== null) { |
296
|
|
|
$data = $this->dataProvider->data[$row]; |
297
|
|
|
$class = $this->evaluateExpression($this->rowCssClassExpression, array('row' => $row, 'data' => $data)); |
298
|
|
|
} elseif (is_array($this->rowCssClass) && ($n = count($this->rowCssClass)) > 0) { |
299
|
|
|
$class = $this->rowCssClass[$row % $n]; |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
if (!empty($class)) { |
303
|
|
|
if (isset($htmlOptions['class'])) { |
304
|
|
|
$htmlOptions['class'] .= ' ' . $class; |
305
|
|
|
} else { |
306
|
|
|
$htmlOptions['class'] = $class; |
307
|
|
|
} |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
echo CHtml::openTag('tr', $htmlOptions); |
311
|
|
|
if (!$this->_changes) { //standart CGridview's render |
|
|
|
|
312
|
|
|
foreach ($this->columns as $column) { |
313
|
|
|
$column->renderDataCell($row); |
314
|
|
|
} |
315
|
|
|
} else { //for grouping |
316
|
|
|
foreach ($this->columns as $column) { |
317
|
|
|
$isGroupColumn = property_exists($column, 'name') && in_array($column->name, $this->mergeColumns); |
318
|
|
|
if (!$isGroupColumn) { |
319
|
|
|
$column->renderDataCell($row); |
320
|
|
|
continue; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
$isChangedColumn = $change && array_key_exists($column->name, $change['columns']); |
324
|
|
|
|
325
|
|
|
//for rowspan show only changes (with rowspan) |
326
|
|
|
switch ($this->mergeType) { |
327
|
|
|
case self::MERGE_SIMPLE: |
328
|
|
|
case self::MERGE_NESTED: |
329
|
|
|
if ($isChangedColumn) { |
330
|
|
|
$options = $column->htmlOptions; |
331
|
|
|
$column->htmlOptions['rowspan'] = $change['columns'][$column->name]['count']; |
332
|
|
|
$column->htmlOptions['class'] = 'merge'; |
333
|
|
|
$style = isset($column->htmlOptions['style']) ? $column->htmlOptions['style'] : ''; |
334
|
|
|
$column->htmlOptions['style'] = $style . ';' . $this->mergeCellCss; |
335
|
|
|
$column->renderDataCell($row); |
336
|
|
|
$column->htmlOptions = $options; |
337
|
|
|
} |
338
|
|
|
break; |
339
|
|
|
|
340
|
|
|
case self::MERGE_FIRSTROW: |
341
|
|
|
if ($isChangedColumn) { |
342
|
|
|
$column->renderDataCell($row); |
343
|
|
|
} else { |
344
|
|
|
echo '<td></td>'; |
345
|
|
|
} |
346
|
|
|
break; |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
} |
350
|
|
|
} |
351
|
|
|
|
352
|
|
|
echo "</tr>\n"; |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* returns array of rendered column values (TD) |
357
|
|
|
* |
358
|
|
|
* @param string[]|TbDataColumn[] $columns |
359
|
|
|
* @param CActiveRecord $data |
360
|
|
|
* @param integer $rowIndex |
361
|
|
|
* |
362
|
|
|
* @throws CException |
363
|
|
|
* @return mixed |
364
|
|
|
*/ |
365
|
|
|
private function getRowValues($columns, $data, $rowIndex) |
366
|
|
|
{ |
367
|
|
|
$result = array(); |
368
|
|
|
foreach ($columns as $column) { |
369
|
|
|
if ($column instanceOf TbDataColumn) { |
370
|
|
|
$result[$column->name] = $this->getDataCellContent($column, $data, $rowIndex); |
371
|
|
|
} elseif (is_string($column)) { |
372
|
|
|
if (is_array($data) && array_key_exists($column, $data)) { |
373
|
|
|
$result[$column] = $data[$column]; |
374
|
|
|
} elseif ($data instanceOf CActiveRecord && $data->hasAttribute($column)) { |
375
|
|
|
$result[$column] = $data->getAttribute($column); |
376
|
|
|
} else { |
377
|
|
|
throw new CException('Column or attribute "' . $column . '" not found!'); |
378
|
|
|
} |
379
|
|
|
} |
380
|
|
|
} |
381
|
|
|
return $result; |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
/** |
385
|
|
|
* renders extra row |
386
|
|
|
* |
387
|
|
|
* @param integer $beforeRow |
388
|
|
|
* @param mixed $change |
389
|
|
|
* @param array $columnsInExtra |
390
|
|
|
*/ |
391
|
|
|
private function renderExtraRow($beforeRow, $change, $columnsInExtra) |
392
|
|
|
{ |
393
|
|
|
$data = $this->dataProvider->data[$beforeRow]; |
394
|
|
|
if ($this->extraRowExpression) { //user defined expression, use it! |
395
|
|
|
$content = $this->evaluateExpression( |
396
|
|
|
$this->extraRowExpression, |
397
|
|
|
array('data' => $data, 'row' => $beforeRow, 'values' => $change['columns']) |
398
|
|
|
); |
399
|
|
|
} else { //generate value |
400
|
|
|
$values = array(); |
401
|
|
|
foreach ($columnsInExtra as $c) { |
402
|
|
|
$values[] = $change['columns'][$c]['value']; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
$content = '<strong>' . implode(' :: ', $values) . '</strong>'; |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
$colspan = count($this->columns); |
409
|
|
|
|
410
|
|
|
echo '<tr class="extrarow">'; |
411
|
|
|
$this->extraRowHtmlOptions['colspan'] = $colspan; |
412
|
|
|
echo CHtml::openTag('td', $this->extraRowHtmlOptions); |
413
|
|
|
echo $content; |
414
|
|
|
echo CHtml::closeTag('td'); |
415
|
|
|
echo '</tr>'; |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
/** |
419
|
|
|
* need to rewrite this function as it is protected in CDataColumn: it is strange as all methods inside are public |
420
|
|
|
* |
421
|
|
|
* @param TbDataColumn $column |
422
|
|
|
* @param mixed $row |
423
|
|
|
* @param mixed $data |
424
|
|
|
* |
425
|
|
|
* @return string |
426
|
|
|
*/ |
427
|
|
|
private function getDataCellContent($column, $data, $row) |
428
|
|
|
{ |
429
|
|
|
if ($column->value !== null) { |
430
|
|
|
$value = $column->evaluateExpression($column->value, array('data' => $data, 'row' => $row)); |
431
|
|
|
} else if ($column->name !== null) { |
432
|
|
|
$value = CHtml::value($data, $column->name); |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
return !isset($value) |
436
|
|
|
? $column->grid->nullDisplay |
437
|
|
|
: $column->grid->getFormatter()->format( |
438
|
|
|
$value, |
439
|
|
|
$column->type |
440
|
|
|
); |
441
|
|
|
} |
442
|
|
|
} |
443
|
|
|
|
This error could be the result of:
1. Missing dependencies
PHP Analyzer uses your
composer.json
file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects thecomposer.json
to be in the root folder of your repository.Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the
require
orrequire-dev
section?2. Missing use statement
PHP does not complain about undefined classes in
ìnstanceof
checks. For example, the following PHP code will work perfectly fine:If you have not tested against this specific condition, such errors might go unnoticed.