commit dd5224308b56a15378fa8732623a06b7083ec24f
Author: Filip Jani <jsem@filek.cz>
Date:   Sat Mar 30 16:42:01 2019 +0100

    Re #7217 přidání datagridů

diff --git a/app/bootstrap.php b/app/bootstrap.php
index 5388b7b..76cff23 100644
--- a/app/bootstrap.php
+++ b/app/bootstrap.php
@@ -17,6 +17,7 @@ $configurator->createRobotLoader()
 $configurator->addConfig(__DIR__ . '/config/config.neon');
 $configurator->addConfig(__DIR__ . '/config/config.local.neon');
 $configurator->addConfig(__DIR__ . '/config/model.neon');
+$configurator->addConfig(__DIR__ . '/config/components.neon');
 
 $container = $configurator->createContainer();
 
diff --git a/app/component/ExampleGrid.php b/app/component/ExampleGrid.php
new file mode 100644
index 0000000..830f1c1
--- /dev/null
+++ b/app/component/ExampleGrid.php
@@ -0,0 +1,66 @@
+<?php
+
+
+namespace App\Components;
+
+
+use App\Utils\DataGrid\DataGrid;
+use Model\Repository\LineRepository;
+use Ublaboo\DataGrid\Exception\DataGridException;
+
+/**
+ * Ukázkový datagrid, bude smazán při předání
+ *  - všechny možnosti použití gridu lze vidět na
+ *  https://gitlab.com/Fifal/dvm/blob/21afc037e3604093197e140defc05925df0b8c28/web-project/app/components/variant/VariantGrid.php
+ *
+ * @package App\Components
+ */
+class ExampleGrid extends DataGrid
+{
+    /** @var LineRepository */
+    private $lineRepository;
+
+    public function __construct(LineRepository $lineRepository)
+    {
+        $this->lineRepository = $lineRepository;
+
+        parent::__construct();
+    }
+
+    /**
+     * Abstraktní metoda, slouží k nastavení primárního klíče a nastavení datasource
+     *  1. $this->setPrimaryKey();
+     *  2. $this->setDataSource();
+     *
+     * @throws DataGridException
+     */
+    public function init()
+    {
+        $this->setPrimaryKey(LineRepository::COLUMN_ID);
+        $this->setDataSource($this->lineRepository->findAll());
+    }
+
+    /**
+     * Definice sloupečků, akcí, vyhledávácích filtrů gridu
+     *
+     * @throws DataGridException
+     */
+    public function define()
+    {
+        $this->addColumnNumber(LineRepository::COLUMN_ID, "ID");
+        $this->addColumnText(LineRepository::COLUMN_TRANSLITERATION, "Transliterace");
+        $this->addFilterText(LineRepository::COLUMN_TRANSLITERATION, "Transliterace");
+
+        $this->addAction(LineRepository::COLUMN_TRANSLITERATION, "Smazat");
+    }
+}
+
+interface IExampleGirdFactory
+{
+    /**
+     * Funkce create je implementována již v předkovi
+     *
+     * @return ExampleGrid
+     */
+    public function create();
+}
\ No newline at end of file
diff --git a/app/config/components.neon b/app/config/components.neon
new file mode 100644
index 0000000..c685bd9
--- /dev/null
+++ b/app/config/components.neon
@@ -0,0 +1,5 @@
+services:
+    # Gridy
+    - App\Components\IExampleGirdFactory
+
+    # Formuláře
\ No newline at end of file
diff --git a/app/presenters/HomepagePresenter.php b/app/presenters/HomepagePresenter.php
index a58c918..ec02ca1 100644
--- a/app/presenters/HomepagePresenter.php
+++ b/app/presenters/HomepagePresenter.php
@@ -2,9 +2,27 @@
 
 namespace App\Presenters;
 
+use App\Components\IExampleGirdFactory;
 use Nette;
 
 
 final class HomepagePresenter extends Nette\Application\UI\Presenter
 {
+    /** @var IExampleGirdFactory  */
+    private $exampleGridFactory;
+
+    public function __construct(IExampleGirdFactory $exampleGridFactory)
+    {
+        parent::__construct();
+
+        $this->exampleGridFactory = $exampleGridFactory;
+    }
+
+    public function actionDefault(){
+
+    }
+
+    public function createComponentDataGrid(){
+        return $this->exampleGridFactory->create();
+    }
 }
diff --git a/app/presenters/templates/@layout.latte b/app/presenters/templates/@layout.latte
index ebae0fe..7db30f1 100644
--- a/app/presenters/templates/@layout.latte
+++ b/app/presenters/templates/@layout.latte
@@ -1,19 +1,61 @@
 <!DOCTYPE html>
 <html>
 <head>
-	<meta charset="utf-8">
-	<meta name="viewport" content="width=device-width">
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width">
 
-	<title>{ifset title}{include title|stripHtml} | {/ifset}Nette Web</title>
+    <title>{ifset title}{include title|stripHtml} | {/ifset}Nette Web</title>
+
+    <!-- jQuery -->
+    <script
+            src="https://code.jquery.com/jquery-3.3.1.min.js"
+            integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
+            crossorigin="anonymous"></script>
+
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.js"
+            integrity="sha256-wqAoCRn9//AnHSl4qbXVhqdvmgFQqN5Elqp4Eb2wOXA=" crossorigin="anonymous"></script>
+
+    <!-- Bootstrap CDN -->
+    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet"
+          integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
+    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"
+            integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T"
+            crossorigin="anonymous"></script>
+
+    <!-- Bootstrap Select -->
+    <link rel="stylesheet"
+          href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.12.4/css/bootstrap-select.min.css">
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.12.4/js/bootstrap-select.min.js"></script>
+
+    <!-- Bootstrap Datepicker -->
+    <link rel="stylesheet"
+          href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.8.0/css/bootstrap-datepicker3.min.css"
+          integrity="sha256-mlKJFBS1jbZwwDrZD1ApO7YFS6MA1XDN37jZ9GDFC64=" crossorigin="anonymous"/>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.8.0/js/bootstrap-datepicker.min.js"
+            integrity="sha256-tW5LzEC7QjhG0CiAvxlseMTs2qJS7u3DRPauDjFJ3zo=" crossorigin="anonymous"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.8.0/locales/bootstrap-datepicker.cs.min.js"
+            integrity="sha256-pbW7ult8QcDlimj+JQplrsCieqCvQ/8VGr7ZdTuev8c=" crossorigin="anonymous"></script>
+
+    <!-- Font awesome -->
+    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.2.0/css/all.css"
+          integrity="sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ" crossorigin="anonymous">
+
+
+    <script src="https://nette.github.io/resources/js/netteForms.min.js"></script>
+    <script src="{$basePath}/js/nette.ajax.js"></script>
 </head>
 
 <body>
-	<div n:foreach="$flashes as $flash" n:class="flash, $flash->type">{$flash->message}</div>
+<div n:foreach="$flashes as $flash" n:class="flash, $flash->type">{$flash->message}</div>
 
-	{include content}
+{include content}
 
-	{block scripts}
-	<script src="https://nette.github.io/resources/js/netteForms.min.js"></script>
-	{/block}
+{block scripts}
+    <script>
+        $(function () {
+            $.nette.init();
+        });
+    </script>
+{/block}
 </body>
 </html>
diff --git a/app/presenters/templates/Homepage/default.latte b/app/presenters/templates/Homepage/default.latte
index 2edaa1c..007920d 100644
--- a/app/presenters/templates/Homepage/default.latte
+++ b/app/presenters/templates/Homepage/default.latte
@@ -1,38 +1,11 @@
 {* This is the welcome page, you can delete it *}
 
 {block content}
-<div id="banner">
-	<h1 n:block=title>Congratulations!</h1>
-</div>
 
-<div id="content">
-	<h2>You have successfully created your <a href="https://nette.org">Nette</a> Web project.</h2>
-
-	<p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEYAAABVCAYAAAD0bJKxAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAACudJREFUeNrMXG1sFMcZfvfu7ECw7+xDaQL+AApE1B+EJLapSNs4QIhtVWpSVTJNVIkqatX+II1apf8SoTZSq/KHiv5oqRolVaUS5UfgD04qtRAcUGipSrDdjxjHxDa0xOIcDBiMOU9nZj9udndmZ2Y/zjen0d7uzs7u+8z7Pu87H7sGCFLvrqfWGABbwMyBqfW5C5BrvhFYBkFFpiMvP3HlQ94JgwPI43izD+du1dpbn8XArLlRmaLLW+Qiznte3n7lPS4wPU/uyuHN6zg/rXpPwzAvb+kfhSzWGJTMg0fHBfGe3ZQ+hbORonIQcN5wAdOz80kCygnWbOrzeWhsbITabBZyuSz/ptYNjU3HwaidXhIBw6SF4i2YW5iGIlownz+FAUpTKLpfsTQnYwqI9tmgVFVVwUMPb4F8fqWjTsW7RQtiDio43WMsg3S6puwChtXE6lQNrKi6D67dnoC5uzOAiqY8qTS1mHWkTGrXjp1rcB0v2hdtfHAj1pAcLBaLUFxkcvEuzkUr33WdIxipZm1QUMiskHLLmiFtVNHyWAzyfGt/8ufPfc3WmD0swCMj/6RZyCucYy35Mcimb8bCJShZog1MBBysNcRyjmawGW1RIdige4vMAy2RgNIqRfUv4mwCgzUGoTo/XbNUgpTuiipJwGg3qHPIV0Sqij47FHckLqBi/Uiwn1F9JkNYMyqft0EJWh+yhEQ2MAINUeEWFzYoPg5ZMgJmrs2UorQQ3CIhGZSghkSqBsnNIhOKW3wgSmRACVoSitdUEVLkGCOoLxDyAcvlwWR1I+4Jg88xOtzCtaSKETAi+fqVQf8mcZFvbAJGMSUf+YbgFtEDLbmAEJLzXO46KrdYWobEalB+ARN11zG3cFkFFNSLVGkhtLsWAVkm4kVJgcfGMTKyNUS8wlynRb4oIWVBMVxiyTE+Pu7nGCOMlyIcg5ZOQKXLVOo1LGywMJk4ngtVmoBhb2zluvr6mNw1CmEiuMCqulZYXpVzDn08fTo2jYuCXzqdJqYk6F3zHLbQXetz97KqLPxg+3HXsbfO7oW/T7wp65smZ6qMHCnR4DHS+Kl2ztjcsqrXV6xlda+7nKLqq2S2TpUx9Ewk2zX8SKum1tW9nGN9sCyThdsLs9EpBkXgGaIxNGqVZFlFSLMVifAEBJJu3bkGlz8bdgHmKs6bfok4fcKrt6RRyAJGoT4pcCpqypoRoy1j06dg7NNTLnOKRcCwc1sOx0QzXefhdFqQNaORSwMwcnnA2W9r6KPEHMvknSb/8PtKcfSwFXoW9SuaqPB2GsbAEE4hJrW8OucAd/bim1K+6FjXD60NvbD+vseca23zJFo4+NEhrJGnlTmI9a4pbTPlNB2yIl+k0IKstlyaGYbbd2bpcQKQi2cknuTFXX+B/q6DFGQWFJLIfltjH3x/+xHoWNuvSVaS3rU2sSuOdnas3e38H/zoN04ZAkznOvMcEYqYEwVNUCsh7Ib6NijcnKDaMXNz0oqPcrQeG6zdWw/CZdwApBF0vFL43jVjWr6YA4nNiAjjmNHUgHPfkaljLnNqwyZydvywcMj0bx//ES5cOUXLeNO7Q7+AH/Uch3xNM93/8oPfhcNnXpC3HCNHKnIA5+h6sJqSX1tDDwOKCQR7GTnGahYKsOuxT0+XQPHcjmjau8P7S4SONZDvmjmG5It8Ax5CDhxS8iAd60pmNDQ14LvPMHNsw/2PQf29TfzoVUHAS4VhF+foxj+ZOKJGhOTXm2bUXgJh8pjvOgJW4caEYwJtjb1w8j+HlJ5r/f3bqJmSTulqsvUQMrl/weIhmWdSGqhSHbySVUOEZN3pt7/ye245ViBCoif/fUjYbnks7FPtL0F7k98z8RqmcGNSY8w3TJfCg4JKsNXJmBERgpiKLDXk24UCdX5+NzzT8aoPEELIrDnyPI5wAgAJNMaQBG/aw4R2y9Y0USHDJGpORGuQ22ye3XawFA8VhuCd8/thaNLNWwe+Na38jCDsXUO4yTYVjWHNiHDIT99+NBDY57vfoOZBUhfWjPf+5Tanns0/doHyqz89jc1zVjlGEY6Fo4C+UtRhQZ7XIMI5BItbVegMrJ2hiQGXOREuYQtveKBkIu98uJ+CInOuog6n79khYNghCjZeoIhQrBn99cJhqas8P3HMrXFN7i4Cm+asWMhbV3tjrzSY84IS2LtWGWYQDT14BSR/O9eXtOUqNqMpHF/IYh6iASw4XUwd3pRf0ewTkHQnera8FKwxIo2FOE0pQE1ZoVgTkZkkW7bR8k52vVOYV+z0TNersP6Bbc6lG/D/vT1H6DW6QxAIacQxSp5KEOA15NtgpRWskXQGm5GqpRKNeQ5KnmcrBnjgnBnmD/xjP3xnhxkH3Yvd9Qs9R33Xz81fo0TfuLLd/5goeKRWSWPUTImvpl0b3GZ06eqwcmh+ax6b0yeMOeG67NPnsTb9YXAvFZ6XRv97Cva99Ygr/iG9blAZvbMHGzoffoTMYXRHMaOWr1+BbMM8J0AjoXnWinZnKb/oDFuQ+IdkJ3j732lPlJyFzc19kK81y9zCQI3iMrQByPW1pesL1ydx40wG3py8aFG93DjxSt/YE1pdApFZIcGKqqmrw5F4i7Q4EUik/XNYqz4YPSxE9+r1CZqV+4BUEEO/SyAEUXX8Vbl/imIpolXUM0tANSZKV4B1hZUiYGRhoPS+UsjBu3AzbolOmoUcpPeWyUS7GfJzTAUIGLr3617ny/SuwYhgS0ssZAzvQyEA/nLWKKstyy1kIubonnDTJRYNUFDMNBLnKlnJhJveclbRw+mudkhYwDiSOeEWcbItyorNxAQM8W4T8k24RSEIw3AHb2UUMNTtZCuq4nDXNqhIZdVmOQXUvZzTsNK3T3TvVAkCRqlMqDFh3z5BlSSgAvhIiWOSfIYyxDdJfGxDbzlrXBpTRgGDL0UcOQxPgGcEX6TzgOV+Qx/F9aYqf31MtI4dqiQBwzZgrO5a0IlEib1mwq8vjne1E3DX4SfqkhAwDkeR2MuiSypgyLpcqzbB/ARTLIGRaC5ae80/BC+g1lotHmT63nrMkxft3vU5jRGGeEwigdgmjirTGVoRxSP129d+dxTDdU4CrPJy+KitqL0EHsSv8OlOy6bMrzIdoRrzhZYWst2DzxKTqqukFox1BkFSoGo5+fSb8frP+sc/sTkG3j/zAfl6YDfOn4WOY2JuQV2uCfM2iq0p1RiUTJVBrMb5iJlxbba0EulLW79IFrQdAOuDXqpp01cLULv6TqjWLstcEacqEpUQTslU0xCFgNL982+O08nw6xgTB5izj9bBjtFF+r9106a1YH5B8XHcZ6rDLNwddLPN35iBXOMCVFySjgFRD/TLX3+vcMA+dnJwEO7Mz4PhcT6Gwtb5v/P5mrpVGzMPkclMywwMS63dW0CGrba0bMnFSx0fHR803P/NGNRA1mchzW4f2Zrn7DKIBqvc4wCv/XDmhAc+15bUmeYJzcmi4ynBcVkdPOB57Y04HY8wr1UtKlzvnDOs6FcmUCrgWNgt+98LDvuQixwBFz3nZFtRPUIgw4ASJHBKsB+UvfcAjvCybH/gxGD2Fz3gpphjwNn3BbcZibmkJMdk/1MKZhekMSigpXn/FxXKiupzmVIqwP5l/KLDRTJiB0WegSBuAL33TET7XI+k6p1AAigEAGAodM2C3mlBgmMywlbZAjjrqtSTobEvKwsaGgMhQFPd56b/CzAArAe2YDJd4I4AAAAASUVORK5CYII=" alt="">
-	If you are exploring Nette for the first time, you should read the
-	<a href="https://doc.nette.org/quickstart">Quick Start</a>, <a href="https://doc.nette.org">documentation</a>,
-	<a href="https://pla.nette.org">tutorials</a> and <a href="https://forum.nette.org">forum</a>.</p>
-
-	<h2>We hope you enjoy Nette!</h2>
-</div>
-
-<style>
-	html { font: normal 18px/1.3 Georgia, "New York CE", utopia, serif; color: #666; -webkit-text-stroke: 1px rgba(0,0,0,0); overflow-y: scroll; }
-	body { background: #3484d2; color: #333; margin: 2em auto; padding: 0 .5em; max-width: 600px; min-width: 320px; }
-
-	a { color: #006aeb; padding: 3px 1px; }
-	a:hover, a:active, a:focus { background-color: #006aeb; text-decoration: none; color: white; }
-
-	#banner { border-radius: 12px 12px 0 0; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAB5CAMAAADPursXAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAGBQTFRFD1CRDkqFDTlmDkF1D06NDT1tDTNZDk2KEFWaDTZgDkiCDTtpDT5wDkZ/DTBVEFacEFOWD1KUDTRcDTFWDkV9DkR7DkN4DkByDTVeDC9TDThjDTxrDkeADkuIDTRbDC9SbsUaggAAAEdJREFUeNqkwYURgAAQA7DH3d3335LSKyxAYpf9vWCpnYbf01qcOdFVXc14w4BznNTjkQfsscAdU3b4wIh9fDVYc4zV8xZgAAYaCMI6vPgLAAAAAElFTkSuQmCC); }
-
-	h1 { font: inherit; color: white; font-size: 50px; line-height: 121px; margin: 0; padding-left: 4%; background: url(https://files.nette.org/images/logo-nette@2.png) no-repeat 95%; background-size: 130px auto; text-shadow: 1px 1px 0 rgba(0, 0, 0, .9); }
-	@media (max-width: 600px) {
-		h1 { background: none; font-size: 40px; }
-	}
-
-	#content { background: white; border: 1px solid #eff4f7; border-radius: 0 0 12px 12px; padding: 10px 4%; overflow: hidden; }
-
-	h2 { font: inherit; padding: 1.2em 0; margin: 0; }
-
-	img { border: none; float: right; margin: 0 0 1em 3em; }
-</style>
+<div class="container">
+    <div class="row">
+        <div class="col-md-12">
+            {control dataGrid}
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/app/utils/datagrid/DataGrid.php b/app/utils/datagrid/DataGrid.php
new file mode 100644
index 0000000..6fd54f3
--- /dev/null
+++ b/app/utils/datagrid/DataGrid.php
@@ -0,0 +1,165 @@
+<?php
+
+namespace App\Utils\DataGrid;
+
+use Nette\ComponentModel\IContainer;
+use Nette\Database\Table\Selection;
+use Ublaboo\DataGrid\Column\ColumnDateTime;
+use Ublaboo\DataGrid\Column\ColumnNumber;
+use Ublaboo\DataGrid\Column\ColumnText;
+use Ublaboo\DataGrid\DataGrid as BaseDataGrid;
+use Ublaboo\DataGrid\Exception\DataGridException;
+use Ublaboo\DataGrid\Filter\FilterText;
+use Ublaboo\DataGrid\Localization\SimpleTranslator;
+
+/**
+ * Třída DataGrid poskytuje DataGrid s českou lokalizací, šablonou pro Bootstrap 4 a vyhledáváním s použitím LIKE
+ * @package App\Utils\DataGrid
+ */
+abstract class DataGrid extends BaseDataGrid
+{
+    /**
+     * DataGrid konstruktor.
+     * @param IContainer|null $parent
+     * @param null $name
+     */
+    public function __construct(IContainer $parent = null, $name = null)
+    {
+        parent::__construct($parent, $name);
+
+        $translator = new SimpleTranslator([
+            'ublaboo_datagrid.no_item_found_reset' => 'Žádné položky nenalezeny. Filtr můžete vynulovat',
+            'ublaboo_datagrid.no_item_found' => 'Žádné položky nenalezeny.',
+            'ublaboo_datagrid.here' => 'zde',
+            'ublaboo_datagrid.items' => 'Položky',
+            'ublaboo_datagrid.all' => 'všechny',
+            'ublaboo_datagrid.from' => 'z',
+            'ublaboo_datagrid.reset_filter' => 'Resetovat filtr',
+            'ublaboo_datagrid.group_actions' => 'Hromadné akce',
+            'ublaboo_datagrid.show_all_columns' => 'Zobrazit všechny sloupce',
+            'ublaboo_datagrid.hide_column' => 'Skrýt sloupec',
+            'ublaboo_datagrid.action' => 'Akce',
+            'ublaboo_datagrid.previous' => 'Předchozí',
+            'ublaboo_datagrid.next' => 'Další',
+            'ublaboo_datagrid.choose' => 'Vyberte',
+            'ublaboo_datagrid.execute' => 'Provést',
+            'ublaboo_datagrid.show_default_columns' => 'Zobrazit výchozí sloupce',
+            'ublaboo_datagrid.filter_submit_button' => 'Filtrovat',
+
+            'Name' => 'Jméno',
+            'Inserted' => 'Vloženo'
+        ]);
+
+        $this->setTranslator($translator);
+
+        $this->setTemplateFile(__DIR__ . '/templates/datagrid.latte');
+
+        $this->setColumnsHideable();
+
+        $this->setAutoSubmit(false);
+
+        $this->setStrictSessionFilterValues(false);
+
+        $this->init();
+
+        $this->define();
+    }
+
+    /**
+     * Abstraktní metoda, slouží k nastavení primárního klíče a nastavení datasource
+     *  1. $this->setPrimaryKey();
+     *  2. $this->setDataSource();
+     *
+     * @throws DataGridException
+     */
+    public abstract function init();
+
+    /**
+     * Definice sloupečků, akcí, vyhledávácích filtrů gridu
+     *
+     * @throws DataGridException
+     */
+    public abstract function define();
+
+    /**
+     * Vrací vytvořený datagrid - používáno v továrničkách
+     *
+     * @return $this
+     */
+    public function create()
+    {
+        return $this;
+    }
+
+    /**
+     * Nastavuje každý sloupeček řaditelný jako výchozí nastavení
+     *
+     * @param string $key : column name
+     * @param string $name : filter label
+     * @param null $column
+     * @return null|ColumnText
+     */
+    public function addColumnText($key, $name, $column = null): ColumnText
+    {
+        $column = parent::addColumnText($key, $name, $column);
+        $column->setSortable();
+
+        return $column;
+    }
+
+    /**
+     * Nastavuje každý sloupeček řaditelný jako výchozí nastavení
+     *
+     * @param string $key : column name
+     * @param string $name : filter label
+     * @param null $column
+     * @return ColumnNumber
+     */
+    public function addColumnNumber($key, $name, $column = null): ColumnNumber
+    {
+        $column = parent::addColumnNumber($key, $name, $column);
+        $column->setSortable();
+
+        return $column;
+    }
+
+    /**
+     * Nastavuje každý sloupeček řaditelný jako výchozí nastavení
+     *
+     * @param string $key : column name
+     * @param string $name : filter label
+     * @param null $column
+     * @return ColumnDateTime
+     */
+    public function addColumnDateTime($key, $name, $column = null): ColumnDateTime
+    {
+        $column = parent::addColumnDateTime($key, $name, $column);
+        $column->setSortable();
+
+        return $column;
+    }
+
+    /**
+     * Umožňění filtrování sloupečků s textem podle jeho jednotlivých částí pomocí LIKE SQL commandu
+     *
+     * @param string $key : column name
+     * @param string $name : filter label
+     * @param null $columns
+     * @return FilterText
+     * @throws DataGridException
+     */
+    public function addFilterText($key, $name, $columns = null): FilterText
+    {
+        $filterText = parent::addFilterText($key, $name, $columns);
+
+        $filter = parent::getFilter($key);
+
+        $filter->setCondition(function ($selection, $value) use ($key)
+        {
+            /** @var Selection $selection */
+            $selection->where($key . ' LIKE', '%' . $value . '%');
+        });
+
+        return $filterText;
+    }
+}
\ No newline at end of file
diff --git a/app/utils/datagrid/templates/column_multi_action.latte b/app/utils/datagrid/templates/column_multi_action.latte
new file mode 100644
index 0000000..6bc5135
--- /dev/null
+++ b/app/utils/datagrid/templates/column_multi_action.latte
@@ -0,0 +1,18 @@
+{**
+ * @param $multi_action Ublaboo\DataGrid\Column\MultiAction
+ *}
+
+<div class="dropdown">
+	{$multi_action->renderButton()|noescape}
+	<ul class="dropdown-menu">
+		<li n:foreach="$multi_action->getActions() as $actionKey => $action">
+			{continueIf !$multi_action->testRowCondition($actionKey, $row)}
+
+			{if $action->hasTemplate()}
+				{include $action->getTemplate(), item => $row->getItem(), (expand) $action->getTemplateVariables(), row => $row}
+			{else}
+				{$action->render($row)|noescape}
+			{/if}
+		</li>
+	</ul>
+</div>
diff --git a/app/utils/datagrid/templates/column_status.latte b/app/utils/datagrid/templates/column_status.latte
new file mode 100644
index 0000000..9bcf933
--- /dev/null
+++ b/app/utils/datagrid/templates/column_status.latte
@@ -0,0 +1,25 @@
+{**
+ * @param $row    Ublaboo\DataGrid\Row
+ * @param $status Ublaboo\DataGrid\Column\ColumnStatus
+ *}
+
+{var $active_option = $status->getCurrentOption($row)}
+
+<div class="dropdown">
+	{if $active_option}
+		<button class="dropdown-toggle {$active_option->getClass()} {$active_option->getClassSecondary()}" type="button" data-toggle="dropdown">
+			{if $active_option->getIcon()}<i class="{$icon_prefix}{$active_option->getIcon()}"></i> {/if}
+			{$active_option->getText()|translate} <i n:if="$status->hasCaret()" class="caret"></i>
+		</button>
+	{else}
+		{$row->getValue($status->getColumn())}
+	{/if}
+	<ul class="dropdown-menu">
+		<li n:foreach="$status->getOptions() as $option">
+			<a class="{$option->getClassInDropdown()}" n:href="changeStatus!, id => $row->getId(), key => $status->getKey(), value => $option->getValue()">
+				{if $option->getIconSecondary()}<i class="datagrid-column-status-option-icon {$icon_prefix}{$option->getIconSecondary()}"></i> {/if}
+				{$option->getText()|translate}
+			</a>
+		</li>
+	</ul>
+</div>
diff --git a/app/utils/datagrid/templates/datagrid.latte b/app/utils/datagrid/templates/datagrid.latte
new file mode 100644
index 0000000..007f679
--- /dev/null
+++ b/app/utils/datagrid/templates/datagrid.latte
@@ -0,0 +1,527 @@
+{**
+ * @param Column[]        $columns            Available columns
+ * @param Action[]        $actions            Available actions
+ * @param Export[]        $exports            Available exports
+ * @param Filter[]        $filters            Available filters
+ * @param ToolBarButton[] $toolbar_buttons    Available toolbar_buttons
+ * @param Form            $filter             Workaround for latte snippets
+ * @param Row[]           $rows               List of rows (each contain a item from data source)
+ * @param DataGrid        $control            Parent (DataGrid)
+ * @param string          $original_template  Original template file path
+ * @param string          $icon_prefix        Icon prefix (fa fa-)
+ * @param array           $columns_visibility What columns are visible
+ * @param InlineEdit|null $inlineEdit  Inline editing data
+ * @param InlineEdit|null $inlineAdd   Inline add data
+ *}
+
+<div class="datagrid datagrid-{$control->getName()}" data-refresh-state="{link refreshState!}">
+	{**
+	 * Own data
+	 *}
+	<div n:snippet="grid">
+	{snippetArea gridSnippets}
+		{form filter, class => 'ajax'}
+			{**
+			 * Filter form
+			 *}
+			{if $control->hasOuterFilterRendering()}
+				{block outer-filters}
+					<div class="row text-right datagrid-collapse-filters-button-row" n:if="$control->hasCollapsibleOuterFilters()">
+						<div class="col-sm-12">
+							<button class="btn btn-xs btn-primary active" type="button" data-toggle="collapse" data-target="#datagrid-{$control->getName()}-row-filters">
+								<i n:block = "icon-filter" class="{$icon_prefix}filter"></i> {='ublaboo_datagrid.show_filter'|translate}
+							</button>
+						</div>
+					</div>
+
+					{if $control->hasCollapsibleOuterFilters() && !$filter_active}
+						{var $filter_row_class = 'row-filters collapse'}
+					{elseif $filter_active}
+						{var $filter_row_class = 'row-filters collapse in'}
+					{else}
+						{var $filter_row_class = 'row-filters'}
+					{/if}
+					<div class="{$filter_row_class}" id="datagrid-{$control->getName()}-row-filters">
+						<div class="row">
+							{var $i = 0}
+							{var $filter_columns_class = 'col-sm-' . (12 / $control->getOuterFilterColumnsCount())}
+							<div class="datagrid-row-outer-filters-group {$filter_columns_class}" n:foreach="$filters as $f">
+								{**
+								 * Each fitler is rendered separately in its own template
+								 *}
+								{var $filter_block = 'filter-' . $f->getKey()}
+								{var $filter_type_block = 'filtertype-' . $f->getType()}
+
+								{ifset #$filter_block}
+									{include #$filter_block, filter => $f, input => $form['filter'][$f->getKey()], outer => TRUE}
+								{else}
+									{ifset #$filter_type_block}
+										{include #$filter_type_block, filter => $f, input => $form['filter'][$f->getKey()], outer => TRUE}
+										{else}
+											{include $f->getTemplate(), filter => $f, input => $form['filter'][$f->getKey()], outer => TRUE}
+									{/ifset}
+								{/ifset}
+								{var $i = $i+1}
+							</div>
+							<div class="col-sm-12" n:if="!$control->hasAutoSubmit()">
+								<div class="text-right datagrid-manual-submit">
+									{input $filter['filter']['submit']}
+								</div>
+							</div>
+						</div>
+					</div>
+				{/block}
+			{/if}
+			<table class="{block table-class}table table-hover table-striped table-bordered table-sm{/block}" n:snippet="table" n:block="data">
+				<thead n:block="header">
+					<tr class="row-group-actions" n:if="$hasGroupActions || $exports || $toolbar_buttons || $control->canHideColumns() || $inlineAdd" n:block="group-actions">
+						<th colspan="{$control->getColumnsCount()}" class="ublaboo-datagrid-th-form-inline">
+							{if $hasGroupActions}
+								{block group_actions}
+									{='ublaboo_datagrid.group_actions'|translate}:
+									{foreach $filter['group_action']->getControls() as $form_control}
+										{if $form_control instanceof \Nette\Forms\Controls\SubmitButton}
+											{input $form_control, class => 'btn btn-primary btn-sm', style => 'display:none'}
+										{elseif $form_control->getName() == 'group_action'}
+											{input $form_control, class => 'form-control input-sm form-control-sm', disabled => TRUE}
+										{else}
+											{input $form_control, style => 'display:none'}
+										{/if}
+									{/foreach}
+									{if $control->shouldShowSelectedRowsCount()}
+										<span class="datagrid-selected-rows-count"></span>
+									{/if}
+								{/block}
+							{/if}
+
+							<div class="datagrid-toolbar" n:if="$control->canHideColumns() || $inlineAdd || $exports || $toolbar_buttons">
+								<span n:if="$toolbar_buttons">
+									{foreach $toolbar_buttons as $toolbar_button}{$toolbar_button->renderButton()}{/foreach}
+								</span>
+
+								<span class="datagrid-exports" n:if="$exports" n:snippet="exports" n:block="exports">
+									{foreach $exports as $export}{$export->render()}{/foreach}
+								</span>
+
+								<div class="datagrid-settings" n:block="settings" n:if="$control->canHideColumns() || $inlineAdd">
+									{**
+									 * Inline add
+									 *}
+									{if $inlineAdd}
+										{$inlineAdd->renderButtonAdd()}
+									{/if}
+
+									{**
+									 * Hideable columns
+									 *}
+									<div class="btn-group">
+										<button type="button" class="btn btn-xs btn-default btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" n:if="$control->canHideColumns()">
+											<i n:block = "icon-gear" class="{$icon_prefix}cog"></i>
+										</button>
+										<ul class="dropdown-menu dropdown-menu-right dropdown-menu--grid">
+											<li n:foreach="$columns_visibility as $v_key => $visibility">
+												{if $visibility['visible']}
+													<a n:href="hideColumn!, column => $v_key" class="ajax dropdown-item">
+														<i n:block = "icon-checked" class="{$icon_prefix}check-square"></i>
+														{include #column-header, column => $visibility['column']}
+													</a>
+												{else}
+													<a n:href="showColumn!, column => $v_key" class="ajax dropdown-item">
+														<i n:block = "icon-unchecked" class="{$icon_prefix}square"></i>
+														{include #column-header, column => $visibility['column']}
+													</a>
+												{/if}
+											</li>
+											<li role="separator" class="divider dropdown-divider"></li>
+											<li>
+												<a n:href="showAllColumns!" class="ajax dropdown-item"><i n:block = "icon-eye" class="{$icon_prefix}eye"></i> {='ublaboo_datagrid.show_all_columns'|translate}</a>
+											</li>
+											<li n:if="$control->hasSomeColumnDefaultHide()">
+												<a n:href="showDefaultColumns!" class="ajax dropdown-item"><i n:block = "icon-repeat" class="{$icon_prefix}redo"></i> {='ublaboo_datagrid.show_default_columns'|translate}</a>
+											</li>
+										</ul>
+									</div>
+								</div>
+							</div>
+						</th>
+					</tr>
+					<tr n:block="header-column-row">
+						<th n:snippet="thead-group-action" n:if="$hasGroupActions" n:attr="'rowspan=2' => !empty($filters) && !$control->hasOuterFilterRendering()" class="col-checkbox">
+							<input n:if="$hasGroupActionOnRows" n:class="$control->useHappyComponents() ? 'happy gray-border' , primary" name="{$control->getName()|lower}-toggle-all" type="checkbox" data-check="{$control->getName()}" data-check-all="{$control->getName()}">
+						</th>
+						{foreach $columns as $key => $column}
+							{var $th = $column->getElementForRender('th', $key)}
+							{$th->startTag()|noescape}
+								{var $col_header = 'col-' . $key . '-header'}
+
+								{**
+								 * Column header can be defined also with block {col-<key>-header}
+								 *}
+								{ifset #$col_header}
+									{include #$col_header, column => $column}
+								{else}
+									{if $column->isSortable()}
+										<a n:class="$column->isSortedBy() ? 'sort' : '', 'ajax', 'success'" href="{link sort!, sort => $control->getSortNext($column)}" id="datagrid-sort-{$key}">
+											{include #column-header, column => $column}
+
+											{if $column->isSortedBy()}
+												{if $column->isSortAsc()}
+													<i n:block="icon-sort-up" class="{$icon_prefix}caret-up"></i>
+												{else}
+													<i n:block="icon-sort-down" class="{$icon_prefix}caret-down"></i>
+												{/if}
+											{else}
+												<i n:block="icon-sort" class="{$icon_prefix}sort"></i>
+											{/if}
+										</a>
+									{else}
+										{include #column-header, column => $column}
+									{/if}
+								{/ifset}
+
+								<div class="datagrid-column-header-additions">
+									<div class="btn-group column-settings-menu" n:if="$control->canHideColumns()">
+										<a class="dropdown-toggle success" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" href="">
+											<i n:block="icon-caret-down" class="{$icon_prefix}caret-down"></i>
+										</a>
+										<ul class="dropdown-menu dropdown-menu--grid">
+											<li>
+												<a n:href="hideColumn!, column => $key" class="ajax dropdown-item">
+													<i n:block = "icon-eye-slash" class="{$icon_prefix}eye-slash"></i> {='ublaboo_datagrid.hide_column'|translate}</a>
+											</li>
+										</ul>
+									</div>
+
+									{if $control->hasColumnReset()}
+										<a data-datagrid-reset-filter-by-column="{$key}" n:href="resetColumnFilter!, key => $key" n:class="isset($filters[$key]) && $filters[$key]->isValueSet() ? '' : 'hidden', 'ajax'" title="{='ublaboo_datagrid.reset_filter'|translate}">
+											<i n:block="icon-remove" class="{$icon_prefix}remove"></i>
+										</a>
+									{/if}
+								</div>
+							{$th->endTag()|noescape}
+						{/foreach}
+						<th n:if="$actions || $control->isSortable() || $items_detail || $inlineEdit || $inlineAdd" class="col-action text-center">
+							{='ublaboo_datagrid.action'|translate}
+						</th>
+					</tr>
+					<tr n:block="header-filters" n:if="!empty($filters) && !$control->hasOuterFilterRendering()">
+						{foreach $columns as $key => $column}
+							{var $th = $column->getElementForRender('th', $key)}
+							{$th->startTag()|noescape}
+							{var $col_header = 'col-filter-' . $key . '-header'}
+							{if !$control->hasOuterFilterRendering() && isset($filters[$key])}
+								{var $i = $filter['filter'][$key]}
+
+									{var $filter_block = 'filter-' . $filters[$key]->getKey()}
+									{var $filter_type_block = 'filtertype-' . $filters[$key]->getType()}
+
+								{ifset #$filter_block}
+									{include #$filter_block, filter => $filters[$key], input => $i, outer => FALSE}
+								{else}
+									{ifset #$filter_type_block}
+										{include #$filter_type_block, filter => $filters[$key], input => $i, outer => FALSE}
+									{else}
+										{include $filters[$key]->getTemplate(), filter => $filters[$key], input => $i, outer => FALSE}
+									{/ifset}
+								{/ifset}
+
+							{/if}
+							{$th->endTag()|noescape}
+						{/foreach}
+						<th n:if="$actions || $control->isSortable() || $items_detail || $inlineEdit || $inlineAdd" class="col-action text-center">
+							{if !$control->hasAutoSubmit() && !$control->hasOuterFilterRendering()}
+								{input $filter['filter']['submit']}
+							{/if}
+						</th>
+					</tr>
+				</thead>
+
+				{block tbody}
+				<tbody n:snippet="tbody" {if $control->isSortable()}data-sortable data-sortable-url="{plink $control->getSortableHandler()}" data-sortable-parent-path="{$control->getSortableParentPath()}"{/}>
+					{snippetArea items}
+						{if $inlineAdd && $inlineAdd->isPositionTop()}
+							{include inlineAddRow, columns => $columns}
+						{/if}
+
+						{foreach $rows as $row}
+							{var $item = $row->getItem()}
+
+							{if !isset($toggle_detail)}
+								{if $inlineEdit && $inlineEdit->getItemId() == $row->getId()}
+									{php $inlineEdit->onSetDefaults($filter['inline_edit'], $item); }
+
+									<tr data-id="{$row->getId()}" n:snippet="item-{$row->getId()}" n:class="$row->getControlClass()">
+										<td n:if="$hasGroupActions" class="col-checkbox"></td>
+
+										{foreach $columns as $key => $column}
+											{var $col = 'col-' . $key}
+
+											{var $td = $column->getElementForRender('td', $key, $row)}
+											{var $td->class[] = 'datagrid-inline-edit'}
+											{$td->startTag()|noescape}
+												{if isset($filter['inline_edit'][$key])}
+													{input $filter['inline_edit'][$key]}
+												{elseif $inlineEdit->showNonEditingColumns()}
+													{include column-value, column => $column, row => $row, key => $key}
+												{/if}
+											{$td->endTag()|noescape}
+										{/foreach}
+
+										<td class="col-action col-action-inline-edit">
+											{input $filter['inline_edit']['cancel'], class => 'btn btn-xs btn-danger'}
+											{input $filter['inline_edit']['submit'], class => 'btn btn-xs btn-primary'}
+											{input $filter['inline_edit']['_id']}
+											{input $filter['inline_edit']['_primary_where_column']}
+										</td>
+									</tr>
+								{else}
+									<tr data-id="{$row->getId()}" n:snippet="item-{$row->getId()}" n:class="$row->getControlClass()">
+										<td n:if="$hasGroupActions" class="col-checkbox">
+											{if $row->hasGroupAction()}
+												<input n:class="$control->useHappyComponents() ? 'happy gray-border' , primary" type="checkbox" data-check="{$control->getName()}" data-check-all-{$control->getName()|noescape} name="{$control->getName()|lower}_group_action_item[{$row->getId()}]">
+											{/if}
+										</td>
+										{foreach $columns as $key => $column}
+											{php $column = $row->applyColumnCallback($key, clone $column)}
+
+											{var $td = $column->getElementForRender('td', $key, $row)}
+											{$td->startTag()|noescape}
+												{include column-value, column => $column, row => $row, key => $key}
+											{$td->endTag()|noescape}
+										{/foreach}
+										<td n:if="$actions || $control->isSortable() || $items_detail || $inlineEdit || $inlineAdd" class="col-action">
+											{foreach $actions as $key => $action}
+												{if $row->hasAction($key)}
+													{if $action->hasTemplate()}
+														{include $action->getTemplate(), item => $item, (expand) $action->getTemplateVariables(), row => $row}
+													{else}
+														{$action->render($row)|noescape}
+													{/if}
+												{/if}
+											{/foreach}
+											<span class="handle-sort btn btn-xs btn-default btn-secondary" n:if="$control->isSortable()">
+												<i n:block = "icon-arrows-v" class="{$icon_prefix}arrows-v"></i>
+											</span>
+											{if $inlineEdit && $row->hasInlineEdit()}
+												{$inlineEdit->renderButton($row)|noescape}
+											{/if}
+											{if $items_detail && $items_detail->shouldBeRendered($row)}
+												{$items_detail->renderButton($row)|noescape}
+											{/if}
+										</td>
+									</tr>
+								{/if}
+							{/if}
+
+							{**
+							 * Item detail
+							 *}
+							{if $items_detail && $items_detail->shouldBeRendered($row)}
+								<tr class="row-item-detail item-detail-{$row->getId()}" n:snippet="item-{$row->getId()}-detail">
+									{if isset($toggle_detail) && $toggle_detail == $row->getId()}
+										{var $item_detail_params = ['item' => $item, '_form' => $filter] + $items_detail->getTemplateVariables()}
+
+										{if isset($filter['items_detail_form'])}
+											{var $item_detail_params['items_detail_form'] = $filter['items_detail_form']}
+										{/if}
+
+										{ifset #detail}
+											<td colspan="{$control->getColumnsCount()}">
+												<div class="item-detail-content">
+													{include #detail, (expand) $item_detail_params}
+												</div>
+											</td>
+										{elseif $items_detail}
+											<td colspan="{$control->getColumnsCount()}">
+												<div class="item-detail-content">
+													{if $items_detail->getType() == 'template'}
+														{include $items_detail->getTemplate(), (expand) $item_detail_params}
+													{else}
+														{$items_detail->render($item)|noescape}
+													{/if}
+												</div>
+											</td>
+										{/ifset}
+									{/if}
+								</tr>
+								<tr class="row-item-detail-helper"></tr>
+							{/if}
+						{/foreach}
+
+						{if $inlineAdd && $inlineAdd->isPositionBottom()}
+							{include inlineAddRow, columns => $columns}
+						{/if}
+
+						<tr class="datagrid-row-columns-summary" n:if="!empty($rows) && ($columnsSummary || $control->hasSomeAggregationFunction())" n:snippet="summary">
+							<td n:if="$hasGroupActions" class="col-checkbox"></td>
+
+							{if $columnsSummary && $columnsSummary->someColumnsExist($columns)}
+								{include columnsSummary, columns => $columns}
+							{/if}
+
+							{if $control->hasSomeAggregationFunction()}
+								{include aggregationFunctions, columns => $columns}
+							{/if}
+
+							<td n:if="$actions || $control->isSortable() || $items_detail || $inlineEdit || $inlineAdd" class="col-action"></td>
+						</tr>
+
+						{block noItems}
+							<tr n:if="!$rows">
+								<td colspan="{$control->getColumnsCount()}">
+									{if $filter_active}
+										{='ublaboo_datagrid.no_item_found_reset'|translate} <a class="link ajax" n:href="resetFilter!">{='ublaboo_datagrid.here'|translate}</a>.
+									{else}
+										{='ublaboo_datagrid.no_item_found'|translate}
+									{/if}
+								</td>
+							</tr>
+						{/block}
+					{/snippetArea}
+				</tbody>
+				{/block}
+				{block tfoot}
+					<tfoot n:snippet="pagination">
+						{if $control->isPaginated() || $filter_active}
+							<tr n:block="pagination">
+								<td colspan="{$control->getColumnsCount()}" n:if="!$control->isTreeView()" class="row-grid-bottom">
+									<div class="col-items">
+										<small class="text-muted" n:if="$control->isPaginated()">
+											({var $paginator = $control['paginator']->getPaginator()}
+
+											{if $control->getPerPage() === 'all'}
+												{='ublaboo_datagrid.items'|translate}: {='ublaboo_datagrid.all'|translate}
+											{else}
+												{='ublaboo_datagrid.items'|translate}: {$paginator->getOffset() > 0 ? $paginator->getOffset() + 1 : ($paginator->getItemCount() > 0 ? 1 : 0)} - {sizeof($rows) + $paginator->getOffset()}
+												{='ublaboo_datagrid.from'|translate} {$paginator->getItemCount()}
+											{/if})
+										</small>
+									</div>
+									<div class="col-pagination text-center">
+										{**
+										* Pagination
+										*}
+										{control paginator}
+									</div>
+									<div class="col-per-page text-right">
+										{**
+										* Items per page form (display only beside paginated grido)
+										*}
+										<a n:if="$filter_active" n:href="resetFilter!" class="ajax btn btn-danger btn-sm reset-filter">{='ublaboo_datagrid.reset_filter'|translate}</a>
+										{if $control->isPaginated()}
+											{input $filter['per_page'], data-autosubmit-per-page => TRUE, class => 'form-control input-sm form-control-sm'}
+											{input $filter['per_page_submit'], class => 'datagrid-per-page-submit'}
+										{/if}
+									</div>
+								</td>
+							</tr>
+						{/if}
+					</tfoot>
+				{/block}
+			</table>
+		{/form}
+	{/snippetArea}
+	</div>
+</div>
+
+
+{define inlineAddRow}
+	{php $inlineAdd->onSetDefaults($filter['inline_add']); }
+
+	<tr class="datagrid-row-inline-add datagrid-row-inline-add-hidden">
+		<td n:if="$hasGroupActions" class="col-checkbox"></td>
+
+		{foreach $columns as $key => $column}
+			{var $col = 'col-' . $key}
+
+			{var $td = clone $column->getElementForRender('td', $key)}
+			{var $td->class[] = 'datagrid-inline-edit'}
+			{$td->startTag()|noescape}
+				{if isset($filter['inline_add'][$key])}
+					{input $filter['inline_add'][$key]}
+				{/if}
+			{$td->endTag()|noescape}
+		{/foreach}
+
+		<td class="col-action col-action-inline-edit">
+			{input $filter['inline_add']['cancel']}
+			{input $filter['inline_add']['submit']}
+		</td>
+	</tr>
+{/define}
+
+
+{define columnsSummary}
+
+	{foreach $columns as $key => $column}
+		{var $td = $column->getElementForRender('td', $key)}
+
+		{$td->startTag()|noescape}
+			{$columnsSummary->render($key)}
+		{$td->endTag()|noescape}
+	{/foreach}
+
+{/define}
+
+
+{define aggregationFunctions}
+
+	{foreach $columns as $key => $column}
+		{var $td = $column->getElementForRender('td', $key)}
+
+		{$td->startTag()|noescape}
+			{if $aggregation_functions}
+				{ifset $aggregation_functions[$key]}
+					{$aggregation_functions[$key]->renderResult()|noescape}
+				{/ifset}
+			{else}
+				{$multiple_aggregation_function->renderResult($key)|noescape}
+			{/if}
+		{$td->endTag()|noescape}
+	{/foreach}
+
+{/define}
+
+
+{define column-header}
+	{if $column->isHeaderEscaped()}
+		{if $column instanceof \Nette\Utils\Html || !$column->isTranslatableHeader()}
+			{$column->getName()|noescape}
+		{else}
+			{$column->getName()|translate|noescape}
+		{/if}
+	{else}
+		{if $column instanceof \Nette\Utils\Html || !$column->isTranslatableHeader()}
+			{$column->getName()}
+		{else}
+			{$column->getName()|translate}
+		{/if}
+	{/if}
+{/define}
+
+
+{define column-value}
+	{var $col = 'col-' . $key}
+	{var $item = $row->getItem()}
+
+	{if $column->hasTemplate()}
+		{include $column->getTemplate(), row => $row, item => $item, (expand) $column->getTemplateVariables()}
+	{else}
+		{ifset #$col}
+			{include #$col, item => $item}
+		{else}
+			{if $column->isTemplateEscaped()}
+				{$column->render($row)}
+			{else}
+				{$column->render($row)|noescape}
+			{/if}
+		{/ifset}
+	{/if}
+{/define}
+
+<link href="{$basePath}/css/datagrid.min.css" rel="stylesheet" type="text/css">
+<link href="{$basePath}/css/datagrid-spinners.min.css" rel="stylesheet" type="text/css">
+
+<script src="{$basePath}/js/datagrid.min.js"></script>
+<script src="{$basePath}/js/datagrid-instant-url-refresh.min.js"></script>
+<script src="{$basePath}/js/datagrid-spinners.min.js"></script>
\ No newline at end of file
diff --git a/app/utils/datagrid/templates/datagrid_filter_date.latte b/app/utils/datagrid/templates/datagrid_filter_date.latte
new file mode 100644
index 0000000..c18da72
--- /dev/null
+++ b/app/utils/datagrid/templates/datagrid_filter_date.latte
@@ -0,0 +1,30 @@
+{**
+ * @param Filter                         $filter
+ * @param Nette\Forms\Controls\TextInput $input
+ * @param string                         $icon_prefix       Icon prefix (fa fa-)
+ *}
+
+{if $outer}
+	<div class="row">
+		{label $input class => 'col-sm-3 control-label' /}
+		<div class="col-sm-9 form-inline">
+			<div class="input-group input-group-sm datagrid-input-group-full-width">
+				{input $input}
+				<label n:name="$input" class="input-group-addon input-group-append">
+					<i class="input-group-text {$icon_prefix}calendar"></i>
+				</label>
+			</div>
+		</div>
+	</div>
+{else}
+	<div class="form-inline">
+		<div class="input-group input-group-sm">
+			{input $input}
+			<label n:name="$input" class="input-group-addon input-group-append">
+				<i class="input-group-text {$icon_prefix}calendar"></i>
+			</label>
+		</div>
+	</div>
+{/if}
+
+
diff --git a/app/utils/datagrid/templates/datagrid_filter_daterange.latte b/app/utils/datagrid/templates/datagrid_filter_daterange.latte
new file mode 100644
index 0000000..091ac2b
--- /dev/null
+++ b/app/utils/datagrid/templates/datagrid_filter_daterange.latte
@@ -0,0 +1,46 @@
+{**
+ * @param Filter                $filter
+ * @param Nette\Forms\Container $input
+ * @param string                $icon_prefix       Icon prefix (fa fa-)
+ *}
+
+{var $container = $input}
+
+{if $outer}
+	<div class="row">
+		{label $container['from'], class => 'col-sm-3 control-label' /}
+		<div class="col-sm-4 form-inline">
+			<div class="input-group input-group-sm">
+				{input $container['from']}
+				<label n:name="$container['from']" class="input-group-addon input-group-append">
+					<i class="input-group-text {$icon_prefix}calendar"></i>
+				</label>
+			</div>
+		</div>
+		{label $container['to'], class => 'filter-range-delimiter col-sm-1 control-label' /}
+		<div class="col-sm-4 form-inline">
+			<div class="input-group input-group-sm">
+				{input $container['to']}
+				<label n:name="$container['to']" class="input-group-addon input-group-append">
+					<i class="input-group-text {$icon_prefix}calendar"></i>
+				</label>
+			</div>
+		</div>
+	</div>
+{else}
+	<div class="datagrid-col-filter-date-range form-inline">
+		<div class="input-group input-group-sm">
+			{input $container['from']}
+			<label n:name="$container['from']" class="input-group-addon input-group-append">
+				<i class="input-group-text {$icon_prefix}calendar"></i>
+			</label>
+        </div>
+
+        <div class="input-group input-group-sm">
+			{input $container['to']}
+			<label n:name="$container['to']" class="input-group-addon input-group-append">
+				<i class="input-group-text {$icon_prefix}calendar"></i>
+			</label>
+		</div>
+	</div>
+{/if}
diff --git a/app/utils/datagrid/templates/datagrid_filter_range.latte b/app/utils/datagrid/templates/datagrid_filter_range.latte
new file mode 100644
index 0000000..16bffa6
--- /dev/null
+++ b/app/utils/datagrid/templates/datagrid_filter_range.latte
@@ -0,0 +1,29 @@
+{**
+ * @param Filter                $filter
+ * @param Nette\Forms\Container $input
+ *}
+
+{var $container = $input}
+
+{if $outer}
+	<div class="row">
+		{label $container['from'], class => 'col-sm-3 control-label' /}
+		<div class="col-sm-4">
+			{input $container['from']}
+		</div>
+		{label $container['to'], class => 'filter-range-delimiter col-sm-1 control-label' /}
+		<div class="col-sm-4">
+			{input $container['to']}
+		</div>
+	</div>
+{else}
+	<div class="datagrid-col-filter-range form-inline">
+		<div class="input-group">
+			{input $container['from']}
+
+			<div class="input-group-addon datagrid-col-filter-datte-range-delimiter">-</div>
+
+			{input $container['to']}
+		</div>
+	</div>
+{/if}
diff --git a/app/utils/datagrid/templates/datagrid_filter_select.latte b/app/utils/datagrid/templates/datagrid_filter_select.latte
new file mode 100644
index 0000000..5899846
--- /dev/null
+++ b/app/utils/datagrid/templates/datagrid_filter_select.latte
@@ -0,0 +1,15 @@
+{**
+ * @param Filter                         $filter
+ * @param Nette\Forms\Controls\SelectBox $input
+ *}
+
+{if $outer}
+	<div class="row">
+		{label $input class => 'col-sm-3 control-label' /}
+		<div class="col-sm-9">
+			{input $input}
+		</div>
+	</div>
+{else}
+	{input $input}
+{/if}
diff --git a/app/utils/datagrid/templates/datagrid_filter_text.latte b/app/utils/datagrid/templates/datagrid_filter_text.latte
new file mode 100644
index 0000000..2c24d83
--- /dev/null
+++ b/app/utils/datagrid/templates/datagrid_filter_text.latte
@@ -0,0 +1,15 @@
+{**
+ * @param Filter                         $filter
+ * @param Nette\Forms\Controls\TextInput $input
+ *}
+
+{if $outer}
+	<div class="row">
+		{label $input class => 'col-sm-3 control-label' /}
+		<div class="col-sm-9">
+			{input $input}
+		</div>
+	</div>
+{else}
+	{input $input}
+{/if}
diff --git a/app/utils/datagrid/templates/datagrid_tree.latte b/app/utils/datagrid/templates/datagrid_tree.latte
new file mode 100644
index 0000000..1a53da3
--- /dev/null
+++ b/app/utils/datagrid/templates/datagrid_tree.latte
@@ -0,0 +1,138 @@
+{**
+ * @param array    $columns           Available columns
+ * @param array    $actions           Available actions
+ * @param array    $exports           Available exports
+ * @param Row[]    $rows              List of rows (each contain a item from data source)
+ * @param DataGrid $control           Parent (DataGrid)
+ * @param string   $original_template Original template file path
+ * @param string   $icon_prefix       Icon prefix (fa fa-)
+ *}
+
+{extends $original_template}
+
+<div class="datagrid-tree-item-children datagrid-tree" n:snippet="table" n:block="data" {if $control->isSortable()}data-sortable-tree data-sortable-url="{plink $control->getSortableHandler()}" data-sortable-parent-path="{$control->getSortableParentPath()}"{/if}>
+	{snippetArea items}
+		<div class="datagrid-tree-item datagrid-tree-header" n:snippet="item-header">
+			<div class="datagrid-tree-item-content" data-has-children="">
+				<div class="datagrid-tree-item-left">
+					{foreach $columns as $key => $column}
+						<strong>{$column->getName()|translate}</strong>
+						{breakIf TRUE}
+					{/foreach}
+				</div>
+
+				<div class="datagrid-tree-item-right">
+					<div class="datagrid-tree-item-right-columns">
+						{foreach $columns as $key => $column}
+							{continueIf $iterator->isFirst()}
+							<div class="datagrid-tree-item-right-columns-column col-{$column->getColumnName()} text-{$column->hasAlign() ? $column->getAlign() : 'left'}">
+								<strong>{$column->getName()|translate}</strong>
+							</div>
+						{/foreach}
+					</div>
+					<div class="datagrid-tree-item-right-actions" n:if="($actions || $control->isSortable()) && $rows">
+						<div class="datagrid-tree-item-right-actions-action">
+							{var $tmp_row = reset($rows)}
+
+							{foreach $actions as $key => $action}
+								{if $tmp_row->hasAction($key)}
+									{if $action->hasTemplate()}
+										{include $action->getTemplate(), item => $tmp_row->getItem(), (expand) $action->getTemplateVariables(), row => $tmp_row}
+									{else}
+										{$action->render($tmp_row)|noescape}
+									{/if}
+								{/if}
+							{/foreach}
+
+							<span class="handle-sort btn btn-xs btn-default btn-secondary" n:if="$control->isSortable()">
+								<i class="{$icon_prefix}arrows"></i>
+							</span>
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>
+
+		{foreach $rows as $row}
+			{var $has_children = $control->hasTreeViewChildrenCallback() ? $control->treeViewChildrenCallback($row->getItem()) : $row->getValue($tree_view_has_children_column)}
+			{var $item = $row->getItem()}
+
+			<div n:class="$has_children ? has-children, 'datagrid-tree-item'" data-id="{$row->getId()}" n:snippet="item-{$row->getId()}">
+				<div n:class="datagrid-tree-item-content, $row->getControlClass()" data-id="{$row->getId()}" data-has-children="{$has_children ? true : false}">
+					<div class="datagrid-tree-item-left">
+						<a n:href="getChildren! parent => $row->getId()" data-toggle-tree="true" n:class="!$has_children ? hidden, 'chevron ajax'">
+							<i n:block="icon-chevron" class="{$icon_prefix}chevron-right"></i>
+						</a>
+						{foreach $columns as $key => $column}
+							{var $col = 'col-' . $key}
+							{php $column = $row->applyColumnCallback($key, clone $column)}
+
+							{if $column->hasTemplate()}
+								{include $column->getTemplate(), item => $item, (expand) $column->getTemplateVariables()}
+							{else}
+								{ifset #$col}
+									{include #$col, item => $item}
+								{else}
+									{if $column->isTemplateEscaped()}
+										{$column->render($row)}
+									{else}
+										{$column->render($row)|noescape}
+									{/if}
+								{/ifset}
+							{/if}
+
+							{breakIf TRUE}
+						{/foreach}
+					</div>
+					<div class="datagrid-tree-item-right">
+						<div class="datagrid-tree-item-right-columns">
+							{foreach $columns as $key => $column}
+								{continueIf $iterator->isFirst()}
+
+								<div class="datagrid-tree-item-right-columns-column text-{$column->hasAlign() ? $column->getAlign() : 'left'}">
+									{var $col = 'col-' . $key}
+									{php $column = $row->applyColumnCallback($key, clone $column)}
+
+									{if $column->hasTemplate()}
+										{include $column->getTemplate(), row => $row, item => $item, (expand) $column->getTemplateVariables()}
+									{else}
+										{ifset #$col}
+											{include #$col, item => $item}
+										{else}
+											{if $column->isTemplateEscaped()}
+												{$column->render($row)}
+											{else}
+												{$column->render($row)|noescape}
+											{/if}
+										{/ifset}
+									{/if}
+								</div>
+							{/foreach}
+						</div>
+						<div class="datagrid-tree-item-right-actions">
+							<div class="datagrid-tree-item-right-actions-action">
+								{foreach $actions as $key => $action}
+									{if $row->hasAction($key)}
+										{if $action->hasTemplate()}
+											{include $action->getTemplate(), item => $item, (expand) $action->getTemplateVariables(), row => $row}
+										{else}
+											{$action->render($row)|noescape}
+										{/if}
+									{/if}
+								{/foreach}
+
+								<span class="handle-sort btn btn-xs btn-default btn-secondary" n:if="$control->isSortable()">
+									<i n:block = "icon-arrows" class="{$icon_prefix}arrows"></i>
+								</span>
+							</div>
+						</div>
+					</div>
+				</div>
+				<div class="datagrid-tree-item-children" {if $control->isSortable()}data-sortable-tree data-sortable-url="{plink $control->getSortableHandler()}"{/}></div>
+			</div>
+		{/foreach}
+		{if !$rows}
+			{='ublaboo_datagrid.no_item_found'|translate}
+		{/if}
+	{/snippetArea}
+</div>
diff --git a/composer.json b/composer.json
index 3a1af1e..949db80 100644
--- a/composer.json
+++ b/composer.json
@@ -19,7 +19,8 @@
 		"nette/security": "^2.4",
 		"nette/utils": "^2.4",
 		"latte/latte": "^2.4",
-		"tracy/tracy": "^2.4"
+		"tracy/tracy": "^2.4",
+		"ublaboo/datagrid": "^5.6"
 	},
 	"require-dev": {
 		"nette/tester": "^2.0"
diff --git a/composer.lock b/composer.lock
index 7df0651..f9de71c 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "29742b99e1ba0714587bf91cdbaecb4d",
+    "content-hash": "6018197517a97474da6c1a7c8e26f4fc",
     "packages": [
         {
             "name": "latte/latte",
@@ -1180,6 +1180,235 @@
             ],
             "time": "2018-09-18T10:22:16+00:00"
         },
+        {
+            "name": "paragonie/random_compat",
+            "version": "v9.99.99",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/paragonie/random_compat.git",
+                "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
+                "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "4.*|5.*",
+                "vimeo/psalm": "^1"
+            },
+            "suggest": {
+                "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
+            },
+            "type": "library",
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Paragon Initiative Enterprises",
+                    "email": "security@paragonie.com",
+                    "homepage": "https://paragonie.com"
+                }
+            ],
+            "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
+            "keywords": [
+                "csprng",
+                "polyfill",
+                "pseudorandom",
+                "random"
+            ],
+            "time": "2018-07-02T15:55:56+00:00"
+        },
+        {
+            "name": "symfony/inflector",
+            "version": "v3.3.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/inflector.git",
+                "reference": "aed5a0874a3bcfd8d0393a2d91b4cf828f29c7fb"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/inflector/zipball/aed5a0874a3bcfd8d0393a2d91b4cf828f29c7fb",
+                "reference": "aed5a0874a3bcfd8d0393a2d91b4cf828f29c7fb",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.5.9"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.3-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Inflector\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "bschussek@gmail.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony Inflector Component",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "inflection",
+                "pluralize",
+                "singularize",
+                "string",
+                "symfony",
+                "words"
+            ],
+            "time": "2017-04-12T14:14:56+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php70",
+            "version": "v1.11.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php70.git",
+                "reference": "bc4858fb611bda58719124ca079baff854149c89"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/bc4858fb611bda58719124ca079baff854149c89",
+                "reference": "bc4858fb611bda58719124ca079baff854149c89",
+                "shasum": ""
+            },
+            "require": {
+                "paragonie/random_compat": "~1.0|~2.0|~9.99",
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.11-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php70\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ],
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2019-02-06T07:57:58+00:00"
+        },
+        {
+            "name": "symfony/property-access",
+            "version": "v3.3.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/property-access.git",
+                "reference": "4cd2bc4afdfd914ad18cec97bb4159fc403384ea"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/property-access/zipball/4cd2bc4afdfd914ad18cec97bb4159fc403384ea",
+                "reference": "4cd2bc4afdfd914ad18cec97bb4159fc403384ea",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.5.9",
+                "symfony/inflector": "~3.1",
+                "symfony/polyfill-php70": "~1.0"
+            },
+            "require-dev": {
+                "symfony/cache": "~3.1"
+            },
+            "suggest": {
+                "psr/cache-implementation": "To cache access methods."
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.3-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\PropertyAccess\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony PropertyAccess Component",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "access",
+                "array",
+                "extraction",
+                "index",
+                "injection",
+                "object",
+                "property",
+                "property path",
+                "reflection"
+            ],
+            "time": "2017-07-03T08:12:02+00:00"
+        },
         {
             "name": "tracy/tracy",
             "version": "v2.5.8",
@@ -1245,6 +1474,116 @@
                 "profiler"
             ],
             "time": "2019-03-21T15:06:29+00:00"
+        },
+        {
+            "name": "ublaboo/datagrid",
+            "version": "v5.7.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/contributte/datagrid.git",
+                "reference": "837db074b494f3b6cb7a34c83ff5842662476c6d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/contributte/datagrid/zipball/837db074b494f3b6cb7a34c83ff5842662476c6d",
+                "reference": "837db074b494f3b6cb7a34c83ff5842662476c6d",
+                "shasum": ""
+            },
+            "require": {
+                "nette/application": "^2.3",
+                "nette/forms": "^2.3",
+                "nette/utils": ">=2.4.0",
+                "php": "^5.6|^7.0",
+                "symfony/property-access": "^3.0.0 || ^4.0.0",
+                "ublaboo/responses": "~1.0.0 || ~2.0.0"
+            },
+            "require-dev": {
+                "dibi/dibi": "^3.0",
+                "doctrine/orm": "~2.5",
+                "mockery/mockery": "~0.9",
+                "nette/database": "^2.3",
+                "nette/tester": "^1.6.1",
+                "nextras/orm": "~2.0",
+                "tharos/leanmapper": "~2.2.0",
+                "tracy/tracy": "^2.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-v5.x": "5.8.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Ublaboo\\DataGrid\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Pavel Janda",
+                    "homepage": "http://paveljanda.com"
+                }
+            ],
+            "description": "DataGrid for Nette Framework: filtering, sorting, pagination, tree view, table view, translator, etc",
+            "keywords": [
+                "datagrid",
+                "grid",
+                "nette"
+            ],
+            "time": "2019-02-28T13:57:58+00:00"
+        },
+        {
+            "name": "ublaboo/responses",
+            "version": "v1.0.7",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ublaboo/responses.git",
+                "reference": "b152c2ee836c315cf1878a7d092551244680ce42"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ublaboo/responses/zipball/b152c2ee836c315cf1878a7d092551244680ce42",
+                "reference": "b152c2ee836c315cf1878a7d092551244680ce42",
+                "shasum": ""
+            },
+            "require": {
+                "nette/http": "^2.3|^2.4",
+                "nette/utils": "^2.3|^2.4"
+            },
+            "suggest": {
+                "psr/http-message": "Allows usage of PSR7 stream response."
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Ublaboo\\Responses\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Pavel Janda",
+                    "homepage": "http://paveljanda.com"
+                }
+            ],
+            "description": "Additional responses for Nette Framework",
+            "homepage": "http://ublaboo.paveljanda.com/responses",
+            "keywords": [
+                "csv",
+                "image",
+                "json",
+                "nette",
+                "responses"
+            ],
+            "abandoned": "contributte/application",
+            "time": "2018-01-09T23:01:42+00:00"
         }
     ],
     "packages-dev": [
diff --git a/www/css/datagrid-spinners.min.css b/www/css/datagrid-spinners.min.css
new file mode 100644
index 0000000..23a60b1
--- /dev/null
+++ b/www/css/datagrid-spinners.min.css
@@ -0,0 +1,44 @@
+@keyframes ublaboo-spinner-icon{0%{transform:rotate(0)}
+50%{transform:rotate(180deg)}
+100%{transform:rotate(360deg)}
+}
+@-webkit-keyframes ublaboo-spinner-icon{0%{transform:rotate(0)}
+50%{transform:rotate(180deg)}
+100%{transform:rotate(360deg)}
+}
+.ublaboo-spinner-icon>span{animation-duration:2s;animation-delay:0;animation-iteration-count:infinite;animation-timing-function:ease;animation-name:ublaboo-spinner-icon}
+@keyframes ublaboo-spinner-small{0%{transform:translate(21.3px,2.2px)}
+11.1%{transform:translate(8.1px,25.2px)}
+22.2%{transform:translate(12.7px,-.7px)}
+33.3%{transform:translate(17.2px,25.2px)}
+44.4%{transform:translate(4.2px,2.2px)}
+55.5%{transform:translate(24.1px,19.5px)}
+66.6%{transform:translate(-.3px,10.3px)}
+77.7%{transform:translate(25.8px,10.3px)}
+88.8%{transform:translate(1.2px,19.3px)}
+100%{transform:translate(21.3px,2.2px)}
+}
+@-webkit-keyframes ublaboo-spinner-small{0%{transform:translate(21.3px,2.2px)}
+11.1%{transform:translate(8.1px,25.2px)}
+22.2%{transform:translate(12.7px,-.7px)}
+33.3%{transform:translate(17.2px,25.2px)}
+44.4%{transform:translate(4.2px,2.2px)}
+55.5%{transform:translate(24.1px,19.5px)}
+66.6%{transform:translate(-.3px,10.3px)}
+77.7%{transform:translate(25.8px,10.3px)}
+88.8%{transform:translate(1.2px,19.3px)}
+100%{transform:translate(21.3px,2.2px)}
+}
+@keyframes ublaboo-spinner-in{0%{opacity:0}
+100%{opacity:1}
+}
+@-webkit-keyframes ublaboo-spinner-in{0%{opacity:0}
+100%{opacity:1}
+}
+.ublaboo-spinner{line-height:0;display:inline-block;position:relative;margin:0 1em -11px;top:1px;opacity:0;animation-duration:150ms;animation-delay:0;animation-iteration-count:1;animation-timing-function:ease-in;animation-name:ublaboo-spinner-in;animation-fill-mode:forwards}
+.ublaboo-spinner>i{position:absolute;background-color:#37434f;left:0;top:0;animation-duration:6s;animation-delay:0;animation-iteration-count:infinite;animation-timing-function:ease}
+.ublaboo-spinner>i:nth-of-type(2){animation-delay:-1.5s}
+.ublaboo-spinner>i:nth-of-type(3){animation-delay:-3s}
+.ublaboo-spinner>i:nth-of-type(4){animation-delay:-4.5s}
+.ublaboo-spinner.ublaboo-spinner-small{width:28px;height:28px}
+.ublaboo-spinner.ublaboo-spinner-small>i{width:4px;height:4px;border-radius:2px;-webkit-border-radius:2px;-moz-border-radius:2px;animation-name:ublaboo-spinner-small}
\ No newline at end of file
diff --git a/www/css/datagrid.min.css b/www/css/datagrid.min.css
new file mode 100644
index 0000000..c3c044f
--- /dev/null
+++ b/www/css/datagrid.min.css
@@ -0,0 +1,116 @@
+.datagrid .datagrid-collapse-filters-button-row,.datagrid .datagrid-manual-submit,.datagrid .row-filters .datagrid-row-outer-filters-group{margin-bottom:.5em}
+.datagrid .datagrid-column-status-option-icon,.datagrid table thead tr th .datagrid-column-header-additions{float:right}
+@keyframes edited{0%{background-color:#A6E2A9}
+100%{background-color:transparent}
+}
+@keyframes edited-error{0%{background-color:#E8AAA4}
+100%{background-color:transparent}
+}
+.datagrid{background-color:#fafafa;padding:1em;box-sizing:border-box}
+.datagrid .datagrid-input-group-full-width{width:100%}
+.datagrid .hidden{display:none!important}
+.datagrid .col-action .dropdown{display:inline-block}
+.datagrid .datagrid-row-inline-add.datagrid-row-inline-add-hidden{display:none}
+.datagrid .datagrid-row-columns-summary td{border-top:2px solid #bbb;border-left:1px solid #eee;border-right:1px solid #eee;font-weight:700}
+.datagrid .datagrid-row-columns-summary td:first-child{border-left:1px solid #ddd}
+.datagrid .datagrid-row-columns-summary td:last-child{border-right:1px solid #ddd}
+.datagrid .datagrid-toolbar{margin-top:.35em;float:right;display:inline-block}
+.datagrid .datagrid-toolbar>span{margin-left:1em}
+.datagrid .datagrid-exports .btn,.datagrid .datagrid-toolbar>span>a{margin-left:.5em}
+.datagrid .datagrid-exports .btn:first-child{margin-left:0}
+.datagrid .datagrid-settings{display:inline-block}
+.datagrid .datagrid-settings .dropdown-menu--grid{font-size:12px}
+.datagrid .datagrid-settings .dropdown-menu--grid li .fa{margin-right:.5em}
+.datagrid .row-reset-filter{text-align:right;margin-bottom:.5em}
+.datagrid .filter-range-delimiter,.datagrid table th.col-action{text-align:center}
+.datagrid .bootstrap-select.input-sm>.btn{padding:5px 25px 5px 10px;font-size:12px;line-height:1.5;border-radius:3px}
+.datagrid table{margin:0}
+.datagrid table thead tr .bootstrap-select:not([class*=col-]):not(.input-group-btn),.datagrid table thead tr .bootstrap-select:not([class*=col-]):not(.input-group-btn)>.btn{width:auto}
+.datagrid table thead tr .bootstrap-select:not([class*=col-]):not(.input-group-btn) .dropdown-menu li{font-size:13px}
+.datagrid table thead tr.row-group-actions th{border-bottom-width:0!important;background-color:#f9f9f9}
+.datagrid table thead tr.row-group-actions .datagrid-selected-rows-count{margin-left:.3em}
+.datagrid table thead tr th{font-size:90%;vertical-align:top}
+.datagrid table thead tr th hr{margin:8px -8px}
+.datagrid table thead tr th .datagrid-column-header-additions a[data-datagrid-reset-filter-by-column]{margin-left:.3em;color:#858585}
+.datagrid table thead tr th .datagrid-column-header-additions .column-settings-menu{opacity:0;cursor:pointer;margin-left:.3em;display:inline-block}
+.datagrid table thead tr th .datagrid-column-header-additions .column-settings-menu .dropdown-menu{font-size:12px}
+.datagrid table thead tr th .datagrid-column-header-additions .column-settings-menu .dropdown-menu li .fa{margin-right:.5em}
+.datagrid table thead tr th .datagrid-column-header-additions .column-settings-menu .dropdown-toggle::after{display:none!important}
+.datagrid table thead tr th .datagrid-col-filter-date-range .input-group .datagrid-filter-date-range-last,.datagrid table thead tr th .datagrid-col-filter-range .input-group .datagrid-filter-date-range-last{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px}
+.datagrid table thead tr th .datagrid-col-filter-date-range .input-group .input-group-addon-first,.datagrid table thead tr th .datagrid-col-filter-range .input-group .input-group-addon-first{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;border-left:0}
+.datagrid table thead tr th .datagrid-col-filter-date-range .input-group .datagrid-col-filter-datte-range-delimiter,.datagrid table thead tr th .datagrid-col-filter-range .input-group .datagrid-col-filter-datte-range-delimiter{background-color:inherit;border:none;padding:.25rem .5rem}
+.datagrid table thead tr th .datagrid-col-filter-range .form-control{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}
+.datagrid table thead tr th:hover .column-settings-menu{opacity:1}
+.datagrid table tbody td{vertical-align:middle}
+.datagrid table tbody tr.ui-sortable-helper{display:table}
+.datagrid table tbody tr.row-item-detail{display:none}
+.datagrid table tbody tr.row-item-detail.toggled{display:table-row}
+.datagrid table tbody tr.row-item-detail .item-detail-content,.datagrid table tbody tr.row-item-detail-helper{display:none}
+.datagrid table tbody tr .datagrid-inline-edit .form-control{margin:-3px;padding-bottom:4px;padding-top:4px;height:28px}
+.datagrid table tbody tr td[data-datagrid-editable-url].editing textarea{padding:2px;margin:-3px}
+.datagrid table tbody tr td.edited{animation-name:edited;animation-duration:1.2s;animation-delay:0}
+.datagrid table tbody tr td.edited-error{animation-name:edited-error;animation-duration:1.6s;animation-delay:0}
+.datagrid table td.col-checkbox,.datagrid table th.col-checkbox{padding:0;width:2.1em;text-align:center;vertical-align:middle}
+.datagrid table td.col-checkbox .happy-checkbox,.datagrid table th.col-checkbox .happy-checkbox{margin-right:0}
+.datagrid table td.col-checkbox.col-checkbox-first,.datagrid table th.col-checkbox.col-checkbox-first{border-top-color:transparent}
+.datagrid table th.col-checkbox{background-color:#f9f9f9}
+.datagrid table td.col-action,.datagrid table th.col-action{white-space:nowrap;width:10px}
+.datagrid table td.col-action{text-align:right}
+.datagrid table td.datagrid-fit-content,.datagrid table th.datagrid-fit-content{width:1%;white-space:nowrap}
+.datagrid .datagrid-tree>.datagrid-tree-header .datagrid-tree-item-right-actions-action{opacity:0}
+.datagrid .datagrid-tree>.datagrid-tree-item{margin-left:20px}
+.datagrid .datagrid-tree .datagrid-tree-item{position:relative}
+.datagrid .datagrid-tree .datagrid-tree-item.ui-sortable-placeholder{visibility:visible!important;background-color:rgba(70,83,93,.1)}
+.datagrid .datagrid-tree .datagrid-tree-item .datagrid-tree-item-content{position:relative;display:flex;flex-direction:row;flex-wrap:nowrap;justify-content:space-between;box-sizing:border-box;height:37px;box-shadow:inset 0 -1px 1px -1px #9B9B9B}
+.datagrid .datagrid-tree .datagrid-tree-item .datagrid-tree-item-content .datagrid-tree-item-left,.datagrid .datagrid-tree .datagrid-tree-item .datagrid-tree-item-content .datagrid-tree-item-right{display:flex;flex-direction:row;flex-wrap:nowrap;justify-content:space-between;align-items:center}
+.datagrid .datagrid-tree .datagrid-tree-item .datagrid-tree-item-content .datagrid-tree-item-left{order:1}
+.datagrid .datagrid-tree .datagrid-tree-item .datagrid-tree-item-content .datagrid-tree-item-left>.chevron{-webkit-border-radius:11px;-moz-border-radius:11px;border-radius:11px;width:22px;height:22px;line-height:20px;vertical-align:middle;background-color:#fff;display:inline-block;text-align:center;position:relative;margin:0 5px 0 -27px;transition:transform .2s ease-in-out}
+.datagrid .datagrid-tree .datagrid-tree-item .datagrid-tree-item-content .datagrid-tree-item-left>.chevron:hover{-webkit-box-shadow:0 0 3px 0 #b4b4b4;-moz-box-shadow:0 0 3px 0 #b4b4b4;box-shadow:0 0 3px 0 #b4b4b4}
+.datagrid .datagrid-tree .datagrid-tree-item .datagrid-tree-item-content .datagrid-tree-item-left>.chevron.toggle-rotate{transform:rotate(90deg)}
+.datagrid .datagrid-tree .datagrid-tree-item .datagrid-tree-item-content .datagrid-tree-item-left>.chevron .fa{font-size:10px;transform:translate(1px,0)}
+.datagrid .datagrid-tree .datagrid-tree-item .datagrid-tree-item-content .datagrid-tree-item-right{position:relative;order:2;flex-basis:50%;display:flex;flex-wrap:nowrap;justify-content:flex-end;flex-direction:row}
+.datagrid .datagrid-tree .datagrid-tree-item .datagrid-tree-item-content .datagrid-tree-item-right .btn{margin-top:-3px}
+.datagrid .datagrid-tree .datagrid-tree-item .datagrid-tree-item-content .datagrid-tree-item-right .datagrid-tree-item-right-columns{white-space:nowrap;display:flex;flex-basis:70%;flex-direction:row;flex-wrap:nowrap;justify-content:flex-end}
+.datagrid .datagrid-tree .datagrid-tree-item .datagrid-tree-item-content .datagrid-tree-item-right .datagrid-tree-item-right-columns .datagrid-tree-item-right-columns-column{padding:0 7px;margin-right:4px;flex-basis:25%}
+.datagrid .datagrid-tree .datagrid-tree-item .datagrid-tree-item-content .datagrid-tree-item-right .datagrid-tree-item-right-columns .datagrid-tree-item-right-columns-column:last-child{margin-right:0}
+.datagrid .datagrid-tree .datagrid-tree-item .datagrid-tree-item-content .datagrid-tree-item-right .datagrid-tree-item-right-actions{margin-left:7px;display:flex;flex-direction:row;flex-wrap:nowrap;justify-content:space-between;align-items:center}
+.datagrid .datagrid-tree .datagrid-tree-item .datagrid-tree-item-content .datagrid-tree-item-right .datagrid-tree-item-right-actions .datagrid-tree-item-right-actions-action{margin-right:4px}
+.datagrid .datagrid-tree .datagrid-tree-item .datagrid-tree-item-content .datagrid-tree-item-right .datagrid-tree-item-right-actions .datagrid-tree-item-right-actions-action:last-child{margin-right:0}
+.datagrid .datagrid-tree .datagrid-tree-item .datagrid-tree-item-children:not(.datagrid-tree){margin-left:28px}
+.datagrid .datagrid-tree .datagrid-tree-item:not(.has-children)>.datagrid-tree-item-children{box-sizing:border-box;position:relative;width:calc(100% - 28px);min-height:9px;margin-top:-9px}
+.datagrid .datagrid-tree .datagrid-tree-item.has-children>.datagrid-tree-item-children{display:none}
+.datagrid .datagrid-tree .datagrid-tree-item.has-children>.datagrid-tree-item-children .datagrid-tree-item-right,.datagrid .datagrid-tree-item-children .datagrid-tree-item-right{flex-basis:calc(50% + 14px)}
+.datagrid .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-right{flex-basis:calc(50% + 28px)!important}
+.datagrid .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-right{flex-basis:calc(50% + 42px)!important}
+.datagrid .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-right{flex-basis:calc(50% + 56px)!important}
+.datagrid .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-right{flex-basis:calc(50% + 74px)!important}
+.datagrid .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-right{flex-basis:calc(50% + 88px)!important}
+.datagrid .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-right{flex-basis:calc(50% + 102px)!important}
+.datagrid .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-children .datagrid-tree-item-right{flex-basis:calc(50% + 116px)!important}
+.datagrid .btn{transition:all .1s ease-in-out;white-space:nowrap}
+.datagrid select{padding:0;text-transform:none}
+.datagrid .row-grid-bottom{font-size:0;padding:8px;background-color:#f9f9f9;border:1px solid #ddd;border-top:0}
+.datagrid .row-grid-bottom .col-items{font-size:14px;display:inline-block;width:25%}
+.datagrid .row-grid-bottom .col-pagination{font-size:14px;display:inline-block;width:50%}
+.datagrid .row-grid-bottom .col-per-page{font-size:14px;display:inline-block;width:25%}
+.datagrid .row-grid-bottom .col-per-page form{display:inline-block}
+.datagrid .row-grid-bottom .col-per-page .form-control{width:auto;display:inline-block}
+.datagrid .row-grid-bottom .datagrid-per-page-submit{position:absolute;visibility:hidden;width:0;top:-200px}
+.datagrid .pagination.active>span{color:#fff}
+.datagrid .pagination>a.disabled{color:#989898;cursor:not-allowed}
+.datagrid .pagination>a.active{pointer-events:none;cursor:default}
+.datagrid .row-group-actions th{font-weight:400}
+.datagrid .col-checkbox{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
+.datagrid .col-checkbox .happy-checkbox{margin-top:2px}
+@media (min-width:768px){.datagrid .ublaboo-datagrid-th-form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}
+.datagrid .ublaboo-datagrid-th-form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}
+.datagrid .ublaboo-datagrid-th-form-inline .input-group{display:inline-table;vertical-align:middle}
+.datagrid .ublaboo-datagrid-th-form-inline .input-group .form-control{width:auto}
+.datagrid .ublaboo-datagrid-th-form-inline .input-group>.form-control{width:100%}
+.datagrid .ublaboo-datagrid-th-form-inline .control-label{margin-bottom:0;vertical-align:middle}
+.datagrid .ublaboo-datagrid-th-form-inline .checkbox,.datagrid .ublaboo-datagrid-th-form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}
+.datagrid .ublaboo-datagrid-th-form-inline .checkbox label,.datagrid .ublaboo-datagrid-th-form-inline .radio label{padding-left:0}
+.datagrid .ublaboo-datagrid-th-form-inline .checkbox input[type=checkbox],.datagrid .ublaboo-datagrid-th-form-inline .radio input[type=radio]{position:relative;margin-left:0}
+}
+.datagrid .btn-group-xs>.btn,.datagrid .btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}
+.datagrid .dropdown-item{padding:3px 20px;line-height:1.42857143;font-size:14px}
\ No newline at end of file
diff --git a/www/js/datagrid-instant-url-refresh.min.js b/www/js/datagrid-instant-url-refresh.min.js
new file mode 100644
index 0000000..1e4da0d
--- /dev/null
+++ b/www/js/datagrid-instant-url-refresh.min.js
@@ -0,0 +1 @@
+$(function(){if($(".datagrid").length)return $.nette.ajax({type:"GET",url:$(".datagrid").first().data("refresh-state")})});
\ No newline at end of file
diff --git a/www/js/datagrid-spinners.min.js b/www/js/datagrid-spinners.min.js
new file mode 100644
index 0000000..85d932b
--- /dev/null
+++ b/www/js/datagrid-spinners.min.js
@@ -0,0 +1 @@
+$.nette.ext("ublaboo-spinners",{before:function(e,i){var t,a,r;if(i.nette){if(t=i.nette.el,r=$('<div class="ublaboo-spinner ublaboo-spinner-small"><i></i><i></i><i></i><i></i></div>'),t.is('.datagrid [name="group_action[submit]"]'))return t.after(r);if(t.is(".datagrid a")&&t.data("toggle-detail")){if(a=i.nette.el.attr("data-toggle-detail"),!$(".item-detail-"+a).hasClass("loaded"))return t.addClass("ublaboo-spinner-icon")}else{if(t.is(".datagrid .col-pagination a"))return t.closest(".row-grid-bottom").find(".col-per-page").prepend(r);if(t.is(".datagrid .datagrid-per-page-submit"))return t.closest(".row-grid-bottom").find(".col-per-page").prepend(r);if(t.is(".datagrid .reset-filter"))return t.closest(".row-grid-bottom").find(".col-per-page").prepend(r)}}},complete:function(){return $(".ublaboo-spinner").remove(),$(".ublaboo-spinner-icon").removeClass("ublaboo-spinner-icon")}});
\ No newline at end of file
diff --git a/www/js/datagrid.min.js b/www/js/datagrid.min.js
new file mode 100644
index 0000000..190b1cf
--- /dev/null
+++ b/www/js/datagrid.min.js
@@ -0,0 +1 @@
+var datagridFitlerMultiSelect,datagridGroupActionMultiSelect,datagridShiftGroupSelection,datagridSortable,datagridSortableTree,getEventDomPath,indexOf=[].indexOf||function(t){for(var e=0,a=this.length;e<a;e++)if(e in this&&this[e]===t)return e;return-1};$(document).on("click","[data-datagrid-confirm]:not(.ajax)",function(t){if(!confirm($(t.target).closest("a").attr("data-datagrid-confirm")))return t.stopPropagation(),t.preventDefault()}),$.nette.ext("datagrid.confirm",{before:function(t,e){var a;if(e.nette&&(a=e.nette.el.data("datagrid-confirm")))return confirm(a)}}),$(document).on("change","select[data-autosubmit-per-page]",function(){var t;return 0===(t=$(this).parent().find("input[type=submit]")).length&&(t=$(this).parent().find("button[type=submit]")),t.click()}).on("change","select[data-autosubmit]",function(){return $(this).closest("form").first().submit()}).on("change","input[data-autosubmit][data-autosubmit-change]",function(t){var e;return t.which||t.keyCode||0,clearTimeout(window.datagrid_autosubmit_timer),e=$(this),window.datagrid_autosubmit_timer=setTimeout(function(){return e.closest("form").first().submit()},200)}).on("keyup","input[data-autosubmit]",function(t){var e,a;if(13===(a=t.which||t.keyCode||0)||!(a>=9&&a<=40||a>=112&&a<=123))return clearTimeout(window.datagrid_autosubmit_timer),e=$(this),window.datagrid_autosubmit_timer=setTimeout(function(){return e.closest("form").first().submit()},200)}).on("keydown",".datagrid-inline-edit input",function(t){if(13===(t.which||t.keyCode||0))return t.stopPropagation(),t.preventDefault(),$(this).closest("tr").find('.col-action-inline-edit [name="inline_edit[submit]"]').click()}),$(document).on("keydown","input[data-datagrid-manualsubmit]",function(t){if(13===(t.which||t.keyCode||0))return t.stopPropagation(),t.preventDefault(),$(this).closest("form").first().submit()}),getEventDomPath=function(t){var e,a;if(indexOf.call(t,a)>=0)return t.path;for(a=[],e=t.target;e!==document.body&&null!==e;)a.push(e),e=e.parentNode;return a},(datagridShiftGroupSelection=function(){var t;return t=null,document.addEventListener("click",function(e){var a,i,d,n,r,o,l,s,c,u,g,f,h,p,m,v,b,_;for(r=0,g=(p=getEventDomPath(e)).length;r<g;r++)if(d=p[r],$(d).is(".col-checkbox")&&t&&e.shiftKey){if(i=$(d).closest("tr"),c=t.closest("tr"),u=c.closest("tbody"),a=u.find("tr").toArray(),i.index()>c.index()?_=a.slice(c.index(),i.index()):i.index()<c.index()&&(_=a.slice(i.index()+1,c.index())),!_)return;for(l=0,f=_.length;l<f;l++)b=_[l],(o=$(b).find(".col-checkbox input[type=checkbox]")[0])&&(o.checked=!0,window.navigator.userAgent.indexOf("MSIE ")?(n=document.createEvent("Event")).initEvent("change",!0,!0):n=new Event("change",{bubbles:!0}),o.dispatchEvent(n))}for(v=[],s=0,h=(m=getEventDomPath(e)).length;s<h;s++)d=m[s],$(d).is(".col-checkbox")?v.push(t=$(d)):v.push(void 0);return v})})(),document.addEventListener("change",function(t){var e,a,i,d,n,r,o,l,s,c,u;if((d=t.target.getAttribute("data-check"))&&(e=document.querySelectorAll("input[data-check-all-"+d+"]:checked"),(c=document.querySelector(".datagrid-"+d+' select[name="group_action[group_action]"]'))&&(a=document.querySelector(".datagrid-"+d+" .datagrid-selected-rows-count"),e.length?(c.disabled=!1,u=document.querySelectorAll("input[data-check-all-"+d+"]").length,a&&(a.innerHTML=e.length+"/"+u)):(c.disabled=!0,c.value="",a&&(a.innerHTML=""))),window.navigator.userAgent.indexOf("MSIE ")?(i=document.createEvent("Event")).initEvent("change",!0,!0):i=new Event("change",{bubbles:!0}),c&&c.dispatchEvent(i)),d=t.target.getAttribute("data-check-all")){for(s=[],n=0,l=(o=document.querySelectorAll("input[type=checkbox][data-check-all-"+d+"]")).length;n<l;n++)(r=o[n]).checked=t.target.checked,window.navigator.userAgent.indexOf("MSIE ")?(i=document.createEvent("Event")).initEvent("change",!0,!0):i=new Event("change",{bubbles:!0}),s.push(r.dispatchEvent(i));return s}}),window.datagridSerializeUrl=function(t,e){var a=[];for(var i in t)if(t.hasOwnProperty(i)){var d=e?e+"["+i+"]":i,n=t[i];if(null!==n&&""!==n)if("object"==typeof n){var r=window.datagridSerializeUrl(n,d);r&&a.push(r)}else a.push(encodeURIComponent(d)+"="+encodeURIComponent(n))}return a.join("&")},datagridSortable=function(){if(void 0!==$.fn.sortable)return $(".datagrid [data-sortable]").sortable({handle:".handle-sort",items:"tr",axis:"y",update:function(t,e){var a,i,d,n,r,o,l;return o=e.item.closest("tr[data-id]"),d=o.data("id"),r=null,n=null,o.prev().length&&(r=o.prev().data("id")),o.next().length&&(n=o.next().data("id")),l=$(this).data("sortable-url"),i={},a=o.closest(".datagrid").find("tbody").attr("data-sortable-parent-path"),i[(a+"-item_id").replace(/^-/,"")]=d,i[(a+"-prev_id").replace(/^-/,"")]=r,i[(a+"-next_id").replace(/^-/,"")]=n,$.nette.ajax({type:"GET",url:l,data:i,error:function(t,e,a){return alert(t.statusText)}})},helper:function(t,e){return e.children().each(function(){return $(this).width($(this).width())}),e}})},$(function(){return datagridSortable()}),void 0===datagridSortableTree&&(datagridSortableTree=function(){if(void 0!==$(".datagrid-tree-item-children").sortable)return $(".datagrid-tree-item-children").sortable({handle:".handle-sort",items:".datagrid-tree-item:not(.datagrid-tree-header)",toleranceElement:"> .datagrid-tree-item-content",connectWith:".datagrid-tree-item-children",update:function(t,e){var a,i,d,n,r,o,l,s,c;if($(".toggle-tree-to-delete").remove(),s=e.item.closest(".datagrid-tree-item[data-id]"),d=s.data("id"),l=null,n=null,o=null,s.prev().length&&(l=s.prev().data("id")),s.next().length&&(n=s.next().data("id")),(r=s.parent().closest(".datagrid-tree-item")).length&&(r.find(".datagrid-tree-item-children").first().css({display:"block"}),r.addClass("has-children"),o=r.data("id")),c=$(this).data("sortable-url"))return r.find("[data-toggle-tree]").first().removeClass("hidden"),a=s.closest(".datagrid-tree").attr("data-sortable-parent-path"),i={},i[(a+"-item_id").replace(/^-/,"")]=d,i[(a+"-prev_id").replace(/^-/,"")]=l,i[(a+"-next_id").replace(/^-/,"")]=n,i[(a+"-parent_id").replace(/^-/,"")]=o,$.nette.ajax({type:"GET",url:c,data:i,error:function(t,e,a){if("abort"!==a)return alert(t.statusText)}})},stop:function(t,e){return $(".toggle-tree-to-delete").removeClass("toggle-tree-to-delete")},start:function(t,e){var a;if((a=e.item.parent().closest(".datagrid-tree-item")).length&&2===a.find(".datagrid-tree-item").length)return a.find("[data-toggle-tree]").addClass("toggle-tree-to-delete")}})}),$(function(){return datagridSortableTree()}),$.nette.ext("datagrid.happy",{success:function(){var t,e,a,i,d,n,r,o,l,s,c;for(window.happy&&window.happy.reset(),c=[],n=0,l=(d=$(".datagrid")).length;n<l;n++){for(e="",o=0,s=(a=d[n].classList).length;o<s;o++)e=e+"."+a[o];1===(t=document.querySelectorAll(e+" input[data-check]:checked")).length&&"toggle-all"===t[0].getAttribute("name")&&(r=document.querySelector(e+" input[name=toggle-all]"))?(r.checked=!1,window.navigator.userAgent.indexOf("MSIE ")?(i=document.createEvent("Event")).initEvent("change",!0,!0):i=new Event("change",{bubbles:!0}),c.push(r.dispatchEvent(i))):c.push(void 0)}return c}}),$.nette.ext("datagrid.sortable",{success:function(){return datagridSortable()}}),$.nette.ext("datagrid.forms",{success:function(){return $(".datagrid").find("form").each(function(){return window.Nette.initForm(this)})}}),$.nette.ext("datagrid.url",{success:function(t){var e,a,i,d;if(t._datagrid_url&&window.history.pushState&&(e=window.location.protocol+"//"+window.location.host,a=window.location.pathname,i=window.datagridSerializeUrl(t.state).replace(/&+$/gm,""),d=i?e+a+"?"+i.replace(/\&*$/,""):e+a,d+=window.location.hash,window.location.href!==d))return window.history.pushState({path:d},"",d)}}),$.nette.ext("datagrid.sort",{success:function(t){var e,a,i,d;if(t._datagrid_sort){i=t._datagrid_sort,d=[];for(a in i)e=i[a],d.push($("#datagrid-sort-"+a).attr("href",e));return d}}}),$.nette.ext("datargid.item_detail",{before:function(t,e){var a,i;if(e.nette&&e.nette.el.attr("data-toggle-detail"))return a=e.nette.el.attr("data-toggle-detail"),(i=$(".item-detail-"+a)).hasClass("loaded")?i.find(".item-detail-content").length?(i.hasClass("toggled")?i.find(".item-detail-content").slideToggle("fast",function(){return i.toggleClass("toggled")}):(i.toggleClass("toggled"),i.find(".item-detail-content").slideToggle("fast")),!1):(i.removeClass("toggled"),!0):i.addClass("loaded")},success:function(t){var e,a;if(t._datagrid_toggle_detail)return e=t._datagrid_toggle_detail,(a=$(".item-detail-"+e)).toggleClass("toggled"),a.find(".item-detail-content").slideToggle("fast")}}),$.nette.ext("datagrid.tree",{before:function(t,e){var a;return!(e.nette&&e.nette.el.attr("data-toggle-tree")&&(e.nette.el.toggleClass("toggle-rotate"),(a=e.nette.el.closest(".datagrid-tree-item").find(".datagrid-tree-item-children").first()).hasClass("loaded")))||(a.slideToggle("fast"),!1)},success:function(t){var e,a,i,d,n,r,o;if(t._datagrid_tree){i=t._datagrid_tree,(e=$('.datagrid-tree-item[data-id="'+i+'"]').find(".datagrid-tree-item-children").first()).addClass("loaded"),n=t.snippets;for(d in n)r=n[d],a=$(r),(o=$('<div class="datagrid-tree-item" id="'+d+'">')).attr("data-id",a.attr("data-id")),o.append(a),a.data("has-children")&&o.addClass("has-children"),e.append(o);e.addClass("loaded"),e.slideToggle("fast"),$.nette.load()}return datagridSortableTree()}}),$(document).on("click","[data-datagrid-editable-url]",function(t){var e,a,i,d,n,r,o,l,s,c;if(d=$(this),"a"!==t.target.tagName.toLowerCase()&&!d.hasClass("datagrid-inline-edit")&&!d.hasClass("editing")){d.addClass("editing"),n=d.html().trim().replace("<br>","\n"),c=d.data("datagrid-editable-value")?d.data("datagrid-editable-value"):n,d.data("originalValue",n),d.data("valueToEdit",c),"textarea"===d.data("datagrid-editable-type")?(l=$("<textarea>"+c+"</textarea>"),o=parseInt(d.css("padding").replace(/[^-\d\.]/g,""),10),r=(d.outerHeight()-2*o)/Math.round(parseFloat(d.css("line-height"))),l.attr("rows",Math.round(r))):"select"===d.data("datagrid-editable-type")?(l=$(d.data("datagrid-editable-element"))).find("option").each(function(){if($(this).text()===c)return l.find("option[value='"+c+"']").prop("selected",!0)}):(l=$('<input type="'+d.data("datagrid-editable-type")+'">')).val(c),i=d.data("datagrid-editable-attrs");for(e in i)a=i[e],l.attr(e,a);return d.removeClass("edited"),d.html(l),s=function(t,e){var a;return(a=e.val())!==t.data("valueToEdit")?$.nette.ajax({url:t.data("datagrid-editable-url"),data:{value:a},method:"POST",success:function(e){return"select"===t.data("datagrid-editable-type")?t.html(l.find("option[value='"+a+"']").html()):(e._datagrid_editable_new_value&&(a=e._datagrid_editable_new_value),t.html(a)),t.addClass("edited")},error:function(){return t.html(t.data("originalValue")),t.addClass("edited-error")}}):t.html(t.data("originalValue")),setTimeout(function(){return t.removeClass("editing")},1200)},d.find("input,textarea,select").focus().on("blur",function(){return s(d,$(this))}).on("keydown",function(t){return"textarea"!==d.data("datagrid-editable-type")&&13===t.which?(t.stopPropagation(),t.preventDefault(),s(d,$(this))):27===t.which?(t.stopPropagation(),t.preventDefault(),d.removeClass("editing"),d.html(d.data("originalValue"))):void 0}),d.find("select").on("change",function(){return s(d,$(this))})}}),$.nette.ext("datagrid.after_inline_edit",{success:function(t){var e;return e=$(".datagrid-"+t._datagrid_name),t._datagrid_inline_edited?(e.find("tr[data-id="+t._datagrid_inline_edited+"] > td").addClass("edited"),e.find(".datagrid-inline-edit-trigger").removeClass("hidden")):t._datagrid_inline_edit_cancel?e.find(".datagrid-inline-edit-trigger").removeClass("hidden"):void 0}}),$(document).on("click","[data-datagrid-toggle-inline-add]",function(t){var e;return t.stopPropagation(),t.preventDefault(),(e=$(this).closest(".datagrid").find(".datagrid-row-inline-add")).hasClass("datagrid-row-inline-add-hidden")&&e.removeClass("datagrid-row-inline-add-hidden"),e.find("input:not([readonly]),textarea:not([readonly])").first().focus()}),$(document).on("mouseup","[data-datagrid-cancel-inline-add]",function(t){if(1===(t.which||t.keyCode||0))return t.stopPropagation(),t.preventDefault(),$(".datagrid-row-inline-add").addClass("datagrid-row-inline-add-hidden")}),$.nette.ext("datagrid-toggle-inline-add",{success:function(t){if(t._datagrid_inline_added)return $(".datagrid-row-inline-add").find("textarea").html(""),$(".datagrid-row-inline-add").find("input[type!=submit]").val(""),$(".datagrid-row-inline-add").addClass("datagrid-row-inline-add-hidden")}}),datagridFitlerMultiSelect=function(){var t;if(t=$(".selectpicker").first(),$.fn.selectpicker)return $.fn.selectpicker.defaults={countSelectedText:t.data("i18n-selected"),iconBase:"",tickIcon:t.data("selected-icon-check")}},$(function(){return datagridFitlerMultiSelect()}),datagridGroupActionMultiSelect=function(){if($.fn.selectpicker)return $("[data-datagrid-multiselect-id]").each(function(){var t;if($(this).hasClass("selectpicker"))return $(this).removeAttr("id"),t=$(this).data("datagrid-multiselect-id"),$(this).on("loaded.bs.select",function(t){return $(this).parent().attr("style","display:none;"),$(this).parent().find(".hidden").removeClass("hidden").addClass("btn-default btn-secondary")}),$(this).on("rendered.bs.select",function(e){return $(this).parent().attr("id",t)})})},$(function(){return datagridGroupActionMultiSelect()}),$.nette.ext("datagrid.fitlerMultiSelect",{success:function(){if(datagridFitlerMultiSelect(),$.fn.selectpicker)return $(".selectpicker").selectpicker({iconBase:"fa"})}}),$.nette.ext("datagrid.groupActionMultiSelect",{success:function(){return datagridGroupActionMultiSelect()}}),$.nette.ext("datagrid.inline-editing",{success:function(t){if(t._datagrid_inline_editing)return $(".datagrid-"+t._datagrid_name).find(".datagrid-inline-edit-trigger").addClass("hidden")}}),$.nette.ext("datagrid.redraw-item",{success:function(t){if(t._datagrid_redraw_item_class)return $("tr[data-id="+t._datagrid_redraw_item_id+"]").attr("class",t._datagrid_redraw_item_class)}}),$.nette.ext("datagrid.reset-filter-by-column",{success:function(t){var e,a,i,d,n,r;if(t._datagrid_name&&((e=$(".datagrid-"+t._datagrid_name)).find("[data-datagrid-reset-filter-by-column]").addClass("hidden"),t.non_empty_filters&&t.non_empty_filters.length)){for(i=0,n=(r=t.non_empty_filters).length;i<n;i++)d=r[i],e.find("[data-datagrid-reset-filter-by-column="+d+"]").removeClass("hidden");return a=e.find(".reset-filter").attr("href"),e.find("[data-datagrid-reset-filter-by-column]").each(function(){var e;return d=$(this).attr("data-datagrid-reset-filter-by-column"),e=a.replace("do="+t._datagrid_name+"-resetFilter","do="+t._datagrid_name+"-resetColumnFilter"),e+="&"+t._datagrid_name+"-key="+d,$(this).attr("href",e)})}}});
\ No newline at end of file
diff --git a/www/js/nette.ajax.js b/www/js/nette.ajax.js
new file mode 100644
index 0000000..cf67cc7
--- /dev/null
+++ b/www/js/nette.ajax.js
@@ -0,0 +1,566 @@
+/**
+ * AJAX Nette Framework plugin for jQuery
+ *
+ * @copyright Copyright (c) 2009, 2010 Jan Marek
+ * @copyright Copyright (c) 2009, 2010 David Grudl
+ * @copyright Copyright (c) 2012-2014 Vojtěch Dobeš
+ * @license MIT
+ *
+ * @version 2.3.0
+ */
+
+(function(window, $, undefined) {
+
+if (typeof $ !== 'function') {
+	return console.error('nette.ajax.js: jQuery is missing, load it please');
+}
+
+var nette = function () {
+	var inner = {
+		self: this,
+		initialized: false,
+		contexts: {},
+		on: {
+			init: {},
+			load: {},
+			prepare: {},
+			before: {},
+			start: {},
+			success: {},
+			complete: {},
+			error: {}
+		},
+		fire: function () {
+			var result = true;
+			var args = Array.prototype.slice.call(arguments);
+			var props = args.shift();
+			var name = (typeof props === 'string') ? props : props.name;
+			var off = (typeof props === 'object') ? props.off || {} : {};
+			args.push(inner.self);
+			$.each(inner.on[name], function (index, reaction) {
+				if (reaction === undefined || $.inArray(index, off) !== -1) return true;
+				var temp = reaction.apply(inner.contexts[index], args);
+				return result = (temp === undefined || temp);
+			});
+			return result;
+		},
+		requestHandler: function (e) {
+			var xhr = inner.self.ajax({}, this, e);
+			if (xhr && xhr._returnFalse) { // for IE 8
+				return false;
+			}
+		},
+		ext: function (callbacks, context, name) {
+			while (!name) {
+				name = 'ext_' + Math.random();
+				if (inner.contexts[name]) {
+					name = undefined;
+				}
+			}
+
+			$.each(callbacks, function (event, callback) {
+				inner.on[event][name] = callback;
+			});
+			inner.contexts[name] = $.extend(context ? context : {}, {
+				name: function () {
+					return name;
+				},
+				ext: function (name, force) {
+					var ext = inner.contexts[name];
+					if (!ext && force) throw "Extension '" + this.name() + "' depends on disabled extension '" + name + "'.";
+					return ext;
+				}
+			});
+		}
+	};
+
+	/**
+	 * Allows manipulation with extensions.
+	 * When called with 1. argument only, it returns extension with given name.
+	 * When called with 2. argument equal to false, it removes extension entirely.
+	 * When called with 2. argument equal to hash of event callbacks, it adds new extension.
+	 *
+	 * @param  {string} Name of extension
+	 * @param  {bool|object|null} Set of callbacks for any events OR false for removing extension.
+	 * @param  {object|null} Context for added extension
+	 * @return {$.nette|object} Provides a fluent interface OR returns extensions with given name
+	 */
+	this.ext = function (name, callbacks, context) {
+		if (typeof name === 'object') {
+			inner.ext(name, callbacks);
+		} else if (callbacks === undefined) {
+			return inner.contexts[name];
+		} else if (!callbacks) {
+			$.each(['init', 'load', 'prepare', 'before', 'start', 'success', 'complete', 'error'], function (index, event) {
+				inner.on[event][name] = undefined;
+			});
+			inner.contexts[name] = undefined;
+		} else if (typeof name === 'string' && inner.contexts[name] !== undefined) {
+			throw "Cannot override already registered nette-ajax extension '" + name + "'.";
+		} else {
+			inner.ext(callbacks, context, name);
+		}
+		return this;
+	};
+
+	/**
+	 * Initializes the plugin:
+	 * - fires 'init' event, then 'load' event
+	 * - when called with any arguments, it will override default 'init' extension
+	 *   with provided callbacks
+	 *
+	 * @param  {function|object|null} Callback for 'load' event or entire set of callbacks for any events
+	 * @param  {object|null} Context provided for callbacks in first argument
+	 * @return {$.nette} Provides a fluent interface
+	 */
+	this.init = function (load, loadContext) {
+		if (inner.initialized) throw 'Cannot initialize nette-ajax twice.';
+
+		if (typeof load === 'function') {
+			this.ext('init', null);
+			this.ext('init', {
+				load: load
+			}, loadContext);
+		} else if (typeof load === 'object') {
+			this.ext('init', null);
+			this.ext('init', load, loadContext);
+		} else if (load !== undefined) {
+			throw 'Argument of init() can be function or function-hash only.';
+		}
+
+		inner.initialized = true;
+
+		inner.fire('init');
+		this.load();
+		return this;
+	};
+
+	/**
+	 * Fires 'load' event
+	 *
+	 * @return {$.nette} Provides a fluent interface
+	 */
+	this.load = function () {
+		inner.fire('load', inner.requestHandler);
+		return this;
+	};
+
+	/**
+	 * Executes AJAX request. Attaches listeners and events.
+	 *
+	 * @param  {object|string} settings or URL
+	 * @param  {Element|null} ussually Anchor or Form
+	 * @param  {event|null} event causing the request
+	 * @return {jqXHR|null}
+	 */
+	this.ajax = function (settings, ui, e) {
+		if ($.type(settings) === 'string') {
+			settings = {url: settings};
+		}
+		if (!settings.nette && ui && e) {
+			var $el = $(ui), xhr, originalBeforeSend;
+			var analyze = settings.nette = {
+				e: e,
+				ui: ui,
+				el: $el,
+				isForm: $el.is('form'),
+				isSubmit: $el.is('input[type=submit]') || $el.is('button[type=submit]'),
+				isImage: $el.is('input[type=image]'),
+				form: null
+			};
+
+			if (analyze.isSubmit || analyze.isImage) {
+				analyze.form = analyze.el.closest('form');
+			} else if (analyze.isForm) {
+				analyze.form = analyze.el;
+			}
+
+			if (!settings.url) {
+				settings.url = analyze.form ? analyze.form.attr('action') || window.location.pathname + window.location.search : ui.href;
+			}
+			if (!settings.type) {
+				settings.type = analyze.form ? analyze.form.attr('method') : 'get';
+			}
+
+			if ($el.is('[data-ajax-off]')) {
+				var rawOff = $el.attr('data-ajax-off');
+				if (rawOff.indexOf('[') === 0) {
+					settings.off = $el.data('ajaxOff');
+				} else if (rawOff.indexOf(',') !== -1) {
+					settings.off = rawOff.split(',');
+				} else if (rawOff.indexOf(' ') !== -1) {
+					settings.off = rawOff.split(' ');
+				} else {
+					settings.off = rawOff;
+				}
+				if (typeof settings.off === 'string') settings.off = [settings.off];
+				settings.off = $.grep($.each(settings.off, function (off) {
+					return $.trim(off);
+				}), function (off) {
+					return off.length;
+				});
+			}
+		}
+
+		inner.fire({
+			name: 'prepare',
+			off: settings.off || {}
+		}, settings);
+		if (settings.prepare) {
+			settings.prepare(settings);
+		}
+
+		originalBeforeSend = settings.beforeSend;
+		settings.beforeSend = function (xhr, settings) {
+			var result = inner.fire({
+				name: 'before',
+				off: settings.off || {}
+			}, xhr, settings);
+			if ((result || result === undefined) && originalBeforeSend) {
+				result = originalBeforeSend(xhr, settings);
+			}
+			return result;
+		};
+
+		return this.handleXHR($.ajax(settings), settings);
+	};
+
+	/**
+	 * Binds extension callbacks to existing XHR object
+	 *
+	 * @param  {jqXHR|null}
+	 * @param  {object} settings
+	 * @return {jqXHR|null}
+	 */
+	this.handleXHR = function (xhr, settings) {
+		settings = settings || {};
+
+		if (xhr && (typeof xhr.statusText === 'undefined' || xhr.statusText !== 'canceled')) {
+			xhr.done(function (payload, status, xhr) {
+				inner.fire({
+					name: 'success',
+					off: settings.off || {}
+				}, payload, status, xhr, settings);
+			}).fail(function (xhr, status, error) {
+				inner.fire({
+					name: 'error',
+					off: settings.off || {}
+				}, xhr, status, error, settings);
+			}).always(function (xhr, status) {
+				inner.fire({
+					name: 'complete',
+					off: settings.off || {}
+				}, xhr, status, settings);
+			});
+			inner.fire({
+				name: 'start',
+				off: settings.off || {}
+			}, xhr, settings);
+			if (settings.start) {
+				settings.start(xhr, settings);
+			}
+		}
+		return xhr;
+	};
+};
+
+$.nette = new ($.extend(nette, $.nette ? $.nette : {}));
+
+$.fn.netteAjax = function (e, options) {
+	return $.nette.ajax(options || {}, this[0], e);
+};
+
+$.fn.netteAjaxOff = function () {
+	return this.off('.nette');
+};
+
+$.nette.ext('validation', {
+	before: function (xhr, settings) {
+		if (!settings.nette) return true;
+		else var analyze = settings.nette;
+		var e = analyze.e;
+
+		var validate = $.extend(this.defaults, settings.validate || (function () {
+			if (!analyze.el.is('[data-ajax-validate]')) return;
+			var attr = analyze.el.data('ajaxValidate');
+			if (attr === false) return {
+				keys: false,
+				url: false,
+				form: false
+			}; else if (typeof attr === 'object') return attr;
+ 		})() || {});
+
+		var passEvent = false;
+		if (analyze.el.attr('data-ajax-pass') !== undefined) {
+			passEvent = analyze.el.data('ajaxPass');
+			passEvent = typeof passEvent === 'bool' ? passEvent : true;
+		}
+
+		if (validate.keys) {
+			// thx to @vrana
+			var explicitNoAjax = e.button || e.ctrlKey || e.shiftKey || e.altKey || e.metaKey;
+
+			if (analyze.form) {
+				if (explicitNoAjax && analyze.isSubmit) {
+					this.explicitNoAjax = true;
+					return false;
+				} else if (analyze.isForm && this.explicitNoAjax) {
+					this.explicitNoAjax = false;
+					return false;
+				}
+			} else if (explicitNoAjax) return false;
+		}
+
+		if (validate.form && analyze.form) {
+			if (analyze.isSubmit || analyze.isImage) {
+				analyze.form.get(0)["nette-submittedBy"] = analyze.el.get(0);
+			}
+			var notValid;
+			if ((typeof Nette.version === 'undefined' || Nette.version == '2.3')) { // Nette 2.3 and older
+				var ie = this.ie();
+				notValid = (analyze.form.get(0).onsubmit && analyze.form.get(0).onsubmit((typeof ie !== 'undefined' && ie < 9) ? undefined : e) === false);
+			} else { // Nette 2.4 and up
+				notValid = ((analyze.form.get(0).onsubmit ? analyze.form.triggerHandler('submit') : Nette.validateForm(analyze.form.get(0))) === false)
+			}
+			if (notValid) {
+				e.stopImmediatePropagation();
+				e.preventDefault();
+				return false;
+			}
+		}
+
+		if (validate.url) {
+			// thx to @vrana
+			var urlToValidate = analyze.form ? settings.url : analyze.el.attr('href');
+			// Check if URL is absolute
+			if (/(?:^[a-z][a-z0-9+.-]*:|\/\/)/.test(urlToValidate)) {
+				// Parse absolute URL
+				var parsedUrl = new URL(urlToValidate);
+				if (/:|^#/.test(parsedUrl['pathname'] + parsedUrl['search'] + parsedUrl['hash'])) return false;
+			} else {
+				if (/:|^#/.test(urlToValidate)) return false;
+			}
+		}
+
+		if (!passEvent) {
+			e.stopPropagation();
+			e.preventDefault();
+			xhr._returnFalse = true; // for IE 8
+		}
+		return true;
+	}
+}, {
+	defaults: {
+		keys: true,
+		url: true,
+		form: true
+	},
+	explicitNoAjax: false,
+	ie: function (undefined) { // http://james.padolsey.com/javascript/detect-ie-in-js-using-conditional-comments/
+		var v = 3;
+		var div = document.createElement('div');
+		var all = div.getElementsByTagName('i');
+		while (
+        		div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->',
+			all[0]
+		);
+		return v > 4 ? v : undefined;
+	}
+});
+
+$.nette.ext('forms', {
+	init: function () {
+		var snippets;
+		if (!window.Nette || !(snippets = this.ext('snippets'))) return;
+
+		snippets.after(function ($el) {
+			$el.find('form').each(function() {
+				window.Nette.initForm(this);
+			});
+		});
+	},
+	prepare: function (settings) {
+		var analyze = settings.nette;
+		if (!analyze || !analyze.form) return;
+		var e = analyze.e;
+		var originalData = settings.data || {};
+		var data = {};
+
+		if (analyze.isSubmit) {
+			data[analyze.el.attr('name')] = analyze.el.val() || '';
+		} else if (analyze.isImage) {
+			var offset = analyze.el.offset();
+			var name = analyze.el.attr('name');
+			var dataOffset = [ Math.max(0, e.pageX - offset.left), Math.max(0, e.pageY - offset.top) ];
+
+			if (name.indexOf('[', 0) !== -1) { // inside a container
+				data[name] = dataOffset;
+			} else {
+				data[name + '.x'] = dataOffset[0];
+				data[name + '.y'] = dataOffset[1];
+			}
+		}
+		
+		// https://developer.mozilla.org/en-US/docs/Web/Guide/Using_FormData_Objects#Sending_files_using_a_FormData_object
+		var formMethod = analyze.form.attr('method');
+		if (formMethod && formMethod.toLowerCase() === 'post' && 'FormData' in window) {
+			var formData = new FormData(analyze.form[0]);
+			for (var i in data) {
+				formData.append(i, data[i]);
+			}
+
+			if (typeof originalData !== 'string') {
+				for (var i in originalData) {
+					formData.append(i, originalData[i]);
+				}
+			}
+
+			settings.data = formData;
+			settings.processData = false;
+			settings.contentType = false;
+		} else {
+			if (typeof originalData !== 'string') {
+				originalData = $.param(originalData);
+			}
+			data = $.param(data);
+			settings.data = analyze.form.serialize() + (data ? '&' + data : '') + '&' + originalData;
+		}
+	}
+});
+
+// default snippet handler
+$.nette.ext('snippets', {
+	success: function (payload) {
+		if (payload.snippets) {
+			this.updateSnippets(payload.snippets);
+		}
+	}
+}, {
+	beforeQueue: $.Callbacks(),
+	afterQueue: $.Callbacks(),
+	completeQueue: $.Callbacks(),
+	before: function (callback) {
+		this.beforeQueue.add(callback);
+	},
+	after: function (callback) {
+		this.afterQueue.add(callback);
+	},
+	complete: function (callback) {
+		this.completeQueue.add(callback);
+	},
+	updateSnippets: function (snippets, back) {
+		var that = this;
+		var elements = [];
+		for (var i in snippets) {
+			var $el = this.getElement(i);
+			if ($el.get(0)) {
+				elements.push($el.get(0));
+			}
+			this.updateSnippet($el, snippets[i], back);
+		}
+		$(elements).promise().done(function () {
+			that.completeQueue.fire();
+		});
+	},
+	updateSnippet: function ($el, html, back) {
+		// Fix for setting document title in IE
+		if ($el.is('title')) {
+			document.title = html;
+		} else {
+			this.beforeQueue.fire($el);
+			this.applySnippet($el, html, back);
+			this.afterQueue.fire($el);
+		}
+	},
+	getElement: function (id) {
+		return $('#' + this.escapeSelector(id));
+	},
+	applySnippet: function ($el, html, back) {
+		if (!back && $el.is('[data-ajax-append]')) {
+			$el.append(html);
+		} else if (!back && $el.is('[data-ajax-prepend]')) {
+			$el.prepend(html);
+		} else if ($el.html() != html || /<[^>]*script/.test(html)) {
+			$el.html(html);
+		}
+	},
+	escapeSelector: function (selector) {
+		// thx to @uestla (https://github.com/uestla)
+		return selector.replace(/[\!"#\$%&'\(\)\*\+,\.\/:;<=>\?@\[\\\]\^`\{\|\}~]/g, '\\$&');
+	}
+});
+
+// support $this->redirect()
+$.nette.ext('redirect', {
+	success: function (payload) {
+		if (payload.redirect) {
+			window.location.href = payload.redirect;
+			return false;
+		}
+	}
+});
+
+// current page state
+$.nette.ext('state', {
+	success: function (payload) {
+		if (payload.state) {
+			this.state = payload.state;
+		}
+	}
+}, {state: null});
+
+// abort last request if new started
+$.nette.ext('unique', {
+	start: function (xhr) {
+		if (this.xhr) {
+			this.xhr.abort();
+		}
+		this.xhr = xhr;
+	},
+	complete: function () {
+		this.xhr = null;
+	}
+}, {xhr: null});
+
+// option to abort by ESC (thx to @vrana)
+$.nette.ext('abort', {
+	init: function () {
+		$('body').keydown($.proxy(function (e) {
+			if (this.xhr && (e.keyCode.toString() === '27' // Esc
+			&& !(e.ctrlKey || e.shiftKey || e.altKey || e.metaKey))
+			) {
+				this.xhr.abort();
+			}
+		}, this));
+	},
+	start: function (xhr) {
+		this.xhr = xhr;
+	},
+	complete: function () {
+		this.xhr = null;
+	}
+}, {xhr: null});
+
+$.nette.ext('load', {
+	success: function () {
+		$.nette.load();
+	}
+});
+
+// default ajaxification (can be overridden in init())
+$.nette.ext('init', {
+	load: function (rh) {
+		$(this.linkSelector).off('click.nette', rh).on('click.nette', rh);
+		$(this.formSelector).off('submit.nette', rh).on('submit.nette', rh)
+			.off('click.nette', ':image', rh).on('click.nette', ':image', rh)
+			.off('click.nette', ':submit', rh).on('click.nette', ':submit', rh);
+		$(this.buttonSelector).closest('form')
+			.off('click.nette', this.buttonSelector, rh).on('click.nette', this.buttonSelector, rh);
+	}
+}, {
+	linkSelector: 'a.ajax',
+	formSelector: 'form.ajax',
+	buttonSelector: 'input.ajax[type="submit"], button.ajax[type="submit"], input.ajax[type="image"]'
+});
+
+})(window, window.jQuery);
