1 <?php
2
3 4 5 6 7 8 9 10
11
12 namespace Grido;
13
14 use Grido\Components\Columns\Column;
15 use Grido\Components\Filters\Filter;
16 use Grido\Components\Paginator;
17
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
41 class Grid extends Components\Container
42 {
43
44 const BUTTONS = 'buttons';
45
46 const CLIENT_SIDE_OPTIONS = 'grido-options';
47
48
49 public $page = 1;
50
51
52 public $perPage;
53
54
55 public $sort = array();
56
57
58 public $filter = array();
59
60
61 public $onRegistered;
62
63
64 public $onRender;
65
66
67 public $onFetchData;
68
69
70 protected $rowCallback;
71
72
73 protected $tablePrototype;
74
75
76 protected $rememberState = FALSE;
77
78
79 protected $primaryKey = 'id';
80
81
82 protected $filterRenderType;
83
84
85 protected $perPageList = array(10, 20, 30, 50, 100);
86
87
88 protected $defaultPerPage = 20;
89
90
91 protected $defaultFilter = array();
92
93
94 protected $defaultSort = array();
95
96
97 protected $model;
98
99
100 protected $count;
101
102
103 protected $data;
104
105
106 protected $paginator;
107
108
109 protected $translator;
110
111
112 protected $propertyAccessor;
113
114 115 116 117 118 119 120
121 public function setModel($model, $forceWrapper = FALSE)
122 {
123 $this->model = $model instanceof DataSources\IDataSource && $forceWrapper === FALSE
124 ? $model
125 : new DataSources\Model($model);
126
127 return $this;
128 }
129
130 131 132 133 134
135 public function setPropertyAccessor(PropertyAccessors\IPropertyAccessor $propertyAccessor)
136 {
137 $this->propertyAccessor = $propertyAccessor;
138 return $this;
139 }
140
141 142 143 144 145
146 public function setDefaultPerPage($perPage)
147 {
148 $perPage = (int) $perPage;
149 $this->defaultPerPage = $perPage;
150
151 if (!in_array($perPage, $this->perPageList)) {
152 $this->perPageList[] = $perPage;
153 sort($this->perPageList);
154 }
155
156 return $this;
157 }
158
159 160 161 162 163
164 public function setDefaultFilter(array $filter)
165 {
166 $this->defaultFilter = array_merge($this->defaultFilter, $filter);
167 return $this;
168 }
169
170 171 172 173 174 175
176 public function setDefaultSort(array $sort)
177 {
178 static $replace = array('asc' => Column::ORDER_ASC, 'desc' => Column::ORDER_DESC);
179
180 foreach ($sort as $column => $dir) {
181 $dir = strtr(strtolower($dir), $replace);
182 if (!in_array($dir, $replace)) {
183 throw new \InvalidArgumentException("Dir '$dir' for column '$column' is not allowed.");
184 }
185
186 $this->defaultSort[$column] = $dir;
187 }
188
189 return $this;
190 }
191
192 193 194 195 196
197 public function setPerPageList(array $perPageList)
198 {
199 $this->perPageList = $perPageList;
200
201 if ($this->hasFilters(FALSE) || $this->hasOperation(FALSE)) {
202 $this['form']['count']->setItems($this->getItemsForCountSelect());
203 }
204
205 return $this;
206 }
207
208 209 210 211 212
213 public function setTranslator(\Nette\Localization\ITranslator $translator)
214 {
215 $this->translator = $translator;
216 return $this;
217 }
218
219 220 221 222 223 224 225
226 public function setFilterRenderType($type)
227 {
228 $type = strtolower($type);
229 if (!in_array($type, array(Filter::RENDER_INNER, Filter::RENDER_OUTER))) {
230 throw new \InvalidArgumentException('Type must be Filter::RENDER_INNER or Filter::RENDER_OUTER.');
231 }
232
233 $this->filterRenderType = $type;
234 return $this;
235 }
236
237 238 239 240 241
242 public function setPaginator(Paginator $paginator)
243 {
244 $this->paginator = $paginator;
245 return $this;
246 }
247
248 249 250 251 252 253
254 public function setPrimaryKey($key)
255 {
256 $this->primaryKey = $key;
257 return $this;
258 }
259
260 261 262 263 264
265 public function setTemplateFile($file)
266 {
267 $this->getTemplate()->setFile($file);
268 return $this;
269 }
270
271 272 273 274 275
276 public function setRememberState($state = TRUE)
277 {
278 $this->getPresenter();
279 $this->getRememberSession(TRUE);
280 $this->rememberState = (bool) $state;
281
282 return $this;
283 }
284
285 286 287 288 289 290
291 public function setRowCallback($callback)
292 {
293 $this->rowCallback = $callback;
294 return $this;
295 }
296
297 298 299 300 301
302 public function setClientSideOptions(array $options)
303 {
304 $this->getTablePrototype()->data[self::CLIENT_SIDE_OPTIONS] = json_encode($options);
305 return $this;
306 }
307
308
309
310 311 312 313
314 public function getCount()
315 {
316 if ($this->count === NULL) {
317 $this->count = $this->model->getCount();
318 }
319
320 return $this->count;
321 }
322
323 324 325 326
327 public function getDefaultPerPage()
328 {
329 if (!in_array($this->defaultPerPage, $this->perPageList)) {
330 $this->defaultPerPage = $this->perPageList[0];
331 }
332
333 return $this->defaultPerPage;
334 }
335
336 337 338 339
340 public function getDefaultFilter()
341 {
342 return $this->defaultFilter;
343 }
344
345 346 347 348
349 public function getDefaultSort()
350 {
351 return $this->defaultSort;
352 }
353
354 355 356 357
358 public function getPerPageList()
359 {
360 return $this->perPageList;
361 }
362
363 364 365 366
367 public function getPrimaryKey()
368 {
369 return $this->primaryKey;
370 }
371
372 373 374 375
376 public function getRememberState()
377 {
378 return $this->rememberState;
379 }
380
381 382 383 384
385 public function getRowCallback()
386 {
387 return $this->rowCallback;
388 }
389
390 391 392 393
394 public function getPerPage()
395 {
396 return $this->perPage === NULL
397 ? $this->getDefaultPerPage()
398 : $this->perPage;
399 }
400
401 402 403 404 405
406 public function getActualFilter($key = NULL)
407 {
408 $filter = $this->filter ? $this->filter : $this->defaultFilter;
409 return $key && isset($filter[$key]) ? $filter[$key] : $filter;
410 }
411
412 413 414 415 416 417 418
419 public function getData($applyPaging = TRUE, $useCache = TRUE)
420 {
421 if ($this->model === NULL) {
422 throw new \Exception('Model cannot be empty, please use method $grid->setModel().');
423 }
424
425 $data = $this->data;
426 if ($data === NULL || $useCache === FALSE) {
427 $this->applyFiltering();
428 $this->applySorting();
429
430 if ($applyPaging) {
431 $this->applyPaging();
432 }
433
434 $data = $this->model->getData();
435
436 if ($useCache === TRUE) {
437 $this->data = $data;
438 }
439
440 if ($applyPaging && $data && !in_array($this->page, range(1, $this->getPaginator()->pageCount))) {
441 trigger_error("Page is out of range.", E_USER_NOTICE);
442 $this->page = 1;
443 }
444
445 if ($this->onFetchData) {
446 $this->onFetchData($this);
447 }
448 }
449
450 return $data;
451 }
452
453 454 455 456
457 public function getTranslator()
458 {
459 if ($this->translator === NULL) {
460 $this->setTranslator(new Translations\FileTranslator);
461 }
462
463 return $this->translator;
464 }
465
466 467 468 469 470
471 public function getRememberSession($forceStart = FALSE)
472 {
473 $presenter = $this->getPresenter();
474 $session = $presenter->getSession();
475
476 if (!$session->isStarted() && $forceStart) {
477 $session->start();
478 }
479
480 return $session->isStarted()
481 ? $session->getSection($presenter->getName() . '\\' . ucfirst($this->getName()))
482 : NULL;
483 }
484
485 486 487 488
489 public function getTablePrototype()
490 {
491 if ($this->tablePrototype === NULL) {
492 $this->tablePrototype = \Nette\Utils\Html::el('table');
493 $this->tablePrototype->id($this->getName())
494 ->class[] = 'table table-striped table-hover';
495 }
496
497 return $this->tablePrototype;
498 }
499
500 501 502 503
504 public function getFilterRenderType()
505 {
506 if ($this->filterRenderType !== NULL) {
507 return $this->filterRenderType;
508 }
509
510 $this->filterRenderType = Filter::RENDER_OUTER;
511 if ($this->hasColumns() && $this->hasFilters() && $this->hasActions()) {
512 $this->filterRenderType = Filter::RENDER_INNER;
513
514 $filters = $this[Filter::ID]->getComponents();
515 foreach ($filters as $filter) {
516 if (!$this[Column::ID]->getComponent($filter->name, FALSE)) {
517 $this->filterRenderType = Filter::RENDER_OUTER;
518 break;
519 }
520 }
521 }
522
523 return $this->filterRenderType;
524 }
525
526 527 528 529
530 public function getModel()
531 {
532 return $this->model;
533 }
534
535 536 537 538
539 public function getPropertyAccessor()
540 {
541 if ($this->propertyAccessor === NULL) {
542 $this->propertyAccessor = new PropertyAccessors\ArrayObjectAccessor;
543 }
544
545 return $this->propertyAccessor;
546 }
547
548 549 550 551
552 public function getPaginator()
553 {
554 if ($this->paginator === NULL) {
555 $this->paginator = new Paginator;
556 $this->paginator->setItemsPerPage($this->getPerPage())
557 ->setGrid($this);
558 }
559
560 return $this->paginator;
561 }
562
563 564 565 566 567
568 public function getRowPrototype($row)
569 {
570 try {
571 $primaryValue = $this->getPropertyAccessor()->getProperty($row, $this->getPrimaryKey());
572 } catch (\Exception $e) {
573 $primaryValue = NULL;
574 }
575
576 $tr = \Nette\Utils\Html::el('tr');
577 $primaryValue ? $tr->class[] = "grid-row-$primaryValue" : NULL;
578
579 if ($this->rowCallback) {
580 $tr = callback($this->rowCallback)->invokeArgs(array($row, $tr));
581 }
582
583 return $tr;
584 }
585
586 587 588 589
590 public function getClientSideOptions()
591 {
592 return (array) json_decode($this->getTablePrototype()->data[self::CLIENT_SIDE_OPTIONS]);
593 }
594
595
596
597 598 599 600 601
602 public function loadState(array $params)
603 {
604
605 $session = $this->getRememberSession();
606 if ($session && $this->getPresenter()->isSignalReceiver($this)) {
607 $session->remove();
608 } elseif ($session && !$params && $session->params) {
609 $params = (array) $session->params;
610 }
611
612 parent::loadState($params);
613 }
614
615 616 617 618 619 620
621 public function saveState(array &$params, $reflection = NULL)
622 {
623 $this->onRegistered && $this->onRegistered($this);
624 return parent::saveState($params, $reflection);
625 }
626
627 628 629 630
631 public function handleRefresh()
632 {
633 $this->reload();
634 }
635
636 637 638 639
640 public function handlePage($page)
641 {
642 $this->reload();
643 }
644
645 646 647 648
649 public function handleSort(array $sort)
650 {
651 $this->page = 1;
652 $this->reload();
653 }
654
655 656 657 658
659 public function handleFilter(\Nette\Forms\Controls\SubmitButton $button)
660 {
661 $values = $button->form->values[Filter::ID];
662 $session = $this->rememberState
663 ? isset($this->getRememberSession(TRUE)->params['filter'])
664 ? $this->getRememberSession(TRUE)->params['filter']
665 : array()
666 : array();
667
668 foreach ($values as $name => $value) {
669 if (is_numeric($value) || !empty($value) || isset($this->defaultFilter[$name]) || isset($session[$name])) {
670 $this->filter[$name] = $this->getFilter($name)->changeValue($value);
671 } elseif (isset($this->filter[$name])) {
672 unset($this->filter[$name]);
673 }
674 }
675
676 $this->page = 1;
677 $this->reload();
678 }
679
680 681 682 683
684 public function handleReset(\Nette\Forms\Controls\SubmitButton $button)
685 {
686 $this->sort = array();
687 $this->filter = array();
688 $this->perPage = NULL;
689
690 if ($session = $this->getRememberSession()) {
691 $session->remove();
692 }
693
694 $button->form->setValues(array(Filter::ID => $this->defaultFilter), TRUE);
695
696 $this->page = 1;
697 $this->reload();
698 }
699
700 701 702 703
704 public function handlePerPage(\Nette\Forms\Controls\SubmitButton $button)
705 {
706 $perPage = (int) $button->form['count']->value;
707 $this->perPage = $perPage == $this->defaultPerPage
708 ? NULL
709 : $perPage;
710
711 $this->page = 1;
712 $this->reload();
713 }
714
715 716 717 718 719
720 public function reload()
721 {
722 if ($this->presenter->isAjax()) {
723 $this->presenter->payload->grido = TRUE;
724 $this->invalidateControl();
725 } else {
726 $this->redirect('this');
727 }
728 }
729
730
731
732 733 734 735 736
737 public function createTemplate($class = NULL)
738 {
739 $template = parent::createTemplate($class);
740 $template->setFile(__DIR__ . '/Grid.latte');
741 $template->registerHelper('translate', callback($this->getTranslator(), 'translate'));
742
743 return $template;
744 }
745
746 747 748 749
750 public function render()
751 {
752 if (!$this->hasColumns()) {
753 throw new \Exception('Grid must have defined a column, please use method $grid->addColumn*().');
754 }
755
756 $this->saveRememberState();
757 $data = $this->getData();
758
759 if ($this->onRender) {
760 $this->onRender($this);
761 }
762
763 $this->template->data = $data;
764 $this->template->form = $form = $this['form'];
765 $this->template->paginator = $this->paginator;
766
767 $form['count']->setValue($this->getPerPage());
768
769 $this->template->render();
770 }
771
772 protected function saveRememberState()
773 {
774 if ($this->rememberState) {
775 $session = $this->getRememberSession(TRUE);
776 $params = array_keys($this->getReflection()->getPersistentParams());
777 foreach ($params as $param) {
778 $session->params[$param] = $this->$param;
779 }
780 }
781 }
782
783 protected function applyFiltering()
784 {
785 $conditions = $this->__getConditions($this->getActualFilter());
786 $this->model->filter($conditions);
787 }
788
789 790 791 792 793
794 public function __getConditions(array $filter)
795 {
796 $conditions = array();
797 if ($filter) {
798 $this['form']->setDefaults(array(Filter::ID => $filter));
799
800 foreach ($filter as $column => $value) {
801 if ($component = $this->getFilter($column, FALSE)) {
802 if ($condition = $component->__getCondition($value)) {
803 $conditions[] = $condition;
804 }
805 } else {
806 trigger_error("Filter with name '$column' does not exist.", E_USER_NOTICE);
807 }
808 }
809 }
810
811 return $conditions;
812 }
813
814 protected function applySorting()
815 {
816 $sort = array();
817 $this->sort = $this->sort ? $this->sort : $this->defaultSort;
818
819 foreach ($this->sort as $column => $dir) {
820 $component = $this->getColumn($column, FALSE);
821 if (!$component) {
822 if (!isset($this->defaultSort[$column])) {
823 trigger_error("Column with name '$column' does not exist.", E_USER_NOTICE);
824 break;
825 }
826
827 } elseif (!$component->isSortable()) {
828 if (isset($this->defaultSort[$column])) {
829 $component->setSortable();
830 } else {
831 trigger_error("Column with name '$column' is not sortable.", E_USER_NOTICE);
832 break;
833 }
834 }
835
836 if (!in_array($dir, array(Column::ORDER_ASC, Column::ORDER_DESC))) {
837 if ($dir == '' && isset($this->defaultSort[$column])) {
838 unset($this->sort[$column]);
839 break;
840 }
841
842 trigger_error("Dir '$dir' is not allowed.", E_USER_NOTICE);
843 break;
844 }
845
846 $sort[$component ? $component->column : $column] = $dir == Column::ORDER_ASC ? 'ASC' : 'DESC';
847 }
848
849 if ($sort) {
850 $this->model->sort($sort);
851 }
852 }
853
854 protected function applyPaging()
855 {
856 $paginator = $this->getPaginator()
857 ->setItemCount($this->getCount())
858 ->setPage($this->page);
859
860 $perPage = $this->getPerPage();
861 if ($perPage !== NULL && !in_array($perPage, $this->perPageList)) {
862 trigger_error("The number '$perPage' of items per page is out of range.", E_USER_NOTICE);
863 $perPage = $this->defaultPerPage;
864 }
865
866 $this->model->limit($paginator->getOffset(), $paginator->getLength());
867 }
868
869 protected function createComponentForm($name)
870 {
871 $form = new \Nette\Application\UI\Form($this, $name);
872 $form->setTranslator($this->getTranslator());
873 $form->setMethod($form::GET);
874
875 $buttons = $form->addContainer(self::BUTTONS);
876 $buttons->addSubmit('search', 'Grido.Search')
877 ->onClick[] = $this->handleFilter;
878 $buttons->addSubmit('reset', 'Grido.Reset')
879 ->onClick[] = $this->handleReset;
880 $buttons->addSubmit('perPage', 'Grido.ItemsPerPage')
881 ->onClick[] = $this->handlePerPage;
882
883 $form->addSelect('count', 'Count', $this->getItemsForCountSelect())
884 ->controlPrototype->attrs['title'] = $this->getTranslator()->translate('Grido.ItemsPerPage');
885 }
886
887 888 889
890 protected function getItemsForCountSelect()
891 {
892 return array_combine($this->perPageList, $this->perPageList);
893 }
894 }
895