From 8fee6af25368f10df9bd9941e401a9f1e28af5e5 Mon Sep 17 00:00:00 2001 From: Jakub Kopriva Date: Tue, 29 Apr 2014 10:45:53 +0200 Subject: [PATCH] Added feature "Inline Editation" [Closed #126] --- Grido/Components/Columns/Column.php | 2 +- Grido/Components/Columns/Date.php | 2 +- Grido/Components/Columns/Editable.php | 258 ++++++++++++++ Grido/Components/Columns/Number.php | 2 +- Grido/Components/Columns/Text.php | 2 +- Grido/Components/Container.php | 26 +- Grido/DataSources/NetteDatabase.php | 13 + Grido/Grid.php | 13 +- client-side/grido.css | 6 + client-side/grido.js | 331 ++++++++++++++++++ tests/Grido/Components/Column.Editable.phpt | 217 ++++++++++++ tests/Grido/Grid/Grid.phpt | 1 - tests/Grido/Grid/files/render.editable.expect | 153 ++++++++ tests/Grido/Grid/render.editable.phpt | 66 ++++ 14 files changed, 1085 insertions(+), 7 deletions(-) create mode 100644 Grido/Components/Columns/Editable.php create mode 100644 tests/Grido/Components/Column.Editable.phpt create mode 100644 tests/Grido/Grid/files/render.editable.expect create mode 100644 tests/Grido/Grid/render.editable.phpt diff --git a/Grido/Components/Columns/Column.php b/Grido/Components/Columns/Column.php index 3928d50b..1ff66a8c 100644 --- a/Grido/Components/Columns/Column.php +++ b/Grido/Components/Columns/Column.php @@ -187,7 +187,7 @@ public function getCellPrototype($row = NULL) */ public function getHeaderPrototype() { - if (!$this->headerPrototype) { + if ($this->headerPrototype === NULL) { $this->headerPrototype = \Nette\Utils\Html::el('th') ->setClass(array('column', 'grid-header-' . $this->getName())); } diff --git a/Grido/Components/Columns/Date.php b/Grido/Components/Columns/Date.php index 3be3086a..718b8697 100644 --- a/Grido/Components/Columns/Date.php +++ b/Grido/Components/Columns/Date.php @@ -20,7 +20,7 @@ * * @property string $dateFormat */ -class Date extends Column +class Date extends Editable { const FORMAT_TEXT = 'd M Y'; const FORMAT_DATE = 'd.m.Y'; diff --git a/Grido/Components/Columns/Editable.php b/Grido/Components/Columns/Editable.php new file mode 100644 index 00000000..01e336bf --- /dev/null +++ b/Grido/Components/Columns/Editable.php @@ -0,0 +1,258 @@ + + * @author Petr Bugyík + * + * @property \Nette\Forms\IControl $editableControl + * @property callback $editableCallback + * @property callback $editableValueCallback + */ +abstract class Editable extends Column +{ + /** @var bool */ + protected $editable = FALSE; + + /** @var bool */ + protected $editableDisabled = FALSE; + + /** @var \Nette\Forms\IControl Custom control for inline editing */ + protected $editableControl; + + /** @var callback for custom handling with edited data; function($id, $newValue, $oldValue, Editable $column) {} */ + protected $editableCallback; + + /** @var callback for custom value; function($row, Columns\Editable $column) {} */ + protected $editableValueCallback; + + /** + * Sets column as editable. + * @param callback $callback function($id, $newValue, $oldValue, Columns\Editable $column) {} {} + * @param \Nette\Forms\IControl $control + * @return Editable + */ + public function setEditable($callback = NULL, \Nette\Forms\IControl $control = NULL) + { + $this->editable = TRUE; + $this->setClientSideOptions(); + + $callback && $this->setEditableCallback($callback); + $control && $this->setEditableControl($control); + + return $this; + } + + /** + * Sets control for inline editation. + * @param \Nette\Forms\IControl $control + * @return Editable + */ + public function setEditableControl(\Nette\Forms\IControl $control) + { + $this->isEditable() ?: $this->setEditable(); + $this->editableControl = $control; + + return $this; + } + + /** + * Sets editable callback. + * @param callback $callback function($id, $newValue, $oldValue, Columns\Editable $column) {} + * @return Editable + */ + public function setEditableCallback($callback) + { + $this->isEditable() ?: $this->setEditable(); + $this->editableCallback = $callback; + + return $this; + } + + /** + * Sets editable value callback. + * @param callback $callback for custom value; function($row, Columns\Editable $column) {} + * @return Editable + */ + public function setEditableValueCallback($callback) + { + $this->isEditable() ?: $this->setEditable(); + $this->editableValueCallback = $callback; + + return $this; + } + + /** + * @return Editable + */ + public function disableEditable() + { + $this->editable = FALSE; + $this->editableDisabled = TRUE; + + return $this; + } + + protected function setClientSideOptions() + { + $options = $this->grid->getClientSideOptions(); + if (!isset($options['editable'])) { //only once + $this->grid->setClientSideOptions(array('editable' => TRUE)); + $this->grid->onRender[] = function(\Grido\Grid $grid) + { + foreach ($grid->getComponent(Column::ID)->getComponents() as $column) { + if (!$column instanceof Editable) { + continue; + } + + $columnName = $column->getColumn(); + $callbackNotSet = $column->isEditable() && $column->editableCallback === NULL; + if (($callbackNotSet && (!is_string($columnName) || strpos($columnName, '.'))) || + ($callbackNotSet && !method_exists($grid->model->dataSource, 'update'))) + { + throw new \Exception("Column '{$column->name}' has error: You must define an own editable callback."); + } + } + }; + } + } + + /**********************************************************************************************/ + + /** + * Returns header cell prototype ( html tag). + * @return \Nette\Utils\Html + */ + public function getHeaderPrototype() + { + $th = parent::getHeaderPrototype(); + + if ($this->isEditable()) { + $th->data['grido-editable-handler'] = $this->link('editable!'); + $th->data['grido-editableControl-handler'] = $this->link('editableControl!'); + } + + return $th; + } + + /** + * Returns cell prototype ( html tag). + * @param mixed $row + * @return \Nette\Utils\Html + */ + public function getCellPrototype($row = NULL) + { + $td = parent::getCellPrototype($row); + + if ($this->isEditable() && $row !== NULL) { + $td->data['grido-editable-value'] = $this->editableValueCallback === NULL + ? parent::getValue($row) + : callback($this->editableValueCallback)->invokeArgs(array($row, $this)); + } + + return $td; + } + + /** + * Returns control for editation. + * @returns \Nette\Forms\Controls\TextInput + */ + public function getEditableControl() + { + if ($this->editableControl === NULL) { + $this->editableControl = new \Nette\Forms\Controls\TextInput; + $this->editableControl->controlPrototype->class[] = 'form-control'; + } + + return $this->editableControl; + } + + /** + * @return callback + * @internal + */ + public function getEditableCallback() + { + return $this->editableCallback; + } + + /** + * @return callback + * @internal + */ + public function getEditableValueCallback() + { + return $this->editableValueCallback; + } + + /** + * @return bool + * @internal + */ + public function isEditable() + { + return $this->editable; + } + + /** + * @return bool + * @internal + */ + public function isEditableDisabled() + { + return $this->editableDisabled; + } + + /**********************************************************************************************/ + + /** + * @internal + */ + public function handleEditable($id, $newValue, $oldValue) + { + $this->grid->onRegistered($this->grid); + + if (!$this->presenter->isAjax() || !$this->isEditable()) { + $this->presenter->terminate(); + } + + $success = $this->editableCallback + ? callback($this->editableCallback)->invokeArgs(array($id, $newValue, $oldValue, $this)) + : $this->grid->model->update($id, array($this->getColumn() => $newValue), $this->grid->primaryKey); + + $response = new \Nette\Application\Responses\JsonResponse(array('updated' => $success)); + $this->presenter->sendResponse($response); + } + + /** + * @internal + */ + public function handleEditableControl($value) + { + $this->grid->onRegistered($this->grid); + + if (!$this->presenter->isAjax() || !$this->isEditable()) { + $this->presenter->terminate(); + } + + $control = $this->getEditableControl()->setValue($value); + $this->getForm()->addComponent($control, 'edit' . $this->getName()); + + $response = new \Nette\Application\Responses\TextResponse($control->getControl()->render()); + $this->presenter->sendResponse($response); + } +} diff --git a/Grido/Components/Columns/Number.php b/Grido/Components/Columns/Number.php index b6a33c78..42bb5b7f 100644 --- a/Grido/Components/Columns/Number.php +++ b/Grido/Components/Columns/Number.php @@ -20,7 +20,7 @@ * * @property array $numberFormat */ -class Number extends Column +class Number extends Editable { /** @var array */ protected $numberFormat = array( diff --git a/Grido/Components/Columns/Text.php b/Grido/Components/Columns/Text.php index a0f535ff..2334fd07 100644 --- a/Grido/Components/Columns/Text.php +++ b/Grido/Components/Columns/Text.php @@ -18,7 +18,7 @@ * @subpackage Components\Columns * @author Petr Bugyík */ -class Text extends Column +class Text extends Editable { /** @var Closure */ protected $truncate; diff --git a/Grido/Components/Container.php b/Grido/Components/Container.php index 57fca1db..c6045214 100644 --- a/Grido/Components/Container.php +++ b/Grido/Components/Container.php @@ -33,7 +33,7 @@ abstract class Container extends \Nette\Application\UI\Control * Returns column component. * @param string $name * @param bool $need - * @return Column + * @return Columns\Editable */ public function getColumn($name, $need = TRUE) { @@ -362,4 +362,28 @@ public function setExport($label = NULL) { return new Export($this, $label); } + + /** + * Sets all columns as editable. + * Callback is optional for user implementation of method for saving modified data. + * @param callback $callback + * @return \Grido\Grid + */ + public function setEditableColumns($callback = NULL) + { + $this->onRegistered[] = function(\Grido\Grid $grid) use ($callback) + { + if (!$grid->hasColumns()) { + return; + } + + foreach ($grid->getComponent(Column::ID)->getComponents() as $column) { + if ($column instanceof Columns\Editable && !$column->isEditable() && !$column->isEditableDisabled()) { + $column->setEditable($callback); + } + } + }; + + return $this; + } } diff --git a/Grido/DataSources/NetteDatabase.php b/Grido/DataSources/NetteDatabase.php index 0693602d..c49a9a10 100644 --- a/Grido/DataSources/NetteDatabase.php +++ b/Grido/DataSources/NetteDatabase.php @@ -141,4 +141,17 @@ public function suggest($column, array $conditions, $limit) return array_values($items); } + + /** + * @param mixed $id + * @param array $values + * @param string $idCol + * @return bool + */ + public function update($id, array $values, $idCol) + { + return (bool) $this->getSelection() + ->where("$idCol = ?", $id) + ->update($values); + } } diff --git a/Grido/Grid.php b/Grido/Grid.php index fd4a3f68..70e9bed0 100755 --- a/Grido/Grid.php +++ b/Grido/Grid.php @@ -43,6 +43,8 @@ class Grid extends Components\Container /***** DEFAULTS ****/ const BUTTONS = 'buttons'; + const CLIENT_SIDE_OPTIONS = 'grido-options'; + /** @var int @persistent */ public $page = 1; @@ -297,7 +299,7 @@ public function setRowCallback($callback) */ public function setClientSideOptions(array $options) { - $this->getTablePrototype()->data['grido-options'] = json_encode($options); + $this->getTablePrototype()->data[self::CLIENT_SIDE_OPTIONS] = json_encode($options); return $this; } @@ -579,6 +581,15 @@ public function getRowPrototype($row) return $tr; } + /** + * Returns client-side options. + * @return array + */ + public function getClientSideOptions() + { + return (array) json_decode($this->getTablePrototype()->data[self::CLIENT_SIDE_OPTIONS]); + } + /**********************************************************************************************/ /** diff --git a/client-side/grido.css b/client-side/grido.css index 31cd363e..5a17ea11 100644 --- a/client-side/grido.css +++ b/client-side/grido.css @@ -196,6 +196,12 @@ width: auto; } +.grido tbody .form-control { + width: 100%; + height: 23px; + padding: 0 0 0 3px; +} + .grido tfoot .form-control { display: inline; } diff --git a/client-side/grido.js b/client-side/grido.js index 2126e4f2..6972eac9 100644 --- a/client-side/grido.js +++ b/client-side/grido.js @@ -54,6 +54,7 @@ this.initActions(); this.initPagePrompt(); this.initCheckNumeric(); + this.initEditable(); this.onInit(); return this; @@ -142,6 +143,18 @@ }); }, + initEditable: function() + { + var that = this; + $('td[class*="grid-cell-"]', this.$element) + .off('dblclick.grido') + .on('dblclick.grido', function(event) { + if (event.metaKey || event.ctrlKey) { + this.editable = new Grido.Editable(that).init($(this)); + } + }); + }, + onInit: function() {}, /** @@ -220,6 +233,11 @@ $('tbody td:not(.checker,.actions a)', this.grido.$table) .off('click.grido') .on('click.grido', function(event) { + if ($(this).hasClass('edit')) { + event.preventDefault(); + return false; + } + if (event.shiftKey) { that.disableSelection.call(that); } @@ -453,6 +471,319 @@ } }; + /** + * Inline editable definition. + * @author Jakub Kopřiva + * @param {Grido} Grido + */ + Grido.Editable = function(Grido) + { + this.grido = Grido; + }; + + Grido.Editable.prototype = + { + init: function($td) + { + if (this.grido.options.ajax !== true || this.grido.options.editable !== true) { + return null; + } + + this.td = $td.addClass('edit'); + this.th = this.getColumnHeader(this.td); + + if (this.getEditHandlerUrl(this.th)) { + this.value = this.getValue(this.td); + this.oldValue = this.getOldValue(this.td); + this.primaryKey = this.getPrimaryKeyValue(this.td.parent()); + + this.componentName = this.getComponentName(this.th); + this.editControlHtml = this.getEditControl(this.componentName, this.getEditControlHandlerUrl(this.th)); + this.renderEditControl(this.td, this.editControlHtml); + + this.editControlObject = this.getEditControlObject(this.td); + this.setFocus(this.editControlObject); + this.initBindings(this.editControlObject); + } + + return this; + }, + + /** + * Returns column object. + * @param {jQuery} $td cell + * @return {jQuery} header cell of column + */ + getColumnHeader: function($td) + { + var headerClass; + var classList = $td.attr('class').replace('cell','header').split(/\s+/); + for (var i = 0; i < classList.length; i++) { + if (classList[i].indexOf('-header-') !== -1) { + headerClass = classList[i]; + } + } + + return $('th[class~="' + headerClass + '"]'); + }, + + /** + * Returns value of primary key. + * @param {jQuery} $tr row + * @return {String} Primary key value + */ + getPrimaryKeyValue: function($tr) + { + return $tr.attr('class').match(/[grid\-row\-]([0-9]+)/)[1]; + }, + + /** + * Returns name of component for AJAX params calls. + * @param {jQuery} $th header cell + * @return {String} component name for AJAX params calls + */ + getComponentName: function($th) + { + var handler = this.getEditControlHandlerUrl($th).replace('/[\.d]*/g', ''); + handler = handler.match(/[\??\&?][do=]+(.*)/)[1]; + + return handler.match(/(.*)\-edit/)[1]; + }, + + /** + * Returns url for AJAX editable handler. + * @param {jQuery} $th header cell + * @return {String} Url from data atribute of header cell + */ + getEditHandlerUrl: function($th) + { + return $th.data('grido-editable-handler'); + }, + + /** + * Returns url for AJAX call to Editable class. + * @param {jQuery} $th header cell + * @return {String} Url from data atribute of header cell + */ + getEditControlHandlerUrl: function($th) + { + return $th.data('grido-editablecontrol-handler'); + }, + + /** + * Returns raw value for editable control. + * @param {jQuery} $td + * @returns {String} + */ + getValue: function($td) + { + return $td.data('grido-editable-value'); + }, + + /** + * Returns value in the cell. + * @param {jQuery} $td + * @returns {jQuery}|{String} + */ + getOldValue: function($td) + { + return $td.children().length > 0 + ? $td.children() + : $td.text().trim(); + }, + + /** + * Returns html of control. + * @param {String} componentName component name for AJAX call + * @param {String} handleLink url for AJAX call + * @return {String} HTML of control + */ + getEditControl: function(componentName, handleLink) + { + var control, + data = {}; + + data[componentName + '-value'] = this.value; + + $.ajax({ + type: "GET", + url: handleLink, + data: data, + async: false + }) + .success(function(data) { + control = data; + }); + + return control; + }, + + /** + * Render html in cell. + * @param {jQuery} $td cell + * @param {String} $html HTML + */ + renderEditControl: function($td, $html) + { + $td.html($html); + }, + + /** + * Returns Children of cell. + * @param {jQuery} $td cell + * @return {jQuery} children of cell + */ + getEditControlObject: function($td) + { + return $td.children(); + }, + + /** + * Sets focus in text input if type="text". + * @param {jQuery} $editControlObject input + */ + setFocus: function($editControlObject) + { + if ($editControlObject[0].type === 'text') { + $editControlObject.focus(); + } + }, + + /** + * AJAX call to Editable handler for saving data. + * @param {String} oldValue value of cell before edit was done + * @param {String} componentName name of component handler for AJAX params + * @param {String} primaryKey value of primary key + * @param {jQuery} $th header cell of column + * @param {jQuery} $td edited cell + */ + saveData: function(oldValue, componentName, primaryKey, $th, $td) + { + var data = {}, + that = this, + newValue = this.editControlObject.val(); + + if (newValue === oldValue) { + $td.html(oldValue); + return; + } + + data[componentName + '-id'] = primaryKey; + data[componentName + '-newValue'] = newValue; + data[componentName + '-oldValue'] = oldValue; + + $.ajax({ + type: "POST", + url: this.getEditHandlerUrl($th), + data: data, + async: true + }) + .success(function(data) { + if (data.updated === true) { + $td.html(newValue); + $td.data('grido-editable-value', newValue); + that.oldValue = newValue; + that.flashSuccess($td); + } else { + that.flashError($td); + that.revertChanges($td); + } + }); + }, + + /** + * Revert changes in cell. + * @param {jQuery} $td edited cell + */ + revertChanges: function($td) + { + $td.html(this.oldValue); + }, + + /** + * Provide feedback to user, cell was succesfully changed. + * @param {jQuery} $td edited cell + */ + flashSuccess: function($td) + { + var transp = 0; + var multiplicator = 1; + var timer = setInterval(function() { + transp += (multiplicator * 0.01); + $td.css('background', 'rgba(196,234,195,'+transp+')'); + if (transp >=1) { + multiplicator = -1; + } + if (transp <= 0) { + clearInterval(timer); + $td.css('background', ''); + } + }, 1 ); + }, + + /** + * Provide feedback to user, cell was not changed. + * @param {jQuery} $td edited cell + */ + flashError: function($td) + { + var transp = 0; + var multiplicator = 1; + var timer = setInterval(function() { + transp += (multiplicator * 0.01); + $td.css('background', 'rgba(240,54,69,'+transp+')'); + if (transp >=1) { + multiplicator = -1; + } + if (transp <= 0) { + clearInterval(timer); + $td.css('background', ''); + } + }, 1 ); + }, + + /** + * Init key bindings to an edited control. + * @param {jQuery} $control + */ + initBindings: function($control) + { + var that = this; + var keypress = function(event) + { + if (event.keyCode === 13) { //enter + if (typeof window.Nette === 'object' && !window.Nette.validateControl(this)) { + event.preventDefault(); + return false; + } + + that.saveData(that.value, that.componentName, that.primaryKey, that.th, that.td); + that.td.removeClass('edit'); + + event.preventDefault(); + return false; + } + }; + + var keydown = function(event) + { + if (event.keyCode === 27) { //esc + that.revertChanges(that.td); + that.td.removeClass('edit'); + + event.preventDefault(); + return false; + } + }; + + $control + .off('keypress.grido') + .on('keypress.grido', keypress) + .off('keydown.grido') + .on('keydown.grido', keydown); + } + }; + /* GRIDO PLUGIN DEFINITION */ /* ========================== */ diff --git a/tests/Grido/Components/Column.Editable.phpt b/tests/Grido/Components/Column.Editable.phpt new file mode 100644 index 00000000..83218e22 --- /dev/null +++ b/tests/Grido/Components/Column.Editable.phpt @@ -0,0 +1,217 @@ + + * @package Grido\Tests + */ + +namespace Grido\Tests; + +use Tester\Assert, + Grido\Grid; + +require_once __DIR__ . '/../bootstrap.php'; +require_once __DIR__ . '/../Helper.inc.php'; + +class EditableTest extends \Tester\TestCase +{ + function testSetEditable() + { + // NOT EDITABLE + $grid = new Grid(); + $column = $grid->addColumnText('column', 'Column'); + Assert::same(FALSE, $column->editable); + Assert::same(NULL, $column->editableCallback); + Assert::type('\Nette\Forms\Controls\TextInput', $column->editableControl); + + // EDITABLE + $grid = new Grid(); + $column = $grid->addColumnText('column', 'Column')->setEditable(); + Assert::same(TRUE, $column->editable); + Assert::same(FALSE, $column->editableDisabled); + Assert::same(NULL, $column->editableCallback); + Assert::type('\Nette\Forms\Controls\TextInput', $column->editableControl); + + // EDITABLE AND DISABLED + $grid = new Grid(); + $column = $grid->addColumnText('column', 'Column')->setEditable(); + $column->disableEditable(); + Assert::same(FALSE, $column->editable); + Assert::same(TRUE, $column->editableDisabled); + Assert::same(NULL, $column->editableCallback); + Assert::type('\Nette\Forms\Controls\TextInput', $column->editableControl); + + // EDITABLE AND AN OWN CALLBACK VIA PARAM + $callback = callback($this, 'test'); + $grid = new Grid(); + $column = $grid->addColumnText('column', 'Column')->setEditable($callback); + Assert::same(TRUE, $column->editable); + Assert::same(FALSE, $column->editableDisabled); + Assert::same($callback, $column->editableCallback); + Assert::type('\Nette\Forms\Controls\TextInput', $column->editableControl); + + // EDITABLE AND AN OWN CALLBACK VIA METHOD + $callback = callback($this, 'test'); + $grid = new Grid(); + $column = $grid->addColumnText('column', 'Column')->setEditable(); + $column->setEditableCallback($callback); + Assert::same(TRUE, $column->editable); + Assert::same(FALSE, $column->editableDisabled); + Assert::same($callback, $column->editableCallback); + Assert::type('\Nette\Forms\Controls\TextInput', $column->editableControl); + + // EDITABLE AND AN OWN CALLBACK, CONTROL VIA PARAM + $callback = callback($this, 'test'); + $control = new \Nette\Forms\Controls\SelectBox(array('1','2','3')); + $grid = new Grid(); + $column = $grid->addColumnText('column', 'Column')->setEditable($callback, $control); + Assert::same(TRUE, $column->editable); + Assert::same(FALSE, $column->editableDisabled); + Assert::same($callback, $column->editableCallback); + Assert::same($control, $column->editableControl); + + // EDITABLE AND AN OWN CONTROL VIA METHOD + $callback = callback($this, 'test'); + $control = new \Nette\Forms\Controls\SelectBox(array('1','2','3')); + $grid = new Grid(); + $column = $grid->addColumnText('column', 'Column')->setEditable(); + $column->setEditableControl($control); + Assert::same(TRUE, $column->editable); + Assert::same(FALSE, $column->editableDisabled); + Assert::same(NULL, $column->editableCallback); + Assert::same($control, $column->editableControl); + + // EDITABLE AND AN OWN VALUE CALLBACK VIA METHOD + $valueCallback = callback($this, 'test'); + $grid = new Grid(); + $column = $grid->addColumnText('column', 'Column')->setEditable(); + $column->setEditableValueCallback($valueCallback); + Assert::same(TRUE, $column->editable); + Assert::same(FALSE, $column->editableDisabled); + Assert::same($valueCallback, $column->editableValueCallback); + + Helper::grid(function(Grid $grid) { + $grid->setModel(array()); + $grid->addColumnText('text', 'Text'); + $grid->addColumnNumber('number', 'Number'); + $grid->addColumnDate('date', 'Date'); + $grid->addColumnHref('href', 'Href'); + $grid->addColumnEmail('email', 'Email'); + $grid->setEditableColumns(); + })->run(); + + foreach (Helper::$grid->getComponent(\Grido\Components\Columns\Column::ID)->getComponents() as $column) { + Assert::type('\Grido\Components\Columns\Editable', $column); + Assert::true($column->isEditable()); + } + } + + function testHandleEditable() + { + $oldValue = 'Trommler'; + $newValue = 'Test'; + $id = 1; + + //copy current db + $database = __DIR__ . '/../DataSources/files/users.s3db'; + $editableSuffix = '.editable'; + copy($database, $database . $editableSuffix); + + Helper::grid(function(Grid $grid) use ($editableSuffix) { + $dsn = $grid->presenter->context->ndb_sqlite->getDsn() . $editableSuffix; + $connection = new \Nette\Database\Connection($dsn); + + $grid->setModel($connection->table('user')); + $grid->presenter->forceAjaxMode = TRUE; + $grid->addColumnText('firstname', 'Firstname')->setEditable(); + $grid->addColumnText('surname', 'Surname'); + $grid->addColumnText('gender', 'Gender'); + }); + + ob_start(); + Helper::request(array( + 'do' => 'grid-columns-firstname-editable', + 'grid-columns-firstname-id' => $id, + 'grid-columns-firstname-newValue' => $newValue, + 'grid-columns-firstname-oldValue' => $oldValue + )); + ob_clean(); + + //TEST INSIDE EDITABLE CALLBACK + Helper::grid(function(Grid $grid) use ($editableSuffix, $newValue, $oldValue, $id) { + + $dsn = $grid->presenter->context->ndb_sqlite->getDsn() . $editableSuffix; + $connection = new \Nette\Database\Connection($dsn); + + $grid->setModel($connection->table('user')); + $grid->presenter->forceAjaxMode = TRUE; + $grid->addColumnText('firstname', 'Firstname')->setEditable( + function($_id, $_newValue, $_oldValue, $_column) use ($newValue, $oldValue, $id) { + Assert::same($_id, $id); + Assert::same($_newValue, $newValue); + Assert::same($_oldValue, $oldValue); + Assert::type('Grido\Components\Columns\Editable',$_column); + return true; + }); + $grid->addColumnText('surname', 'Surname'); + $grid->addColumnText('gender', 'Gender'); + }); + + ob_start(); + Helper::request(array( + 'do' => 'grid-columns-firstname-editable', + 'grid-columns-firstname-id' => $id, + 'grid-columns-firstname-newValue' => $newValue, + 'grid-columns-firstname-oldValue' => $oldValue + )); + ob_clean(); + + //cleaup + unlink($database . $editableSuffix); + } + + function testHandleEditableControl() + { + Helper::grid(function(Grid $grid) { + $grid->setModel(array()); + $grid->presenter->forceAjaxMode = TRUE; + $grid->addColumnText('firstname', 'Firstname')->setEditable(NULL, new TextInput); + }); + + ob_start(); + Helper::request(array( + 'do' => 'grid-columns-firstname-editableControl', + 'grid-columns-firstname-value' => 'Test', + )); + $output = ob_get_clean(); + Assert::same('', $output); + } +} + +class TextInput extends \Nette\Forms\Controls\TextInput +{ + public function getControl() + { + return new Html(parent::getControl()); + } +} + +class Html extends \Nette\Utils\Html +{ + private $control; + + public function __construct($control) + { + $this->control = $control; + } + + public function render($indent = NULL) + { + print $this->control->render(); + } +} + +$test = new EditableTest(); +$test->run(); diff --git a/tests/Grido/Grid/Grid.phpt b/tests/Grido/Grid/Grid.phpt index a28f2b11..c0602160 100644 --- a/tests/Grido/Grid/Grid.phpt +++ b/tests/Grido/Grid/Grid.phpt @@ -22,7 +22,6 @@ class GridTest extends \Tester\TestCase function testOnRegisteredEvent() { $called = FALSE; - Helper::grid(function(Grid $grid) use (&$called) { $grid->onRegistered[] = function(Grid $grid) use(&$called) { $called = TRUE; diff --git a/tests/Grido/Grid/files/render.editable.expect b/tests/Grido/Grid/files/render.editable.expect new file mode 100644 index 00000000..d50a9b10 --- /dev/null +++ b/tests/Grido/Grid/files/render.editable.expect @@ -0,0 +1,153 @@ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + Firstname + + + Surname + + + Gender + + + Birthday + + + Actions +
+ + + + +
+ + + + + + Previous + 1 + 2 + 3 + 4 + ... + 7 + ... + 10 + ... + 13 + Next + + + Items 1 - 4 of 50 + + +
+ + + Trommler + + Trommler + + Martina + + 25.10.1964 + +EditDelete
+ + + Marij + + Rašpolić + + male + + 16.12.1972 + +EditDelete
+ + + Lyyli + + Vaara + + female + + 05.05.1963 + +EditDelete
+ + + Dýrleif + + Björnsdóttir + + female + + 24.11.1972 + +EditDelete
+
+
+
\ No newline at end of file diff --git a/tests/Grido/Grid/render.editable.phpt b/tests/Grido/Grid/render.editable.phpt new file mode 100644 index 00000000..9605f130 --- /dev/null +++ b/tests/Grido/Grid/render.editable.phpt @@ -0,0 +1,66 @@ +model = json_decode(file_get_contents(__DIR__ . '/../DataSources/files/users.json'), 1); + $grid->defaultPerPage = 4; + $grid->rowCallback = function(array $row, \Nette\Utils\Html $tr) { + $tr->class[] = $row['firstname']; + return $tr; + }; + + $grid->addColumnText('firstname', 'Firstname') + ->setSortable() + ->setEditable(function($id, $new, $old) {}) + ->setFilterText(); + + $grid->addColumnText('surname', 'Surname') + ->setSortable() + ->setFilterText(); + + $grid->addColumnText('gender', 'Gender') + ->setSortable() + ->setFilterText(); + + $grid->addColumnDate('birthday', 'Birthday') + ->setSortable() + ->setEditable(function($id, $new, $old) {}) + ->setFilterDate(); + + $grid->addActionHref('edit', 'Edit') + ->elementPrototype->addAttributes(array('class' => 'xxx')); + + $grid->addActionEvent('delete', 'Delete') + ->elementPrototype->class[] = 'yyy'; + + $grid->addActionEvent('print', 'Print') + ->elementPrototype = \Nette\Utils\Html::el('button'); + + $grid->setOperation(array('print' => 'Print'), function(){}); + $grid->setExport(); + + })->run(); + + ob_start(); + Helper::$grid->render(); + $output = ob_get_clean(); + + Assert::matchFile(__DIR__ . "/files/render.editable.expect", $output); +});