1: <?php
2:
3: 4: 5: 6: 7: 8: 9:
10:
11: namespace Nextras\Datagrid;
12:
13: use Nette;
14: use Nette\Application\UI;
15: use Nette\Utils\Paginator;
16: use Nette\Localization\ITranslator;
17:
18:
19:
20: class Datagrid extends UI\Control
21: {
22:
23: const ORDER_ASC = 'asc';
24:
25:
26: const ORDER_DESC = 'desc';
27:
28:
29: public $filter = array();
30:
31:
32: public $orderColumn;
33:
34:
35: public $orderType = self::ORDER_ASC;
36:
37:
38: public $page = 1;
39:
40:
41: protected $filterDataSource = array();
42:
43:
44: protected $columns = array();
45:
46:
47: protected $columnGetterCallback;
48:
49:
50: protected $dataSourceCallback;
51:
52:
53: protected $editFormFactory;
54:
55:
56: protected $editFormCallback;
57:
58:
59: protected $filterFormFactory;
60:
61:
62: protected $paginator;
63:
64:
65: protected $translator;
66:
67:
68: protected $paginatorItemsCountCallback;
69:
70:
71: protected $editRowKey;
72:
73:
74: protected $rowPrimaryKey;
75:
76:
77: protected $data;
78:
79:
80: protected $cellsTemplates = array();
81:
82:
83:
84: 85: 86: 87: 88: 89:
90: public function addColumn($name, $label = NULL)
91: {
92: if (!$this->rowPrimaryKey) {
93: $this->rowPrimaryKey = $name;
94: }
95:
96: $label = $label ? $this->translate($label) : ucfirst($name);
97: return $this->columns[] = new Column($name, $label, $this);
98: }
99:
100:
101:
102: public function setRowPrimaryKey($columnName)
103: {
104: $this->rowPrimaryKey = (string) $columnName;
105: }
106:
107:
108:
109: public function getRowPrimaryKey()
110: {
111: return $this->rowPrimaryKey;
112: }
113:
114:
115:
116: public function setColumnGetterCallback($getterCallback)
117: {
118: $this->columnGetterCallback = new Nette\Callback($getterCallback);
119: }
120:
121:
122:
123: public function getColumnGetterCallback()
124: {
125: return $this->columnGetterCallback;
126: }
127:
128:
129:
130: public function setDataSourceCallback($dataSourceCallback)
131: {
132: $this->dataSourceCallback = new Nette\Callback($dataSourceCallback);
133: }
134:
135:
136:
137: public function getDataSourceCallback()
138: {
139: return $this->dataSourceCallback;
140: }
141:
142:
143:
144: public function setEditFormFactory($editFormFactory)
145: {
146: $this->editFormFactory = $editFormFactory;
147: }
148:
149:
150:
151: public function getEditFormFactory()
152: {
153: return $this->editFormFactory;
154: }
155:
156:
157:
158: public function setEditFormCallback($editFormCallback)
159: {
160: $this->editFormCallback = $editFormCallback;
161: }
162:
163:
164:
165: public function getEditFormCallback()
166: {
167: return $this->editFormCallback;
168: }
169:
170:
171:
172: public function setFilterFormFactory($filterFormFactory)
173: {
174: $this->filterFormFactory = new Nette\Callback($filterFormFactory);
175: }
176:
177:
178:
179: public function getFilterFormFactory()
180: {
181: return $this->filterFormFactory;
182: }
183:
184:
185:
186: public function ($itemsPerPage, $itemsCountCallback = NULL)
187: {
188: if ($itemsPerPage === FALSE) {
189: $this->paginator = NULL;
190: $this->paginatorItemsCountCallback = NULL;
191: } else {
192: if ($itemsCountCallback === NULL) {
193: throw new \InvalidArgumentException('Items count callback must be set.');
194: }
195:
196: $this->paginator = new Paginator();
197: $this->paginator->itemsPerPage = $itemsPerPage;
198: $this->paginatorItemsCountCallback = new Nette\Callback($itemsCountCallback);
199: }
200: }
201:
202:
203:
204: public function addCellsTemplate($path)
205: {
206: $this->cellsTemplates[] = $path;
207: }
208:
209:
210:
211: public function getCellsTemplate()
212: {
213: return $this->cellsTemplates;
214: }
215:
216:
217:
218: public function setTranslator(ITranslator $translator)
219: {
220: $this->translator = $translator;
221: }
222:
223:
224:
225: public function getTranslator()
226: {
227: return $this->translator;
228: }
229:
230:
231:
232: public function translate($s, $count = NULL)
233: {
234: $translator = $this->getTranslator();
235: return $translator === NULL || $s == NULL || $s instanceof Html
236: ? $s
237: : $translator->translate((string) $s, $count);
238: }
239:
240:
241:
242:
243:
244:
245:
246: public function render()
247: {
248: if ($this->filterFormFactory) {
249: $this['form']['filter']->setDefaults($this->filter);
250: }
251:
252: $this->template->data = $this->getData();
253: $this->template->columns = $this->columns;
254: $this->template->editRowKey = $this->editRowKey;
255: $this->template->rowPrimaryKey = $this->rowPrimaryKey;
256: $this->template->paginator = $this->paginator;
257:
258: foreach ($this->cellsTemplates as &$cellsTemplate) {
259: if ($cellsTemplate instanceof Nette\Templating\IFileTemplate) {
260: $cellsTemplate = $cellsTemplate->getFile();
261: }
262: if (!file_exists($cellsTemplate)) {
263: throw new \RuntimeException("Cells template '{$cellsTemplate}' does not exist.");
264: }
265: }
266:
267: $this->template->cellsTemplates = $this->cellsTemplates;
268: $this->template->setFile(__DIR__ . '/Datagrid.latte');
269: $this->template->render();
270: }
271:
272:
273:
274: public function invalidateRow($primaryValue)
275: {
276: if ($this->presenter->isAjax()) {
277: if (isset($this->filterDataSource[$this->rowPrimaryKey]) && is_string($this->filterDataSource[$this->rowPrimaryKey])) {
278: $this->filterDataSource[$this->rowPrimaryKey] = array($this->filterDataSource[$this->rowPrimaryKey]);
279: }
280:
281: $this->filterDataSource[$this->rowPrimaryKey][] = $primaryValue;
282: parent::invalidateControl('rows');
283: $this->invalidateControl('rows-' . $primaryValue);
284: }
285: }
286:
287:
288:
289: public function invalidateControl($snippet = NULL)
290: {
291: parent::invalidateControl($snippet);
292: if ($snippet === NULL || $snippet === 'rows') {
293: $this->template->echoSnippets = TRUE;
294: }
295: }
296:
297:
298:
299:
300:
301:
302:
303: protected function attached($presenter)
304: {
305: parent::attached($presenter);
306: $this->filterDataSource = $this->filter;
307: }
308:
309:
310:
311: protected function getData($key = NULL)
312: {
313: if (!$this->data) {
314: $onlyRow = $key && $this->presenter->isAjax();
315: if (!$onlyRow && $this->paginator) {
316: $itemsCount = $this->paginatorItemsCountCallback->invokeArgs(array(
317: $this->filter,
318: $this->orderColumn ? array($this->orderColumn, strtoupper($this->orderType)) : NULL,
319: ));
320:
321: $this->paginator->setItemCount($itemsCount);
322: if ($this->paginator->page !== $this->page) {
323: $this->paginator->page = $this->page = 1;
324: }
325: }
326:
327: $this->data = $this->dataSourceCallback->invokeArgs(array(
328: $this->filterDataSource,
329: $this->orderColumn ? array($this->orderColumn, strtoupper($this->orderType)) : NULL,
330: $onlyRow ? NULL : $this->paginator,
331: ));
332: }
333:
334: if ($key === NULL) {
335: return $this->data;
336: }
337:
338: foreach ($this->data as $row) {
339: if ($this->getter($row, $this->rowPrimaryKey) == $key) {
340: return $row;
341: }
342: }
343:
344: throw new \Exception('Row not found');
345: }
346:
347:
348:
349: 350: 351: 352:
353: public function getter($row, $column, $need = TRUE)
354: {
355: if ($this->columnGetterCallback) {
356: return $this->columnGetterCallback->invokeArgs(array($row, $column));
357: } else {
358: if (!isset($row->$column)) {
359: if ($need) {
360: throw new \InvalidArgumentException("Result row does not have '{$column}' column.");
361: } else {
362: return NULL;
363: }
364: }
365:
366: return $row->$column;
367: }
368: }
369:
370:
371:
372: public function handleEdit($primaryValue, $cancelEditPrimaryValue = NULL)
373: {
374: $this->editRowKey = $primaryValue;
375: $this->invalidateRow($primaryValue);
376: if ($cancelEditPrimaryValue) {
377: foreach (explode(',', $cancelEditPrimaryValue) as $pv) {
378: $this->invalidateRow($pv);
379: }
380: }
381: }
382:
383:
384:
385: public function handleSort()
386: {
387: if ($this->presenter->isAjax()) {
388: $this->invalidateControl('rows');
389: } else {
390: $this->redirect('this');
391: }
392: }
393:
394:
395:
396: public function createComponentForm()
397: {
398: $form = new UI\Form;
399:
400: if ($this->editFormFactory && ($this->editRowKey || !empty($_POST['edit']))) {
401: $data = $this->editRowKey && empty($_POST) ? $this->getData($this->editRowKey) : NULL;
402: $form['edit'] = Nette\Callback::create($this->editFormFactory)->invokeArgs(array($data));
403:
404: if (!isset($form['edit']['save']))
405: $form['edit']->addSubmit('save', 'Save');
406: if (!isset($form['edit']['cancel']))
407: $form['edit']->addSubmit('cancel', 'Cancel');
408: if (!isset($form['edit'][$this->rowPrimaryKey]))
409: $form['edit']->addHidden($this->rowPrimaryKey);
410:
411: $form['edit'][$this->rowPrimaryKey]
412: ->setDefaultValue($this->editRowKey)
413: ->setOption('rendered', TRUE);
414: }
415:
416: if ($this->filterFormFactory) {
417: $form['filter'] = $this->filterFormFactory->invoke();
418: if (!isset($form['filter']['filter'])) {
419: $form['filter']->addSubmit('filter', 'Filter');
420: }
421: if (!isset($form['filter']['cancel'])) {
422: $form['filter']->addSubmit('cancel', 'Cancel');
423: }
424: }
425:
426: $form->onSuccess[] = function() {};
427: $form->onSubmit[] = $this->processForm;
428: return $form;
429: }
430:
431:
432:
433: public function processForm(UI\Form $form)
434: {
435: $allowRedirect = TRUE;
436: if (isset($form['edit'])) {
437: if ($form['edit']['save']->isSubmittedBy()) {
438: if ($form['edit']->isValid()) {
439: Nette\Callback::create($this->editFormCallback)->invokeArgs(array(
440: $form['edit']
441: ));
442: } else {
443: $this->editRowKey = $form['edit'][$this->rowPrimaryKey]->getValue();
444: $allowRedirect = FALSE;
445: }
446: }
447: if ($form['edit']['cancel']->isSubmittedBy() || ($form['edit']['save']->isSubmittedBy() && $form['edit']->isValid())) {
448: $editRowKey = $form['edit'][$this->rowPrimaryKey]->getValue();
449: $this->invalidateRow($editRowKey);
450: $this->getData($editRowKey);
451: }
452: if ($this->editRowKey) {
453: $this->invalidateRow($this->editRowKey);
454: }
455: }
456:
457: if (isset($form['filter'])) {
458: if ($form['filter']['filter']->isSubmittedBy()) {
459: $values = $form['filter']->getValues(TRUE);
460: unset($values['filter']);
461: $values = array_filter($values, function($val) {
462: return strlen($val) > 0;
463: });
464: if ($this->paginator) {
465: $this->page = $this->paginator->page = 1;
466: }
467: $this->filter = $this->filterDataSource = $values;
468: $this->invalidateControl('rows');
469: } elseif ($form['filter']['cancel']->isSubmittedBy()) {
470: if ($this->paginator) {
471: $this->page = $this->paginator->page = 1;
472: }
473: $this->filter = $this->filterDataSource = array();
474: $form['filter']->setValues(array(), TRUE);
475: $this->invalidateControl('rows');
476: }
477: }
478:
479: if (!$this->presenter->isAjax() && $allowRedirect) {
480: $this->redirect('this');
481: }
482: }
483:
484:
485:
486: public function loadState(array $params)
487: {
488: parent::loadState($params);
489: if ($this->paginator) {
490: $this->paginator->page = $this->page;
491: }
492: }
493:
494:
495:
496: protected function createTemplate($class = NULL)
497: {
498: $template = parent::createTemplate($class);
499: if ($translator = $this->getTranslator()) {
500: $template->setTranslator($translator);
501: }
502: return $template;
503: }
504:
505:
506:
507: public function handlePaginate()
508: {
509: if ($this->presenter->isAjax()) {
510: $this->invalidateControl('rows');
511: } else {
512: $this->redirect('this');
513: }
514: }
515:
516: }
517: