├── bootstrap-style ├── @bootstrap3.datagrid.latte ├── @bootstrap3.extended-pagination.datagrid.latte └── bootstrap3.nextras.datagrid.css ├── composer.json ├── js ├── nextras.datagrid.js └── nittro.nextras.datagrid.js ├── license.md ├── readme.md └── src ├── Column.php ├── Datagrid.blocks.latte ├── Datagrid.latte └── Datagrid.php /bootstrap-style/@bootstrap3.datagrid.latte: -------------------------------------------------------------------------------- 1 | {define table-open-tag} 2 | 3 | {/define} 4 | 5 | {define global-actions} 6 |
7 |
8 | {input $form[actions][action] class => 'form-control'} 9 | 10 | {input $form[actions][process] class => 'btn btn-primary'} 11 | 12 |
13 |
14 | {/define} 15 | 16 | {define global-filter-actions} 17 | {input filter class => "btn btn-primary btn-sm"} 18 | {if $showCancel} 19 | {input cancel class => "btn btn-default btn-sm"} 20 | {/if} 21 | {/define} 22 | 23 | {define col-filter} 24 | {input $column->name class => "input-sm form-control"} 25 | {/define} 26 | 27 | {define row-actions-edit} 28 | {input save class => "btn btn-primary btn-xs"} 29 | {input cancel class => "btn btn-default btn-xs"} 30 | {/define} 31 | 32 | {define row-actions-edit-link} 33 | {$control->translate(Edit)} 34 | {/define} 35 | 36 | {define pagination} 37 | 58 | {/define} 59 | -------------------------------------------------------------------------------- /bootstrap-style/@bootstrap3.extended-pagination.datagrid.latte: -------------------------------------------------------------------------------- 1 | {define pagination} 2 | {*******************************} 3 | {php $page = $paginator->getPage()} 4 | {if $paginator->pageCount < 2} 5 | {php $steps = [$page]} 6 | {else} 7 | {php $arr = range(max($paginator->firstPage, $page - 3), min($paginator->lastPage, $page + 3))} 8 | {php $count = 4} 9 | {php $quotient = ($paginator->pageCount - 1) / $count} 10 | {for $i = 0; $i <= $count; $i++} 11 | {php $arr[] = round($quotient * $i) + $paginator->firstPage} 12 | {/for} 13 | {php sort($arr)} 14 | {php $steps = array_values(array_unique($arr))} 15 | {/if} 16 | {*******************************} 17 | 39 | {/define} 40 | -------------------------------------------------------------------------------- /bootstrap-style/bootstrap3.nextras.datagrid.css: -------------------------------------------------------------------------------- 1 | .grid td, 2 | .grid th { 3 | border-color: #CACACA; 4 | } 5 | .grid .grid-col-actions { 6 | text-align: center; 7 | } 8 | .grid .btn { 9 | margin-right: 3px; 10 | } 11 | .grid thead .grid-columns, 12 | .grid thead .grid-filters { 13 | background: #EEE; 14 | } 15 | .grid thead .grid-columns th, 16 | .grid thead .grid-filters th { 17 | border-bottom-width: 1px; 18 | } 19 | .grid thead tr:last-child th { 20 | border-bottom-width: 2px; 21 | } 22 | .grid tbody > tr > td { 23 | vertical-align: middle; 24 | } 25 | .grid tfoot { 26 | background: #EEE; 27 | } 28 | .grid tfoot th { 29 | text-align: center; 30 | } 31 | .grid tfoot .pagination, .grid tfoot .grid-global-actions { 32 | margin: 5px; 33 | } 34 | .grid tfoot .grid-global-actions { 35 | margin-right: -100%; 36 | } 37 | .grid .grid-col-global-actions { 38 | width: 16px; 39 | } 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextras/datagrid", 3 | "type": "library", 4 | "description": "Datagrid component for Nette Framework.", 5 | "keywords": ["nextras", "nette", "datagrid"], 6 | "license": ["MIT"], 7 | "authors": [ 8 | { 9 | "name": "Nextras Community", 10 | "homepage": "https://github.com/nextras/forms/graphs/contributors" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=7.1", 15 | "nette/application": "~3.0", 16 | "nette/component-model": "~3.0", 17 | "nette/forms": "~3.0", 18 | "nette/utils": "~3.0 || ~4.0", 19 | "latte/latte": "~2.9" 20 | }, 21 | "extra": { 22 | "branch-alias": { 23 | "dev-master": "3.0-dev" 24 | } 25 | }, 26 | "autoload": { 27 | "psr-4": { "Nextras\\Datagrid\\": "src" } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /js/nextras.datagrid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the Nextras community extensions of Nette Framework 3 | * 4 | * @license MIT 5 | * @link https://github.com/nextras 6 | * @author Jan Skrasek 7 | */ 8 | 9 | $.nette.ext('datagrid', { 10 | init: function() { 11 | var datagrid = this; 12 | this.grids = $('.grid').each(function() { 13 | datagrid.load($(this)); 14 | }); 15 | }, 16 | load: function() { 17 | var datagrid = this; 18 | $('.grid thead input').off('keypress.datagrid').on('keypress.datagrid', function(e) { 19 | if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) { 20 | $(this).parents('tr').find('[name=filter\\[filter\\]]').trigger(datagrid.createClickEvent($(this))); 21 | e.preventDefault(); 22 | } 23 | }); 24 | $('.grid thead select').off('change.datagrid').on('change.datagrid', function(e) { 25 | $(this).parents('tr').find('[name=filter\\[filter\\]]').trigger(datagrid.createClickEvent($(this))); 26 | e.preventDefault(); 27 | }); 28 | $('.grid tbody td:not(.grid-col-actions)').off('click.datagrid').on('click.datagrid', function(e) { 29 | if (e.ctrlKey) { 30 | $(this).parents('tr').find('a[data-datagrid-edit]').trigger(datagrid.createClickEvent($(this))); 31 | e.preventDefault(); 32 | } 33 | }); 34 | $('.grid tbody input').off('keypress.datagrid').on('keypress.datagrid', function(e) { 35 | if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) { 36 | $(this).parents('tr').find('[name=edit\\[save\\]]').trigger(datagrid.createClickEvent($(this))); 37 | e.preventDefault(); 38 | } 39 | }); 40 | }, 41 | before: function(xhr, settings) { 42 | if (('nette' in settings) && ('el' in settings.nette)) { 43 | this.grid = settings.nette.el.parents('.grid'); 44 | } 45 | }, 46 | success: function() { 47 | if ('grid' in this) { 48 | this.load(this.grid); 49 | } 50 | } 51 | }, { 52 | activeGrid: null, 53 | load: function(grid) { 54 | var idToClose = []; 55 | var paramName = grid.attr('data-grid-name'); 56 | grid.find('tr:has([name=edit\\[cancel\\]])').each(function(i, el) { 57 | $(el).find('input').get(0).focus(); 58 | idToClose.push($(el).find('.grid-primary-value').val()); 59 | }); 60 | 61 | if (idToClose.length == 0) { 62 | return; 63 | } 64 | 65 | grid.find('a[data-datagrid-edit]').each(function() { 66 | var href = $(this).data('grid-href'); 67 | if (!href) { 68 | $(this).data('grid-href', href = $(this).attr('href')); 69 | } 70 | 71 | $(this).attr('href', href + '&' + paramName + '-cancelEditPrimaryValue=' + idToClose.join(',')); 72 | }); 73 | }, 74 | createClickEvent: function(item) { 75 | var offset = item.offset(); 76 | return jQuery.Event('click', { 77 | pageX: offset.left + item.width(), 78 | pageY: offset.top + item.height() 79 | }); 80 | } 81 | }); 82 | -------------------------------------------------------------------------------- /js/nittro.nextras.datagrid.js: -------------------------------------------------------------------------------- 1 | (window._stack = window._stack || []).push([function (di, DOM) { 2 | DOM.getByClassName('grid').forEach(function (grid) { 3 | DOM.addListener(grid, 'click', function (evt) { 4 | var link = DOM.closest(evt.target, 'a'), 5 | frm = grid.getElementsByTagName('form').item(0); 6 | 7 | if (link && link.hasAttribute('data-datagrid-edit')) { 8 | evt.preventDefault(); 9 | 10 | var btns = frm.elements.namedItem('edit[cancel]') || [], 11 | data = {}; 12 | 13 | if (btns.tagName) { 14 | btns = [btns]; 15 | } 16 | 17 | data[DOM.getData(grid, 'grid-name') + '-cancelEditPrimaryValue'] = btns 18 | .map(function (btn) { 19 | return DOM.getByClassName('grid-primary-value', DOM.closest(btn, 'tr'))[0].value; 20 | }) 21 | .join(','); 22 | 23 | di.getService('page').open(link.href, 'get', data); 24 | } 25 | }); 26 | }); 27 | }, { 28 | DOM: 'Utils.DOM' 29 | }]); 30 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | =============== 3 | 4 | Copyright (c) 2013, 2015 Nextras Project 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Nextras Datagrid 2 | ================ 3 | 4 | [![Downloads this Month](https://img.shields.io/packagist/dm/nextras/datagrid.svg?style=flat)](https://packagist.org/packages/nextras/datagrid) 5 | [![Stable Version](https://poser.pugx.org/nextras/datagrid/v/stable)](https://packagist.org/packages/nextras/datagrid) 6 | 7 | Easy to use datagrid with powerfull API. 8 | 9 | ### Installation 10 | 11 | Use composer: 12 | 13 | ```bash 14 | $ composer require nextras/datagrid 15 | ``` 16 | 17 | ### Docs & sources 18 | 19 | - [Documentation](https://nextras.org/datagrid/docs) 20 | - [Demo App](https://github.com/nextras/datagrid-demo) 21 | -------------------------------------------------------------------------------- /src/Column.php: -------------------------------------------------------------------------------- 1 | name = $name; 35 | $this->label = $label; 36 | $this->grid = $grid; 37 | } 38 | 39 | 40 | public function enableSort($default = NULL) 41 | { 42 | $this->sort = TRUE; 43 | if ($default !== NULL) { 44 | if ($default !== Datagrid::ORDER_ASC && $default !== Datagrid::ORDER_DESC) { 45 | throw new \InvalidArgumentException('Unknown order type.'); 46 | } 47 | 48 | $this->grid->orderColumn = $this->name; 49 | $this->grid->orderType = $default; 50 | } 51 | return $this; 52 | } 53 | 54 | 55 | public function canSort() 56 | { 57 | return $this->sort; 58 | } 59 | 60 | 61 | public function getNewState() 62 | { 63 | if ($this->isAsc()) { 64 | return Datagrid::ORDER_DESC; 65 | } elseif ($this->isDesc()) { 66 | return ''; 67 | } else { 68 | return Datagrid::ORDER_ASC; 69 | } 70 | } 71 | 72 | 73 | public function isAsc() 74 | { 75 | return $this->grid->orderColumn === $this->name && $this->grid->orderType === Datagrid::ORDER_ASC; 76 | } 77 | 78 | 79 | public function isDesc() 80 | { 81 | return $this->grid->orderColumn === $this->name && $this->grid->orderType === Datagrid::ORDER_DESC; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Datagrid.blocks.latte: -------------------------------------------------------------------------------- 1 | {** 2 | * This file is part of the Nextras community extensions of Nette Framework 3 | * 4 | * @license MIT 5 | * @link https://github.com/nextras 6 | *} 7 | 8 | {define table-open-tag} 9 |
10 | {/define} 11 | 12 | {define table-close-tag} 13 |
14 | {/define} 15 | 16 | {define table-thead-open-tag} 17 | 18 | {/define} 19 | 20 | {define table-thead-close-tag} 21 | 22 | {/define} 23 | 24 | {define table-tbody-open-tag} 25 | 26 | {/define} 27 | 28 | {define table-tbody-close-tag} 29 | 30 | {/define} 31 | 32 | {define global-filter-actions} 33 | {input filter} 34 | {if $showCancel} 35 | {input cancel} 36 | {/if} 37 | {/define} 38 | 39 | {define row-head-columns} 40 | 41 | {ifset $form[actions]} 42 | 43 | {/ifset} 44 | {foreach $columns as $column} 45 | 46 | {if $column->canSort()} 47 | {$column->label} 48 | {if $column->isAsc()} 49 | 50 | {elseif $column->isDesc()} 51 | 52 | {else} 53 | 54 | {/if} 55 | {else} 56 | {$column->label} 57 | {/if} 58 | 59 | {/foreach} 60 | {if $hasActionsColumn} 61 | 62 | {/if} 63 | 64 | {/define} 65 | 66 | {define col-filter} 67 | {input $column->name} 68 | {/define} 69 | 70 | {define global-actions} 71 |
72 | {input $form[actions][action]} 73 | {input $form[actions][process]} 74 |
75 | {/define} 76 | 77 | {define row-head-filter} 78 | 79 | {ifset $form[actions]} 80 | 81 | {/ifset} 82 | {formContainer filter} 83 | {foreach $columns as $column} 84 | 85 | {if isset($form['filter'][$column->name])} 86 | {ifset #col-filter-{$column->name}} 87 | {include #"col-filter-{$column->name}" column => $column} 88 | {else} 89 | {include #col-filter column => $column} 90 | {/ifset} 91 | {/if} 92 | 93 | {/foreach} 94 | 95 | {include #global-filter-actions showCancel => $showFilterCancel} 96 | 97 | {/formContainer} 98 | 99 | {/define} 100 | 101 | {define row-actions-edit} 102 | {input save} 103 | {input cancel} 104 | {/define} 105 | 106 | {define row-actions-edit-link} 107 | {$control->translate(Edit)} 108 | {/define} 109 | 110 | {define row} 111 | 112 | {include #row-inner row => $row} 113 | 114 | {/define} 115 | 116 | {define row-inner} 117 | {var $primary = $control->getter($row, $rowPrimaryKey)} 118 | {php if (!$sendOnlyRowParentSnippet): $this->global->snippetDriver->enter("rows-$primary", "dynamic"); endif;} 119 | 120 | {var $editRow = $editRowKey == $primary && $primary !== NULL && $editRowKey !== NULL} 121 | {ifset $form[actions]} 122 | 123 | {formContainer actions} 124 | {input items:$primary} 125 | {/formContainer} 126 | 127 | {/ifset} 128 | {foreach $columns as $column} 129 | {var $cell = $control->getter($row, $column->name, FALSE)} 130 | {if $editRow && $column->name != $rowPrimaryKey && (isset($form['edit'][$column->name]) || isset($this->blockQueue["cell-edit-{$column->name}"]))} 131 | 132 | {ifset #cell-edit-{$column->name}} 133 | {include #"cell-edit-{$column->name}" form => $form, column => $column, row => $row} 134 | {else} 135 | {formContainer edit} 136 | {input $column->name} 137 | {if $form[edit][$column->name]->hasErrors()} 138 |

{$error}

139 | {/if} 140 | {/formContainer} 141 | {/ifset} 142 | 143 | {else} 144 | {ifset #col-{$column->name}} 145 | {include #"col-{$column->name}" row => $row, cell => $cell, iterator => $iterator} 146 | {else} 147 | 148 | {ifset #cell-{$column->name}} 149 | {include #"cell-{$column->name}" row => $row, cell => $cell, iterator => $iterator} 150 | {else} 151 | {$cell} 152 | {/ifset} 153 | 154 | {/ifset} 155 | {/if} 156 | {/foreach} 157 | {if $hasActionsColumn} 158 | 159 | {if $editRow} 160 | {formContainer edit} 161 | {input $rowPrimaryKey class => 'grid-primary-value'} 162 | {include #row-actions-edit} 163 | {/formContainer} 164 | {else} 165 | {ifset #row-actions} 166 | {include #row-actions row => $row, primary => $primary} 167 | {elseif $control->getEditFormFactory()} 168 | {include #row-actions-edit-link row => $row, primary => $primary} 169 | {/ifset} 170 | {/if} 171 | 172 | {/if} 173 | {php if (!$sendOnlyRowParentSnippet): $this->global->snippetDriver->leave(); endif;} 174 | {/define} 175 | 176 | {define pagination} 177 |
178 | {if $paginator->isFirst()} 179 | « {$control->translate(First)} 180 | « {$control->translate(Previous)} 181 | {else} 182 | « {$control->translate(First)} 183 | « {$control->translate(Previous)} 184 | {/if} 185 | 186 | 187 | {$paginator->page} / {$paginator->pageCount} 188 | 189 | 190 | {if $paginator->isLast()} 191 | {$control->translate(Next)} » 192 | {$control->translate(Last)} » 193 | {else} 194 | {$control->translate(Next)} » 195 | {$control->translate(Last)} » 196 | {/if} 197 |
198 | {/define} 199 | -------------------------------------------------------------------------------- /src/Datagrid.latte: -------------------------------------------------------------------------------- 1 | {** 2 | * This file is part of the Nextras community extensions of Nette Framework 3 | * 4 | * @license MIT 5 | * @link https://github.com/nextras 6 | *} 7 |
8 | {snippet rows} 9 | 10 | {var $_templates = []} 11 | {foreach $cellsTemplates as $cellsTemplate} 12 | {php 13 | $_template = $this->createTemplate($cellsTemplate, $this->params, "includeblock"); 14 | $_template->render(); 15 | $_templates[] = $_template; 16 | } 17 | {/foreach} 18 | 19 | {form form class => 'ajax'} 20 | 21 | {php 22 | $hasActionsColumn = 23 | (bool) $control->getEditFormFactory() /* we may render only one row so the form[filter] may not be created */ 24 | || $this->hasBlock("row-actions"); 25 | $hasGlobalActionsColumn = isset($form['actions']); 26 | 27 | foreach ($_templates as $_template): 28 | $_template->params['hasActionsColumn'] = $hasActionsColumn; 29 | $_template->params['hasGlobalActionsColumn'] = $hasGlobalActionsColumn; 30 | endforeach; 31 | $this->params['hasActionsColumn'] = $hasActionsColumn; 32 | $this->params['hasGlobalActionsColumn'] = $hasGlobalActionsColumn; 33 | } 34 | 35 | {include #table-open-tag} 36 | {include #table-thead-open-tag} 37 | {include #row-head-columns} 38 | {ifset $form['filter']} 39 | {include #row-head-filter} 40 | {/ifset} 41 | {include #table-thead-close-tag} 42 | {include #table-tbody-open-tag} 43 | {if count($data)} 44 | {foreach $data as $row} 45 | {var $primary = $control->getter($row, $rowPrimaryKey)} 46 | {var $rowId = $this->global->snippetDriver->getHtmlId("rows-$primary")} 47 | {include #row row => $row, rowId => $rowId} 48 | {/foreach} 49 | {else} 50 | {ifset #empty-result}{include #empty-result}{/ifset} 51 | {/if} 52 | {include #table-tbody-close-tag} 53 | 54 | 55 | 56 | {if $hasGlobalActionsColumn} 57 | {include #global-actions} 58 | {/if} 59 | {ifset $paginator} 60 | {include #pagination} 61 | {/ifset} 62 | 63 | 64 | 65 | {include #table-close-tag} 66 | 67 | {/form} 68 | 69 | {/snippet} 70 |
71 | -------------------------------------------------------------------------------- /src/Datagrid.php: -------------------------------------------------------------------------------- 1 | rowPrimaryKey) { 106 | $this->rowPrimaryKey = $name; 107 | } 108 | 109 | $label = $label ? $this->translate($label) : ucfirst($name); 110 | return $this->columns[$name] = new Column($name, $label, $this); 111 | } 112 | 113 | 114 | /** 115 | * @param string $name 116 | * @return Column 117 | */ 118 | public function getColumn($name) 119 | { 120 | if (!isset($this->columns[$name])) { 121 | throw new \InvalidArgumentException("Unknown column $name."); 122 | } 123 | return $this->columns[$name]; 124 | } 125 | 126 | 127 | public function setRowPrimaryKey($columnName) 128 | { 129 | $this->rowPrimaryKey = (string) $columnName; 130 | } 131 | 132 | 133 | public function getRowPrimaryKey() 134 | { 135 | return $this->rowPrimaryKey; 136 | } 137 | 138 | 139 | public function setColumnGetterCallback(callable $getterCallback = null) 140 | { 141 | $this->columnGetterCallback = $getterCallback; 142 | } 143 | 144 | 145 | public function getColumnGetterCallback() 146 | { 147 | return $this->columnGetterCallback; 148 | } 149 | 150 | 151 | public function setDataSourceCallback(callable $dataSourceCallback) 152 | { 153 | $this->dataSourceCallback = $dataSourceCallback; 154 | } 155 | 156 | 157 | public function getDataSourceCallback() 158 | { 159 | return $this->dataSourceCallback; 160 | } 161 | 162 | 163 | public function setEditFormFactory(callable $editFormFactory = null) 164 | { 165 | $this->editFormFactory = $editFormFactory; 166 | } 167 | 168 | 169 | public function getEditFormFactory() 170 | { 171 | return $this->editFormFactory; 172 | } 173 | 174 | 175 | public function setEditFormCallback(callable $editFormCallback = null) 176 | { 177 | $this->editFormCallback = $editFormCallback; 178 | } 179 | 180 | 181 | public function getEditFormCallback() 182 | { 183 | return $this->editFormCallback; 184 | } 185 | 186 | 187 | public function setFilterFormFactory(callable $filterFormFactory = null) 188 | { 189 | $this->filterFormFactory = $filterFormFactory; 190 | } 191 | 192 | 193 | public function getFilterFormFactory() 194 | { 195 | return $this->filterFormFactory; 196 | } 197 | 198 | 199 | public function addGlobalAction($name, $label, callable $action) 200 | { 201 | $this->globalActions[$name] = [$label, $action]; 202 | } 203 | 204 | 205 | public function setPagination($itemsPerPage, callable $itemsCountCallback = null) 206 | { 207 | if ($itemsPerPage === false) { 208 | $this->paginator = null; 209 | $this->paginatorItemsCountCallback = null; 210 | } else { 211 | if ($itemsCountCallback === null) { 212 | throw new \InvalidArgumentException('Items count callback must be set.'); 213 | } 214 | 215 | $this->paginator = new Paginator(); 216 | $this->paginator->itemsPerPage = $itemsPerPage; 217 | $this->paginatorItemsCountCallback = $itemsCountCallback; 218 | } 219 | } 220 | 221 | 222 | /** 223 | * @param string|Template $path 224 | */ 225 | public function addCellsTemplate($path) 226 | { 227 | if ($path instanceof Template) { 228 | $path = $path->getFile(); 229 | } 230 | if (!file_exists($path)) { 231 | throw new \InvalidArgumentException("Template '{$path}' does not exist."); 232 | } 233 | $this->cellsTemplates[] = $path; 234 | } 235 | 236 | 237 | public function getCellsTemplates() 238 | { 239 | $templates = $this->cellsTemplates; 240 | $templates[] = __DIR__ . '/Datagrid.blocks.latte'; 241 | return $templates; 242 | } 243 | 244 | 245 | public function setTranslator(ITranslator $translator) 246 | { 247 | $this->translator = $translator; 248 | } 249 | 250 | 251 | public function getTranslator() 252 | { 253 | return $this->translator; 254 | } 255 | 256 | 257 | public function translate($s, $count = null) 258 | { 259 | $translator = $this->getTranslator(); 260 | return $translator === null || $s == null || $s instanceof Html // intentionally == 261 | ? $s 262 | : $translator->translate((string) $s, $count); 263 | } 264 | 265 | 266 | /*******************************************************************************/ 267 | 268 | 269 | public function render() 270 | { 271 | if ($this->filterFormFactory) { 272 | $this['form']['filter']->setDefaults($this->filter); 273 | } 274 | 275 | $this->template->form = $this['form']; 276 | $this->template->data = $this->getData(); 277 | $this->template->columns = $this->columns; 278 | $this->template->editRowKey = $this->editRowKey; 279 | $this->template->rowPrimaryKey = $this->rowPrimaryKey; 280 | $this->template->paginator = $this->paginator; 281 | $this->template->sendOnlyRowParentSnippet = $this->sendOnlyRowParentSnippet; 282 | $this->template->cellsTemplates = $this->getCellsTemplates(); 283 | $this->template->showFilterCancel = $this->filterDataSource != $this->filterDefaults; // @ intentionaly 284 | $this->template->setFile(__DIR__ . '/Datagrid.latte'); 285 | 286 | $this->onRender($this); 287 | $this->template->render(); 288 | } 289 | 290 | 291 | public function redrawRow($primaryValue) 292 | { 293 | if ($this->presenter->isAjax()) { 294 | if (isset($this->filterDataSource[$this->rowPrimaryKey])) { 295 | $this->filterDataSource = [$this->rowPrimaryKey => $this->filterDataSource[$this->rowPrimaryKey]]; 296 | if (is_string($this->filterDataSource[$this->rowPrimaryKey])) { 297 | $this->filterDataSource[$this->rowPrimaryKey] = [$this->filterDataSource[$this->rowPrimaryKey]]; 298 | } 299 | } else { 300 | $this->filterDataSource = []; 301 | } 302 | 303 | $this->filterDataSource[$this->rowPrimaryKey][] = $primaryValue; 304 | parent::redrawControl('rows'); 305 | $this->redrawControl('rows-' . $primaryValue); 306 | } 307 | } 308 | 309 | 310 | public function redrawControl(string $snippet = null, $redraw = true): void 311 | { 312 | parent::redrawControl($snippet, $redraw); 313 | if ($snippet === null || $snippet === 'rows') { 314 | $this->sendOnlyRowParentSnippet = $redraw; 315 | } 316 | } 317 | 318 | 319 | /** @deprecated */ 320 | function invalidateRow($primaryValue) 321 | { 322 | trigger_error(__METHOD__ . '() is deprecated; use $this->redrawRow($primaryValue) instead.', E_USER_DEPRECATED); 323 | $this->redrawRow($primaryValue); 324 | } 325 | 326 | 327 | /*******************************************************************************/ 328 | protected function validateParent(Nette\ComponentModel\IContainer $parent): void 329 | { 330 | parent::validateParent($parent); 331 | $this->monitor(UI\Presenter::class, function () { 332 | $this->filterDataSource = $this->filter; 333 | }); 334 | } 335 | 336 | protected function getData($key = null) 337 | { 338 | if (!$this->data) { 339 | $onlyRow = $key !== null && $this->presenter->isAjax(); 340 | 341 | if ($this->orderColumn !== NULL && !isset($this->columns[$this->orderColumn])) { 342 | $this->orderColumn = NULL; 343 | } 344 | 345 | if (!$onlyRow && $this->paginator) { 346 | $itemsCount = call_user_func( 347 | $this->paginatorItemsCountCallback, 348 | $this->filterDataSource, 349 | $this->orderColumn ? [$this->orderColumn, strtoupper($this->orderType)] : null 350 | ); 351 | 352 | $this->paginator->setItemCount($itemsCount); 353 | if ($this->paginator->page !== $this->page) { 354 | $this->paginator->page = $this->page = 1; 355 | } 356 | } 357 | 358 | $this->data = call_user_func( 359 | $this->dataSourceCallback, 360 | $this->filterDataSource, 361 | $this->orderColumn ? [$this->orderColumn, strtoupper($this->orderType)] : null, 362 | $onlyRow ? null : $this->paginator 363 | ); 364 | } 365 | 366 | if ($key === null) { 367 | return $this->data; 368 | } 369 | 370 | foreach ($this->data as $row) { 371 | if ($this->getter($row, $this->rowPrimaryKey) == $key) { 372 | return $row; 373 | } 374 | } 375 | 376 | throw new \Exception('Row not found'); 377 | } 378 | 379 | 380 | /** 381 | * @internal 382 | * @ignore 383 | */ 384 | public function getter($row, $column, $need = true) 385 | { 386 | if ($this->columnGetterCallback) { 387 | return call_user_func($this->columnGetterCallback, $row, $column, $need); 388 | } else { 389 | if (!isset($row->$column)) { 390 | if ((is_array($row) || $row instanceof \ArrayAccess) && isset($row[$column])) { 391 | return $row[$column]; 392 | } 393 | 394 | if ($need) { 395 | throw new \InvalidArgumentException("Result row does not have '{$column}' column."); 396 | } else { 397 | return null; 398 | } 399 | } 400 | 401 | return $row->$column; 402 | } 403 | } 404 | 405 | 406 | public function handleEdit($primaryValue, $cancelEditPrimaryValue = null) 407 | { 408 | $this->editRowKey = $primaryValue; 409 | if ($this->presenter->isAjax()) { 410 | $this->redrawRow($primaryValue); 411 | if ($cancelEditPrimaryValue) { 412 | foreach (explode(',', $cancelEditPrimaryValue) as $pv) { 413 | $this->redrawRow($pv); 414 | } 415 | } 416 | } 417 | } 418 | 419 | 420 | public function handleSort() 421 | { 422 | if ($this->presenter->isAjax()) { 423 | $this->redrawControl('rows'); 424 | } 425 | } 426 | 427 | 428 | public function createComponentForm() 429 | { 430 | $form = new UI\Form; 431 | 432 | if ($this->filterFormFactory) { 433 | $form['filter'] = call_user_func($this->filterFormFactory); 434 | if (!isset($form['filter']['filter'])) { 435 | $form['filter']->addSubmit('filter', $this->translate('Filter')); 436 | } 437 | if (!isset($form['filter']['cancel'])) { 438 | $form['filter']->addSubmit('cancel', $this->translate('Cancel')); 439 | } 440 | 441 | $this->prepareFilterDefaults($form['filter']); 442 | if (!$this->filterDataSource) { 443 | $this->filterDataSource = $this->filterDefaults; 444 | } 445 | } 446 | 447 | if ($this->editFormFactory && ($this->editRowKey !== null || !empty($_POST['edit']))) { 448 | $data = $this->editRowKey !== null && empty($_POST) ? $this->getData($this->editRowKey) : null; 449 | $form['edit'] = call_user_func($this->editFormFactory, $data); 450 | 451 | if (!isset($form['edit']['save'])) 452 | $form['edit']->addSubmit('save', 'Save'); 453 | if (!isset($form['edit']['cancel'])) 454 | $form['edit']->addSubmit('cancel', 'Cancel'); 455 | if (!isset($form['edit'][$this->rowPrimaryKey])) 456 | $form['edit']->addHidden($this->rowPrimaryKey); 457 | 458 | $form['edit'][$this->rowPrimaryKey] 459 | ->setDefaultValue($this->editRowKey) 460 | ->setOption('rendered', true); 461 | } 462 | 463 | if ($this->globalActions) { 464 | $actions = array_map(function($row) { return $row[0]; }, $this->globalActions); 465 | $form['actions'] = new Container(); 466 | $form['actions']->addSelect('action', 'Action', $actions) 467 | ->setPrompt('- select action -'); 468 | $form['actions']->addCheckboxList('items', '', []); 469 | $form['actions']->addSubmit('process', 'Do'); 470 | } 471 | 472 | if ($this->translator) { 473 | $form->setTranslator($this->translator); 474 | } 475 | 476 | $form->onSuccess[] = function() {}; // fix for Nette Framework 2.0.x 477 | $form->onSubmit[] = [$this, 'processForm']; 478 | return $form; 479 | } 480 | 481 | 482 | public function processForm(UI\Form $form) 483 | { 484 | $allowRedirect = true; 485 | if (isset($form['edit'])) { 486 | if ($form['edit']['save']->isSubmittedBy()) { 487 | if ($form['edit']->isValid()) { 488 | call_user_func($this->editFormCallback, $form['edit']); 489 | } else { 490 | $this->editRowKey = $form['edit'][$this->rowPrimaryKey]->getValue(); 491 | $allowRedirect = false; 492 | } 493 | } 494 | if ($form['edit']['cancel']->isSubmittedBy() || ($form['edit']['save']->isSubmittedBy() && $form['edit']->isValid())) { 495 | $editRowKey = $form['edit'][$this->rowPrimaryKey]->getValue(); 496 | $this->redrawRow($editRowKey); 497 | $this->getData($editRowKey); 498 | } 499 | if ($this->editRowKey !== null) { 500 | $this->redrawRow($this->editRowKey); 501 | } 502 | } 503 | 504 | if (isset($form['filter'])) { 505 | if ($form['filter']['filter']->isSubmittedBy()) { 506 | $values = $form['filter']->getValues(true); 507 | unset($values['filter']); 508 | $values = $this->filterFormFilter($values); 509 | if ($this->paginator) { 510 | $this->page = $this->paginator->page = 1; 511 | } 512 | $this->filter = $this->filterDataSource = $values; 513 | $this->redrawControl('rows'); 514 | } elseif ($form['filter']['cancel']->isSubmittedBy()) { 515 | if ($this->paginator) { 516 | $this->page = $this->paginator->page = 1; 517 | } 518 | $this->filter = $this->filterDataSource = $this->filterDefaults; 519 | $form['filter']->setValues($this->filter, true); 520 | $this->redrawControl('rows'); 521 | } 522 | } 523 | 524 | if (isset($form['actions'])) { 525 | if ($form['actions']['process']->isSubmittedBy()) { 526 | $action = $form['actions']['action']->getValue(); 527 | if ($action) { 528 | $rows = []; 529 | foreach($this->getData() as $row) { 530 | $rows[] = $this->getter($row, $this->rowPrimaryKey); 531 | } 532 | $ids = array_intersect($rows, $form->getHttpData($form::DATA_TEXT, 'actions[items][]')); 533 | $callback = $this->globalActions[$action][1]; 534 | $callback($ids, $this); 535 | $this->data = null; 536 | $form['actions']->setValues(['action' => null, 'items' => []]); 537 | } 538 | } 539 | } 540 | 541 | if (!$this->presenter->isAjax() && $allowRedirect) { 542 | $this->redirect('this'); 543 | } 544 | } 545 | 546 | 547 | public function loadState(array $params): void 548 | { 549 | parent::loadState($params); 550 | if ($this->paginator) { 551 | $this->paginator->page = $this->page; 552 | } 553 | } 554 | 555 | 556 | protected function createTemplate(): UI\ITemplate 557 | { 558 | $template = parent::createTemplate(); 559 | if ($translator = $this->getTranslator()) { 560 | $template->setTranslator($translator); 561 | } 562 | return $template; 563 | } 564 | 565 | 566 | public function handlePaginate() 567 | { 568 | if ($this->presenter->isAjax()) { 569 | $this->redrawControl('rows'); 570 | } 571 | } 572 | 573 | 574 | private function prepareFilterDefaults(Container $container) 575 | { 576 | $this->filterDefaults = []; 577 | foreach ($container->controls as $name => $control) { 578 | if ($control instanceof Button) { 579 | continue; 580 | } 581 | 582 | $value = $control->getValue(); 583 | if (!self::isEmptyValue($value)) { 584 | $this->filterDefaults[$name] = $value; 585 | } 586 | } 587 | } 588 | 589 | 590 | private function filterFormFilter(array $values) 591 | { 592 | $filtered = []; 593 | foreach ($values as $key => $value) { 594 | $isDefaultDifferent = isset($this->filterDefaults[$key]) && $this->filterDefaults[$key] !== $value; 595 | if ($isDefaultDifferent || !self::isEmptyValue($value)) { 596 | $filtered[$key] = $value; 597 | } 598 | } 599 | return $filtered; 600 | } 601 | 602 | 603 | private static function isEmptyValue($value) 604 | { 605 | return $value === NULL || $value === '' || $value === [] || $value === false; 606 | } 607 | } 608 | --------------------------------------------------------------------------------